autoremediator 0.5.0 → 0.7.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 +7 -3
- package/dist/chunk-7XSZTGU7.js +16 -0
- package/dist/chunk-7XSZTGU7.js.map +1 -0
- package/dist/{chunk-VLXGEH7U.js → chunk-MUFP2DQX.js} +2623 -1732
- package/dist/chunk-MUFP2DQX.js.map +1 -0
- package/dist/cli.js +114 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +5 -210
- package/dist/index.js +17 -1
- package/dist/mcp/server.d.ts +3 -241
- package/dist/mcp/server.js +14 -69
- package/dist/mcp/server.js.map +1 -1
- package/dist/openapi/server.d.ts +9 -242
- package/dist/openapi/server.js +16 -90
- package/dist/openapi/server.js.map +1 -1
- package/dist/options-schema-DfLBOsPI.d.ts +37 -0
- package/dist/remediate-from-scan-C-E7gqxF.d.ts +211 -0
- package/llms.txt +21 -6
- package/package.json +2 -2
- package/dist/chunk-VLXGEH7U.js.map +0 -1
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
// src/remediation/pipeline.ts
|
|
2
2
|
import { generateText as generateText2 } from "ai";
|
|
3
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
4
|
-
import { join as join8 } from "path";
|
|
5
|
-
import semver4 from "semver";
|
|
6
3
|
|
|
7
4
|
// src/platform/config.ts
|
|
8
5
|
function resolveProvider(options = {}) {
|
|
@@ -162,567 +159,1108 @@ function parseListOutput(pm, stdout) {
|
|
|
162
159
|
return versions;
|
|
163
160
|
}
|
|
164
161
|
|
|
165
|
-
// src/remediation/tools/
|
|
162
|
+
// src/remediation/tools/apply-version-bump.ts
|
|
166
163
|
import { tool } from "ai";
|
|
167
164
|
import { z } from "zod";
|
|
165
|
+
import { join as join4 } from "path";
|
|
166
|
+
import { readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
167
|
+
import { execa } from "execa";
|
|
168
|
+
import semver from "semver";
|
|
168
169
|
|
|
169
|
-
// src/
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
return res.json();
|
|
181
|
-
}
|
|
182
|
-
function osvEventsToSemverRange(events) {
|
|
183
|
-
const parts = [];
|
|
184
|
-
for (const event of events) {
|
|
185
|
-
if (event.introduced !== void 0) {
|
|
186
|
-
const v = event.introduced === "0" ? "0.0.0" : event.introduced;
|
|
187
|
-
parts.push(`>=${v}`);
|
|
188
|
-
}
|
|
189
|
-
if (event.fixed !== void 0) {
|
|
190
|
-
parts.push(`<${event.fixed}`);
|
|
191
|
-
}
|
|
192
|
-
if (event.last_affected !== void 0) {
|
|
193
|
-
parts.push(`<=${event.last_affected}`);
|
|
194
|
-
}
|
|
170
|
+
// src/platform/policy.ts
|
|
171
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
172
|
+
import { join as join2 } from "path";
|
|
173
|
+
var DEFAULT_POLICY = {
|
|
174
|
+
allowMajorBumps: false,
|
|
175
|
+
denyPackages: [],
|
|
176
|
+
allowPackages: [],
|
|
177
|
+
constraints: {
|
|
178
|
+
directDependenciesOnly: false,
|
|
179
|
+
preferVersionBump: false
|
|
195
180
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
firstPatchedVersion: fixedEvent?.fixed,
|
|
214
|
-
source: "osv"
|
|
215
|
-
});
|
|
181
|
+
};
|
|
182
|
+
function loadPolicy(cwd, explicitPath) {
|
|
183
|
+
const candidate = explicitPath ?? join2(cwd, ".autoremediator.json");
|
|
184
|
+
if (!existsSync2(candidate)) return DEFAULT_POLICY;
|
|
185
|
+
try {
|
|
186
|
+
const parsed = JSON.parse(readFileSync(candidate, "utf8"));
|
|
187
|
+
return {
|
|
188
|
+
allowMajorBumps: parsed.allowMajorBumps ?? DEFAULT_POLICY.allowMajorBumps,
|
|
189
|
+
denyPackages: parsed.denyPackages ?? DEFAULT_POLICY.denyPackages,
|
|
190
|
+
allowPackages: parsed.allowPackages ?? DEFAULT_POLICY.allowPackages,
|
|
191
|
+
constraints: {
|
|
192
|
+
directDependenciesOnly: parsed.constraints?.directDependenciesOnly ?? DEFAULT_POLICY.constraints?.directDependenciesOnly ?? false,
|
|
193
|
+
preferVersionBump: parsed.constraints?.preferVersionBump ?? DEFAULT_POLICY.constraints?.preferVersionBump ?? false
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
} catch {
|
|
197
|
+
return DEFAULT_POLICY;
|
|
216
198
|
}
|
|
217
|
-
const severity = deriveSeverity(vuln.severity);
|
|
218
|
-
return {
|
|
219
|
-
id: vuln.id,
|
|
220
|
-
summary: vuln.summary ?? vuln.details ?? "No summary available.",
|
|
221
|
-
severity,
|
|
222
|
-
references: vuln.references?.map((r) => r.url) ?? [],
|
|
223
|
-
affectedPackages: npmAffected
|
|
224
|
-
};
|
|
225
199
|
}
|
|
226
|
-
function
|
|
227
|
-
if (
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (scoreMatch) {
|
|
231
|
-
const score = parseFloat(scoreMatch[1]);
|
|
232
|
-
if (score >= 9) return "CRITICAL";
|
|
233
|
-
if (score >= 7) return "HIGH";
|
|
234
|
-
if (score >= 4) return "MEDIUM";
|
|
235
|
-
return "LOW";
|
|
200
|
+
function isPackageAllowed(policy, packageName) {
|
|
201
|
+
if (policy.denyPackages.includes(packageName)) return false;
|
|
202
|
+
if (policy.allowPackages.length > 0 && !policy.allowPackages.includes(packageName)) {
|
|
203
|
+
return false;
|
|
236
204
|
}
|
|
237
|
-
return
|
|
238
|
-
}
|
|
239
|
-
async function lookupCveOsv(cveId) {
|
|
240
|
-
const vuln = await fetchOsvVuln(cveId);
|
|
241
|
-
if (!vuln) return null;
|
|
242
|
-
return parseOsvVuln(vuln);
|
|
205
|
+
return true;
|
|
243
206
|
}
|
|
244
207
|
|
|
245
|
-
// src/
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
"X-GitHub-Api-Version": "2022-11-28"
|
|
251
|
-
};
|
|
252
|
-
const token = getGitHubToken();
|
|
253
|
-
if (token) {
|
|
254
|
-
headers.Authorization = `Bearer ${token}`;
|
|
255
|
-
}
|
|
256
|
-
return headers;
|
|
257
|
-
}
|
|
258
|
-
async function fetchGhAdvisories(cveId) {
|
|
259
|
-
const url = new URL(GH_ADVISORY_BASE);
|
|
260
|
-
url.searchParams.set("cve_id", cveId);
|
|
261
|
-
url.searchParams.set("ecosystem", "npm");
|
|
262
|
-
url.searchParams.set("type", "reviewed");
|
|
263
|
-
url.searchParams.set("per_page", "10");
|
|
264
|
-
const res = await fetch(url.toString(), { headers: buildHeaders() });
|
|
265
|
-
if (res.status === 404) return [];
|
|
266
|
-
if (!res.ok) {
|
|
267
|
-
console.warn(
|
|
268
|
-
`[autoremediator] GitHub Advisory API returned ${res.status} for ${cveId} \u2014 skipping.`
|
|
269
|
-
);
|
|
270
|
-
return [];
|
|
271
|
-
}
|
|
272
|
-
return res.json();
|
|
273
|
-
}
|
|
274
|
-
function parseGhAdvisories(advisories) {
|
|
275
|
-
const packages = [];
|
|
276
|
-
for (const advisory of advisories) {
|
|
277
|
-
for (const vuln of advisory.vulnerabilities) {
|
|
278
|
-
if (vuln.package.ecosystem.toLowerCase() !== "npm") continue;
|
|
279
|
-
packages.push({
|
|
280
|
-
name: vuln.package.name,
|
|
281
|
-
ecosystem: "npm",
|
|
282
|
-
vulnerableRange: vuln.vulnerable_version_range ?? ">=0.0.0",
|
|
283
|
-
firstPatchedVersion: vuln.first_patched_version ?? void 0,
|
|
284
|
-
source: "github-advisory"
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
return packages;
|
|
208
|
+
// src/platform/repo-lock.ts
|
|
209
|
+
import { mkdir, rm } from "fs/promises";
|
|
210
|
+
import { join as join3 } from "path";
|
|
211
|
+
async function sleep(ms) {
|
|
212
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
289
213
|
}
|
|
290
|
-
function
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
214
|
+
async function acquireRepoLock(cwd, options = {}) {
|
|
215
|
+
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
216
|
+
const retryDelayMs = options.retryDelayMs ?? 125;
|
|
217
|
+
const lockRoot = join3(cwd, ".autoremediator", "locks");
|
|
218
|
+
const lockPath = join3(cwd, ".autoremediator", "locks", "remediation.lock");
|
|
219
|
+
const startedAt = Date.now();
|
|
220
|
+
await mkdir(lockRoot, { recursive: true });
|
|
221
|
+
while (true) {
|
|
222
|
+
try {
|
|
223
|
+
await mkdir(lockPath, { recursive: false });
|
|
224
|
+
return {
|
|
225
|
+
lockPath,
|
|
226
|
+
release: async () => {
|
|
227
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
} catch {
|
|
231
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
232
|
+
throw new Error(`Timed out waiting for repository lock at ${lockPath}.`);
|
|
299
233
|
}
|
|
300
|
-
|
|
301
|
-
enriched.affectedPackages.push(ghPkg);
|
|
234
|
+
await sleep(retryDelayMs);
|
|
302
235
|
}
|
|
303
236
|
}
|
|
304
|
-
return enriched;
|
|
305
|
-
}
|
|
306
|
-
async function lookupCveGitHub(cveId) {
|
|
307
|
-
const advisories = await fetchGhAdvisories(cveId);
|
|
308
|
-
return parseGhAdvisories(advisories);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// src/intelligence/sources/nvd.ts
|
|
312
|
-
var NVD_BASE = "https://services.nvd.nist.gov/rest/json/cves/2.0";
|
|
313
|
-
function buildNvdHeaders() {
|
|
314
|
-
const { apiKey } = getNvdConfig();
|
|
315
|
-
const headers = { Accept: "application/json" };
|
|
316
|
-
if (apiKey) {
|
|
317
|
-
headers.apiKey = apiKey;
|
|
318
|
-
}
|
|
319
|
-
return headers;
|
|
320
237
|
}
|
|
321
|
-
async function
|
|
322
|
-
const
|
|
238
|
+
async function withRepoLock(cwd, fn, options) {
|
|
239
|
+
const lock = await acquireRepoLock(cwd, options);
|
|
323
240
|
try {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const vuln = data.vulnerabilities?.[0];
|
|
328
|
-
if (!vuln) return void 0;
|
|
329
|
-
const metrics = vuln.cve.metrics;
|
|
330
|
-
const metric = metrics?.cvssMetricV31?.[0] ?? metrics?.cvssMetricV30?.[0] ?? metrics?.cvssMetricV2?.[0];
|
|
331
|
-
if (!metric) return void 0;
|
|
332
|
-
const score = metric.cvssData.baseScore;
|
|
333
|
-
const rawSeverity = metric.cvssData.baseSeverity.toUpperCase();
|
|
334
|
-
const severityMap = {
|
|
335
|
-
CRITICAL: "CRITICAL",
|
|
336
|
-
HIGH: "HIGH",
|
|
337
|
-
MEDIUM: "MEDIUM",
|
|
338
|
-
LOW: "LOW"
|
|
339
|
-
};
|
|
340
|
-
return {
|
|
341
|
-
score,
|
|
342
|
-
severity: severityMap[rawSeverity] ?? "UNKNOWN"
|
|
343
|
-
};
|
|
344
|
-
} catch {
|
|
345
|
-
return void 0;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
async function enrichWithNvd(details) {
|
|
349
|
-
const cvss = await fetchNvdCvss(details.id);
|
|
350
|
-
if (cvss) {
|
|
351
|
-
details.cvssScore = cvss.score;
|
|
352
|
-
if (details.severity === "UNKNOWN") {
|
|
353
|
-
details.severity = cvss.severity;
|
|
354
|
-
}
|
|
241
|
+
return await fn();
|
|
242
|
+
} finally {
|
|
243
|
+
await lock.release();
|
|
355
244
|
}
|
|
356
|
-
return details;
|
|
357
245
|
}
|
|
358
246
|
|
|
359
|
-
// src/
|
|
360
|
-
var
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
247
|
+
// src/remediation/tools/apply-version-bump.ts
|
|
248
|
+
var applyVersionBumpTool = tool({
|
|
249
|
+
description: "Update package.json to use the safe version of a vulnerable package and run the project's package manager install. In dry-run mode, only reports what would change.",
|
|
250
|
+
parameters: z.object({
|
|
251
|
+
cwd: z.string().describe("Absolute path to the consumer project root"),
|
|
252
|
+
packageManager: z.enum(["npm", "pnpm", "yarn"]).optional().describe("Package manager used by the target project (auto-detected if omitted)"),
|
|
253
|
+
packageName: z.string().describe("The npm package to upgrade"),
|
|
254
|
+
fromVersion: z.string().describe("The currently installed vulnerable version"),
|
|
255
|
+
toVersion: z.string().describe("The safe target version to upgrade to"),
|
|
256
|
+
dryRun: z.boolean().default(false).describe("If true, report changes but do not write"),
|
|
257
|
+
policy: z.string().optional().describe("Optional path to .autoremediator policy file"),
|
|
258
|
+
runTests: z.boolean().default(false).describe("If true, run test validation after applying the fix")
|
|
259
|
+
}),
|
|
260
|
+
execute: async ({
|
|
261
|
+
cwd,
|
|
262
|
+
packageManager,
|
|
263
|
+
packageName,
|
|
264
|
+
fromVersion,
|
|
265
|
+
toVersion,
|
|
266
|
+
dryRun,
|
|
267
|
+
policy,
|
|
268
|
+
runTests
|
|
269
|
+
}) => {
|
|
270
|
+
const pm = packageManager ?? detectPackageManager(cwd);
|
|
271
|
+
const commands = getPackageManagerCommands(pm);
|
|
272
|
+
const pkgPath = join4(cwd, "package.json");
|
|
273
|
+
const loadedPolicy = loadPolicy(cwd, policy);
|
|
274
|
+
if (!isPackageAllowed(loadedPolicy, packageName)) {
|
|
275
|
+
return {
|
|
276
|
+
packageName,
|
|
277
|
+
strategy: "none",
|
|
278
|
+
fromVersion,
|
|
279
|
+
toVersion,
|
|
280
|
+
applied: false,
|
|
281
|
+
dryRun,
|
|
282
|
+
unresolvedReason: "policy-blocked",
|
|
283
|
+
message: `Policy blocked changes for package "${packageName}".`
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const isMajorBump = semver.valid(fromVersion) && semver.valid(toVersion) && semver.major(toVersion) > semver.major(fromVersion);
|
|
287
|
+
if (isMajorBump && !loadedPolicy.allowMajorBumps) {
|
|
288
|
+
return {
|
|
289
|
+
packageName,
|
|
290
|
+
strategy: "none",
|
|
291
|
+
fromVersion,
|
|
292
|
+
toVersion,
|
|
293
|
+
applied: false,
|
|
294
|
+
dryRun,
|
|
295
|
+
unresolvedReason: "major-bump-required",
|
|
296
|
+
message: `Policy blocked major bump for "${packageName}" (${fromVersion} -> ${toVersion}).`
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
let pkgJson;
|
|
300
|
+
try {
|
|
301
|
+
pkgJson = JSON.parse(readFileSync2(pkgPath, "utf8"));
|
|
302
|
+
} catch {
|
|
303
|
+
return {
|
|
304
|
+
packageName,
|
|
305
|
+
strategy: "none",
|
|
306
|
+
fromVersion,
|
|
307
|
+
applied: false,
|
|
308
|
+
dryRun,
|
|
309
|
+
unresolvedReason: "package-json-not-found",
|
|
310
|
+
message: `Could not read package.json at "${pkgPath}".`
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const depField = ["dependencies", "devDependencies", "peerDependencies"].find(
|
|
314
|
+
(f) => pkgJson[f]?.[packageName] !== void 0
|
|
315
|
+
);
|
|
316
|
+
if (!depField) {
|
|
317
|
+
return {
|
|
318
|
+
packageName,
|
|
319
|
+
strategy: "none",
|
|
320
|
+
fromVersion,
|
|
321
|
+
applied: false,
|
|
322
|
+
dryRun,
|
|
323
|
+
unresolvedReason: "indirect-dependency",
|
|
324
|
+
message: `"${packageName}" was not found in package.json dependencies (it may be a transitive dep). Cannot auto-bump.`
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const currentRange = pkgJson[depField][packageName];
|
|
328
|
+
const prefixMatch = currentRange.match(/^([~^]?)/);
|
|
329
|
+
const prefix = prefixMatch?.[1] ?? "";
|
|
330
|
+
const newRange = `${prefix}${toVersion}`;
|
|
331
|
+
if (dryRun) {
|
|
332
|
+
const installCmd = commands.installPreferOffline.join(" ");
|
|
333
|
+
const testCmd = commands.test.join(" ");
|
|
334
|
+
return {
|
|
335
|
+
packageName,
|
|
336
|
+
strategy: "version-bump",
|
|
337
|
+
fromVersion,
|
|
338
|
+
toVersion,
|
|
339
|
+
applied: false,
|
|
340
|
+
dryRun: true,
|
|
341
|
+
message: `[DRY RUN] Would update ${depField}.${packageName}: "${currentRange}" -> "${newRange}", then run ${installCmd}${runTests ? ` and ${testCmd}` : ""}.`
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
return withRepoLock(cwd, async () => {
|
|
345
|
+
pkgJson[depField][packageName] = newRange;
|
|
346
|
+
writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
347
|
+
try {
|
|
348
|
+
const [installCmd, ...installArgs] = commands.installPreferOffline;
|
|
349
|
+
await execa(installCmd, installArgs, {
|
|
350
|
+
cwd,
|
|
351
|
+
stdio: "pipe"
|
|
352
|
+
});
|
|
353
|
+
} catch (err) {
|
|
354
|
+
pkgJson[depField][packageName] = currentRange;
|
|
355
|
+
writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
356
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
357
|
+
return {
|
|
358
|
+
packageName,
|
|
359
|
+
strategy: "version-bump",
|
|
360
|
+
fromVersion,
|
|
361
|
+
toVersion,
|
|
362
|
+
applied: false,
|
|
363
|
+
dryRun: false,
|
|
364
|
+
unresolvedReason: "install-failed",
|
|
365
|
+
message: `${commands.installPreferOffline.join(" ")} failed after updating "${packageName}" to ${toVersion}. Reverted. Error: ${message}`
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
if (runTests) {
|
|
369
|
+
try {
|
|
370
|
+
const [testCmd, ...testArgs] = commands.test;
|
|
371
|
+
await execa(testCmd, testArgs, {
|
|
372
|
+
cwd,
|
|
373
|
+
stdio: "pipe"
|
|
374
|
+
});
|
|
375
|
+
} catch (err) {
|
|
376
|
+
pkgJson[depField][packageName] = currentRange;
|
|
377
|
+
writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
378
|
+
try {
|
|
379
|
+
const [rollbackCmd, ...rollbackArgs] = commands.installPreferOffline;
|
|
380
|
+
await execa(rollbackCmd, rollbackArgs, {
|
|
381
|
+
cwd,
|
|
382
|
+
stdio: "pipe"
|
|
383
|
+
});
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
386
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
387
|
+
return {
|
|
388
|
+
packageName,
|
|
389
|
+
strategy: "version-bump",
|
|
390
|
+
fromVersion,
|
|
391
|
+
toVersion,
|
|
392
|
+
applied: false,
|
|
393
|
+
dryRun: false,
|
|
394
|
+
unresolvedReason: "validation-failed",
|
|
395
|
+
message: `${commands.test.join(" ")} failed after upgrading "${packageName}" to ${toVersion}. Rolled back to ${currentRange}. Error: ${message}`
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
packageName,
|
|
401
|
+
strategy: "version-bump",
|
|
402
|
+
fromVersion,
|
|
403
|
+
toVersion,
|
|
404
|
+
applied: true,
|
|
405
|
+
dryRun: false,
|
|
406
|
+
message: `Successfully upgraded "${packageName}" from ${fromVersion} to ${toVersion}, ran ${commands.installPreferOffline.join(" ")}${runTests ? `, and passed ${commands.test.join(" ")}` : ""}.`
|
|
407
|
+
};
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// src/remediation/tools/apply-package-override.ts
|
|
413
|
+
import { tool as tool2 } from "ai";
|
|
414
|
+
import { z as z2 } from "zod";
|
|
415
|
+
import { join as join5 } from "path";
|
|
416
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
417
|
+
import { execa as execa2 } from "execa";
|
|
418
|
+
import semver2 from "semver";
|
|
419
|
+
var applyPackageOverrideTool = tool2({
|
|
420
|
+
description: "Apply a package-manager-native package.json override for a vulnerable transitive dependency and reinstall. Uses npm overrides, pnpm.overrides, or yarn resolutions.",
|
|
421
|
+
parameters: z2.object({
|
|
422
|
+
cwd: z2.string().describe("Absolute path to the consumer project root"),
|
|
423
|
+
packageManager: z2.enum(["npm", "pnpm", "yarn"]).optional().describe("Package manager used by the target project (auto-detected if omitted)"),
|
|
424
|
+
packageName: z2.string().describe("The npm package to override"),
|
|
425
|
+
fromVersion: z2.string().describe("The currently installed vulnerable version"),
|
|
426
|
+
toVersion: z2.string().describe("The safe target version to override to"),
|
|
427
|
+
dryRun: z2.boolean().default(false).describe("If true, report changes but do not write"),
|
|
428
|
+
policy: z2.string().optional().describe("Optional path to .autoremediator policy file"),
|
|
429
|
+
runTests: z2.boolean().default(false).describe("If true, run test validation after applying the override")
|
|
430
|
+
}),
|
|
431
|
+
execute: async ({
|
|
432
|
+
cwd,
|
|
433
|
+
packageManager,
|
|
434
|
+
packageName,
|
|
435
|
+
fromVersion,
|
|
436
|
+
toVersion,
|
|
437
|
+
dryRun,
|
|
438
|
+
policy,
|
|
439
|
+
runTests
|
|
440
|
+
}) => {
|
|
441
|
+
const pm = packageManager ?? detectPackageManager(cwd);
|
|
442
|
+
const commands = getPackageManagerCommands(pm);
|
|
443
|
+
const pkgPath = join5(cwd, "package.json");
|
|
444
|
+
const loadedPolicy = loadPolicy(cwd, policy);
|
|
445
|
+
if (!isPackageAllowed(loadedPolicy, packageName)) {
|
|
446
|
+
return {
|
|
447
|
+
packageName,
|
|
448
|
+
strategy: "none",
|
|
449
|
+
fromVersion,
|
|
450
|
+
toVersion,
|
|
451
|
+
applied: false,
|
|
452
|
+
dryRun,
|
|
453
|
+
unresolvedReason: "policy-blocked",
|
|
454
|
+
message: `Policy blocked changes for package "${packageName}".`
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
const isMajorBump = semver2.valid(fromVersion) && semver2.valid(toVersion) && semver2.major(toVersion) > semver2.major(fromVersion);
|
|
458
|
+
if (isMajorBump && !loadedPolicy.allowMajorBumps) {
|
|
459
|
+
return {
|
|
460
|
+
packageName,
|
|
461
|
+
strategy: "none",
|
|
462
|
+
fromVersion,
|
|
463
|
+
toVersion,
|
|
464
|
+
applied: false,
|
|
465
|
+
dryRun,
|
|
466
|
+
unresolvedReason: "major-bump-required",
|
|
467
|
+
message: `Policy blocked major override for "${packageName}" (${fromVersion} -> ${toVersion}).`
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
let pkgJson;
|
|
471
|
+
try {
|
|
472
|
+
pkgJson = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
473
|
+
} catch {
|
|
474
|
+
return {
|
|
475
|
+
packageName,
|
|
476
|
+
strategy: "none",
|
|
477
|
+
fromVersion,
|
|
478
|
+
toVersion,
|
|
479
|
+
applied: false,
|
|
480
|
+
dryRun,
|
|
481
|
+
unresolvedReason: "package-json-not-found",
|
|
482
|
+
message: `Could not read package.json at "${pkgPath}".`
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
const overrideLabel = describeOverrideField(pm);
|
|
486
|
+
const previousValue = getOverrideValue(pkgJson, pm, packageName);
|
|
487
|
+
if (dryRun) {
|
|
488
|
+
return {
|
|
489
|
+
packageName,
|
|
490
|
+
strategy: "override",
|
|
491
|
+
fromVersion,
|
|
492
|
+
toVersion,
|
|
493
|
+
applied: false,
|
|
494
|
+
dryRun: true,
|
|
495
|
+
message: `[DRY RUN] Would set ${overrideLabel}.${packageName} to "${toVersion}", then run ${commands.installPreferOffline.join(" ")}${runTests ? ` and ${commands.test.join(" ")}` : ""}.`
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
return withRepoLock(cwd, async () => {
|
|
499
|
+
setOverrideValue(pkgJson, pm, packageName, toVersion);
|
|
500
|
+
writeFileSync2(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
501
|
+
try {
|
|
502
|
+
const [installCmd, ...installArgs] = commands.installPreferOffline;
|
|
503
|
+
await execa2(installCmd, installArgs, { cwd, stdio: "pipe" });
|
|
504
|
+
} catch (err) {
|
|
505
|
+
restoreOverrideValue(pkgJson, pm, packageName, previousValue);
|
|
506
|
+
writeFileSync2(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
507
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
508
|
+
return {
|
|
509
|
+
packageName,
|
|
510
|
+
strategy: "override",
|
|
511
|
+
fromVersion,
|
|
512
|
+
toVersion,
|
|
513
|
+
applied: false,
|
|
514
|
+
dryRun: false,
|
|
515
|
+
unresolvedReason: "override-apply-failed",
|
|
516
|
+
message: `${commands.installPreferOffline.join(" ")} failed after applying ${overrideLabel} for "${packageName}" to ${toVersion}. Reverted. Error: ${message}`
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
if (runTests) {
|
|
520
|
+
try {
|
|
521
|
+
const [testCmd, ...testArgs] = commands.test;
|
|
522
|
+
await execa2(testCmd, testArgs, { cwd, stdio: "pipe" });
|
|
523
|
+
} catch (err) {
|
|
524
|
+
restoreOverrideValue(pkgJson, pm, packageName, previousValue);
|
|
525
|
+
writeFileSync2(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
526
|
+
try {
|
|
527
|
+
const [rollbackCmd, ...rollbackArgs] = commands.installPreferOffline;
|
|
528
|
+
await execa2(rollbackCmd, rollbackArgs, { cwd, stdio: "pipe" });
|
|
529
|
+
} catch {
|
|
530
|
+
}
|
|
531
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
532
|
+
return {
|
|
533
|
+
packageName,
|
|
534
|
+
strategy: "override",
|
|
535
|
+
fromVersion,
|
|
536
|
+
toVersion,
|
|
537
|
+
applied: false,
|
|
538
|
+
dryRun: false,
|
|
539
|
+
unresolvedReason: "validation-failed",
|
|
540
|
+
message: `${commands.test.join(" ")} failed after applying ${overrideLabel} for "${packageName}" to ${toVersion}. Reverted. Error: ${message}`
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
packageName,
|
|
546
|
+
strategy: "override",
|
|
547
|
+
fromVersion,
|
|
548
|
+
toVersion,
|
|
549
|
+
applied: true,
|
|
550
|
+
dryRun: false,
|
|
551
|
+
message: `Successfully applied ${overrideLabel} for "${packageName}" from ${fromVersion} to ${toVersion}, then ran ${commands.installPreferOffline.join(" ")}${runTests ? ` and passed ${commands.test.join(" ")}` : ""}.`
|
|
552
|
+
};
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
function describeOverrideField(packageManager) {
|
|
557
|
+
if (packageManager === "npm") return "overrides";
|
|
558
|
+
if (packageManager === "pnpm") return "pnpm.overrides";
|
|
559
|
+
return "resolutions";
|
|
560
|
+
}
|
|
561
|
+
function getOverrideValue(pkgJson, packageManager, packageName) {
|
|
562
|
+
if (packageManager === "npm") return pkgJson.overrides?.[packageName];
|
|
563
|
+
if (packageManager === "pnpm") return pkgJson.pnpm?.overrides?.[packageName];
|
|
564
|
+
return pkgJson.resolutions?.[packageName];
|
|
565
|
+
}
|
|
566
|
+
function setOverrideValue(pkgJson, packageManager, packageName, version) {
|
|
567
|
+
if (packageManager === "npm") {
|
|
568
|
+
pkgJson.overrides = { ...pkgJson.overrides ?? {}, [packageName]: version };
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (packageManager === "pnpm") {
|
|
572
|
+
pkgJson.pnpm = {
|
|
573
|
+
...pkgJson.pnpm ?? {},
|
|
574
|
+
overrides: {
|
|
575
|
+
...pkgJson.pnpm?.overrides ?? {},
|
|
576
|
+
[packageName]: version
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
pkgJson.resolutions = { ...pkgJson.resolutions ?? {}, [packageName]: version };
|
|
582
|
+
}
|
|
583
|
+
function restoreOverrideValue(pkgJson, packageManager, packageName, previousValue) {
|
|
584
|
+
if (packageManager === "npm") {
|
|
585
|
+
pkgJson.overrides = restoreRecord(pkgJson.overrides, packageName, previousValue);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (packageManager === "pnpm") {
|
|
589
|
+
pkgJson.pnpm = {
|
|
590
|
+
...pkgJson.pnpm ?? {},
|
|
591
|
+
overrides: restoreRecord(pkgJson.pnpm?.overrides, packageName, previousValue)
|
|
592
|
+
};
|
|
593
|
+
if (!pkgJson.pnpm.overrides) {
|
|
594
|
+
delete pkgJson.pnpm.overrides;
|
|
595
|
+
}
|
|
596
|
+
if (Object.keys(pkgJson.pnpm).length === 0) {
|
|
597
|
+
delete pkgJson.pnpm;
|
|
598
|
+
}
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
pkgJson.resolutions = restoreRecord(pkgJson.resolutions, packageName, previousValue);
|
|
602
|
+
}
|
|
603
|
+
function restoreRecord(record, key, previousValue) {
|
|
604
|
+
const nextRecord = { ...record ?? {} };
|
|
605
|
+
if (previousValue === void 0) {
|
|
606
|
+
delete nextRecord[key];
|
|
607
|
+
} else {
|
|
608
|
+
nextRecord[key] = previousValue;
|
|
609
|
+
}
|
|
610
|
+
return Object.keys(nextRecord).length > 0 ? nextRecord : void 0;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// src/remediation/tools/apply-patch-file.ts
|
|
614
|
+
import { tool as tool3 } from "ai";
|
|
615
|
+
import { z as z3 } from "zod";
|
|
616
|
+
import { existsSync as existsSync4 } from "fs";
|
|
617
|
+
import { mkdir as mkdir2, mkdtemp, readFile, rm as rm2, writeFile } from "fs/promises";
|
|
618
|
+
import { tmpdir } from "os";
|
|
619
|
+
import { join as join7 } from "path";
|
|
620
|
+
import { execa as execa4 } from "execa";
|
|
621
|
+
|
|
622
|
+
// src/remediation/strategies/patch-utils.ts
|
|
623
|
+
import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync3, readFileSync as readFileSync4 } from "fs";
|
|
624
|
+
import { join as join6 } from "path";
|
|
625
|
+
import { execa as execa3 } from "execa";
|
|
626
|
+
function validatePatchDiff(patchContent) {
|
|
627
|
+
if (!patchContent || typeof patchContent !== "string") {
|
|
628
|
+
return {
|
|
629
|
+
valid: false,
|
|
630
|
+
error: "Patch content must be a non-empty string"
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
const hasFromLine = /^---\s+\S+/m.test(patchContent);
|
|
634
|
+
const hasToLine = /^\+\+\+\s+\S+/m.test(patchContent);
|
|
635
|
+
const hasHunkHeader = /^@@\s+-\d+/m.test(patchContent);
|
|
636
|
+
if (!hasFromLine) {
|
|
637
|
+
return {
|
|
638
|
+
valid: false,
|
|
639
|
+
error: 'Missing "---" line in patch format'
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
if (!hasToLine) {
|
|
643
|
+
return {
|
|
644
|
+
valid: false,
|
|
645
|
+
error: 'Missing "+++" line in patch format'
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
if (!hasHunkHeader) {
|
|
649
|
+
return {
|
|
650
|
+
valid: false,
|
|
651
|
+
error: "No hunk headers (@@...) found in patch"
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
return { valid: true };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/remediation/tools/apply-patch-file.ts
|
|
658
|
+
var applyPatchFileTool = tool3({
|
|
659
|
+
description: "Write generated patch file and apply it using package-manager-native patch flow when available, falling back to patch-package when needed.",
|
|
660
|
+
parameters: z3.object({
|
|
661
|
+
packageName: z3.string().min(1).describe("The npm package name"),
|
|
662
|
+
vulnerableVersion: z3.string().describe("The vulnerable version string"),
|
|
663
|
+
patchContent: z3.string().min(10).optional().describe("Unified diff patch content from generate-patch"),
|
|
664
|
+
patches: z3.array(
|
|
665
|
+
z3.object({
|
|
666
|
+
filePath: z3.string().min(1),
|
|
667
|
+
unifiedDiff: z3.string().min(10)
|
|
668
|
+
})
|
|
669
|
+
).optional().describe("Patch list from generate-patch; first patch is applied"),
|
|
670
|
+
patchesDir: z3.string().optional().default("./patches").describe("Directory to store patch files"),
|
|
671
|
+
cwd: z3.string().describe("Project root directory (for package.json)"),
|
|
672
|
+
packageManager: z3.enum(["npm", "pnpm", "yarn"]).optional().describe("Package manager used by the target project (auto-detected if omitted)"),
|
|
673
|
+
validateWithTests: z3.boolean().optional().default(true).describe("Run package manager test command to validate patch doesn't break anything"),
|
|
674
|
+
dryRun: z3.boolean().optional().default(false).describe("If true, report but do not mutate files")
|
|
675
|
+
}).refine((value) => Boolean(value.patchContent || value.patches && value.patches.length > 0), {
|
|
676
|
+
message: "Either patchContent or patches must be provided"
|
|
677
|
+
}),
|
|
678
|
+
execute: async ({
|
|
679
|
+
packageName,
|
|
680
|
+
vulnerableVersion,
|
|
681
|
+
patchContent,
|
|
682
|
+
patches,
|
|
683
|
+
patchesDir,
|
|
684
|
+
cwd,
|
|
685
|
+
packageManager,
|
|
686
|
+
validateWithTests,
|
|
687
|
+
dryRun
|
|
688
|
+
}) => {
|
|
689
|
+
try {
|
|
690
|
+
const pm = packageManager ?? detectPackageManager(cwd);
|
|
691
|
+
const selectedPatch = patchContent ?? patches?.[0]?.unifiedDiff;
|
|
692
|
+
if (!selectedPatch) {
|
|
693
|
+
return {
|
|
694
|
+
success: false,
|
|
695
|
+
packageName,
|
|
696
|
+
vulnerableVersion,
|
|
697
|
+
applied: false,
|
|
698
|
+
dryRun,
|
|
699
|
+
message: "No patch content provided.",
|
|
700
|
+
error: "No patch content provided."
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
const patchValidation = validatePatchDiff(selectedPatch);
|
|
704
|
+
if (!patchValidation.valid) {
|
|
705
|
+
return {
|
|
706
|
+
success: false,
|
|
707
|
+
packageName,
|
|
708
|
+
vulnerableVersion,
|
|
709
|
+
applied: false,
|
|
710
|
+
dryRun,
|
|
711
|
+
message: patchValidation.error ?? "Patch content is not a valid unified diff.",
|
|
712
|
+
error: patchValidation.error ?? "Patch content is not a valid unified diff."
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
const patchFileName = buildPatchFileName(packageName, vulnerableVersion);
|
|
716
|
+
const patchFilePath = join7(cwd, patchesDir, patchFileName);
|
|
717
|
+
if (dryRun) {
|
|
718
|
+
return {
|
|
719
|
+
success: true,
|
|
720
|
+
packageName,
|
|
721
|
+
vulnerableVersion,
|
|
722
|
+
applied: false,
|
|
723
|
+
dryRun: true,
|
|
724
|
+
message: `[DRY RUN] Would write and configure patch at ${patchFilePath}.`,
|
|
725
|
+
patchFilePath,
|
|
726
|
+
patchPath: patchFilePath
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
return withRepoLock(cwd, async () => {
|
|
730
|
+
const packageJsonSnapshot = patchModeRequiresPackageJsonSnapshot(pm, cwd) ? await capturePackageJsonSnapshot(cwd) : void 0;
|
|
731
|
+
const patchesDirPath = join7(cwd, patchesDir);
|
|
732
|
+
await mkdir2(patchesDirPath, { recursive: true });
|
|
733
|
+
await writeFile(patchFilePath, selectedPatch, "utf8");
|
|
734
|
+
let validationResult;
|
|
735
|
+
const patchMode = await resolvePatchMode(pm, cwd);
|
|
736
|
+
const commands = getPackageManagerCommands(pm);
|
|
737
|
+
const applyResult = patchMode === "patch-package" ? await configurePatchPackagePostinstall(cwd, pm) : await applyNativePatch({
|
|
738
|
+
cwd,
|
|
739
|
+
packageName,
|
|
740
|
+
vulnerableVersion,
|
|
741
|
+
patchContent: selectedPatch,
|
|
742
|
+
patchMode
|
|
743
|
+
});
|
|
744
|
+
if (!applyResult.success) {
|
|
745
|
+
await cleanupPatchArtifacts({
|
|
746
|
+
cwd,
|
|
747
|
+
packageManager: pm,
|
|
748
|
+
patchFilePath,
|
|
749
|
+
patchMode,
|
|
750
|
+
packageJsonSnapshot,
|
|
751
|
+
rerunInstall: patchMode === "patch-package"
|
|
752
|
+
});
|
|
753
|
+
return {
|
|
754
|
+
success: false,
|
|
755
|
+
packageName,
|
|
756
|
+
vulnerableVersion,
|
|
757
|
+
applied: false,
|
|
758
|
+
dryRun: false,
|
|
759
|
+
message: applyResult.error,
|
|
760
|
+
patchFilePath,
|
|
761
|
+
patchPath: patchFilePath,
|
|
762
|
+
patchMode,
|
|
763
|
+
postinstallConfigured: patchMode === "patch-package" ? false : void 0,
|
|
764
|
+
error: applyResult.error
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
if (patchMode === "patch-package") {
|
|
768
|
+
try {
|
|
769
|
+
const [installCmd, ...installArgs] = commands.installPreferOffline;
|
|
770
|
+
await execa4(installCmd, installArgs, {
|
|
771
|
+
cwd,
|
|
772
|
+
stdio: "pipe"
|
|
773
|
+
});
|
|
774
|
+
} catch (err) {
|
|
775
|
+
await cleanupPatchArtifacts({
|
|
776
|
+
cwd,
|
|
777
|
+
packageManager: pm,
|
|
778
|
+
patchFilePath,
|
|
779
|
+
patchMode,
|
|
780
|
+
packageJsonSnapshot,
|
|
781
|
+
rerunInstall: true
|
|
782
|
+
});
|
|
783
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
784
|
+
return {
|
|
785
|
+
success: false,
|
|
786
|
+
packageName,
|
|
787
|
+
vulnerableVersion,
|
|
788
|
+
applied: false,
|
|
789
|
+
dryRun: false,
|
|
790
|
+
message: `Failed to apply patch-package workflow for ${packageName}@${vulnerableVersion}: ${error}`,
|
|
791
|
+
patchFilePath,
|
|
792
|
+
patchPath: patchFilePath,
|
|
793
|
+
patchMode,
|
|
794
|
+
postinstallConfigured: false,
|
|
795
|
+
error: `Failed to apply patch-package workflow for ${packageName}@${vulnerableVersion}: ${error}`
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (validateWithTests) {
|
|
800
|
+
validationResult = await validatePatchWithTests(cwd, pm);
|
|
801
|
+
if (!validationResult.passed) {
|
|
802
|
+
await cleanupPatchArtifacts({
|
|
803
|
+
cwd,
|
|
804
|
+
packageManager: pm,
|
|
805
|
+
patchFilePath,
|
|
806
|
+
patchMode,
|
|
807
|
+
packageJsonSnapshot,
|
|
808
|
+
rerunInstall: patchMode === "patch-package"
|
|
809
|
+
});
|
|
810
|
+
const validationError = "Patch validation failed after apply; patch marked unresolved.";
|
|
811
|
+
return {
|
|
812
|
+
success: false,
|
|
813
|
+
packageName,
|
|
814
|
+
vulnerableVersion,
|
|
815
|
+
applied: false,
|
|
816
|
+
dryRun: false,
|
|
817
|
+
message: validationError,
|
|
818
|
+
patchFilePath,
|
|
819
|
+
patchPath: patchFilePath,
|
|
820
|
+
patchMode,
|
|
821
|
+
postinstallConfigured: false,
|
|
822
|
+
validation: validationResult,
|
|
823
|
+
error: validationError
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
success: true,
|
|
829
|
+
packageName,
|
|
830
|
+
vulnerableVersion,
|
|
831
|
+
applied: true,
|
|
832
|
+
dryRun: false,
|
|
833
|
+
message: `Patch applied successfully for ${packageName}@${vulnerableVersion}.`,
|
|
834
|
+
patchFilePath,
|
|
835
|
+
patchPath: patchFilePath,
|
|
836
|
+
patchMode,
|
|
837
|
+
postinstallConfigured: patchMode === "patch-package",
|
|
838
|
+
validation: validationResult
|
|
839
|
+
};
|
|
840
|
+
});
|
|
841
|
+
} catch (err) {
|
|
842
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
843
|
+
return {
|
|
844
|
+
success: false,
|
|
845
|
+
packageName,
|
|
846
|
+
vulnerableVersion,
|
|
847
|
+
applied: false,
|
|
848
|
+
dryRun,
|
|
849
|
+
message: `Failed to apply patch file: ${message}`,
|
|
850
|
+
error: `Failed to apply patch file: ${message}`
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
async function resolvePatchMode(packageManager, cwd) {
|
|
856
|
+
if (packageManager === "npm") return "patch-package";
|
|
857
|
+
if (packageManager === "pnpm") return "native-pnpm";
|
|
858
|
+
try {
|
|
859
|
+
const result = await execa4("yarn", ["--version"], {
|
|
860
|
+
cwd,
|
|
861
|
+
stdio: "pipe"
|
|
862
|
+
});
|
|
863
|
+
const version = result.stdout.trim();
|
|
864
|
+
const major = Number.parseInt(version.split(".")[0] || "0", 10);
|
|
865
|
+
return major >= 2 ? "native-yarn" : "patch-package";
|
|
866
|
+
} catch {
|
|
867
|
+
return "patch-package";
|
|
868
|
+
}
|
|
376
869
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
870
|
+
function patchModeRequiresPackageJsonSnapshot(packageManager, cwd) {
|
|
871
|
+
if (packageManager === "npm") return true;
|
|
872
|
+
if (packageManager === "pnpm") return false;
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
function buildPatchFileName(packageName, vulnerableVersion) {
|
|
876
|
+
const safeName = packageName.replace(/^@/, "").replace(/\//g, "+");
|
|
877
|
+
return `${safeName}+${vulnerableVersion}.patch`;
|
|
878
|
+
}
|
|
879
|
+
async function configurePatchPackagePostinstall(cwd, packageManager) {
|
|
880
|
+
const pkgJsonPath = join7(cwd, "package.json");
|
|
881
|
+
let pkgJson;
|
|
882
|
+
try {
|
|
883
|
+
pkgJson = JSON.parse(await readFile(pkgJsonPath, "utf8"));
|
|
884
|
+
} catch {
|
|
885
|
+
return {
|
|
886
|
+
success: false,
|
|
887
|
+
error: `Could not read package.json at ${pkgJsonPath}`
|
|
888
|
+
};
|
|
390
889
|
}
|
|
391
|
-
|
|
890
|
+
const devDependencies = pkgJson.devDependencies ?? {};
|
|
891
|
+
if (!devDependencies["patch-package"]) {
|
|
892
|
+
try {
|
|
893
|
+
const commands = getPackageManagerCommands(packageManager);
|
|
894
|
+
const [cmd, ...args] = commands.installDev("patch-package");
|
|
895
|
+
await execa4(cmd, args, {
|
|
896
|
+
cwd,
|
|
897
|
+
stdio: "pipe"
|
|
898
|
+
});
|
|
899
|
+
} catch (err) {
|
|
900
|
+
return {
|
|
901
|
+
success: false,
|
|
902
|
+
error: `Failed to install patch-package: ${err instanceof Error ? err.message : String(err)}`
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (!pkgJson.scripts) {
|
|
907
|
+
pkgJson.scripts = {};
|
|
908
|
+
}
|
|
909
|
+
const patchApplyCmd = "patch-package";
|
|
910
|
+
const currentPostinstall = pkgJson.scripts.postinstall || "";
|
|
911
|
+
if (currentPostinstall && !currentPostinstall.includes("patch-package")) {
|
|
912
|
+
pkgJson.scripts.postinstall = `${currentPostinstall} && ${patchApplyCmd}`;
|
|
913
|
+
} else if (!currentPostinstall) {
|
|
914
|
+
pkgJson.scripts.postinstall = patchApplyCmd;
|
|
915
|
+
}
|
|
916
|
+
await writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
917
|
+
return { success: true };
|
|
392
918
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
async function fetchEpss(cveId) {
|
|
396
|
-
const { epssApi } = getIntelligenceSourceConfig();
|
|
397
|
-
if (!epssApi) return void 0;
|
|
919
|
+
async function capturePackageJsonSnapshot(cwd) {
|
|
920
|
+
const path = join7(cwd, "package.json");
|
|
398
921
|
try {
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
const res = await fetch(url.toString(), {
|
|
402
|
-
headers: { Accept: "application/json" }
|
|
403
|
-
});
|
|
404
|
-
if (!res.ok) return void 0;
|
|
405
|
-
const body = await res.json();
|
|
406
|
-
return body.data?.[0];
|
|
922
|
+
const content = await readFile(path, "utf8");
|
|
923
|
+
return { path, content };
|
|
407
924
|
} catch {
|
|
408
925
|
return void 0;
|
|
409
926
|
}
|
|
410
927
|
}
|
|
411
|
-
async function
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
928
|
+
async function cleanupPatchArtifacts(params) {
|
|
929
|
+
const { cwd, packageManager, patchFilePath, patchMode, packageJsonSnapshot, rerunInstall } = params;
|
|
930
|
+
await rm2(patchFilePath, { force: true }).catch(() => void 0);
|
|
931
|
+
if (patchMode === "patch-package" && packageJsonSnapshot) {
|
|
932
|
+
await writeFile(packageJsonSnapshot.path, packageJsonSnapshot.content, "utf8").catch(() => void 0);
|
|
933
|
+
}
|
|
934
|
+
if (!rerunInstall) return;
|
|
935
|
+
try {
|
|
936
|
+
const commands = getPackageManagerCommands(packageManager);
|
|
937
|
+
const [installCmd, ...installArgs] = commands.installPreferOffline;
|
|
938
|
+
await execa4(installCmd, installArgs, {
|
|
939
|
+
cwd,
|
|
940
|
+
stdio: "pipe"
|
|
941
|
+
});
|
|
942
|
+
} catch {
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
async function applyNativePatch(params) {
|
|
946
|
+
const { cwd, packageName, vulnerableVersion, patchContent, patchMode } = params;
|
|
947
|
+
const packageSpec = `${packageName}@${vulnerableVersion}`;
|
|
948
|
+
const createCommand = patchMode === "native-pnpm" ? "pnpm" : "yarn";
|
|
949
|
+
const createArgs = ["patch", packageSpec];
|
|
950
|
+
let patchDir;
|
|
951
|
+
try {
|
|
952
|
+
const createResult = await execa4(createCommand, createArgs, {
|
|
953
|
+
cwd,
|
|
954
|
+
stdio: "pipe"
|
|
955
|
+
});
|
|
956
|
+
patchDir = extractPatchDirectory(`${createResult.stdout}
|
|
957
|
+
${createResult.stderr}`);
|
|
958
|
+
} catch (err) {
|
|
959
|
+
return {
|
|
960
|
+
success: false,
|
|
961
|
+
error: `Failed to create native patch workspace for ${packageSpec}: ${err instanceof Error ? err.message : String(err)}`
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
if (!patchDir) {
|
|
965
|
+
return {
|
|
966
|
+
success: false,
|
|
967
|
+
error: `Could not determine native patch directory for ${packageSpec}.`
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
const tempPatchDir = await mkdtemp(join7(tmpdir(), "autoremediator-native-patch-"));
|
|
971
|
+
const tempPatchFile = join7(tempPatchDir, "change.patch");
|
|
972
|
+
try {
|
|
973
|
+
await writeFile(tempPatchFile, patchContent, "utf8");
|
|
974
|
+
await execa4("patch", ["-p1", "-i", tempPatchFile], {
|
|
975
|
+
cwd: patchDir,
|
|
976
|
+
stdio: "pipe"
|
|
977
|
+
});
|
|
978
|
+
const commitCommand = patchMode === "native-pnpm" ? "pnpm" : "yarn";
|
|
979
|
+
const commitArgs = patchMode === "native-pnpm" ? ["patch-commit", patchDir] : ["patch-commit", "-s", patchDir];
|
|
980
|
+
await execa4(commitCommand, commitArgs, {
|
|
981
|
+
cwd,
|
|
982
|
+
stdio: "pipe"
|
|
983
|
+
});
|
|
984
|
+
} catch (err) {
|
|
985
|
+
return {
|
|
986
|
+
success: false,
|
|
987
|
+
error: `Failed to apply native patch for ${packageSpec}: ${err instanceof Error ? err.message : String(err)}`
|
|
988
|
+
};
|
|
989
|
+
} finally {
|
|
990
|
+
await rm2(tempPatchDir, { recursive: true, force: true });
|
|
418
991
|
}
|
|
419
|
-
|
|
420
|
-
score,
|
|
421
|
-
percentile,
|
|
422
|
-
date: row.date
|
|
423
|
-
};
|
|
424
|
-
return details;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// src/intelligence/sources/cve-services.ts
|
|
428
|
-
function pickEnglishDescription(container) {
|
|
429
|
-
if (!container?.descriptions?.length) return void 0;
|
|
430
|
-
const en = container.descriptions.find((d) => d.lang === "en" && d.value);
|
|
431
|
-
return (en?.value ?? container.descriptions[0]?.value)?.trim() || void 0;
|
|
992
|
+
return { success: true };
|
|
432
993
|
}
|
|
433
|
-
function
|
|
434
|
-
const
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
994
|
+
function extractPatchDirectory(output) {
|
|
995
|
+
const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
996
|
+
for (const line of lines) {
|
|
997
|
+
if (existsSync4(line)) {
|
|
998
|
+
return line;
|
|
999
|
+
}
|
|
1000
|
+
const tokens = line.split(/\s+/).map((token) => token.replace(/^['"]|['"]$/g, ""));
|
|
1001
|
+
for (const token of tokens) {
|
|
1002
|
+
if (token.startsWith("/") && existsSync4(token)) {
|
|
1003
|
+
return token;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
439
1006
|
}
|
|
440
|
-
return
|
|
1007
|
+
return "";
|
|
441
1008
|
}
|
|
442
|
-
async function
|
|
443
|
-
const { cveServicesApi } = getIntelligenceSourceConfig();
|
|
444
|
-
if (!cveServicesApi) return void 0;
|
|
1009
|
+
async function validatePatchWithTests(cwd, packageManager) {
|
|
445
1010
|
try {
|
|
446
|
-
const
|
|
447
|
-
|
|
1011
|
+
const commands = getPackageManagerCommands(packageManager);
|
|
1012
|
+
const [cmd, ...args] = commands.test;
|
|
1013
|
+
const result = await execa4(cmd, args, {
|
|
1014
|
+
cwd,
|
|
1015
|
+
timeout: 6e4,
|
|
1016
|
+
// 60 second timeout
|
|
1017
|
+
stdio: "pipe"
|
|
448
1018
|
});
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
1019
|
+
return {
|
|
1020
|
+
passed: true,
|
|
1021
|
+
output: result.stdout
|
|
1022
|
+
};
|
|
1023
|
+
} catch (err) {
|
|
1024
|
+
const errorOutput = typeof err === "object" && err !== null && "stdout" in err ? String(err.stdout ?? "") : "";
|
|
1025
|
+
const failedTests = extractFailedTests(errorOutput);
|
|
1026
|
+
return {
|
|
1027
|
+
passed: false,
|
|
1028
|
+
error: failedTests.length > 0 ? `Failed tests: ${failedTests.join(", ")}` : "Package-manager test validation failed.",
|
|
1029
|
+
output: errorOutput,
|
|
1030
|
+
failedTests
|
|
1031
|
+
};
|
|
453
1032
|
}
|
|
454
1033
|
}
|
|
455
|
-
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
1034
|
+
function extractFailedTests(output) {
|
|
1035
|
+
const failedTests = [];
|
|
1036
|
+
const patterns = [
|
|
1037
|
+
/✖\s+(.+?)(?:\n|$)/g,
|
|
1038
|
+
// Mocha style
|
|
1039
|
+
/●\s+(.+)(?:\n|$)/g,
|
|
1040
|
+
// Jest style
|
|
1041
|
+
/^FAIL\s+(.+?)(?:\n|$)/gm
|
|
1042
|
+
// Generic FAIL
|
|
1043
|
+
];
|
|
1044
|
+
for (const pattern of patterns) {
|
|
1045
|
+
let match;
|
|
1046
|
+
while ((match = pattern.exec(output)) !== null) {
|
|
1047
|
+
if (match[1]) {
|
|
1048
|
+
failedTests.push(match[1].trim());
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
466
1051
|
}
|
|
467
|
-
|
|
468
|
-
...details.intelligence ?? {},
|
|
469
|
-
cveServicesEnriched: true
|
|
470
|
-
};
|
|
471
|
-
return details;
|
|
1052
|
+
return failedTests.slice(0, 5);
|
|
472
1053
|
}
|
|
473
1054
|
|
|
474
|
-
// src/
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
1055
|
+
// src/remediation/local/run.ts
|
|
1056
|
+
import semver4 from "semver";
|
|
1057
|
+
|
|
1058
|
+
// src/intelligence/sources/osv.ts
|
|
1059
|
+
var OSV_BASE = "https://api.osv.dev/v1";
|
|
1060
|
+
async function fetchOsvVuln(cveId) {
|
|
1061
|
+
const url = `${OSV_BASE}/vulns/${encodeURIComponent(cveId)}`;
|
|
1062
|
+
const res = await fetch(url, {
|
|
1063
|
+
headers: { Accept: "application/json" }
|
|
1064
|
+
});
|
|
1065
|
+
if (res.status === 404) return null;
|
|
1066
|
+
if (!res.ok) {
|
|
1067
|
+
throw new Error(`OSV API error ${res.status} for ${cveId}: ${await res.text()}`);
|
|
1068
|
+
}
|
|
1069
|
+
return res.json();
|
|
480
1070
|
}
|
|
481
|
-
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
if (
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
} catch {
|
|
495
|
-
return [];
|
|
1071
|
+
function osvEventsToSemverRange(events) {
|
|
1072
|
+
const parts = [];
|
|
1073
|
+
for (const event of events) {
|
|
1074
|
+
if (event.introduced !== void 0) {
|
|
1075
|
+
const v = event.introduced === "0" ? "0.0.0" : event.introduced;
|
|
1076
|
+
parts.push(`>=${v}`);
|
|
1077
|
+
}
|
|
1078
|
+
if (event.fixed !== void 0) {
|
|
1079
|
+
parts.push(`<${event.fixed}`);
|
|
1080
|
+
}
|
|
1081
|
+
if (event.last_affected !== void 0) {
|
|
1082
|
+
parts.push(`<=${event.last_affected}`);
|
|
1083
|
+
}
|
|
496
1084
|
}
|
|
1085
|
+
return parts.join(" ") || ">=0.0.0";
|
|
497
1086
|
}
|
|
498
|
-
|
|
499
|
-
const
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
1087
|
+
function parseOsvVuln(vuln) {
|
|
1088
|
+
const npmAffected = [];
|
|
1089
|
+
for (const affected of vuln.affected ?? []) {
|
|
1090
|
+
const ecosystem = affected.package?.ecosystem;
|
|
1091
|
+
const packageName = affected.package?.name;
|
|
1092
|
+
if (!ecosystem || typeof ecosystem !== "string") continue;
|
|
1093
|
+
if (!packageName || typeof packageName !== "string") continue;
|
|
1094
|
+
if (ecosystem.toLowerCase() !== "npm") continue;
|
|
1095
|
+
const semverRange = affected.ranges?.find((r) => r.type === "SEMVER");
|
|
1096
|
+
const vulnerableRange = semverRange ? osvEventsToSemverRange(semverRange.events) : ">=0.0.0";
|
|
1097
|
+
const fixedEvent = semverRange?.events.find((e) => e.fixed !== void 0);
|
|
1098
|
+
npmAffected.push({
|
|
1099
|
+
name: packageName,
|
|
1100
|
+
ecosystem: "npm",
|
|
1101
|
+
vulnerableRange,
|
|
1102
|
+
firstPatchedVersion: fixedEvent?.fixed,
|
|
1103
|
+
source: "osv"
|
|
1104
|
+
});
|
|
506
1105
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
1106
|
+
const severity = deriveSeverity(vuln.severity);
|
|
1107
|
+
return {
|
|
1108
|
+
id: vuln.id,
|
|
1109
|
+
summary: vuln.summary ?? vuln.details ?? "No summary available.",
|
|
1110
|
+
severity,
|
|
1111
|
+
references: vuln.references?.map((r) => r.url) ?? [],
|
|
1112
|
+
affectedPackages: npmAffected
|
|
510
1113
|
};
|
|
511
|
-
|
|
1114
|
+
}
|
|
1115
|
+
function deriveSeverity(severityEntries) {
|
|
1116
|
+
if (!severityEntries?.length) return "UNKNOWN";
|
|
1117
|
+
const cvssEntry = severityEntries.find((s) => s.type === "CVSS_V3") ?? severityEntries[0];
|
|
1118
|
+
const scoreMatch = cvssEntry.score.match(/(\d+\.\d+)$/);
|
|
1119
|
+
if (scoreMatch) {
|
|
1120
|
+
const score = parseFloat(scoreMatch[1]);
|
|
1121
|
+
if (score >= 9) return "CRITICAL";
|
|
1122
|
+
if (score >= 7) return "HIGH";
|
|
1123
|
+
if (score >= 4) return "MEDIUM";
|
|
1124
|
+
return "LOW";
|
|
1125
|
+
}
|
|
1126
|
+
return "UNKNOWN";
|
|
1127
|
+
}
|
|
1128
|
+
async function lookupCveOsv(cveId) {
|
|
1129
|
+
const vuln = await fetchOsvVuln(cveId);
|
|
1130
|
+
if (!vuln) return null;
|
|
1131
|
+
return parseOsvVuln(vuln);
|
|
512
1132
|
}
|
|
513
1133
|
|
|
514
|
-
// src/intelligence/sources/
|
|
515
|
-
var
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
});
|
|
525
|
-
if (!res.ok) return void 0;
|
|
526
|
-
const html = await res.text();
|
|
527
|
-
const match = html.match(/https:\/\/www\.kb\.cert\.org\/vuls\/id\/\d+/i);
|
|
528
|
-
return match?.[0] ?? void 0;
|
|
529
|
-
} catch {
|
|
530
|
-
return void 0;
|
|
1134
|
+
// src/intelligence/sources/github-advisory.ts
|
|
1135
|
+
var GH_ADVISORY_BASE = "https://api.github.com/advisories";
|
|
1136
|
+
function buildHeaders() {
|
|
1137
|
+
const headers = {
|
|
1138
|
+
Accept: "application/vnd.github+json",
|
|
1139
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
1140
|
+
};
|
|
1141
|
+
const token = getGitHubToken();
|
|
1142
|
+
if (token) {
|
|
1143
|
+
headers.Authorization = `Bearer ${token}`;
|
|
531
1144
|
}
|
|
1145
|
+
return headers;
|
|
532
1146
|
}
|
|
533
|
-
async function
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
1147
|
+
async function fetchGhAdvisories(cveId) {
|
|
1148
|
+
const url = new URL(GH_ADVISORY_BASE);
|
|
1149
|
+
url.searchParams.set("cve_id", cveId);
|
|
1150
|
+
url.searchParams.set("ecosystem", "npm");
|
|
1151
|
+
url.searchParams.set("type", "reviewed");
|
|
1152
|
+
url.searchParams.set("per_page", "10");
|
|
1153
|
+
const res = await fetch(url.toString(), { headers: buildHeaders() });
|
|
1154
|
+
if (res.status === 404) return [];
|
|
1155
|
+
if (!res.ok) {
|
|
1156
|
+
console.warn(
|
|
1157
|
+
`[autoremediator] GitHub Advisory API returned ${res.status} for ${cveId} \u2014 skipping.`
|
|
1158
|
+
);
|
|
1159
|
+
return [];
|
|
538
1160
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
1161
|
+
return res.json();
|
|
1162
|
+
}
|
|
1163
|
+
function parseGhAdvisories(advisories) {
|
|
1164
|
+
const packages = [];
|
|
1165
|
+
for (const advisory of advisories) {
|
|
1166
|
+
for (const vuln of advisory.vulnerabilities) {
|
|
1167
|
+
if (vuln.package.ecosystem.toLowerCase() !== "npm") continue;
|
|
1168
|
+
packages.push({
|
|
1169
|
+
name: vuln.package.name,
|
|
1170
|
+
ecosystem: "npm",
|
|
1171
|
+
vulnerableRange: vuln.vulnerable_version_range ?? ">=0.0.0",
|
|
1172
|
+
firstPatchedVersion: vuln.first_patched_version ?? void 0,
|
|
1173
|
+
source: "github-advisory"
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
545
1176
|
}
|
|
546
|
-
return
|
|
1177
|
+
return packages;
|
|
547
1178
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
1179
|
+
function mergeGhDataIntoCveDetails(details, ghPackages) {
|
|
1180
|
+
const enriched = { ...details };
|
|
1181
|
+
for (const ghPkg of ghPackages) {
|
|
1182
|
+
const existing = enriched.affectedPackages.find(
|
|
1183
|
+
(p) => p.name === ghPkg.name
|
|
1184
|
+
);
|
|
1185
|
+
if (existing) {
|
|
1186
|
+
if (!existing.firstPatchedVersion && ghPkg.firstPatchedVersion) {
|
|
1187
|
+
existing.firstPatchedVersion = ghPkg.firstPatchedVersion;
|
|
1188
|
+
}
|
|
1189
|
+
} else {
|
|
1190
|
+
enriched.affectedPackages.push(ghPkg);
|
|
1191
|
+
}
|
|
559
1192
|
}
|
|
1193
|
+
return enriched;
|
|
560
1194
|
}
|
|
561
|
-
async function
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
const checks = await Promise.all(names.map((name) => fetchDepsDevPackage(name)));
|
|
565
|
-
const matched = checks.filter(Boolean).length;
|
|
566
|
-
if (matched === 0) return details;
|
|
567
|
-
details.intelligence = {
|
|
568
|
-
...details.intelligence ?? {},
|
|
569
|
-
depsDevEnrichedPackages: matched
|
|
570
|
-
};
|
|
571
|
-
return details;
|
|
1195
|
+
async function lookupCveGitHub(cveId) {
|
|
1196
|
+
const advisories = await fetchGhAdvisories(cveId);
|
|
1197
|
+
return parseGhAdvisories(advisories);
|
|
572
1198
|
}
|
|
573
1199
|
|
|
574
|
-
// src/intelligence/sources/
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
const res = await fetch(url.toString(), {
|
|
582
|
-
headers: { Accept: "application/json" }
|
|
583
|
-
});
|
|
584
|
-
return res.ok;
|
|
585
|
-
} catch {
|
|
586
|
-
return false;
|
|
1200
|
+
// src/intelligence/sources/nvd.ts
|
|
1201
|
+
var NVD_BASE = "https://services.nvd.nist.gov/rest/json/cves/2.0";
|
|
1202
|
+
function buildNvdHeaders() {
|
|
1203
|
+
const { apiKey } = getNvdConfig();
|
|
1204
|
+
const headers = { Accept: "application/json" };
|
|
1205
|
+
if (apiKey) {
|
|
1206
|
+
headers.apiKey = apiKey;
|
|
587
1207
|
}
|
|
1208
|
+
return headers;
|
|
588
1209
|
}
|
|
589
|
-
async function
|
|
590
|
-
const
|
|
591
|
-
new Set(details.affectedPackages.map((p) => `github.com/${p.name}/${p.name}`))
|
|
592
|
-
).slice(0, 10);
|
|
593
|
-
if (projects.length === 0) return details;
|
|
594
|
-
const checks = await Promise.all(projects.map((project) => checkProject(project)));
|
|
595
|
-
const matched = checks.filter(Boolean).length;
|
|
596
|
-
if (matched === 0) return details;
|
|
597
|
-
details.intelligence = {
|
|
598
|
-
...details.intelligence ?? {},
|
|
599
|
-
scorecardProjects: matched
|
|
600
|
-
};
|
|
601
|
-
return details;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// src/intelligence/sources/external-feeds.ts
|
|
605
|
-
async function probeFeed(url, cveId, token) {
|
|
1210
|
+
async function fetchNvdCvss(cveId) {
|
|
1211
|
+
const url = `${NVD_BASE}?cveId=${encodeURIComponent(cveId)}`;
|
|
606
1212
|
try {
|
|
607
|
-
const
|
|
608
|
-
feedUrl.searchParams.set("cve", cveId);
|
|
609
|
-
const headers = { Accept: "application/json" };
|
|
610
|
-
if (token) headers.Authorization = `Bearer ${token}`;
|
|
611
|
-
const res = await fetch(feedUrl.toString(), { headers });
|
|
1213
|
+
const res = await fetch(url, { headers: buildNvdHeaders() });
|
|
612
1214
|
if (!res.ok) return void 0;
|
|
613
|
-
|
|
1215
|
+
const data = await res.json();
|
|
1216
|
+
const vuln = data.vulnerabilities?.[0];
|
|
1217
|
+
if (!vuln) return void 0;
|
|
1218
|
+
const metrics = vuln.cve.metrics;
|
|
1219
|
+
const metric = metrics?.cvssMetricV31?.[0] ?? metrics?.cvssMetricV30?.[0] ?? metrics?.cvssMetricV2?.[0];
|
|
1220
|
+
if (!metric) return void 0;
|
|
1221
|
+
const score = metric.cvssData.baseScore;
|
|
1222
|
+
const rawSeverity = metric.cvssData.baseSeverity.toUpperCase();
|
|
1223
|
+
const severityMap = {
|
|
1224
|
+
CRITICAL: "CRITICAL",
|
|
1225
|
+
HIGH: "HIGH",
|
|
1226
|
+
MEDIUM: "MEDIUM",
|
|
1227
|
+
LOW: "LOW"
|
|
1228
|
+
};
|
|
1229
|
+
return {
|
|
1230
|
+
score,
|
|
1231
|
+
severity: severityMap[rawSeverity] ?? "UNKNOWN"
|
|
1232
|
+
};
|
|
614
1233
|
} catch {
|
|
615
1234
|
return void 0;
|
|
616
1235
|
}
|
|
617
1236
|
}
|
|
618
|
-
async function
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const commercialHits = (await Promise.all(
|
|
626
|
-
commercialFeeds.map((url) => probeFeed(url, details.id, commercialFeedToken))
|
|
627
|
-
)).filter((v) => Boolean(v));
|
|
628
|
-
if (vendorHits.length === 0 && commercialHits.length === 0) {
|
|
629
|
-
return details;
|
|
1237
|
+
async function enrichWithNvd(details) {
|
|
1238
|
+
const cvss = await fetchNvdCvss(details.id);
|
|
1239
|
+
if (cvss) {
|
|
1240
|
+
details.cvssScore = cvss.score;
|
|
1241
|
+
if (details.severity === "UNKNOWN") {
|
|
1242
|
+
details.severity = cvss.severity;
|
|
1243
|
+
}
|
|
630
1244
|
}
|
|
631
|
-
details.intelligence = {
|
|
632
|
-
...details.intelligence ?? {},
|
|
633
|
-
vendorAdvisories: vendorHits.length > 0 ? vendorHits : details.intelligence?.vendorAdvisories,
|
|
634
|
-
commercialFeeds: commercialHits.length > 0 ? commercialHits : details.intelligence?.commercialFeeds
|
|
635
|
-
};
|
|
636
|
-
const mergedRefs = /* @__PURE__ */ new Set([...details.references, ...vendorHits, ...commercialHits]);
|
|
637
|
-
details.references = Array.from(mergedRefs);
|
|
638
1245
|
return details;
|
|
639
1246
|
}
|
|
640
1247
|
|
|
641
|
-
// src/remediation/tools/lookup-cve.ts
|
|
642
|
-
var lookupCveTool = tool({
|
|
643
|
-
description: "Look up a CVE ID and return the list of affected npm packages, their vulnerable version ranges, and the first patched version. Always call this first.",
|
|
644
|
-
parameters: z.object({
|
|
645
|
-
cveId: z.string().regex(/^CVE-\d{4}-\d+$/i, "Must be a valid CVE ID like CVE-2021-23337")
|
|
646
|
-
}),
|
|
647
|
-
execute: async ({ cveId }) => {
|
|
648
|
-
const normalizedId = cveId.toUpperCase();
|
|
649
|
-
const [osvDetails, ghPackages] = await Promise.all([
|
|
650
|
-
lookupCveOsv(normalizedId),
|
|
651
|
-
lookupCveGitHub(normalizedId)
|
|
652
|
-
]);
|
|
653
|
-
if (!osvDetails && ghPackages.length === 0) {
|
|
654
|
-
return {
|
|
655
|
-
success: false,
|
|
656
|
-
error: `CVE "${normalizedId}" was not found in OSV or GitHub Advisory databases. It may be too new, or not affect npm packages.`
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
let details = osvDetails ?? {
|
|
660
|
-
id: normalizedId,
|
|
661
|
-
summary: "Details sourced from GitHub Advisory Database.",
|
|
662
|
-
severity: "UNKNOWN",
|
|
663
|
-
references: [],
|
|
664
|
-
affectedPackages: []
|
|
665
|
-
};
|
|
666
|
-
if (ghPackages.length > 0) {
|
|
667
|
-
details = mergeGhDataIntoCveDetails(details, ghPackages);
|
|
668
|
-
}
|
|
669
|
-
const sourceHealth = {};
|
|
670
|
-
const applyEnricher = async (sourceName, enricher) => {
|
|
671
|
-
const before = JSON.stringify(details);
|
|
672
|
-
try {
|
|
673
|
-
details = await enricher(details);
|
|
674
|
-
const after = JSON.stringify(details);
|
|
675
|
-
sourceHealth[sourceName] = {
|
|
676
|
-
attempted: true,
|
|
677
|
-
changed: before !== after
|
|
678
|
-
};
|
|
679
|
-
} catch (error) {
|
|
680
|
-
sourceHealth[sourceName] = {
|
|
681
|
-
attempted: true,
|
|
682
|
-
changed: false,
|
|
683
|
-
error: error instanceof Error ? error.message : String(error)
|
|
684
|
-
};
|
|
685
|
-
}
|
|
686
|
-
};
|
|
687
|
-
await applyEnricher("nvd", enrichWithNvd);
|
|
688
|
-
await applyEnricher("cisa-kev", enrichWithCisaKev);
|
|
689
|
-
await applyEnricher("epss", enrichWithEpss);
|
|
690
|
-
await applyEnricher("cve-services", enrichWithCveServices);
|
|
691
|
-
await applyEnricher("gitlab-advisory", enrichWithGitLabAdvisory);
|
|
692
|
-
await applyEnricher("certcc", enrichWithCertCc);
|
|
693
|
-
await applyEnricher("deps-dev", enrichWithDepsDev);
|
|
694
|
-
await applyEnricher("ossf-scorecard", enrichWithOssfScorecard);
|
|
695
|
-
await applyEnricher("external-feeds", enrichWithExternalFeeds);
|
|
696
|
-
details.intelligence = {
|
|
697
|
-
...details.intelligence ?? {},
|
|
698
|
-
sourceHealth
|
|
699
|
-
};
|
|
700
|
-
if (details.affectedPackages.length === 0) {
|
|
701
|
-
return {
|
|
702
|
-
success: false,
|
|
703
|
-
error: `CVE "${normalizedId}" was found but has no npm-specific affected packages listed. It may affect a different ecosystem.`
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
return { success: true, data: details };
|
|
707
|
-
}
|
|
708
|
-
});
|
|
709
|
-
|
|
710
1248
|
// src/remediation/tools/check-inventory.ts
|
|
711
|
-
import { tool as
|
|
712
|
-
import { z as
|
|
713
|
-
import { readFileSync } from "fs";
|
|
714
|
-
import { join as
|
|
715
|
-
import { execa } from "execa";
|
|
716
|
-
var checkInventoryTool =
|
|
1249
|
+
import { tool as tool4 } from "ai";
|
|
1250
|
+
import { z as z4 } from "zod";
|
|
1251
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
1252
|
+
import { join as join8 } from "path";
|
|
1253
|
+
import { execa as execa5 } from "execa";
|
|
1254
|
+
var checkInventoryTool = tool4({
|
|
717
1255
|
description: "Read the project's package.json and installed dependencies to list packages and exact versions. Must be called before checking version matches.",
|
|
718
|
-
parameters:
|
|
719
|
-
cwd:
|
|
720
|
-
packageManager:
|
|
1256
|
+
parameters: z4.object({
|
|
1257
|
+
cwd: z4.string().describe("Absolute path to the consumer project's root directory"),
|
|
1258
|
+
packageManager: z4.enum(["npm", "pnpm", "yarn"]).optional().describe("Package manager used by the target project (auto-detected if omitted)")
|
|
721
1259
|
}),
|
|
722
1260
|
execute: async ({ cwd, packageManager }) => {
|
|
723
1261
|
let pkgJson;
|
|
724
1262
|
try {
|
|
725
|
-
pkgJson = JSON.parse(
|
|
1263
|
+
pkgJson = JSON.parse(readFileSync5(join8(cwd, "package.json"), "utf8"));
|
|
726
1264
|
} catch {
|
|
727
1265
|
return {
|
|
728
1266
|
packages: [],
|
|
@@ -734,7 +1272,7 @@ var checkInventoryTool = tool2({
|
|
|
734
1272
|
let installedVersions = /* @__PURE__ */ new Map();
|
|
735
1273
|
try {
|
|
736
1274
|
const [cmd, ...args] = commands.list;
|
|
737
|
-
const listResult = await
|
|
1275
|
+
const listResult = await execa5(cmd, args, {
|
|
738
1276
|
cwd,
|
|
739
1277
|
stdio: "pipe",
|
|
740
1278
|
reject: false
|
|
@@ -761,66 +1299,12 @@ var checkInventoryTool = tool2({
|
|
|
761
1299
|
packages.push({ name, version: cleaned, type: "direct" });
|
|
762
1300
|
}
|
|
763
1301
|
}
|
|
764
|
-
return { packages };
|
|
765
|
-
}
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
// src/remediation/tools/check-version-match.ts
|
|
769
|
-
import { tool as tool3 } from "ai";
|
|
770
|
-
import { z as z3 } from "zod";
|
|
771
|
-
import semver from "semver";
|
|
772
|
-
var affectedPackageSchema = z3.object({
|
|
773
|
-
name: z3.string(),
|
|
774
|
-
ecosystem: z3.literal("npm"),
|
|
775
|
-
vulnerableRange: z3.string(),
|
|
776
|
-
firstPatchedVersion: z3.string().optional(),
|
|
777
|
-
source: z3.enum(["osv", "github-advisory"])
|
|
778
|
-
});
|
|
779
|
-
var inventoryPackageSchema = z3.object({
|
|
780
|
-
name: z3.string(),
|
|
781
|
-
version: z3.string(),
|
|
782
|
-
type: z3.enum(["direct", "indirect"])
|
|
783
|
-
});
|
|
784
|
-
var checkVersionMatchTool = tool3({
|
|
785
|
-
description: "Check which of the project's installed packages fall within the CVE's vulnerable version ranges. Returns only the packages that are actually vulnerable.",
|
|
786
|
-
parameters: z3.object({
|
|
787
|
-
installedPackages: z3.array(inventoryPackageSchema).describe("Output from the check-inventory tool"),
|
|
788
|
-
affectedPackages: z3.array(affectedPackageSchema).describe("affectedPackages array from the lookup-cve tool result")
|
|
789
|
-
}),
|
|
790
|
-
execute: async ({ installedPackages, affectedPackages }) => {
|
|
791
|
-
const vulnerable = [];
|
|
792
|
-
for (const affected of affectedPackages) {
|
|
793
|
-
const matches = installedPackages.filter(
|
|
794
|
-
(p) => p.name === affected.name
|
|
795
|
-
);
|
|
796
|
-
for (const installed of matches) {
|
|
797
|
-
if (!semver.valid(installed.version)) continue;
|
|
798
|
-
let isVulnerable = false;
|
|
799
|
-
try {
|
|
800
|
-
isVulnerable = semver.satisfies(installed.version, affected.vulnerableRange, {
|
|
801
|
-
includePrerelease: false
|
|
802
|
-
});
|
|
803
|
-
} catch {
|
|
804
|
-
continue;
|
|
805
|
-
}
|
|
806
|
-
if (isVulnerable) {
|
|
807
|
-
vulnerable.push({ installed, affected });
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
return {
|
|
812
|
-
vulnerablePackages: vulnerable,
|
|
813
|
-
checkedCount: installedPackages.length
|
|
814
|
-
};
|
|
1302
|
+
return { packages };
|
|
815
1303
|
}
|
|
816
1304
|
});
|
|
817
1305
|
|
|
818
|
-
// src/remediation/tools/find-fixed-version.ts
|
|
819
|
-
import { tool as tool4 } from "ai";
|
|
820
|
-
import { z as z4 } from "zod";
|
|
821
|
-
|
|
822
1306
|
// src/intelligence/sources/registry.ts
|
|
823
|
-
import
|
|
1307
|
+
import semver3 from "semver";
|
|
824
1308
|
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
825
1309
|
async function fetchPackageVersions(packageName) {
|
|
826
1310
|
const url = `${NPM_REGISTRY}/${encodeURIComponent(packageName)}`;
|
|
@@ -836,322 +1320,218 @@ async function fetchPackageVersions(packageName) {
|
|
|
836
1320
|
const data = await res.json();
|
|
837
1321
|
return Object.keys(data.versions);
|
|
838
1322
|
}
|
|
839
|
-
async function
|
|
1323
|
+
async function resolveSafeUpgradeVersion(packageName, installedVersion, firstPatchedVersion, vulnerableRange) {
|
|
840
1324
|
const versions = await fetchPackageVersions(packageName);
|
|
841
|
-
if (!versions.length)
|
|
842
|
-
|
|
843
|
-
|
|
1325
|
+
if (!versions.length) {
|
|
1326
|
+
return {
|
|
1327
|
+
candidates: {},
|
|
1328
|
+
majorOnlyFixAvailable: false
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
const installed = semver3.parse(installedVersion);
|
|
1332
|
+
const candidates = versions.filter((v) => semver3.valid(v) && semver3.gte(v, firstPatchedVersion)).filter((v) => {
|
|
844
1333
|
if (!vulnerableRange) return true;
|
|
845
1334
|
try {
|
|
846
|
-
return !
|
|
1335
|
+
return !semver3.satisfies(v, vulnerableRange, { includePrerelease: false });
|
|
847
1336
|
} catch {
|
|
848
1337
|
return true;
|
|
849
1338
|
}
|
|
850
|
-
}).sort(
|
|
851
|
-
if (!candidates.length)
|
|
852
|
-
const sameMajor = candidates.find(
|
|
853
|
-
(v) => semver2.major(v) === installedMajor
|
|
854
|
-
);
|
|
855
|
-
if (sameMajor) return sameMajor;
|
|
856
|
-
return candidates[0];
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
// src/remediation/tools/find-fixed-version.ts
|
|
860
|
-
var findFixedVersionTool = tool4({
|
|
861
|
-
description: "Query the npm registry to find the lowest published version of a package that is >= the first patched version. Prefer same-major upgrades. Returns undefined if no safe version exists.",
|
|
862
|
-
parameters: z4.object({
|
|
863
|
-
packageName: z4.string().describe("The npm package name"),
|
|
864
|
-
installedVersion: z4.string().describe("The currently installed version (exact semver)"),
|
|
865
|
-
firstPatchedVersion: z4.string().describe(
|
|
866
|
-
"The first version that is NOT vulnerable (from lookup-cve). Use this as the floor."
|
|
867
|
-
),
|
|
868
|
-
vulnerableRange: z4.string().optional().describe("Optional vulnerable semver range used to exclude still-vulnerable versions")
|
|
869
|
-
}),
|
|
870
|
-
execute: async ({
|
|
871
|
-
packageName,
|
|
872
|
-
installedVersion,
|
|
873
|
-
firstPatchedVersion,
|
|
874
|
-
vulnerableRange
|
|
875
|
-
}) => {
|
|
876
|
-
const safeVersion = await findSafeUpgradeVersion(
|
|
877
|
-
packageName,
|
|
878
|
-
installedVersion,
|
|
879
|
-
firstPatchedVersion,
|
|
880
|
-
vulnerableRange
|
|
881
|
-
);
|
|
882
|
-
if (!safeVersion) {
|
|
883
|
-
return {
|
|
884
|
-
isMajorBump: false,
|
|
885
|
-
message: `No safe upgrade version found for "${packageName}". The patch-file path will be needed.`
|
|
886
|
-
};
|
|
887
|
-
}
|
|
888
|
-
const installedMajor = parseInt(installedVersion.split(".")[0] ?? "0", 10);
|
|
889
|
-
const safeMajor = parseInt(safeVersion.split(".")[0] ?? "0", 10);
|
|
890
|
-
const isMajorBump = safeMajor > installedMajor;
|
|
1339
|
+
}).sort(semver3.compare);
|
|
1340
|
+
if (!candidates.length) {
|
|
891
1341
|
return {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
message: isMajorBump ? `Found safe version ${safeVersion} for "${packageName}", but it is a major bump from ${installedVersion}. Applying anyway \u2014 consumer should review for breaking changes.` : `Found safe version ${safeVersion} for "${packageName}" (from ${installedVersion}).`
|
|
1342
|
+
candidates: {},
|
|
1343
|
+
majorOnlyFixAvailable: false
|
|
895
1344
|
};
|
|
896
1345
|
}
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
import { execa as execa2 } from "execa";
|
|
905
|
-
import semver3 from "semver";
|
|
906
|
-
|
|
907
|
-
// src/platform/policy.ts
|
|
908
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
909
|
-
import { join as join3 } from "path";
|
|
910
|
-
var DEFAULT_POLICY = {
|
|
911
|
-
allowMajorBumps: false,
|
|
912
|
-
denyPackages: [],
|
|
913
|
-
allowPackages: [],
|
|
914
|
-
constraints: {
|
|
915
|
-
directDependenciesOnly: false,
|
|
916
|
-
preferVersionBump: false
|
|
1346
|
+
const categorizedCandidates = {};
|
|
1347
|
+
for (const candidate of candidates) {
|
|
1348
|
+
const level = classifyUpgradeLevel(installedVersion, candidate);
|
|
1349
|
+
if (!level) continue;
|
|
1350
|
+
if (!categorizedCandidates[level]) {
|
|
1351
|
+
categorizedCandidates[level] = candidate;
|
|
1352
|
+
}
|
|
917
1353
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
const candidate = explicitPath ?? join3(cwd, ".autoremediator.json");
|
|
921
|
-
if (!existsSync2(candidate)) return DEFAULT_POLICY;
|
|
922
|
-
try {
|
|
923
|
-
const parsed = JSON.parse(readFileSync2(candidate, "utf8"));
|
|
1354
|
+
const safeVersion = categorizedCandidates.patch ?? categorizedCandidates.minor ?? categorizedCandidates.major;
|
|
1355
|
+
if (!safeVersion) {
|
|
924
1356
|
return {
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
allowPackages: parsed.allowPackages ?? DEFAULT_POLICY.allowPackages,
|
|
928
|
-
constraints: {
|
|
929
|
-
directDependenciesOnly: parsed.constraints?.directDependenciesOnly ?? DEFAULT_POLICY.constraints?.directDependenciesOnly ?? false,
|
|
930
|
-
preferVersionBump: parsed.constraints?.preferVersionBump ?? DEFAULT_POLICY.constraints?.preferVersionBump ?? false
|
|
931
|
-
}
|
|
1357
|
+
candidates: categorizedCandidates,
|
|
1358
|
+
majorOnlyFixAvailable: false
|
|
932
1359
|
};
|
|
933
|
-
} catch {
|
|
934
|
-
return DEFAULT_POLICY;
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
function isPackageAllowed(policy, packageName) {
|
|
938
|
-
if (policy.denyPackages.includes(packageName)) return false;
|
|
939
|
-
if (policy.allowPackages.length > 0 && !policy.allowPackages.includes(packageName)) {
|
|
940
|
-
return false;
|
|
941
1360
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
}
|
|
951
|
-
async function acquireRepoLock(cwd, options = {}) {
|
|
952
|
-
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
953
|
-
const retryDelayMs = options.retryDelayMs ?? 125;
|
|
954
|
-
const lockRoot = join4(cwd, ".autoremediator", "locks");
|
|
955
|
-
const lockPath = join4(cwd, ".autoremediator", "locks", "remediation.lock");
|
|
956
|
-
const startedAt = Date.now();
|
|
957
|
-
await mkdir(lockRoot, { recursive: true });
|
|
958
|
-
while (true) {
|
|
959
|
-
try {
|
|
960
|
-
await mkdir(lockPath, { recursive: false });
|
|
961
|
-
return {
|
|
962
|
-
lockPath,
|
|
963
|
-
release: async () => {
|
|
964
|
-
await rm(lockPath, { recursive: true, force: true });
|
|
965
|
-
}
|
|
966
|
-
};
|
|
967
|
-
} catch {
|
|
968
|
-
if (Date.now() - startedAt > timeoutMs) {
|
|
969
|
-
throw new Error(`Timed out waiting for repository lock at ${lockPath}.`);
|
|
970
|
-
}
|
|
971
|
-
await sleep(retryDelayMs);
|
|
972
|
-
}
|
|
1361
|
+
const upgradeLevel = classifyUpgradeLevel(installedVersion, safeVersion);
|
|
1362
|
+
const majorOnlyFixAvailable = !categorizedCandidates.patch && !categorizedCandidates.minor && Boolean(categorizedCandidates.major);
|
|
1363
|
+
if (!installed || !upgradeLevel) {
|
|
1364
|
+
return {
|
|
1365
|
+
safeVersion,
|
|
1366
|
+
upgradeLevel,
|
|
1367
|
+
candidates: categorizedCandidates,
|
|
1368
|
+
majorOnlyFixAvailable
|
|
1369
|
+
};
|
|
973
1370
|
}
|
|
1371
|
+
return {
|
|
1372
|
+
safeVersion,
|
|
1373
|
+
upgradeLevel,
|
|
1374
|
+
candidates: categorizedCandidates,
|
|
1375
|
+
majorOnlyFixAvailable
|
|
1376
|
+
};
|
|
974
1377
|
}
|
|
975
|
-
|
|
976
|
-
const
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1378
|
+
function classifyUpgradeLevel(installedVersion, candidateVersion) {
|
|
1379
|
+
const installed = semver3.parse(installedVersion);
|
|
1380
|
+
const candidate = semver3.parse(candidateVersion);
|
|
1381
|
+
if (!installed || !candidate) return void 0;
|
|
1382
|
+
if (candidate.major > installed.major) return "major";
|
|
1383
|
+
if (candidate.minor > installed.minor) return "minor";
|
|
1384
|
+
if (candidate.patch > installed.patch || candidate.version === installed.version) {
|
|
1385
|
+
return "patch";
|
|
981
1386
|
}
|
|
1387
|
+
return void 0;
|
|
982
1388
|
}
|
|
983
1389
|
|
|
984
|
-
// src/remediation/
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
fromVersion: z5.string().describe("The currently installed vulnerable version"),
|
|
992
|
-
toVersion: z5.string().describe("The safe target version to upgrade to"),
|
|
993
|
-
dryRun: z5.boolean().default(false).describe("If true, report changes but do not write"),
|
|
994
|
-
policy: z5.string().optional().describe("Optional path to .autoremediator policy file"),
|
|
995
|
-
runTests: z5.boolean().default(false).describe("If true, run test validation after applying the fix")
|
|
996
|
-
}),
|
|
997
|
-
execute: async ({
|
|
998
|
-
cwd,
|
|
999
|
-
packageManager,
|
|
1000
|
-
packageName,
|
|
1001
|
-
fromVersion,
|
|
1002
|
-
toVersion,
|
|
1003
|
-
dryRun,
|
|
1004
|
-
policy,
|
|
1005
|
-
runTests
|
|
1006
|
-
}) => {
|
|
1007
|
-
const pm = packageManager ?? detectPackageManager(cwd);
|
|
1008
|
-
const commands = getPackageManagerCommands(pm);
|
|
1009
|
-
const pkgPath = join5(cwd, "package.json");
|
|
1010
|
-
const loadedPolicy = loadPolicy(cwd, policy);
|
|
1011
|
-
if (!isPackageAllowed(loadedPolicy, packageName)) {
|
|
1390
|
+
// src/remediation/local/primary-strategy.ts
|
|
1391
|
+
async function resolvePrimaryResult(params) {
|
|
1392
|
+
const { vulnerable, cwd, packageManager, dryRun, policy, runTests, constraints } = params;
|
|
1393
|
+
const pkg = vulnerable.installed;
|
|
1394
|
+
const firstPatchedVersion = vulnerable.affected.firstPatchedVersion;
|
|
1395
|
+
if (pkg.type === "indirect") {
|
|
1396
|
+
if (constraints.directDependenciesOnly) {
|
|
1012
1397
|
return {
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1398
|
+
steps: 0,
|
|
1399
|
+
result: {
|
|
1400
|
+
packageName: pkg.name,
|
|
1401
|
+
strategy: "none",
|
|
1402
|
+
fromVersion: pkg.version,
|
|
1403
|
+
applied: false,
|
|
1404
|
+
dryRun,
|
|
1405
|
+
unresolvedReason: "constraint-blocked",
|
|
1406
|
+
message: `Constraint blocked remediation for indirect dependency "${pkg.name}".`
|
|
1407
|
+
}
|
|
1020
1408
|
};
|
|
1021
1409
|
}
|
|
1022
|
-
|
|
1023
|
-
if (isMajorBump && !loadedPolicy.allowMajorBumps) {
|
|
1410
|
+
if (constraints.preferVersionBump) {
|
|
1024
1411
|
return {
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1412
|
+
steps: 0,
|
|
1413
|
+
result: {
|
|
1414
|
+
packageName: pkg.name,
|
|
1415
|
+
strategy: "none",
|
|
1416
|
+
fromVersion: pkg.version,
|
|
1417
|
+
applied: false,
|
|
1418
|
+
dryRun,
|
|
1419
|
+
unresolvedReason: "constraint-blocked",
|
|
1420
|
+
message: `Constraint prefers version-bump and rejected override remediation for "${pkg.name}".`
|
|
1421
|
+
}
|
|
1032
1422
|
};
|
|
1033
1423
|
}
|
|
1034
|
-
|
|
1035
|
-
try {
|
|
1036
|
-
pkgJson = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
1037
|
-
} catch {
|
|
1424
|
+
if (!firstPatchedVersion) {
|
|
1038
1425
|
return {
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1426
|
+
steps: 0,
|
|
1427
|
+
result: {
|
|
1428
|
+
packageName: pkg.name,
|
|
1429
|
+
strategy: "none",
|
|
1430
|
+
fromVersion: pkg.version,
|
|
1431
|
+
applied: false,
|
|
1432
|
+
dryRun,
|
|
1433
|
+
unresolvedReason: "no-safe-version",
|
|
1434
|
+
message: `No firstPatchedVersion available for ${pkg.name}; cannot resolve deterministic override in local mode.`
|
|
1435
|
+
}
|
|
1045
1436
|
};
|
|
1046
1437
|
}
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1438
|
+
const safeUpgrade2 = await resolveSafeUpgradeVersion(
|
|
1439
|
+
pkg.name,
|
|
1440
|
+
pkg.version,
|
|
1441
|
+
firstPatchedVersion,
|
|
1442
|
+
vulnerable.affected.vulnerableRange
|
|
1049
1443
|
);
|
|
1050
|
-
if (!
|
|
1051
|
-
return {
|
|
1052
|
-
packageName,
|
|
1053
|
-
strategy: "none",
|
|
1054
|
-
fromVersion,
|
|
1055
|
-
applied: false,
|
|
1056
|
-
dryRun,
|
|
1057
|
-
message: `"${packageName}" was not found in package.json dependencies (it may be a transitive dep). Cannot auto-bump.`
|
|
1058
|
-
};
|
|
1059
|
-
}
|
|
1060
|
-
const currentRange = pkgJson[depField][packageName];
|
|
1061
|
-
const prefixMatch = currentRange.match(/^([~^]?)/);
|
|
1062
|
-
const prefix = prefixMatch?.[1] ?? "";
|
|
1063
|
-
const newRange = `${prefix}${toVersion}`;
|
|
1064
|
-
if (dryRun) {
|
|
1065
|
-
const installCmd = commands.installPreferOffline.join(" ");
|
|
1066
|
-
const testCmd = commands.test.join(" ");
|
|
1444
|
+
if (!safeUpgrade2.safeVersion) {
|
|
1067
1445
|
return {
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
dryRun: true,
|
|
1074
|
-
message: `[DRY RUN] Would update ${depField}.${packageName}: "${currentRange}" -> "${newRange}", then run ${installCmd}${runTests ? ` and ${testCmd}` : ""}.`
|
|
1075
|
-
};
|
|
1076
|
-
}
|
|
1077
|
-
return withRepoLock(cwd, async () => {
|
|
1078
|
-
pkgJson[depField][packageName] = newRange;
|
|
1079
|
-
writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
1080
|
-
try {
|
|
1081
|
-
const [installCmd, ...installArgs] = commands.installPreferOffline;
|
|
1082
|
-
await execa2(installCmd, installArgs, {
|
|
1083
|
-
cwd,
|
|
1084
|
-
stdio: "pipe"
|
|
1085
|
-
});
|
|
1086
|
-
} catch (err) {
|
|
1087
|
-
pkgJson[depField][packageName] = currentRange;
|
|
1088
|
-
writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
1089
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1090
|
-
return {
|
|
1091
|
-
packageName,
|
|
1092
|
-
strategy: "version-bump",
|
|
1093
|
-
fromVersion,
|
|
1094
|
-
toVersion,
|
|
1446
|
+
steps: 1,
|
|
1447
|
+
result: {
|
|
1448
|
+
packageName: pkg.name,
|
|
1449
|
+
strategy: "none",
|
|
1450
|
+
fromVersion: pkg.version,
|
|
1095
1451
|
applied: false,
|
|
1096
|
-
dryRun
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
}
|
|
1100
|
-
if (runTests) {
|
|
1101
|
-
try {
|
|
1102
|
-
const [testCmd, ...testArgs] = commands.test;
|
|
1103
|
-
await execa2(testCmd, testArgs, {
|
|
1104
|
-
cwd,
|
|
1105
|
-
stdio: "pipe"
|
|
1106
|
-
});
|
|
1107
|
-
} catch (err) {
|
|
1108
|
-
pkgJson[depField][packageName] = currentRange;
|
|
1109
|
-
writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
1110
|
-
try {
|
|
1111
|
-
const [rollbackCmd, ...rollbackArgs] = commands.installPreferOffline;
|
|
1112
|
-
await execa2(rollbackCmd, rollbackArgs, {
|
|
1113
|
-
cwd,
|
|
1114
|
-
stdio: "pipe"
|
|
1115
|
-
});
|
|
1116
|
-
} catch {
|
|
1117
|
-
}
|
|
1118
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1119
|
-
return {
|
|
1120
|
-
packageName,
|
|
1121
|
-
strategy: "version-bump",
|
|
1122
|
-
fromVersion,
|
|
1123
|
-
toVersion,
|
|
1124
|
-
applied: false,
|
|
1125
|
-
dryRun: false,
|
|
1126
|
-
message: `${commands.test.join(" ")} failed after upgrading "${packageName}" to ${toVersion}. Rolled back to ${currentRange}. Error: ${message}`
|
|
1127
|
-
};
|
|
1452
|
+
dryRun,
|
|
1453
|
+
unresolvedReason: "no-safe-version",
|
|
1454
|
+
message: `No safe override version found for ${pkg.name}.`
|
|
1128
1455
|
}
|
|
1129
|
-
}
|
|
1130
|
-
return {
|
|
1131
|
-
packageName,
|
|
1132
|
-
strategy: "version-bump",
|
|
1133
|
-
fromVersion,
|
|
1134
|
-
toVersion,
|
|
1135
|
-
applied: true,
|
|
1136
|
-
dryRun: false,
|
|
1137
|
-
message: `Successfully upgraded "${packageName}" from ${fromVersion} to ${toVersion}, ran ${commands.installPreferOffline.join(" ")}${runTests ? `, and passed ${commands.test.join(" ")}` : ""}.`
|
|
1138
1456
|
};
|
|
1457
|
+
}
|
|
1458
|
+
const overrideResult = await applyPackageOverrideTool.execute({
|
|
1459
|
+
cwd,
|
|
1460
|
+
packageManager,
|
|
1461
|
+
packageName: pkg.name,
|
|
1462
|
+
fromVersion: pkg.version,
|
|
1463
|
+
toVersion: safeUpgrade2.safeVersion,
|
|
1464
|
+
dryRun,
|
|
1465
|
+
policy,
|
|
1466
|
+
runTests
|
|
1139
1467
|
});
|
|
1468
|
+
return {
|
|
1469
|
+
steps: 2,
|
|
1470
|
+
result: overrideResult
|
|
1471
|
+
};
|
|
1140
1472
|
}
|
|
1141
|
-
|
|
1473
|
+
if (!firstPatchedVersion) {
|
|
1474
|
+
return {
|
|
1475
|
+
steps: 0,
|
|
1476
|
+
result: {
|
|
1477
|
+
packageName: pkg.name,
|
|
1478
|
+
strategy: "none",
|
|
1479
|
+
fromVersion: pkg.version,
|
|
1480
|
+
applied: false,
|
|
1481
|
+
dryRun,
|
|
1482
|
+
unresolvedReason: "no-safe-version",
|
|
1483
|
+
message: `No firstPatchedVersion available for ${pkg.name}; cannot resolve deterministic upgrade in local mode.`
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
const safeUpgrade = await resolveSafeUpgradeVersion(
|
|
1488
|
+
pkg.name,
|
|
1489
|
+
pkg.version,
|
|
1490
|
+
firstPatchedVersion,
|
|
1491
|
+
vulnerable.affected.vulnerableRange
|
|
1492
|
+
);
|
|
1493
|
+
if (!safeUpgrade.safeVersion) {
|
|
1494
|
+
return {
|
|
1495
|
+
steps: 1,
|
|
1496
|
+
result: {
|
|
1497
|
+
packageName: pkg.name,
|
|
1498
|
+
strategy: "none",
|
|
1499
|
+
fromVersion: pkg.version,
|
|
1500
|
+
applied: false,
|
|
1501
|
+
dryRun,
|
|
1502
|
+
unresolvedReason: "no-safe-version",
|
|
1503
|
+
message: `No safe upgrade version found for ${pkg.name}.`
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
const applyResult = await applyVersionBumpTool.execute({
|
|
1508
|
+
cwd,
|
|
1509
|
+
packageManager,
|
|
1510
|
+
packageName: pkg.name,
|
|
1511
|
+
fromVersion: pkg.version,
|
|
1512
|
+
toVersion: safeUpgrade.safeVersion,
|
|
1513
|
+
dryRun,
|
|
1514
|
+
policy,
|
|
1515
|
+
runTests
|
|
1516
|
+
});
|
|
1517
|
+
return {
|
|
1518
|
+
steps: 2,
|
|
1519
|
+
result: applyResult
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1142
1522
|
|
|
1143
1523
|
// src/remediation/tools/fetch-package-source.ts
|
|
1144
|
-
import { tool as
|
|
1145
|
-
import { z as
|
|
1146
|
-
import { mkdir as
|
|
1147
|
-
import { join as
|
|
1148
|
-
import { execa as
|
|
1149
|
-
var fetchPackageSourceTool =
|
|
1524
|
+
import { tool as tool5 } from "ai";
|
|
1525
|
+
import { z as z5 } from "zod";
|
|
1526
|
+
import { mkdir as mkdir3, readdir, readFile as readFile2, rm as rm3 } from "fs/promises";
|
|
1527
|
+
import { join as join9 } from "path";
|
|
1528
|
+
import { execa as execa6 } from "execa";
|
|
1529
|
+
var fetchPackageSourceTool = tool5({
|
|
1150
1530
|
description: "Download package tarball from npm and extract source files for CVE analysis. Supports custom file patterns (default: *.js, *.ts).",
|
|
1151
|
-
parameters:
|
|
1152
|
-
packageName:
|
|
1153
|
-
version:
|
|
1154
|
-
filePatterns:
|
|
1531
|
+
parameters: z5.object({
|
|
1532
|
+
packageName: z5.string().min(1).describe("The npm package name (e.g., 'lodash', '@scope/package')"),
|
|
1533
|
+
version: z5.string().regex(/^\d+\.\d+\.\d+/, "Must be a valid semver version").describe("Exact package version to download"),
|
|
1534
|
+
filePatterns: z5.array(z5.string()).optional().default(["*.js", "*.ts"]).describe(
|
|
1155
1535
|
"File patterns to extract (glob patterns, default: *.js, *.ts)"
|
|
1156
1536
|
)
|
|
1157
1537
|
}),
|
|
@@ -1161,23 +1541,23 @@ var fetchPackageSourceTool = tool6({
|
|
|
1161
1541
|
filePatterns
|
|
1162
1542
|
}) => {
|
|
1163
1543
|
const tempBaseDir = `/tmp/autoremediator-pkg-${Date.now()}`;
|
|
1164
|
-
const extractDir =
|
|
1544
|
+
const extractDir = join9(tempBaseDir, "out");
|
|
1165
1545
|
try {
|
|
1166
1546
|
const npmUrl = `https://registry.npmjs.org/${packageName}/-/${packageName.split("/").pop()}-${version}.tgz`;
|
|
1167
|
-
await
|
|
1168
|
-
const tarballPath =
|
|
1169
|
-
await
|
|
1170
|
-
await
|
|
1171
|
-
await
|
|
1547
|
+
await mkdir3(tempBaseDir, { recursive: true });
|
|
1548
|
+
const tarballPath = join9(tempBaseDir, "package.tgz");
|
|
1549
|
+
await execa6("curl", ["-L", "-o", tarballPath, npmUrl]);
|
|
1550
|
+
await mkdir3(extractDir, { recursive: true });
|
|
1551
|
+
await execa6("tar", ["-xzf", tarballPath, "-C", extractDir]);
|
|
1172
1552
|
const extractedContents = await readdir(extractDir);
|
|
1173
|
-
const packageRootDir = extractedContents.includes("package") ?
|
|
1553
|
+
const packageRootDir = extractedContents.includes("package") ? join9(extractDir, "package") : extractDir;
|
|
1174
1554
|
const sourceCode = {};
|
|
1175
1555
|
async function walkDir(dir, relativeBase) {
|
|
1176
1556
|
try {
|
|
1177
1557
|
const files = await readdir(dir, { withFileTypes: true });
|
|
1178
1558
|
for (const file of files) {
|
|
1179
|
-
const fullPath =
|
|
1180
|
-
const relPath =
|
|
1559
|
+
const fullPath = join9(dir, file.name);
|
|
1560
|
+
const relPath = join9(relativeBase, file.name);
|
|
1181
1561
|
if (file.isDirectory()) {
|
|
1182
1562
|
if (![
|
|
1183
1563
|
"node_modules",
|
|
@@ -1199,7 +1579,7 @@ var fetchPackageSourceTool = tool6({
|
|
|
1199
1579
|
});
|
|
1200
1580
|
if (matches) {
|
|
1201
1581
|
try {
|
|
1202
|
-
const content = await
|
|
1582
|
+
const content = await readFile2(fullPath, "utf8");
|
|
1203
1583
|
sourceCode[relPath] = content;
|
|
1204
1584
|
} catch {
|
|
1205
1585
|
}
|
|
@@ -1234,14 +1614,14 @@ var fetchPackageSourceTool = tool6({
|
|
|
1234
1614
|
error: `Failed to fetch and extract package ${packageName}@${version}: ${message}`
|
|
1235
1615
|
};
|
|
1236
1616
|
} finally {
|
|
1237
|
-
await
|
|
1617
|
+
await rm3(tempBaseDir, { recursive: true, force: true });
|
|
1238
1618
|
}
|
|
1239
1619
|
}
|
|
1240
1620
|
});
|
|
1241
1621
|
|
|
1242
1622
|
// src/remediation/tools/generate-patch.ts
|
|
1243
|
-
import { tool as
|
|
1244
|
-
import { z as
|
|
1623
|
+
import { tool as tool6 } from "ai";
|
|
1624
|
+
import { z as z6 } from "zod";
|
|
1245
1625
|
import { generateText } from "ai";
|
|
1246
1626
|
var VULNERABILITY_DESCRIPTIONS = {
|
|
1247
1627
|
redos: "Regular Expression Denial of Service (ReDoS): The vulnerability is caused by poorly constructed regular expressions that cause excessive backtracking when processing certain inputs. The fix should optimize the regex to avoid catastrophic backtracking or replace it with a safer alternative.",
|
|
@@ -1249,18 +1629,18 @@ var VULNERABILITY_DESCRIPTIONS = {
|
|
|
1249
1629
|
"path-traversal": "Path Traversal: The vulnerability allows access to files outside intended directories through path traversal sequences (../, etc.). The fix must validate and normalize file paths, use path.resolve() and path.relative() checks.",
|
|
1250
1630
|
unknown: "Unknown vulnerability type: Analyze the CVE summary carefully and implement the most appropriate fix for the security issue described."
|
|
1251
1631
|
};
|
|
1252
|
-
var generatePatchTool =
|
|
1632
|
+
var generatePatchTool = tool6({
|
|
1253
1633
|
description: "Generate a unified diff patch for a CVE using LLM analysis of vulnerable source code.",
|
|
1254
|
-
parameters:
|
|
1255
|
-
packageName:
|
|
1256
|
-
vulnerableVersion:
|
|
1257
|
-
cveId:
|
|
1258
|
-
cveSummary:
|
|
1259
|
-
sourceFiles:
|
|
1634
|
+
parameters: z6.object({
|
|
1635
|
+
packageName: z6.string().min(1).describe("The npm package name"),
|
|
1636
|
+
vulnerableVersion: z6.string().describe("The vulnerable version string"),
|
|
1637
|
+
cveId: z6.string().regex(/^CVE-\d{4}-\d+$/i).describe("CVE ID (e.g., CVE-2021-23337)"),
|
|
1638
|
+
cveSummary: z6.string().min(10).describe("CVE description and impact"),
|
|
1639
|
+
sourceFiles: z6.record(z6.string()).describe(
|
|
1260
1640
|
"Map of file paths to source code contents from fetch-package-source"
|
|
1261
1641
|
),
|
|
1262
|
-
vulnerabilityCategory:
|
|
1263
|
-
dryRun:
|
|
1642
|
+
vulnerabilityCategory: z6.enum(["redos", "code-injection", "path-traversal", "unknown"]).optional().default("unknown").describe("Category of the vulnerability for better context"),
|
|
1643
|
+
dryRun: z6.boolean().optional().default(false).describe("If true, return analysis without generating patches")
|
|
1264
1644
|
}),
|
|
1265
1645
|
execute: async ({
|
|
1266
1646
|
packageName,
|
|
@@ -1446,589 +1826,730 @@ function generateUnifiedDiff(original, fixed, filePath) {
|
|
|
1446
1826
|
return diff.join("\n");
|
|
1447
1827
|
}
|
|
1448
1828
|
|
|
1449
|
-
// src/remediation/
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
dryRun: z8.boolean().optional().default(false).describe("If true, report but do not mutate files")
|
|
1474
|
-
}).refine((value) => Boolean(value.patchContent || value.patches && value.patches.length > 0), {
|
|
1475
|
-
message: "Either patchContent or patches must be provided"
|
|
1476
|
-
}),
|
|
1477
|
-
execute: async ({
|
|
1478
|
-
packageName,
|
|
1479
|
-
vulnerableVersion,
|
|
1480
|
-
patchContent,
|
|
1481
|
-
patches,
|
|
1482
|
-
patchesDir,
|
|
1483
|
-
cwd,
|
|
1484
|
-
packageManager,
|
|
1485
|
-
validateWithTests,
|
|
1486
|
-
dryRun
|
|
1487
|
-
}) => {
|
|
1488
|
-
try {
|
|
1489
|
-
const pm = packageManager ?? detectPackageManager(cwd);
|
|
1490
|
-
const selectedPatch = patchContent ?? patches?.[0]?.unifiedDiff;
|
|
1491
|
-
if (!selectedPatch) {
|
|
1492
|
-
return {
|
|
1493
|
-
success: false,
|
|
1494
|
-
packageName,
|
|
1495
|
-
vulnerableVersion,
|
|
1496
|
-
applied: false,
|
|
1497
|
-
dryRun,
|
|
1498
|
-
message: "No patch content provided.",
|
|
1499
|
-
error: "No patch content provided."
|
|
1500
|
-
};
|
|
1829
|
+
// src/remediation/local/fallback.ts
|
|
1830
|
+
function shouldAttemptPatchFallback(result, preferVersionBump) {
|
|
1831
|
+
if (preferVersionBump) return false;
|
|
1832
|
+
if (result.applied || result.dryRun) return false;
|
|
1833
|
+
return result.unresolvedReason === "no-safe-version" || result.unresolvedReason === "install-failed" || result.unresolvedReason === "override-apply-failed" || result.unresolvedReason === "validation-failed" || result.unresolvedReason === "major-bump-required" || result.unresolvedReason === "indirect-dependency";
|
|
1834
|
+
}
|
|
1835
|
+
async function tryLocalPatchFallback(params) {
|
|
1836
|
+
let steps = 0;
|
|
1837
|
+
const sourceResult = await fetchPackageSourceTool.execute({
|
|
1838
|
+
packageName: params.packageName,
|
|
1839
|
+
version: params.vulnerableVersion
|
|
1840
|
+
});
|
|
1841
|
+
steps += 1;
|
|
1842
|
+
if (!sourceResult?.success || !sourceResult.sourceFiles) {
|
|
1843
|
+
return {
|
|
1844
|
+
steps,
|
|
1845
|
+
result: {
|
|
1846
|
+
packageName: params.packageName,
|
|
1847
|
+
strategy: "none",
|
|
1848
|
+
fromVersion: params.vulnerableVersion,
|
|
1849
|
+
applied: false,
|
|
1850
|
+
dryRun: params.dryRun,
|
|
1851
|
+
unresolvedReason: "source-fetch-failed",
|
|
1852
|
+
message: sourceResult?.error ?? `Failed to fetch source for ${params.packageName}@${params.vulnerableVersion}.`
|
|
1501
1853
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
const patchResult = await generatePatchTool.execute({
|
|
1857
|
+
packageName: params.packageName,
|
|
1858
|
+
vulnerableVersion: params.vulnerableVersion,
|
|
1859
|
+
cveId: params.cveId,
|
|
1860
|
+
cveSummary: params.cveSummary,
|
|
1861
|
+
sourceFiles: sourceResult.sourceFiles,
|
|
1862
|
+
vulnerabilityCategory: "unknown",
|
|
1863
|
+
dryRun: params.dryRun
|
|
1864
|
+
});
|
|
1865
|
+
steps += 1;
|
|
1866
|
+
if (!patchResult?.success) {
|
|
1867
|
+
const error = patchResult?.error ?? "Patch generation failed.";
|
|
1868
|
+
const unresolvedReason = error.includes("API_KEY") || error.includes("does not create a language model") ? "requires-llm-fallback" : "patch-generation-failed";
|
|
1869
|
+
return {
|
|
1870
|
+
steps,
|
|
1871
|
+
result: {
|
|
1872
|
+
packageName: params.packageName,
|
|
1873
|
+
strategy: "none",
|
|
1874
|
+
fromVersion: params.vulnerableVersion,
|
|
1875
|
+
applied: false,
|
|
1876
|
+
dryRun: params.dryRun,
|
|
1877
|
+
unresolvedReason,
|
|
1878
|
+
message: error
|
|
1515
1879
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
vulnerableVersion,
|
|
1526
|
-
patchContent: selectedPatch,
|
|
1527
|
-
patchMode
|
|
1528
|
-
});
|
|
1529
|
-
if (!applyResult.success) {
|
|
1530
|
-
return {
|
|
1531
|
-
success: false,
|
|
1532
|
-
packageName,
|
|
1533
|
-
vulnerableVersion,
|
|
1534
|
-
applied: false,
|
|
1535
|
-
dryRun: false,
|
|
1536
|
-
message: applyResult.error,
|
|
1537
|
-
patchFilePath,
|
|
1538
|
-
patchPath: patchFilePath,
|
|
1539
|
-
patchMode,
|
|
1540
|
-
postinstallConfigured: patchMode === "patch-package" ? false : void 0,
|
|
1541
|
-
error: applyResult.error
|
|
1542
|
-
};
|
|
1543
|
-
}
|
|
1544
|
-
if (validateWithTests) {
|
|
1545
|
-
validationResult = await validatePatchWithTests(cwd, pm);
|
|
1546
|
-
if (!validationResult.passed) {
|
|
1547
|
-
const validationError = "Patch validation failed after apply; patch marked unresolved.";
|
|
1548
|
-
return {
|
|
1549
|
-
success: false,
|
|
1550
|
-
packageName,
|
|
1551
|
-
vulnerableVersion,
|
|
1552
|
-
applied: false,
|
|
1553
|
-
dryRun: false,
|
|
1554
|
-
message: validationError,
|
|
1555
|
-
patchFilePath,
|
|
1556
|
-
patchPath: patchFilePath,
|
|
1557
|
-
patchMode,
|
|
1558
|
-
postinstallConfigured: patchMode === "patch-package",
|
|
1559
|
-
validation: validationResult,
|
|
1560
|
-
error: validationError
|
|
1561
|
-
};
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
return {
|
|
1565
|
-
success: true,
|
|
1566
|
-
packageName,
|
|
1567
|
-
vulnerableVersion,
|
|
1568
|
-
applied: true,
|
|
1569
|
-
dryRun: false,
|
|
1570
|
-
message: `Patch applied successfully for ${packageName}@${vulnerableVersion}.`,
|
|
1571
|
-
patchFilePath,
|
|
1572
|
-
patchPath: patchFilePath,
|
|
1573
|
-
patchMode,
|
|
1574
|
-
postinstallConfigured: patchMode === "patch-package",
|
|
1575
|
-
validation: validationResult
|
|
1576
|
-
};
|
|
1577
|
-
});
|
|
1578
|
-
} catch (err) {
|
|
1579
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1580
|
-
return {
|
|
1581
|
-
success: false,
|
|
1582
|
-
packageName,
|
|
1583
|
-
vulnerableVersion,
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
if (typeof patchResult.confidence === "number" && patchResult.confidence < 0.7) {
|
|
1883
|
+
return {
|
|
1884
|
+
steps,
|
|
1885
|
+
result: {
|
|
1886
|
+
packageName: params.packageName,
|
|
1887
|
+
strategy: "none",
|
|
1888
|
+
fromVersion: params.vulnerableVersion,
|
|
1584
1889
|
applied: false,
|
|
1890
|
+
dryRun: params.dryRun,
|
|
1891
|
+
unresolvedReason: "patch-confidence-too-low",
|
|
1892
|
+
message: `Patch confidence ${patchResult.confidence.toFixed(2)} is below threshold 0.70.`
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
const applyResult = await applyPatchFileTool.execute({
|
|
1897
|
+
packageName: params.packageName,
|
|
1898
|
+
vulnerableVersion: params.vulnerableVersion,
|
|
1899
|
+
patchContent: patchResult.patchContent,
|
|
1900
|
+
patches: patchResult.patches,
|
|
1901
|
+
patchesDir: params.patchesDir,
|
|
1902
|
+
cwd: params.cwd,
|
|
1903
|
+
packageManager: params.packageManager,
|
|
1904
|
+
validateWithTests: params.runTests,
|
|
1905
|
+
dryRun: params.dryRun
|
|
1906
|
+
});
|
|
1907
|
+
steps += 1;
|
|
1908
|
+
return {
|
|
1909
|
+
steps,
|
|
1910
|
+
result: {
|
|
1911
|
+
packageName: params.packageName,
|
|
1912
|
+
strategy: "patch-file",
|
|
1913
|
+
fromVersion: params.vulnerableVersion,
|
|
1914
|
+
patchFilePath: applyResult.patchFilePath ?? applyResult.patchPath,
|
|
1915
|
+
applied: Boolean(applyResult.applied),
|
|
1916
|
+
dryRun: Boolean(applyResult.dryRun),
|
|
1917
|
+
unresolvedReason: !Boolean(applyResult.applied) && !Boolean(applyResult.dryRun) ? applyResult.validation?.passed === false ? "patch-validation-failed" : "patch-apply-failed" : void 0,
|
|
1918
|
+
message: applyResult.message ?? applyResult.error ?? "Patch-file strategy finished.",
|
|
1919
|
+
validation: typeof applyResult.validation?.passed === "boolean" ? {
|
|
1920
|
+
passed: applyResult.validation.passed,
|
|
1921
|
+
error: applyResult.validation.error
|
|
1922
|
+
} : void 0
|
|
1923
|
+
}
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// src/remediation/local/run.ts
|
|
1928
|
+
async function runLocalRemediationPipeline(cveId, options = {}) {
|
|
1929
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1930
|
+
const packageManager = options.packageManager ?? detectPackageManager(cwd);
|
|
1931
|
+
const preview = options.preview ?? false;
|
|
1932
|
+
const dryRun = (options.dryRun ?? false) || preview;
|
|
1933
|
+
const runTests = options.runTests ?? false;
|
|
1934
|
+
const policy = options.policy ?? "";
|
|
1935
|
+
const patchesDir = options.patchesDir || "./patches";
|
|
1936
|
+
const constraints = options.constraints ?? {};
|
|
1937
|
+
const collectedResults = [];
|
|
1938
|
+
const vulnerablePackages = [];
|
|
1939
|
+
let cveDetails = null;
|
|
1940
|
+
let agentSteps = 0;
|
|
1941
|
+
const normalizedId = cveId.toUpperCase();
|
|
1942
|
+
const [osvDetails, ghPackages] = await Promise.all([
|
|
1943
|
+
lookupCveOsv(normalizedId),
|
|
1944
|
+
lookupCveGitHub(normalizedId).catch(() => [])
|
|
1945
|
+
]);
|
|
1946
|
+
agentSteps += 2;
|
|
1947
|
+
if (!osvDetails && ghPackages.length === 0) {
|
|
1948
|
+
return {
|
|
1949
|
+
cveId,
|
|
1950
|
+
cveDetails: null,
|
|
1951
|
+
vulnerablePackages,
|
|
1952
|
+
results: collectedResults,
|
|
1953
|
+
agentSteps,
|
|
1954
|
+
summary: `Local mode failed at lookup-cve: ${normalizedId} not found in OSV or GitHub advisory data.`,
|
|
1955
|
+
correlation: {
|
|
1956
|
+
requestId: options.requestId,
|
|
1957
|
+
sessionId: options.sessionId,
|
|
1958
|
+
parentRunId: options.parentRunId
|
|
1959
|
+
}
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
cveDetails = osvDetails ?? {
|
|
1963
|
+
id: normalizedId,
|
|
1964
|
+
summary: "Details sourced from GitHub Advisory Database.",
|
|
1965
|
+
severity: "UNKNOWN",
|
|
1966
|
+
references: [],
|
|
1967
|
+
affectedPackages: []
|
|
1968
|
+
};
|
|
1969
|
+
if (ghPackages.length > 0) {
|
|
1970
|
+
cveDetails = mergeGhDataIntoCveDetails(cveDetails, ghPackages);
|
|
1971
|
+
}
|
|
1972
|
+
cveDetails = await enrichWithNvd(cveDetails);
|
|
1973
|
+
if (cveDetails.affectedPackages.length === 0) {
|
|
1974
|
+
return {
|
|
1975
|
+
cveId,
|
|
1976
|
+
cveDetails,
|
|
1977
|
+
vulnerablePackages,
|
|
1978
|
+
results: collectedResults,
|
|
1979
|
+
agentSteps,
|
|
1980
|
+
summary: `Local mode lookup succeeded but no npm affected packages were found for ${normalizedId}.`,
|
|
1981
|
+
correlation: {
|
|
1982
|
+
requestId: options.requestId,
|
|
1983
|
+
sessionId: options.sessionId,
|
|
1984
|
+
parentRunId: options.parentRunId
|
|
1985
|
+
}
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
const inventory = await checkInventoryTool.execute({ cwd, packageManager });
|
|
1989
|
+
agentSteps += 1;
|
|
1990
|
+
if (inventory?.error) {
|
|
1991
|
+
return {
|
|
1992
|
+
cveId,
|
|
1993
|
+
cveDetails,
|
|
1994
|
+
vulnerablePackages,
|
|
1995
|
+
results: collectedResults,
|
|
1996
|
+
agentSteps,
|
|
1997
|
+
summary: `Local mode failed at check-inventory: ${inventory.error}`,
|
|
1998
|
+
correlation: {
|
|
1999
|
+
requestId: options.requestId,
|
|
2000
|
+
sessionId: options.sessionId,
|
|
2001
|
+
parentRunId: options.parentRunId
|
|
2002
|
+
}
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
const installedPackages = inventory.packages ?? [];
|
|
2006
|
+
for (const affected of cveDetails.affectedPackages) {
|
|
2007
|
+
if (!affected || typeof affected !== "object") continue;
|
|
2008
|
+
if (!affected.name || !affected.vulnerableRange) continue;
|
|
2009
|
+
if (affected.ecosystem !== "npm") continue;
|
|
2010
|
+
const matches = installedPackages.filter((pkg) => pkg.name === affected.name);
|
|
2011
|
+
for (const installed of matches) {
|
|
2012
|
+
if (!semver4.valid(installed.version)) continue;
|
|
2013
|
+
let isVulnerable = false;
|
|
2014
|
+
try {
|
|
2015
|
+
isVulnerable = semver4.satisfies(installed.version, affected.vulnerableRange, {
|
|
2016
|
+
includePrerelease: false
|
|
2017
|
+
});
|
|
2018
|
+
} catch {
|
|
2019
|
+
continue;
|
|
2020
|
+
}
|
|
2021
|
+
if (isVulnerable) {
|
|
2022
|
+
vulnerablePackages.push({ installed, affected });
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
agentSteps += 1;
|
|
2027
|
+
for (const vulnerable of vulnerablePackages) {
|
|
2028
|
+
const primary = await resolvePrimaryResult({
|
|
2029
|
+
vulnerable,
|
|
2030
|
+
cwd,
|
|
2031
|
+
packageManager,
|
|
2032
|
+
dryRun,
|
|
2033
|
+
policy,
|
|
2034
|
+
runTests,
|
|
2035
|
+
constraints
|
|
2036
|
+
});
|
|
2037
|
+
agentSteps += primary.steps;
|
|
2038
|
+
if (shouldAttemptPatchFallback(primary.result, constraints.preferVersionBump ?? false)) {
|
|
2039
|
+
const fallback = await tryLocalPatchFallback({
|
|
2040
|
+
cwd,
|
|
2041
|
+
packageManager,
|
|
2042
|
+
packageName: vulnerable.installed.name,
|
|
2043
|
+
vulnerableVersion: vulnerable.installed.version,
|
|
2044
|
+
cveId: normalizedId,
|
|
2045
|
+
cveSummary: cveDetails?.summary ?? normalizedId,
|
|
1585
2046
|
dryRun,
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
};
|
|
2047
|
+
runTests,
|
|
2048
|
+
patchesDir
|
|
2049
|
+
});
|
|
2050
|
+
agentSteps += fallback.steps;
|
|
2051
|
+
collectedResults.push(fallback.result);
|
|
2052
|
+
continue;
|
|
1589
2053
|
}
|
|
2054
|
+
collectedResults.push(primary.result);
|
|
1590
2055
|
}
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
2056
|
+
const appliedCount = collectedResults.filter((result) => result.applied).length;
|
|
2057
|
+
const unresolvedCount = collectedResults.filter((result) => !result.applied && !result.dryRun).length;
|
|
2058
|
+
const dryRunCount = collectedResults.filter((result) => result.dryRun).length;
|
|
2059
|
+
return {
|
|
2060
|
+
cveId,
|
|
2061
|
+
cveDetails,
|
|
2062
|
+
vulnerablePackages,
|
|
2063
|
+
results: collectedResults,
|
|
2064
|
+
agentSteps,
|
|
2065
|
+
summary: `Local mode completed: vulnerable=${vulnerablePackages.length}, applied=${appliedCount}, dryRun=${dryRunCount}, unresolved=${unresolvedCount}`,
|
|
2066
|
+
correlation: {
|
|
2067
|
+
requestId: options.requestId,
|
|
2068
|
+
sessionId: options.sessionId,
|
|
2069
|
+
parentRunId: options.parentRunId
|
|
2070
|
+
}
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// src/remediation/tools/lookup-cve.ts
|
|
2075
|
+
import { tool as tool7 } from "ai";
|
|
2076
|
+
import { z as z7 } from "zod";
|
|
2077
|
+
|
|
2078
|
+
// src/intelligence/sources/cisa-kev.ts
|
|
2079
|
+
var CISA_KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json";
|
|
2080
|
+
async function fetchCisaKevFeed() {
|
|
1595
2081
|
try {
|
|
1596
|
-
const
|
|
1597
|
-
|
|
1598
|
-
stdio: "pipe"
|
|
2082
|
+
const res = await fetch(CISA_KEV_URL, {
|
|
2083
|
+
headers: { Accept: "application/json" }
|
|
1599
2084
|
});
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
return major >= 2 ? "native-yarn" : "patch-package";
|
|
2085
|
+
if (!res.ok) return void 0;
|
|
2086
|
+
return await res.json();
|
|
1603
2087
|
} catch {
|
|
1604
|
-
return
|
|
2088
|
+
return void 0;
|
|
1605
2089
|
}
|
|
1606
2090
|
}
|
|
1607
|
-
function
|
|
1608
|
-
|
|
1609
|
-
|
|
2091
|
+
function findKevEntry(feed, cveId) {
|
|
2092
|
+
if (!feed?.vulnerabilities?.length) return void 0;
|
|
2093
|
+
const normalized = cveId.toUpperCase();
|
|
2094
|
+
return feed.vulnerabilities.find((v) => v.cveID.toUpperCase() === normalized);
|
|
1610
2095
|
}
|
|
1611
|
-
async function
|
|
1612
|
-
const
|
|
1613
|
-
|
|
2096
|
+
async function enrichWithCisaKev(details) {
|
|
2097
|
+
const feed = await fetchCisaKevFeed();
|
|
2098
|
+
const entry = findKevEntry(feed, details.id);
|
|
2099
|
+
if (!entry) return details;
|
|
2100
|
+
details.kev = {
|
|
2101
|
+
knownExploited: true,
|
|
2102
|
+
dateAdded: entry.dateAdded,
|
|
2103
|
+
dueDate: entry.dueDate,
|
|
2104
|
+
requiredAction: entry.requiredAction,
|
|
2105
|
+
knownRansomwareCampaignUse: entry.knownRansomwareCampaignUse
|
|
2106
|
+
};
|
|
2107
|
+
if (!details.references.includes(CISA_KEV_URL)) {
|
|
2108
|
+
details.references.push(CISA_KEV_URL);
|
|
2109
|
+
}
|
|
2110
|
+
return details;
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// src/intelligence/sources/epss.ts
|
|
2114
|
+
async function fetchEpss(cveId) {
|
|
2115
|
+
const { epssApi } = getIntelligenceSourceConfig();
|
|
2116
|
+
if (!epssApi) return void 0;
|
|
1614
2117
|
try {
|
|
1615
|
-
|
|
2118
|
+
const url = new URL(epssApi);
|
|
2119
|
+
url.searchParams.set("cve", cveId);
|
|
2120
|
+
const res = await fetch(url.toString(), {
|
|
2121
|
+
headers: { Accept: "application/json" }
|
|
2122
|
+
});
|
|
2123
|
+
if (!res.ok) return void 0;
|
|
2124
|
+
const body = await res.json();
|
|
2125
|
+
return body.data?.[0];
|
|
1616
2126
|
} catch {
|
|
1617
|
-
return
|
|
1618
|
-
success: false,
|
|
1619
|
-
error: `Could not read package.json at ${pkgJsonPath}`
|
|
1620
|
-
};
|
|
1621
|
-
}
|
|
1622
|
-
const devDependencies = pkgJson.devDependencies ?? {};
|
|
1623
|
-
if (!devDependencies["patch-package"]) {
|
|
1624
|
-
try {
|
|
1625
|
-
const commands = getPackageManagerCommands(packageManager);
|
|
1626
|
-
const [cmd, ...args] = commands.installDev("patch-package");
|
|
1627
|
-
await execa4(cmd, args, {
|
|
1628
|
-
cwd,
|
|
1629
|
-
stdio: "pipe"
|
|
1630
|
-
});
|
|
1631
|
-
} catch (err) {
|
|
1632
|
-
return {
|
|
1633
|
-
success: false,
|
|
1634
|
-
error: `Failed to install patch-package: ${err instanceof Error ? err.message : String(err)}`
|
|
1635
|
-
};
|
|
1636
|
-
}
|
|
2127
|
+
return void 0;
|
|
1637
2128
|
}
|
|
1638
|
-
|
|
1639
|
-
|
|
2129
|
+
}
|
|
2130
|
+
async function enrichWithEpss(details) {
|
|
2131
|
+
const row = await fetchEpss(details.id);
|
|
2132
|
+
if (!row) return details;
|
|
2133
|
+
const score = Number.parseFloat(row.epss);
|
|
2134
|
+
const percentile = Number.parseFloat(row.percentile);
|
|
2135
|
+
if (!Number.isFinite(score) || !Number.isFinite(percentile)) {
|
|
2136
|
+
return details;
|
|
1640
2137
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
}
|
|
1646
|
-
|
|
2138
|
+
details.epss = {
|
|
2139
|
+
score,
|
|
2140
|
+
percentile,
|
|
2141
|
+
date: row.date
|
|
2142
|
+
};
|
|
2143
|
+
return details;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
// src/intelligence/sources/cve-services.ts
|
|
2147
|
+
function pickEnglishDescription(container) {
|
|
2148
|
+
if (!container?.descriptions?.length) return void 0;
|
|
2149
|
+
const en = container.descriptions.find((d) => d.lang === "en" && d.value);
|
|
2150
|
+
return (en?.value ?? container.descriptions[0]?.value)?.trim() || void 0;
|
|
2151
|
+
}
|
|
2152
|
+
function collectReferences(record) {
|
|
2153
|
+
const refs = /* @__PURE__ */ new Set();
|
|
2154
|
+
const cnaRefs = record.containers?.cna?.references ?? [];
|
|
2155
|
+
const adpRefs = (record.containers?.adp ?? []).flatMap((c) => c.references ?? []);
|
|
2156
|
+
for (const ref of [...cnaRefs, ...adpRefs]) {
|
|
2157
|
+
if (ref.url) refs.add(ref.url);
|
|
1647
2158
|
}
|
|
1648
|
-
|
|
1649
|
-
return { success: true };
|
|
2159
|
+
return Array.from(refs);
|
|
1650
2160
|
}
|
|
1651
|
-
async function
|
|
1652
|
-
const {
|
|
1653
|
-
|
|
1654
|
-
const createCommand = patchMode === "native-pnpm" ? "pnpm" : "yarn";
|
|
1655
|
-
const createArgs = ["patch", packageSpec];
|
|
1656
|
-
let patchDir;
|
|
2161
|
+
async function fetchCveServicesRecord(cveId) {
|
|
2162
|
+
const { cveServicesApi } = getIntelligenceSourceConfig();
|
|
2163
|
+
if (!cveServicesApi) return void 0;
|
|
1657
2164
|
try {
|
|
1658
|
-
const
|
|
1659
|
-
|
|
1660
|
-
stdio: "pipe"
|
|
2165
|
+
const res = await fetch(`${cveServicesApi}/${encodeURIComponent(cveId)}`, {
|
|
2166
|
+
headers: { Accept: "application/json" }
|
|
1661
2167
|
});
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
} catch
|
|
1665
|
-
return
|
|
1666
|
-
success: false,
|
|
1667
|
-
error: `Failed to create native patch workspace for ${packageSpec}: ${err instanceof Error ? err.message : String(err)}`
|
|
1668
|
-
};
|
|
2168
|
+
if (!res.ok) return void 0;
|
|
2169
|
+
return await res.json();
|
|
2170
|
+
} catch {
|
|
2171
|
+
return void 0;
|
|
1669
2172
|
}
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
2173
|
+
}
|
|
2174
|
+
async function enrichWithCveServices(details) {
|
|
2175
|
+
const record = await fetchCveServicesRecord(details.id);
|
|
2176
|
+
if (!record) return details;
|
|
2177
|
+
const summary = pickEnglishDescription(record.containers?.cna);
|
|
2178
|
+
if (summary && (!details.summary || details.summary.includes("No summary available"))) {
|
|
2179
|
+
details.summary = summary;
|
|
1675
2180
|
}
|
|
1676
|
-
const
|
|
1677
|
-
|
|
2181
|
+
const refs = collectReferences(record);
|
|
2182
|
+
if (refs.length > 0) {
|
|
2183
|
+
const merged = /* @__PURE__ */ new Set([...details.references, ...refs]);
|
|
2184
|
+
details.references = Array.from(merged);
|
|
2185
|
+
}
|
|
2186
|
+
details.intelligence = {
|
|
2187
|
+
...details.intelligence ?? {},
|
|
2188
|
+
cveServicesEnriched: true
|
|
2189
|
+
};
|
|
2190
|
+
return details;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
// src/intelligence/sources/gitlab-advisory.ts
|
|
2194
|
+
function advisoryMatchesCve(advisory, cveId) {
|
|
2195
|
+
const normalized = cveId.toUpperCase();
|
|
2196
|
+
return (advisory.identifiers ?? []).some(
|
|
2197
|
+
(id) => id.type?.toUpperCase() === "CVE" && id.value?.toUpperCase() === normalized
|
|
2198
|
+
);
|
|
2199
|
+
}
|
|
2200
|
+
async function fetchGitLabAdvisories(cveId) {
|
|
2201
|
+
const { gitLabAdvisoryApi } = getIntelligenceSourceConfig();
|
|
2202
|
+
if (!gitLabAdvisoryApi) return [];
|
|
1678
2203
|
try {
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
const commitCommand = patchMode === "native-pnpm" ? "pnpm" : "yarn";
|
|
1685
|
-
const commitArgs = patchMode === "native-pnpm" ? ["patch-commit", patchDir] : ["patch-commit", "-s", patchDir];
|
|
1686
|
-
await execa4(commitCommand, commitArgs, {
|
|
1687
|
-
cwd,
|
|
1688
|
-
stdio: "pipe"
|
|
2204
|
+
const url = new URL(gitLabAdvisoryApi);
|
|
2205
|
+
url.searchParams.set("identifier", cveId);
|
|
2206
|
+
url.searchParams.set("ecosystem", "npm");
|
|
2207
|
+
const res = await fetch(url.toString(), {
|
|
2208
|
+
headers: { Accept: "application/json" }
|
|
1689
2209
|
});
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
} finally {
|
|
1696
|
-
await rm3(tempPatchDir, { recursive: true, force: true });
|
|
2210
|
+
if (!res.ok) return [];
|
|
2211
|
+
const body = await res.json();
|
|
2212
|
+
return Array.isArray(body) ? body : [];
|
|
2213
|
+
} catch {
|
|
2214
|
+
return [];
|
|
1697
2215
|
}
|
|
1698
|
-
return { success: true };
|
|
1699
2216
|
}
|
|
1700
|
-
function
|
|
1701
|
-
const
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
const
|
|
1707
|
-
|
|
1708
|
-
if (token.startsWith("/") && existsSync3(token)) {
|
|
1709
|
-
return token;
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
2217
|
+
async function enrichWithGitLabAdvisory(details) {
|
|
2218
|
+
const advisories = await fetchGitLabAdvisories(details.id);
|
|
2219
|
+
const matched = advisories.filter((a) => advisoryMatchesCve(a, details.id));
|
|
2220
|
+
if (matched.length === 0) return details;
|
|
2221
|
+
const refs = matched.flatMap((m) => m.references ?? []);
|
|
2222
|
+
if (refs.length > 0) {
|
|
2223
|
+
const merged = /* @__PURE__ */ new Set([...details.references, ...refs]);
|
|
2224
|
+
details.references = Array.from(merged);
|
|
1712
2225
|
}
|
|
1713
|
-
|
|
2226
|
+
details.intelligence = {
|
|
2227
|
+
...details.intelligence ?? {},
|
|
2228
|
+
gitlabAdvisoryMatched: true
|
|
2229
|
+
};
|
|
2230
|
+
return details;
|
|
1714
2231
|
}
|
|
1715
|
-
|
|
2232
|
+
|
|
2233
|
+
// src/intelligence/sources/certcc.ts
|
|
2234
|
+
var CERTCC_HOME = "https://www.kb.cert.org/vuls/";
|
|
2235
|
+
async function findCertCcReference(cveId) {
|
|
2236
|
+
const { certCcSearchUrl } = getIntelligenceSourceConfig();
|
|
2237
|
+
if (!certCcSearchUrl) return void 0;
|
|
1716
2238
|
try {
|
|
1717
|
-
const
|
|
1718
|
-
|
|
1719
|
-
const
|
|
1720
|
-
|
|
1721
|
-
timeout: 6e4,
|
|
1722
|
-
// 60 second timeout
|
|
1723
|
-
stdio: "pipe"
|
|
2239
|
+
const url = new URL(certCcSearchUrl);
|
|
2240
|
+
url.searchParams.set("query", cveId);
|
|
2241
|
+
const res = await fetch(url.toString(), {
|
|
2242
|
+
headers: { Accept: "text/html" }
|
|
1724
2243
|
});
|
|
1725
|
-
return
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
} catch
|
|
1730
|
-
|
|
1731
|
-
const failedTests = extractFailedTests(errorOutput);
|
|
1732
|
-
return {
|
|
1733
|
-
passed: false,
|
|
1734
|
-
output: errorOutput,
|
|
1735
|
-
failedTests
|
|
1736
|
-
};
|
|
2244
|
+
if (!res.ok) return void 0;
|
|
2245
|
+
const html = await res.text();
|
|
2246
|
+
const match = html.match(/https:\/\/www\.kb\.cert\.org\/vuls\/id\/\d+/i);
|
|
2247
|
+
return match?.[0] ?? void 0;
|
|
2248
|
+
} catch {
|
|
2249
|
+
return void 0;
|
|
1737
2250
|
}
|
|
1738
2251
|
}
|
|
1739
|
-
function
|
|
1740
|
-
const
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
/●\s+(.+)(?:\n|$)/g,
|
|
1745
|
-
// Jest style
|
|
1746
|
-
/FAIL.*?(.+?)(?:\n|$)/g
|
|
1747
|
-
// Generic FAIL
|
|
1748
|
-
];
|
|
1749
|
-
for (const pattern of patterns) {
|
|
1750
|
-
let match;
|
|
1751
|
-
while ((match = pattern.exec(output)) !== null) {
|
|
1752
|
-
if (match[1]) {
|
|
1753
|
-
failedTests.push(match[1].trim());
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
2252
|
+
async function enrichWithCertCc(details) {
|
|
2253
|
+
const ref = await findCertCcReference(details.id);
|
|
2254
|
+
if (!ref) return details;
|
|
2255
|
+
if (!details.references.includes(ref)) {
|
|
2256
|
+
details.references.push(ref);
|
|
1756
2257
|
}
|
|
1757
|
-
|
|
2258
|
+
details.intelligence = {
|
|
2259
|
+
...details.intelligence ?? {},
|
|
2260
|
+
certCcMatched: true
|
|
2261
|
+
};
|
|
2262
|
+
if (!details.references.includes(CERTCC_HOME)) {
|
|
2263
|
+
details.references.push(CERTCC_HOME);
|
|
2264
|
+
}
|
|
2265
|
+
return details;
|
|
1758
2266
|
}
|
|
1759
2267
|
|
|
1760
|
-
// src/
|
|
1761
|
-
async function
|
|
1762
|
-
const
|
|
1763
|
-
if (
|
|
1764
|
-
|
|
2268
|
+
// src/intelligence/sources/deps-dev.ts
|
|
2269
|
+
async function fetchDepsDevPackage(name) {
|
|
2270
|
+
const { depsDevApi } = getIntelligenceSourceConfig();
|
|
2271
|
+
if (!depsDevApi) return false;
|
|
2272
|
+
try {
|
|
2273
|
+
const url = `${depsDevApi}/systems/npm/packages/${encodeURIComponent(name)}`;
|
|
2274
|
+
const res = await fetch(url, { headers: { Accept: "application/json" } });
|
|
2275
|
+
return res.ok;
|
|
2276
|
+
} catch {
|
|
2277
|
+
return false;
|
|
1765
2278
|
}
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
const
|
|
1769
|
-
|
|
1770
|
-
const
|
|
1771
|
-
const
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
cwd,
|
|
1777
|
-
dryRun,
|
|
1778
|
-
runTests,
|
|
1779
|
-
policy,
|
|
1780
|
-
patchesDir,
|
|
1781
|
-
packageManager
|
|
1782
|
-
});
|
|
1783
|
-
const prompt = `Patch vulnerable dependencies affected by ${cveId} in the project at: ${cwd}. Package manager: ${packageManager}.`;
|
|
1784
|
-
const collectedResults = [];
|
|
1785
|
-
const vulnerablePackages = [];
|
|
1786
|
-
let cveDetails = null;
|
|
1787
|
-
let agentSteps = 0;
|
|
1788
|
-
const applyVersionBumpToolForRun = preview ? {
|
|
1789
|
-
...applyVersionBumpTool,
|
|
1790
|
-
execute: async (input) => applyVersionBumpTool.execute({ ...input, dryRun: true })
|
|
1791
|
-
} : applyVersionBumpTool;
|
|
1792
|
-
const applyPatchFileToolForRun = preview ? {
|
|
1793
|
-
...applyPatchFileTool,
|
|
1794
|
-
execute: async (input) => applyPatchFileTool.execute({ ...input, dryRun: true })
|
|
1795
|
-
} : applyPatchFileTool;
|
|
1796
|
-
const result = await generateText2({
|
|
1797
|
-
model,
|
|
1798
|
-
system: systemPrompt,
|
|
1799
|
-
prompt,
|
|
1800
|
-
tools: {
|
|
1801
|
-
"lookup-cve": lookupCveTool,
|
|
1802
|
-
"check-inventory": checkInventoryTool,
|
|
1803
|
-
"check-version-match": checkVersionMatchTool,
|
|
1804
|
-
"find-fixed-version": findFixedVersionTool,
|
|
1805
|
-
"apply-version-bump": applyVersionBumpToolForRun,
|
|
1806
|
-
"fetch-package-source": fetchPackageSourceTool,
|
|
1807
|
-
"generate-patch": generatePatchTool,
|
|
1808
|
-
"apply-patch-file": applyPatchFileToolForRun
|
|
1809
|
-
},
|
|
1810
|
-
maxSteps: 25,
|
|
1811
|
-
onStepFinish(stepResult) {
|
|
1812
|
-
agentSteps += 1;
|
|
1813
|
-
const { toolResults } = stepResult;
|
|
1814
|
-
for (const tr of toolResults ?? []) {
|
|
1815
|
-
const toolResult = tr.result;
|
|
1816
|
-
if (tr.toolName === "lookup-cve" && toolResult?.data) {
|
|
1817
|
-
cveDetails = toolResult.data;
|
|
1818
|
-
}
|
|
1819
|
-
if (tr.toolName === "check-version-match" && toolResult?.vulnerablePackages) {
|
|
1820
|
-
vulnerablePackages.push(...toolResult.vulnerablePackages);
|
|
1821
|
-
}
|
|
1822
|
-
if (tr.toolName === "apply-version-bump") {
|
|
1823
|
-
collectedResults.push(toolResult);
|
|
1824
|
-
}
|
|
1825
|
-
if (tr.toolName === "apply-patch-file" && toolResult) {
|
|
1826
|
-
const validation = toolResult.validation;
|
|
1827
|
-
const message = typeof toolResult.message === "string" ? toolResult.message : typeof toolResult.error === "string" ? toolResult.error : "Patch-file strategy finished.";
|
|
1828
|
-
collectedResults.push({
|
|
1829
|
-
packageName: typeof toolResult.packageName === "string" ? toolResult.packageName : "unknown-package",
|
|
1830
|
-
strategy: "patch-file",
|
|
1831
|
-
fromVersion: typeof toolResult.vulnerableVersion === "string" ? toolResult.vulnerableVersion : "unknown",
|
|
1832
|
-
patchFilePath: typeof toolResult.patchFilePath === "string" ? toolResult.patchFilePath : typeof toolResult.patchPath === "string" ? toolResult.patchPath : void 0,
|
|
1833
|
-
applied: Boolean(toolResult.applied),
|
|
1834
|
-
dryRun: Boolean(toolResult.dryRun),
|
|
1835
|
-
message,
|
|
1836
|
-
validation: validation && typeof validation.passed === "boolean" ? {
|
|
1837
|
-
passed: validation.passed,
|
|
1838
|
-
error: typeof validation.error === "string" ? validation.error : void 0
|
|
1839
|
-
} : void 0
|
|
1840
|
-
});
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
}
|
|
1844
|
-
});
|
|
1845
|
-
return {
|
|
1846
|
-
cveId,
|
|
1847
|
-
cveDetails,
|
|
1848
|
-
vulnerablePackages,
|
|
1849
|
-
results: collectedResults,
|
|
1850
|
-
agentSteps,
|
|
1851
|
-
summary: result.text,
|
|
1852
|
-
correlation: {
|
|
1853
|
-
requestId: options.requestId,
|
|
1854
|
-
sessionId: options.sessionId,
|
|
1855
|
-
parentRunId: options.parentRunId
|
|
1856
|
-
}
|
|
2279
|
+
}
|
|
2280
|
+
async function enrichWithDepsDev(details) {
|
|
2281
|
+
const names = Array.from(new Set(details.affectedPackages.map((p) => p.name))).slice(0, 20);
|
|
2282
|
+
if (names.length === 0) return details;
|
|
2283
|
+
const checks = await Promise.all(names.map((name) => fetchDepsDevPackage(name)));
|
|
2284
|
+
const matched = checks.filter(Boolean).length;
|
|
2285
|
+
if (matched === 0) return details;
|
|
2286
|
+
details.intelligence = {
|
|
2287
|
+
...details.intelligence ?? {},
|
|
2288
|
+
depsDevEnrichedPackages: matched
|
|
1857
2289
|
};
|
|
2290
|
+
return details;
|
|
1858
2291
|
}
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
const
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
lookupCveGitHub(normalizedId).catch(() => [])
|
|
1874
|
-
]);
|
|
1875
|
-
agentSteps += 2;
|
|
1876
|
-
if (!osvDetails && ghPackages.length === 0) {
|
|
1877
|
-
return {
|
|
1878
|
-
cveId,
|
|
1879
|
-
cveDetails: null,
|
|
1880
|
-
vulnerablePackages,
|
|
1881
|
-
results: collectedResults,
|
|
1882
|
-
agentSteps,
|
|
1883
|
-
summary: `Local mode failed at lookup-cve: ${normalizedId} not found in OSV or GitHub advisory data.`,
|
|
1884
|
-
correlation: {
|
|
1885
|
-
requestId: options.requestId,
|
|
1886
|
-
sessionId: options.sessionId,
|
|
1887
|
-
parentRunId: options.parentRunId
|
|
1888
|
-
}
|
|
1889
|
-
};
|
|
2292
|
+
|
|
2293
|
+
// src/intelligence/sources/ossf-scorecard.ts
|
|
2294
|
+
async function checkProject(project) {
|
|
2295
|
+
const { scorecardApi } = getIntelligenceSourceConfig();
|
|
2296
|
+
if (!scorecardApi) return false;
|
|
2297
|
+
try {
|
|
2298
|
+
const url = new URL(`${scorecardApi}/projects`);
|
|
2299
|
+
url.searchParams.set("project", project);
|
|
2300
|
+
const res = await fetch(url.toString(), {
|
|
2301
|
+
headers: { Accept: "application/json" }
|
|
2302
|
+
});
|
|
2303
|
+
return res.ok;
|
|
2304
|
+
} catch {
|
|
2305
|
+
return false;
|
|
1890
2306
|
}
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
2307
|
+
}
|
|
2308
|
+
async function enrichWithOssfScorecard(details) {
|
|
2309
|
+
const projects = Array.from(
|
|
2310
|
+
new Set(details.affectedPackages.map((p) => `github.com/${p.name}/${p.name}`))
|
|
2311
|
+
).slice(0, 10);
|
|
2312
|
+
if (projects.length === 0) return details;
|
|
2313
|
+
const checks = await Promise.all(projects.map((project) => checkProject(project)));
|
|
2314
|
+
const matched = checks.filter(Boolean).length;
|
|
2315
|
+
if (matched === 0) return details;
|
|
2316
|
+
details.intelligence = {
|
|
2317
|
+
...details.intelligence ?? {},
|
|
2318
|
+
scorecardProjects: matched
|
|
1897
2319
|
};
|
|
1898
|
-
|
|
1899
|
-
|
|
2320
|
+
return details;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
// src/intelligence/sources/external-feeds.ts
|
|
2324
|
+
async function probeFeed(url, cveId, token) {
|
|
2325
|
+
try {
|
|
2326
|
+
const feedUrl = new URL(url);
|
|
2327
|
+
feedUrl.searchParams.set("cve", cveId);
|
|
2328
|
+
const headers = { Accept: "application/json" };
|
|
2329
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
2330
|
+
const res = await fetch(feedUrl.toString(), { headers });
|
|
2331
|
+
if (!res.ok) return void 0;
|
|
2332
|
+
return feedUrl.toString();
|
|
2333
|
+
} catch {
|
|
2334
|
+
return void 0;
|
|
1900
2335
|
}
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
}
|
|
1915
|
-
};
|
|
2336
|
+
}
|
|
2337
|
+
async function enrichWithExternalFeeds(details) {
|
|
2338
|
+
const {
|
|
2339
|
+
vendorAdvisoryFeeds,
|
|
2340
|
+
commercialFeeds,
|
|
2341
|
+
commercialFeedToken
|
|
2342
|
+
} = getIntelligenceSourceConfig();
|
|
2343
|
+
const vendorHits = (await Promise.all(vendorAdvisoryFeeds.map((url) => probeFeed(url, details.id)))).filter((v) => Boolean(v));
|
|
2344
|
+
const commercialHits = (await Promise.all(
|
|
2345
|
+
commercialFeeds.map((url) => probeFeed(url, details.id, commercialFeedToken))
|
|
2346
|
+
)).filter((v) => Boolean(v));
|
|
2347
|
+
if (vendorHits.length === 0 && commercialHits.length === 0) {
|
|
2348
|
+
return details;
|
|
1916
2349
|
}
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
2350
|
+
details.intelligence = {
|
|
2351
|
+
...details.intelligence ?? {},
|
|
2352
|
+
vendorAdvisories: vendorHits.length > 0 ? vendorHits : details.intelligence?.vendorAdvisories,
|
|
2353
|
+
commercialFeeds: commercialHits.length > 0 ? commercialHits : details.intelligence?.commercialFeeds
|
|
2354
|
+
};
|
|
2355
|
+
const mergedRefs = /* @__PURE__ */ new Set([...details.references, ...vendorHits, ...commercialHits]);
|
|
2356
|
+
details.references = Array.from(mergedRefs);
|
|
2357
|
+
return details;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
// src/remediation/tools/lookup-cve.ts
|
|
2361
|
+
var lookupCveTool = tool7({
|
|
2362
|
+
description: "Look up a CVE ID and return the list of affected npm packages, their vulnerable version ranges, and the first patched version. Always call this first.",
|
|
2363
|
+
parameters: z7.object({
|
|
2364
|
+
cveId: z7.string().regex(/^CVE-\d{4}-\d+$/i, "Must be a valid CVE ID like CVE-2021-23337")
|
|
2365
|
+
}),
|
|
2366
|
+
execute: async ({ cveId }) => {
|
|
2367
|
+
const normalizedId = cveId.toUpperCase();
|
|
2368
|
+
const [osvDetails, ghPackages] = await Promise.all([
|
|
2369
|
+
lookupCveOsv(normalizedId),
|
|
2370
|
+
lookupCveGitHub(normalizedId)
|
|
2371
|
+
]);
|
|
2372
|
+
if (!osvDetails && ghPackages.length === 0) {
|
|
2373
|
+
return {
|
|
2374
|
+
success: false,
|
|
2375
|
+
error: `CVE "${normalizedId}" was not found in OSV or GitHub Advisory databases. It may be too new, or not affect npm packages.`
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
let details = osvDetails ?? {
|
|
2379
|
+
id: normalizedId,
|
|
2380
|
+
summary: "Details sourced from GitHub Advisory Database.",
|
|
2381
|
+
severity: "UNKNOWN",
|
|
2382
|
+
references: [],
|
|
2383
|
+
affectedPackages: []
|
|
1932
2384
|
};
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
const matches = installedPackages.filter((p) => p.name === affected.name);
|
|
1940
|
-
for (const installed of matches) {
|
|
1941
|
-
if (!semver4.valid(installed.version)) continue;
|
|
1942
|
-
let isVulnerable = false;
|
|
2385
|
+
if (ghPackages.length > 0) {
|
|
2386
|
+
details = mergeGhDataIntoCveDetails(details, ghPackages);
|
|
2387
|
+
}
|
|
2388
|
+
const sourceHealth = {};
|
|
2389
|
+
const applyEnricher = async (sourceName, enricher) => {
|
|
2390
|
+
const before = JSON.stringify(details);
|
|
1943
2391
|
try {
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
2392
|
+
details = await enricher(details);
|
|
2393
|
+
const after = JSON.stringify(details);
|
|
2394
|
+
sourceHealth[sourceName] = {
|
|
2395
|
+
attempted: true,
|
|
2396
|
+
changed: before !== after
|
|
2397
|
+
};
|
|
2398
|
+
} catch (error) {
|
|
2399
|
+
sourceHealth[sourceName] = {
|
|
2400
|
+
attempted: true,
|
|
2401
|
+
changed: false,
|
|
2402
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2403
|
+
};
|
|
1952
2404
|
}
|
|
2405
|
+
};
|
|
2406
|
+
await applyEnricher("nvd", enrichWithNvd);
|
|
2407
|
+
await applyEnricher("cisa-kev", enrichWithCisaKev);
|
|
2408
|
+
await applyEnricher("epss", enrichWithEpss);
|
|
2409
|
+
await applyEnricher("cve-services", enrichWithCveServices);
|
|
2410
|
+
await applyEnricher("gitlab-advisory", enrichWithGitLabAdvisory);
|
|
2411
|
+
await applyEnricher("certcc", enrichWithCertCc);
|
|
2412
|
+
await applyEnricher("deps-dev", enrichWithDepsDev);
|
|
2413
|
+
await applyEnricher("ossf-scorecard", enrichWithOssfScorecard);
|
|
2414
|
+
await applyEnricher("external-feeds", enrichWithExternalFeeds);
|
|
2415
|
+
details.intelligence = {
|
|
2416
|
+
...details.intelligence ?? {},
|
|
2417
|
+
sourceHealth
|
|
2418
|
+
};
|
|
2419
|
+
if (details.affectedPackages.length === 0) {
|
|
2420
|
+
return {
|
|
2421
|
+
success: false,
|
|
2422
|
+
error: `CVE "${normalizedId}" was found but has no npm-specific affected packages listed. It may affect a different ecosystem.`
|
|
2423
|
+
};
|
|
1953
2424
|
}
|
|
2425
|
+
return { success: true, data: details };
|
|
1954
2426
|
}
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2427
|
+
});
|
|
2428
|
+
|
|
2429
|
+
// src/remediation/tools/check-version-match.ts
|
|
2430
|
+
import { tool as tool8 } from "ai";
|
|
2431
|
+
import { z as z8 } from "zod";
|
|
2432
|
+
import semver5 from "semver";
|
|
2433
|
+
var affectedPackageSchema = z8.object({
|
|
2434
|
+
name: z8.string(),
|
|
2435
|
+
ecosystem: z8.literal("npm"),
|
|
2436
|
+
vulnerableRange: z8.string(),
|
|
2437
|
+
firstPatchedVersion: z8.string().optional(),
|
|
2438
|
+
source: z8.enum(["osv", "github-advisory"])
|
|
2439
|
+
});
|
|
2440
|
+
var inventoryPackageSchema = z8.object({
|
|
2441
|
+
name: z8.string(),
|
|
2442
|
+
version: z8.string(),
|
|
2443
|
+
type: z8.enum(["direct", "indirect"])
|
|
2444
|
+
});
|
|
2445
|
+
var checkVersionMatchTool = tool8({
|
|
2446
|
+
description: "Check which of the project's installed packages fall within the CVE's vulnerable version ranges. Returns only the packages that are actually vulnerable.",
|
|
2447
|
+
parameters: z8.object({
|
|
2448
|
+
installedPackages: z8.array(inventoryPackageSchema).describe("Output from the check-inventory tool"),
|
|
2449
|
+
affectedPackages: z8.array(affectedPackageSchema).describe("affectedPackages array from the lookup-cve tool result")
|
|
2450
|
+
}),
|
|
2451
|
+
execute: async ({ installedPackages, affectedPackages }) => {
|
|
2452
|
+
const vulnerable = [];
|
|
2453
|
+
for (const affected of affectedPackages) {
|
|
2454
|
+
const matches = installedPackages.filter(
|
|
2455
|
+
(p) => p.name === affected.name
|
|
2456
|
+
);
|
|
2457
|
+
for (const installed of matches) {
|
|
2458
|
+
if (!semver5.valid(installed.version)) continue;
|
|
2459
|
+
let isVulnerable = false;
|
|
2460
|
+
try {
|
|
2461
|
+
isVulnerable = semver5.satisfies(installed.version, affected.vulnerableRange, {
|
|
2462
|
+
includePrerelease: false
|
|
2463
|
+
});
|
|
2464
|
+
} catch {
|
|
2465
|
+
continue;
|
|
2466
|
+
}
|
|
2467
|
+
if (isVulnerable) {
|
|
2468
|
+
vulnerable.push({ installed, affected });
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
1980
2471
|
}
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2472
|
+
return {
|
|
2473
|
+
vulnerablePackages: vulnerable,
|
|
2474
|
+
checkedCount: installedPackages.length
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
});
|
|
2478
|
+
|
|
2479
|
+
// src/remediation/tools/find-fixed-version.ts
|
|
2480
|
+
import { tool as tool9 } from "ai";
|
|
2481
|
+
import { z as z9 } from "zod";
|
|
2482
|
+
var findFixedVersionTool = tool9({
|
|
2483
|
+
description: "Query the npm registry to find the safest published upgrade version for a package that is >= the first patched version. Prefer patch upgrades first, then minor, and only fall back to major when no same-major fix exists.",
|
|
2484
|
+
parameters: z9.object({
|
|
2485
|
+
packageName: z9.string().describe("The npm package name"),
|
|
2486
|
+
installedVersion: z9.string().describe("The currently installed version (exact semver)"),
|
|
2487
|
+
firstPatchedVersion: z9.string().describe(
|
|
2488
|
+
"The first version that is NOT vulnerable (from lookup-cve). Use this as the floor."
|
|
2489
|
+
),
|
|
2490
|
+
vulnerableRange: z9.string().optional().describe("Optional vulnerable semver range used to exclude still-vulnerable versions")
|
|
2491
|
+
}),
|
|
2492
|
+
execute: async ({
|
|
2493
|
+
packageName,
|
|
2494
|
+
installedVersion,
|
|
2495
|
+
firstPatchedVersion,
|
|
2496
|
+
vulnerableRange
|
|
2497
|
+
}) => {
|
|
2498
|
+
const resolution = await resolveSafeUpgradeVersion(
|
|
2499
|
+
packageName,
|
|
2500
|
+
installedVersion,
|
|
1984
2501
|
firstPatchedVersion,
|
|
1985
|
-
|
|
2502
|
+
vulnerableRange
|
|
1986
2503
|
);
|
|
1987
|
-
|
|
2504
|
+
const { safeVersion, upgradeLevel, candidates, majorOnlyFixAvailable } = resolution;
|
|
1988
2505
|
if (!safeVersion) {
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
message: `No safe upgrade version found for ${pkg.name}.`
|
|
1996
|
-
});
|
|
1997
|
-
continue;
|
|
2506
|
+
return {
|
|
2507
|
+
candidates,
|
|
2508
|
+
isMajorBump: false,
|
|
2509
|
+
majorOnlyFixAvailable: false,
|
|
2510
|
+
message: `No safe upgrade version found for "${packageName}". The patch-file path will be needed.`
|
|
2511
|
+
};
|
|
1998
2512
|
}
|
|
1999
|
-
const
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
collectedResults.push(applyResult);
|
|
2513
|
+
const installedMajor = parseInt(installedVersion.split(".")[0] ?? "0", 10);
|
|
2514
|
+
const safeMajor = parseInt(safeVersion.split(".")[0] ?? "0", 10);
|
|
2515
|
+
const isMajorBump = safeMajor > installedMajor;
|
|
2516
|
+
return {
|
|
2517
|
+
safeVersion,
|
|
2518
|
+
upgradeLevel,
|
|
2519
|
+
candidates,
|
|
2520
|
+
isMajorBump,
|
|
2521
|
+
majorOnlyFixAvailable,
|
|
2522
|
+
message: isMajorBump ? `Found safe version ${safeVersion} for "${packageName}", but only a major upgrade is available from ${installedVersion}. This should remain blocked unless policy explicitly allows major bumps.` : `Found ${upgradeLevel ?? "safe"} upgrade ${safeVersion} for "${packageName}" (from ${installedVersion}).`
|
|
2523
|
+
};
|
|
2011
2524
|
}
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
correlation: {
|
|
2023
|
-
requestId: options.requestId,
|
|
2024
|
-
sessionId: options.sessionId,
|
|
2025
|
-
parentRunId: options.parentRunId
|
|
2026
|
-
}
|
|
2525
|
+
});
|
|
2526
|
+
|
|
2527
|
+
// src/remediation/runtime-tools.ts
|
|
2528
|
+
function buildRuntimeTools(ctx) {
|
|
2529
|
+
const tools = {
|
|
2530
|
+
"lookup-cve": lookupCveTool,
|
|
2531
|
+
"check-inventory": checkInventoryTool,
|
|
2532
|
+
"check-version-match": checkVersionMatchTool,
|
|
2533
|
+
"find-fixed-version": findFixedVersionTool,
|
|
2534
|
+
"apply-version-bump": ctx.applyVersionBumpToolForRun
|
|
2027
2535
|
};
|
|
2536
|
+
if (!ctx.constraints.directDependenciesOnly && !ctx.constraints.preferVersionBump) {
|
|
2537
|
+
tools["apply-package-override"] = ctx.applyPackageOverrideToolForRun;
|
|
2538
|
+
}
|
|
2539
|
+
if (!ctx.constraints.preferVersionBump) {
|
|
2540
|
+
tools["fetch-package-source"] = fetchPackageSourceTool;
|
|
2541
|
+
tools["generate-patch"] = generatePatchTool;
|
|
2542
|
+
tools["apply-patch-file"] = ctx.applyPatchFileToolForRun;
|
|
2543
|
+
}
|
|
2544
|
+
return tools;
|
|
2028
2545
|
}
|
|
2546
|
+
|
|
2547
|
+
// src/remediation/orchestration-prompt.ts
|
|
2548
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
|
|
2549
|
+
import { join as join10 } from "path";
|
|
2029
2550
|
function loadOrchestrationPrompt(ctx) {
|
|
2030
|
-
const promptPath =
|
|
2031
|
-
if (!
|
|
2551
|
+
const promptPath = join10(process.cwd(), ".github", "instructions", "orchestration.instructions.md");
|
|
2552
|
+
if (!existsSync5(promptPath)) {
|
|
2032
2553
|
return `You are autoremediator, an agentic security remediation system for Node.js package dependencies.
|
|
2033
2554
|
Working directory: ${ctx.cwd}
|
|
2034
2555
|
Package manager: ${ctx.packageManager}
|
|
@@ -2036,6 +2557,8 @@ Dry run: ${ctx.dryRun}
|
|
|
2036
2557
|
Run tests: ${ctx.runTests}
|
|
2037
2558
|
Policy: ${ctx.policy || "undefined"}
|
|
2038
2559
|
Patches dir: ${ctx.patchesDir}
|
|
2560
|
+
Direct dependencies only: ${String(ctx.constraints.directDependenciesOnly ?? false)}
|
|
2561
|
+
Prefer version bump: ${String(ctx.constraints.preferVersionBump ?? false)}
|
|
2039
2562
|
|
|
2040
2563
|
Required sequence:
|
|
2041
2564
|
1. lookup-cve
|
|
@@ -2043,185 +2566,364 @@ Required sequence:
|
|
|
2043
2566
|
3. check-version-match
|
|
2044
2567
|
4. find-fixed-version
|
|
2045
2568
|
5. apply-version-bump
|
|
2569
|
+
6. apply-package-override
|
|
2046
2570
|
|
|
2047
|
-
Fallback sequence (when
|
|
2571
|
+
Fallback sequence (when neither version bump nor override can be applied):
|
|
2048
2572
|
1. fetch-package-source
|
|
2049
2573
|
2. generate-patch
|
|
2050
2574
|
3. apply-patch-file
|
|
2051
2575
|
|
|
2052
2576
|
Always respect dryRun and policy constraints.`;
|
|
2053
2577
|
}
|
|
2054
|
-
const template =
|
|
2055
|
-
return template.replaceAll("{{cveId}}", ctx.cveId).replaceAll("{{cwd}}", ctx.cwd).replaceAll("{{packageManager}}", ctx.packageManager).replaceAll("{{dryRun}}", String(ctx.dryRun)).replaceAll("{{runTests}}", String(ctx.runTests)).replaceAll("{{policy}}", ctx.policy || "undefined").replaceAll("{{patchesDir}}", ctx.patchesDir);
|
|
2578
|
+
const template = readFileSync6(promptPath, "utf8");
|
|
2579
|
+
return template.replaceAll("{{cveId}}", ctx.cveId).replaceAll("{{cwd}}", ctx.cwd).replaceAll("{{packageManager}}", ctx.packageManager).replaceAll("{{dryRun}}", String(ctx.dryRun)).replaceAll("{{runTests}}", String(ctx.runTests)).replaceAll("{{policy}}", ctx.policy || "undefined").replaceAll("{{patchesDir}}", ctx.patchesDir).replaceAll("{{directDependenciesOnly}}", String(ctx.constraints.directDependenciesOnly ?? false)).replaceAll("{{preferVersionBump}}", String(ctx.constraints.preferVersionBump ?? false));
|
|
2056
2580
|
}
|
|
2057
2581
|
|
|
2058
|
-
// src/
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
import { readFileSync as readFileSync5 } from "fs";
|
|
2064
|
-
var CVE_REGEX = /CVE-\d{4}-\d+/gi;
|
|
2065
|
-
function normalizeSeverity(raw) {
|
|
2066
|
-
if (!raw) return "UNKNOWN";
|
|
2067
|
-
const up = raw.toUpperCase();
|
|
2068
|
-
if (up === "CRITICAL" || up === "HIGH" || up === "MEDIUM" || up === "LOW") {
|
|
2069
|
-
return up;
|
|
2582
|
+
// src/remediation/pipeline.ts
|
|
2583
|
+
async function runRemediationPipeline(cveId, options = {}) {
|
|
2584
|
+
const provider = resolveProvider(options);
|
|
2585
|
+
if (provider === "local") {
|
|
2586
|
+
return runLocalRemediationPipeline(cveId, options);
|
|
2070
2587
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
const
|
|
2075
|
-
const
|
|
2076
|
-
const
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2588
|
+
const cwd = options.cwd ?? process.cwd();
|
|
2589
|
+
const packageManager = options.packageManager ?? detectPackageManager(cwd);
|
|
2590
|
+
const preview = options.preview ?? false;
|
|
2591
|
+
const dryRun = (options.dryRun ?? false) || preview;
|
|
2592
|
+
const runTests = options.runTests ?? false;
|
|
2593
|
+
const policy = options.policy ?? "";
|
|
2594
|
+
const patchesDir = options.patchesDir || "./patches";
|
|
2595
|
+
const constraints = options.constraints ?? {};
|
|
2596
|
+
const model = await createModel(options);
|
|
2597
|
+
const systemPrompt = loadOrchestrationPrompt({
|
|
2598
|
+
cveId,
|
|
2599
|
+
cwd,
|
|
2600
|
+
dryRun,
|
|
2601
|
+
runTests,
|
|
2602
|
+
policy,
|
|
2603
|
+
patchesDir,
|
|
2604
|
+
packageManager,
|
|
2605
|
+
constraints
|
|
2606
|
+
});
|
|
2607
|
+
const prompt = `Patch vulnerable dependencies affected by ${cveId} in the project at: ${cwd}. Package manager: ${packageManager}.`;
|
|
2608
|
+
const collectedResults = [];
|
|
2609
|
+
const vulnerablePackages = [];
|
|
2610
|
+
let cveDetails = null;
|
|
2611
|
+
let agentSteps = 0;
|
|
2612
|
+
const applyVersionBumpToolForRun = preview ? {
|
|
2613
|
+
...applyVersionBumpTool,
|
|
2614
|
+
execute: async (input) => applyVersionBumpTool.execute({ ...input, dryRun: true })
|
|
2615
|
+
} : applyVersionBumpTool;
|
|
2616
|
+
const applyPackageOverrideToolForRun = preview ? {
|
|
2617
|
+
...applyPackageOverrideTool,
|
|
2618
|
+
execute: async (input) => applyPackageOverrideTool.execute({ ...input, dryRun: true })
|
|
2619
|
+
} : applyPackageOverrideTool;
|
|
2620
|
+
const applyPatchFileToolForRun = preview ? {
|
|
2621
|
+
...applyPatchFileTool,
|
|
2622
|
+
execute: async (input) => applyPatchFileTool.execute({ ...input, dryRun: true })
|
|
2623
|
+
} : applyPatchFileTool;
|
|
2624
|
+
const tools = buildRuntimeTools({
|
|
2625
|
+
applyVersionBumpToolForRun,
|
|
2626
|
+
applyPackageOverrideToolForRun,
|
|
2627
|
+
applyPatchFileToolForRun,
|
|
2628
|
+
constraints
|
|
2629
|
+
});
|
|
2630
|
+
const result = await generateText2({
|
|
2631
|
+
model,
|
|
2632
|
+
system: systemPrompt,
|
|
2633
|
+
prompt,
|
|
2634
|
+
tools,
|
|
2635
|
+
maxSteps: 25,
|
|
2636
|
+
onStepFinish(stepResult) {
|
|
2637
|
+
agentSteps += 1;
|
|
2638
|
+
const toolResults = stepResult.toolResults ?? [];
|
|
2639
|
+
for (const tr of toolResults) {
|
|
2640
|
+
const toolResult = tr.result;
|
|
2641
|
+
if (tr.toolName === "lookup-cve" && toolResult?.data) {
|
|
2642
|
+
cveDetails = toolResult.data;
|
|
2643
|
+
}
|
|
2644
|
+
if (tr.toolName === "check-version-match" && toolResult?.vulnerablePackages) {
|
|
2645
|
+
vulnerablePackages.push(...toolResult.vulnerablePackages);
|
|
2646
|
+
}
|
|
2647
|
+
if (tr.toolName === "apply-version-bump") {
|
|
2648
|
+
collectedResults.push(toolResult);
|
|
2649
|
+
}
|
|
2650
|
+
if (tr.toolName === "apply-package-override") {
|
|
2651
|
+
collectedResults.push(toolResult);
|
|
2652
|
+
}
|
|
2653
|
+
if (tr.toolName === "apply-patch-file" && toolResult) {
|
|
2654
|
+
const validation = toolResult.validation;
|
|
2655
|
+
const message = typeof toolResult.message === "string" ? toolResult.message : typeof toolResult.error === "string" ? toolResult.error : "Patch-file strategy finished.";
|
|
2656
|
+
collectedResults.push({
|
|
2657
|
+
packageName: typeof toolResult.packageName === "string" ? toolResult.packageName : "unknown-package",
|
|
2658
|
+
strategy: "patch-file",
|
|
2659
|
+
fromVersion: typeof toolResult.vulnerableVersion === "string" ? toolResult.vulnerableVersion : "unknown",
|
|
2660
|
+
patchFilePath: typeof toolResult.patchFilePath === "string" ? toolResult.patchFilePath : typeof toolResult.patchPath === "string" ? toolResult.patchPath : void 0,
|
|
2661
|
+
applied: Boolean(toolResult.applied),
|
|
2662
|
+
dryRun: Boolean(toolResult.dryRun),
|
|
2663
|
+
unresolvedReason: !Boolean(toolResult.applied) && !Boolean(toolResult.dryRun) ? validation && validation.passed === false ? "patch-validation-failed" : "patch-apply-failed" : void 0,
|
|
2664
|
+
message,
|
|
2665
|
+
validation: validation && typeof validation.passed === "boolean" ? {
|
|
2666
|
+
passed: validation.passed,
|
|
2667
|
+
error: typeof validation.error === "string" ? validation.error : void 0
|
|
2668
|
+
} : void 0
|
|
2669
|
+
});
|
|
2670
|
+
}
|
|
2092
2671
|
}
|
|
2093
2672
|
}
|
|
2094
|
-
}
|
|
2095
|
-
return
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2673
|
+
});
|
|
2674
|
+
return {
|
|
2675
|
+
cveId,
|
|
2676
|
+
cveDetails,
|
|
2677
|
+
vulnerablePackages,
|
|
2678
|
+
results: collectedResults,
|
|
2679
|
+
agentSteps,
|
|
2680
|
+
summary: result.text,
|
|
2681
|
+
correlation: {
|
|
2682
|
+
requestId: options.requestId,
|
|
2683
|
+
sessionId: options.sessionId,
|
|
2684
|
+
parentRunId: options.parentRunId
|
|
2685
|
+
}
|
|
2686
|
+
};
|
|
2100
2687
|
}
|
|
2101
2688
|
|
|
2102
|
-
// src/
|
|
2103
|
-
|
|
2104
|
-
var
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2689
|
+
// src/api/options-schema.ts
|
|
2690
|
+
var PACKAGE_MANAGER_VALUES = ["npm", "pnpm", "yarn"];
|
|
2691
|
+
var LLM_PROVIDER_VALUES = ["openai", "anthropic", "local"];
|
|
2692
|
+
var PROVENANCE_SOURCE_VALUES = ["cli", "sdk", "mcp", "openapi", "unknown"];
|
|
2693
|
+
var OPTION_DESCRIPTIONS = {
|
|
2694
|
+
cveId: "CVE ID, e.g. CVE-2021-23337",
|
|
2695
|
+
inputPath: "Absolute path to the scanner output file",
|
|
2696
|
+
cwd: "Absolute path to the project root (default: process.cwd())",
|
|
2697
|
+
packageManager: "Package manager override (auto-detected by default)",
|
|
2698
|
+
dryRun: "If true, plan changes but write nothing",
|
|
2699
|
+
preview: "If true, enforce non-mutating preview mode",
|
|
2700
|
+
runTests: "Run package-manager test command after applying fix",
|
|
2701
|
+
llmProvider: "LLM provider override",
|
|
2702
|
+
patchesDir: "Directory to write .patch files (default: ./patches)",
|
|
2703
|
+
policy: "Optional path to .autoremediator policy file",
|
|
2704
|
+
requestId: "Request correlation ID",
|
|
2705
|
+
sessionId: "Session correlation ID",
|
|
2706
|
+
parentRunId: "Parent run correlation ID",
|
|
2707
|
+
idempotencyKey: "Idempotency key for replay-safe execution",
|
|
2708
|
+
resume: "Return cached result for matching idempotency key when available",
|
|
2709
|
+
actor: "Actor identity for evidence provenance",
|
|
2710
|
+
source: "Source system for provenance",
|
|
2711
|
+
format: "Scanner format (default: auto)",
|
|
2712
|
+
evidence: "Write evidence JSON to .autoremediator/evidence/ (default: true)",
|
|
2713
|
+
directDependenciesOnly: "Restrict remediation to direct dependencies only",
|
|
2714
|
+
preferVersionBump: "Reject override and patch remediation when version-bump-only policy is required"
|
|
2715
|
+
};
|
|
2716
|
+
function createConstraintSchemaProperties() {
|
|
2717
|
+
return {
|
|
2718
|
+
directDependenciesOnly: { type: "boolean", description: OPTION_DESCRIPTIONS.directDependenciesOnly },
|
|
2719
|
+
preferVersionBump: { type: "boolean", description: OPTION_DESCRIPTIONS.preferVersionBump }
|
|
2720
|
+
};
|
|
2112
2721
|
}
|
|
2113
|
-
function
|
|
2114
|
-
const
|
|
2115
|
-
const
|
|
2116
|
-
const
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
cveId,
|
|
2138
|
-
source: "yarn-audit",
|
|
2139
|
-
packageName,
|
|
2140
|
-
severity
|
|
2141
|
-
});
|
|
2722
|
+
function createRemediateOptionSchemaProperties(options) {
|
|
2723
|
+
const includeDryRun = options?.includeDryRun ?? true;
|
|
2724
|
+
const includePreview = options?.includePreview ?? true;
|
|
2725
|
+
const includeEvidence = options?.includeEvidence ?? true;
|
|
2726
|
+
return {
|
|
2727
|
+
cwd: { type: "string", description: OPTION_DESCRIPTIONS.cwd },
|
|
2728
|
+
packageManager: { type: "string", enum: [...PACKAGE_MANAGER_VALUES], description: OPTION_DESCRIPTIONS.packageManager },
|
|
2729
|
+
...includeDryRun ? { dryRun: { type: "boolean", description: OPTION_DESCRIPTIONS.dryRun } } : {},
|
|
2730
|
+
...includePreview ? { preview: { type: "boolean", description: OPTION_DESCRIPTIONS.preview } } : {},
|
|
2731
|
+
runTests: { type: "boolean", description: OPTION_DESCRIPTIONS.runTests },
|
|
2732
|
+
llmProvider: { type: "string", enum: [...LLM_PROVIDER_VALUES], description: OPTION_DESCRIPTIONS.llmProvider },
|
|
2733
|
+
patchesDir: { type: "string", description: OPTION_DESCRIPTIONS.patchesDir },
|
|
2734
|
+
policy: { type: "string", description: OPTION_DESCRIPTIONS.policy },
|
|
2735
|
+
...includeEvidence ? { evidence: { type: "boolean", description: OPTION_DESCRIPTIONS.evidence } } : {},
|
|
2736
|
+
requestId: { type: "string", description: OPTION_DESCRIPTIONS.requestId },
|
|
2737
|
+
sessionId: { type: "string", description: OPTION_DESCRIPTIONS.sessionId },
|
|
2738
|
+
parentRunId: { type: "string", description: OPTION_DESCRIPTIONS.parentRunId },
|
|
2739
|
+
idempotencyKey: { type: "string", description: OPTION_DESCRIPTIONS.idempotencyKey },
|
|
2740
|
+
resume: { type: "boolean", description: OPTION_DESCRIPTIONS.resume },
|
|
2741
|
+
actor: { type: "string", description: OPTION_DESCRIPTIONS.actor },
|
|
2742
|
+
source: { type: "string", enum: [...PROVENANCE_SOURCE_VALUES], description: OPTION_DESCRIPTIONS.source },
|
|
2743
|
+
constraints: {
|
|
2744
|
+
type: "object",
|
|
2745
|
+
properties: createConstraintSchemaProperties()
|
|
2142
2746
|
}
|
|
2143
|
-
}
|
|
2144
|
-
return findings;
|
|
2747
|
+
};
|
|
2145
2748
|
}
|
|
2146
|
-
function
|
|
2147
|
-
|
|
2148
|
-
|
|
2749
|
+
function createScanOptionSchemaProperties() {
|
|
2750
|
+
return {
|
|
2751
|
+
...createRemediateOptionSchemaProperties({ includeEvidence: true }),
|
|
2752
|
+
format: { type: "string", enum: ["npm-audit", "yarn-audit", "sarif", "auto"], description: OPTION_DESCRIPTIONS.format }
|
|
2753
|
+
};
|
|
2754
|
+
}
|
|
2755
|
+
function createScanReportSchemaProperties() {
|
|
2756
|
+
return {
|
|
2757
|
+
schemaVersion: { type: "string" },
|
|
2758
|
+
status: { type: "string", enum: ["ok", "partial", "failed"] },
|
|
2759
|
+
generatedAt: { type: "string" },
|
|
2760
|
+
cveIds: { type: "array", items: { type: "string" } },
|
|
2761
|
+
reports: { type: "array", items: { type: "object" } },
|
|
2762
|
+
successCount: { type: "number" },
|
|
2763
|
+
failedCount: { type: "number" },
|
|
2764
|
+
errors: { type: "array", items: { type: "object" } },
|
|
2765
|
+
evidenceFile: { type: "string" },
|
|
2766
|
+
patchCount: { type: "number" },
|
|
2767
|
+
patchValidationFailures: { type: "array", items: { type: "object" } },
|
|
2768
|
+
strategyCounts: {
|
|
2769
|
+
type: "object",
|
|
2770
|
+
additionalProperties: { type: "number" }
|
|
2771
|
+
},
|
|
2772
|
+
dependencyScopeCounts: {
|
|
2773
|
+
type: "object",
|
|
2774
|
+
additionalProperties: { type: "number" }
|
|
2775
|
+
},
|
|
2776
|
+
unresolvedByReason: {
|
|
2777
|
+
type: "object",
|
|
2778
|
+
additionalProperties: { type: "number" }
|
|
2779
|
+
},
|
|
2780
|
+
patchesDir: { type: "string" },
|
|
2781
|
+
correlation: { type: "object" },
|
|
2782
|
+
provenance: { type: "object" },
|
|
2783
|
+
constraints: { type: "object" },
|
|
2784
|
+
idempotencyKey: { type: "string" }
|
|
2785
|
+
};
|
|
2149
2786
|
}
|
|
2150
2787
|
|
|
2151
|
-
// src/
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2788
|
+
// src/api/reporting.ts
|
|
2789
|
+
function buildStrategyCounts(reports) {
|
|
2790
|
+
const counts = {};
|
|
2791
|
+
for (const report of reports) {
|
|
2792
|
+
for (const result of report.results) {
|
|
2793
|
+
counts[result.strategy] = (counts[result.strategy] ?? 0) + 1;
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
return Object.keys(counts).length > 0 ? counts : void 0;
|
|
2157
2797
|
}
|
|
2158
|
-
function
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
if (seen.has(key)) continue;
|
|
2171
|
-
seen.add(key);
|
|
2172
|
-
findings.push({
|
|
2173
|
-
cveId,
|
|
2174
|
-
source: "sarif",
|
|
2175
|
-
packageName: pkg,
|
|
2176
|
-
severity: "UNKNOWN"
|
|
2177
|
-
});
|
|
2798
|
+
function toDependencyScope(installedType) {
|
|
2799
|
+
return installedType === "direct" ? "direct" : "transitive";
|
|
2800
|
+
}
|
|
2801
|
+
function buildDependencyScopeCounts(reports) {
|
|
2802
|
+
const counts = {};
|
|
2803
|
+
for (const report of reports) {
|
|
2804
|
+
const packageScopes = /* @__PURE__ */ new Map();
|
|
2805
|
+
for (const vulnerablePackage of report.vulnerablePackages) {
|
|
2806
|
+
const scope = toDependencyScope(vulnerablePackage.installed.type);
|
|
2807
|
+
const current = packageScopes.get(vulnerablePackage.installed.name);
|
|
2808
|
+
if (!current || current !== "direct") {
|
|
2809
|
+
packageScopes.set(vulnerablePackage.installed.name, scope);
|
|
2178
2810
|
}
|
|
2179
2811
|
}
|
|
2812
|
+
for (const result of report.results) {
|
|
2813
|
+
const scope = packageScopes.get(result.packageName);
|
|
2814
|
+
if (!scope) continue;
|
|
2815
|
+
counts[scope] = (counts[scope] ?? 0) + 1;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
return Object.keys(counts).length > 0 ? counts : void 0;
|
|
2819
|
+
}
|
|
2820
|
+
function buildUnresolvedReasonCounts(reports) {
|
|
2821
|
+
const counts = {};
|
|
2822
|
+
for (const report of reports) {
|
|
2823
|
+
for (const result of report.results) {
|
|
2824
|
+
if (!result.unresolvedReason) continue;
|
|
2825
|
+
counts[result.unresolvedReason] = (counts[result.unresolvedReason] ?? 0) + 1;
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
return Object.keys(counts).length > 0 ? counts : void 0;
|
|
2829
|
+
}
|
|
2830
|
+
function toCiSummary(report) {
|
|
2831
|
+
let remediationCount = 0;
|
|
2832
|
+
for (const cveReport of report.reports) {
|
|
2833
|
+
remediationCount += cveReport.results.length;
|
|
2180
2834
|
}
|
|
2181
|
-
return
|
|
2835
|
+
return {
|
|
2836
|
+
schemaVersion: report.schemaVersion,
|
|
2837
|
+
status: report.status,
|
|
2838
|
+
generatedAt: report.generatedAt,
|
|
2839
|
+
cveCount: report.cveIds.length,
|
|
2840
|
+
remediationCount,
|
|
2841
|
+
successCount: report.successCount,
|
|
2842
|
+
failedCount: report.failedCount,
|
|
2843
|
+
errors: report.errors,
|
|
2844
|
+
evidenceFile: report.evidenceFile,
|
|
2845
|
+
patchCount: report.patchCount || 0,
|
|
2846
|
+
patchValidationFailures: report.patchValidationFailures,
|
|
2847
|
+
strategyCounts: report.strategyCounts,
|
|
2848
|
+
dependencyScopeCounts: report.dependencyScopeCounts,
|
|
2849
|
+
unresolvedByReason: report.unresolvedByReason,
|
|
2850
|
+
patchesDir: report.patchesDir,
|
|
2851
|
+
correlation: report.correlation,
|
|
2852
|
+
provenance: report.provenance,
|
|
2853
|
+
constraints: report.constraints,
|
|
2854
|
+
idempotencyKey: report.idempotencyKey
|
|
2855
|
+
};
|
|
2182
2856
|
}
|
|
2183
|
-
function
|
|
2184
|
-
|
|
2185
|
-
return parseSarifFromString(content);
|
|
2857
|
+
function ciExitCode(summary) {
|
|
2858
|
+
return summary.failedCount > 0 ? 1 : 0;
|
|
2186
2859
|
}
|
|
2187
2860
|
|
|
2188
|
-
// src/
|
|
2189
|
-
function
|
|
2190
|
-
|
|
2191
|
-
if (
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
if (resolved === "yarn-audit") {
|
|
2195
|
-
return parseYarnAuditJsonFile(filePath);
|
|
2196
|
-
}
|
|
2197
|
-
if (resolved === "sarif") {
|
|
2198
|
-
return parseSarifFile(filePath);
|
|
2199
|
-
}
|
|
2200
|
-
throw new Error(`Unsupported input format: ${resolved}`);
|
|
2861
|
+
// src/api/sarif.ts
|
|
2862
|
+
function severityToSarifLevel(severity) {
|
|
2863
|
+
if (severity === "CRITICAL" || severity === "HIGH") return "error";
|
|
2864
|
+
if (severity === "MEDIUM") return "warning";
|
|
2865
|
+
if (severity === "LOW") return "note";
|
|
2866
|
+
return "warning";
|
|
2201
2867
|
}
|
|
2202
|
-
function
|
|
2203
|
-
const
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
const
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2868
|
+
function toSarifOutput(report) {
|
|
2869
|
+
const rules = [];
|
|
2870
|
+
const results = [];
|
|
2871
|
+
const seenRules = /* @__PURE__ */ new Set();
|
|
2872
|
+
for (const cveReport of report.reports) {
|
|
2873
|
+
const severity = cveReport.cveDetails?.severity ?? "UNKNOWN";
|
|
2874
|
+
const level = severityToSarifLevel(severity);
|
|
2875
|
+
const summary = cveReport.cveDetails?.summary ?? cveReport.cveId;
|
|
2876
|
+
if (!seenRules.has(cveReport.cveId)) {
|
|
2877
|
+
seenRules.add(cveReport.cveId);
|
|
2878
|
+
rules.push({
|
|
2879
|
+
id: cveReport.cveId,
|
|
2880
|
+
name: "VulnerableDependency",
|
|
2881
|
+
shortDescription: { text: cveReport.cveId },
|
|
2882
|
+
fullDescription: { text: summary },
|
|
2883
|
+
defaultConfiguration: { level },
|
|
2884
|
+
helpUri: `https://osv.dev/vulnerability/${cveReport.cveId}`,
|
|
2885
|
+
properties: { severity }
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
for (const vulnerablePackage of cveReport.vulnerablePackages) {
|
|
2889
|
+
const fixText = vulnerablePackage.affected.firstPatchedVersion ? ` Fix: upgrade to ${vulnerablePackage.affected.firstPatchedVersion}.` : " No fixed version available.";
|
|
2890
|
+
results.push({
|
|
2891
|
+
ruleId: cveReport.cveId,
|
|
2892
|
+
level,
|
|
2893
|
+
message: {
|
|
2894
|
+
text: `${vulnerablePackage.installed.name}@${vulnerablePackage.installed.version} is vulnerable to ${cveReport.cveId}: ${summary}${fixText}`
|
|
2895
|
+
},
|
|
2896
|
+
locations: [
|
|
2897
|
+
{
|
|
2898
|
+
physicalLocation: {
|
|
2899
|
+
artifactLocation: { uri: "package.json", uriBaseId: "%SRCROOT%" }
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
]
|
|
2903
|
+
});
|
|
2213
2904
|
}
|
|
2214
|
-
} catch {
|
|
2215
2905
|
}
|
|
2216
|
-
return
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2906
|
+
return {
|
|
2907
|
+
version: "2.1.0",
|
|
2908
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Documents/CommitteeSpecifications/2.1.0/sarif-schema-2.1.0.json",
|
|
2909
|
+
runs: [
|
|
2910
|
+
{
|
|
2911
|
+
tool: {
|
|
2912
|
+
driver: {
|
|
2913
|
+
name: "autoremediator",
|
|
2914
|
+
informationUri: "https://github.com/Rawlings/autoremediator",
|
|
2915
|
+
rules
|
|
2916
|
+
}
|
|
2917
|
+
},
|
|
2918
|
+
results
|
|
2919
|
+
}
|
|
2920
|
+
]
|
|
2921
|
+
};
|
|
2220
2922
|
}
|
|
2221
2923
|
|
|
2222
2924
|
// src/platform/evidence.ts
|
|
2223
|
-
import { mkdirSync, writeFileSync as
|
|
2224
|
-
import { join as
|
|
2925
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
2926
|
+
import { join as join11 } from "path";
|
|
2225
2927
|
function createEvidenceLog(cwd, cveIds, context = {}) {
|
|
2226
2928
|
return {
|
|
2227
2929
|
runId: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
@@ -2251,31 +2953,31 @@ function finalizeEvidence(log) {
|
|
|
2251
2953
|
return log;
|
|
2252
2954
|
}
|
|
2253
2955
|
function writeEvidenceLog(cwd, log) {
|
|
2254
|
-
const dir =
|
|
2255
|
-
|
|
2256
|
-
const filePath =
|
|
2257
|
-
|
|
2956
|
+
const dir = join11(cwd, ".autoremediator", "evidence");
|
|
2957
|
+
mkdirSync2(dir, { recursive: true });
|
|
2958
|
+
const filePath = join11(dir, `${log.runId}.json`);
|
|
2959
|
+
writeFileSync4(filePath, JSON.stringify(log, null, 2) + "\n", "utf8");
|
|
2258
2960
|
return filePath;
|
|
2259
2961
|
}
|
|
2260
2962
|
|
|
2261
2963
|
// src/platform/idempotency.ts
|
|
2262
|
-
import { existsSync as
|
|
2263
|
-
import { join as
|
|
2964
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
2965
|
+
import { join as join12 } from "path";
|
|
2264
2966
|
var DEFAULT_INDEX = {
|
|
2265
2967
|
schemaVersion: "1.0",
|
|
2266
2968
|
entries: {}
|
|
2267
2969
|
};
|
|
2268
2970
|
function indexFilePath(cwd) {
|
|
2269
|
-
return
|
|
2971
|
+
return join12(cwd, ".autoremediator", "state", "idempotency.json");
|
|
2270
2972
|
}
|
|
2271
2973
|
function entryKey(idempotencyKey, cveId) {
|
|
2272
2974
|
return `${idempotencyKey}::${cveId.toUpperCase()}`;
|
|
2273
2975
|
}
|
|
2274
2976
|
function loadIndex(cwd) {
|
|
2275
2977
|
const filePath = indexFilePath(cwd);
|
|
2276
|
-
if (!
|
|
2978
|
+
if (!existsSync6(filePath)) return DEFAULT_INDEX;
|
|
2277
2979
|
try {
|
|
2278
|
-
const parsed = JSON.parse(
|
|
2980
|
+
const parsed = JSON.parse(readFileSync7(filePath, "utf8"));
|
|
2279
2981
|
if (parsed && parsed.schemaVersion === "1.0" && parsed.entries) {
|
|
2280
2982
|
return parsed;
|
|
2281
2983
|
}
|
|
@@ -2286,8 +2988,8 @@ function loadIndex(cwd) {
|
|
|
2286
2988
|
}
|
|
2287
2989
|
function saveIndex(cwd, index) {
|
|
2288
2990
|
const filePath = indexFilePath(cwd);
|
|
2289
|
-
|
|
2290
|
-
|
|
2991
|
+
mkdirSync3(join12(cwd, ".autoremediator", "state"), { recursive: true });
|
|
2992
|
+
writeFileSync5(filePath, JSON.stringify(index, null, 2) + "\n", "utf8");
|
|
2291
2993
|
}
|
|
2292
2994
|
function readIdempotentReport(cwd, idempotencyKey, cveId) {
|
|
2293
2995
|
const index = loadIndex(cwd);
|
|
@@ -2306,7 +3008,7 @@ function storeIdempotentReport(cwd, idempotencyKey, cveId, report) {
|
|
|
2306
3008
|
saveIndex(cwd, index);
|
|
2307
3009
|
}
|
|
2308
3010
|
|
|
2309
|
-
// src/api.ts
|
|
3011
|
+
// src/api/context.ts
|
|
2310
3012
|
function buildRequestId() {
|
|
2311
3013
|
return `req-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2312
3014
|
}
|
|
@@ -2330,118 +3032,312 @@ function resolveConstraints(options, cwd) {
|
|
|
2330
3032
|
preferVersionBump: options.constraints?.preferVersionBump ?? policy.constraints?.preferVersionBump ?? false
|
|
2331
3033
|
};
|
|
2332
3034
|
}
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
)
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
3035
|
+
|
|
3036
|
+
// src/api/remediate.ts
|
|
3037
|
+
async function remediate(cveId, options = {}) {
|
|
3038
|
+
if (!/^CVE-\d{4}-\d+$/i.test(cveId)) {
|
|
3039
|
+
throw new Error(
|
|
3040
|
+
`Invalid CVE ID: "${cveId}". Expected format: CVE-YYYY-NNNNN (e.g. CVE-2021-23337).`
|
|
3041
|
+
);
|
|
3042
|
+
}
|
|
3043
|
+
const normalizedCveId = cveId.toUpperCase();
|
|
3044
|
+
const cwd = options.cwd ?? process.cwd();
|
|
3045
|
+
const constraints = resolveConstraints(options, cwd);
|
|
3046
|
+
const provenance = resolveProvenanceContext(options);
|
|
3047
|
+
const correlation = resolveCorrelationContext(options);
|
|
3048
|
+
const evidenceEnabled = options.evidence !== false;
|
|
3049
|
+
const evidence = evidenceEnabled ? createEvidenceLog(cwd, [normalizedCveId], {
|
|
3050
|
+
...correlation,
|
|
3051
|
+
actor: provenance.actor,
|
|
3052
|
+
source: provenance.source,
|
|
3053
|
+
idempotencyKey: options.idempotencyKey
|
|
3054
|
+
}) : void 0;
|
|
3055
|
+
if (options.resume && options.idempotencyKey) {
|
|
3056
|
+
const cached = readIdempotentReport(cwd, options.idempotencyKey, normalizedCveId);
|
|
3057
|
+
if (cached) {
|
|
3058
|
+
if (evidence) {
|
|
3059
|
+
addEvidenceStep(evidence, "remediate.resume-cache", { cveId: normalizedCveId });
|
|
3060
|
+
finalizeEvidence(evidence);
|
|
3061
|
+
}
|
|
3062
|
+
const evidenceFile2 = evidence ? writeEvidenceLog(cwd, evidence) : void 0;
|
|
3063
|
+
return {
|
|
3064
|
+
...cached,
|
|
3065
|
+
summary: `${cached.summary} (resumed from idempotency cache)`,
|
|
3066
|
+
evidenceFile: evidenceFile2,
|
|
3067
|
+
correlation,
|
|
3068
|
+
provenance,
|
|
3069
|
+
constraints,
|
|
3070
|
+
resumedFromCache: true
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
if (evidence) {
|
|
3075
|
+
addEvidenceStep(
|
|
3076
|
+
evidence,
|
|
3077
|
+
"remediate.start",
|
|
3078
|
+
{
|
|
3079
|
+
cveId: normalizedCveId,
|
|
3080
|
+
dryRun: Boolean(options.dryRun),
|
|
3081
|
+
preview: Boolean(options.preview)
|
|
3082
|
+
},
|
|
3083
|
+
{
|
|
3084
|
+
directDependenciesOnly: Boolean(constraints.directDependenciesOnly),
|
|
3085
|
+
preferVersionBump: Boolean(constraints.preferVersionBump)
|
|
3086
|
+
}
|
|
3087
|
+
);
|
|
3088
|
+
}
|
|
3089
|
+
let report;
|
|
3090
|
+
try {
|
|
3091
|
+
report = await runRemediationPipeline(normalizedCveId, {
|
|
3092
|
+
...options,
|
|
3093
|
+
...correlation,
|
|
3094
|
+
constraints
|
|
3095
|
+
});
|
|
3096
|
+
} catch (error) {
|
|
3097
|
+
if (evidence) {
|
|
3098
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3099
|
+
addEvidenceStep(evidence, "remediate.error", { cveId: normalizedCveId }, void 0, message);
|
|
3100
|
+
finalizeEvidence(evidence);
|
|
3101
|
+
writeEvidenceLog(cwd, evidence);
|
|
3102
|
+
}
|
|
3103
|
+
throw error;
|
|
3104
|
+
}
|
|
3105
|
+
if (evidence) {
|
|
3106
|
+
for (const result of report.results) {
|
|
3107
|
+
addEvidenceStep(
|
|
3108
|
+
evidence,
|
|
3109
|
+
"remediate.package-result",
|
|
3110
|
+
{
|
|
3111
|
+
packageName: result.packageName,
|
|
3112
|
+
strategy: result.strategy,
|
|
3113
|
+
fromVersion: result.fromVersion,
|
|
3114
|
+
toVersion: result.toVersion
|
|
3115
|
+
},
|
|
3116
|
+
{
|
|
3117
|
+
applied: result.applied,
|
|
3118
|
+
dryRun: result.dryRun,
|
|
3119
|
+
unresolvedReason: result.unresolvedReason
|
|
3120
|
+
}
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3123
|
+
addEvidenceStep(
|
|
3124
|
+
evidence,
|
|
3125
|
+
"remediate.finish",
|
|
3126
|
+
{ cveId: normalizedCveId },
|
|
3127
|
+
{
|
|
3128
|
+
resultCount: report.results.length,
|
|
3129
|
+
vulnerableCount: report.vulnerablePackages.length
|
|
3130
|
+
}
|
|
3131
|
+
);
|
|
3132
|
+
finalizeEvidence(evidence);
|
|
3133
|
+
}
|
|
3134
|
+
const evidenceFile = evidence ? writeEvidenceLog(cwd, evidence) : void 0;
|
|
3135
|
+
const finalReport = {
|
|
3136
|
+
...report,
|
|
3137
|
+
evidenceFile,
|
|
3138
|
+
correlation,
|
|
3139
|
+
provenance,
|
|
3140
|
+
constraints,
|
|
3141
|
+
resumedFromCache: false
|
|
3142
|
+
};
|
|
3143
|
+
if (options.idempotencyKey && !options.dryRun && !options.preview) {
|
|
3144
|
+
storeIdempotentReport(cwd, options.idempotencyKey, normalizedCveId, finalReport);
|
|
3145
|
+
}
|
|
3146
|
+
return finalReport;
|
|
3147
|
+
}
|
|
3148
|
+
async function planRemediation(cveId, options = {}) {
|
|
3149
|
+
return remediate(cveId, {
|
|
3150
|
+
...options,
|
|
3151
|
+
preview: true,
|
|
3152
|
+
dryRun: true
|
|
3153
|
+
});
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
// src/scanner/parse-input.ts
|
|
3157
|
+
import { extname } from "path";
|
|
3158
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
3159
|
+
|
|
3160
|
+
// src/scanner/adapters/npm-audit.ts
|
|
3161
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
3162
|
+
var CVE_REGEX = /CVE-\d{4}-\d+/gi;
|
|
3163
|
+
function normalizeSeverity(raw) {
|
|
3164
|
+
if (!raw) return "UNKNOWN";
|
|
3165
|
+
const up = raw.toUpperCase();
|
|
3166
|
+
if (up === "CRITICAL" || up === "HIGH" || up === "MEDIUM" || up === "LOW") {
|
|
3167
|
+
return up;
|
|
3168
|
+
}
|
|
3169
|
+
return "UNKNOWN";
|
|
3170
|
+
}
|
|
3171
|
+
function parseNpmAuditJsonFromString(content) {
|
|
3172
|
+
const report = JSON.parse(content);
|
|
3173
|
+
const findings = [];
|
|
3174
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3175
|
+
for (const vuln of Object.values(report.vulnerabilities ?? {})) {
|
|
3176
|
+
for (const viaEntry of vuln.via ?? []) {
|
|
3177
|
+
const text = typeof viaEntry === "string" ? viaEntry : `${viaEntry.url ?? ""} ${viaEntry.name ?? ""}`;
|
|
3178
|
+
const matches = text.match(CVE_REGEX) ?? [];
|
|
3179
|
+
for (const match of matches) {
|
|
3180
|
+
const cveId = match.toUpperCase();
|
|
3181
|
+
const key = `${cveId}:${vuln.name}`;
|
|
3182
|
+
if (seen.has(key)) continue;
|
|
3183
|
+
seen.add(key);
|
|
3184
|
+
findings.push({
|
|
3185
|
+
cveId,
|
|
3186
|
+
source: "npm-audit",
|
|
3187
|
+
packageName: vuln.name,
|
|
3188
|
+
severity: normalizeSeverity(vuln.severity)
|
|
3189
|
+
});
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
return findings;
|
|
3194
|
+
}
|
|
3195
|
+
function parseNpmAuditJsonFile(filePath) {
|
|
3196
|
+
const content = readFileSync8(filePath, "utf8");
|
|
3197
|
+
return parseNpmAuditJsonFromString(content);
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
// src/scanner/adapters/yarn-audit.ts
|
|
3201
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
3202
|
+
var CVE_REGEX2 = /CVE-\d{4}-\d+/gi;
|
|
3203
|
+
function normalizeSeverity2(raw) {
|
|
3204
|
+
if (!raw) return "UNKNOWN";
|
|
3205
|
+
const up = raw.toUpperCase();
|
|
3206
|
+
if (up === "CRITICAL" || up === "HIGH" || up === "MEDIUM" || up === "LOW") {
|
|
3207
|
+
return up;
|
|
3208
|
+
}
|
|
3209
|
+
return "UNKNOWN";
|
|
3210
|
+
}
|
|
3211
|
+
function parseYarnAuditJsonFromString(content) {
|
|
3212
|
+
const findings = [];
|
|
3213
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3214
|
+
const lines = content.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
3215
|
+
for (const line of lines) {
|
|
3216
|
+
let parsed;
|
|
3217
|
+
try {
|
|
3218
|
+
parsed = JSON.parse(line);
|
|
3219
|
+
} catch {
|
|
3220
|
+
continue;
|
|
3221
|
+
}
|
|
3222
|
+
const event = parsed;
|
|
3223
|
+
if (event.type !== "auditAdvisory") continue;
|
|
3224
|
+
const advisory = event.data?.advisory;
|
|
3225
|
+
const packageName = advisory?.module_name;
|
|
3226
|
+
const severity = normalizeSeverity2(advisory?.severity);
|
|
3227
|
+
const text = `${advisory?.url ?? ""} ${(advisory?.cves ?? []).join(" ")}`;
|
|
3228
|
+
const matches = text.match(CVE_REGEX2) ?? [];
|
|
3229
|
+
for (const match of matches) {
|
|
3230
|
+
const cveId = match.toUpperCase();
|
|
3231
|
+
const key = `${cveId}:${packageName ?? ""}`;
|
|
3232
|
+
if (seen.has(key)) continue;
|
|
3233
|
+
seen.add(key);
|
|
3234
|
+
findings.push({
|
|
3235
|
+
cveId,
|
|
3236
|
+
source: "yarn-audit",
|
|
3237
|
+
packageName,
|
|
3238
|
+
severity
|
|
3239
|
+
});
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
return findings;
|
|
3243
|
+
}
|
|
3244
|
+
function parseYarnAuditJsonFile(filePath) {
|
|
3245
|
+
const content = readFileSync9(filePath, "utf8");
|
|
3246
|
+
return parseYarnAuditJsonFromString(content);
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
// src/scanner/adapters/sarif.ts
|
|
3250
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
3251
|
+
var CVE_REGEX3 = /CVE-\d{4}-\d+/gi;
|
|
3252
|
+
function extractPackageName(result) {
|
|
3253
|
+
const pkg = result.properties?.["packageName"];
|
|
3254
|
+
return typeof pkg === "string" ? pkg : void 0;
|
|
3255
|
+
}
|
|
3256
|
+
function parseSarifFromString(content) {
|
|
3257
|
+
const report = JSON.parse(content);
|
|
3258
|
+
const findings = [];
|
|
3259
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3260
|
+
for (const run of report.runs ?? []) {
|
|
3261
|
+
for (const result of run.results ?? []) {
|
|
3262
|
+
const combined = `${result.ruleId ?? ""} ${result.message?.text ?? ""}`;
|
|
3263
|
+
const matches = combined.match(CVE_REGEX3) ?? [];
|
|
3264
|
+
for (const match of matches) {
|
|
3265
|
+
const cveId = match.toUpperCase();
|
|
3266
|
+
const pkg = extractPackageName(result);
|
|
3267
|
+
const key = `${cveId}:${pkg ?? ""}`;
|
|
3268
|
+
if (seen.has(key)) continue;
|
|
3269
|
+
seen.add(key);
|
|
3270
|
+
findings.push({
|
|
3271
|
+
cveId,
|
|
3272
|
+
source: "sarif",
|
|
3273
|
+
packageName: pkg,
|
|
3274
|
+
severity: "UNKNOWN"
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
2353
3277
|
}
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
return {
|
|
2357
|
-
...report,
|
|
2358
|
-
results: nextResults,
|
|
2359
|
-
constraints
|
|
2360
|
-
};
|
|
3278
|
+
}
|
|
3279
|
+
return findings;
|
|
2361
3280
|
}
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
3281
|
+
function parseSarifFile(filePath) {
|
|
3282
|
+
const content = readFileSync10(filePath, "utf8");
|
|
3283
|
+
return parseSarifFromString(content);
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
// src/scanner/parse-input.ts
|
|
3287
|
+
function parseScanInput(filePath, format) {
|
|
3288
|
+
const resolved = format === "auto" ? inferFormat(filePath) : format;
|
|
3289
|
+
if (resolved === "npm-audit") {
|
|
3290
|
+
return parseNpmAuditJsonFile(filePath);
|
|
2367
3291
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
const provenance = resolveProvenanceContext(options);
|
|
2371
|
-
const correlation = resolveCorrelationContext(options);
|
|
2372
|
-
if (options.resume && options.idempotencyKey) {
|
|
2373
|
-
const cached = readIdempotentReport(cwd, options.idempotencyKey, cveId.toUpperCase());
|
|
2374
|
-
if (cached) {
|
|
2375
|
-
return {
|
|
2376
|
-
...cached,
|
|
2377
|
-
summary: `${cached.summary} (resumed from idempotency cache)`,
|
|
2378
|
-
correlation,
|
|
2379
|
-
provenance,
|
|
2380
|
-
constraints,
|
|
2381
|
-
resumedFromCache: true
|
|
2382
|
-
};
|
|
2383
|
-
}
|
|
3292
|
+
if (resolved === "yarn-audit") {
|
|
3293
|
+
return parseYarnAuditJsonFile(filePath);
|
|
2384
3294
|
}
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
...correlation,
|
|
2388
|
-
constraints
|
|
2389
|
-
});
|
|
2390
|
-
const constrainedReport = enforceConstraints(report, constraints);
|
|
2391
|
-
const finalReport = {
|
|
2392
|
-
...constrainedReport,
|
|
2393
|
-
correlation,
|
|
2394
|
-
provenance,
|
|
2395
|
-
constraints,
|
|
2396
|
-
resumedFromCache: false
|
|
2397
|
-
};
|
|
2398
|
-
if (options.idempotencyKey && !options.dryRun && !options.preview) {
|
|
2399
|
-
storeIdempotentReport(cwd, options.idempotencyKey, cveId.toUpperCase(), finalReport);
|
|
3295
|
+
if (resolved === "sarif") {
|
|
3296
|
+
return parseSarifFile(filePath);
|
|
2400
3297
|
}
|
|
2401
|
-
|
|
2402
|
-
...finalReport
|
|
2403
|
-
};
|
|
3298
|
+
throw new Error(`Unsupported input format: ${resolved}`);
|
|
2404
3299
|
}
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
3300
|
+
function inferFormat(filePath) {
|
|
3301
|
+
const ext = extname(filePath).toLowerCase();
|
|
3302
|
+
if (ext === ".sarif") return "sarif";
|
|
3303
|
+
try {
|
|
3304
|
+
const content = readFileSync11(filePath, "utf8");
|
|
3305
|
+
const firstLine = content.split("\n").find((line) => line.trim().startsWith("{"));
|
|
3306
|
+
if (firstLine) {
|
|
3307
|
+
const parsed = JSON.parse(firstLine);
|
|
3308
|
+
if (parsed.type === "auditAdvisory" || parsed.type === "auditSummary") {
|
|
3309
|
+
return "yarn-audit";
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
} catch {
|
|
3313
|
+
}
|
|
3314
|
+
return "npm-audit";
|
|
2411
3315
|
}
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
const provenance = resolveProvenanceContext(options);
|
|
2421
|
-
const constraints = resolveConstraints(options, cwd);
|
|
2422
|
-
const evidence = createEvidenceLog(cwd, cveIds, {
|
|
2423
|
-
...correlation,
|
|
2424
|
-
actor: provenance.actor,
|
|
2425
|
-
source: provenance.source,
|
|
2426
|
-
idempotencyKey: options.idempotencyKey
|
|
2427
|
-
});
|
|
2428
|
-
addEvidenceStep(evidence, "scan.parse", { inputPath, format }, { findingCount: findings.length, cveCount: cveIds.length });
|
|
3316
|
+
|
|
3317
|
+
// src/scanner/unique-cve-ids.ts
|
|
3318
|
+
function uniqueCveIds(findings) {
|
|
3319
|
+
return [...new Set(findings.map((f) => f.cveId.toUpperCase()))];
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
// src/api/scan-execution.ts
|
|
3323
|
+
async function executeScanRemediations(params) {
|
|
2429
3324
|
const reports = [];
|
|
2430
3325
|
const errors = [];
|
|
2431
3326
|
const patchValidationFailures = [];
|
|
2432
3327
|
let patchCount = 0;
|
|
2433
|
-
for (const cveId of cveIds) {
|
|
3328
|
+
for (const cveId of params.cveIds) {
|
|
2434
3329
|
try {
|
|
2435
|
-
addEvidenceStep(evidence, "remediate.start", { cveId });
|
|
3330
|
+
addEvidenceStep(params.evidence, "remediate.start", { cveId });
|
|
2436
3331
|
const report = await remediate(cveId, {
|
|
2437
|
-
...options,
|
|
2438
|
-
patchesDir,
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
3332
|
+
...params.options,
|
|
3333
|
+
patchesDir: params.patchesDir,
|
|
3334
|
+
evidence: false,
|
|
3335
|
+
...params.correlation,
|
|
3336
|
+
actor: params.provenance.actor,
|
|
3337
|
+
source: params.provenance.source,
|
|
3338
|
+
constraints: params.constraints
|
|
2443
3339
|
});
|
|
2444
|
-
report.results = report.results.filter((
|
|
3340
|
+
report.results = report.results.filter((result) => isPackageAllowed(params.policy, result.packageName));
|
|
2445
3341
|
for (const result of report.results) {
|
|
2446
3342
|
if (result.strategy === "patch-file") {
|
|
2447
3343
|
patchCount += 1;
|
|
@@ -2455,16 +3351,26 @@ async function remediateFromScan(inputPath, options = {}) {
|
|
|
2455
3351
|
}
|
|
2456
3352
|
}
|
|
2457
3353
|
reports.push(report);
|
|
2458
|
-
addEvidenceStep(evidence, "remediate.finish", { cveId }, { results: report.results.length });
|
|
3354
|
+
addEvidenceStep(params.evidence, "remediate.finish", { cveId }, { results: report.results.length });
|
|
2459
3355
|
} catch (error) {
|
|
2460
3356
|
const message = error instanceof Error ? error.message : String(error);
|
|
2461
3357
|
errors.push({ cveId, message });
|
|
2462
|
-
addEvidenceStep(evidence, "remediate.error", { cveId }, void 0, message);
|
|
3358
|
+
addEvidenceStep(params.evidence, "remediate.error", { cveId }, void 0, message);
|
|
2463
3359
|
}
|
|
2464
3360
|
}
|
|
3361
|
+
return {
|
|
3362
|
+
reports,
|
|
3363
|
+
errors,
|
|
3364
|
+
patchCount,
|
|
3365
|
+
patchValidationFailures
|
|
3366
|
+
};
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
// src/api/scan-outcome.ts
|
|
3370
|
+
function buildScanOutcome(params) {
|
|
2465
3371
|
let successCount = 0;
|
|
2466
3372
|
let failedCount = 0;
|
|
2467
|
-
for (const report of reports) {
|
|
3373
|
+
for (const report of params.reports) {
|
|
2468
3374
|
for (const result of report.results) {
|
|
2469
3375
|
if (result.applied || result.dryRun) {
|
|
2470
3376
|
successCount += 1;
|
|
@@ -2473,13 +3379,75 @@ async function remediateFromScan(inputPath, options = {}) {
|
|
|
2473
3379
|
}
|
|
2474
3380
|
}
|
|
2475
3381
|
}
|
|
2476
|
-
failedCount += errors.length;
|
|
3382
|
+
failedCount += params.errors.length;
|
|
2477
3383
|
let status = "ok";
|
|
2478
3384
|
if (failedCount > 0 && successCount > 0) {
|
|
2479
3385
|
status = "partial";
|
|
2480
3386
|
} else if (failedCount > 0 && successCount === 0) {
|
|
2481
3387
|
status = "failed";
|
|
2482
3388
|
}
|
|
3389
|
+
const strategyCounts = buildStrategyCounts(params.reports);
|
|
3390
|
+
const dependencyScopeCounts = buildDependencyScopeCounts(params.reports);
|
|
3391
|
+
const unresolvedByReason = buildUnresolvedReasonCounts(params.reports);
|
|
3392
|
+
const remediationCount = params.reports.reduce((sum, report) => sum + report.results.length, 0);
|
|
3393
|
+
return {
|
|
3394
|
+
status,
|
|
3395
|
+
successCount,
|
|
3396
|
+
failedCount,
|
|
3397
|
+
strategyCounts,
|
|
3398
|
+
dependencyScopeCounts,
|
|
3399
|
+
unresolvedByReason,
|
|
3400
|
+
remediationCount
|
|
3401
|
+
};
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
// src/api/remediate-from-scan.ts
|
|
3405
|
+
async function remediateFromScan(inputPath, options = {}) {
|
|
3406
|
+
const cwd = options.cwd ?? process.cwd();
|
|
3407
|
+
const format = options.format ?? "auto";
|
|
3408
|
+
const patchesDir = options.patchesDir ?? "./patches";
|
|
3409
|
+
const findings = parseScanInput(inputPath, format);
|
|
3410
|
+
const cveIds = uniqueCveIds(findings);
|
|
3411
|
+
const policy = loadPolicy(cwd, options.policy);
|
|
3412
|
+
const correlation = resolveCorrelationContext(options);
|
|
3413
|
+
const provenance = resolveProvenanceContext(options);
|
|
3414
|
+
const constraints = resolveConstraints(options, cwd);
|
|
3415
|
+
const evidence = createEvidenceLog(cwd, cveIds, {
|
|
3416
|
+
...correlation,
|
|
3417
|
+
actor: provenance.actor,
|
|
3418
|
+
source: provenance.source,
|
|
3419
|
+
idempotencyKey: options.idempotencyKey
|
|
3420
|
+
});
|
|
3421
|
+
addEvidenceStep(evidence, "scan.parse", { inputPath, format }, { findingCount: findings.length, cveCount: cveIds.length });
|
|
3422
|
+
const execution = await executeScanRemediations({
|
|
3423
|
+
cveIds,
|
|
3424
|
+
options,
|
|
3425
|
+
patchesDir,
|
|
3426
|
+
policy,
|
|
3427
|
+
correlation,
|
|
3428
|
+
provenance,
|
|
3429
|
+
constraints,
|
|
3430
|
+
evidence
|
|
3431
|
+
});
|
|
3432
|
+
const reports = execution.reports;
|
|
3433
|
+
const errors = execution.errors;
|
|
3434
|
+
const patchCount = execution.patchCount;
|
|
3435
|
+
const patchValidationFailures = execution.patchValidationFailures;
|
|
3436
|
+
const outcome = buildScanOutcome({ reports, errors });
|
|
3437
|
+
const { status, successCount, failedCount, strategyCounts, dependencyScopeCounts, unresolvedByReason, remediationCount } = outcome;
|
|
3438
|
+
evidence.summary = {
|
|
3439
|
+
status,
|
|
3440
|
+
cveCount: cveIds.length,
|
|
3441
|
+
remediationCount,
|
|
3442
|
+
successCount,
|
|
3443
|
+
failedCount,
|
|
3444
|
+
patchCount,
|
|
3445
|
+
patchValidationFailures: patchValidationFailures.length > 0 ? patchValidationFailures : void 0,
|
|
3446
|
+
strategyCounts,
|
|
3447
|
+
dependencyScopeCounts,
|
|
3448
|
+
unresolvedByReason,
|
|
3449
|
+
patchesDir: patchCount > 0 ? patchesDir : void 0
|
|
3450
|
+
};
|
|
2483
3451
|
finalizeEvidence(evidence);
|
|
2484
3452
|
const evidenceFile = options.evidence === false ? void 0 : writeEvidenceLog(cwd, evidence);
|
|
2485
3453
|
return {
|
|
@@ -2494,6 +3462,9 @@ async function remediateFromScan(inputPath, options = {}) {
|
|
|
2494
3462
|
evidenceFile,
|
|
2495
3463
|
patchCount,
|
|
2496
3464
|
patchValidationFailures: patchValidationFailures.length > 0 ? patchValidationFailures : void 0,
|
|
3465
|
+
strategyCounts,
|
|
3466
|
+
dependencyScopeCounts,
|
|
3467
|
+
unresolvedByReason,
|
|
2497
3468
|
patchesDir: patchCount > 0 ? patchesDir : void 0,
|
|
2498
3469
|
correlation,
|
|
2499
3470
|
provenance,
|
|
@@ -2501,102 +3472,22 @@ async function remediateFromScan(inputPath, options = {}) {
|
|
|
2501
3472
|
idempotencyKey: options.idempotencyKey
|
|
2502
3473
|
};
|
|
2503
3474
|
}
|
|
2504
|
-
function toCiSummary(report) {
|
|
2505
|
-
let remediationCount = 0;
|
|
2506
|
-
for (const cveReport of report.reports) {
|
|
2507
|
-
remediationCount += cveReport.results.length;
|
|
2508
|
-
}
|
|
2509
|
-
return {
|
|
2510
|
-
schemaVersion: report.schemaVersion,
|
|
2511
|
-
status: report.status,
|
|
2512
|
-
generatedAt: report.generatedAt,
|
|
2513
|
-
cveCount: report.cveIds.length,
|
|
2514
|
-
remediationCount,
|
|
2515
|
-
successCount: report.successCount,
|
|
2516
|
-
failedCount: report.failedCount,
|
|
2517
|
-
errors: report.errors,
|
|
2518
|
-
evidenceFile: report.evidenceFile,
|
|
2519
|
-
patchCount: report.patchCount || 0,
|
|
2520
|
-
patchValidationFailures: report.patchValidationFailures,
|
|
2521
|
-
patchesDir: report.patchesDir,
|
|
2522
|
-
correlation: report.correlation,
|
|
2523
|
-
provenance: report.provenance,
|
|
2524
|
-
constraints: report.constraints,
|
|
2525
|
-
idempotencyKey: report.idempotencyKey
|
|
2526
|
-
};
|
|
2527
|
-
}
|
|
2528
|
-
function ciExitCode(summary) {
|
|
2529
|
-
return summary.failedCount > 0 ? 1 : 0;
|
|
2530
|
-
}
|
|
2531
|
-
function severityToSarifLevel(severity) {
|
|
2532
|
-
if (severity === "CRITICAL" || severity === "HIGH") return "error";
|
|
2533
|
-
if (severity === "MEDIUM") return "warning";
|
|
2534
|
-
if (severity === "LOW") return "note";
|
|
2535
|
-
return "warning";
|
|
2536
|
-
}
|
|
2537
|
-
function toSarifOutput(report) {
|
|
2538
|
-
const rules = [];
|
|
2539
|
-
const results = [];
|
|
2540
|
-
const seenRules = /* @__PURE__ */ new Set();
|
|
2541
|
-
for (const r of report.reports) {
|
|
2542
|
-
const severity = r.cveDetails?.severity ?? "UNKNOWN";
|
|
2543
|
-
const level = severityToSarifLevel(severity);
|
|
2544
|
-
const summary = r.cveDetails?.summary ?? r.cveId;
|
|
2545
|
-
if (!seenRules.has(r.cveId)) {
|
|
2546
|
-
seenRules.add(r.cveId);
|
|
2547
|
-
rules.push({
|
|
2548
|
-
id: r.cveId,
|
|
2549
|
-
name: "VulnerableDependency",
|
|
2550
|
-
shortDescription: { text: r.cveId },
|
|
2551
|
-
fullDescription: { text: summary },
|
|
2552
|
-
defaultConfiguration: { level },
|
|
2553
|
-
helpUri: `https://osv.dev/vulnerability/${r.cveId}`,
|
|
2554
|
-
properties: { severity }
|
|
2555
|
-
});
|
|
2556
|
-
}
|
|
2557
|
-
for (const vp of r.vulnerablePackages) {
|
|
2558
|
-
const fixText = vp.affected.firstPatchedVersion ? ` Fix: upgrade to ${vp.affected.firstPatchedVersion}.` : " No fixed version available.";
|
|
2559
|
-
results.push({
|
|
2560
|
-
ruleId: r.cveId,
|
|
2561
|
-
level,
|
|
2562
|
-
message: {
|
|
2563
|
-
text: `${vp.installed.name}@${vp.installed.version} is vulnerable to ${r.cveId}: ${summary}${fixText}`
|
|
2564
|
-
},
|
|
2565
|
-
locations: [
|
|
2566
|
-
{
|
|
2567
|
-
physicalLocation: {
|
|
2568
|
-
artifactLocation: { uri: "package.json", uriBaseId: "%SRCROOT%" }
|
|
2569
|
-
}
|
|
2570
|
-
}
|
|
2571
|
-
]
|
|
2572
|
-
});
|
|
2573
|
-
}
|
|
2574
|
-
}
|
|
2575
|
-
return {
|
|
2576
|
-
version: "2.1.0",
|
|
2577
|
-
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Documents/CommitteeSpecifications/2.1.0/sarif-schema-2.1.0.json",
|
|
2578
|
-
runs: [
|
|
2579
|
-
{
|
|
2580
|
-
tool: {
|
|
2581
|
-
driver: {
|
|
2582
|
-
name: "autoremediator",
|
|
2583
|
-
informationUri: "https://github.com/Rawlings/autoremediator",
|
|
2584
|
-
rules
|
|
2585
|
-
}
|
|
2586
|
-
},
|
|
2587
|
-
results
|
|
2588
|
-
}
|
|
2589
|
-
]
|
|
2590
|
-
};
|
|
2591
|
-
}
|
|
2592
3475
|
|
|
2593
3476
|
export {
|
|
2594
3477
|
runRemediationPipeline,
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
3478
|
+
PACKAGE_MANAGER_VALUES,
|
|
3479
|
+
LLM_PROVIDER_VALUES,
|
|
3480
|
+
PROVENANCE_SOURCE_VALUES,
|
|
3481
|
+
OPTION_DESCRIPTIONS,
|
|
3482
|
+
createConstraintSchemaProperties,
|
|
3483
|
+
createRemediateOptionSchemaProperties,
|
|
3484
|
+
createScanOptionSchemaProperties,
|
|
3485
|
+
createScanReportSchemaProperties,
|
|
2598
3486
|
toCiSummary,
|
|
2599
3487
|
ciExitCode,
|
|
2600
|
-
toSarifOutput
|
|
3488
|
+
toSarifOutput,
|
|
3489
|
+
remediate,
|
|
3490
|
+
planRemediation,
|
|
3491
|
+
remediateFromScan
|
|
2601
3492
|
};
|
|
2602
|
-
//# sourceMappingURL=chunk-
|
|
3493
|
+
//# sourceMappingURL=chunk-MUFP2DQX.js.map
|