vibepro 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.ja.md +448 -0
- package/README.md +520 -0
- package/agent-instructions/codex/AGENTS.vibepro.md +45 -0
- package/bin/vibepro.js +9 -0
- package/docs/assets/vibepro-header.png +0 -0
- package/package.json +51 -0
- package/skills/vibepro-diagnosis-packages/SKILL.md +133 -0
- package/skills/vibepro-human-review/SKILL.md +73 -0
- package/skills/vibepro-story-refactor/SKILL.md +89 -0
- package/skills/vibepro-workflow/SKILL.md +139 -0
- package/src/agent-harness-map.js +230 -0
- package/src/agent-harness-scanner.js +337 -0
- package/src/agent-review.js +2180 -0
- package/src/api-boundary-scanner.js +452 -0
- package/src/architecture-profiler.js +423 -0
- package/src/authorization-scoring.js +149 -0
- package/src/brainbase-importer.js +534 -0
- package/src/change-risk-classifier.js +195 -0
- package/src/check-packs.js +605 -0
- package/src/checkpoint-manager.js +233 -0
- package/src/cli.js +2213 -0
- package/src/code-quality-scanner.js +310 -0
- package/src/codex-manager.js +143 -0
- package/src/component-style-scanner.js +336 -0
- package/src/coverage-report.js +99 -0
- package/src/database-access-scanner.js +163 -0
- package/src/decision-records.js +315 -0
- package/src/design-modernize.js +1435 -0
- package/src/design-system.js +1732 -0
- package/src/diagnostic-engine.js +1945 -0
- package/src/diagram-requirement-resolver.js +194 -0
- package/src/doctor.js +677 -0
- package/src/environment-graph.js +424 -0
- package/src/execution-state.js +849 -0
- package/src/explore-evidence.js +425 -0
- package/src/flow-design-scanner.js +896 -0
- package/src/flow-verifier.js +887 -0
- package/src/gesture-interaction-scanner.js +330 -0
- package/src/graph-context.js +263 -0
- package/src/graphify-adapter.js +189 -0
- package/src/html-report.js +1035 -0
- package/src/journey-map.js +1299 -0
- package/src/language.js +48 -0
- package/src/lazy-pattern-detector.js +182 -0
- package/src/local-dev-scanner.js +135 -0
- package/src/managed-worktree-gate.js +187 -0
- package/src/managed-worktree.js +766 -0
- package/src/merge-manager.js +501 -0
- package/src/network-contract-scanner.js +442 -0
- package/src/nocodb-story-sync.js +386 -0
- package/src/oss-readiness-scanner.js +417 -0
- package/src/performance-evidence.js +756 -0
- package/src/performance-measurer.js +591 -0
- package/src/pr-manager.js +8220 -0
- package/src/presets.js +682 -0
- package/src/public-discovery-scanner.js +519 -0
- package/src/refactoring-delta-reporter.js +367 -0
- package/src/refactoring-opportunity-generator.js +797 -0
- package/src/regression-risk-scanner.js +146 -0
- package/src/repo-status.js +266 -0
- package/src/report-fingerprint.js +188 -0
- package/src/report-pr-body-prompt-template.md +108 -0
- package/src/report-pr-body-schema.json +95 -0
- package/src/report-store.js +135 -0
- package/src/report-validator.js +192 -0
- package/src/requirement-consistency.js +1066 -0
- package/src/runtime-info.js +134 -0
- package/src/self-dogfood-scanner.js +476 -0
- package/src/session-learning.js +164 -0
- package/src/skills-manager.js +157 -0
- package/src/spec-drift.js +378 -0
- package/src/spec-fingerprint.js +445 -0
- package/src/spec-prompt-template.md +155 -0
- package/src/spec-schema.json +219 -0
- package/src/spec-store.js +258 -0
- package/src/spec-validator.js +459 -0
- package/src/static-site-scanner.js +316 -0
- package/src/story-candidate-generator.js +85 -0
- package/src/story-catalog-generator.js +2813 -0
- package/src/story-html.js +156 -0
- package/src/story-manager.js +2144 -0
- package/src/story-task-generator.js +522 -0
- package/src/task-manager.js +1029 -0
- package/src/terminal-link-scanner.js +238 -0
- package/src/usage-report.js +417 -0
- package/src/verification-evidence.js +284 -0
- package/src/workspace.js +126 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
const TOOL_TIMEOUT_MS = 120000;
|
|
6
|
+
const MAX_BUFFER = 1024 * 1024 * 8;
|
|
7
|
+
const SCORECARD_REVIEW_THRESHOLD = 7;
|
|
8
|
+
|
|
9
|
+
export async function scanOssReadiness(repoRoot, options = {}) {
|
|
10
|
+
const root = repoRoot;
|
|
11
|
+
const env = options.env ?? process.env;
|
|
12
|
+
const repoUrl = await resolveGithubRepoUrl(root, env);
|
|
13
|
+
const tools = [];
|
|
14
|
+
const findings = [];
|
|
15
|
+
|
|
16
|
+
const gitleaks = await runGitleaks(root, env);
|
|
17
|
+
tools.push(gitleaks.tool);
|
|
18
|
+
findings.push(...gitleaks.findings);
|
|
19
|
+
|
|
20
|
+
const scorecard = await runScorecard(root, env, repoUrl);
|
|
21
|
+
tools.push(scorecard.tool);
|
|
22
|
+
findings.push(...scorecard.findings);
|
|
23
|
+
|
|
24
|
+
const syft = await runSyft(root, env);
|
|
25
|
+
tools.push(syft.tool);
|
|
26
|
+
findings.push(...syft.findings);
|
|
27
|
+
|
|
28
|
+
const grype = await runGrype(root, env);
|
|
29
|
+
tools.push(grype.tool);
|
|
30
|
+
findings.push(...grype.findings);
|
|
31
|
+
|
|
32
|
+
const reuse = await runReuse(root, env);
|
|
33
|
+
tools.push(reuse.tool);
|
|
34
|
+
findings.push(...reuse.findings);
|
|
35
|
+
|
|
36
|
+
const riskSummary = summarizeFindings(findings);
|
|
37
|
+
return {
|
|
38
|
+
schema_version: '0.1.0',
|
|
39
|
+
status: statusFromToolsAndFindings(tools, riskSummary),
|
|
40
|
+
summary: {
|
|
41
|
+
tool_count: tools.length,
|
|
42
|
+
pass: tools.filter((tool) => tool.status === 'pass').length,
|
|
43
|
+
needs_setup: tools.filter((tool) => tool.status === 'needs_setup').length,
|
|
44
|
+
needs_review: tools.filter((tool) => tool.status === 'needs_review').length,
|
|
45
|
+
fail: tools.filter((tool) => tool.status === 'fail').length,
|
|
46
|
+
findings: findings.length
|
|
47
|
+
},
|
|
48
|
+
tools,
|
|
49
|
+
findings,
|
|
50
|
+
risk_summary: {
|
|
51
|
+
findings: riskSummary
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function runGitleaks(root, env) {
|
|
57
|
+
const command = 'gitleaks detect --source . --report-format json --no-banner';
|
|
58
|
+
const result = await runTool('gitleaks', ['detect', '--source', '.', '--report-format', 'json', '--no-banner'], { cwd: root, env });
|
|
59
|
+
if (result.missing) return missingTool('gitleaks', command, 'Install gitleaks and rerun `vibepro check oss-readiness <repo>`.');
|
|
60
|
+
if (result.ok && !result.stdout.trim()) {
|
|
61
|
+
return {
|
|
62
|
+
tool: {
|
|
63
|
+
id: 'gitleaks',
|
|
64
|
+
label: 'Gitleaks secret scan',
|
|
65
|
+
status: 'pass',
|
|
66
|
+
command,
|
|
67
|
+
summary: 'No secret candidates reported'
|
|
68
|
+
},
|
|
69
|
+
findings: []
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (!result.ok && !result.stdout.trim()) return unusableTool('gitleaks', command, result);
|
|
73
|
+
|
|
74
|
+
const parsed = parseToolJson('gitleaks', command, result);
|
|
75
|
+
if (parsed.error) return parsed.error;
|
|
76
|
+
const leaks = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.findings) ? parsed.findings : [];
|
|
77
|
+
const findings = leaks.map((leak, index) => ({
|
|
78
|
+
id: `oss_readiness.gitleaks.secret.${leak.RuleID ?? leak.rule_id ?? index + 1}`,
|
|
79
|
+
severity: 'critical',
|
|
80
|
+
gate_effect: 'block',
|
|
81
|
+
path: leak.File ?? leak.file ?? '',
|
|
82
|
+
detail: `Gitleaks detected a secret candidate (${leak.RuleID ?? leak.Description ?? 'unknown rule'}). Secret value was redacted by VibePro.`,
|
|
83
|
+
required_action: 'Remove the secret, rotate the credential if it was real, and record any false positive as a VibePro decision artifact.',
|
|
84
|
+
line: leak.StartLine ?? leak.Line ?? leak.line ?? null
|
|
85
|
+
}));
|
|
86
|
+
return {
|
|
87
|
+
tool: {
|
|
88
|
+
id: 'gitleaks',
|
|
89
|
+
label: 'Gitleaks secret scan',
|
|
90
|
+
status: findings.length > 0 ? 'fail' : 'pass',
|
|
91
|
+
command,
|
|
92
|
+
summary: findings.length > 0 ? `${findings.length} secret candidates` : 'No secret candidates reported'
|
|
93
|
+
},
|
|
94
|
+
findings
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function runScorecard(root, env, repoUrl) {
|
|
99
|
+
const command = repoUrl
|
|
100
|
+
? `scorecard --repo ${repoUrl} --format json`
|
|
101
|
+
: 'scorecard --repo <github-repo> --format json';
|
|
102
|
+
if (!repoUrl) {
|
|
103
|
+
return {
|
|
104
|
+
tool: {
|
|
105
|
+
id: 'scorecard',
|
|
106
|
+
label: 'OpenSSF Scorecard',
|
|
107
|
+
status: 'needs_setup',
|
|
108
|
+
command,
|
|
109
|
+
summary: 'GitHub origin URL could not be resolved',
|
|
110
|
+
setup: {
|
|
111
|
+
required: true,
|
|
112
|
+
next_commands: ['Set a GitHub origin remote, then rerun `vibepro check oss-readiness <repo>`.']
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
findings: [setupFinding('scorecard.repo_url_missing', 'OpenSSF Scorecard needs a GitHub repository URL.', 'Configure a GitHub origin remote or run Scorecard separately and attach the result.')]
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const result = await runTool('scorecard', ['--repo', repoUrl, '--format', 'json'], { cwd: root, env });
|
|
119
|
+
if (result.missing) return missingTool('scorecard', command, 'Install OpenSSF Scorecard and rerun `vibepro check oss-readiness <repo>`.');
|
|
120
|
+
if (!result.ok && !result.stdout.trim()) return unusableTool('scorecard', command, result);
|
|
121
|
+
|
|
122
|
+
const parsed = parseToolJson('scorecard', command, result);
|
|
123
|
+
if (parsed.error) return parsed.error;
|
|
124
|
+
const score = Number(parsed?.score ?? parsed?.Score);
|
|
125
|
+
const checks = Array.isArray(parsed?.checks) ? parsed.checks : Array.isArray(parsed?.Checks) ? parsed.Checks : [];
|
|
126
|
+
const findings = [];
|
|
127
|
+
if (Number.isFinite(score) && score < SCORECARD_REVIEW_THRESHOLD) {
|
|
128
|
+
findings.push({
|
|
129
|
+
id: 'oss_readiness.scorecard.low_score',
|
|
130
|
+
severity: 'medium',
|
|
131
|
+
gate_effect: 'review',
|
|
132
|
+
detail: `OpenSSF Scorecard score is ${score}; threshold is ${SCORECARD_REVIEW_THRESHOLD}.`,
|
|
133
|
+
required_action: 'Review low-scoring Scorecard checks before OSS publication.'
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
for (const check of checks) {
|
|
137
|
+
const checkScore = Number(check.score ?? check.Score);
|
|
138
|
+
if (!Number.isFinite(checkScore) || checkScore >= 0) continue;
|
|
139
|
+
findings.push({
|
|
140
|
+
id: `oss_readiness.scorecard.negative.${slug(check.name ?? check.Name ?? 'check')}`,
|
|
141
|
+
severity: 'medium',
|
|
142
|
+
gate_effect: 'review',
|
|
143
|
+
detail: `Scorecard check ${check.name ?? check.Name ?? 'unknown'} returned ${checkScore}.`,
|
|
144
|
+
required_action: 'Inspect the Scorecard check detail and decide whether to fix or waive it.'
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
tool: {
|
|
149
|
+
id: 'scorecard',
|
|
150
|
+
label: 'OpenSSF Scorecard',
|
|
151
|
+
status: findings.length > 0 ? 'needs_review' : 'pass',
|
|
152
|
+
command,
|
|
153
|
+
summary: Number.isFinite(score) ? `score=${score}` : 'Scorecard completed'
|
|
154
|
+
},
|
|
155
|
+
findings
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function runSyft(root, env) {
|
|
160
|
+
const command = 'syft . -o cyclonedx-json';
|
|
161
|
+
const result = await runTool('syft', ['.', '-o', 'cyclonedx-json'], { cwd: root, env });
|
|
162
|
+
if (result.missing) return missingTool('syft', command, 'Install syft and rerun `vibepro check oss-readiness <repo>`.');
|
|
163
|
+
if (!result.ok && !result.stdout.trim()) return unusableTool('syft', command, result);
|
|
164
|
+
|
|
165
|
+
const parsed = parseToolJson('syft', command, result);
|
|
166
|
+
if (parsed.error) return parsed.error;
|
|
167
|
+
const components = Array.isArray(parsed?.components) ? parsed.components : Array.isArray(parsed?.artifacts) ? parsed.artifacts : [];
|
|
168
|
+
return {
|
|
169
|
+
tool: {
|
|
170
|
+
id: 'syft',
|
|
171
|
+
label: 'Syft SBOM generation',
|
|
172
|
+
status: 'pass',
|
|
173
|
+
command,
|
|
174
|
+
summary: `${components.length} SBOM components`
|
|
175
|
+
},
|
|
176
|
+
findings: []
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function runGrype(root, env) {
|
|
181
|
+
const command = 'grype . -o json';
|
|
182
|
+
const result = await runTool('grype', ['.', '-o', 'json'], { cwd: root, env });
|
|
183
|
+
if (result.missing) return missingTool('grype', command, 'Install grype and rerun `vibepro check oss-readiness <repo>`.');
|
|
184
|
+
if (!result.ok && !result.stdout.trim()) return unusableTool('grype', command, result);
|
|
185
|
+
|
|
186
|
+
const parsed = parseToolJson('grype', command, result);
|
|
187
|
+
if (parsed.error) return parsed.error;
|
|
188
|
+
const matches = Array.isArray(parsed?.matches) ? parsed.matches : [];
|
|
189
|
+
const findings = matches.map((match, index) => {
|
|
190
|
+
const vulnerability = match.vulnerability ?? {};
|
|
191
|
+
const artifact = match.artifact ?? {};
|
|
192
|
+
const severity = normalizeSeverity(vulnerability.severity);
|
|
193
|
+
return {
|
|
194
|
+
id: `oss_readiness.grype.${slug(vulnerability.id ?? `vulnerability-${index + 1}`)}`,
|
|
195
|
+
severity: grypeSeverityToFindingSeverity(severity),
|
|
196
|
+
gate_effect: grypeSeverityToGateEffect(severity),
|
|
197
|
+
path: artifact.name ?? '',
|
|
198
|
+
detail: `${vulnerability.id ?? 'Unknown vulnerability'} in ${artifact.name ?? 'unknown artifact'} (${severity}).`,
|
|
199
|
+
required_action: severity === 'critical' || severity === 'high'
|
|
200
|
+
? 'Upgrade or remove the vulnerable dependency before OSS publication, or record a non-critical waiver with evidence.'
|
|
201
|
+
: 'Review the vulnerable dependency and record the accepted risk or fix.'
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
return {
|
|
205
|
+
tool: {
|
|
206
|
+
id: 'grype',
|
|
207
|
+
label: 'Grype vulnerability scan',
|
|
208
|
+
status: statusFromFindings(findings),
|
|
209
|
+
command,
|
|
210
|
+
summary: `${matches.length} vulnerability matches`
|
|
211
|
+
},
|
|
212
|
+
findings
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function runReuse(root, env) {
|
|
217
|
+
const command = 'reuse lint --json';
|
|
218
|
+
const result = await runTool('reuse', ['lint', '--json'], { cwd: root, env });
|
|
219
|
+
if (result.missing) return missingTool('reuse', command, 'Install reuse and rerun `vibepro check oss-readiness <repo>`.');
|
|
220
|
+
if (!result.ok && !result.stdout.trim()) return unusableTool('reuse', command, result);
|
|
221
|
+
|
|
222
|
+
const parsed = parseToolJson('reuse', command, result);
|
|
223
|
+
if (parsed.error) return parsed.error;
|
|
224
|
+
const compliant = parsed?.compliant === true || parsed?.summary?.compliant === true;
|
|
225
|
+
const badFiles = [
|
|
226
|
+
...asArray(parsed?.files_without_copyright),
|
|
227
|
+
...asArray(parsed?.files_without_license),
|
|
228
|
+
...asArray(parsed?.bad_licenses)
|
|
229
|
+
].filter(Boolean);
|
|
230
|
+
const findings = compliant || badFiles.length === 0
|
|
231
|
+
? []
|
|
232
|
+
: badFiles.slice(0, 50).map((file, index) => ({
|
|
233
|
+
id: `oss_readiness.reuse.non_compliant.${index + 1}`,
|
|
234
|
+
severity: 'medium',
|
|
235
|
+
gate_effect: 'review',
|
|
236
|
+
path: typeof file === 'string' ? file : file.path ?? '',
|
|
237
|
+
detail: 'REUSE reported missing or invalid license/copyright metadata.',
|
|
238
|
+
required_action: 'Add machine-readable license and copyright metadata, or record an explicit waiver.'
|
|
239
|
+
}));
|
|
240
|
+
if (!compliant && findings.length === 0) {
|
|
241
|
+
findings.push({
|
|
242
|
+
id: 'oss_readiness.reuse.non_compliant',
|
|
243
|
+
severity: 'medium',
|
|
244
|
+
gate_effect: 'review',
|
|
245
|
+
detail: 'REUSE reported the repository as non-compliant.',
|
|
246
|
+
required_action: 'Run `reuse lint` locally and fix or waive the reported license metadata gaps.'
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
tool: {
|
|
251
|
+
id: 'reuse',
|
|
252
|
+
label: 'REUSE license metadata',
|
|
253
|
+
status: findings.length > 0 ? 'needs_review' : 'pass',
|
|
254
|
+
command,
|
|
255
|
+
summary: findings.length > 0 ? `${findings.length} license metadata findings` : 'REUSE compliant'
|
|
256
|
+
},
|
|
257
|
+
findings
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function runTool(command, args, options) {
|
|
262
|
+
try {
|
|
263
|
+
const result = await execFileAsync(command, args, {
|
|
264
|
+
cwd: options.cwd,
|
|
265
|
+
env: options.env,
|
|
266
|
+
encoding: 'utf8',
|
|
267
|
+
timeout: TOOL_TIMEOUT_MS,
|
|
268
|
+
maxBuffer: MAX_BUFFER
|
|
269
|
+
});
|
|
270
|
+
return { ok: true, stdout: result.stdout ?? '', stderr: result.stderr ?? '', exitCode: 0 };
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (error.code === 'ENOENT') {
|
|
273
|
+
return { ok: false, missing: true, stdout: '', stderr: '', exitCode: null };
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
ok: false,
|
|
277
|
+
stdout: String(error.stdout ?? ''),
|
|
278
|
+
stderr: String(error.stderr ?? error.message ?? ''),
|
|
279
|
+
exitCode: typeof error.code === 'number' ? error.code : null
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function resolveGithubRepoUrl(root, env) {
|
|
285
|
+
const result = await runTool('git', ['config', '--get', 'remote.origin.url'], { cwd: root, env });
|
|
286
|
+
const remote = result.stdout.trim();
|
|
287
|
+
if (!remote) return null;
|
|
288
|
+
const https = remote.match(/^https:\/\/github\.com\/([^/]+\/[^/.]+)(?:\.git)?$/);
|
|
289
|
+
if (https) return `github.com/${https[1]}`;
|
|
290
|
+
const ssh = remote.match(/^git@github\.com:([^/]+\/[^/.]+)(?:\.git)?$/);
|
|
291
|
+
if (ssh) return `github.com/${ssh[1]}`;
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function missingTool(id, command, guidance) {
|
|
296
|
+
return {
|
|
297
|
+
tool: {
|
|
298
|
+
id,
|
|
299
|
+
label: labelForTool(id),
|
|
300
|
+
status: 'needs_setup',
|
|
301
|
+
command,
|
|
302
|
+
summary: `${id} is not installed or not on PATH`,
|
|
303
|
+
setup: {
|
|
304
|
+
required: true,
|
|
305
|
+
next_commands: [guidance]
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
findings: [setupFinding(`${id}.missing`, `${id} is not installed or not on PATH.`, guidance)]
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function unusableTool(id, command, result) {
|
|
313
|
+
return {
|
|
314
|
+
tool: {
|
|
315
|
+
id,
|
|
316
|
+
label: labelForTool(id),
|
|
317
|
+
status: 'needs_setup',
|
|
318
|
+
command,
|
|
319
|
+
summary: `${id} failed before producing usable JSON`
|
|
320
|
+
},
|
|
321
|
+
findings: [{
|
|
322
|
+
id: `oss_readiness.${id}.execution_failed`,
|
|
323
|
+
severity: 'medium',
|
|
324
|
+
gate_effect: 'review',
|
|
325
|
+
detail: `${id} failed before producing usable JSON. stderr: ${redact(String(result.stderr ?? '')).slice(0, 500)}`,
|
|
326
|
+
required_action: `Run \`${command}\` locally, fix tool setup or repository issues, and rerun VibePro.`
|
|
327
|
+
}]
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function parseToolJson(id, command, result) {
|
|
332
|
+
const parsed = parseJson(result.stdout);
|
|
333
|
+
return parsed === null ? { error: unusableTool(id, command, result) } : parsed;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function setupFinding(id, detail, requiredAction) {
|
|
337
|
+
return {
|
|
338
|
+
id: `oss_readiness.setup.${id}`,
|
|
339
|
+
severity: 'medium',
|
|
340
|
+
gate_effect: 'review',
|
|
341
|
+
detail,
|
|
342
|
+
required_action: requiredAction
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function summarizeFindings(findings) {
|
|
347
|
+
const summary = { block: 0, review: 0, info: 0 };
|
|
348
|
+
for (const finding of findings) {
|
|
349
|
+
const effect = ['block', 'review', 'info'].includes(finding.gate_effect) ? finding.gate_effect : 'info';
|
|
350
|
+
summary[effect] += 1;
|
|
351
|
+
}
|
|
352
|
+
return summary;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function statusFromToolsAndFindings(tools, riskSummary) {
|
|
356
|
+
if ((riskSummary.block ?? 0) > 0 || tools.some((tool) => tool.status === 'fail')) return 'fail';
|
|
357
|
+
if (tools.some((tool) => tool.status === 'needs_setup')) return 'needs_setup';
|
|
358
|
+
if ((riskSummary.review ?? 0) > 0 || tools.some((tool) => tool.status === 'needs_review')) return 'needs_review';
|
|
359
|
+
return 'pass';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function statusFromFindings(findings) {
|
|
363
|
+
const summary = summarizeFindings(findings);
|
|
364
|
+
if (summary.block > 0) return 'fail';
|
|
365
|
+
if (summary.review > 0) return 'needs_review';
|
|
366
|
+
return 'pass';
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function grypeSeverityToGateEffect(severity) {
|
|
370
|
+
if (severity === 'critical' || severity === 'high') return 'block';
|
|
371
|
+
if (severity === 'medium') return 'review';
|
|
372
|
+
return 'info';
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function grypeSeverityToFindingSeverity(severity) {
|
|
376
|
+
if (severity === 'critical') return 'critical';
|
|
377
|
+
if (severity === 'high') return 'high';
|
|
378
|
+
if (severity === 'medium') return 'medium';
|
|
379
|
+
return 'low';
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function normalizeSeverity(value) {
|
|
383
|
+
const severity = String(value ?? 'unknown').toLowerCase();
|
|
384
|
+
return ['critical', 'high', 'medium', 'low', 'negligible', 'unknown'].includes(severity) ? severity : 'unknown';
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function parseJson(value) {
|
|
388
|
+
try {
|
|
389
|
+
return JSON.parse(value);
|
|
390
|
+
} catch {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function asArray(value) {
|
|
396
|
+
return Array.isArray(value) ? value : [];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function slug(value) {
|
|
400
|
+
return String(value).toLowerCase().replace(/[^a-z0-9_.-]+/g, '-').replace(/^-|-$/g, '') || 'unknown';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function labelForTool(id) {
|
|
404
|
+
return {
|
|
405
|
+
gitleaks: 'Gitleaks secret scan',
|
|
406
|
+
scorecard: 'OpenSSF Scorecard',
|
|
407
|
+
syft: 'Syft SBOM generation',
|
|
408
|
+
grype: 'Grype vulnerability scan',
|
|
409
|
+
reuse: 'REUSE license metadata'
|
|
410
|
+
}[id] ?? id;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function redact(value) {
|
|
414
|
+
return value
|
|
415
|
+
.replace(/\bsk-[A-Za-z0-9]{8,}\b/g, 'sk-REDACTED')
|
|
416
|
+
.replace(/([A-Za-z0-9_-]*(?:token|secret|key)[A-Za-z0-9_-]*\s*[:=]\s*)\S+/gi, '$1[REDACTED]');
|
|
417
|
+
}
|