简介这是一款面向渗透测试团队与安全从业者的报告管理工具以JavaScript技术栈实现帮助团队跟踪参与项目、定义测试范围并自动执行nmap、dirb、showmount、nikto及EyeWitness截图等重复性扫描活动。工具内置报告模块可描述发现、划分严重性等级并附加截图支持通过可自定义模板导出为doc格式实验性知识模块还能借助已知SSH凭据自动完成部分横向移动。资源包共109个文件以26个js与16个py构成前后端主体逻辑辅以11个css样式、10个sh脚本及dockerfile、json等配置整体约90.93MB。其架构在内部启动Web服务、动作模块与MongoDB三个Docker容器目录划分清晰便于二次开发与部署调试。目前已有416人学习关注适合需要规范化渗透测试流程、沉淀扫描结果与报告输出的安全团队参考使用。1. 渗透测试报告工具为什么“写报告”比“打点”更消耗人做渗透测试的人大多有个共识真正拖垮项目节奏的往往不是拿 shell 的那一刻而是收尾阶段那份几十页的报告。打点可能两小时写报告能磨两天。pentest-report这类渗透测试报告工具要解决的正是这个“最后一公里”问题——把散落在终端里的命令输出、截图、漏洞描述、修复建议收敛成一份结构统一、可交付、可复盘的文档。它适合三类人一是常年接项目的独立测试者报告量大且重复劳动多二是团队里负责质量把关的负责人需要统一报告格式和漏洞分级口径三是刚入行的新手靠模板把“发现的问题”讲清楚而不是只丢一句“存在 SQL 注入”。这篇笔记不讲空泛概念而是把这类工具的原理、落地步骤、参数配置和踩坑点拆开让你能照着搭出一套自己的报告流水线。核心思路只有一句把报告从“手写文档”变成“数据驱动的渲染产物”。2. 报告工具的技术底座从原始记录到结构化数据2.1 为什么不能直接拿 Word 模板套很多人第一反应是找个 Word 模板复制粘贴。这个做法在单项目、单漏洞时没问题但一旦漏洞数量上到二三十个问题就暴露了编号会乱、严重等级前后不一致、同一类漏洞的修复建议每次措辞都不同、截图路径一改就全断。更麻烦的是客户要求“按风险等级排序”或者“按资产维度重新组织”时你得手动搬一遍。pentest-report这类工具的本质是把报告拆成两层数据层和渲染层。数据层用结构化格式常见的是 YAML 或 JSON描述每个漏洞渲染层用模板引擎Jinja2、Mustache 之类生成最终文档。这样排序、筛选、换模板都是改配置不动内容。我一般会强调先定数据结构再谈排版顺序反了后面全是返工。2.2 漏洞条目的最小字段设计一个能用的漏洞条目字段不能太少也不能太杂。太少渲染时信息不够太多录入成本高测试者会偷懒。下面是我常用的最小字段集用 YAML 表示# vuln.yaml —— 单个漏洞条目的最小结构 id: VULN-001 # 全局唯一编号渲染时用于排序和引用 title: 登录接口 SQL 注入 # 简短标题出现在目录和摘要 severity: high # 等级critical/high/medium/low/info asset: https://api.example.com/login # 受影响资产 category: injection # 漏洞分类便于统计和分组 description: | # 漏洞描述支持多行 登录接口的 username 参数未做参数化处理 拼接进 SQL 语句后可被注入。 evidence: | # 复现证据命令输出或请求响应 payload: OR 11 -- response: 200 OK, 返回了全部用户记录 remediation: | # 修复建议 使用参数化查询禁止字符串拼接 SQL。 references: # 参考链接可选 - https://owasp.org/www-community/attacks/SQL_Injection这里每个字段都有明确用途id保证引用稳定severity驱动排序category支撑统计图表evidence和remediation是客户最关心的两块。description用多行块避免长文本挤在一行难维护。字段名尽量用英文因为模板引擎和脚本处理英文键更省心但值可以是中文。2.3 模板引擎的选型理由渲染层选什么取决于你的输出格式。如果只出 Markdown 或 HTMLJinja2 足够语法直观Python 生态里资料多。如果要出 Word.docx常见做法是用python-docx配合占位符替换或者先用 Markdown 再转 docx。如果客户要 PDF走 HTML 转 PDF 的链路比如 WeasyPrint比直接操作 PDF 库更可控。选型时看三个点模板语法是否好学、是否支持循环和条件判断、社区是否活跃。Jinja2 在这三点上都稳所以我一般推荐它作为起点。下面是一个最小的 Jinja2 模板片段展示如何遍历漏洞列表{# report.md.j2 —— 报告模板片段 #} # 渗透测试报告 共发现 {{ vulns | length }} 个漏洞。 {% for v in vulns | sort(attributeseverity) %} ## {{ v.id }} {{ v.title }} - 风险等级{{ v.severity }} - 影响资产{{ v.asset }} {{ v.description }} **复现证据**{{ v.evidence }}**修复建议** {{ v.remediation }} {% endfor %}{% for %}负责循环sort(attributeseverity)做排序{{ }}做变量替换。注意模板里的代码块反引号在真实文件里要处理好转义否则渲染出来格式会乱。这个模板跑通后你只需要维护 YAML 数据报告随时重新生成。3. 把工具跑起来从零搭建报告生成流水线3.1 环境准备与依赖安装先明确技术栈Python 3.8 以上、Jinja2、PyYAML如果要出 Word 再加 python-docx。用虚拟环境隔离依赖避免污染系统 Python。下面是完整的环境搭建命令# 创建项目目录并进入 mkdir pentest-report cd pentest-report # 创建虚拟环境 python3 -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装核心依赖 pip install jinja2 pyyaml # 如果要导出 Word追加安装 pip install python-docxvenv保证依赖独立jinja2负责模板渲染pyyaml解析漏洞数据。安装完可以用pip list确认版本。这里不锁死版本号因为这几个库的 API 长期稳定但生产环境建议用requirements.txt固定版本避免某次升级导致模板语法不兼容。3.2 目录结构怎么摆目录结构直接影响后续维护成本。我习惯按“数据、模板、脚本、输出”四块分pentest-report/ ├── data/ │ └── vulns.yaml # 漏洞数据 ├── templates/ │ └── report.md.j2 # 报告模板 ├── output/ # 生成的报告放这里 ├── generate.py # 渲染脚本 └── requirements.txtdata和templates分离好处是同一份数据可以套不同模板给客户的详细版、给内部的简版。output单独放方便加进.gitignore避免把生成的报告误提交。generate.py是入口脚本逻辑尽量薄只做“读数据、渲染、写文件”三件事。3.3 渲染脚本的完整实现下面是generate.py的完整代码包含数据加载、模板渲染和文件输出# generate.py —— 报告生成入口 import yaml from jinja2 import Environment, FileSystemLoader from pathlib import Path # 1. 加载漏洞数据 def load_vulns(path): with open(path, r, encodingutf-8) as f: data yaml.safe_load(f) # 支持两种结构顶层列表或 {vulns: [...]} if isinstance(data, dict): return data.get(vulns, []) return data # 2. 配置模板环境 def build_env(template_dir): env Environment( loaderFileSystemLoader(template_dir), trim_blocksTrue, # 去掉块标签后的换行 lstrip_blocksTrue, # 去掉块标签前的空白 ) return env # 3. 渲染并写出 def render(vulns, env, template_name, out_path): template env.get_template(template_name) content template.render(vulnsvulns) Path(out_path).parent.mkdir(parentsTrue, exist_okTrue) with open(out_path, w, encodingutf-8) as f: f.write(content) print(f报告已生成{out_path}共 {len(vulns)} 个漏洞) if __name__ __main__: vulns load_vulns(data/vulns.yaml) env build_env(templates) render(vulns, env, report.md.j2, output/report.md)load_vulns兼容列表和字典两种顶层结构实际项目里数据来源可能是脚本导出的格式不统一很常见。trim_blocks和lstrip_blocks这两个参数很关键不加的话模板里每个{% %}都会留下空行渲染出来满屏空白。render里用mkdir(parentsTrue, exist_okTrue)保证输出目录存在避免第一次跑就报错。3.4 数据录入的两种方式数据从哪来常见两种。一是手工写 YAML适合漏洞少、需要精细描述的场景。二是从扫描器或测试脚本导出再转成 YAML。手工录入时建议用编辑器插件做 YAML 语法校验缩进错了整个文件解析失败。批量转换时写个小脚本把 CSV 或 JSON 转 YAML# convert.py —— 把 CSV 转成 vulns.yaml import csv, yaml rows [] with open(raw.csv, encodingutf-8) as f: for r in csv.DictReader(f): rows.append({ id: r[id], title: r[title], severity: r[severity].lower(), asset: r[asset], category: r.get(category, unknown), description: r[description], evidence: r.get(evidence, ), remediation: r[remediation], }) with open(data/vulns.yaml, w, encodingutf-8) as f: yaml.safe_dump({vulns: rows}, f, allow_unicodeTrue, sort_keysFalse)allow_unicodeTrue保证中文不被转义成\uXXXXsort_keysFalse保持字段顺序方便人工核对。转换脚本跑一次就行后续增量更新直接改 YAML。4. 参数与模板调优让报告真正能交付4.1 严重等级排序与分组客户看报告第一眼是风险排序。Jinja2 的sort默认按字母序critical会排在high前面吗不会字母序里c在h前但info的i也在h后顺序是 critical、high、info、low、medium明显不对。正确做法是给等级定义权重在数据加载时排序SEVERITY_ORDER {critical: 0, high: 1, medium: 2, low: 3, info: 4} def sort_vulns(vulns): return sorted(vulns, keylambda v: SEVERITY_ORDER.get(v[severity], 99))在render前调用sort_vulns模板里就不用再排。权重表放在脚本顶部改等级口径只动一处。如果客户要求按资产分组再加一层groupby模板里嵌套循环即可。4.2 模板里的条件渲染不是每个漏洞都有references模板里直接输出会得到空行或None。用条件判断处理{% if v.references %} **参考** {% for ref in v.references %} - {{ ref }} {% endfor %} {% endif %}{% if %}包住整块字段缺失时整段不渲染。同理evidence为空时可以输出“无”或跳过。这类判断在模板里越多数据录入就越灵活测试者不用为了凑字段而写废话。4.3 输出格式的切换同一份数据出多种格式靠模板切换。Markdown 模板出.mdHTML 模板出.htmlWord 用 python-docx 单独写一个渲染函数。下面是一个 HTML 模板的关键片段加了简单的 CSS 让报告在浏览器里可读{# report.html.j2 #} !DOCTYPE html html langzh head meta charsetutf-8 title渗透测试报告/title style body { font-family: sans-serif; max-width: 900px; margin: 2em auto; } .critical { color: #b00; font-weight: bold; } .high { color: #d60; } .medium { color: #a80; } /style /head body {% for v in vulns %} h2{{ v.id }} {{ v.title }}/h2 p classseverity {{ v.severity }}风险等级{{ v.severity }}/p pre{{ v.evidence }}/pre {% endfor %} /body /htmlCSS 里按等级给不同颜色客户扫一眼就知道哪些要优先修。HTML 转 PDF 时这些样式会保留比直接写 PDF 省事得多。5. 避坑与排查那些让报告返工的细节5.1 YAML 缩进错误导致解析失败现象脚本报yaml.scanner.ScannerError指向某一行。原因YAML 对缩进敏感多行文本块|下面的内容缩进不一致或者用了 Tab。解决统一用空格多行块内保持相同缩进用python -c import yaml; yaml.safe_load(open(data/vulns.yaml))单独校验文件比跑整个脚本更快定位。5.2 模板渲染出大量空行现象生成的 Markdown 每个漏洞之间隔了七八个空行。原因Jinja2 默认保留块标签前后的换行和空白。解决创建Environment时加trim_blocksTrue和lstrip_blocksTrue这两个参数能消掉绝大部分多余空行。如果还有检查模板里{% %}是否单独占行。5.3 中文乱码或转义现象YAML 里的中文在输出里变成\u6d4b\u8bd5。原因yaml.safe_dump默认allow_unicodeFalse。解决导出时显式传allow_unicodeTrue。读取时用encodingutf-8写文件同理。Windows 环境下还要注意终端编码必要时设PYTHONIOENCODINGutf-8。5.4 截图路径失效现象报告里的图片链接打不开。原因YAML 里写的是绝对路径换台机器就断或者图片没跟着报告一起打包。解决统一用相对路径图片放在output/assets/下模板里引用assets/xxx.png。生成报告后把整个output目录打包交付路径就不会断。5.5 漏洞编号重复或跳号现象两个漏洞都是VULN-001或者编号从 001 跳到 005。原因手工录入时复制粘贴没改或者删除条目后没重排。解决编号在数据加载后统一重排用脚本按顺序赋VULN-{i:03d}不依赖手工维护。这样增删条目都不会乱。6. 进阶把报告工具接进测试流程6.1 用命令行参数控制输出脚本写死后每次改路径很烦。加argparse支持命令行传参import argparse parser argparse.ArgumentParser(description渗透测试报告生成器) parser.add_argument(-d, --data, defaultdata/vulns.yaml, help漏洞数据文件) parser.add_argument(-t, --template, defaultreport.md.j2, help模板文件名) parser.add_argument(-o, --output, defaultoutput/report.md, help输出路径) args parser.parse_args() vulns sort_vulns(load_vulns(args.data)) env build_env(templates) render(vulns, env, args.template, args.output)这样一条命令就能切换数据源和模板python generate.py -d data/project_a.yaml -o output/a.md。接进 CI 或批处理脚本时参数化是前提。6.2 自动统计与摘要生成报告开头通常要有统计摘要。在渲染前算好传给模板from collections import Counter def build_summary(vulns): counter Counter(v[severity] for v in vulns) return { total: len(vulns), critical: counter.get(critical, 0), high: counter.get(high, 0), medium: counter.get(medium, 0), low: counter.get(low, 0), info: counter.get(info, 0), }模板里用{{ summary.total }}等变量输出。统计逻辑放脚本里模板只负责展示职责清晰。如果要做图表把 summary 导出成 JSON前端图表库直接读。6.3 验证报告完整性的检查清单生成完别急着交付跑一遍检查。我习惯用下面这个清单逐项过检查项方法通过标准漏洞数量对比数据文件条目数一致等级分布看摘要统计与预期相符编号连续脚本校验无重复无跳号图片可访问打开报告点链接全部能显示中文正常搜索关键词无乱码无转义修复建议非空遍历字段每条都有内容这个清单可以写成脚本自动跑也可以人工过。关键是形成习惯别让低级错误毁了一份内容扎实的报告。6.4 我踩过的最深的一个坑早期我图省事把漏洞描述直接写在模板里数据文件只存编号。结果客户临时要求“把所有高危漏洞单独出一份摘要”我得从模板里一条条抠出来改了半天还漏了两条。从那以后我定了个规矩模板里不出现任何具体漏洞内容所有内容都来自数据文件。模板只负责“怎么展示”数据负责“展示什么”。这条规矩让后面所有需求变更都变成改数据或加模板再没返工过。希望帮到你。本文还有配套的精品资源点击获取
