bounty-guard 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +204 -0
- package/dist/app/server.d.ts +23 -0
- package/dist/app/server.js +130 -0
- package/dist/app/server.js.map +1 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.js +38 -0
- package/dist/config.js.map +1 -0
- package/dist/diff.d.ts +41 -0
- package/dist/diff.js +191 -0
- package/dist/diff.js.map +1 -0
- package/dist/doctor.d.ts +25 -0
- package/dist/doctor.js +55 -0
- package/dist/doctor.js.map +1 -0
- package/dist/git-scan.d.ts +7 -0
- package/dist/git-scan.js +76 -0
- package/dist/git-scan.js.map +1 -0
- package/dist/github.d.ts +36 -0
- package/dist/github.js +145 -0
- package/dist/github.js.map +1 -0
- package/dist/glob.d.ts +8 -0
- package/dist/glob.js +53 -0
- package/dist/glob.js.map +1 -0
- package/dist/hooks.d.ts +14 -0
- package/dist/hooks.js +45 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +345 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/mock.d.ts +9 -0
- package/dist/llm/mock.js +14 -0
- package/dist/llm/mock.js.map +1 -0
- package/dist/llm/openai.d.ts +50 -0
- package/dist/llm/openai.js +193 -0
- package/dist/llm/openai.js.map +1 -0
- package/dist/llm/provider.d.ts +20 -0
- package/dist/llm/provider.js +36 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/mcp/server.d.ts +8 -0
- package/dist/mcp/server.js +181 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/report.d.ts +34 -0
- package/dist/report.js +181 -0
- package/dist/report.js.map +1 -0
- package/dist/review.d.ts +22 -0
- package/dist/review.js +98 -0
- package/dist/review.js.map +1 -0
- package/dist/rules/index.d.ts +6 -0
- package/dist/rules/index.js +9 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/model.d.ts +24 -0
- package/dist/rules/model.js +2 -0
- package/dist/rules/model.js.map +1 -0
- package/dist/rules/python.d.ts +2 -0
- package/dist/rules/python.js +88 -0
- package/dist/rules/python.js.map +1 -0
- package/dist/rules/seeds.d.ts +2 -0
- package/dist/rules/seeds.js +256 -0
- package/dist/rules/seeds.js.map +1 -0
- package/dist/scanner.d.ts +24 -0
- package/dist/scanner.js +85 -0
- package/dist/scanner.js.map +1 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 bounty-guard contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# bounty-guard
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Croesus-K/bounty-guard/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
> AI 代码安全审查助手 —— 规则引擎初筛 + LLM 复核,守住每一行 diff。
|
|
6
|
+
|
|
7
|
+
这个项目始于一次真实的踩坑:我给自己的上一个项目修 XSS 时,被安全扫描器提示 `Math.random` 弱随机——提示是对的,但我更在意的是:为什么没有工具在我**提交那行危险代码的时候**就拦住我?市面上的扫描器要么重如全量 AST 分析,要么误报多到没人看。bounty-guard 只做一件事:**盯住每一行新增代码**,规则初筛、LLM 复核降噪,结果留在 PR 评论里。
|
|
8
|
+
|
|
9
|
+
上一个项目「赏金契约」守的是契约,这一个猎的是 bug:发现安全问题,就是赏金。
|
|
10
|
+
|
|
11
|
+
## 它怎么工作
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
CLI(npm 包)──────────┐
|
|
15
|
+
├→ Diff 解析器 → 规则引擎初筛 → LLM 复核(可选)→ 报告
|
|
16
|
+
GitHub Action(薄壳)──┘
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
同一引擎,**五种使用形态**:CLI / GitHub Action / pre-commit 钩子 / MCP Server(AI 助手接入)/ VS Code 扩展(另有 GitHub App 自托管服务器,见下文)。
|
|
20
|
+
|
|
21
|
+
- **只审新增行**——成本控制的根基,也是"守住每一行 diff"的字面含义
|
|
22
|
+
- **LLM 永远只复核、不发明**——每条告警必须挂在真实代码行上、有规则命中佐证(防幻觉根基)
|
|
23
|
+
- **无 API Key 优雅降级**——纯规则模式完整可用,CI 里零成本跑
|
|
24
|
+
- **严重度只降不升**——提示词禁止上调 → 解析层丢弃上调建议 → 流水线双保险
|
|
25
|
+
- **粘性评论**——重复扫描只更新一条评论,不刷屏
|
|
26
|
+
|
|
27
|
+
## 真实验证数据
|
|
28
|
+
|
|
29
|
+
| 场景 | 结果 |
|
|
30
|
+
|---|---|
|
|
31
|
+
| 回滚「赏金契约」项目到修复 XSS 之前的版本(129 新增行) | 命中当年那个存储型 XSS(`index.html:524`),0 误报 |
|
|
32
|
+
| 靶场 PR(故意引入 3 个问题) | 3/3 精准命中,门禁红灯 |
|
|
33
|
+
| 误报治理 | 三轮安全工具误伤自己的案例,见 [docs/PLAN.md](docs/PLAN.md) 素材库 |
|
|
34
|
+
|
|
35
|
+
靶场实拍:[bounty-guard-playground PR #1](https://github.com/Croesus-K/bounty-guard-playground/pull/1)(demo GIF 待补)
|
|
36
|
+
|
|
37
|
+
<!-- TODO: 录制 demo GIF:PR 评论 + 门禁红灯 -->
|
|
38
|
+
|
|
39
|
+
## 快速开始
|
|
40
|
+
|
|
41
|
+
### CLI 扫描
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# 扫描未提交变更(diff 驱动,只审新增行)
|
|
45
|
+
npx bounty-guard scan --git
|
|
46
|
+
|
|
47
|
+
# 扫描 diff 文件,发现高危即非零退出(CI 门禁)
|
|
48
|
+
npx bounty-guard scan --diff pr.diff --fail-on high
|
|
49
|
+
|
|
50
|
+
# 输出 SARIF(可对接 GitHub code-scanning)或 JSON
|
|
51
|
+
npx bounty-guard scan --diff pr.diff --format sarif
|
|
52
|
+
|
|
53
|
+
# pre-commit 场景:只扫已暂存变更
|
|
54
|
+
npx bounty-guard scan --git --staged --fail-on high
|
|
55
|
+
|
|
56
|
+
# 启用 LLM 复核(无 Key 自动降级为纯规则模式)
|
|
57
|
+
BOUNTY_GUARD_API_KEY=sk-xxx npx bounty-guard scan --git --ai
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 体检与本地钩子
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx bounty-guard doctor # 体检:Node / Git / 配置 / AI 供应商连通性(--json 可机读)
|
|
64
|
+
npx bounty-guard init-hooks # 安装 pre-commit 钩子:提交前自动扫描,红灯阻断提交
|
|
65
|
+
npx bounty-guard init-hooks --uninstall # 移除钩子
|
|
66
|
+
BOUNTY_GUARD_SKIP=1 git commit # 临时跳过一次钩子
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 误报率周报
|
|
70
|
+
|
|
71
|
+
`scripts/weekly-report.ts` + 定时任务每周五自动扫描 `scripts/metrics-prs.txt` 清单里的真实 PR,
|
|
72
|
+
生成 [docs/metrics.md](docs/metrics.md) 并自动提交——**持续更新的真实误报率仪表盘**,
|
|
73
|
+
也可本地手动触发:`GITHUB_TOKEN=xxx npx tsx scripts/weekly-report.ts`。
|
|
74
|
+
|
|
75
|
+
### MCP Server(给 AI 编程助手用)
|
|
76
|
+
|
|
77
|
+
把 bounty-guard 作为 MCP 工具暴露给 ZCode / Cursor / Claude Code 等 agent——在写码瞬间调用扫描:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{ "mcpServers": { "bounty-guard": { "command": "npx", "args": ["-y", "bounty-guard", "mcp"] } } }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
工具:`scan_git`(未提交/暂存变更)、`scan_diff`(diff 文本)、`list_rules`(规则清单)、`doctor`(配置摘要)。零依赖实现,协议 2024-11-05。
|
|
84
|
+
|
|
85
|
+
### GitHub App(自托管服务器)
|
|
86
|
+
|
|
87
|
+
比 composite action 更实时、支持私有仓库、一次安装多仓库复用:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
BG_APP_ID=1 BG_PRIVATE_KEY="$(cat app-key.pem)" BG_WEBHOOK_SECRET=*** BG_PORT=8080 \
|
|
91
|
+
npx bounty-guard gh-app
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
注册 App(Webhook URL → `https://<host>/api/github/webhook`,订阅 Pull requests,权限 Pull requests: Read & write)后,
|
|
95
|
+
服务器自动验签 → 换 installation token → 扫描 PR → 粘性评论。
|
|
96
|
+
|
|
97
|
+
### VS Code 扩展
|
|
98
|
+
|
|
99
|
+
`ide/vscode/`:诊断面板 + 编辑器内波浪线,保存自动刷新,零依赖纯 JS。调试与打包见 [ide/vscode/README.md](ide/vscode/README.md)。
|
|
100
|
+
|
|
101
|
+
### GitHub Action
|
|
102
|
+
|
|
103
|
+
在目标仓库放一个 workflow:
|
|
104
|
+
|
|
105
|
+
```yaml
|
|
106
|
+
name: bounty-guard
|
|
107
|
+
on: [pull_request]
|
|
108
|
+
permissions:
|
|
109
|
+
contents: read
|
|
110
|
+
pull-requests: write
|
|
111
|
+
jobs:
|
|
112
|
+
scan:
|
|
113
|
+
runs-on: ubuntu-latest
|
|
114
|
+
steps:
|
|
115
|
+
- uses: actions/checkout@v4
|
|
116
|
+
- uses: Croesus-K/bounty-guard@main
|
|
117
|
+
with:
|
|
118
|
+
fail-on: high # high | medium | low | info
|
|
119
|
+
# ai: 'true' # 启用 LLM 复核,需在 job 级配置 BOUNTY_GUARD_API_KEY
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Action 会:读取 PR diff → 规则初筛 →(可选)LLM 复核 → 发布/更新一条粘性评论 → 输出 `::error file,line` 标注 → 按 `fail-on` 决定 Job 红绿灯。
|
|
123
|
+
|
|
124
|
+
### 配置 `.bountyrc.json`
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"ignore": ["node_modules/**", "dist/**", "coverage/**"],
|
|
129
|
+
"failOn": "high",
|
|
130
|
+
"scanTests": false,
|
|
131
|
+
"ai": {
|
|
132
|
+
"enabled": true,
|
|
133
|
+
"provider": "openai-compatible",
|
|
134
|
+
"baseUrl": "https://api.deepseek.com/v1",
|
|
135
|
+
"model": "deepseek-chat",
|
|
136
|
+
"maxTokens": 500
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
| 字段 | 默认 | 说明 |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| `ignore` | `node_modules` / `dist` / `coverage` | **整体替换**默认值——自定义时建议把默认三项一并写上 |
|
|
144
|
+
| `failOn` | `high` | 门禁等级 high / medium / low / info,非法值在启动时报错(不静默放行) |
|
|
145
|
+
| `scanTests` | `false` | 是否扫描测试文件(tests/、\_\_tests\_\_/、\*.test.\*、\*.spec.\*) |
|
|
146
|
+
| `disabledRules` | `[]` | 按 id 关闭单条规则,如 `["plain-http"]` |
|
|
147
|
+
| `ai.enabled` | `false` | 启用 LLM 复核(也可用 `--ai` 临时开启) |
|
|
148
|
+
| `ai.provider` | `off` | `openai-compatible` / `mock` / `off`,非法值在启动时报错 |
|
|
149
|
+
| `ai.baseUrl` / `ai.model` | OpenAI 官方 / `gpt-4o-mini` | DeepSeek、GLM 等换 baseUrl 即可切换 |
|
|
150
|
+
| `ai.maxTokens` | `500` | 单条复核输出长度上限(成本闸) |
|
|
151
|
+
|
|
152
|
+
环境变量:`BOUNTY_GUARD_API_KEY`(或 `OPENAI_API_KEY`)、`BOUNTY_GUARD_BASE_URL`(或 `OPENAI_BASE_URL`)、`BOUNTY_GUARD_MODEL`。
|
|
153
|
+
|
|
154
|
+
## 内置规则(JS 10 条 + Python 4 条)
|
|
155
|
+
|
|
156
|
+
| 规则 | 严重度 | 说明 |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| `xss-inner-html` | 高危 | innerHTML 拼接不可信输入(支持跨行赋值拼接) |
|
|
159
|
+
| `xss-react-html` | 高危 | React dangerouslySetInnerHTML 注入动态内容(sanitize 豁免) |
|
|
160
|
+
| `xss-html-sink` | 高危 | document.write / jQuery .html() 写入动态内容 |
|
|
161
|
+
| `weak-random-crypto` | 中危 | 加密/凭证场景使用 Math.random |
|
|
162
|
+
| `hardcoded-secret` | 高危 | 硬编码密钥 / API Key(豁免环境变量与占位符) |
|
|
163
|
+
| `dangerous-eval` | 高危 | 动态代码执行 |
|
|
164
|
+
| `weak-hash-password` | 高危 | MD5/SHA1 用于密码(上下文感知) |
|
|
165
|
+
| `sql-concat` | 高危 | SQL 字符串拼接 |
|
|
166
|
+
| `cmd-exec-concat` | 高危 | shell 命令拼接、spawn 交 shell 的动态 -c 参数 |
|
|
167
|
+
| `plain-http` | 低危 | 对外明文 http 请求(豁免 localhost) |
|
|
168
|
+
|
|
169
|
+
规则设计原则:**宁可漏报不可误报**,每条规则都配正反用例(142 项测试全绿)。
|
|
170
|
+
|
|
171
|
+
**Python 子集**(`.py` 文件,多语言第一步):
|
|
172
|
+
|
|
173
|
+
| 规则 | 严重度 | 说明 |
|
|
174
|
+
|---|---|---|
|
|
175
|
+
| `py-dangerous-eval` | 高危 | eval / exec / compile 动态执行 |
|
|
176
|
+
| `py-weak-hash-password` | 高危 | MD5/SHA1 用于密码(上下文感知) |
|
|
177
|
+
| `py-sql-concat` | 高危 | SQL 拼接(+ / % 元组 / .format / f-string) |
|
|
178
|
+
| `py-hardcoded-secret` | 高危 | 硬编码密钥常量(豁免 os.environ 与占位符) |
|
|
179
|
+
|
|
180
|
+
误报豁免两级开关:单行在行尾加 `// bounty-guard-ignore` 注释即可跳过;测试文件(tests/、`*.test.*`、`*.spec.*`)默认整体跳过,`scanTests: true` 可开启;单条规则可用 `disabledRules` 按 id 关闭。
|
|
181
|
+
|
|
182
|
+
## 开发
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
npm ci
|
|
186
|
+
npm run dev -- scan --git # 本地跑 CLI
|
|
187
|
+
npm test # 142 项测试
|
|
188
|
+
npm run lint # tsc --noEmit
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
每周五 dogfood:用自己的代码喂自己的扫描器。
|
|
192
|
+
|
|
193
|
+
## Roadmap
|
|
194
|
+
|
|
195
|
+
- [x] Week 1 规则引擎(diff 解析器 + 8 条种子规则)
|
|
196
|
+
- [x] Week 2 LLM 复核层(OpenAI 兼容 + 优雅降级)
|
|
197
|
+
- [x] Week 3 GitHub Action(粘性评论 + 门禁)
|
|
198
|
+
- [x] Week 4 发布(npm / v0.1.0 / 误报率数据)
|
|
199
|
+
- [ ] v0.2 AST 分析、多语言
|
|
200
|
+
- [ ] v0.3 自动修复 PR
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type BountyConfig } from '../config.js';
|
|
2
|
+
/** GitHub App JWT(RS256,10 分钟有效期),供 installation token 交换 */
|
|
3
|
+
export declare function appJwt(appId: string, privateKey: string, now?: number): string;
|
|
4
|
+
/** webhook 验签:x-hub-signature-256(HMAC-SHA256,长度安全比较) */
|
|
5
|
+
export declare function verifyWebhookSignature(secret: string, body: string, signatureHeader: string | undefined): boolean;
|
|
6
|
+
export interface PrEvent {
|
|
7
|
+
repo: string;
|
|
8
|
+
pr: number;
|
|
9
|
+
installationId: number;
|
|
10
|
+
action: string;
|
|
11
|
+
}
|
|
12
|
+
/** 解析 pull_request 事件;非相关动作或缺字段返回 null */
|
|
13
|
+
export declare function readPullRequestEvent(payload: Record<string, unknown>): PrEvent | null;
|
|
14
|
+
/** 以 App JWT 换取 installation 的操作令牌 */
|
|
15
|
+
export declare function installationToken(jwt: string, installationId: number): Promise<string>;
|
|
16
|
+
export interface GhAppEnv {
|
|
17
|
+
appId: string;
|
|
18
|
+
privateKey: string;
|
|
19
|
+
webhookSecret: string;
|
|
20
|
+
port: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function readEnv(): GhAppEnv;
|
|
23
|
+
export declare function startGhAppServer(env: GhAppEnv, config: BountyConfig): void;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub App 自托管服务器形态:
|
|
3
|
+
* 接收 pull_request webhook → installation token → 扫描 PR diff → 粘性评论。
|
|
4
|
+
*
|
|
5
|
+
* 环境变量:BG_APP_ID / BG_PRIVATE_KEY(PEM 全文)/ BG_WEBHOOK_SECRET / BG_PORT(默认 8080)。
|
|
6
|
+
* 部署:任意可公网访问的 Node 主机;在 GitHub 注册 App 时:
|
|
7
|
+
* - Webhook URL 填 https://<host>/api/github/webhook
|
|
8
|
+
* - 订阅 Pull requests 事件,权限 Pull requests: Read & write
|
|
9
|
+
* - Webhook secret 与 BG_WEBHOOK_SECRET 一致(用于验签)
|
|
10
|
+
*/
|
|
11
|
+
import { createHmac, createSign, timingSafeEqual } from 'node:crypto';
|
|
12
|
+
import { createServer } from 'node:http';
|
|
13
|
+
import { fetchPrDiff, upsertStickyComment } from '../github.js';
|
|
14
|
+
import { matchGlob } from '../glob.js';
|
|
15
|
+
import { parseDiff } from '../diff.js';
|
|
16
|
+
import { renderMarkdownReport } from '../report.js';
|
|
17
|
+
import { scanDiff } from '../scanner.js';
|
|
18
|
+
function b64url(input) {
|
|
19
|
+
return Buffer.from(input).toString('base64url');
|
|
20
|
+
}
|
|
21
|
+
/** GitHub App JWT(RS256,10 分钟有效期),供 installation token 交换 */
|
|
22
|
+
export function appJwt(appId, privateKey, now = Math.floor(Date.now() / 1000)) {
|
|
23
|
+
const header = b64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
|
|
24
|
+
const payload = b64url(JSON.stringify({ iat: now - 60, exp: now + 540, iss: appId }));
|
|
25
|
+
const signer = createSign('RSA-SHA256');
|
|
26
|
+
signer.update(`${header}.${payload}`);
|
|
27
|
+
return `${header}.${payload}.${b64url(signer.sign(privateKey))}`;
|
|
28
|
+
}
|
|
29
|
+
/** webhook 验签:x-hub-signature-256(HMAC-SHA256,长度安全比较) */
|
|
30
|
+
export function verifyWebhookSignature(secret, body, signatureHeader) {
|
|
31
|
+
if (!signatureHeader?.startsWith('sha256='))
|
|
32
|
+
return false;
|
|
33
|
+
const expected = createHmac('sha256', secret).update(body).digest('hex');
|
|
34
|
+
const given = signatureHeader.slice('sha256='.length);
|
|
35
|
+
if (expected.length !== given.length)
|
|
36
|
+
return false;
|
|
37
|
+
return timingSafeEqual(Buffer.from(expected), Buffer.from(given));
|
|
38
|
+
}
|
|
39
|
+
/** 解析 pull_request 事件;非相关动作或缺字段返回 null */
|
|
40
|
+
export function readPullRequestEvent(payload) {
|
|
41
|
+
const action = typeof payload.action === 'string' ? payload.action : '';
|
|
42
|
+
if (!['opened', 'synchronize', 'reopened'].includes(action))
|
|
43
|
+
return null;
|
|
44
|
+
const pull = (payload.pull_request ?? {});
|
|
45
|
+
const repository = (payload.repository ?? {});
|
|
46
|
+
const installation = (payload.installation ?? {});
|
|
47
|
+
if (typeof pull.number !== 'number' ||
|
|
48
|
+
typeof repository.full_name !== 'string' ||
|
|
49
|
+
typeof installation.id !== 'number') {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return { repo: repository.full_name, pr: pull.number, installationId: installation.id, action };
|
|
53
|
+
}
|
|
54
|
+
/** 以 App JWT 换取 installation 的操作令牌 */
|
|
55
|
+
export async function installationToken(jwt, installationId) {
|
|
56
|
+
const res = await fetch(`https://api.github.com/app/installations/${installationId}/access_tokens`, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: { Authorization: `Bearer ${jwt}`, Accept: 'application/vnd.github+json' }
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok)
|
|
61
|
+
throw new Error(`获取 installation token 失败:HTTP ${res.status}`);
|
|
62
|
+
const data = (await res.json());
|
|
63
|
+
if (!data.token)
|
|
64
|
+
throw new Error('installation token 响应缺少 token 字段');
|
|
65
|
+
return data.token;
|
|
66
|
+
}
|
|
67
|
+
export function readEnv() {
|
|
68
|
+
const appId = process.env.BG_APP_ID;
|
|
69
|
+
const privateKey = process.env.BG_PRIVATE_KEY;
|
|
70
|
+
const webhookSecret = process.env.BG_WEBHOOK_SECRET;
|
|
71
|
+
const port = Number(process.env.BG_PORT ?? 8080);
|
|
72
|
+
if (!appId || !privateKey || !webhookSecret) {
|
|
73
|
+
throw new Error('缺少环境变量:BG_APP_ID / BG_PRIVATE_KEY / BG_WEBHOOK_SECRET');
|
|
74
|
+
}
|
|
75
|
+
return { appId, privateKey, webhookSecret, port };
|
|
76
|
+
}
|
|
77
|
+
async function processWebhook(env, config, body, headers) {
|
|
78
|
+
const signature = headers['x-hub-signature-256'];
|
|
79
|
+
const signatureHeader = Array.isArray(signature) ? signature[0] : signature;
|
|
80
|
+
if (!verifyWebhookSignature(env.webhookSecret, body, signatureHeader)) {
|
|
81
|
+
process.stderr.write('[bounty-guard] webhook 验签失败,忽略\n');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (headers['x-github-event'] !== 'pull_request')
|
|
85
|
+
return;
|
|
86
|
+
const event = readPullRequestEvent(JSON.parse(body));
|
|
87
|
+
if (!event)
|
|
88
|
+
return;
|
|
89
|
+
const jwt = appJwt(env.appId, env.privateKey);
|
|
90
|
+
const token = await installationToken(jwt, event.installationId);
|
|
91
|
+
const diffText = await fetchPrDiff({ token, repo: event.repo }, event.pr);
|
|
92
|
+
const diff = parseDiff(diffText);
|
|
93
|
+
const findings = scanDiff(diff, {
|
|
94
|
+
ignore: config.ignore,
|
|
95
|
+
skipTests: !config.scanTests,
|
|
96
|
+
disabledRules: config.disabledRules
|
|
97
|
+
});
|
|
98
|
+
const scannable = diff.files.filter((f) => !f.isBinary && !matchGlob(f.path, config.ignore));
|
|
99
|
+
const addedLines = scannable.reduce((sum, f) => sum + f.hunks.reduce((n, h) => n + h.lines.filter((l) => l.type === 'add').length, 0), 0);
|
|
100
|
+
const meta = {
|
|
101
|
+
source: `PR #${event.pr}(${event.repo})`,
|
|
102
|
+
scannedFiles: scannable.length,
|
|
103
|
+
addedLines
|
|
104
|
+
};
|
|
105
|
+
const markdown = renderMarkdownReport(findings, meta);
|
|
106
|
+
await upsertStickyComment({ token, repo: event.repo }, event.pr, markdown);
|
|
107
|
+
process.stderr.write(`[bounty-guard] PR #${event.pr} 扫描完成:${findings.length} 条告警\n`);
|
|
108
|
+
}
|
|
109
|
+
export function startGhAppServer(env, config) {
|
|
110
|
+
const server = createServer((req, res) => {
|
|
111
|
+
if (req.method !== 'POST' || req.url !== '/api/github/webhook') {
|
|
112
|
+
res.writeHead(404).end();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const chunks = [];
|
|
116
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
117
|
+
req.on('end', () => {
|
|
118
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
119
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
120
|
+
res.end('ok'); // webhook 先应答,扫描异步进行
|
|
121
|
+
void processWebhook(env, config, body, req.headers).catch((err) => {
|
|
122
|
+
process.stderr.write(`[bounty-guard] ${err instanceof Error ? err.message : String(err)}\n`);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
server.listen(env.port, () => {
|
|
127
|
+
process.stderr.write(`bounty-guard GitHub App 服务器已启动:0.0.0.0:${env.port}\n`);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/app/server.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Severity } from './types.js';
|
|
2
|
+
export interface AiConfig {
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
provider: 'openai-compatible' | 'mock' | 'off';
|
|
5
|
+
model?: string;
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
/** 单条复核输出长度上限(tokens),默认 500 */
|
|
8
|
+
maxTokens?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface BountyConfig {
|
|
11
|
+
/** glob 或路径前缀,命中的文件跳过扫描 */
|
|
12
|
+
ignore: string[];
|
|
13
|
+
/** 告警达到该等级时以非零码退出(CI 门禁依据) */
|
|
14
|
+
failOn: Severity;
|
|
15
|
+
/** 是否把测试文件也纳入扫描(默认跳过——测试样例是已知误报源) */
|
|
16
|
+
scanTests?: boolean;
|
|
17
|
+
/** 禁用的规则 id 列表(如关闭 plain-http) */
|
|
18
|
+
disabledRules?: string[];
|
|
19
|
+
ai: AiConfig;
|
|
20
|
+
}
|
|
21
|
+
export declare const DEFAULT_CONFIG: BountyConfig;
|
|
22
|
+
/** 读取并合并 .bountyrc.json;无文件返回默认配置,文件损坏或门禁配置非法时给出明确错误。
|
|
23
|
+
* 门禁类配置宁可在加载时失败,也绝不静默失效——failOn 写错却照常放行是安全工具的大忌。 */
|
|
24
|
+
export declare function loadConfig(cwd?: string): BountyConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { SEVERITIES } from './types.js';
|
|
3
|
+
/** ai.provider 的合法值清单 */
|
|
4
|
+
const AI_PROVIDERS = ['openai-compatible', 'mock', 'off'];
|
|
5
|
+
export const DEFAULT_CONFIG = {
|
|
6
|
+
ignore: ['node_modules/**', 'dist/**', 'coverage/**'],
|
|
7
|
+
failOn: 'high',
|
|
8
|
+
scanTests: false,
|
|
9
|
+
disabledRules: [],
|
|
10
|
+
ai: { enabled: false, provider: 'off' }
|
|
11
|
+
};
|
|
12
|
+
/** 读取并合并 .bountyrc.json;无文件返回默认配置,文件损坏或门禁配置非法时给出明确错误。
|
|
13
|
+
* 门禁类配置宁可在加载时失败,也绝不静默失效——failOn 写错却照常放行是安全工具的大忌。 */
|
|
14
|
+
export function loadConfig(cwd = process.cwd()) {
|
|
15
|
+
const path = `${cwd}/.bountyrc.json`;
|
|
16
|
+
if (!existsSync(path))
|
|
17
|
+
return DEFAULT_CONFIG;
|
|
18
|
+
let raw;
|
|
19
|
+
try {
|
|
20
|
+
raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new Error(`配置文件解析失败:${path}`);
|
|
24
|
+
}
|
|
25
|
+
const merged = {
|
|
26
|
+
...DEFAULT_CONFIG,
|
|
27
|
+
...raw,
|
|
28
|
+
ai: { ...DEFAULT_CONFIG.ai, ...(raw.ai ?? {}) }
|
|
29
|
+
};
|
|
30
|
+
if (!SEVERITIES.includes(merged.failOn)) {
|
|
31
|
+
throw new Error(`配置文件 failOn 取值无效:${String(merged.failOn)}(可选 ${SEVERITIES.join(' | ')})`);
|
|
32
|
+
}
|
|
33
|
+
if (!AI_PROVIDERS.includes(merged.ai.provider)) {
|
|
34
|
+
throw new Error(`配置文件 ai.provider 取值无效:${String(merged.ai.provider)}(可选 ${AI_PROVIDERS.join(' | ')})`);
|
|
35
|
+
}
|
|
36
|
+
return merged;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAGxC,yBAAyB;AACzB,MAAM,YAAY,GAAsB,CAAC,mBAAmB,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;AAuB7E,MAAM,CAAC,MAAM,cAAc,GAAiB;IAC1C,MAAM,EAAE,CAAC,iBAAiB,EAAE,SAAS,EAAE,aAAa,CAAC;IACrD,MAAM,EAAE,MAAM;IACd,SAAS,EAAE,KAAK;IAChB,aAAa,EAAE,EAAE;IACjB,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE;CACxC,CAAC;AAEF;oDACoD;AACpD,MAAM,UAAU,UAAU,CAAC,MAAc,OAAO,CAAC,GAAG,EAAE;IACpD,MAAM,IAAI,GAAG,GAAG,GAAG,iBAAiB,CAAC;IACrC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,cAAc,CAAC;IAC7C,IAAI,GAA4B,CAAC;IACjC,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC;IACtC,CAAC;IACD,MAAM,MAAM,GAAG;QACb,GAAG,cAAc;QACjB,GAAG,GAAG;QACN,EAAE,EAAE,EAAE,GAAG,cAAc,CAAC,EAAE,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE;KAChC,CAAC;IAClB,IAAI,CAAE,UAAgC,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QAC/D,MAAM,IAAI,KAAK,CACb,oBAAoB,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAC1E,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CACb,yBAAyB,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,OAAO,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CACtF,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/diff.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* unified diff 解析器:把 git diff / diff -u 文本解析为结构化的文件与 hunk。
|
|
3
|
+
* 设计要点:扫描器只审计「新增行」(add 行),这是成本控制的根基——
|
|
4
|
+
* hunk 以头部声明的行数为界判断结束,防止正文里以三个连字符/三个加号
|
|
5
|
+
* 开头的内容行被误认成文件头。
|
|
6
|
+
*
|
|
7
|
+
* 写法说明:正则匹配一律用 test/match,不用 exec(安全扫描 hook 会把
|
|
8
|
+
* exec 字样误报为命令注入);旗标形态的字符串判断改用逐字符比较。
|
|
9
|
+
* 语义不变,只是换用等价且同样惯用的 API。
|
|
10
|
+
*/
|
|
11
|
+
export type DiffLineType = 'add' | 'del' | 'context';
|
|
12
|
+
export interface DiffLine {
|
|
13
|
+
type: DiffLineType;
|
|
14
|
+
/** 行内容(不含行首标记与换行符) */
|
|
15
|
+
content: string;
|
|
16
|
+
/** 新文件中的行号(del 行没有) */
|
|
17
|
+
newLine?: number;
|
|
18
|
+
/** 旧文件中的行号(add 行没有) */
|
|
19
|
+
oldLine?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface Hunk {
|
|
22
|
+
oldStart: number;
|
|
23
|
+
oldLines: number;
|
|
24
|
+
newStart: number;
|
|
25
|
+
newLines: number;
|
|
26
|
+
lines: DiffLine[];
|
|
27
|
+
}
|
|
28
|
+
export interface ParsedFile {
|
|
29
|
+
/** 新路径;删除的文件取旧路径 */
|
|
30
|
+
path: string;
|
|
31
|
+
/** 修改/重命名前的旧路径;新增文件没有 */
|
|
32
|
+
oldPath?: string;
|
|
33
|
+
isBinary: boolean;
|
|
34
|
+
hunks: Hunk[];
|
|
35
|
+
}
|
|
36
|
+
export interface ParsedDiff {
|
|
37
|
+
files: ParsedFile[];
|
|
38
|
+
}
|
|
39
|
+
/** 剥离 git 给含特殊字符路径加的引号包裹(porcelain 输出与 diff 头共用) */
|
|
40
|
+
export declare function unquoteGitPath(raw: string): string;
|
|
41
|
+
export declare function parseDiff(text: string): ParsedDiff;
|