autoremediator 0.16.0 → 0.18.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 +2 -1
- package/dist/api/change-request/index.d.ts +2 -1
- package/dist/api/change-request/index.d.ts.map +1 -1
- package/dist/api/delta-scan/delta-scan.test.d.ts +2 -0
- package/dist/api/delta-scan/delta-scan.test.d.ts.map +1 -0
- package/dist/api/delta-scan/index.d.ts +5 -0
- package/dist/api/delta-scan/index.d.ts.map +1 -0
- package/dist/api/evaluate/evaluate.test.d.ts +2 -0
- package/dist/api/evaluate/evaluate.test.d.ts.map +1 -0
- package/dist/api/evaluate/index.d.ts +5 -0
- package/dist/api/evaluate/index.d.ts.map +1 -0
- package/dist/api/index.d.ts +5 -2
- package/dist/api/index.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/reachability/index.d.ts +3 -0
- package/dist/api/reachability/index.d.ts.map +1 -0
- package/dist/api/reachability/reachability.test.d.ts +2 -0
- package/dist/api/reachability/reachability.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/api/reporting.d.ts.map +1 -1
- package/dist/api/vex/index.d.ts +1 -1
- package/dist/api/vex/index.d.ts.map +1 -1
- package/dist/{chunk-CQXNIRHT.js → chunk-FXOUJDQ3.js} +2003 -756
- package/dist/cli/program.d.ts.map +1 -1
- package/dist/cli/runners.d.ts +22 -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 +2 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli.js +365 -3
- package/dist/index.js +1967 -721
- package/dist/index.js.map +1 -1
- package/dist/intelligence/index.d.ts +1 -1
- package/dist/intelligence/index.d.ts.map +1 -1
- package/dist/intelligence/sources/github-advisory.d.ts +8 -0
- package/dist/intelligence/sources/github-advisory.d.ts.map +1 -1
- package/dist/intelligence/sources/osv.d.ts +4 -0
- package/dist/intelligence/sources/osv.d.ts.map +1 -1
- package/dist/mcp/server.d.ts +106 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +100 -1
- package/dist/openapi/rbac.d.ts +2 -2
- package/dist/openapi/rbac.d.ts.map +1 -1
- package/dist/openapi/server.d.ts.map +1 -1
- package/dist/openapi/server.js +13 -6
- 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 +121 -4
- 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/evaluate-package.d.ts +3 -0
- package/dist/remediation/evaluate-package.d.ts.map +1 -0
- package/dist/remediation/local/options.d.ts.map +1 -1
- package/dist/remediation/local/primary-strategy.d.ts +3 -1
- package/dist/remediation/local/primary-strategy.d.ts.map +1 -1
- package/dist/remediation/runtime-tools.d.ts +1 -1
- package/dist/remediation/runtime-tools.d.ts.map +1 -1
- package/dist/remediation/strategies/patch-synthesis-prompt.d.ts.map +1 -1
- package/dist/remediation/tools/apply-patch-file/helpers.d.ts.map +1 -1
- package/dist/remediation/tools/apply-patch-file/index.d.ts +1 -0
- package/dist/remediation/tools/apply-patch-file/index.d.ts.map +1 -1
- package/dist/remediation/tools/apply-version-bump.d.ts +1 -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 +3 -0
- package/dist/scanner/delta-scan.d.ts.map +1 -0
- 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 +7 -9
package/dist/index.js
CHANGED
|
@@ -534,6 +534,24 @@ function buildResultSimulation(report, result, options) {
|
|
|
534
534
|
const dependencyScope = result.dependencyScope ?? dependencyScopeLookup.get(result.packageName);
|
|
535
535
|
const plannedMutations = buildPlannedMutations(result, options.runTests === true);
|
|
536
536
|
const wouldMutate = plannedMutations.length > 0;
|
|
537
|
+
let unifiedDiff;
|
|
538
|
+
if (result.patchArtifact?.patchFilePath) {
|
|
539
|
+
unifiedDiff = `--- ${result.patchArtifact.patchFilePath}
|
|
540
|
+
+++ ${result.patchArtifact.patchFilePath}`;
|
|
541
|
+
} else if (result.strategy === "version-bump" && result.toVersion) {
|
|
542
|
+
unifiedDiff = `--- package.json
|
|
543
|
+
+++ package.json
|
|
544
|
+
@@ -1,3 +1,3 @@
|
|
545
|
+
- "${result.packageName}": "${result.fromVersion}"
|
|
546
|
+
+ "${result.packageName}": "^${result.toVersion}"`;
|
|
547
|
+
} else if (result.strategy === "override" && result.toVersion) {
|
|
548
|
+
unifiedDiff = `--- package.json
|
|
549
|
+
+++ package.json
|
|
550
|
+
@@ -1,3 +1,3 @@
|
|
551
|
+
+ "overrides": {
|
|
552
|
+
+ "${result.packageName}": "${result.toVersion}"
|
|
553
|
+
+ }`;
|
|
554
|
+
}
|
|
537
555
|
return {
|
|
538
556
|
mode,
|
|
539
557
|
wouldMutate,
|
|
@@ -544,7 +562,8 @@ function buildResultSimulation(report, result, options) {
|
|
|
544
562
|
dependencyScope,
|
|
545
563
|
wouldMutate,
|
|
546
564
|
runTests: options.runTests === true
|
|
547
|
-
})
|
|
565
|
+
}),
|
|
566
|
+
unifiedDiff
|
|
548
567
|
};
|
|
549
568
|
}
|
|
550
569
|
function buildSimulationSummary(reports) {
|
|
@@ -868,12 +887,12 @@ import { readdir, readFile as readFile2, stat } from "fs/promises";
|
|
|
868
887
|
import { join as join5 } from "path";
|
|
869
888
|
|
|
870
889
|
// src/remediation/strategies/patch-utils.ts
|
|
871
|
-
import { existsSync as existsSync3, mkdirSync, writeFileSync, readFileSync as
|
|
890
|
+
import { existsSync as existsSync3, mkdirSync, writeFileSync, readFileSync as readFileSync3 } from "fs";
|
|
872
891
|
import { join as join4 } from "path";
|
|
873
892
|
import { execa as execa2 } from "execa";
|
|
874
893
|
|
|
875
894
|
// src/platform/package-manager/index.ts
|
|
876
|
-
import { existsSync as existsSync2 } from "fs";
|
|
895
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
877
896
|
import { join as join3 } from "path";
|
|
878
897
|
import { execa } from "execa";
|
|
879
898
|
|
|
@@ -1066,6 +1085,32 @@ function resolveTestCommand(pm, constraints) {
|
|
|
1066
1085
|
const base = pm === "pnpm" ? ["pnpm", "test"] : pm === "yarn" ? ["yarn", "test"] : pm === "bun" ? ["bun", "test"] : pm === "deno" ? ["deno", "test"] : ["npm", "test"];
|
|
1067
1086
|
return withWorkspace(base, pm, constraints?.workspace);
|
|
1068
1087
|
}
|
|
1088
|
+
function resolveTypecheckCommand(pm, constraints, cwd = process.cwd()) {
|
|
1089
|
+
const pkgJsonPath = join3(cwd, "package.json");
|
|
1090
|
+
let hasTypecheckScript = false;
|
|
1091
|
+
const hasTsConfig = existsSync2(join3(cwd, "tsconfig.json"));
|
|
1092
|
+
if (existsSync2(pkgJsonPath)) {
|
|
1093
|
+
try {
|
|
1094
|
+
const pkg = JSON.parse(readFileSync2(pkgJsonPath, "utf8"));
|
|
1095
|
+
if (pkg.scripts?.typecheck) {
|
|
1096
|
+
hasTypecheckScript = true;
|
|
1097
|
+
}
|
|
1098
|
+
} catch {
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (hasTypecheckScript) {
|
|
1102
|
+
const base = [pm, "run", "typecheck"];
|
|
1103
|
+
return withWorkspace(base, pm, constraints?.workspace);
|
|
1104
|
+
}
|
|
1105
|
+
if (hasTsConfig) {
|
|
1106
|
+
if (pm === "deno") {
|
|
1107
|
+
return withWorkspace(["deno", "check"], pm, constraints?.workspace);
|
|
1108
|
+
}
|
|
1109
|
+
const base = ["npx", "tsc", "--noEmit"];
|
|
1110
|
+
return withWorkspace(base, pm, constraints?.workspace);
|
|
1111
|
+
}
|
|
1112
|
+
return [];
|
|
1113
|
+
}
|
|
1069
1114
|
function resolveAuditCommand(pm, constraints) {
|
|
1070
1115
|
if (pm === "deno") {
|
|
1071
1116
|
throw new Error(
|
|
@@ -1310,12 +1355,12 @@ function defineTool(config) {
|
|
|
1310
1355
|
|
|
1311
1356
|
// src/remediation/tools/check-inventory.ts
|
|
1312
1357
|
import { z } from "zod";
|
|
1313
|
-
import { readFileSync as
|
|
1314
|
-
import { join as
|
|
1358
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1359
|
+
import { join as join8 } from "path";
|
|
1315
1360
|
import { execa as execa3 } from "execa";
|
|
1316
1361
|
|
|
1317
1362
|
// src/platform/policy.ts
|
|
1318
|
-
import { existsSync as existsSync6, readFileSync as
|
|
1363
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
|
|
1319
1364
|
import { join as join6 } from "path";
|
|
1320
1365
|
import { parse as yamlParse } from "yaml";
|
|
1321
1366
|
var DEFAULT_POLICY = {
|
|
@@ -1350,7 +1395,7 @@ function loadPolicy(cwd, explicitPath) {
|
|
|
1350
1395
|
const candidate = explicitPath ?? join6(cwd, ".github", "autoremediator.yml");
|
|
1351
1396
|
if (!existsSync6(candidate)) return DEFAULT_POLICY;
|
|
1352
1397
|
try {
|
|
1353
|
-
const parsed = yamlParse(
|
|
1398
|
+
const parsed = yamlParse(readFileSync4(candidate, "utf8"));
|
|
1354
1399
|
return {
|
|
1355
1400
|
allowMajorBumps: parsed.allowMajorBumps ?? DEFAULT_POLICY.allowMajorBumps,
|
|
1356
1401
|
denyPackages: parsed.denyPackages ?? DEFAULT_POLICY.denyPackages,
|
|
@@ -1411,7 +1456,7 @@ function isActiveSuppression(suppression) {
|
|
|
1411
1456
|
}
|
|
1412
1457
|
function loadSuppressionsFile(filePath) {
|
|
1413
1458
|
try {
|
|
1414
|
-
const content =
|
|
1459
|
+
const content = readFileSync4(filePath, "utf8");
|
|
1415
1460
|
const parsed = yamlParse(content);
|
|
1416
1461
|
return Array.isArray(parsed?.suppressions) ? parsed.suppressions : [];
|
|
1417
1462
|
} catch {
|
|
@@ -1437,6 +1482,140 @@ function checkSlaBreach(cveId, severity, publishedAt, slaPolicy) {
|
|
|
1437
1482
|
};
|
|
1438
1483
|
}
|
|
1439
1484
|
|
|
1485
|
+
// src/platform/workspace.ts
|
|
1486
|
+
import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync5, statSync } from "fs";
|
|
1487
|
+
import { join as join7, resolve as resolve2, relative } from "path";
|
|
1488
|
+
import yaml from "yaml";
|
|
1489
|
+
function findWorkspaceRoot(startDir) {
|
|
1490
|
+
let curr = resolve2(startDir);
|
|
1491
|
+
while (true) {
|
|
1492
|
+
if (existsSync7(join7(curr, "pnpm-workspace.yaml"))) return curr;
|
|
1493
|
+
const pkgPath = join7(curr, "package.json");
|
|
1494
|
+
if (existsSync7(pkgPath)) {
|
|
1495
|
+
try {
|
|
1496
|
+
const parsed = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
1497
|
+
if (parsed.workspaces) return curr;
|
|
1498
|
+
} catch {
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
const parent = resolve2(curr, "..");
|
|
1502
|
+
if (parent === curr) break;
|
|
1503
|
+
curr = parent;
|
|
1504
|
+
}
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1507
|
+
function findWorkspacePackages(cwd) {
|
|
1508
|
+
const rootCwd = findWorkspaceRoot(cwd) ?? resolve2(cwd);
|
|
1509
|
+
const packages = [];
|
|
1510
|
+
const packageGlobs = [];
|
|
1511
|
+
const pnpmWorkspaceFile = join7(rootCwd, "pnpm-workspace.yaml");
|
|
1512
|
+
if (existsSync7(pnpmWorkspaceFile)) {
|
|
1513
|
+
try {
|
|
1514
|
+
const content = readFileSync5(pnpmWorkspaceFile, "utf8");
|
|
1515
|
+
const parsed = yaml.parse(content);
|
|
1516
|
+
if (Array.isArray(parsed?.packages)) {
|
|
1517
|
+
packageGlobs.push(...parsed.packages);
|
|
1518
|
+
}
|
|
1519
|
+
} catch {
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
const rootPkgFile = join7(rootCwd, "package.json");
|
|
1523
|
+
if (existsSync7(rootPkgFile)) {
|
|
1524
|
+
try {
|
|
1525
|
+
const rootPkg = JSON.parse(readFileSync5(rootPkgFile, "utf8"));
|
|
1526
|
+
if (Array.isArray(rootPkg.workspaces)) {
|
|
1527
|
+
packageGlobs.push(...rootPkg.workspaces);
|
|
1528
|
+
} else if (Array.isArray(rootPkg.workspaces?.packages)) {
|
|
1529
|
+
packageGlobs.push(...rootPkg.workspaces.packages);
|
|
1530
|
+
}
|
|
1531
|
+
} catch {
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
const denoJsonFile = join7(rootCwd, "deno.json");
|
|
1535
|
+
if (existsSync7(denoJsonFile)) {
|
|
1536
|
+
try {
|
|
1537
|
+
const denoJson = JSON.parse(readFileSync5(denoJsonFile, "utf8"));
|
|
1538
|
+
if (Array.isArray(denoJson.workspace)) {
|
|
1539
|
+
packageGlobs.push(...denoJson.workspace);
|
|
1540
|
+
}
|
|
1541
|
+
} catch {
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
if (packageGlobs.length === 0) {
|
|
1545
|
+
return packages;
|
|
1546
|
+
}
|
|
1547
|
+
const seenDirs = /* @__PURE__ */ new Set();
|
|
1548
|
+
for (const glob of packageGlobs) {
|
|
1549
|
+
const cleanGlob = glob.trim().replace(/\/+$/, "");
|
|
1550
|
+
if (!cleanGlob || cleanGlob.startsWith("!")) continue;
|
|
1551
|
+
if (cleanGlob.endsWith("/*")) {
|
|
1552
|
+
const parentDir = join7(rootCwd, cleanGlob.slice(0, -2));
|
|
1553
|
+
if (!existsSync7(parentDir)) continue;
|
|
1554
|
+
try {
|
|
1555
|
+
const entries = readdirSync(parentDir);
|
|
1556
|
+
for (const entry of entries) {
|
|
1557
|
+
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
1558
|
+
const subDir = join7(parentDir, entry);
|
|
1559
|
+
try {
|
|
1560
|
+
if (statSync(subDir).isDirectory()) {
|
|
1561
|
+
addWorkspacePackage(rootCwd, subDir, packages, seenDirs);
|
|
1562
|
+
}
|
|
1563
|
+
} catch {
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
} catch {
|
|
1567
|
+
}
|
|
1568
|
+
} else {
|
|
1569
|
+
const directDir = join7(rootCwd, cleanGlob);
|
|
1570
|
+
if (existsSync7(directDir)) {
|
|
1571
|
+
try {
|
|
1572
|
+
if (statSync(directDir).isDirectory()) {
|
|
1573
|
+
addWorkspacePackage(rootCwd, directDir, packages, seenDirs);
|
|
1574
|
+
}
|
|
1575
|
+
} catch {
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
return packages;
|
|
1581
|
+
}
|
|
1582
|
+
function addWorkspacePackage(rootCwd, dir, out, seen) {
|
|
1583
|
+
const norm = resolve2(dir);
|
|
1584
|
+
if (seen.has(norm)) return;
|
|
1585
|
+
seen.add(norm);
|
|
1586
|
+
const manifestPath = join7(dir, "package.json");
|
|
1587
|
+
if (!existsSync7(manifestPath)) return;
|
|
1588
|
+
try {
|
|
1589
|
+
const content = readFileSync5(manifestPath, "utf8");
|
|
1590
|
+
const manifest = JSON.parse(content);
|
|
1591
|
+
out.push({
|
|
1592
|
+
name: manifest.name ?? relative(rootCwd, dir),
|
|
1593
|
+
relativeDir: relative(rootCwd, dir),
|
|
1594
|
+
absoluteDir: norm,
|
|
1595
|
+
manifestPath,
|
|
1596
|
+
manifest
|
|
1597
|
+
});
|
|
1598
|
+
} catch {
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
function findDeclaringWorkspacePackage(cwd, dependencyName, filterWorkspace) {
|
|
1602
|
+
const workspaces = findWorkspacePackages(cwd);
|
|
1603
|
+
if (workspaces.length === 0) return null;
|
|
1604
|
+
const matchesFilter = (wp) => {
|
|
1605
|
+
if (!filterWorkspace) return true;
|
|
1606
|
+
const filter = filterWorkspace.trim();
|
|
1607
|
+
return wp.name === filter || wp.relativeDir === filter || wp.relativeDir.replace(/\\/g, "/") === filter.replace(/\\/g, "/");
|
|
1608
|
+
};
|
|
1609
|
+
for (const wp of workspaces) {
|
|
1610
|
+
if (!matchesFilter(wp)) continue;
|
|
1611
|
+
const isDeclared = Boolean(wp.manifest.dependencies?.[dependencyName]) || Boolean(wp.manifest.devDependencies?.[dependencyName]) || Boolean(wp.manifest.peerDependencies?.[dependencyName]);
|
|
1612
|
+
if (isDeclared) {
|
|
1613
|
+
return wp;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
return null;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1440
1619
|
// src/remediation/tools/check-inventory.ts
|
|
1441
1620
|
var checkInventoryTool = defineTool({
|
|
1442
1621
|
description: "Read the project's package.json and installed dependencies to list packages and exact versions. Must be called before checking version matches.",
|
|
@@ -1454,7 +1633,7 @@ var checkInventoryTool = defineTool({
|
|
|
1454
1633
|
}) => {
|
|
1455
1634
|
let pkgJson;
|
|
1456
1635
|
try {
|
|
1457
|
-
pkgJson = JSON.parse(
|
|
1636
|
+
pkgJson = JSON.parse(readFileSync6(join8(cwd, "package.json"), "utf8"));
|
|
1458
1637
|
} catch {
|
|
1459
1638
|
return {
|
|
1460
1639
|
packages: [],
|
|
@@ -1486,7 +1665,8 @@ var checkInventoryTool = defineTool({
|
|
|
1486
1665
|
}
|
|
1487
1666
|
const packages = [];
|
|
1488
1667
|
for (const [name, version] of installedVersions.entries()) {
|
|
1489
|
-
const
|
|
1668
|
+
const declaringWorkspace = findDeclaringWorkspacePackage(cwd, name, workspace);
|
|
1669
|
+
const isDirect = Boolean(pkgJson.dependencies?.[name]) || Boolean(pkgJson.devDependencies?.[name]) || Boolean(pkgJson.peerDependencies?.[name]) || Boolean(declaringWorkspace);
|
|
1490
1670
|
packages.push({
|
|
1491
1671
|
name,
|
|
1492
1672
|
version,
|
|
@@ -1609,173 +1789,14 @@ async function validatePatchArtifact(patchFilePath, options = {}) {
|
|
|
1609
1789
|
};
|
|
1610
1790
|
}
|
|
1611
1791
|
|
|
1612
|
-
// src/
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
`Unsupported LLM provider "${raw}". Set AUTOREMEDIATOR_LLM_PROVIDER to "remote" or "local".`
|
|
1618
|
-
);
|
|
1619
|
-
}
|
|
1620
|
-
return raw;
|
|
1621
|
-
}
|
|
1622
|
-
function resolveModelName(provider, options = {}, routing = {}) {
|
|
1623
|
-
if (options.model) return options.model;
|
|
1624
|
-
if (process.env.AUTOREMEDIATOR_MODEL) return process.env.AUTOREMEDIATOR_MODEL;
|
|
1625
|
-
const cwd = options.cwd ?? process.cwd();
|
|
1626
|
-
const policy = loadPolicy(cwd, options.policy);
|
|
1627
|
-
const providerEnvModel = provider === "remote" ? process.env.AUTOREMEDIATOR_MODEL_REMOTE : process.env.AUTOREMEDIATOR_MODEL_LOCAL;
|
|
1628
|
-
if (providerEnvModel) return providerEnvModel;
|
|
1629
|
-
const policyPinnedModel = provider === "remote" ? policy.modelDefaults?.remote : policy.modelDefaults?.local;
|
|
1630
|
-
if (policyPinnedModel) return policyPinnedModel;
|
|
1631
|
-
const defaults = {
|
|
1632
|
-
remote: "remote-default",
|
|
1633
|
-
local: "local"
|
|
1634
|
-
};
|
|
1635
|
-
const routingEnabled = options.dynamicModelRouting ?? policy.dynamicModelRouting ?? false;
|
|
1636
|
-
const threshold = options.dynamicRoutingThresholdChars ?? policy.dynamicRoutingThresholdChars ?? 18e3;
|
|
1637
|
-
if (provider === "remote" && routingEnabled && typeof routing.inputChars === "number" && routing.inputChars >= threshold) {
|
|
1638
|
-
return process.env.AUTOREMEDIATOR_MODEL_REMOTE_LARGE ?? "remote-large";
|
|
1639
|
-
}
|
|
1640
|
-
return defaults[provider];
|
|
1641
|
-
}
|
|
1642
|
-
async function loadRemoteFactory() {
|
|
1643
|
-
const moduleName = process.env.AUTOREMEDIATOR_REMOTE_CLIENT_MODULE;
|
|
1644
|
-
const exportName = process.env.AUTOREMEDIATOR_REMOTE_CLIENT_FACTORY ?? "createRemoteClient";
|
|
1645
|
-
if (!moduleName) {
|
|
1646
|
-
throw new Error(
|
|
1647
|
-
"AUTOREMEDIATOR_REMOTE_CLIENT_MODULE is required for remote provider model loading."
|
|
1648
|
-
);
|
|
1649
|
-
}
|
|
1650
|
-
if (moduleName.startsWith("./") || moduleName.startsWith("../") || moduleName.startsWith("/") || moduleName.startsWith("file:")) {
|
|
1651
|
-
throw new Error(
|
|
1652
|
-
`AUTOREMEDIATOR_REMOTE_CLIENT_MODULE must be a package name, not a file path: ${moduleName}`
|
|
1653
|
-
);
|
|
1654
|
-
}
|
|
1655
|
-
const loaded = await import(moduleName);
|
|
1656
|
-
const factory = loaded[exportName];
|
|
1657
|
-
if (typeof factory !== "function") {
|
|
1658
|
-
throw new Error(
|
|
1659
|
-
`Remote client factory "${exportName}" was not found in module "${moduleName}".`
|
|
1660
|
-
);
|
|
1661
|
-
}
|
|
1662
|
-
return factory;
|
|
1663
|
-
}
|
|
1664
|
-
async function createModel(options = {}, routing = {}) {
|
|
1665
|
-
const provider = resolveProvider(options);
|
|
1666
|
-
if (provider === "local") {
|
|
1667
|
-
throw new Error(
|
|
1668
|
-
"Local provider does not create a language model. Use the deterministic pipeline path instead."
|
|
1669
|
-
);
|
|
1670
|
-
}
|
|
1671
|
-
const apiKey = process.env.AUTOREMEDIATOR_REMOTE_API_KEY;
|
|
1672
|
-
if (!apiKey) {
|
|
1673
|
-
throw new Error(
|
|
1674
|
-
"AUTOREMEDIATOR_REMOTE_API_KEY environment variable is required for remote provider."
|
|
1675
|
-
);
|
|
1676
|
-
}
|
|
1677
|
-
const modelName = resolveModelName(provider, options, routing);
|
|
1678
|
-
const createRemoteClient = await loadRemoteFactory();
|
|
1679
|
-
const remoteClient = createRemoteClient({ apiKey });
|
|
1680
|
-
return remoteClient(modelName);
|
|
1681
|
-
}
|
|
1682
|
-
function getPatchConfidenceThreshold(provider, safetyProfile = "relaxed", riskLevel = "medium", overrides) {
|
|
1683
|
-
const relaxed = {
|
|
1684
|
-
remote: { low: 0.65, medium: 0.7, high: 0.8 },
|
|
1685
|
-
local: { low: 0.65, medium: 0.7, high: 0.8 }
|
|
1686
|
-
};
|
|
1687
|
-
const strict = {
|
|
1688
|
-
remote: { low: 0.8, medium: 0.85, high: 0.9 },
|
|
1689
|
-
local: { low: 0.8, medium: 0.85, high: 0.9 }
|
|
1690
|
-
};
|
|
1691
|
-
const baseThreshold = safetyProfile === "strict" ? strict[provider][riskLevel] : relaxed[provider][riskLevel];
|
|
1692
|
-
const override = overrides?.[riskLevel];
|
|
1693
|
-
if (typeof override === "number" && Number.isFinite(override)) {
|
|
1694
|
-
return Math.max(0, Math.min(1, override));
|
|
1695
|
-
}
|
|
1696
|
-
return baseThreshold;
|
|
1697
|
-
}
|
|
1698
|
-
function estimateModelCostUsd(params) {
|
|
1699
|
-
const inputTokens = params.promptChars / 4;
|
|
1700
|
-
const outputTokens = params.completionChars / 4;
|
|
1701
|
-
const pricePerThousand = params.provider === "remote" ? { input: 6e-3, output: 0.02 } : { input: 0, output: 0 };
|
|
1702
|
-
return inputTokens / 1e3 * pricePerThousand.input + outputTokens / 1e3 * pricePerThousand.output;
|
|
1703
|
-
}
|
|
1704
|
-
function getNvdConfig() {
|
|
1705
|
-
return {
|
|
1706
|
-
apiKey: process.env.AUTOREMEDIATOR_NVD_API_KEY
|
|
1707
|
-
};
|
|
1708
|
-
}
|
|
1709
|
-
function getGitHubToken() {
|
|
1710
|
-
return process.env.GITHUB_TOKEN;
|
|
1711
|
-
}
|
|
1712
|
-
function getIntelligenceSourceConfig() {
|
|
1713
|
-
return {
|
|
1714
|
-
gitLabAdvisoryApi: process.env.AUTOREMEDIATOR_GITLAB_ADVISORY_API ?? "https://advisories.gitlab.com/api/v1/advisories",
|
|
1715
|
-
certCcSearchUrl: process.env.AUTOREMEDIATOR_CERTCC_SEARCH_URL ?? "https://www.kb.cert.org/vuls/search",
|
|
1716
|
-
epssApi: process.env.AUTOREMEDIATOR_EPSS_API ?? "https://api.first.org/data/v1/epss",
|
|
1717
|
-
cveServicesApi: process.env.AUTOREMEDIATOR_CVE_SERVICES_API ?? "https://cveawg.mitre.org/api/cve",
|
|
1718
|
-
depsDevApi: process.env.AUTOREMEDIATOR_DEPSDEV_API ?? "https://api.deps.dev/v3",
|
|
1719
|
-
scorecardApi: process.env.AUTOREMEDIATOR_SCORECARD_API ?? "https://api.securityscorecards.dev",
|
|
1720
|
-
vendorAdvisoryFeeds: (process.env.AUTOREMEDIATOR_VENDOR_ADVISORY_FEEDS ?? "").split(",").map((v) => v.trim()).filter(Boolean),
|
|
1721
|
-
commercialFeeds: (process.env.AUTOREMEDIATOR_COMMERCIAL_FEEDS ?? "").split(",").map((v) => v.trim()).filter(Boolean),
|
|
1722
|
-
commercialFeedToken: process.env.AUTOREMEDIATOR_COMMERCIAL_FEED_TOKEN
|
|
1723
|
-
};
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
// src/platform/idempotency.ts
|
|
1727
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
1728
|
-
import { join as join8 } from "path";
|
|
1729
|
-
var DEFAULT_INDEX = {
|
|
1730
|
-
schemaVersion: "1.0",
|
|
1731
|
-
entries: {}
|
|
1732
|
-
};
|
|
1733
|
-
function indexFilePath(cwd) {
|
|
1734
|
-
return join8(cwd, ".autoremediator", "state", "idempotency.json");
|
|
1735
|
-
}
|
|
1736
|
-
function entryKey(idempotencyKey, cveId) {
|
|
1737
|
-
return `${idempotencyKey}::${cveId.toUpperCase()}`;
|
|
1738
|
-
}
|
|
1739
|
-
function loadIndex(cwd) {
|
|
1740
|
-
const filePath = indexFilePath(cwd);
|
|
1741
|
-
if (!existsSync7(filePath)) return DEFAULT_INDEX;
|
|
1742
|
-
try {
|
|
1743
|
-
const parsed = JSON.parse(readFileSync5(filePath, "utf8"));
|
|
1744
|
-
if (parsed && parsed.schemaVersion === "1.0" && parsed.entries) {
|
|
1745
|
-
return parsed;
|
|
1746
|
-
}
|
|
1747
|
-
return DEFAULT_INDEX;
|
|
1748
|
-
} catch {
|
|
1749
|
-
return DEFAULT_INDEX;
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
function saveIndex(cwd, index) {
|
|
1753
|
-
const filePath = indexFilePath(cwd);
|
|
1754
|
-
mkdirSync2(join8(cwd, ".autoremediator", "state"), { recursive: true });
|
|
1755
|
-
writeFileSync2(filePath, JSON.stringify(index, null, 2) + "\n", "utf8");
|
|
1756
|
-
}
|
|
1757
|
-
function readIdempotentReport(cwd, idempotencyKey, cveId) {
|
|
1758
|
-
const index = loadIndex(cwd);
|
|
1759
|
-
const key = entryKey(idempotencyKey, cveId);
|
|
1760
|
-
return index.entries[key]?.report;
|
|
1761
|
-
}
|
|
1762
|
-
function storeIdempotentReport(cwd, idempotencyKey, cveId, report) {
|
|
1763
|
-
const index = loadIndex(cwd);
|
|
1764
|
-
const key = entryKey(idempotencyKey, cveId);
|
|
1765
|
-
index.entries[key] = {
|
|
1766
|
-
key: idempotencyKey,
|
|
1767
|
-
cveId: cveId.toUpperCase(),
|
|
1768
|
-
report,
|
|
1769
|
-
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1770
|
-
};
|
|
1771
|
-
saveIndex(cwd, index);
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
// src/remediation/pipeline.ts
|
|
1775
|
-
import { generateText as generateText2 } from "ai";
|
|
1792
|
+
// src/api/patches/reconcile.ts
|
|
1793
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1794
|
+
import { readFile as readFile3, rm, writeFile } from "fs/promises";
|
|
1795
|
+
import { join as join10 } from "path";
|
|
1796
|
+
import { execa as execa5 } from "execa";
|
|
1776
1797
|
|
|
1777
|
-
// src/remediation/
|
|
1778
|
-
import
|
|
1798
|
+
// src/remediation/evaluate-package.ts
|
|
1799
|
+
import semver2 from "semver";
|
|
1779
1800
|
|
|
1780
1801
|
// src/platform/http-client.ts
|
|
1781
1802
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
@@ -1863,6 +1884,37 @@ async function fetchOsvVuln(cveId) {
|
|
|
1863
1884
|
throw err;
|
|
1864
1885
|
}
|
|
1865
1886
|
}
|
|
1887
|
+
async function queryOsvPackage(packageName, version) {
|
|
1888
|
+
const url = `${OSV_BASE}/query`;
|
|
1889
|
+
const body = {
|
|
1890
|
+
package: {
|
|
1891
|
+
name: packageName,
|
|
1892
|
+
ecosystem: "npm"
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
if (version) {
|
|
1896
|
+
body.version = version;
|
|
1897
|
+
}
|
|
1898
|
+
try {
|
|
1899
|
+
const res = await httpClient({
|
|
1900
|
+
url,
|
|
1901
|
+
method: "POST",
|
|
1902
|
+
body,
|
|
1903
|
+
headers: { "Content-Type": "application/json" }
|
|
1904
|
+
});
|
|
1905
|
+
if (res.status === 404) return [];
|
|
1906
|
+
if (!res.ok) {
|
|
1907
|
+
throw new Error(`OSV API error ${res.status} for query "${packageName}": ${res.text}`);
|
|
1908
|
+
}
|
|
1909
|
+
const data = res.data;
|
|
1910
|
+
return data.vulns ?? [];
|
|
1911
|
+
} catch (err) {
|
|
1912
|
+
if (err instanceof Error) {
|
|
1913
|
+
throw new Error(`OSV API error for query "${packageName}": ${err.message}`);
|
|
1914
|
+
}
|
|
1915
|
+
throw err;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1866
1918
|
function osvEventsToSemverRange(events) {
|
|
1867
1919
|
const parts = [];
|
|
1868
1920
|
for (const event of events) {
|
|
@@ -1926,25 +1978,756 @@ async function lookupCveOsv(cveId) {
|
|
|
1926
1978
|
return parseOsvVuln(vuln);
|
|
1927
1979
|
}
|
|
1928
1980
|
|
|
1929
|
-
// src/
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1981
|
+
// src/platform/config.ts
|
|
1982
|
+
function resolveProvider(options = {}) {
|
|
1983
|
+
const raw = options.llmProvider ?? process.env.AUTOREMEDIATOR_LLM_PROVIDER ?? "remote";
|
|
1984
|
+
if (raw !== "remote" && raw !== "local") {
|
|
1985
|
+
throw new Error(
|
|
1986
|
+
`Unsupported LLM provider "${raw}". Set AUTOREMEDIATOR_LLM_PROVIDER to "remote" or "local".`
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
return raw;
|
|
1990
|
+
}
|
|
1991
|
+
function resolveModelName(provider, options = {}, routing = {}) {
|
|
1992
|
+
if (options.model) return options.model;
|
|
1993
|
+
if (process.env.AUTOREMEDIATOR_MODEL) return process.env.AUTOREMEDIATOR_MODEL;
|
|
1994
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1995
|
+
const policy = loadPolicy(cwd, options.policy);
|
|
1996
|
+
const providerEnvModel = provider === "remote" ? process.env.AUTOREMEDIATOR_MODEL_REMOTE : process.env.AUTOREMEDIATOR_MODEL_LOCAL;
|
|
1997
|
+
if (providerEnvModel) return providerEnvModel;
|
|
1998
|
+
const policyPinnedModel = provider === "remote" ? policy.modelDefaults?.remote : policy.modelDefaults?.local;
|
|
1999
|
+
if (policyPinnedModel) return policyPinnedModel;
|
|
2000
|
+
const defaults = {
|
|
2001
|
+
remote: "remote-default",
|
|
2002
|
+
local: "local"
|
|
2003
|
+
};
|
|
2004
|
+
const routingEnabled = options.dynamicModelRouting ?? policy.dynamicModelRouting ?? false;
|
|
2005
|
+
const threshold = options.dynamicRoutingThresholdChars ?? policy.dynamicRoutingThresholdChars ?? 18e3;
|
|
2006
|
+
if (provider === "remote" && routingEnabled && typeof routing.inputChars === "number" && routing.inputChars >= threshold) {
|
|
2007
|
+
return process.env.AUTOREMEDIATOR_MODEL_REMOTE_LARGE ?? "remote-large";
|
|
2008
|
+
}
|
|
2009
|
+
return defaults[provider];
|
|
2010
|
+
}
|
|
2011
|
+
async function loadRemoteFactory() {
|
|
2012
|
+
const moduleName = process.env.AUTOREMEDIATOR_REMOTE_CLIENT_MODULE;
|
|
2013
|
+
const exportName = process.env.AUTOREMEDIATOR_REMOTE_CLIENT_FACTORY ?? "createRemoteClient";
|
|
2014
|
+
if (!moduleName) {
|
|
2015
|
+
throw new Error(
|
|
2016
|
+
"AUTOREMEDIATOR_REMOTE_CLIENT_MODULE is required for remote provider model loading."
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
if (moduleName.startsWith("./") || moduleName.startsWith("../") || moduleName.startsWith("/") || moduleName.startsWith("file:")) {
|
|
2020
|
+
throw new Error(
|
|
2021
|
+
`AUTOREMEDIATOR_REMOTE_CLIENT_MODULE must be a package name, not a file path: ${moduleName}`
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
const loaded = await import(moduleName);
|
|
2025
|
+
const factory = loaded[exportName];
|
|
2026
|
+
if (typeof factory !== "function") {
|
|
2027
|
+
throw new Error(
|
|
2028
|
+
`Remote client factory "${exportName}" was not found in module "${moduleName}".`
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
return factory;
|
|
2032
|
+
}
|
|
2033
|
+
async function createModel(options = {}, routing = {}) {
|
|
2034
|
+
const provider = resolveProvider(options);
|
|
2035
|
+
if (provider === "local") {
|
|
2036
|
+
throw new Error(
|
|
2037
|
+
"Local provider does not create a language model. Use the deterministic pipeline path instead."
|
|
2038
|
+
);
|
|
2039
|
+
}
|
|
2040
|
+
const apiKey = process.env.AUTOREMEDIATOR_REMOTE_API_KEY;
|
|
2041
|
+
if (!apiKey) {
|
|
2042
|
+
throw new Error(
|
|
2043
|
+
"AUTOREMEDIATOR_REMOTE_API_KEY environment variable is required for remote provider."
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
const modelName = resolveModelName(provider, options, routing);
|
|
2047
|
+
const createRemoteClient = await loadRemoteFactory();
|
|
2048
|
+
const remoteClient = createRemoteClient({ apiKey });
|
|
2049
|
+
return remoteClient(modelName);
|
|
2050
|
+
}
|
|
2051
|
+
function getPatchConfidenceThreshold(provider, safetyProfile = "relaxed", riskLevel = "medium", overrides) {
|
|
2052
|
+
const relaxed = {
|
|
2053
|
+
remote: { low: 0.65, medium: 0.7, high: 0.8 },
|
|
2054
|
+
local: { low: 0.65, medium: 0.7, high: 0.8 }
|
|
2055
|
+
};
|
|
2056
|
+
const strict = {
|
|
2057
|
+
remote: { low: 0.8, medium: 0.85, high: 0.9 },
|
|
2058
|
+
local: { low: 0.8, medium: 0.85, high: 0.9 }
|
|
2059
|
+
};
|
|
2060
|
+
const baseThreshold = safetyProfile === "strict" ? strict[provider][riskLevel] : relaxed[provider][riskLevel];
|
|
2061
|
+
const override = overrides?.[riskLevel];
|
|
2062
|
+
if (typeof override === "number" && Number.isFinite(override)) {
|
|
2063
|
+
return Math.max(0, Math.min(1, override));
|
|
2064
|
+
}
|
|
2065
|
+
return baseThreshold;
|
|
2066
|
+
}
|
|
2067
|
+
function estimateModelCostUsd(params) {
|
|
2068
|
+
const inputTokens = params.promptChars / 4;
|
|
2069
|
+
const outputTokens = params.completionChars / 4;
|
|
2070
|
+
const pricePerThousand = params.provider === "remote" ? { input: 6e-3, output: 0.02 } : { input: 0, output: 0 };
|
|
2071
|
+
return inputTokens / 1e3 * pricePerThousand.input + outputTokens / 1e3 * pricePerThousand.output;
|
|
2072
|
+
}
|
|
2073
|
+
function getNvdConfig() {
|
|
2074
|
+
return {
|
|
2075
|
+
apiKey: process.env.AUTOREMEDIATOR_NVD_API_KEY
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
function getGitHubToken() {
|
|
2079
|
+
return process.env.GITHUB_TOKEN;
|
|
2080
|
+
}
|
|
2081
|
+
function getIntelligenceSourceConfig() {
|
|
2082
|
+
return {
|
|
2083
|
+
gitLabAdvisoryApi: process.env.AUTOREMEDIATOR_GITLAB_ADVISORY_API ?? "https://advisories.gitlab.com/api/v1/advisories",
|
|
2084
|
+
certCcSearchUrl: process.env.AUTOREMEDIATOR_CERTCC_SEARCH_URL ?? "https://www.kb.cert.org/vuls/search",
|
|
2085
|
+
epssApi: process.env.AUTOREMEDIATOR_EPSS_API ?? "https://api.first.org/data/v1/epss",
|
|
2086
|
+
cveServicesApi: process.env.AUTOREMEDIATOR_CVE_SERVICES_API ?? "https://cveawg.mitre.org/api/cve",
|
|
2087
|
+
depsDevApi: process.env.AUTOREMEDIATOR_DEPSDEV_API ?? "https://api.deps.dev/v3",
|
|
2088
|
+
scorecardApi: process.env.AUTOREMEDIATOR_SCORECARD_API ?? "https://api.securityscorecards.dev",
|
|
2089
|
+
vendorAdvisoryFeeds: (process.env.AUTOREMEDIATOR_VENDOR_ADVISORY_FEEDS ?? "").split(",").map((v) => v.trim()).filter(Boolean),
|
|
2090
|
+
commercialFeeds: (process.env.AUTOREMEDIATOR_COMMERCIAL_FEEDS ?? "").split(",").map((v) => v.trim()).filter(Boolean),
|
|
2091
|
+
commercialFeedToken: process.env.AUTOREMEDIATOR_COMMERCIAL_FEED_TOKEN
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
// src/intelligence/sources/epss.ts
|
|
2096
|
+
async function fetchEpss(cveId) {
|
|
2097
|
+
const { epssApi } = getIntelligenceSourceConfig();
|
|
2098
|
+
if (!epssApi) return void 0;
|
|
2099
|
+
try {
|
|
2100
|
+
const url = new URL(epssApi);
|
|
2101
|
+
url.searchParams.set("cve", cveId);
|
|
2102
|
+
const res = await httpClient({ url: url.toString() });
|
|
2103
|
+
if (!res.ok) return void 0;
|
|
2104
|
+
const body = res.data;
|
|
2105
|
+
return body.data?.[0];
|
|
2106
|
+
} catch {
|
|
2107
|
+
return void 0;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
async function enrichWithEpss(details) {
|
|
2111
|
+
const row = await fetchEpss(details.id);
|
|
2112
|
+
if (!row) return details;
|
|
2113
|
+
const score = Number.parseFloat(row.epss);
|
|
2114
|
+
const percentile = Number.parseFloat(row.percentile);
|
|
2115
|
+
if (!Number.isFinite(score) || !Number.isFinite(percentile)) {
|
|
2116
|
+
return details;
|
|
2117
|
+
}
|
|
2118
|
+
details.epss = {
|
|
2119
|
+
score,
|
|
2120
|
+
percentile,
|
|
2121
|
+
date: row.date
|
|
2122
|
+
};
|
|
2123
|
+
return details;
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
// src/intelligence/sources/cisa-kev.ts
|
|
2127
|
+
var CISA_KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json";
|
|
2128
|
+
async function fetchCisaKevFeed() {
|
|
2129
|
+
try {
|
|
2130
|
+
const res = await httpClient({ url: CISA_KEV_URL });
|
|
2131
|
+
if (!res.ok) return void 0;
|
|
2132
|
+
return res.data;
|
|
2133
|
+
} catch {
|
|
2134
|
+
return void 0;
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
function findKevEntry(feed, cveId) {
|
|
2138
|
+
if (!feed?.vulnerabilities?.length) return void 0;
|
|
2139
|
+
const normalized = cveId.toUpperCase();
|
|
2140
|
+
return feed.vulnerabilities.find((v) => v.cveID.toUpperCase() === normalized);
|
|
2141
|
+
}
|
|
2142
|
+
async function enrichWithCisaKev(details) {
|
|
2143
|
+
const feed = await fetchCisaKevFeed();
|
|
2144
|
+
const entry = findKevEntry(feed, details.id);
|
|
2145
|
+
if (!entry) return details;
|
|
2146
|
+
details.kev = {
|
|
2147
|
+
knownExploited: true,
|
|
2148
|
+
dateAdded: entry.dateAdded,
|
|
2149
|
+
dueDate: entry.dueDate,
|
|
2150
|
+
requiredAction: entry.requiredAction,
|
|
2151
|
+
knownRansomwareCampaignUse: entry.knownRansomwareCampaignUse
|
|
2152
|
+
};
|
|
2153
|
+
if (!details.references.includes(CISA_KEV_URL)) {
|
|
2154
|
+
details.references.push(CISA_KEV_URL);
|
|
2155
|
+
}
|
|
2156
|
+
return details;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
// src/intelligence/sources/registry.ts
|
|
2160
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
2161
|
+
import { join as join9 } from "path";
|
|
2162
|
+
import { execa as execa4 } from "execa";
|
|
2163
|
+
import semver from "semver";
|
|
2164
|
+
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
2165
|
+
async function fetchPackageVersions(packageName) {
|
|
2166
|
+
const url = `${NPM_REGISTRY}/${encodeURIComponent(packageName)}`;
|
|
2167
|
+
try {
|
|
2168
|
+
const res = await httpClient({ url });
|
|
2169
|
+
if (res.status === 404) return [];
|
|
2170
|
+
if (!res.ok) {
|
|
2171
|
+
throw new Error(`npm registry error ${res.status} for "${packageName}": ${res.text}`);
|
|
2172
|
+
}
|
|
2173
|
+
const data = res.data;
|
|
2174
|
+
return Object.keys(data.versions);
|
|
2175
|
+
} catch (err) {
|
|
2176
|
+
if (err instanceof Error && err.message.includes("registry error")) {
|
|
2177
|
+
throw err;
|
|
2178
|
+
}
|
|
2179
|
+
return [];
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
async function findSafeUpgradeVersion(packageName, installedVersion, firstPatchedVersion, vulnerableRange) {
|
|
2183
|
+
const resolution = await resolveSafeUpgradeVersion(
|
|
2184
|
+
packageName,
|
|
2185
|
+
installedVersion,
|
|
2186
|
+
firstPatchedVersion,
|
|
2187
|
+
vulnerableRange
|
|
2188
|
+
);
|
|
2189
|
+
return resolution.safeVersion;
|
|
2190
|
+
}
|
|
2191
|
+
async function resolveSafeUpgradeVersion(packageName, installedVersion, firstPatchedVersion, vulnerableRange) {
|
|
2192
|
+
const versions = await fetchPackageVersions(packageName);
|
|
2193
|
+
if (!versions.length) {
|
|
2194
|
+
return {
|
|
2195
|
+
candidates: {},
|
|
2196
|
+
majorOnlyFixAvailable: false
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
const installed = semver.parse(installedVersion);
|
|
2200
|
+
const candidates = versions.filter((v) => semver.valid(v) && semver.gte(v, firstPatchedVersion)).filter((v) => {
|
|
2201
|
+
if (!vulnerableRange) return true;
|
|
2202
|
+
try {
|
|
2203
|
+
return !semver.satisfies(v, vulnerableRange, { includePrerelease: false });
|
|
2204
|
+
} catch {
|
|
2205
|
+
return true;
|
|
2206
|
+
}
|
|
2207
|
+
}).sort(semver.compare);
|
|
2208
|
+
if (!candidates.length) {
|
|
2209
|
+
return {
|
|
2210
|
+
candidates: {},
|
|
2211
|
+
majorOnlyFixAvailable: false
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
const categorizedCandidates = {};
|
|
2215
|
+
for (const candidate of candidates) {
|
|
2216
|
+
const level = classifyUpgradeLevel(installedVersion, candidate);
|
|
2217
|
+
if (!level) continue;
|
|
2218
|
+
if (!categorizedCandidates[level]) {
|
|
2219
|
+
categorizedCandidates[level] = candidate;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
const safeVersion = categorizedCandidates.patch ?? categorizedCandidates.minor ?? categorizedCandidates.major;
|
|
2223
|
+
if (!safeVersion) {
|
|
2224
|
+
return {
|
|
2225
|
+
candidates: categorizedCandidates,
|
|
2226
|
+
majorOnlyFixAvailable: false
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
const upgradeLevel = classifyUpgradeLevel(installedVersion, safeVersion);
|
|
2230
|
+
const majorOnlyFixAvailable = !categorizedCandidates.patch && !categorizedCandidates.minor && Boolean(categorizedCandidates.major);
|
|
2231
|
+
if (!installed || !upgradeLevel) {
|
|
2232
|
+
return {
|
|
2233
|
+
safeVersion,
|
|
2234
|
+
upgradeLevel,
|
|
2235
|
+
candidates: categorizedCandidates,
|
|
2236
|
+
majorOnlyFixAvailable
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
return {
|
|
2240
|
+
safeVersion,
|
|
2241
|
+
upgradeLevel,
|
|
2242
|
+
candidates: categorizedCandidates,
|
|
2243
|
+
majorOnlyFixAvailable
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
function classifyUpgradeLevel(installedVersion, candidateVersion) {
|
|
2247
|
+
const installed = semver.parse(installedVersion);
|
|
2248
|
+
const candidate = semver.parse(candidateVersion);
|
|
2249
|
+
if (!installed || !candidate) return void 0;
|
|
2250
|
+
if (candidate.major > installed.major) return "major";
|
|
2251
|
+
if (candidate.minor > installed.minor) return "minor";
|
|
2252
|
+
if (candidate.patch > installed.patch || candidate.version === installed.version) {
|
|
2253
|
+
return "patch";
|
|
2254
|
+
}
|
|
2255
|
+
return void 0;
|
|
2256
|
+
}
|
|
2257
|
+
async function queryOutdatedPackages(cwd, options = {}) {
|
|
2258
|
+
const pm = options.packageManager ?? detectPackageManager(cwd);
|
|
2259
|
+
let stdout;
|
|
2260
|
+
try {
|
|
2261
|
+
const args = pm === "pnpm" ? ["outdated", "--json"] : ["outdated", "--json"];
|
|
2262
|
+
const yarnArgs = pm === "yarn" ? ["outdated", "--json"] : void 0;
|
|
2263
|
+
const finalArgs = yarnArgs ?? args;
|
|
2264
|
+
const result2 = await execa4(pm, finalArgs, {
|
|
2265
|
+
cwd,
|
|
2266
|
+
reject: false,
|
|
2267
|
+
stdio: "pipe"
|
|
2268
|
+
});
|
|
2269
|
+
if (result2.exitCode !== 0 && result2.exitCode !== 1) {
|
|
2270
|
+
const stderr = typeof result2.stderr === "string" ? result2.stderr : "";
|
|
2271
|
+
if (!result2.stdout) {
|
|
2272
|
+
throw new Error(
|
|
2273
|
+
`${pm} outdated --json failed (exit ${result2.exitCode}): ${stderr.slice(0, 500)}`
|
|
2274
|
+
);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
stdout = typeof result2.stdout === "string" ? result2.stdout : "";
|
|
2278
|
+
} catch (err) {
|
|
2279
|
+
if (err instanceof Error && !err.message.includes("outdated --json failed")) {
|
|
2280
|
+
throw new Error(`Failed to query outdated packages: ${err.message}`);
|
|
2281
|
+
}
|
|
2282
|
+
throw err;
|
|
2283
|
+
}
|
|
2284
|
+
if (!stdout.trim()) {
|
|
2285
|
+
return /* @__PURE__ */ new Map();
|
|
2286
|
+
}
|
|
2287
|
+
const rawEntries = {};
|
|
2288
|
+
if (pm === "yarn") {
|
|
2289
|
+
for (const line of stdout.split("\n")) {
|
|
2290
|
+
const trimmed = line.trim();
|
|
2291
|
+
if (!trimmed) continue;
|
|
2292
|
+
try {
|
|
2293
|
+
const parsed = JSON.parse(trimmed);
|
|
2294
|
+
if (parsed.type === "table" && parsed.data) {
|
|
2295
|
+
const tableData = parsed.data;
|
|
2296
|
+
const head = tableData.head ?? [];
|
|
2297
|
+
const currentIdx = head.indexOf("Current");
|
|
2298
|
+
const wantedIdx = head.indexOf("Wanted");
|
|
2299
|
+
const latestIdx = head.indexOf("Latest");
|
|
2300
|
+
for (const row of tableData.body ?? []) {
|
|
2301
|
+
const name = row[0];
|
|
2302
|
+
if (!name) continue;
|
|
2303
|
+
rawEntries[name] = {
|
|
2304
|
+
current: row[currentIdx] ?? "",
|
|
2305
|
+
wanted: row[wantedIdx] ?? "",
|
|
2306
|
+
latest: row[latestIdx] ?? "",
|
|
2307
|
+
type: row[head.indexOf("Package Type")] ?? "dependencies"
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
} catch {
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
} else {
|
|
2315
|
+
try {
|
|
2316
|
+
const parsed = JSON.parse(stdout);
|
|
2317
|
+
if (Array.isArray(parsed)) {
|
|
2318
|
+
for (const entry of parsed) {
|
|
2319
|
+
const name = entry.name ?? entry.packageName;
|
|
2320
|
+
if (!name) continue;
|
|
2321
|
+
rawEntries[name] = {
|
|
2322
|
+
current: entry.current,
|
|
2323
|
+
wanted: entry.wanted,
|
|
2324
|
+
latest: entry.latest,
|
|
2325
|
+
type: entry.dependencyType
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
} else {
|
|
2329
|
+
Object.assign(rawEntries, parsed);
|
|
2330
|
+
}
|
|
2331
|
+
} catch {
|
|
2332
|
+
throw new Error(`Could not parse ${pm} outdated --json output.`);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
let directDeps;
|
|
2336
|
+
try {
|
|
2337
|
+
const pkgRaw = JSON.parse(readFileSync7(join9(cwd, "package.json"), "utf8"));
|
|
2338
|
+
directDeps = /* @__PURE__ */ new Set([
|
|
2339
|
+
...Object.keys(pkgRaw.dependencies ?? {}),
|
|
2340
|
+
...Object.keys(pkgRaw.devDependencies ?? {})
|
|
2341
|
+
]);
|
|
2342
|
+
} catch {
|
|
2343
|
+
directDeps = /* @__PURE__ */ new Set();
|
|
2344
|
+
}
|
|
2345
|
+
const result = /* @__PURE__ */ new Map();
|
|
2346
|
+
for (const [name, entry] of Object.entries(rawEntries)) {
|
|
2347
|
+
const currentVersion = entry.current ?? "";
|
|
2348
|
+
const wantedVersion = entry.wanted ?? currentVersion;
|
|
2349
|
+
const latestVersion = entry.latest ?? currentVersion;
|
|
2350
|
+
if (!currentVersion || !latestVersion) continue;
|
|
2351
|
+
const dependencyScope = directDeps.has(name) ? "direct" : "transitive";
|
|
2352
|
+
if (!options.includeTransitive && dependencyScope === "transitive") continue;
|
|
2353
|
+
const isMajorBump = semver.valid(currentVersion) !== null && semver.valid(latestVersion) !== null && semver.major(latestVersion) > semver.major(currentVersion);
|
|
2354
|
+
result.set(name, {
|
|
2355
|
+
currentVersion,
|
|
2356
|
+
wantedVersion,
|
|
2357
|
+
latestVersion,
|
|
2358
|
+
isMajorBump,
|
|
2359
|
+
dependencyScope
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
return result;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
// src/remediation/evaluate-package.ts
|
|
2366
|
+
async function evaluatePackage(packageName, options = {}) {
|
|
2367
|
+
const version = options.version?.trim();
|
|
2368
|
+
const enrich = options.enrichIntelligence !== false;
|
|
2369
|
+
let versions = [];
|
|
2370
|
+
let latestVersion;
|
|
2371
|
+
try {
|
|
2372
|
+
versions = await fetchPackageVersions(packageName);
|
|
2373
|
+
if (versions.length > 0) {
|
|
2374
|
+
const valid = versions.filter((v) => semver2.valid(v)).sort((a, b) => semver2.compare(a, b));
|
|
2375
|
+
latestVersion = valid[valid.length - 1];
|
|
2376
|
+
}
|
|
2377
|
+
} catch {
|
|
2378
|
+
}
|
|
2379
|
+
let rawVulns = [];
|
|
2380
|
+
try {
|
|
2381
|
+
rawVulns = await queryOsvPackage(packageName, version);
|
|
2382
|
+
} catch {
|
|
2383
|
+
rawVulns = [];
|
|
2384
|
+
}
|
|
2385
|
+
let kevFeed = null;
|
|
2386
|
+
if (enrich) {
|
|
2387
|
+
try {
|
|
2388
|
+
kevFeed = await fetchCisaKevFeed();
|
|
2389
|
+
} catch {
|
|
2390
|
+
kevFeed = null;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
const entries = [];
|
|
2394
|
+
for (const raw of rawVulns) {
|
|
2395
|
+
const cveDetails = parseOsvVuln(raw);
|
|
2396
|
+
const affected = cveDetails.affectedPackages.find(
|
|
2397
|
+
(p) => p.name.toLowerCase() === packageName.toLowerCase()
|
|
2398
|
+
);
|
|
2399
|
+
if (version && affected?.vulnerableRange) {
|
|
2400
|
+
const cleanVer = semver2.clean(version) || version;
|
|
2401
|
+
if (semver2.valid(cleanVer) && !semver2.satisfies(cleanVer, affected.vulnerableRange)) {
|
|
2402
|
+
continue;
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
let inCisaKev = false;
|
|
2406
|
+
let epssScore;
|
|
2407
|
+
let epssPercentile;
|
|
2408
|
+
if (enrich && cveDetails.id.startsWith("CVE-")) {
|
|
2409
|
+
if (kevFeed) {
|
|
2410
|
+
inCisaKev = Boolean(findKevEntry(kevFeed, cveDetails.id));
|
|
2411
|
+
}
|
|
2412
|
+
try {
|
|
2413
|
+
const epss = await fetchEpss(cveDetails.id);
|
|
2414
|
+
if (epss) {
|
|
2415
|
+
epssScore = Number.parseFloat(epss.epss);
|
|
2416
|
+
epssPercentile = Number.parseFloat(epss.percentile);
|
|
2417
|
+
}
|
|
2418
|
+
} catch {
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
let safeUpgradeVersion;
|
|
2422
|
+
if (affected?.firstPatchedVersion) {
|
|
2423
|
+
try {
|
|
2424
|
+
const safe = await findSafeUpgradeVersion(
|
|
2425
|
+
packageName,
|
|
2426
|
+
version || "0.0.0",
|
|
2427
|
+
affected.firstPatchedVersion
|
|
2428
|
+
);
|
|
2429
|
+
safeUpgradeVersion = safe ?? affected.firstPatchedVersion;
|
|
2430
|
+
} catch {
|
|
2431
|
+
safeUpgradeVersion = affected.firstPatchedVersion;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
entries.push({
|
|
2435
|
+
cveId: cveDetails.id,
|
|
2436
|
+
severity: cveDetails.severity,
|
|
2437
|
+
summary: cveDetails.summary,
|
|
2438
|
+
vulnerableRange: affected?.vulnerableRange,
|
|
2439
|
+
firstPatchedVersion: affected?.firstPatchedVersion,
|
|
2440
|
+
safeUpgradeVersion,
|
|
2441
|
+
epssScore,
|
|
2442
|
+
epssPercentile,
|
|
2443
|
+
inCisaKev,
|
|
2444
|
+
references: cveDetails.references
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
const isVulnerable = entries.length > 0;
|
|
2448
|
+
let verdict = "safe";
|
|
2449
|
+
if (isVulnerable) {
|
|
2450
|
+
const hasKevOrHighEpss = entries.some((e) => e.inCisaKev || e.epssScore && e.epssScore > 0.5);
|
|
2451
|
+
const hasHighOrCritical = entries.some(
|
|
2452
|
+
(e) => e.severity === "CRITICAL" || e.severity === "HIGH"
|
|
2453
|
+
);
|
|
2454
|
+
if (hasKevOrHighEpss) {
|
|
2455
|
+
verdict = "actively-exploited";
|
|
2456
|
+
} else if (hasHighOrCritical) {
|
|
2457
|
+
verdict = "vulnerable";
|
|
2458
|
+
} else {
|
|
2459
|
+
verdict = "caution";
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
let recommendedVersion;
|
|
2463
|
+
if (isVulnerable) {
|
|
2464
|
+
const upgradeCandidates = entries.map((e) => e.safeUpgradeVersion).filter((v) => Boolean(v && semver2.valid(v))).sort((a, b) => semver2.compare(a, b));
|
|
2465
|
+
recommendedVersion = upgradeCandidates[upgradeCandidates.length - 1] || latestVersion;
|
|
2466
|
+
} else {
|
|
2467
|
+
recommendedVersion = version || latestVersion;
|
|
2468
|
+
}
|
|
2469
|
+
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.`;
|
|
2470
|
+
return {
|
|
2471
|
+
schemaVersion: "1.0",
|
|
2472
|
+
packageName,
|
|
2473
|
+
evaluatedVersion: version,
|
|
2474
|
+
isVulnerable,
|
|
2475
|
+
verdict,
|
|
2476
|
+
summary,
|
|
2477
|
+
vulnerabilities: entries,
|
|
2478
|
+
recommendedVersion,
|
|
2479
|
+
latestVersion,
|
|
2480
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
// src/api/patches/reconcile.ts
|
|
2485
|
+
async function reconcilePatchArtifacts(options = {}) {
|
|
2486
|
+
const cwd = options.cwd ?? process.cwd();
|
|
2487
|
+
const pm = options.packageManager ?? detectPackageManager(cwd);
|
|
2488
|
+
const summaries = await listPatchArtifacts(options);
|
|
2489
|
+
const results = [];
|
|
2490
|
+
for (const summary of summaries) {
|
|
2491
|
+
const inspection = await inspectPatchArtifact(summary.patchFilePath, options);
|
|
2492
|
+
let packageName = inspection.packageName;
|
|
2493
|
+
let vulnerableVersion = inspection.vulnerableVersion;
|
|
2494
|
+
const cveId = inspection.cveId;
|
|
2495
|
+
if (!packageName && summary.patchFileName) {
|
|
2496
|
+
const basename = summary.patchFileName.replace(/\.patch$/, "");
|
|
2497
|
+
const plusIdx = basename.lastIndexOf("+");
|
|
2498
|
+
if (plusIdx > 0) {
|
|
2499
|
+
let rawPkg = basename.slice(0, plusIdx);
|
|
2500
|
+
vulnerableVersion = basename.slice(plusIdx + 1);
|
|
2501
|
+
if (rawPkg.includes("+")) {
|
|
2502
|
+
rawPkg = "@" + rawPkg.replace("+", "/");
|
|
2503
|
+
}
|
|
2504
|
+
packageName = rawPkg;
|
|
2505
|
+
} else {
|
|
2506
|
+
packageName = basename;
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
if (!packageName) {
|
|
2510
|
+
results.push({
|
|
2511
|
+
patchFilePath: summary.patchFilePath,
|
|
2512
|
+
packageName: "unknown",
|
|
2513
|
+
cveId,
|
|
2514
|
+
status: "patch-retained",
|
|
2515
|
+
reason: "Cannot determine package name from patch artifact."
|
|
2516
|
+
});
|
|
2517
|
+
continue;
|
|
2518
|
+
}
|
|
2519
|
+
let safeUpgradeVersion;
|
|
2520
|
+
try {
|
|
2521
|
+
const evaluation = await evaluatePackage(packageName, {
|
|
2522
|
+
version: vulnerableVersion,
|
|
2523
|
+
enrichIntelligence: false
|
|
2524
|
+
});
|
|
2525
|
+
if (cveId) {
|
|
2526
|
+
const vuln = evaluation.vulnerabilities.find(
|
|
2527
|
+
(v) => v.cveId.toLowerCase() === cveId.toLowerCase()
|
|
2528
|
+
);
|
|
2529
|
+
if (vuln?.safeUpgradeVersion) {
|
|
2530
|
+
safeUpgradeVersion = vuln.safeUpgradeVersion;
|
|
2531
|
+
}
|
|
2532
|
+
} else {
|
|
2533
|
+
safeUpgradeVersion = evaluation.vulnerabilities.find(
|
|
2534
|
+
(v) => v.safeUpgradeVersion
|
|
2535
|
+
)?.safeUpgradeVersion;
|
|
2536
|
+
}
|
|
2537
|
+
} catch {
|
|
2538
|
+
}
|
|
2539
|
+
if (!safeUpgradeVersion) {
|
|
2540
|
+
results.push({
|
|
2541
|
+
patchFilePath: summary.patchFilePath,
|
|
2542
|
+
packageName,
|
|
2543
|
+
cveId,
|
|
2544
|
+
installedVersion: vulnerableVersion,
|
|
2545
|
+
status: "patch-retained",
|
|
2546
|
+
reason: "No safe upstream release exists yet; patch remains necessary."
|
|
2547
|
+
});
|
|
2548
|
+
continue;
|
|
2549
|
+
}
|
|
2550
|
+
if (!options.autoApply) {
|
|
2551
|
+
results.push({
|
|
2552
|
+
patchFilePath: summary.patchFilePath,
|
|
2553
|
+
packageName,
|
|
2554
|
+
cveId,
|
|
2555
|
+
installedVersion: vulnerableVersion,
|
|
2556
|
+
safeUpstreamVersion: safeUpgradeVersion,
|
|
2557
|
+
status: "safe-version-available",
|
|
2558
|
+
reason: `Safe upstream version ${safeUpgradeVersion} is available. Re-run with autoApply: true or --auto-apply to upgrade.`
|
|
2559
|
+
});
|
|
2560
|
+
continue;
|
|
2561
|
+
}
|
|
2562
|
+
try {
|
|
2563
|
+
const pkgJsonPath = join10(cwd, "package.json");
|
|
2564
|
+
if (existsSync8(pkgJsonPath)) {
|
|
2565
|
+
const pkgJson = JSON.parse(await readFile3(pkgJsonPath, "utf8"));
|
|
2566
|
+
let updated = false;
|
|
2567
|
+
const depSections = [
|
|
2568
|
+
"dependencies",
|
|
2569
|
+
"devDependencies",
|
|
2570
|
+
"peerDependencies",
|
|
2571
|
+
"optionalDependencies"
|
|
2572
|
+
];
|
|
2573
|
+
for (const depSection of depSections) {
|
|
2574
|
+
if (pkgJson[depSection] && pkgJson[depSection][packageName]) {
|
|
2575
|
+
pkgJson[depSection][packageName] = `^${safeUpgradeVersion}`;
|
|
2576
|
+
updated = true;
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
if (pkgJson.pnpm?.patchedDependencies) {
|
|
2580
|
+
for (const key of Object.keys(pkgJson.pnpm.patchedDependencies)) {
|
|
2581
|
+
if (key.startsWith(`${packageName}@`)) {
|
|
2582
|
+
delete pkgJson.pnpm.patchedDependencies[key];
|
|
2583
|
+
updated = true;
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
if (updated) {
|
|
2588
|
+
await writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
await rm(summary.patchFilePath, { force: true });
|
|
2592
|
+
if (inspection.manifestFilePath) {
|
|
2593
|
+
await rm(inspection.manifestFilePath, { force: true });
|
|
2594
|
+
}
|
|
2595
|
+
const installCmd = resolveInstallCommand(pm);
|
|
2596
|
+
await execa5(installCmd[0], installCmd.slice(1), { cwd, reject: false });
|
|
2597
|
+
if (options.runTests) {
|
|
2598
|
+
const testCmd = resolveTestCommand(pm);
|
|
2599
|
+
await execa5(testCmd[0], testCmd.slice(1), { cwd, reject: false });
|
|
2600
|
+
}
|
|
2601
|
+
results.push({
|
|
2602
|
+
patchFilePath: summary.patchFilePath,
|
|
2603
|
+
packageName,
|
|
2604
|
+
cveId,
|
|
2605
|
+
installedVersion: vulnerableVersion,
|
|
2606
|
+
safeUpstreamVersion: safeUpgradeVersion,
|
|
2607
|
+
status: "upgraded",
|
|
2608
|
+
reason: `Successfully upgraded to ${safeUpgradeVersion} and retired patch artifact.`
|
|
2609
|
+
});
|
|
2610
|
+
} catch (err) {
|
|
2611
|
+
results.push({
|
|
2612
|
+
patchFilePath: summary.patchFilePath,
|
|
2613
|
+
packageName,
|
|
2614
|
+
cveId,
|
|
2615
|
+
installedVersion: vulnerableVersion,
|
|
2616
|
+
safeUpstreamVersion: safeUpgradeVersion,
|
|
2617
|
+
status: "error",
|
|
2618
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
const reconciledCount = results.filter((r) => r.status === "upgraded").length;
|
|
2623
|
+
const retainedCount = results.filter(
|
|
2624
|
+
(r) => r.status === "patch-retained" || r.status === "safe-version-available"
|
|
2625
|
+
).length;
|
|
2626
|
+
return {
|
|
2627
|
+
reconciled: results,
|
|
2628
|
+
reconciledCount,
|
|
2629
|
+
retainedCount
|
|
2630
|
+
};
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
// src/platform/idempotency.ts
|
|
2634
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "fs";
|
|
2635
|
+
import { join as join11 } from "path";
|
|
2636
|
+
var DEFAULT_INDEX = {
|
|
2637
|
+
schemaVersion: "1.0",
|
|
2638
|
+
entries: {}
|
|
2639
|
+
};
|
|
2640
|
+
function indexFilePath(cwd) {
|
|
2641
|
+
return join11(cwd, ".autoremediator", "state", "idempotency.json");
|
|
2642
|
+
}
|
|
2643
|
+
function entryKey(idempotencyKey, cveId) {
|
|
2644
|
+
return `${idempotencyKey}::${cveId.toUpperCase()}`;
|
|
2645
|
+
}
|
|
2646
|
+
function loadIndex(cwd) {
|
|
2647
|
+
const filePath = indexFilePath(cwd);
|
|
2648
|
+
if (!existsSync9(filePath)) return DEFAULT_INDEX;
|
|
2649
|
+
try {
|
|
2650
|
+
const parsed = JSON.parse(readFileSync8(filePath, "utf8"));
|
|
2651
|
+
if (parsed && parsed.schemaVersion === "1.0" && parsed.entries) {
|
|
2652
|
+
return parsed;
|
|
2653
|
+
}
|
|
2654
|
+
return DEFAULT_INDEX;
|
|
2655
|
+
} catch {
|
|
2656
|
+
return DEFAULT_INDEX;
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
function saveIndex(cwd, index) {
|
|
2660
|
+
const filePath = indexFilePath(cwd);
|
|
2661
|
+
mkdirSync2(join11(cwd, ".autoremediator", "state"), { recursive: true });
|
|
2662
|
+
writeFileSync2(filePath, JSON.stringify(index, null, 2) + "\n", "utf8");
|
|
2663
|
+
}
|
|
2664
|
+
function readIdempotentReport(cwd, idempotencyKey, cveId) {
|
|
2665
|
+
const index = loadIndex(cwd);
|
|
2666
|
+
const key = entryKey(idempotencyKey, cveId);
|
|
2667
|
+
return index.entries[key]?.report;
|
|
2668
|
+
}
|
|
2669
|
+
function storeIdempotentReport(cwd, idempotencyKey, cveId, report) {
|
|
2670
|
+
const index = loadIndex(cwd);
|
|
2671
|
+
const key = entryKey(idempotencyKey, cveId);
|
|
2672
|
+
index.entries[key] = {
|
|
2673
|
+
key: idempotencyKey,
|
|
2674
|
+
cveId: cveId.toUpperCase(),
|
|
2675
|
+
report,
|
|
2676
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2677
|
+
};
|
|
2678
|
+
saveIndex(cwd, index);
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
// src/remediation/pipeline.ts
|
|
2682
|
+
import { generateText as generateText2 } from "ai";
|
|
2683
|
+
|
|
2684
|
+
// src/remediation/local/run.ts
|
|
2685
|
+
import semver6 from "semver";
|
|
2686
|
+
|
|
2687
|
+
// src/intelligence/sources/github-advisory.ts
|
|
2688
|
+
var GITHUB_API_BASE = "https://api.github.com/advisories";
|
|
2689
|
+
async function fetchGhAdvisories(cveId) {
|
|
2690
|
+
const token = getGitHubToken();
|
|
2691
|
+
const headers = {
|
|
2692
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
2693
|
+
"User-Agent": "autoremediator"
|
|
2694
|
+
};
|
|
2695
|
+
if (token) {
|
|
2696
|
+
headers["Authorization"] = `token ${token}`;
|
|
2697
|
+
}
|
|
2698
|
+
const normalized = cveId.trim();
|
|
2699
|
+
if (normalized.toUpperCase().startsWith("GHSA-")) {
|
|
2700
|
+
const url2 = `${GITHUB_API_BASE}/${encodeURIComponent(normalized)}`;
|
|
2701
|
+
try {
|
|
2702
|
+
const res = await httpClient({ url: url2, headers });
|
|
2703
|
+
if (!res.ok) {
|
|
2704
|
+
if (res.status === 404) return [];
|
|
2705
|
+
throw new Error(`HTTP ${res.status}: ${res.text}`);
|
|
1945
2706
|
}
|
|
2707
|
+
return res.data ? [res.data] : [];
|
|
2708
|
+
} catch (err) {
|
|
2709
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
2710
|
+
console.warn(
|
|
2711
|
+
`[autoremediator] GitHub Advisory API error for ${cveId}: ${errorMsg} \u2014 skipping.`
|
|
2712
|
+
);
|
|
2713
|
+
return [];
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
const url = new URL(GITHUB_API_BASE);
|
|
2717
|
+
url.searchParams.set("cve_id", cveId);
|
|
2718
|
+
url.searchParams.set("ecosystem", "npm");
|
|
2719
|
+
url.searchParams.set("type", "reviewed");
|
|
2720
|
+
url.searchParams.set("per_page", "10");
|
|
2721
|
+
try {
|
|
2722
|
+
const res = await httpClient({
|
|
2723
|
+
url: url.toString(),
|
|
2724
|
+
headers
|
|
1946
2725
|
});
|
|
1947
|
-
|
|
2726
|
+
if (!res.ok) {
|
|
2727
|
+
if (res.status === 404) return [];
|
|
2728
|
+
throw new Error(`HTTP ${res.status}: ${res.text}`);
|
|
2729
|
+
}
|
|
2730
|
+
return Array.isArray(res.data) ? res.data : [];
|
|
1948
2731
|
} catch (err) {
|
|
1949
2732
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1950
2733
|
console.warn(
|
|
@@ -2038,9 +2821,9 @@ async function enrichWithNvd(details) {
|
|
|
2038
2821
|
}
|
|
2039
2822
|
|
|
2040
2823
|
// src/intelligence/sources/snapshot.ts
|
|
2041
|
-
import { readFileSync as
|
|
2824
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
2042
2825
|
function loadIntelligenceSnapshot(filePath) {
|
|
2043
|
-
const raw =
|
|
2826
|
+
const raw = readFileSync9(filePath, "utf8");
|
|
2044
2827
|
const parsed = JSON.parse(raw);
|
|
2045
2828
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2046
2829
|
throw new Error(`Intelligence snapshot at ${filePath} must be a JSON object keyed by CVE ID.`);
|
|
@@ -2052,8 +2835,8 @@ function lookupSnapshotCve(snapshot, cveId) {
|
|
|
2052
2835
|
}
|
|
2053
2836
|
|
|
2054
2837
|
// src/remediation/tools/check-reachability.ts
|
|
2055
|
-
import { existsSync as
|
|
2056
|
-
import { isAbsolute as isAbsolute2, join as
|
|
2838
|
+
import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync10, statSync as statSync2 } from "fs";
|
|
2839
|
+
import { isAbsolute as isAbsolute2, join as join12, resolve as resolve3 } from "path";
|
|
2057
2840
|
import { parseSync } from "oxc-parser";
|
|
2058
2841
|
import { z as z2 } from "zod";
|
|
2059
2842
|
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"]);
|
|
@@ -2069,16 +2852,16 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
2069
2852
|
]);
|
|
2070
2853
|
function loadTsconfigPaths(cwd) {
|
|
2071
2854
|
for (const configName of ["tsconfig.json", "jsconfig.json"]) {
|
|
2072
|
-
const fullPath =
|
|
2073
|
-
if (!
|
|
2855
|
+
const fullPath = join12(cwd, configName);
|
|
2856
|
+
if (!existsSync10(fullPath)) continue;
|
|
2074
2857
|
try {
|
|
2075
|
-
const raw =
|
|
2858
|
+
const raw = readFileSync10(fullPath, "utf8");
|
|
2076
2859
|
const cleaned = raw.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "");
|
|
2077
2860
|
const parsed = JSON.parse(cleaned);
|
|
2078
2861
|
const compilerOptions = parsed.compilerOptions;
|
|
2079
2862
|
if (compilerOptions?.paths) {
|
|
2080
2863
|
return {
|
|
2081
|
-
baseUrl: compilerOptions.baseUrl ?
|
|
2864
|
+
baseUrl: compilerOptions.baseUrl ? resolve3(cwd, compilerOptions.baseUrl) : cwd,
|
|
2082
2865
|
paths: compilerOptions.paths
|
|
2083
2866
|
};
|
|
2084
2867
|
}
|
|
@@ -2093,14 +2876,14 @@ function resolveAliasedPath(importSource, config) {
|
|
|
2093
2876
|
}
|
|
2094
2877
|
for (const [pattern, replacements] of Object.entries(config.paths)) {
|
|
2095
2878
|
if (pattern === importSource && replacements[0]) {
|
|
2096
|
-
return
|
|
2879
|
+
return resolve3(config.baseUrl, replacements[0]);
|
|
2097
2880
|
}
|
|
2098
2881
|
if (pattern.endsWith("/*")) {
|
|
2099
2882
|
const prefix = pattern.slice(0, -2);
|
|
2100
2883
|
if (importSource.startsWith(prefix)) {
|
|
2101
2884
|
const suffix = importSource.slice(prefix.length);
|
|
2102
2885
|
const replacement = replacements[0]?.slice(0, -2) ?? "";
|
|
2103
|
-
return
|
|
2886
|
+
return resolve3(config.baseUrl, replacement + suffix);
|
|
2104
2887
|
}
|
|
2105
2888
|
}
|
|
2106
2889
|
}
|
|
@@ -2109,16 +2892,16 @@ function resolveAliasedPath(importSource, config) {
|
|
|
2109
2892
|
function* walkSourceFiles(dir) {
|
|
2110
2893
|
let entries;
|
|
2111
2894
|
try {
|
|
2112
|
-
entries =
|
|
2895
|
+
entries = readdirSync2(dir);
|
|
2113
2896
|
} catch {
|
|
2114
2897
|
return;
|
|
2115
2898
|
}
|
|
2116
2899
|
for (const entry of entries) {
|
|
2117
2900
|
if (IGNORED_DIRS.has(entry)) continue;
|
|
2118
|
-
const full =
|
|
2901
|
+
const full = join12(dir, entry);
|
|
2119
2902
|
let stat2;
|
|
2120
2903
|
try {
|
|
2121
|
-
stat2 =
|
|
2904
|
+
stat2 = statSync2(full);
|
|
2122
2905
|
} catch {
|
|
2123
2906
|
continue;
|
|
2124
2907
|
}
|
|
@@ -2156,7 +2939,31 @@ function walkAst(node, visitor) {
|
|
|
2156
2939
|
}
|
|
2157
2940
|
}
|
|
2158
2941
|
}
|
|
2159
|
-
function
|
|
2942
|
+
function toRootPackageName(importSource) {
|
|
2943
|
+
if (importSource.startsWith("@")) {
|
|
2944
|
+
const parts = importSource.split("/");
|
|
2945
|
+
return parts.slice(0, 2).join("/");
|
|
2946
|
+
}
|
|
2947
|
+
return importSource.split("/")[0] ?? importSource;
|
|
2948
|
+
}
|
|
2949
|
+
function findTransitiveDependencyPath(cwd, targetPackage, importedDirectPackages) {
|
|
2950
|
+
for (const directPkg of importedDirectPackages) {
|
|
2951
|
+
const manifestPath = join12(cwd, "node_modules", directPkg, "package.json");
|
|
2952
|
+
if (!existsSync10(manifestPath)) continue;
|
|
2953
|
+
try {
|
|
2954
|
+
const manifest = JSON.parse(readFileSync10(manifestPath, "utf8"));
|
|
2955
|
+
if (manifest.dependencies && manifest.dependencies[targetPackage]) {
|
|
2956
|
+
return {
|
|
2957
|
+
directDependency: directPkg,
|
|
2958
|
+
version: manifest.dependencies[targetPackage]
|
|
2959
|
+
};
|
|
2960
|
+
}
|
|
2961
|
+
} catch {
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
return null;
|
|
2965
|
+
}
|
|
2966
|
+
function extractPackageReferences(filename, content, packageName, tsconfig, outImportedDirectPackages) {
|
|
2160
2967
|
const results = [];
|
|
2161
2968
|
let parseResult;
|
|
2162
2969
|
try {
|
|
@@ -2168,6 +2975,9 @@ function extractPackageReferences(filename, content, packageName, tsconfig) {
|
|
|
2168
2975
|
walkAst(parseResult.program, (node) => {
|
|
2169
2976
|
if (node.type === "ImportDeclaration") {
|
|
2170
2977
|
const source = node.source;
|
|
2978
|
+
if (source?.value && !source.value.startsWith(".") && !isAbsolute2(source.value)) {
|
|
2979
|
+
outImportedDirectPackages?.add(toRootPackageName(source.value));
|
|
2980
|
+
}
|
|
2171
2981
|
if (source?.value && matchesPackage(source.value, packageName, tsconfig)) {
|
|
2172
2982
|
const symbols = [];
|
|
2173
2983
|
const importedIdentifiers = /* @__PURE__ */ new Map();
|
|
@@ -2202,6 +3012,9 @@ function extractPackageReferences(filename, content, packageName, tsconfig) {
|
|
|
2202
3012
|
}
|
|
2203
3013
|
if (node.type === "ImportExpression") {
|
|
2204
3014
|
const source = node.source;
|
|
3015
|
+
if (source?.value && !source.value.startsWith(".") && !isAbsolute2(source.value)) {
|
|
3016
|
+
outImportedDirectPackages?.add(toRootPackageName(source.value));
|
|
3017
|
+
}
|
|
2205
3018
|
if (source?.value && matchesPackage(source.value, packageName, tsconfig)) {
|
|
2206
3019
|
results.push({
|
|
2207
3020
|
matchType: "dynamic-import",
|
|
@@ -2213,6 +3026,9 @@ function extractPackageReferences(filename, content, packageName, tsconfig) {
|
|
|
2213
3026
|
}
|
|
2214
3027
|
if (node.type === "CallExpression" && node.callee?.name === "require" && Array.isArray(node.arguments) && node.arguments.length > 0) {
|
|
2215
3028
|
const firstArg = node.arguments[0];
|
|
3029
|
+
if (firstArg?.value && typeof firstArg.value === "string" && !firstArg.value.startsWith(".") && !isAbsolute2(firstArg.value)) {
|
|
3030
|
+
outImportedDirectPackages?.add(toRootPackageName(firstArg.value));
|
|
3031
|
+
}
|
|
2216
3032
|
if (firstArg?.value && typeof firstArg.value === "string" && matchesPackage(firstArg.value, packageName, tsconfig)) {
|
|
2217
3033
|
results.push({
|
|
2218
3034
|
matchType: "require",
|
|
@@ -2224,6 +3040,9 @@ function extractPackageReferences(filename, content, packageName, tsconfig) {
|
|
|
2224
3040
|
}
|
|
2225
3041
|
if (node.type === "ExportNamedDeclaration" || node.type === "ExportAllDeclaration") {
|
|
2226
3042
|
const source = node.source;
|
|
3043
|
+
if (source?.value && !source.value.startsWith(".") && !isAbsolute2(source.value)) {
|
|
3044
|
+
outImportedDirectPackages?.add(toRootPackageName(source.value));
|
|
3045
|
+
}
|
|
2227
3046
|
if (source?.value && matchesPackage(source.value, packageName, tsconfig)) {
|
|
2228
3047
|
const symbols = [];
|
|
2229
3048
|
if (node.type === "ExportAllDeclaration") {
|
|
@@ -2300,15 +3119,16 @@ function assessPackageReachability(cwd, packageName, targetSymbol) {
|
|
|
2300
3119
|
let hasReExport = false;
|
|
2301
3120
|
let totalInvocations = 0;
|
|
2302
3121
|
let hasPackageImport = false;
|
|
3122
|
+
const importedDirectPackages = /* @__PURE__ */ new Set();
|
|
2303
3123
|
for (const filePath of walkSourceFiles(cwd)) {
|
|
2304
3124
|
scanned++;
|
|
2305
3125
|
let content;
|
|
2306
3126
|
try {
|
|
2307
|
-
content =
|
|
3127
|
+
content = readFileSync10(filePath, "utf8");
|
|
2308
3128
|
} catch {
|
|
2309
3129
|
continue;
|
|
2310
3130
|
}
|
|
2311
|
-
const refs = extractPackageReferences(filePath, content, packageName, tsconfig);
|
|
3131
|
+
const refs = extractPackageReferences(filePath, content, packageName, tsconfig, importedDirectPackages);
|
|
2312
3132
|
if (refs.length > 0) {
|
|
2313
3133
|
hasPackageImport = true;
|
|
2314
3134
|
}
|
|
@@ -2351,6 +3171,16 @@ function assessPackageReachability(cwd, packageName, targetSymbol) {
|
|
|
2351
3171
|
};
|
|
2352
3172
|
}
|
|
2353
3173
|
if (!hasPackageImport) {
|
|
3174
|
+
const transitivePath = findTransitiveDependencyPath(cwd, packageName, importedDirectPackages);
|
|
3175
|
+
if (transitivePath) {
|
|
3176
|
+
return {
|
|
3177
|
+
packageName,
|
|
3178
|
+
status: "reachable",
|
|
3179
|
+
reason: `Package '${packageName}' is a transitive dependency of imported package '${transitivePath.directDependency}'`,
|
|
3180
|
+
reachabilityBasis: "transitive-path-found",
|
|
3181
|
+
evidence: []
|
|
3182
|
+
};
|
|
3183
|
+
}
|
|
2354
3184
|
return {
|
|
2355
3185
|
packageName,
|
|
2356
3186
|
status: "not-reachable",
|
|
@@ -2510,230 +3340,33 @@ function buildSbom(packages, vulnerableNames, results) {
|
|
|
2510
3340
|
statusByPackage.set(result.packageName, "suppressed");
|
|
2511
3341
|
} else if (!result.applied && result.strategy === "none") {
|
|
2512
3342
|
statusByPackage.set(result.packageName, "skipped");
|
|
2513
|
-
} else if (result.applied) {
|
|
2514
|
-
statusByPackage.set(result.packageName, "patched");
|
|
2515
|
-
} else {
|
|
2516
|
-
statusByPackage.set(result.packageName, "unpatched");
|
|
2517
|
-
}
|
|
2518
|
-
}
|
|
2519
|
-
return packages.map((pkg) => {
|
|
2520
|
-
const isVulnerable = vulnerableNames.has(pkg.name);
|
|
2521
|
-
const entry = {
|
|
2522
|
-
name: pkg.name,
|
|
2523
|
-
version: pkg.version,
|
|
2524
|
-
scope: pkg.type === "direct" ? "direct" : "transitive"
|
|
2525
|
-
};
|
|
2526
|
-
if (isVulnerable) {
|
|
2527
|
-
entry.status = statusByPackage.get(pkg.name) ?? "unpatched";
|
|
2528
|
-
}
|
|
2529
|
-
return entry;
|
|
2530
|
-
});
|
|
2531
|
-
}
|
|
2532
|
-
|
|
2533
|
-
// src/intelligence/sources/registry.ts
|
|
2534
|
-
import { readFileSync as readFileSync8 } from "fs";
|
|
2535
|
-
import { join as join10 } from "path";
|
|
2536
|
-
import { execa as execa4 } from "execa";
|
|
2537
|
-
import semver from "semver";
|
|
2538
|
-
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
2539
|
-
async function fetchPackageVersions(packageName) {
|
|
2540
|
-
const url = `${NPM_REGISTRY}/${encodeURIComponent(packageName)}`;
|
|
2541
|
-
try {
|
|
2542
|
-
const res = await httpClient({ url });
|
|
2543
|
-
if (res.status === 404) return [];
|
|
2544
|
-
if (!res.ok) {
|
|
2545
|
-
throw new Error(`npm registry error ${res.status} for "${packageName}": ${res.text}`);
|
|
2546
|
-
}
|
|
2547
|
-
const data = res.data;
|
|
2548
|
-
return Object.keys(data.versions);
|
|
2549
|
-
} catch (err) {
|
|
2550
|
-
if (err instanceof Error && err.message.includes("registry error")) {
|
|
2551
|
-
throw err;
|
|
2552
|
-
}
|
|
2553
|
-
return [];
|
|
2554
|
-
}
|
|
2555
|
-
}
|
|
2556
|
-
async function resolveSafeUpgradeVersion(packageName, installedVersion, firstPatchedVersion, vulnerableRange) {
|
|
2557
|
-
const versions = await fetchPackageVersions(packageName);
|
|
2558
|
-
if (!versions.length) {
|
|
2559
|
-
return {
|
|
2560
|
-
candidates: {},
|
|
2561
|
-
majorOnlyFixAvailable: false
|
|
2562
|
-
};
|
|
2563
|
-
}
|
|
2564
|
-
const installed = semver.parse(installedVersion);
|
|
2565
|
-
const candidates = versions.filter((v) => semver.valid(v) && semver.gte(v, firstPatchedVersion)).filter((v) => {
|
|
2566
|
-
if (!vulnerableRange) return true;
|
|
2567
|
-
try {
|
|
2568
|
-
return !semver.satisfies(v, vulnerableRange, { includePrerelease: false });
|
|
2569
|
-
} catch {
|
|
2570
|
-
return true;
|
|
2571
|
-
}
|
|
2572
|
-
}).sort(semver.compare);
|
|
2573
|
-
if (!candidates.length) {
|
|
2574
|
-
return {
|
|
2575
|
-
candidates: {},
|
|
2576
|
-
majorOnlyFixAvailable: false
|
|
2577
|
-
};
|
|
2578
|
-
}
|
|
2579
|
-
const categorizedCandidates = {};
|
|
2580
|
-
for (const candidate of candidates) {
|
|
2581
|
-
const level = classifyUpgradeLevel(installedVersion, candidate);
|
|
2582
|
-
if (!level) continue;
|
|
2583
|
-
if (!categorizedCandidates[level]) {
|
|
2584
|
-
categorizedCandidates[level] = candidate;
|
|
2585
|
-
}
|
|
2586
|
-
}
|
|
2587
|
-
const safeVersion = categorizedCandidates.patch ?? categorizedCandidates.minor ?? categorizedCandidates.major;
|
|
2588
|
-
if (!safeVersion) {
|
|
2589
|
-
return {
|
|
2590
|
-
candidates: categorizedCandidates,
|
|
2591
|
-
majorOnlyFixAvailable: false
|
|
2592
|
-
};
|
|
2593
|
-
}
|
|
2594
|
-
const upgradeLevel = classifyUpgradeLevel(installedVersion, safeVersion);
|
|
2595
|
-
const majorOnlyFixAvailable = !categorizedCandidates.patch && !categorizedCandidates.minor && Boolean(categorizedCandidates.major);
|
|
2596
|
-
if (!installed || !upgradeLevel) {
|
|
2597
|
-
return {
|
|
2598
|
-
safeVersion,
|
|
2599
|
-
upgradeLevel,
|
|
2600
|
-
candidates: categorizedCandidates,
|
|
2601
|
-
majorOnlyFixAvailable
|
|
2602
|
-
};
|
|
2603
|
-
}
|
|
2604
|
-
return {
|
|
2605
|
-
safeVersion,
|
|
2606
|
-
upgradeLevel,
|
|
2607
|
-
candidates: categorizedCandidates,
|
|
2608
|
-
majorOnlyFixAvailable
|
|
2609
|
-
};
|
|
2610
|
-
}
|
|
2611
|
-
function classifyUpgradeLevel(installedVersion, candidateVersion) {
|
|
2612
|
-
const installed = semver.parse(installedVersion);
|
|
2613
|
-
const candidate = semver.parse(candidateVersion);
|
|
2614
|
-
if (!installed || !candidate) return void 0;
|
|
2615
|
-
if (candidate.major > installed.major) return "major";
|
|
2616
|
-
if (candidate.minor > installed.minor) return "minor";
|
|
2617
|
-
if (candidate.patch > installed.patch || candidate.version === installed.version) {
|
|
2618
|
-
return "patch";
|
|
2619
|
-
}
|
|
2620
|
-
return void 0;
|
|
2621
|
-
}
|
|
2622
|
-
async function queryOutdatedPackages(cwd, options = {}) {
|
|
2623
|
-
const pm = options.packageManager ?? detectPackageManager(cwd);
|
|
2624
|
-
let stdout;
|
|
2625
|
-
try {
|
|
2626
|
-
const args = pm === "pnpm" ? ["outdated", "--json"] : ["outdated", "--json"];
|
|
2627
|
-
const yarnArgs = pm === "yarn" ? ["outdated", "--json"] : void 0;
|
|
2628
|
-
const finalArgs = yarnArgs ?? args;
|
|
2629
|
-
const result2 = await execa4(pm, finalArgs, {
|
|
2630
|
-
cwd,
|
|
2631
|
-
reject: false,
|
|
2632
|
-
stdio: "pipe"
|
|
2633
|
-
});
|
|
2634
|
-
if (result2.exitCode !== 0 && result2.exitCode !== 1) {
|
|
2635
|
-
const stderr = typeof result2.stderr === "string" ? result2.stderr : "";
|
|
2636
|
-
if (!result2.stdout) {
|
|
2637
|
-
throw new Error(
|
|
2638
|
-
`${pm} outdated --json failed (exit ${result2.exitCode}): ${stderr.slice(0, 500)}`
|
|
2639
|
-
);
|
|
2640
|
-
}
|
|
2641
|
-
}
|
|
2642
|
-
stdout = typeof result2.stdout === "string" ? result2.stdout : "";
|
|
2643
|
-
} catch (err) {
|
|
2644
|
-
if (err instanceof Error && !err.message.includes("outdated --json failed")) {
|
|
2645
|
-
throw new Error(`Failed to query outdated packages: ${err.message}`);
|
|
2646
|
-
}
|
|
2647
|
-
throw err;
|
|
2648
|
-
}
|
|
2649
|
-
if (!stdout.trim()) {
|
|
2650
|
-
return /* @__PURE__ */ new Map();
|
|
2651
|
-
}
|
|
2652
|
-
const rawEntries = {};
|
|
2653
|
-
if (pm === "yarn") {
|
|
2654
|
-
for (const line of stdout.split("\n")) {
|
|
2655
|
-
const trimmed = line.trim();
|
|
2656
|
-
if (!trimmed) continue;
|
|
2657
|
-
try {
|
|
2658
|
-
const parsed = JSON.parse(trimmed);
|
|
2659
|
-
if (parsed.type === "table" && parsed.data) {
|
|
2660
|
-
const tableData = parsed.data;
|
|
2661
|
-
const head = tableData.head ?? [];
|
|
2662
|
-
const currentIdx = head.indexOf("Current");
|
|
2663
|
-
const wantedIdx = head.indexOf("Wanted");
|
|
2664
|
-
const latestIdx = head.indexOf("Latest");
|
|
2665
|
-
for (const row of tableData.body ?? []) {
|
|
2666
|
-
const name = row[0];
|
|
2667
|
-
if (!name) continue;
|
|
2668
|
-
rawEntries[name] = {
|
|
2669
|
-
current: row[currentIdx] ?? "",
|
|
2670
|
-
wanted: row[wantedIdx] ?? "",
|
|
2671
|
-
latest: row[latestIdx] ?? "",
|
|
2672
|
-
type: row[head.indexOf("Package Type")] ?? "dependencies"
|
|
2673
|
-
};
|
|
2674
|
-
}
|
|
2675
|
-
}
|
|
2676
|
-
} catch {
|
|
2677
|
-
}
|
|
2678
|
-
}
|
|
2679
|
-
} else {
|
|
2680
|
-
try {
|
|
2681
|
-
const parsed = JSON.parse(stdout);
|
|
2682
|
-
if (Array.isArray(parsed)) {
|
|
2683
|
-
for (const entry of parsed) {
|
|
2684
|
-
const name = entry.name ?? entry.packageName;
|
|
2685
|
-
if (!name) continue;
|
|
2686
|
-
rawEntries[name] = {
|
|
2687
|
-
current: entry.current,
|
|
2688
|
-
wanted: entry.wanted,
|
|
2689
|
-
latest: entry.latest,
|
|
2690
|
-
type: entry.dependencyType
|
|
2691
|
-
};
|
|
2692
|
-
}
|
|
2693
|
-
} else {
|
|
2694
|
-
Object.assign(rawEntries, parsed);
|
|
2695
|
-
}
|
|
2696
|
-
} catch {
|
|
2697
|
-
throw new Error(`Could not parse ${pm} outdated --json output.`);
|
|
3343
|
+
} else if (result.applied) {
|
|
3344
|
+
statusByPackage.set(result.packageName, "patched");
|
|
3345
|
+
} else {
|
|
3346
|
+
statusByPackage.set(result.packageName, "unpatched");
|
|
2698
3347
|
}
|
|
2699
3348
|
}
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
const
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
const currentVersion = entry.current ?? "";
|
|
2713
|
-
const wantedVersion = entry.wanted ?? currentVersion;
|
|
2714
|
-
const latestVersion = entry.latest ?? currentVersion;
|
|
2715
|
-
if (!currentVersion || !latestVersion) continue;
|
|
2716
|
-
const dependencyScope = directDeps.has(name) ? "direct" : "transitive";
|
|
2717
|
-
if (!options.includeTransitive && dependencyScope === "transitive") continue;
|
|
2718
|
-
const isMajorBump = semver.valid(currentVersion) !== null && semver.valid(latestVersion) !== null && semver.major(latestVersion) > semver.major(currentVersion);
|
|
2719
|
-
result.set(name, {
|
|
2720
|
-
currentVersion,
|
|
2721
|
-
wantedVersion,
|
|
2722
|
-
latestVersion,
|
|
2723
|
-
isMajorBump,
|
|
2724
|
-
dependencyScope
|
|
2725
|
-
});
|
|
2726
|
-
}
|
|
2727
|
-
return result;
|
|
3349
|
+
return packages.map((pkg) => {
|
|
3350
|
+
const isVulnerable = vulnerableNames.has(pkg.name);
|
|
3351
|
+
const entry = {
|
|
3352
|
+
name: pkg.name,
|
|
3353
|
+
version: pkg.version,
|
|
3354
|
+
scope: pkg.type === "direct" ? "direct" : "transitive"
|
|
3355
|
+
};
|
|
3356
|
+
if (isVulnerable) {
|
|
3357
|
+
entry.status = statusByPackage.get(pkg.name) ?? "unpatched";
|
|
3358
|
+
}
|
|
3359
|
+
return entry;
|
|
3360
|
+
});
|
|
2728
3361
|
}
|
|
2729
3362
|
|
|
2730
3363
|
// src/remediation/tools/apply-version-bump.ts
|
|
2731
3364
|
import { z as z4 } from "zod";
|
|
2732
|
-
import { join as
|
|
2733
|
-
import { readFileSync as
|
|
3365
|
+
import { join as join14 } from "path";
|
|
3366
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync3 } from "fs";
|
|
2734
3367
|
|
|
2735
3368
|
// src/platform/safe-exec.ts
|
|
2736
|
-
import { execa as
|
|
3369
|
+
import { execa as execa6 } from "execa";
|
|
2737
3370
|
var DANGEROUS_ENV_VARS = /* @__PURE__ */ new Set([
|
|
2738
3371
|
"NODE_OPTIONS",
|
|
2739
3372
|
"NODE_PATH",
|
|
@@ -2782,7 +3415,7 @@ function safeExeca(file, args = [], options = {}) {
|
|
|
2782
3415
|
options.env ?? process.env,
|
|
2783
3416
|
options.allowNodeOptions
|
|
2784
3417
|
);
|
|
2785
|
-
return
|
|
3418
|
+
return execa6(file, args, {
|
|
2786
3419
|
...options,
|
|
2787
3420
|
timeout,
|
|
2788
3421
|
env: sanitizedEnv
|
|
@@ -2790,19 +3423,19 @@ function safeExeca(file, args = [], options = {}) {
|
|
|
2790
3423
|
}
|
|
2791
3424
|
|
|
2792
3425
|
// src/remediation/tools/apply-version-bump.ts
|
|
2793
|
-
import
|
|
3426
|
+
import semver3 from "semver";
|
|
2794
3427
|
|
|
2795
3428
|
// src/platform/repo-lock.ts
|
|
2796
|
-
import { mkdir, rm } from "fs/promises";
|
|
2797
|
-
import { join as
|
|
3429
|
+
import { mkdir, rm as rm2 } from "fs/promises";
|
|
3430
|
+
import { join as join13 } from "path";
|
|
2798
3431
|
async function sleep(ms) {
|
|
2799
|
-
await new Promise((
|
|
3432
|
+
await new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
2800
3433
|
}
|
|
2801
3434
|
async function acquireRepoLock(cwd, options = {}) {
|
|
2802
3435
|
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
2803
3436
|
const retryDelayMs = options.retryDelayMs ?? 125;
|
|
2804
|
-
const lockRoot =
|
|
2805
|
-
const lockPath =
|
|
3437
|
+
const lockRoot = join13(cwd, ".autoremediator", "locks");
|
|
3438
|
+
const lockPath = join13(cwd, ".autoremediator", "locks", "remediation.lock");
|
|
2806
3439
|
const startedAt = Date.now();
|
|
2807
3440
|
await mkdir(lockRoot, { recursive: true });
|
|
2808
3441
|
while (true) {
|
|
@@ -2811,7 +3444,7 @@ async function acquireRepoLock(cwd, options = {}) {
|
|
|
2811
3444
|
return {
|
|
2812
3445
|
lockPath,
|
|
2813
3446
|
release: async () => {
|
|
2814
|
-
await
|
|
3447
|
+
await rm2(lockPath, { recursive: true, force: true });
|
|
2815
3448
|
}
|
|
2816
3449
|
};
|
|
2817
3450
|
} catch {
|
|
@@ -2843,6 +3476,7 @@ var applyVersionBumpTool = defineTool({
|
|
|
2843
3476
|
dryRun: z4.boolean().default(false).describe("If true, report changes but do not write"),
|
|
2844
3477
|
policy: z4.string().optional().describe("Optional path to .autoremediator policy file"),
|
|
2845
3478
|
runTests: z4.boolean().default(false).describe("If true, run test validation after applying the fix"),
|
|
3479
|
+
typecheck: z4.boolean().default(false).describe("If true, run typecheck validation after applying the fix"),
|
|
2846
3480
|
installMode: z4.enum(["standard", "prefer-offline", "deterministic"]).optional(),
|
|
2847
3481
|
installPreferOffline: z4.boolean().optional(),
|
|
2848
3482
|
enforceFrozenLockfile: z4.boolean().optional(),
|
|
@@ -2857,13 +3491,14 @@ var applyVersionBumpTool = defineTool({
|
|
|
2857
3491
|
dryRun,
|
|
2858
3492
|
policy,
|
|
2859
3493
|
runTests,
|
|
3494
|
+
typecheck,
|
|
2860
3495
|
installMode,
|
|
2861
3496
|
installPreferOffline,
|
|
2862
3497
|
enforceFrozenLockfile,
|
|
2863
3498
|
workspace
|
|
2864
3499
|
}) => {
|
|
2865
3500
|
const pm = packageManager ?? detectPackageManager(cwd);
|
|
2866
|
-
|
|
3501
|
+
let pkgPath = join14(cwd, "package.json");
|
|
2867
3502
|
const loadedPolicy = loadPolicy(cwd, policy);
|
|
2868
3503
|
const commandConstraints = {
|
|
2869
3504
|
...loadedPolicy.constraints,
|
|
@@ -2873,9 +3508,6 @@ var applyVersionBumpTool = defineTool({
|
|
|
2873
3508
|
workspace: workspace ?? loadedPolicy.constraints?.workspace
|
|
2874
3509
|
};
|
|
2875
3510
|
const yarnMajor = pm === "yarn" ? await getYarnMajorVersion(cwd) : void 0;
|
|
2876
|
-
const installCommand = resolveInstallCommand(pm, commandConstraints, yarnMajor);
|
|
2877
|
-
const testCommand = resolveTestCommand(pm, commandConstraints);
|
|
2878
|
-
const dedupeCommand = resolveDedupeCommand(pm, commandConstraints);
|
|
2879
3511
|
if (!isPackageAllowed(loadedPolicy, packageName)) {
|
|
2880
3512
|
return {
|
|
2881
3513
|
packageName,
|
|
@@ -2888,7 +3520,7 @@ var applyVersionBumpTool = defineTool({
|
|
|
2888
3520
|
message: `Policy blocked changes for package "${packageName}".`
|
|
2889
3521
|
};
|
|
2890
3522
|
}
|
|
2891
|
-
const isMajorBump =
|
|
3523
|
+
const isMajorBump = semver3.valid(fromVersion) && semver3.valid(toVersion) && semver3.major(toVersion) > semver3.major(fromVersion);
|
|
2892
3524
|
if (isMajorBump && !loadedPolicy.allowMajorBumps) {
|
|
2893
3525
|
return {
|
|
2894
3526
|
packageName,
|
|
@@ -2901,9 +3533,10 @@ var applyVersionBumpTool = defineTool({
|
|
|
2901
3533
|
message: `Policy blocked major bump for "${packageName}" (${fromVersion} -> ${toVersion}).`
|
|
2902
3534
|
};
|
|
2903
3535
|
}
|
|
3536
|
+
pkgPath = join14(cwd, "package.json");
|
|
2904
3537
|
let pkgJson;
|
|
2905
3538
|
try {
|
|
2906
|
-
pkgJson = JSON.parse(
|
|
3539
|
+
pkgJson = JSON.parse(readFileSync11(pkgPath, "utf8"));
|
|
2907
3540
|
} catch {
|
|
2908
3541
|
return {
|
|
2909
3542
|
packageName,
|
|
@@ -2915,9 +3548,20 @@ var applyVersionBumpTool = defineTool({
|
|
|
2915
3548
|
message: `Could not read package.json at "${pkgPath}".`
|
|
2916
3549
|
};
|
|
2917
3550
|
}
|
|
2918
|
-
|
|
3551
|
+
let depField = ["dependencies", "devDependencies", "peerDependencies"].find(
|
|
2919
3552
|
(f) => pkgJson[f]?.[packageName] !== void 0
|
|
2920
3553
|
);
|
|
3554
|
+
if (!depField) {
|
|
3555
|
+
const declaringWorkspace = findDeclaringWorkspacePackage(cwd, packageName, workspace);
|
|
3556
|
+
if (declaringWorkspace) {
|
|
3557
|
+
pkgPath = declaringWorkspace.manifestPath;
|
|
3558
|
+
pkgJson = declaringWorkspace.manifest;
|
|
3559
|
+
depField = ["dependencies", "devDependencies", "peerDependencies"].find(
|
|
3560
|
+
(f) => pkgJson[f]?.[packageName] !== void 0
|
|
3561
|
+
);
|
|
3562
|
+
commandConstraints.workspace = workspace ?? declaringWorkspace.name;
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
2921
3565
|
if (!depField) {
|
|
2922
3566
|
return {
|
|
2923
3567
|
packageName,
|
|
@@ -2933,9 +3577,14 @@ var applyVersionBumpTool = defineTool({
|
|
|
2933
3577
|
const prefixMatch = currentRange.match(/^([~^]?)/);
|
|
2934
3578
|
const prefix = prefixMatch?.[1] ?? "";
|
|
2935
3579
|
const newRange = `${prefix}${toVersion}`;
|
|
3580
|
+
const effectiveInstallCommand = resolveInstallCommand(pm, commandConstraints, yarnMajor);
|
|
3581
|
+
const effectiveTestCommand = resolveTestCommand(pm, commandConstraints);
|
|
3582
|
+
const effectiveTypecheckCommand = resolveTypecheckCommand(pm, commandConstraints, cwd);
|
|
3583
|
+
const effectiveDedupeCommand = resolveDedupeCommand(pm, commandConstraints);
|
|
3584
|
+
const shouldTypecheck = typecheck || Boolean(commandConstraints.typecheck);
|
|
2936
3585
|
if (dryRun) {
|
|
2937
|
-
const installCmd =
|
|
2938
|
-
const testCmd =
|
|
3586
|
+
const installCmd = effectiveInstallCommand.join(" ");
|
|
3587
|
+
const testCmd = effectiveTestCommand.join(" ");
|
|
2939
3588
|
return {
|
|
2940
3589
|
packageName,
|
|
2941
3590
|
strategy: "version-bump",
|
|
@@ -2943,14 +3592,14 @@ var applyVersionBumpTool = defineTool({
|
|
|
2943
3592
|
toVersion,
|
|
2944
3593
|
applied: false,
|
|
2945
3594
|
dryRun: true,
|
|
2946
|
-
message: `[DRY RUN] Would update ${depField}.${packageName}: "${currentRange}" -> "${newRange}", then run ${installCmd}${runTests ? ` and ${testCmd}` : ""}${
|
|
3595
|
+
message: `[DRY RUN] Would update ${depField}.${packageName}: "${currentRange}" -> "${newRange}", then run ${installCmd}${runTests ? ` and ${testCmd}` : ""}${effectiveDedupeCommand.length > 0 ? ` and ${effectiveDedupeCommand.join(" ")} (best-effort)` : ""}.`
|
|
2947
3596
|
};
|
|
2948
3597
|
}
|
|
2949
3598
|
return withRepoLock(cwd, async () => {
|
|
2950
3599
|
pkgJson[depField][packageName] = newRange;
|
|
2951
3600
|
writeFileSync3(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
2952
3601
|
try {
|
|
2953
|
-
const [installCmd, ...installArgs] =
|
|
3602
|
+
const [installCmd, ...installArgs] = effectiveInstallCommand;
|
|
2954
3603
|
await safeExeca(installCmd, installArgs, {
|
|
2955
3604
|
cwd,
|
|
2956
3605
|
stdio: "pipe"
|
|
@@ -2967,12 +3616,12 @@ var applyVersionBumpTool = defineTool({
|
|
|
2967
3616
|
applied: false,
|
|
2968
3617
|
dryRun: false,
|
|
2969
3618
|
unresolvedReason: "install-failed",
|
|
2970
|
-
message: `${
|
|
3619
|
+
message: `${effectiveInstallCommand.join(" ")} failed after updating "${packageName}" to ${toVersion}. Reverted. Error: ${message}`
|
|
2971
3620
|
};
|
|
2972
3621
|
}
|
|
2973
3622
|
if (runTests) {
|
|
2974
3623
|
try {
|
|
2975
|
-
const [testCmd, ...testArgs] =
|
|
3624
|
+
const [testCmd, ...testArgs] = effectiveTestCommand;
|
|
2976
3625
|
await safeExeca(testCmd, testArgs, {
|
|
2977
3626
|
cwd,
|
|
2978
3627
|
stdio: "pipe"
|
|
@@ -2981,7 +3630,38 @@ var applyVersionBumpTool = defineTool({
|
|
|
2981
3630
|
pkgJson[depField][packageName] = currentRange;
|
|
2982
3631
|
writeFileSync3(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
2983
3632
|
try {
|
|
2984
|
-
const [rollbackCmd, ...rollbackArgs] =
|
|
3633
|
+
const [rollbackCmd, ...rollbackArgs] = effectiveInstallCommand;
|
|
3634
|
+
await safeExeca(rollbackCmd, rollbackArgs, {
|
|
3635
|
+
cwd,
|
|
3636
|
+
stdio: "pipe"
|
|
3637
|
+
});
|
|
3638
|
+
} catch {
|
|
3639
|
+
}
|
|
3640
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3641
|
+
return {
|
|
3642
|
+
packageName,
|
|
3643
|
+
strategy: "version-bump",
|
|
3644
|
+
fromVersion,
|
|
3645
|
+
toVersion,
|
|
3646
|
+
applied: false,
|
|
3647
|
+
dryRun: false,
|
|
3648
|
+
unresolvedReason: "validation-failed",
|
|
3649
|
+
message: `${effectiveTestCommand.join(" ")} failed after upgrading "${packageName}" to ${toVersion}. Rolled back to ${currentRange}. Error: ${message}`
|
|
3650
|
+
};
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
if (shouldTypecheck && effectiveTypecheckCommand.length > 0) {
|
|
3654
|
+
try {
|
|
3655
|
+
const [tcCmd, ...tcArgs] = effectiveTypecheckCommand;
|
|
3656
|
+
await safeExeca(tcCmd, tcArgs, {
|
|
3657
|
+
cwd,
|
|
3658
|
+
stdio: "pipe"
|
|
3659
|
+
});
|
|
3660
|
+
} catch (err) {
|
|
3661
|
+
pkgJson[depField][packageName] = currentRange;
|
|
3662
|
+
writeFileSync3(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
3663
|
+
try {
|
|
3664
|
+
const [rollbackCmd, ...rollbackArgs] = effectiveInstallCommand;
|
|
2985
3665
|
await safeExeca(rollbackCmd, rollbackArgs, {
|
|
2986
3666
|
cwd,
|
|
2987
3667
|
stdio: "pipe"
|
|
@@ -2997,20 +3677,22 @@ var applyVersionBumpTool = defineTool({
|
|
|
2997
3677
|
applied: false,
|
|
2998
3678
|
dryRun: false,
|
|
2999
3679
|
unresolvedReason: "validation-failed",
|
|
3000
|
-
message:
|
|
3680
|
+
message: `TypeCheck validation failed (${effectiveTypecheckCommand.join(" ")}) after upgrading "${packageName}" to ${toVersion}. Rolled back to ${currentRange}. Error: ${message}`
|
|
3001
3681
|
};
|
|
3002
3682
|
}
|
|
3003
3683
|
}
|
|
3004
3684
|
let dedupeNote = "";
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3685
|
+
if (effectiveDedupeCommand.length > 0) {
|
|
3686
|
+
try {
|
|
3687
|
+
const [dedupeCmd, ...dedupeArgs] = effectiveDedupeCommand;
|
|
3688
|
+
await safeExeca(dedupeCmd, dedupeArgs, {
|
|
3689
|
+
cwd,
|
|
3690
|
+
stdio: "pipe"
|
|
3691
|
+
});
|
|
3692
|
+
} catch (err) {
|
|
3693
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3694
|
+
dedupeNote = ` Dedupe warning: ${effectiveDedupeCommand.join(" ")} failed (${message}).`;
|
|
3695
|
+
}
|
|
3014
3696
|
}
|
|
3015
3697
|
return {
|
|
3016
3698
|
packageName,
|
|
@@ -3019,7 +3701,7 @@ var applyVersionBumpTool = defineTool({
|
|
|
3019
3701
|
toVersion,
|
|
3020
3702
|
applied: true,
|
|
3021
3703
|
dryRun: false,
|
|
3022
|
-
message: `Successfully upgraded "${packageName}" from ${fromVersion} to ${toVersion}, ran ${
|
|
3704
|
+
message: `Successfully upgraded "${packageName}" from ${fromVersion} to ${toVersion} in ${depField}, then ran ${effectiveInstallCommand.join(" ")}${runTests ? ` and passed ${effectiveTestCommand.join(" ")}` : ""}.${dedupeNote}`
|
|
3023
3705
|
};
|
|
3024
3706
|
});
|
|
3025
3707
|
}
|
|
@@ -3027,9 +3709,9 @@ var applyVersionBumpTool = defineTool({
|
|
|
3027
3709
|
|
|
3028
3710
|
// src/remediation/tools/apply-package-override/index.ts
|
|
3029
3711
|
import { z as z5 } from "zod";
|
|
3030
|
-
import { join as
|
|
3031
|
-
import { existsSync as
|
|
3032
|
-
import
|
|
3712
|
+
import { join as join15 } from "path";
|
|
3713
|
+
import { existsSync as existsSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
|
|
3714
|
+
import semver4 from "semver";
|
|
3033
3715
|
|
|
3034
3716
|
// src/remediation/tools/apply-package-override/helpers.ts
|
|
3035
3717
|
async function collectDependencyTrace(cwd, pm, packageName, constraints) {
|
|
@@ -3171,9 +3853,9 @@ var applyPackageOverrideTool = defineTool({
|
|
|
3171
3853
|
workspace
|
|
3172
3854
|
}) => {
|
|
3173
3855
|
const pm = packageManager ?? detectPackageManager(cwd);
|
|
3174
|
-
const pkgPath =
|
|
3175
|
-
const denoJsonPath =
|
|
3176
|
-
const isDenoNative = pm === "deno" && !
|
|
3856
|
+
const pkgPath = join15(cwd, "package.json");
|
|
3857
|
+
const denoJsonPath = join15(cwd, "deno.json");
|
|
3858
|
+
const isDenoNative = pm === "deno" && !existsSync11(pkgPath);
|
|
3177
3859
|
const loadedPolicy = loadPolicy(cwd, policy);
|
|
3178
3860
|
const commandConstraints = {
|
|
3179
3861
|
...loadedPolicy.constraints,
|
|
@@ -3199,7 +3881,7 @@ var applyPackageOverrideTool = defineTool({
|
|
|
3199
3881
|
message: `Policy blocked changes for package "${packageName}".`
|
|
3200
3882
|
};
|
|
3201
3883
|
}
|
|
3202
|
-
const isMajorBump =
|
|
3884
|
+
const isMajorBump = semver4.valid(fromVersion) && semver4.valid(toVersion) && semver4.major(toVersion) > semver4.major(fromVersion);
|
|
3203
3885
|
if (isMajorBump && !loadedPolicy.allowMajorBumps) {
|
|
3204
3886
|
return {
|
|
3205
3887
|
packageName,
|
|
@@ -3215,7 +3897,7 @@ var applyPackageOverrideTool = defineTool({
|
|
|
3215
3897
|
if (isDenoNative) {
|
|
3216
3898
|
let denoJson;
|
|
3217
3899
|
try {
|
|
3218
|
-
denoJson = JSON.parse(
|
|
3900
|
+
denoJson = JSON.parse(readFileSync12(denoJsonPath, "utf8"));
|
|
3219
3901
|
} catch {
|
|
3220
3902
|
return {
|
|
3221
3903
|
packageName,
|
|
@@ -3318,7 +4000,7 @@ var applyPackageOverrideTool = defineTool({
|
|
|
3318
4000
|
}
|
|
3319
4001
|
let pkgJson;
|
|
3320
4002
|
try {
|
|
3321
|
-
pkgJson = JSON.parse(
|
|
4003
|
+
pkgJson = JSON.parse(readFileSync12(pkgPath, "utf8"));
|
|
3322
4004
|
} catch {
|
|
3323
4005
|
return {
|
|
3324
4006
|
packageName,
|
|
@@ -3415,6 +4097,76 @@ var applyPackageOverrideTool = defineTool({
|
|
|
3415
4097
|
}
|
|
3416
4098
|
});
|
|
3417
4099
|
|
|
4100
|
+
// src/platform/supply-chain.ts
|
|
4101
|
+
async function verifyPackageTrust(packageName, targetVersion, policy) {
|
|
4102
|
+
if (!policy || !policy.minPublishAgeHours && !policy.blockInstallScripts && !policy.requireProvenance) {
|
|
4103
|
+
return { trusted: true };
|
|
4104
|
+
}
|
|
4105
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;
|
|
4106
|
+
let packument = null;
|
|
4107
|
+
try {
|
|
4108
|
+
const res = await httpClient({
|
|
4109
|
+
url,
|
|
4110
|
+
headers: { Accept: "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8" }
|
|
4111
|
+
});
|
|
4112
|
+
if (res.ok && res.data) {
|
|
4113
|
+
packument = res.data;
|
|
4114
|
+
}
|
|
4115
|
+
} catch (err) {
|
|
4116
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4117
|
+
return {
|
|
4118
|
+
trusted: false,
|
|
4119
|
+
reason: `Could not verify supply-chain trust for "${packageName}@${targetVersion}": ${msg}`
|
|
4120
|
+
};
|
|
4121
|
+
}
|
|
4122
|
+
if (!packument) {
|
|
4123
|
+
return {
|
|
4124
|
+
trusted: false,
|
|
4125
|
+
reason: `Package packument for "${packageName}" not found on registry.`
|
|
4126
|
+
};
|
|
4127
|
+
}
|
|
4128
|
+
let publishAgeHours;
|
|
4129
|
+
if (packument.time?.[targetVersion]) {
|
|
4130
|
+
const publishedAt = new Date(packument.time[targetVersion]).getTime();
|
|
4131
|
+
publishAgeHours = Math.max(0, (Date.now() - publishedAt) / (1e3 * 60 * 60));
|
|
4132
|
+
if (policy.minPublishAgeHours && publishAgeHours < policy.minPublishAgeHours) {
|
|
4133
|
+
return {
|
|
4134
|
+
trusted: false,
|
|
4135
|
+
publishAgeHours,
|
|
4136
|
+
reason: `Target release "${packageName}@${targetVersion}" was published only ${publishAgeHours.toFixed(1)}h ago (minimum required age is ${policy.minPublishAgeHours}h).`
|
|
4137
|
+
};
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
const versionData = packument.versions?.[targetVersion];
|
|
4141
|
+
const scripts = versionData?.scripts ?? {};
|
|
4142
|
+
const installScriptKeys = ["preinstall", "install", "postinstall"].filter((k) => Boolean(scripts[k]));
|
|
4143
|
+
const hasInstallScripts = installScriptKeys.length > 0;
|
|
4144
|
+
if (policy.blockInstallScripts && hasInstallScripts) {
|
|
4145
|
+
return {
|
|
4146
|
+
trusted: false,
|
|
4147
|
+
publishAgeHours,
|
|
4148
|
+
hasInstallScripts: true,
|
|
4149
|
+
reason: `Target release "${packageName}@${targetVersion}" defines risky lifecycle scripts (${installScriptKeys.join(", ")}).`
|
|
4150
|
+
};
|
|
4151
|
+
}
|
|
4152
|
+
const hasProvenance = Boolean(versionData?.dist?.attestations?.provenance || versionData?.dist?.attestations?.url);
|
|
4153
|
+
if (policy.requireProvenance && !hasProvenance) {
|
|
4154
|
+
return {
|
|
4155
|
+
trusted: false,
|
|
4156
|
+
publishAgeHours,
|
|
4157
|
+
hasInstallScripts,
|
|
4158
|
+
hasProvenance: false,
|
|
4159
|
+
reason: `Target release "${packageName}@${targetVersion}" does not have cryptographic build provenance attestations.`
|
|
4160
|
+
};
|
|
4161
|
+
}
|
|
4162
|
+
return {
|
|
4163
|
+
trusted: true,
|
|
4164
|
+
publishAgeHours,
|
|
4165
|
+
hasInstallScripts,
|
|
4166
|
+
hasProvenance
|
|
4167
|
+
};
|
|
4168
|
+
}
|
|
4169
|
+
|
|
3418
4170
|
// src/remediation/local/primary-strategy.ts
|
|
3419
4171
|
async function resolvePrimaryResult(params) {
|
|
3420
4172
|
const { vulnerable, cwd, packageManager, dryRun, policy, runTests, constraints } = params;
|
|
@@ -3485,6 +4237,22 @@ async function resolvePrimaryResult(params) {
|
|
|
3485
4237
|
}
|
|
3486
4238
|
};
|
|
3487
4239
|
}
|
|
4240
|
+
const trust2 = await verifyPackageTrust(pkg.name, safeUpgrade2.safeVersion, constraints.supplyChainPolicy);
|
|
4241
|
+
if (!trust2.trusted) {
|
|
4242
|
+
return {
|
|
4243
|
+
steps: 1,
|
|
4244
|
+
result: {
|
|
4245
|
+
packageName: pkg.name,
|
|
4246
|
+
strategy: "none",
|
|
4247
|
+
fromVersion: pkg.version,
|
|
4248
|
+
applied: false,
|
|
4249
|
+
dryRun,
|
|
4250
|
+
unresolvedReason: "policy-blocked",
|
|
4251
|
+
vulnerableRange: vulnerable.affected.vulnerableRange,
|
|
4252
|
+
message: `Supply chain trust gate blocked "${pkg.name}@${safeUpgrade2.safeVersion}": ${trust2.reason}`
|
|
4253
|
+
}
|
|
4254
|
+
};
|
|
4255
|
+
}
|
|
3488
4256
|
const overrideResult = await applyPackageOverrideTool.execute({
|
|
3489
4257
|
cwd,
|
|
3490
4258
|
packageManager,
|
|
@@ -3540,6 +4308,22 @@ async function resolvePrimaryResult(params) {
|
|
|
3540
4308
|
}
|
|
3541
4309
|
};
|
|
3542
4310
|
}
|
|
4311
|
+
const trust = await verifyPackageTrust(pkg.name, safeUpgrade.safeVersion, constraints.supplyChainPolicy);
|
|
4312
|
+
if (!trust.trusted) {
|
|
4313
|
+
return {
|
|
4314
|
+
steps: 1,
|
|
4315
|
+
result: {
|
|
4316
|
+
packageName: pkg.name,
|
|
4317
|
+
strategy: "none",
|
|
4318
|
+
fromVersion: pkg.version,
|
|
4319
|
+
applied: false,
|
|
4320
|
+
dryRun,
|
|
4321
|
+
unresolvedReason: "policy-blocked",
|
|
4322
|
+
vulnerableRange: vulnerable.affected.vulnerableRange,
|
|
4323
|
+
message: `Supply chain trust gate blocked "${pkg.name}@${safeUpgrade.safeVersion}": ${trust.reason}`
|
|
4324
|
+
}
|
|
4325
|
+
};
|
|
4326
|
+
}
|
|
3543
4327
|
const applyResult = await applyVersionBumpTool.execute({
|
|
3544
4328
|
cwd,
|
|
3545
4329
|
packageManager,
|
|
@@ -3549,6 +4333,7 @@ async function resolvePrimaryResult(params) {
|
|
|
3549
4333
|
dryRun,
|
|
3550
4334
|
policy,
|
|
3551
4335
|
runTests,
|
|
4336
|
+
typecheck: constraints.typecheck,
|
|
3552
4337
|
installMode: constraints.installMode,
|
|
3553
4338
|
installPreferOffline: constraints.installPreferOffline,
|
|
3554
4339
|
enforceFrozenLockfile: constraints.enforceFrozenLockfile,
|
|
@@ -3562,10 +4347,10 @@ async function resolvePrimaryResult(params) {
|
|
|
3562
4347
|
|
|
3563
4348
|
// src/remediation/tools/fetch-package-source.ts
|
|
3564
4349
|
import { z as z6 } from "zod";
|
|
3565
|
-
import { mkdir as mkdir2, mkdtemp, readdir as readdir2, readFile as
|
|
3566
|
-
import { join as
|
|
4350
|
+
import { mkdir as mkdir2, mkdtemp, readdir as readdir2, readFile as readFile4, rm as rm3 } from "fs/promises";
|
|
4351
|
+
import { join as join16 } from "path";
|
|
3567
4352
|
import { tmpdir } from "os";
|
|
3568
|
-
import { execa as
|
|
4353
|
+
import { execa as execa7 } from "execa";
|
|
3569
4354
|
var fetchPackageSourceTool = defineTool({
|
|
3570
4355
|
description: "Download package tarball from npm and extract source files for CVE analysis. Supports custom file patterns (default: *.js, *.ts).",
|
|
3571
4356
|
parameters: z6.object({
|
|
@@ -3583,25 +4368,25 @@ var fetchPackageSourceTool = defineTool({
|
|
|
3583
4368
|
if (safePatterns.length === 0) {
|
|
3584
4369
|
return { success: false, error: "No valid file patterns provided." };
|
|
3585
4370
|
}
|
|
3586
|
-
const tempBaseDir = await mkdtemp(
|
|
3587
|
-
const extractDir =
|
|
4371
|
+
const tempBaseDir = await mkdtemp(join16(tmpdir(), "autoremediator-pkg-"));
|
|
4372
|
+
const extractDir = join16(tempBaseDir, "out");
|
|
3588
4373
|
try {
|
|
3589
4374
|
const scopedName = packageName.split("/").pop();
|
|
3590
4375
|
const npmUrl = `https://registry.npmjs.org/${packageName}/-/${scopedName}-${version}.tgz`;
|
|
3591
4376
|
await mkdir2(tempBaseDir, { recursive: true });
|
|
3592
|
-
const tarballPath =
|
|
3593
|
-
await
|
|
4377
|
+
const tarballPath = join16(tempBaseDir, "package.tgz");
|
|
4378
|
+
await execa7("curl", ["-L", "-o", tarballPath, npmUrl]);
|
|
3594
4379
|
await mkdir2(extractDir, { recursive: true });
|
|
3595
|
-
await
|
|
4380
|
+
await execa7("tar", ["-xzf", tarballPath, "-C", extractDir]);
|
|
3596
4381
|
const extractedContents = await readdir2(extractDir);
|
|
3597
|
-
const packageRootDir = extractedContents.includes("package") ?
|
|
4382
|
+
const packageRootDir = extractedContents.includes("package") ? join16(extractDir, "package") : extractDir;
|
|
3598
4383
|
const sourceCode = {};
|
|
3599
4384
|
async function walkDir(dir, relativeBase) {
|
|
3600
4385
|
try {
|
|
3601
4386
|
const files = await readdir2(dir, { withFileTypes: true });
|
|
3602
4387
|
for (const file of files) {
|
|
3603
|
-
const fullPath =
|
|
3604
|
-
const relPath =
|
|
4388
|
+
const fullPath = join16(dir, file.name);
|
|
4389
|
+
const relPath = join16(relativeBase, file.name);
|
|
3605
4390
|
if (file.isDirectory()) {
|
|
3606
4391
|
if (!["node_modules", ".git", "dist", "build", "coverage", ".next", "out"].includes(
|
|
3607
4392
|
file.name
|
|
@@ -3615,7 +4400,7 @@ var fetchPackageSourceTool = defineTool({
|
|
|
3615
4400
|
});
|
|
3616
4401
|
if (matches) {
|
|
3617
4402
|
try {
|
|
3618
|
-
const content = await
|
|
4403
|
+
const content = await readFile4(fullPath, "utf8");
|
|
3619
4404
|
sourceCode[relPath] = content;
|
|
3620
4405
|
} catch {
|
|
3621
4406
|
}
|
|
@@ -3650,7 +4435,7 @@ var fetchPackageSourceTool = defineTool({
|
|
|
3650
4435
|
error: `Failed to fetch and extract package ${packageName}@${version}: ${message}`
|
|
3651
4436
|
};
|
|
3652
4437
|
} finally {
|
|
3653
|
-
await
|
|
4438
|
+
await rm3(tempBaseDir, { recursive: true, force: true });
|
|
3654
4439
|
}
|
|
3655
4440
|
}
|
|
3656
4441
|
});
|
|
@@ -3708,6 +4493,7 @@ Analyze the source code to:
|
|
|
3708
4493
|
2. Explain the root cause of the security issue
|
|
3709
4494
|
3. Propose a secure fix that addresses the vulnerability
|
|
3710
4495
|
4. Provide the complete fixed version of affected files
|
|
4496
|
+
5. Optionally provide a standalone Node.js reproduction/regression test script
|
|
3711
4497
|
|
|
3712
4498
|
## Response Format
|
|
3713
4499
|
Respond ONLY with valid JSON (no markdown, no extra text):
|
|
@@ -3718,7 +4504,8 @@ Respond ONLY with valid JSON (no markdown, no extra text):
|
|
|
3718
4504
|
"path/to/other.ts": "Complete fixed source code for this file"
|
|
3719
4505
|
},
|
|
3720
4506
|
"confidence": 0.95,
|
|
3721
|
-
"riskLevel": "medium"
|
|
4507
|
+
"riskLevel": "medium",
|
|
4508
|
+
"reproductionTest": "// Node.js script asserting the patch resolves the exploit/regression"
|
|
3722
4509
|
}
|
|
3723
4510
|
|
|
3724
4511
|
Important:
|
|
@@ -3998,6 +4785,7 @@ var generatePatchTool = defineTool({
|
|
|
3998
4785
|
success: true,
|
|
3999
4786
|
patches,
|
|
4000
4787
|
patchContent: patches[0]?.unifiedDiff,
|
|
4788
|
+
reproductionTest: analysis.reproductionTest,
|
|
4001
4789
|
llmProvider: provider,
|
|
4002
4790
|
llmModel: modelName,
|
|
4003
4791
|
latencyMs,
|
|
@@ -4022,17 +4810,17 @@ var generatePatchTool = defineTool({
|
|
|
4022
4810
|
|
|
4023
4811
|
// src/remediation/tools/apply-patch-file/index.ts
|
|
4024
4812
|
import { z as z8 } from "zod";
|
|
4025
|
-
import { mkdir as mkdir3, writeFile as
|
|
4026
|
-
import { join as
|
|
4027
|
-
import { execa as
|
|
4813
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
4814
|
+
import { join as join18 } from "path";
|
|
4815
|
+
import { execa as execa9 } from "execa";
|
|
4028
4816
|
|
|
4029
4817
|
// src/remediation/tools/apply-patch-file/helpers.ts
|
|
4030
|
-
import { existsSync as
|
|
4031
|
-
import { mkdtemp as mkdtemp2, readFile as
|
|
4818
|
+
import { existsSync as existsSync12 } from "fs";
|
|
4819
|
+
import { mkdtemp as mkdtemp2, readFile as readFile5, rm as rm4, writeFile as writeFile2 } from "fs/promises";
|
|
4032
4820
|
import { createHash } from "crypto";
|
|
4033
4821
|
import { tmpdir as tmpdir2 } from "os";
|
|
4034
|
-
import { join as
|
|
4035
|
-
import { execa as
|
|
4822
|
+
import { join as join17 } from "path";
|
|
4823
|
+
import { execa as execa8 } from "execa";
|
|
4036
4824
|
async function resolvePatchMode(packageManager, cwd) {
|
|
4037
4825
|
if (packageManager === "npm" || packageManager === "bun" || packageManager === "deno")
|
|
4038
4826
|
return "patch-package";
|
|
@@ -4063,13 +4851,13 @@ function computePatchIntegrity(patchContent) {
|
|
|
4063
4851
|
return `sha256:${hex}`;
|
|
4064
4852
|
}
|
|
4065
4853
|
async function writePatchManifest(manifestFilePath, artifact) {
|
|
4066
|
-
await
|
|
4854
|
+
await writeFile2(manifestFilePath, JSON.stringify(artifact, null, 2) + "\n", "utf8");
|
|
4067
4855
|
}
|
|
4068
4856
|
async function configurePatchPackagePostinstall(cwd, packageManager) {
|
|
4069
|
-
const pkgJsonPath =
|
|
4857
|
+
const pkgJsonPath = join17(cwd, "package.json");
|
|
4070
4858
|
let pkgJson;
|
|
4071
4859
|
try {
|
|
4072
|
-
pkgJson = JSON.parse(await
|
|
4860
|
+
pkgJson = JSON.parse(await readFile5(pkgJsonPath, "utf8"));
|
|
4073
4861
|
} catch {
|
|
4074
4862
|
return {
|
|
4075
4863
|
success: false,
|
|
@@ -4081,7 +4869,7 @@ async function configurePatchPackagePostinstall(cwd, packageManager) {
|
|
|
4081
4869
|
try {
|
|
4082
4870
|
const commands = getPackageManagerCommands(packageManager);
|
|
4083
4871
|
const [cmd, ...args] = commands.installDev("patch-package");
|
|
4084
|
-
await
|
|
4872
|
+
await execa8(cmd, args, {
|
|
4085
4873
|
cwd,
|
|
4086
4874
|
stdio: "pipe"
|
|
4087
4875
|
});
|
|
@@ -4102,13 +4890,13 @@ async function configurePatchPackagePostinstall(cwd, packageManager) {
|
|
|
4102
4890
|
} else if (!currentPostinstall) {
|
|
4103
4891
|
pkgJson.scripts.postinstall = patchApplyCmd;
|
|
4104
4892
|
}
|
|
4105
|
-
await
|
|
4893
|
+
await writeFile2(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
4106
4894
|
return { success: true };
|
|
4107
4895
|
}
|
|
4108
4896
|
async function capturePackageJsonSnapshot(cwd) {
|
|
4109
|
-
const path =
|
|
4897
|
+
const path = join17(cwd, "package.json");
|
|
4110
4898
|
try {
|
|
4111
|
-
const content = await
|
|
4899
|
+
const content = await readFile5(path, "utf8");
|
|
4112
4900
|
return { path, content };
|
|
4113
4901
|
} catch {
|
|
4114
4902
|
return void 0;
|
|
@@ -4124,19 +4912,19 @@ async function cleanupPatchArtifacts(params) {
|
|
|
4124
4912
|
installCommand,
|
|
4125
4913
|
rerunInstall
|
|
4126
4914
|
} = params;
|
|
4127
|
-
await
|
|
4915
|
+
await rm4(patchFilePath, { force: true }).catch(() => void 0);
|
|
4128
4916
|
if (manifestFilePath) {
|
|
4129
|
-
await
|
|
4917
|
+
await rm4(manifestFilePath, { force: true }).catch(() => void 0);
|
|
4130
4918
|
}
|
|
4131
4919
|
if (patchMode === "patch-package" && packageJsonSnapshot) {
|
|
4132
|
-
await
|
|
4920
|
+
await writeFile2(packageJsonSnapshot.path, packageJsonSnapshot.content, "utf8").catch(
|
|
4133
4921
|
() => void 0
|
|
4134
4922
|
);
|
|
4135
4923
|
}
|
|
4136
4924
|
if (!rerunInstall) return;
|
|
4137
4925
|
try {
|
|
4138
4926
|
const [installCmd, ...installArgs] = installCommand;
|
|
4139
|
-
await
|
|
4927
|
+
await execa8(installCmd, installArgs, {
|
|
4140
4928
|
cwd,
|
|
4141
4929
|
stdio: "pipe"
|
|
4142
4930
|
});
|
|
@@ -4150,7 +4938,7 @@ async function applyNativePatch(params) {
|
|
|
4150
4938
|
const createArgs = ["patch", packageSpec];
|
|
4151
4939
|
let patchDir;
|
|
4152
4940
|
try {
|
|
4153
|
-
const createResult = await
|
|
4941
|
+
const createResult = await execa8(createCommand, createArgs, {
|
|
4154
4942
|
cwd,
|
|
4155
4943
|
stdio: "pipe"
|
|
4156
4944
|
});
|
|
@@ -4178,17 +4966,17 @@ ${createResult.stderr}`);
|
|
|
4178
4966
|
error: `Could not determine native patch directory for ${packageSpec}.`
|
|
4179
4967
|
};
|
|
4180
4968
|
}
|
|
4181
|
-
const tempPatchDir = await mkdtemp2(
|
|
4182
|
-
const tempPatchFile =
|
|
4969
|
+
const tempPatchDir = await mkdtemp2(join17(tmpdir2(), "autoremediator-native-patch-"));
|
|
4970
|
+
const tempPatchFile = join17(tempPatchDir, "change.patch");
|
|
4183
4971
|
try {
|
|
4184
|
-
await
|
|
4185
|
-
await
|
|
4972
|
+
await writeFile2(tempPatchFile, patchContent, "utf8");
|
|
4973
|
+
await execa8("patch", ["-p1", "-i", tempPatchFile], {
|
|
4186
4974
|
cwd: patchDir,
|
|
4187
4975
|
stdio: "pipe"
|
|
4188
4976
|
});
|
|
4189
4977
|
const commitCommand = patchMode === "native-pnpm" ? "pnpm" : "yarn";
|
|
4190
4978
|
const commitArgs = patchMode === "native-pnpm" ? ["patch-commit", patchDir] : ["patch-commit", "-s", patchDir];
|
|
4191
|
-
await
|
|
4979
|
+
await execa8(commitCommand, commitArgs, {
|
|
4192
4980
|
cwd,
|
|
4193
4981
|
stdio: "pipe"
|
|
4194
4982
|
});
|
|
@@ -4203,19 +4991,19 @@ ${createResult.stderr}`);
|
|
|
4203
4991
|
error: `Failed to apply native patch for ${packageSpec}: ${err instanceof Error ? err.message : String(err)}`
|
|
4204
4992
|
};
|
|
4205
4993
|
} finally {
|
|
4206
|
-
await
|
|
4994
|
+
await rm4(tempPatchDir, { recursive: true, force: true });
|
|
4207
4995
|
}
|
|
4208
4996
|
return { success: true };
|
|
4209
4997
|
}
|
|
4210
4998
|
function extractPatchDirectory(output) {
|
|
4211
4999
|
const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
4212
5000
|
for (const line of lines) {
|
|
4213
|
-
if (
|
|
5001
|
+
if (existsSync12(line)) {
|
|
4214
5002
|
return line;
|
|
4215
5003
|
}
|
|
4216
5004
|
const tokens = line.split(/\s+/).map((token) => token.replace(/^['"]|['"]$/g, ""));
|
|
4217
5005
|
for (const token of tokens) {
|
|
4218
|
-
if (token.startsWith("/") &&
|
|
5006
|
+
if (token.startsWith("/") && existsSync12(token)) {
|
|
4219
5007
|
return token;
|
|
4220
5008
|
}
|
|
4221
5009
|
}
|
|
@@ -4225,7 +5013,7 @@ function extractPatchDirectory(output) {
|
|
|
4225
5013
|
async function validatePatchWithTests(cwd, testCommand) {
|
|
4226
5014
|
try {
|
|
4227
5015
|
const [cmd, ...args] = testCommand;
|
|
4228
|
-
const result = await
|
|
5016
|
+
const result = await execa8(cmd, args, {
|
|
4229
5017
|
cwd,
|
|
4230
5018
|
timeout: 6e4,
|
|
4231
5019
|
stdio: "pipe"
|
|
@@ -4235,7 +5023,8 @@ async function validatePatchWithTests(cwd, testCommand) {
|
|
|
4235
5023
|
output: result.stdout
|
|
4236
5024
|
};
|
|
4237
5025
|
} catch (err) {
|
|
4238
|
-
const
|
|
5026
|
+
const stdoutVal = typeof err === "object" && err !== null && "stdout" in err ? err.stdout : "";
|
|
5027
|
+
const errorOutput = typeof stdoutVal === "string" ? stdoutVal : Buffer.isBuffer(stdoutVal) ? stdoutVal.toString("utf8") : "";
|
|
4239
5028
|
const failedTests = extractFailedTests(errorOutput);
|
|
4240
5029
|
return {
|
|
4241
5030
|
passed: false,
|
|
@@ -4284,6 +5073,7 @@ var applyPatchFileTool = defineTool({
|
|
|
4284
5073
|
enforceFrozenLockfile: z8.boolean().optional(),
|
|
4285
5074
|
workspace: z8.string().optional(),
|
|
4286
5075
|
validateWithTests: z8.boolean().optional().default(true).describe("Run package manager test command to validate patch doesn't break anything"),
|
|
5076
|
+
reproductionTest: z8.string().optional().describe("Optional synthesized reproduction test script verifying fix"),
|
|
4287
5077
|
dryRun: z8.boolean().optional().default(false).describe("If true, report but do not mutate files")
|
|
4288
5078
|
}).refine((value) => Boolean(value.patchContent || value.patches && value.patches.length > 0), {
|
|
4289
5079
|
message: "Either patchContent or patches must be provided"
|
|
@@ -4305,6 +5095,7 @@ var applyPatchFileTool = defineTool({
|
|
|
4305
5095
|
workspace,
|
|
4306
5096
|
riskLevel,
|
|
4307
5097
|
validateWithTests,
|
|
5098
|
+
reproductionTest,
|
|
4308
5099
|
dryRun
|
|
4309
5100
|
}) => {
|
|
4310
5101
|
try {
|
|
@@ -4355,7 +5146,7 @@ var applyPatchFileTool = defineTool({
|
|
|
4355
5146
|
};
|
|
4356
5147
|
}
|
|
4357
5148
|
const patchFileName = buildPatchFileName(packageName, vulnerableVersion);
|
|
4358
|
-
const patchFilePath =
|
|
5149
|
+
const patchFilePath = join18(cwd, patchesDir, patchFileName);
|
|
4359
5150
|
const manifestFilePath = `${patchFilePath}.json`;
|
|
4360
5151
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4361
5152
|
const baseArtifact = {
|
|
@@ -4394,9 +5185,9 @@ var applyPatchFileTool = defineTool({
|
|
|
4394
5185
|
}
|
|
4395
5186
|
return withRepoLock(cwd, async () => {
|
|
4396
5187
|
const packageJsonSnapshot = patchModeRequiresPackageJsonSnapshot(pm) ? await capturePackageJsonSnapshot(cwd) : void 0;
|
|
4397
|
-
const patchesDirPath =
|
|
5188
|
+
const patchesDirPath = join18(cwd, patchesDir);
|
|
4398
5189
|
await mkdir3(patchesDirPath, { recursive: true });
|
|
4399
|
-
await
|
|
5190
|
+
await writeFile3(patchFilePath, selectedPatch, "utf8");
|
|
4400
5191
|
validationPhases.push({
|
|
4401
5192
|
phase: "patch-write",
|
|
4402
5193
|
passed: true,
|
|
@@ -4464,7 +5255,7 @@ var applyPatchFileTool = defineTool({
|
|
|
4464
5255
|
if (patchMode === "patch-package") {
|
|
4465
5256
|
try {
|
|
4466
5257
|
const [installCmd, ...installArgs] = installCommand;
|
|
4467
|
-
await
|
|
5258
|
+
await execa9(installCmd, installArgs, {
|
|
4468
5259
|
cwd,
|
|
4469
5260
|
stdio: "pipe"
|
|
4470
5261
|
});
|
|
@@ -4557,10 +5348,33 @@ var applyPatchFileTool = defineTool({
|
|
|
4557
5348
|
message: "Patch validation tests passed."
|
|
4558
5349
|
});
|
|
4559
5350
|
}
|
|
5351
|
+
let reproductionTestPath;
|
|
5352
|
+
if (reproductionTest) {
|
|
5353
|
+
const safeName = packageName.replace(/^@/, "").replace(/\//g, "+");
|
|
5354
|
+
reproductionTestPath = join18(patchesDirPath, `${safeName}+${vulnerableVersion}.test.cjs`);
|
|
5355
|
+
await writeFile3(reproductionTestPath, reproductionTest, "utf8");
|
|
5356
|
+
try {
|
|
5357
|
+
const reproRun = await execa9("node", [reproductionTestPath], { cwd, stdio: "pipe" });
|
|
5358
|
+
validationPhases.push({
|
|
5359
|
+
phase: "reproduction-test",
|
|
5360
|
+
passed: reproRun.exitCode === 0,
|
|
5361
|
+
message: "Reproduction test verified fix successfully."
|
|
5362
|
+
});
|
|
5363
|
+
} catch (err) {
|
|
5364
|
+
const reproErr = err instanceof Error ? err.message : String(err);
|
|
5365
|
+
validationPhases.push({
|
|
5366
|
+
phase: "reproduction-test",
|
|
5367
|
+
passed: false,
|
|
5368
|
+
error: `Reproduction test failed: ${reproErr}`
|
|
5369
|
+
});
|
|
5370
|
+
}
|
|
5371
|
+
}
|
|
4560
5372
|
const finalArtifact = {
|
|
4561
5373
|
...artifact,
|
|
4562
5374
|
applied: true,
|
|
4563
5375
|
dryRun: false,
|
|
5376
|
+
reproductionTest,
|
|
5377
|
+
reproductionTestPath,
|
|
4564
5378
|
validationPhases
|
|
4565
5379
|
};
|
|
4566
5380
|
await writePatchManifest(manifestFilePath, finalArtifact);
|
|
@@ -4870,7 +5684,11 @@ function resolveLocalRunOptions(options) {
|
|
|
4870
5684
|
const runTests = options.runTests ?? false;
|
|
4871
5685
|
const policy = options.policy ?? "";
|
|
4872
5686
|
const patchesDir = options.patchesDir || "./patches";
|
|
4873
|
-
const constraints =
|
|
5687
|
+
const constraints = {
|
|
5688
|
+
...options.constraints,
|
|
5689
|
+
typecheck: options.typecheck ?? options.constraints?.typecheck,
|
|
5690
|
+
supplyChainPolicy: options.supplyChainPolicy ?? options.constraints?.supplyChainPolicy
|
|
5691
|
+
};
|
|
4874
5692
|
const loadedPolicy = loadPolicy(cwd, options.policy);
|
|
4875
5693
|
const llmProvider = resolveProvider(options);
|
|
4876
5694
|
return {
|
|
@@ -4907,7 +5725,7 @@ function resolveLocalRunOptions(options) {
|
|
|
4907
5725
|
}
|
|
4908
5726
|
|
|
4909
5727
|
// src/remediation/local/vulnerability-match.ts
|
|
4910
|
-
import
|
|
5728
|
+
import semver5 from "semver";
|
|
4911
5729
|
function findVulnerablePackages(cveDetails, installedPackages) {
|
|
4912
5730
|
const vulnerablePackages = [];
|
|
4913
5731
|
for (const affected of cveDetails.affectedPackages) {
|
|
@@ -4916,10 +5734,10 @@ function findVulnerablePackages(cveDetails, installedPackages) {
|
|
|
4916
5734
|
if (affected.ecosystem !== "npm") continue;
|
|
4917
5735
|
const matches = installedPackages.filter((pkg) => pkg.name === affected.name);
|
|
4918
5736
|
for (const installed of matches) {
|
|
4919
|
-
if (!
|
|
5737
|
+
if (!semver5.valid(installed.version)) continue;
|
|
4920
5738
|
let isVulnerable = false;
|
|
4921
5739
|
try {
|
|
4922
|
-
isVulnerable =
|
|
5740
|
+
isVulnerable = semver5.satisfies(installed.version, affected.vulnerableRange, {
|
|
4923
5741
|
includePrerelease: false
|
|
4924
5742
|
});
|
|
4925
5743
|
} catch {
|
|
@@ -5262,7 +6080,7 @@ async function runLocalRemediationPipeline(cveId, options = {}) {
|
|
|
5262
6080
|
};
|
|
5263
6081
|
if (regressionCheck && primaryResult.applied && !dryRun && primaryResult.toVersion) {
|
|
5264
6082
|
try {
|
|
5265
|
-
if (
|
|
6083
|
+
if (semver6.satisfies(primaryResult.toVersion, vulnerable.affected.vulnerableRange, {
|
|
5266
6084
|
includePrerelease: false
|
|
5267
6085
|
})) {
|
|
5268
6086
|
primaryResult.regressionDetected = true;
|
|
@@ -5353,8 +6171,8 @@ function accumulateStepResults(params) {
|
|
|
5353
6171
|
}
|
|
5354
6172
|
|
|
5355
6173
|
// src/remediation/orchestration-prompt.ts
|
|
5356
|
-
import { existsSync as
|
|
5357
|
-
import { join as
|
|
6174
|
+
import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
|
|
6175
|
+
import { join as join19 } from "path";
|
|
5358
6176
|
function buildProviderAddendum(provider, personality = "balanced") {
|
|
5359
6177
|
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.";
|
|
5360
6178
|
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.";
|
|
@@ -5365,13 +6183,13 @@ Provider profile:
|
|
|
5365
6183
|
- ${personalityDirective}`;
|
|
5366
6184
|
}
|
|
5367
6185
|
function loadOrchestrationPrompt(ctx) {
|
|
5368
|
-
const promptPath =
|
|
6186
|
+
const promptPath = join19(
|
|
5369
6187
|
process.cwd(),
|
|
5370
6188
|
".github",
|
|
5371
6189
|
"instructions",
|
|
5372
6190
|
"orchestration.instructions.md"
|
|
5373
6191
|
);
|
|
5374
|
-
if (!
|
|
6192
|
+
if (!existsSync13(promptPath)) {
|
|
5375
6193
|
return `You are autoremediator, an agentic security remediation system for Node.js package dependencies.
|
|
5376
6194
|
Working directory: ${ctx.cwd}
|
|
5377
6195
|
Package manager: ${ctx.packageManager}
|
|
@@ -5379,112 +6197,48 @@ Dry run: ${ctx.dryRun}
|
|
|
5379
6197
|
Run tests: ${ctx.runTests}
|
|
5380
6198
|
Policy: ${ctx.policy || "undefined"}
|
|
5381
6199
|
Patches dir: ${ctx.patchesDir}
|
|
5382
|
-
Direct dependencies only: ${String(ctx.constraints.directDependenciesOnly ?? false)}
|
|
5383
|
-
Prefer version bump: ${String(ctx.constraints.preferVersionBump ?? false)}
|
|
5384
|
-
|
|
5385
|
-
Required sequence:
|
|
5386
|
-
1. lookup-cve
|
|
5387
|
-
2. check-inventory
|
|
5388
|
-
3. check-version-match
|
|
5389
|
-
4. find-fixed-version
|
|
5390
|
-
5. apply-version-bump
|
|
5391
|
-
6. apply-package-override
|
|
5392
|
-
|
|
5393
|
-
Fallback sequence (when neither version bump nor override can be applied):
|
|
5394
|
-
1. fetch-package-source
|
|
5395
|
-
2. generate-patch
|
|
5396
|
-
3. apply-patch-file
|
|
5397
|
-
|
|
5398
|
-
Always respect dryRun and policy constraints.`;
|
|
5399
|
-
}
|
|
5400
|
-
const template = readFileSync11(promptPath, "utf8");
|
|
5401
|
-
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(
|
|
5402
|
-
"{{directDependenciesOnly}}",
|
|
5403
|
-
String(ctx.constraints.directDependenciesOnly ?? false)
|
|
5404
|
-
).replaceAll("{{preferVersionBump}}", String(ctx.constraints.preferVersionBump ?? false)) + buildProviderAddendum(ctx.llmProvider, ctx.modelPersonality);
|
|
5405
|
-
}
|
|
5406
|
-
|
|
5407
|
-
// src/remediation/pipeline-runtime/progress.ts
|
|
5408
|
-
function createProgressEmitter(options) {
|
|
5409
|
-
return (stage, detail, extra) => {
|
|
5410
|
-
if (!options.onProgress) return;
|
|
5411
|
-
options.onProgress({
|
|
5412
|
-
stage,
|
|
5413
|
-
detail,
|
|
5414
|
-
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5415
|
-
provider: extra?.provider,
|
|
5416
|
-
model: extra?.model
|
|
5417
|
-
});
|
|
5418
|
-
};
|
|
5419
|
-
}
|
|
5420
|
-
|
|
5421
|
-
// src/remediation/tools/lookup-cve.ts
|
|
5422
|
-
import { z as z9 } from "zod";
|
|
5423
|
-
|
|
5424
|
-
// src/intelligence/sources/cisa-kev.ts
|
|
5425
|
-
var CISA_KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json";
|
|
5426
|
-
async function fetchCisaKevFeed() {
|
|
5427
|
-
try {
|
|
5428
|
-
const res = await httpClient({ url: CISA_KEV_URL });
|
|
5429
|
-
if (!res.ok) return void 0;
|
|
5430
|
-
return res.data;
|
|
5431
|
-
} catch {
|
|
5432
|
-
return void 0;
|
|
5433
|
-
}
|
|
5434
|
-
}
|
|
5435
|
-
function findKevEntry(feed, cveId) {
|
|
5436
|
-
if (!feed?.vulnerabilities?.length) return void 0;
|
|
5437
|
-
const normalized = cveId.toUpperCase();
|
|
5438
|
-
return feed.vulnerabilities.find((v) => v.cveID.toUpperCase() === normalized);
|
|
5439
|
-
}
|
|
5440
|
-
async function enrichWithCisaKev(details) {
|
|
5441
|
-
const feed = await fetchCisaKevFeed();
|
|
5442
|
-
const entry = findKevEntry(feed, details.id);
|
|
5443
|
-
if (!entry) return details;
|
|
5444
|
-
details.kev = {
|
|
5445
|
-
knownExploited: true,
|
|
5446
|
-
dateAdded: entry.dateAdded,
|
|
5447
|
-
dueDate: entry.dueDate,
|
|
5448
|
-
requiredAction: entry.requiredAction,
|
|
5449
|
-
knownRansomwareCampaignUse: entry.knownRansomwareCampaignUse
|
|
5450
|
-
};
|
|
5451
|
-
if (!details.references.includes(CISA_KEV_URL)) {
|
|
5452
|
-
details.references.push(CISA_KEV_URL);
|
|
5453
|
-
}
|
|
5454
|
-
return details;
|
|
5455
|
-
}
|
|
6200
|
+
Direct dependencies only: ${String(ctx.constraints.directDependenciesOnly ?? false)}
|
|
6201
|
+
Prefer version bump: ${String(ctx.constraints.preferVersionBump ?? false)}
|
|
5456
6202
|
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
6203
|
+
Required sequence:
|
|
6204
|
+
1. lookup-cve
|
|
6205
|
+
2. check-inventory
|
|
6206
|
+
3. check-version-match
|
|
6207
|
+
4. find-fixed-version
|
|
6208
|
+
5. apply-version-bump
|
|
6209
|
+
6. apply-package-override
|
|
6210
|
+
|
|
6211
|
+
Fallback sequence (when neither version bump nor override can be applied):
|
|
6212
|
+
1. fetch-package-source
|
|
6213
|
+
2. generate-patch
|
|
6214
|
+
3. apply-patch-file
|
|
6215
|
+
|
|
6216
|
+
Always respect dryRun and policy constraints.`;
|
|
5470
6217
|
}
|
|
6218
|
+
const template = readFileSync13(promptPath, "utf8");
|
|
6219
|
+
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(
|
|
6220
|
+
"{{directDependenciesOnly}}",
|
|
6221
|
+
String(ctx.constraints.directDependenciesOnly ?? false)
|
|
6222
|
+
).replaceAll("{{preferVersionBump}}", String(ctx.constraints.preferVersionBump ?? false)) + buildProviderAddendum(ctx.llmProvider, ctx.modelPersonality);
|
|
5471
6223
|
}
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
6224
|
+
|
|
6225
|
+
// src/remediation/pipeline-runtime/progress.ts
|
|
6226
|
+
function createProgressEmitter(options) {
|
|
6227
|
+
return (stage, detail, extra) => {
|
|
6228
|
+
if (!options.onProgress) return;
|
|
6229
|
+
options.onProgress({
|
|
6230
|
+
stage,
|
|
6231
|
+
detail,
|
|
6232
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6233
|
+
provider: extra?.provider,
|
|
6234
|
+
model: extra?.model
|
|
6235
|
+
});
|
|
5484
6236
|
};
|
|
5485
|
-
return details;
|
|
5486
6237
|
}
|
|
5487
6238
|
|
|
6239
|
+
// src/remediation/tools/lookup-cve.ts
|
|
6240
|
+
import { z as z9 } from "zod";
|
|
6241
|
+
|
|
5488
6242
|
// src/intelligence/sources/cve-services.ts
|
|
5489
6243
|
function pickEnglishDescription(container) {
|
|
5490
6244
|
if (!container?.descriptions?.length) return void 0;
|
|
@@ -5768,7 +6522,7 @@ var lookupCveTool = defineTool({
|
|
|
5768
6522
|
|
|
5769
6523
|
// src/remediation/tools/check-version-match.ts
|
|
5770
6524
|
import { z as z10 } from "zod";
|
|
5771
|
-
import
|
|
6525
|
+
import semver7 from "semver";
|
|
5772
6526
|
var affectedPackageSchema = z10.object({
|
|
5773
6527
|
name: z10.string(),
|
|
5774
6528
|
ecosystem: z10.literal("npm"),
|
|
@@ -5797,10 +6551,10 @@ var checkVersionMatchTool = defineTool({
|
|
|
5797
6551
|
(p) => p.name === affected.name
|
|
5798
6552
|
);
|
|
5799
6553
|
for (const installed of matches) {
|
|
5800
|
-
if (!
|
|
6554
|
+
if (!semver7.valid(installed.version)) continue;
|
|
5801
6555
|
let isVulnerable = false;
|
|
5802
6556
|
try {
|
|
5803
|
-
isVulnerable =
|
|
6557
|
+
isVulnerable = semver7.satisfies(installed.version, affected.vulnerableRange, {
|
|
5804
6558
|
includePrerelease: false
|
|
5805
6559
|
});
|
|
5806
6560
|
} catch {
|
|
@@ -6118,7 +6872,7 @@ async function runRemediationPipeline(cveId, options = {}) {
|
|
|
6118
6872
|
}
|
|
6119
6873
|
|
|
6120
6874
|
// src/api/change-request/index.ts
|
|
6121
|
-
import { execa as
|
|
6875
|
+
import { execa as execa10 } from "execa";
|
|
6122
6876
|
import { randomBytes } from "crypto";
|
|
6123
6877
|
function sanitizeBranchToken(value) {
|
|
6124
6878
|
return value.toLowerCase().replace(/[^a-z0-9._/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
@@ -6147,22 +6901,68 @@ function resolveProvider2(options) {
|
|
|
6147
6901
|
return options.provider ?? "github";
|
|
6148
6902
|
}
|
|
6149
6903
|
function resolveGrouping(options) {
|
|
6150
|
-
|
|
6904
|
+
const allowed = ["all", "per-cve", "per-package", "by-severity"];
|
|
6905
|
+
if (options.grouping && !allowed.includes(options.grouping)) {
|
|
6151
6906
|
throw new Error(
|
|
6152
|
-
|
|
6907
|
+
`Unsupported changeRequest.grouping: "${options.grouping}". Supported values: ${allowed.join(", ")}`
|
|
6153
6908
|
);
|
|
6154
6909
|
}
|
|
6155
|
-
return "all";
|
|
6910
|
+
return options.grouping ?? "all";
|
|
6911
|
+
}
|
|
6912
|
+
function groupReports(reports, grouping) {
|
|
6913
|
+
if (reports.length === 0) return [];
|
|
6914
|
+
if (grouping === "all") {
|
|
6915
|
+
return [reports];
|
|
6916
|
+
}
|
|
6917
|
+
if (grouping === "per-cve") {
|
|
6918
|
+
const map = /* @__PURE__ */ new Map();
|
|
6919
|
+
for (const report of reports) {
|
|
6920
|
+
const key = report.cveId.toUpperCase();
|
|
6921
|
+
const list = map.get(key) ?? [];
|
|
6922
|
+
list.push(report);
|
|
6923
|
+
map.set(key, list);
|
|
6924
|
+
}
|
|
6925
|
+
return Array.from(map.values());
|
|
6926
|
+
}
|
|
6927
|
+
if (grouping === "per-package") {
|
|
6928
|
+
const map = /* @__PURE__ */ new Map();
|
|
6929
|
+
for (const report of reports) {
|
|
6930
|
+
const pkgs = new Set(report.results.map((r) => r.packageName));
|
|
6931
|
+
if (pkgs.size === 0) {
|
|
6932
|
+
const list = map.get("misc") ?? [];
|
|
6933
|
+
list.push(report);
|
|
6934
|
+
map.set("misc", list);
|
|
6935
|
+
} else {
|
|
6936
|
+
for (const pkg of pkgs) {
|
|
6937
|
+
const list = map.get(pkg) ?? [];
|
|
6938
|
+
list.push(report);
|
|
6939
|
+
map.set(pkg, list);
|
|
6940
|
+
}
|
|
6941
|
+
}
|
|
6942
|
+
}
|
|
6943
|
+
return Array.from(map.values());
|
|
6944
|
+
}
|
|
6945
|
+
if (grouping === "by-severity") {
|
|
6946
|
+
const map = /* @__PURE__ */ new Map();
|
|
6947
|
+
for (const report of reports) {
|
|
6948
|
+
const sev = report.cveDetails?.severity ?? "UNKNOWN";
|
|
6949
|
+
const list = map.get(sev) ?? [];
|
|
6950
|
+
list.push(report);
|
|
6951
|
+
map.set(sev, list);
|
|
6952
|
+
}
|
|
6953
|
+
return Array.from(map.values());
|
|
6954
|
+
}
|
|
6955
|
+
return [reports];
|
|
6156
6956
|
}
|
|
6157
6957
|
async function detectCurrentBranch(cwd) {
|
|
6158
|
-
const result = await
|
|
6958
|
+
const result = await execa10("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
6159
6959
|
cwd,
|
|
6160
6960
|
stdio: "pipe"
|
|
6161
6961
|
});
|
|
6162
6962
|
return result.stdout.trim();
|
|
6163
6963
|
}
|
|
6164
6964
|
async function ensureRepoHasChanges(cwd) {
|
|
6165
|
-
const diff = await
|
|
6965
|
+
const diff = await execa10("git", ["status", "--porcelain"], {
|
|
6166
6966
|
cwd,
|
|
6167
6967
|
stdio: "pipe"
|
|
6168
6968
|
});
|
|
@@ -6191,7 +6991,7 @@ async function createGitHubRequest(params) {
|
|
|
6191
6991
|
if (params.draft) {
|
|
6192
6992
|
args.push("--draft");
|
|
6193
6993
|
}
|
|
6194
|
-
const result = await
|
|
6994
|
+
const result = await execa10("gh", args, {
|
|
6195
6995
|
cwd: params.cwd,
|
|
6196
6996
|
stdio: "pipe"
|
|
6197
6997
|
});
|
|
@@ -6216,7 +7016,7 @@ async function createGitLabRequest(params) {
|
|
|
6216
7016
|
if (params.draft) {
|
|
6217
7017
|
args.push("--draft");
|
|
6218
7018
|
}
|
|
6219
|
-
const result = await
|
|
7019
|
+
const result = await execa10("glab", args, {
|
|
6220
7020
|
cwd: params.cwd,
|
|
6221
7021
|
stdio: "pipe"
|
|
6222
7022
|
});
|
|
@@ -6226,64 +7026,68 @@ async function createChangeRequestsForReports(params) {
|
|
|
6226
7026
|
const { cwd, options, reports } = params;
|
|
6227
7027
|
const provider = resolveProvider2(options);
|
|
6228
7028
|
const grouping = resolveGrouping(options);
|
|
6229
|
-
const
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
const
|
|
7029
|
+
const groups = groupReports(reports, grouping);
|
|
7030
|
+
if (groups.length === 0) {
|
|
7031
|
+
return [];
|
|
7032
|
+
}
|
|
7033
|
+
const results = [];
|
|
7034
|
+
const hasChanges = await ensureRepoHasChanges(cwd).catch(() => false);
|
|
7035
|
+
const baseBranch = options.baseBranch ?? await detectCurrentBranch(cwd).catch(() => "main");
|
|
7036
|
+
const branchPrefix = options.branchPrefix ?? "autoremediator";
|
|
7037
|
+
const pushRemote = options.pushRemote ?? "origin";
|
|
7038
|
+
if (options.pushRemote && !/^[a-zA-Z0-9._-]+$/.test(options.pushRemote)) {
|
|
7039
|
+
throw new Error(`Invalid pushRemote value: ${options.pushRemote}`);
|
|
7040
|
+
}
|
|
7041
|
+
if (options.repository && !/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(options.repository)) {
|
|
7042
|
+
throw new Error(`Invalid repository format: ${options.repository}`);
|
|
7043
|
+
}
|
|
7044
|
+
for (const group of groups) {
|
|
7045
|
+
const plan = buildPlan(group);
|
|
7046
|
+
const titlePrefix = options.titlePrefix?.trim().slice(0, 200);
|
|
7047
|
+
const bodyFooter = options.bodyFooter ? options.bodyFooter.slice(0, 2e3) : void 0;
|
|
7048
|
+
const title = titlePrefix ? `${titlePrefix} ${plan.title}` : plan.title;
|
|
7049
|
+
const body = bodyFooter ? `${plan.body}
|
|
6234
7050
|
|
|
6235
7051
|
${bodyFooter}` : plan.body;
|
|
6236
|
-
try {
|
|
6237
|
-
const hasChanges = await ensureRepoHasChanges(cwd);
|
|
6238
7052
|
if (!hasChanges) {
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
];
|
|
7053
|
+
results.push({
|
|
7054
|
+
provider,
|
|
7055
|
+
grouping,
|
|
7056
|
+
repository: options.repository,
|
|
7057
|
+
branchName: "",
|
|
7058
|
+
title,
|
|
7059
|
+
body,
|
|
7060
|
+
created: false,
|
|
7061
|
+
cveIds: plan.cveIds,
|
|
7062
|
+
packageNames: plan.packageNames,
|
|
7063
|
+
error: "No repository changes to include in change request."
|
|
7064
|
+
});
|
|
7065
|
+
continue;
|
|
6253
7066
|
}
|
|
6254
|
-
const baseBranch = options.baseBranch ?? await detectCurrentBranch(cwd);
|
|
6255
|
-
const branchPrefix = options.branchPrefix ?? "autoremediator";
|
|
6256
7067
|
const branchName = toBranchName(branchPrefix, plan.cveIds);
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
target: baseBranch,
|
|
6281
|
-
title,
|
|
6282
|
-
body,
|
|
6283
|
-
draft: options.draft
|
|
6284
|
-
});
|
|
6285
|
-
return [
|
|
6286
|
-
{
|
|
7068
|
+
try {
|
|
7069
|
+
await execa10("git", ["checkout", "-b", branchName], { cwd, stdio: "pipe" });
|
|
7070
|
+
await execa10("git", ["add", "-A"], { cwd, stdio: "pipe" });
|
|
7071
|
+
await execa10("git", ["commit", "-m", title], { cwd, stdio: "pipe" });
|
|
7072
|
+
await execa10("git", ["push", "-u", pushRemote, branchName], { cwd, stdio: "pipe" });
|
|
7073
|
+
const url = provider === "github" ? await createGitHubRequest({
|
|
7074
|
+
cwd,
|
|
7075
|
+
repository: options.repository,
|
|
7076
|
+
head: branchName,
|
|
7077
|
+
base: baseBranch,
|
|
7078
|
+
title,
|
|
7079
|
+
body,
|
|
7080
|
+
draft: options.draft
|
|
7081
|
+
}) : await createGitLabRequest({
|
|
7082
|
+
cwd,
|
|
7083
|
+
repository: options.repository,
|
|
7084
|
+
source: branchName,
|
|
7085
|
+
target: baseBranch,
|
|
7086
|
+
title,
|
|
7087
|
+
body,
|
|
7088
|
+
draft: options.draft
|
|
7089
|
+
});
|
|
7090
|
+
results.push({
|
|
6287
7091
|
provider,
|
|
6288
7092
|
grouping,
|
|
6289
7093
|
repository: options.repository,
|
|
@@ -6295,11 +7099,13 @@ ${bodyFooter}` : plan.body;
|
|
|
6295
7099
|
url,
|
|
6296
7100
|
cveIds: plan.cveIds,
|
|
6297
7101
|
packageNames: plan.packageNames
|
|
7102
|
+
});
|
|
7103
|
+
if (groups.length > 1) {
|
|
7104
|
+
await execa10("git", ["checkout", baseBranch], { cwd, stdio: "pipe" }).catch(() => {
|
|
7105
|
+
});
|
|
6298
7106
|
}
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
return [
|
|
6302
|
-
{
|
|
7107
|
+
} catch (error) {
|
|
7108
|
+
results.push({
|
|
6303
7109
|
provider,
|
|
6304
7110
|
grouping,
|
|
6305
7111
|
repository: options.repository,
|
|
@@ -6311,9 +7117,10 @@ ${bodyFooter}` : plan.body;
|
|
|
6311
7117
|
cveIds: plan.cveIds,
|
|
6312
7118
|
packageNames: plan.packageNames,
|
|
6313
7119
|
error: error instanceof Error ? error.message : String(error)
|
|
6314
|
-
}
|
|
6315
|
-
|
|
7120
|
+
});
|
|
7121
|
+
}
|
|
6316
7122
|
}
|
|
7123
|
+
return results;
|
|
6317
7124
|
}
|
|
6318
7125
|
|
|
6319
7126
|
// src/api/context.ts
|
|
@@ -6347,7 +7154,7 @@ function resolveConstraints(options, cwd) {
|
|
|
6347
7154
|
|
|
6348
7155
|
// src/platform/evidence.ts
|
|
6349
7156
|
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
6350
|
-
import { join as
|
|
7157
|
+
import { join as join20 } from "path";
|
|
6351
7158
|
function createEvidenceLog(cwd, cveIds, context = {}) {
|
|
6352
7159
|
return {
|
|
6353
7160
|
runId: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
@@ -6379,9 +7186,9 @@ function finalizeEvidence(log) {
|
|
|
6379
7186
|
return log;
|
|
6380
7187
|
}
|
|
6381
7188
|
function writeEvidenceLog(cwd, log) {
|
|
6382
|
-
const dir =
|
|
7189
|
+
const dir = join20(cwd, ".autoremediator", "evidence");
|
|
6383
7190
|
mkdirSync3(dir, { recursive: true });
|
|
6384
|
-
const filePath =
|
|
7191
|
+
const filePath = join20(dir, `${log.runId}.json`);
|
|
6385
7192
|
writeFileSync5(filePath, JSON.stringify(log, null, 2) + "\n", "utf8");
|
|
6386
7193
|
return filePath;
|
|
6387
7194
|
}
|
|
@@ -6520,10 +7327,15 @@ function writeRemediateEvidence(cwd, evidence) {
|
|
|
6520
7327
|
}
|
|
6521
7328
|
|
|
6522
7329
|
// src/api/remediate/validators.ts
|
|
7330
|
+
var CVE_ID_REGEX = /^CVE-\d{4}-\d{1,7}$/i;
|
|
7331
|
+
var GHSA_ID_REGEX = /^GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$/i;
|
|
7332
|
+
function isVulnerabilityId(id) {
|
|
7333
|
+
return CVE_ID_REGEX.test(id) || GHSA_ID_REGEX.test(id);
|
|
7334
|
+
}
|
|
6523
7335
|
function assertValidCveId(cveId) {
|
|
6524
|
-
if (
|
|
7336
|
+
if (!isVulnerabilityId(cveId)) {
|
|
6525
7337
|
throw new Error(
|
|
6526
|
-
`Invalid
|
|
7338
|
+
`Invalid vulnerability ID: "${cveId}". Expected format: CVE-YYYY-NNNNN (e.g. CVE-2021-23337) or GHSA-xxxx-xxxx-xxxx.`
|
|
6527
7339
|
);
|
|
6528
7340
|
}
|
|
6529
7341
|
}
|
|
@@ -6619,13 +7431,14 @@ async function planRemediation(cveId, options = {}) {
|
|
|
6619
7431
|
}
|
|
6620
7432
|
|
|
6621
7433
|
// src/scanner/parse-input.ts
|
|
6622
|
-
import { extname, resolve as
|
|
6623
|
-
import { readFileSync as
|
|
6624
|
-
import { execa as
|
|
7434
|
+
import { extname, resolve as resolve4 } from "path";
|
|
7435
|
+
import { readFileSync as readFileSync17 } from "fs";
|
|
7436
|
+
import { execa as execa11 } from "execa";
|
|
6625
7437
|
|
|
6626
7438
|
// src/scanner/adapters/npm-audit.ts
|
|
6627
|
-
import { readFileSync as
|
|
7439
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
6628
7440
|
var CVE_REGEX = /CVE-\d{4}-\d{1,7}/gi;
|
|
7441
|
+
var GHSA_REGEX = /GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}/gi;
|
|
6629
7442
|
function normalizeSeverity(raw) {
|
|
6630
7443
|
if (!raw) return "UNKNOWN";
|
|
6631
7444
|
const up = raw.toUpperCase();
|
|
@@ -6640,7 +7453,8 @@ function parseNpmAuditJsonFromString(content) {
|
|
|
6640
7453
|
for (const vuln of Object.values(report.vulnerabilities ?? {})) {
|
|
6641
7454
|
for (const viaEntry of vuln.via ?? []) {
|
|
6642
7455
|
const text = typeof viaEntry === "string" ? viaEntry : `${viaEntry.url ?? ""} ${viaEntry.name ?? ""}`;
|
|
6643
|
-
const
|
|
7456
|
+
const cveMatches = text.match(CVE_REGEX) ?? [];
|
|
7457
|
+
const matches = cveMatches.length > 0 ? cveMatches : text.match(GHSA_REGEX) ?? [];
|
|
6644
7458
|
for (const match of matches) {
|
|
6645
7459
|
const cveId = match.toUpperCase();
|
|
6646
7460
|
const key = `${cveId}:${vuln.name}`;
|
|
@@ -6658,13 +7472,14 @@ function parseNpmAuditJsonFromString(content) {
|
|
|
6658
7472
|
return findings;
|
|
6659
7473
|
}
|
|
6660
7474
|
function parseNpmAuditJsonFile(filePath) {
|
|
6661
|
-
const content =
|
|
7475
|
+
const content = readFileSync14(filePath, "utf8");
|
|
6662
7476
|
return parseNpmAuditJsonFromString(content);
|
|
6663
7477
|
}
|
|
6664
7478
|
|
|
6665
7479
|
// src/scanner/adapters/yarn-audit.ts
|
|
6666
|
-
import { readFileSync as
|
|
7480
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
6667
7481
|
var CVE_REGEX2 = /CVE-\d{4}-\d{1,7}/gi;
|
|
7482
|
+
var GHSA_REGEX2 = /GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}/gi;
|
|
6668
7483
|
function normalizeSeverity2(raw) {
|
|
6669
7484
|
if (!raw) return "UNKNOWN";
|
|
6670
7485
|
const up = raw.toUpperCase();
|
|
@@ -6689,7 +7504,8 @@ function parseYarnAuditJsonFromString(content) {
|
|
|
6689
7504
|
const packageName = advisory?.module_name;
|
|
6690
7505
|
const severity = normalizeSeverity2(advisory?.severity);
|
|
6691
7506
|
const text = `${advisory?.url ?? ""} ${(advisory?.cves ?? []).join(" ")}`;
|
|
6692
|
-
const
|
|
7507
|
+
const cveMatches = text.match(CVE_REGEX2) ?? [];
|
|
7508
|
+
const matches = cveMatches.length > 0 ? cveMatches : text.match(GHSA_REGEX2) ?? [];
|
|
6693
7509
|
for (const match of matches) {
|
|
6694
7510
|
const cveId = match.toUpperCase();
|
|
6695
7511
|
const key = `${cveId}:${packageName ?? ""}`;
|
|
@@ -6706,12 +7522,12 @@ function parseYarnAuditJsonFromString(content) {
|
|
|
6706
7522
|
return findings;
|
|
6707
7523
|
}
|
|
6708
7524
|
function parseYarnAuditJsonFile(filePath) {
|
|
6709
|
-
const content =
|
|
7525
|
+
const content = readFileSync15(filePath, "utf8");
|
|
6710
7526
|
return parseYarnAuditJsonFromString(content);
|
|
6711
7527
|
}
|
|
6712
7528
|
|
|
6713
7529
|
// src/scanner/adapters/sarif.ts
|
|
6714
|
-
import { readFileSync as
|
|
7530
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
6715
7531
|
var CVE_REGEX3 = /CVE-\d{4}-\d{1,7}/gi;
|
|
6716
7532
|
function extractPackageName(result) {
|
|
6717
7533
|
const pkg = result.properties?.["packageName"];
|
|
@@ -6749,7 +7565,7 @@ function parseSarifFromString(content) {
|
|
|
6749
7565
|
return findings;
|
|
6750
7566
|
}
|
|
6751
7567
|
function parseSarifFile(filePath) {
|
|
6752
|
-
const content =
|
|
7568
|
+
const content = readFileSync16(filePath, "utf8");
|
|
6753
7569
|
return parseSarifFromString(content);
|
|
6754
7570
|
}
|
|
6755
7571
|
|
|
@@ -6758,7 +7574,7 @@ function parseScanInput(filePath, format) {
|
|
|
6758
7574
|
if (filePath.includes("\0")) {
|
|
6759
7575
|
throw new Error("Invalid scan input path: path contains null bytes");
|
|
6760
7576
|
}
|
|
6761
|
-
const resolvedPath =
|
|
7577
|
+
const resolvedPath = resolve4(filePath);
|
|
6762
7578
|
const resolved = format === "auto" ? inferFormat(resolvedPath) : format;
|
|
6763
7579
|
if (resolved === "npm-audit") {
|
|
6764
7580
|
return parseNpmAuditJsonFile(resolvedPath);
|
|
@@ -6769,7 +7585,7 @@ function parseScanInput(filePath, format) {
|
|
|
6769
7585
|
if (resolved === "sarif") {
|
|
6770
7586
|
return parseSarifFile(resolvedPath);
|
|
6771
7587
|
}
|
|
6772
|
-
throw new Error(`Unsupported input format: ${resolved}`);
|
|
7588
|
+
throw new Error(`Unsupported input format: ${String(resolved)}`);
|
|
6773
7589
|
}
|
|
6774
7590
|
async function parseScanInputFromAudit(params) {
|
|
6775
7591
|
const pm = params.packageManager ?? detectPackageManager(params.cwd);
|
|
@@ -6780,7 +7596,7 @@ async function parseScanInputFromAudit(params) {
|
|
|
6780
7596
|
}
|
|
6781
7597
|
const command = resolveAuditCommand(pm, { workspace: params.workspace });
|
|
6782
7598
|
const [cmd, ...args] = command;
|
|
6783
|
-
const result = await
|
|
7599
|
+
const result = await execa11(cmd, args, {
|
|
6784
7600
|
cwd: params.cwd,
|
|
6785
7601
|
stdio: "pipe",
|
|
6786
7602
|
reject: false
|
|
@@ -6834,7 +7650,7 @@ function inferFormat(filePath) {
|
|
|
6834
7650
|
const ext = extname(filePath).toLowerCase();
|
|
6835
7651
|
if (ext === ".sarif") return "sarif";
|
|
6836
7652
|
try {
|
|
6837
|
-
const content =
|
|
7653
|
+
const content = readFileSync17(filePath, "utf8");
|
|
6838
7654
|
const firstLine = content.split("\n").find((line) => line.trim().startsWith("{"));
|
|
6839
7655
|
if (firstLine) {
|
|
6840
7656
|
const parsed = JSON.parse(firstLine);
|
|
@@ -7245,14 +8061,14 @@ async function remediatePortfolio(targets, options = {}) {
|
|
|
7245
8061
|
}
|
|
7246
8062
|
|
|
7247
8063
|
// src/api/update-outdated/index.ts
|
|
7248
|
-
import { join as
|
|
7249
|
-
import { readFileSync as
|
|
7250
|
-
import { execa as
|
|
8064
|
+
import { join as join21 } from "path";
|
|
8065
|
+
import { readFileSync as readFileSync18, writeFileSync as writeFileSync6 } from "fs";
|
|
8066
|
+
import { execa as execa12 } from "execa";
|
|
7251
8067
|
async function applyBump(params) {
|
|
7252
|
-
const pkgPath =
|
|
8068
|
+
const pkgPath = join21(params.cwd, "package.json");
|
|
7253
8069
|
let pkgJson;
|
|
7254
8070
|
try {
|
|
7255
|
-
pkgJson = JSON.parse(
|
|
8071
|
+
pkgJson = JSON.parse(readFileSync18(pkgPath, "utf8"));
|
|
7256
8072
|
} catch {
|
|
7257
8073
|
return {
|
|
7258
8074
|
applied: false,
|
|
@@ -7277,7 +8093,7 @@ async function applyBump(params) {
|
|
|
7277
8093
|
writeFileSync6(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
7278
8094
|
try {
|
|
7279
8095
|
const [installCmd, ...installArgs] = params.installCommand;
|
|
7280
|
-
await
|
|
8096
|
+
await execa12(installCmd, installArgs, { cwd: params.cwd, stdio: "pipe" });
|
|
7281
8097
|
} catch (err) {
|
|
7282
8098
|
pkgJson[depField][params.packageName] = currentRange;
|
|
7283
8099
|
writeFileSync6(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
@@ -7290,13 +8106,13 @@ async function applyBump(params) {
|
|
|
7290
8106
|
if (params.runTests) {
|
|
7291
8107
|
try {
|
|
7292
8108
|
const [testCmd, ...testArgs] = params.testCommand;
|
|
7293
|
-
await
|
|
8109
|
+
await execa12(testCmd, testArgs, { cwd: params.cwd, stdio: "pipe" });
|
|
7294
8110
|
} catch (err) {
|
|
7295
8111
|
pkgJson[depField][params.packageName] = currentRange;
|
|
7296
8112
|
writeFileSync6(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
7297
8113
|
try {
|
|
7298
8114
|
const [rollbackCmd, ...rollbackArgs] = params.installCommand;
|
|
7299
|
-
await
|
|
8115
|
+
await execa12(rollbackCmd, rollbackArgs, { cwd: params.cwd, stdio: "pipe" });
|
|
7300
8116
|
} catch {
|
|
7301
8117
|
}
|
|
7302
8118
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -7491,6 +8307,432 @@ async function updateOutdated(options = {}) {
|
|
|
7491
8307
|
};
|
|
7492
8308
|
}
|
|
7493
8309
|
|
|
8310
|
+
// src/api/reachability/index.ts
|
|
8311
|
+
async function checkReachability(options) {
|
|
8312
|
+
const cwd = options.cwd ?? process.cwd();
|
|
8313
|
+
return assessPackageReachability(cwd, options.packageName, options.symbol);
|
|
8314
|
+
}
|
|
8315
|
+
|
|
8316
|
+
// src/scanner/delta-scan.ts
|
|
8317
|
+
import { existsSync as existsSync14, readFileSync as readFileSync19 } from "fs";
|
|
8318
|
+
import { join as join22 } from "path";
|
|
8319
|
+
import { execa as execa13 } from "execa";
|
|
8320
|
+
import semver9 from "semver";
|
|
8321
|
+
|
|
8322
|
+
// src/scanner/lockfile-diff.ts
|
|
8323
|
+
import YAML2 from "yaml";
|
|
8324
|
+
import semver8 from "semver";
|
|
8325
|
+
function parseLockfileDependencies(filename, content) {
|
|
8326
|
+
const result = /* @__PURE__ */ new Map();
|
|
8327
|
+
const add = (name, ver) => {
|
|
8328
|
+
if (!name || !ver) return;
|
|
8329
|
+
const cleanVer = semver8.clean(ver) || semver8.coerce(ver)?.version || ver;
|
|
8330
|
+
let set = result.get(name);
|
|
8331
|
+
if (!set) {
|
|
8332
|
+
set = /* @__PURE__ */ new Set();
|
|
8333
|
+
result.set(name, set);
|
|
8334
|
+
}
|
|
8335
|
+
set.add(cleanVer);
|
|
8336
|
+
};
|
|
8337
|
+
if (filename.endsWith("package-lock.json")) {
|
|
8338
|
+
try {
|
|
8339
|
+
const data = JSON.parse(content);
|
|
8340
|
+
if (data.packages && typeof data.packages === "object") {
|
|
8341
|
+
for (const [key, pkg] of Object.entries(data.packages)) {
|
|
8342
|
+
if (!key || key === "" || typeof pkg !== "object" || !pkg) continue;
|
|
8343
|
+
const idx = key.lastIndexOf("node_modules/");
|
|
8344
|
+
if (idx !== -1) {
|
|
8345
|
+
const rawName = key.slice(idx + "node_modules/".length);
|
|
8346
|
+
const ver = pkg.version;
|
|
8347
|
+
add(rawName, ver);
|
|
8348
|
+
}
|
|
8349
|
+
}
|
|
8350
|
+
}
|
|
8351
|
+
if (data.dependencies && typeof data.dependencies === "object") {
|
|
8352
|
+
const walkDeps = (deps) => {
|
|
8353
|
+
for (const [name, info] of Object.entries(deps)) {
|
|
8354
|
+
if (!info || typeof info !== "object") continue;
|
|
8355
|
+
const depObj = info;
|
|
8356
|
+
add(name, depObj.version);
|
|
8357
|
+
if (depObj.dependencies && typeof depObj.dependencies === "object") {
|
|
8358
|
+
walkDeps(depObj.dependencies);
|
|
8359
|
+
}
|
|
8360
|
+
}
|
|
8361
|
+
};
|
|
8362
|
+
walkDeps(data.dependencies);
|
|
8363
|
+
}
|
|
8364
|
+
} catch {
|
|
8365
|
+
}
|
|
8366
|
+
} else if (filename.endsWith("pnpm-lock.yaml")) {
|
|
8367
|
+
try {
|
|
8368
|
+
const data = YAML2.parse(content);
|
|
8369
|
+
if (data?.packages && typeof data.packages === "object") {
|
|
8370
|
+
for (const [key, val] of Object.entries(data.packages)) {
|
|
8371
|
+
let rawKey = key.replace(/^\//, "");
|
|
8372
|
+
const parenIdx = rawKey.indexOf("(");
|
|
8373
|
+
if (parenIdx !== -1) {
|
|
8374
|
+
rawKey = rawKey.slice(0, parenIdx);
|
|
8375
|
+
}
|
|
8376
|
+
const lastAt = rawKey.lastIndexOf("@");
|
|
8377
|
+
if (lastAt > 0) {
|
|
8378
|
+
const name = val?.name ?? rawKey.slice(0, lastAt);
|
|
8379
|
+
const version = val?.version ?? rawKey.slice(lastAt + 1);
|
|
8380
|
+
add(name, version);
|
|
8381
|
+
}
|
|
8382
|
+
}
|
|
8383
|
+
}
|
|
8384
|
+
if (data?.snapshots && typeof data.snapshots === "object") {
|
|
8385
|
+
for (const key of Object.keys(data.snapshots)) {
|
|
8386
|
+
let rawKey = key.replace(/^\//, "");
|
|
8387
|
+
const parenIdx = rawKey.indexOf("(");
|
|
8388
|
+
if (parenIdx !== -1) {
|
|
8389
|
+
rawKey = rawKey.slice(0, parenIdx);
|
|
8390
|
+
}
|
|
8391
|
+
const lastAt = rawKey.lastIndexOf("@");
|
|
8392
|
+
if (lastAt > 0) {
|
|
8393
|
+
const name = rawKey.slice(0, lastAt);
|
|
8394
|
+
const version = rawKey.slice(lastAt + 1);
|
|
8395
|
+
add(name, version);
|
|
8396
|
+
}
|
|
8397
|
+
}
|
|
8398
|
+
}
|
|
8399
|
+
} catch {
|
|
8400
|
+
}
|
|
8401
|
+
} else if (filename.endsWith("yarn.lock")) {
|
|
8402
|
+
const lines = content.split("\n");
|
|
8403
|
+
let currentPackages = [];
|
|
8404
|
+
for (const line of lines) {
|
|
8405
|
+
if (line.startsWith("#") || line.trim() === "") continue;
|
|
8406
|
+
if (!line.startsWith(" ")) {
|
|
8407
|
+
currentPackages = [];
|
|
8408
|
+
const parts = line.split(",").map((p) => p.trim().replace(/^"/, "").replace(/":?$/, ""));
|
|
8409
|
+
for (const part of parts) {
|
|
8410
|
+
const lastAt = part.lastIndexOf("@");
|
|
8411
|
+
if (lastAt > 0) {
|
|
8412
|
+
currentPackages.push(part.slice(0, lastAt));
|
|
8413
|
+
}
|
|
8414
|
+
}
|
|
8415
|
+
} else {
|
|
8416
|
+
const match = line.match(/^\s+version\s+"?([^"\s]+)"?/);
|
|
8417
|
+
if (match && currentPackages.length > 0) {
|
|
8418
|
+
const ver = match[1];
|
|
8419
|
+
for (const pkg of currentPackages) {
|
|
8420
|
+
add(pkg, ver);
|
|
8421
|
+
}
|
|
8422
|
+
currentPackages = [];
|
|
8423
|
+
}
|
|
8424
|
+
}
|
|
8425
|
+
}
|
|
8426
|
+
}
|
|
8427
|
+
return result;
|
|
8428
|
+
}
|
|
8429
|
+
function computeLockfileDiff(filename, oldContent, newContent) {
|
|
8430
|
+
const changes = [];
|
|
8431
|
+
const oldDeps = oldContent ? parseLockfileDependencies(filename, oldContent) : /* @__PURE__ */ new Map();
|
|
8432
|
+
const newDeps = newContent ? parseLockfileDependencies(filename, newContent) : /* @__PURE__ */ new Map();
|
|
8433
|
+
const allPkgs = /* @__PURE__ */ new Set([...oldDeps.keys(), ...newDeps.keys()]);
|
|
8434
|
+
for (const pkg of allPkgs) {
|
|
8435
|
+
const oldVers = oldDeps.get(pkg) ?? /* @__PURE__ */ new Set();
|
|
8436
|
+
const newVers = newDeps.get(pkg) ?? /* @__PURE__ */ new Set();
|
|
8437
|
+
for (const nVer of newVers) {
|
|
8438
|
+
if (!oldVers.has(nVer)) {
|
|
8439
|
+
if (oldVers.size === 0) {
|
|
8440
|
+
changes.push({
|
|
8441
|
+
packageName: pkg,
|
|
8442
|
+
changeType: "added",
|
|
8443
|
+
newVersion: nVer,
|
|
8444
|
+
dependencyType: "transitive",
|
|
8445
|
+
manifestPath: filename
|
|
8446
|
+
});
|
|
8447
|
+
} else {
|
|
8448
|
+
const [firstOld] = oldVers;
|
|
8449
|
+
const cleanOld = semver8.clean(firstOld) || semver8.coerce(firstOld)?.version;
|
|
8450
|
+
const cleanNew = semver8.clean(nVer) || semver8.coerce(nVer)?.version;
|
|
8451
|
+
const isUpgrade = cleanOld && cleanNew && semver8.valid(cleanOld) && semver8.valid(cleanNew) ? semver8.gt(cleanNew, cleanOld) : true;
|
|
8452
|
+
changes.push({
|
|
8453
|
+
packageName: pkg,
|
|
8454
|
+
changeType: isUpgrade ? "upgraded" : "downgraded",
|
|
8455
|
+
oldVersion: firstOld,
|
|
8456
|
+
newVersion: nVer,
|
|
8457
|
+
dependencyType: "transitive",
|
|
8458
|
+
manifestPath: filename
|
|
8459
|
+
});
|
|
8460
|
+
}
|
|
8461
|
+
}
|
|
8462
|
+
}
|
|
8463
|
+
if (newVers.size === 0 && oldVers.size > 0) {
|
|
8464
|
+
for (const oVer of oldVers) {
|
|
8465
|
+
changes.push({
|
|
8466
|
+
packageName: pkg,
|
|
8467
|
+
changeType: "removed",
|
|
8468
|
+
oldVersion: oVer,
|
|
8469
|
+
dependencyType: "transitive",
|
|
8470
|
+
manifestPath: filename
|
|
8471
|
+
});
|
|
8472
|
+
}
|
|
8473
|
+
}
|
|
8474
|
+
}
|
|
8475
|
+
return changes;
|
|
8476
|
+
}
|
|
8477
|
+
|
|
8478
|
+
// src/scanner/delta-scan.ts
|
|
8479
|
+
var DEP_SECTIONS = [
|
|
8480
|
+
"dependencies",
|
|
8481
|
+
"devDependencies",
|
|
8482
|
+
"peerDependencies",
|
|
8483
|
+
"optionalDependencies"
|
|
8484
|
+
];
|
|
8485
|
+
async function getChangedManifestPaths(cwd, baseRef) {
|
|
8486
|
+
const manifests = /* @__PURE__ */ new Set();
|
|
8487
|
+
try {
|
|
8488
|
+
const { stdout: diffFiles } = await execa13(
|
|
8489
|
+
"git",
|
|
8490
|
+
["diff", "--name-only", baseRef, "--", "package.json", "**/package.json"],
|
|
8491
|
+
{ cwd, reject: false }
|
|
8492
|
+
);
|
|
8493
|
+
for (const f of diffFiles.split("\n")) {
|
|
8494
|
+
const trimmed = f.trim();
|
|
8495
|
+
if (trimmed && trimmed.endsWith("package.json") && !trimmed.includes("node_modules")) {
|
|
8496
|
+
manifests.add(trimmed);
|
|
8497
|
+
}
|
|
8498
|
+
}
|
|
8499
|
+
const { stdout: untrackedFiles } = await execa13(
|
|
8500
|
+
"git",
|
|
8501
|
+
["ls-files", "--others", "--exclude-standard", "--", "package.json", "**/package.json"],
|
|
8502
|
+
{ cwd, reject: false }
|
|
8503
|
+
);
|
|
8504
|
+
for (const f of untrackedFiles.split("\n")) {
|
|
8505
|
+
const trimmed = f.trim();
|
|
8506
|
+
if (trimmed && trimmed.endsWith("package.json") && !trimmed.includes("node_modules")) {
|
|
8507
|
+
manifests.add(trimmed);
|
|
8508
|
+
}
|
|
8509
|
+
}
|
|
8510
|
+
} catch {
|
|
8511
|
+
if (existsSync14(join22(cwd, "package.json"))) {
|
|
8512
|
+
manifests.add("package.json");
|
|
8513
|
+
}
|
|
8514
|
+
}
|
|
8515
|
+
return Array.from(manifests);
|
|
8516
|
+
}
|
|
8517
|
+
async function getOldManifestContent(cwd, baseRef, relPath) {
|
|
8518
|
+
try {
|
|
8519
|
+
const { stdout } = await execa13("git", ["show", `${baseRef}:${relPath}`], {
|
|
8520
|
+
cwd,
|
|
8521
|
+
reject: false
|
|
8522
|
+
});
|
|
8523
|
+
if (!stdout || !stdout.trim()) return null;
|
|
8524
|
+
return JSON.parse(stdout);
|
|
8525
|
+
} catch {
|
|
8526
|
+
return null;
|
|
8527
|
+
}
|
|
8528
|
+
}
|
|
8529
|
+
function getNewManifestContent(cwd, relPath) {
|
|
8530
|
+
const fullPath = join22(cwd, relPath);
|
|
8531
|
+
if (!existsSync14(fullPath)) return null;
|
|
8532
|
+
try {
|
|
8533
|
+
return JSON.parse(readFileSync19(fullPath, "utf8"));
|
|
8534
|
+
} catch {
|
|
8535
|
+
return null;
|
|
8536
|
+
}
|
|
8537
|
+
}
|
|
8538
|
+
function computeDependencyDiff(relPath, oldPkg, newPkg) {
|
|
8539
|
+
const changes = [];
|
|
8540
|
+
for (const section of DEP_SECTIONS) {
|
|
8541
|
+
const oldDeps = oldPkg?.[section] ?? {};
|
|
8542
|
+
const newDeps = newPkg?.[section] ?? {};
|
|
8543
|
+
for (const [pkgName, newVer] of Object.entries(newDeps)) {
|
|
8544
|
+
const oldVer = oldDeps[pkgName];
|
|
8545
|
+
if (!oldVer) {
|
|
8546
|
+
changes.push({
|
|
8547
|
+
packageName: pkgName,
|
|
8548
|
+
changeType: "added",
|
|
8549
|
+
newVersion: newVer,
|
|
8550
|
+
dependencyType: section,
|
|
8551
|
+
manifestPath: relPath
|
|
8552
|
+
});
|
|
8553
|
+
} else if (oldVer !== newVer) {
|
|
8554
|
+
const cleanOld = semver9.clean(oldVer) || semver9.coerce(oldVer)?.version;
|
|
8555
|
+
const cleanNew = semver9.clean(newVer) || semver9.coerce(newVer)?.version;
|
|
8556
|
+
const isUpgrade = cleanOld && cleanNew && semver9.valid(cleanOld) && semver9.valid(cleanNew) ? semver9.gt(cleanNew, cleanOld) : true;
|
|
8557
|
+
changes.push({
|
|
8558
|
+
packageName: pkgName,
|
|
8559
|
+
changeType: isUpgrade ? "upgraded" : "downgraded",
|
|
8560
|
+
oldVersion: oldVer,
|
|
8561
|
+
newVersion: newVer,
|
|
8562
|
+
dependencyType: section,
|
|
8563
|
+
manifestPath: relPath
|
|
8564
|
+
});
|
|
8565
|
+
}
|
|
8566
|
+
}
|
|
8567
|
+
for (const [pkgName, oldVer] of Object.entries(oldDeps)) {
|
|
8568
|
+
if (!(pkgName in newDeps)) {
|
|
8569
|
+
changes.push({
|
|
8570
|
+
packageName: pkgName,
|
|
8571
|
+
changeType: "removed",
|
|
8572
|
+
oldVersion: oldVer,
|
|
8573
|
+
dependencyType: section,
|
|
8574
|
+
manifestPath: relPath
|
|
8575
|
+
});
|
|
8576
|
+
}
|
|
8577
|
+
}
|
|
8578
|
+
}
|
|
8579
|
+
return changes;
|
|
8580
|
+
}
|
|
8581
|
+
var LOCKFILE_PATTERNS = [
|
|
8582
|
+
"package-lock.json",
|
|
8583
|
+
"**/package-lock.json",
|
|
8584
|
+
"pnpm-lock.yaml",
|
|
8585
|
+
"**/pnpm-lock.yaml",
|
|
8586
|
+
"yarn.lock",
|
|
8587
|
+
"**/yarn.lock"
|
|
8588
|
+
];
|
|
8589
|
+
async function getChangedLockfilePaths(cwd, baseRef) {
|
|
8590
|
+
const lockfiles = /* @__PURE__ */ new Set();
|
|
8591
|
+
try {
|
|
8592
|
+
const { stdout: diffFiles } = await execa13(
|
|
8593
|
+
"git",
|
|
8594
|
+
["diff", "--name-only", baseRef, "--", ...LOCKFILE_PATTERNS],
|
|
8595
|
+
{ cwd, reject: false }
|
|
8596
|
+
);
|
|
8597
|
+
for (const f of diffFiles.split("\n")) {
|
|
8598
|
+
const trimmed = f.trim();
|
|
8599
|
+
if (trimmed && !trimmed.includes("node_modules") && (trimmed.endsWith("package-lock.json") || trimmed.endsWith("pnpm-lock.yaml") || trimmed.endsWith("yarn.lock"))) {
|
|
8600
|
+
lockfiles.add(trimmed);
|
|
8601
|
+
}
|
|
8602
|
+
}
|
|
8603
|
+
const { stdout: untrackedFiles } = await execa13(
|
|
8604
|
+
"git",
|
|
8605
|
+
["ls-files", "--others", "--exclude-standard", "--", ...LOCKFILE_PATTERNS],
|
|
8606
|
+
{ cwd, reject: false }
|
|
8607
|
+
);
|
|
8608
|
+
for (const f of untrackedFiles.split("\n")) {
|
|
8609
|
+
const trimmed = f.trim();
|
|
8610
|
+
if (trimmed && !trimmed.includes("node_modules") && (trimmed.endsWith("package-lock.json") || trimmed.endsWith("pnpm-lock.yaml") || trimmed.endsWith("yarn.lock"))) {
|
|
8611
|
+
lockfiles.add(trimmed);
|
|
8612
|
+
}
|
|
8613
|
+
}
|
|
8614
|
+
} catch {
|
|
8615
|
+
}
|
|
8616
|
+
return Array.from(lockfiles);
|
|
8617
|
+
}
|
|
8618
|
+
async function getFileContentAtRef(cwd, baseRef, relPath) {
|
|
8619
|
+
try {
|
|
8620
|
+
const { stdout } = await execa13("git", ["show", `${baseRef}:${relPath}`], {
|
|
8621
|
+
cwd,
|
|
8622
|
+
reject: false
|
|
8623
|
+
});
|
|
8624
|
+
return stdout || null;
|
|
8625
|
+
} catch {
|
|
8626
|
+
return null;
|
|
8627
|
+
}
|
|
8628
|
+
}
|
|
8629
|
+
async function scanDelta(options = {}) {
|
|
8630
|
+
const cwd = options.cwd ?? process.cwd();
|
|
8631
|
+
const baseRef = options.baseRef ?? "HEAD";
|
|
8632
|
+
const changedManifests = await getChangedManifestPaths(cwd, baseRef);
|
|
8633
|
+
const changedLockfiles = await getChangedLockfilePaths(cwd, baseRef);
|
|
8634
|
+
const dependencyChanges = [];
|
|
8635
|
+
const directPkgs = /* @__PURE__ */ new Set();
|
|
8636
|
+
for (const manifestRel of changedManifests) {
|
|
8637
|
+
const oldPkg = await getOldManifestContent(cwd, baseRef, manifestRel);
|
|
8638
|
+
const newPkg = getNewManifestContent(cwd, manifestRel);
|
|
8639
|
+
const diffs = computeDependencyDiff(manifestRel, oldPkg, newPkg);
|
|
8640
|
+
for (const d of diffs) {
|
|
8641
|
+
directPkgs.add(d.packageName);
|
|
8642
|
+
}
|
|
8643
|
+
dependencyChanges.push(...diffs);
|
|
8644
|
+
}
|
|
8645
|
+
for (const lockfileRel of changedLockfiles) {
|
|
8646
|
+
const oldContent = await getFileContentAtRef(cwd, baseRef, lockfileRel);
|
|
8647
|
+
const fullPath = join22(cwd, lockfileRel);
|
|
8648
|
+
const newContent = existsSync14(fullPath) ? readFileSync19(fullPath, "utf8") : null;
|
|
8649
|
+
const lockDiffs = computeLockfileDiff(lockfileRel, oldContent, newContent);
|
|
8650
|
+
for (const change of lockDiffs) {
|
|
8651
|
+
if (!directPkgs.has(change.packageName)) {
|
|
8652
|
+
dependencyChanges.push(change);
|
|
8653
|
+
}
|
|
8654
|
+
}
|
|
8655
|
+
if (!changedManifests.includes(lockfileRel)) {
|
|
8656
|
+
changedManifests.push(lockfileRel);
|
|
8657
|
+
}
|
|
8658
|
+
}
|
|
8659
|
+
const introducedFindings = [];
|
|
8660
|
+
const resolvedFindings = [];
|
|
8661
|
+
for (const change of dependencyChanges) {
|
|
8662
|
+
if (change.changeType === "added" || change.changeType === "upgraded" || change.changeType === "downgraded") {
|
|
8663
|
+
const cleanVer = semver9.coerce(change.newVersion)?.version ?? change.newVersion;
|
|
8664
|
+
const evaluation = await evaluatePackage(change.packageName, {
|
|
8665
|
+
version: cleanVer,
|
|
8666
|
+
enrichIntelligence: true
|
|
8667
|
+
});
|
|
8668
|
+
for (const vuln of evaluation.vulnerabilities) {
|
|
8669
|
+
introducedFindings.push({
|
|
8670
|
+
packageName: change.packageName,
|
|
8671
|
+
cveId: vuln.cveId,
|
|
8672
|
+
severity: vuln.severity,
|
|
8673
|
+
summary: vuln.summary,
|
|
8674
|
+
deltaType: "introduced",
|
|
8675
|
+
installedVersion: change.newVersion,
|
|
8676
|
+
safeUpgradeVersion: vuln.safeUpgradeVersion,
|
|
8677
|
+
inCisaKev: vuln.inCisaKev,
|
|
8678
|
+
epssScore: vuln.epssScore
|
|
8679
|
+
});
|
|
8680
|
+
}
|
|
8681
|
+
}
|
|
8682
|
+
if (change.oldVersion && (change.changeType === "upgraded" || change.changeType === "removed")) {
|
|
8683
|
+
const cleanOldVer = semver9.coerce(change.oldVersion)?.version ?? change.oldVersion;
|
|
8684
|
+
const oldEval = await evaluatePackage(change.packageName, {
|
|
8685
|
+
version: cleanOldVer,
|
|
8686
|
+
enrichIntelligence: true
|
|
8687
|
+
});
|
|
8688
|
+
const cleanNewVer = change.newVersion ? semver9.coerce(change.newVersion)?.version ?? change.newVersion : void 0;
|
|
8689
|
+
const newEval = cleanNewVer ? await evaluatePackage(change.packageName, {
|
|
8690
|
+
version: cleanNewVer,
|
|
8691
|
+
enrichIntelligence: false
|
|
8692
|
+
}) : { vulnerabilities: [] };
|
|
8693
|
+
const newCveSet = new Set(newEval.vulnerabilities.map((v) => v.cveId));
|
|
8694
|
+
for (const oldVuln of oldEval.vulnerabilities) {
|
|
8695
|
+
if (!newCveSet.has(oldVuln.cveId)) {
|
|
8696
|
+
resolvedFindings.push({
|
|
8697
|
+
packageName: change.packageName,
|
|
8698
|
+
cveId: oldVuln.cveId,
|
|
8699
|
+
severity: oldVuln.severity,
|
|
8700
|
+
summary: oldVuln.summary,
|
|
8701
|
+
deltaType: "resolved",
|
|
8702
|
+
installedVersion: change.oldVersion,
|
|
8703
|
+
safeUpgradeVersion: oldVuln.safeUpgradeVersion,
|
|
8704
|
+
inCisaKev: oldVuln.inCisaKev,
|
|
8705
|
+
epssScore: oldVuln.epssScore
|
|
8706
|
+
});
|
|
8707
|
+
}
|
|
8708
|
+
}
|
|
8709
|
+
}
|
|
8710
|
+
}
|
|
8711
|
+
let netRiskVerdict = "neutral";
|
|
8712
|
+
let status = "ok";
|
|
8713
|
+
if (introducedFindings.length > 0) {
|
|
8714
|
+
netRiskVerdict = "degraded";
|
|
8715
|
+
status = "degraded";
|
|
8716
|
+
} else if (resolvedFindings.length > 0) {
|
|
8717
|
+
netRiskVerdict = "improved";
|
|
8718
|
+
status = "improved";
|
|
8719
|
+
}
|
|
8720
|
+
const summary = introducedFindings.length > 0 ? `Delta scan against ${baseRef} found ${introducedFindings.length} newly introduced vulnerability(ies) across ${dependencyChanges.length} dependency change(s) (net risk: degraded).` : resolvedFindings.length > 0 ? `Delta scan against ${baseRef} resolved ${resolvedFindings.length} vulnerability(ies) across ${dependencyChanges.length} dependency change(s) (net risk: improved).` : `Delta scan against ${baseRef} found 0 new vulnerabilities across ${dependencyChanges.length} dependency change(s) (net risk: neutral).`;
|
|
8721
|
+
return {
|
|
8722
|
+
schemaVersion: "1.0",
|
|
8723
|
+
status,
|
|
8724
|
+
baseRef,
|
|
8725
|
+
targetRef: "working-tree",
|
|
8726
|
+
changedManifests,
|
|
8727
|
+
dependencyChanges,
|
|
8728
|
+
introducedFindings,
|
|
8729
|
+
resolvedFindings,
|
|
8730
|
+
netRiskVerdict,
|
|
8731
|
+
summary,
|
|
8732
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8733
|
+
};
|
|
8734
|
+
}
|
|
8735
|
+
|
|
7494
8736
|
// src/api/jobs/index.ts
|
|
7495
8737
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
7496
8738
|
|
|
@@ -7549,19 +8791,23 @@ export {
|
|
|
7549
8791
|
buildDependencyScopeCounts,
|
|
7550
8792
|
buildSlaBreachSummary,
|
|
7551
8793
|
buildStrategyCounts,
|
|
8794
|
+
checkReachability,
|
|
7552
8795
|
ciExitCode,
|
|
7553
8796
|
createConstraintSchemaProperties,
|
|
7554
8797
|
createRemediateOptionSchemaProperties,
|
|
7555
8798
|
createScanOptionSchemaProperties,
|
|
7556
8799
|
createScanReportSchemaProperties,
|
|
7557
8800
|
createUpdateOutdatedOptionSchemaProperties,
|
|
8801
|
+
evaluatePackage,
|
|
7558
8802
|
inspectPatchArtifact,
|
|
7559
8803
|
listPatchArtifacts,
|
|
7560
8804
|
planRemediation,
|
|
7561
8805
|
pollJob,
|
|
8806
|
+
reconcilePatchArtifacts,
|
|
7562
8807
|
remediate,
|
|
7563
8808
|
remediateFromScan,
|
|
7564
8809
|
remediatePortfolio,
|
|
8810
|
+
scanDelta,
|
|
7565
8811
|
submitPortfolioJob,
|
|
7566
8812
|
submitRemediateJob,
|
|
7567
8813
|
submitScanJob,
|