from pathlib import Path
import os
import tempfile
import zipfile

from docx import Document
from docx.enum.section import WD_SECTION
from docx.enum.style import WD_STYLE_TYPE
from docx.enum.table import WD_ALIGN_VERTICAL, WD_TABLE_ALIGNMENT
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Cm, Pt, RGBColor


SOURCE = Path('/tmp/code-audit-docx-20260910/working.docx')
OUTPUT = Path('/Users/wangshuo/Desktop/等保整改证明材料-20260916/01-代码审计/01-代码审计整改证明材料-执行中版.docx')
REFERENCE = Path('/Users/wangshuo/.codex/worktrees/693e/aizhct/standards-stack/agent-skills/skills/kangbite-word-template/assets/kangbite-digital-sports-word-template.docx')


def set_east_asian(run, name='宋体'):
    run.font.name = name
    run._element.get_or_add_rPr().rFonts.set(qn('w:eastAsia'), name)


def set_cell_shading(cell, fill):
    tc_pr = cell._tc.get_or_add_tcPr()
    shd = tc_pr.find(qn('w:shd'))
    if shd is None:
        shd = OxmlElement('w:shd')
        tc_pr.append(shd)
    shd.set(qn('w:fill'), fill)


def set_cell_margins(cell, top=90, start=110, bottom=90, end=110):
    tc = cell._tc
    tc_pr = tc.get_or_add_tcPr()
    tc_mar = tc_pr.first_child_found_in('w:tcMar')
    if tc_mar is None:
        tc_mar = OxmlElement('w:tcMar')
        tc_pr.append(tc_mar)
    for m, value in [('top', top), ('start', start), ('bottom', bottom), ('end', end)]:
        node = tc_mar.find(qn(f'w:{m}'))
        if node is None:
            node = OxmlElement(f'w:{m}')
            tc_mar.append(node)
        node.set(qn('w:w'), str(value))
        node.set(qn('w:type'), 'dxa')


def set_table_borders(table, color='D9D9D9', size='6'):
    tbl_pr = table._tbl.tblPr
    borders = tbl_pr.first_child_found_in('w:tblBorders')
    if borders is None:
        borders = OxmlElement('w:tblBorders')
        tbl_pr.append(borders)
    for edge in ('top', 'left', 'bottom', 'right', 'insideH', 'insideV'):
        el = borders.find(qn(f'w:{edge}'))
        if el is None:
            el = OxmlElement(f'w:{edge}')
            borders.append(el)
        el.set(qn('w:val'), 'single')
        el.set(qn('w:sz'), size)
        el.set(qn('w:color'), color)


def add_page_number(paragraph):
    paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
    run = paragraph.add_run('第 ')
    set_east_asian(run)
    begin = OxmlElement('w:fldChar')
    begin.set(qn('w:fldCharType'), 'begin')
    instr = OxmlElement('w:instrText')
    instr.set(qn('xml:space'), 'preserve')
    instr.text = ' PAGE '
    separate = OxmlElement('w:fldChar')
    separate.set(qn('w:fldCharType'), 'separate')
    text = OxmlElement('w:t')
    text.text = '1'
    end = OxmlElement('w:fldChar')
    end.set(qn('w:fldCharType'), 'end')
    run._r.extend([begin, instr, separate, text, end])
    run2 = paragraph.add_run(' 页')
    set_east_asian(run2)
    for r in paragraph.runs:
        r.font.size = Pt(9)
        r.font.color.rgb = RGBColor(89, 89, 89)


def add_para(doc, text='', bold_lead=None, align=None, first_indent=True, after=5):
    p = doc.add_paragraph()
    if bold_lead and text.startswith(bold_lead):
        r1 = p.add_run(bold_lead)
        r1.bold = True
        set_east_asian(r1)
        r2 = p.add_run(text[len(bold_lead):])
        set_east_asian(r2)
    else:
        r = p.add_run(text)
        set_east_asian(r)
    p.paragraph_format.line_spacing = 1.45
    p.paragraph_format.space_after = Pt(after)
    if first_indent:
        p.paragraph_format.first_line_indent = Cm(0.74)
    if align is not None:
        p.alignment = align
    return p


