proof-pr 0.1.3 → 0.1.6
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/README.md +18 -6
- package/dist/index.js +919 -98
- package/package.json +3 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,25 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
ProofPR 的命令行工具。
|
|
4
4
|
|
|
5
|
-
ProofPR 帮助维护者在投入深入 review 之前,先检查 PR
|
|
5
|
+
ProofPR 帮助维护者在投入深入 review 之前,先检查 PR 的证据、范围和安全风险。报告会输出风险等级、0-100 证据评分,以及 Review 门禁建议。
|
|
6
|
+
|
|
7
|
+
## 它什么时候运行?
|
|
8
|
+
|
|
9
|
+
作为 GitHub Action 使用时,ProofPR 默认在 PR 打开、PR 分支更新、PR 重新打开时运行。普通分支 push 不会单独生成报告。
|
|
10
|
+
|
|
11
|
+
报告会出现在 PR 评论区、GitHub Actions job summary 和 PR checks 状态里。
|
|
12
|
+
`v0.1.5` 起还可以输出 GitHub annotations,并通过 `sarif-output` 写出 SARIF 文件。当前版本还会识别依赖大版本升级、包生命周期脚本和 `pull_request_target` workflow 触发器。
|
|
6
13
|
|
|
7
14
|
## 使用
|
|
8
15
|
|
|
9
16
|
可以直接通过 npm 使用:
|
|
10
17
|
|
|
11
18
|
```bash
|
|
12
|
-
npx proof-pr init
|
|
13
|
-
npx proof-pr
|
|
14
|
-
npx proof-pr scan --base origin/main --head HEAD
|
|
15
|
-
npx proof-pr scan --base origin/main --
|
|
19
|
+
npx proof-pr@latest init
|
|
20
|
+
npx proof-pr@latest init --preset security-strict
|
|
21
|
+
npx proof-pr@latest scan --base origin/main --head HEAD
|
|
22
|
+
npx proof-pr@latest scan --base origin/main --head HEAD --locale zh-CN
|
|
23
|
+
npx proof-pr@latest scan --base origin/main --pr-body-file pr-body.md --format json
|
|
16
24
|
```
|
|
17
25
|
|
|
26
|
+
可用预设:`balanced`、`open-source-maintainer`、`security-strict`、`ai-generated-pr`、`mcp-security`、`dependency-careful`。
|
|
27
|
+
|
|
18
28
|
## GitHub Action
|
|
19
29
|
|
|
20
30
|
```yaml
|
|
21
|
-
- uses: linsk27/proof-pr@v0.1.
|
|
31
|
+
- uses: linsk27/proof-pr@v0.1.5
|
|
22
32
|
with:
|
|
23
33
|
fail-on: high
|
|
34
|
+
comment: "true"
|
|
35
|
+
annotations: "true"
|
|
24
36
|
```
|
|
25
37
|
|
|
26
38
|
完整文档见仓库 README:
|
package/dist/index.js
CHANGED
|
@@ -23112,47 +23112,138 @@ function preprocess(fn, schema) {
|
|
|
23112
23112
|
|
|
23113
23113
|
const riskLevelSchema = schemas_enum(["low", "medium", "high"]);
|
|
23114
23114
|
const localeSchema = schemas_enum(["en", "zh-CN"]);
|
|
23115
|
+
const configPresetSchema = schemas_enum([
|
|
23116
|
+
"balanced",
|
|
23117
|
+
"open-source-maintainer",
|
|
23118
|
+
"security-strict",
|
|
23119
|
+
"ai-generated-pr",
|
|
23120
|
+
"mcp-security",
|
|
23121
|
+
"dependency-careful"
|
|
23122
|
+
]);
|
|
23123
|
+
const CONFIG_PRESETS = [
|
|
23124
|
+
"balanced",
|
|
23125
|
+
"open-source-maintainer",
|
|
23126
|
+
"security-strict",
|
|
23127
|
+
"ai-generated-pr",
|
|
23128
|
+
"mcp-security",
|
|
23129
|
+
"dependency-careful"
|
|
23130
|
+
];
|
|
23131
|
+
const DEFAULT_SENSITIVE_PATHS = [
|
|
23132
|
+
".github/workflows/**",
|
|
23133
|
+
".github/actions/**",
|
|
23134
|
+
"**/.env*",
|
|
23135
|
+
"**/mcp*.json",
|
|
23136
|
+
"**/*mcp*.json",
|
|
23137
|
+
"Dockerfile",
|
|
23138
|
+
"**/Dockerfile",
|
|
23139
|
+
"package.json",
|
|
23140
|
+
"pnpm-lock.yaml",
|
|
23141
|
+
"package-lock.json",
|
|
23142
|
+
"yarn.lock",
|
|
23143
|
+
"bun.lockb",
|
|
23144
|
+
"requirements.txt",
|
|
23145
|
+
"pyproject.toml",
|
|
23146
|
+
"Cargo.toml",
|
|
23147
|
+
"Cargo.lock",
|
|
23148
|
+
"go.mod",
|
|
23149
|
+
"go.sum"
|
|
23150
|
+
];
|
|
23151
|
+
const DEFAULT_TEST_PATHS = ["src/**", "packages/**/src/**", "app/**", "lib/**"];
|
|
23152
|
+
const PRESET_DEFAULTS = {
|
|
23153
|
+
balanced: {},
|
|
23154
|
+
"open-source-maintainer": {
|
|
23155
|
+
riskThreshold: "high",
|
|
23156
|
+
sensitivePaths: DEFAULT_SENSITIVE_PATHS,
|
|
23157
|
+
requireTests: {
|
|
23158
|
+
enabled: true,
|
|
23159
|
+
paths: DEFAULT_TEST_PATHS
|
|
23160
|
+
}
|
|
23161
|
+
},
|
|
23162
|
+
"security-strict": {
|
|
23163
|
+
riskThreshold: "medium",
|
|
23164
|
+
sensitivePaths: [
|
|
23165
|
+
...DEFAULT_SENSITIVE_PATHS,
|
|
23166
|
+
".npmrc",
|
|
23167
|
+
"**/.npmrc",
|
|
23168
|
+
".pypirc",
|
|
23169
|
+
"**/.pypirc",
|
|
23170
|
+
".dockerignore",
|
|
23171
|
+
"docker-compose*.yml",
|
|
23172
|
+
"**/docker-compose*.yml",
|
|
23173
|
+
".github/dependabot.yml",
|
|
23174
|
+
".github/codeql/**",
|
|
23175
|
+
"terraform/**/*.tf",
|
|
23176
|
+
"**/*.pem",
|
|
23177
|
+
"**/*.key"
|
|
23178
|
+
],
|
|
23179
|
+
requireTests: {
|
|
23180
|
+
enabled: true,
|
|
23181
|
+
paths: ["src/**", "packages/**/src/**", "app/**", "lib/**", "server/**", "api/**"]
|
|
23182
|
+
}
|
|
23183
|
+
},
|
|
23184
|
+
"ai-generated-pr": {
|
|
23185
|
+
riskThreshold: "medium",
|
|
23186
|
+
sensitivePaths: DEFAULT_SENSITIVE_PATHS,
|
|
23187
|
+
requireTests: {
|
|
23188
|
+
enabled: true,
|
|
23189
|
+
paths: ["src/**", "packages/**/src/**", "app/**", "lib/**", "server/**", "api/**", "components/**"]
|
|
23190
|
+
}
|
|
23191
|
+
},
|
|
23192
|
+
"mcp-security": {
|
|
23193
|
+
riskThreshold: "medium",
|
|
23194
|
+
sensitivePaths: [
|
|
23195
|
+
...DEFAULT_SENSITIVE_PATHS,
|
|
23196
|
+
".cursor/**",
|
|
23197
|
+
".vscode/**"
|
|
23198
|
+
],
|
|
23199
|
+
requireTests: {
|
|
23200
|
+
enabled: true,
|
|
23201
|
+
paths: DEFAULT_TEST_PATHS
|
|
23202
|
+
}
|
|
23203
|
+
},
|
|
23204
|
+
"dependency-careful": {
|
|
23205
|
+
riskThreshold: "medium",
|
|
23206
|
+
sensitivePaths: [
|
|
23207
|
+
...DEFAULT_SENSITIVE_PATHS,
|
|
23208
|
+
"poetry.lock",
|
|
23209
|
+
"Pipfile",
|
|
23210
|
+
"Pipfile.lock",
|
|
23211
|
+
"pom.xml",
|
|
23212
|
+
"build.gradle",
|
|
23213
|
+
"build.gradle.kts",
|
|
23214
|
+
"Gemfile",
|
|
23215
|
+
"Gemfile.lock"
|
|
23216
|
+
],
|
|
23217
|
+
requireTests: {
|
|
23218
|
+
enabled: true,
|
|
23219
|
+
paths: DEFAULT_TEST_PATHS
|
|
23220
|
+
}
|
|
23221
|
+
}
|
|
23222
|
+
};
|
|
23115
23223
|
const configSchema = object({
|
|
23224
|
+
preset: configPresetSchema.default("balanced"),
|
|
23116
23225
|
locale: localeSchema.default("en"),
|
|
23117
23226
|
riskThreshold: riskLevelSchema.default("high"),
|
|
23118
23227
|
ignorePaths: array(schemas_string()).default([]),
|
|
23119
|
-
sensitivePaths: array(schemas_string())
|
|
23120
|
-
.default([
|
|
23121
|
-
".github/workflows/**",
|
|
23122
|
-
".github/actions/**",
|
|
23123
|
-
"**/.env*",
|
|
23124
|
-
"**/mcp*.json",
|
|
23125
|
-
"**/*mcp*.json",
|
|
23126
|
-
"Dockerfile",
|
|
23127
|
-
"**/Dockerfile",
|
|
23128
|
-
"package.json",
|
|
23129
|
-
"pnpm-lock.yaml",
|
|
23130
|
-
"package-lock.json",
|
|
23131
|
-
"yarn.lock",
|
|
23132
|
-
"bun.lockb",
|
|
23133
|
-
"requirements.txt",
|
|
23134
|
-
"pyproject.toml",
|
|
23135
|
-
"Cargo.toml",
|
|
23136
|
-
"Cargo.lock",
|
|
23137
|
-
"go.mod",
|
|
23138
|
-
"go.sum"
|
|
23139
|
-
]),
|
|
23228
|
+
sensitivePaths: array(schemas_string()).default(DEFAULT_SENSITIVE_PATHS),
|
|
23140
23229
|
requireTests: object({
|
|
23141
23230
|
enabled: schemas_boolean().default(true),
|
|
23142
|
-
paths: array(schemas_string())
|
|
23143
|
-
.default(["src/**", "packages/**/src/**", "app/**", "lib/**"])
|
|
23231
|
+
paths: array(schemas_string()).default(DEFAULT_TEST_PATHS)
|
|
23144
23232
|
})
|
|
23145
|
-
.default({ enabled: true, paths:
|
|
23233
|
+
.default({ enabled: true, paths: DEFAULT_TEST_PATHS }),
|
|
23146
23234
|
secrets: object({ enabled: schemas_boolean().default(true) }).default({ enabled: true }),
|
|
23147
23235
|
dependencies: object({
|
|
23148
23236
|
flagNewPackages: schemas_boolean().default(true),
|
|
23149
|
-
flagMajorUpgrades: schemas_boolean().default(true)
|
|
23237
|
+
flagMajorUpgrades: schemas_boolean().default(true),
|
|
23238
|
+
flagLifecycleScripts: schemas_boolean().default(true)
|
|
23150
23239
|
})
|
|
23151
|
-
.default({ flagNewPackages: true, flagMajorUpgrades: true }),
|
|
23240
|
+
.default({ flagNewPackages: true, flagMajorUpgrades: true, flagLifecycleScripts: true }),
|
|
23152
23241
|
comment: object({ enabled: schemas_boolean().default(true) }).default({ enabled: true })
|
|
23153
23242
|
});
|
|
23154
23243
|
function parseConfig(input) {
|
|
23155
|
-
|
|
23244
|
+
const raw = isRecord(input) ? input : {};
|
|
23245
|
+
const preset = configPresetSchema.parse(raw.preset ?? "balanced");
|
|
23246
|
+
return configSchema.parse(deepMerge(PRESET_DEFAULTS[preset], raw, { preset }));
|
|
23156
23247
|
}
|
|
23157
23248
|
async function loadConfig(path) {
|
|
23158
23249
|
try {
|
|
@@ -23180,6 +23271,33 @@ function parseLocale(value, fallback = "en") {
|
|
|
23180
23271
|
const result = localeSchema.safeParse(value);
|
|
23181
23272
|
return result.success ? result.data : fallback;
|
|
23182
23273
|
}
|
|
23274
|
+
function parsePreset(value, fallback = "balanced") {
|
|
23275
|
+
const result = configPresetSchema.safeParse(value);
|
|
23276
|
+
return result.success ? result.data : fallback;
|
|
23277
|
+
}
|
|
23278
|
+
function listConfigPresets() {
|
|
23279
|
+
return CONFIG_PRESETS;
|
|
23280
|
+
}
|
|
23281
|
+
function deepMerge(...items) {
|
|
23282
|
+
const output = {};
|
|
23283
|
+
for (const item of items) {
|
|
23284
|
+
if (!isRecord(item)) {
|
|
23285
|
+
continue;
|
|
23286
|
+
}
|
|
23287
|
+
for (const [key, value] of Object.entries(item)) {
|
|
23288
|
+
if (isRecord(value) && isRecord(output[key])) {
|
|
23289
|
+
output[key] = deepMerge(output[key], value);
|
|
23290
|
+
}
|
|
23291
|
+
else {
|
|
23292
|
+
output[key] = value;
|
|
23293
|
+
}
|
|
23294
|
+
}
|
|
23295
|
+
}
|
|
23296
|
+
return output;
|
|
23297
|
+
}
|
|
23298
|
+
function isRecord(value) {
|
|
23299
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23300
|
+
}
|
|
23183
23301
|
function isMissingFileError(error) {
|
|
23184
23302
|
return (typeof error === "object" &&
|
|
23185
23303
|
error !== null &&
|
|
@@ -23244,6 +23362,8 @@ function renderEnglishMarkdownReport(result) {
|
|
|
23244
23362
|
"# ProofPR Review",
|
|
23245
23363
|
"",
|
|
23246
23364
|
`Risk: **${result.risk}**`,
|
|
23365
|
+
`Evidence score: **${result.evidenceScore.value}/100 (${formatEvidenceGrade(result.evidenceScore.grade, "en")})**`,
|
|
23366
|
+
`Review gate: **${formatReviewDecision(result.reviewDecision, "en")}**`,
|
|
23247
23367
|
"",
|
|
23248
23368
|
"## Evidence",
|
|
23249
23369
|
"",
|
|
@@ -23257,6 +23377,8 @@ function renderEnglishMarkdownReport(result) {
|
|
|
23257
23377
|
`- Reproduction context: ${formatBoolean(result.summary.reproductionEvidence)}`,
|
|
23258
23378
|
""
|
|
23259
23379
|
];
|
|
23380
|
+
appendEvidenceScoreSection(lines, result, "en");
|
|
23381
|
+
appendReviewPlanSection(lines, result, "en");
|
|
23260
23382
|
if (result.findings.length === 0) {
|
|
23261
23383
|
lines.push("## Findings", "", "No review-risk findings detected by the enabled rules.", "");
|
|
23262
23384
|
return lines.join("\n");
|
|
@@ -23274,6 +23396,8 @@ function renderChineseMarkdownReport(result) {
|
|
|
23274
23396
|
"# ProofPR 审查报告",
|
|
23275
23397
|
"",
|
|
23276
23398
|
`风险等级:**${translateRisk(result.risk)}**`,
|
|
23399
|
+
`证据评分:**${result.evidenceScore.value}/100(${formatEvidenceGrade(result.evidenceScore.grade, "zh-CN")})**`,
|
|
23400
|
+
`Review 门禁:**${formatReviewDecision(result.reviewDecision, "zh-CN")}**`,
|
|
23277
23401
|
"",
|
|
23278
23402
|
"## 证据概览",
|
|
23279
23403
|
"",
|
|
@@ -23287,6 +23411,8 @@ function renderChineseMarkdownReport(result) {
|
|
|
23287
23411
|
`- 复现上下文:${formatChineseBoolean(result.summary.reproductionEvidence)}`,
|
|
23288
23412
|
""
|
|
23289
23413
|
];
|
|
23414
|
+
appendEvidenceScoreSection(lines, result, "zh-CN");
|
|
23415
|
+
appendReviewPlanSection(lines, result, "zh-CN");
|
|
23290
23416
|
if (result.findings.length === 0) {
|
|
23291
23417
|
lines.push("## 风险发现", "", "启用的规则没有发现需要优先关注的 review 风险。", "");
|
|
23292
23418
|
return lines.join("\n");
|
|
@@ -23298,6 +23424,52 @@ function renderChineseMarkdownReport(result) {
|
|
|
23298
23424
|
lines.push("## 维护者关注点", "", ...maintainerFocus(result.findings, "zh-CN").map((item) => `- ${item}`), "");
|
|
23299
23425
|
return lines.join("\n");
|
|
23300
23426
|
}
|
|
23427
|
+
function appendEvidenceScoreSection(lines, result, locale) {
|
|
23428
|
+
lines.push(locale === "zh-CN" ? "## 证据评分细节" : "## Evidence Score", "");
|
|
23429
|
+
if (result.evidenceScore.strengths.length > 0) {
|
|
23430
|
+
for (const strength of result.evidenceScore.strengths) {
|
|
23431
|
+
lines.push(locale === "zh-CN"
|
|
23432
|
+
? `- 证据优势:${translateScoreMessage(strength)}`
|
|
23433
|
+
: `- Strength: ${strength}`);
|
|
23434
|
+
}
|
|
23435
|
+
}
|
|
23436
|
+
else {
|
|
23437
|
+
lines.push(locale === "zh-CN" ? "- 证据优势:暂无明显优势信号。" : "- Strength: No strong evidence signals detected.");
|
|
23438
|
+
}
|
|
23439
|
+
if (result.evidenceScore.deductions.length > 0) {
|
|
23440
|
+
for (const deduction of result.evidenceScore.deductions) {
|
|
23441
|
+
lines.push(locale === "zh-CN"
|
|
23442
|
+
? `- 扣分项:-${deduction.points},${translateDeduction(deduction.reasonId, deduction.message)}`
|
|
23443
|
+
: `- Deduction: -${deduction.points}, ${deduction.message}`);
|
|
23444
|
+
}
|
|
23445
|
+
}
|
|
23446
|
+
else {
|
|
23447
|
+
lines.push(locale === "zh-CN" ? "- 扣分项:无。" : "- Deduction: none.");
|
|
23448
|
+
}
|
|
23449
|
+
lines.push("");
|
|
23450
|
+
}
|
|
23451
|
+
function appendReviewPlanSection(lines, result, locale) {
|
|
23452
|
+
lines.push(locale === "zh-CN" ? "## Review 行动清单" : "## Review Plan", "");
|
|
23453
|
+
if (result.reviewPlan.actionItems.length > 0) {
|
|
23454
|
+
for (const action of result.reviewPlan.actionItems) {
|
|
23455
|
+
lines.push(locale === "zh-CN"
|
|
23456
|
+
? `- [ ] ${translateReviewActionTitle(action.actionId, action.title)}(${formatPriority(action.priority, locale)}):${translateReviewActionDetail(action.actionId, action.detail)}`
|
|
23457
|
+
: `- [ ] ${action.title} (${formatPriority(action.priority, locale)}): ${action.detail}`);
|
|
23458
|
+
}
|
|
23459
|
+
}
|
|
23460
|
+
else {
|
|
23461
|
+
lines.push(locale === "zh-CN" ? "- [ ] 没有额外行动项。" : "- [ ] No additional action items.");
|
|
23462
|
+
}
|
|
23463
|
+
if (result.reviewPlan.focusFiles.length > 0) {
|
|
23464
|
+
lines.push("", locale === "zh-CN" ? "重点文件:" : "Focus files:");
|
|
23465
|
+
for (const file of result.reviewPlan.focusFiles) {
|
|
23466
|
+
lines.push(locale === "zh-CN"
|
|
23467
|
+
? `- \`${file.path}\`(${formatPriority(file.priority, locale)}):${translateFocusReason(file.reasonId, file.reason)}`
|
|
23468
|
+
: `- \`${file.path}\` (${formatPriority(file.priority, locale)}): ${file.reason}`);
|
|
23469
|
+
}
|
|
23470
|
+
}
|
|
23471
|
+
lines.push("");
|
|
23472
|
+
}
|
|
23301
23473
|
function formatEnglishFinding(finding) {
|
|
23302
23474
|
const lines = [
|
|
23303
23475
|
`### ${finding.title}`,
|
|
@@ -23372,6 +23544,21 @@ function maintainerFocus(findings, locale) {
|
|
|
23372
23544
|
? "要求拆分 PR,或提供逐文件 review 指南。"
|
|
23373
23545
|
: "Request a smaller PR or a file-by-file review guide.");
|
|
23374
23546
|
}
|
|
23547
|
+
else if (finding.ruleId === "dependency-major-upgrade") {
|
|
23548
|
+
focus.add(locale === "zh-CN"
|
|
23549
|
+
? "重点核查依赖大版本升级的迁移说明、兼容性和测试覆盖。"
|
|
23550
|
+
: "Review dependency major upgrade migration notes, compatibility, and test coverage.");
|
|
23551
|
+
}
|
|
23552
|
+
else if (finding.ruleId === "dependency-lifecycle-script") {
|
|
23553
|
+
focus.add(locale === "zh-CN"
|
|
23554
|
+
? "合并前审查包生命周期脚本是否会在安装或发布时执行非预期代码。"
|
|
23555
|
+
: "Review package lifecycle scripts for unexpected install or publish-time execution.");
|
|
23556
|
+
}
|
|
23557
|
+
else if (finding.ruleId === "workflow-dangerous-trigger") {
|
|
23558
|
+
focus.add(locale === "zh-CN"
|
|
23559
|
+
? "重点审查 pull_request_target 是否会用高权限 token 执行不可信 PR 代码。"
|
|
23560
|
+
: "Review whether pull_request_target can execute untrusted PR code with privileged tokens.");
|
|
23561
|
+
}
|
|
23375
23562
|
else if (finding.ruleId === "mcp-credential-risk") {
|
|
23376
23563
|
focus.add(locale === "zh-CN"
|
|
23377
23564
|
? "重点审查 MCP command、args 和凭证处理方式。"
|
|
@@ -23433,6 +23620,20 @@ function translateFinding(finding) {
|
|
|
23433
23620
|
recommendation: "请确认包名、许可证、来源可信度,以及 lockfile 是否匹配预期依赖变化。"
|
|
23434
23621
|
};
|
|
23435
23622
|
}
|
|
23623
|
+
if (finding.ruleId === "dependency-major-upgrade") {
|
|
23624
|
+
return {
|
|
23625
|
+
title: "依赖发生大版本升级",
|
|
23626
|
+
message: finding.path ? `${finding.path} 中有依赖跨越了大版本边界。` : finding.message,
|
|
23627
|
+
recommendation: "请核查 changelog、迁移说明、peer dependencies 影响,以及测试是否覆盖升级后的关键路径。"
|
|
23628
|
+
};
|
|
23629
|
+
}
|
|
23630
|
+
if (finding.ruleId === "dependency-lifecycle-script") {
|
|
23631
|
+
return {
|
|
23632
|
+
title: "包生命周期脚本发生变更",
|
|
23633
|
+
message: finding.path ? `${finding.path} 新增或修改了安装/发布阶段可能自动执行的脚本。` : finding.message,
|
|
23634
|
+
recommendation: "请确认该脚本是否必要,是否下载或执行远程代码,以及是否会影响安装该包的用户。"
|
|
23635
|
+
};
|
|
23636
|
+
}
|
|
23436
23637
|
if (finding.ruleId === "workflow-permission-change") {
|
|
23437
23638
|
return {
|
|
23438
23639
|
title: "Workflow 权限发生变更",
|
|
@@ -23440,6 +23641,13 @@ function translateFinding(finding) {
|
|
|
23440
23641
|
recommendation: "请确认 workflow 是否真的需要写权限或 token 权限,并检查不可信 PR 是否能触达该 workflow。"
|
|
23441
23642
|
};
|
|
23442
23643
|
}
|
|
23644
|
+
if (finding.ruleId === "workflow-dangerous-trigger") {
|
|
23645
|
+
return {
|
|
23646
|
+
title: "Workflow 使用了 pull_request_target",
|
|
23647
|
+
message: finding.path ? `${finding.path} 新增了 pull_request_target 触发器。` : finding.message,
|
|
23648
|
+
recommendation: "请确认该 workflow 不会用高权限 token、secret 或写权限执行不可信 PR 代码。"
|
|
23649
|
+
};
|
|
23650
|
+
}
|
|
23443
23651
|
if (finding.ruleId === "mcp-credential-risk") {
|
|
23444
23652
|
return {
|
|
23445
23653
|
title: "MCP 配置需要重点审查",
|
|
@@ -23472,6 +23680,122 @@ function translateSeverity(severity) {
|
|
|
23472
23680
|
function translateDescriptionState(state) {
|
|
23473
23681
|
return { unavailable: "不可用", missing: "缺失", thin: "过薄", present: "充足" }[state] ?? state;
|
|
23474
23682
|
}
|
|
23683
|
+
function formatEvidenceGrade(grade, locale) {
|
|
23684
|
+
if (locale === "zh-CN") {
|
|
23685
|
+
return {
|
|
23686
|
+
strong: "证据充分",
|
|
23687
|
+
adequate: "基本充分",
|
|
23688
|
+
thin: "证据偏薄",
|
|
23689
|
+
risky: "证据不足"
|
|
23690
|
+
}[grade] ?? grade;
|
|
23691
|
+
}
|
|
23692
|
+
return grade;
|
|
23693
|
+
}
|
|
23694
|
+
function formatReviewDecision(decision, locale) {
|
|
23695
|
+
if (locale === "zh-CN") {
|
|
23696
|
+
return {
|
|
23697
|
+
ready: "可以进入常规 review",
|
|
23698
|
+
"review-carefully": "带着重点进入 review",
|
|
23699
|
+
"needs-evidence": "先要求补充证据",
|
|
23700
|
+
"block-merge": "处理风险前不建议合并"
|
|
23701
|
+
}[decision] ?? decision;
|
|
23702
|
+
}
|
|
23703
|
+
return {
|
|
23704
|
+
ready: "Ready for normal review",
|
|
23705
|
+
"review-carefully": "Review with focused attention",
|
|
23706
|
+
"needs-evidence": "Ask for evidence before deep review",
|
|
23707
|
+
"block-merge": "Block merge until risks are handled"
|
|
23708
|
+
}[decision] ?? decision;
|
|
23709
|
+
}
|
|
23710
|
+
function formatPriority(priority, locale) {
|
|
23711
|
+
if (locale === "zh-CN") {
|
|
23712
|
+
return { low: "低优先级", medium: "中优先级", high: "高优先级" }[priority] ?? priority;
|
|
23713
|
+
}
|
|
23714
|
+
return `${priority} priority`;
|
|
23715
|
+
}
|
|
23716
|
+
function translateReviewActionTitle(actionId, fallback) {
|
|
23717
|
+
return {
|
|
23718
|
+
"block-merge-until-resolved": "风险处理前不要合并",
|
|
23719
|
+
"ask-for-evidence-before-review": "深入 review 前先要求补充证据",
|
|
23720
|
+
"review-with-focus": "带着重点清单进行 review",
|
|
23721
|
+
"normal-review": "进入常规 review",
|
|
23722
|
+
"improve-pr-description": "要求补充更清楚的 PR 描述",
|
|
23723
|
+
"add-verification-evidence": "要求补充测试或手动验证证据",
|
|
23724
|
+
"add-reproduction-context": "要求补充复现或 before/after 上下文",
|
|
23725
|
+
"rotate-secret": "轮换并移除暴露的凭证",
|
|
23726
|
+
"justify-workflow-permissions": "要求说明 workflow 权限最小化理由",
|
|
23727
|
+
"review-privileged-pr-trigger": "审查 pull_request_target 高权限触发器",
|
|
23728
|
+
"review-package-lifecycle-script": "审查包生命周期脚本",
|
|
23729
|
+
"review-mcp-execution-surface": "审查 MCP 命令、参数和凭证处理",
|
|
23730
|
+
"request-review-map-or-split": "要求拆分 PR 或提供逐文件 review map",
|
|
23731
|
+
"verify-dependency-change": "核查依赖来源和 lockfile 影响",
|
|
23732
|
+
"review-major-dependency-upgrade": "核查依赖大版本升级影响",
|
|
23733
|
+
"assign-sensitive-file-review": "安排敏感文件重点 review"
|
|
23734
|
+
}[actionId] ?? fallback;
|
|
23735
|
+
}
|
|
23736
|
+
function translateReviewActionDetail(actionId, fallback) {
|
|
23737
|
+
return {
|
|
23738
|
+
"block-merge-until-resolved": "在高风险 finding 被解释、降低或移除前,把这个 PR 视为不可合并。",
|
|
23739
|
+
"ask-for-evidence-before-review": "要求测试、截图、复现步骤或更清楚的 PR 描述,再投入详细 review。",
|
|
23740
|
+
"review-with-focus": "优先使用下面的风险发现和重点文件作为第一轮 review map。",
|
|
23741
|
+
"normal-review": "当前证据足够支撑维护者进行常规 review。",
|
|
23742
|
+
"improve-pr-description": "贡献者应说明为什么改、改了什么、如何验证,以及是否有发布或兼容性风险。",
|
|
23743
|
+
"add-verification-evidence": "要求测试输出、CI 链接、截图,或简短的手动验证说明。",
|
|
23744
|
+
"add-reproduction-context": "PR 应包含复现步骤、预期/实际行为,或相关 before/after 截图。",
|
|
23745
|
+
"rotate-secret": "在 secret 从 PR 中移除并完成轮换前,不要合并。",
|
|
23746
|
+
"justify-workflow-permissions": "确认写权限或 OIDC 是否必要,并检查不可信 PR 是否能触发该 workflow。",
|
|
23747
|
+
"review-privileged-pr-trigger": "确认 workflow 不会用写权限 token、secret 或仓库权限执行不可信 PR 代码。",
|
|
23748
|
+
"review-package-lifecycle-script": "检查 install、postinstall、prepare 或 publish 脚本是否会执行非预期代码。",
|
|
23749
|
+
"review-mcp-execution-surface": "检查 MCP 配置是否提交凭证,或意外扩大本地执行面。",
|
|
23750
|
+
"request-review-map-or-split": "要求贡献者拆分无关改动,或标出最需要重点 review 的文件。",
|
|
23751
|
+
"verify-dependency-change": "检查包名、维护者、许可证、安装脚本,以及 lockfile 是否符合预期依赖变化。",
|
|
23752
|
+
"review-major-dependency-upgrade": "检查 changelog、迁移说明、peer dependencies,以及测试是否覆盖升级后的关键路径。",
|
|
23753
|
+
"assign-sensitive-file-review": "合并前由维护者有意识地检查敏感文件改动。"
|
|
23754
|
+
}[actionId] ?? fallback;
|
|
23755
|
+
}
|
|
23756
|
+
function translateFocusReason(reasonId, fallback) {
|
|
23757
|
+
return {
|
|
23758
|
+
"change-size": "review 面积相关 finding",
|
|
23759
|
+
"sensitive-path": "敏感路径发生变更",
|
|
23760
|
+
"dependency-added": "依赖清单发生变更",
|
|
23761
|
+
"dependency-major-upgrade": "依赖发生大版本升级",
|
|
23762
|
+
"dependency-lifecycle-script": "包生命周期脚本发生变更",
|
|
23763
|
+
"workflow-permission-change": "workflow 权限发生变更",
|
|
23764
|
+
"workflow-dangerous-trigger": "workflow 使用了高风险触发器",
|
|
23765
|
+
"mcp-credential-risk": "MCP 配置存在执行面或凭证风险",
|
|
23766
|
+
"missing-tests": "代码改动缺少测试或验证证据"
|
|
23767
|
+
}[reasonId] ?? fallback;
|
|
23768
|
+
}
|
|
23769
|
+
function translateScoreMessage(message) {
|
|
23770
|
+
return {
|
|
23771
|
+
"PR description provides review context.": "PR 描述提供了 review 上下文。",
|
|
23772
|
+
"Verification evidence was found.": "检测到测试或手动验证证据。",
|
|
23773
|
+
"Reproduction or before/after context was found.": "检测到复现步骤或 before/after 上下文。",
|
|
23774
|
+
"Test files changed with the PR.": "PR 同时修改了测试文件。",
|
|
23775
|
+
"No configured sensitive files changed.": "没有改动已配置的敏感文件。"
|
|
23776
|
+
}[message] ?? message;
|
|
23777
|
+
}
|
|
23778
|
+
function translateDeduction(reasonId, fallback) {
|
|
23779
|
+
return {
|
|
23780
|
+
"missing-pr-description": "PR 描述缺失。",
|
|
23781
|
+
"thin-pr-description": "PR 描述过薄,不足以支撑可靠 review。",
|
|
23782
|
+
"no-pr-context": "扫描时没有可用的 PR 描述上下文。",
|
|
23783
|
+
"missing-verification": "没有检测到测试或手动验证证据。",
|
|
23784
|
+
"missing-reproduction-context": "没有检测到复现步骤、before/after 或预期/实际上下文。",
|
|
23785
|
+
"secret-detected": "检测到疑似已提交 secret。",
|
|
23786
|
+
"workflow-permission-change": "Workflow 权限变化需要重点审查。",
|
|
23787
|
+
"mcp-credential-risk": "MCP 配置扩大了本地执行面或凭证风险。",
|
|
23788
|
+
"large-review-surface": "PR 规模过大,常规 review 可靠性会下降。",
|
|
23789
|
+
"broad-review-surface": "PR review 面积偏大。",
|
|
23790
|
+
"sensitive-path-high": "高敏感文件发生变更,需要重点 review。",
|
|
23791
|
+
"sensitive-path-medium": "敏感文件发生变更,需要重点 review。",
|
|
23792
|
+
"dependency-change": "依赖清单发生变更。",
|
|
23793
|
+
"dependency-major-upgrade": "依赖发生大版本升级。",
|
|
23794
|
+
"dependency-lifecycle-script": "包生命周期脚本可能在安装或发布阶段执行代码。",
|
|
23795
|
+
"workflow-dangerous-trigger": "pull_request_target workflow 需要重点审查高权限触发路径。",
|
|
23796
|
+
"missing-tests": "代码发生变更,但缺少测试变更或验证说明。"
|
|
23797
|
+
}[reasonId] ?? fallback;
|
|
23798
|
+
}
|
|
23475
23799
|
function formatBoolean(value) {
|
|
23476
23800
|
return value ? "yes" : "no";
|
|
23477
23801
|
}
|
|
@@ -23780,6 +24104,7 @@ function analyzeDiffFiles(files, config, pullRequest) {
|
|
|
23780
24104
|
findings.push(...analyzePullRequestEvidence(activeFiles, pullRequest));
|
|
23781
24105
|
findings.push(...analyzeDependencyChanges(activeFiles, config));
|
|
23782
24106
|
findings.push(...analyzeWorkflowPermissions(activeFiles));
|
|
24107
|
+
findings.push(...analyzeWorkflowDangerousTriggers(activeFiles));
|
|
23783
24108
|
findings.push(...analyzeMcpConfigs(activeFiles));
|
|
23784
24109
|
if (config.secrets.enabled) {
|
|
23785
24110
|
for (const file of activeFiles) {
|
|
@@ -23905,58 +24230,137 @@ function analyzePullRequestEvidence(files, pullRequest) {
|
|
|
23905
24230
|
return findings;
|
|
23906
24231
|
}
|
|
23907
24232
|
function analyzeDependencyChanges(files, config) {
|
|
23908
|
-
if (!config.dependencies.flagNewPackages) {
|
|
23909
|
-
return [];
|
|
23910
|
-
}
|
|
23911
24233
|
const findings = [];
|
|
23912
24234
|
for (const file of files.filter((candidate) => isDependencyManifest(candidate.path))) {
|
|
23913
|
-
|
|
23914
|
-
.
|
|
23915
|
-
|
|
23916
|
-
|
|
23917
|
-
|
|
24235
|
+
if (config.dependencies.flagNewPackages) {
|
|
24236
|
+
const addedDependencyLines = file.addedLines.filter((line) => isDependencyLikeAddition(file.path, line.value.trim()));
|
|
24237
|
+
if (addedDependencyLines.length > 0) {
|
|
24238
|
+
findings.push({
|
|
24239
|
+
ruleId: "dependency-added",
|
|
24240
|
+
title: "Dependency manifest changed",
|
|
24241
|
+
message: `${file.path} adds or changes dependency-like entries.`,
|
|
24242
|
+
severity: "medium",
|
|
24243
|
+
path: file.path,
|
|
24244
|
+
evidence: addedDependencyLines.slice(0, 5).map(formatEvidenceLine),
|
|
24245
|
+
recommendation: "Verify package names, licenses, provenance, and whether the lockfile matches the intended dependency change."
|
|
24246
|
+
});
|
|
24247
|
+
}
|
|
24248
|
+
}
|
|
24249
|
+
if (config.dependencies.flagMajorUpgrades) {
|
|
24250
|
+
findings.push(...analyzeMajorDependencyUpgrades(file));
|
|
24251
|
+
}
|
|
24252
|
+
if (config.dependencies.flagLifecycleScripts) {
|
|
24253
|
+
findings.push(...analyzeLifecycleScripts(file));
|
|
23918
24254
|
}
|
|
23919
|
-
findings.push({
|
|
23920
|
-
ruleId: "dependency-added",
|
|
23921
|
-
title: "Dependency manifest changed",
|
|
23922
|
-
message: `${file.path} adds or changes dependency-like entries.`,
|
|
23923
|
-
severity: "medium",
|
|
23924
|
-
path: file.path,
|
|
23925
|
-
evidence: addedDependencyLines.slice(0, 5),
|
|
23926
|
-
recommendation: "Verify package names, licenses, provenance, and whether the lockfile matches the intended dependency change."
|
|
23927
|
-
});
|
|
23928
24255
|
}
|
|
23929
24256
|
return findings;
|
|
23930
24257
|
}
|
|
23931
24258
|
function isDependencyLikeAddition(path, line) {
|
|
24259
|
+
return parseDependencyLine(path, { value: line }) !== undefined;
|
|
24260
|
+
}
|
|
24261
|
+
function analyzeMajorDependencyUpgrades(file) {
|
|
24262
|
+
const removedDependencies = new Map();
|
|
24263
|
+
for (const line of file.removedLines) {
|
|
24264
|
+
const parsed = parseDependencyLine(file.path, line);
|
|
24265
|
+
if (parsed) {
|
|
24266
|
+
removedDependencies.set(parsed.name, parsed);
|
|
24267
|
+
}
|
|
24268
|
+
}
|
|
24269
|
+
const upgrades = file.addedLines
|
|
24270
|
+
.map((line) => parseDependencyLine(file.path, line))
|
|
24271
|
+
.filter((line) => Boolean(line))
|
|
24272
|
+
.map((added) => ({ added, removed: removedDependencies.get(added.name) }))
|
|
24273
|
+
.filter((change) => change.removed !== undefined && isMajorUpgrade(change.removed.version, change.added.version));
|
|
24274
|
+
if (upgrades.length === 0) {
|
|
24275
|
+
return [];
|
|
24276
|
+
}
|
|
24277
|
+
return [
|
|
24278
|
+
{
|
|
24279
|
+
ruleId: "dependency-major-upgrade",
|
|
24280
|
+
title: "Dependency major version upgrade",
|
|
24281
|
+
message: `${file.path} upgrades one or more dependencies across a major version boundary.`,
|
|
24282
|
+
severity: "medium",
|
|
24283
|
+
path: file.path,
|
|
24284
|
+
evidence: upgrades.slice(0, 5).map(({ added, removed }) => `${added.line.lineNumber ? `line ${added.line.lineNumber}: ` : ""}${added.name} ${removed.version} -> ${added.version}`),
|
|
24285
|
+
recommendation: "Check changelogs, migration notes, peer dependency impact, and whether tests cover the upgraded package surface."
|
|
24286
|
+
}
|
|
24287
|
+
];
|
|
24288
|
+
}
|
|
24289
|
+
function analyzeLifecycleScripts(file) {
|
|
24290
|
+
if (!file.path.endsWith("package.json")) {
|
|
24291
|
+
return [];
|
|
24292
|
+
}
|
|
24293
|
+
const lifecycleLines = file.addedLines.filter((line) => /^"(?:preinstall|install|postinstall|prepare|prepublish|prepublishOnly)"\s*:/.test(line.value.trim()));
|
|
24294
|
+
if (lifecycleLines.length === 0) {
|
|
24295
|
+
return [];
|
|
24296
|
+
}
|
|
24297
|
+
return [
|
|
24298
|
+
{
|
|
24299
|
+
ruleId: "dependency-lifecycle-script",
|
|
24300
|
+
title: "Package lifecycle script changed",
|
|
24301
|
+
message: `${file.path} adds or changes npm lifecycle scripts that may run during install or publish.`,
|
|
24302
|
+
severity: "high",
|
|
24303
|
+
path: file.path,
|
|
24304
|
+
evidence: lifecycleLines.slice(0, 5).map(formatEvidenceLine),
|
|
24305
|
+
recommendation: "Review whether the lifecycle script is necessary, whether it downloads or executes remote code, and whether it can affect consumers during install."
|
|
24306
|
+
}
|
|
24307
|
+
];
|
|
24308
|
+
}
|
|
24309
|
+
function parseDependencyLine(path, line) {
|
|
24310
|
+
const value = line.value.trim();
|
|
23932
24311
|
if (path.endsWith("package.json")) {
|
|
23933
|
-
const match = /^"(?<key>[@A-Za-z0-9_.-]+)"\s*:\s*"(?<
|
|
24312
|
+
const match = /^"(?<key>[@A-Za-z0-9_.-]+)"\s*:\s*"(?<version>[^"]*)"/.exec(value);
|
|
23934
24313
|
if (!match?.groups) {
|
|
23935
|
-
return
|
|
24314
|
+
return undefined;
|
|
23936
24315
|
}
|
|
23937
|
-
const { key,
|
|
23938
|
-
if (!key || !
|
|
23939
|
-
return
|
|
24316
|
+
const { key, version } = match.groups;
|
|
24317
|
+
if (!key || !version || PACKAGE_JSON_NON_DEPENDENCY_KEYS.has(key)) {
|
|
24318
|
+
return undefined;
|
|
23940
24319
|
}
|
|
23941
|
-
|
|
24320
|
+
if (!/^(?:\^|~|>=?|<=?|\d|workspace:|npm:|file:|link:|portal:|git\+|https?:|github:)/.test(version)) {
|
|
24321
|
+
return undefined;
|
|
24322
|
+
}
|
|
24323
|
+
return { name: key, version, line };
|
|
23942
24324
|
}
|
|
23943
24325
|
if (path.endsWith("requirements.txt")) {
|
|
23944
|
-
|
|
24326
|
+
const match = /^(?<name>[A-Za-z0-9_.-]+)(?:\[.*\])?\s*(?:==|>=|<=|~=|>|<)\s*(?<version>[^#\s]+)/.exec(value);
|
|
24327
|
+
return match?.groups?.name && match.groups.version
|
|
24328
|
+
? { name: match.groups.name, version: match.groups.version, line }
|
|
24329
|
+
: undefined;
|
|
23945
24330
|
}
|
|
23946
24331
|
if (path.endsWith("pyproject.toml") || path.endsWith("Cargo.toml")) {
|
|
23947
|
-
|
|
24332
|
+
const match = /^(?<name>[A-Za-z0-9_.-]+)\s*=\s*"(?<version>(?:\^|~|>=?|<=?|\d|workspace:|path\s*=|git\s*=)[^"]*)"/.exec(value);
|
|
24333
|
+
return match?.groups?.name && match.groups.version
|
|
24334
|
+
? { name: match.groups.name, version: match.groups.version, line }
|
|
24335
|
+
: undefined;
|
|
23948
24336
|
}
|
|
23949
24337
|
if (path.endsWith("go.mod")) {
|
|
23950
|
-
|
|
24338
|
+
const match = /^(?:require\s+)?(?<name>[A-Za-z0-9_.\-/]+)\s+(?<version>v\d+\.\d+\.\d+)/.exec(value);
|
|
24339
|
+
return match?.groups?.name && match.groups.version
|
|
24340
|
+
? { name: match.groups.name, version: match.groups.version, line }
|
|
24341
|
+
: undefined;
|
|
23951
24342
|
}
|
|
23952
|
-
return
|
|
24343
|
+
return undefined;
|
|
24344
|
+
}
|
|
24345
|
+
function isMajorUpgrade(previousVersion, nextVersion) {
|
|
24346
|
+
const previousMajor = extractMajorVersion(previousVersion);
|
|
24347
|
+
const nextMajor = extractMajorVersion(nextVersion);
|
|
24348
|
+
return previousMajor !== undefined && nextMajor !== undefined && nextMajor > previousMajor;
|
|
24349
|
+
}
|
|
24350
|
+
function extractMajorVersion(version) {
|
|
24351
|
+
const normalized = version
|
|
24352
|
+
.replace(/^workspace:/, "")
|
|
24353
|
+
.replace(/^npm:[^@]+@/, "")
|
|
24354
|
+
.replace(/^[~^<>=\s]+/, "")
|
|
24355
|
+
.replace(/^v/, "");
|
|
24356
|
+
const match = /(?<major>\d+)\.\d+\.\d+/.exec(normalized);
|
|
24357
|
+
const major = match?.groups?.major ? Number(match.groups.major) : undefined;
|
|
24358
|
+
return major !== undefined && Number.isInteger(major) ? major : undefined;
|
|
23953
24359
|
}
|
|
23954
24360
|
function analyzeWorkflowPermissions(files) {
|
|
23955
24361
|
const findings = [];
|
|
23956
24362
|
for (const file of files.filter((candidate) => isWorkflowPath(candidate.path))) {
|
|
23957
|
-
const permissionLines = file.addedLines
|
|
23958
|
-
.map((line) => line.value.trim())
|
|
23959
|
-
.filter((line) => /permissions:|contents:\s*write|packages:\s*write|id-token:\s*write|pull-requests:\s*write/.test(line));
|
|
24363
|
+
const permissionLines = file.addedLines.filter((line) => /permissions:|contents:\s*write|packages:\s*write|id-token:\s*write|pull-requests:\s*write/.test(line.value.trim()));
|
|
23960
24364
|
if (permissionLines.length === 0) {
|
|
23961
24365
|
continue;
|
|
23962
24366
|
}
|
|
@@ -23966,18 +24370,35 @@ function analyzeWorkflowPermissions(files) {
|
|
|
23966
24370
|
message: `${file.path} adds or changes GitHub Actions permissions.`,
|
|
23967
24371
|
severity: "high",
|
|
23968
24372
|
path: file.path,
|
|
23969
|
-
evidence: permissionLines.slice(0, 5),
|
|
24373
|
+
evidence: permissionLines.slice(0, 5).map(formatEvidenceLine),
|
|
23970
24374
|
recommendation: "Check whether the workflow really needs write or token permissions and whether untrusted pull requests can reach it."
|
|
23971
24375
|
});
|
|
23972
24376
|
}
|
|
23973
24377
|
return findings;
|
|
23974
24378
|
}
|
|
24379
|
+
function analyzeWorkflowDangerousTriggers(files) {
|
|
24380
|
+
const findings = [];
|
|
24381
|
+
for (const file of files.filter((candidate) => isWorkflowPath(candidate.path))) {
|
|
24382
|
+
const triggerLines = file.addedLines.filter((line) => /\bpull_request_target\b/.test(line.value.trim()));
|
|
24383
|
+
if (triggerLines.length === 0) {
|
|
24384
|
+
continue;
|
|
24385
|
+
}
|
|
24386
|
+
findings.push({
|
|
24387
|
+
ruleId: "workflow-dangerous-trigger",
|
|
24388
|
+
title: "Workflow uses pull_request_target",
|
|
24389
|
+
message: `${file.path} adds pull_request_target, which runs with base repository context and can be risky for untrusted PRs.`,
|
|
24390
|
+
severity: "high",
|
|
24391
|
+
path: file.path,
|
|
24392
|
+
evidence: triggerLines.slice(0, 5).map(formatEvidenceLine),
|
|
24393
|
+
recommendation: "Confirm the workflow does not check out or execute untrusted PR code with privileged tokens or write permissions."
|
|
24394
|
+
});
|
|
24395
|
+
}
|
|
24396
|
+
return findings;
|
|
24397
|
+
}
|
|
23975
24398
|
function analyzeMcpConfigs(files) {
|
|
23976
24399
|
const findings = [];
|
|
23977
24400
|
for (const file of files.filter((candidate) => isMcpConfigPath(candidate.path))) {
|
|
23978
|
-
const riskyLines = file.addedLines
|
|
23979
|
-
.map((line) => line.value.trim())
|
|
23980
|
-
.filter((line) => /env|token|secret|password|api[_-]?key|command|args/i.test(line));
|
|
24401
|
+
const riskyLines = file.addedLines.filter((line) => /env|token|secret|password|api[_-]?key|command|args/i.test(line.value.trim()));
|
|
23981
24402
|
if (riskyLines.length === 0) {
|
|
23982
24403
|
continue;
|
|
23983
24404
|
}
|
|
@@ -23987,12 +24408,16 @@ function analyzeMcpConfigs(files) {
|
|
|
23987
24408
|
message: `${file.path} adds MCP configuration lines related to commands or credentials.`,
|
|
23988
24409
|
severity: "high",
|
|
23989
24410
|
path: file.path,
|
|
23990
|
-
evidence: riskyLines.slice(0, 5),
|
|
24411
|
+
evidence: riskyLines.slice(0, 5).map(formatEvidenceLine),
|
|
23991
24412
|
recommendation: "Avoid committing credentials in MCP config. Review command and args values as local execution surface."
|
|
23992
24413
|
});
|
|
23993
24414
|
}
|
|
23994
24415
|
return findings;
|
|
23995
24416
|
}
|
|
24417
|
+
function formatEvidenceLine(line) {
|
|
24418
|
+
const value = line.value.trim();
|
|
24419
|
+
return line.lineNumber ? `line ${line.lineNumber}: ${value}` : value;
|
|
24420
|
+
}
|
|
23996
24421
|
function sensitivePathSeverity(path) {
|
|
23997
24422
|
if (matchesAny(path, [
|
|
23998
24423
|
"**/.env*",
|
|
@@ -24015,8 +24440,14 @@ function scanDiff(diffText, options = {}) {
|
|
|
24015
24440
|
const files = parseUnifiedDiff(diffText);
|
|
24016
24441
|
const findings = dedupeFindings(analyzeDiffFiles(files, config, options.pullRequest));
|
|
24017
24442
|
const summary = summarizeDiffFiles(files, config, options.pullRequest);
|
|
24443
|
+
const risk = calculateRisk(findings);
|
|
24444
|
+
const evidenceScore = calculateEvidenceScore(summary, findings);
|
|
24445
|
+
const reviewDecision = calculateReviewDecision(risk, evidenceScore, findings);
|
|
24018
24446
|
return {
|
|
24019
|
-
risk
|
|
24447
|
+
risk,
|
|
24448
|
+
evidenceScore,
|
|
24449
|
+
reviewDecision,
|
|
24450
|
+
reviewPlan: buildReviewPlan(reviewDecision, findings, evidenceScore),
|
|
24020
24451
|
summary,
|
|
24021
24452
|
findings
|
|
24022
24453
|
};
|
|
@@ -24033,6 +24464,373 @@ function calculateRisk(findings) {
|
|
|
24033
24464
|
}
|
|
24034
24465
|
return "low";
|
|
24035
24466
|
}
|
|
24467
|
+
function calculateEvidenceScore(summary, findings) {
|
|
24468
|
+
const deductions = new Map();
|
|
24469
|
+
const addDeduction = (reasonId, points, message) => {
|
|
24470
|
+
const existing = deductions.get(reasonId);
|
|
24471
|
+
if (existing) {
|
|
24472
|
+
existing.points = Math.max(existing.points, points);
|
|
24473
|
+
return;
|
|
24474
|
+
}
|
|
24475
|
+
deductions.set(reasonId, { message, points });
|
|
24476
|
+
};
|
|
24477
|
+
if (summary.pullRequestDescription === "missing") {
|
|
24478
|
+
addDeduction("missing-pr-description", 25, "PR description is missing.");
|
|
24479
|
+
}
|
|
24480
|
+
else if (summary.pullRequestDescription === "thin") {
|
|
24481
|
+
addDeduction("thin-pr-description", 15, "PR description is too thin for confident review.");
|
|
24482
|
+
}
|
|
24483
|
+
else if (summary.pullRequestDescription === "unavailable") {
|
|
24484
|
+
addDeduction("no-pr-context", 10, "PR description was not available to the scanner.");
|
|
24485
|
+
}
|
|
24486
|
+
const needsVerificationEvidence = findings.some((finding) => [
|
|
24487
|
+
"change-size",
|
|
24488
|
+
"sensitive-path",
|
|
24489
|
+
"missing-tests",
|
|
24490
|
+
"dependency-added",
|
|
24491
|
+
"dependency-major-upgrade",
|
|
24492
|
+
"dependency-lifecycle-script",
|
|
24493
|
+
"workflow-permission-change",
|
|
24494
|
+
"workflow-dangerous-trigger",
|
|
24495
|
+
"mcp-credential-risk"
|
|
24496
|
+
].includes(finding.ruleId));
|
|
24497
|
+
if (needsVerificationEvidence && !summary.verificationEvidence) {
|
|
24498
|
+
addDeduction("missing-verification", 20, "No test or manual verification evidence was found.");
|
|
24499
|
+
}
|
|
24500
|
+
if ((summary.sensitiveFilesChanged > 0 || summary.filesChanged >= 5) && !summary.reproductionEvidence) {
|
|
24501
|
+
addDeduction("missing-reproduction-context", 15, "No reproduction, before/after, or expected/actual context was found.");
|
|
24502
|
+
}
|
|
24503
|
+
for (const finding of findings) {
|
|
24504
|
+
if (finding.ruleId.startsWith("secret-detected")) {
|
|
24505
|
+
addDeduction("secret-detected", 40, "Possible committed secret detected.");
|
|
24506
|
+
}
|
|
24507
|
+
else if (finding.ruleId === "workflow-permission-change") {
|
|
24508
|
+
addDeduction("workflow-permission-change", 25, "Workflow permission changes need deliberate review.");
|
|
24509
|
+
}
|
|
24510
|
+
else if (finding.ruleId === "workflow-dangerous-trigger") {
|
|
24511
|
+
addDeduction("workflow-dangerous-trigger", 30, "pull_request_target workflows need privileged trigger review.");
|
|
24512
|
+
}
|
|
24513
|
+
else if (finding.ruleId === "mcp-credential-risk") {
|
|
24514
|
+
addDeduction("mcp-credential-risk", 25, "MCP configuration expands local execution or credential risk.");
|
|
24515
|
+
}
|
|
24516
|
+
else if (finding.ruleId === "change-size") {
|
|
24517
|
+
addDeduction(finding.severity === "high" ? "large-review-surface" : "broad-review-surface", finding.severity === "high" ? 20 : 10, finding.severity === "high"
|
|
24518
|
+
? "The PR is large enough that normal review is likely unreliable."
|
|
24519
|
+
: "The PR has a broad review surface.");
|
|
24520
|
+
}
|
|
24521
|
+
else if (finding.ruleId === "sensitive-path") {
|
|
24522
|
+
addDeduction(`sensitive-path-${finding.severity}`, finding.severity === "high" ? 20 : 10, "Sensitive files changed and need focused review.");
|
|
24523
|
+
}
|
|
24524
|
+
else if (finding.ruleId === "dependency-added") {
|
|
24525
|
+
addDeduction("dependency-change", 10, "Dependency manifest changed.");
|
|
24526
|
+
}
|
|
24527
|
+
else if (finding.ruleId === "dependency-major-upgrade") {
|
|
24528
|
+
addDeduction("dependency-major-upgrade", 15, "Dependency major version changed.");
|
|
24529
|
+
}
|
|
24530
|
+
else if (finding.ruleId === "dependency-lifecycle-script") {
|
|
24531
|
+
addDeduction("dependency-lifecycle-script", 25, "Package lifecycle scripts can run during install or publish.");
|
|
24532
|
+
}
|
|
24533
|
+
else if (finding.ruleId === "missing-tests") {
|
|
24534
|
+
addDeduction("missing-tests", finding.severity === "medium" ? 20 : 12, "Code changed without test changes or verification notes.");
|
|
24535
|
+
}
|
|
24536
|
+
}
|
|
24537
|
+
const strengths = collectEvidenceStrengths(summary);
|
|
24538
|
+
const value = clampScore(100 - [...deductions.values()].reduce((sum, item) => sum + item.points, 0));
|
|
24539
|
+
return {
|
|
24540
|
+
value,
|
|
24541
|
+
grade: gradeEvidenceScore(value),
|
|
24542
|
+
strengths,
|
|
24543
|
+
deductions: [...deductions.entries()].map(([reasonId, item]) => ({
|
|
24544
|
+
reasonId,
|
|
24545
|
+
message: item.message,
|
|
24546
|
+
points: item.points
|
|
24547
|
+
}))
|
|
24548
|
+
};
|
|
24549
|
+
}
|
|
24550
|
+
function collectEvidenceStrengths(summary) {
|
|
24551
|
+
const strengths = [];
|
|
24552
|
+
if (summary.pullRequestDescription === "present") {
|
|
24553
|
+
strengths.push("PR description provides review context.");
|
|
24554
|
+
}
|
|
24555
|
+
if (summary.verificationEvidence) {
|
|
24556
|
+
strengths.push("Verification evidence was found.");
|
|
24557
|
+
}
|
|
24558
|
+
if (summary.reproductionEvidence) {
|
|
24559
|
+
strengths.push("Reproduction or before/after context was found.");
|
|
24560
|
+
}
|
|
24561
|
+
if (summary.testFilesChanged > 0) {
|
|
24562
|
+
strengths.push("Test files changed with the PR.");
|
|
24563
|
+
}
|
|
24564
|
+
if (summary.filesChanged > 0 && summary.sensitiveFilesChanged === 0) {
|
|
24565
|
+
strengths.push("No configured sensitive files changed.");
|
|
24566
|
+
}
|
|
24567
|
+
return strengths;
|
|
24568
|
+
}
|
|
24569
|
+
function gradeEvidenceScore(value) {
|
|
24570
|
+
if (value >= 85) {
|
|
24571
|
+
return "strong";
|
|
24572
|
+
}
|
|
24573
|
+
if (value >= 70) {
|
|
24574
|
+
return "adequate";
|
|
24575
|
+
}
|
|
24576
|
+
if (value >= 50) {
|
|
24577
|
+
return "thin";
|
|
24578
|
+
}
|
|
24579
|
+
return "risky";
|
|
24580
|
+
}
|
|
24581
|
+
function calculateReviewDecision(risk, evidenceScore, findings) {
|
|
24582
|
+
const hasBlockingSecurityFinding = findings.some((finding) => finding.ruleId.startsWith("secret-detected") ||
|
|
24583
|
+
finding.ruleId === "workflow-permission-change" ||
|
|
24584
|
+
finding.ruleId === "workflow-dangerous-trigger" ||
|
|
24585
|
+
finding.ruleId === "dependency-lifecycle-script" ||
|
|
24586
|
+
finding.ruleId === "mcp-credential-risk");
|
|
24587
|
+
if (hasBlockingSecurityFinding || evidenceScore.value < 50 || risk === "high") {
|
|
24588
|
+
return "block-merge";
|
|
24589
|
+
}
|
|
24590
|
+
if (evidenceScore.value < 70 ||
|
|
24591
|
+
findings.some((finding) => finding.ruleId === "missing-tests" || finding.ruleId === "thin-pr-description")) {
|
|
24592
|
+
return "needs-evidence";
|
|
24593
|
+
}
|
|
24594
|
+
if (risk === "medium") {
|
|
24595
|
+
return "review-carefully";
|
|
24596
|
+
}
|
|
24597
|
+
return "ready";
|
|
24598
|
+
}
|
|
24599
|
+
function clampScore(value) {
|
|
24600
|
+
return Math.max(0, Math.min(100, value));
|
|
24601
|
+
}
|
|
24602
|
+
function buildReviewPlan(reviewDecision, findings, evidenceScore) {
|
|
24603
|
+
const actionItems = dedupeReviewActions([
|
|
24604
|
+
...reviewDecisionActions(reviewDecision),
|
|
24605
|
+
...evidenceScoreActions(evidenceScore),
|
|
24606
|
+
...findings.flatMap((finding) => reviewActionsForFinding(finding))
|
|
24607
|
+
]);
|
|
24608
|
+
const focusFiles = dedupeFocusFiles(findings
|
|
24609
|
+
.filter((finding) => finding.path)
|
|
24610
|
+
.map((finding) => ({
|
|
24611
|
+
path: finding.path,
|
|
24612
|
+
reasonId: finding.ruleId,
|
|
24613
|
+
reason: finding.title,
|
|
24614
|
+
priority: finding.severity === "high" ? "high" : finding.severity === "medium" ? "medium" : "low"
|
|
24615
|
+
})));
|
|
24616
|
+
return {
|
|
24617
|
+
actionItems: actionItems.slice(0, 8),
|
|
24618
|
+
focusFiles: focusFiles.slice(0, 8)
|
|
24619
|
+
};
|
|
24620
|
+
}
|
|
24621
|
+
function reviewDecisionActions(reviewDecision) {
|
|
24622
|
+
if (reviewDecision === "block-merge") {
|
|
24623
|
+
return [
|
|
24624
|
+
{
|
|
24625
|
+
actionId: "block-merge-until-resolved",
|
|
24626
|
+
title: "Block merge until the flagged risks are handled.",
|
|
24627
|
+
detail: "Treat this PR as not ready for merge until the high-risk findings are explained, reduced, or removed.",
|
|
24628
|
+
priority: "high",
|
|
24629
|
+
relatedRuleIds: []
|
|
24630
|
+
}
|
|
24631
|
+
];
|
|
24632
|
+
}
|
|
24633
|
+
if (reviewDecision === "needs-evidence") {
|
|
24634
|
+
return [
|
|
24635
|
+
{
|
|
24636
|
+
actionId: "ask-for-evidence-before-review",
|
|
24637
|
+
title: "Ask for missing evidence before deep review.",
|
|
24638
|
+
detail: "Request tests, screenshots, reproduction steps, or a clearer PR description before spending detailed review time.",
|
|
24639
|
+
priority: "medium",
|
|
24640
|
+
relatedRuleIds: []
|
|
24641
|
+
}
|
|
24642
|
+
];
|
|
24643
|
+
}
|
|
24644
|
+
if (reviewDecision === "review-carefully") {
|
|
24645
|
+
return [
|
|
24646
|
+
{
|
|
24647
|
+
actionId: "review-with-focus",
|
|
24648
|
+
title: "Review with a focused checklist.",
|
|
24649
|
+
detail: "Use the findings and focus files below as the first-pass review map.",
|
|
24650
|
+
priority: "medium",
|
|
24651
|
+
relatedRuleIds: []
|
|
24652
|
+
}
|
|
24653
|
+
];
|
|
24654
|
+
}
|
|
24655
|
+
return [
|
|
24656
|
+
{
|
|
24657
|
+
actionId: "normal-review",
|
|
24658
|
+
title: "Proceed with normal review.",
|
|
24659
|
+
detail: "The PR has enough evidence for a standard maintainer review pass.",
|
|
24660
|
+
priority: "low",
|
|
24661
|
+
relatedRuleIds: []
|
|
24662
|
+
}
|
|
24663
|
+
];
|
|
24664
|
+
}
|
|
24665
|
+
function evidenceScoreActions(evidenceScore) {
|
|
24666
|
+
return evidenceScore.deductions.flatMap((deduction) => {
|
|
24667
|
+
if (deduction.reasonId === "missing-pr-description" ||
|
|
24668
|
+
deduction.reasonId === "thin-pr-description" ||
|
|
24669
|
+
deduction.reasonId === "no-pr-context") {
|
|
24670
|
+
return [
|
|
24671
|
+
{
|
|
24672
|
+
actionId: "improve-pr-description",
|
|
24673
|
+
title: "Ask for a clearer PR description.",
|
|
24674
|
+
detail: "The contributor should explain why the change is needed, what changed, how it was verified, and any rollout or compatibility risk.",
|
|
24675
|
+
priority: "medium",
|
|
24676
|
+
relatedRuleIds: ["thin-pr-description"]
|
|
24677
|
+
}
|
|
24678
|
+
];
|
|
24679
|
+
}
|
|
24680
|
+
if (deduction.reasonId === "missing-verification" || deduction.reasonId === "missing-tests") {
|
|
24681
|
+
return [
|
|
24682
|
+
{
|
|
24683
|
+
actionId: "add-verification-evidence",
|
|
24684
|
+
title: "Ask for test or manual verification evidence.",
|
|
24685
|
+
detail: "Require test output, CI links, screenshots, or a short manual verification note before approving.",
|
|
24686
|
+
priority: "medium",
|
|
24687
|
+
relatedRuleIds: ["missing-tests"]
|
|
24688
|
+
}
|
|
24689
|
+
];
|
|
24690
|
+
}
|
|
24691
|
+
if (deduction.reasonId === "missing-reproduction-context") {
|
|
24692
|
+
return [
|
|
24693
|
+
{
|
|
24694
|
+
actionId: "add-reproduction-context",
|
|
24695
|
+
title: "Ask for reproduction or before/after context.",
|
|
24696
|
+
detail: "The PR should include steps to reproduce, expected and actual behavior, or before/after screenshots where relevant.",
|
|
24697
|
+
priority: "medium",
|
|
24698
|
+
relatedRuleIds: ["missing-reproduction-context"]
|
|
24699
|
+
}
|
|
24700
|
+
];
|
|
24701
|
+
}
|
|
24702
|
+
return [];
|
|
24703
|
+
});
|
|
24704
|
+
}
|
|
24705
|
+
function reviewActionsForFinding(finding) {
|
|
24706
|
+
if (finding.ruleId.startsWith("secret-detected")) {
|
|
24707
|
+
return [
|
|
24708
|
+
{
|
|
24709
|
+
actionId: "rotate-secret",
|
|
24710
|
+
title: "Rotate and remove the exposed credential.",
|
|
24711
|
+
detail: "Do not merge until the secret is removed from the PR and any exposed value has been rotated.",
|
|
24712
|
+
priority: "high",
|
|
24713
|
+
relatedRuleIds: [finding.ruleId]
|
|
24714
|
+
}
|
|
24715
|
+
];
|
|
24716
|
+
}
|
|
24717
|
+
if (finding.ruleId === "workflow-permission-change") {
|
|
24718
|
+
return [
|
|
24719
|
+
{
|
|
24720
|
+
actionId: "justify-workflow-permissions",
|
|
24721
|
+
title: "Require a least-privilege explanation for workflow permissions.",
|
|
24722
|
+
detail: "Confirm whether write permissions or OIDC are necessary and whether untrusted PRs can reach this workflow.",
|
|
24723
|
+
priority: "high",
|
|
24724
|
+
relatedRuleIds: [finding.ruleId]
|
|
24725
|
+
}
|
|
24726
|
+
];
|
|
24727
|
+
}
|
|
24728
|
+
if (finding.ruleId === "workflow-dangerous-trigger") {
|
|
24729
|
+
return [
|
|
24730
|
+
{
|
|
24731
|
+
actionId: "review-privileged-pr-trigger",
|
|
24732
|
+
title: "Review privileged pull_request_target usage.",
|
|
24733
|
+
detail: "Confirm the workflow does not execute untrusted PR code with write tokens, secrets, or repository permissions.",
|
|
24734
|
+
priority: "high",
|
|
24735
|
+
relatedRuleIds: [finding.ruleId]
|
|
24736
|
+
}
|
|
24737
|
+
];
|
|
24738
|
+
}
|
|
24739
|
+
if (finding.ruleId === "dependency-lifecycle-script") {
|
|
24740
|
+
return [
|
|
24741
|
+
{
|
|
24742
|
+
actionId: "review-package-lifecycle-script",
|
|
24743
|
+
title: "Review package lifecycle scripts before merge.",
|
|
24744
|
+
detail: "Check whether install, postinstall, prepare, or publish scripts can execute unexpected code for contributors or consumers.",
|
|
24745
|
+
priority: "high",
|
|
24746
|
+
relatedRuleIds: [finding.ruleId]
|
|
24747
|
+
}
|
|
24748
|
+
];
|
|
24749
|
+
}
|
|
24750
|
+
if (finding.ruleId === "mcp-credential-risk") {
|
|
24751
|
+
return [
|
|
24752
|
+
{
|
|
24753
|
+
actionId: "review-mcp-execution-surface",
|
|
24754
|
+
title: "Review MCP commands, args, and credential handling.",
|
|
24755
|
+
detail: "Check that MCP config does not commit secrets and does not unexpectedly expand local execution surface.",
|
|
24756
|
+
priority: "high",
|
|
24757
|
+
relatedRuleIds: [finding.ruleId]
|
|
24758
|
+
}
|
|
24759
|
+
];
|
|
24760
|
+
}
|
|
24761
|
+
if (finding.ruleId === "change-size") {
|
|
24762
|
+
return [
|
|
24763
|
+
{
|
|
24764
|
+
actionId: "request-review-map-or-split",
|
|
24765
|
+
title: "Request a smaller PR or a file-by-file review map.",
|
|
24766
|
+
detail: "Ask the contributor to split unrelated changes or identify the files that need the closest review.",
|
|
24767
|
+
priority: finding.severity === "high" ? "high" : "medium",
|
|
24768
|
+
relatedRuleIds: [finding.ruleId]
|
|
24769
|
+
}
|
|
24770
|
+
];
|
|
24771
|
+
}
|
|
24772
|
+
if (finding.ruleId === "dependency-added") {
|
|
24773
|
+
return [
|
|
24774
|
+
{
|
|
24775
|
+
actionId: "verify-dependency-change",
|
|
24776
|
+
title: "Verify dependency provenance and lockfile impact.",
|
|
24777
|
+
detail: "Check package name, maintainer, license, install scripts, and whether the lockfile matches the intended dependency change.",
|
|
24778
|
+
priority: "medium",
|
|
24779
|
+
relatedRuleIds: [finding.ruleId]
|
|
24780
|
+
}
|
|
24781
|
+
];
|
|
24782
|
+
}
|
|
24783
|
+
if (finding.ruleId === "dependency-major-upgrade") {
|
|
24784
|
+
return [
|
|
24785
|
+
{
|
|
24786
|
+
actionId: "review-major-dependency-upgrade",
|
|
24787
|
+
title: "Review major dependency upgrade impact.",
|
|
24788
|
+
detail: "Check changelogs, migration notes, peer dependencies, and whether tests cover the upgraded surface.",
|
|
24789
|
+
priority: "medium",
|
|
24790
|
+
relatedRuleIds: [finding.ruleId]
|
|
24791
|
+
}
|
|
24792
|
+
];
|
|
24793
|
+
}
|
|
24794
|
+
if (finding.ruleId === "sensitive-path") {
|
|
24795
|
+
return [
|
|
24796
|
+
{
|
|
24797
|
+
actionId: "assign-sensitive-file-review",
|
|
24798
|
+
title: "Assign focused review for sensitive files.",
|
|
24799
|
+
detail: "Have a maintainer deliberately inspect the sensitive file changes before approval.",
|
|
24800
|
+
priority: finding.severity === "high" ? "high" : "medium",
|
|
24801
|
+
relatedRuleIds: [finding.ruleId]
|
|
24802
|
+
}
|
|
24803
|
+
];
|
|
24804
|
+
}
|
|
24805
|
+
return [];
|
|
24806
|
+
}
|
|
24807
|
+
function dedupeReviewActions(actions) {
|
|
24808
|
+
const seen = new Set();
|
|
24809
|
+
const unique = [];
|
|
24810
|
+
for (const action of actions.sort((left, right) => priorityRank(right.priority) - priorityRank(left.priority))) {
|
|
24811
|
+
if (seen.has(action.actionId)) {
|
|
24812
|
+
continue;
|
|
24813
|
+
}
|
|
24814
|
+
seen.add(action.actionId);
|
|
24815
|
+
unique.push(action);
|
|
24816
|
+
}
|
|
24817
|
+
return unique;
|
|
24818
|
+
}
|
|
24819
|
+
function dedupeFocusFiles(files) {
|
|
24820
|
+
const seen = new Set();
|
|
24821
|
+
const unique = [];
|
|
24822
|
+
for (const file of files.sort((left, right) => priorityRank(right.priority) - priorityRank(left.priority))) {
|
|
24823
|
+
if (seen.has(file.path)) {
|
|
24824
|
+
continue;
|
|
24825
|
+
}
|
|
24826
|
+
seen.add(file.path);
|
|
24827
|
+
unique.push(file);
|
|
24828
|
+
}
|
|
24829
|
+
return unique;
|
|
24830
|
+
}
|
|
24831
|
+
function priorityRank(priority) {
|
|
24832
|
+
return { low: 1, medium: 2, high: 3 }[priority];
|
|
24833
|
+
}
|
|
24036
24834
|
function dedupeFindings(findings) {
|
|
24037
24835
|
const seen = new Set();
|
|
24038
24836
|
const unique = [];
|
|
@@ -24066,7 +24864,7 @@ const build_program = new Command();
|
|
|
24066
24864
|
build_program
|
|
24067
24865
|
.name("proof-pr")
|
|
24068
24866
|
.description("Review pull request evidence, scope, and safety before maintainers spend time on it.")
|
|
24069
|
-
.version("0.1.
|
|
24867
|
+
.version("0.1.6");
|
|
24070
24868
|
build_program
|
|
24071
24869
|
.command("scan", { isDefault: true })
|
|
24072
24870
|
.description("Scan a git diff and print a ProofPR report.")
|
|
@@ -24102,10 +24900,11 @@ build_program
|
|
|
24102
24900
|
.description("Create a starter .proofpr.yml and GitHub Actions workflow.")
|
|
24103
24901
|
.option("--config-path <path>", "Path to write the ProofPR configuration file.", ".proofpr.yml")
|
|
24104
24902
|
.option("--workflow-path <path>", "Path to write the GitHub Actions workflow.", ".github/workflows/proofpr.yml")
|
|
24903
|
+
.option("--preset <preset>", `Config preset: ${listConfigPresets().join(", ")}.`, parsePresetOption, "open-source-maintainer")
|
|
24105
24904
|
.option("--fail-on <level>", "Workflow failure threshold: low, medium, high, or never.", parseFailLevel, "high")
|
|
24106
24905
|
.option("--force", "Overwrite existing files.", false)
|
|
24107
24906
|
.action(async (options) => {
|
|
24108
|
-
await writeIfMissing(options.configPath, renderConfigTemplate(), options.force);
|
|
24907
|
+
await writeIfMissing(options.configPath, renderConfigTemplate(options.preset), options.force);
|
|
24109
24908
|
await writeIfMissing(options.workflowPath, renderWorkflowTemplate(options.failOn), options.force);
|
|
24110
24909
|
process.stdout.write(`ProofPR initialized:\n- ${options.configPath}\n- ${options.workflowPath}\n`);
|
|
24111
24910
|
});
|
|
@@ -24144,42 +24943,56 @@ async function pathExists(path) {
|
|
|
24144
24943
|
return false;
|
|
24145
24944
|
}
|
|
24146
24945
|
}
|
|
24147
|
-
function renderConfigTemplate() {
|
|
24946
|
+
function renderConfigTemplate(preset) {
|
|
24148
24947
|
return `locale: zh-CN
|
|
24149
|
-
|
|
24150
|
-
riskThreshold: high
|
|
24151
|
-
|
|
24152
|
-
sensitivePaths:
|
|
24153
|
-
- ".github/workflows/**"
|
|
24154
|
-
- ".github/actions/**"
|
|
24155
|
-
- "**/.env*"
|
|
24156
|
-
- "**/mcp*.json"
|
|
24157
|
-
- "**/*mcp*.json"
|
|
24158
|
-
- "Dockerfile"
|
|
24159
|
-
- "**/Dockerfile"
|
|
24160
|
-
- "package.json"
|
|
24161
|
-
- "pnpm-lock.yaml"
|
|
24162
|
-
- "package-lock.json"
|
|
24163
|
-
- "yarn.lock"
|
|
24164
|
-
- "bun.lockb"
|
|
24165
|
-
|
|
24166
|
-
requireTests:
|
|
24167
|
-
enabled: true
|
|
24168
|
-
paths:
|
|
24169
|
-
- "src/**"
|
|
24170
|
-
- "packages/**/src/**"
|
|
24171
|
-
- "app/**"
|
|
24172
|
-
- "lib/**"
|
|
24173
|
-
|
|
24174
|
-
secrets:
|
|
24175
|
-
enabled: true
|
|
24176
|
-
|
|
24177
|
-
dependencies:
|
|
24178
|
-
flagNewPackages: true
|
|
24179
|
-
flagMajorUpgrades: true
|
|
24948
|
+
preset: ${preset}
|
|
24180
24949
|
|
|
24181
24950
|
comment:
|
|
24182
24951
|
enabled: true
|
|
24952
|
+
|
|
24953
|
+
# 如需更严格或更宽松,可以先换 preset:
|
|
24954
|
+
# preset: security-strict
|
|
24955
|
+
#
|
|
24956
|
+
# 可用预设:
|
|
24957
|
+
# - balanced
|
|
24958
|
+
# - open-source-maintainer
|
|
24959
|
+
# - security-strict
|
|
24960
|
+
# - ai-generated-pr
|
|
24961
|
+
# - mcp-security
|
|
24962
|
+
# - dependency-careful
|
|
24963
|
+
#
|
|
24964
|
+
# 也可以取消注释下面这些字段,覆盖 preset 的默认值。
|
|
24965
|
+
# riskThreshold: high
|
|
24966
|
+
#
|
|
24967
|
+
# sensitivePaths:
|
|
24968
|
+
# - ".github/workflows/**"
|
|
24969
|
+
# - ".github/actions/**"
|
|
24970
|
+
# - "**/.env*"
|
|
24971
|
+
# - "**/mcp*.json"
|
|
24972
|
+
# - "**/*mcp*.json"
|
|
24973
|
+
# - "Dockerfile"
|
|
24974
|
+
# - "**/Dockerfile"
|
|
24975
|
+
# - "package.json"
|
|
24976
|
+
# - "pnpm-lock.yaml"
|
|
24977
|
+
# - "package-lock.json"
|
|
24978
|
+
# - "yarn.lock"
|
|
24979
|
+
# - "bun.lockb"
|
|
24980
|
+
#
|
|
24981
|
+
# requireTests:
|
|
24982
|
+
# enabled: true
|
|
24983
|
+
# paths:
|
|
24984
|
+
# - "src/**"
|
|
24985
|
+
# - "packages/**/src/**"
|
|
24986
|
+
# - "app/**"
|
|
24987
|
+
# - "lib/**"
|
|
24988
|
+
#
|
|
24989
|
+
# secrets:
|
|
24990
|
+
# enabled: true
|
|
24991
|
+
#
|
|
24992
|
+
# dependencies:
|
|
24993
|
+
# flagNewPackages: true
|
|
24994
|
+
# flagMajorUpgrades: true
|
|
24995
|
+
# flagLifecycleScripts: true
|
|
24183
24996
|
`;
|
|
24184
24997
|
}
|
|
24185
24998
|
function renderWorkflowTemplate(failOn) {
|
|
@@ -24198,10 +25011,11 @@ jobs:
|
|
|
24198
25011
|
runs-on: ubuntu-latest
|
|
24199
25012
|
steps:
|
|
24200
25013
|
- uses: actions/checkout@v4
|
|
24201
|
-
- uses: linsk27/proof-pr@v0.1.
|
|
25014
|
+
- uses: linsk27/proof-pr@v0.1.6
|
|
24202
25015
|
with:
|
|
24203
25016
|
fail-on: ${failOn}
|
|
24204
25017
|
comment: "true"
|
|
25018
|
+
annotations: "true"
|
|
24205
25019
|
`;
|
|
24206
25020
|
}
|
|
24207
25021
|
function renderOutput(result, format, locale) {
|
|
@@ -24225,4 +25039,11 @@ function parseFailLevel(value) {
|
|
|
24225
25039
|
}
|
|
24226
25040
|
throw new InvalidArgumentError("fail-on must be one of: low, medium, high, never");
|
|
24227
25041
|
}
|
|
25042
|
+
function parsePresetOption(value) {
|
|
25043
|
+
const preset = parsePreset(value);
|
|
25044
|
+
if (preset === value) {
|
|
25045
|
+
return preset;
|
|
25046
|
+
}
|
|
25047
|
+
throw new InvalidArgumentError(`preset must be one of: ${listConfigPresets().join(", ")}`);
|
|
25048
|
+
}
|
|
24228
25049
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proof-pr",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "CLI for ProofPR, a maintainer-focused pull request evidence scanner.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"secrets-scanning",
|
|
28
28
|
"github-actions-security",
|
|
29
29
|
"dependency-review",
|
|
30
|
+
"sarif",
|
|
31
|
+
"code-scanning",
|
|
30
32
|
"ai-coding",
|
|
31
33
|
"ai-generated-code",
|
|
32
34
|
"mcp",
|
package/dist/index.d.ts
DELETED
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|