news 2026/9/26 2:39:06

Deepsec Matcher 编写实战:从 setup 自动生成的声明式 Matcher 到手写 MatcherPlugin

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Deepsec Matcher 编写实战:从 setup 自动生成的声明式 Matcher 到手写 MatcherPlugin
  • 应用安全
  • 漏洞扫描
  • 人工智能
  • AI Agent

【免费下载链接】deepsec

Deepsec is a security harness for finding vulnerabilities in your codebase powered by coding agents

项目地址:https://gitcode.com/gh_mirrors/deeps/deepsec
点击查看免费下载

Deepsec 的扫描覆盖能力由「matcher(匹配器)」驱动:初始化阶段由 setup agent 自动生成严格受限的声明式 matcher 来填补扫描盲区,而当规则需要负向条件、跨搜索或组织特定语义时,则需要手写 TypeScriptMatcherPlugin。本文以 docs/writing-matchers.md 为骨架,结合仓库源码(packages/scanner/src/declarative-matcher.ts、packages/core/src/plugin.ts、packages/deepsec/src/setup/coverage.ts等)深入讲解这两条路径:从「保留还是编辑生成的 matcher」的取舍,到「手写 matcher 的字段、噪音分级与注册方式」的完整实操,读完你可以在自己的.deepsec/工作区里安全地新增、调优并贡献 matcher。

从 setup coverage 开始:初始化阶段如何决定是否需要自定义 matcher

正常初始化流程已经内置了「是否需要自定义 matcher」的决策:

npx deepsec init

setup agent 会完成三件事:

  1. 盘点仓库的入口面(ingress surfaces):对仓库做只读分析,产出结构化的 surface inventory。从packages/deepsec/src/setup/coverage.ts可以看到,surface 被划分为http、rpc、queue、cron、cli、webhook、agent-tool、other等 kind,并标记public/authenticated/internal/mixed/unknown暴露级别——这正是「哪些入口值得安全审查」的机器可读抽象。
  2. 运行内置 matcher:加载默认注册表(createDefaultRegistry,见 packages/scanner/src/matchers/index.ts)对仓库做一次免费的全量扫描。
  3. 应用确定性的覆盖策略:对每个 surface 检查文件级与代表文件级覆盖率。只有存在具体缺口时(例如某个未被覆盖的内部 RPC 注册表、队列消费者家族、或框架路由原语),才生成 matcher 提案。

被接受的提案会写入.deepsec/generated-matchers.ts,作为严格的数据(strict data)存在,并通过generatedMatchersPlugin加载。这一点在源码中有直接对应:writeGeneratedMatchers(packages/deepsec/src/setup/generated-matchers.ts)把 JSON 规格序列化进一个.ts文件,文件内容只是compileDeclarativeMatchers(specs)的调用,没有任何模型手写的执行代码。

务必 review 并提交generated-matchers.ts。不要复制生成的 setup inventory 或 setup-state 文件——它们是可再生产物,已被 gitignore。

初始化阶段的整体位置可以对照 docs/architecture.md 的流水线图:scaffold → install → link/model/Sandbox → INFO + inventory → baseline scan → coverage policy → declarative matcher generation → final scan → process,matcher 生成是覆盖策略与付费 AI 处理之间的必经关卡。

声明式 matcher 的安全契约:模型只写数据,不写代码

生成的规格由compileDeclarativeMatchers编译;模型写出的 TypeScript 永远不会被执行。编译入口在 packages/scanner/src/declarative-matcher.ts:先用 Zod 严格解析(.strict()拒绝未知字段),再按规格构造正则与排除 glob,最后返回一个普通MatcherPlugin。测试 packages/scanner/src/tests/declarative-matcher.test.ts 明确验证了「带match可执行字段的输入会被拒绝」以及「输入数据会被structuredClone脱离,调用方后续修改不污染编译结果」。