def add_heading(doc, text, level=1):
    p = doc.add_paragraph(style=f'Heading {level}')
    p.paragraph_format.keep_with_next = True
    r = p.add_run(text)
    set_east_asian(r, '黑体')
    r.font.color.rgb = RGBColor(0, 0, 0)
    return p


def add_table(doc, headers, rows, widths=None, font_size=9):
    table = doc.add_table(rows=1, cols=len(headers))
    table.alignment = WD_TABLE_ALIGNMENT.CENTER
    table.autofit = False
    set_table_borders(table)
    hdr = table.rows[0].cells
    for i, value in enumerate(headers):
        hdr[i].text = value
        set_cell_shading(hdr[i], 'D9EAF7')
        hdr[i].vertical_alignment = WD_ALIGN_VERTICAL.CENTER
        if widths:
            hdr[i].width = Cm(widths[i])
        for p in hdr[i].paragraphs:
            p.alignment = WD_ALIGN_PARAGRAPH.CENTER
            p.paragraph_format.space_after = Pt(0)
            for run in p.runs:
                run.bold = True
                run.font.size = Pt(font_size)
                set_east_asian(run, '黑体')
        set_cell_margins(hdr[i])
    for ri, row in enumerate(rows):
        cells = table.add_row().cells
        for i, value in enumerate(row):
            cells[i].text = str(value)
            cells[i].vertical_alignment = WD_ALIGN_VERTICAL.CENTER
            if widths:
                cells[i].width = Cm(widths[i])
            if ri % 2:
                set_cell_shading(cells[i], 'F7FAFC')
            set_cell_margins(cells[i])
            for p in cells[i].paragraphs:
                p.paragraph_format.space_after = Pt(0)
                p.paragraph_format.line_spacing = 1.15
                p.alignment = WD_ALIGN_PARAGRAPH.CENTER if i in (0, 1) else WD_ALIGN_PARAGRAPH.LEFT
                for run in p.runs:
                    run.font.size = Pt(font_size)
                    set_east_asian(run)
    table.rows[0]._tr.get_or_add_trPr().append(OxmlElement('w:tblHeader'))
    doc.add_paragraph().paragraph_format.space_after = Pt(2)
    return table


doc = Document(SOURCE)

normal = doc.styles['Normal']
normal.font.size = Pt(11)
normal.font.color.rgb = RGBColor(0, 0, 0)
normal._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
normal.paragraph_format.line_spacing = 1.45
normal.paragraph_format.space_after = Pt(5)

for style_name, size in [('Title', 22), ('Heading 1', 16), ('Heading 2', 13), ('Heading 3', 11)]:
    try:
        style = doc.styles[style_name]
    except KeyError:
        style = doc.styles.add_style(style_name, WD_STYLE_TYPE.PARAGRAPH)
    style.font.name = '黑体'
    style.font.size = Pt(size)
    style.font.color.rgb = RGBColor(0, 0, 0)
    style._element.rPr.rFonts.set(qn('w:eastAsia'), '黑体')
    style.paragraph_format.space_before = Pt(12 if style_name != 'Title' else 0)
    style.paragraph_format.space_after = Pt(7)
    style.paragraph_format.keep_with_next = True

for section in doc.sections:
    section.top_margin = Cm(2.54)
    section.bottom_margin = Cm(2.2)
    section.left_margin = Cm(3.175)
    section.right_margin = Cm(3.175)
    footer = section.footer
    if footer.paragraphs:
        footer.paragraphs[0].clear()
        add_page_number(footer.paragraphs[0])

