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, 安全插件, 逆向工具, 静态应用安全测试