secwest/copilot-security-opus

GitHub: secwest/copilot-security-opus

一款 GitHub Copilot CLI 安全扫描插件,通过威胁建模、发现、验证和攻击路径分析等可审计的 pipeline 来查找、证明并修复代码漏洞,同时输出密封的机器可读扫描契约和 SARIF 报告。

Stars: 0 | Forks: 0

# Copilot 安全 `copilot-security` 使用 [GitHub Copilot CLI](https://docs.github.com/copilot/how-tos/copilot-cli) 查找、验证并修复代码中的安全漏洞。 它是一个独立的扫描器:一个承载着安全工作流、密封的机器可读扫描契约的 Copilot CLI 插件,以及用于驱动并验证它的 CLI 和 TypeScript SDK。部分扫描契约和工作流源自先前的 Apache-2.0 工作;请参阅 [`NOTICE`](NOTICE)。 ## 它的功能 一次扫描并不是对不良模式的 grep。它运行一个明确且可审计的 pipeline: 1. **威胁建模** — 确定仓库保护的内容、其信任边界以及其受攻击者控制的输入。 2. **发现** — 审查范围内的每个文件,并记录具有具体 source、control 和 sink 位置的候选发现。 3. **验证** — 尝试*证明或证伪*每个候选结果,优先使用崩溃的 PoC、聚焦的测试或真实的接口复现,而不是仅仅做出断言。当无法进行验证时,每个候选结果都会获得一个处理结论和明确的证明缺口。 4. **攻击路径分析** — 根据威胁模型确定可达性,寻找反证,然后校准严重程度。 5. **报告** — 输出密封的、机器可读的契约以及确定性的 Markdown 和 SARIF 投影。 输出区分了**“未观察到”**和**“未扫描”**。覆盖范围不完整的扫描会以非零状态退出,而不是看起来像顺利通过。 ### 为什么空结果不会被自动相信 “未发现问题”和“未检查任何内容”会产生完全相同的 `findings.json` 文档。一个无法区分这两者的扫描器,最终会给一个它从未读取过的仓库开出一张健康的无暇证明。 因此,空的发现集必须有确凿证据支持。要么发现阶段记录了随后被抑制的候选结果——这证明发现和验证阶段都运行了的直接证据——要么覆盖范围会以不止一行笼统的记录来列举其检查过的表面。如果既没有候选结果,覆盖范围又不是详尽的文件清单,扫描器会将覆盖范围降级为 `unknown`,发出警告并以状态码 `2` 退出。一个空仓库仍然会正常通过。 在每次运行后,规范文档也会根据内置的 JSON Schema 进行验证。当它们验证失败时,CLI 会将确切的错误返回以进行有限的修复过程,而不是丢弃一份合理的分析——一个发现了真实 Bug 但写出了格式错误契约的扫描是值得修复的,而不是重新运行。 ## 要求 - Node.js 22.13+、24.x 或 26.x - Python 3.10 或更高版本(3.10 还需要 `tomli`) - GitHub Copilot CLI 1.0.70 或更高版本:`npm install -g @github/copilot` - 拥有 Copilot 访问权限的 GitHub 账户 ## 快速开始 ``` npm install -g @secwest/copilot-security copilot-security login copilot-security scan . ``` 常见变体: ``` copilot-security scan . --path src --path lib # scope to paths copilot-security scan . --diff origin/main # committed changes copilot-security scan . --working-tree # staged + unstaged copilot-security scan . --mode deep # repeated discovery copilot-security scan . --json --fail-on-severity high ``` ## 选择模型 扫描默认使用 `--model auto`,这让 Copilot 可以路由到您的账户有资格使用的任何模型。当您需要可复现性时,请固定一个模型: ``` copilot-security models # list known models copilot-security scan . --model gpt-5.6-sol --effort xhigh ``` 默认值来自您自己的 Copilot `settings.json`,因为这反映了您的账户有资格使用的内容。推理努力默认为 `xhigh`;不继承更便宜的交互设置,因为低于 `high` 的任何设置都会导致明显更差的 source 到 sink 的追踪。CLI 完全拒绝低于 `low` 的努力设置。 模型授权是按账户划分的,无法从外部枚举。如果固定的模型不可用,Copilot 会在任何推理运行之前拒绝它,因此扫描会使用 `--model auto` 重试一次,并报告该替换情况,而不是因为一个您没有选择的细节而失败。 请注意,`--model auto` 会忽略 `--effort`:路由会根据每次请求选择模型,并因此决定其支持的努力等级。当您需要保证推理努力时,请固定一个模型。 ## 当提供商拒绝扫描时 读取易受攻击代码的扫描器有时会触发模型提供商的 滥用过滤器: ``` CAPIError: 422 This content was flagged for possible cybersecurity risk. ``` 这对防御性审查来说是一个误报——您是在寻找自己 仓库中的缺陷——而且提供商自己的消息也说明 重新表述并重试。 `copilot-security` 会自动执行此操作,仅在必要时升级, 并报告每一个步骤: | 尝试 | 变化 | | --- | --- | | 1 | 明确声明防御目的来重述请求 | | 2 | 同时停止要求提供有效的漏洞利用;通过阅读代码来验证发现 | | 3 | 同时使用不同提供商的模型重试,因为内容策略是按提供商应用的 | 第二和第三步**确实削弱了验证能力**,因此扫描会在 其警告中说明这一点,并且发现的置信度较低。这是一个 诚实的权衡:通过阅读来证明弱于通过复现来证明,并且报告 决不能暗示并非如此。 它故意不采取的做法是伪装扫描的本质、对内容 进行编码以混过分类器,或者删除表明这是安全工作的 声明。那些属于规避而不是澄清, 并且它们会产生更糟糕的扫描。 使用 `--refusal-attempts ` 限制或禁用它(默认 3 次,设为 `0` 则 立即失败)。如果拒绝在阶梯式升级后仍然存在,错误信息会直接说明 —— 届时这是一个账户授权问题,而提供商的 可信访问计划才是真正的解决办法。 ## 身份验证 Copilot CLI 会*优先于*已存储的登录来使用 `COPILOT_GITHUB_TOKEN`、`GH_TOKEN` 和 `GITHUB_TOKEN`。因此,您 shell 中的过期 token 将覆盖一个完全有效的登录,并导致每次扫描都以 401 失败。 `copilot-security` 会在开始前检查 token,并在 GitHub 拒绝它时回退到已存储的登录: ``` copilot-security login status # GH_TOKEN 被 github.com 拒绝。回退到已存储的 Copilot # 为 登录。取消设置 GH_TOKEN 以忽略此消息。 ``` 使用 `--auth` 显式选择凭据: | 值 | 行为 | | --- | --- | | `auto`(默认) | Copilot 优先级,当环境 token 被拒绝时进行回退 | | `token` | 需要环境 token | | `stored` | 忽略环境 token 并使用已存储的登录 | 对于 CI,请设置一个具有 **Copilot Requests** 权限的 token 并使用 `--auth token`。Copilot CLI 不支持经典的 `ghp_` token。 ## 扫描目标 | 标志 | 目标 | 覆盖模式 | | --- | --- | --- | | *(无)* | 整个仓库 | `repository` | | `--path ` | 指定路径(可重复) | `scoped_path` | | `--diff ` | 已提交的范围 | `commit` / `branch_diff` | | `--diff --diff-head ` | 显式范围 | `branch_diff` | | `--working-tree` | 暂存和未暂存的更改 | `working_tree` | | `--mode deep` | 重复的仓库发现 | `deep_repository` | 扫描目标类型反映了*审查的内容*,而不是扫描是如何调用的。 ## 深度模式 `--mode deep` 通过独立运行多次发现并合并结果来减少差异,而不是信任单次扫描。Worker 之间永远不会看到彼此的候选结果——这种独立性是其目的所在。 当连续几轮停止产生新的候选结果时(饱和),循环就会停止,而不是在固定的次数下停止,因此稀有发现的尾部仍然可以触及。然后,验证、攻击路径分析和报告在合并后的集合上运行一次。 深度模式的成本显著增加。请使用 `--max-credits` 对其进行限制。 ## 输出 每次扫描都会将一个 bundle 写入到 `//`: | 文件 | 作用 | | --- | --- | | `scan-manifest.json` | 包含构件摘要的密封已完成扫描凭证 | | `findings.json` | 规范的语义发现记录 | | `coverage.json` | 已审查、已排除和已延迟的内容 | | `report.md` | 确定性的 Markdown 投影 | | `exports/results.sarif` | SARIF 投影 | | `artifacts/` | 威胁模型、文件清单、候选账本、证据 | 这三个 JSON 文档是事实来源;`report.md` 和 SARIF 是投影,可以重新生成。CLI 在报告结果之前会验证每个密封的摘要,因此更改过的 bundle 会导致错误,而不是静默通过。 结果必须位于被扫描的工作树之外——将它们写入工作树内部会改变正在被扫描的树,并面临提交漏洞利用细节的风险。 ``` copilot-security export --export-format sarif --output results.sarif copilot-security export --export-format csv --output findings.csv copilot-security export --export-format json --output findings.json ``` 导出操作读取的是密封的 bundle,并且从不启动 Copilot 或加载凭据。 ## CI ``` copilot-security scan . \ --diff origin/main \ --output-dir "$RUNNER_TEMP/results" \ --auth token \ --json \ --fail-on-severity high > findings.json ``` 退出代码: | 代码 | 含义 | | --- | --- | | `0` | 仅完成报告的扫描,或通过的策略 | | `1` | 完成的扫描违反了 `--fail-on-severity` | | `2` | 无效的输入、不完整的覆盖范围,或运行时/导出错误 | | `130` | 被中断 | | `143` | 被终止 | 覆盖范围不完整会退出 `2`,绝不是 `0`。无法审查所有内容的扫描决不能读取为通过。 ## 桌面应用 ``` copilot-security gui ``` 在您的浏览器中打开一个本地应用,涵盖与 CLI 相同的工作流: 目标和范围选择、模式、模型和努力、实时进度、包含 严重性、位置、CWE 和修复细节的发现列表、导出、驳回、 生成的报告以及会话历史。它驱动的是相同的 SDK,因此 GUI 和 CLI 不会产生分歧。 服务器仅绑定到 loopback,并生成一个包含在启动 URL 中的会话 token。任何拥有该 URL 的人都可以启动扫描并读取 您能读取的文件,因此请像对待终端会话一样对待 它。`--host` 拒绝任何非 loopback 地址。 ## 其他命令 ``` copilot-security validate "Possible SQL injection in src/query.ts:42" copilot-security patch findings.json copilot-security install-hook # pre-commit working-tree scan copilot-security info --json copilot-security models ``` `validate` 和 `patch` 接受文件路径或字面文本,最多 64 个输入且总计不超过 1 MiB。 ### 驳回误报 一个每次运行都报告相同错误发现的扫描器,比一个漏掉它的扫描器更糟糕:审查者会学会忽略输出结果。 ``` copilot-security findings false-positive OCCURRENCE_ID \ --reason "The route already checks tenant ownership" copilot-security findings list copilot-security findings undismiss OCCURRENCE_ID ``` 驳回的范围仅限于其**原因**,而不仅仅是一个 ID。随后的扫描会重新检查该原因在当前代码下是否仍然成立,因此驳回的寿命不能超过它所依赖的控制。正是由于这个原因,`--reason` 是必填的。驳回作为不受信任的数据传递给每次扫描,绝不作为指令。 ## TypeScript SDK ``` import { CopilotSecurity } from "@secwest/copilot-security"; const security = new CopilotSecurity(); try { const result = await security.run("/path/to/repository", { outputDir: "/path/outside/repository/results", failureSeverity: "high", }); console.log(result.reportPath, result.summary.total, result.complete); } finally { await security.close(); } ``` `security.preflight(...)` 会验证仓库、目标、模式和输出位置,而无需启动 runtime 或加载凭据。 ## 配置 | 变量 | 效果 | | --- | --- | | `COPILOT_SECURITY_STATE_DIR` | 私有状态、工作台数据库和默认的 artifact 根目录 | | `COPILOT_HOME` | 外部 Copilot 主目录;扫描使用位于状态目录下的私有主目录 | | `COPILOT_CLI_PATH` | Copilot CLI 的 `npm-loader.js` 的路径 | | `PYTHON` | 用于插件脚本的 Python 解释器 | | `COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, `GITHUB_TOKEN` | 扫描凭据,按此优先级排列 | 诸如 `COPILOT_SECURITY_SCAN_ID`、`COPILOT_SECURITY_SCAN_DIR` 和 `COPILOT_SECURITY_TARGET_PATHS_FILE` 等变量是每次扫描生成的。它们是内部的 runtime 数据,而不是用户配置。 ### 与其他扫描器一起运行 此扫描器在磁盘上拥有的所有内容都进行了命名空间隔离,因此它可以与基于相同插件布局构建的其他安全扫描器共存: | 资源 | 位置 | | --- | --- | | 状态和工作台数据库 | `$COPILOT_HOME/state/plugins/copilot-security-opus/` | | 私有凭据主目录 | `…/copilot-security-opus/copilot-security-opus-home/` | | 扫描 bundle | `…/copilot-security-opus/scans/`, 或 `/copilot-security-opus-scans/` | | Copilot 插件名称 | `copilot-security-opus` | | 环境前缀 | `COPILOT_SECURITY_*` | | 文档类型和指纹 | `copilot-security.*`, `copilot-security/v1` | 因此,来自不同扫描器的发现可以在 SARIF 和规范的 JSON 中区分开来,并且任何一方都不能覆盖另一方的工作台或凭据。 ## 本地安全模型 `copilot-security` 使用您的本地操作系统权限运行。仅扫描您信任且您拥有或被授权评估的仓库。 每次扫描都是针对一个**私有的 `COPILOT_HOME`** 运行的,因此无关的用户配置、MCP 服务器、已安装的插件和自定义指令无法更改扫描的操作或所见内容。只有您的 Copilot 账户状态(`config.json`)会被保留,因为 Copilot 需要从中解析模型授权。runtime 会从扫描子进程中剥离云端和模型提供商凭据(`AWS_*`、`AZURE_CLIENT_SECRET`、`OPENAI_API_KEY 等),因为扫描会读取您的整个仓库并运行受仓库影响的命令。 **被扫描的仓库不能指挥其自身的扫描。** Copilot 会从工作目录及其 Git 根目录中发现项目技能和自定义指令,因此,如果仓库中包含 `.github/skills/`,该技能 otherwise 会被加载到审查它的扫描中。因此,扫描从中性目录运行,并通过 `--add-dir` 访问仓库。仓库内容——包括 `AGENTS.md`、`README.md` 和任何指令文件——在 prompt 中被命名为关于代码的不受信任的证据,而不是作为指令。 扫描 prompt 作为进程参数传递,从不通过 shell。在 Windows 上,这意味着直接解析 Copilot CLI 的 Node 入口点,而不是其 `.cmd` shim,因此不受信任的文本永远不会到达 `cmd.exe` 引用。 用户提供的上下文(`--context`、知识库文件、`SECURITY.md`)在 prompt 中被标记为不受信任。它可以缩小焦点或声明排除项;但不能重定向工作流。 针对未修复的 bug,扫描结果包含漏洞利用细节。在 POSIX 主机上,现有的结果目录的权限必须设为 `700`。请将结果目录保留在仓库之外,并限制只有被授权的审查者才能访问。 ## 衡量扫描器 一个您无法衡量的扫描器就是您无法改进的扫描器。 [`benchmark/`](benchmark/) 根据具有已知真实情况的 fixture 对此扫描器进行评分: ``` cd benchmark node run.mjs --label baseline --model gpt-5.6-sol --effort high node compare.mjs results/baseline.json results/after-change.json ``` 它报告召回率、精确率、严重性准确率、成本,以及两件 容易被忽略的事情:**干净 fixture 上的误报**——带有 正确防护措施的、具有相同危险形状的控制——以及**覆盖范围 诚实度**,这是单独评分的,因为一个什么都没审查并且 什么都没报告的扫描 otherwise 看起来会是完美无瑕的。 ## 测试 ``` cd sdk/typescript && npm test # TypeScript: SDK, CLI surface, GUI, plugin integrity python -m unittest discover -s plugin/tests -v # Python: contract pipeline node benchmark/selftest.mjs # Benchmark scorer node benchmark/validate-fixtures.mjs ``` ## 仓库布局 ``` benchmark/ Fixtures with ground truth, scorer, and runner plugin/ Copilot CLI plugin (skills, agents, schemas, scripts) skills/ 13 security workflow skills agents/ Discovery and validation worker agents references/ Shared contracts, guidance, vulnerability taxonomy schemas/ Canonical JSON Schemas scripts/ Python workbench, normalizer, finalizer, SARIF preflight/ Capability profile registry sdk/typescript/ TypeScript SDK and `copilot-security` CLI src/gui/ Local desktop app ``` ## 许可证 Apache-2.0。请参阅 [`LICENSE`](LICENSE) 和 [`NOTICE`](NOTICE)。
标签:GitHub Copilot, MITM代理, SARIF, TypeScript, 安全插件, 逆向工具, 静态应用安全测试