first = doc.paragraphs[0]
first.style = doc.styles['Title']
first.alignment = WD_ALIGN_PARAGRAPH.CENTER
first.paragraph_format.space_before = Pt(55)
first.paragraph_format.space_after = Pt(18)
r = first.add_run('智慧营养健康餐厅管理系统\n代码审计整改证明材料')
set_east_asian(r, '黑体')
r.font.size = Pt(22)
r.bold = True

p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.paragraph_format.space_after = Pt(26)
r = p.add_run('执行中工作底稿')
set_east_asian(r, '黑体')
r.bold = True
r.font.size = Pt(14)

add_table(doc, ['项目', '内容'], [
    ('单位', '北京康比特体育科技股份有限公司'),
    ('对应问题', '系统上线前未开展代码审计'),
    ('材料日期', '2026年9月10日'),
    ('整改期限', '2026年9月16日前完成工作并提交证明材料'),
    ('当前状态', '执行中'),
], widths=[3.2, 10.0], font_size=10)

add_para(doc, '本材料用于记录截至2026年9月10日已完成的制度证据核查和指定源码快照有限范围内部检查。当前尚未取得该快照与生产上线版本的映射证明，发现问题亦待整改复核，因此本材料不得作为“代码审计已完成”或“当前生产系统已通过代码审计”的证明。', after=8)

doc.add_page_break()
add_heading(doc, '一 落实情况说明')
add_para(doc, '我单位正在推进智慧营养健康餐厅管理系统应用代码及相关接口的代码审计工作，重点检查身份鉴别、服务端访问控制、输入校验和敏感信息保护。现已完成既有制度和表单真实性核查，并对指定的2026年8月24日专项源码快照开展有限范围静态检查，形成问题跟踪项。')
add_para(doc, '截至本材料日期，代码审计仍处于执行中。指定源码快照尚未与当前生产部署版本完成映射确认，新发现问题尚未形成完整的整改和复核证据，审计执行人、复核人及批准人亦待公司指定并签署。相关工作和最终证明材料应于2026年9月16日前完成。')
add_para(doc, '本材料不涉及回函第三项所列1个中风险、8个低风险漏洞的数量、结论或整改状态；本次代码检查发现只作为第一项内部问题跟踪，不追加或替代第三项监管问题清单。')

add_heading(doc, '二 现有制度与职责依据')
add_table(doc, ['证据', '可证明内容', '不能证明内容'], [
    ('安全开发管理规定', '安全测试、阶段评审、源码保护、版本一致性、上线前安全评估或审查', '不能单独证明本次代码审计已执行'),
    ('组织及岗位职责管理规定', '安全检查、整改跟踪、审计记录和批准分工', '不能替代本次审计授权、执行和复核记录'),
    ('变更管理规定', '问题整改后的申请、影响分析、测试、回退和验收要求', '空白表单不能证明实际变更'),
    ('漏洞和风险管理规定', '风险发现、报告、整改和复核的一般要求', '漏洞扫描不能替代代码审计'),
    ('授权和审批管理规定', '程序变更、安全测试和评估活动的审批要求', '未形成的审批不能写为已批准'),
    ('系统安全管理规定', '源码受控、日志和运维安全管理要求', '不能证明当前版本已完成代码审计'),
    ('2026年7月26日培训记录', '存在一份已填写且有手写记录人、审批人的安全培训记录', '未附课件、签到表、现场照片，不能证明全部列名人员实际签到或代码审计专项培训'),
], widths=[3.1, 5.1, 5.0], font_size=8.5)

