- 应用安全
- 漏洞扫描
- 人工智能
- AI Agent
【免费下载链接】deepsec
Deepsec is a security harness for finding vulnerabilities in your codebase powered by coding agents
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 initsetup agent 会完成三件事:
- 盘点仓库的入口面(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暴露级别——这正是「哪些入口值得安全审查」的机器可读抽象。 - 运行内置 matcher:加载默认注册表(
createDefaultRegistry,见 packages/scanner/src/matchers/index.ts)对仓库做一次免费的全量扫描。 - 应用确定性的覆盖策略:对每个 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/**、绝对路径、盘符路径、!/#前缀、**、**/*等无约束 glob | globSafetyError |
| 重复 slug | 组内重复、与既有内置/插件 slug 冲突 | declarativeMatcherSpecsSchema+existingSlugs检查 |
| 空正则 | 空字符串 source | regexPatternSchema的z.string().min(1) |
| 反向引用 / 后顾 | \1、\k<name>、(?<=、(?<! | regexSafetyError |
| 指数回溯形状 | 嵌套量词(a+)+、量化的多选(a\|aa)+、多个通配重复.*...* | regexSafetyError |
| 超大重复 | 重复次数上限超过 1000 | regexSafetyError |
| 不触发的示例 | 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):
| 字段 | 类型 | 说明 |
|---|---|---|
slug | string | 全局唯一的 kebab-case 标识,会写入候选的vulnSlug |
description | string | 人类可读的描述 |
noiseTier | "precise" \| "normal" \| "noisy" | 噪音分级,用于排序处理优先级 |
filePatterns | string[] | 该 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 编写时,给它以下阅读清单:
.deepsec/data/<id>/setup/surface-inventory.json—— 目标 surface 的结构化清单;.deepsec/generated-matchers.ts—— 已覆盖的缺口,避免重复;.deepsec/data/<id>/files/—— 候选数量与已 revalidate 的 findings,判断真实命中率;.deepsec/node_modules/deepsec/dist/config.d.ts——MatcherPlugin类型定义(安装后工作区内的类型声明;仓库源码对应 packages/core/src/plugin.ts);.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
相关推荐
从零编写自己的安全规则:Deepsec 自定义 Matcher 插件开发实战
从零编写自己的安全规则:Deepsec 自定义 Matcher 插件开发实战 Deepsec 是一款由 AI 编码智能体驱动的安全漏洞扫描工具,能在你的代码库中
应用安全漏洞扫描人工智能AI AgentAI 自动补全扫描盲区:Deepsec 声明式 Matcher 生成与安全校验机制深度解析
AI 自动补全扫描盲区:Deepsec 声明式 Matcher 生成与安全校验机制深度解析 Deepsec 是一款由编码智能体驱动的漏洞扫描器(security
应用安全漏洞扫描人工智能AI AgentOpenCreator:视频翻译配音一键出片的本地开源AI工作台
OpenCreator:视频翻译配音一键出片的本地开源AI工作台 OpenCreator(原名 KrillinAI)是一个面向创作者的开源 AI 工作台,以 C
人工智能AI 应用AI AgentAI 技能媒体生成音视频桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考