@mainahq/core 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/package.json +37 -0
- package/src/ai/__tests__/ai.test.ts +207 -0
- package/src/ai/__tests__/design-approaches.test.ts +192 -0
- package/src/ai/__tests__/spec-questions.test.ts +191 -0
- package/src/ai/__tests__/tiers.test.ts +110 -0
- package/src/ai/commit-msg.ts +28 -0
- package/src/ai/design-approaches.ts +76 -0
- package/src/ai/index.ts +205 -0
- package/src/ai/pr-summary.ts +60 -0
- package/src/ai/spec-questions.ts +74 -0
- package/src/ai/tiers.ts +52 -0
- package/src/ai/try-generate.ts +89 -0
- package/src/ai/validate.ts +66 -0
- package/src/benchmark/__tests__/reporter.test.ts +525 -0
- package/src/benchmark/__tests__/runner.test.ts +113 -0
- package/src/benchmark/__tests__/story-loader.test.ts +152 -0
- package/src/benchmark/reporter.ts +332 -0
- package/src/benchmark/runner.ts +91 -0
- package/src/benchmark/story-loader.ts +88 -0
- package/src/benchmark/types.ts +95 -0
- package/src/cache/__tests__/keys.test.ts +97 -0
- package/src/cache/__tests__/manager.test.ts +312 -0
- package/src/cache/__tests__/ttl.test.ts +94 -0
- package/src/cache/keys.ts +44 -0
- package/src/cache/manager.ts +231 -0
- package/src/cache/ttl.ts +77 -0
- package/src/config/__tests__/config.test.ts +376 -0
- package/src/config/index.ts +198 -0
- package/src/context/__tests__/budget.test.ts +179 -0
- package/src/context/__tests__/engine.test.ts +163 -0
- package/src/context/__tests__/episodic.test.ts +291 -0
- package/src/context/__tests__/relevance.test.ts +323 -0
- package/src/context/__tests__/retrieval.test.ts +143 -0
- package/src/context/__tests__/selector.test.ts +174 -0
- package/src/context/__tests__/semantic.test.ts +252 -0
- package/src/context/__tests__/treesitter.test.ts +229 -0
- package/src/context/__tests__/working.test.ts +236 -0
- package/src/context/budget.ts +130 -0
- package/src/context/engine.ts +394 -0
- package/src/context/episodic.ts +251 -0
- package/src/context/relevance.ts +325 -0
- package/src/context/retrieval.ts +325 -0
- package/src/context/selector.ts +93 -0
- package/src/context/semantic.ts +331 -0
- package/src/context/treesitter.ts +216 -0
- package/src/context/working.ts +192 -0
- package/src/db/__tests__/db.test.ts +151 -0
- package/src/db/index.ts +211 -0
- package/src/db/schema.ts +84 -0
- package/src/design/__tests__/design.test.ts +310 -0
- package/src/design/__tests__/generate-hld-lld.test.ts +109 -0
- package/src/design/__tests__/review.test.ts +561 -0
- package/src/design/index.ts +297 -0
- package/src/design/review.ts +327 -0
- package/src/explain/__tests__/explain.test.ts +173 -0
- package/src/explain/index.ts +181 -0
- package/src/features/__tests__/analyzer.test.ts +358 -0
- package/src/features/__tests__/checklist.test.ts +454 -0
- package/src/features/__tests__/numbering.test.ts +319 -0
- package/src/features/__tests__/quality.test.ts +295 -0
- package/src/features/__tests__/traceability.test.ts +147 -0
- package/src/features/analyzer.ts +445 -0
- package/src/features/checklist.ts +366 -0
- package/src/features/index.ts +18 -0
- package/src/features/numbering.ts +404 -0
- package/src/features/quality.ts +349 -0
- package/src/features/test-stubs.ts +157 -0
- package/src/features/traceability.ts +260 -0
- package/src/feedback/__tests__/async-feedback.test.ts +52 -0
- package/src/feedback/__tests__/collector.test.ts +219 -0
- package/src/feedback/__tests__/compress.test.ts +150 -0
- package/src/feedback/__tests__/preferences.test.ts +169 -0
- package/src/feedback/collector.ts +135 -0
- package/src/feedback/compress.ts +92 -0
- package/src/feedback/preferences.ts +108 -0
- package/src/git/__tests__/git.test.ts +62 -0
- package/src/git/index.ts +110 -0
- package/src/hooks/__tests__/runner.test.ts +266 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/runner.ts +130 -0
- package/src/index.ts +356 -0
- package/src/init/__tests__/init.test.ts +228 -0
- package/src/init/index.ts +364 -0
- package/src/language/__tests__/detect.test.ts +77 -0
- package/src/language/__tests__/profile.test.ts +51 -0
- package/src/language/detect.ts +70 -0
- package/src/language/profile.ts +110 -0
- package/src/prompts/__tests__/defaults.test.ts +52 -0
- package/src/prompts/__tests__/engine.test.ts +183 -0
- package/src/prompts/__tests__/evolution-resolve.test.ts +169 -0
- package/src/prompts/__tests__/evolution.test.ts +187 -0
- package/src/prompts/__tests__/loader.test.ts +105 -0
- package/src/prompts/candidates/review-v2.md +55 -0
- package/src/prompts/defaults/ai-review.md +49 -0
- package/src/prompts/defaults/commit.md +30 -0
- package/src/prompts/defaults/context.md +26 -0
- package/src/prompts/defaults/design-approaches.md +57 -0
- package/src/prompts/defaults/design-hld-lld.md +55 -0
- package/src/prompts/defaults/design.md +53 -0
- package/src/prompts/defaults/explain.md +31 -0
- package/src/prompts/defaults/fix.md +32 -0
- package/src/prompts/defaults/index.ts +38 -0
- package/src/prompts/defaults/review.md +41 -0
- package/src/prompts/defaults/spec-questions.md +59 -0
- package/src/prompts/defaults/tests.md +72 -0
- package/src/prompts/engine.ts +137 -0
- package/src/prompts/evolution.ts +409 -0
- package/src/prompts/loader.ts +71 -0
- package/src/review/__tests__/review.test.ts +288 -0
- package/src/review/comprehensive.ts +362 -0
- package/src/review/index.ts +417 -0
- package/src/stats/__tests__/tracker.test.ts +323 -0
- package/src/stats/index.ts +11 -0
- package/src/stats/tracker.ts +492 -0
- package/src/ticket/__tests__/ticket.test.ts +273 -0
- package/src/ticket/index.ts +185 -0
- package/src/utils.ts +87 -0
- package/src/verify/__tests__/ai-review.test.ts +242 -0
- package/src/verify/__tests__/coverage.test.ts +83 -0
- package/src/verify/__tests__/detect.test.ts +175 -0
- package/src/verify/__tests__/diff-filter.test.ts +338 -0
- package/src/verify/__tests__/fix.test.ts +478 -0
- package/src/verify/__tests__/linters/clippy.test.ts +45 -0
- package/src/verify/__tests__/linters/go-vet.test.ts +27 -0
- package/src/verify/__tests__/linters/ruff.test.ts +64 -0
- package/src/verify/__tests__/mutation.test.ts +141 -0
- package/src/verify/__tests__/pipeline.test.ts +553 -0
- package/src/verify/__tests__/proof.test.ts +97 -0
- package/src/verify/__tests__/secretlint.test.ts +190 -0
- package/src/verify/__tests__/semgrep.test.ts +217 -0
- package/src/verify/__tests__/slop.test.ts +366 -0
- package/src/verify/__tests__/sonar.test.ts +113 -0
- package/src/verify/__tests__/syntax-guard.test.ts +227 -0
- package/src/verify/__tests__/trivy.test.ts +191 -0
- package/src/verify/__tests__/visual.test.ts +139 -0
- package/src/verify/ai-review.ts +276 -0
- package/src/verify/coverage.ts +134 -0
- package/src/verify/detect.ts +171 -0
- package/src/verify/diff-filter.ts +183 -0
- package/src/verify/fix.ts +317 -0
- package/src/verify/linters/clippy.ts +52 -0
- package/src/verify/linters/go-vet.ts +32 -0
- package/src/verify/linters/ruff.ts +47 -0
- package/src/verify/mutation.ts +143 -0
- package/src/verify/pipeline.ts +328 -0
- package/src/verify/proof.ts +277 -0
- package/src/verify/secretlint.ts +168 -0
- package/src/verify/semgrep.ts +170 -0
- package/src/verify/slop.ts +493 -0
- package/src/verify/sonar.ts +146 -0
- package/src/verify/syntax-guard.ts +251 -0
- package/src/verify/trivy.ts +161 -0
- package/src/verify/visual.ts +460 -0
- package/src/workflow/__tests__/context.test.ts +110 -0
- package/src/workflow/context.ts +81 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Syntax Guard — the first gate in the Verify Engine pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Runs Biome check on the provided files and returns structured errors.
|
|
5
|
+
* If syntax fails, the pipeline rejects immediately — no tests, no coverage,
|
|
6
|
+
* no slop detection. This must complete in <500ms for 10 files.
|
|
7
|
+
*
|
|
8
|
+
* Uses `--reporter=json` for machine-parseable output.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import type { Result } from "../db/index";
|
|
14
|
+
import type { LanguageProfile } from "../language/profile";
|
|
15
|
+
import { TYPESCRIPT_PROFILE } from "../language/profile";
|
|
16
|
+
import { parseClippyOutput } from "./linters/clippy";
|
|
17
|
+
import { parseGoVetOutput } from "./linters/go-vet";
|
|
18
|
+
import { parseRuffOutput } from "./linters/ruff";
|
|
19
|
+
|
|
20
|
+
export interface SyntaxDiagnostic {
|
|
21
|
+
file: string;
|
|
22
|
+
line: number;
|
|
23
|
+
column: number;
|
|
24
|
+
message: string;
|
|
25
|
+
severity: "error" | "warning";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type SyntaxGuardResult = Result<void, SyntaxDiagnostic[]>;
|
|
29
|
+
|
|
30
|
+
/** Shape of a single diagnostic in Biome's JSON reporter output. */
|
|
31
|
+
interface BiomeDiagnostic {
|
|
32
|
+
severity: string;
|
|
33
|
+
message: string;
|
|
34
|
+
category: string;
|
|
35
|
+
location: {
|
|
36
|
+
path: string;
|
|
37
|
+
start: { line: number; column: number };
|
|
38
|
+
end: { line: number; column: number };
|
|
39
|
+
};
|
|
40
|
+
advices: unknown[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Shape of Biome's JSON reporter output. */
|
|
44
|
+
interface BiomeJsonOutput {
|
|
45
|
+
summary: {
|
|
46
|
+
changed: number;
|
|
47
|
+
unchanged: number;
|
|
48
|
+
errors: number;
|
|
49
|
+
warnings: number;
|
|
50
|
+
};
|
|
51
|
+
diagnostics: BiomeDiagnostic[];
|
|
52
|
+
command: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Find the biome binary — prefer local node_modules/.bin, fall back to global.
|
|
57
|
+
*/
|
|
58
|
+
function findBiomeBinary(cwd: string): string {
|
|
59
|
+
const localBin = join(cwd, "node_modules", ".bin", "biome");
|
|
60
|
+
if (existsSync(localBin)) {
|
|
61
|
+
return localBin;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Walk up to find node_modules/.bin
|
|
65
|
+
let dir = cwd;
|
|
66
|
+
const root = "/";
|
|
67
|
+
while (dir !== root) {
|
|
68
|
+
const binPath = join(dir, "node_modules", ".bin", "biome");
|
|
69
|
+
if (existsSync(binPath)) {
|
|
70
|
+
return binPath;
|
|
71
|
+
}
|
|
72
|
+
const parent = join(dir, "..");
|
|
73
|
+
if (parent === dir) break;
|
|
74
|
+
dir = parent;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Fall back to bare "biome" (assumes it's on PATH)
|
|
78
|
+
return "biome";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parse Biome's JSON reporter output into structured SyntaxDiagnostic[].
|
|
83
|
+
* Returns an empty array if the output cannot be parsed.
|
|
84
|
+
*/
|
|
85
|
+
export function parseBiomeOutput(output: string): SyntaxDiagnostic[] {
|
|
86
|
+
try {
|
|
87
|
+
const parsed: BiomeJsonOutput = JSON.parse(output);
|
|
88
|
+
if (!parsed.diagnostics || !Array.isArray(parsed.diagnostics)) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return parsed.diagnostics
|
|
93
|
+
.filter(
|
|
94
|
+
(d) =>
|
|
95
|
+
d.location?.path &&
|
|
96
|
+
d.location?.start &&
|
|
97
|
+
typeof d.location.start.line === "number",
|
|
98
|
+
)
|
|
99
|
+
.map((d) => ({
|
|
100
|
+
file: d.location.path,
|
|
101
|
+
line: d.location.start.line,
|
|
102
|
+
column: d.location.start.column,
|
|
103
|
+
message: d.message,
|
|
104
|
+
severity:
|
|
105
|
+
d.severity === "error" ? ("error" as const) : ("warning" as const),
|
|
106
|
+
}));
|
|
107
|
+
} catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Run Biome check on the provided files and return a Result.
|
|
114
|
+
*
|
|
115
|
+
* - If no files are provided, returns Ok immediately.
|
|
116
|
+
* - If Biome exits with code 0, returns Ok.
|
|
117
|
+
* - If Biome exits non-zero, parses errors and returns Err if any errors exist.
|
|
118
|
+
* - Only errors (not warnings) trigger a rejection.
|
|
119
|
+
*/
|
|
120
|
+
export async function syntaxGuard(
|
|
121
|
+
files: string[],
|
|
122
|
+
cwd?: string,
|
|
123
|
+
profile?: LanguageProfile,
|
|
124
|
+
): Promise<SyntaxGuardResult> {
|
|
125
|
+
if (files.length === 0) {
|
|
126
|
+
return { ok: true, value: undefined };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const lang = profile ?? TYPESCRIPT_PROFILE;
|
|
130
|
+
const workDir = cwd ?? process.cwd();
|
|
131
|
+
|
|
132
|
+
// Route to language-specific linter for non-TypeScript
|
|
133
|
+
if (lang.id !== "typescript") {
|
|
134
|
+
return runLanguageLinter(files, workDir, lang);
|
|
135
|
+
}
|
|
136
|
+
const biomeBin = findBiomeBinary(workDir);
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const proc = Bun.spawn(
|
|
140
|
+
[
|
|
141
|
+
biomeBin,
|
|
142
|
+
"check",
|
|
143
|
+
"--reporter=json",
|
|
144
|
+
"--no-errors-on-unmatched",
|
|
145
|
+
"--colors=off",
|
|
146
|
+
...files,
|
|
147
|
+
],
|
|
148
|
+
{
|
|
149
|
+
cwd: workDir,
|
|
150
|
+
stdout: "pipe",
|
|
151
|
+
stderr: "pipe",
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const stdout = await new Response(proc.stdout).text();
|
|
156
|
+
const exitCode = await proc.exited;
|
|
157
|
+
|
|
158
|
+
if (exitCode === 0) {
|
|
159
|
+
return { ok: true, value: undefined };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Parse diagnostics from JSON output
|
|
163
|
+
const allDiagnostics = parseBiomeOutput(stdout);
|
|
164
|
+
|
|
165
|
+
// Only reject on errors, not warnings
|
|
166
|
+
const errors = allDiagnostics.filter((d) => d.severity === "error");
|
|
167
|
+
|
|
168
|
+
if (errors.length === 0) {
|
|
169
|
+
// Only warnings — pass
|
|
170
|
+
return { ok: true, value: undefined };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Return all diagnostics (errors + warnings) for context,
|
|
174
|
+
// but the presence of errors triggers the rejection.
|
|
175
|
+
return { ok: false, error: allDiagnostics };
|
|
176
|
+
} catch (e) {
|
|
177
|
+
// Biome not found or spawn failure
|
|
178
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
error: [
|
|
182
|
+
{
|
|
183
|
+
file: "",
|
|
184
|
+
line: 0,
|
|
185
|
+
column: 0,
|
|
186
|
+
message: `Failed to run biome: ${message}`,
|
|
187
|
+
severity: "error",
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Run a language-specific linter and return structured diagnostics.
|
|
196
|
+
* Gracefully handles tool-not-found by returning an error diagnostic.
|
|
197
|
+
*/
|
|
198
|
+
async function runLanguageLinter(
|
|
199
|
+
files: string[],
|
|
200
|
+
cwd: string,
|
|
201
|
+
profile: LanguageProfile,
|
|
202
|
+
): Promise<SyntaxGuardResult> {
|
|
203
|
+
const args = profile.syntaxArgs(files, cwd);
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const proc = Bun.spawn(args, {
|
|
207
|
+
cwd,
|
|
208
|
+
stdout: "pipe",
|
|
209
|
+
stderr: "pipe",
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const stdout = await new Response(proc.stdout).text();
|
|
213
|
+
const stderr = await new Response(proc.stderr).text();
|
|
214
|
+
await proc.exited;
|
|
215
|
+
|
|
216
|
+
let diagnostics: SyntaxDiagnostic[] = [];
|
|
217
|
+
|
|
218
|
+
switch (profile.id) {
|
|
219
|
+
case "python":
|
|
220
|
+
diagnostics = parseRuffOutput(stdout);
|
|
221
|
+
break;
|
|
222
|
+
case "go":
|
|
223
|
+
diagnostics = parseGoVetOutput(stderr);
|
|
224
|
+
break;
|
|
225
|
+
case "rust":
|
|
226
|
+
diagnostics = parseClippyOutput(stdout);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const errors = diagnostics.filter((d) => d.severity === "error");
|
|
231
|
+
if (errors.length === 0) {
|
|
232
|
+
return { ok: true, value: undefined };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { ok: false, error: diagnostics };
|
|
236
|
+
} catch (e) {
|
|
237
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
error: [
|
|
241
|
+
{
|
|
242
|
+
file: "",
|
|
243
|
+
line: 0,
|
|
244
|
+
column: 0,
|
|
245
|
+
message: `Failed to run ${profile.syntaxTool}: ${message}`,
|
|
246
|
+
severity: "error",
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trivy Integration for the Verify Engine.
|
|
3
|
+
*
|
|
4
|
+
* Runs Trivy for dependency CVE scanning.
|
|
5
|
+
* Parses JSON output into the unified Finding type.
|
|
6
|
+
* Gracefully skips if trivy is not installed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { isToolAvailable } from "./detect";
|
|
10
|
+
import type { Finding } from "./diff-filter";
|
|
11
|
+
|
|
12
|
+
// ─── Types ────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export interface TrivyOptions {
|
|
15
|
+
scanType?: "fs" | "repo";
|
|
16
|
+
cwd?: string;
|
|
17
|
+
/** Pre-resolved availability — skips redundant detection if provided. */
|
|
18
|
+
available?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TrivyResult {
|
|
22
|
+
findings: Finding[];
|
|
23
|
+
skipped: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── JSON Parsing ─────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Map Trivy severity string to unified severity.
|
|
30
|
+
*/
|
|
31
|
+
function mapTrivySeverity(severity: string): "error" | "warning" | "info" {
|
|
32
|
+
switch (severity.toUpperCase()) {
|
|
33
|
+
case "CRITICAL":
|
|
34
|
+
case "HIGH":
|
|
35
|
+
return "error";
|
|
36
|
+
case "MEDIUM":
|
|
37
|
+
return "warning";
|
|
38
|
+
default:
|
|
39
|
+
return "info";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse Trivy JSON output into Finding[].
|
|
45
|
+
*
|
|
46
|
+
* Trivy JSON has this structure:
|
|
47
|
+
* ```json
|
|
48
|
+
* {
|
|
49
|
+
* "Results": [{
|
|
50
|
+
* "Target": "package-lock.json",
|
|
51
|
+
* "Type": "npm",
|
|
52
|
+
* "Vulnerabilities": [{
|
|
53
|
+
* "VulnerabilityID": "CVE-...",
|
|
54
|
+
* "PkgName": "lodash",
|
|
55
|
+
* "InstalledVersion": "4.17.20",
|
|
56
|
+
* "FixedVersion": "4.17.21",
|
|
57
|
+
* "Severity": "HIGH",
|
|
58
|
+
* "Title": "Prototype Pollution"
|
|
59
|
+
* }]
|
|
60
|
+
* }]
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* Handles malformed JSON and unexpected structures gracefully.
|
|
65
|
+
*/
|
|
66
|
+
export function parseTrivyJson(json: string): Finding[] {
|
|
67
|
+
let parsed: Record<string, unknown>;
|
|
68
|
+
try {
|
|
69
|
+
parsed = JSON.parse(json) as Record<string, unknown>;
|
|
70
|
+
} catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const results = parsed.Results;
|
|
75
|
+
if (!Array.isArray(results)) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const findings: Finding[] = [];
|
|
80
|
+
|
|
81
|
+
for (const result of results) {
|
|
82
|
+
const r = result as Record<string, unknown>;
|
|
83
|
+
const target = (r.Target as string) ?? "";
|
|
84
|
+
const vulnerabilities = r.Vulnerabilities;
|
|
85
|
+
|
|
86
|
+
if (!Array.isArray(vulnerabilities)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const vuln of vulnerabilities) {
|
|
91
|
+
const v = vuln as Record<string, unknown>;
|
|
92
|
+
const vulnId = (v.VulnerabilityID as string) ?? "";
|
|
93
|
+
const pkgName = (v.PkgName as string) ?? "";
|
|
94
|
+
const installedVersion = (v.InstalledVersion as string) ?? "";
|
|
95
|
+
const fixedVersion = (v.FixedVersion as string) ?? undefined;
|
|
96
|
+
const severity = (v.Severity as string) ?? "UNKNOWN";
|
|
97
|
+
const title = (v.Title as string) ?? "";
|
|
98
|
+
|
|
99
|
+
let message = `${pkgName}@${installedVersion}: ${title}`;
|
|
100
|
+
if (fixedVersion) {
|
|
101
|
+
message += ` (fix: ${fixedVersion})`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
findings.push({
|
|
105
|
+
tool: "trivy",
|
|
106
|
+
file: target,
|
|
107
|
+
line: 0,
|
|
108
|
+
message,
|
|
109
|
+
severity: mapTrivySeverity(severity),
|
|
110
|
+
ruleId: vulnId || undefined,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return findings;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── Runner ───────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Run Trivy and return parsed findings.
|
|
122
|
+
*
|
|
123
|
+
* If trivy is not installed, returns `{ findings: [], skipped: true }`.
|
|
124
|
+
* If trivy fails, returns `{ findings: [], skipped: false }`.
|
|
125
|
+
*/
|
|
126
|
+
export async function runTrivy(options?: TrivyOptions): Promise<TrivyResult> {
|
|
127
|
+
const toolAvailable = options?.available ?? (await isToolAvailable("trivy"));
|
|
128
|
+
if (!toolAvailable) {
|
|
129
|
+
return { findings: [], skipped: true };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const scanType = options?.scanType ?? "fs";
|
|
133
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
134
|
+
|
|
135
|
+
const args = [
|
|
136
|
+
"trivy",
|
|
137
|
+
scanType,
|
|
138
|
+
"--format",
|
|
139
|
+
"json",
|
|
140
|
+
"--scanners",
|
|
141
|
+
"vuln",
|
|
142
|
+
".",
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const proc = Bun.spawn(args, {
|
|
147
|
+
cwd,
|
|
148
|
+
stdout: "pipe",
|
|
149
|
+
stderr: "pipe",
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const stdout = await new Response(proc.stdout).text();
|
|
153
|
+
await new Response(proc.stderr).text();
|
|
154
|
+
await proc.exited;
|
|
155
|
+
|
|
156
|
+
const findings = parseTrivyJson(stdout);
|
|
157
|
+
return { findings, skipped: false };
|
|
158
|
+
} catch {
|
|
159
|
+
return { findings: [], skipped: false };
|
|
160
|
+
}
|
|
161
|
+
}
|