add_heading(doc, '三 安全开发 代码审查和上线检查补充要求 待审定稿')
add_para(doc, '本节用于补齐现行制度中代码审计范围、方法、记录和闭环要求。未经公司批准和发布前，仅作为待审定稿，不表述为已生效制度。')
rules = [
    ('职责', '研发负责人确认审计版本和代码范围；安全审计人员实施或组织检查；测试人员设计反向权限、异常输入和回归用例；运维人员提供上线版本映射；安全主管复核结论；批准人决定是否具备上线条件。'),
    ('触发', '新系统上线、重大版本发布、核心鉴权权限逻辑变更、敏感数据处理变更、重大漏洞整改后均应开展代码审查；高风险问题关闭前不得直接给出通过结论。'),
    ('范围', '至少覆盖登录与会话、服务端权限和对象级数据范围、输入校验与数据库访问、文件和命令处理、敏感配置与日志、密码和令牌、外部接口与回调。'),
    ('方法', '记录人工审查、静态扫描、依赖检查、定向测试和动态验证中实际使用的方法；未执行的方法必须列为未覆盖，不能用计划替代执行记录。'),
    ('版本', '检查记录必须包含仓库、分支、完整Git SHA、代码树或文件校验值，并与发布单、流水线产物或服务器版本建立可回读映射。'),
    ('闭环', '每个问题使用唯一编号，记录级别、位置、责任人、整改提交、测试证据、复核人、复核日期、残余风险和结论。未复核问题保持开放状态。'),
    ('上线门禁', '高风险问题未关闭、生产版本映射缺失、关键权限和输入校验反向测试未通过、敏感配置未处置时，不得签署上线检查通过。'),
    ('归档', '归档审计范围、工具和规则版本、检查记录、问题台账、整改提交、测试日志、复核结论、批准记录和例外说明。'),
]
add_table(doc, ['条目', '补充要求'], rules, widths=[2.2, 11.0], font_size=9)

add_heading(doc, '四 指定源码快照有限范围内部检查记录')
add_heading(doc, '四点一 版本和方法', level=2)
add_table(doc, ['字段', '记录'], [
    ('检查日期', '2026年9月10日'),
    ('本地路径', '/Users/wangshuo/Codex/zhctproject/csfzx-mlps-remediation-20260821/store'),
    ('远端标识', 'Codeup zhct/store 仓库'),
    ('分支', 'codex/csfzx-staff-info-horizontal-auth-20260824'),
    ('提交', '3428f1522d9b7c1a3dfed1c372963e0e15c25e44'),
    ('Git tree', '86158685b3dddcaab82a04250ab48b5a175a146d'),
    ('工作树', '干净；本次未fetch、未连接服务器'),
    ('方法', '有限范围人工辅助静态检查；未运行应用、未发送攻击载荷、未做第三方全量审计'),
    ('版本限制', '只能证明本地保存的2026年8月24日专项快照；不能证明2026年9月10日远端最新或当前生产部署'),
], widths=[2.4, 10.8], font_size=9)

add_heading(doc, '四点二 检查范围', level=2)
add_para(doc, '抽样覆盖后台基础控制器、登录控制器与逻辑、人员管理控制器与逻辑、日期配餐控制器与查询链路、人员模型、登录和人员校验器、公共函数、公共配置、城市副中心专项配置，以及2026年8月24日人员详情水平越权历史旁证。未覆盖范围见第七部分。')

add_heading(doc, '四点三 已确认控制', level=2)
for item in [
    '登录逻辑存在失败计数与锁定、密码过期修改、密码复杂度以及Cookie的HttpOnly、SameSite和HTTPS感知设置。',
    '人员详情接口对UUID和授权人员范围执行对象级校验，并在详情查询前拒绝越权。',
    '基础控制器提供登录、功能权限及授权部门和人员范围计算能力，但子控制器可覆盖前置动作，不能外推为所有接口均自动执行完整检查。',
    '人员新增和修改采用一定的入参白名单和字段校验；日期配餐列表使用授权人员范围过滤。',
]:
    p = doc.add_paragraph()
    p.paragraph_format.space_after = Pt(4)
    r = p.add_run('• ' + item)
    set_east_asian(r)

