autoremediator 0.17.0 → 0.19.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 +20 -27
- package/dist/api/change-request/index.d.ts +2 -1
- package/dist/api/change-request/index.d.ts.map +1 -1
- package/dist/api/context.d.ts.map +1 -1
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/options-schema.d.ts +3 -0
- package/dist/api/options-schema.d.ts.map +1 -1
- package/dist/api/patches/index.d.ts +1 -0
- package/dist/api/patches/index.d.ts.map +1 -1
- package/dist/api/patches/reconcile.d.ts +7 -0
- package/dist/api/patches/reconcile.d.ts.map +1 -0
- package/dist/api/patches/reconcile.test.d.ts +2 -0
- package/dist/api/patches/reconcile.test.d.ts.map +1 -0
- package/dist/api/remediate/validators.d.ts +3 -0
- package/dist/api/remediate/validators.d.ts.map +1 -1
- package/dist/{chunk-U4LIJIGD.js → chunk-7BORIBPY.js} +2476 -885
- package/dist/cli/program.d.ts.map +1 -1
- package/dist/cli/runners.d.ts +5 -0
- package/dist/cli/runners.d.ts.map +1 -1
- package/dist/cli/triage.d.ts +7 -0
- package/dist/cli/triage.d.ts.map +1 -0
- package/dist/cli/types.d.ts +5 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli.js +233 -6
- package/dist/index.js +2479 -889
- package/dist/index.js.map +1 -1
- package/dist/intelligence/sources/github-advisory.d.ts.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/openapi/server.js +1 -1
- package/dist/platform/evidence.d.ts +2 -0
- package/dist/platform/evidence.d.ts.map +1 -1
- package/dist/platform/package-manager/index.d.ts +1 -0
- package/dist/platform/package-manager/index.d.ts.map +1 -1
- package/dist/platform/supply-chain.d.ts +10 -0
- package/dist/platform/supply-chain.d.ts.map +1 -0
- package/dist/platform/supply-chain.test.d.ts +2 -0
- package/dist/platform/supply-chain.test.d.ts.map +1 -0
- package/dist/platform/types.d.ts +69 -6
- package/dist/platform/types.d.ts.map +1 -1
- package/dist/platform/workspace.d.ts +28 -0
- package/dist/platform/workspace.d.ts.map +1 -0
- package/dist/platform/workspace.test.d.ts +2 -0
- package/dist/platform/workspace.test.d.ts.map +1 -0
- package/dist/remediation/local/fallback.d.ts +2 -0
- package/dist/remediation/local/fallback.d.ts.map +1 -1
- package/dist/remediation/local/options.d.ts +3 -0
- package/dist/remediation/local/options.d.ts.map +1 -1
- package/dist/remediation/local/primary-strategy.d.ts +2 -9
- package/dist/remediation/local/primary-strategy.d.ts.map +1 -1
- package/dist/remediation/local/run.d.ts.map +1 -1
- package/dist/remediation/pipeline-runtime/tool-wrappers.d.ts.map +1 -1
- package/dist/remediation/proof-of-fix/runner.d.ts +16 -0
- package/dist/remediation/proof-of-fix/runner.d.ts.map +1 -0
- package/dist/remediation/proof-of-fix/runner.test.d.ts +2 -0
- package/dist/remediation/proof-of-fix/runner.test.d.ts.map +1 -0
- package/dist/remediation/refactor/call-site-finder.d.ts +15 -0
- package/dist/remediation/refactor/call-site-finder.d.ts.map +1 -0
- package/dist/remediation/refactor/refactor-agent.d.ts +34 -0
- package/dist/remediation/refactor/refactor-agent.d.ts.map +1 -0
- package/dist/remediation/refactor/refactor.test.d.ts +2 -0
- package/dist/remediation/refactor/refactor.test.d.ts.map +1 -0
- package/dist/remediation/strategies/patch-synthesis-prompt.d.ts.map +1 -1
- package/dist/remediation/tools/apply-patch-file/index.d.ts +5 -1
- package/dist/remediation/tools/apply-patch-file/index.d.ts.map +1 -1
- package/dist/remediation/tools/apply-version-bump.d.ts +2 -0
- package/dist/remediation/tools/apply-version-bump.d.ts.map +1 -1
- package/dist/remediation/tools/check-inventory.d.ts.map +1 -1
- package/dist/remediation/tools/check-reachability.d.ts +6 -1
- package/dist/remediation/tools/check-reachability.d.ts.map +1 -1
- package/dist/remediation/tools/generate-patch/helpers.d.ts +1 -0
- package/dist/remediation/tools/generate-patch/helpers.d.ts.map +1 -1
- package/dist/remediation/tools/generate-patch/index.d.ts +1 -0
- package/dist/remediation/tools/generate-patch/index.d.ts.map +1 -1
- package/dist/scanner/adapters/npm-audit.d.ts.map +1 -1
- package/dist/scanner/adapters/yarn-audit.d.ts.map +1 -1
- package/dist/scanner/delta-scan.d.ts.map +1 -1
- package/dist/scanner/lockfile-diff.d.ts +10 -0
- package/dist/scanner/lockfile-diff.d.ts.map +1 -0
- package/dist/scanner/lockfile-diff.test.d.ts +2 -0
- package/dist/scanner/lockfile-diff.test.d.ts.map +1 -0
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -65,7 +65,10 @@ var OPTION_DESCRIPTIONS = {
|
|
|
65
65
|
campaignMode: "Enable portfolio campaign mode to risk-rank targets before execution",
|
|
66
66
|
escalationGraph: "Optional mapping from unresolved reasons to intended escalation actions",
|
|
67
67
|
offlineIntelligence: "Skip all network calls to intelligence sources. Only local inventory and npm-registry version resolution proceed.",
|
|
68
|
-
intelligenceSnapshotPath: "Path to a local JSON snapshot of pre-fetched CVE intelligence (keyed by CVE ID) used when offline mode is active."
|
|
68
|
+
intelligenceSnapshotPath: "Path to a local JSON snapshot of pre-fetched CVE intelligence (keyed by CVE ID) used when offline mode is active.",
|
|
69
|
+
proofOfFix: "If true, enforce dual-stage exploit reproduction proof-of-fix verification (assert failure pre-patch, assert pass post-patch)",
|
|
70
|
+
requireProofOfFix: "If true, reject patches and roll back if proof-of-fix verification fails",
|
|
71
|
+
breakingChangeRefactor: "If true, attempt agentic call-site refactoring when major version upgrades introduce breaking API changes"
|
|
69
72
|
};
|
|
70
73
|
function createConstraintSchemaProperties() {
|
|
71
74
|
return {
|
|
@@ -87,7 +90,12 @@ function createConstraintSchemaProperties() {
|
|
|
87
90
|
type: "boolean",
|
|
88
91
|
description: OPTION_DESCRIPTIONS.enforceFrozenLockfile
|
|
89
92
|
},
|
|
90
|
-
workspace: { type: "string", description: OPTION_DESCRIPTIONS.workspace }
|
|
93
|
+
workspace: { type: "string", description: OPTION_DESCRIPTIONS.workspace },
|
|
94
|
+
requireProofOfFix: { type: "boolean", description: OPTION_DESCRIPTIONS.requireProofOfFix },
|
|
95
|
+
enableBreakingChangeRefactor: {
|
|
96
|
+
type: "boolean",
|
|
97
|
+
description: OPTION_DESCRIPTIONS.breakingChangeRefactor
|
|
98
|
+
}
|
|
91
99
|
};
|
|
92
100
|
}
|
|
93
101
|
function createRemediateOptionSchemaProperties(options) {
|
|
@@ -306,6 +314,14 @@ function createRemediateOptionSchemaProperties(options) {
|
|
|
306
314
|
"validation-failed": {
|
|
307
315
|
type: "string",
|
|
308
316
|
enum: ["open-issue", "notify-channel", "create-draft-pr", "hold-branch", "none"]
|
|
317
|
+
},
|
|
318
|
+
"proof-of-fix-failed": {
|
|
319
|
+
type: "string",
|
|
320
|
+
enum: ["open-issue", "notify-channel", "create-draft-pr", "hold-branch", "none"]
|
|
321
|
+
},
|
|
322
|
+
"breaking-change-unresolved": {
|
|
323
|
+
type: "string",
|
|
324
|
+
enum: ["open-issue", "notify-channel", "create-draft-pr", "hold-branch", "none"]
|
|
309
325
|
}
|
|
310
326
|
},
|
|
311
327
|
additionalProperties: false
|
|
@@ -314,6 +330,12 @@ function createRemediateOptionSchemaProperties(options) {
|
|
|
314
330
|
intelligenceSnapshotPath: {
|
|
315
331
|
type: "string",
|
|
316
332
|
description: OPTION_DESCRIPTIONS.intelligenceSnapshotPath
|
|
333
|
+
},
|
|
334
|
+
proofOfFix: { type: "boolean", description: OPTION_DESCRIPTIONS.proofOfFix },
|
|
335
|
+
requireProofOfFix: { type: "boolean", description: OPTION_DESCRIPTIONS.requireProofOfFix },
|
|
336
|
+
breakingChangeRefactor: {
|
|
337
|
+
type: "boolean",
|
|
338
|
+
description: OPTION_DESCRIPTIONS.breakingChangeRefactor
|
|
317
339
|
}
|
|
318
340
|
};
|
|
319
341
|
}
|
|
@@ -887,12 +909,12 @@ import { readdir, readFile as readFile2, stat } from "fs/promises";
|
|
|
887
909
|
import { join as join5 } from "path";
|
|
888
910
|
|
|
889
911
|
// src/remediation/strategies/patch-utils.ts
|
|
890
|
-
import { existsSync as existsSync3, mkdirSync, writeFileSync, readFileSync as
|
|
912
|
+
import { existsSync as existsSync3, mkdirSync, writeFileSync, readFileSync as readFileSync3 } from "fs";
|
|
891
913
|
import { join as join4 } from "path";
|
|
892
914
|
import { execa as execa2 } from "execa";
|
|
893
915
|
|
|
894
916
|
// src/platform/package-manager/index.ts
|
|
895
|
-
import { existsSync as existsSync2 } from "fs";
|
|
917
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
896
918
|
import { join as join3 } from "path";
|
|
897
919
|
import { execa } from "execa";
|
|
898
920
|
|
|
@@ -1085,6 +1107,32 @@ function resolveTestCommand(pm, constraints) {
|
|
|
1085
1107
|
const base = pm === "pnpm" ? ["pnpm", "test"] : pm === "yarn" ? ["yarn", "test"] : pm === "bun" ? ["bun", "test"] : pm === "deno" ? ["deno", "test"] : ["npm", "test"];
|
|
1086
1108
|
return withWorkspace(base, pm, constraints?.workspace);
|
|
1087
1109
|
}
|
|
1110
|
+
function resolveTypecheckCommand(pm, constraints, cwd = process.cwd()) {
|
|
1111
|
+
const pkgJsonPath = join3(cwd, "package.json");
|
|
1112
|
+
let hasTypecheckScript = false;
|
|
1113
|
+
const hasTsConfig = existsSync2(join3(cwd, "tsconfig.json"));
|
|
1114
|
+
if (existsSync2(pkgJsonPath)) {
|
|
1115
|
+
try {
|
|
1116
|
+
const pkg = JSON.parse(readFileSync2(pkgJsonPath, "utf8"));
|
|
1117
|
+
if (pkg.scripts?.typecheck) {
|
|
1118
|
+
hasTypecheckScript = true;
|
|
1119
|
+
}
|
|
1120
|
+
} catch {
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
if (hasTypecheckScript) {
|
|
1124
|
+
const base = [pm, "run", "typecheck"];
|
|
1125
|
+
return withWorkspace(base, pm, constraints?.workspace);
|
|
1126
|
+
}
|
|
1127
|
+
if (hasTsConfig) {
|
|
1128
|
+
if (pm === "deno") {
|
|
1129
|
+
return withWorkspace(["deno", "check"], pm, constraints?.workspace);
|
|
1130
|
+
}
|
|
1131
|
+
const base = ["npx", "tsc", "--noEmit"];
|
|
1132
|
+
return withWorkspace(base, pm, constraints?.workspace);
|
|
1133
|
+
}
|
|
1134
|
+
return [];
|
|
1135
|
+
}
|
|
1088
1136
|
function resolveAuditCommand(pm, constraints) {
|
|
1089
1137
|
if (pm === "deno") {
|
|
1090
1138
|
throw new Error(
|
|
@@ -1329,12 +1377,12 @@ function defineTool(config) {
|
|
|
1329
1377
|
|
|
1330
1378
|
// src/remediation/tools/check-inventory.ts
|
|
1331
1379
|
import { z } from "zod";
|
|
1332
|
-
import { readFileSync as
|
|
1333
|
-
import { join as
|
|
1380
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1381
|
+
import { join as join8 } from "path";
|
|
1334
1382
|
import { execa as execa3 } from "execa";
|
|
1335
1383
|
|
|
1336
1384
|
// src/platform/policy.ts
|
|
1337
|
-
import { existsSync as existsSync6, readFileSync as
|
|
1385
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
|
|
1338
1386
|
import { join as join6 } from "path";
|
|
1339
1387
|
import { parse as yamlParse } from "yaml";
|
|
1340
1388
|
var DEFAULT_POLICY = {
|
|
@@ -1369,7 +1417,7 @@ function loadPolicy(cwd, explicitPath) {
|
|
|
1369
1417
|
const candidate = explicitPath ?? join6(cwd, ".github", "autoremediator.yml");
|
|
1370
1418
|
if (!existsSync6(candidate)) return DEFAULT_POLICY;
|
|
1371
1419
|
try {
|
|
1372
|
-
const parsed = yamlParse(
|
|
1420
|
+
const parsed = yamlParse(readFileSync4(candidate, "utf8"));
|
|
1373
1421
|
return {
|
|
1374
1422
|
allowMajorBumps: parsed.allowMajorBumps ?? DEFAULT_POLICY.allowMajorBumps,
|
|
1375
1423
|
denyPackages: parsed.denyPackages ?? DEFAULT_POLICY.denyPackages,
|
|
@@ -1430,7 +1478,7 @@ function isActiveSuppression(suppression) {
|
|
|
1430
1478
|
}
|
|
1431
1479
|
function loadSuppressionsFile(filePath) {
|
|
1432
1480
|
try {
|
|
1433
|
-
const content =
|
|
1481
|
+
const content = readFileSync4(filePath, "utf8");
|
|
1434
1482
|
const parsed = yamlParse(content);
|
|
1435
1483
|
return Array.isArray(parsed?.suppressions) ? parsed.suppressions : [];
|
|
1436
1484
|
} catch {
|
|
@@ -1456,6 +1504,140 @@ function checkSlaBreach(cveId, severity, publishedAt, slaPolicy) {
|
|
|
1456
1504
|
};
|
|
1457
1505
|
}
|
|
1458
1506
|
|
|
1507
|
+
// src/platform/workspace.ts
|
|
1508
|
+
import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync5, statSync } from "fs";
|
|
1509
|
+
import { join as join7, resolve as resolve2, relative } from "path";
|
|
1510
|
+
import yaml from "yaml";
|
|
1511
|
+
function findWorkspaceRoot(startDir) {
|
|
1512
|
+
let curr = resolve2(startDir);
|
|
1513
|
+
while (true) {
|
|
1514
|
+
if (existsSync7(join7(curr, "pnpm-workspace.yaml"))) return curr;
|
|
1515
|
+
const pkgPath = join7(curr, "package.json");
|
|
1516
|
+
if (existsSync7(pkgPath)) {
|
|
1517
|
+
try {
|
|
1518
|
+
const parsed = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
1519
|
+
if (parsed.workspaces) return curr;
|
|
1520
|
+
} catch {
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
const parent = resolve2(curr, "..");
|
|
1524
|
+
if (parent === curr) break;
|
|
1525
|
+
curr = parent;
|
|
1526
|
+
}
|
|
1527
|
+
return null;
|
|
1528
|
+
}
|
|
1529
|
+
function findWorkspacePackages(cwd) {
|
|
1530
|
+
const rootCwd = findWorkspaceRoot(cwd) ?? resolve2(cwd);
|
|
1531
|
+
const packages = [];
|
|
1532
|
+
const packageGlobs = [];
|
|
1533
|
+
const pnpmWorkspaceFile = join7(rootCwd, "pnpm-workspace.yaml");
|
|
1534
|
+
if (existsSync7(pnpmWorkspaceFile)) {
|
|
1535
|
+
try {
|
|
1536
|
+
const content = readFileSync5(pnpmWorkspaceFile, "utf8");
|
|
1537
|
+
const parsed = yaml.parse(content);
|
|
1538
|
+
if (Array.isArray(parsed?.packages)) {
|
|
1539
|
+
packageGlobs.push(...parsed.packages);
|
|
1540
|
+
}
|
|
1541
|
+
} catch {
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
const rootPkgFile = join7(rootCwd, "package.json");
|
|
1545
|
+
if (existsSync7(rootPkgFile)) {
|
|
1546
|
+
try {
|
|
1547
|
+
const rootPkg = JSON.parse(readFileSync5(rootPkgFile, "utf8"));
|
|
1548
|
+
if (Array.isArray(rootPkg.workspaces)) {
|
|
1549
|
+
packageGlobs.push(...rootPkg.workspaces);
|
|
1550
|
+
} else if (Array.isArray(rootPkg.workspaces?.packages)) {
|
|
1551
|
+
packageGlobs.push(...rootPkg.workspaces.packages);
|
|
1552
|
+
}
|
|
1553
|
+
} catch {
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
const denoJsonFile = join7(rootCwd, "deno.json");
|
|
1557
|
+
if (existsSync7(denoJsonFile)) {
|
|
1558
|
+
try {
|
|
1559
|
+
const denoJson = JSON.parse(readFileSync5(denoJsonFile, "utf8"));
|
|
1560
|
+
if (Array.isArray(denoJson.workspace)) {
|
|
1561
|
+
packageGlobs.push(...denoJson.workspace);
|
|
1562
|
+
}
|
|
1563
|
+
} catch {
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
if (packageGlobs.length === 0) {
|
|
1567
|
+
return packages;
|
|
1568
|
+
}
|
|
1569
|
+
const seenDirs = /* @__PURE__ */ new Set();
|
|
1570
|
+
for (const glob of packageGlobs) {
|
|
1571
|
+
const cleanGlob = glob.trim().replace(/\/+$/, "");
|
|
1572
|
+
if (!cleanGlob || cleanGlob.startsWith("!")) continue;
|
|
1573
|
+
if (cleanGlob.endsWith("/*")) {
|
|
1574
|
+
const parentDir = join7(rootCwd, cleanGlob.slice(0, -2));
|
|
1575
|
+
if (!existsSync7(parentDir)) continue;
|
|
1576
|
+
try {
|
|
1577
|
+
const entries = readdirSync(parentDir);
|
|
1578
|
+
for (const entry of entries) {
|
|
1579
|
+
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
1580
|
+
const subDir = join7(parentDir, entry);
|
|
1581
|
+
try {
|
|
1582
|
+
if (statSync(subDir).isDirectory()) {
|
|
1583
|
+
addWorkspacePackage(rootCwd, subDir, packages, seenDirs);
|
|
1584
|
+
}
|
|
1585
|
+
} catch {
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
} catch {
|
|
1589
|
+
}
|
|
1590
|
+
} else {
|
|
1591
|
+
const directDir = join7(rootCwd, cleanGlob);
|
|
1592
|
+
if (existsSync7(directDir)) {
|
|
1593
|
+
try {
|
|
1594
|
+
if (statSync(directDir).isDirectory()) {
|
|
1595
|
+
addWorkspacePackage(rootCwd, directDir, packages, seenDirs);
|
|
1596
|
+
}
|
|
1597
|
+
} catch {
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return packages;
|
|
1603
|
+
}
|
|
1604
|
+
function addWorkspacePackage(rootCwd, dir, out, seen) {
|
|
1605
|
+
const norm = resolve2(dir);
|
|
1606
|
+
if (seen.has(norm)) return;
|
|
1607
|
+
seen.add(norm);
|
|
1608
|
+
const manifestPath = join7(dir, "package.json");
|
|
1609
|
+
if (!existsSync7(manifestPath)) return;
|
|
1610
|
+
try {
|
|
1611
|
+
const content = readFileSync5(manifestPath, "utf8");
|
|
1612
|
+
const manifest = JSON.parse(content);
|
|
1613
|
+
out.push({
|
|
1614
|
+
name: manifest.name ?? relative(rootCwd, dir),
|
|
1615
|
+
relativeDir: relative(rootCwd, dir),
|
|
1616
|
+
absoluteDir: norm,
|
|
1617
|
+
manifestPath,
|
|
1618
|
+
manifest
|
|
1619
|
+
});
|
|
1620
|
+
} catch {
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
function findDeclaringWorkspacePackage(cwd, dependencyName, filterWorkspace) {
|
|
1624
|
+
const workspaces = findWorkspacePackages(cwd);
|
|
1625
|
+
if (workspaces.length === 0) return null;
|
|
1626
|
+
const matchesFilter = (wp) => {
|
|
1627
|
+
if (!filterWorkspace) return true;
|
|
1628
|
+
const filter = filterWorkspace.trim();
|
|
1629
|
+
return wp.name === filter || wp.relativeDir === filter || wp.relativeDir.replace(/\\/g, "/") === filter.replace(/\\/g, "/");
|
|
1630
|
+
};
|
|
1631
|
+
for (const wp of workspaces) {
|
|
1632
|
+
if (!matchesFilter(wp)) continue;
|
|
1633
|
+
const isDeclared = Boolean(wp.manifest.dependencies?.[dependencyName]) || Boolean(wp.manifest.devDependencies?.[dependencyName]) || Boolean(wp.manifest.peerDependencies?.[dependencyName]);
|
|
1634
|
+
if (isDeclared) {
|
|
1635
|
+
return wp;
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1459
1641
|
// src/remediation/tools/check-inventory.ts
|
|
1460
1642
|
var checkInventoryTool = defineTool({
|
|
1461
1643
|
description: "Read the project's package.json and installed dependencies to list packages and exact versions. Must be called before checking version matches.",
|
|
@@ -1473,7 +1655,7 @@ var checkInventoryTool = defineTool({
|
|
|
1473
1655
|
}) => {
|
|
1474
1656
|
let pkgJson;
|
|
1475
1657
|
try {
|
|
1476
|
-
pkgJson = JSON.parse(
|
|
1658
|
+
pkgJson = JSON.parse(readFileSync6(join8(cwd, "package.json"), "utf8"));
|
|
1477
1659
|
} catch {
|
|
1478
1660
|
return {
|
|
1479
1661
|
packages: [],
|
|
@@ -1505,7 +1687,8 @@ var checkInventoryTool = defineTool({
|
|
|
1505
1687
|
}
|
|
1506
1688
|
const packages = [];
|
|
1507
1689
|
for (const [name, version] of installedVersions.entries()) {
|
|
1508
|
-
const
|
|
1690
|
+
const declaringWorkspace = findDeclaringWorkspacePackage(cwd, name, workspace);
|
|
1691
|
+
const isDirect = Boolean(pkgJson.dependencies?.[name]) || Boolean(pkgJson.devDependencies?.[name]) || Boolean(pkgJson.peerDependencies?.[name]) || Boolean(declaringWorkspace);
|
|
1509
1692
|
packages.push({
|
|
1510
1693
|
name,
|
|
1511
1694
|
version,
|
|
@@ -1628,173 +1811,14 @@ async function validatePatchArtifact(patchFilePath, options = {}) {
|
|
|
1628
1811
|
};
|
|
1629
1812
|
}
|
|
1630
1813
|
|
|
1631
|
-
// src/
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
`Unsupported LLM provider "${raw}". Set AUTOREMEDIATOR_LLM_PROVIDER to "remote" or "local".`
|
|
1637
|
-
);
|
|
1638
|
-
}
|
|
1639
|
-
return raw;
|
|
1640
|
-
}
|
|
1641
|
-
function resolveModelName(provider, options = {}, routing = {}) {
|
|
1642
|
-
if (options.model) return options.model;
|
|
1643
|
-
if (process.env.AUTOREMEDIATOR_MODEL) return process.env.AUTOREMEDIATOR_MODEL;
|
|
1644
|
-
const cwd = options.cwd ?? process.cwd();
|
|
1645
|
-
const policy = loadPolicy(cwd, options.policy);
|
|
1646
|
-
const providerEnvModel = provider === "remote" ? process.env.AUTOREMEDIATOR_MODEL_REMOTE : process.env.AUTOREMEDIATOR_MODEL_LOCAL;
|
|
1647
|
-
if (providerEnvModel) return providerEnvModel;
|
|
1648
|
-
const policyPinnedModel = provider === "remote" ? policy.modelDefaults?.remote : policy.modelDefaults?.local;
|
|
1649
|
-
if (policyPinnedModel) return policyPinnedModel;
|
|
1650
|
-
const defaults = {
|
|
1651
|
-
remote: "remote-default",
|
|
1652
|
-
local: "local"
|
|
1653
|
-
};
|
|
1654
|
-
const routingEnabled = options.dynamicModelRouting ?? policy.dynamicModelRouting ?? false;
|
|
1655
|
-
const threshold = options.dynamicRoutingThresholdChars ?? policy.dynamicRoutingThresholdChars ?? 18e3;
|
|
1656
|
-
if (provider === "remote" && routingEnabled && typeof routing.inputChars === "number" && routing.inputChars >= threshold) {
|
|
1657
|
-
return process.env.AUTOREMEDIATOR_MODEL_REMOTE_LARGE ?? "remote-large";
|
|
1658
|
-
}
|
|
1659
|
-
return defaults[provider];
|
|
1660
|
-
}
|
|
1661
|
-
async function loadRemoteFactory() {
|
|
1662
|
-
const moduleName = process.env.AUTOREMEDIATOR_REMOTE_CLIENT_MODULE;
|
|
1663
|
-
const exportName = process.env.AUTOREMEDIATOR_REMOTE_CLIENT_FACTORY ?? "createRemoteClient";
|
|
1664
|
-
if (!moduleName) {
|
|
1665
|
-
throw new Error(
|
|
1666
|
-
"AUTOREMEDIATOR_REMOTE_CLIENT_MODULE is required for remote provider model loading."
|
|
1667
|
-
);
|
|
1668
|
-
}
|
|
1669
|
-
if (moduleName.startsWith("./") || moduleName.startsWith("../") || moduleName.startsWith("/") || moduleName.startsWith("file:")) {
|
|
1670
|
-
throw new Error(
|
|
1671
|
-
`AUTOREMEDIATOR_REMOTE_CLIENT_MODULE must be a package name, not a file path: ${moduleName}`
|
|
1672
|
-
);
|
|
1673
|
-
}
|
|
1674
|
-
const loaded = await import(moduleName);
|
|
1675
|
-
const factory = loaded[exportName];
|
|
1676
|
-
if (typeof factory !== "function") {
|
|
1677
|
-
throw new Error(
|
|
1678
|
-
`Remote client factory "${exportName}" was not found in module "${moduleName}".`
|
|
1679
|
-
);
|
|
1680
|
-
}
|
|
1681
|
-
return factory;
|
|
1682
|
-
}
|
|
1683
|
-
async function createModel(options = {}, routing = {}) {
|
|
1684
|
-
const provider = resolveProvider(options);
|
|
1685
|
-
if (provider === "local") {
|
|
1686
|
-
throw new Error(
|
|
1687
|
-
"Local provider does not create a language model. Use the deterministic pipeline path instead."
|
|
1688
|
-
);
|
|
1689
|
-
}
|
|
1690
|
-
const apiKey = process.env.AUTOREMEDIATOR_REMOTE_API_KEY;
|
|
1691
|
-
if (!apiKey) {
|
|
1692
|
-
throw new Error(
|
|
1693
|
-
"AUTOREMEDIATOR_REMOTE_API_KEY environment variable is required for remote provider."
|
|
1694
|
-
);
|
|
1695
|
-
}
|
|
1696
|
-
const modelName = resolveModelName(provider, options, routing);
|
|
1697
|
-
const createRemoteClient = await loadRemoteFactory();
|
|
1698
|
-
const remoteClient = createRemoteClient({ apiKey });
|
|
1699
|
-
return remoteClient(modelName);
|
|
1700
|
-
}
|
|
1701
|
-
function getPatchConfidenceThreshold(provider, safetyProfile = "relaxed", riskLevel = "medium", overrides) {
|
|
1702
|
-
const relaxed = {
|
|
1703
|
-
remote: { low: 0.65, medium: 0.7, high: 0.8 },
|
|
1704
|
-
local: { low: 0.65, medium: 0.7, high: 0.8 }
|
|
1705
|
-
};
|
|
1706
|
-
const strict = {
|
|
1707
|
-
remote: { low: 0.8, medium: 0.85, high: 0.9 },
|
|
1708
|
-
local: { low: 0.8, medium: 0.85, high: 0.9 }
|
|
1709
|
-
};
|
|
1710
|
-
const baseThreshold = safetyProfile === "strict" ? strict[provider][riskLevel] : relaxed[provider][riskLevel];
|
|
1711
|
-
const override = overrides?.[riskLevel];
|
|
1712
|
-
if (typeof override === "number" && Number.isFinite(override)) {
|
|
1713
|
-
return Math.max(0, Math.min(1, override));
|
|
1714
|
-
}
|
|
1715
|
-
return baseThreshold;
|
|
1716
|
-
}
|
|
1717
|
-
function estimateModelCostUsd(params) {
|
|
1718
|
-
const inputTokens = params.promptChars / 4;
|
|
1719
|
-
const outputTokens = params.completionChars / 4;
|
|
1720
|
-
const pricePerThousand = params.provider === "remote" ? { input: 6e-3, output: 0.02 } : { input: 0, output: 0 };
|
|
1721
|
-
return inputTokens / 1e3 * pricePerThousand.input + outputTokens / 1e3 * pricePerThousand.output;
|
|
1722
|
-
}
|
|
1723
|
-
function getNvdConfig() {
|
|
1724
|
-
return {
|
|
1725
|
-
apiKey: process.env.AUTOREMEDIATOR_NVD_API_KEY
|
|
1726
|
-
};
|
|
1727
|
-
}
|
|
1728
|
-
function getGitHubToken() {
|
|
1729
|
-
return process.env.GITHUB_TOKEN;
|
|
1730
|
-
}
|
|
1731
|
-
function getIntelligenceSourceConfig() {
|
|
1732
|
-
return {
|
|
1733
|
-
gitLabAdvisoryApi: process.env.AUTOREMEDIATOR_GITLAB_ADVISORY_API ?? "https://advisories.gitlab.com/api/v1/advisories",
|
|
1734
|
-
certCcSearchUrl: process.env.AUTOREMEDIATOR_CERTCC_SEARCH_URL ?? "https://www.kb.cert.org/vuls/search",
|
|
1735
|
-
epssApi: process.env.AUTOREMEDIATOR_EPSS_API ?? "https://api.first.org/data/v1/epss",
|
|
1736
|
-
cveServicesApi: process.env.AUTOREMEDIATOR_CVE_SERVICES_API ?? "https://cveawg.mitre.org/api/cve",
|
|
1737
|
-
depsDevApi: process.env.AUTOREMEDIATOR_DEPSDEV_API ?? "https://api.deps.dev/v3",
|
|
1738
|
-
scorecardApi: process.env.AUTOREMEDIATOR_SCORECARD_API ?? "https://api.securityscorecards.dev",
|
|
1739
|
-
vendorAdvisoryFeeds: (process.env.AUTOREMEDIATOR_VENDOR_ADVISORY_FEEDS ?? "").split(",").map((v) => v.trim()).filter(Boolean),
|
|
1740
|
-
commercialFeeds: (process.env.AUTOREMEDIATOR_COMMERCIAL_FEEDS ?? "").split(",").map((v) => v.trim()).filter(Boolean),
|
|
1741
|
-
commercialFeedToken: process.env.AUTOREMEDIATOR_COMMERCIAL_FEED_TOKEN
|
|
1742
|
-
};
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
// src/platform/idempotency.ts
|
|
1746
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
1747
|
-
import { join as join8 } from "path";
|
|
1748
|
-
var DEFAULT_INDEX = {
|
|
1749
|
-
schemaVersion: "1.0",
|
|
1750
|
-
entries: {}
|
|
1751
|
-
};
|
|
1752
|
-
function indexFilePath(cwd) {
|
|
1753
|
-
return join8(cwd, ".autoremediator", "state", "idempotency.json");
|
|
1754
|
-
}
|
|
1755
|
-
function entryKey(idempotencyKey, cveId) {
|
|
1756
|
-
return `${idempotencyKey}::${cveId.toUpperCase()}`;
|
|
1757
|
-
}
|
|
1758
|
-
function loadIndex(cwd) {
|
|
1759
|
-
const filePath = indexFilePath(cwd);
|
|
1760
|
-
if (!existsSync7(filePath)) return DEFAULT_INDEX;
|
|
1761
|
-
try {
|
|
1762
|
-
const parsed = JSON.parse(readFileSync5(filePath, "utf8"));
|
|
1763
|
-
if (parsed && parsed.schemaVersion === "1.0" && parsed.entries) {
|
|
1764
|
-
return parsed;
|
|
1765
|
-
}
|
|
1766
|
-
return DEFAULT_INDEX;
|
|
1767
|
-
} catch {
|
|
1768
|
-
return DEFAULT_INDEX;
|
|
1769
|
-
}
|
|
1770
|
-
}
|
|
1771
|
-
function saveIndex(cwd, index) {
|
|
1772
|
-
const filePath = indexFilePath(cwd);
|
|
1773
|
-
mkdirSync2(join8(cwd, ".autoremediator", "state"), { recursive: true });
|
|
1774
|
-
writeFileSync2(filePath, JSON.stringify(index, null, 2) + "\n", "utf8");
|
|
1775
|
-
}
|
|
1776
|
-
function readIdempotentReport(cwd, idempotencyKey, cveId) {
|
|
1777
|
-
const index = loadIndex(cwd);
|
|
1778
|
-
const key = entryKey(idempotencyKey, cveId);
|
|
1779
|
-
return index.entries[key]?.report;
|
|
1780
|
-
}
|
|
1781
|
-
function storeIdempotentReport(cwd, idempotencyKey, cveId, report) {
|
|
1782
|
-
const index = loadIndex(cwd);
|
|
1783
|
-
const key = entryKey(idempotencyKey, cveId);
|
|
1784
|
-
index.entries[key] = {
|
|
1785
|
-
key: idempotencyKey,
|
|
1786
|
-
cveId: cveId.toUpperCase(),
|
|
1787
|
-
report,
|
|
1788
|
-
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1789
|
-
};
|
|
1790
|
-
saveIndex(cwd, index);
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
// src/remediation/pipeline.ts
|
|
1794
|
-
import { generateText as generateText2 } from "ai";
|
|
1814
|
+
// src/api/patches/reconcile.ts
|
|
1815
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1816
|
+
import { readFile as readFile3, rm, writeFile } from "fs/promises";
|
|
1817
|
+
import { join as join10 } from "path";
|
|
1818
|
+
import { execa as execa5 } from "execa";
|
|
1795
1819
|
|
|
1796
|
-
// src/remediation/
|
|
1797
|
-
import
|
|
1820
|
+
// src/remediation/evaluate-package.ts
|
|
1821
|
+
import semver2 from "semver";
|
|
1798
1822
|
|
|
1799
1823
|
// src/platform/http-client.ts
|
|
1800
1824
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
@@ -1976,22 +2000,746 @@ async function lookupCveOsv(cveId) {
|
|
|
1976
2000
|
return parseOsvVuln(vuln);
|
|
1977
2001
|
}
|
|
1978
2002
|
|
|
1979
|
-
// src/
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2003
|
+
// src/platform/config.ts
|
|
2004
|
+
function resolveProvider(options = {}) {
|
|
2005
|
+
const raw = options.llmProvider ?? process.env.AUTOREMEDIATOR_LLM_PROVIDER ?? "remote";
|
|
2006
|
+
if (raw !== "remote" && raw !== "local") {
|
|
2007
|
+
throw new Error(
|
|
2008
|
+
`Unsupported LLM provider "${raw}". Set AUTOREMEDIATOR_LLM_PROVIDER to "remote" or "local".`
|
|
2009
|
+
);
|
|
2010
|
+
}
|
|
2011
|
+
return raw;
|
|
2012
|
+
}
|
|
2013
|
+
function resolveModelName(provider, options = {}, routing = {}) {
|
|
2014
|
+
if (options.model) return options.model;
|
|
2015
|
+
if (process.env.AUTOREMEDIATOR_MODEL) return process.env.AUTOREMEDIATOR_MODEL;
|
|
2016
|
+
const cwd = options.cwd ?? process.cwd();
|
|
2017
|
+
const policy = loadPolicy(cwd, options.policy);
|
|
2018
|
+
const providerEnvModel = provider === "remote" ? process.env.AUTOREMEDIATOR_MODEL_REMOTE : process.env.AUTOREMEDIATOR_MODEL_LOCAL;
|
|
2019
|
+
if (providerEnvModel) return providerEnvModel;
|
|
2020
|
+
const policyPinnedModel = provider === "remote" ? policy.modelDefaults?.remote : policy.modelDefaults?.local;
|
|
2021
|
+
if (policyPinnedModel) return policyPinnedModel;
|
|
2022
|
+
const defaults = {
|
|
2023
|
+
remote: "remote-default",
|
|
2024
|
+
local: "local"
|
|
2025
|
+
};
|
|
2026
|
+
const routingEnabled = options.dynamicModelRouting ?? policy.dynamicModelRouting ?? false;
|
|
2027
|
+
const threshold = options.dynamicRoutingThresholdChars ?? policy.dynamicRoutingThresholdChars ?? 18e3;
|
|
2028
|
+
if (provider === "remote" && routingEnabled && typeof routing.inputChars === "number" && routing.inputChars >= threshold) {
|
|
2029
|
+
return process.env.AUTOREMEDIATOR_MODEL_REMOTE_LARGE ?? "remote-large";
|
|
2030
|
+
}
|
|
2031
|
+
return defaults[provider];
|
|
2032
|
+
}
|
|
2033
|
+
async function loadRemoteFactory() {
|
|
2034
|
+
const moduleName = process.env.AUTOREMEDIATOR_REMOTE_CLIENT_MODULE;
|
|
2035
|
+
const exportName = process.env.AUTOREMEDIATOR_REMOTE_CLIENT_FACTORY ?? "createRemoteClient";
|
|
2036
|
+
if (!moduleName) {
|
|
2037
|
+
throw new Error(
|
|
2038
|
+
"AUTOREMEDIATOR_REMOTE_CLIENT_MODULE is required for remote provider model loading."
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
2041
|
+
if (moduleName.startsWith("./") || moduleName.startsWith("../") || moduleName.startsWith("/") || moduleName.startsWith("file:")) {
|
|
2042
|
+
throw new Error(
|
|
2043
|
+
`AUTOREMEDIATOR_REMOTE_CLIENT_MODULE must be a package name, not a file path: ${moduleName}`
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
const loaded = await import(moduleName);
|
|
2047
|
+
const factory = loaded[exportName];
|
|
2048
|
+
if (typeof factory !== "function") {
|
|
2049
|
+
throw new Error(
|
|
2050
|
+
`Remote client factory "${exportName}" was not found in module "${moduleName}".`
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
2053
|
+
return factory;
|
|
2054
|
+
}
|
|
2055
|
+
async function createModel(options = {}, routing = {}) {
|
|
2056
|
+
const provider = resolveProvider(options);
|
|
2057
|
+
if (provider === "local") {
|
|
2058
|
+
throw new Error(
|
|
2059
|
+
"Local provider does not create a language model. Use the deterministic pipeline path instead."
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
const apiKey = process.env.AUTOREMEDIATOR_REMOTE_API_KEY;
|
|
2063
|
+
if (!apiKey) {
|
|
2064
|
+
throw new Error(
|
|
2065
|
+
"AUTOREMEDIATOR_REMOTE_API_KEY environment variable is required for remote provider."
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
const modelName = resolveModelName(provider, options, routing);
|
|
2069
|
+
const createRemoteClient = await loadRemoteFactory();
|
|
2070
|
+
const remoteClient = createRemoteClient({ apiKey });
|
|
2071
|
+
return remoteClient(modelName);
|
|
2072
|
+
}
|
|
2073
|
+
function getPatchConfidenceThreshold(provider, safetyProfile = "relaxed", riskLevel = "medium", overrides) {
|
|
2074
|
+
const relaxed = {
|
|
2075
|
+
remote: { low: 0.65, medium: 0.7, high: 0.8 },
|
|
2076
|
+
local: { low: 0.65, medium: 0.7, high: 0.8 }
|
|
2077
|
+
};
|
|
2078
|
+
const strict = {
|
|
2079
|
+
remote: { low: 0.8, medium: 0.85, high: 0.9 },
|
|
2080
|
+
local: { low: 0.8, medium: 0.85, high: 0.9 }
|
|
2081
|
+
};
|
|
2082
|
+
const baseThreshold = safetyProfile === "strict" ? strict[provider][riskLevel] : relaxed[provider][riskLevel];
|
|
2083
|
+
const override = overrides?.[riskLevel];
|
|
2084
|
+
if (typeof override === "number" && Number.isFinite(override)) {
|
|
2085
|
+
return Math.max(0, Math.min(1, override));
|
|
2086
|
+
}
|
|
2087
|
+
return baseThreshold;
|
|
2088
|
+
}
|
|
2089
|
+
function estimateModelCostUsd(params) {
|
|
2090
|
+
const inputTokens = params.promptChars / 4;
|
|
2091
|
+
const outputTokens = params.completionChars / 4;
|
|
2092
|
+
const pricePerThousand = params.provider === "remote" ? { input: 6e-3, output: 0.02 } : { input: 0, output: 0 };
|
|
2093
|
+
return inputTokens / 1e3 * pricePerThousand.input + outputTokens / 1e3 * pricePerThousand.output;
|
|
2094
|
+
}
|
|
2095
|
+
function getNvdConfig() {
|
|
2096
|
+
return {
|
|
2097
|
+
apiKey: process.env.AUTOREMEDIATOR_NVD_API_KEY
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
function getGitHubToken() {
|
|
2101
|
+
return process.env.GITHUB_TOKEN;
|
|
2102
|
+
}
|
|
2103
|
+
function getIntelligenceSourceConfig() {
|
|
2104
|
+
return {
|
|
2105
|
+
gitLabAdvisoryApi: process.env.AUTOREMEDIATOR_GITLAB_ADVISORY_API ?? "https://advisories.gitlab.com/api/v1/advisories",
|
|
2106
|
+
certCcSearchUrl: process.env.AUTOREMEDIATOR_CERTCC_SEARCH_URL ?? "https://www.kb.cert.org/vuls/search",
|
|
2107
|
+
epssApi: process.env.AUTOREMEDIATOR_EPSS_API ?? "https://api.first.org/data/v1/epss",
|
|
2108
|
+
cveServicesApi: process.env.AUTOREMEDIATOR_CVE_SERVICES_API ?? "https://cveawg.mitre.org/api/cve",
|
|
2109
|
+
depsDevApi: process.env.AUTOREMEDIATOR_DEPSDEV_API ?? "https://api.deps.dev/v3",
|
|
2110
|
+
scorecardApi: process.env.AUTOREMEDIATOR_SCORECARD_API ?? "https://api.securityscorecards.dev",
|
|
2111
|
+
vendorAdvisoryFeeds: (process.env.AUTOREMEDIATOR_VENDOR_ADVISORY_FEEDS ?? "").split(",").map((v) => v.trim()).filter(Boolean),
|
|
2112
|
+
commercialFeeds: (process.env.AUTOREMEDIATOR_COMMERCIAL_FEEDS ?? "").split(",").map((v) => v.trim()).filter(Boolean),
|
|
2113
|
+
commercialFeedToken: process.env.AUTOREMEDIATOR_COMMERCIAL_FEED_TOKEN
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// src/intelligence/sources/epss.ts
|
|
2118
|
+
async function fetchEpss(cveId) {
|
|
2119
|
+
const { epssApi } = getIntelligenceSourceConfig();
|
|
2120
|
+
if (!epssApi) return void 0;
|
|
2121
|
+
try {
|
|
2122
|
+
const url = new URL(epssApi);
|
|
2123
|
+
url.searchParams.set("cve", cveId);
|
|
2124
|
+
const res = await httpClient({ url: url.toString() });
|
|
2125
|
+
if (!res.ok) return void 0;
|
|
2126
|
+
const body = res.data;
|
|
2127
|
+
return body.data?.[0];
|
|
2128
|
+
} catch {
|
|
2129
|
+
return void 0;
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
async function enrichWithEpss(details) {
|
|
2133
|
+
const row = await fetchEpss(details.id);
|
|
2134
|
+
if (!row) return details;
|
|
2135
|
+
const score = Number.parseFloat(row.epss);
|
|
2136
|
+
const percentile = Number.parseFloat(row.percentile);
|
|
2137
|
+
if (!Number.isFinite(score) || !Number.isFinite(percentile)) {
|
|
2138
|
+
return details;
|
|
2139
|
+
}
|
|
2140
|
+
details.epss = {
|
|
2141
|
+
score,
|
|
2142
|
+
percentile,
|
|
2143
|
+
date: row.date
|
|
2144
|
+
};
|
|
2145
|
+
return details;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// src/intelligence/sources/cisa-kev.ts
|
|
2149
|
+
var CISA_KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json";
|
|
2150
|
+
async function fetchCisaKevFeed() {
|
|
2151
|
+
try {
|
|
2152
|
+
const res = await httpClient({ url: CISA_KEV_URL });
|
|
2153
|
+
if (!res.ok) return void 0;
|
|
2154
|
+
return res.data;
|
|
2155
|
+
} catch {
|
|
2156
|
+
return void 0;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
function findKevEntry(feed, cveId) {
|
|
2160
|
+
if (!feed?.vulnerabilities?.length) return void 0;
|
|
2161
|
+
const normalized = cveId.toUpperCase();
|
|
2162
|
+
return feed.vulnerabilities.find((v) => v.cveID.toUpperCase() === normalized);
|
|
2163
|
+
}
|
|
2164
|
+
async function enrichWithCisaKev(details) {
|
|
2165
|
+
const feed = await fetchCisaKevFeed();
|
|
2166
|
+
const entry = findKevEntry(feed, details.id);
|
|
2167
|
+
if (!entry) return details;
|
|
2168
|
+
details.kev = {
|
|
2169
|
+
knownExploited: true,
|
|
2170
|
+
dateAdded: entry.dateAdded,
|
|
2171
|
+
dueDate: entry.dueDate,
|
|
2172
|
+
requiredAction: entry.requiredAction,
|
|
2173
|
+
knownRansomwareCampaignUse: entry.knownRansomwareCampaignUse
|
|
2174
|
+
};
|
|
2175
|
+
if (!details.references.includes(CISA_KEV_URL)) {
|
|
2176
|
+
details.references.push(CISA_KEV_URL);
|
|
2177
|
+
}
|
|
2178
|
+
return details;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// src/intelligence/sources/registry.ts
|
|
2182
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
2183
|
+
import { join as join9 } from "path";
|
|
2184
|
+
import { execa as execa4 } from "execa";
|
|
2185
|
+
import semver from "semver";
|
|
2186
|
+
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
2187
|
+
async function fetchPackageVersions(packageName) {
|
|
2188
|
+
const url = `${NPM_REGISTRY}/${encodeURIComponent(packageName)}`;
|
|
2189
|
+
try {
|
|
2190
|
+
const res = await httpClient({ url });
|
|
2191
|
+
if (res.status === 404) return [];
|
|
2192
|
+
if (!res.ok) {
|
|
2193
|
+
throw new Error(`npm registry error ${res.status} for "${packageName}": ${res.text}`);
|
|
2194
|
+
}
|
|
2195
|
+
const data = res.data;
|
|
2196
|
+
return Object.keys(data.versions);
|
|
2197
|
+
} catch (err) {
|
|
2198
|
+
if (err instanceof Error && err.message.includes("registry error")) {
|
|
2199
|
+
throw err;
|
|
2200
|
+
}
|
|
2201
|
+
return [];
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
async function findSafeUpgradeVersion(packageName, installedVersion, firstPatchedVersion, vulnerableRange) {
|
|
2205
|
+
const resolution = await resolveSafeUpgradeVersion(
|
|
2206
|
+
packageName,
|
|
2207
|
+
installedVersion,
|
|
2208
|
+
firstPatchedVersion,
|
|
2209
|
+
vulnerableRange
|
|
2210
|
+
);
|
|
2211
|
+
return resolution.safeVersion;
|
|
2212
|
+
}
|
|
2213
|
+
async function resolveSafeUpgradeVersion(packageName, installedVersion, firstPatchedVersion, vulnerableRange) {
|
|
2214
|
+
const versions = await fetchPackageVersions(packageName);
|
|
2215
|
+
if (!versions.length) {
|
|
2216
|
+
return {
|
|
2217
|
+
candidates: {},
|
|
2218
|
+
majorOnlyFixAvailable: false
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
const installed = semver.parse(installedVersion);
|
|
2222
|
+
const candidates = versions.filter((v) => semver.valid(v) && semver.gte(v, firstPatchedVersion)).filter((v) => {
|
|
2223
|
+
if (!vulnerableRange) return true;
|
|
2224
|
+
try {
|
|
2225
|
+
return !semver.satisfies(v, vulnerableRange, { includePrerelease: false });
|
|
2226
|
+
} catch {
|
|
2227
|
+
return true;
|
|
2228
|
+
}
|
|
2229
|
+
}).sort(semver.compare);
|
|
2230
|
+
if (!candidates.length) {
|
|
2231
|
+
return {
|
|
2232
|
+
candidates: {},
|
|
2233
|
+
majorOnlyFixAvailable: false
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
const categorizedCandidates = {};
|
|
2237
|
+
for (const candidate of candidates) {
|
|
2238
|
+
const level = classifyUpgradeLevel(installedVersion, candidate);
|
|
2239
|
+
if (!level) continue;
|
|
2240
|
+
if (!categorizedCandidates[level]) {
|
|
2241
|
+
categorizedCandidates[level] = candidate;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
const safeVersion = categorizedCandidates.patch ?? categorizedCandidates.minor ?? categorizedCandidates.major;
|
|
2245
|
+
if (!safeVersion) {
|
|
2246
|
+
return {
|
|
2247
|
+
candidates: categorizedCandidates,
|
|
2248
|
+
majorOnlyFixAvailable: false
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
const upgradeLevel = classifyUpgradeLevel(installedVersion, safeVersion);
|
|
2252
|
+
const majorOnlyFixAvailable = !categorizedCandidates.patch && !categorizedCandidates.minor && Boolean(categorizedCandidates.major);
|
|
2253
|
+
if (!installed || !upgradeLevel) {
|
|
2254
|
+
return {
|
|
2255
|
+
safeVersion,
|
|
2256
|
+
upgradeLevel,
|
|
2257
|
+
candidates: categorizedCandidates,
|
|
2258
|
+
majorOnlyFixAvailable
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
return {
|
|
2262
|
+
safeVersion,
|
|
2263
|
+
upgradeLevel,
|
|
2264
|
+
candidates: categorizedCandidates,
|
|
2265
|
+
majorOnlyFixAvailable
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
function classifyUpgradeLevel(installedVersion, candidateVersion) {
|
|
2269
|
+
const installed = semver.parse(installedVersion);
|
|
2270
|
+
const candidate = semver.parse(candidateVersion);
|
|
2271
|
+
if (!installed || !candidate) return void 0;
|
|
2272
|
+
if (candidate.major > installed.major) return "major";
|
|
2273
|
+
if (candidate.minor > installed.minor) return "minor";
|
|
2274
|
+
if (candidate.patch > installed.patch || candidate.version === installed.version) {
|
|
2275
|
+
return "patch";
|
|
2276
|
+
}
|
|
2277
|
+
return void 0;
|
|
2278
|
+
}
|
|
2279
|
+
async function queryOutdatedPackages(cwd, options = {}) {
|
|
2280
|
+
const pm = options.packageManager ?? detectPackageManager(cwd);
|
|
2281
|
+
let stdout;
|
|
2282
|
+
try {
|
|
2283
|
+
const args = pm === "pnpm" ? ["outdated", "--json"] : ["outdated", "--json"];
|
|
2284
|
+
const yarnArgs = pm === "yarn" ? ["outdated", "--json"] : void 0;
|
|
2285
|
+
const finalArgs = yarnArgs ?? args;
|
|
2286
|
+
const result2 = await execa4(pm, finalArgs, {
|
|
2287
|
+
cwd,
|
|
2288
|
+
reject: false,
|
|
2289
|
+
stdio: "pipe"
|
|
2290
|
+
});
|
|
2291
|
+
if (result2.exitCode !== 0 && result2.exitCode !== 1) {
|
|
2292
|
+
const stderr = typeof result2.stderr === "string" ? result2.stderr : "";
|
|
2293
|
+
if (!result2.stdout) {
|
|
2294
|
+
throw new Error(
|
|
2295
|
+
`${pm} outdated --json failed (exit ${result2.exitCode}): ${stderr.slice(0, 500)}`
|
|
2296
|
+
);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
stdout = typeof result2.stdout === "string" ? result2.stdout : "";
|
|
2300
|
+
} catch (err) {
|
|
2301
|
+
if (err instanceof Error && !err.message.includes("outdated --json failed")) {
|
|
2302
|
+
throw new Error(`Failed to query outdated packages: ${err.message}`);
|
|
2303
|
+
}
|
|
2304
|
+
throw err;
|
|
2305
|
+
}
|
|
2306
|
+
if (!stdout.trim()) {
|
|
2307
|
+
return /* @__PURE__ */ new Map();
|
|
2308
|
+
}
|
|
2309
|
+
const rawEntries = {};
|
|
2310
|
+
if (pm === "yarn") {
|
|
2311
|
+
for (const line of stdout.split("\n")) {
|
|
2312
|
+
const trimmed = line.trim();
|
|
2313
|
+
if (!trimmed) continue;
|
|
2314
|
+
try {
|
|
2315
|
+
const parsed = JSON.parse(trimmed);
|
|
2316
|
+
if (parsed.type === "table" && parsed.data) {
|
|
2317
|
+
const tableData = parsed.data;
|
|
2318
|
+
const head = tableData.head ?? [];
|
|
2319
|
+
const currentIdx = head.indexOf("Current");
|
|
2320
|
+
const wantedIdx = head.indexOf("Wanted");
|
|
2321
|
+
const latestIdx = head.indexOf("Latest");
|
|
2322
|
+
for (const row of tableData.body ?? []) {
|
|
2323
|
+
const name = row[0];
|
|
2324
|
+
if (!name) continue;
|
|
2325
|
+
rawEntries[name] = {
|
|
2326
|
+
current: row[currentIdx] ?? "",
|
|
2327
|
+
wanted: row[wantedIdx] ?? "",
|
|
2328
|
+
latest: row[latestIdx] ?? "",
|
|
2329
|
+
type: row[head.indexOf("Package Type")] ?? "dependencies"
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
} catch {
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
} else {
|
|
2337
|
+
try {
|
|
2338
|
+
const parsed = JSON.parse(stdout);
|
|
2339
|
+
if (Array.isArray(parsed)) {
|
|
2340
|
+
for (const entry of parsed) {
|
|
2341
|
+
const name = entry.name ?? entry.packageName;
|
|
2342
|
+
if (!name) continue;
|
|
2343
|
+
rawEntries[name] = {
|
|
2344
|
+
current: entry.current,
|
|
2345
|
+
wanted: entry.wanted,
|
|
2346
|
+
latest: entry.latest,
|
|
2347
|
+
type: entry.dependencyType
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
} else {
|
|
2351
|
+
Object.assign(rawEntries, parsed);
|
|
2352
|
+
}
|
|
2353
|
+
} catch {
|
|
2354
|
+
throw new Error(`Could not parse ${pm} outdated --json output.`);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
let directDeps;
|
|
2358
|
+
try {
|
|
2359
|
+
const pkgRaw = JSON.parse(readFileSync7(join9(cwd, "package.json"), "utf8"));
|
|
2360
|
+
directDeps = /* @__PURE__ */ new Set([
|
|
2361
|
+
...Object.keys(pkgRaw.dependencies ?? {}),
|
|
2362
|
+
...Object.keys(pkgRaw.devDependencies ?? {})
|
|
2363
|
+
]);
|
|
2364
|
+
} catch {
|
|
2365
|
+
directDeps = /* @__PURE__ */ new Set();
|
|
2366
|
+
}
|
|
2367
|
+
const result = /* @__PURE__ */ new Map();
|
|
2368
|
+
for (const [name, entry] of Object.entries(rawEntries)) {
|
|
2369
|
+
const currentVersion = entry.current ?? "";
|
|
2370
|
+
const wantedVersion = entry.wanted ?? currentVersion;
|
|
2371
|
+
const latestVersion = entry.latest ?? currentVersion;
|
|
2372
|
+
if (!currentVersion || !latestVersion) continue;
|
|
2373
|
+
const dependencyScope = directDeps.has(name) ? "direct" : "transitive";
|
|
2374
|
+
if (!options.includeTransitive && dependencyScope === "transitive") continue;
|
|
2375
|
+
const isMajorBump = semver.valid(currentVersion) !== null && semver.valid(latestVersion) !== null && semver.major(latestVersion) > semver.major(currentVersion);
|
|
2376
|
+
result.set(name, {
|
|
2377
|
+
currentVersion,
|
|
2378
|
+
wantedVersion,
|
|
2379
|
+
latestVersion,
|
|
2380
|
+
isMajorBump,
|
|
2381
|
+
dependencyScope
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
return result;
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
// src/remediation/evaluate-package.ts
|
|
2388
|
+
async function evaluatePackage(packageName, options = {}) {
|
|
2389
|
+
const version = options.version?.trim();
|
|
2390
|
+
const enrich = options.enrichIntelligence !== false;
|
|
2391
|
+
let versions = [];
|
|
2392
|
+
let latestVersion;
|
|
2393
|
+
try {
|
|
2394
|
+
versions = await fetchPackageVersions(packageName);
|
|
2395
|
+
if (versions.length > 0) {
|
|
2396
|
+
const valid = versions.filter((v) => semver2.valid(v)).sort((a, b) => semver2.compare(a, b));
|
|
2397
|
+
latestVersion = valid[valid.length - 1];
|
|
2398
|
+
}
|
|
2399
|
+
} catch {
|
|
2400
|
+
}
|
|
2401
|
+
let rawVulns = [];
|
|
2402
|
+
try {
|
|
2403
|
+
rawVulns = await queryOsvPackage(packageName, version);
|
|
2404
|
+
} catch {
|
|
2405
|
+
rawVulns = [];
|
|
2406
|
+
}
|
|
2407
|
+
let kevFeed = null;
|
|
2408
|
+
if (enrich) {
|
|
2409
|
+
try {
|
|
2410
|
+
kevFeed = await fetchCisaKevFeed();
|
|
2411
|
+
} catch {
|
|
2412
|
+
kevFeed = null;
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
const entries = [];
|
|
2416
|
+
for (const raw of rawVulns) {
|
|
2417
|
+
const cveDetails = parseOsvVuln(raw);
|
|
2418
|
+
const affected = cveDetails.affectedPackages.find(
|
|
2419
|
+
(p) => p.name.toLowerCase() === packageName.toLowerCase()
|
|
2420
|
+
);
|
|
2421
|
+
if (version && affected?.vulnerableRange) {
|
|
2422
|
+
const cleanVer = semver2.clean(version) || version;
|
|
2423
|
+
if (semver2.valid(cleanVer) && !semver2.satisfies(cleanVer, affected.vulnerableRange)) {
|
|
2424
|
+
continue;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
let inCisaKev = false;
|
|
2428
|
+
let epssScore;
|
|
2429
|
+
let epssPercentile;
|
|
2430
|
+
if (enrich && cveDetails.id.startsWith("CVE-")) {
|
|
2431
|
+
if (kevFeed) {
|
|
2432
|
+
inCisaKev = Boolean(findKevEntry(kevFeed, cveDetails.id));
|
|
2433
|
+
}
|
|
2434
|
+
try {
|
|
2435
|
+
const epss = await fetchEpss(cveDetails.id);
|
|
2436
|
+
if (epss) {
|
|
2437
|
+
epssScore = Number.parseFloat(epss.epss);
|
|
2438
|
+
epssPercentile = Number.parseFloat(epss.percentile);
|
|
2439
|
+
}
|
|
2440
|
+
} catch {
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
let safeUpgradeVersion;
|
|
2444
|
+
if (affected?.firstPatchedVersion) {
|
|
2445
|
+
try {
|
|
2446
|
+
const safe = await findSafeUpgradeVersion(
|
|
2447
|
+
packageName,
|
|
2448
|
+
version || "0.0.0",
|
|
2449
|
+
affected.firstPatchedVersion
|
|
2450
|
+
);
|
|
2451
|
+
safeUpgradeVersion = safe ?? affected.firstPatchedVersion;
|
|
2452
|
+
} catch {
|
|
2453
|
+
safeUpgradeVersion = affected.firstPatchedVersion;
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
entries.push({
|
|
2457
|
+
cveId: cveDetails.id,
|
|
2458
|
+
severity: cveDetails.severity,
|
|
2459
|
+
summary: cveDetails.summary,
|
|
2460
|
+
vulnerableRange: affected?.vulnerableRange,
|
|
2461
|
+
firstPatchedVersion: affected?.firstPatchedVersion,
|
|
2462
|
+
safeUpgradeVersion,
|
|
2463
|
+
epssScore,
|
|
2464
|
+
epssPercentile,
|
|
2465
|
+
inCisaKev,
|
|
2466
|
+
references: cveDetails.references
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
const isVulnerable = entries.length > 0;
|
|
2470
|
+
let verdict = "safe";
|
|
2471
|
+
if (isVulnerable) {
|
|
2472
|
+
const hasKevOrHighEpss = entries.some((e) => e.inCisaKev || e.epssScore && e.epssScore > 0.5);
|
|
2473
|
+
const hasHighOrCritical = entries.some(
|
|
2474
|
+
(e) => e.severity === "CRITICAL" || e.severity === "HIGH"
|
|
2475
|
+
);
|
|
2476
|
+
if (hasKevOrHighEpss) {
|
|
2477
|
+
verdict = "actively-exploited";
|
|
2478
|
+
} else if (hasHighOrCritical) {
|
|
2479
|
+
verdict = "vulnerable";
|
|
2480
|
+
} else {
|
|
2481
|
+
verdict = "caution";
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
let recommendedVersion;
|
|
2485
|
+
if (isVulnerable) {
|
|
2486
|
+
const upgradeCandidates = entries.map((e) => e.safeUpgradeVersion).filter((v) => Boolean(v && semver2.valid(v))).sort((a, b) => semver2.compare(a, b));
|
|
2487
|
+
recommendedVersion = upgradeCandidates[upgradeCandidates.length - 1] || latestVersion;
|
|
2488
|
+
} else {
|
|
2489
|
+
recommendedVersion = version || latestVersion;
|
|
2490
|
+
}
|
|
2491
|
+
const summary = isVulnerable ? `Package "${packageName}${version ? `@${version}` : ""}" has ${entries.length} known vulnerability(ies) (verdict: ${verdict}). Recommended safe version: ${recommendedVersion || "none"}.` : `Package "${packageName}${version ? `@${version}` : ""}" has no known vulnerabilities. Safe to install.`;
|
|
2492
|
+
return {
|
|
2493
|
+
schemaVersion: "1.0",
|
|
2494
|
+
packageName,
|
|
2495
|
+
evaluatedVersion: version,
|
|
2496
|
+
isVulnerable,
|
|
2497
|
+
verdict,
|
|
2498
|
+
summary,
|
|
2499
|
+
vulnerabilities: entries,
|
|
2500
|
+
recommendedVersion,
|
|
2501
|
+
latestVersion,
|
|
2502
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
// src/api/patches/reconcile.ts
|
|
2507
|
+
async function reconcilePatchArtifacts(options = {}) {
|
|
2508
|
+
const cwd = options.cwd ?? process.cwd();
|
|
2509
|
+
const pm = options.packageManager ?? detectPackageManager(cwd);
|
|
2510
|
+
const summaries = await listPatchArtifacts(options);
|
|
2511
|
+
const results = [];
|
|
2512
|
+
for (const summary of summaries) {
|
|
2513
|
+
const inspection = await inspectPatchArtifact(summary.patchFilePath, options);
|
|
2514
|
+
let packageName = inspection.packageName;
|
|
2515
|
+
let vulnerableVersion = inspection.vulnerableVersion;
|
|
2516
|
+
const cveId = inspection.cveId;
|
|
2517
|
+
if (!packageName && summary.patchFileName) {
|
|
2518
|
+
const basename = summary.patchFileName.replace(/\.patch$/, "");
|
|
2519
|
+
const plusIdx = basename.lastIndexOf("+");
|
|
2520
|
+
if (plusIdx > 0) {
|
|
2521
|
+
let rawPkg = basename.slice(0, plusIdx);
|
|
2522
|
+
vulnerableVersion = basename.slice(plusIdx + 1);
|
|
2523
|
+
if (rawPkg.includes("+")) {
|
|
2524
|
+
rawPkg = "@" + rawPkg.replace("+", "/");
|
|
2525
|
+
}
|
|
2526
|
+
packageName = rawPkg;
|
|
2527
|
+
} else {
|
|
2528
|
+
packageName = basename;
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
if (!packageName) {
|
|
2532
|
+
results.push({
|
|
2533
|
+
patchFilePath: summary.patchFilePath,
|
|
2534
|
+
packageName: "unknown",
|
|
2535
|
+
cveId,
|
|
2536
|
+
status: "patch-retained",
|
|
2537
|
+
reason: "Cannot determine package name from patch artifact."
|
|
2538
|
+
});
|
|
2539
|
+
continue;
|
|
2540
|
+
}
|
|
2541
|
+
let safeUpgradeVersion;
|
|
2542
|
+
try {
|
|
2543
|
+
const evaluation = await evaluatePackage(packageName, {
|
|
2544
|
+
version: vulnerableVersion,
|
|
2545
|
+
enrichIntelligence: false
|
|
2546
|
+
});
|
|
2547
|
+
if (cveId) {
|
|
2548
|
+
const vuln = evaluation.vulnerabilities.find(
|
|
2549
|
+
(v) => v.cveId.toLowerCase() === cveId.toLowerCase()
|
|
2550
|
+
);
|
|
2551
|
+
if (vuln?.safeUpgradeVersion) {
|
|
2552
|
+
safeUpgradeVersion = vuln.safeUpgradeVersion;
|
|
2553
|
+
}
|
|
2554
|
+
} else {
|
|
2555
|
+
safeUpgradeVersion = evaluation.vulnerabilities.find(
|
|
2556
|
+
(v) => v.safeUpgradeVersion
|
|
2557
|
+
)?.safeUpgradeVersion;
|
|
2558
|
+
}
|
|
2559
|
+
} catch {
|
|
2560
|
+
}
|
|
2561
|
+
if (!safeUpgradeVersion) {
|
|
2562
|
+
results.push({
|
|
2563
|
+
patchFilePath: summary.patchFilePath,
|
|
2564
|
+
packageName,
|
|
2565
|
+
cveId,
|
|
2566
|
+
installedVersion: vulnerableVersion,
|
|
2567
|
+
status: "patch-retained",
|
|
2568
|
+
reason: "No safe upstream release exists yet; patch remains necessary."
|
|
2569
|
+
});
|
|
2570
|
+
continue;
|
|
2571
|
+
}
|
|
2572
|
+
if (!options.autoApply) {
|
|
2573
|
+
results.push({
|
|
2574
|
+
patchFilePath: summary.patchFilePath,
|
|
2575
|
+
packageName,
|
|
2576
|
+
cveId,
|
|
2577
|
+
installedVersion: vulnerableVersion,
|
|
2578
|
+
safeUpstreamVersion: safeUpgradeVersion,
|
|
2579
|
+
status: "safe-version-available",
|
|
2580
|
+
reason: `Safe upstream version ${safeUpgradeVersion} is available. Re-run with autoApply: true or --auto-apply to upgrade.`
|
|
2581
|
+
});
|
|
2582
|
+
continue;
|
|
2583
|
+
}
|
|
2584
|
+
try {
|
|
2585
|
+
const pkgJsonPath = join10(cwd, "package.json");
|
|
2586
|
+
if (existsSync8(pkgJsonPath)) {
|
|
2587
|
+
const pkgJson = JSON.parse(await readFile3(pkgJsonPath, "utf8"));
|
|
2588
|
+
let updated = false;
|
|
2589
|
+
const depSections = [
|
|
2590
|
+
"dependencies",
|
|
2591
|
+
"devDependencies",
|
|
2592
|
+
"peerDependencies",
|
|
2593
|
+
"optionalDependencies"
|
|
2594
|
+
];
|
|
2595
|
+
for (const depSection of depSections) {
|
|
2596
|
+
if (pkgJson[depSection] && pkgJson[depSection][packageName]) {
|
|
2597
|
+
pkgJson[depSection][packageName] = `^${safeUpgradeVersion}`;
|
|
2598
|
+
updated = true;
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
if (pkgJson.pnpm?.patchedDependencies) {
|
|
2602
|
+
for (const key of Object.keys(pkgJson.pnpm.patchedDependencies)) {
|
|
2603
|
+
if (key.startsWith(`${packageName}@`)) {
|
|
2604
|
+
delete pkgJson.pnpm.patchedDependencies[key];
|
|
2605
|
+
updated = true;
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
if (updated) {
|
|
2610
|
+
await writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
await rm(summary.patchFilePath, { force: true });
|
|
2614
|
+
if (inspection.manifestFilePath) {
|
|
2615
|
+
await rm(inspection.manifestFilePath, { force: true });
|
|
2616
|
+
}
|
|
2617
|
+
const installCmd = resolveInstallCommand(pm);
|
|
2618
|
+
await execa5(installCmd[0], installCmd.slice(1), { cwd, reject: false });
|
|
2619
|
+
if (options.runTests) {
|
|
2620
|
+
const testCmd = resolveTestCommand(pm);
|
|
2621
|
+
await execa5(testCmd[0], testCmd.slice(1), { cwd, reject: false });
|
|
2622
|
+
}
|
|
2623
|
+
results.push({
|
|
2624
|
+
patchFilePath: summary.patchFilePath,
|
|
2625
|
+
packageName,
|
|
2626
|
+
cveId,
|
|
2627
|
+
installedVersion: vulnerableVersion,
|
|
2628
|
+
safeUpstreamVersion: safeUpgradeVersion,
|
|
2629
|
+
status: "upgraded",
|
|
2630
|
+
reason: `Successfully upgraded to ${safeUpgradeVersion} and retired patch artifact.`
|
|
2631
|
+
});
|
|
2632
|
+
} catch (err) {
|
|
2633
|
+
results.push({
|
|
2634
|
+
patchFilePath: summary.patchFilePath,
|
|
2635
|
+
packageName,
|
|
2636
|
+
cveId,
|
|
2637
|
+
installedVersion: vulnerableVersion,
|
|
2638
|
+
safeUpstreamVersion: safeUpgradeVersion,
|
|
2639
|
+
status: "error",
|
|
2640
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
2641
|
+
});
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
const reconciledCount = results.filter((r) => r.status === "upgraded").length;
|
|
2645
|
+
const retainedCount = results.filter(
|
|
2646
|
+
(r) => r.status === "patch-retained" || r.status === "safe-version-available"
|
|
2647
|
+
).length;
|
|
2648
|
+
return {
|
|
2649
|
+
reconciled: results,
|
|
2650
|
+
reconciledCount,
|
|
2651
|
+
retainedCount
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
// src/platform/idempotency.ts
|
|
2656
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "fs";
|
|
2657
|
+
import { join as join11 } from "path";
|
|
2658
|
+
var DEFAULT_INDEX = {
|
|
2659
|
+
schemaVersion: "1.0",
|
|
2660
|
+
entries: {}
|
|
2661
|
+
};
|
|
2662
|
+
function indexFilePath(cwd) {
|
|
2663
|
+
return join11(cwd, ".autoremediator", "state", "idempotency.json");
|
|
2664
|
+
}
|
|
2665
|
+
function entryKey(idempotencyKey, cveId) {
|
|
2666
|
+
return `${idempotencyKey}::${cveId.toUpperCase()}`;
|
|
2667
|
+
}
|
|
2668
|
+
function loadIndex(cwd) {
|
|
2669
|
+
const filePath = indexFilePath(cwd);
|
|
2670
|
+
if (!existsSync9(filePath)) return DEFAULT_INDEX;
|
|
2671
|
+
try {
|
|
2672
|
+
const parsed = JSON.parse(readFileSync8(filePath, "utf8"));
|
|
2673
|
+
if (parsed && parsed.schemaVersion === "1.0" && parsed.entries) {
|
|
2674
|
+
return parsed;
|
|
2675
|
+
}
|
|
2676
|
+
return DEFAULT_INDEX;
|
|
2677
|
+
} catch {
|
|
2678
|
+
return DEFAULT_INDEX;
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
function saveIndex(cwd, index) {
|
|
2682
|
+
const filePath = indexFilePath(cwd);
|
|
2683
|
+
mkdirSync2(join11(cwd, ".autoremediator", "state"), { recursive: true });
|
|
2684
|
+
writeFileSync2(filePath, JSON.stringify(index, null, 2) + "\n", "utf8");
|
|
2685
|
+
}
|
|
2686
|
+
function readIdempotentReport(cwd, idempotencyKey, cveId) {
|
|
2687
|
+
const index = loadIndex(cwd);
|
|
2688
|
+
const key = entryKey(idempotencyKey, cveId);
|
|
2689
|
+
return index.entries[key]?.report;
|
|
2690
|
+
}
|
|
2691
|
+
function storeIdempotentReport(cwd, idempotencyKey, cveId, report) {
|
|
2692
|
+
const index = loadIndex(cwd);
|
|
2693
|
+
const key = entryKey(idempotencyKey, cveId);
|
|
2694
|
+
index.entries[key] = {
|
|
2695
|
+
key: idempotencyKey,
|
|
2696
|
+
cveId: cveId.toUpperCase(),
|
|
2697
|
+
report,
|
|
2698
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2699
|
+
};
|
|
2700
|
+
saveIndex(cwd, index);
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
// src/remediation/pipeline.ts
|
|
2704
|
+
import { generateText as generateText2 } from "ai";
|
|
2705
|
+
|
|
2706
|
+
// src/remediation/local/run.ts
|
|
2707
|
+
import semver6 from "semver";
|
|
2708
|
+
|
|
2709
|
+
// src/intelligence/sources/github-advisory.ts
|
|
2710
|
+
var GITHUB_API_BASE = "https://api.github.com/advisories";
|
|
2711
|
+
async function fetchGhAdvisories(cveId) {
|
|
2712
|
+
const token = getGitHubToken();
|
|
2713
|
+
const headers = {
|
|
2714
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
2715
|
+
"User-Agent": "autoremediator"
|
|
2716
|
+
};
|
|
2717
|
+
if (token) {
|
|
2718
|
+
headers["Authorization"] = `token ${token}`;
|
|
2719
|
+
}
|
|
2720
|
+
const normalized = cveId.trim();
|
|
2721
|
+
if (normalized.toUpperCase().startsWith("GHSA-")) {
|
|
2722
|
+
const url2 = `${GITHUB_API_BASE}/${encodeURIComponent(normalized)}`;
|
|
2723
|
+
try {
|
|
2724
|
+
const res = await httpClient({ url: url2, headers });
|
|
2725
|
+
if (!res.ok) {
|
|
2726
|
+
if (res.status === 404) return [];
|
|
2727
|
+
throw new Error(`HTTP ${res.status}: ${res.text}`);
|
|
2728
|
+
}
|
|
2729
|
+
return res.data ? [res.data] : [];
|
|
2730
|
+
} catch (err) {
|
|
2731
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
2732
|
+
console.warn(
|
|
2733
|
+
`[autoremediator] GitHub Advisory API error for ${cveId}: ${errorMsg} \u2014 skipping.`
|
|
2734
|
+
);
|
|
2735
|
+
return [];
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
const url = new URL(GITHUB_API_BASE);
|
|
2739
|
+
url.searchParams.set("cve_id", cveId);
|
|
2740
|
+
url.searchParams.set("ecosystem", "npm");
|
|
2741
|
+
url.searchParams.set("type", "reviewed");
|
|
2742
|
+
url.searchParams.set("per_page", "10");
|
|
1995
2743
|
try {
|
|
1996
2744
|
const res = await httpClient({
|
|
1997
2745
|
url: url.toString(),
|
|
@@ -2095,9 +2843,9 @@ async function enrichWithNvd(details) {
|
|
|
2095
2843
|
}
|
|
2096
2844
|
|
|
2097
2845
|
// src/intelligence/sources/snapshot.ts
|
|
2098
|
-
import { readFileSync as
|
|
2846
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
2099
2847
|
function loadIntelligenceSnapshot(filePath) {
|
|
2100
|
-
const raw =
|
|
2848
|
+
const raw = readFileSync9(filePath, "utf8");
|
|
2101
2849
|
const parsed = JSON.parse(raw);
|
|
2102
2850
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2103
2851
|
throw new Error(`Intelligence snapshot at ${filePath} must be a JSON object keyed by CVE ID.`);
|
|
@@ -2109,8 +2857,8 @@ function lookupSnapshotCve(snapshot, cveId) {
|
|
|
2109
2857
|
}
|
|
2110
2858
|
|
|
2111
2859
|
// src/remediation/tools/check-reachability.ts
|
|
2112
|
-
import { existsSync as
|
|
2113
|
-
import { isAbsolute as isAbsolute2, join as
|
|
2860
|
+
import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync10, statSync as statSync2 } from "fs";
|
|
2861
|
+
import { isAbsolute as isAbsolute2, join as join12, resolve as resolve3 } from "path";
|
|
2114
2862
|
import { parseSync } from "oxc-parser";
|
|
2115
2863
|
import { z as z2 } from "zod";
|
|
2116
2864
|
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"]);
|
|
@@ -2126,16 +2874,16 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
2126
2874
|
]);
|
|
2127
2875
|
function loadTsconfigPaths(cwd) {
|
|
2128
2876
|
for (const configName of ["tsconfig.json", "jsconfig.json"]) {
|
|
2129
|
-
const fullPath =
|
|
2130
|
-
if (!
|
|
2877
|
+
const fullPath = join12(cwd, configName);
|
|
2878
|
+
if (!existsSync10(fullPath)) continue;
|
|
2131
2879
|
try {
|
|
2132
|
-
const raw =
|
|
2880
|
+
const raw = readFileSync10(fullPath, "utf8");
|
|
2133
2881
|
const cleaned = raw.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "");
|
|
2134
2882
|
const parsed = JSON.parse(cleaned);
|
|
2135
2883
|
const compilerOptions = parsed.compilerOptions;
|
|
2136
2884
|
if (compilerOptions?.paths) {
|
|
2137
2885
|
return {
|
|
2138
|
-
baseUrl: compilerOptions.baseUrl ?
|
|
2886
|
+
baseUrl: compilerOptions.baseUrl ? resolve3(cwd, compilerOptions.baseUrl) : cwd,
|
|
2139
2887
|
paths: compilerOptions.paths
|
|
2140
2888
|
};
|
|
2141
2889
|
}
|
|
@@ -2150,14 +2898,14 @@ function resolveAliasedPath(importSource, config) {
|
|
|
2150
2898
|
}
|
|
2151
2899
|
for (const [pattern, replacements] of Object.entries(config.paths)) {
|
|
2152
2900
|
if (pattern === importSource && replacements[0]) {
|
|
2153
|
-
return
|
|
2901
|
+
return resolve3(config.baseUrl, replacements[0]);
|
|
2154
2902
|
}
|
|
2155
2903
|
if (pattern.endsWith("/*")) {
|
|
2156
2904
|
const prefix = pattern.slice(0, -2);
|
|
2157
2905
|
if (importSource.startsWith(prefix)) {
|
|
2158
2906
|
const suffix = importSource.slice(prefix.length);
|
|
2159
2907
|
const replacement = replacements[0]?.slice(0, -2) ?? "";
|
|
2160
|
-
return
|
|
2908
|
+
return resolve3(config.baseUrl, replacement + suffix);
|
|
2161
2909
|
}
|
|
2162
2910
|
}
|
|
2163
2911
|
}
|
|
@@ -2166,16 +2914,16 @@ function resolveAliasedPath(importSource, config) {
|
|
|
2166
2914
|
function* walkSourceFiles(dir) {
|
|
2167
2915
|
let entries;
|
|
2168
2916
|
try {
|
|
2169
|
-
entries =
|
|
2917
|
+
entries = readdirSync2(dir);
|
|
2170
2918
|
} catch {
|
|
2171
2919
|
return;
|
|
2172
2920
|
}
|
|
2173
2921
|
for (const entry of entries) {
|
|
2174
2922
|
if (IGNORED_DIRS.has(entry)) continue;
|
|
2175
|
-
const full =
|
|
2923
|
+
const full = join12(dir, entry);
|
|
2176
2924
|
let stat2;
|
|
2177
2925
|
try {
|
|
2178
|
-
stat2 =
|
|
2926
|
+
stat2 = statSync2(full);
|
|
2179
2927
|
} catch {
|
|
2180
2928
|
continue;
|
|
2181
2929
|
}
|
|
@@ -2213,7 +2961,31 @@ function walkAst(node, visitor) {
|
|
|
2213
2961
|
}
|
|
2214
2962
|
}
|
|
2215
2963
|
}
|
|
2216
|
-
function
|
|
2964
|
+
function toRootPackageName(importSource) {
|
|
2965
|
+
if (importSource.startsWith("@")) {
|
|
2966
|
+
const parts = importSource.split("/");
|
|
2967
|
+
return parts.slice(0, 2).join("/");
|
|
2968
|
+
}
|
|
2969
|
+
return importSource.split("/")[0] ?? importSource;
|
|
2970
|
+
}
|
|
2971
|
+
function findTransitiveDependencyPath(cwd, targetPackage, importedDirectPackages) {
|
|
2972
|
+
for (const directPkg of importedDirectPackages) {
|
|
2973
|
+
const manifestPath = join12(cwd, "node_modules", directPkg, "package.json");
|
|
2974
|
+
if (!existsSync10(manifestPath)) continue;
|
|
2975
|
+
try {
|
|
2976
|
+
const manifest = JSON.parse(readFileSync10(manifestPath, "utf8"));
|
|
2977
|
+
if (manifest.dependencies && manifest.dependencies[targetPackage]) {
|
|
2978
|
+
return {
|
|
2979
|
+
directDependency: directPkg,
|
|
2980
|
+
version: manifest.dependencies[targetPackage]
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
} catch {
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
return null;
|
|
2987
|
+
}
|
|
2988
|
+
function extractPackageReferences(filename, content, packageName, tsconfig, outImportedDirectPackages) {
|
|
2217
2989
|
const results = [];
|
|
2218
2990
|
let parseResult;
|
|
2219
2991
|
try {
|
|
@@ -2225,6 +2997,9 @@ function extractPackageReferences(filename, content, packageName, tsconfig) {
|
|
|
2225
2997
|
walkAst(parseResult.program, (node) => {
|
|
2226
2998
|
if (node.type === "ImportDeclaration") {
|
|
2227
2999
|
const source = node.source;
|
|
3000
|
+
if (source?.value && !source.value.startsWith(".") && !isAbsolute2(source.value)) {
|
|
3001
|
+
outImportedDirectPackages?.add(toRootPackageName(source.value));
|
|
3002
|
+
}
|
|
2228
3003
|
if (source?.value && matchesPackage(source.value, packageName, tsconfig)) {
|
|
2229
3004
|
const symbols = [];
|
|
2230
3005
|
const importedIdentifiers = /* @__PURE__ */ new Map();
|
|
@@ -2259,6 +3034,9 @@ function extractPackageReferences(filename, content, packageName, tsconfig) {
|
|
|
2259
3034
|
}
|
|
2260
3035
|
if (node.type === "ImportExpression") {
|
|
2261
3036
|
const source = node.source;
|
|
3037
|
+
if (source?.value && !source.value.startsWith(".") && !isAbsolute2(source.value)) {
|
|
3038
|
+
outImportedDirectPackages?.add(toRootPackageName(source.value));
|
|
3039
|
+
}
|
|
2262
3040
|
if (source?.value && matchesPackage(source.value, packageName, tsconfig)) {
|
|
2263
3041
|
results.push({
|
|
2264
3042
|
matchType: "dynamic-import",
|
|
@@ -2270,6 +3048,9 @@ function extractPackageReferences(filename, content, packageName, tsconfig) {
|
|
|
2270
3048
|
}
|
|
2271
3049
|
if (node.type === "CallExpression" && node.callee?.name === "require" && Array.isArray(node.arguments) && node.arguments.length > 0) {
|
|
2272
3050
|
const firstArg = node.arguments[0];
|
|
3051
|
+
if (firstArg?.value && typeof firstArg.value === "string" && !firstArg.value.startsWith(".") && !isAbsolute2(firstArg.value)) {
|
|
3052
|
+
outImportedDirectPackages?.add(toRootPackageName(firstArg.value));
|
|
3053
|
+
}
|
|
2273
3054
|
if (firstArg?.value && typeof firstArg.value === "string" && matchesPackage(firstArg.value, packageName, tsconfig)) {
|
|
2274
3055
|
results.push({
|
|
2275
3056
|
matchType: "require",
|
|
@@ -2281,6 +3062,9 @@ function extractPackageReferences(filename, content, packageName, tsconfig) {
|
|
|
2281
3062
|
}
|
|
2282
3063
|
if (node.type === "ExportNamedDeclaration" || node.type === "ExportAllDeclaration") {
|
|
2283
3064
|
const source = node.source;
|
|
3065
|
+
if (source?.value && !source.value.startsWith(".") && !isAbsolute2(source.value)) {
|
|
3066
|
+
outImportedDirectPackages?.add(toRootPackageName(source.value));
|
|
3067
|
+
}
|
|
2284
3068
|
if (source?.value && matchesPackage(source.value, packageName, tsconfig)) {
|
|
2285
3069
|
const symbols = [];
|
|
2286
3070
|
if (node.type === "ExportAllDeclaration") {
|
|
@@ -2357,15 +3141,22 @@ function assessPackageReachability(cwd, packageName, targetSymbol) {
|
|
|
2357
3141
|
let hasReExport = false;
|
|
2358
3142
|
let totalInvocations = 0;
|
|
2359
3143
|
let hasPackageImport = false;
|
|
3144
|
+
const importedDirectPackages = /* @__PURE__ */ new Set();
|
|
2360
3145
|
for (const filePath of walkSourceFiles(cwd)) {
|
|
2361
3146
|
scanned++;
|
|
2362
3147
|
let content;
|
|
2363
3148
|
try {
|
|
2364
|
-
content =
|
|
3149
|
+
content = readFileSync10(filePath, "utf8");
|
|
2365
3150
|
} catch {
|
|
2366
3151
|
continue;
|
|
2367
3152
|
}
|
|
2368
|
-
const refs = extractPackageReferences(
|
|
3153
|
+
const refs = extractPackageReferences(
|
|
3154
|
+
filePath,
|
|
3155
|
+
content,
|
|
3156
|
+
packageName,
|
|
3157
|
+
tsconfig,
|
|
3158
|
+
importedDirectPackages
|
|
3159
|
+
);
|
|
2369
3160
|
if (refs.length > 0) {
|
|
2370
3161
|
hasPackageImport = true;
|
|
2371
3162
|
}
|
|
@@ -2408,6 +3199,16 @@ function assessPackageReachability(cwd, packageName, targetSymbol) {
|
|
|
2408
3199
|
};
|
|
2409
3200
|
}
|
|
2410
3201
|
if (!hasPackageImport) {
|
|
3202
|
+
const transitivePath = findTransitiveDependencyPath(cwd, packageName, importedDirectPackages);
|
|
3203
|
+
if (transitivePath) {
|
|
3204
|
+
return {
|
|
3205
|
+
packageName,
|
|
3206
|
+
status: "reachable",
|
|
3207
|
+
reason: `Package '${packageName}' is a transitive dependency of imported package '${transitivePath.directDependency}'`,
|
|
3208
|
+
reachabilityBasis: "transitive-path-found",
|
|
3209
|
+
evidence: []
|
|
3210
|
+
};
|
|
3211
|
+
}
|
|
2411
3212
|
return {
|
|
2412
3213
|
packageName,
|
|
2413
3214
|
status: "not-reachable",
|
|
@@ -2552,254 +3353,48 @@ async function runSecOpsPreflight(normalizedId, cveDetails, opts) {
|
|
|
2552
3353
|
opts.slaPolicy
|
|
2553
3354
|
);
|
|
2554
3355
|
if (breach) {
|
|
2555
|
-
slaBreaches = [breach];
|
|
2556
|
-
}
|
|
2557
|
-
}
|
|
2558
|
-
return { suppressed: false, exploitSignalTriggered, slaBreaches };
|
|
2559
|
-
}
|
|
2560
|
-
|
|
2561
|
-
// src/remediation/local/sbom.ts
|
|
2562
|
-
function buildSbom(packages, vulnerableNames, results) {
|
|
2563
|
-
const statusByPackage = /* @__PURE__ */ new Map();
|
|
2564
|
-
for (const result of results) {
|
|
2565
|
-
if (!vulnerableNames.has(result.packageName)) continue;
|
|
2566
|
-
if (result.suppressedBy) {
|
|
2567
|
-
statusByPackage.set(result.packageName, "suppressed");
|
|
2568
|
-
} else if (!result.applied && result.strategy === "none") {
|
|
2569
|
-
statusByPackage.set(result.packageName, "skipped");
|
|
2570
|
-
} else if (result.applied) {
|
|
2571
|
-
statusByPackage.set(result.packageName, "patched");
|
|
2572
|
-
} else {
|
|
2573
|
-
statusByPackage.set(result.packageName, "unpatched");
|
|
2574
|
-
}
|
|
2575
|
-
}
|
|
2576
|
-
return packages.map((pkg) => {
|
|
2577
|
-
const isVulnerable = vulnerableNames.has(pkg.name);
|
|
2578
|
-
const entry = {
|
|
2579
|
-
name: pkg.name,
|
|
2580
|
-
version: pkg.version,
|
|
2581
|
-
scope: pkg.type === "direct" ? "direct" : "transitive"
|
|
2582
|
-
};
|
|
2583
|
-
if (isVulnerable) {
|
|
2584
|
-
entry.status = statusByPackage.get(pkg.name) ?? "unpatched";
|
|
2585
|
-
}
|
|
2586
|
-
return entry;
|
|
2587
|
-
});
|
|
2588
|
-
}
|
|
2589
|
-
|
|
2590
|
-
// src/intelligence/sources/registry.ts
|
|
2591
|
-
import { readFileSync as readFileSync8 } from "fs";
|
|
2592
|
-
import { join as join10 } from "path";
|
|
2593
|
-
import { execa as execa4 } from "execa";
|
|
2594
|
-
import semver from "semver";
|
|
2595
|
-
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
2596
|
-
async function fetchPackageVersions(packageName) {
|
|
2597
|
-
const url = `${NPM_REGISTRY}/${encodeURIComponent(packageName)}`;
|
|
2598
|
-
try {
|
|
2599
|
-
const res = await httpClient({ url });
|
|
2600
|
-
if (res.status === 404) return [];
|
|
2601
|
-
if (!res.ok) {
|
|
2602
|
-
throw new Error(`npm registry error ${res.status} for "${packageName}": ${res.text}`);
|
|
2603
|
-
}
|
|
2604
|
-
const data = res.data;
|
|
2605
|
-
return Object.keys(data.versions);
|
|
2606
|
-
} catch (err) {
|
|
2607
|
-
if (err instanceof Error && err.message.includes("registry error")) {
|
|
2608
|
-
throw err;
|
|
2609
|
-
}
|
|
2610
|
-
return [];
|
|
2611
|
-
}
|
|
2612
|
-
}
|
|
2613
|
-
async function findSafeUpgradeVersion(packageName, installedVersion, firstPatchedVersion, vulnerableRange) {
|
|
2614
|
-
const resolution = await resolveSafeUpgradeVersion(
|
|
2615
|
-
packageName,
|
|
2616
|
-
installedVersion,
|
|
2617
|
-
firstPatchedVersion,
|
|
2618
|
-
vulnerableRange
|
|
2619
|
-
);
|
|
2620
|
-
return resolution.safeVersion;
|
|
2621
|
-
}
|
|
2622
|
-
async function resolveSafeUpgradeVersion(packageName, installedVersion, firstPatchedVersion, vulnerableRange) {
|
|
2623
|
-
const versions = await fetchPackageVersions(packageName);
|
|
2624
|
-
if (!versions.length) {
|
|
2625
|
-
return {
|
|
2626
|
-
candidates: {},
|
|
2627
|
-
majorOnlyFixAvailable: false
|
|
2628
|
-
};
|
|
2629
|
-
}
|
|
2630
|
-
const installed = semver.parse(installedVersion);
|
|
2631
|
-
const candidates = versions.filter((v) => semver.valid(v) && semver.gte(v, firstPatchedVersion)).filter((v) => {
|
|
2632
|
-
if (!vulnerableRange) return true;
|
|
2633
|
-
try {
|
|
2634
|
-
return !semver.satisfies(v, vulnerableRange, { includePrerelease: false });
|
|
2635
|
-
} catch {
|
|
2636
|
-
return true;
|
|
2637
|
-
}
|
|
2638
|
-
}).sort(semver.compare);
|
|
2639
|
-
if (!candidates.length) {
|
|
2640
|
-
return {
|
|
2641
|
-
candidates: {},
|
|
2642
|
-
majorOnlyFixAvailable: false
|
|
2643
|
-
};
|
|
2644
|
-
}
|
|
2645
|
-
const categorizedCandidates = {};
|
|
2646
|
-
for (const candidate of candidates) {
|
|
2647
|
-
const level = classifyUpgradeLevel(installedVersion, candidate);
|
|
2648
|
-
if (!level) continue;
|
|
2649
|
-
if (!categorizedCandidates[level]) {
|
|
2650
|
-
categorizedCandidates[level] = candidate;
|
|
2651
|
-
}
|
|
2652
|
-
}
|
|
2653
|
-
const safeVersion = categorizedCandidates.patch ?? categorizedCandidates.minor ?? categorizedCandidates.major;
|
|
2654
|
-
if (!safeVersion) {
|
|
2655
|
-
return {
|
|
2656
|
-
candidates: categorizedCandidates,
|
|
2657
|
-
majorOnlyFixAvailable: false
|
|
2658
|
-
};
|
|
2659
|
-
}
|
|
2660
|
-
const upgradeLevel = classifyUpgradeLevel(installedVersion, safeVersion);
|
|
2661
|
-
const majorOnlyFixAvailable = !categorizedCandidates.patch && !categorizedCandidates.minor && Boolean(categorizedCandidates.major);
|
|
2662
|
-
if (!installed || !upgradeLevel) {
|
|
2663
|
-
return {
|
|
2664
|
-
safeVersion,
|
|
2665
|
-
upgradeLevel,
|
|
2666
|
-
candidates: categorizedCandidates,
|
|
2667
|
-
majorOnlyFixAvailable
|
|
2668
|
-
};
|
|
2669
|
-
}
|
|
2670
|
-
return {
|
|
2671
|
-
safeVersion,
|
|
2672
|
-
upgradeLevel,
|
|
2673
|
-
candidates: categorizedCandidates,
|
|
2674
|
-
majorOnlyFixAvailable
|
|
2675
|
-
};
|
|
2676
|
-
}
|
|
2677
|
-
function classifyUpgradeLevel(installedVersion, candidateVersion) {
|
|
2678
|
-
const installed = semver.parse(installedVersion);
|
|
2679
|
-
const candidate = semver.parse(candidateVersion);
|
|
2680
|
-
if (!installed || !candidate) return void 0;
|
|
2681
|
-
if (candidate.major > installed.major) return "major";
|
|
2682
|
-
if (candidate.minor > installed.minor) return "minor";
|
|
2683
|
-
if (candidate.patch > installed.patch || candidate.version === installed.version) {
|
|
2684
|
-
return "patch";
|
|
2685
|
-
}
|
|
2686
|
-
return void 0;
|
|
2687
|
-
}
|
|
2688
|
-
async function queryOutdatedPackages(cwd, options = {}) {
|
|
2689
|
-
const pm = options.packageManager ?? detectPackageManager(cwd);
|
|
2690
|
-
let stdout;
|
|
2691
|
-
try {
|
|
2692
|
-
const args = pm === "pnpm" ? ["outdated", "--json"] : ["outdated", "--json"];
|
|
2693
|
-
const yarnArgs = pm === "yarn" ? ["outdated", "--json"] : void 0;
|
|
2694
|
-
const finalArgs = yarnArgs ?? args;
|
|
2695
|
-
const result2 = await execa4(pm, finalArgs, {
|
|
2696
|
-
cwd,
|
|
2697
|
-
reject: false,
|
|
2698
|
-
stdio: "pipe"
|
|
2699
|
-
});
|
|
2700
|
-
if (result2.exitCode !== 0 && result2.exitCode !== 1) {
|
|
2701
|
-
const stderr = typeof result2.stderr === "string" ? result2.stderr : "";
|
|
2702
|
-
if (!result2.stdout) {
|
|
2703
|
-
throw new Error(
|
|
2704
|
-
`${pm} outdated --json failed (exit ${result2.exitCode}): ${stderr.slice(0, 500)}`
|
|
2705
|
-
);
|
|
2706
|
-
}
|
|
2707
|
-
}
|
|
2708
|
-
stdout = typeof result2.stdout === "string" ? result2.stdout : "";
|
|
2709
|
-
} catch (err) {
|
|
2710
|
-
if (err instanceof Error && !err.message.includes("outdated --json failed")) {
|
|
2711
|
-
throw new Error(`Failed to query outdated packages: ${err.message}`);
|
|
2712
|
-
}
|
|
2713
|
-
throw err;
|
|
2714
|
-
}
|
|
2715
|
-
if (!stdout.trim()) {
|
|
2716
|
-
return /* @__PURE__ */ new Map();
|
|
2717
|
-
}
|
|
2718
|
-
const rawEntries = {};
|
|
2719
|
-
if (pm === "yarn") {
|
|
2720
|
-
for (const line of stdout.split("\n")) {
|
|
2721
|
-
const trimmed = line.trim();
|
|
2722
|
-
if (!trimmed) continue;
|
|
2723
|
-
try {
|
|
2724
|
-
const parsed = JSON.parse(trimmed);
|
|
2725
|
-
if (parsed.type === "table" && parsed.data) {
|
|
2726
|
-
const tableData = parsed.data;
|
|
2727
|
-
const head = tableData.head ?? [];
|
|
2728
|
-
const currentIdx = head.indexOf("Current");
|
|
2729
|
-
const wantedIdx = head.indexOf("Wanted");
|
|
2730
|
-
const latestIdx = head.indexOf("Latest");
|
|
2731
|
-
for (const row of tableData.body ?? []) {
|
|
2732
|
-
const name = row[0];
|
|
2733
|
-
if (!name) continue;
|
|
2734
|
-
rawEntries[name] = {
|
|
2735
|
-
current: row[currentIdx] ?? "",
|
|
2736
|
-
wanted: row[wantedIdx] ?? "",
|
|
2737
|
-
latest: row[latestIdx] ?? "",
|
|
2738
|
-
type: row[head.indexOf("Package Type")] ?? "dependencies"
|
|
2739
|
-
};
|
|
2740
|
-
}
|
|
2741
|
-
}
|
|
2742
|
-
} catch {
|
|
2743
|
-
}
|
|
2744
|
-
}
|
|
2745
|
-
} else {
|
|
2746
|
-
try {
|
|
2747
|
-
const parsed = JSON.parse(stdout);
|
|
2748
|
-
if (Array.isArray(parsed)) {
|
|
2749
|
-
for (const entry of parsed) {
|
|
2750
|
-
const name = entry.name ?? entry.packageName;
|
|
2751
|
-
if (!name) continue;
|
|
2752
|
-
rawEntries[name] = {
|
|
2753
|
-
current: entry.current,
|
|
2754
|
-
wanted: entry.wanted,
|
|
2755
|
-
latest: entry.latest,
|
|
2756
|
-
type: entry.dependencyType
|
|
2757
|
-
};
|
|
2758
|
-
}
|
|
2759
|
-
} else {
|
|
2760
|
-
Object.assign(rawEntries, parsed);
|
|
2761
|
-
}
|
|
2762
|
-
} catch {
|
|
2763
|
-
throw new Error(`Could not parse ${pm} outdated --json output.`);
|
|
3356
|
+
slaBreaches = [breach];
|
|
2764
3357
|
}
|
|
2765
3358
|
}
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
if (!options.includeTransitive && dependencyScope === "transitive") continue;
|
|
2784
|
-
const isMajorBump = semver.valid(currentVersion) !== null && semver.valid(latestVersion) !== null && semver.major(latestVersion) > semver.major(currentVersion);
|
|
2785
|
-
result.set(name, {
|
|
2786
|
-
currentVersion,
|
|
2787
|
-
wantedVersion,
|
|
2788
|
-
latestVersion,
|
|
2789
|
-
isMajorBump,
|
|
2790
|
-
dependencyScope
|
|
2791
|
-
});
|
|
3359
|
+
return { suppressed: false, exploitSignalTriggered, slaBreaches };
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
// src/remediation/local/sbom.ts
|
|
3363
|
+
function buildSbom(packages, vulnerableNames, results) {
|
|
3364
|
+
const statusByPackage = /* @__PURE__ */ new Map();
|
|
3365
|
+
for (const result of results) {
|
|
3366
|
+
if (!vulnerableNames.has(result.packageName)) continue;
|
|
3367
|
+
if (result.suppressedBy) {
|
|
3368
|
+
statusByPackage.set(result.packageName, "suppressed");
|
|
3369
|
+
} else if (!result.applied && result.strategy === "none") {
|
|
3370
|
+
statusByPackage.set(result.packageName, "skipped");
|
|
3371
|
+
} else if (result.applied) {
|
|
3372
|
+
statusByPackage.set(result.packageName, "patched");
|
|
3373
|
+
} else {
|
|
3374
|
+
statusByPackage.set(result.packageName, "unpatched");
|
|
3375
|
+
}
|
|
2792
3376
|
}
|
|
2793
|
-
return
|
|
3377
|
+
return packages.map((pkg) => {
|
|
3378
|
+
const isVulnerable = vulnerableNames.has(pkg.name);
|
|
3379
|
+
const entry = {
|
|
3380
|
+
name: pkg.name,
|
|
3381
|
+
version: pkg.version,
|
|
3382
|
+
scope: pkg.type === "direct" ? "direct" : "transitive"
|
|
3383
|
+
};
|
|
3384
|
+
if (isVulnerable) {
|
|
3385
|
+
entry.status = statusByPackage.get(pkg.name) ?? "unpatched";
|
|
3386
|
+
}
|
|
3387
|
+
return entry;
|
|
3388
|
+
});
|
|
2794
3389
|
}
|
|
2795
3390
|
|
|
2796
3391
|
// src/remediation/tools/apply-version-bump.ts
|
|
2797
3392
|
import { z as z4 } from "zod";
|
|
2798
|
-
import { join as
|
|
2799
|
-
import { readFileSync as
|
|
3393
|
+
import { join as join15 } from "path";
|
|
3394
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync4 } from "fs";
|
|
2800
3395
|
|
|
2801
3396
|
// src/platform/safe-exec.ts
|
|
2802
|
-
import { execa as
|
|
3397
|
+
import { execa as execa6 } from "execa";
|
|
2803
3398
|
var DANGEROUS_ENV_VARS = /* @__PURE__ */ new Set([
|
|
2804
3399
|
"NODE_OPTIONS",
|
|
2805
3400
|
"NODE_PATH",
|
|
@@ -2848,7 +3443,7 @@ function safeExeca(file, args = [], options = {}) {
|
|
|
2848
3443
|
options.env ?? process.env,
|
|
2849
3444
|
options.allowNodeOptions
|
|
2850
3445
|
);
|
|
2851
|
-
return
|
|
3446
|
+
return execa6(file, args, {
|
|
2852
3447
|
...options,
|
|
2853
3448
|
timeout,
|
|
2854
3449
|
env: sanitizedEnv
|
|
@@ -2856,19 +3451,19 @@ function safeExeca(file, args = [], options = {}) {
|
|
|
2856
3451
|
}
|
|
2857
3452
|
|
|
2858
3453
|
// src/remediation/tools/apply-version-bump.ts
|
|
2859
|
-
import
|
|
3454
|
+
import semver3 from "semver";
|
|
2860
3455
|
|
|
2861
3456
|
// src/platform/repo-lock.ts
|
|
2862
|
-
import { mkdir, rm } from "fs/promises";
|
|
2863
|
-
import { join as
|
|
3457
|
+
import { mkdir, rm as rm2 } from "fs/promises";
|
|
3458
|
+
import { join as join13 } from "path";
|
|
2864
3459
|
async function sleep(ms) {
|
|
2865
|
-
await new Promise((
|
|
3460
|
+
await new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
2866
3461
|
}
|
|
2867
3462
|
async function acquireRepoLock(cwd, options = {}) {
|
|
2868
3463
|
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
2869
3464
|
const retryDelayMs = options.retryDelayMs ?? 125;
|
|
2870
|
-
const lockRoot =
|
|
2871
|
-
const lockPath =
|
|
3465
|
+
const lockRoot = join13(cwd, ".autoremediator", "locks");
|
|
3466
|
+
const lockPath = join13(cwd, ".autoremediator", "locks", "remediation.lock");
|
|
2872
3467
|
const startedAt = Date.now();
|
|
2873
3468
|
await mkdir(lockRoot, { recursive: true });
|
|
2874
3469
|
while (true) {
|
|
@@ -2877,7 +3472,7 @@ async function acquireRepoLock(cwd, options = {}) {
|
|
|
2877
3472
|
return {
|
|
2878
3473
|
lockPath,
|
|
2879
3474
|
release: async () => {
|
|
2880
|
-
await
|
|
3475
|
+
await rm2(lockPath, { recursive: true, force: true });
|
|
2881
3476
|
}
|
|
2882
3477
|
};
|
|
2883
3478
|
} catch {
|
|
@@ -2897,6 +3492,380 @@ async function withRepoLock(cwd, fn, options) {
|
|
|
2897
3492
|
}
|
|
2898
3493
|
}
|
|
2899
3494
|
|
|
3495
|
+
// src/remediation/refactor/call-site-finder.ts
|
|
3496
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync11, statSync as statSync3 } from "fs";
|
|
3497
|
+
import { join as join14 } from "path";
|
|
3498
|
+
import { parseSync as parseSync2 } from "oxc-parser";
|
|
3499
|
+
var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"]);
|
|
3500
|
+
var IGNORED_DIRS2 = /* @__PURE__ */ new Set([
|
|
3501
|
+
"node_modules",
|
|
3502
|
+
".git",
|
|
3503
|
+
"dist",
|
|
3504
|
+
"build",
|
|
3505
|
+
"coverage",
|
|
3506
|
+
".next",
|
|
3507
|
+
".turbo",
|
|
3508
|
+
".pnpm-store"
|
|
3509
|
+
]);
|
|
3510
|
+
function* walkFiles(dir) {
|
|
3511
|
+
let entries;
|
|
3512
|
+
try {
|
|
3513
|
+
entries = readdirSync3(dir);
|
|
3514
|
+
} catch {
|
|
3515
|
+
return;
|
|
3516
|
+
}
|
|
3517
|
+
for (const entry of entries) {
|
|
3518
|
+
if (IGNORED_DIRS2.has(entry)) continue;
|
|
3519
|
+
const full = join14(dir, entry);
|
|
3520
|
+
let stat2;
|
|
3521
|
+
try {
|
|
3522
|
+
stat2 = statSync3(full);
|
|
3523
|
+
} catch {
|
|
3524
|
+
continue;
|
|
3525
|
+
}
|
|
3526
|
+
if (stat2.isDirectory()) {
|
|
3527
|
+
yield* walkFiles(full);
|
|
3528
|
+
} else {
|
|
3529
|
+
const ext = full.slice(full.lastIndexOf("."));
|
|
3530
|
+
if (SOURCE_EXTENSIONS2.has(ext)) {
|
|
3531
|
+
yield full;
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
function findPackageCallSites(cwd, packageName) {
|
|
3537
|
+
const callSites = [];
|
|
3538
|
+
for (const filePath of walkFiles(cwd)) {
|
|
3539
|
+
let content;
|
|
3540
|
+
try {
|
|
3541
|
+
content = readFileSync11(filePath, "utf8");
|
|
3542
|
+
} catch {
|
|
3543
|
+
continue;
|
|
3544
|
+
}
|
|
3545
|
+
if (!content.includes(packageName)) {
|
|
3546
|
+
continue;
|
|
3547
|
+
}
|
|
3548
|
+
try {
|
|
3549
|
+
const parseResult = parseSync2(filePath, content);
|
|
3550
|
+
if (!parseResult.program) continue;
|
|
3551
|
+
const program = parseResult.program;
|
|
3552
|
+
const imports = [];
|
|
3553
|
+
const importSources = [];
|
|
3554
|
+
const localNames = /* @__PURE__ */ new Set();
|
|
3555
|
+
for (const statement of program.body) {
|
|
3556
|
+
if (statement.type === "ImportDeclaration" && statement.source?.value) {
|
|
3557
|
+
const importSource = statement.source.value;
|
|
3558
|
+
if (importSource === packageName || importSource.startsWith(`${packageName}/`)) {
|
|
3559
|
+
importSources.push(importSource);
|
|
3560
|
+
for (const spec of statement.specifiers || []) {
|
|
3561
|
+
const localName = spec.local?.name || "";
|
|
3562
|
+
if (!localName) continue;
|
|
3563
|
+
localNames.add(localName);
|
|
3564
|
+
if (spec.type === "ImportDefaultSpecifier") {
|
|
3565
|
+
imports.push({
|
|
3566
|
+
localName,
|
|
3567
|
+
importedName: "default",
|
|
3568
|
+
isDefault: true,
|
|
3569
|
+
isNamespace: false
|
|
3570
|
+
});
|
|
3571
|
+
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
3572
|
+
imports.push({
|
|
3573
|
+
localName,
|
|
3574
|
+
importedName: "*",
|
|
3575
|
+
isDefault: false,
|
|
3576
|
+
isNamespace: true
|
|
3577
|
+
});
|
|
3578
|
+
} else if (spec.type === "ImportSpecifier") {
|
|
3579
|
+
imports.push({
|
|
3580
|
+
localName,
|
|
3581
|
+
importedName: spec.imported?.name || localName,
|
|
3582
|
+
isDefault: false,
|
|
3583
|
+
isNamespace: false
|
|
3584
|
+
});
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
if (imports.length > 0 || importSources.length > 0) {
|
|
3591
|
+
const lines = content.split("\n");
|
|
3592
|
+
const referencedLines = [];
|
|
3593
|
+
lines.forEach((line, index) => {
|
|
3594
|
+
for (const local of localNames) {
|
|
3595
|
+
const regex = new RegExp(`\\b${local}\\b`);
|
|
3596
|
+
if (regex.test(line)) {
|
|
3597
|
+
referencedLines.push(index + 1);
|
|
3598
|
+
break;
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
});
|
|
3602
|
+
callSites.push({
|
|
3603
|
+
filePath,
|
|
3604
|
+
sourceContent: content,
|
|
3605
|
+
importSources,
|
|
3606
|
+
imports,
|
|
3607
|
+
referencedLines
|
|
3608
|
+
});
|
|
3609
|
+
}
|
|
3610
|
+
} catch {
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
return callSites;
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
// src/remediation/refactor/refactor-agent.ts
|
|
3617
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync3 } from "fs";
|
|
3618
|
+
function parseCompilerDiagnostics(diagnosticError) {
|
|
3619
|
+
const diagnostics = [];
|
|
3620
|
+
const lines = diagnosticError.split("\n");
|
|
3621
|
+
for (const line of lines) {
|
|
3622
|
+
const trimmed = line.trim();
|
|
3623
|
+
if (!trimmed) continue;
|
|
3624
|
+
const match = trimmed.match(
|
|
3625
|
+
/(?:([^\s:(]+)[:(](\d+)[:,](\d+)\)?:?\s*(?:-\s*)?(?:error\s+)?(TS\d+)?:?\s*(.*))/i
|
|
3626
|
+
);
|
|
3627
|
+
if (match) {
|
|
3628
|
+
const [, filePath, lineStr, colStr, code, message] = match;
|
|
3629
|
+
const diag = {
|
|
3630
|
+
filePath,
|
|
3631
|
+
line: lineStr ? parseInt(lineStr, 10) : void 0,
|
|
3632
|
+
column: colStr ? parseInt(colStr, 10) : void 0,
|
|
3633
|
+
code,
|
|
3634
|
+
message: message || trimmed
|
|
3635
|
+
};
|
|
3636
|
+
const suggestionMatch = diag.message.match(/Did you mean ['"]([a-zA-Z0-9_$]+)['"]\?/i);
|
|
3637
|
+
if (suggestionMatch) {
|
|
3638
|
+
diag.suggestedSymbol = suggestionMatch[1];
|
|
3639
|
+
}
|
|
3640
|
+
const missingMatch = diag.message.match(/has no exported member ['"]([a-zA-Z0-9_$]+)['"]/i) || diag.message.match(/Property ['"]([a-zA-Z0-9_$]+)['"] does not exist/i);
|
|
3641
|
+
if (missingMatch) {
|
|
3642
|
+
diag.missingMember = missingMatch[1];
|
|
3643
|
+
}
|
|
3644
|
+
diagnostics.push(diag);
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
return diagnostics;
|
|
3648
|
+
}
|
|
3649
|
+
function revertRefactorSnapshots(snapshots) {
|
|
3650
|
+
for (const [filePath, content] of Object.entries(snapshots)) {
|
|
3651
|
+
try {
|
|
3652
|
+
writeFileSync3(filePath, content, "utf8");
|
|
3653
|
+
} catch {
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
var KNOWN_MIGRATIONS = {
|
|
3658
|
+
rimraf: [
|
|
3659
|
+
{
|
|
3660
|
+
detect: (content, diag) => diag.includes("sync") && content.includes("rimraf.sync("),
|
|
3661
|
+
transform: (content) => {
|
|
3662
|
+
let res = content.replace(/import\s+rimraf\s+from\s+['"]rimraf['"];?/g, "");
|
|
3663
|
+
res = `import { rimrafSync } from "rimraf";
|
|
3664
|
+
` + res;
|
|
3665
|
+
return res.replace(/rimraf\.sync\(/g, "rimrafSync(");
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
],
|
|
3669
|
+
uuid: [
|
|
3670
|
+
{
|
|
3671
|
+
detect: (content, diag) => diag.includes("v4") && content.includes("uuid.v4("),
|
|
3672
|
+
transform: (content) => {
|
|
3673
|
+
let res = content.replace(/import\s+uuid\s+from\s+['"]uuid['"];?/g, "");
|
|
3674
|
+
res = `import { v4 as uuidv4 } from "uuid";
|
|
3675
|
+
` + res;
|
|
3676
|
+
return res.replace(/uuid\.v4\(/g, "uuidv4(");
|
|
3677
|
+
}
|
|
3678
|
+
},
|
|
3679
|
+
{
|
|
3680
|
+
// Subpath import migration: `import v4 from "uuid/v4"` -> `import { v4 } from "uuid"`
|
|
3681
|
+
detect: (content) => /from\s+['"]uuid\/v4['"]/.test(content),
|
|
3682
|
+
transform: (content) => content.replace(
|
|
3683
|
+
/import\s+([a-zA-Z0-9_$]+)\s+from\s+['"]uuid\/v4['"];?/g,
|
|
3684
|
+
`import { v4 as $1 } from "uuid";`
|
|
3685
|
+
)
|
|
3686
|
+
}
|
|
3687
|
+
],
|
|
3688
|
+
glob: [
|
|
3689
|
+
{
|
|
3690
|
+
detect: (content, diag) => diag.includes("sync") && content.includes("glob.sync("),
|
|
3691
|
+
transform: (content) => {
|
|
3692
|
+
let res = content.replace(/import\s+glob\s+from\s+['"]glob['"];?/g, "");
|
|
3693
|
+
res = `import { globSync } from "glob";
|
|
3694
|
+
` + res;
|
|
3695
|
+
return res.replace(/glob\.sync\(/g, "globSync(");
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
],
|
|
3699
|
+
mkdirp: [
|
|
3700
|
+
{
|
|
3701
|
+
detect: (content, diag) => diag.includes("sync") && content.includes("mkdirp.sync("),
|
|
3702
|
+
transform: (content) => {
|
|
3703
|
+
let res = content.replace(/import\s+mkdirp\s+from\s+['"]mkdirp['"];?/g, "");
|
|
3704
|
+
res = `import { mkdirpSync } from "mkdirp";
|
|
3705
|
+
` + res;
|
|
3706
|
+
return res.replace(/mkdirp\.sync\(/g, "mkdirpSync(");
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
]
|
|
3710
|
+
};
|
|
3711
|
+
function escapeRegex(str) {
|
|
3712
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3713
|
+
}
|
|
3714
|
+
function transformFileContent(content, packageName, diagnosticError, filePath) {
|
|
3715
|
+
let modified = false;
|
|
3716
|
+
let newContent = content;
|
|
3717
|
+
const migrations = KNOWN_MIGRATIONS[packageName];
|
|
3718
|
+
if (migrations) {
|
|
3719
|
+
for (const mig of migrations) {
|
|
3720
|
+
if (mig.detect(newContent, diagnosticError)) {
|
|
3721
|
+
const transformed = mig.transform(newContent);
|
|
3722
|
+
if (transformed !== newContent) {
|
|
3723
|
+
newContent = transformed;
|
|
3724
|
+
modified = true;
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
if (diagnosticError.includes("no default export") || diagnosticError.includes("has no exported member 'default'") || diagnosticError.includes("has no exported member default") || diagnosticError.includes("TS2614") || diagnosticError.includes("TS1192")) {
|
|
3730
|
+
const defaultImportRegex = new RegExp(
|
|
3731
|
+
`import\\s+([a-zA-Z0-9_$]+)\\s+from\\s+['"]${escapeRegex(packageName)}['"]`,
|
|
3732
|
+
"g"
|
|
3733
|
+
);
|
|
3734
|
+
if (defaultImportRegex.test(newContent)) {
|
|
3735
|
+
newContent = newContent.replace(
|
|
3736
|
+
defaultImportRegex,
|
|
3737
|
+
`import * as $1 from "${packageName}"`
|
|
3738
|
+
);
|
|
3739
|
+
modified = true;
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
if (diagnosticError.includes("can only be used with a default import") || diagnosticError.includes("only be used with a default import") || diagnosticError.includes("TS1259")) {
|
|
3743
|
+
const starImportRegex = new RegExp(
|
|
3744
|
+
`import\\s+\\*\\s+as\\s+([a-zA-Z0-9_$]+)\\s+from\\s+['"]${escapeRegex(packageName)}['"]`,
|
|
3745
|
+
"g"
|
|
3746
|
+
);
|
|
3747
|
+
if (starImportRegex.test(newContent)) {
|
|
3748
|
+
newContent = newContent.replace(
|
|
3749
|
+
starImportRegex,
|
|
3750
|
+
`import $1 from "${packageName}"`
|
|
3751
|
+
);
|
|
3752
|
+
modified = true;
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
const diagnostics = parseCompilerDiagnostics(diagnosticError);
|
|
3756
|
+
for (const diag of diagnostics) {
|
|
3757
|
+
if (diag.missingMember && diag.suggestedSymbol) {
|
|
3758
|
+
const missingMember = diag.missingMember;
|
|
3759
|
+
const suggestedSymbol = diag.suggestedSymbol;
|
|
3760
|
+
if (filePath && diag.filePath && filePath.endsWith(diag.filePath) && diag.line && diag.line > 0) {
|
|
3761
|
+
const fileLines = newContent.split("\n");
|
|
3762
|
+
const idx = diag.line - 1;
|
|
3763
|
+
if (idx >= 0 && idx < fileLines.length) {
|
|
3764
|
+
const line = fileLines[idx];
|
|
3765
|
+
const replaced = line.replace(new RegExp(`\\.${escapeRegex(missingMember)}\\b`, "g"), `.${suggestedSymbol}`).replace(new RegExp(`\\b${escapeRegex(missingMember)}\\b`, "g"), suggestedSymbol);
|
|
3766
|
+
if (replaced !== line) {
|
|
3767
|
+
fileLines[idx] = replaced;
|
|
3768
|
+
newContent = fileLines.join("\n");
|
|
3769
|
+
modified = true;
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
} else {
|
|
3773
|
+
const memberRegex = new RegExp(`\\.${escapeRegex(missingMember)}\\b`, "g");
|
|
3774
|
+
if (memberRegex.test(newContent)) {
|
|
3775
|
+
newContent = newContent.replace(memberRegex, `.${suggestedSymbol}`);
|
|
3776
|
+
modified = true;
|
|
3777
|
+
}
|
|
3778
|
+
const importPattern = new RegExp(
|
|
3779
|
+
`import\\s*\\{[^}]*\\b${escapeRegex(missingMember)}\\b[^}]*\\}\\s*from\\s*['"]${escapeRegex(packageName)}['"]`,
|
|
3780
|
+
"g"
|
|
3781
|
+
);
|
|
3782
|
+
if (importPattern.test(newContent)) {
|
|
3783
|
+
newContent = newContent.replace(
|
|
3784
|
+
importPattern,
|
|
3785
|
+
(match) => match.replace(new RegExp(`\\b${escapeRegex(missingMember)}\\b`, "g"), suggestedSymbol)
|
|
3786
|
+
);
|
|
3787
|
+
modified = true;
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
const lineDiags = diagnostics.filter(
|
|
3793
|
+
(d) => Boolean(filePath && d.filePath && filePath.endsWith(d.filePath) && typeof d.line === "number" && d.line > 0)
|
|
3794
|
+
).sort((a, b) => b.line - a.line);
|
|
3795
|
+
const processedLines = /* @__PURE__ */ new Set();
|
|
3796
|
+
for (const diag of lineDiags) {
|
|
3797
|
+
if (processedLines.has(diag.line)) continue;
|
|
3798
|
+
processedLines.add(diag.line);
|
|
3799
|
+
const fileLines = newContent.split("\n");
|
|
3800
|
+
const targetLineIdx = diag.line - 1;
|
|
3801
|
+
if (targetLineIdx >= 0 && targetLineIdx < fileLines.length) {
|
|
3802
|
+
const prevLine = targetLineIdx > 0 ? fileLines[targetLineIdx - 1] : "";
|
|
3803
|
+
if (!prevLine?.includes("@ts-expect-error") && !fileLines[targetLineIdx]?.includes("@ts-expect-error")) {
|
|
3804
|
+
const indent = fileLines[targetLineIdx]?.match(/^\s*/)?.[0] || "";
|
|
3805
|
+
fileLines.splice(
|
|
3806
|
+
targetLineIdx,
|
|
3807
|
+
0,
|
|
3808
|
+
`${indent}// @ts-expect-error [autoremediator-refactor]: breaking change in ${packageName}`
|
|
3809
|
+
);
|
|
3810
|
+
newContent = fileLines.join("\n");
|
|
3811
|
+
modified = true;
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
return { modified, newContent };
|
|
3816
|
+
}
|
|
3817
|
+
async function refactorConsumerCallSites(params) {
|
|
3818
|
+
const { packageName, diagnosticError, callSites } = params;
|
|
3819
|
+
if (callSites.length === 0) {
|
|
3820
|
+
return {
|
|
3821
|
+
applied: false,
|
|
3822
|
+
refactoredFiles: [],
|
|
3823
|
+
snapshots: {},
|
|
3824
|
+
error: `No consumer call sites found for "${packageName}".`
|
|
3825
|
+
};
|
|
3826
|
+
}
|
|
3827
|
+
const snapshots = {};
|
|
3828
|
+
const refactoredFiles = [];
|
|
3829
|
+
try {
|
|
3830
|
+
for (const site of callSites) {
|
|
3831
|
+
const originalContent = site.sourceContent || readFileSync12(site.filePath, "utf8");
|
|
3832
|
+
snapshots[site.filePath] = originalContent;
|
|
3833
|
+
const { modified, newContent } = transformFileContent(
|
|
3834
|
+
originalContent,
|
|
3835
|
+
packageName,
|
|
3836
|
+
diagnosticError,
|
|
3837
|
+
site.filePath
|
|
3838
|
+
);
|
|
3839
|
+
if (modified && newContent !== originalContent) {
|
|
3840
|
+
writeFileSync3(site.filePath, newContent, "utf8");
|
|
3841
|
+
refactoredFiles.push(site.filePath);
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
if (refactoredFiles.length === 0) {
|
|
3845
|
+
return {
|
|
3846
|
+
applied: false,
|
|
3847
|
+
refactoredFiles: [],
|
|
3848
|
+
snapshots,
|
|
3849
|
+
error: "No automated codemod pattern matched the diagnostic error."
|
|
3850
|
+
};
|
|
3851
|
+
}
|
|
3852
|
+
return {
|
|
3853
|
+
applied: true,
|
|
3854
|
+
refactoredFiles,
|
|
3855
|
+
snapshots
|
|
3856
|
+
};
|
|
3857
|
+
} catch (err) {
|
|
3858
|
+
revertRefactorSnapshots(snapshots);
|
|
3859
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3860
|
+
return {
|
|
3861
|
+
applied: false,
|
|
3862
|
+
refactoredFiles: [],
|
|
3863
|
+
snapshots: {},
|
|
3864
|
+
error: `Failed to apply breaking change refactoring: ${message}`
|
|
3865
|
+
};
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
|
|
2900
3869
|
// src/remediation/tools/apply-version-bump.ts
|
|
2901
3870
|
var applyVersionBumpTool = defineTool({
|
|
2902
3871
|
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.",
|
|
@@ -2909,10 +3878,12 @@ var applyVersionBumpTool = defineTool({
|
|
|
2909
3878
|
dryRun: z4.boolean().default(false).describe("If true, report changes but do not write"),
|
|
2910
3879
|
policy: z4.string().optional().describe("Optional path to .autoremediator policy file"),
|
|
2911
3880
|
runTests: z4.boolean().default(false).describe("If true, run test validation after applying the fix"),
|
|
3881
|
+
typecheck: z4.boolean().default(false).describe("If true, run typecheck validation after applying the fix"),
|
|
2912
3882
|
installMode: z4.enum(["standard", "prefer-offline", "deterministic"]).optional(),
|
|
2913
3883
|
installPreferOffline: z4.boolean().optional(),
|
|
2914
3884
|
enforceFrozenLockfile: z4.boolean().optional(),
|
|
2915
|
-
workspace: z4.string().optional()
|
|
3885
|
+
workspace: z4.string().optional(),
|
|
3886
|
+
breakingChangeRefactor: z4.boolean().optional().describe("If true, attempt agentic call-site refactoring when validation fails after major upgrade")
|
|
2916
3887
|
}),
|
|
2917
3888
|
execute: async ({
|
|
2918
3889
|
cwd,
|
|
@@ -2923,13 +3894,15 @@ var applyVersionBumpTool = defineTool({
|
|
|
2923
3894
|
dryRun,
|
|
2924
3895
|
policy,
|
|
2925
3896
|
runTests,
|
|
3897
|
+
typecheck,
|
|
2926
3898
|
installMode,
|
|
2927
3899
|
installPreferOffline,
|
|
2928
3900
|
enforceFrozenLockfile,
|
|
2929
|
-
workspace
|
|
3901
|
+
workspace,
|
|
3902
|
+
breakingChangeRefactor
|
|
2930
3903
|
}) => {
|
|
2931
3904
|
const pm = packageManager ?? detectPackageManager(cwd);
|
|
2932
|
-
|
|
3905
|
+
let pkgPath = join15(cwd, "package.json");
|
|
2933
3906
|
const loadedPolicy = loadPolicy(cwd, policy);
|
|
2934
3907
|
const commandConstraints = {
|
|
2935
3908
|
...loadedPolicy.constraints,
|
|
@@ -2939,9 +3912,6 @@ var applyVersionBumpTool = defineTool({
|
|
|
2939
3912
|
workspace: workspace ?? loadedPolicy.constraints?.workspace
|
|
2940
3913
|
};
|
|
2941
3914
|
const yarnMajor = pm === "yarn" ? await getYarnMajorVersion(cwd) : void 0;
|
|
2942
|
-
const installCommand = resolveInstallCommand(pm, commandConstraints, yarnMajor);
|
|
2943
|
-
const testCommand = resolveTestCommand(pm, commandConstraints);
|
|
2944
|
-
const dedupeCommand = resolveDedupeCommand(pm, commandConstraints);
|
|
2945
3915
|
if (!isPackageAllowed(loadedPolicy, packageName)) {
|
|
2946
3916
|
return {
|
|
2947
3917
|
packageName,
|
|
@@ -2954,7 +3924,7 @@ var applyVersionBumpTool = defineTool({
|
|
|
2954
3924
|
message: `Policy blocked changes for package "${packageName}".`
|
|
2955
3925
|
};
|
|
2956
3926
|
}
|
|
2957
|
-
const isMajorBump =
|
|
3927
|
+
const isMajorBump = semver3.valid(fromVersion) && semver3.valid(toVersion) && semver3.major(toVersion) > semver3.major(fromVersion);
|
|
2958
3928
|
if (isMajorBump && !loadedPolicy.allowMajorBumps) {
|
|
2959
3929
|
return {
|
|
2960
3930
|
packageName,
|
|
@@ -2967,9 +3937,10 @@ var applyVersionBumpTool = defineTool({
|
|
|
2967
3937
|
message: `Policy blocked major bump for "${packageName}" (${fromVersion} -> ${toVersion}).`
|
|
2968
3938
|
};
|
|
2969
3939
|
}
|
|
3940
|
+
pkgPath = join15(cwd, "package.json");
|
|
2970
3941
|
let pkgJson;
|
|
2971
3942
|
try {
|
|
2972
|
-
pkgJson = JSON.parse(
|
|
3943
|
+
pkgJson = JSON.parse(readFileSync13(pkgPath, "utf8"));
|
|
2973
3944
|
} catch {
|
|
2974
3945
|
return {
|
|
2975
3946
|
packageName,
|
|
@@ -2981,9 +3952,20 @@ var applyVersionBumpTool = defineTool({
|
|
|
2981
3952
|
message: `Could not read package.json at "${pkgPath}".`
|
|
2982
3953
|
};
|
|
2983
3954
|
}
|
|
2984
|
-
|
|
3955
|
+
let depField = ["dependencies", "devDependencies", "peerDependencies"].find(
|
|
2985
3956
|
(f) => pkgJson[f]?.[packageName] !== void 0
|
|
2986
3957
|
);
|
|
3958
|
+
if (!depField) {
|
|
3959
|
+
const declaringWorkspace = findDeclaringWorkspacePackage(cwd, packageName, workspace);
|
|
3960
|
+
if (declaringWorkspace) {
|
|
3961
|
+
pkgPath = declaringWorkspace.manifestPath;
|
|
3962
|
+
pkgJson = declaringWorkspace.manifest;
|
|
3963
|
+
depField = ["dependencies", "devDependencies", "peerDependencies"].find(
|
|
3964
|
+
(f) => pkgJson[f]?.[packageName] !== void 0
|
|
3965
|
+
);
|
|
3966
|
+
commandConstraints.workspace = workspace ?? declaringWorkspace.name;
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
2987
3969
|
if (!depField) {
|
|
2988
3970
|
return {
|
|
2989
3971
|
packageName,
|
|
@@ -2999,9 +3981,14 @@ var applyVersionBumpTool = defineTool({
|
|
|
2999
3981
|
const prefixMatch = currentRange.match(/^([~^]?)/);
|
|
3000
3982
|
const prefix = prefixMatch?.[1] ?? "";
|
|
3001
3983
|
const newRange = `${prefix}${toVersion}`;
|
|
3984
|
+
const effectiveInstallCommand = resolveInstallCommand(pm, commandConstraints, yarnMajor);
|
|
3985
|
+
const effectiveTestCommand = resolveTestCommand(pm, commandConstraints);
|
|
3986
|
+
const effectiveTypecheckCommand = resolveTypecheckCommand(pm, commandConstraints, cwd);
|
|
3987
|
+
const effectiveDedupeCommand = resolveDedupeCommand(pm, commandConstraints);
|
|
3988
|
+
const shouldTypecheck = typecheck || Boolean(commandConstraints.typecheck);
|
|
3002
3989
|
if (dryRun) {
|
|
3003
|
-
const installCmd =
|
|
3004
|
-
const testCmd =
|
|
3990
|
+
const installCmd = effectiveInstallCommand.join(" ");
|
|
3991
|
+
const testCmd = effectiveTestCommand.join(" ");
|
|
3005
3992
|
return {
|
|
3006
3993
|
packageName,
|
|
3007
3994
|
strategy: "version-bump",
|
|
@@ -3009,21 +3996,21 @@ var applyVersionBumpTool = defineTool({
|
|
|
3009
3996
|
toVersion,
|
|
3010
3997
|
applied: false,
|
|
3011
3998
|
dryRun: true,
|
|
3012
|
-
message: `[DRY RUN] Would update ${depField}.${packageName}: "${currentRange}" -> "${newRange}", then run ${installCmd}${runTests ? ` and ${testCmd}` : ""}${
|
|
3999
|
+
message: `[DRY RUN] Would update ${depField}.${packageName}: "${currentRange}" -> "${newRange}", then run ${installCmd}${runTests ? ` and ${testCmd}` : ""}${effectiveDedupeCommand.length > 0 ? ` and ${effectiveDedupeCommand.join(" ")} (best-effort)` : ""}.`
|
|
3013
4000
|
};
|
|
3014
4001
|
}
|
|
3015
4002
|
return withRepoLock(cwd, async () => {
|
|
3016
4003
|
pkgJson[depField][packageName] = newRange;
|
|
3017
|
-
|
|
4004
|
+
writeFileSync4(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
3018
4005
|
try {
|
|
3019
|
-
const [installCmd, ...installArgs] =
|
|
4006
|
+
const [installCmd, ...installArgs] = effectiveInstallCommand;
|
|
3020
4007
|
await safeExeca(installCmd, installArgs, {
|
|
3021
4008
|
cwd,
|
|
3022
4009
|
stdio: "pipe"
|
|
3023
4010
|
});
|
|
3024
4011
|
} catch (err) {
|
|
3025
4012
|
pkgJson[depField][packageName] = currentRange;
|
|
3026
|
-
|
|
4013
|
+
writeFileSync4(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
3027
4014
|
const message = err instanceof Error ? err.message : String(err);
|
|
3028
4015
|
return {
|
|
3029
4016
|
packageName,
|
|
@@ -3032,52 +4019,116 @@ var applyVersionBumpTool = defineTool({
|
|
|
3032
4019
|
toVersion,
|
|
3033
4020
|
applied: false,
|
|
3034
4021
|
dryRun: false,
|
|
3035
|
-
unresolvedReason: "install-failed",
|
|
3036
|
-
message: `${
|
|
4022
|
+
unresolvedReason: "install-failed",
|
|
4023
|
+
message: `${effectiveInstallCommand.join(" ")} failed after updating "${packageName}" to ${toVersion}. Reverted. Error: ${message}`
|
|
4024
|
+
};
|
|
4025
|
+
}
|
|
4026
|
+
const shouldAttemptRefactor = Boolean(
|
|
4027
|
+
breakingChangeRefactor ?? loadedPolicy.constraints?.enableBreakingChangeRefactor
|
|
4028
|
+
);
|
|
4029
|
+
async function runValidationSteps() {
|
|
4030
|
+
if (runTests) {
|
|
4031
|
+
try {
|
|
4032
|
+
const [testCmd, ...testArgs] = effectiveTestCommand;
|
|
4033
|
+
await safeExeca(testCmd, testArgs, { cwd, stdio: "pipe" });
|
|
4034
|
+
} catch (err) {
|
|
4035
|
+
const execErr = err;
|
|
4036
|
+
const fullOutput = [execErr.stdout, execErr.stderr, execErr.message].filter(Boolean).join("\n");
|
|
4037
|
+
return {
|
|
4038
|
+
passed: false,
|
|
4039
|
+
error: fullOutput || (err instanceof Error ? err.message : String(err))
|
|
4040
|
+
};
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
if (shouldTypecheck && effectiveTypecheckCommand.length > 0) {
|
|
4044
|
+
try {
|
|
4045
|
+
const [tcCmd, ...tcArgs] = effectiveTypecheckCommand;
|
|
4046
|
+
await safeExeca(tcCmd, tcArgs, { cwd, stdio: "pipe" });
|
|
4047
|
+
} catch (err) {
|
|
4048
|
+
const execErr = err;
|
|
4049
|
+
const fullOutput = [execErr.stdout, execErr.stderr, execErr.message].filter(Boolean).join("\n");
|
|
4050
|
+
return {
|
|
4051
|
+
passed: false,
|
|
4052
|
+
isTypecheck: true,
|
|
4053
|
+
error: fullOutput || (err instanceof Error ? err.message : String(err))
|
|
4054
|
+
};
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
return { passed: true };
|
|
4058
|
+
}
|
|
4059
|
+
let validationResult = await runValidationSteps();
|
|
4060
|
+
let refactoredFilesList;
|
|
4061
|
+
if (!validationResult.passed && shouldAttemptRefactor) {
|
|
4062
|
+
const callSites = findPackageCallSites(cwd, packageName);
|
|
4063
|
+
if (callSites.length > 0) {
|
|
4064
|
+
const refactorAttempt = await refactorConsumerCallSites({
|
|
4065
|
+
cwd,
|
|
4066
|
+
packageName,
|
|
4067
|
+
fromVersion,
|
|
4068
|
+
toVersion,
|
|
4069
|
+
diagnosticError: validationResult.error || "",
|
|
4070
|
+
callSites
|
|
4071
|
+
});
|
|
4072
|
+
if (refactorAttempt.applied) {
|
|
4073
|
+
const postRefactorValidation = await runValidationSteps();
|
|
4074
|
+
if (postRefactorValidation.passed) {
|
|
4075
|
+
validationResult = { passed: true };
|
|
4076
|
+
refactoredFilesList = refactorAttempt.refactoredFiles;
|
|
4077
|
+
} else {
|
|
4078
|
+
revertRefactorSnapshots(refactorAttempt.snapshots);
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
if (!validationResult.passed) {
|
|
4084
|
+
pkgJson[depField][packageName] = currentRange;
|
|
4085
|
+
writeFileSync4(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
4086
|
+
try {
|
|
4087
|
+
const [rollbackCmd, ...rollbackArgs] = effectiveInstallCommand;
|
|
4088
|
+
await safeExeca(rollbackCmd, rollbackArgs, {
|
|
4089
|
+
cwd,
|
|
4090
|
+
stdio: "pipe"
|
|
4091
|
+
});
|
|
4092
|
+
} catch {
|
|
4093
|
+
}
|
|
4094
|
+
const failureMessage = validationResult.error || "Unknown validation error";
|
|
4095
|
+
const prefix2 = validationResult.isTypecheck ? `TypeCheck validation failed (${effectiveTypecheckCommand.join(" ")})` : `${effectiveTestCommand.join(" ")} failed`;
|
|
4096
|
+
return {
|
|
4097
|
+
packageName,
|
|
4098
|
+
strategy: "version-bump",
|
|
4099
|
+
fromVersion,
|
|
4100
|
+
toVersion,
|
|
4101
|
+
applied: false,
|
|
4102
|
+
dryRun: false,
|
|
4103
|
+
unresolvedReason: shouldAttemptRefactor ? "breaking-change-unresolved" : "validation-failed",
|
|
4104
|
+
message: `${prefix2} after upgrading "${packageName}" to ${toVersion}. Rolled back to ${currentRange}. Error: ${failureMessage}`
|
|
4105
|
+
};
|
|
4106
|
+
}
|
|
4107
|
+
if (refactoredFilesList && refactoredFilesList.length > 0) {
|
|
4108
|
+
return {
|
|
4109
|
+
packageName,
|
|
4110
|
+
strategy: "breaking-change-refactor",
|
|
4111
|
+
fromVersion,
|
|
4112
|
+
toVersion,
|
|
4113
|
+
applied: true,
|
|
4114
|
+
dryRun: false,
|
|
4115
|
+
refactoredFiles: refactoredFilesList,
|
|
4116
|
+
message: `Upgraded "${packageName}" from ${fromVersion} to ${toVersion} and refactored ${refactoredFilesList.length} call-site file(s) across breaking API changes.`
|
|
3037
4117
|
};
|
|
3038
4118
|
}
|
|
3039
|
-
|
|
4119
|
+
let dedupeNote = "";
|
|
4120
|
+
if (effectiveDedupeCommand.length > 0) {
|
|
3040
4121
|
try {
|
|
3041
|
-
const [
|
|
3042
|
-
await safeExeca(
|
|
4122
|
+
const [dedupeCmd, ...dedupeArgs] = effectiveDedupeCommand;
|
|
4123
|
+
await safeExeca(dedupeCmd, dedupeArgs, {
|
|
3043
4124
|
cwd,
|
|
3044
4125
|
stdio: "pipe"
|
|
3045
4126
|
});
|
|
3046
4127
|
} catch (err) {
|
|
3047
|
-
pkgJson[depField][packageName] = currentRange;
|
|
3048
|
-
writeFileSync3(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
3049
|
-
try {
|
|
3050
|
-
const [rollbackCmd, ...rollbackArgs] = installCommand;
|
|
3051
|
-
await safeExeca(rollbackCmd, rollbackArgs, {
|
|
3052
|
-
cwd,
|
|
3053
|
-
stdio: "pipe"
|
|
3054
|
-
});
|
|
3055
|
-
} catch {
|
|
3056
|
-
}
|
|
3057
4128
|
const message = err instanceof Error ? err.message : String(err);
|
|
3058
|
-
|
|
3059
|
-
packageName,
|
|
3060
|
-
strategy: "version-bump",
|
|
3061
|
-
fromVersion,
|
|
3062
|
-
toVersion,
|
|
3063
|
-
applied: false,
|
|
3064
|
-
dryRun: false,
|
|
3065
|
-
unresolvedReason: "validation-failed",
|
|
3066
|
-
message: `${testCommand.join(" ")} failed after upgrading "${packageName}" to ${toVersion}. Rolled back to ${currentRange}. Error: ${message}`
|
|
3067
|
-
};
|
|
4129
|
+
dedupeNote = ` Dedupe warning: ${effectiveDedupeCommand.join(" ")} failed (${message}).`;
|
|
3068
4130
|
}
|
|
3069
4131
|
}
|
|
3070
|
-
let dedupeNote = "";
|
|
3071
|
-
try {
|
|
3072
|
-
const [dedupeCmd, ...dedupeArgs] = dedupeCommand;
|
|
3073
|
-
await safeExeca(dedupeCmd, dedupeArgs, {
|
|
3074
|
-
cwd,
|
|
3075
|
-
stdio: "pipe"
|
|
3076
|
-
});
|
|
3077
|
-
} catch (err) {
|
|
3078
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
3079
|
-
dedupeNote = ` Dedupe warning: ${dedupeCommand.join(" ")} failed (${message}).`;
|
|
3080
|
-
}
|
|
3081
4132
|
return {
|
|
3082
4133
|
packageName,
|
|
3083
4134
|
strategy: "version-bump",
|
|
@@ -3085,7 +4136,7 @@ var applyVersionBumpTool = defineTool({
|
|
|
3085
4136
|
toVersion,
|
|
3086
4137
|
applied: true,
|
|
3087
4138
|
dryRun: false,
|
|
3088
|
-
message: `Successfully upgraded "${packageName}" from ${fromVersion} to ${toVersion}, ran ${
|
|
4139
|
+
message: `Successfully upgraded "${packageName}" from ${fromVersion} to ${toVersion} in ${depField}, then ran ${effectiveInstallCommand.join(" ")}${runTests ? ` and passed ${effectiveTestCommand.join(" ")}` : ""}.${dedupeNote}`
|
|
3089
4140
|
};
|
|
3090
4141
|
});
|
|
3091
4142
|
}
|
|
@@ -3093,9 +4144,9 @@ var applyVersionBumpTool = defineTool({
|
|
|
3093
4144
|
|
|
3094
4145
|
// src/remediation/tools/apply-package-override/index.ts
|
|
3095
4146
|
import { z as z5 } from "zod";
|
|
3096
|
-
import { join as
|
|
3097
|
-
import { existsSync as
|
|
3098
|
-
import
|
|
4147
|
+
import { join as join16 } from "path";
|
|
4148
|
+
import { existsSync as existsSync12, readFileSync as readFileSync14, writeFileSync as writeFileSync5 } from "fs";
|
|
4149
|
+
import semver4 from "semver";
|
|
3099
4150
|
|
|
3100
4151
|
// src/remediation/tools/apply-package-override/helpers.ts
|
|
3101
4152
|
async function collectDependencyTrace(cwd, pm, packageName, constraints) {
|
|
@@ -3237,9 +4288,9 @@ var applyPackageOverrideTool = defineTool({
|
|
|
3237
4288
|
workspace
|
|
3238
4289
|
}) => {
|
|
3239
4290
|
const pm = packageManager ?? detectPackageManager(cwd);
|
|
3240
|
-
const pkgPath =
|
|
3241
|
-
const denoJsonPath =
|
|
3242
|
-
const isDenoNative = pm === "deno" && !
|
|
4291
|
+
const pkgPath = join16(cwd, "package.json");
|
|
4292
|
+
const denoJsonPath = join16(cwd, "deno.json");
|
|
4293
|
+
const isDenoNative = pm === "deno" && !existsSync12(pkgPath);
|
|
3243
4294
|
const loadedPolicy = loadPolicy(cwd, policy);
|
|
3244
4295
|
const commandConstraints = {
|
|
3245
4296
|
...loadedPolicy.constraints,
|
|
@@ -3265,7 +4316,7 @@ var applyPackageOverrideTool = defineTool({
|
|
|
3265
4316
|
message: `Policy blocked changes for package "${packageName}".`
|
|
3266
4317
|
};
|
|
3267
4318
|
}
|
|
3268
|
-
const isMajorBump =
|
|
4319
|
+
const isMajorBump = semver4.valid(fromVersion) && semver4.valid(toVersion) && semver4.major(toVersion) > semver4.major(fromVersion);
|
|
3269
4320
|
if (isMajorBump && !loadedPolicy.allowMajorBumps) {
|
|
3270
4321
|
return {
|
|
3271
4322
|
packageName,
|
|
@@ -3281,7 +4332,7 @@ var applyPackageOverrideTool = defineTool({
|
|
|
3281
4332
|
if (isDenoNative) {
|
|
3282
4333
|
let denoJson;
|
|
3283
4334
|
try {
|
|
3284
|
-
denoJson = JSON.parse(
|
|
4335
|
+
denoJson = JSON.parse(readFileSync14(denoJsonPath, "utf8"));
|
|
3285
4336
|
} catch {
|
|
3286
4337
|
return {
|
|
3287
4338
|
packageName,
|
|
@@ -3327,13 +4378,13 @@ var applyPackageOverrideTool = defineTool({
|
|
|
3327
4378
|
}
|
|
3328
4379
|
return withRepoLock(cwd, async () => {
|
|
3329
4380
|
setDenoJsonImportValue(denoJson, overrideSelector, toVersion);
|
|
3330
|
-
|
|
4381
|
+
writeFileSync5(denoJsonPath, JSON.stringify(denoJson, null, 2) + "\n", "utf8");
|
|
3331
4382
|
try {
|
|
3332
4383
|
const [installCmd, ...installArgs] = installCommand;
|
|
3333
4384
|
await safeExeca(installCmd, installArgs, { cwd, stdio: "pipe" });
|
|
3334
4385
|
} catch (err) {
|
|
3335
4386
|
restoreDenoJsonImportValue(denoJson, overrideSelector, existingEntry);
|
|
3336
|
-
|
|
4387
|
+
writeFileSync5(denoJsonPath, JSON.stringify(denoJson, null, 2) + "\n", "utf8");
|
|
3337
4388
|
const message = err instanceof Error ? err.message : String(err);
|
|
3338
4389
|
return {
|
|
3339
4390
|
packageName,
|
|
@@ -3352,7 +4403,7 @@ var applyPackageOverrideTool = defineTool({
|
|
|
3352
4403
|
await safeExeca(testCmd, testArgs, { cwd, stdio: "pipe" });
|
|
3353
4404
|
} catch (err) {
|
|
3354
4405
|
restoreDenoJsonImportValue(denoJson, overrideSelector, existingEntry);
|
|
3355
|
-
|
|
4406
|
+
writeFileSync5(denoJsonPath, JSON.stringify(denoJson, null, 2) + "\n", "utf8");
|
|
3356
4407
|
try {
|
|
3357
4408
|
const [rollbackCmd, ...rollbackArgs] = installCommand;
|
|
3358
4409
|
await safeExeca(rollbackCmd, rollbackArgs, { cwd, stdio: "pipe" });
|
|
@@ -3384,7 +4435,7 @@ var applyPackageOverrideTool = defineTool({
|
|
|
3384
4435
|
}
|
|
3385
4436
|
let pkgJson;
|
|
3386
4437
|
try {
|
|
3387
|
-
pkgJson = JSON.parse(
|
|
4438
|
+
pkgJson = JSON.parse(readFileSync14(pkgPath, "utf8"));
|
|
3388
4439
|
} catch {
|
|
3389
4440
|
return {
|
|
3390
4441
|
packageName,
|
|
@@ -3414,13 +4465,13 @@ var applyPackageOverrideTool = defineTool({
|
|
|
3414
4465
|
}
|
|
3415
4466
|
return withRepoLock(cwd, async () => {
|
|
3416
4467
|
setOverrideValue(pkgJson, pm, overrideSelector, toVersion);
|
|
3417
|
-
|
|
4468
|
+
writeFileSync5(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
3418
4469
|
try {
|
|
3419
4470
|
const [installCmd, ...installArgs] = installCommand;
|
|
3420
4471
|
await safeExeca(installCmd, installArgs, { cwd, stdio: "pipe" });
|
|
3421
4472
|
} catch (err) {
|
|
3422
4473
|
restoreOverrideValue(pkgJson, pm, overrideSelector, previousValue);
|
|
3423
|
-
|
|
4474
|
+
writeFileSync5(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
3424
4475
|
const message = err instanceof Error ? err.message : String(err);
|
|
3425
4476
|
return {
|
|
3426
4477
|
packageName,
|
|
@@ -3439,7 +4490,7 @@ var applyPackageOverrideTool = defineTool({
|
|
|
3439
4490
|
await safeExeca(testCmd, testArgs, { cwd, stdio: "pipe" });
|
|
3440
4491
|
} catch (err) {
|
|
3441
4492
|
restoreOverrideValue(pkgJson, pm, overrideSelector, previousValue);
|
|
3442
|
-
|
|
4493
|
+
writeFileSync5(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
3443
4494
|
try {
|
|
3444
4495
|
const [rollbackCmd, ...rollbackArgs] = installCommand;
|
|
3445
4496
|
await safeExeca(rollbackCmd, rollbackArgs, { cwd, stdio: "pipe" });
|
|
@@ -3481,6 +4532,80 @@ var applyPackageOverrideTool = defineTool({
|
|
|
3481
4532
|
}
|
|
3482
4533
|
});
|
|
3483
4534
|
|
|
4535
|
+
// src/platform/supply-chain.ts
|
|
4536
|
+
async function verifyPackageTrust(packageName, targetVersion, policy) {
|
|
4537
|
+
if (!policy || !policy.minPublishAgeHours && !policy.blockInstallScripts && !policy.requireProvenance) {
|
|
4538
|
+
return { trusted: true };
|
|
4539
|
+
}
|
|
4540
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;
|
|
4541
|
+
let packument = null;
|
|
4542
|
+
try {
|
|
4543
|
+
const res = await httpClient({
|
|
4544
|
+
url,
|
|
4545
|
+
headers: { Accept: "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8" }
|
|
4546
|
+
});
|
|
4547
|
+
if (res.ok && res.data) {
|
|
4548
|
+
packument = res.data;
|
|
4549
|
+
}
|
|
4550
|
+
} catch (err) {
|
|
4551
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4552
|
+
return {
|
|
4553
|
+
trusted: false,
|
|
4554
|
+
reason: `Could not verify supply-chain trust for "${packageName}@${targetVersion}": ${msg}`
|
|
4555
|
+
};
|
|
4556
|
+
}
|
|
4557
|
+
if (!packument) {
|
|
4558
|
+
return {
|
|
4559
|
+
trusted: false,
|
|
4560
|
+
reason: `Package packument for "${packageName}" not found on registry.`
|
|
4561
|
+
};
|
|
4562
|
+
}
|
|
4563
|
+
let publishAgeHours;
|
|
4564
|
+
if (packument.time?.[targetVersion]) {
|
|
4565
|
+
const publishedAt = new Date(packument.time[targetVersion]).getTime();
|
|
4566
|
+
publishAgeHours = Math.max(0, (Date.now() - publishedAt) / (1e3 * 60 * 60));
|
|
4567
|
+
if (policy.minPublishAgeHours && publishAgeHours < policy.minPublishAgeHours) {
|
|
4568
|
+
return {
|
|
4569
|
+
trusted: false,
|
|
4570
|
+
publishAgeHours,
|
|
4571
|
+
reason: `Target release "${packageName}@${targetVersion}" was published only ${publishAgeHours.toFixed(1)}h ago (minimum required age is ${policy.minPublishAgeHours}h).`
|
|
4572
|
+
};
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4575
|
+
const versionData = packument.versions?.[targetVersion];
|
|
4576
|
+
const scripts = versionData?.scripts ?? {};
|
|
4577
|
+
const installScriptKeys = ["preinstall", "install", "postinstall"].filter(
|
|
4578
|
+
(k) => Boolean(scripts[k])
|
|
4579
|
+
);
|
|
4580
|
+
const hasInstallScripts = installScriptKeys.length > 0;
|
|
4581
|
+
if (policy.blockInstallScripts && hasInstallScripts) {
|
|
4582
|
+
return {
|
|
4583
|
+
trusted: false,
|
|
4584
|
+
publishAgeHours,
|
|
4585
|
+
hasInstallScripts: true,
|
|
4586
|
+
reason: `Target release "${packageName}@${targetVersion}" defines risky lifecycle scripts (${installScriptKeys.join(", ")}).`
|
|
4587
|
+
};
|
|
4588
|
+
}
|
|
4589
|
+
const hasProvenance = Boolean(
|
|
4590
|
+
versionData?.dist?.attestations?.provenance || versionData?.dist?.attestations?.url
|
|
4591
|
+
);
|
|
4592
|
+
if (policy.requireProvenance && !hasProvenance) {
|
|
4593
|
+
return {
|
|
4594
|
+
trusted: false,
|
|
4595
|
+
publishAgeHours,
|
|
4596
|
+
hasInstallScripts,
|
|
4597
|
+
hasProvenance: false,
|
|
4598
|
+
reason: `Target release "${packageName}@${targetVersion}" does not have cryptographic build provenance attestations.`
|
|
4599
|
+
};
|
|
4600
|
+
}
|
|
4601
|
+
return {
|
|
4602
|
+
trusted: true,
|
|
4603
|
+
publishAgeHours,
|
|
4604
|
+
hasInstallScripts,
|
|
4605
|
+
hasProvenance
|
|
4606
|
+
};
|
|
4607
|
+
}
|
|
4608
|
+
|
|
3484
4609
|
// src/remediation/local/primary-strategy.ts
|
|
3485
4610
|
async function resolvePrimaryResult(params) {
|
|
3486
4611
|
const { vulnerable, cwd, packageManager, dryRun, policy, runTests, constraints } = params;
|
|
@@ -3551,6 +4676,26 @@ async function resolvePrimaryResult(params) {
|
|
|
3551
4676
|
}
|
|
3552
4677
|
};
|
|
3553
4678
|
}
|
|
4679
|
+
const trust2 = await verifyPackageTrust(
|
|
4680
|
+
pkg.name,
|
|
4681
|
+
safeUpgrade2.safeVersion,
|
|
4682
|
+
constraints.supplyChainPolicy
|
|
4683
|
+
);
|
|
4684
|
+
if (!trust2.trusted) {
|
|
4685
|
+
return {
|
|
4686
|
+
steps: 1,
|
|
4687
|
+
result: {
|
|
4688
|
+
packageName: pkg.name,
|
|
4689
|
+
strategy: "none",
|
|
4690
|
+
fromVersion: pkg.version,
|
|
4691
|
+
applied: false,
|
|
4692
|
+
dryRun,
|
|
4693
|
+
unresolvedReason: "policy-blocked",
|
|
4694
|
+
vulnerableRange: vulnerable.affected.vulnerableRange,
|
|
4695
|
+
message: `Supply chain trust gate blocked "${pkg.name}@${safeUpgrade2.safeVersion}": ${trust2.reason}`
|
|
4696
|
+
}
|
|
4697
|
+
};
|
|
4698
|
+
}
|
|
3554
4699
|
const overrideResult = await applyPackageOverrideTool.execute({
|
|
3555
4700
|
cwd,
|
|
3556
4701
|
packageManager,
|
|
@@ -3606,6 +4751,26 @@ async function resolvePrimaryResult(params) {
|
|
|
3606
4751
|
}
|
|
3607
4752
|
};
|
|
3608
4753
|
}
|
|
4754
|
+
const trust = await verifyPackageTrust(
|
|
4755
|
+
pkg.name,
|
|
4756
|
+
safeUpgrade.safeVersion,
|
|
4757
|
+
constraints.supplyChainPolicy
|
|
4758
|
+
);
|
|
4759
|
+
if (!trust.trusted) {
|
|
4760
|
+
return {
|
|
4761
|
+
steps: 1,
|
|
4762
|
+
result: {
|
|
4763
|
+
packageName: pkg.name,
|
|
4764
|
+
strategy: "none",
|
|
4765
|
+
fromVersion: pkg.version,
|
|
4766
|
+
applied: false,
|
|
4767
|
+
dryRun,
|
|
4768
|
+
unresolvedReason: "policy-blocked",
|
|
4769
|
+
vulnerableRange: vulnerable.affected.vulnerableRange,
|
|
4770
|
+
message: `Supply chain trust gate blocked "${pkg.name}@${safeUpgrade.safeVersion}": ${trust.reason}`
|
|
4771
|
+
}
|
|
4772
|
+
};
|
|
4773
|
+
}
|
|
3609
4774
|
const applyResult = await applyVersionBumpTool.execute({
|
|
3610
4775
|
cwd,
|
|
3611
4776
|
packageManager,
|
|
@@ -3615,10 +4780,12 @@ async function resolvePrimaryResult(params) {
|
|
|
3615
4780
|
dryRun,
|
|
3616
4781
|
policy,
|
|
3617
4782
|
runTests,
|
|
4783
|
+
typecheck: constraints.typecheck,
|
|
3618
4784
|
installMode: constraints.installMode,
|
|
3619
4785
|
installPreferOffline: constraints.installPreferOffline,
|
|
3620
4786
|
enforceFrozenLockfile: constraints.enforceFrozenLockfile,
|
|
3621
|
-
workspace: constraints.workspace
|
|
4787
|
+
workspace: constraints.workspace,
|
|
4788
|
+
breakingChangeRefactor: constraints.enableBreakingChangeRefactor
|
|
3622
4789
|
});
|
|
3623
4790
|
return {
|
|
3624
4791
|
steps: 2,
|
|
@@ -3628,10 +4795,10 @@ async function resolvePrimaryResult(params) {
|
|
|
3628
4795
|
|
|
3629
4796
|
// src/remediation/tools/fetch-package-source.ts
|
|
3630
4797
|
import { z as z6 } from "zod";
|
|
3631
|
-
import { mkdir as mkdir2, mkdtemp, readdir as readdir2, readFile as
|
|
3632
|
-
import { join as
|
|
4798
|
+
import { mkdir as mkdir2, mkdtemp, readdir as readdir2, readFile as readFile4, rm as rm3 } from "fs/promises";
|
|
4799
|
+
import { join as join17 } from "path";
|
|
3633
4800
|
import { tmpdir } from "os";
|
|
3634
|
-
import { execa as
|
|
4801
|
+
import { execa as execa7 } from "execa";
|
|
3635
4802
|
var fetchPackageSourceTool = defineTool({
|
|
3636
4803
|
description: "Download package tarball from npm and extract source files for CVE analysis. Supports custom file patterns (default: *.js, *.ts).",
|
|
3637
4804
|
parameters: z6.object({
|
|
@@ -3649,25 +4816,25 @@ var fetchPackageSourceTool = defineTool({
|
|
|
3649
4816
|
if (safePatterns.length === 0) {
|
|
3650
4817
|
return { success: false, error: "No valid file patterns provided." };
|
|
3651
4818
|
}
|
|
3652
|
-
const tempBaseDir = await mkdtemp(
|
|
3653
|
-
const extractDir =
|
|
4819
|
+
const tempBaseDir = await mkdtemp(join17(tmpdir(), "autoremediator-pkg-"));
|
|
4820
|
+
const extractDir = join17(tempBaseDir, "out");
|
|
3654
4821
|
try {
|
|
3655
4822
|
const scopedName = packageName.split("/").pop();
|
|
3656
4823
|
const npmUrl = `https://registry.npmjs.org/${packageName}/-/${scopedName}-${version}.tgz`;
|
|
3657
4824
|
await mkdir2(tempBaseDir, { recursive: true });
|
|
3658
|
-
const tarballPath =
|
|
3659
|
-
await
|
|
4825
|
+
const tarballPath = join17(tempBaseDir, "package.tgz");
|
|
4826
|
+
await execa7("curl", ["-L", "-o", tarballPath, npmUrl]);
|
|
3660
4827
|
await mkdir2(extractDir, { recursive: true });
|
|
3661
|
-
await
|
|
4828
|
+
await execa7("tar", ["-xzf", tarballPath, "-C", extractDir]);
|
|
3662
4829
|
const extractedContents = await readdir2(extractDir);
|
|
3663
|
-
const packageRootDir = extractedContents.includes("package") ?
|
|
4830
|
+
const packageRootDir = extractedContents.includes("package") ? join17(extractDir, "package") : extractDir;
|
|
3664
4831
|
const sourceCode = {};
|
|
3665
4832
|
async function walkDir(dir, relativeBase) {
|
|
3666
4833
|
try {
|
|
3667
4834
|
const files = await readdir2(dir, { withFileTypes: true });
|
|
3668
4835
|
for (const file of files) {
|
|
3669
|
-
const fullPath =
|
|
3670
|
-
const relPath =
|
|
4836
|
+
const fullPath = join17(dir, file.name);
|
|
4837
|
+
const relPath = join17(relativeBase, file.name);
|
|
3671
4838
|
if (file.isDirectory()) {
|
|
3672
4839
|
if (!["node_modules", ".git", "dist", "build", "coverage", ".next", "out"].includes(
|
|
3673
4840
|
file.name
|
|
@@ -3681,7 +4848,7 @@ var fetchPackageSourceTool = defineTool({
|
|
|
3681
4848
|
});
|
|
3682
4849
|
if (matches) {
|
|
3683
4850
|
try {
|
|
3684
|
-
const content = await
|
|
4851
|
+
const content = await readFile4(fullPath, "utf8");
|
|
3685
4852
|
sourceCode[relPath] = content;
|
|
3686
4853
|
} catch {
|
|
3687
4854
|
}
|
|
@@ -3716,7 +4883,7 @@ var fetchPackageSourceTool = defineTool({
|
|
|
3716
4883
|
error: `Failed to fetch and extract package ${packageName}@${version}: ${message}`
|
|
3717
4884
|
};
|
|
3718
4885
|
} finally {
|
|
3719
|
-
await
|
|
4886
|
+
await rm3(tempBaseDir, { recursive: true, force: true });
|
|
3720
4887
|
}
|
|
3721
4888
|
}
|
|
3722
4889
|
});
|
|
@@ -3774,6 +4941,7 @@ Analyze the source code to:
|
|
|
3774
4941
|
2. Explain the root cause of the security issue
|
|
3775
4942
|
3. Propose a secure fix that addresses the vulnerability
|
|
3776
4943
|
4. Provide the complete fixed version of affected files
|
|
4944
|
+
5. Optionally provide a standalone Node.js reproduction/regression test script
|
|
3777
4945
|
|
|
3778
4946
|
## Response Format
|
|
3779
4947
|
Respond ONLY with valid JSON (no markdown, no extra text):
|
|
@@ -3784,7 +4952,8 @@ Respond ONLY with valid JSON (no markdown, no extra text):
|
|
|
3784
4952
|
"path/to/other.ts": "Complete fixed source code for this file"
|
|
3785
4953
|
},
|
|
3786
4954
|
"confidence": 0.95,
|
|
3787
|
-
"riskLevel": "medium"
|
|
4955
|
+
"riskLevel": "medium",
|
|
4956
|
+
"reproductionTest": "// Node.js script asserting the patch resolves the exploit/regression"
|
|
3788
4957
|
}
|
|
3789
4958
|
|
|
3790
4959
|
Important:
|
|
@@ -3795,7 +4964,7 @@ Important:
|
|
|
3795
4964
|
}
|
|
3796
4965
|
|
|
3797
4966
|
// src/remediation/tools/generate-patch/helpers.ts
|
|
3798
|
-
import { parseSync as
|
|
4967
|
+
import { parseSync as parseSync3 } from "oxc-parser";
|
|
3799
4968
|
import YAML from "yaml";
|
|
3800
4969
|
function parseLlmAnalysisResponse(text) {
|
|
3801
4970
|
try {
|
|
@@ -3818,7 +4987,7 @@ function validatePatchedCodeSyntax(filePath, code) {
|
|
|
3818
4987
|
const lowerPath = filePath.toLowerCase();
|
|
3819
4988
|
if (/\.[cm]?[jt]sx?$/i.test(lowerPath)) {
|
|
3820
4989
|
try {
|
|
3821
|
-
const res =
|
|
4990
|
+
const res = parseSync3(filePath, code);
|
|
3822
4991
|
if (res.errors && res.errors.length > 0) {
|
|
3823
4992
|
const errorMsg = res.errors.map((e) => e.message).join("; ");
|
|
3824
4993
|
return {
|
|
@@ -4064,6 +5233,7 @@ var generatePatchTool = defineTool({
|
|
|
4064
5233
|
success: true,
|
|
4065
5234
|
patches,
|
|
4066
5235
|
patchContent: patches[0]?.unifiedDiff,
|
|
5236
|
+
reproductionTest: analysis.reproductionTest,
|
|
4067
5237
|
llmProvider: provider,
|
|
4068
5238
|
llmModel: modelName,
|
|
4069
5239
|
latencyMs,
|
|
@@ -4088,17 +5258,126 @@ var generatePatchTool = defineTool({
|
|
|
4088
5258
|
|
|
4089
5259
|
// src/remediation/tools/apply-patch-file/index.ts
|
|
4090
5260
|
import { z as z8 } from "zod";
|
|
4091
|
-
import { mkdir as mkdir3, writeFile as
|
|
4092
|
-
import {
|
|
4093
|
-
import {
|
|
5261
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
5262
|
+
import { existsSync as existsSync14 } from "fs";
|
|
5263
|
+
import { isAbsolute as isAbsolute4, join as join19 } from "path";
|
|
5264
|
+
import { execa as execa9 } from "execa";
|
|
5265
|
+
|
|
5266
|
+
// src/remediation/proof-of-fix/runner.ts
|
|
5267
|
+
async function runProofOfFixPhase(options) {
|
|
5268
|
+
const { stage, testFilePath, cwd, timeoutMs = 15e3 } = options;
|
|
5269
|
+
const started = Date.now();
|
|
5270
|
+
const isTs = testFilePath.endsWith(".ts");
|
|
5271
|
+
const nodeArgs = isTs ? ["--experimental-strip-types", testFilePath] : [testFilePath];
|
|
5272
|
+
try {
|
|
5273
|
+
const result = await safeExeca("node", nodeArgs, {
|
|
5274
|
+
cwd,
|
|
5275
|
+
timeoutMs,
|
|
5276
|
+
stdio: "pipe"
|
|
5277
|
+
});
|
|
5278
|
+
const durationMs = Date.now() - started;
|
|
5279
|
+
const output = `${result.stdout || ""}
|
|
5280
|
+
${result.stderr || ""}`.trim();
|
|
5281
|
+
if (stage === "negative") {
|
|
5282
|
+
return {
|
|
5283
|
+
executed: true,
|
|
5284
|
+
passed: false,
|
|
5285
|
+
exitCode: 0,
|
|
5286
|
+
output,
|
|
5287
|
+
durationMs,
|
|
5288
|
+
error: "Exploit reproduction test passed on vulnerable target (failed to reproduce vulnerability)."
|
|
5289
|
+
};
|
|
5290
|
+
}
|
|
5291
|
+
return {
|
|
5292
|
+
executed: true,
|
|
5293
|
+
passed: true,
|
|
5294
|
+
exitCode: 0,
|
|
5295
|
+
output,
|
|
5296
|
+
durationMs
|
|
5297
|
+
};
|
|
5298
|
+
} catch (err) {
|
|
5299
|
+
const durationMs = Date.now() - started;
|
|
5300
|
+
const execaErr = err;
|
|
5301
|
+
const exitCode = typeof execaErr.exitCode === "number" ? execaErr.exitCode : 1;
|
|
5302
|
+
const errorMsg = execaErr.message || String(err);
|
|
5303
|
+
const output = [execaErr.stdout, execaErr.stderr, errorMsg].filter(Boolean).join("\n").trim();
|
|
5304
|
+
if (stage === "negative") {
|
|
5305
|
+
if (execaErr.timedOut || errorMsg.includes("timed out")) {
|
|
5306
|
+
return {
|
|
5307
|
+
executed: true,
|
|
5308
|
+
passed: false,
|
|
5309
|
+
exitCode,
|
|
5310
|
+
output,
|
|
5311
|
+
durationMs,
|
|
5312
|
+
error: "Exploit reproduction test timed out (execution hung)."
|
|
5313
|
+
};
|
|
5314
|
+
}
|
|
5315
|
+
const isHarnessFailure = exitCode === 127 || errorMsg.includes("ENOENT") || output.includes("bad option:") || output.includes("Cannot find module") && output.includes(testFilePath);
|
|
5316
|
+
if (isHarnessFailure) {
|
|
5317
|
+
return {
|
|
5318
|
+
executed: false,
|
|
5319
|
+
passed: false,
|
|
5320
|
+
exitCode,
|
|
5321
|
+
output,
|
|
5322
|
+
durationMs,
|
|
5323
|
+
error: `Reproduction test failed to execute (harness error): ${errorMsg}`
|
|
5324
|
+
};
|
|
5325
|
+
}
|
|
5326
|
+
return {
|
|
5327
|
+
executed: true,
|
|
5328
|
+
passed: true,
|
|
5329
|
+
exitCode,
|
|
5330
|
+
output,
|
|
5331
|
+
durationMs
|
|
5332
|
+
};
|
|
5333
|
+
}
|
|
5334
|
+
return {
|
|
5335
|
+
executed: true,
|
|
5336
|
+
passed: false,
|
|
5337
|
+
exitCode,
|
|
5338
|
+
output,
|
|
5339
|
+
durationMs,
|
|
5340
|
+
error: `Reproduction test failed on patched target: ${errorMsg}`
|
|
5341
|
+
};
|
|
5342
|
+
}
|
|
5343
|
+
}
|
|
5344
|
+
function assembleProofOfFixVerification(params) {
|
|
5345
|
+
const { reproductionTestPath, negativePhase, positivePhase, regressionPhase } = params;
|
|
5346
|
+
const regPhase = regressionPhase ?? {
|
|
5347
|
+
executed: false,
|
|
5348
|
+
passed: true
|
|
5349
|
+
};
|
|
5350
|
+
let status = "verified";
|
|
5351
|
+
let failureReason;
|
|
5352
|
+
if (!negativePhase.passed) {
|
|
5353
|
+
status = "failed";
|
|
5354
|
+
failureReason = negativePhase.error || "negative-phase-failed";
|
|
5355
|
+
} else if (!positivePhase.passed) {
|
|
5356
|
+
status = "failed";
|
|
5357
|
+
failureReason = positivePhase.error || "positive-phase-failed";
|
|
5358
|
+
} else if (regPhase.executed && !regPhase.passed) {
|
|
5359
|
+
status = "failed";
|
|
5360
|
+
failureReason = regPhase.error || "regression-phase-failed";
|
|
5361
|
+
}
|
|
5362
|
+
const durationMs = (negativePhase.durationMs || 0) + (positivePhase.durationMs || 0) + (regPhase.durationMs || 0);
|
|
5363
|
+
return {
|
|
5364
|
+
status,
|
|
5365
|
+
reproductionTestPath,
|
|
5366
|
+
negativePhase,
|
|
5367
|
+
positivePhase,
|
|
5368
|
+
regressionPhase: regPhase,
|
|
5369
|
+
durationMs,
|
|
5370
|
+
failureReason
|
|
5371
|
+
};
|
|
5372
|
+
}
|
|
4094
5373
|
|
|
4095
5374
|
// src/remediation/tools/apply-patch-file/helpers.ts
|
|
4096
|
-
import { existsSync as
|
|
4097
|
-
import { mkdtemp as mkdtemp2, readFile as
|
|
5375
|
+
import { existsSync as existsSync13 } from "fs";
|
|
5376
|
+
import { mkdtemp as mkdtemp2, readFile as readFile5, rm as rm4, writeFile as writeFile2 } from "fs/promises";
|
|
4098
5377
|
import { createHash } from "crypto";
|
|
4099
5378
|
import { tmpdir as tmpdir2 } from "os";
|
|
4100
|
-
import { join as
|
|
4101
|
-
import { execa as
|
|
5379
|
+
import { join as join18 } from "path";
|
|
5380
|
+
import { execa as execa8 } from "execa";
|
|
4102
5381
|
async function resolvePatchMode(packageManager, cwd) {
|
|
4103
5382
|
if (packageManager === "npm" || packageManager === "bun" || packageManager === "deno")
|
|
4104
5383
|
return "patch-package";
|
|
@@ -4129,13 +5408,13 @@ function computePatchIntegrity(patchContent) {
|
|
|
4129
5408
|
return `sha256:${hex}`;
|
|
4130
5409
|
}
|
|
4131
5410
|
async function writePatchManifest(manifestFilePath, artifact) {
|
|
4132
|
-
await
|
|
5411
|
+
await writeFile2(manifestFilePath, JSON.stringify(artifact, null, 2) + "\n", "utf8");
|
|
4133
5412
|
}
|
|
4134
5413
|
async function configurePatchPackagePostinstall(cwd, packageManager) {
|
|
4135
|
-
const pkgJsonPath =
|
|
5414
|
+
const pkgJsonPath = join18(cwd, "package.json");
|
|
4136
5415
|
let pkgJson;
|
|
4137
5416
|
try {
|
|
4138
|
-
pkgJson = JSON.parse(await
|
|
5417
|
+
pkgJson = JSON.parse(await readFile5(pkgJsonPath, "utf8"));
|
|
4139
5418
|
} catch {
|
|
4140
5419
|
return {
|
|
4141
5420
|
success: false,
|
|
@@ -4147,7 +5426,7 @@ async function configurePatchPackagePostinstall(cwd, packageManager) {
|
|
|
4147
5426
|
try {
|
|
4148
5427
|
const commands = getPackageManagerCommands(packageManager);
|
|
4149
5428
|
const [cmd, ...args] = commands.installDev("patch-package");
|
|
4150
|
-
await
|
|
5429
|
+
await execa8(cmd, args, {
|
|
4151
5430
|
cwd,
|
|
4152
5431
|
stdio: "pipe"
|
|
4153
5432
|
});
|
|
@@ -4168,13 +5447,13 @@ async function configurePatchPackagePostinstall(cwd, packageManager) {
|
|
|
4168
5447
|
} else if (!currentPostinstall) {
|
|
4169
5448
|
pkgJson.scripts.postinstall = patchApplyCmd;
|
|
4170
5449
|
}
|
|
4171
|
-
await
|
|
5450
|
+
await writeFile2(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
4172
5451
|
return { success: true };
|
|
4173
5452
|
}
|
|
4174
5453
|
async function capturePackageJsonSnapshot(cwd) {
|
|
4175
|
-
const path =
|
|
5454
|
+
const path = join18(cwd, "package.json");
|
|
4176
5455
|
try {
|
|
4177
|
-
const content = await
|
|
5456
|
+
const content = await readFile5(path, "utf8");
|
|
4178
5457
|
return { path, content };
|
|
4179
5458
|
} catch {
|
|
4180
5459
|
return void 0;
|
|
@@ -4190,19 +5469,19 @@ async function cleanupPatchArtifacts(params) {
|
|
|
4190
5469
|
installCommand,
|
|
4191
5470
|
rerunInstall
|
|
4192
5471
|
} = params;
|
|
4193
|
-
await
|
|
5472
|
+
await rm4(patchFilePath, { force: true }).catch(() => void 0);
|
|
4194
5473
|
if (manifestFilePath) {
|
|
4195
|
-
await
|
|
5474
|
+
await rm4(manifestFilePath, { force: true }).catch(() => void 0);
|
|
4196
5475
|
}
|
|
4197
5476
|
if (patchMode === "patch-package" && packageJsonSnapshot) {
|
|
4198
|
-
await
|
|
5477
|
+
await writeFile2(packageJsonSnapshot.path, packageJsonSnapshot.content, "utf8").catch(
|
|
4199
5478
|
() => void 0
|
|
4200
5479
|
);
|
|
4201
5480
|
}
|
|
4202
5481
|
if (!rerunInstall) return;
|
|
4203
5482
|
try {
|
|
4204
5483
|
const [installCmd, ...installArgs] = installCommand;
|
|
4205
|
-
await
|
|
5484
|
+
await execa8(installCmd, installArgs, {
|
|
4206
5485
|
cwd,
|
|
4207
5486
|
stdio: "pipe"
|
|
4208
5487
|
});
|
|
@@ -4216,7 +5495,7 @@ async function applyNativePatch(params) {
|
|
|
4216
5495
|
const createArgs = ["patch", packageSpec];
|
|
4217
5496
|
let patchDir;
|
|
4218
5497
|
try {
|
|
4219
|
-
const createResult = await
|
|
5498
|
+
const createResult = await execa8(createCommand, createArgs, {
|
|
4220
5499
|
cwd,
|
|
4221
5500
|
stdio: "pipe"
|
|
4222
5501
|
});
|
|
@@ -4244,17 +5523,17 @@ ${createResult.stderr}`);
|
|
|
4244
5523
|
error: `Could not determine native patch directory for ${packageSpec}.`
|
|
4245
5524
|
};
|
|
4246
5525
|
}
|
|
4247
|
-
const tempPatchDir = await mkdtemp2(
|
|
4248
|
-
const tempPatchFile =
|
|
5526
|
+
const tempPatchDir = await mkdtemp2(join18(tmpdir2(), "autoremediator-native-patch-"));
|
|
5527
|
+
const tempPatchFile = join18(tempPatchDir, "change.patch");
|
|
4249
5528
|
try {
|
|
4250
|
-
await
|
|
4251
|
-
await
|
|
5529
|
+
await writeFile2(tempPatchFile, patchContent, "utf8");
|
|
5530
|
+
await execa8("patch", ["-p1", "-i", tempPatchFile], {
|
|
4252
5531
|
cwd: patchDir,
|
|
4253
5532
|
stdio: "pipe"
|
|
4254
5533
|
});
|
|
4255
5534
|
const commitCommand = patchMode === "native-pnpm" ? "pnpm" : "yarn";
|
|
4256
5535
|
const commitArgs = patchMode === "native-pnpm" ? ["patch-commit", patchDir] : ["patch-commit", "-s", patchDir];
|
|
4257
|
-
await
|
|
5536
|
+
await execa8(commitCommand, commitArgs, {
|
|
4258
5537
|
cwd,
|
|
4259
5538
|
stdio: "pipe"
|
|
4260
5539
|
});
|
|
@@ -4269,19 +5548,19 @@ ${createResult.stderr}`);
|
|
|
4269
5548
|
error: `Failed to apply native patch for ${packageSpec}: ${err instanceof Error ? err.message : String(err)}`
|
|
4270
5549
|
};
|
|
4271
5550
|
} finally {
|
|
4272
|
-
await
|
|
5551
|
+
await rm4(tempPatchDir, { recursive: true, force: true });
|
|
4273
5552
|
}
|
|
4274
5553
|
return { success: true };
|
|
4275
5554
|
}
|
|
4276
5555
|
function extractPatchDirectory(output) {
|
|
4277
5556
|
const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
4278
5557
|
for (const line of lines) {
|
|
4279
|
-
if (
|
|
5558
|
+
if (existsSync13(line)) {
|
|
4280
5559
|
return line;
|
|
4281
5560
|
}
|
|
4282
5561
|
const tokens = line.split(/\s+/).map((token) => token.replace(/^['"]|['"]$/g, ""));
|
|
4283
5562
|
for (const token of tokens) {
|
|
4284
|
-
if (token.startsWith("/") &&
|
|
5563
|
+
if (token.startsWith("/") && existsSync13(token)) {
|
|
4285
5564
|
return token;
|
|
4286
5565
|
}
|
|
4287
5566
|
}
|
|
@@ -4291,7 +5570,7 @@ function extractPatchDirectory(output) {
|
|
|
4291
5570
|
async function validatePatchWithTests(cwd, testCommand) {
|
|
4292
5571
|
try {
|
|
4293
5572
|
const [cmd, ...args] = testCommand;
|
|
4294
|
-
const result = await
|
|
5573
|
+
const result = await execa8(cmd, args, {
|
|
4295
5574
|
cwd,
|
|
4296
5575
|
timeout: 6e4,
|
|
4297
5576
|
stdio: "pipe"
|
|
@@ -4351,6 +5630,11 @@ var applyPatchFileTool = defineTool({
|
|
|
4351
5630
|
enforceFrozenLockfile: z8.boolean().optional(),
|
|
4352
5631
|
workspace: z8.string().optional(),
|
|
4353
5632
|
validateWithTests: z8.boolean().optional().default(true).describe("Run package manager test command to validate patch doesn't break anything"),
|
|
5633
|
+
reproductionTest: z8.string().optional().describe("Optional synthesized reproduction test script verifying fix"),
|
|
5634
|
+
proofOfFix: z8.union([z8.boolean(), z8.string()]).optional().describe(
|
|
5635
|
+
"If true or script path/content, enforce dual-stage exploit reproduction (assert failure pre-patch, assert pass post-patch)"
|
|
5636
|
+
),
|
|
5637
|
+
requireProofOfFix: z8.boolean().optional().describe("If true, fail and roll back patch if proof-of-fix verification fails"),
|
|
4354
5638
|
dryRun: z8.boolean().optional().default(false).describe("If true, report but do not mutate files")
|
|
4355
5639
|
}).refine((value) => Boolean(value.patchContent || value.patches && value.patches.length > 0), {
|
|
4356
5640
|
message: "Either patchContent or patches must be provided"
|
|
@@ -4372,6 +5656,9 @@ var applyPatchFileTool = defineTool({
|
|
|
4372
5656
|
workspace,
|
|
4373
5657
|
riskLevel,
|
|
4374
5658
|
validateWithTests,
|
|
5659
|
+
reproductionTest,
|
|
5660
|
+
proofOfFix,
|
|
5661
|
+
requireProofOfFix,
|
|
4375
5662
|
dryRun
|
|
4376
5663
|
}) => {
|
|
4377
5664
|
try {
|
|
@@ -4422,7 +5709,7 @@ var applyPatchFileTool = defineTool({
|
|
|
4422
5709
|
};
|
|
4423
5710
|
}
|
|
4424
5711
|
const patchFileName = buildPatchFileName(packageName, vulnerableVersion);
|
|
4425
|
-
const patchFilePath =
|
|
5712
|
+
const patchFilePath = join19(cwd, patchesDir, patchFileName);
|
|
4426
5713
|
const manifestFilePath = `${patchFilePath}.json`;
|
|
4427
5714
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4428
5715
|
const baseArtifact = {
|
|
@@ -4461,9 +5748,9 @@ var applyPatchFileTool = defineTool({
|
|
|
4461
5748
|
}
|
|
4462
5749
|
return withRepoLock(cwd, async () => {
|
|
4463
5750
|
const packageJsonSnapshot = patchModeRequiresPackageJsonSnapshot(pm) ? await capturePackageJsonSnapshot(cwd) : void 0;
|
|
4464
|
-
const patchesDirPath =
|
|
5751
|
+
const patchesDirPath = join19(cwd, patchesDir);
|
|
4465
5752
|
await mkdir3(patchesDirPath, { recursive: true });
|
|
4466
|
-
await
|
|
5753
|
+
await writeFile3(patchFilePath, selectedPatch, "utf8");
|
|
4467
5754
|
validationPhases.push({
|
|
4468
5755
|
phase: "patch-write",
|
|
4469
5756
|
passed: true,
|
|
@@ -4484,6 +5771,101 @@ var applyPatchFileTool = defineTool({
|
|
|
4484
5771
|
});
|
|
4485
5772
|
artifact.validationPhases = validationPhases;
|
|
4486
5773
|
await writePatchManifest(manifestFilePath, artifact);
|
|
5774
|
+
let reproductionTestPath;
|
|
5775
|
+
let negativePhaseResult;
|
|
5776
|
+
if (typeof proofOfFix === "string" && proofOfFix.trim().length > 0) {
|
|
5777
|
+
const resolvedPath = isAbsolute4(proofOfFix) ? proofOfFix : join19(cwd, proofOfFix);
|
|
5778
|
+
if (existsSync14(resolvedPath)) {
|
|
5779
|
+
reproductionTestPath = resolvedPath;
|
|
5780
|
+
} else {
|
|
5781
|
+
const safeName = packageName.replace(/^@/, "").replace(/\//g, "+");
|
|
5782
|
+
reproductionTestPath = join19(patchesDirPath, `${safeName}+${vulnerableVersion}.test.cjs`);
|
|
5783
|
+
await writeFile3(reproductionTestPath, proofOfFix, "utf8");
|
|
5784
|
+
}
|
|
5785
|
+
} else if (reproductionTest) {
|
|
5786
|
+
const safeName = packageName.replace(/^@/, "").replace(/\//g, "+");
|
|
5787
|
+
reproductionTestPath = join19(patchesDirPath, `${safeName}+${vulnerableVersion}.test.cjs`);
|
|
5788
|
+
await writeFile3(reproductionTestPath, reproductionTest, "utf8");
|
|
5789
|
+
}
|
|
5790
|
+
const isProofOfFixActive = Boolean(proofOfFix || requireProofOfFix);
|
|
5791
|
+
if (isProofOfFixActive) {
|
|
5792
|
+
if (!reproductionTestPath) {
|
|
5793
|
+
if (requireProofOfFix) {
|
|
5794
|
+
await cleanupPatchArtifacts({
|
|
5795
|
+
cwd,
|
|
5796
|
+
patchFilePath,
|
|
5797
|
+
manifestFilePath,
|
|
5798
|
+
patchMode,
|
|
5799
|
+
packageJsonSnapshot,
|
|
5800
|
+
installCommand,
|
|
5801
|
+
rerunInstall: false
|
|
5802
|
+
});
|
|
5803
|
+
return {
|
|
5804
|
+
success: false,
|
|
5805
|
+
packageName,
|
|
5806
|
+
vulnerableVersion,
|
|
5807
|
+
applied: false,
|
|
5808
|
+
dryRun: false,
|
|
5809
|
+
message: "Proof-of-fix verification required, but no reproduction test or script was supplied.",
|
|
5810
|
+
patchFilePath,
|
|
5811
|
+
manifestFilePath,
|
|
5812
|
+
patchPath: patchFilePath,
|
|
5813
|
+
patchMode,
|
|
5814
|
+
postinstallConfigured: false,
|
|
5815
|
+
patchArtifact: {
|
|
5816
|
+
...artifact,
|
|
5817
|
+
applied: false,
|
|
5818
|
+
validationPhases
|
|
5819
|
+
},
|
|
5820
|
+
validationPhases,
|
|
5821
|
+
error: "Proof-of-fix verification required, but no reproduction test or script was supplied."
|
|
5822
|
+
};
|
|
5823
|
+
}
|
|
5824
|
+
} else {
|
|
5825
|
+
negativePhaseResult = await runProofOfFixPhase({
|
|
5826
|
+
stage: "negative",
|
|
5827
|
+
testFilePath: reproductionTestPath,
|
|
5828
|
+
cwd
|
|
5829
|
+
});
|
|
5830
|
+
validationPhases.push({
|
|
5831
|
+
phase: "proof-of-fix-negative",
|
|
5832
|
+
passed: negativePhaseResult.passed,
|
|
5833
|
+
message: negativePhaseResult.passed ? "Reproduction test reproduced vulnerability on unpatched code." : void 0,
|
|
5834
|
+
error: negativePhaseResult.passed ? void 0 : negativePhaseResult.error
|
|
5835
|
+
});
|
|
5836
|
+
if (!negativePhaseResult.passed && requireProofOfFix) {
|
|
5837
|
+
await cleanupPatchArtifacts({
|
|
5838
|
+
cwd,
|
|
5839
|
+
patchFilePath,
|
|
5840
|
+
manifestFilePath,
|
|
5841
|
+
patchMode,
|
|
5842
|
+
packageJsonSnapshot,
|
|
5843
|
+
installCommand,
|
|
5844
|
+
rerunInstall: false
|
|
5845
|
+
});
|
|
5846
|
+
return {
|
|
5847
|
+
success: false,
|
|
5848
|
+
packageName,
|
|
5849
|
+
vulnerableVersion,
|
|
5850
|
+
applied: false,
|
|
5851
|
+
dryRun: false,
|
|
5852
|
+
message: `Proof-of-fix negative phase failed: ${negativePhaseResult.error}`,
|
|
5853
|
+
patchFilePath,
|
|
5854
|
+
manifestFilePath,
|
|
5855
|
+
patchPath: patchFilePath,
|
|
5856
|
+
patchMode,
|
|
5857
|
+
postinstallConfigured: false,
|
|
5858
|
+
patchArtifact: {
|
|
5859
|
+
...artifact,
|
|
5860
|
+
applied: false,
|
|
5861
|
+
validationPhases
|
|
5862
|
+
},
|
|
5863
|
+
validationPhases,
|
|
5864
|
+
error: `Proof-of-fix negative phase failed: ${negativePhaseResult.error}`
|
|
5865
|
+
};
|
|
5866
|
+
}
|
|
5867
|
+
}
|
|
5868
|
+
}
|
|
4487
5869
|
const applyResult = patchMode === "patch-package" ? await configurePatchPackagePostinstall(cwd, pm) : await applyNativePatch({
|
|
4488
5870
|
cwd,
|
|
4489
5871
|
packageName,
|
|
@@ -4531,7 +5913,7 @@ var applyPatchFileTool = defineTool({
|
|
|
4531
5913
|
if (patchMode === "patch-package") {
|
|
4532
5914
|
try {
|
|
4533
5915
|
const [installCmd, ...installArgs] = installCommand;
|
|
4534
|
-
await
|
|
5916
|
+
await execa9(installCmd, installArgs, {
|
|
4535
5917
|
cwd,
|
|
4536
5918
|
stdio: "pipe"
|
|
4537
5919
|
});
|
|
@@ -4624,11 +6006,90 @@ var applyPatchFileTool = defineTool({
|
|
|
4624
6006
|
message: "Patch validation tests passed."
|
|
4625
6007
|
});
|
|
4626
6008
|
}
|
|
6009
|
+
let positivePhaseResult;
|
|
6010
|
+
if (reproductionTestPath) {
|
|
6011
|
+
if (isProofOfFixActive) {
|
|
6012
|
+
positivePhaseResult = await runProofOfFixPhase({
|
|
6013
|
+
stage: "positive",
|
|
6014
|
+
testFilePath: reproductionTestPath,
|
|
6015
|
+
cwd
|
|
6016
|
+
});
|
|
6017
|
+
validationPhases.push({
|
|
6018
|
+
phase: "proof-of-fix-positive",
|
|
6019
|
+
passed: positivePhaseResult.passed,
|
|
6020
|
+
message: positivePhaseResult.passed ? "Reproduction test passed on patched code (vulnerability mitigated)." : void 0,
|
|
6021
|
+
error: positivePhaseResult.passed ? void 0 : positivePhaseResult.error
|
|
6022
|
+
});
|
|
6023
|
+
if (!positivePhaseResult.passed && (requireProofOfFix || validateWithTests)) {
|
|
6024
|
+
await cleanupPatchArtifacts({
|
|
6025
|
+
cwd,
|
|
6026
|
+
patchFilePath,
|
|
6027
|
+
manifestFilePath,
|
|
6028
|
+
patchMode,
|
|
6029
|
+
packageJsonSnapshot,
|
|
6030
|
+
installCommand,
|
|
6031
|
+
rerunInstall: patchMode === "patch-package"
|
|
6032
|
+
});
|
|
6033
|
+
return {
|
|
6034
|
+
success: false,
|
|
6035
|
+
packageName,
|
|
6036
|
+
vulnerableVersion,
|
|
6037
|
+
applied: false,
|
|
6038
|
+
dryRun: false,
|
|
6039
|
+
message: `Proof-of-fix positive phase failed: ${positivePhaseResult.error}`,
|
|
6040
|
+
patchFilePath,
|
|
6041
|
+
manifestFilePath,
|
|
6042
|
+
patchPath: patchFilePath,
|
|
6043
|
+
patchMode,
|
|
6044
|
+
postinstallConfigured: false,
|
|
6045
|
+
patchArtifact: {
|
|
6046
|
+
...artifact,
|
|
6047
|
+
applied: false,
|
|
6048
|
+
validationPhases
|
|
6049
|
+
},
|
|
6050
|
+
validationPhases,
|
|
6051
|
+
error: `Proof-of-fix positive phase failed: ${positivePhaseResult.error}`
|
|
6052
|
+
};
|
|
6053
|
+
}
|
|
6054
|
+
} else {
|
|
6055
|
+
try {
|
|
6056
|
+
const reproRun = await execa9("node", [reproductionTestPath], { cwd, stdio: "pipe" });
|
|
6057
|
+
validationPhases.push({
|
|
6058
|
+
phase: "reproduction-test",
|
|
6059
|
+
passed: reproRun.exitCode === 0,
|
|
6060
|
+
message: "Reproduction test verified fix successfully."
|
|
6061
|
+
});
|
|
6062
|
+
} catch (err) {
|
|
6063
|
+
const reproErr = err instanceof Error ? err.message : String(err);
|
|
6064
|
+
validationPhases.push({
|
|
6065
|
+
phase: "reproduction-test",
|
|
6066
|
+
passed: false,
|
|
6067
|
+
error: `Reproduction test failed: ${reproErr}`
|
|
6068
|
+
});
|
|
6069
|
+
}
|
|
6070
|
+
}
|
|
6071
|
+
}
|
|
6072
|
+
let proofOfFixVerification;
|
|
6073
|
+
if (negativePhaseResult && positivePhaseResult) {
|
|
6074
|
+
proofOfFixVerification = assembleProofOfFixVerification({
|
|
6075
|
+
reproductionTestPath,
|
|
6076
|
+
negativePhase: negativePhaseResult,
|
|
6077
|
+
positivePhase: positivePhaseResult,
|
|
6078
|
+
regressionPhase: validationResult ? {
|
|
6079
|
+
executed: true,
|
|
6080
|
+
passed: validationResult.passed,
|
|
6081
|
+
error: validationResult.error
|
|
6082
|
+
} : void 0
|
|
6083
|
+
});
|
|
6084
|
+
}
|
|
4627
6085
|
const finalArtifact = {
|
|
4628
6086
|
...artifact,
|
|
4629
6087
|
applied: true,
|
|
4630
6088
|
dryRun: false,
|
|
4631
|
-
|
|
6089
|
+
reproductionTest,
|
|
6090
|
+
reproductionTestPath,
|
|
6091
|
+
validationPhases,
|
|
6092
|
+
proofOfFix: proofOfFixVerification
|
|
4632
6093
|
};
|
|
4633
6094
|
await writePatchManifest(manifestFilePath, finalArtifact);
|
|
4634
6095
|
return {
|
|
@@ -4645,7 +6106,8 @@ var applyPatchFileTool = defineTool({
|
|
|
4645
6106
|
postinstallConfigured: patchMode === "patch-package",
|
|
4646
6107
|
patchArtifact: finalArtifact,
|
|
4647
6108
|
validation: validationResult,
|
|
4648
|
-
validationPhases
|
|
6109
|
+
validationPhases,
|
|
6110
|
+
proofOfFix: proofOfFixVerification
|
|
4649
6111
|
};
|
|
4650
6112
|
});
|
|
4651
6113
|
} catch (err) {
|
|
@@ -4889,6 +6351,9 @@ async function tryLocalPatchFallback(params) {
|
|
|
4889
6351
|
workspace: params.workspace,
|
|
4890
6352
|
riskLevel: patchResult.riskLevel,
|
|
4891
6353
|
validateWithTests: params.runTests,
|
|
6354
|
+
reproductionTest: patchResult.reproductionTest,
|
|
6355
|
+
proofOfFix: params.proofOfFix,
|
|
6356
|
+
requireProofOfFix: params.requireProofOfFix,
|
|
4892
6357
|
dryRun: params.dryRun
|
|
4893
6358
|
});
|
|
4894
6359
|
steps += 1;
|
|
@@ -4937,8 +6402,14 @@ function resolveLocalRunOptions(options) {
|
|
|
4937
6402
|
const runTests = options.runTests ?? false;
|
|
4938
6403
|
const policy = options.policy ?? "";
|
|
4939
6404
|
const patchesDir = options.patchesDir || "./patches";
|
|
4940
|
-
const constraints = options.constraints ?? {};
|
|
4941
6405
|
const loadedPolicy = loadPolicy(cwd, options.policy);
|
|
6406
|
+
const constraints = {
|
|
6407
|
+
...options.constraints,
|
|
6408
|
+
typecheck: options.typecheck ?? options.constraints?.typecheck ?? loadedPolicy.constraints?.typecheck,
|
|
6409
|
+
supplyChainPolicy: options.supplyChainPolicy ?? options.constraints?.supplyChainPolicy ?? loadedPolicy.constraints?.supplyChainPolicy,
|
|
6410
|
+
enableBreakingChangeRefactor: options.breakingChangeRefactor ?? options.constraints?.enableBreakingChangeRefactor ?? loadedPolicy.constraints?.enableBreakingChangeRefactor,
|
|
6411
|
+
requireProofOfFix: options.requireProofOfFix ?? options.constraints?.requireProofOfFix ?? loadedPolicy.constraints?.requireProofOfFix
|
|
6412
|
+
};
|
|
4942
6413
|
const llmProvider = resolveProvider(options);
|
|
4943
6414
|
return {
|
|
4944
6415
|
cwd,
|
|
@@ -4969,12 +6440,15 @@ function resolveLocalRunOptions(options) {
|
|
|
4969
6440
|
regressionCheck: options.regressionCheck ?? false,
|
|
4970
6441
|
escalationGraph: options.escalationGraph ?? loadedPolicy.escalationGraph,
|
|
4971
6442
|
offlineIntelligence: options.offlineIntelligence ?? false,
|
|
4972
|
-
intelligenceSnapshotPath: options.intelligenceSnapshotPath
|
|
6443
|
+
intelligenceSnapshotPath: options.intelligenceSnapshotPath,
|
|
6444
|
+
proofOfFix: options.proofOfFix,
|
|
6445
|
+
requireProofOfFix: options.requireProofOfFix ?? constraints.requireProofOfFix,
|
|
6446
|
+
breakingChangeRefactor: options.breakingChangeRefactor ?? constraints.enableBreakingChangeRefactor
|
|
4973
6447
|
};
|
|
4974
6448
|
}
|
|
4975
6449
|
|
|
4976
6450
|
// src/remediation/local/vulnerability-match.ts
|
|
4977
|
-
import
|
|
6451
|
+
import semver5 from "semver";
|
|
4978
6452
|
function findVulnerablePackages(cveDetails, installedPackages) {
|
|
4979
6453
|
const vulnerablePackages = [];
|
|
4980
6454
|
for (const affected of cveDetails.affectedPackages) {
|
|
@@ -4983,10 +6457,10 @@ function findVulnerablePackages(cveDetails, installedPackages) {
|
|
|
4983
6457
|
if (affected.ecosystem !== "npm") continue;
|
|
4984
6458
|
const matches = installedPackages.filter((pkg) => pkg.name === affected.name);
|
|
4985
6459
|
for (const installed of matches) {
|
|
4986
|
-
if (!
|
|
6460
|
+
if (!semver5.valid(installed.version)) continue;
|
|
4987
6461
|
let isVulnerable = false;
|
|
4988
6462
|
try {
|
|
4989
|
-
isVulnerable =
|
|
6463
|
+
isVulnerable = semver5.satisfies(installed.version, affected.vulnerableRange, {
|
|
4990
6464
|
includePrerelease: false
|
|
4991
6465
|
});
|
|
4992
6466
|
} catch {
|
|
@@ -5137,7 +6611,9 @@ async function runLocalRemediationPipeline(cveId, options = {}) {
|
|
|
5137
6611
|
regressionCheck,
|
|
5138
6612
|
escalationGraph,
|
|
5139
6613
|
offlineIntelligence,
|
|
5140
|
-
intelligenceSnapshotPath
|
|
6614
|
+
intelligenceSnapshotPath,
|
|
6615
|
+
proofOfFix,
|
|
6616
|
+
requireProofOfFix
|
|
5141
6617
|
} = resolved;
|
|
5142
6618
|
const correlation = {
|
|
5143
6619
|
requestId: options.requestId,
|
|
@@ -5314,7 +6790,9 @@ async function runLocalRemediationPipeline(cveId, options = {}) {
|
|
|
5314
6790
|
installPreferOffline: constraints.installPreferOffline,
|
|
5315
6791
|
enforceFrozenLockfile: constraints.enforceFrozenLockfile,
|
|
5316
6792
|
workspace: constraints.workspace,
|
|
5317
|
-
vulnerableRange: vulnerable.affected.vulnerableRange
|
|
6793
|
+
vulnerableRange: vulnerable.affected.vulnerableRange,
|
|
6794
|
+
proofOfFix,
|
|
6795
|
+
requireProofOfFix
|
|
5318
6796
|
});
|
|
5319
6797
|
agentSteps += fallback.steps;
|
|
5320
6798
|
collectedResults.push(finalizePatchResult(fallback.result));
|
|
@@ -5329,7 +6807,7 @@ async function runLocalRemediationPipeline(cveId, options = {}) {
|
|
|
5329
6807
|
};
|
|
5330
6808
|
if (regressionCheck && primaryResult.applied && !dryRun && primaryResult.toVersion) {
|
|
5331
6809
|
try {
|
|
5332
|
-
if (
|
|
6810
|
+
if (semver6.satisfies(primaryResult.toVersion, vulnerable.affected.vulnerableRange, {
|
|
5333
6811
|
includePrerelease: false
|
|
5334
6812
|
})) {
|
|
5335
6813
|
primaryResult.regressionDetected = true;
|
|
@@ -5420,8 +6898,8 @@ function accumulateStepResults(params) {
|
|
|
5420
6898
|
}
|
|
5421
6899
|
|
|
5422
6900
|
// src/remediation/orchestration-prompt.ts
|
|
5423
|
-
import { existsSync as
|
|
5424
|
-
import { join as
|
|
6901
|
+
import { existsSync as existsSync15, readFileSync as readFileSync15 } from "fs";
|
|
6902
|
+
import { join as join20 } from "path";
|
|
5425
6903
|
function buildProviderAddendum(provider, personality = "balanced") {
|
|
5426
6904
|
const personalityDirective = personality === "analytical" ? "Use concise, explicit rationale for tool decisions and unresolved outcomes." : personality === "pragmatic" ? "Prefer the smallest safe remediation path while preserving policy and validation gates." : "Balance concise execution with brief rationale for risky or unresolved outcomes.";
|
|
5427
6905
|
const providerDirective = provider === "remote" ? "Use strict structured output and deterministic reporting fields." : "Use deterministic-first behavior and only rely on remote model fallback when required by patch generation.";
|
|
@@ -5432,13 +6910,13 @@ Provider profile:
|
|
|
5432
6910
|
- ${personalityDirective}`;
|
|
5433
6911
|
}
|
|
5434
6912
|
function loadOrchestrationPrompt(ctx) {
|
|
5435
|
-
const promptPath =
|
|
6913
|
+
const promptPath = join20(
|
|
5436
6914
|
process.cwd(),
|
|
5437
6915
|
".github",
|
|
5438
6916
|
"instructions",
|
|
5439
6917
|
"orchestration.instructions.md"
|
|
5440
6918
|
);
|
|
5441
|
-
if (!
|
|
6919
|
+
if (!existsSync15(promptPath)) {
|
|
5442
6920
|
return `You are autoremediator, an agentic security remediation system for Node.js package dependencies.
|
|
5443
6921
|
Working directory: ${ctx.cwd}
|
|
5444
6922
|
Package manager: ${ctx.packageManager}
|
|
@@ -5464,7 +6942,7 @@ Fallback sequence (when neither version bump nor override can be applied):
|
|
|
5464
6942
|
|
|
5465
6943
|
Always respect dryRun and policy constraints.`;
|
|
5466
6944
|
}
|
|
5467
|
-
const template =
|
|
6945
|
+
const template = readFileSync15(promptPath, "utf8");
|
|
5468
6946
|
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(
|
|
5469
6947
|
"{{directDependenciesOnly}}",
|
|
5470
6948
|
String(ctx.constraints.directDependenciesOnly ?? false)
|
|
@@ -5473,85 +6951,21 @@ Always respect dryRun and policy constraints.`;
|
|
|
5473
6951
|
|
|
5474
6952
|
// src/remediation/pipeline-runtime/progress.ts
|
|
5475
6953
|
function createProgressEmitter(options) {
|
|
5476
|
-
return (stage, detail, extra) => {
|
|
5477
|
-
if (!options.onProgress) return;
|
|
5478
|
-
options.onProgress({
|
|
5479
|
-
stage,
|
|
5480
|
-
detail,
|
|
5481
|
-
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5482
|
-
provider: extra?.provider,
|
|
5483
|
-
model: extra?.model
|
|
5484
|
-
});
|
|
5485
|
-
};
|
|
5486
|
-
}
|
|
5487
|
-
|
|
5488
|
-
// src/remediation/tools/lookup-cve.ts
|
|
5489
|
-
import { z as z9 } from "zod";
|
|
5490
|
-
|
|
5491
|
-
// src/intelligence/sources/cisa-kev.ts
|
|
5492
|
-
var CISA_KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json";
|
|
5493
|
-
async function fetchCisaKevFeed() {
|
|
5494
|
-
try {
|
|
5495
|
-
const res = await httpClient({ url: CISA_KEV_URL });
|
|
5496
|
-
if (!res.ok) return void 0;
|
|
5497
|
-
return res.data;
|
|
5498
|
-
} catch {
|
|
5499
|
-
return void 0;
|
|
5500
|
-
}
|
|
5501
|
-
}
|
|
5502
|
-
function findKevEntry(feed, cveId) {
|
|
5503
|
-
if (!feed?.vulnerabilities?.length) return void 0;
|
|
5504
|
-
const normalized = cveId.toUpperCase();
|
|
5505
|
-
return feed.vulnerabilities.find((v) => v.cveID.toUpperCase() === normalized);
|
|
5506
|
-
}
|
|
5507
|
-
async function enrichWithCisaKev(details) {
|
|
5508
|
-
const feed = await fetchCisaKevFeed();
|
|
5509
|
-
const entry = findKevEntry(feed, details.id);
|
|
5510
|
-
if (!entry) return details;
|
|
5511
|
-
details.kev = {
|
|
5512
|
-
knownExploited: true,
|
|
5513
|
-
dateAdded: entry.dateAdded,
|
|
5514
|
-
dueDate: entry.dueDate,
|
|
5515
|
-
requiredAction: entry.requiredAction,
|
|
5516
|
-
knownRansomwareCampaignUse: entry.knownRansomwareCampaignUse
|
|
5517
|
-
};
|
|
5518
|
-
if (!details.references.includes(CISA_KEV_URL)) {
|
|
5519
|
-
details.references.push(CISA_KEV_URL);
|
|
5520
|
-
}
|
|
5521
|
-
return details;
|
|
5522
|
-
}
|
|
5523
|
-
|
|
5524
|
-
// src/intelligence/sources/epss.ts
|
|
5525
|
-
async function fetchEpss(cveId) {
|
|
5526
|
-
const { epssApi } = getIntelligenceSourceConfig();
|
|
5527
|
-
if (!epssApi) return void 0;
|
|
5528
|
-
try {
|
|
5529
|
-
const url = new URL(epssApi);
|
|
5530
|
-
url.searchParams.set("cve", cveId);
|
|
5531
|
-
const res = await httpClient({ url: url.toString() });
|
|
5532
|
-
if (!res.ok) return void 0;
|
|
5533
|
-
const body = res.data;
|
|
5534
|
-
return body.data?.[0];
|
|
5535
|
-
} catch {
|
|
5536
|
-
return void 0;
|
|
5537
|
-
}
|
|
5538
|
-
}
|
|
5539
|
-
async function enrichWithEpss(details) {
|
|
5540
|
-
const row = await fetchEpss(details.id);
|
|
5541
|
-
if (!row) return details;
|
|
5542
|
-
const score = Number.parseFloat(row.epss);
|
|
5543
|
-
const percentile = Number.parseFloat(row.percentile);
|
|
5544
|
-
if (!Number.isFinite(score) || !Number.isFinite(percentile)) {
|
|
5545
|
-
return details;
|
|
5546
|
-
}
|
|
5547
|
-
details.epss = {
|
|
5548
|
-
score,
|
|
5549
|
-
percentile,
|
|
5550
|
-
date: row.date
|
|
6954
|
+
return (stage, detail, extra) => {
|
|
6955
|
+
if (!options.onProgress) return;
|
|
6956
|
+
options.onProgress({
|
|
6957
|
+
stage,
|
|
6958
|
+
detail,
|
|
6959
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6960
|
+
provider: extra?.provider,
|
|
6961
|
+
model: extra?.model
|
|
6962
|
+
});
|
|
5551
6963
|
};
|
|
5552
|
-
return details;
|
|
5553
6964
|
}
|
|
5554
6965
|
|
|
6966
|
+
// src/remediation/tools/lookup-cve.ts
|
|
6967
|
+
import { z as z9 } from "zod";
|
|
6968
|
+
|
|
5555
6969
|
// src/intelligence/sources/cve-services.ts
|
|
5556
6970
|
function pickEnglishDescription(container) {
|
|
5557
6971
|
if (!container?.descriptions?.length) return void 0;
|
|
@@ -5835,7 +7249,7 @@ var lookupCveTool = defineTool({
|
|
|
5835
7249
|
|
|
5836
7250
|
// src/remediation/tools/check-version-match.ts
|
|
5837
7251
|
import { z as z10 } from "zod";
|
|
5838
|
-
import
|
|
7252
|
+
import semver7 from "semver";
|
|
5839
7253
|
var affectedPackageSchema = z10.object({
|
|
5840
7254
|
name: z10.string(),
|
|
5841
7255
|
ecosystem: z10.literal("npm"),
|
|
@@ -5864,10 +7278,10 @@ var checkVersionMatchTool = defineTool({
|
|
|
5864
7278
|
(p) => p.name === affected.name
|
|
5865
7279
|
);
|
|
5866
7280
|
for (const installed of matches) {
|
|
5867
|
-
if (!
|
|
7281
|
+
if (!semver7.valid(installed.version)) continue;
|
|
5868
7282
|
let isVulnerable = false;
|
|
5869
7283
|
try {
|
|
5870
|
-
isVulnerable =
|
|
7284
|
+
isVulnerable = semver7.satisfies(installed.version, affected.vulnerableRange, {
|
|
5871
7285
|
includePrerelease: false
|
|
5872
7286
|
});
|
|
5873
7287
|
} catch {
|
|
@@ -6007,6 +7421,8 @@ function createRuntimeToolsForRun(options) {
|
|
|
6007
7421
|
installPreferOffline: constraints.installPreferOffline,
|
|
6008
7422
|
enforceFrozenLockfile: constraints.enforceFrozenLockfile,
|
|
6009
7423
|
workspace: constraints.workspace,
|
|
7424
|
+
typecheck: input.typecheck ?? options.typecheck ?? constraints.typecheck,
|
|
7425
|
+
breakingChangeRefactor: input.breakingChangeRefactor ?? options.breakingChangeRefactor ?? constraints.enableBreakingChangeRefactor,
|
|
6010
7426
|
dryRun: preview ? true : input.dryRun
|
|
6011
7427
|
})
|
|
6012
7428
|
};
|
|
@@ -6031,6 +7447,8 @@ function createRuntimeToolsForRun(options) {
|
|
|
6031
7447
|
installPreferOffline: constraints.installPreferOffline,
|
|
6032
7448
|
enforceFrozenLockfile: constraints.enforceFrozenLockfile,
|
|
6033
7449
|
workspace: constraints.workspace,
|
|
7450
|
+
proofOfFix: input.proofOfFix ?? options.proofOfFix,
|
|
7451
|
+
requireProofOfFix: input.requireProofOfFix ?? options.requireProofOfFix ?? constraints.requireProofOfFix,
|
|
6034
7452
|
dryRun: preview ? true : input.dryRun
|
|
6035
7453
|
})
|
|
6036
7454
|
};
|
|
@@ -6185,7 +7603,7 @@ async function runRemediationPipeline(cveId, options = {}) {
|
|
|
6185
7603
|
}
|
|
6186
7604
|
|
|
6187
7605
|
// src/api/change-request/index.ts
|
|
6188
|
-
import { execa as
|
|
7606
|
+
import { execa as execa10 } from "execa";
|
|
6189
7607
|
import { randomBytes } from "crypto";
|
|
6190
7608
|
function sanitizeBranchToken(value) {
|
|
6191
7609
|
return value.toLowerCase().replace(/[^a-z0-9._/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
@@ -6214,22 +7632,68 @@ function resolveProvider2(options) {
|
|
|
6214
7632
|
return options.provider ?? "github";
|
|
6215
7633
|
}
|
|
6216
7634
|
function resolveGrouping(options) {
|
|
6217
|
-
|
|
7635
|
+
const allowed = ["all", "per-cve", "per-package", "by-severity"];
|
|
7636
|
+
if (options.grouping && !allowed.includes(options.grouping)) {
|
|
6218
7637
|
throw new Error(
|
|
6219
|
-
|
|
7638
|
+
`Unsupported changeRequest.grouping: "${options.grouping}". Supported values: ${allowed.join(", ")}`
|
|
6220
7639
|
);
|
|
6221
7640
|
}
|
|
6222
|
-
return "all";
|
|
7641
|
+
return options.grouping ?? "all";
|
|
7642
|
+
}
|
|
7643
|
+
function groupReports(reports, grouping) {
|
|
7644
|
+
if (reports.length === 0) return [];
|
|
7645
|
+
if (grouping === "all") {
|
|
7646
|
+
return [reports];
|
|
7647
|
+
}
|
|
7648
|
+
if (grouping === "per-cve") {
|
|
7649
|
+
const map = /* @__PURE__ */ new Map();
|
|
7650
|
+
for (const report of reports) {
|
|
7651
|
+
const key = report.cveId.toUpperCase();
|
|
7652
|
+
const list = map.get(key) ?? [];
|
|
7653
|
+
list.push(report);
|
|
7654
|
+
map.set(key, list);
|
|
7655
|
+
}
|
|
7656
|
+
return Array.from(map.values());
|
|
7657
|
+
}
|
|
7658
|
+
if (grouping === "per-package") {
|
|
7659
|
+
const map = /* @__PURE__ */ new Map();
|
|
7660
|
+
for (const report of reports) {
|
|
7661
|
+
const pkgs = new Set(report.results.map((r) => r.packageName));
|
|
7662
|
+
if (pkgs.size === 0) {
|
|
7663
|
+
const list = map.get("misc") ?? [];
|
|
7664
|
+
list.push(report);
|
|
7665
|
+
map.set("misc", list);
|
|
7666
|
+
} else {
|
|
7667
|
+
for (const pkg of pkgs) {
|
|
7668
|
+
const list = map.get(pkg) ?? [];
|
|
7669
|
+
list.push(report);
|
|
7670
|
+
map.set(pkg, list);
|
|
7671
|
+
}
|
|
7672
|
+
}
|
|
7673
|
+
}
|
|
7674
|
+
return Array.from(map.values());
|
|
7675
|
+
}
|
|
7676
|
+
if (grouping === "by-severity") {
|
|
7677
|
+
const map = /* @__PURE__ */ new Map();
|
|
7678
|
+
for (const report of reports) {
|
|
7679
|
+
const sev = report.cveDetails?.severity ?? "UNKNOWN";
|
|
7680
|
+
const list = map.get(sev) ?? [];
|
|
7681
|
+
list.push(report);
|
|
7682
|
+
map.set(sev, list);
|
|
7683
|
+
}
|
|
7684
|
+
return Array.from(map.values());
|
|
7685
|
+
}
|
|
7686
|
+
return [reports];
|
|
6223
7687
|
}
|
|
6224
7688
|
async function detectCurrentBranch(cwd) {
|
|
6225
|
-
const result = await
|
|
7689
|
+
const result = await execa10("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
6226
7690
|
cwd,
|
|
6227
7691
|
stdio: "pipe"
|
|
6228
7692
|
});
|
|
6229
7693
|
return result.stdout.trim();
|
|
6230
7694
|
}
|
|
6231
7695
|
async function ensureRepoHasChanges(cwd) {
|
|
6232
|
-
const diff = await
|
|
7696
|
+
const diff = await execa10("git", ["status", "--porcelain"], {
|
|
6233
7697
|
cwd,
|
|
6234
7698
|
stdio: "pipe"
|
|
6235
7699
|
});
|
|
@@ -6258,7 +7722,7 @@ async function createGitHubRequest(params) {
|
|
|
6258
7722
|
if (params.draft) {
|
|
6259
7723
|
args.push("--draft");
|
|
6260
7724
|
}
|
|
6261
|
-
const result = await
|
|
7725
|
+
const result = await execa10("gh", args, {
|
|
6262
7726
|
cwd: params.cwd,
|
|
6263
7727
|
stdio: "pipe"
|
|
6264
7728
|
});
|
|
@@ -6283,7 +7747,7 @@ async function createGitLabRequest(params) {
|
|
|
6283
7747
|
if (params.draft) {
|
|
6284
7748
|
args.push("--draft");
|
|
6285
7749
|
}
|
|
6286
|
-
const result = await
|
|
7750
|
+
const result = await execa10("glab", args, {
|
|
6287
7751
|
cwd: params.cwd,
|
|
6288
7752
|
stdio: "pipe"
|
|
6289
7753
|
});
|
|
@@ -6293,64 +7757,68 @@ async function createChangeRequestsForReports(params) {
|
|
|
6293
7757
|
const { cwd, options, reports } = params;
|
|
6294
7758
|
const provider = resolveProvider2(options);
|
|
6295
7759
|
const grouping = resolveGrouping(options);
|
|
6296
|
-
const
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
const
|
|
7760
|
+
const groups = groupReports(reports, grouping);
|
|
7761
|
+
if (groups.length === 0) {
|
|
7762
|
+
return [];
|
|
7763
|
+
}
|
|
7764
|
+
const results = [];
|
|
7765
|
+
const hasChanges = await ensureRepoHasChanges(cwd).catch(() => false);
|
|
7766
|
+
const baseBranch = options.baseBranch ?? await detectCurrentBranch(cwd).catch(() => "main");
|
|
7767
|
+
const branchPrefix = options.branchPrefix ?? "autoremediator";
|
|
7768
|
+
const pushRemote = options.pushRemote ?? "origin";
|
|
7769
|
+
if (options.pushRemote && !/^[a-zA-Z0-9._-]+$/.test(options.pushRemote)) {
|
|
7770
|
+
throw new Error(`Invalid pushRemote value: ${options.pushRemote}`);
|
|
7771
|
+
}
|
|
7772
|
+
if (options.repository && !/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(options.repository)) {
|
|
7773
|
+
throw new Error(`Invalid repository format: ${options.repository}`);
|
|
7774
|
+
}
|
|
7775
|
+
for (const group of groups) {
|
|
7776
|
+
const plan = buildPlan(group);
|
|
7777
|
+
const titlePrefix = options.titlePrefix?.trim().slice(0, 200);
|
|
7778
|
+
const bodyFooter = options.bodyFooter ? options.bodyFooter.slice(0, 2e3) : void 0;
|
|
7779
|
+
const title = titlePrefix ? `${titlePrefix} ${plan.title}` : plan.title;
|
|
7780
|
+
const body = bodyFooter ? `${plan.body}
|
|
6301
7781
|
|
|
6302
7782
|
${bodyFooter}` : plan.body;
|
|
6303
|
-
try {
|
|
6304
|
-
const hasChanges = await ensureRepoHasChanges(cwd);
|
|
6305
7783
|
if (!hasChanges) {
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
];
|
|
7784
|
+
results.push({
|
|
7785
|
+
provider,
|
|
7786
|
+
grouping,
|
|
7787
|
+
repository: options.repository,
|
|
7788
|
+
branchName: "",
|
|
7789
|
+
title,
|
|
7790
|
+
body,
|
|
7791
|
+
created: false,
|
|
7792
|
+
cveIds: plan.cveIds,
|
|
7793
|
+
packageNames: plan.packageNames,
|
|
7794
|
+
error: "No repository changes to include in change request."
|
|
7795
|
+
});
|
|
7796
|
+
continue;
|
|
6320
7797
|
}
|
|
6321
|
-
const baseBranch = options.baseBranch ?? await detectCurrentBranch(cwd);
|
|
6322
|
-
const branchPrefix = options.branchPrefix ?? "autoremediator";
|
|
6323
7798
|
const branchName = toBranchName(branchPrefix, plan.cveIds);
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
target: baseBranch,
|
|
6348
|
-
title,
|
|
6349
|
-
body,
|
|
6350
|
-
draft: options.draft
|
|
6351
|
-
});
|
|
6352
|
-
return [
|
|
6353
|
-
{
|
|
7799
|
+
try {
|
|
7800
|
+
await execa10("git", ["checkout", "-b", branchName], { cwd, stdio: "pipe" });
|
|
7801
|
+
await execa10("git", ["add", "-A"], { cwd, stdio: "pipe" });
|
|
7802
|
+
await execa10("git", ["commit", "-m", title], { cwd, stdio: "pipe" });
|
|
7803
|
+
await execa10("git", ["push", "-u", pushRemote, branchName], { cwd, stdio: "pipe" });
|
|
7804
|
+
const url = provider === "github" ? await createGitHubRequest({
|
|
7805
|
+
cwd,
|
|
7806
|
+
repository: options.repository,
|
|
7807
|
+
head: branchName,
|
|
7808
|
+
base: baseBranch,
|
|
7809
|
+
title,
|
|
7810
|
+
body,
|
|
7811
|
+
draft: options.draft
|
|
7812
|
+
}) : await createGitLabRequest({
|
|
7813
|
+
cwd,
|
|
7814
|
+
repository: options.repository,
|
|
7815
|
+
source: branchName,
|
|
7816
|
+
target: baseBranch,
|
|
7817
|
+
title,
|
|
7818
|
+
body,
|
|
7819
|
+
draft: options.draft
|
|
7820
|
+
});
|
|
7821
|
+
results.push({
|
|
6354
7822
|
provider,
|
|
6355
7823
|
grouping,
|
|
6356
7824
|
repository: options.repository,
|
|
@@ -6362,11 +7830,13 @@ ${bodyFooter}` : plan.body;
|
|
|
6362
7830
|
url,
|
|
6363
7831
|
cveIds: plan.cveIds,
|
|
6364
7832
|
packageNames: plan.packageNames
|
|
7833
|
+
});
|
|
7834
|
+
if (groups.length > 1) {
|
|
7835
|
+
await execa10("git", ["checkout", baseBranch], { cwd, stdio: "pipe" }).catch(() => {
|
|
7836
|
+
});
|
|
6365
7837
|
}
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
return [
|
|
6369
|
-
{
|
|
7838
|
+
} catch (error) {
|
|
7839
|
+
results.push({
|
|
6370
7840
|
provider,
|
|
6371
7841
|
grouping,
|
|
6372
7842
|
repository: options.repository,
|
|
@@ -6378,9 +7848,10 @@ ${bodyFooter}` : plan.body;
|
|
|
6378
7848
|
cveIds: plan.cveIds,
|
|
6379
7849
|
packageNames: plan.packageNames,
|
|
6380
7850
|
error: error instanceof Error ? error.message : String(error)
|
|
6381
|
-
}
|
|
6382
|
-
|
|
7851
|
+
});
|
|
7852
|
+
}
|
|
6383
7853
|
}
|
|
7854
|
+
return results;
|
|
6384
7855
|
}
|
|
6385
7856
|
|
|
6386
7857
|
// src/api/context.ts
|
|
@@ -6408,13 +7879,17 @@ function resolveConstraints(options, cwd) {
|
|
|
6408
7879
|
installMode: options.constraints?.installMode ?? policy.constraints?.installMode ?? "deterministic",
|
|
6409
7880
|
installPreferOffline: options.constraints?.installPreferOffline ?? policy.constraints?.installPreferOffline,
|
|
6410
7881
|
enforceFrozenLockfile: options.constraints?.enforceFrozenLockfile ?? policy.constraints?.enforceFrozenLockfile,
|
|
6411
|
-
workspace: options.constraints?.workspace ?? policy.constraints?.workspace
|
|
7882
|
+
workspace: options.constraints?.workspace ?? policy.constraints?.workspace,
|
|
7883
|
+
typecheck: options.typecheck ?? options.constraints?.typecheck ?? policy.constraints?.typecheck,
|
|
7884
|
+
requireProofOfFix: options.requireProofOfFix ?? options.constraints?.requireProofOfFix ?? policy.constraints?.requireProofOfFix,
|
|
7885
|
+
enableBreakingChangeRefactor: options.breakingChangeRefactor ?? options.constraints?.enableBreakingChangeRefactor ?? policy.constraints?.enableBreakingChangeRefactor,
|
|
7886
|
+
supplyChainPolicy: options.supplyChainPolicy ?? options.constraints?.supplyChainPolicy ?? policy.constraints?.supplyChainPolicy
|
|
6412
7887
|
};
|
|
6413
7888
|
}
|
|
6414
7889
|
|
|
6415
7890
|
// src/platform/evidence.ts
|
|
6416
|
-
import { mkdirSync as mkdirSync3, writeFileSync as
|
|
6417
|
-
import { join as
|
|
7891
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
7892
|
+
import { join as join21 } from "path";
|
|
6418
7893
|
function createEvidenceLog(cwd, cveIds, context = {}) {
|
|
6419
7894
|
return {
|
|
6420
7895
|
runId: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
@@ -6446,10 +7921,10 @@ function finalizeEvidence(log) {
|
|
|
6446
7921
|
return log;
|
|
6447
7922
|
}
|
|
6448
7923
|
function writeEvidenceLog(cwd, log) {
|
|
6449
|
-
const dir =
|
|
7924
|
+
const dir = join21(cwd, ".autoremediator", "evidence");
|
|
6450
7925
|
mkdirSync3(dir, { recursive: true });
|
|
6451
|
-
const filePath =
|
|
6452
|
-
|
|
7926
|
+
const filePath = join21(dir, `${log.runId}.json`);
|
|
7927
|
+
writeFileSync6(filePath, JSON.stringify(log, null, 2) + "\n", "utf8");
|
|
6453
7928
|
return filePath;
|
|
6454
7929
|
}
|
|
6455
7930
|
|
|
@@ -6587,10 +8062,15 @@ function writeRemediateEvidence(cwd, evidence) {
|
|
|
6587
8062
|
}
|
|
6588
8063
|
|
|
6589
8064
|
// src/api/remediate/validators.ts
|
|
8065
|
+
var CVE_ID_REGEX = /^CVE-\d{4}-\d{1,7}$/i;
|
|
8066
|
+
var GHSA_ID_REGEX = /^GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$/i;
|
|
8067
|
+
function isVulnerabilityId(id) {
|
|
8068
|
+
return CVE_ID_REGEX.test(id) || GHSA_ID_REGEX.test(id);
|
|
8069
|
+
}
|
|
6590
8070
|
function assertValidCveId(cveId) {
|
|
6591
|
-
if (
|
|
8071
|
+
if (!isVulnerabilityId(cveId)) {
|
|
6592
8072
|
throw new Error(
|
|
6593
|
-
`Invalid
|
|
8073
|
+
`Invalid vulnerability ID: "${cveId}". Expected format: CVE-YYYY-NNNNN (e.g. CVE-2021-23337) or GHSA-xxxx-xxxx-xxxx.`
|
|
6594
8074
|
);
|
|
6595
8075
|
}
|
|
6596
8076
|
}
|
|
@@ -6686,13 +8166,14 @@ async function planRemediation(cveId, options = {}) {
|
|
|
6686
8166
|
}
|
|
6687
8167
|
|
|
6688
8168
|
// src/scanner/parse-input.ts
|
|
6689
|
-
import { extname, resolve as
|
|
6690
|
-
import { readFileSync as
|
|
6691
|
-
import { execa as
|
|
8169
|
+
import { extname, resolve as resolve5 } from "path";
|
|
8170
|
+
import { readFileSync as readFileSync19 } from "fs";
|
|
8171
|
+
import { execa as execa11 } from "execa";
|
|
6692
8172
|
|
|
6693
8173
|
// src/scanner/adapters/npm-audit.ts
|
|
6694
|
-
import { readFileSync as
|
|
8174
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
6695
8175
|
var CVE_REGEX = /CVE-\d{4}-\d{1,7}/gi;
|
|
8176
|
+
var GHSA_REGEX = /GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}/gi;
|
|
6696
8177
|
function normalizeSeverity(raw) {
|
|
6697
8178
|
if (!raw) return "UNKNOWN";
|
|
6698
8179
|
const up = raw.toUpperCase();
|
|
@@ -6707,7 +8188,8 @@ function parseNpmAuditJsonFromString(content) {
|
|
|
6707
8188
|
for (const vuln of Object.values(report.vulnerabilities ?? {})) {
|
|
6708
8189
|
for (const viaEntry of vuln.via ?? []) {
|
|
6709
8190
|
const text = typeof viaEntry === "string" ? viaEntry : `${viaEntry.url ?? ""} ${viaEntry.name ?? ""}`;
|
|
6710
|
-
const
|
|
8191
|
+
const cveMatches = text.match(CVE_REGEX) ?? [];
|
|
8192
|
+
const matches = cveMatches.length > 0 ? cveMatches : text.match(GHSA_REGEX) ?? [];
|
|
6711
8193
|
for (const match of matches) {
|
|
6712
8194
|
const cveId = match.toUpperCase();
|
|
6713
8195
|
const key = `${cveId}:${vuln.name}`;
|
|
@@ -6725,13 +8207,14 @@ function parseNpmAuditJsonFromString(content) {
|
|
|
6725
8207
|
return findings;
|
|
6726
8208
|
}
|
|
6727
8209
|
function parseNpmAuditJsonFile(filePath) {
|
|
6728
|
-
const content =
|
|
8210
|
+
const content = readFileSync16(filePath, "utf8");
|
|
6729
8211
|
return parseNpmAuditJsonFromString(content);
|
|
6730
8212
|
}
|
|
6731
8213
|
|
|
6732
8214
|
// src/scanner/adapters/yarn-audit.ts
|
|
6733
|
-
import { readFileSync as
|
|
8215
|
+
import { readFileSync as readFileSync17 } from "fs";
|
|
6734
8216
|
var CVE_REGEX2 = /CVE-\d{4}-\d{1,7}/gi;
|
|
8217
|
+
var GHSA_REGEX2 = /GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}/gi;
|
|
6735
8218
|
function normalizeSeverity2(raw) {
|
|
6736
8219
|
if (!raw) return "UNKNOWN";
|
|
6737
8220
|
const up = raw.toUpperCase();
|
|
@@ -6756,7 +8239,8 @@ function parseYarnAuditJsonFromString(content) {
|
|
|
6756
8239
|
const packageName = advisory?.module_name;
|
|
6757
8240
|
const severity = normalizeSeverity2(advisory?.severity);
|
|
6758
8241
|
const text = `${advisory?.url ?? ""} ${(advisory?.cves ?? []).join(" ")}`;
|
|
6759
|
-
const
|
|
8242
|
+
const cveMatches = text.match(CVE_REGEX2) ?? [];
|
|
8243
|
+
const matches = cveMatches.length > 0 ? cveMatches : text.match(GHSA_REGEX2) ?? [];
|
|
6760
8244
|
for (const match of matches) {
|
|
6761
8245
|
const cveId = match.toUpperCase();
|
|
6762
8246
|
const key = `${cveId}:${packageName ?? ""}`;
|
|
@@ -6773,12 +8257,12 @@ function parseYarnAuditJsonFromString(content) {
|
|
|
6773
8257
|
return findings;
|
|
6774
8258
|
}
|
|
6775
8259
|
function parseYarnAuditJsonFile(filePath) {
|
|
6776
|
-
const content =
|
|
8260
|
+
const content = readFileSync17(filePath, "utf8");
|
|
6777
8261
|
return parseYarnAuditJsonFromString(content);
|
|
6778
8262
|
}
|
|
6779
8263
|
|
|
6780
8264
|
// src/scanner/adapters/sarif.ts
|
|
6781
|
-
import { readFileSync as
|
|
8265
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
6782
8266
|
var CVE_REGEX3 = /CVE-\d{4}-\d{1,7}/gi;
|
|
6783
8267
|
function extractPackageName(result) {
|
|
6784
8268
|
const pkg = result.properties?.["packageName"];
|
|
@@ -6816,7 +8300,7 @@ function parseSarifFromString(content) {
|
|
|
6816
8300
|
return findings;
|
|
6817
8301
|
}
|
|
6818
8302
|
function parseSarifFile(filePath) {
|
|
6819
|
-
const content =
|
|
8303
|
+
const content = readFileSync18(filePath, "utf8");
|
|
6820
8304
|
return parseSarifFromString(content);
|
|
6821
8305
|
}
|
|
6822
8306
|
|
|
@@ -6825,7 +8309,7 @@ function parseScanInput(filePath, format) {
|
|
|
6825
8309
|
if (filePath.includes("\0")) {
|
|
6826
8310
|
throw new Error("Invalid scan input path: path contains null bytes");
|
|
6827
8311
|
}
|
|
6828
|
-
const resolvedPath =
|
|
8312
|
+
const resolvedPath = resolve5(filePath);
|
|
6829
8313
|
const resolved = format === "auto" ? inferFormat(resolvedPath) : format;
|
|
6830
8314
|
if (resolved === "npm-audit") {
|
|
6831
8315
|
return parseNpmAuditJsonFile(resolvedPath);
|
|
@@ -6847,7 +8331,7 @@ async function parseScanInputFromAudit(params) {
|
|
|
6847
8331
|
}
|
|
6848
8332
|
const command = resolveAuditCommand(pm, { workspace: params.workspace });
|
|
6849
8333
|
const [cmd, ...args] = command;
|
|
6850
|
-
const result = await
|
|
8334
|
+
const result = await execa11(cmd, args, {
|
|
6851
8335
|
cwd: params.cwd,
|
|
6852
8336
|
stdio: "pipe",
|
|
6853
8337
|
reject: false
|
|
@@ -6901,7 +8385,7 @@ function inferFormat(filePath) {
|
|
|
6901
8385
|
const ext = extname(filePath).toLowerCase();
|
|
6902
8386
|
if (ext === ".sarif") return "sarif";
|
|
6903
8387
|
try {
|
|
6904
|
-
const content =
|
|
8388
|
+
const content = readFileSync19(filePath, "utf8");
|
|
6905
8389
|
const firstLine = content.split("\n").find((line) => line.trim().startsWith("{"));
|
|
6906
8390
|
if (firstLine) {
|
|
6907
8391
|
const parsed = JSON.parse(firstLine);
|
|
@@ -7312,14 +8796,14 @@ async function remediatePortfolio(targets, options = {}) {
|
|
|
7312
8796
|
}
|
|
7313
8797
|
|
|
7314
8798
|
// src/api/update-outdated/index.ts
|
|
7315
|
-
import { join as
|
|
7316
|
-
import { readFileSync as
|
|
7317
|
-
import { execa as
|
|
8799
|
+
import { join as join22 } from "path";
|
|
8800
|
+
import { readFileSync as readFileSync20, writeFileSync as writeFileSync7 } from "fs";
|
|
8801
|
+
import { execa as execa12 } from "execa";
|
|
7318
8802
|
async function applyBump(params) {
|
|
7319
|
-
const pkgPath =
|
|
8803
|
+
const pkgPath = join22(params.cwd, "package.json");
|
|
7320
8804
|
let pkgJson;
|
|
7321
8805
|
try {
|
|
7322
|
-
pkgJson = JSON.parse(
|
|
8806
|
+
pkgJson = JSON.parse(readFileSync20(pkgPath, "utf8"));
|
|
7323
8807
|
} catch {
|
|
7324
8808
|
return {
|
|
7325
8809
|
applied: false,
|
|
@@ -7341,13 +8825,13 @@ async function applyBump(params) {
|
|
|
7341
8825
|
const newRange = `${prefix}${params.toVersion}`;
|
|
7342
8826
|
return withRepoLock(params.cwd, async () => {
|
|
7343
8827
|
pkgJson[depField][params.packageName] = newRange;
|
|
7344
|
-
|
|
8828
|
+
writeFileSync7(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
7345
8829
|
try {
|
|
7346
8830
|
const [installCmd, ...installArgs] = params.installCommand;
|
|
7347
|
-
await
|
|
8831
|
+
await execa12(installCmd, installArgs, { cwd: params.cwd, stdio: "pipe" });
|
|
7348
8832
|
} catch (err) {
|
|
7349
8833
|
pkgJson[depField][params.packageName] = currentRange;
|
|
7350
|
-
|
|
8834
|
+
writeFileSync7(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
7351
8835
|
const message = err instanceof Error ? err.message : String(err);
|
|
7352
8836
|
return {
|
|
7353
8837
|
applied: false,
|
|
@@ -7357,13 +8841,13 @@ async function applyBump(params) {
|
|
|
7357
8841
|
if (params.runTests) {
|
|
7358
8842
|
try {
|
|
7359
8843
|
const [testCmd, ...testArgs] = params.testCommand;
|
|
7360
|
-
await
|
|
8844
|
+
await execa12(testCmd, testArgs, { cwd: params.cwd, stdio: "pipe" });
|
|
7361
8845
|
} catch (err) {
|
|
7362
8846
|
pkgJson[depField][params.packageName] = currentRange;
|
|
7363
|
-
|
|
8847
|
+
writeFileSync7(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
7364
8848
|
try {
|
|
7365
8849
|
const [rollbackCmd, ...rollbackArgs] = params.installCommand;
|
|
7366
|
-
await
|
|
8850
|
+
await execa12(rollbackCmd, rollbackArgs, { cwd: params.cwd, stdio: "pipe" });
|
|
7367
8851
|
} catch {
|
|
7368
8852
|
}
|
|
7369
8853
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -7564,131 +9048,169 @@ async function checkReachability(options) {
|
|
|
7564
9048
|
return assessPackageReachability(cwd, options.packageName, options.symbol);
|
|
7565
9049
|
}
|
|
7566
9050
|
|
|
7567
|
-
// src/
|
|
7568
|
-
import
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
}
|
|
7588
|
-
|
|
7589
|
-
if (enrich) {
|
|
9051
|
+
// src/scanner/delta-scan.ts
|
|
9052
|
+
import { existsSync as existsSync16, readFileSync as readFileSync21 } from "fs";
|
|
9053
|
+
import { join as join23 } from "path";
|
|
9054
|
+
import { execa as execa13 } from "execa";
|
|
9055
|
+
import semver9 from "semver";
|
|
9056
|
+
|
|
9057
|
+
// src/scanner/lockfile-diff.ts
|
|
9058
|
+
import YAML2 from "yaml";
|
|
9059
|
+
import semver8 from "semver";
|
|
9060
|
+
function parseLockfileDependencies(filename, content) {
|
|
9061
|
+
const result = /* @__PURE__ */ new Map();
|
|
9062
|
+
const add = (name, ver) => {
|
|
9063
|
+
if (!name || !ver) return;
|
|
9064
|
+
const cleanVer = semver8.clean(ver) || semver8.coerce(ver)?.version || ver;
|
|
9065
|
+
let set = result.get(name);
|
|
9066
|
+
if (!set) {
|
|
9067
|
+
set = /* @__PURE__ */ new Set();
|
|
9068
|
+
result.set(name, set);
|
|
9069
|
+
}
|
|
9070
|
+
set.add(cleanVer);
|
|
9071
|
+
};
|
|
9072
|
+
if (filename.endsWith("package-lock.json")) {
|
|
7590
9073
|
try {
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
9074
|
+
const data = JSON.parse(content);
|
|
9075
|
+
if (data.packages && typeof data.packages === "object") {
|
|
9076
|
+
for (const [key, pkg] of Object.entries(data.packages)) {
|
|
9077
|
+
if (!key || key === "" || typeof pkg !== "object" || !pkg) continue;
|
|
9078
|
+
const idx = key.lastIndexOf("node_modules/");
|
|
9079
|
+
if (idx !== -1) {
|
|
9080
|
+
const rawName = key.slice(idx + "node_modules/".length);
|
|
9081
|
+
const ver = pkg.version;
|
|
9082
|
+
add(rawName, ver);
|
|
9083
|
+
}
|
|
9084
|
+
}
|
|
9085
|
+
}
|
|
9086
|
+
if (data.dependencies && typeof data.dependencies === "object") {
|
|
9087
|
+
const walkDeps = (deps) => {
|
|
9088
|
+
for (const [name, info] of Object.entries(deps)) {
|
|
9089
|
+
if (!info || typeof info !== "object") continue;
|
|
9090
|
+
const depObj = info;
|
|
9091
|
+
add(name, depObj.version);
|
|
9092
|
+
if (depObj.dependencies && typeof depObj.dependencies === "object") {
|
|
9093
|
+
walkDeps(depObj.dependencies);
|
|
9094
|
+
}
|
|
9095
|
+
}
|
|
9096
|
+
};
|
|
9097
|
+
walkDeps(data.dependencies);
|
|
7606
9098
|
}
|
|
9099
|
+
} catch {
|
|
7607
9100
|
}
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
9101
|
+
} else if (filename.endsWith("pnpm-lock.yaml")) {
|
|
9102
|
+
try {
|
|
9103
|
+
const data = YAML2.parse(content);
|
|
9104
|
+
if (data?.packages && typeof data.packages === "object") {
|
|
9105
|
+
for (const [key, val] of Object.entries(data.packages)) {
|
|
9106
|
+
let rawKey = key.replace(/^\//, "");
|
|
9107
|
+
const parenIdx = rawKey.indexOf("(");
|
|
9108
|
+
if (parenIdx !== -1) {
|
|
9109
|
+
rawKey = rawKey.slice(0, parenIdx);
|
|
9110
|
+
}
|
|
9111
|
+
const lastAt = rawKey.lastIndexOf("@");
|
|
9112
|
+
if (lastAt > 0) {
|
|
9113
|
+
const name = val?.name ?? rawKey.slice(0, lastAt);
|
|
9114
|
+
const version = val?.version ?? rawKey.slice(lastAt + 1);
|
|
9115
|
+
add(name, version);
|
|
9116
|
+
}
|
|
9117
|
+
}
|
|
7614
9118
|
}
|
|
7615
|
-
|
|
7616
|
-
const
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
9119
|
+
if (data?.snapshots && typeof data.snapshots === "object") {
|
|
9120
|
+
for (const key of Object.keys(data.snapshots)) {
|
|
9121
|
+
let rawKey = key.replace(/^\//, "");
|
|
9122
|
+
const parenIdx = rawKey.indexOf("(");
|
|
9123
|
+
if (parenIdx !== -1) {
|
|
9124
|
+
rawKey = rawKey.slice(0, parenIdx);
|
|
9125
|
+
}
|
|
9126
|
+
const lastAt = rawKey.lastIndexOf("@");
|
|
9127
|
+
if (lastAt > 0) {
|
|
9128
|
+
const name = rawKey.slice(0, lastAt);
|
|
9129
|
+
const version = rawKey.slice(lastAt + 1);
|
|
9130
|
+
add(name, version);
|
|
9131
|
+
}
|
|
7620
9132
|
}
|
|
7621
|
-
} catch {
|
|
7622
9133
|
}
|
|
9134
|
+
} catch {
|
|
7623
9135
|
}
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
);
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
9136
|
+
} else if (filename.endsWith("yarn.lock")) {
|
|
9137
|
+
const lines = content.split("\n");
|
|
9138
|
+
let currentPackages = [];
|
|
9139
|
+
for (const line of lines) {
|
|
9140
|
+
if (line.startsWith("#") || line.trim() === "") continue;
|
|
9141
|
+
if (!line.startsWith(" ")) {
|
|
9142
|
+
currentPackages = [];
|
|
9143
|
+
const parts = line.split(",").map((p) => p.trim().replace(/^"/, "").replace(/":?$/, ""));
|
|
9144
|
+
for (const part of parts) {
|
|
9145
|
+
const lastAt = part.lastIndexOf("@");
|
|
9146
|
+
if (lastAt > 0) {
|
|
9147
|
+
currentPackages.push(part.slice(0, lastAt));
|
|
9148
|
+
}
|
|
9149
|
+
}
|
|
9150
|
+
} else {
|
|
9151
|
+
const match = line.match(/^\s+version\s+"?([^"\s]+)"?/);
|
|
9152
|
+
if (match && currentPackages.length > 0) {
|
|
9153
|
+
const ver = match[1];
|
|
9154
|
+
for (const pkg of currentPackages) {
|
|
9155
|
+
add(pkg, ver);
|
|
9156
|
+
}
|
|
9157
|
+
currentPackages = [];
|
|
9158
|
+
}
|
|
7635
9159
|
}
|
|
7636
9160
|
}
|
|
7637
|
-
entries.push({
|
|
7638
|
-
cveId: cveDetails.id,
|
|
7639
|
-
severity: cveDetails.severity,
|
|
7640
|
-
summary: cveDetails.summary,
|
|
7641
|
-
vulnerableRange: affected?.vulnerableRange,
|
|
7642
|
-
firstPatchedVersion: affected?.firstPatchedVersion,
|
|
7643
|
-
safeUpgradeVersion,
|
|
7644
|
-
epssScore,
|
|
7645
|
-
epssPercentile,
|
|
7646
|
-
inCisaKev,
|
|
7647
|
-
references: cveDetails.references
|
|
7648
|
-
});
|
|
7649
9161
|
}
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
9162
|
+
return result;
|
|
9163
|
+
}
|
|
9164
|
+
function computeLockfileDiff(filename, oldContent, newContent) {
|
|
9165
|
+
const changes = [];
|
|
9166
|
+
const oldDeps = oldContent ? parseLockfileDependencies(filename, oldContent) : /* @__PURE__ */ new Map();
|
|
9167
|
+
const newDeps = newContent ? parseLockfileDependencies(filename, newContent) : /* @__PURE__ */ new Map();
|
|
9168
|
+
const allPkgs = /* @__PURE__ */ new Set([...oldDeps.keys(), ...newDeps.keys()]);
|
|
9169
|
+
for (const pkg of allPkgs) {
|
|
9170
|
+
const oldVers = oldDeps.get(pkg) ?? /* @__PURE__ */ new Set();
|
|
9171
|
+
const newVers = newDeps.get(pkg) ?? /* @__PURE__ */ new Set();
|
|
9172
|
+
for (const nVer of newVers) {
|
|
9173
|
+
if (!oldVers.has(nVer)) {
|
|
9174
|
+
if (oldVers.size === 0) {
|
|
9175
|
+
changes.push({
|
|
9176
|
+
packageName: pkg,
|
|
9177
|
+
changeType: "added",
|
|
9178
|
+
newVersion: nVer,
|
|
9179
|
+
dependencyType: "transitive",
|
|
9180
|
+
manifestPath: filename
|
|
9181
|
+
});
|
|
9182
|
+
} else {
|
|
9183
|
+
const [firstOld] = oldVers;
|
|
9184
|
+
const cleanOld = semver8.clean(firstOld) || semver8.coerce(firstOld)?.version;
|
|
9185
|
+
const cleanNew = semver8.clean(nVer) || semver8.coerce(nVer)?.version;
|
|
9186
|
+
const isUpgrade = cleanOld && cleanNew && semver8.valid(cleanOld) && semver8.valid(cleanNew) ? semver8.gt(cleanNew, cleanOld) : true;
|
|
9187
|
+
changes.push({
|
|
9188
|
+
packageName: pkg,
|
|
9189
|
+
changeType: isUpgrade ? "upgraded" : "downgraded",
|
|
9190
|
+
oldVersion: firstOld,
|
|
9191
|
+
newVersion: nVer,
|
|
9192
|
+
dependencyType: "transitive",
|
|
9193
|
+
manifestPath: filename
|
|
9194
|
+
});
|
|
9195
|
+
}
|
|
9196
|
+
}
|
|
9197
|
+
}
|
|
9198
|
+
if (newVers.size === 0 && oldVers.size > 0) {
|
|
9199
|
+
for (const oVer of oldVers) {
|
|
9200
|
+
changes.push({
|
|
9201
|
+
packageName: pkg,
|
|
9202
|
+
changeType: "removed",
|
|
9203
|
+
oldVersion: oVer,
|
|
9204
|
+
dependencyType: "transitive",
|
|
9205
|
+
manifestPath: filename
|
|
9206
|
+
});
|
|
9207
|
+
}
|
|
7663
9208
|
}
|
|
7664
9209
|
}
|
|
7665
|
-
|
|
7666
|
-
if (isVulnerable) {
|
|
7667
|
-
const upgradeCandidates = entries.map((e) => e.safeUpgradeVersion).filter((v) => Boolean(v && semver7.valid(v))).sort((a, b) => semver7.compare(a, b));
|
|
7668
|
-
recommendedVersion = upgradeCandidates[upgradeCandidates.length - 1] || latestVersion;
|
|
7669
|
-
} else {
|
|
7670
|
-
recommendedVersion = version || latestVersion;
|
|
7671
|
-
}
|
|
7672
|
-
const summary = isVulnerable ? `Package "${packageName}${version ? `@${version}` : ""}" has ${entries.length} known vulnerability(ies) (verdict: ${verdict}). Recommended safe version: ${recommendedVersion || "none"}.` : `Package "${packageName}${version ? `@${version}` : ""}" has no known vulnerabilities. Safe to install.`;
|
|
7673
|
-
return {
|
|
7674
|
-
schemaVersion: "1.0",
|
|
7675
|
-
packageName,
|
|
7676
|
-
evaluatedVersion: version,
|
|
7677
|
-
isVulnerable,
|
|
7678
|
-
verdict,
|
|
7679
|
-
summary,
|
|
7680
|
-
vulnerabilities: entries,
|
|
7681
|
-
recommendedVersion,
|
|
7682
|
-
latestVersion,
|
|
7683
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7684
|
-
};
|
|
9210
|
+
return changes;
|
|
7685
9211
|
}
|
|
7686
9212
|
|
|
7687
9213
|
// src/scanner/delta-scan.ts
|
|
7688
|
-
import { existsSync as existsSync12, readFileSync as readFileSync17 } from "fs";
|
|
7689
|
-
import { join as join20 } from "path";
|
|
7690
|
-
import { execa as execa12 } from "execa";
|
|
7691
|
-
import semver8 from "semver";
|
|
7692
9214
|
var DEP_SECTIONS = [
|
|
7693
9215
|
"dependencies",
|
|
7694
9216
|
"devDependencies",
|
|
@@ -7698,7 +9220,7 @@ var DEP_SECTIONS = [
|
|
|
7698
9220
|
async function getChangedManifestPaths(cwd, baseRef) {
|
|
7699
9221
|
const manifests = /* @__PURE__ */ new Set();
|
|
7700
9222
|
try {
|
|
7701
|
-
const { stdout: diffFiles } = await
|
|
9223
|
+
const { stdout: diffFiles } = await execa13(
|
|
7702
9224
|
"git",
|
|
7703
9225
|
["diff", "--name-only", baseRef, "--", "package.json", "**/package.json"],
|
|
7704
9226
|
{ cwd, reject: false }
|
|
@@ -7709,7 +9231,7 @@ async function getChangedManifestPaths(cwd, baseRef) {
|
|
|
7709
9231
|
manifests.add(trimmed);
|
|
7710
9232
|
}
|
|
7711
9233
|
}
|
|
7712
|
-
const { stdout: untrackedFiles } = await
|
|
9234
|
+
const { stdout: untrackedFiles } = await execa13(
|
|
7713
9235
|
"git",
|
|
7714
9236
|
["ls-files", "--others", "--exclude-standard", "--", "package.json", "**/package.json"],
|
|
7715
9237
|
{ cwd, reject: false }
|
|
@@ -7721,7 +9243,7 @@ async function getChangedManifestPaths(cwd, baseRef) {
|
|
|
7721
9243
|
}
|
|
7722
9244
|
}
|
|
7723
9245
|
} catch {
|
|
7724
|
-
if (
|
|
9246
|
+
if (existsSync16(join23(cwd, "package.json"))) {
|
|
7725
9247
|
manifests.add("package.json");
|
|
7726
9248
|
}
|
|
7727
9249
|
}
|
|
@@ -7729,7 +9251,7 @@ async function getChangedManifestPaths(cwd, baseRef) {
|
|
|
7729
9251
|
}
|
|
7730
9252
|
async function getOldManifestContent(cwd, baseRef, relPath) {
|
|
7731
9253
|
try {
|
|
7732
|
-
const { stdout } = await
|
|
9254
|
+
const { stdout } = await execa13("git", ["show", `${baseRef}:${relPath}`], {
|
|
7733
9255
|
cwd,
|
|
7734
9256
|
reject: false
|
|
7735
9257
|
});
|
|
@@ -7740,10 +9262,10 @@ async function getOldManifestContent(cwd, baseRef, relPath) {
|
|
|
7740
9262
|
}
|
|
7741
9263
|
}
|
|
7742
9264
|
function getNewManifestContent(cwd, relPath) {
|
|
7743
|
-
const fullPath =
|
|
7744
|
-
if (!
|
|
9265
|
+
const fullPath = join23(cwd, relPath);
|
|
9266
|
+
if (!existsSync16(fullPath)) return null;
|
|
7745
9267
|
try {
|
|
7746
|
-
return JSON.parse(
|
|
9268
|
+
return JSON.parse(readFileSync21(fullPath, "utf8"));
|
|
7747
9269
|
} catch {
|
|
7748
9270
|
return null;
|
|
7749
9271
|
}
|
|
@@ -7764,9 +9286,9 @@ function computeDependencyDiff(relPath, oldPkg, newPkg) {
|
|
|
7764
9286
|
manifestPath: relPath
|
|
7765
9287
|
});
|
|
7766
9288
|
} else if (oldVer !== newVer) {
|
|
7767
|
-
const cleanOld =
|
|
7768
|
-
const cleanNew =
|
|
7769
|
-
const isUpgrade = cleanOld && cleanNew &&
|
|
9289
|
+
const cleanOld = semver9.clean(oldVer) || semver9.coerce(oldVer)?.version;
|
|
9290
|
+
const cleanNew = semver9.clean(newVer) || semver9.coerce(newVer)?.version;
|
|
9291
|
+
const isUpgrade = cleanOld && cleanNew && semver9.valid(cleanOld) && semver9.valid(cleanNew) ? semver9.gt(cleanNew, cleanOld) : true;
|
|
7770
9292
|
changes.push({
|
|
7771
9293
|
packageName: pkgName,
|
|
7772
9294
|
changeType: isUpgrade ? "upgraded" : "downgraded",
|
|
@@ -7791,22 +9313,89 @@ function computeDependencyDiff(relPath, oldPkg, newPkg) {
|
|
|
7791
9313
|
}
|
|
7792
9314
|
return changes;
|
|
7793
9315
|
}
|
|
9316
|
+
var LOCKFILE_PATTERNS = [
|
|
9317
|
+
"package-lock.json",
|
|
9318
|
+
"**/package-lock.json",
|
|
9319
|
+
"pnpm-lock.yaml",
|
|
9320
|
+
"**/pnpm-lock.yaml",
|
|
9321
|
+
"yarn.lock",
|
|
9322
|
+
"**/yarn.lock"
|
|
9323
|
+
];
|
|
9324
|
+
async function getChangedLockfilePaths(cwd, baseRef) {
|
|
9325
|
+
const lockfiles = /* @__PURE__ */ new Set();
|
|
9326
|
+
try {
|
|
9327
|
+
const { stdout: diffFiles } = await execa13(
|
|
9328
|
+
"git",
|
|
9329
|
+
["diff", "--name-only", baseRef, "--", ...LOCKFILE_PATTERNS],
|
|
9330
|
+
{ cwd, reject: false }
|
|
9331
|
+
);
|
|
9332
|
+
for (const f of diffFiles.split("\n")) {
|
|
9333
|
+
const trimmed = f.trim();
|
|
9334
|
+
if (trimmed && !trimmed.includes("node_modules") && (trimmed.endsWith("package-lock.json") || trimmed.endsWith("pnpm-lock.yaml") || trimmed.endsWith("yarn.lock"))) {
|
|
9335
|
+
lockfiles.add(trimmed);
|
|
9336
|
+
}
|
|
9337
|
+
}
|
|
9338
|
+
const { stdout: untrackedFiles } = await execa13(
|
|
9339
|
+
"git",
|
|
9340
|
+
["ls-files", "--others", "--exclude-standard", "--", ...LOCKFILE_PATTERNS],
|
|
9341
|
+
{ cwd, reject: false }
|
|
9342
|
+
);
|
|
9343
|
+
for (const f of untrackedFiles.split("\n")) {
|
|
9344
|
+
const trimmed = f.trim();
|
|
9345
|
+
if (trimmed && !trimmed.includes("node_modules") && (trimmed.endsWith("package-lock.json") || trimmed.endsWith("pnpm-lock.yaml") || trimmed.endsWith("yarn.lock"))) {
|
|
9346
|
+
lockfiles.add(trimmed);
|
|
9347
|
+
}
|
|
9348
|
+
}
|
|
9349
|
+
} catch {
|
|
9350
|
+
}
|
|
9351
|
+
return Array.from(lockfiles);
|
|
9352
|
+
}
|
|
9353
|
+
async function getFileContentAtRef(cwd, baseRef, relPath) {
|
|
9354
|
+
try {
|
|
9355
|
+
const { stdout } = await execa13("git", ["show", `${baseRef}:${relPath}`], {
|
|
9356
|
+
cwd,
|
|
9357
|
+
reject: false
|
|
9358
|
+
});
|
|
9359
|
+
return stdout || null;
|
|
9360
|
+
} catch {
|
|
9361
|
+
return null;
|
|
9362
|
+
}
|
|
9363
|
+
}
|
|
7794
9364
|
async function scanDelta(options = {}) {
|
|
7795
9365
|
const cwd = options.cwd ?? process.cwd();
|
|
7796
9366
|
const baseRef = options.baseRef ?? "HEAD";
|
|
7797
9367
|
const changedManifests = await getChangedManifestPaths(cwd, baseRef);
|
|
9368
|
+
const changedLockfiles = await getChangedLockfilePaths(cwd, baseRef);
|
|
7798
9369
|
const dependencyChanges = [];
|
|
9370
|
+
const directPkgs = /* @__PURE__ */ new Set();
|
|
7799
9371
|
for (const manifestRel of changedManifests) {
|
|
7800
9372
|
const oldPkg = await getOldManifestContent(cwd, baseRef, manifestRel);
|
|
7801
9373
|
const newPkg = getNewManifestContent(cwd, manifestRel);
|
|
7802
9374
|
const diffs = computeDependencyDiff(manifestRel, oldPkg, newPkg);
|
|
9375
|
+
for (const d of diffs) {
|
|
9376
|
+
directPkgs.add(d.packageName);
|
|
9377
|
+
}
|
|
7803
9378
|
dependencyChanges.push(...diffs);
|
|
7804
9379
|
}
|
|
9380
|
+
for (const lockfileRel of changedLockfiles) {
|
|
9381
|
+
const oldContent = await getFileContentAtRef(cwd, baseRef, lockfileRel);
|
|
9382
|
+
const fullPath = join23(cwd, lockfileRel);
|
|
9383
|
+
const newContent = existsSync16(fullPath) ? readFileSync21(fullPath, "utf8") : null;
|
|
9384
|
+
const lockDiffs = computeLockfileDiff(lockfileRel, oldContent, newContent);
|
|
9385
|
+
for (const change of lockDiffs) {
|
|
9386
|
+
if (!directPkgs.has(change.packageName)) {
|
|
9387
|
+
dependencyChanges.push(change);
|
|
9388
|
+
}
|
|
9389
|
+
}
|
|
9390
|
+
if (!changedManifests.includes(lockfileRel)) {
|
|
9391
|
+
changedManifests.push(lockfileRel);
|
|
9392
|
+
}
|
|
9393
|
+
}
|
|
7805
9394
|
const introducedFindings = [];
|
|
7806
9395
|
const resolvedFindings = [];
|
|
7807
9396
|
for (const change of dependencyChanges) {
|
|
7808
9397
|
if (change.changeType === "added" || change.changeType === "upgraded" || change.changeType === "downgraded") {
|
|
7809
|
-
const cleanVer =
|
|
9398
|
+
const cleanVer = semver9.coerce(change.newVersion)?.version ?? change.newVersion;
|
|
7810
9399
|
const evaluation = await evaluatePackage(change.packageName, {
|
|
7811
9400
|
version: cleanVer,
|
|
7812
9401
|
enrichIntelligence: true
|
|
@@ -7826,12 +9415,12 @@ async function scanDelta(options = {}) {
|
|
|
7826
9415
|
}
|
|
7827
9416
|
}
|
|
7828
9417
|
if (change.oldVersion && (change.changeType === "upgraded" || change.changeType === "removed")) {
|
|
7829
|
-
const cleanOldVer =
|
|
9418
|
+
const cleanOldVer = semver9.coerce(change.oldVersion)?.version ?? change.oldVersion;
|
|
7830
9419
|
const oldEval = await evaluatePackage(change.packageName, {
|
|
7831
9420
|
version: cleanOldVer,
|
|
7832
9421
|
enrichIntelligence: true
|
|
7833
9422
|
});
|
|
7834
|
-
const cleanNewVer = change.newVersion ?
|
|
9423
|
+
const cleanNewVer = change.newVersion ? semver9.coerce(change.newVersion)?.version ?? change.newVersion : void 0;
|
|
7835
9424
|
const newEval = cleanNewVer ? await evaluatePackage(change.packageName, {
|
|
7836
9425
|
version: cleanNewVer,
|
|
7837
9426
|
enrichIntelligence: false
|
|
@@ -7949,6 +9538,7 @@ export {
|
|
|
7949
9538
|
listPatchArtifacts,
|
|
7950
9539
|
planRemediation,
|
|
7951
9540
|
pollJob,
|
|
9541
|
+
reconcilePatchArtifacts,
|
|
7952
9542
|
remediate,
|
|
7953
9543
|
remediateFromScan,
|
|
7954
9544
|
remediatePortfolio,
|