关于 deep-app-audit
deep-app-audit 用于对 Frappe 应用代码库开展安全性、正确性和定制化审计。它调度 Agent CLI 任务、验证发现并生成报告,还会记录运行状态以便续跑。
主要功能
- 01安全、正确性和定制化检查
- 02并行调度 Agent CLI 任务
- 03扫描与验证任务分开执行
- 04可选测试站点和 Semgrep 准备
- 05记录状态并支持续跑
- 06汇总任务结果生成报告
适用场景
- 01按选定轨道审计 Frappe 应用代码库
- 02从有效任务结果恢复中断的审计
- 03不创建测试站点,仅检查源代码
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ frappe/ skills --skill deep-app-audit安装后在 Claude Code 中输入/deep-app-audit确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents.example.jsonprompts/check.mdcontext.mdreport.mdscan.mdverify.mdquality/_conventions.mdquality/A-customization/A01-no-monkey-patching.mdA02-override-class-calls-super.mdA03-extend-instead-of-override.mdA04-regional-override-first.mdA05-override-whitelisted-method-contract.mdA06-hook-the-transaction-not-the-ledger.mdA07-reuse-the-existing-doctype.mdA08-custom-fields-in-code.mdA09-fixtures-overwrite-the-site.mdA10-remove-customizations-on-uninstall.mdA11-declare-required-apps.mdA12-declare-versions-and-branches.mdA13-clean-bench-install.mdA14-dotted-paths-must-resolve.mdA15-scheduler-method-path-is-an-api.mdA16-prefix-custom-fieldnames.mdA17-keep-standard-field-constraints.mdA18-check-every-writer.mdA19-bound-the-custom-field-count.mdA20-server-side-rules-not-in-client-scripts.mdA22-app-logic-not-in-server-scripts.mdA24-hooks-py-is-static-data.mdA25-handler-signature-matches-call-site.md详细介绍
深度应用审计
此技能从三个方面审计一个 Frappe 应用检出目录,并生成一份报告:
| 方面 | 提示词 | ID 前缀 |
|---|---|---|
| 安全性 | security/ 的各区域目录中的范围 | S-A 到 S-L |
| 安全性 | security/checks/ 中的检查项 | S-P |
| 正确性 | quality/B-correctness/ 中的规则 | Q-B |
| 定制化 | quality/A-customization/ 中的规则 | Q-A |
| 定制化 | quality/checks/ 中的检查项 | Q-K |
提示词的 ID 由其方面前缀和文件 ID 组成:security/A-authorization/A02-*.md 的 ID 是
S-A02。quality/mechanisms/ 是质量规则的背景资料,不是一组提示词。
两个方面的工作方式相同。security/_conventions.md 和 quality/_conventions.md 包含相同的章节,每个文件都是其所属方面的约定:方法、问题认定标准、严重性等级、验证步骤和问题格式。prompts/ 中的任务提示词对两个方面相同,并会引导每个 Agent 参照其所属方面的约定。
运行方式
一次运行分为两部分:
| 部分 | 执行者 | 步骤 |
|---|---|---|
| 设置:参数、目标、清单、semgrep、测试站点 | 你,协调者 | 1 到 4 |
| 任务:扫描、检查、验证、报告 | scripts/run_audit.py | 5 |
设置需要判断:每台机器上的 bench 都不一样,而且每个应用都有自己的参与者。因此由你来完成。任务的调度不需要判断,因此由脚本来完成。脚本会将每项任务作为一个 Agent CLI 的新进程启动,因此每项任务都有全新的上下文。看过扫描结果的验证器并不独立,而且单个上下文无法容纳 160 项扫描。
该脚本不依赖某一种模型或运行框架。agents.json 会将每种任务类型映射到一个命令;该命令可以是任何能够接收提示词、读取文件、运行命令并写入文件的 CLI。扫描任务和验证任务可以使用不同的模型。
脚本会:
- 并行运行任务,并为每个工作进程分配一组测试用户
- 在扫描文件生成后,将其验证任务加入队列;在数量限制内,按严重程度从高到低处理
- 检查每个结果文件的格式;文件缺失或格式错误时,重新运行相应任务
- 在每项任务后运行站点重置命令,并在站点停止响应时停止运行
- 写入
run.json(每项任务的状态)和summary.json(计数),并在任务完成后启动报告
每项任务都会将结果写入运行目录中的一个文件。结果文件有效的任务即视为完成,因此脚本再次启动时,会从已停止的位置继续运行。
不要自行审计应用,也不要自行启动任务 Agent。
1. 读取参数
参数是要审计的应用检出目录,后面可以用普通文字指定选项。未提供路径时,使用当前目录。
| 选项 | 示例 | 默认值 |
|---|---|---|
only:要运行的 ID 前缀 | S-A、Q-B05 | 全部 |
skip:要跳过的 ID 前缀 | S-L、S-P05 | 无 |
no checks:不运行全范围检查 | 运行检查 | |
no site:仅读取源代码 | 创建站点 | |
site:重用现有的临时站点 | myapp-audit.localhost | 创建一个站点 |
db root password:用于 bench new-site | 无 | |
bench:bench 目录 | 在检出目录上方查找 | |
output:运行目录 | <target>/../<app>-deep-audit/ | |
semgrep rules:frappe/semgrep-rules 的本地克隆 | ~/src/semgrep-rules | 克隆到运行目录 |
no semgrep:不运行 semgrep 规则 | 运行 semgrep | |
drop site:结束时删除所创建的站点 | 保留以供排查 | |
agents:Agent 命令文件 | ~/audit-agents.json | <run dir>/agents.json |
jobs:同时运行的 Agent 数量 | 4 | 16 |
max candidates:每次扫描的验证上限 | 15 | 15 |
max verifications:本次运行的验证上限 | 600 | 600 |
timeout:运行停止前的小时数 | 12 | 8 |
“仅检查安全性”对应 only: S-。“仅检查正确性”对应 only: Q-B。“仅检查质量”对应 only: Q-。
2. 确认目标
确认该路径下包含一个 Frappe 应用:其中一个包内含有 hooks.py。如果没有,停止并告知用户。
用两三行告诉用户将运行哪些内容:应用、检查类别、是否创建测试站点、Agent CLI 和模型,以及 Agent 的大致数量。完整运行大约需要 160 个扫描和检查 Agent,外加每个候选项对应的一个 Agent。Agent 总数可能达到数百个。用户已提供路径和选项时,不要请求确认。执行命令即表示同意。
测试站点需要对 bench 具有写入权限。如果检出目录不在 bench 中,或者 bench 看起来是生产环境,请使用 no site 并说明原因。
3. 准备运行环境
请自行完成。不要更改应用检出目录中的任何文件。
-
将检出目录解析为绝对路径。记录应用包名称(包含
hooks.py的目录)和短提交哈希,使用git -C <target> rev-parse --short HEAD获取。 -
以只读方式查找 bench。bench 目录包含
sites/、apps/和Procfile。已安装的应用通常位于<bench>/apps/<app>。从apps/frappe/frappe/__init__.py记录 Frappe 版本,并记录目标应用要求的应用(hooks.py、pyproject.toml中的required_apps)或导入的应用,以及它们在 bench 上各自的路径。 -
创建运行目录。如果其中已有
setup.json,则这是一次恢复运行:读取该文件,并跳到第一个尚未完成的步骤。 -
构建入口点清单:
python <skill dir>/scripts/build_inventory.py <run dir>/inventory.json --root <target>此脚本为只读脚本。它不需要 bench、site 或数据库。如果
frappe和目标应用所需的应用位于检出目录旁的<bench>/apps中,脚本还会加载它们的 DocType。对于位于其他目录中的每个此类应用,添加--doctypes-from <path>。如果执行失败,记录错误。审计将继续,不使用该清单。 -
运行 Frappe semgrep 规则,除非用户指定了
no semgrep。这些规则是质量规则中semgrep字段的来源,因此在这里运行一次,不要在每次扫描中重复运行。-
获取 semgrep。如果
PATH中有semgrep,则使用它。否则,如果PATH中有uv,运行uv tool install semgrep,并以uv tool run semgrep的形式调用 semgrep,因为 uv 工具目录不一定在PATH中。不要以其他方式安装 uv 或 pip 包。 -
获取规则。如果指定了
semgrep rules选项,则按原样使用该克隆。否则,如果<run dir>/semgrep-rules不存在,则克隆:git clone --depth 1 https://github.com/frappe/semgrep-rules <run dir>/semgrep-rules。 将克隆放在运行目录中,可确保一次运行中的规则固定,即使运行恢复时也如此。记录克隆的短提交哈希。 -
扫描检出目录,并为每个匹配项提供裸规则 ID。Semgrep 会在规则 ID 前加上规则目录的路径,因此请移除最后一个
.之前的所有内容:<semgrep> scan --config <rules>/rules --json --metrics=off --quiet --output <run dir>/semgrep-raw.json <target> jq --arg t "<target>/" '[.results[] | {rule: (.check_id | split(".") | last), file: (.path | ltrimstr($t)), line: .start.line, message: .extra.message}]' \ <run dir>/semgrep-raw.json > <run dir>/semgrep.json
如果上述任一步骤失败,记录错误。审计将继续,不使用 semgrep。
-
-
列出提示词。包含上表所列目录中的每个
*.md文件。排除文件名以_开头的文件。对 ID 应用only和skip,并排除带有no checks的检查项。 -
写入
<run dir>/setup.json。notes为可选项:记录所有 Agent 都需要了解的应用事实,例如与 DocType 名称不同的产品名称。{ "target": "/abs/path/to/app", "app": "myapp", "commit": "abc1234", "bench": "/abs/path/to/bench", "frameworkVersion": "16.0.0-dev", "dependencies": [{"app": "erpnext", "path": "/abs/path/to/bench/apps/erpnext"}], "inventory": "/abs/run/dir/inventory.json", "inventoryError": null, "semgrep": "/abs/run/dir/semgrep.json", "semgrepRules": "/abs/run/dir/semgrep-rules", "semgrepRulesCommit": "def5678", "semgrepError": null, "scans": ["S-A01", "Q-B05"], "checks": ["S-P01", "Q-K01"], "notes": "In the UI, the DocType Sales Invoice is called Bill." } -
获取 Agent 命令。如果指定了
agents选项,则使用该文件。否则,如果<run dir>/agents.json不存在,将<skill dir>/agents.example.json复制到该路径;如果文件中有适用于你所用工具框架的配置,则将每个角色设置为你自己的工具框架 CLI。如果文件中没有适用于你所用工具框架的配置,或者你不知道自己运行的是哪个 CLI,请询问用户。配置允许使用的工具就是每个任务 Agent 可使用的工具,因此不要扩大权限。
4. 启动测试 site
使用 no site 跳过此步骤。请自行完成,或将以下步骤交给一个 Agent。site 可让验证者发送真实请求。没有 site 时,审计只会读取源代码。
-
在 bench 目录中运行
bench --version,确认 bench 是否可用。如果没有 bench,则继续,但不使用站点。不要安装 bench 或数据库服务器,也不要更改机器的配置。 -
获取一个站点。使用
site选项时,重用该站点。先确认它不是生产站点:如果其中存有真实数据,则停止,并继续但不使用站点。否则,站点名称为<app>-audit.localhost。如果存在同名站点,则重用它:该名称由本次审计专用。否则创建该站点:bench new-site <site> --admin-password audit-admin-pw [--db-root-password <pw>]命令绝不能停下来等待输入。如果命令失败,记录错误并继续,但不使用站点。绝不要创建、更改、删除或迁移其他站点。
-
安装目标应用所需的应用,然后安装目标应用:
bench --site <site> install-app <app>。 -
创建测试用户。每个运行工作进程都有自己的一组用户,这样任务就能更改自己的用户,且任务后的重置不会影响其他任务。创建
jobs组,编号从 0 开始。第k组包含以下用户,每个用户的密码均为audit-user-pw,enabled = 1,且不需要完成引导流程或重置密码:milkshake-s<k>[email protected]:Website User,不赋予其他角色milkshake-s<k>[email protected]:普通 System User,不赋予其他角色。如果框架根据角色推导用户类型,则没有 desk 角色的用户属于 Website User。然后创建一个名为milkshake Desk User的角色,赋予 desk 访问权限但不授予任何权限,并将该角色分配给此用户。milkshake-s<k>[email protected]:System User 和 System Manager,用于对比管理员已有的权限- 对于应用定义的每个角色(即应用 DocType、页面和报表权限中出现的、框架未定义的角色):创建两个用户,
milkshake-s<k>-<role slug>[email protected]和[email protected],且每个用户仅有该角色。同一角色的两个用户可让验证者测试其访问另一用户记录的权限。
记录每个用户保存后的角色。应用逻辑可能会添加角色。
-
编写重置脚本
<run dir>/reset_site.py,并记录运行该脚本的命令。每个任务完成后,运行程序都会调用此命令,并将{slot}替换为组编号。脚本会将该组的每个用户恢复到记录的状态:角色(精确一致)、enabled = 1、用户类型和密码。脚本还会再次将 Administrator 的密码设为audit-admin-pw。脚本不做其他任何更改,且在几秒内完成,不使用bench console。例如,可使用<bench>/sites中的 bench Python 运行脚本,并针对站点调用frappe.init和frappe.connect。对每组各运行一次该命令,并确认退出码为 0。 -
在后台提供站点服务:在 bench 目录中运行
bench serve --port 8199。如果站点需要Host标头,则记录该标头。 -
记录会影响控件行为的配置。读取
sites/common_site_config.json和站点的site_config.json。至少记录ignore_csrf、developer_mode、allow_tests、server_script_enabled、disable_website_cache、maintenance_mode,以及每个*_disabled或allow_*键和值。对于会禁用或放宽控件的键,将其标记为weakens: true,并用一行说明其影响。设置了ignore_csrf: 1的 bench 会接受生产环境会拒绝的跨站请求,因此该环境返回 200 不能证明生产环境也会接受。 -
证明站点正常运行:
curl -sS -o /dev/null -w '%{http_code}' <base url>/api/method/ping返回 200- 使用
[email protected]通过/api/method/login登录,返回 200 并设置 cookie bench --site <site> list-apps显示该应用 检查失败时,不要使用站点。一个勉强可用的站点比没有站点更糟,因为验证者会把它的错误视为证据。
-
写入
<run dir>/site.json。slots按集合顺序,为每个集合保存一个用户列表。notes保存每个 Agent 都必须了解的站点信息:例如,站点初始时没有记录。{ "ready": true, "site": "myapp-audit.localhost", "baseUrl": "http://localhost:8199", "hostHeader": "myapp-audit.localhost", "bench": "/abs/path/to/bench", "created": true, "apps": ["frappe", "myapp"], "adminPassword": "audit-admin-pw", "slots": [[{"email": "[email protected]", "password": "audit-user-pw", "actor": "plain System User", "roles": ["milkshake Desk User"]}]], "resetCommand": "cd /abs/path/to/bench/sites && ../env/bin/python /abs/run/dir/reset_site.py {slot}", "notes": "The site starts with no records.", "config": [{"key": "ignore_csrf", "value": "1", "weakens": true, "effect": "CSRF is not checked"}] }如果没有站点,写入
{"ready": false, "reason": "..."}。
5. 运行任务
从技能目录启动脚本,使其与运行环境分离,并将输出发送到
<run dir>/run.log:
mkdir -p <run dir>/tmp && [ -e <run dir>/tmp/last-check ] || touch <run dir>/tmp/last-check
setsid nohup python <skill dir>/scripts/run_audit.py <run dir> --agents <agents file> \
--jobs <jobs> --timeout <timeout> \
--max-candidates <max candidates> --max-verifications <max verifications> \
[--only <prefix> ...] [--skip <prefix> ...] >> <run dir>/run.log 2>&1 < /dev/null &
不要将脚本作为运行环境的后台命令运行。运行环境通常会在固定时间后停止后台命令,例如 2 小时,而一次运行所需的时间更长。脚本有自己的时间限制 --timeout。到达该时限后,它会停止 Agent 并退出。
先使用 --dry-run 启动一次。试运行不会启动 Agent。它会显示任务列表、每种任务的命令,以及每种任务的一份完整提示词。阅读其中一份提示词,并确认站点、用户和路径都正确。
如果运行环境无法启动分离进程,或不允许启动 Agent 进程,请把命令提供给用户,然后停止。用户运行命令,并在运行完成后向你询问其余步骤。
脚本运行期间,不要启动 Agent。脚本会为每个已结束的任务打印一行内容。<run dir>/run.json 包含每个任务的状态,而 <run dir>/logs/<task>.log 包含每个 Agent 的输出。如果脚本在结束前停止,请使用相同参数重新启动。它会继续处理没有有效结果的任务。
告知用户进度
一次运行需要数小时。如果没有更新,用户无法判断是运行较慢还是已停止。因此,请每隔 10 到 15 分钟检查一次进度,直到脚本退出。使用计时器或由你的运行环境唤醒。运行环境不会告知你后台脚本何时退出,因此每次唤醒时都必须启动下一次检查。如果运行环境不具备这两种能力,请告诉用户如何自行查看运行情况(tail -f <run dir>/run.log),并等待脚本退出。
每次检查都在运行目录中执行以下命令:
kill -0 "$(cat run.pid)" 2>/dev/null && echo "script: running" || echo "script: exited"
grep -E '^(ok|FAIL) ' run.log | tail -n 1
grep -vE '^(ok|FAIL) ' run.log | tail -n 5
jq -r '"complete: \([.tasks[] | select(.ok)] | length), failed: \(.failed | length), verifications left in the cap: \(.verificationsLeft), site down: \(.siteDown), timed out: \(.timedOut)"' run.json
jq -r '"now: \(now | strflocaltime("%F %T")), updated: \(.updated), last progress: \(.lastProgress), deadline: \(.deadline)", (.running | to_entries[] | "attempt running since \(.value): \(.key)")' run.json
find verdicts -name '*.json' -newer tmp/last-check -print0 | xargs -0r jq -r \
'select(.verdict == "confirmed") | "\(.severity) \(input_filename | split("/")[1]): \(.finding.title)"'
touch tmp/last-check
最后两条命令只显示上次检查后确认的发现,因此不会重复告知同一条发现,重启后也一样。
用两到三行告知用户:
- 任务计数:已完成、排队中、正在运行和失败
- 每条新确认的发现单独占一行,注明严重程度和 ID。并说明仍需由人员确认。
- 问题:任务失败、重置失败或网站停止响应
只读取这些输出。不要打开扫描文件和判定文件,也不要评判发现;报告任务会处理这些。
每次检查还必须确认运行仍在取得进展:
- 脚本已退出。
run.log的最后一行是=== exit <code>。如果没有这一行,脚本就崩溃或被停止了。告知用户run.log的最后几行,并使用相同参数重新启动脚本。 - 脚本没有响应。 脚本每分钟更新一次
run.json。如果updated超过 5 分钟未更新,说明脚本卡住了。使用kill "$(cat run.pid)"停止它,这也会停止它的 Agent,然后重新启动。 - 没有任务结束。 脚本会在 Agent 配置文件中的
timeout时限到达时停止 Agent;agents.example.json中的时限为 1 小时。last progress表示上一次 Agent 尝试结束的时间,或脚本启动的时间。因此,如果last progress,或running中某次尝试的时间,早于该时限加 15 分钟,则说明运行卡住了。按上述方式停止并重新启动。 如果同一次停止发生两次,就停止运行并告知用户。
重启后会继续处理没有有效结果的任务,不会重复执行已完成的任务。
如果测试网站停止响应,脚本就会停止,因为验证器会将已失效网站的错误视为证据。重新启动网站,使用步骤 4.8 确认网站正常,然后重新启动脚本。
所有任务完成时,脚本以 0 退出。以 1 退出时,run.json 会列出失败的任务。报告仍会生成,并会列出这些任务。
当 run.json 中有 "timedOut": true 时,说明运行已达到时限,且报告未运行。告知用户已完成的任务数量,并询问是否继续。要继续,请使用相同参数重新启动脚本。每次启动都会获得完整的 --timeout 时限。
仅当运行创建了该网站时,才使用 drop site:停止 bench serve,并运行 bench drop-site <site> --force。
6. 告知用户
读取 <run dir>/summary.json,以及 <run dir>/report.md 的标题和汇总表。告知用户:
- 报告和运行目录的路径
summary.json中各赛道和严重程度对应的已确认候选数量,以及已证伪和不确定的数量。报告会按根本原因合并候选,因此其中的发现较少。请读取报告标题中的去重后数量。- 报告汇总表中 Critical 和 High 级别发现的标题,每行一个
- 如果运行保留了测试网站,则告知用户网站名称,以便复现发现
- 运行中遗漏的任何内容:失败的任务、因数量上限而未验证的候选、标记为
not applicable的检查、失败的 semgrep 步骤
不要在回复中复制报告内容。不要提出修复建议。
如果报告任务失败,运行目录中仍保留所有结果。告知用户这一点,并提出重新启动脚本;它只会运行报告。
提示词
任务提示词模板位于 prompts/ 中。脚本会使用 setup.json 和
site.json 填充这些模板,并将填充后的提示词分别保存到 <run dir>/prompts/<task>.md。
| 模板 | 用途 |
|---|---|
context.md | 每个提示词的开头部分:路径、基准、清单、semgrep、站点、用户、规则 |
scan.md | 一个范围或规则,并写入 scans/<id>.json |
verify.md | 一个候选项,并写入 verdicts/<id>/<n>.json |
check.md | 一项针对整个表面的检查,并写入 checks/<id>.json |
report.md | 报告,并写入 report.md |
{{name}} 是一个值。{{#if name}}...{{else}}...{{/if}} 会在值已设置时保留其中一个部分。
如果模板引用了脚本不认识的值,脚本会在开始时停止。更改模板中的结果格式时,也要一并更改 run_audit.py 中 validate() 的检查。
警告
输出由 Agent 生成。任何人采取行动前,必须由人员确认每一项发现。请注意误报和遗漏。报告中未发现问题,并不能证明应用是安全或正确的。