add_heading(doc, '四点四 发现及当前状态', level=2)
add_table(doc, ['编号', '级别', '发现摘要', '主要位置', '状态'], [
    ('CA-01', '高', '短信二次认证验证码状态位于客户端Cookie，未见可靠的一次认证上下文和服务端验证码状态绑定', 'Login控制器127-165；Login逻辑156-225', '待整改和隔离环境复核'),
    ('CA-02', '高', '人员修改、删除及部分展示接口缺少目标人员或部门范围的对象级授权检查', 'Staff控制器12-14、313-378、421-443；对应逻辑', '待整改和反向权限测试'),
    ('CA-03', '中', '日期配餐请求值进入原生SQL动态拼接路径', 'DateMenu控制器46-77；逻辑508-556；Staff模型544-570', '待参数化整改和复核'),
    ('CA-04', '高条件项', 'Git跟踪配置中存在明文敏感凭据', '城市副中心及共享配置对应行', '待移出、轮换并核实历史暴露'),
    ('CA-05', '中高', '会话令牌密钥管理、完整性保护及Header token超时和账号状态复核不足', 'Login逻辑237-302、511-556；Security；AdminController；common', '待改造并核实运行环境'),
    ('CA-06', '中', '密码存储使用双重MD5和低强度随机盐', 'application/common.php 99-114', '待制定兼容迁移和回归方案'),
], widths=[1.2, 1.4, 5.0, 3.3, 2.3], font_size=7.6)
add_para(doc, '以上问题只作为第一项内部审计问题跟踪，不追加为回函第三项所列漏洞数量。具体凭据值、利用步骤和敏感数据未写入本材料。', after=8)

add_heading(doc, '五 问题整改和复核记录')
add_para(doc, '下表为必须继续填写的真实闭环记录。当前未取得修复提交、测试证据和复核签署，故状态均保持开放。')
add_table(doc, ['编号', '责任人', '整改证据', '测试证据', '复核人和日期', '结论'], [
    ('CA-01', '待指定', '待补提交或配置记录', '待补二次认证反向测试', '待填写', '开放'),
    ('CA-02', '待指定', '待补提交', '待补授权内外对象矩阵', '待填写', '开放'),
    ('CA-03', '待指定', '待补参数化提交', '待补异常输入回归', '待填写', '开放'),
    ('CA-04', '待指定', '待补移出和轮换记录', '待补秘密扫描和有效性复核', '待填写', '开放'),
    ('CA-05', '待指定', '待补令牌改造记录', '待补过期、篡改、停用账号测试', '待填写', '开放'),
    ('CA-06', '待指定', '待补兼容迁移记录', '待补旧新哈希登录回归', '待填写', '开放'),
], widths=[1.2, 1.7, 3.1, 3.3, 2.3, 1.4], font_size=7.8)

add_heading(doc, '六 现有记录真实性核查')
add_table(doc, ['材料', '核查结果', '使用结论'], [
    ('安全检查表', '部分预填；应用安全检查结果为空', '不能作为已执行证据'),
    ('系统审批表', '实施时间、负责人意见和签名为空', '不能作为已审批证据'),
    ('评审记录', '时间、结果和签署为空', '不能证明评审发生'),
    ('变更记录表', '全空模板', '不能证明已变更'),
    ('可编辑培训记录', '日期、总结和签署为空', '不能作为已培训证据'),
    ('培训记录扫描件', '日期2026年7月26日；视觉见手写记录人和审批人', '可作既有培训旁证，但不扩大为全部人员签到或专项审计培训'),
], widths=[3.0, 5.7, 4.5], font_size=8.5)

add_heading(doc, '七 未覆盖范围和提交条件')
add_heading(doc, '七点一 未覆盖范围', level=2)
for item in [
    '当前生产部署版本、服务器与网关行为、数据库权限和真实运行配置。',
    '全部后台控制器、移动端、设备接口、文件上传、支付、消息回调、定时任务、前端和第三方组件。',
    '动态渗透、运行时日志、真实角色权限矩阵、凭据历史清理验证和整改后复测。',
    '第三方独立性审计和全系统通过结论。',
]:
    p = doc.add_paragraph()
    r = p.add_run('• ' + item)
    set_east_asian(r)

