proof-pr 0.1.2 → 0.1.5
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 -5
- package/dist/index.js +939 -115
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -2,24 +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 文件。
|
|
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 --
|
|
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
|
|
15
24
|
```
|
|
16
25
|
|
|
26
|
+
可用预设:`balanced`、`open-source-maintainer`、`security-strict`、`ai-generated-pr`、`mcp-security`、`dependency-careful`。
|
|
27
|
+
|
|
17
28
|
## GitHub Action
|
|
18
29
|
|
|
19
30
|
```yaml
|
|
20
|
-
- uses: linsk27/proof-pr@v0.1.
|
|
31
|
+
- uses: linsk27/proof-pr@v0.1.5
|
|
21
32
|
with:
|
|
22
33
|
fail-on: high
|
|
34
|
+
comment: "true"
|
|
35
|
+
annotations: "true"
|
|
23
36
|
```
|
|
24
37
|
|
|
25
38
|
完整文档见仓库 README:
|
package/dist/index.js
CHANGED
|
@@ -23111,36 +23111,126 @@ function preprocess(fn, schema) {
|
|
|
23111
23111
|
|
|
23112
23112
|
|
|
23113
23113
|
const riskLevelSchema = schemas_enum(["low", "medium", "high"]);
|
|
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
|
+
};
|
|
23114
23223
|
const configSchema = object({
|
|
23224
|
+
preset: configPresetSchema.default("balanced"),
|
|
23225
|
+
locale: localeSchema.default("en"),
|
|
23115
23226
|
riskThreshold: riskLevelSchema.default("high"),
|
|
23116
23227
|
ignorePaths: array(schemas_string()).default([]),
|
|
23117
|
-
sensitivePaths: array(schemas_string())
|
|
23118
|
-
.default([
|
|
23119
|
-
".github/workflows/**",
|
|
23120
|
-
".github/actions/**",
|
|
23121
|
-
"**/.env*",
|
|
23122
|
-
"**/mcp*.json",
|
|
23123
|
-
"**/*mcp*.json",
|
|
23124
|
-
"Dockerfile",
|
|
23125
|
-
"**/Dockerfile",
|
|
23126
|
-
"package.json",
|
|
23127
|
-
"pnpm-lock.yaml",
|
|
23128
|
-
"package-lock.json",
|
|
23129
|
-
"yarn.lock",
|
|
23130
|
-
"bun.lockb",
|
|
23131
|
-
"requirements.txt",
|
|
23132
|
-
"pyproject.toml",
|
|
23133
|
-
"Cargo.toml",
|
|
23134
|
-
"Cargo.lock",
|
|
23135
|
-
"go.mod",
|
|
23136
|
-
"go.sum"
|
|
23137
|
-
]),
|
|
23228
|
+
sensitivePaths: array(schemas_string()).default(DEFAULT_SENSITIVE_PATHS),
|
|
23138
23229
|
requireTests: object({
|
|
23139
23230
|
enabled: schemas_boolean().default(true),
|
|
23140
|
-
paths: array(schemas_string())
|
|
23141
|
-
.default(["src/**", "packages/**/src/**", "app/**", "lib/**"])
|
|
23231
|
+
paths: array(schemas_string()).default(DEFAULT_TEST_PATHS)
|
|
23142
23232
|
})
|
|
23143
|
-
.default({ enabled: true, paths:
|
|
23233
|
+
.default({ enabled: true, paths: DEFAULT_TEST_PATHS }),
|
|
23144
23234
|
secrets: object({ enabled: schemas_boolean().default(true) }).default({ enabled: true }),
|
|
23145
23235
|
dependencies: object({
|
|
23146
23236
|
flagNewPackages: schemas_boolean().default(true),
|
|
@@ -23150,7 +23240,9 @@ const configSchema = object({
|
|
|
23150
23240
|
comment: object({ enabled: schemas_boolean().default(true) }).default({ enabled: true })
|
|
23151
23241
|
});
|
|
23152
23242
|
function parseConfig(input) {
|
|
23153
|
-
|
|
23243
|
+
const raw = isRecord(input) ? input : {};
|
|
23244
|
+
const preset = configPresetSchema.parse(raw.preset ?? "balanced");
|
|
23245
|
+
return configSchema.parse(deepMerge(PRESET_DEFAULTS[preset], raw, { preset }));
|
|
23154
23246
|
}
|
|
23155
23247
|
async function loadConfig(path) {
|
|
23156
23248
|
try {
|
|
@@ -23174,6 +23266,37 @@ function riskMeetsThreshold(risk, threshold) {
|
|
|
23174
23266
|
function riskRank(risk) {
|
|
23175
23267
|
return { low: 1, medium: 2, high: 3 }[risk];
|
|
23176
23268
|
}
|
|
23269
|
+
function parseLocale(value, fallback = "en") {
|
|
23270
|
+
const result = localeSchema.safeParse(value);
|
|
23271
|
+
return result.success ? result.data : fallback;
|
|
23272
|
+
}
|
|
23273
|
+
function parsePreset(value, fallback = "balanced") {
|
|
23274
|
+
const result = configPresetSchema.safeParse(value);
|
|
23275
|
+
return result.success ? result.data : fallback;
|
|
23276
|
+
}
|
|
23277
|
+
function listConfigPresets() {
|
|
23278
|
+
return CONFIG_PRESETS;
|
|
23279
|
+
}
|
|
23280
|
+
function deepMerge(...items) {
|
|
23281
|
+
const output = {};
|
|
23282
|
+
for (const item of items) {
|
|
23283
|
+
if (!isRecord(item)) {
|
|
23284
|
+
continue;
|
|
23285
|
+
}
|
|
23286
|
+
for (const [key, value] of Object.entries(item)) {
|
|
23287
|
+
if (isRecord(value) && isRecord(output[key])) {
|
|
23288
|
+
output[key] = deepMerge(output[key], value);
|
|
23289
|
+
}
|
|
23290
|
+
else {
|
|
23291
|
+
output[key] = value;
|
|
23292
|
+
}
|
|
23293
|
+
}
|
|
23294
|
+
}
|
|
23295
|
+
return output;
|
|
23296
|
+
}
|
|
23297
|
+
function isRecord(value) {
|
|
23298
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23299
|
+
}
|
|
23177
23300
|
function isMissingFileError(error) {
|
|
23178
23301
|
return (typeof error === "object" &&
|
|
23179
23302
|
error !== null &&
|
|
@@ -23183,35 +23306,11 @@ function isMissingFileError(error) {
|
|
|
23183
23306
|
//# sourceMappingURL=config.js.map
|
|
23184
23307
|
;// CONCATENATED MODULE: ../core/dist/reporters.js
|
|
23185
23308
|
const REPORT_MARKER = "<!-- proof-pr-report -->";
|
|
23186
|
-
function renderMarkdownReport(result) {
|
|
23187
|
-
|
|
23188
|
-
|
|
23189
|
-
"# ProofPR Review",
|
|
23190
|
-
"",
|
|
23191
|
-
`Risk: **${result.risk}**`,
|
|
23192
|
-
"",
|
|
23193
|
-
"## Evidence",
|
|
23194
|
-
"",
|
|
23195
|
-
`- Files changed: ${result.summary.filesChanged}`,
|
|
23196
|
-
`- Additions: ${result.summary.additions}`,
|
|
23197
|
-
`- Deletions: ${result.summary.deletions}`,
|
|
23198
|
-
`- Test files changed: ${result.summary.testFilesChanged}`,
|
|
23199
|
-
`- Sensitive files changed: ${result.summary.sensitiveFilesChanged}`,
|
|
23200
|
-
`- PR description: ${result.summary.pullRequestDescription}`,
|
|
23201
|
-
`- Verification evidence: ${formatBoolean(result.summary.verificationEvidence)}`,
|
|
23202
|
-
`- Reproduction context: ${formatBoolean(result.summary.reproductionEvidence)}`,
|
|
23203
|
-
""
|
|
23204
|
-
];
|
|
23205
|
-
if (result.findings.length === 0) {
|
|
23206
|
-
lines.push("## Findings", "", "No review-risk findings detected by the enabled rules.", "");
|
|
23207
|
-
return lines.join("\n");
|
|
23309
|
+
function renderMarkdownReport(result, locale = "en") {
|
|
23310
|
+
if (locale === "zh-CN") {
|
|
23311
|
+
return renderChineseMarkdownReport(result);
|
|
23208
23312
|
}
|
|
23209
|
-
|
|
23210
|
-
for (const finding of result.findings) {
|
|
23211
|
-
lines.push(formatFinding(finding), "");
|
|
23212
|
-
}
|
|
23213
|
-
lines.push("## Maintainer Focus", "", ...maintainerFocus(result.findings).map((item) => `- ${item}`), "");
|
|
23214
|
-
return lines.join("\n");
|
|
23313
|
+
return renderEnglishMarkdownReport(result);
|
|
23215
23314
|
}
|
|
23216
23315
|
function getReportMarker() {
|
|
23217
23316
|
return REPORT_MARKER;
|
|
@@ -23234,7 +23333,7 @@ function renderSarifReport(result) {
|
|
|
23234
23333
|
tool: {
|
|
23235
23334
|
driver: {
|
|
23236
23335
|
name: "ProofPR",
|
|
23237
|
-
informationUri: "https://github.com/
|
|
23336
|
+
informationUri: "https://github.com/linsk27/proof-pr",
|
|
23238
23337
|
rules: [...rules.values()]
|
|
23239
23338
|
}
|
|
23240
23339
|
},
|
|
@@ -23256,7 +23355,121 @@ function renderSarifReport(result) {
|
|
|
23256
23355
|
]
|
|
23257
23356
|
}, null, 2);
|
|
23258
23357
|
}
|
|
23259
|
-
function
|
|
23358
|
+
function renderEnglishMarkdownReport(result) {
|
|
23359
|
+
const lines = [
|
|
23360
|
+
REPORT_MARKER,
|
|
23361
|
+
"# ProofPR Review",
|
|
23362
|
+
"",
|
|
23363
|
+
`Risk: **${result.risk}**`,
|
|
23364
|
+
`Evidence score: **${result.evidenceScore.value}/100 (${formatEvidenceGrade(result.evidenceScore.grade, "en")})**`,
|
|
23365
|
+
`Review gate: **${formatReviewDecision(result.reviewDecision, "en")}**`,
|
|
23366
|
+
"",
|
|
23367
|
+
"## Evidence",
|
|
23368
|
+
"",
|
|
23369
|
+
`- Files changed: ${result.summary.filesChanged}`,
|
|
23370
|
+
`- Additions: ${result.summary.additions}`,
|
|
23371
|
+
`- Deletions: ${result.summary.deletions}`,
|
|
23372
|
+
`- Test files changed: ${result.summary.testFilesChanged}`,
|
|
23373
|
+
`- Sensitive files changed: ${result.summary.sensitiveFilesChanged}`,
|
|
23374
|
+
`- PR description: ${result.summary.pullRequestDescription}`,
|
|
23375
|
+
`- Verification evidence: ${formatBoolean(result.summary.verificationEvidence)}`,
|
|
23376
|
+
`- Reproduction context: ${formatBoolean(result.summary.reproductionEvidence)}`,
|
|
23377
|
+
""
|
|
23378
|
+
];
|
|
23379
|
+
appendEvidenceScoreSection(lines, result, "en");
|
|
23380
|
+
appendReviewPlanSection(lines, result, "en");
|
|
23381
|
+
if (result.findings.length === 0) {
|
|
23382
|
+
lines.push("## Findings", "", "No review-risk findings detected by the enabled rules.", "");
|
|
23383
|
+
return lines.join("\n");
|
|
23384
|
+
}
|
|
23385
|
+
lines.push("## Findings", "");
|
|
23386
|
+
for (const finding of result.findings) {
|
|
23387
|
+
lines.push(formatEnglishFinding(finding), "");
|
|
23388
|
+
}
|
|
23389
|
+
lines.push("## Maintainer Focus", "", ...maintainerFocus(result.findings, "en").map((item) => `- ${item}`), "");
|
|
23390
|
+
return lines.join("\n");
|
|
23391
|
+
}
|
|
23392
|
+
function renderChineseMarkdownReport(result) {
|
|
23393
|
+
const lines = [
|
|
23394
|
+
REPORT_MARKER,
|
|
23395
|
+
"# ProofPR 审查报告",
|
|
23396
|
+
"",
|
|
23397
|
+
`风险等级:**${translateRisk(result.risk)}**`,
|
|
23398
|
+
`证据评分:**${result.evidenceScore.value}/100(${formatEvidenceGrade(result.evidenceScore.grade, "zh-CN")})**`,
|
|
23399
|
+
`Review 门禁:**${formatReviewDecision(result.reviewDecision, "zh-CN")}**`,
|
|
23400
|
+
"",
|
|
23401
|
+
"## 证据概览",
|
|
23402
|
+
"",
|
|
23403
|
+
`- 改动文件数:${result.summary.filesChanged}`,
|
|
23404
|
+
`- 新增行数:${result.summary.additions}`,
|
|
23405
|
+
`- 删除行数:${result.summary.deletions}`,
|
|
23406
|
+
`- 测试文件改动数:${result.summary.testFilesChanged}`,
|
|
23407
|
+
`- 敏感文件改动数:${result.summary.sensitiveFilesChanged}`,
|
|
23408
|
+
`- PR 描述质量:${translateDescriptionState(result.summary.pullRequestDescription)}`,
|
|
23409
|
+
`- 验证证据:${formatChineseBoolean(result.summary.verificationEvidence)}`,
|
|
23410
|
+
`- 复现上下文:${formatChineseBoolean(result.summary.reproductionEvidence)}`,
|
|
23411
|
+
""
|
|
23412
|
+
];
|
|
23413
|
+
appendEvidenceScoreSection(lines, result, "zh-CN");
|
|
23414
|
+
appendReviewPlanSection(lines, result, "zh-CN");
|
|
23415
|
+
if (result.findings.length === 0) {
|
|
23416
|
+
lines.push("## 风险发现", "", "启用的规则没有发现需要优先关注的 review 风险。", "");
|
|
23417
|
+
return lines.join("\n");
|
|
23418
|
+
}
|
|
23419
|
+
lines.push("## 风险发现", "");
|
|
23420
|
+
for (const finding of result.findings) {
|
|
23421
|
+
lines.push(formatChineseFinding(finding), "");
|
|
23422
|
+
}
|
|
23423
|
+
lines.push("## 维护者关注点", "", ...maintainerFocus(result.findings, "zh-CN").map((item) => `- ${item}`), "");
|
|
23424
|
+
return lines.join("\n");
|
|
23425
|
+
}
|
|
23426
|
+
function appendEvidenceScoreSection(lines, result, locale) {
|
|
23427
|
+
lines.push(locale === "zh-CN" ? "## 证据评分细节" : "## Evidence Score", "");
|
|
23428
|
+
if (result.evidenceScore.strengths.length > 0) {
|
|
23429
|
+
for (const strength of result.evidenceScore.strengths) {
|
|
23430
|
+
lines.push(locale === "zh-CN"
|
|
23431
|
+
? `- 证据优势:${translateScoreMessage(strength)}`
|
|
23432
|
+
: `- Strength: ${strength}`);
|
|
23433
|
+
}
|
|
23434
|
+
}
|
|
23435
|
+
else {
|
|
23436
|
+
lines.push(locale === "zh-CN" ? "- 证据优势:暂无明显优势信号。" : "- Strength: No strong evidence signals detected.");
|
|
23437
|
+
}
|
|
23438
|
+
if (result.evidenceScore.deductions.length > 0) {
|
|
23439
|
+
for (const deduction of result.evidenceScore.deductions) {
|
|
23440
|
+
lines.push(locale === "zh-CN"
|
|
23441
|
+
? `- 扣分项:-${deduction.points},${translateDeduction(deduction.reasonId, deduction.message)}`
|
|
23442
|
+
: `- Deduction: -${deduction.points}, ${deduction.message}`);
|
|
23443
|
+
}
|
|
23444
|
+
}
|
|
23445
|
+
else {
|
|
23446
|
+
lines.push(locale === "zh-CN" ? "- 扣分项:无。" : "- Deduction: none.");
|
|
23447
|
+
}
|
|
23448
|
+
lines.push("");
|
|
23449
|
+
}
|
|
23450
|
+
function appendReviewPlanSection(lines, result, locale) {
|
|
23451
|
+
lines.push(locale === "zh-CN" ? "## Review 行动清单" : "## Review Plan", "");
|
|
23452
|
+
if (result.reviewPlan.actionItems.length > 0) {
|
|
23453
|
+
for (const action of result.reviewPlan.actionItems) {
|
|
23454
|
+
lines.push(locale === "zh-CN"
|
|
23455
|
+
? `- [ ] ${translateReviewActionTitle(action.actionId, action.title)}(${formatPriority(action.priority, locale)}):${translateReviewActionDetail(action.actionId, action.detail)}`
|
|
23456
|
+
: `- [ ] ${action.title} (${formatPriority(action.priority, locale)}): ${action.detail}`);
|
|
23457
|
+
}
|
|
23458
|
+
}
|
|
23459
|
+
else {
|
|
23460
|
+
lines.push(locale === "zh-CN" ? "- [ ] 没有额外行动项。" : "- [ ] No additional action items.");
|
|
23461
|
+
}
|
|
23462
|
+
if (result.reviewPlan.focusFiles.length > 0) {
|
|
23463
|
+
lines.push("", locale === "zh-CN" ? "重点文件:" : "Focus files:");
|
|
23464
|
+
for (const file of result.reviewPlan.focusFiles) {
|
|
23465
|
+
lines.push(locale === "zh-CN"
|
|
23466
|
+
? `- \`${file.path}\`(${formatPriority(file.priority, locale)}):${translateFocusReason(file.reasonId, file.reason)}`
|
|
23467
|
+
: `- \`${file.path}\` (${formatPriority(file.priority, locale)}): ${file.reason}`);
|
|
23468
|
+
}
|
|
23469
|
+
}
|
|
23470
|
+
lines.push("");
|
|
23471
|
+
}
|
|
23472
|
+
function formatEnglishFinding(finding) {
|
|
23260
23473
|
const lines = [
|
|
23261
23474
|
`### ${finding.title}`,
|
|
23262
23475
|
"",
|
|
@@ -23276,39 +23489,270 @@ function formatFinding(finding) {
|
|
|
23276
23489
|
}
|
|
23277
23490
|
return lines.join("\n");
|
|
23278
23491
|
}
|
|
23279
|
-
function
|
|
23492
|
+
function formatChineseFinding(finding) {
|
|
23493
|
+
const translated = translateFinding(finding);
|
|
23494
|
+
const lines = [
|
|
23495
|
+
`### ${translated.title}`,
|
|
23496
|
+
"",
|
|
23497
|
+
`- 规则:\`${finding.ruleId}\``,
|
|
23498
|
+
`- 严重程度:\`${translateSeverity(finding.severity)}\``,
|
|
23499
|
+
finding.path ? `- 路径:\`${finding.path}\`` : undefined,
|
|
23500
|
+
`- 详情:${translated.message}`
|
|
23501
|
+
].filter((line) => Boolean(line));
|
|
23502
|
+
if (finding.evidence && finding.evidence.length > 0) {
|
|
23503
|
+
lines.push("- 证据:");
|
|
23504
|
+
for (const item of finding.evidence) {
|
|
23505
|
+
lines.push(` - \`${translateEvidence(item)}\``);
|
|
23506
|
+
}
|
|
23507
|
+
}
|
|
23508
|
+
if (translated.recommendation) {
|
|
23509
|
+
lines.push(`- 建议:${translated.recommendation}`);
|
|
23510
|
+
}
|
|
23511
|
+
return lines.join("\n");
|
|
23512
|
+
}
|
|
23513
|
+
function maintainerFocus(findings, locale) {
|
|
23280
23514
|
const focus = new Set();
|
|
23281
23515
|
for (const finding of findings) {
|
|
23282
23516
|
if (finding.ruleId.startsWith("secret-detected")) {
|
|
23283
|
-
focus.add(
|
|
23517
|
+
focus.add(locale === "zh-CN"
|
|
23518
|
+
? "轮换任何可能暴露的凭证,并在移除 secret 前阻止合并。"
|
|
23519
|
+
: "Rotate any exposed credential and block the PR until secrets are removed.");
|
|
23284
23520
|
}
|
|
23285
23521
|
else if (finding.ruleId === "workflow-permission-change") {
|
|
23286
|
-
focus.add(
|
|
23522
|
+
focus.add(locale === "zh-CN"
|
|
23523
|
+
? "合并前重点审查 GitHub Actions 权限。"
|
|
23524
|
+
: "Review GitHub Actions permissions before merging.");
|
|
23287
23525
|
}
|
|
23288
23526
|
else if (finding.ruleId === "missing-tests") {
|
|
23289
|
-
focus.add(
|
|
23527
|
+
focus.add(locale === "zh-CN"
|
|
23528
|
+
? "要求补充测试或清晰的手动验证说明。"
|
|
23529
|
+
: "Ask for tests or a manual verification note.");
|
|
23290
23530
|
}
|
|
23291
23531
|
else if (finding.ruleId === "thin-pr-description") {
|
|
23292
|
-
focus.add(
|
|
23532
|
+
focus.add(locale === "zh-CN"
|
|
23533
|
+
? "深入 review 前要求补充更清楚的 PR 描述。"
|
|
23534
|
+
: "Ask for a clearer PR description before deep review.");
|
|
23293
23535
|
}
|
|
23294
23536
|
else if (finding.ruleId === "missing-reproduction-context") {
|
|
23295
|
-
focus.add(
|
|
23537
|
+
focus.add(locale === "zh-CN"
|
|
23538
|
+
? "要求补充复现步骤或 before/after 上下文。"
|
|
23539
|
+
: "Ask for reproduction steps or before/after context.");
|
|
23296
23540
|
}
|
|
23297
23541
|
else if (finding.ruleId === "change-size") {
|
|
23298
|
-
focus.add(
|
|
23542
|
+
focus.add(locale === "zh-CN"
|
|
23543
|
+
? "要求拆分 PR,或提供逐文件 review 指南。"
|
|
23544
|
+
: "Request a smaller PR or a file-by-file review guide.");
|
|
23299
23545
|
}
|
|
23300
23546
|
else if (finding.ruleId === "mcp-credential-risk") {
|
|
23301
|
-
focus.add(
|
|
23547
|
+
focus.add(locale === "zh-CN"
|
|
23548
|
+
? "重点审查 MCP command、args 和凭证处理方式。"
|
|
23549
|
+
: "Review MCP commands, args, and credential handling.");
|
|
23302
23550
|
}
|
|
23303
23551
|
}
|
|
23304
23552
|
if (focus.size === 0) {
|
|
23305
|
-
focus.add(
|
|
23553
|
+
focus.add(locale === "zh-CN"
|
|
23554
|
+
? "审查列出的敏感文件;如果上下文不足,要求贡献者补充证据。"
|
|
23555
|
+
: "Review the listed sensitive files and ask for evidence where context is thin.");
|
|
23306
23556
|
}
|
|
23307
23557
|
return [...focus];
|
|
23308
23558
|
}
|
|
23559
|
+
function translateFinding(finding) {
|
|
23560
|
+
if (finding.ruleId === "change-size") {
|
|
23561
|
+
const files = finding.evidence?.find((item) => item.startsWith("files: "))?.replace("files: ", "");
|
|
23562
|
+
const lines = finding.evidence?.find((item) => item.startsWith("changed lines: "))?.replace("changed lines: ", "");
|
|
23563
|
+
return {
|
|
23564
|
+
title: finding.severity === "high" ? "review 面积过大" : "review 面积偏大",
|
|
23565
|
+
message: files && lines ? `该改动涉及 ${files} 个文件、${lines} 行变更。` : finding.message,
|
|
23566
|
+
recommendation: finding.severity === "high"
|
|
23567
|
+
? "建议要求拆分 PR,或提供清晰的 review map 后再投入深度 review。"
|
|
23568
|
+
: "建议要求贡献者解释改动边界,并标出最需要重点 review 的文件。"
|
|
23569
|
+
};
|
|
23570
|
+
}
|
|
23571
|
+
if (finding.ruleId === "sensitive-path") {
|
|
23572
|
+
return {
|
|
23573
|
+
title: "敏感文件发生变更",
|
|
23574
|
+
message: finding.path ? `${finding.path} 命中了敏感路径配置。` : finding.message,
|
|
23575
|
+
recommendation: "请重点审查权限、凭证、发布、依赖和 CI 相关变更。"
|
|
23576
|
+
};
|
|
23577
|
+
}
|
|
23578
|
+
if (finding.ruleId === "missing-tests") {
|
|
23579
|
+
return {
|
|
23580
|
+
title: "缺少验证证据",
|
|
23581
|
+
message: "代码发生变更,但没有检测到测试文件改动或 PR 验证说明。",
|
|
23582
|
+
recommendation: "建议要求补充测试,或提供清晰的手动验证说明后再深入 review。"
|
|
23583
|
+
};
|
|
23584
|
+
}
|
|
23585
|
+
if (finding.ruleId === "thin-pr-description") {
|
|
23586
|
+
const missing = finding.title.toLowerCase().includes("missing");
|
|
23587
|
+
return {
|
|
23588
|
+
title: missing ? "PR 描述为空" : "PR 描述过薄",
|
|
23589
|
+
message: missing ? "PR 正文为空,维护者缺少 review 前的上下文。" : "PR 正文较短,可能不足以支撑有效 review。",
|
|
23590
|
+
recommendation: "建议要求补充改动动机、验证证据、兼容性和发布影响说明。"
|
|
23591
|
+
};
|
|
23592
|
+
}
|
|
23593
|
+
if (finding.ruleId === "missing-reproduction-context") {
|
|
23594
|
+
return {
|
|
23595
|
+
title: "缺少复现或 before/after 上下文",
|
|
23596
|
+
message: "PR 未提到复现步骤、预期行为、实际行为或 before/after 说明。",
|
|
23597
|
+
recommendation: "建议要求补充复现步骤或 before/after 说明,方便 reviewer 验证改动路径。"
|
|
23598
|
+
};
|
|
23599
|
+
}
|
|
23600
|
+
if (finding.ruleId === "dependency-added") {
|
|
23601
|
+
return {
|
|
23602
|
+
title: "依赖清单发生变更",
|
|
23603
|
+
message: finding.path ? `${finding.path} 中新增或修改了类似依赖的条目。` : finding.message,
|
|
23604
|
+
recommendation: "请确认包名、许可证、来源可信度,以及 lockfile 是否匹配预期依赖变化。"
|
|
23605
|
+
};
|
|
23606
|
+
}
|
|
23607
|
+
if (finding.ruleId === "workflow-permission-change") {
|
|
23608
|
+
return {
|
|
23609
|
+
title: "Workflow 权限发生变更",
|
|
23610
|
+
message: finding.path ? `${finding.path} 新增或修改了 GitHub Actions 权限。` : finding.message,
|
|
23611
|
+
recommendation: "请确认 workflow 是否真的需要写权限或 token 权限,并检查不可信 PR 是否能触达该 workflow。"
|
|
23612
|
+
};
|
|
23613
|
+
}
|
|
23614
|
+
if (finding.ruleId === "mcp-credential-risk") {
|
|
23615
|
+
return {
|
|
23616
|
+
title: "MCP 配置需要重点审查",
|
|
23617
|
+
message: finding.path ? `${finding.path} 新增了与命令或凭证相关的 MCP 配置。` : finding.message,
|
|
23618
|
+
recommendation: "避免在 MCP 配置中提交凭证,并审查 command 与 args 是否会扩大本地执行面。"
|
|
23619
|
+
};
|
|
23620
|
+
}
|
|
23621
|
+
if (finding.ruleId.startsWith("secret-detected")) {
|
|
23622
|
+
return {
|
|
23623
|
+
title: "可能提交了 secret",
|
|
23624
|
+
message: finding.message.replace("Added line looks like it contains", "新增行疑似包含"),
|
|
23625
|
+
recommendation: "请将凭证移到 secret manager 或 CI secret store,轮换任何已暴露的值,并只提交占位符。"
|
|
23626
|
+
};
|
|
23627
|
+
}
|
|
23628
|
+
return finding;
|
|
23629
|
+
}
|
|
23630
|
+
function translateEvidence(item) {
|
|
23631
|
+
return item
|
|
23632
|
+
.replace("files: ", "文件数:")
|
|
23633
|
+
.replace("changed lines: ", "变更行数:")
|
|
23634
|
+
.replace("line ", "第 ")
|
|
23635
|
+
.replace(": ", " 行:");
|
|
23636
|
+
}
|
|
23637
|
+
function translateRisk(risk) {
|
|
23638
|
+
return { low: "低", medium: "中", high: "高" }[risk] ?? risk;
|
|
23639
|
+
}
|
|
23640
|
+
function translateSeverity(severity) {
|
|
23641
|
+
return { info: "信息", low: "低", medium: "中", high: "高" }[severity] ?? severity;
|
|
23642
|
+
}
|
|
23643
|
+
function translateDescriptionState(state) {
|
|
23644
|
+
return { unavailable: "不可用", missing: "缺失", thin: "过薄", present: "充足" }[state] ?? state;
|
|
23645
|
+
}
|
|
23646
|
+
function formatEvidenceGrade(grade, locale) {
|
|
23647
|
+
if (locale === "zh-CN") {
|
|
23648
|
+
return {
|
|
23649
|
+
strong: "证据充分",
|
|
23650
|
+
adequate: "基本充分",
|
|
23651
|
+
thin: "证据偏薄",
|
|
23652
|
+
risky: "证据不足"
|
|
23653
|
+
}[grade] ?? grade;
|
|
23654
|
+
}
|
|
23655
|
+
return grade;
|
|
23656
|
+
}
|
|
23657
|
+
function formatReviewDecision(decision, locale) {
|
|
23658
|
+
if (locale === "zh-CN") {
|
|
23659
|
+
return {
|
|
23660
|
+
ready: "可以进入常规 review",
|
|
23661
|
+
"review-carefully": "带着重点进入 review",
|
|
23662
|
+
"needs-evidence": "先要求补充证据",
|
|
23663
|
+
"block-merge": "处理风险前不建议合并"
|
|
23664
|
+
}[decision] ?? decision;
|
|
23665
|
+
}
|
|
23666
|
+
return {
|
|
23667
|
+
ready: "Ready for normal review",
|
|
23668
|
+
"review-carefully": "Review with focused attention",
|
|
23669
|
+
"needs-evidence": "Ask for evidence before deep review",
|
|
23670
|
+
"block-merge": "Block merge until risks are handled"
|
|
23671
|
+
}[decision] ?? decision;
|
|
23672
|
+
}
|
|
23673
|
+
function formatPriority(priority, locale) {
|
|
23674
|
+
if (locale === "zh-CN") {
|
|
23675
|
+
return { low: "低优先级", medium: "中优先级", high: "高优先级" }[priority] ?? priority;
|
|
23676
|
+
}
|
|
23677
|
+
return `${priority} priority`;
|
|
23678
|
+
}
|
|
23679
|
+
function translateReviewActionTitle(actionId, fallback) {
|
|
23680
|
+
return {
|
|
23681
|
+
"block-merge-until-resolved": "风险处理前不要合并",
|
|
23682
|
+
"ask-for-evidence-before-review": "深入 review 前先要求补充证据",
|
|
23683
|
+
"review-with-focus": "带着重点清单进行 review",
|
|
23684
|
+
"normal-review": "进入常规 review",
|
|
23685
|
+
"improve-pr-description": "要求补充更清楚的 PR 描述",
|
|
23686
|
+
"add-verification-evidence": "要求补充测试或手动验证证据",
|
|
23687
|
+
"add-reproduction-context": "要求补充复现或 before/after 上下文",
|
|
23688
|
+
"rotate-secret": "轮换并移除暴露的凭证",
|
|
23689
|
+
"justify-workflow-permissions": "要求说明 workflow 权限最小化理由",
|
|
23690
|
+
"review-mcp-execution-surface": "审查 MCP 命令、参数和凭证处理",
|
|
23691
|
+
"request-review-map-or-split": "要求拆分 PR 或提供逐文件 review map",
|
|
23692
|
+
"verify-dependency-change": "核查依赖来源和 lockfile 影响",
|
|
23693
|
+
"assign-sensitive-file-review": "安排敏感文件重点 review"
|
|
23694
|
+
}[actionId] ?? fallback;
|
|
23695
|
+
}
|
|
23696
|
+
function translateReviewActionDetail(actionId, fallback) {
|
|
23697
|
+
return {
|
|
23698
|
+
"block-merge-until-resolved": "在高风险 finding 被解释、降低或移除前,把这个 PR 视为不可合并。",
|
|
23699
|
+
"ask-for-evidence-before-review": "要求测试、截图、复现步骤或更清楚的 PR 描述,再投入详细 review。",
|
|
23700
|
+
"review-with-focus": "优先使用下面的风险发现和重点文件作为第一轮 review map。",
|
|
23701
|
+
"normal-review": "当前证据足够支撑维护者进行常规 review。",
|
|
23702
|
+
"improve-pr-description": "贡献者应说明为什么改、改了什么、如何验证,以及是否有发布或兼容性风险。",
|
|
23703
|
+
"add-verification-evidence": "要求测试输出、CI 链接、截图,或简短的手动验证说明。",
|
|
23704
|
+
"add-reproduction-context": "PR 应包含复现步骤、预期/实际行为,或相关 before/after 截图。",
|
|
23705
|
+
"rotate-secret": "在 secret 从 PR 中移除并完成轮换前,不要合并。",
|
|
23706
|
+
"justify-workflow-permissions": "确认写权限或 OIDC 是否必要,并检查不可信 PR 是否能触发该 workflow。",
|
|
23707
|
+
"review-mcp-execution-surface": "检查 MCP 配置是否提交凭证,或意外扩大本地执行面。",
|
|
23708
|
+
"request-review-map-or-split": "要求贡献者拆分无关改动,或标出最需要重点 review 的文件。",
|
|
23709
|
+
"verify-dependency-change": "检查包名、维护者、许可证、安装脚本,以及 lockfile 是否符合预期依赖变化。",
|
|
23710
|
+
"assign-sensitive-file-review": "合并前由维护者有意识地检查敏感文件改动。"
|
|
23711
|
+
}[actionId] ?? fallback;
|
|
23712
|
+
}
|
|
23713
|
+
function translateFocusReason(reasonId, fallback) {
|
|
23714
|
+
return {
|
|
23715
|
+
"change-size": "review 面积相关 finding",
|
|
23716
|
+
"sensitive-path": "敏感路径发生变更",
|
|
23717
|
+
"dependency-added": "依赖清单发生变更",
|
|
23718
|
+
"workflow-permission-change": "workflow 权限发生变更",
|
|
23719
|
+
"mcp-credential-risk": "MCP 配置存在执行面或凭证风险",
|
|
23720
|
+
"missing-tests": "代码改动缺少测试或验证证据"
|
|
23721
|
+
}[reasonId] ?? fallback;
|
|
23722
|
+
}
|
|
23723
|
+
function translateScoreMessage(message) {
|
|
23724
|
+
return {
|
|
23725
|
+
"PR description provides review context.": "PR 描述提供了 review 上下文。",
|
|
23726
|
+
"Verification evidence was found.": "检测到测试或手动验证证据。",
|
|
23727
|
+
"Reproduction or before/after context was found.": "检测到复现步骤或 before/after 上下文。",
|
|
23728
|
+
"Test files changed with the PR.": "PR 同时修改了测试文件。",
|
|
23729
|
+
"No configured sensitive files changed.": "没有改动已配置的敏感文件。"
|
|
23730
|
+
}[message] ?? message;
|
|
23731
|
+
}
|
|
23732
|
+
function translateDeduction(reasonId, fallback) {
|
|
23733
|
+
return {
|
|
23734
|
+
"missing-pr-description": "PR 描述缺失。",
|
|
23735
|
+
"thin-pr-description": "PR 描述过薄,不足以支撑可靠 review。",
|
|
23736
|
+
"no-pr-context": "扫描时没有可用的 PR 描述上下文。",
|
|
23737
|
+
"missing-verification": "没有检测到测试或手动验证证据。",
|
|
23738
|
+
"missing-reproduction-context": "没有检测到复现步骤、before/after 或预期/实际上下文。",
|
|
23739
|
+
"secret-detected": "检测到疑似已提交 secret。",
|
|
23740
|
+
"workflow-permission-change": "Workflow 权限变化需要重点审查。",
|
|
23741
|
+
"mcp-credential-risk": "MCP 配置扩大了本地执行面或凭证风险。",
|
|
23742
|
+
"large-review-surface": "PR 规模过大,常规 review 可靠性会下降。",
|
|
23743
|
+
"broad-review-surface": "PR review 面积偏大。",
|
|
23744
|
+
"sensitive-path-high": "高敏感文件发生变更,需要重点 review。",
|
|
23745
|
+
"sensitive-path-medium": "敏感文件发生变更,需要重点 review。",
|
|
23746
|
+
"dependency-change": "依赖清单发生变更。",
|
|
23747
|
+
"missing-tests": "代码发生变更,但缺少测试变更或验证说明。"
|
|
23748
|
+
}[reasonId] ?? fallback;
|
|
23749
|
+
}
|
|
23309
23750
|
function formatBoolean(value) {
|
|
23310
23751
|
return value ? "yes" : "no";
|
|
23311
23752
|
}
|
|
23753
|
+
function formatChineseBoolean(value) {
|
|
23754
|
+
return value ? "有" : "无";
|
|
23755
|
+
}
|
|
23312
23756
|
function sarifLevel(severity) {
|
|
23313
23757
|
if (severity === "high") {
|
|
23314
23758
|
return "error";
|
|
@@ -23579,6 +24023,29 @@ function redactLine(line) {
|
|
|
23579
24023
|
|
|
23580
24024
|
|
|
23581
24025
|
|
|
24026
|
+
const PACKAGE_JSON_NON_DEPENDENCY_KEYS = new Set([
|
|
24027
|
+
"author",
|
|
24028
|
+
"bin",
|
|
24029
|
+
"bugs",
|
|
24030
|
+
"description",
|
|
24031
|
+
"engines",
|
|
24032
|
+
"exports",
|
|
24033
|
+
"files",
|
|
24034
|
+
"homepage",
|
|
24035
|
+
"keywords",
|
|
24036
|
+
"license",
|
|
24037
|
+
"main",
|
|
24038
|
+
"module",
|
|
24039
|
+
"name",
|
|
24040
|
+
"packageManager",
|
|
24041
|
+
"private",
|
|
24042
|
+
"publishConfig",
|
|
24043
|
+
"repository",
|
|
24044
|
+
"scripts",
|
|
24045
|
+
"type",
|
|
24046
|
+
"types",
|
|
24047
|
+
"version"
|
|
24048
|
+
]);
|
|
23582
24049
|
function analyzeDiffFiles(files, config, pullRequest) {
|
|
23583
24050
|
const activeFiles = files.filter((file) => !matchesAny(file.path, config.ignorePaths));
|
|
23584
24051
|
const findings = [];
|
|
@@ -23718,9 +24185,7 @@ function analyzeDependencyChanges(files, config) {
|
|
|
23718
24185
|
}
|
|
23719
24186
|
const findings = [];
|
|
23720
24187
|
for (const file of files.filter((candidate) => isDependencyManifest(candidate.path))) {
|
|
23721
|
-
const addedDependencyLines = file.addedLines
|
|
23722
|
-
.map((line) => line.value.trim())
|
|
23723
|
-
.filter((line) => isDependencyLikeAddition(file.path, line));
|
|
24188
|
+
const addedDependencyLines = file.addedLines.filter((line) => isDependencyLikeAddition(file.path, line.value.trim()));
|
|
23724
24189
|
if (addedDependencyLines.length === 0) {
|
|
23725
24190
|
continue;
|
|
23726
24191
|
}
|
|
@@ -23730,7 +24195,7 @@ function analyzeDependencyChanges(files, config) {
|
|
|
23730
24195
|
message: `${file.path} adds or changes dependency-like entries.`,
|
|
23731
24196
|
severity: "medium",
|
|
23732
24197
|
path: file.path,
|
|
23733
|
-
evidence: addedDependencyLines.slice(0, 5),
|
|
24198
|
+
evidence: addedDependencyLines.slice(0, 5).map(formatEvidenceLine),
|
|
23734
24199
|
recommendation: "Verify package names, licenses, provenance, and whether the lockfile matches the intended dependency change."
|
|
23735
24200
|
});
|
|
23736
24201
|
}
|
|
@@ -23738,7 +24203,15 @@ function analyzeDependencyChanges(files, config) {
|
|
|
23738
24203
|
}
|
|
23739
24204
|
function isDependencyLikeAddition(path, line) {
|
|
23740
24205
|
if (path.endsWith("package.json")) {
|
|
23741
|
-
|
|
24206
|
+
const match = /^"(?<key>[@A-Za-z0-9_.-]+)"\s*:\s*"(?<value>[^"]*)"/.exec(line);
|
|
24207
|
+
if (!match?.groups) {
|
|
24208
|
+
return false;
|
|
24209
|
+
}
|
|
24210
|
+
const { key, value } = match.groups;
|
|
24211
|
+
if (!key || !value || PACKAGE_JSON_NON_DEPENDENCY_KEYS.has(key)) {
|
|
24212
|
+
return false;
|
|
24213
|
+
}
|
|
24214
|
+
return /^(?:\^|~|>=?|<=?|\d|workspace:|npm:|file:|link:|portal:|git\+|https?:|github:)/.test(value);
|
|
23742
24215
|
}
|
|
23743
24216
|
if (path.endsWith("requirements.txt")) {
|
|
23744
24217
|
return /^[A-Za-z0-9_.-]+(?:\[.*\])?\s*(?:==|>=|<=|~=|>|<)\s*[^#\s]+/.test(line);
|
|
@@ -23754,9 +24227,7 @@ function isDependencyLikeAddition(path, line) {
|
|
|
23754
24227
|
function analyzeWorkflowPermissions(files) {
|
|
23755
24228
|
const findings = [];
|
|
23756
24229
|
for (const file of files.filter((candidate) => isWorkflowPath(candidate.path))) {
|
|
23757
|
-
const permissionLines = file.addedLines
|
|
23758
|
-
.map((line) => line.value.trim())
|
|
23759
|
-
.filter((line) => /permissions:|contents:\s*write|packages:\s*write|id-token:\s*write|pull-requests:\s*write/.test(line));
|
|
24230
|
+
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()));
|
|
23760
24231
|
if (permissionLines.length === 0) {
|
|
23761
24232
|
continue;
|
|
23762
24233
|
}
|
|
@@ -23766,7 +24237,7 @@ function analyzeWorkflowPermissions(files) {
|
|
|
23766
24237
|
message: `${file.path} adds or changes GitHub Actions permissions.`,
|
|
23767
24238
|
severity: "high",
|
|
23768
24239
|
path: file.path,
|
|
23769
|
-
evidence: permissionLines.slice(0, 5),
|
|
24240
|
+
evidence: permissionLines.slice(0, 5).map(formatEvidenceLine),
|
|
23770
24241
|
recommendation: "Check whether the workflow really needs write or token permissions and whether untrusted pull requests can reach it."
|
|
23771
24242
|
});
|
|
23772
24243
|
}
|
|
@@ -23775,9 +24246,7 @@ function analyzeWorkflowPermissions(files) {
|
|
|
23775
24246
|
function analyzeMcpConfigs(files) {
|
|
23776
24247
|
const findings = [];
|
|
23777
24248
|
for (const file of files.filter((candidate) => isMcpConfigPath(candidate.path))) {
|
|
23778
|
-
const riskyLines = file.addedLines
|
|
23779
|
-
.map((line) => line.value.trim())
|
|
23780
|
-
.filter((line) => /env|token|secret|password|api[_-]?key|command|args/i.test(line));
|
|
24249
|
+
const riskyLines = file.addedLines.filter((line) => /env|token|secret|password|api[_-]?key|command|args/i.test(line.value.trim()));
|
|
23781
24250
|
if (riskyLines.length === 0) {
|
|
23782
24251
|
continue;
|
|
23783
24252
|
}
|
|
@@ -23787,12 +24256,16 @@ function analyzeMcpConfigs(files) {
|
|
|
23787
24256
|
message: `${file.path} adds MCP configuration lines related to commands or credentials.`,
|
|
23788
24257
|
severity: "high",
|
|
23789
24258
|
path: file.path,
|
|
23790
|
-
evidence: riskyLines.slice(0, 5),
|
|
24259
|
+
evidence: riskyLines.slice(0, 5).map(formatEvidenceLine),
|
|
23791
24260
|
recommendation: "Avoid committing credentials in MCP config. Review command and args values as local execution surface."
|
|
23792
24261
|
});
|
|
23793
24262
|
}
|
|
23794
24263
|
return findings;
|
|
23795
24264
|
}
|
|
24265
|
+
function formatEvidenceLine(line) {
|
|
24266
|
+
const value = line.value.trim();
|
|
24267
|
+
return line.lineNumber ? `line ${line.lineNumber}: ${value}` : value;
|
|
24268
|
+
}
|
|
23796
24269
|
function sensitivePathSeverity(path) {
|
|
23797
24270
|
if (matchesAny(path, [
|
|
23798
24271
|
"**/.env*",
|
|
@@ -23815,8 +24288,14 @@ function scanDiff(diffText, options = {}) {
|
|
|
23815
24288
|
const files = parseUnifiedDiff(diffText);
|
|
23816
24289
|
const findings = dedupeFindings(analyzeDiffFiles(files, config, options.pullRequest));
|
|
23817
24290
|
const summary = summarizeDiffFiles(files, config, options.pullRequest);
|
|
24291
|
+
const risk = calculateRisk(findings);
|
|
24292
|
+
const evidenceScore = calculateEvidenceScore(summary, findings);
|
|
24293
|
+
const reviewDecision = calculateReviewDecision(risk, evidenceScore, findings);
|
|
23818
24294
|
return {
|
|
23819
|
-
risk
|
|
24295
|
+
risk,
|
|
24296
|
+
evidenceScore,
|
|
24297
|
+
reviewDecision,
|
|
24298
|
+
reviewPlan: buildReviewPlan(reviewDecision, findings, evidenceScore),
|
|
23820
24299
|
summary,
|
|
23821
24300
|
findings
|
|
23822
24301
|
};
|
|
@@ -23833,6 +24312,325 @@ function calculateRisk(findings) {
|
|
|
23833
24312
|
}
|
|
23834
24313
|
return "low";
|
|
23835
24314
|
}
|
|
24315
|
+
function calculateEvidenceScore(summary, findings) {
|
|
24316
|
+
const deductions = new Map();
|
|
24317
|
+
const addDeduction = (reasonId, points, message) => {
|
|
24318
|
+
const existing = deductions.get(reasonId);
|
|
24319
|
+
if (existing) {
|
|
24320
|
+
existing.points = Math.max(existing.points, points);
|
|
24321
|
+
return;
|
|
24322
|
+
}
|
|
24323
|
+
deductions.set(reasonId, { message, points });
|
|
24324
|
+
};
|
|
24325
|
+
if (summary.pullRequestDescription === "missing") {
|
|
24326
|
+
addDeduction("missing-pr-description", 25, "PR description is missing.");
|
|
24327
|
+
}
|
|
24328
|
+
else if (summary.pullRequestDescription === "thin") {
|
|
24329
|
+
addDeduction("thin-pr-description", 15, "PR description is too thin for confident review.");
|
|
24330
|
+
}
|
|
24331
|
+
else if (summary.pullRequestDescription === "unavailable") {
|
|
24332
|
+
addDeduction("no-pr-context", 10, "PR description was not available to the scanner.");
|
|
24333
|
+
}
|
|
24334
|
+
const needsVerificationEvidence = findings.some((finding) => [
|
|
24335
|
+
"change-size",
|
|
24336
|
+
"sensitive-path",
|
|
24337
|
+
"missing-tests",
|
|
24338
|
+
"dependency-added",
|
|
24339
|
+
"workflow-permission-change",
|
|
24340
|
+
"mcp-credential-risk"
|
|
24341
|
+
].includes(finding.ruleId));
|
|
24342
|
+
if (needsVerificationEvidence && !summary.verificationEvidence) {
|
|
24343
|
+
addDeduction("missing-verification", 20, "No test or manual verification evidence was found.");
|
|
24344
|
+
}
|
|
24345
|
+
if ((summary.sensitiveFilesChanged > 0 || summary.filesChanged >= 5) && !summary.reproductionEvidence) {
|
|
24346
|
+
addDeduction("missing-reproduction-context", 15, "No reproduction, before/after, or expected/actual context was found.");
|
|
24347
|
+
}
|
|
24348
|
+
for (const finding of findings) {
|
|
24349
|
+
if (finding.ruleId.startsWith("secret-detected")) {
|
|
24350
|
+
addDeduction("secret-detected", 40, "Possible committed secret detected.");
|
|
24351
|
+
}
|
|
24352
|
+
else if (finding.ruleId === "workflow-permission-change") {
|
|
24353
|
+
addDeduction("workflow-permission-change", 25, "Workflow permission changes need deliberate review.");
|
|
24354
|
+
}
|
|
24355
|
+
else if (finding.ruleId === "mcp-credential-risk") {
|
|
24356
|
+
addDeduction("mcp-credential-risk", 25, "MCP configuration expands local execution or credential risk.");
|
|
24357
|
+
}
|
|
24358
|
+
else if (finding.ruleId === "change-size") {
|
|
24359
|
+
addDeduction(finding.severity === "high" ? "large-review-surface" : "broad-review-surface", finding.severity === "high" ? 20 : 10, finding.severity === "high"
|
|
24360
|
+
? "The PR is large enough that normal review is likely unreliable."
|
|
24361
|
+
: "The PR has a broad review surface.");
|
|
24362
|
+
}
|
|
24363
|
+
else if (finding.ruleId === "sensitive-path") {
|
|
24364
|
+
addDeduction(`sensitive-path-${finding.severity}`, finding.severity === "high" ? 20 : 10, "Sensitive files changed and need focused review.");
|
|
24365
|
+
}
|
|
24366
|
+
else if (finding.ruleId === "dependency-added") {
|
|
24367
|
+
addDeduction("dependency-change", 10, "Dependency manifest changed.");
|
|
24368
|
+
}
|
|
24369
|
+
else if (finding.ruleId === "missing-tests") {
|
|
24370
|
+
addDeduction("missing-tests", finding.severity === "medium" ? 20 : 12, "Code changed without test changes or verification notes.");
|
|
24371
|
+
}
|
|
24372
|
+
}
|
|
24373
|
+
const strengths = collectEvidenceStrengths(summary);
|
|
24374
|
+
const value = clampScore(100 - [...deductions.values()].reduce((sum, item) => sum + item.points, 0));
|
|
24375
|
+
return {
|
|
24376
|
+
value,
|
|
24377
|
+
grade: gradeEvidenceScore(value),
|
|
24378
|
+
strengths,
|
|
24379
|
+
deductions: [...deductions.entries()].map(([reasonId, item]) => ({
|
|
24380
|
+
reasonId,
|
|
24381
|
+
message: item.message,
|
|
24382
|
+
points: item.points
|
|
24383
|
+
}))
|
|
24384
|
+
};
|
|
24385
|
+
}
|
|
24386
|
+
function collectEvidenceStrengths(summary) {
|
|
24387
|
+
const strengths = [];
|
|
24388
|
+
if (summary.pullRequestDescription === "present") {
|
|
24389
|
+
strengths.push("PR description provides review context.");
|
|
24390
|
+
}
|
|
24391
|
+
if (summary.verificationEvidence) {
|
|
24392
|
+
strengths.push("Verification evidence was found.");
|
|
24393
|
+
}
|
|
24394
|
+
if (summary.reproductionEvidence) {
|
|
24395
|
+
strengths.push("Reproduction or before/after context was found.");
|
|
24396
|
+
}
|
|
24397
|
+
if (summary.testFilesChanged > 0) {
|
|
24398
|
+
strengths.push("Test files changed with the PR.");
|
|
24399
|
+
}
|
|
24400
|
+
if (summary.filesChanged > 0 && summary.sensitiveFilesChanged === 0) {
|
|
24401
|
+
strengths.push("No configured sensitive files changed.");
|
|
24402
|
+
}
|
|
24403
|
+
return strengths;
|
|
24404
|
+
}
|
|
24405
|
+
function gradeEvidenceScore(value) {
|
|
24406
|
+
if (value >= 85) {
|
|
24407
|
+
return "strong";
|
|
24408
|
+
}
|
|
24409
|
+
if (value >= 70) {
|
|
24410
|
+
return "adequate";
|
|
24411
|
+
}
|
|
24412
|
+
if (value >= 50) {
|
|
24413
|
+
return "thin";
|
|
24414
|
+
}
|
|
24415
|
+
return "risky";
|
|
24416
|
+
}
|
|
24417
|
+
function calculateReviewDecision(risk, evidenceScore, findings) {
|
|
24418
|
+
const hasBlockingSecurityFinding = findings.some((finding) => finding.ruleId.startsWith("secret-detected") ||
|
|
24419
|
+
finding.ruleId === "workflow-permission-change" ||
|
|
24420
|
+
finding.ruleId === "mcp-credential-risk");
|
|
24421
|
+
if (hasBlockingSecurityFinding || evidenceScore.value < 50 || risk === "high") {
|
|
24422
|
+
return "block-merge";
|
|
24423
|
+
}
|
|
24424
|
+
if (evidenceScore.value < 70 || findings.some((finding) => finding.ruleId === "missing-tests" || finding.ruleId === "thin-pr-description")) {
|
|
24425
|
+
return "needs-evidence";
|
|
24426
|
+
}
|
|
24427
|
+
if (risk === "medium") {
|
|
24428
|
+
return "review-carefully";
|
|
24429
|
+
}
|
|
24430
|
+
return "ready";
|
|
24431
|
+
}
|
|
24432
|
+
function clampScore(value) {
|
|
24433
|
+
return Math.max(0, Math.min(100, value));
|
|
24434
|
+
}
|
|
24435
|
+
function buildReviewPlan(reviewDecision, findings, evidenceScore) {
|
|
24436
|
+
const actionItems = dedupeReviewActions([
|
|
24437
|
+
...reviewDecisionActions(reviewDecision),
|
|
24438
|
+
...evidenceScoreActions(evidenceScore),
|
|
24439
|
+
...findings.flatMap((finding) => reviewActionsForFinding(finding))
|
|
24440
|
+
]);
|
|
24441
|
+
const focusFiles = dedupeFocusFiles(findings
|
|
24442
|
+
.filter((finding) => finding.path)
|
|
24443
|
+
.map((finding) => ({
|
|
24444
|
+
path: finding.path,
|
|
24445
|
+
reasonId: finding.ruleId,
|
|
24446
|
+
reason: finding.title,
|
|
24447
|
+
priority: finding.severity === "high" ? "high" : finding.severity === "medium" ? "medium" : "low"
|
|
24448
|
+
})));
|
|
24449
|
+
return {
|
|
24450
|
+
actionItems: actionItems.slice(0, 8),
|
|
24451
|
+
focusFiles: focusFiles.slice(0, 8)
|
|
24452
|
+
};
|
|
24453
|
+
}
|
|
24454
|
+
function reviewDecisionActions(reviewDecision) {
|
|
24455
|
+
if (reviewDecision === "block-merge") {
|
|
24456
|
+
return [
|
|
24457
|
+
{
|
|
24458
|
+
actionId: "block-merge-until-resolved",
|
|
24459
|
+
title: "Block merge until the flagged risks are handled.",
|
|
24460
|
+
detail: "Treat this PR as not ready for merge until the high-risk findings are explained, reduced, or removed.",
|
|
24461
|
+
priority: "high",
|
|
24462
|
+
relatedRuleIds: []
|
|
24463
|
+
}
|
|
24464
|
+
];
|
|
24465
|
+
}
|
|
24466
|
+
if (reviewDecision === "needs-evidence") {
|
|
24467
|
+
return [
|
|
24468
|
+
{
|
|
24469
|
+
actionId: "ask-for-evidence-before-review",
|
|
24470
|
+
title: "Ask for missing evidence before deep review.",
|
|
24471
|
+
detail: "Request tests, screenshots, reproduction steps, or a clearer PR description before spending detailed review time.",
|
|
24472
|
+
priority: "medium",
|
|
24473
|
+
relatedRuleIds: []
|
|
24474
|
+
}
|
|
24475
|
+
];
|
|
24476
|
+
}
|
|
24477
|
+
if (reviewDecision === "review-carefully") {
|
|
24478
|
+
return [
|
|
24479
|
+
{
|
|
24480
|
+
actionId: "review-with-focus",
|
|
24481
|
+
title: "Review with a focused checklist.",
|
|
24482
|
+
detail: "Use the findings and focus files below as the first-pass review map.",
|
|
24483
|
+
priority: "medium",
|
|
24484
|
+
relatedRuleIds: []
|
|
24485
|
+
}
|
|
24486
|
+
];
|
|
24487
|
+
}
|
|
24488
|
+
return [
|
|
24489
|
+
{
|
|
24490
|
+
actionId: "normal-review",
|
|
24491
|
+
title: "Proceed with normal review.",
|
|
24492
|
+
detail: "The PR has enough evidence for a standard maintainer review pass.",
|
|
24493
|
+
priority: "low",
|
|
24494
|
+
relatedRuleIds: []
|
|
24495
|
+
}
|
|
24496
|
+
];
|
|
24497
|
+
}
|
|
24498
|
+
function evidenceScoreActions(evidenceScore) {
|
|
24499
|
+
return evidenceScore.deductions.flatMap((deduction) => {
|
|
24500
|
+
if (deduction.reasonId === "missing-pr-description" ||
|
|
24501
|
+
deduction.reasonId === "thin-pr-description" ||
|
|
24502
|
+
deduction.reasonId === "no-pr-context") {
|
|
24503
|
+
return [
|
|
24504
|
+
{
|
|
24505
|
+
actionId: "improve-pr-description",
|
|
24506
|
+
title: "Ask for a clearer PR description.",
|
|
24507
|
+
detail: "The contributor should explain why the change is needed, what changed, how it was verified, and any rollout or compatibility risk.",
|
|
24508
|
+
priority: "medium",
|
|
24509
|
+
relatedRuleIds: ["thin-pr-description"]
|
|
24510
|
+
}
|
|
24511
|
+
];
|
|
24512
|
+
}
|
|
24513
|
+
if (deduction.reasonId === "missing-verification" || deduction.reasonId === "missing-tests") {
|
|
24514
|
+
return [
|
|
24515
|
+
{
|
|
24516
|
+
actionId: "add-verification-evidence",
|
|
24517
|
+
title: "Ask for test or manual verification evidence.",
|
|
24518
|
+
detail: "Require test output, CI links, screenshots, or a short manual verification note before approving.",
|
|
24519
|
+
priority: "medium",
|
|
24520
|
+
relatedRuleIds: ["missing-tests"]
|
|
24521
|
+
}
|
|
24522
|
+
];
|
|
24523
|
+
}
|
|
24524
|
+
if (deduction.reasonId === "missing-reproduction-context") {
|
|
24525
|
+
return [
|
|
24526
|
+
{
|
|
24527
|
+
actionId: "add-reproduction-context",
|
|
24528
|
+
title: "Ask for reproduction or before/after context.",
|
|
24529
|
+
detail: "The PR should include steps to reproduce, expected and actual behavior, or before/after screenshots where relevant.",
|
|
24530
|
+
priority: "medium",
|
|
24531
|
+
relatedRuleIds: ["missing-reproduction-context"]
|
|
24532
|
+
}
|
|
24533
|
+
];
|
|
24534
|
+
}
|
|
24535
|
+
return [];
|
|
24536
|
+
});
|
|
24537
|
+
}
|
|
24538
|
+
function reviewActionsForFinding(finding) {
|
|
24539
|
+
if (finding.ruleId.startsWith("secret-detected")) {
|
|
24540
|
+
return [
|
|
24541
|
+
{
|
|
24542
|
+
actionId: "rotate-secret",
|
|
24543
|
+
title: "Rotate and remove the exposed credential.",
|
|
24544
|
+
detail: "Do not merge until the secret is removed from the PR and any exposed value has been rotated.",
|
|
24545
|
+
priority: "high",
|
|
24546
|
+
relatedRuleIds: [finding.ruleId]
|
|
24547
|
+
}
|
|
24548
|
+
];
|
|
24549
|
+
}
|
|
24550
|
+
if (finding.ruleId === "workflow-permission-change") {
|
|
24551
|
+
return [
|
|
24552
|
+
{
|
|
24553
|
+
actionId: "justify-workflow-permissions",
|
|
24554
|
+
title: "Require a least-privilege explanation for workflow permissions.",
|
|
24555
|
+
detail: "Confirm whether write permissions or OIDC are necessary and whether untrusted PRs can reach this workflow.",
|
|
24556
|
+
priority: "high",
|
|
24557
|
+
relatedRuleIds: [finding.ruleId]
|
|
24558
|
+
}
|
|
24559
|
+
];
|
|
24560
|
+
}
|
|
24561
|
+
if (finding.ruleId === "mcp-credential-risk") {
|
|
24562
|
+
return [
|
|
24563
|
+
{
|
|
24564
|
+
actionId: "review-mcp-execution-surface",
|
|
24565
|
+
title: "Review MCP commands, args, and credential handling.",
|
|
24566
|
+
detail: "Check that MCP config does not commit secrets and does not unexpectedly expand local execution surface.",
|
|
24567
|
+
priority: "high",
|
|
24568
|
+
relatedRuleIds: [finding.ruleId]
|
|
24569
|
+
}
|
|
24570
|
+
];
|
|
24571
|
+
}
|
|
24572
|
+
if (finding.ruleId === "change-size") {
|
|
24573
|
+
return [
|
|
24574
|
+
{
|
|
24575
|
+
actionId: "request-review-map-or-split",
|
|
24576
|
+
title: "Request a smaller PR or a file-by-file review map.",
|
|
24577
|
+
detail: "Ask the contributor to split unrelated changes or identify the files that need the closest review.",
|
|
24578
|
+
priority: finding.severity === "high" ? "high" : "medium",
|
|
24579
|
+
relatedRuleIds: [finding.ruleId]
|
|
24580
|
+
}
|
|
24581
|
+
];
|
|
24582
|
+
}
|
|
24583
|
+
if (finding.ruleId === "dependency-added") {
|
|
24584
|
+
return [
|
|
24585
|
+
{
|
|
24586
|
+
actionId: "verify-dependency-change",
|
|
24587
|
+
title: "Verify dependency provenance and lockfile impact.",
|
|
24588
|
+
detail: "Check package name, maintainer, license, install scripts, and whether the lockfile matches the intended dependency change.",
|
|
24589
|
+
priority: "medium",
|
|
24590
|
+
relatedRuleIds: [finding.ruleId]
|
|
24591
|
+
}
|
|
24592
|
+
];
|
|
24593
|
+
}
|
|
24594
|
+
if (finding.ruleId === "sensitive-path") {
|
|
24595
|
+
return [
|
|
24596
|
+
{
|
|
24597
|
+
actionId: "assign-sensitive-file-review",
|
|
24598
|
+
title: "Assign focused review for sensitive files.",
|
|
24599
|
+
detail: "Have a maintainer deliberately inspect the sensitive file changes before approval.",
|
|
24600
|
+
priority: finding.severity === "high" ? "high" : "medium",
|
|
24601
|
+
relatedRuleIds: [finding.ruleId]
|
|
24602
|
+
}
|
|
24603
|
+
];
|
|
24604
|
+
}
|
|
24605
|
+
return [];
|
|
24606
|
+
}
|
|
24607
|
+
function dedupeReviewActions(actions) {
|
|
24608
|
+
const seen = new Set();
|
|
24609
|
+
const unique = [];
|
|
24610
|
+
for (const action of actions.sort((left, right) => priorityRank(right.priority) - priorityRank(left.priority))) {
|
|
24611
|
+
if (seen.has(action.actionId)) {
|
|
24612
|
+
continue;
|
|
24613
|
+
}
|
|
24614
|
+
seen.add(action.actionId);
|
|
24615
|
+
unique.push(action);
|
|
24616
|
+
}
|
|
24617
|
+
return unique;
|
|
24618
|
+
}
|
|
24619
|
+
function dedupeFocusFiles(files) {
|
|
24620
|
+
const seen = new Set();
|
|
24621
|
+
const unique = [];
|
|
24622
|
+
for (const file of files.sort((left, right) => priorityRank(right.priority) - priorityRank(left.priority))) {
|
|
24623
|
+
if (seen.has(file.path)) {
|
|
24624
|
+
continue;
|
|
24625
|
+
}
|
|
24626
|
+
seen.add(file.path);
|
|
24627
|
+
unique.push(file);
|
|
24628
|
+
}
|
|
24629
|
+
return unique;
|
|
24630
|
+
}
|
|
24631
|
+
function priorityRank(priority) {
|
|
24632
|
+
return { low: 1, medium: 2, high: 3 }[priority];
|
|
24633
|
+
}
|
|
23836
24634
|
function dedupeFindings(findings) {
|
|
23837
24635
|
const seen = new Set();
|
|
23838
24636
|
const unique = [];
|
|
@@ -23866,7 +24664,7 @@ const build_program = new Command();
|
|
|
23866
24664
|
build_program
|
|
23867
24665
|
.name("proof-pr")
|
|
23868
24666
|
.description("Review pull request evidence, scope, and safety before maintainers spend time on it.")
|
|
23869
|
-
.version("0.1.
|
|
24667
|
+
.version("0.1.5");
|
|
23870
24668
|
build_program
|
|
23871
24669
|
.command("scan", { isDefault: true })
|
|
23872
24670
|
.description("Scan a git diff and print a ProofPR report.")
|
|
@@ -23878,6 +24676,7 @@ build_program
|
|
|
23878
24676
|
.option("--pr-body-file <path>", "Read a pull request body from a Markdown file.")
|
|
23879
24677
|
.option("--config <path>", "Path to .proofpr.yml.", ".proofpr.yml")
|
|
23880
24678
|
.option("--format <format>", "Output format: markdown, json, or sarif.", parseFormat, "markdown")
|
|
24679
|
+
.option("--locale <locale>", "Report language: en or zh-CN.")
|
|
23881
24680
|
.option("--fail-on <level>", "Exit with code 1 on risk level: low, medium, high, or never.", parseFailLevel, "never")
|
|
23882
24681
|
.action(async (options) => {
|
|
23883
24682
|
const diffText = options.diffFile
|
|
@@ -23889,7 +24688,8 @@ build_program
|
|
|
23889
24688
|
? { title: options.prTitle, body: prBody }
|
|
23890
24689
|
: undefined;
|
|
23891
24690
|
const result = scanDiff(diffText, { config, pullRequest });
|
|
23892
|
-
const
|
|
24691
|
+
const locale = parseLocale(options.locale, config.locale);
|
|
24692
|
+
const output = renderOutput(result, options.format, locale);
|
|
23893
24693
|
process.stdout.write(`${output}\n`);
|
|
23894
24694
|
if (riskMeetsThreshold(result.risk, options.failOn)) {
|
|
23895
24695
|
process.exitCode = 1;
|
|
@@ -23900,10 +24700,11 @@ build_program
|
|
|
23900
24700
|
.description("Create a starter .proofpr.yml and GitHub Actions workflow.")
|
|
23901
24701
|
.option("--config-path <path>", "Path to write the ProofPR configuration file.", ".proofpr.yml")
|
|
23902
24702
|
.option("--workflow-path <path>", "Path to write the GitHub Actions workflow.", ".github/workflows/proofpr.yml")
|
|
24703
|
+
.option("--preset <preset>", `Config preset: ${listConfigPresets().join(", ")}.`, parsePresetOption, "open-source-maintainer")
|
|
23903
24704
|
.option("--fail-on <level>", "Workflow failure threshold: low, medium, high, or never.", parseFailLevel, "high")
|
|
23904
24705
|
.option("--force", "Overwrite existing files.", false)
|
|
23905
24706
|
.action(async (options) => {
|
|
23906
|
-
await writeIfMissing(options.configPath, renderConfigTemplate(), options.force);
|
|
24707
|
+
await writeIfMissing(options.configPath, renderConfigTemplate(options.preset), options.force);
|
|
23907
24708
|
await writeIfMissing(options.workflowPath, renderWorkflowTemplate(options.failOn), options.force);
|
|
23908
24709
|
process.stdout.write(`ProofPR initialized:\n- ${options.configPath}\n- ${options.workflowPath}\n`);
|
|
23909
24710
|
});
|
|
@@ -23942,40 +24743,55 @@ async function pathExists(path) {
|
|
|
23942
24743
|
return false;
|
|
23943
24744
|
}
|
|
23944
24745
|
}
|
|
23945
|
-
function renderConfigTemplate() {
|
|
23946
|
-
return `
|
|
23947
|
-
|
|
23948
|
-
sensitivePaths:
|
|
23949
|
-
- ".github/workflows/**"
|
|
23950
|
-
- ".github/actions/**"
|
|
23951
|
-
- "**/.env*"
|
|
23952
|
-
- "**/mcp*.json"
|
|
23953
|
-
- "**/*mcp*.json"
|
|
23954
|
-
- "Dockerfile"
|
|
23955
|
-
- "**/Dockerfile"
|
|
23956
|
-
- "package.json"
|
|
23957
|
-
- "pnpm-lock.yaml"
|
|
23958
|
-
- "package-lock.json"
|
|
23959
|
-
- "yarn.lock"
|
|
23960
|
-
- "bun.lockb"
|
|
23961
|
-
|
|
23962
|
-
requireTests:
|
|
23963
|
-
enabled: true
|
|
23964
|
-
paths:
|
|
23965
|
-
- "src/**"
|
|
23966
|
-
- "packages/**/src/**"
|
|
23967
|
-
- "app/**"
|
|
23968
|
-
- "lib/**"
|
|
23969
|
-
|
|
23970
|
-
secrets:
|
|
23971
|
-
enabled: true
|
|
23972
|
-
|
|
23973
|
-
dependencies:
|
|
23974
|
-
flagNewPackages: true
|
|
23975
|
-
flagMajorUpgrades: true
|
|
24746
|
+
function renderConfigTemplate(preset) {
|
|
24747
|
+
return `locale: zh-CN
|
|
24748
|
+
preset: ${preset}
|
|
23976
24749
|
|
|
23977
24750
|
comment:
|
|
23978
24751
|
enabled: true
|
|
24752
|
+
|
|
24753
|
+
# 如需更严格或更宽松,可以先换 preset:
|
|
24754
|
+
# preset: security-strict
|
|
24755
|
+
#
|
|
24756
|
+
# 可用预设:
|
|
24757
|
+
# - balanced
|
|
24758
|
+
# - open-source-maintainer
|
|
24759
|
+
# - security-strict
|
|
24760
|
+
# - ai-generated-pr
|
|
24761
|
+
# - mcp-security
|
|
24762
|
+
# - dependency-careful
|
|
24763
|
+
#
|
|
24764
|
+
# 也可以取消注释下面这些字段,覆盖 preset 的默认值。
|
|
24765
|
+
# riskThreshold: high
|
|
24766
|
+
#
|
|
24767
|
+
# sensitivePaths:
|
|
24768
|
+
# - ".github/workflows/**"
|
|
24769
|
+
# - ".github/actions/**"
|
|
24770
|
+
# - "**/.env*"
|
|
24771
|
+
# - "**/mcp*.json"
|
|
24772
|
+
# - "**/*mcp*.json"
|
|
24773
|
+
# - "Dockerfile"
|
|
24774
|
+
# - "**/Dockerfile"
|
|
24775
|
+
# - "package.json"
|
|
24776
|
+
# - "pnpm-lock.yaml"
|
|
24777
|
+
# - "package-lock.json"
|
|
24778
|
+
# - "yarn.lock"
|
|
24779
|
+
# - "bun.lockb"
|
|
24780
|
+
#
|
|
24781
|
+
# requireTests:
|
|
24782
|
+
# enabled: true
|
|
24783
|
+
# paths:
|
|
24784
|
+
# - "src/**"
|
|
24785
|
+
# - "packages/**/src/**"
|
|
24786
|
+
# - "app/**"
|
|
24787
|
+
# - "lib/**"
|
|
24788
|
+
#
|
|
24789
|
+
# secrets:
|
|
24790
|
+
# enabled: true
|
|
24791
|
+
#
|
|
24792
|
+
# dependencies:
|
|
24793
|
+
# flagNewPackages: true
|
|
24794
|
+
# flagMajorUpgrades: true
|
|
23979
24795
|
`;
|
|
23980
24796
|
}
|
|
23981
24797
|
function renderWorkflowTemplate(failOn) {
|
|
@@ -23994,20 +24810,21 @@ jobs:
|
|
|
23994
24810
|
runs-on: ubuntu-latest
|
|
23995
24811
|
steps:
|
|
23996
24812
|
- uses: actions/checkout@v4
|
|
23997
|
-
- uses: linsk27/proof-pr@v0.1.
|
|
24813
|
+
- uses: linsk27/proof-pr@v0.1.5
|
|
23998
24814
|
with:
|
|
23999
24815
|
fail-on: ${failOn}
|
|
24000
24816
|
comment: "true"
|
|
24817
|
+
annotations: "true"
|
|
24001
24818
|
`;
|
|
24002
24819
|
}
|
|
24003
|
-
function renderOutput(result, format) {
|
|
24820
|
+
function renderOutput(result, format, locale) {
|
|
24004
24821
|
if (format === "json") {
|
|
24005
24822
|
return JSON.stringify(result, null, 2);
|
|
24006
24823
|
}
|
|
24007
24824
|
if (format === "sarif") {
|
|
24008
24825
|
return renderSarifReport(result);
|
|
24009
24826
|
}
|
|
24010
|
-
return renderMarkdownReport(result);
|
|
24827
|
+
return renderMarkdownReport(result, locale);
|
|
24011
24828
|
}
|
|
24012
24829
|
function parseFormat(value) {
|
|
24013
24830
|
if (value === "json" || value === "markdown" || value === "sarif") {
|
|
@@ -24021,4 +24838,11 @@ function parseFailLevel(value) {
|
|
|
24021
24838
|
}
|
|
24022
24839
|
throw new InvalidArgumentError("fail-on must be one of: low, medium, high, never");
|
|
24023
24840
|
}
|
|
24841
|
+
function parsePresetOption(value) {
|
|
24842
|
+
const preset = parsePreset(value);
|
|
24843
|
+
if (preset === value) {
|
|
24844
|
+
return preset;
|
|
24845
|
+
}
|
|
24846
|
+
throw new InvalidArgumentError(`preset must be one of: ${listConfigPresets().join(", ")}`);
|
|
24847
|
+
}
|
|
24024
24848
|
//# 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.5",
|
|
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",
|