每个 spec 必须声明:

  • 唯一的kebab-case slug、描述和噪音分级(noise tier);
  • 受限的相对文件 glob(约束目录、文件名或扩展名);
  • 可选的技术栈 / 哨兵文件门(tech / sentinel-file gates);
  • 有界正则,且只允许i、m或im三种 flag(allowedFlagsSchema = z.enum(["i", "m", "im"]));
  • examples——每个被提案的 matcher 必须真实命中的示例;
  • 该 matcher 声称要关闭的surface ID 列表(closesSurfaceIds)。

校验拒绝清单(源码级)

验证会拒绝以下全部情况,具体规则都能在declarative-matcher.ts中找到实现:

类别被拒绝的内容源码依据
未知字段schema 之外的任何可执行字段Zod.strict()
遍历/兜底 glob../src/**、绝对路径、盘符路径、!/#前缀、**、**/*等无约束 globglobSafetyError
重复 slug组内重复、与既有内置/插件 slug 冲突declarativeMatcherSpecsSchema+existingSlugs检查
空正则空字符串 sourceregexPatternSchema的z.string().min(1)
反向引用 / 后顾\1、\k<name>、(?<=、(?<!regexSafetyError
指数回溯形状嵌套量词(a+)+、量化的多选(a\|aa)+、多个通配重复.*...*regexSafetyError
超大重复重复次数上限超过 1000regexSafetyError
不触发的示例examples 必须被至少一个声明 pattern 命中superRefine对每个 example 逐一测试
越界限制正则 >500 字符、glob >240 字符、example >10000 字符、patterns 1–32 个、filePatterns 1–32 个、closesSurfaceIds 1–64 个、整套 specs ≤64 个各 schema 常量

glob 的约束细节值得一提:必须用正斜杠、必须相对仓库根、不允许../.遍历段、花括号组 ≤3、逗号备选 ≤16、星号 ≤12,并且会跑一组广度探针(README.md、src/index.ts、.env、deep/a/config.yaml)——如果这些互不相关的文件全部命中,说明 glob 太宽,直接拒绝。这套规则的意图是:setup agent 只写覆盖性 matcher,不需要任何依赖引擎微妙回溯行为的正则特性。

编译后的爆炸策略与可恢复的停止

编译之后,setup 会重新扫描并应用爆炸策略(explosion policy)。在evaluateCoverage中,每个新 matcher 的命中文件数会被统计:

  • 命中文件数 >matcherMaximumFiles(默认500)——直接判为爆炸;
  • 或命中文件数占非忽略源文件比例 >matcherMaximumSourceRatio(默认20%,且仓库 ≥5 个文件时才启用比例门)——同样判为爆炸。

爆炸的 matcher 会从插件中移除,其已持久化的候选(candidates)在再次尝试前被删除。每次调用 setup最多做两次生成/重扫尝试;如果覆盖仍然失败,则在任何付费 AI 处理开始前停止。这个停止是可恢复的:停止时打印的 actions 指向已保存的提案与覆盖证据,重新运行 setup 会再做两次全新的修复尝试。

何时保留或编辑一个生成的 matcher

保留它的条件:其文件范围和正则描述的是一个稳定存在的仓库原语(stable repository primitive),且候选数量接近对应的入口点数量(说明它命中了你真正想审查的入口,而不是泛泛的噪音)。

编辑或删除它,当出现以下任一情况:

  • glob 跟随的是生成代码(generated code)而非真实的入口家族;
  • 正则命中的是某个偶然的标识符(incidental identifier),而不是框架形态本身;
  • examples 不能代表仓库的真实语法;
  • 它声称关闭某个 surface,实际却无法触及该 surface;
  • 一个手写 matcher 能以更精确的方式表达同一条件。

编辑完数据后,运行两条命令做验证:

pnpm deepsec scan --matchers <slug> pnpm deepsec setup

第一条是聚焦的定点检查(spot-check),第二条用完整的覆盖评估来对账。--matchers过滤在 packages/deepsec/src/commands/scan.ts 中解析(逗号分隔、按 slug 过滤),扫描结束后还会输出Matchers that fired命中表、Top files by candidate count和低覆盖警告——低覆盖警告甚至会直接提示你去看 docs/writing-matchers.md 编写自定义 matcher。

何时该手写 MatcherPlugin

声明式 matcher 被刻意限制为安全的正则扫描。当规则需要以下能力时,就应该写 TypeScriptMatcherPlugin:

  • 负向条件:例如「没有任何 auth helper 的路由声明」——纯正则只能表达「命中了什么」,无法表达「缺少什么」;
  • 对同一文件的多次相关搜索:需要把多个模式的命中结果组合判断;
  • 语法感知的预处理或上下文窗口:需要读取文件内容做结构化分析(如排除 test 文件、跳过_internal/目录);
  • 组织特定的语义:这些规则应当以代码形式被 review,而不是藏在数据里;
  • 值得向上游贡献的可复用公共框架 / CWE 规则。

另外,当一次 revalidate(重新验证)确认的 true positive 暴露出一个稳定的兄弟模式(sibling pattern),而 setup 的入口点覆盖并没有建模它时,也值得考虑手写 matcher。

一个现实中的负向条件范例是 samples/webapp/matchers/webapp-route-no-rate-limit.ts:它先排除测试文件、_internal/与 webhook 路径,再检查文件中是否出现withRateLimit(、rateLimiter.check(、ratelimit.limit(等保护性调用,若全无且存在export ... GET/POST/...导出,才产出候选——这是纯声明式 spec 无法表达的「缺失检测」。

手写 matcher 的工作区布局

把更丰富的 matcher 放在生成的插件旁边:

.deepsec/ ├── deepsec.config.ts ├── generated-matchers.ts └── matchers/ ├── my-route-no-auth.ts └── my-internal-rpc.ts

通过一个叠加式(additive)内联插件注册它们:

import { defineConfig, type DeepsecPlugin } from "deepsec/config"; import { generatedMatchersPlugin } from "./generated-matchers.js"; import { myRouteNoAuth } from "./matchers/my-route-no-auth.js"; import { myInternalRpc } from "./matchers/my-internal-rpc.js"; const projectMatchers: DeepsecPlugin = { name: "my-app-matchers", matchers: [myRouteNoAuth, myInternalRpc], }; export default defineConfig({ ai: { mode: "gateway", provider: "vercel" }, projects: [{ id: "my-app", root: ".." }], plugins: [generatedMatchersPlugin, projectMatchers], });

要点:

  • slug 必须全局唯一。一次性生成器(one-shot generator)会拒绝与内置 matcher、插件 matcher 以及同批响应内部的 slug 冲突(见compileDeclarativeMatchers的existingSlugs去重逻辑与 packages/scanner/src/tests/declarative-matcher.test.ts 的冲突测试)。手写的变体请使用独立的 slug,而不是依赖注册顺序去覆盖内置规则。
  • 插件系统是叠加的:matchers、notifiers、agents都是 additive,多个插件的贡献会全部注册(ownership、people、executor才是后写覆盖)。插件接口定义见 packages/core/src/plugin.ts。
  • 想直观地看到一个「被长期维护过的扫描工作区」长什么样,可阅读 samples/webapp/README.md 与它的 deepsec.config.ts——它在配置里内联读取INFO.md、注册了两个自定义 matcher 并通过priorityPaths声明了重点审查目录。

Matcher 形态详解:MatcherPlugin 与 regexMatcher

一个完整的手写 matcher:

import { regexMatcher, type MatcherPlugin } from "deepsec/config"; export const myInternalRpc: MatcherPlugin = { slug: "my-internal-rpc", description: "Internal RPC entry points", noiseTier: "normal", filePatterns: ["src/rpc/**/*.ts"], examples: ['registerRpc("users.get", handler)'], match(content) { return regexMatcher( "my-internal-rpc", [{ regex: /registerRpc\s*\(/g, label: "RPC registration" }], content, ); }, };

MatcherPlugin的完整字段(见 packages/core/src/plugin.ts):

字段类型说明
slugstring全局唯一的 kebab-case 标识,会写入候选的vulnSlug
descriptionstring人类可读的描述
noiseTier"precise" \| "normal" \| "noisy"噪音分级,用于排序处理优先级
filePatternsstring[]该 matcher 作用的文件 glob
requires?MatcherGate可选门控:tech(技术栈 tag)、sentinelFiles(哨兵文件 glob)、sentinelContains(对命中文件内容做更深检查)
examples?string[]内联测试用例:每个字符串必须产生 ≥1 个候选
match(content, filePath) => CandidateMatch[]核心匹配逻辑

门控(MatcherGate):让 matcher 只在合适的仓库里激活

requires门在每次扫描开始时对项目根解析一次(不是每个文件一次),提供两层控制:

  • tech:匹配detectTech()归一化后的技术栈 tag(如"laravel"、"nextjs"、"django"、"rails"),任意命中即激活。这是首选快捷方式。
  • sentinelFiles+ 可选sentinelContains:当detectTech还不认识你的技术栈、或需要比一个 tag 更精细的判断时使用(例如「只有装了 Livewire 的 Laravel 项目」)。glob 在每次扫描时对项目根求值一次,sentinelContains接收命中的相对路径与文件内容做谓词判断。

内置 matcher 大量使用这一机制。例如 js-express-route.ts 声明requires: { tech: ["express"] }并在文件级排除 test/spec 与node_modules,避免在恰好出现app.get(...)的随机 Node 脚本上误触发。扫描输出里的dormantmatcher 就是这些被门控按下的内置规则。

regexMatcher:逐行扫描、带回溯上下文

regexMatcher是官方辅助函数:对每个 pattern 逐行测试,命中时记录1-based 行号,并截取命中行前 2 行、后 3 行作为snippet上下文;同一文件同一 pattern 只产出一个CandidateMatch(vulnSlug+lineNumbers+snippet+matchedPattern)。多 pattern 时每个 pattern 独立产出候选,这正是声明式 matcher 编译后的运行时行为。

内联 examples 是可执行文档

examples不是运行时逻辑,而是开发期契约:packages/scanner/src/tests/matcher-examples.test.ts 会自动遍历注册表中所有带 examples 的 matcher,为每个示例生成一个测试用例,断言该 matcher 至少产出 1 个候选。因此:

  • 为 matcher 加一个 example 只需要在examples数组里加一行,无需任何测试接线;
  • 好的实践是覆盖每一个子 pattern 的典型语法变体(不同动词、大小写、标识符、空白),任何子 pattern 的笔误都会让 CI 失败;
  • 示例字符串同时充当该 matcher 想要捕获什么的可读文档。

噪音分级

Tier适用场景
precise匹配到的语法本身就是一个强漏洞信号(例如原始 SQL 拼接、硬编码密钥)
normal模式选出值得审查的候选,由 AI 进一步消歧
noisy一个严格有界的入口点家族里的每个文件都值得审查(例如 Express 的每个路由注册)

filePatterns要尽量收窄。避免仓库级的大噪音 glob——内置的 Express matcher 已经用noisy表示「每个路由都值得看一眼」,如果你的自定义规则也是这种语义,请确保 glob 严格限定在入口家族目录内。

用 coding agent 辅助手写 matcher 的工作流

让一个编码 agent 完成一次 matcher 编写时,给它以下阅读清单:

  1. .deepsec/data/<id>/setup/surface-inventory.json—— 目标 surface 的结构化清单;
  2. .deepsec/generated-matchers.ts—— 已覆盖的缺口,避免重复;
  3. .deepsec/data/<id>/files/—— 候选数量与已 revalidate 的 findings,判断真实命中率;
  4. .deepsec/node_modules/deepsec/dist/config.d.ts——MatcherPlugin类型定义(安装后工作区内的类型声明;仓库源码对应 packages/core/src/plugin.ts);
  5. .deepsec/node_modules/deepsec/dist/samples/webapp/—— 更丰富的参考示例(仓库中的对应源码在 samples/webapp/)。

要求 agent:

  • 解释它发现的未被覆盖的 surface;
  • 提出一个有界的 matcher;
  • 补上 examples;
  • 在不移除generatedMatchersPlugin的前提下注册新插件;
  • 跑一次聚焦扫描:
pnpm deepsec scan --matchers <new-slug>

打开几个候选文件核对命中质量,微调 matcher 后,跑全量扫描与 setup 对账。需要提交的是deepsec.config.ts、generated-matchers.ts和matchers/;不要提交生成的data/<id>/setup/证据(gitignored、可再生)。

把可复用的 matcher 贡献进内置注册表

如果这个形态属于某个公共框架或广泛适用的弱点类别,应该把它加进 deepsec 的内置 matcher 注册表,而不是保留一份组织专属副本。内置注册表由 packages/scanner/src/matchers/index.ts 的createDefaultRegistry()组装,按生态分组注册了覆盖 Express、Fastify、NestJS、Django、Flask、Rails、Gin、Spring、.NET、Terraform、Next.js 等框架的路由/控制器/handler matcher,以及跨生态的原始 SQL 逃生舱 matcher(jsSqlRawMatcher、pySqlRawMatcher、jvmSqlRawMatcher等)。

贡献时遵循仓库根目录的 CONTRIBUTING.md,注意:

  • 附带具有代表性的 examples(新 matcher 会自动进入matcher-examples.test.ts的测试矩阵);
  • technology / sentinel 门控尽量收窄,让 matcher 在无关仓库上保持 dormant;
  • 保持 kebab-case slug 全局唯一。

内置 matcher 的写法是很好的学习范本:阅读 packages/scanner/src/matchers/js-express-route.ts 的「wide-net + tech 门控」风格,以及 samples/webapp/matchers/webapp-route-no-rate-limit.ts 的「负向 + 排除路径」风格,几乎涵盖了手写 matcher 的两大类典型模式。

结语与延伸阅读

matcher 体系是 Deepsec 在「免费的正则扫描」与「付费的 AI 分析」之间的桥梁:声明式 matcher 以数据形式安全地承载 setup 自动发现的覆盖缺口,手写MatcherPlugin则以代码形式表达更精细的负向与组织特定规则。判断何时使用哪种形态,核心准则是——能否用一条受约束的正则安全表达?能,就用生成的声明式 spec;不能,就手写插件并配好 examples。

与本文配套的仓库文档:

  • docs/architecture.md —— setup、scan、process、revalidate 流水线与插件架构全貌;
  • docs/configuration.md ——deepsec.config.ts字段、matcher 过滤(matchers: { only, exclude })与插件顺序语义;
  • docs/getting-started.md —— 从npx deepsec init开始的一次性初始化与断点恢复。
  • 应用安全
  • 漏洞扫描
  • 人工智能
  • AI Agent

【免费下载链接】deepsec

Deepsec is a security harness for finding vulnerabilities in your codebase powered by coding agents

项目地址:https://gitcode.com/gh_mirrors/deeps/deepsec
点击查看免费下载

相关推荐

上一篇:3个简单步骤让Mac与Android设备秒传文件:NearDrop完全攻略
下一篇:Cataclysm-DDA Flatpak 构建指南:从清单解析、自定义构建到本地安装分发

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 2:37:57

TensorFlow CNN水果识别毕业设计源码:从环境搭建到模型评估全流程

简介&#xff1a;这份资源是面向计算机相关专业毕业设计学生与希望提升工程能力的开发者的一套TensorFlow卷积神经网络水果图像识别项目源码&#xff0c;难度定位中等&#xff0c;适合作为课程设计、期末项目或毕业设计参考。压缩包共1058个文件&#xff0c;约79.95MB&#xff…

作者头像 李华
网站建设 2026/9/26 2:36:37

LLMs 中的提示缓存:直觉、配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 2:36:31

App请求签名与加密码还原实战:从抓包识别到本地复现

简介&#xff1a;这份资源面向移动应用、小程序与网站开发者&#xff0c;聚焦数字签名与加密的实战代码整理&#xff0c;覆盖自如、小红书、蛋壳公寓、瑞幸咖啡等生活服务类App的签名与加密实现思路&#xff0c;适合需要研究接口安全、逆向分析或加固方案的中高级开发者参考。压…

作者头像 李华
网站建设 2026/9/26 2:36:07

数据库课程设计实战:员工考勤管理系统从需求到SQL实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 2:35:44

QT QTextEdit 自动滚动到底部怎么关?TaoToken 配置骨架与验证清单

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华