add_heading(doc, '七点二 形成最终提交版前的最少必要补件', level=2)
add_table(doc, ['优先级', '补件', '完成标准'], [
    ('P0', '上线版本映射', '发布单、流水线产物或服务器版本可回读并对应完整Git SHA'),
    ('P0', '审计授权和范围签署', '明确对象、版本、模块、方法、执行人、复核人和日期；使用真实日期'),
    ('P0', '六类发现整改复核', '逐项具备修复提交、测试证据、复核结论和残余风险'),
    ('P0', '正式结论签署', '审计人、复核人、安全主管和批准人真实签署'),
    ('P1', '补充要求审定发布', '公司完成批准并记录版本和生效日期'),
], widths=[1.7, 4.0, 7.5], font_size=8.5)

add_heading(doc, '八 责任人复核与签署')
add_para(doc, '本记录为执行中工作底稿。签署人应先核实上线版本映射、检查范围、发现内容、整改提交和测试证据，不得在缺项未关闭时签署“通过”或“整改完成”。')
add_table(doc, ['角色', '姓名', '意见', '签名', '日期'], [
    ('审计执行人', '', '', '', ''),
    ('研发负责人', '', '', '', ''),
    ('测试复核人', '', '', '', ''),
    ('运维负责人', '', '', '', ''),
    ('安全主管', '', '', '', ''),
    ('批准人', '', '', '', ''),
], widths=[2.2, 2.2, 4.2, 2.2, 2.4], font_size=9)

add_heading(doc, '九 附件目录')
add_table(doc, ['序号', '附件', '关键页码或用途'], [
    ('1', '安全开发管理规定', '第2-4页、第7-12页'),
    ('2', '组织及岗位职责管理规定', '第4页、第6页、第9-10页'),
    ('3', '变更管理规定', '第3-5页'),
    ('4', '漏洞和风险管理规定', '第2-3页'),
    ('5', '授权和审批管理规定', '第2-4页'),
    ('6', '系统安全管理规定', '第3-5页'),
    ('7', '安全培训记录', '2026年7月26日已填写扫描件'),
    ('8', '证据索引CSV', '证明点、原件路径、页码、真实性和缺口'),
], widths=[1.3, 5.5, 6.3], font_size=9)

doc.core_properties.title = '智慧营养健康餐厅管理系统代码审计整改证明材料'
doc.core_properties.subject = '系统上线前未开展代码审计整改 第一项 执行中工作底稿'
doc.core_properties.author = '北京康比特体育科技股份有限公司'
doc.core_properties.comments = '2026-09-10编制；未取得公司责任人签署前不得作为完成证明。'

OUTPUT.parent.mkdir(parents=True, exist_ok=True)
doc.save(OUTPUT)

# python-docx rewrites untouched XML parts during save. Restore the retained
# Kangbite header relationship and logo byte-for-byte from the authoritative
# template after content generation.
preserve_parts = {
    'word/header1.xml',
    'word/_rels/header1.xml.rels',
    'word/media/image1.png',
}
with zipfile.ZipFile(REFERENCE, 'r') as source_zip, zipfile.ZipFile(OUTPUT, 'r') as output_zip:
    source_bytes = {name: source_zip.read(name) for name in preserve_parts}
    with tempfile.NamedTemporaryFile(prefix='code-audit-docx-', suffix='.docx', delete=False) as handle:
        tmp_name = Path(handle.name)
    with zipfile.ZipFile(tmp_name, 'w', zipfile.ZIP_DEFLATED) as patched:
        for info in output_zip.infolist():
            payload = source_bytes.get(info.filename, output_zip.read(info.filename))
            patched.writestr(info, payload)
os.replace(tmp_name, OUTPUT)
print(OUTPUT)
