autoremediator 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -27
- package/dist/api/change-request/index.d.ts +2 -1
- package/dist/api/change-request/index.d.ts.map +1 -1
- package/dist/api/context.d.ts.map +1 -1
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/options-schema.d.ts +3 -0
- package/dist/api/options-schema.d.ts.map +1 -1
- package/dist/api/patches/index.d.ts +1 -0
- package/dist/api/patches/index.d.ts.map +1 -1
- package/dist/api/patches/reconcile.d.ts +7 -0
- package/dist/api/patches/reconcile.d.ts.map +1 -0
- package/dist/api/patches/reconcile.test.d.ts +2 -0
- package/dist/api/patches/reconcile.test.d.ts.map +1 -0
- package/dist/api/remediate/validators.d.ts +3 -0
- package/dist/api/remediate/validators.d.ts.map +1 -1
- package/dist/{chunk-U4LIJIGD.js → chunk-7BORIBPY.js} +2476 -885
- package/dist/cli/program.d.ts.map +1 -1
- package/dist/cli/runners.d.ts +5 -0
- package/dist/cli/runners.d.ts.map +1 -1
- package/dist/cli/triage.d.ts +7 -0
- package/dist/cli/triage.d.ts.map +1 -0
- package/dist/cli/types.d.ts +5 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli.js +233 -6
- package/dist/index.js +2479 -889
- package/dist/index.js.map +1 -1
- package/dist/intelligence/sources/github-advisory.d.ts.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/openapi/server.js +1 -1
- package/dist/platform/evidence.d.ts +2 -0
- package/dist/platform/evidence.d.ts.map +1 -1
- package/dist/platform/package-manager/index.d.ts +1 -0
- package/dist/platform/package-manager/index.d.ts.map +1 -1
- package/dist/platform/supply-chain.d.ts +10 -0
- package/dist/platform/supply-chain.d.ts.map +1 -0
- package/dist/platform/supply-chain.test.d.ts +2 -0
- package/dist/platform/supply-chain.test.d.ts.map +1 -0
- package/dist/platform/types.d.ts +69 -6
- package/dist/platform/types.d.ts.map +1 -1
- package/dist/platform/workspace.d.ts +28 -0
- package/dist/platform/workspace.d.ts.map +1 -0
- package/dist/platform/workspace.test.d.ts +2 -0
- package/dist/platform/workspace.test.d.ts.map +1 -0
- package/dist/remediation/local/fallback.d.ts +2 -0
- package/dist/remediation/local/fallback.d.ts.map +1 -1
- package/dist/remediation/local/options.d.ts +3 -0
- package/dist/remediation/local/options.d.ts.map +1 -1
- package/dist/remediation/local/primary-strategy.d.ts +2 -9
- package/dist/remediation/local/primary-strategy.d.ts.map +1 -1
- package/dist/remediation/local/run.d.ts.map +1 -1
- package/dist/remediation/pipeline-runtime/tool-wrappers.d.ts.map +1 -1
- package/dist/remediation/proof-of-fix/runner.d.ts +16 -0
- package/dist/remediation/proof-of-fix/runner.d.ts.map +1 -0
- package/dist/remediation/proof-of-fix/runner.test.d.ts +2 -0
- package/dist/remediation/proof-of-fix/runner.test.d.ts.map +1 -0
- package/dist/remediation/refactor/call-site-finder.d.ts +15 -0
- package/dist/remediation/refactor/call-site-finder.d.ts.map +1 -0
- package/dist/remediation/refactor/refactor-agent.d.ts +34 -0
- package/dist/remediation/refactor/refactor-agent.d.ts.map +1 -0
- package/dist/remediation/refactor/refactor.test.d.ts +2 -0
- package/dist/remediation/refactor/refactor.test.d.ts.map +1 -0
- package/dist/remediation/strategies/patch-synthesis-prompt.d.ts.map +1 -1
- package/dist/remediation/tools/apply-patch-file/index.d.ts +5 -1
- package/dist/remediation/tools/apply-patch-file/index.d.ts.map +1 -1
- package/dist/remediation/tools/apply-version-bump.d.ts +2 -0
- package/dist/remediation/tools/apply-version-bump.d.ts.map +1 -1
- package/dist/remediation/tools/check-inventory.d.ts.map +1 -1
- package/dist/remediation/tools/check-reachability.d.ts +6 -1
- package/dist/remediation/tools/check-reachability.d.ts.map +1 -1
- package/dist/remediation/tools/generate-patch/helpers.d.ts +1 -0
- package/dist/remediation/tools/generate-patch/helpers.d.ts.map +1 -1
- package/dist/remediation/tools/generate-patch/index.d.ts +1 -0
- package/dist/remediation/tools/generate-patch/index.d.ts.map +1 -1
- package/dist/scanner/adapters/npm-audit.d.ts.map +1 -1
- package/dist/scanner/adapters/yarn-audit.d.ts.map +1 -1
- package/dist/scanner/delta-scan.d.ts.map +1 -1
- package/dist/scanner/lockfile-diff.d.ts +10 -0
- package/dist/scanner/lockfile-diff.d.ts.map +1 -0
- package/dist/scanner/lockfile-diff.test.d.ts +2 -0
- package/dist/scanner/lockfile-diff.test.d.ts.map +1 -0
- package/package.json +8 -8
|
@@ -65,7 +65,10 @@ var OPTION_DESCRIPTIONS = {
|
|
|
65
65
|
campaignMode: "Enable portfolio campaign mode to risk-rank targets before execution",
|
|
66
66
|
escalationGraph: "Optional mapping from unresolved reasons to intended escalation actions",
|
|
67
67
|
offlineIntelligence: "Skip all network calls to intelligence sources. Only local inventory and npm-registry version resolution proceed.",
|
|
68
|
-
intelligenceSnapshotPath: "Path to a local JSON snapshot of pre-fetched CVE intelligence (keyed by CVE ID) used when offline mode is active."
|
|
68
|
+
intelligenceSnapshotPath: "Path to a local JSON snapshot of pre-fetched CVE intelligence (keyed by CVE ID) used when offline mode is active.",
|
|
69
|
+
proofOfFix: "If true, enforce dual-stage exploit reproduction proof-of-fix verification (assert failure pre-patch, assert pass post-patch)",
|
|
70
|
+
requireProofOfFix: "If true, reject patches and roll back if proof-of-fix verification fails",
|
|
71
|
+
breakingChangeRefactor: "If true, attempt agentic call-site refactoring when major version upgrades introduce breaking API changes"
|
|
69
72
|
};
|
|
70
73
|
function createConstraintSchemaProperties() {
|
|
71
74
|
return {
|
|
@@ -87,7 +90,12 @@ function createConstraintSchemaProperties() {
|
|
|
87
90
|
type: "boolean",
|
|
88
91
|
description: OPTION_DESCRIPTIONS.enforceFrozenLockfile
|
|
89
92
|
},
|
|
90
|
-
workspace: { type: "string", description: OPTION_DESCRIPTIONS.workspace }
|
|
93
|
+
workspace: { type: "string", description: OPTION_DESCRIPTIONS.workspace },
|
|
94
|
+
requireProofOfFix: { type: "boolean", description: OPTION_DESCRIPTIONS.requireProofOfFix },
|
|
95
|
+
enableBreakingChangeRefactor: {
|
|
96
|
+
type: "boolean",
|
|
97
|
+
description: OPTION_DESCRIPTIONS.breakingChangeRefactor
|
|
98
|
+
}
|
|
91
99
|
};
|
|
92
100
|
}
|
|
93
101
|
function createRemediateOptionSchemaProperties(options) {
|
|
@@ -306,6 +314,14 @@ function createRemediateOptionSchemaProperties(options) {
|
|
|
306
314
|
"validation-failed": {
|
|
307
315
|
type: "string",
|
|
308
316
|
enum: ["open-issue", "notify-channel", "create-draft-pr", "hold-branch", "none"]
|
|
317
|
+
},
|
|
318
|
+
"proof-of-fix-failed": {
|
|
319
|
+
type: "string",
|
|
320
|
+
enum: ["open-issue", "notify-channel", "create-draft-pr", "hold-branch", "none"]
|
|
321
|
+
},
|
|
322
|
+
"breaking-change-unresolved": {
|
|
323
|
+
type: "string",
|
|
324
|
+
enum: ["open-issue", "notify-channel", "create-draft-pr", "hold-branch", "none"]
|
|
309
325
|
}
|
|
310
326
|
},
|
|
311
327
|
additionalProperties: false
|
|
@@ -314,6 +330,12 @@ function createRemediateOptionSchemaProperties(options) {
|
|
|
314
330
|
intelligenceSnapshotPath: {
|
|
315
331
|
type: "string",
|
|
316
332
|
description: OPTION_DESCRIPTIONS.intelligenceSnapshotPath
|
|
333
|
+
},
|
|
334
|
+
proofOfFix: { type: "boolean", description: OPTION_DESCRIPTIONS.proofOfFix },
|
|
335
|
+
requireProofOfFix: { type: "boolean", description: OPTION_DESCRIPTIONS.requireProofOfFix },
|
|
336
|
+
breakingChangeRefactor: {
|
|
337
|
+
type: "boolean",
|
|
338
|
+
description: OPTION_DESCRIPTIONS.breakingChangeRefactor
|
|
317
339
|
}
|
|
318
340
|
};
|
|
319
341
|
}
|
|
@@ -463,12 +485,12 @@ import { readdir, readFile as readFile2, stat } from "fs/promises";
|
|
|
463
485
|
import { join as join5 } from "path";
|
|
464
486
|
|
|
465
487
|
// src/remediation/strategies/patch-utils.ts
|
|
466
|
-
import { existsSync as existsSync3, mkdirSync, writeFileSync, readFileSync as
|
|
488
|
+
import { existsSync as existsSync3, mkdirSync, writeFileSync, readFileSync as readFileSync3 } from "fs";
|
|
467
489
|
import { join as join4 } from "path";
|
|
468
490
|
import { execa as execa2 } from "execa";
|
|
469
491
|
|
|
470
492
|
// src/platform/package-manager/index.ts
|
|
471
|
-
import { existsSync as existsSync2 } from "fs";
|
|
493
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
472
494
|
import { join as join3 } from "path";
|
|
473
495
|
import { execa } from "execa";
|
|
474
496
|
|
|
@@ -661,6 +683,32 @@ function resolveTestCommand(pm, constraints) {
|
|
|
661
683
|
const base = pm === "pnpm" ? ["pnpm", "test"] : pm === "yarn" ? ["yarn", "test"] : pm === "bun" ? ["bun", "test"] : pm === "deno" ? ["deno", "test"] : ["npm", "test"];
|
|
662
684
|
return withWorkspace(base, pm, constraints?.workspace);
|
|
663
685
|
}
|
|
686
|
+
function resolveTypecheckCommand(pm, constraints, cwd = process.cwd()) {
|
|
687
|
+
const pkgJsonPath = join3(cwd, "package.json");
|
|
688
|
+
let hasTypecheckScript = false;
|
|
689
|
+
const hasTsConfig = existsSync2(join3(cwd, "tsconfig.json"));
|
|
690
|
+
if (existsSync2(pkgJsonPath)) {
|
|
691
|
+
try {
|
|
692
|
+
const pkg = JSON.parse(readFileSync2(pkgJsonPath, "utf8"));
|
|
693
|
+
if (pkg.scripts?.typecheck) {
|
|
694
|
+
hasTypecheckScript = true;
|
|
695
|
+
}
|
|
696
|
+
} catch {
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (hasTypecheckScript) {
|
|
700
|
+
const base = [pm, "run", "typecheck"];
|
|
701
|
+
return withWorkspace(base, pm, constraints?.workspace);
|
|
702
|
+
}
|
|
703
|
+
if (hasTsConfig) {
|
|
704
|
+
if (pm === "deno") {
|
|
705
|
+
return withWorkspace(["deno", "check"], pm, constraints?.workspace);
|
|
706
|
+
}
|
|
707
|
+
const base = ["npx", "tsc", "--noEmit"];
|
|
708
|
+
return withWorkspace(base, pm, constraints?.workspace);
|
|
709
|
+
}
|
|
710
|
+
return [];
|
|
711
|
+
}
|
|
664
712
|
function resolveAuditCommand(pm, constraints) {
|
|
665
713
|
if (pm === "deno") {
|
|
666
714
|
throw new Error(
|
|
@@ -905,12 +953,12 @@ function defineTool(config) {
|
|
|
905
953
|
|
|
906
954
|
// src/remediation/tools/check-inventory.ts
|
|
907
955
|
import { z } from "zod";
|
|
908
|
-
import { readFileSync as
|
|
909
|
-
import { join as
|
|
956
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
957
|
+
import { join as join8 } from "path";
|
|
910
958
|
import { execa as execa3 } from "execa";
|
|
911
959
|
|
|
912
960
|
// src/platform/policy.ts
|
|
913
|
-
import { existsSync as existsSync6, readFileSync as
|
|
961
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
|
|
914
962
|
import { join as join6 } from "path";
|
|
915
963
|
import { parse as yamlParse } from "yaml";
|
|
916
964
|
var DEFAULT_POLICY = {
|
|
@@ -945,7 +993,7 @@ function loadPolicy(cwd, explicitPath) {
|
|
|
945
993
|
const candidate = explicitPath ?? join6(cwd, ".github", "autoremediator.yml");
|
|
946
994
|
if (!existsSync6(candidate)) return DEFAULT_POLICY;
|
|
947
995
|
try {
|
|
948
|
-
const parsed = yamlParse(
|
|
996
|
+
const parsed = yamlParse(readFileSync4(candidate, "utf8"));
|
|
949
997
|
return {
|
|
950
998
|
allowMajorBumps: parsed.allowMajorBumps ?? DEFAULT_POLICY.allowMajorBumps,
|
|
951
999
|
denyPackages: parsed.denyPackages ?? DEFAULT_POLICY.denyPackages,
|
|
@@ -1006,7 +1054,7 @@ function isActiveSuppression(suppression) {
|
|
|
1006
1054
|
}
|
|
1007
1055
|
function loadSuppressionsFile(filePath) {
|
|
1008
1056
|
try {
|
|
1009
|
-
const content =
|
|
1057
|
+
const content = readFileSync4(filePath, "utf8");
|
|
1010
1058
|
const parsed = yamlParse(content);
|
|
1011
1059
|
return Array.isArray(parsed?.suppressions) ? parsed.suppressions : [];
|
|
1012
1060
|
} catch {
|
|
@@ -1032,6 +1080,140 @@ function checkSlaBreach(cveId, severity, publishedAt, slaPolicy) {
|
|
|
1032
1080
|
};
|
|
1033
1081
|
}
|
|
1034
1082
|
|
|
1083
|
+
// src/platform/workspace.ts
|
|
1084
|
+
import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync5, statSync } from "fs";
|
|
1085
|
+
import { join as join7, resolve as resolve2, relative } from "path";
|
|
1086
|
+
import yaml from "yaml";
|
|
1087
|
+
function findWorkspaceRoot(startDir) {
|
|
1088
|
+
let curr = resolve2(startDir);
|
|
1089
|
+
while (true) {
|
|
1090
|
+
if (existsSync7(join7(curr, "pnpm-workspace.yaml"))) return curr;
|
|
1091
|
+
const pkgPath = join7(curr, "package.json");
|
|
1092
|
+
if (existsSync7(pkgPath)) {
|
|
1093
|
+
try {
|
|
1094
|
+
const parsed = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
1095
|
+
if (parsed.workspaces) return curr;
|
|
1096
|
+
} catch {
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
const parent = resolve2(curr, "..");
|
|
1100
|
+
if (parent === curr) break;
|
|
1101
|
+
curr = parent;
|
|
1102
|
+
}
|
|
1103
|
+
return null;
|
|
1104
|
+
}
|
|
1105
|
+
function findWorkspacePackages(cwd) {
|
|
1106
|
+
const rootCwd = findWorkspaceRoot(cwd) ?? resolve2(cwd);
|
|
1107
|
+
const packages = [];
|
|
1108
|
+
const packageGlobs = [];
|
|
1109
|
+
const pnpmWorkspaceFile = join7(rootCwd, "pnpm-workspace.yaml");
|
|
1110
|
+
if (existsSync7(pnpmWorkspaceFile)) {
|
|
1111
|
+
try {
|
|
1112
|
+
const content = readFileSync5(pnpmWorkspaceFile, "utf8");
|
|
1113
|
+
const parsed = yaml.parse(content);
|
|
1114
|
+
if (Array.isArray(parsed?.packages)) {
|
|
1115
|
+
packageGlobs.push(...parsed.packages);
|
|
1116
|
+
}
|
|
1117
|
+
} catch {
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
const rootPkgFile = join7(rootCwd, "package.json");
|
|
1121
|
+
if (existsSync7(rootPkgFile)) {
|
|
1122
|
+
try {
|
|
1123
|
+
const rootPkg = JSON.parse(readFileSync5(rootPkgFile, "utf8"));
|
|
1124
|
+
if (Array.isArray(rootPkg.workspaces)) {
|
|
1125
|
+
packageGlobs.push(...rootPkg.workspaces);
|
|
1126
|
+
} else if (Array.isArray(rootPkg.workspaces?.packages)) {
|
|
1127
|
+
packageGlobs.push(...rootPkg.workspaces.packages);
|
|
1128
|
+
}
|
|
1129
|
+
} catch {
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
const denoJsonFile = join7(rootCwd, "deno.json");
|
|
1133
|
+
if (existsSync7(denoJsonFile)) {
|
|
1134
|
+
try {
|
|
1135
|
+
const denoJson = JSON.parse(readFileSync5(denoJsonFile, "utf8"));
|
|
1136
|
+
if (Array.isArray(denoJson.workspace)) {
|
|
1137
|
+
packageGlobs.push(...denoJson.workspace);
|
|
1138
|
+
}
|
|
1139
|
+
} catch {
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
if (packageGlobs.length === 0) {
|
|
1143
|
+
return packages;
|
|
1144
|
+
}
|
|
1145
|
+
const seenDirs = /* @__PURE__ */ new Set();
|
|
1146
|
+
for (const glob of packageGlobs) {
|
|
1147
|
+
const cleanGlob = glob.trim().replace(/\/+$/, "");
|
|
1148
|
+
if (!cleanGlob || cleanGlob.startsWith("!")) continue;
|
|
1149
|
+
if (cleanGlob.endsWith("/*")) {
|
|
1150
|
+
const parentDir = join7(rootCwd, cleanGlob.slice(0, -2));
|
|
1151
|
+
if (!existsSync7(parentDir)) continue;
|
|
1152
|
+
try {
|
|
1153
|
+
const entries = readdirSync(parentDir);
|
|
1154
|
+
for (const entry of entries) {
|
|
1155
|
+
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
1156
|
+
const subDir = join7(parentDir, entry);
|
|
1157
|
+
try {
|
|
1158
|
+
if (statSync(subDir).isDirectory()) {
|
|
1159
|
+
addWorkspacePackage(rootCwd, subDir, packages, seenDirs);
|
|
1160
|
+
}
|
|
1161
|
+
} catch {
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
} catch {
|
|
1165
|
+
}
|
|
1166
|
+
} else {
|
|
1167
|
+
const directDir = join7(rootCwd, cleanGlob);
|
|
1168
|
+
if (existsSync7(directDir)) {
|
|
1169
|
+
try {
|
|
1170
|
+
if (statSync(directDir).isDirectory()) {
|
|
1171
|
+
addWorkspacePackage(rootCwd, directDir, packages, seenDirs);
|
|
1172
|
+
}
|
|
1173
|
+
} catch {
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
return packages;
|
|
1179
|
+
}
|
|
1180
|
+
function addWorkspacePackage(rootCwd, dir, out, seen) {
|
|
1181
|
+
const norm = resolve2(dir);
|
|
1182
|
+
if (seen.has(norm)) return;
|
|
1183
|
+
seen.add(norm);
|
|
1184
|
+
const manifestPath = join7(dir, "package.json");
|
|
1185
|
+
if (!existsSync7(manifestPath)) return;
|
|
1186
|
+
try {
|
|
1187
|
+
const content = readFileSync5(manifestPath, "utf8");
|
|
1188
|
+
const manifest = JSON.parse(content);
|
|
1189
|
+
out.push({
|
|
1190
|
+
name: manifest.name ?? relative(rootCwd, dir),
|
|
1191
|
+
relativeDir: relative(rootCwd, dir),
|
|
1192
|
+
absoluteDir: norm,
|
|
1193
|
+
manifestPath,
|
|
1194
|
+
manifest
|
|
1195
|
+
});
|
|
1196
|
+
} catch {
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
function findDeclaringWorkspacePackage(cwd, dependencyName, filterWorkspace) {
|
|
1200
|
+
const workspaces = findWorkspacePackages(cwd);
|
|
1201
|
+
if (workspaces.length === 0) return null;
|
|
1202
|
+
const matchesFilter = (wp) => {
|
|
1203
|
+
if (!filterWorkspace) return true;
|
|
1204
|
+
const filter = filterWorkspace.trim();
|
|
1205
|
+
return wp.name === filter || wp.relativeDir === filter || wp.relativeDir.replace(/\\/g, "/") === filter.replace(/\\/g, "/");
|
|
1206
|
+
};
|
|
1207
|
+
for (const wp of workspaces) {
|
|
1208
|
+
if (!matchesFilter(wp)) continue;
|
|
1209
|
+
const isDeclared = Boolean(wp.manifest.dependencies?.[dependencyName]) || Boolean(wp.manifest.devDependencies?.[dependencyName]) || Boolean(wp.manifest.peerDependencies?.[dependencyName]);
|
|
1210
|
+
if (isDeclared) {
|
|
1211
|
+
return wp;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return null;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1035
1217
|
// src/remediation/tools/check-inventory.ts
|
|
1036
1218
|
var checkInventoryTool = defineTool({
|
|
1037
1219
|
description: "Read the project's package.json and installed dependencies to list packages and exact versions. Must be called before checking version matches.",
|
|
@@ -1049,7 +1231,7 @@ var checkInventoryTool = defineTool({
|
|
|
1049
1231
|
}) => {
|
|
1050
1232
|
let pkgJson;
|
|
1051
1233
|
try {
|
|
1052
|
-
pkgJson = JSON.parse(
|
|
1234
|
+
pkgJson = JSON.parse(readFileSync6(join8(cwd, "package.json"), "utf8"));
|
|
1053
1235
|
} catch {
|
|
1054
1236
|
return {
|
|
1055
1237
|
packages: [],
|
|
@@ -1081,7 +1263,8 @@ var checkInventoryTool = defineTool({
|
|
|
1081
1263
|
}
|
|
1082
1264
|
const packages = [];
|
|
1083
1265
|
for (const [name, version] of installedVersions.entries()) {
|
|
1084
|
-
const
|
|
1266
|
+
const declaringWorkspace = findDeclaringWorkspacePackage(cwd, name, workspace);
|
|
1267
|
+
const isDirect = Boolean(pkgJson.dependencies?.[name]) || Boolean(pkgJson.devDependencies?.[name]) || Boolean(pkgJson.peerDependencies?.[name]) || Boolean(declaringWorkspace);
|
|
1085
1268
|
packages.push({
|
|
1086
1269
|
name,
|
|
1087
1270
|
version,
|
|
@@ -1204,173 +1387,8 @@ async function validatePatchArtifact(patchFilePath, options = {}) {
|
|
|
1204
1387
|
};
|
|
1205
1388
|
}
|
|
1206
1389
|
|
|
1207
|
-
// src/
|
|
1208
|
-
|
|
1209
|
-
const raw = options.llmProvider ?? process.env.AUTOREMEDIATOR_LLM_PROVIDER ?? "remote";
|
|
1210
|
-
if (raw !== "remote" && raw !== "local") {
|
|
1211
|
-
throw new Error(
|
|
1212
|
-
`Unsupported LLM provider "${raw}". Set AUTOREMEDIATOR_LLM_PROVIDER to "remote" or "local".`
|
|
1213
|
-
);
|
|
1214
|
-
}
|
|
1215
|
-
return raw;
|
|
1216
|
-
}
|
|
1217
|
-
function resolveModelName(provider, options = {}, routing = {}) {
|
|
1218
|
-
if (options.model) return options.model;
|
|
1219
|
-
if (process.env.AUTOREMEDIATOR_MODEL) return process.env.AUTOREMEDIATOR_MODEL;
|
|
1220
|
-
const cwd = options.cwd ?? process.cwd();
|
|
1221
|
-
const policy = loadPolicy(cwd, options.policy);
|
|
1222
|
-
const providerEnvModel = provider === "remote" ? process.env.AUTOREMEDIATOR_MODEL_REMOTE : process.env.AUTOREMEDIATOR_MODEL_LOCAL;
|
|
1223
|
-
if (providerEnvModel) return providerEnvModel;
|
|
1224
|
-
const policyPinnedModel = provider === "remote" ? policy.modelDefaults?.remote : policy.modelDefaults?.local;
|
|
1225
|
-
if (policyPinnedModel) return policyPinnedModel;
|
|
1226
|
-
const defaults = {
|
|
1227
|
-
remote: "remote-default",
|
|
1228
|
-
local: "local"
|
|
1229
|
-
};
|
|
1230
|
-
const routingEnabled = options.dynamicModelRouting ?? policy.dynamicModelRouting ?? false;
|
|
1231
|
-
const threshold = options.dynamicRoutingThresholdChars ?? policy.dynamicRoutingThresholdChars ?? 18e3;
|
|
1232
|
-
if (provider === "remote" && routingEnabled && typeof routing.inputChars === "number" && routing.inputChars >= threshold) {
|
|
1233
|
-
return process.env.AUTOREMEDIATOR_MODEL_REMOTE_LARGE ?? "remote-large";
|
|
1234
|
-
}
|
|
1235
|
-
return defaults[provider];
|
|
1236
|
-
}
|
|
1237
|
-
async function loadRemoteFactory() {
|
|
1238
|
-
const moduleName = process.env.AUTOREMEDIATOR_REMOTE_CLIENT_MODULE;
|
|
1239
|
-
const exportName = process.env.AUTOREMEDIATOR_REMOTE_CLIENT_FACTORY ?? "createRemoteClient";
|
|
1240
|
-
if (!moduleName) {
|
|
1241
|
-
throw new Error(
|
|
1242
|
-
"AUTOREMEDIATOR_REMOTE_CLIENT_MODULE is required for remote provider model loading."
|
|
1243
|
-
);
|
|
1244
|
-
}
|
|
1245
|
-
if (moduleName.startsWith("./") || moduleName.startsWith("../") || moduleName.startsWith("/") || moduleName.startsWith("file:")) {
|
|
1246
|
-
throw new Error(
|
|
1247
|
-
`AUTOREMEDIATOR_REMOTE_CLIENT_MODULE must be a package name, not a file path: ${moduleName}`
|
|
1248
|
-
);
|
|
1249
|
-
}
|
|
1250
|
-
const loaded = await import(moduleName);
|
|
1251
|
-
const factory = loaded[exportName];
|
|
1252
|
-
if (typeof factory !== "function") {
|
|
1253
|
-
throw new Error(
|
|
1254
|
-
`Remote client factory "${exportName}" was not found in module "${moduleName}".`
|
|
1255
|
-
);
|
|
1256
|
-
}
|
|
1257
|
-
return factory;
|
|
1258
|
-
}
|
|
1259
|
-
async function createModel(options = {}, routing = {}) {
|
|
1260
|
-
const provider = resolveProvider(options);
|
|
1261
|
-
if (provider === "local") {
|
|
1262
|
-
throw new Error(
|
|
1263
|
-
"Local provider does not create a language model. Use the deterministic pipeline path instead."
|
|
1264
|
-
);
|
|
1265
|
-
}
|
|
1266
|
-
const apiKey = process.env.AUTOREMEDIATOR_REMOTE_API_KEY;
|
|
1267
|
-
if (!apiKey) {
|
|
1268
|
-
throw new Error(
|
|
1269
|
-
"AUTOREMEDIATOR_REMOTE_API_KEY environment variable is required for remote provider."
|
|
1270
|
-
);
|
|
1271
|
-
}
|
|
1272
|
-
const modelName = resolveModelName(provider, options, routing);
|
|
1273
|
-
const createRemoteClient = await loadRemoteFactory();
|
|
1274
|
-
const remoteClient = createRemoteClient({ apiKey });
|
|
1275
|
-
return remoteClient(modelName);
|
|
1276
|
-
}
|
|
1277
|
-
function getPatchConfidenceThreshold(provider, safetyProfile = "relaxed", riskLevel = "medium", overrides) {
|
|
1278
|
-
const relaxed = {
|
|
1279
|
-
remote: { low: 0.65, medium: 0.7, high: 0.8 },
|
|
1280
|
-
local: { low: 0.65, medium: 0.7, high: 0.8 }
|
|
1281
|
-
};
|
|
1282
|
-
const strict = {
|
|
1283
|
-
remote: { low: 0.8, medium: 0.85, high: 0.9 },
|
|
1284
|
-
local: { low: 0.8, medium: 0.85, high: 0.9 }
|
|
1285
|
-
};
|
|
1286
|
-
const baseThreshold = safetyProfile === "strict" ? strict[provider][riskLevel] : relaxed[provider][riskLevel];
|
|
1287
|
-
const override = overrides?.[riskLevel];
|
|
1288
|
-
if (typeof override === "number" && Number.isFinite(override)) {
|
|
1289
|
-
return Math.max(0, Math.min(1, override));
|
|
1290
|
-
}
|
|
1291
|
-
return baseThreshold;
|
|
1292
|
-
}
|
|
1293
|
-
function estimateModelCostUsd(params) {
|
|
1294
|
-
const inputTokens = params.promptChars / 4;
|
|
1295
|
-
const outputTokens = params.completionChars / 4;
|
|
1296
|
-
const pricePerThousand = params.provider === "remote" ? { input: 6e-3, output: 0.02 } : { input: 0, output: 0 };
|
|
1297
|
-
return inputTokens / 1e3 * pricePerThousand.input + outputTokens / 1e3 * pricePerThousand.output;
|
|
1298
|
-
}
|
|
1299
|
-
function getNvdConfig() {
|
|
1300
|
-
return {
|
|
1301
|
-
apiKey: process.env.AUTOREMEDIATOR_NVD_API_KEY
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
function getGitHubToken() {
|
|
1305
|
-
return process.env.GITHUB_TOKEN;
|
|
1306
|
-
}
|
|
1307
|
-
function getIntelligenceSourceConfig() {
|
|
1308
|
-
return {
|
|
1309
|
-
gitLabAdvisoryApi: process.env.AUTOREMEDIATOR_GITLAB_ADVISORY_API ?? "https://advisories.gitlab.com/api/v1/advisories",
|
|
1310
|
-
certCcSearchUrl: process.env.AUTOREMEDIATOR_CERTCC_SEARCH_URL ?? "https://www.kb.cert.org/vuls/search",
|
|
1311
|
-
epssApi: process.env.AUTOREMEDIATOR_EPSS_API ?? "https://api.first.org/data/v1/epss",
|
|
1312
|
-
cveServicesApi: process.env.AUTOREMEDIATOR_CVE_SERVICES_API ?? "https://cveawg.mitre.org/api/cve",
|
|
1313
|
-
depsDevApi: process.env.AUTOREMEDIATOR_DEPSDEV_API ?? "https://api.deps.dev/v3",
|
|
1314
|
-
scorecardApi: process.env.AUTOREMEDIATOR_SCORECARD_API ?? "https://api.securityscorecards.dev",
|
|
1315
|
-
vendorAdvisoryFeeds: (process.env.AUTOREMEDIATOR_VENDOR_ADVISORY_FEEDS ?? "").split(",").map((v) => v.trim()).filter(Boolean),
|
|
1316
|
-
commercialFeeds: (process.env.AUTOREMEDIATOR_COMMERCIAL_FEEDS ?? "").split(",").map((v) => v.trim()).filter(Boolean),
|
|
1317
|
-
commercialFeedToken: process.env.AUTOREMEDIATOR_COMMERCIAL_FEED_TOKEN
|
|
1318
|
-
};
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
// src/platform/idempotency.ts
|
|
1322
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
1323
|
-
import { join as join8 } from "path";
|
|
1324
|
-
var DEFAULT_INDEX = {
|
|
1325
|
-
schemaVersion: "1.0",
|
|
1326
|
-
entries: {}
|
|
1327
|
-
};
|
|
1328
|
-
function indexFilePath(cwd) {
|
|
1329
|
-
return join8(cwd, ".autoremediator", "state", "idempotency.json");
|
|
1330
|
-
}
|
|
1331
|
-
function entryKey(idempotencyKey, cveId) {
|
|
1332
|
-
return `${idempotencyKey}::${cveId.toUpperCase()}`;
|
|
1333
|
-
}
|
|
1334
|
-
function loadIndex(cwd) {
|
|
1335
|
-
const filePath = indexFilePath(cwd);
|
|
1336
|
-
if (!existsSync7(filePath)) return DEFAULT_INDEX;
|
|
1337
|
-
try {
|
|
1338
|
-
const parsed = JSON.parse(readFileSync5(filePath, "utf8"));
|
|
1339
|
-
if (parsed && parsed.schemaVersion === "1.0" && parsed.entries) {
|
|
1340
|
-
return parsed;
|
|
1341
|
-
}
|
|
1342
|
-
return DEFAULT_INDEX;
|
|
1343
|
-
} catch {
|
|
1344
|
-
return DEFAULT_INDEX;
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
function saveIndex(cwd, index) {
|
|
1348
|
-
const filePath = indexFilePath(cwd);
|
|
1349
|
-
mkdirSync2(join8(cwd, ".autoremediator", "state"), { recursive: true });
|
|
1350
|
-
writeFileSync2(filePath, JSON.stringify(index, null, 2) + "\n", "utf8");
|
|
1351
|
-
}
|
|
1352
|
-
function readIdempotentReport(cwd, idempotencyKey, cveId) {
|
|
1353
|
-
const index = loadIndex(cwd);
|
|
1354
|
-
const key = entryKey(idempotencyKey, cveId);
|
|
1355
|
-
return index.entries[key]?.report;
|
|
1356
|
-
}
|
|
1357
|
-
function storeIdempotentReport(cwd, idempotencyKey, cveId, report) {
|
|
1358
|
-
const index = loadIndex(cwd);
|
|
1359
|
-
const key = entryKey(idempotencyKey, cveId);
|
|
1360
|
-
index.entries[key] = {
|
|
1361
|
-
key: idempotencyKey,
|
|
1362
|
-
cveId: cveId.toUpperCase(),
|
|
1363
|
-
report,
|
|
1364
|
-
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1365
|
-
};
|
|
1366
|
-
saveIndex(cwd, index);
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
// src/remediation/pipeline.ts
|
|
1370
|
-
import { generateText as generateText2 } from "ai";
|
|
1371
|
-
|
|
1372
|
-
// src/remediation/local/run.ts
|
|
1373
|
-
import semver5 from "semver";
|
|
1390
|
+
// src/remediation/evaluate-package.ts
|
|
1391
|
+
import semver2 from "semver";
|
|
1374
1392
|
|
|
1375
1393
|
// src/platform/http-client.ts
|
|
1376
1394
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
@@ -1552,16 +1570,744 @@ async function lookupCveOsv(cveId) {
|
|
|
1552
1570
|
return parseOsvVuln(vuln);
|
|
1553
1571
|
}
|
|
1554
1572
|
|
|
1555
|
-
// src/
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1573
|
+
// src/platform/config.ts
|
|
1574
|
+
function resolveProvider(options = {}) {
|
|
1575
|
+
const raw = options.llmProvider ?? process.env.AUTOREMEDIATOR_LLM_PROVIDER ?? "remote";
|
|
1576
|
+
if (raw !== "remote" && raw !== "local") {
|
|
1577
|
+
throw new Error(
|
|
1578
|
+
`Unsupported LLM provider "${raw}". Set AUTOREMEDIATOR_LLM_PROVIDER to "remote" or "local".`
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
return raw;
|
|
1582
|
+
}
|
|
1583
|
+
function resolveModelName(provider, options = {}, routing = {}) {
|
|
1584
|
+
if (options.model) return options.model;
|
|
1585
|
+
if (process.env.AUTOREMEDIATOR_MODEL) return process.env.AUTOREMEDIATOR_MODEL;
|
|
1586
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1587
|
+
const policy = loadPolicy(cwd, options.policy);
|
|
1588
|
+
const providerEnvModel = provider === "remote" ? process.env.AUTOREMEDIATOR_MODEL_REMOTE : process.env.AUTOREMEDIATOR_MODEL_LOCAL;
|
|
1589
|
+
if (providerEnvModel) return providerEnvModel;
|
|
1590
|
+
const policyPinnedModel = provider === "remote" ? policy.modelDefaults?.remote : policy.modelDefaults?.local;
|
|
1591
|
+
if (policyPinnedModel) return policyPinnedModel;
|
|
1592
|
+
const defaults = {
|
|
1593
|
+
remote: "remote-default",
|
|
1594
|
+
local: "local"
|
|
1595
|
+
};
|
|
1596
|
+
const routingEnabled = options.dynamicModelRouting ?? policy.dynamicModelRouting ?? false;
|
|
1597
|
+
const threshold = options.dynamicRoutingThresholdChars ?? policy.dynamicRoutingThresholdChars ?? 18e3;
|
|
1598
|
+
if (provider === "remote" && routingEnabled && typeof routing.inputChars === "number" && routing.inputChars >= threshold) {
|
|
1599
|
+
return process.env.AUTOREMEDIATOR_MODEL_REMOTE_LARGE ?? "remote-large";
|
|
1600
|
+
}
|
|
1601
|
+
return defaults[provider];
|
|
1602
|
+
}
|
|
1603
|
+
async function loadRemoteFactory() {
|
|
1604
|
+
const moduleName = process.env.AUTOREMEDIATOR_REMOTE_CLIENT_MODULE;
|
|
1605
|
+
const exportName = process.env.AUTOREMEDIATOR_REMOTE_CLIENT_FACTORY ?? "createRemoteClient";
|
|
1606
|
+
if (!moduleName) {
|
|
1607
|
+
throw new Error(
|
|
1608
|
+
"AUTOREMEDIATOR_REMOTE_CLIENT_MODULE is required for remote provider model loading."
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
if (moduleName.startsWith("./") || moduleName.startsWith("../") || moduleName.startsWith("/") || moduleName.startsWith("file:")) {
|
|
1612
|
+
throw new Error(
|
|
1613
|
+
`AUTOREMEDIATOR_REMOTE_CLIENT_MODULE must be a package name, not a file path: ${moduleName}`
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
const loaded = await import(moduleName);
|
|
1617
|
+
const factory = loaded[exportName];
|
|
1618
|
+
if (typeof factory !== "function") {
|
|
1619
|
+
throw new Error(
|
|
1620
|
+
`Remote client factory "${exportName}" was not found in module "${moduleName}".`
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
return factory;
|
|
1624
|
+
}
|
|
1625
|
+
async function createModel(options = {}, routing = {}) {
|
|
1626
|
+
const provider = resolveProvider(options);
|
|
1627
|
+
if (provider === "local") {
|
|
1628
|
+
throw new Error(
|
|
1629
|
+
"Local provider does not create a language model. Use the deterministic pipeline path instead."
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
const apiKey = process.env.AUTOREMEDIATOR_REMOTE_API_KEY;
|
|
1633
|
+
if (!apiKey) {
|
|
1634
|
+
throw new Error(
|
|
1635
|
+
"AUTOREMEDIATOR_REMOTE_API_KEY environment variable is required for remote provider."
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
const modelName = resolveModelName(provider, options, routing);
|
|
1639
|
+
const createRemoteClient = await loadRemoteFactory();
|
|
1640
|
+
const remoteClient = createRemoteClient({ apiKey });
|
|
1641
|
+
return remoteClient(modelName);
|
|
1642
|
+
}
|
|
1643
|
+
function getPatchConfidenceThreshold(provider, safetyProfile = "relaxed", riskLevel = "medium", overrides) {
|
|
1644
|
+
const relaxed = {
|
|
1645
|
+
remote: { low: 0.65, medium: 0.7, high: 0.8 },
|
|
1646
|
+
local: { low: 0.65, medium: 0.7, high: 0.8 }
|
|
1647
|
+
};
|
|
1648
|
+
const strict = {
|
|
1649
|
+
remote: { low: 0.8, medium: 0.85, high: 0.9 },
|
|
1650
|
+
local: { low: 0.8, medium: 0.85, high: 0.9 }
|
|
1651
|
+
};
|
|
1652
|
+
const baseThreshold = safetyProfile === "strict" ? strict[provider][riskLevel] : relaxed[provider][riskLevel];
|
|
1653
|
+
const override = overrides?.[riskLevel];
|
|
1654
|
+
if (typeof override === "number" && Number.isFinite(override)) {
|
|
1655
|
+
return Math.max(0, Math.min(1, override));
|
|
1656
|
+
}
|
|
1657
|
+
return baseThreshold;
|
|
1658
|
+
}
|
|
1659
|
+
function estimateModelCostUsd(params) {
|
|
1660
|
+
const inputTokens = params.promptChars / 4;
|
|
1661
|
+
const outputTokens = params.completionChars / 4;
|
|
1662
|
+
const pricePerThousand = params.provider === "remote" ? { input: 6e-3, output: 0.02 } : { input: 0, output: 0 };
|
|
1663
|
+
return inputTokens / 1e3 * pricePerThousand.input + outputTokens / 1e3 * pricePerThousand.output;
|
|
1664
|
+
}
|
|
1665
|
+
function getNvdConfig() {
|
|
1666
|
+
return {
|
|
1667
|
+
apiKey: process.env.AUTOREMEDIATOR_NVD_API_KEY
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
function getGitHubToken() {
|
|
1671
|
+
return process.env.GITHUB_TOKEN;
|
|
1672
|
+
}
|
|
1673
|
+
function getIntelligenceSourceConfig() {
|
|
1674
|
+
return {
|
|
1675
|
+
gitLabAdvisoryApi: process.env.AUTOREMEDIATOR_GITLAB_ADVISORY_API ?? "https://advisories.gitlab.com/api/v1/advisories",
|
|
1676
|
+
certCcSearchUrl: process.env.AUTOREMEDIATOR_CERTCC_SEARCH_URL ?? "https://www.kb.cert.org/vuls/search",
|
|
1677
|
+
epssApi: process.env.AUTOREMEDIATOR_EPSS_API ?? "https://api.first.org/data/v1/epss",
|
|
1678
|
+
cveServicesApi: process.env.AUTOREMEDIATOR_CVE_SERVICES_API ?? "https://cveawg.mitre.org/api/cve",
|
|
1679
|
+
depsDevApi: process.env.AUTOREMEDIATOR_DEPSDEV_API ?? "https://api.deps.dev/v3",
|
|
1680
|
+
scorecardApi: process.env.AUTOREMEDIATOR_SCORECARD_API ?? "https://api.securityscorecards.dev",
|
|
1681
|
+
vendorAdvisoryFeeds: (process.env.AUTOREMEDIATOR_VENDOR_ADVISORY_FEEDS ?? "").split(",").map((v) => v.trim()).filter(Boolean),
|
|
1682
|
+
commercialFeeds: (process.env.AUTOREMEDIATOR_COMMERCIAL_FEEDS ?? "").split(",").map((v) => v.trim()).filter(Boolean),
|
|
1683
|
+
commercialFeedToken: process.env.AUTOREMEDIATOR_COMMERCIAL_FEED_TOKEN
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// src/intelligence/sources/epss.ts
|
|
1688
|
+
async function fetchEpss(cveId) {
|
|
1689
|
+
const { epssApi } = getIntelligenceSourceConfig();
|
|
1690
|
+
if (!epssApi) return void 0;
|
|
1691
|
+
try {
|
|
1692
|
+
const url = new URL(epssApi);
|
|
1693
|
+
url.searchParams.set("cve", cveId);
|
|
1694
|
+
const res = await httpClient({ url: url.toString() });
|
|
1695
|
+
if (!res.ok) return void 0;
|
|
1696
|
+
const body = res.data;
|
|
1697
|
+
return body.data?.[0];
|
|
1698
|
+
} catch {
|
|
1699
|
+
return void 0;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
async function enrichWithEpss(details) {
|
|
1703
|
+
const row = await fetchEpss(details.id);
|
|
1704
|
+
if (!row) return details;
|
|
1705
|
+
const score = Number.parseFloat(row.epss);
|
|
1706
|
+
const percentile = Number.parseFloat(row.percentile);
|
|
1707
|
+
if (!Number.isFinite(score) || !Number.isFinite(percentile)) {
|
|
1708
|
+
return details;
|
|
1709
|
+
}
|
|
1710
|
+
details.epss = {
|
|
1711
|
+
score,
|
|
1712
|
+
percentile,
|
|
1713
|
+
date: row.date
|
|
1714
|
+
};
|
|
1715
|
+
return details;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// src/intelligence/sources/cisa-kev.ts
|
|
1719
|
+
var CISA_KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json";
|
|
1720
|
+
async function fetchCisaKevFeed() {
|
|
1721
|
+
try {
|
|
1722
|
+
const res = await httpClient({ url: CISA_KEV_URL });
|
|
1723
|
+
if (!res.ok) return void 0;
|
|
1724
|
+
return res.data;
|
|
1725
|
+
} catch {
|
|
1726
|
+
return void 0;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
function findKevEntry(feed, cveId) {
|
|
1730
|
+
if (!feed?.vulnerabilities?.length) return void 0;
|
|
1731
|
+
const normalized = cveId.toUpperCase();
|
|
1732
|
+
return feed.vulnerabilities.find((v) => v.cveID.toUpperCase() === normalized);
|
|
1733
|
+
}
|
|
1734
|
+
async function enrichWithCisaKev(details) {
|
|
1735
|
+
const feed = await fetchCisaKevFeed();
|
|
1736
|
+
const entry = findKevEntry(feed, details.id);
|
|
1737
|
+
if (!entry) return details;
|
|
1738
|
+
details.kev = {
|
|
1739
|
+
knownExploited: true,
|
|
1740
|
+
dateAdded: entry.dateAdded,
|
|
1741
|
+
dueDate: entry.dueDate,
|
|
1742
|
+
requiredAction: entry.requiredAction,
|
|
1743
|
+
knownRansomwareCampaignUse: entry.knownRansomwareCampaignUse
|
|
1744
|
+
};
|
|
1745
|
+
if (!details.references.includes(CISA_KEV_URL)) {
|
|
1746
|
+
details.references.push(CISA_KEV_URL);
|
|
1747
|
+
}
|
|
1748
|
+
return details;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// src/intelligence/sources/registry.ts
|
|
1752
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
1753
|
+
import { join as join9 } from "path";
|
|
1754
|
+
import { execa as execa4 } from "execa";
|
|
1755
|
+
import semver from "semver";
|
|
1756
|
+
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
1757
|
+
async function fetchPackageVersions(packageName) {
|
|
1758
|
+
const url = `${NPM_REGISTRY}/${encodeURIComponent(packageName)}`;
|
|
1759
|
+
try {
|
|
1760
|
+
const res = await httpClient({ url });
|
|
1761
|
+
if (res.status === 404) return [];
|
|
1762
|
+
if (!res.ok) {
|
|
1763
|
+
throw new Error(`npm registry error ${res.status} for "${packageName}": ${res.text}`);
|
|
1764
|
+
}
|
|
1765
|
+
const data = res.data;
|
|
1766
|
+
return Object.keys(data.versions);
|
|
1767
|
+
} catch (err) {
|
|
1768
|
+
if (err instanceof Error && err.message.includes("registry error")) {
|
|
1769
|
+
throw err;
|
|
1770
|
+
}
|
|
1771
|
+
return [];
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
async function findSafeUpgradeVersion(packageName, installedVersion, firstPatchedVersion, vulnerableRange) {
|
|
1775
|
+
const resolution = await resolveSafeUpgradeVersion(
|
|
1776
|
+
packageName,
|
|
1777
|
+
installedVersion,
|
|
1778
|
+
firstPatchedVersion,
|
|
1779
|
+
vulnerableRange
|
|
1780
|
+
);
|
|
1781
|
+
return resolution.safeVersion;
|
|
1782
|
+
}
|
|
1783
|
+
async function resolveSafeUpgradeVersion(packageName, installedVersion, firstPatchedVersion, vulnerableRange) {
|
|
1784
|
+
const versions = await fetchPackageVersions(packageName);
|
|
1785
|
+
if (!versions.length) {
|
|
1786
|
+
return {
|
|
1787
|
+
candidates: {},
|
|
1788
|
+
majorOnlyFixAvailable: false
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
const installed = semver.parse(installedVersion);
|
|
1792
|
+
const candidates = versions.filter((v) => semver.valid(v) && semver.gte(v, firstPatchedVersion)).filter((v) => {
|
|
1793
|
+
if (!vulnerableRange) return true;
|
|
1794
|
+
try {
|
|
1795
|
+
return !semver.satisfies(v, vulnerableRange, { includePrerelease: false });
|
|
1796
|
+
} catch {
|
|
1797
|
+
return true;
|
|
1798
|
+
}
|
|
1799
|
+
}).sort(semver.compare);
|
|
1800
|
+
if (!candidates.length) {
|
|
1801
|
+
return {
|
|
1802
|
+
candidates: {},
|
|
1803
|
+
majorOnlyFixAvailable: false
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
const categorizedCandidates = {};
|
|
1807
|
+
for (const candidate of candidates) {
|
|
1808
|
+
const level = classifyUpgradeLevel(installedVersion, candidate);
|
|
1809
|
+
if (!level) continue;
|
|
1810
|
+
if (!categorizedCandidates[level]) {
|
|
1811
|
+
categorizedCandidates[level] = candidate;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
const safeVersion = categorizedCandidates.patch ?? categorizedCandidates.minor ?? categorizedCandidates.major;
|
|
1815
|
+
if (!safeVersion) {
|
|
1816
|
+
return {
|
|
1817
|
+
candidates: categorizedCandidates,
|
|
1818
|
+
majorOnlyFixAvailable: false
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
const upgradeLevel = classifyUpgradeLevel(installedVersion, safeVersion);
|
|
1822
|
+
const majorOnlyFixAvailable = !categorizedCandidates.patch && !categorizedCandidates.minor && Boolean(categorizedCandidates.major);
|
|
1823
|
+
if (!installed || !upgradeLevel) {
|
|
1824
|
+
return {
|
|
1825
|
+
safeVersion,
|
|
1826
|
+
upgradeLevel,
|
|
1827
|
+
candidates: categorizedCandidates,
|
|
1828
|
+
majorOnlyFixAvailable
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
return {
|
|
1832
|
+
safeVersion,
|
|
1833
|
+
upgradeLevel,
|
|
1834
|
+
candidates: categorizedCandidates,
|
|
1835
|
+
majorOnlyFixAvailable
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
function classifyUpgradeLevel(installedVersion, candidateVersion) {
|
|
1839
|
+
const installed = semver.parse(installedVersion);
|
|
1840
|
+
const candidate = semver.parse(candidateVersion);
|
|
1841
|
+
if (!installed || !candidate) return void 0;
|
|
1842
|
+
if (candidate.major > installed.major) return "major";
|
|
1843
|
+
if (candidate.minor > installed.minor) return "minor";
|
|
1844
|
+
if (candidate.patch > installed.patch || candidate.version === installed.version) {
|
|
1845
|
+
return "patch";
|
|
1846
|
+
}
|
|
1847
|
+
return void 0;
|
|
1848
|
+
}
|
|
1849
|
+
async function queryOutdatedPackages(cwd, options = {}) {
|
|
1850
|
+
const pm = options.packageManager ?? detectPackageManager(cwd);
|
|
1851
|
+
let stdout;
|
|
1852
|
+
try {
|
|
1853
|
+
const args = pm === "pnpm" ? ["outdated", "--json"] : ["outdated", "--json"];
|
|
1854
|
+
const yarnArgs = pm === "yarn" ? ["outdated", "--json"] : void 0;
|
|
1855
|
+
const finalArgs = yarnArgs ?? args;
|
|
1856
|
+
const result2 = await execa4(pm, finalArgs, {
|
|
1857
|
+
cwd,
|
|
1858
|
+
reject: false,
|
|
1859
|
+
stdio: "pipe"
|
|
1860
|
+
});
|
|
1861
|
+
if (result2.exitCode !== 0 && result2.exitCode !== 1) {
|
|
1862
|
+
const stderr = typeof result2.stderr === "string" ? result2.stderr : "";
|
|
1863
|
+
if (!result2.stdout) {
|
|
1864
|
+
throw new Error(
|
|
1865
|
+
`${pm} outdated --json failed (exit ${result2.exitCode}): ${stderr.slice(0, 500)}`
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
stdout = typeof result2.stdout === "string" ? result2.stdout : "";
|
|
1870
|
+
} catch (err) {
|
|
1871
|
+
if (err instanceof Error && !err.message.includes("outdated --json failed")) {
|
|
1872
|
+
throw new Error(`Failed to query outdated packages: ${err.message}`);
|
|
1873
|
+
}
|
|
1874
|
+
throw err;
|
|
1875
|
+
}
|
|
1876
|
+
if (!stdout.trim()) {
|
|
1877
|
+
return /* @__PURE__ */ new Map();
|
|
1878
|
+
}
|
|
1879
|
+
const rawEntries = {};
|
|
1880
|
+
if (pm === "yarn") {
|
|
1881
|
+
for (const line of stdout.split("\n")) {
|
|
1882
|
+
const trimmed = line.trim();
|
|
1883
|
+
if (!trimmed) continue;
|
|
1884
|
+
try {
|
|
1885
|
+
const parsed = JSON.parse(trimmed);
|
|
1886
|
+
if (parsed.type === "table" && parsed.data) {
|
|
1887
|
+
const tableData = parsed.data;
|
|
1888
|
+
const head = tableData.head ?? [];
|
|
1889
|
+
const currentIdx = head.indexOf("Current");
|
|
1890
|
+
const wantedIdx = head.indexOf("Wanted");
|
|
1891
|
+
const latestIdx = head.indexOf("Latest");
|
|
1892
|
+
for (const row of tableData.body ?? []) {
|
|
1893
|
+
const name = row[0];
|
|
1894
|
+
if (!name) continue;
|
|
1895
|
+
rawEntries[name] = {
|
|
1896
|
+
current: row[currentIdx] ?? "",
|
|
1897
|
+
wanted: row[wantedIdx] ?? "",
|
|
1898
|
+
latest: row[latestIdx] ?? "",
|
|
1899
|
+
type: row[head.indexOf("Package Type")] ?? "dependencies"
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
} catch {
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
} else {
|
|
1907
|
+
try {
|
|
1908
|
+
const parsed = JSON.parse(stdout);
|
|
1909
|
+
if (Array.isArray(parsed)) {
|
|
1910
|
+
for (const entry of parsed) {
|
|
1911
|
+
const name = entry.name ?? entry.packageName;
|
|
1912
|
+
if (!name) continue;
|
|
1913
|
+
rawEntries[name] = {
|
|
1914
|
+
current: entry.current,
|
|
1915
|
+
wanted: entry.wanted,
|
|
1916
|
+
latest: entry.latest,
|
|
1917
|
+
type: entry.dependencyType
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
} else {
|
|
1921
|
+
Object.assign(rawEntries, parsed);
|
|
1922
|
+
}
|
|
1923
|
+
} catch {
|
|
1924
|
+
throw new Error(`Could not parse ${pm} outdated --json output.`);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
let directDeps;
|
|
1928
|
+
try {
|
|
1929
|
+
const pkgRaw = JSON.parse(readFileSync7(join9(cwd, "package.json"), "utf8"));
|
|
1930
|
+
directDeps = /* @__PURE__ */ new Set([
|
|
1931
|
+
...Object.keys(pkgRaw.dependencies ?? {}),
|
|
1932
|
+
...Object.keys(pkgRaw.devDependencies ?? {})
|
|
1933
|
+
]);
|
|
1934
|
+
} catch {
|
|
1935
|
+
directDeps = /* @__PURE__ */ new Set();
|
|
1936
|
+
}
|
|
1937
|
+
const result = /* @__PURE__ */ new Map();
|
|
1938
|
+
for (const [name, entry] of Object.entries(rawEntries)) {
|
|
1939
|
+
const currentVersion = entry.current ?? "";
|
|
1940
|
+
const wantedVersion = entry.wanted ?? currentVersion;
|
|
1941
|
+
const latestVersion = entry.latest ?? currentVersion;
|
|
1942
|
+
if (!currentVersion || !latestVersion) continue;
|
|
1943
|
+
const dependencyScope = directDeps.has(name) ? "direct" : "transitive";
|
|
1944
|
+
if (!options.includeTransitive && dependencyScope === "transitive") continue;
|
|
1945
|
+
const isMajorBump = semver.valid(currentVersion) !== null && semver.valid(latestVersion) !== null && semver.major(latestVersion) > semver.major(currentVersion);
|
|
1946
|
+
result.set(name, {
|
|
1947
|
+
currentVersion,
|
|
1948
|
+
wantedVersion,
|
|
1949
|
+
latestVersion,
|
|
1950
|
+
isMajorBump,
|
|
1951
|
+
dependencyScope
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
return result;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
// src/remediation/evaluate-package.ts
|
|
1958
|
+
async function evaluatePackage(packageName, options = {}) {
|
|
1959
|
+
const version = options.version?.trim();
|
|
1960
|
+
const enrich = options.enrichIntelligence !== false;
|
|
1961
|
+
let versions = [];
|
|
1962
|
+
let latestVersion;
|
|
1963
|
+
try {
|
|
1964
|
+
versions = await fetchPackageVersions(packageName);
|
|
1965
|
+
if (versions.length > 0) {
|
|
1966
|
+
const valid = versions.filter((v) => semver2.valid(v)).sort((a, b) => semver2.compare(a, b));
|
|
1967
|
+
latestVersion = valid[valid.length - 1];
|
|
1968
|
+
}
|
|
1969
|
+
} catch {
|
|
1970
|
+
}
|
|
1971
|
+
let rawVulns = [];
|
|
1972
|
+
try {
|
|
1973
|
+
rawVulns = await queryOsvPackage(packageName, version);
|
|
1974
|
+
} catch {
|
|
1975
|
+
rawVulns = [];
|
|
1976
|
+
}
|
|
1977
|
+
let kevFeed = null;
|
|
1978
|
+
if (enrich) {
|
|
1979
|
+
try {
|
|
1980
|
+
kevFeed = await fetchCisaKevFeed();
|
|
1981
|
+
} catch {
|
|
1982
|
+
kevFeed = null;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
const entries = [];
|
|
1986
|
+
for (const raw of rawVulns) {
|
|
1987
|
+
const cveDetails = parseOsvVuln(raw);
|
|
1988
|
+
const affected = cveDetails.affectedPackages.find(
|
|
1989
|
+
(p) => p.name.toLowerCase() === packageName.toLowerCase()
|
|
1990
|
+
);
|
|
1991
|
+
if (version && affected?.vulnerableRange) {
|
|
1992
|
+
const cleanVer = semver2.clean(version) || version;
|
|
1993
|
+
if (semver2.valid(cleanVer) && !semver2.satisfies(cleanVer, affected.vulnerableRange)) {
|
|
1994
|
+
continue;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
let inCisaKev = false;
|
|
1998
|
+
let epssScore;
|
|
1999
|
+
let epssPercentile;
|
|
2000
|
+
if (enrich && cveDetails.id.startsWith("CVE-")) {
|
|
2001
|
+
if (kevFeed) {
|
|
2002
|
+
inCisaKev = Boolean(findKevEntry(kevFeed, cveDetails.id));
|
|
2003
|
+
}
|
|
2004
|
+
try {
|
|
2005
|
+
const epss = await fetchEpss(cveDetails.id);
|
|
2006
|
+
if (epss) {
|
|
2007
|
+
epssScore = Number.parseFloat(epss.epss);
|
|
2008
|
+
epssPercentile = Number.parseFloat(epss.percentile);
|
|
2009
|
+
}
|
|
2010
|
+
} catch {
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
let safeUpgradeVersion;
|
|
2014
|
+
if (affected?.firstPatchedVersion) {
|
|
2015
|
+
try {
|
|
2016
|
+
const safe = await findSafeUpgradeVersion(
|
|
2017
|
+
packageName,
|
|
2018
|
+
version || "0.0.0",
|
|
2019
|
+
affected.firstPatchedVersion
|
|
2020
|
+
);
|
|
2021
|
+
safeUpgradeVersion = safe ?? affected.firstPatchedVersion;
|
|
2022
|
+
} catch {
|
|
2023
|
+
safeUpgradeVersion = affected.firstPatchedVersion;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
entries.push({
|
|
2027
|
+
cveId: cveDetails.id,
|
|
2028
|
+
severity: cveDetails.severity,
|
|
2029
|
+
summary: cveDetails.summary,
|
|
2030
|
+
vulnerableRange: affected?.vulnerableRange,
|
|
2031
|
+
firstPatchedVersion: affected?.firstPatchedVersion,
|
|
2032
|
+
safeUpgradeVersion,
|
|
2033
|
+
epssScore,
|
|
2034
|
+
epssPercentile,
|
|
2035
|
+
inCisaKev,
|
|
2036
|
+
references: cveDetails.references
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
const isVulnerable = entries.length > 0;
|
|
2040
|
+
let verdict = "safe";
|
|
2041
|
+
if (isVulnerable) {
|
|
2042
|
+
const hasKevOrHighEpss = entries.some((e) => e.inCisaKev || e.epssScore && e.epssScore > 0.5);
|
|
2043
|
+
const hasHighOrCritical = entries.some(
|
|
2044
|
+
(e) => e.severity === "CRITICAL" || e.severity === "HIGH"
|
|
2045
|
+
);
|
|
2046
|
+
if (hasKevOrHighEpss) {
|
|
2047
|
+
verdict = "actively-exploited";
|
|
2048
|
+
} else if (hasHighOrCritical) {
|
|
2049
|
+
verdict = "vulnerable";
|
|
2050
|
+
} else {
|
|
2051
|
+
verdict = "caution";
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
let recommendedVersion;
|
|
2055
|
+
if (isVulnerable) {
|
|
2056
|
+
const upgradeCandidates = entries.map((e) => e.safeUpgradeVersion).filter((v) => Boolean(v && semver2.valid(v))).sort((a, b) => semver2.compare(a, b));
|
|
2057
|
+
recommendedVersion = upgradeCandidates[upgradeCandidates.length - 1] || latestVersion;
|
|
2058
|
+
} else {
|
|
2059
|
+
recommendedVersion = version || latestVersion;
|
|
2060
|
+
}
|
|
2061
|
+
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.`;
|
|
2062
|
+
return {
|
|
2063
|
+
schemaVersion: "1.0",
|
|
2064
|
+
packageName,
|
|
2065
|
+
evaluatedVersion: version,
|
|
2066
|
+
isVulnerable,
|
|
2067
|
+
verdict,
|
|
2068
|
+
summary,
|
|
2069
|
+
vulnerabilities: entries,
|
|
2070
|
+
recommendedVersion,
|
|
2071
|
+
latestVersion,
|
|
2072
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// src/api/patches/reconcile.ts
|
|
2077
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2078
|
+
import { readFile as readFile3, rm, writeFile } from "fs/promises";
|
|
2079
|
+
import { join as join10 } from "path";
|
|
2080
|
+
import { execa as execa5 } from "execa";
|
|
2081
|
+
async function reconcilePatchArtifacts(options = {}) {
|
|
2082
|
+
const cwd = options.cwd ?? process.cwd();
|
|
2083
|
+
const pm = options.packageManager ?? detectPackageManager(cwd);
|
|
2084
|
+
const summaries = await listPatchArtifacts(options);
|
|
2085
|
+
const results = [];
|
|
2086
|
+
for (const summary of summaries) {
|
|
2087
|
+
const inspection = await inspectPatchArtifact(summary.patchFilePath, options);
|
|
2088
|
+
let packageName = inspection.packageName;
|
|
2089
|
+
let vulnerableVersion = inspection.vulnerableVersion;
|
|
2090
|
+
const cveId = inspection.cveId;
|
|
2091
|
+
if (!packageName && summary.patchFileName) {
|
|
2092
|
+
const basename = summary.patchFileName.replace(/\.patch$/, "");
|
|
2093
|
+
const plusIdx = basename.lastIndexOf("+");
|
|
2094
|
+
if (plusIdx > 0) {
|
|
2095
|
+
let rawPkg = basename.slice(0, plusIdx);
|
|
2096
|
+
vulnerableVersion = basename.slice(plusIdx + 1);
|
|
2097
|
+
if (rawPkg.includes("+")) {
|
|
2098
|
+
rawPkg = "@" + rawPkg.replace("+", "/");
|
|
2099
|
+
}
|
|
2100
|
+
packageName = rawPkg;
|
|
2101
|
+
} else {
|
|
2102
|
+
packageName = basename;
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
if (!packageName) {
|
|
2106
|
+
results.push({
|
|
2107
|
+
patchFilePath: summary.patchFilePath,
|
|
2108
|
+
packageName: "unknown",
|
|
2109
|
+
cveId,
|
|
2110
|
+
status: "patch-retained",
|
|
2111
|
+
reason: "Cannot determine package name from patch artifact."
|
|
2112
|
+
});
|
|
2113
|
+
continue;
|
|
2114
|
+
}
|
|
2115
|
+
let safeUpgradeVersion;
|
|
2116
|
+
try {
|
|
2117
|
+
const evaluation = await evaluatePackage(packageName, {
|
|
2118
|
+
version: vulnerableVersion,
|
|
2119
|
+
enrichIntelligence: false
|
|
2120
|
+
});
|
|
2121
|
+
if (cveId) {
|
|
2122
|
+
const vuln = evaluation.vulnerabilities.find(
|
|
2123
|
+
(v) => v.cveId.toLowerCase() === cveId.toLowerCase()
|
|
2124
|
+
);
|
|
2125
|
+
if (vuln?.safeUpgradeVersion) {
|
|
2126
|
+
safeUpgradeVersion = vuln.safeUpgradeVersion;
|
|
2127
|
+
}
|
|
2128
|
+
} else {
|
|
2129
|
+
safeUpgradeVersion = evaluation.vulnerabilities.find(
|
|
2130
|
+
(v) => v.safeUpgradeVersion
|
|
2131
|
+
)?.safeUpgradeVersion;
|
|
2132
|
+
}
|
|
2133
|
+
} catch {
|
|
2134
|
+
}
|
|
2135
|
+
if (!safeUpgradeVersion) {
|
|
2136
|
+
results.push({
|
|
2137
|
+
patchFilePath: summary.patchFilePath,
|
|
2138
|
+
packageName,
|
|
2139
|
+
cveId,
|
|
2140
|
+
installedVersion: vulnerableVersion,
|
|
2141
|
+
status: "patch-retained",
|
|
2142
|
+
reason: "No safe upstream release exists yet; patch remains necessary."
|
|
2143
|
+
});
|
|
2144
|
+
continue;
|
|
2145
|
+
}
|
|
2146
|
+
if (!options.autoApply) {
|
|
2147
|
+
results.push({
|
|
2148
|
+
patchFilePath: summary.patchFilePath,
|
|
2149
|
+
packageName,
|
|
2150
|
+
cveId,
|
|
2151
|
+
installedVersion: vulnerableVersion,
|
|
2152
|
+
safeUpstreamVersion: safeUpgradeVersion,
|
|
2153
|
+
status: "safe-version-available",
|
|
2154
|
+
reason: `Safe upstream version ${safeUpgradeVersion} is available. Re-run with autoApply: true or --auto-apply to upgrade.`
|
|
2155
|
+
});
|
|
2156
|
+
continue;
|
|
2157
|
+
}
|
|
2158
|
+
try {
|
|
2159
|
+
const pkgJsonPath = join10(cwd, "package.json");
|
|
2160
|
+
if (existsSync8(pkgJsonPath)) {
|
|
2161
|
+
const pkgJson = JSON.parse(await readFile3(pkgJsonPath, "utf8"));
|
|
2162
|
+
let updated = false;
|
|
2163
|
+
const depSections = [
|
|
2164
|
+
"dependencies",
|
|
2165
|
+
"devDependencies",
|
|
2166
|
+
"peerDependencies",
|
|
2167
|
+
"optionalDependencies"
|
|
2168
|
+
];
|
|
2169
|
+
for (const depSection of depSections) {
|
|
2170
|
+
if (pkgJson[depSection] && pkgJson[depSection][packageName]) {
|
|
2171
|
+
pkgJson[depSection][packageName] = `^${safeUpgradeVersion}`;
|
|
2172
|
+
updated = true;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
if (pkgJson.pnpm?.patchedDependencies) {
|
|
2176
|
+
for (const key of Object.keys(pkgJson.pnpm.patchedDependencies)) {
|
|
2177
|
+
if (key.startsWith(`${packageName}@`)) {
|
|
2178
|
+
delete pkgJson.pnpm.patchedDependencies[key];
|
|
2179
|
+
updated = true;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
if (updated) {
|
|
2184
|
+
await writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
await rm(summary.patchFilePath, { force: true });
|
|
2188
|
+
if (inspection.manifestFilePath) {
|
|
2189
|
+
await rm(inspection.manifestFilePath, { force: true });
|
|
2190
|
+
}
|
|
2191
|
+
const installCmd = resolveInstallCommand(pm);
|
|
2192
|
+
await execa5(installCmd[0], installCmd.slice(1), { cwd, reject: false });
|
|
2193
|
+
if (options.runTests) {
|
|
2194
|
+
const testCmd = resolveTestCommand(pm);
|
|
2195
|
+
await execa5(testCmd[0], testCmd.slice(1), { cwd, reject: false });
|
|
2196
|
+
}
|
|
2197
|
+
results.push({
|
|
2198
|
+
patchFilePath: summary.patchFilePath,
|
|
2199
|
+
packageName,
|
|
2200
|
+
cveId,
|
|
2201
|
+
installedVersion: vulnerableVersion,
|
|
2202
|
+
safeUpstreamVersion: safeUpgradeVersion,
|
|
2203
|
+
status: "upgraded",
|
|
2204
|
+
reason: `Successfully upgraded to ${safeUpgradeVersion} and retired patch artifact.`
|
|
2205
|
+
});
|
|
2206
|
+
} catch (err) {
|
|
2207
|
+
results.push({
|
|
2208
|
+
patchFilePath: summary.patchFilePath,
|
|
2209
|
+
packageName,
|
|
2210
|
+
cveId,
|
|
2211
|
+
installedVersion: vulnerableVersion,
|
|
2212
|
+
safeUpstreamVersion: safeUpgradeVersion,
|
|
2213
|
+
status: "error",
|
|
2214
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
const reconciledCount = results.filter((r) => r.status === "upgraded").length;
|
|
2219
|
+
const retainedCount = results.filter(
|
|
2220
|
+
(r) => r.status === "patch-retained" || r.status === "safe-version-available"
|
|
2221
|
+
).length;
|
|
2222
|
+
return {
|
|
2223
|
+
reconciled: results,
|
|
2224
|
+
reconciledCount,
|
|
2225
|
+
retainedCount
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
// src/platform/idempotency.ts
|
|
2230
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "fs";
|
|
2231
|
+
import { join as join11 } from "path";
|
|
2232
|
+
var DEFAULT_INDEX = {
|
|
2233
|
+
schemaVersion: "1.0",
|
|
2234
|
+
entries: {}
|
|
2235
|
+
};
|
|
2236
|
+
function indexFilePath(cwd) {
|
|
2237
|
+
return join11(cwd, ".autoremediator", "state", "idempotency.json");
|
|
2238
|
+
}
|
|
2239
|
+
function entryKey(idempotencyKey, cveId) {
|
|
2240
|
+
return `${idempotencyKey}::${cveId.toUpperCase()}`;
|
|
2241
|
+
}
|
|
2242
|
+
function loadIndex(cwd) {
|
|
2243
|
+
const filePath = indexFilePath(cwd);
|
|
2244
|
+
if (!existsSync9(filePath)) return DEFAULT_INDEX;
|
|
2245
|
+
try {
|
|
2246
|
+
const parsed = JSON.parse(readFileSync8(filePath, "utf8"));
|
|
2247
|
+
if (parsed && parsed.schemaVersion === "1.0" && parsed.entries) {
|
|
2248
|
+
return parsed;
|
|
2249
|
+
}
|
|
2250
|
+
return DEFAULT_INDEX;
|
|
2251
|
+
} catch {
|
|
2252
|
+
return DEFAULT_INDEX;
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
function saveIndex(cwd, index) {
|
|
2256
|
+
const filePath = indexFilePath(cwd);
|
|
2257
|
+
mkdirSync2(join11(cwd, ".autoremediator", "state"), { recursive: true });
|
|
2258
|
+
writeFileSync2(filePath, JSON.stringify(index, null, 2) + "\n", "utf8");
|
|
2259
|
+
}
|
|
2260
|
+
function readIdempotentReport(cwd, idempotencyKey, cveId) {
|
|
2261
|
+
const index = loadIndex(cwd);
|
|
2262
|
+
const key = entryKey(idempotencyKey, cveId);
|
|
2263
|
+
return index.entries[key]?.report;
|
|
2264
|
+
}
|
|
2265
|
+
function storeIdempotentReport(cwd, idempotencyKey, cveId, report) {
|
|
2266
|
+
const index = loadIndex(cwd);
|
|
2267
|
+
const key = entryKey(idempotencyKey, cveId);
|
|
2268
|
+
index.entries[key] = {
|
|
2269
|
+
key: idempotencyKey,
|
|
2270
|
+
cveId: cveId.toUpperCase(),
|
|
2271
|
+
report,
|
|
2272
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2273
|
+
};
|
|
2274
|
+
saveIndex(cwd, index);
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
// src/remediation/pipeline.ts
|
|
2278
|
+
import { generateText as generateText2 } from "ai";
|
|
2279
|
+
|
|
2280
|
+
// src/remediation/local/run.ts
|
|
2281
|
+
import semver6 from "semver";
|
|
2282
|
+
|
|
2283
|
+
// src/intelligence/sources/github-advisory.ts
|
|
2284
|
+
var GITHUB_API_BASE = "https://api.github.com/advisories";
|
|
2285
|
+
async function fetchGhAdvisories(cveId) {
|
|
2286
|
+
const token = getGitHubToken();
|
|
2287
|
+
const headers = {
|
|
2288
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
2289
|
+
"User-Agent": "autoremediator"
|
|
2290
|
+
};
|
|
2291
|
+
if (token) {
|
|
2292
|
+
headers["Authorization"] = `token ${token}`;
|
|
2293
|
+
}
|
|
2294
|
+
const normalized = cveId.trim();
|
|
2295
|
+
if (normalized.toUpperCase().startsWith("GHSA-")) {
|
|
2296
|
+
const url2 = `${GITHUB_API_BASE}/${encodeURIComponent(normalized)}`;
|
|
2297
|
+
try {
|
|
2298
|
+
const res = await httpClient({ url: url2, headers });
|
|
2299
|
+
if (!res.ok) {
|
|
2300
|
+
if (res.status === 404) return [];
|
|
2301
|
+
throw new Error(`HTTP ${res.status}: ${res.text}`);
|
|
2302
|
+
}
|
|
2303
|
+
return res.data ? [res.data] : [];
|
|
2304
|
+
} catch (err) {
|
|
2305
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
2306
|
+
console.warn(
|
|
2307
|
+
`[autoremediator] GitHub Advisory API error for ${cveId}: ${errorMsg} \u2014 skipping.`
|
|
2308
|
+
);
|
|
2309
|
+
return [];
|
|
2310
|
+
}
|
|
1565
2311
|
}
|
|
1566
2312
|
const url = new URL(GITHUB_API_BASE);
|
|
1567
2313
|
url.searchParams.set("cve_id", cveId);
|
|
@@ -1671,9 +2417,9 @@ async function enrichWithNvd(details) {
|
|
|
1671
2417
|
}
|
|
1672
2418
|
|
|
1673
2419
|
// src/intelligence/sources/snapshot.ts
|
|
1674
|
-
import { readFileSync as
|
|
2420
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
1675
2421
|
function loadIntelligenceSnapshot(filePath) {
|
|
1676
|
-
const raw =
|
|
2422
|
+
const raw = readFileSync9(filePath, "utf8");
|
|
1677
2423
|
const parsed = JSON.parse(raw);
|
|
1678
2424
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1679
2425
|
throw new Error(`Intelligence snapshot at ${filePath} must be a JSON object keyed by CVE ID.`);
|
|
@@ -1685,8 +2431,8 @@ function lookupSnapshotCve(snapshot, cveId) {
|
|
|
1685
2431
|
}
|
|
1686
2432
|
|
|
1687
2433
|
// src/remediation/tools/check-reachability.ts
|
|
1688
|
-
import { existsSync as
|
|
1689
|
-
import { isAbsolute as isAbsolute2, join as
|
|
2434
|
+
import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync10, statSync as statSync2 } from "fs";
|
|
2435
|
+
import { isAbsolute as isAbsolute2, join as join12, resolve as resolve3 } from "path";
|
|
1690
2436
|
import { parseSync } from "oxc-parser";
|
|
1691
2437
|
import { z as z2 } from "zod";
|
|
1692
2438
|
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"]);
|
|
@@ -1702,16 +2448,16 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
1702
2448
|
]);
|
|
1703
2449
|
function loadTsconfigPaths(cwd) {
|
|
1704
2450
|
for (const configName of ["tsconfig.json", "jsconfig.json"]) {
|
|
1705
|
-
const fullPath =
|
|
1706
|
-
if (!
|
|
2451
|
+
const fullPath = join12(cwd, configName);
|
|
2452
|
+
if (!existsSync10(fullPath)) continue;
|
|
1707
2453
|
try {
|
|
1708
|
-
const raw =
|
|
2454
|
+
const raw = readFileSync10(fullPath, "utf8");
|
|
1709
2455
|
const cleaned = raw.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "");
|
|
1710
2456
|
const parsed = JSON.parse(cleaned);
|
|
1711
2457
|
const compilerOptions = parsed.compilerOptions;
|
|
1712
2458
|
if (compilerOptions?.paths) {
|
|
1713
2459
|
return {
|
|
1714
|
-
baseUrl: compilerOptions.baseUrl ?
|
|
2460
|
+
baseUrl: compilerOptions.baseUrl ? resolve3(cwd, compilerOptions.baseUrl) : cwd,
|
|
1715
2461
|
paths: compilerOptions.paths
|
|
1716
2462
|
};
|
|
1717
2463
|
}
|
|
@@ -1726,14 +2472,14 @@ function resolveAliasedPath(importSource, config) {
|
|
|
1726
2472
|
}
|
|
1727
2473
|
for (const [pattern, replacements] of Object.entries(config.paths)) {
|
|
1728
2474
|
if (pattern === importSource && replacements[0]) {
|
|
1729
|
-
return
|
|
2475
|
+
return resolve3(config.baseUrl, replacements[0]);
|
|
1730
2476
|
}
|
|
1731
2477
|
if (pattern.endsWith("/*")) {
|
|
1732
2478
|
const prefix = pattern.slice(0, -2);
|
|
1733
2479
|
if (importSource.startsWith(prefix)) {
|
|
1734
2480
|
const suffix = importSource.slice(prefix.length);
|
|
1735
2481
|
const replacement = replacements[0]?.slice(0, -2) ?? "";
|
|
1736
|
-
return
|
|
2482
|
+
return resolve3(config.baseUrl, replacement + suffix);
|
|
1737
2483
|
}
|
|
1738
2484
|
}
|
|
1739
2485
|
}
|
|
@@ -1742,16 +2488,16 @@ function resolveAliasedPath(importSource, config) {
|
|
|
1742
2488
|
function* walkSourceFiles(dir) {
|
|
1743
2489
|
let entries;
|
|
1744
2490
|
try {
|
|
1745
|
-
entries =
|
|
2491
|
+
entries = readdirSync2(dir);
|
|
1746
2492
|
} catch {
|
|
1747
2493
|
return;
|
|
1748
2494
|
}
|
|
1749
2495
|
for (const entry of entries) {
|
|
1750
2496
|
if (IGNORED_DIRS.has(entry)) continue;
|
|
1751
|
-
const full =
|
|
2497
|
+
const full = join12(dir, entry);
|
|
1752
2498
|
let stat2;
|
|
1753
2499
|
try {
|
|
1754
|
-
stat2 =
|
|
2500
|
+
stat2 = statSync2(full);
|
|
1755
2501
|
} catch {
|
|
1756
2502
|
continue;
|
|
1757
2503
|
}
|
|
@@ -1789,7 +2535,31 @@ function walkAst(node, visitor) {
|
|
|
1789
2535
|
}
|
|
1790
2536
|
}
|
|
1791
2537
|
}
|
|
1792
|
-
function
|
|
2538
|
+
function toRootPackageName(importSource) {
|
|
2539
|
+
if (importSource.startsWith("@")) {
|
|
2540
|
+
const parts = importSource.split("/");
|
|
2541
|
+
return parts.slice(0, 2).join("/");
|
|
2542
|
+
}
|
|
2543
|
+
return importSource.split("/")[0] ?? importSource;
|
|
2544
|
+
}
|
|
2545
|
+
function findTransitiveDependencyPath(cwd, targetPackage, importedDirectPackages) {
|
|
2546
|
+
for (const directPkg of importedDirectPackages) {
|
|
2547
|
+
const manifestPath = join12(cwd, "node_modules", directPkg, "package.json");
|
|
2548
|
+
if (!existsSync10(manifestPath)) continue;
|
|
2549
|
+
try {
|
|
2550
|
+
const manifest = JSON.parse(readFileSync10(manifestPath, "utf8"));
|
|
2551
|
+
if (manifest.dependencies && manifest.dependencies[targetPackage]) {
|
|
2552
|
+
return {
|
|
2553
|
+
directDependency: directPkg,
|
|
2554
|
+
version: manifest.dependencies[targetPackage]
|
|
2555
|
+
};
|
|
2556
|
+
}
|
|
2557
|
+
} catch {
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
return null;
|
|
2561
|
+
}
|
|
2562
|
+
function extractPackageReferences(filename, content, packageName, tsconfig, outImportedDirectPackages) {
|
|
1793
2563
|
const results = [];
|
|
1794
2564
|
let parseResult;
|
|
1795
2565
|
try {
|
|
@@ -1801,6 +2571,9 @@ function extractPackageReferences(filename, content, packageName, tsconfig) {
|
|
|
1801
2571
|
walkAst(parseResult.program, (node) => {
|
|
1802
2572
|
if (node.type === "ImportDeclaration") {
|
|
1803
2573
|
const source = node.source;
|
|
2574
|
+
if (source?.value && !source.value.startsWith(".") && !isAbsolute2(source.value)) {
|
|
2575
|
+
outImportedDirectPackages?.add(toRootPackageName(source.value));
|
|
2576
|
+
}
|
|
1804
2577
|
if (source?.value && matchesPackage(source.value, packageName, tsconfig)) {
|
|
1805
2578
|
const symbols = [];
|
|
1806
2579
|
const importedIdentifiers = /* @__PURE__ */ new Map();
|
|
@@ -1835,6 +2608,9 @@ function extractPackageReferences(filename, content, packageName, tsconfig) {
|
|
|
1835
2608
|
}
|
|
1836
2609
|
if (node.type === "ImportExpression") {
|
|
1837
2610
|
const source = node.source;
|
|
2611
|
+
if (source?.value && !source.value.startsWith(".") && !isAbsolute2(source.value)) {
|
|
2612
|
+
outImportedDirectPackages?.add(toRootPackageName(source.value));
|
|
2613
|
+
}
|
|
1838
2614
|
if (source?.value && matchesPackage(source.value, packageName, tsconfig)) {
|
|
1839
2615
|
results.push({
|
|
1840
2616
|
matchType: "dynamic-import",
|
|
@@ -1846,6 +2622,9 @@ function extractPackageReferences(filename, content, packageName, tsconfig) {
|
|
|
1846
2622
|
}
|
|
1847
2623
|
if (node.type === "CallExpression" && node.callee?.name === "require" && Array.isArray(node.arguments) && node.arguments.length > 0) {
|
|
1848
2624
|
const firstArg = node.arguments[0];
|
|
2625
|
+
if (firstArg?.value && typeof firstArg.value === "string" && !firstArg.value.startsWith(".") && !isAbsolute2(firstArg.value)) {
|
|
2626
|
+
outImportedDirectPackages?.add(toRootPackageName(firstArg.value));
|
|
2627
|
+
}
|
|
1849
2628
|
if (firstArg?.value && typeof firstArg.value === "string" && matchesPackage(firstArg.value, packageName, tsconfig)) {
|
|
1850
2629
|
results.push({
|
|
1851
2630
|
matchType: "require",
|
|
@@ -1857,6 +2636,9 @@ function extractPackageReferences(filename, content, packageName, tsconfig) {
|
|
|
1857
2636
|
}
|
|
1858
2637
|
if (node.type === "ExportNamedDeclaration" || node.type === "ExportAllDeclaration") {
|
|
1859
2638
|
const source = node.source;
|
|
2639
|
+
if (source?.value && !source.value.startsWith(".") && !isAbsolute2(source.value)) {
|
|
2640
|
+
outImportedDirectPackages?.add(toRootPackageName(source.value));
|
|
2641
|
+
}
|
|
1860
2642
|
if (source?.value && matchesPackage(source.value, packageName, tsconfig)) {
|
|
1861
2643
|
const symbols = [];
|
|
1862
2644
|
if (node.type === "ExportAllDeclaration") {
|
|
@@ -1933,15 +2715,22 @@ function assessPackageReachability(cwd, packageName, targetSymbol) {
|
|
|
1933
2715
|
let hasReExport = false;
|
|
1934
2716
|
let totalInvocations = 0;
|
|
1935
2717
|
let hasPackageImport = false;
|
|
2718
|
+
const importedDirectPackages = /* @__PURE__ */ new Set();
|
|
1936
2719
|
for (const filePath of walkSourceFiles(cwd)) {
|
|
1937
2720
|
scanned++;
|
|
1938
2721
|
let content;
|
|
1939
2722
|
try {
|
|
1940
|
-
content =
|
|
2723
|
+
content = readFileSync10(filePath, "utf8");
|
|
1941
2724
|
} catch {
|
|
1942
2725
|
continue;
|
|
1943
2726
|
}
|
|
1944
|
-
const refs = extractPackageReferences(
|
|
2727
|
+
const refs = extractPackageReferences(
|
|
2728
|
+
filePath,
|
|
2729
|
+
content,
|
|
2730
|
+
packageName,
|
|
2731
|
+
tsconfig,
|
|
2732
|
+
importedDirectPackages
|
|
2733
|
+
);
|
|
1945
2734
|
if (refs.length > 0) {
|
|
1946
2735
|
hasPackageImport = true;
|
|
1947
2736
|
}
|
|
@@ -1984,6 +2773,16 @@ function assessPackageReachability(cwd, packageName, targetSymbol) {
|
|
|
1984
2773
|
};
|
|
1985
2774
|
}
|
|
1986
2775
|
if (!hasPackageImport) {
|
|
2776
|
+
const transitivePath = findTransitiveDependencyPath(cwd, packageName, importedDirectPackages);
|
|
2777
|
+
if (transitivePath) {
|
|
2778
|
+
return {
|
|
2779
|
+
packageName,
|
|
2780
|
+
status: "reachable",
|
|
2781
|
+
reason: `Package '${packageName}' is a transitive dependency of imported package '${transitivePath.directDependency}'`,
|
|
2782
|
+
reachabilityBasis: "transitive-path-found",
|
|
2783
|
+
evidence: []
|
|
2784
|
+
};
|
|
2785
|
+
}
|
|
1987
2786
|
return {
|
|
1988
2787
|
packageName,
|
|
1989
2788
|
status: "not-reachable",
|
|
@@ -2128,254 +2927,48 @@ async function runSecOpsPreflight(normalizedId, cveDetails, opts) {
|
|
|
2128
2927
|
opts.slaPolicy
|
|
2129
2928
|
);
|
|
2130
2929
|
if (breach) {
|
|
2131
|
-
slaBreaches = [breach];
|
|
2132
|
-
}
|
|
2133
|
-
}
|
|
2134
|
-
return { suppressed: false, exploitSignalTriggered, slaBreaches };
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
// src/remediation/local/sbom.ts
|
|
2138
|
-
function buildSbom(packages, vulnerableNames, results) {
|
|
2139
|
-
const statusByPackage = /* @__PURE__ */ new Map();
|
|
2140
|
-
for (const result of results) {
|
|
2141
|
-
if (!vulnerableNames.has(result.packageName)) continue;
|
|
2142
|
-
if (result.suppressedBy) {
|
|
2143
|
-
statusByPackage.set(result.packageName, "suppressed");
|
|
2144
|
-
} else if (!result.applied && result.strategy === "none") {
|
|
2145
|
-
statusByPackage.set(result.packageName, "skipped");
|
|
2146
|
-
} else if (result.applied) {
|
|
2147
|
-
statusByPackage.set(result.packageName, "patched");
|
|
2148
|
-
} else {
|
|
2149
|
-
statusByPackage.set(result.packageName, "unpatched");
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
return packages.map((pkg) => {
|
|
2153
|
-
const isVulnerable = vulnerableNames.has(pkg.name);
|
|
2154
|
-
const entry = {
|
|
2155
|
-
name: pkg.name,
|
|
2156
|
-
version: pkg.version,
|
|
2157
|
-
scope: pkg.type === "direct" ? "direct" : "transitive"
|
|
2158
|
-
};
|
|
2159
|
-
if (isVulnerable) {
|
|
2160
|
-
entry.status = statusByPackage.get(pkg.name) ?? "unpatched";
|
|
2161
|
-
}
|
|
2162
|
-
return entry;
|
|
2163
|
-
});
|
|
2164
|
-
}
|
|
2165
|
-
|
|
2166
|
-
// src/intelligence/sources/registry.ts
|
|
2167
|
-
import { readFileSync as readFileSync8 } from "fs";
|
|
2168
|
-
import { join as join10 } from "path";
|
|
2169
|
-
import { execa as execa4 } from "execa";
|
|
2170
|
-
import semver from "semver";
|
|
2171
|
-
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
2172
|
-
async function fetchPackageVersions(packageName) {
|
|
2173
|
-
const url = `${NPM_REGISTRY}/${encodeURIComponent(packageName)}`;
|
|
2174
|
-
try {
|
|
2175
|
-
const res = await httpClient({ url });
|
|
2176
|
-
if (res.status === 404) return [];
|
|
2177
|
-
if (!res.ok) {
|
|
2178
|
-
throw new Error(`npm registry error ${res.status} for "${packageName}": ${res.text}`);
|
|
2179
|
-
}
|
|
2180
|
-
const data = res.data;
|
|
2181
|
-
return Object.keys(data.versions);
|
|
2182
|
-
} catch (err) {
|
|
2183
|
-
if (err instanceof Error && err.message.includes("registry error")) {
|
|
2184
|
-
throw err;
|
|
2185
|
-
}
|
|
2186
|
-
return [];
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
async function findSafeUpgradeVersion(packageName, installedVersion, firstPatchedVersion, vulnerableRange) {
|
|
2190
|
-
const resolution = await resolveSafeUpgradeVersion(
|
|
2191
|
-
packageName,
|
|
2192
|
-
installedVersion,
|
|
2193
|
-
firstPatchedVersion,
|
|
2194
|
-
vulnerableRange
|
|
2195
|
-
);
|
|
2196
|
-
return resolution.safeVersion;
|
|
2197
|
-
}
|
|
2198
|
-
async function resolveSafeUpgradeVersion(packageName, installedVersion, firstPatchedVersion, vulnerableRange) {
|
|
2199
|
-
const versions = await fetchPackageVersions(packageName);
|
|
2200
|
-
if (!versions.length) {
|
|
2201
|
-
return {
|
|
2202
|
-
candidates: {},
|
|
2203
|
-
majorOnlyFixAvailable: false
|
|
2204
|
-
};
|
|
2205
|
-
}
|
|
2206
|
-
const installed = semver.parse(installedVersion);
|
|
2207
|
-
const candidates = versions.filter((v) => semver.valid(v) && semver.gte(v, firstPatchedVersion)).filter((v) => {
|
|
2208
|
-
if (!vulnerableRange) return true;
|
|
2209
|
-
try {
|
|
2210
|
-
return !semver.satisfies(v, vulnerableRange, { includePrerelease: false });
|
|
2211
|
-
} catch {
|
|
2212
|
-
return true;
|
|
2213
|
-
}
|
|
2214
|
-
}).sort(semver.compare);
|
|
2215
|
-
if (!candidates.length) {
|
|
2216
|
-
return {
|
|
2217
|
-
candidates: {},
|
|
2218
|
-
majorOnlyFixAvailable: false
|
|
2219
|
-
};
|
|
2220
|
-
}
|
|
2221
|
-
const categorizedCandidates = {};
|
|
2222
|
-
for (const candidate of candidates) {
|
|
2223
|
-
const level = classifyUpgradeLevel(installedVersion, candidate);
|
|
2224
|
-
if (!level) continue;
|
|
2225
|
-
if (!categorizedCandidates[level]) {
|
|
2226
|
-
categorizedCandidates[level] = candidate;
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
const safeVersion = categorizedCandidates.patch ?? categorizedCandidates.minor ?? categorizedCandidates.major;
|
|
2230
|
-
if (!safeVersion) {
|
|
2231
|
-
return {
|
|
2232
|
-
candidates: categorizedCandidates,
|
|
2233
|
-
majorOnlyFixAvailable: false
|
|
2234
|
-
};
|
|
2235
|
-
}
|
|
2236
|
-
const upgradeLevel = classifyUpgradeLevel(installedVersion, safeVersion);
|
|
2237
|
-
const majorOnlyFixAvailable = !categorizedCandidates.patch && !categorizedCandidates.minor && Boolean(categorizedCandidates.major);
|
|
2238
|
-
if (!installed || !upgradeLevel) {
|
|
2239
|
-
return {
|
|
2240
|
-
safeVersion,
|
|
2241
|
-
upgradeLevel,
|
|
2242
|
-
candidates: categorizedCandidates,
|
|
2243
|
-
majorOnlyFixAvailable
|
|
2244
|
-
};
|
|
2245
|
-
}
|
|
2246
|
-
return {
|
|
2247
|
-
safeVersion,
|
|
2248
|
-
upgradeLevel,
|
|
2249
|
-
candidates: categorizedCandidates,
|
|
2250
|
-
majorOnlyFixAvailable
|
|
2251
|
-
};
|
|
2252
|
-
}
|
|
2253
|
-
function classifyUpgradeLevel(installedVersion, candidateVersion) {
|
|
2254
|
-
const installed = semver.parse(installedVersion);
|
|
2255
|
-
const candidate = semver.parse(candidateVersion);
|
|
2256
|
-
if (!installed || !candidate) return void 0;
|
|
2257
|
-
if (candidate.major > installed.major) return "major";
|
|
2258
|
-
if (candidate.minor > installed.minor) return "minor";
|
|
2259
|
-
if (candidate.patch > installed.patch || candidate.version === installed.version) {
|
|
2260
|
-
return "patch";
|
|
2261
|
-
}
|
|
2262
|
-
return void 0;
|
|
2263
|
-
}
|
|
2264
|
-
async function queryOutdatedPackages(cwd, options = {}) {
|
|
2265
|
-
const pm = options.packageManager ?? detectPackageManager(cwd);
|
|
2266
|
-
let stdout;
|
|
2267
|
-
try {
|
|
2268
|
-
const args = pm === "pnpm" ? ["outdated", "--json"] : ["outdated", "--json"];
|
|
2269
|
-
const yarnArgs = pm === "yarn" ? ["outdated", "--json"] : void 0;
|
|
2270
|
-
const finalArgs = yarnArgs ?? args;
|
|
2271
|
-
const result2 = await execa4(pm, finalArgs, {
|
|
2272
|
-
cwd,
|
|
2273
|
-
reject: false,
|
|
2274
|
-
stdio: "pipe"
|
|
2275
|
-
});
|
|
2276
|
-
if (result2.exitCode !== 0 && result2.exitCode !== 1) {
|
|
2277
|
-
const stderr = typeof result2.stderr === "string" ? result2.stderr : "";
|
|
2278
|
-
if (!result2.stdout) {
|
|
2279
|
-
throw new Error(
|
|
2280
|
-
`${pm} outdated --json failed (exit ${result2.exitCode}): ${stderr.slice(0, 500)}`
|
|
2281
|
-
);
|
|
2282
|
-
}
|
|
2283
|
-
}
|
|
2284
|
-
stdout = typeof result2.stdout === "string" ? result2.stdout : "";
|
|
2285
|
-
} catch (err) {
|
|
2286
|
-
if (err instanceof Error && !err.message.includes("outdated --json failed")) {
|
|
2287
|
-
throw new Error(`Failed to query outdated packages: ${err.message}`);
|
|
2288
|
-
}
|
|
2289
|
-
throw err;
|
|
2290
|
-
}
|
|
2291
|
-
if (!stdout.trim()) {
|
|
2292
|
-
return /* @__PURE__ */ new Map();
|
|
2293
|
-
}
|
|
2294
|
-
const rawEntries = {};
|
|
2295
|
-
if (pm === "yarn") {
|
|
2296
|
-
for (const line of stdout.split("\n")) {
|
|
2297
|
-
const trimmed = line.trim();
|
|
2298
|
-
if (!trimmed) continue;
|
|
2299
|
-
try {
|
|
2300
|
-
const parsed = JSON.parse(trimmed);
|
|
2301
|
-
if (parsed.type === "table" && parsed.data) {
|
|
2302
|
-
const tableData = parsed.data;
|
|
2303
|
-
const head = tableData.head ?? [];
|
|
2304
|
-
const currentIdx = head.indexOf("Current");
|
|
2305
|
-
const wantedIdx = head.indexOf("Wanted");
|
|
2306
|
-
const latestIdx = head.indexOf("Latest");
|
|
2307
|
-
for (const row of tableData.body ?? []) {
|
|
2308
|
-
const name = row[0];
|
|
2309
|
-
if (!name) continue;
|
|
2310
|
-
rawEntries[name] = {
|
|
2311
|
-
current: row[currentIdx] ?? "",
|
|
2312
|
-
wanted: row[wantedIdx] ?? "",
|
|
2313
|
-
latest: row[latestIdx] ?? "",
|
|
2314
|
-
type: row[head.indexOf("Package Type")] ?? "dependencies"
|
|
2315
|
-
};
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
} catch {
|
|
2319
|
-
}
|
|
2320
|
-
}
|
|
2321
|
-
} else {
|
|
2322
|
-
try {
|
|
2323
|
-
const parsed = JSON.parse(stdout);
|
|
2324
|
-
if (Array.isArray(parsed)) {
|
|
2325
|
-
for (const entry of parsed) {
|
|
2326
|
-
const name = entry.name ?? entry.packageName;
|
|
2327
|
-
if (!name) continue;
|
|
2328
|
-
rawEntries[name] = {
|
|
2329
|
-
current: entry.current,
|
|
2330
|
-
wanted: entry.wanted,
|
|
2331
|
-
latest: entry.latest,
|
|
2332
|
-
type: entry.dependencyType
|
|
2333
|
-
};
|
|
2334
|
-
}
|
|
2335
|
-
} else {
|
|
2336
|
-
Object.assign(rawEntries, parsed);
|
|
2337
|
-
}
|
|
2338
|
-
} catch {
|
|
2339
|
-
throw new Error(`Could not parse ${pm} outdated --json output.`);
|
|
2930
|
+
slaBreaches = [breach];
|
|
2340
2931
|
}
|
|
2341
2932
|
}
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
if (!options.includeTransitive && dependencyScope === "transitive") continue;
|
|
2360
|
-
const isMajorBump = semver.valid(currentVersion) !== null && semver.valid(latestVersion) !== null && semver.major(latestVersion) > semver.major(currentVersion);
|
|
2361
|
-
result.set(name, {
|
|
2362
|
-
currentVersion,
|
|
2363
|
-
wantedVersion,
|
|
2364
|
-
latestVersion,
|
|
2365
|
-
isMajorBump,
|
|
2366
|
-
dependencyScope
|
|
2367
|
-
});
|
|
2933
|
+
return { suppressed: false, exploitSignalTriggered, slaBreaches };
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
// src/remediation/local/sbom.ts
|
|
2937
|
+
function buildSbom(packages, vulnerableNames, results) {
|
|
2938
|
+
const statusByPackage = /* @__PURE__ */ new Map();
|
|
2939
|
+
for (const result of results) {
|
|
2940
|
+
if (!vulnerableNames.has(result.packageName)) continue;
|
|
2941
|
+
if (result.suppressedBy) {
|
|
2942
|
+
statusByPackage.set(result.packageName, "suppressed");
|
|
2943
|
+
} else if (!result.applied && result.strategy === "none") {
|
|
2944
|
+
statusByPackage.set(result.packageName, "skipped");
|
|
2945
|
+
} else if (result.applied) {
|
|
2946
|
+
statusByPackage.set(result.packageName, "patched");
|
|
2947
|
+
} else {
|
|
2948
|
+
statusByPackage.set(result.packageName, "unpatched");
|
|
2949
|
+
}
|
|
2368
2950
|
}
|
|
2369
|
-
return
|
|
2951
|
+
return packages.map((pkg) => {
|
|
2952
|
+
const isVulnerable = vulnerableNames.has(pkg.name);
|
|
2953
|
+
const entry = {
|
|
2954
|
+
name: pkg.name,
|
|
2955
|
+
version: pkg.version,
|
|
2956
|
+
scope: pkg.type === "direct" ? "direct" : "transitive"
|
|
2957
|
+
};
|
|
2958
|
+
if (isVulnerable) {
|
|
2959
|
+
entry.status = statusByPackage.get(pkg.name) ?? "unpatched";
|
|
2960
|
+
}
|
|
2961
|
+
return entry;
|
|
2962
|
+
});
|
|
2370
2963
|
}
|
|
2371
2964
|
|
|
2372
2965
|
// src/remediation/tools/apply-version-bump.ts
|
|
2373
2966
|
import { z as z4 } from "zod";
|
|
2374
|
-
import { join as
|
|
2375
|
-
import { readFileSync as
|
|
2967
|
+
import { join as join15 } from "path";
|
|
2968
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync4 } from "fs";
|
|
2376
2969
|
|
|
2377
2970
|
// src/platform/safe-exec.ts
|
|
2378
|
-
import { execa as
|
|
2971
|
+
import { execa as execa6 } from "execa";
|
|
2379
2972
|
var DANGEROUS_ENV_VARS = /* @__PURE__ */ new Set([
|
|
2380
2973
|
"NODE_OPTIONS",
|
|
2381
2974
|
"NODE_PATH",
|
|
@@ -2424,7 +3017,7 @@ function safeExeca(file, args = [], options = {}) {
|
|
|
2424
3017
|
options.env ?? process.env,
|
|
2425
3018
|
options.allowNodeOptions
|
|
2426
3019
|
);
|
|
2427
|
-
return
|
|
3020
|
+
return execa6(file, args, {
|
|
2428
3021
|
...options,
|
|
2429
3022
|
timeout,
|
|
2430
3023
|
env: sanitizedEnv
|
|
@@ -2432,19 +3025,19 @@ function safeExeca(file, args = [], options = {}) {
|
|
|
2432
3025
|
}
|
|
2433
3026
|
|
|
2434
3027
|
// src/remediation/tools/apply-version-bump.ts
|
|
2435
|
-
import
|
|
3028
|
+
import semver3 from "semver";
|
|
2436
3029
|
|
|
2437
3030
|
// src/platform/repo-lock.ts
|
|
2438
|
-
import { mkdir, rm } from "fs/promises";
|
|
2439
|
-
import { join as
|
|
3031
|
+
import { mkdir, rm as rm2 } from "fs/promises";
|
|
3032
|
+
import { join as join13 } from "path";
|
|
2440
3033
|
async function sleep(ms) {
|
|
2441
|
-
await new Promise((
|
|
3034
|
+
await new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
2442
3035
|
}
|
|
2443
3036
|
async function acquireRepoLock(cwd, options = {}) {
|
|
2444
3037
|
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
2445
3038
|
const retryDelayMs = options.retryDelayMs ?? 125;
|
|
2446
|
-
const lockRoot =
|
|
2447
|
-
const lockPath =
|
|
3039
|
+
const lockRoot = join13(cwd, ".autoremediator", "locks");
|
|
3040
|
+
const lockPath = join13(cwd, ".autoremediator", "locks", "remediation.lock");
|
|
2448
3041
|
const startedAt = Date.now();
|
|
2449
3042
|
await mkdir(lockRoot, { recursive: true });
|
|
2450
3043
|
while (true) {
|
|
@@ -2453,7 +3046,7 @@ async function acquireRepoLock(cwd, options = {}) {
|
|
|
2453
3046
|
return {
|
|
2454
3047
|
lockPath,
|
|
2455
3048
|
release: async () => {
|
|
2456
|
-
await
|
|
3049
|
+
await rm2(lockPath, { recursive: true, force: true });
|
|
2457
3050
|
}
|
|
2458
3051
|
};
|
|
2459
3052
|
} catch {
|
|
@@ -2473,6 +3066,380 @@ async function withRepoLock(cwd, fn, options) {
|
|
|
2473
3066
|
}
|
|
2474
3067
|
}
|
|
2475
3068
|
|
|
3069
|
+
// src/remediation/refactor/call-site-finder.ts
|
|
3070
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync11, statSync as statSync3 } from "fs";
|
|
3071
|
+
import { join as join14 } from "path";
|
|
3072
|
+
import { parseSync as parseSync2 } from "oxc-parser";
|
|
3073
|
+
var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"]);
|
|
3074
|
+
var IGNORED_DIRS2 = /* @__PURE__ */ new Set([
|
|
3075
|
+
"node_modules",
|
|
3076
|
+
".git",
|
|
3077
|
+
"dist",
|
|
3078
|
+
"build",
|
|
3079
|
+
"coverage",
|
|
3080
|
+
".next",
|
|
3081
|
+
".turbo",
|
|
3082
|
+
".pnpm-store"
|
|
3083
|
+
]);
|
|
3084
|
+
function* walkFiles(dir) {
|
|
3085
|
+
let entries;
|
|
3086
|
+
try {
|
|
3087
|
+
entries = readdirSync3(dir);
|
|
3088
|
+
} catch {
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
for (const entry of entries) {
|
|
3092
|
+
if (IGNORED_DIRS2.has(entry)) continue;
|
|
3093
|
+
const full = join14(dir, entry);
|
|
3094
|
+
let stat2;
|
|
3095
|
+
try {
|
|
3096
|
+
stat2 = statSync3(full);
|
|
3097
|
+
} catch {
|
|
3098
|
+
continue;
|
|
3099
|
+
}
|
|
3100
|
+
if (stat2.isDirectory()) {
|
|
3101
|
+
yield* walkFiles(full);
|
|
3102
|
+
} else {
|
|
3103
|
+
const ext = full.slice(full.lastIndexOf("."));
|
|
3104
|
+
if (SOURCE_EXTENSIONS2.has(ext)) {
|
|
3105
|
+
yield full;
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
function findPackageCallSites(cwd, packageName) {
|
|
3111
|
+
const callSites = [];
|
|
3112
|
+
for (const filePath of walkFiles(cwd)) {
|
|
3113
|
+
let content;
|
|
3114
|
+
try {
|
|
3115
|
+
content = readFileSync11(filePath, "utf8");
|
|
3116
|
+
} catch {
|
|
3117
|
+
continue;
|
|
3118
|
+
}
|
|
3119
|
+
if (!content.includes(packageName)) {
|
|
3120
|
+
continue;
|
|
3121
|
+
}
|
|
3122
|
+
try {
|
|
3123
|
+
const parseResult = parseSync2(filePath, content);
|
|
3124
|
+
if (!parseResult.program) continue;
|
|
3125
|
+
const program = parseResult.program;
|
|
3126
|
+
const imports = [];
|
|
3127
|
+
const importSources = [];
|
|
3128
|
+
const localNames = /* @__PURE__ */ new Set();
|
|
3129
|
+
for (const statement of program.body) {
|
|
3130
|
+
if (statement.type === "ImportDeclaration" && statement.source?.value) {
|
|
3131
|
+
const importSource = statement.source.value;
|
|
3132
|
+
if (importSource === packageName || importSource.startsWith(`${packageName}/`)) {
|
|
3133
|
+
importSources.push(importSource);
|
|
3134
|
+
for (const spec of statement.specifiers || []) {
|
|
3135
|
+
const localName = spec.local?.name || "";
|
|
3136
|
+
if (!localName) continue;
|
|
3137
|
+
localNames.add(localName);
|
|
3138
|
+
if (spec.type === "ImportDefaultSpecifier") {
|
|
3139
|
+
imports.push({
|
|
3140
|
+
localName,
|
|
3141
|
+
importedName: "default",
|
|
3142
|
+
isDefault: true,
|
|
3143
|
+
isNamespace: false
|
|
3144
|
+
});
|
|
3145
|
+
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
3146
|
+
imports.push({
|
|
3147
|
+
localName,
|
|
3148
|
+
importedName: "*",
|
|
3149
|
+
isDefault: false,
|
|
3150
|
+
isNamespace: true
|
|
3151
|
+
});
|
|
3152
|
+
} else if (spec.type === "ImportSpecifier") {
|
|
3153
|
+
imports.push({
|
|
3154
|
+
localName,
|
|
3155
|
+
importedName: spec.imported?.name || localName,
|
|
3156
|
+
isDefault: false,
|
|
3157
|
+
isNamespace: false
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
if (imports.length > 0 || importSources.length > 0) {
|
|
3165
|
+
const lines = content.split("\n");
|
|
3166
|
+
const referencedLines = [];
|
|
3167
|
+
lines.forEach((line, index) => {
|
|
3168
|
+
for (const local of localNames) {
|
|
3169
|
+
const regex = new RegExp(`\\b${local}\\b`);
|
|
3170
|
+
if (regex.test(line)) {
|
|
3171
|
+
referencedLines.push(index + 1);
|
|
3172
|
+
break;
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
});
|
|
3176
|
+
callSites.push({
|
|
3177
|
+
filePath,
|
|
3178
|
+
sourceContent: content,
|
|
3179
|
+
importSources,
|
|
3180
|
+
imports,
|
|
3181
|
+
referencedLines
|
|
3182
|
+
});
|
|
3183
|
+
}
|
|
3184
|
+
} catch {
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
return callSites;
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
// src/remediation/refactor/refactor-agent.ts
|
|
3191
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync3 } from "fs";
|
|
3192
|
+
function parseCompilerDiagnostics(diagnosticError) {
|
|
3193
|
+
const diagnostics = [];
|
|
3194
|
+
const lines = diagnosticError.split("\n");
|
|
3195
|
+
for (const line of lines) {
|
|
3196
|
+
const trimmed = line.trim();
|
|
3197
|
+
if (!trimmed) continue;
|
|
3198
|
+
const match = trimmed.match(
|
|
3199
|
+
/(?:([^\s:(]+)[:(](\d+)[:,](\d+)\)?:?\s*(?:-\s*)?(?:error\s+)?(TS\d+)?:?\s*(.*))/i
|
|
3200
|
+
);
|
|
3201
|
+
if (match) {
|
|
3202
|
+
const [, filePath, lineStr, colStr, code, message] = match;
|
|
3203
|
+
const diag = {
|
|
3204
|
+
filePath,
|
|
3205
|
+
line: lineStr ? parseInt(lineStr, 10) : void 0,
|
|
3206
|
+
column: colStr ? parseInt(colStr, 10) : void 0,
|
|
3207
|
+
code,
|
|
3208
|
+
message: message || trimmed
|
|
3209
|
+
};
|
|
3210
|
+
const suggestionMatch = diag.message.match(/Did you mean ['"]([a-zA-Z0-9_$]+)['"]\?/i);
|
|
3211
|
+
if (suggestionMatch) {
|
|
3212
|
+
diag.suggestedSymbol = suggestionMatch[1];
|
|
3213
|
+
}
|
|
3214
|
+
const missingMatch = diag.message.match(/has no exported member ['"]([a-zA-Z0-9_$]+)['"]/i) || diag.message.match(/Property ['"]([a-zA-Z0-9_$]+)['"] does not exist/i);
|
|
3215
|
+
if (missingMatch) {
|
|
3216
|
+
diag.missingMember = missingMatch[1];
|
|
3217
|
+
}
|
|
3218
|
+
diagnostics.push(diag);
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
return diagnostics;
|
|
3222
|
+
}
|
|
3223
|
+
function revertRefactorSnapshots(snapshots) {
|
|
3224
|
+
for (const [filePath, content] of Object.entries(snapshots)) {
|
|
3225
|
+
try {
|
|
3226
|
+
writeFileSync3(filePath, content, "utf8");
|
|
3227
|
+
} catch {
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
var KNOWN_MIGRATIONS = {
|
|
3232
|
+
rimraf: [
|
|
3233
|
+
{
|
|
3234
|
+
detect: (content, diag) => diag.includes("sync") && content.includes("rimraf.sync("),
|
|
3235
|
+
transform: (content) => {
|
|
3236
|
+
let res = content.replace(/import\s+rimraf\s+from\s+['"]rimraf['"];?/g, "");
|
|
3237
|
+
res = `import { rimrafSync } from "rimraf";
|
|
3238
|
+
` + res;
|
|
3239
|
+
return res.replace(/rimraf\.sync\(/g, "rimrafSync(");
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
],
|
|
3243
|
+
uuid: [
|
|
3244
|
+
{
|
|
3245
|
+
detect: (content, diag) => diag.includes("v4") && content.includes("uuid.v4("),
|
|
3246
|
+
transform: (content) => {
|
|
3247
|
+
let res = content.replace(/import\s+uuid\s+from\s+['"]uuid['"];?/g, "");
|
|
3248
|
+
res = `import { v4 as uuidv4 } from "uuid";
|
|
3249
|
+
` + res;
|
|
3250
|
+
return res.replace(/uuid\.v4\(/g, "uuidv4(");
|
|
3251
|
+
}
|
|
3252
|
+
},
|
|
3253
|
+
{
|
|
3254
|
+
// Subpath import migration: `import v4 from "uuid/v4"` -> `import { v4 } from "uuid"`
|
|
3255
|
+
detect: (content) => /from\s+['"]uuid\/v4['"]/.test(content),
|
|
3256
|
+
transform: (content) => content.replace(
|
|
3257
|
+
/import\s+([a-zA-Z0-9_$]+)\s+from\s+['"]uuid\/v4['"];?/g,
|
|
3258
|
+
`import { v4 as $1 } from "uuid";`
|
|
3259
|
+
)
|
|
3260
|
+
}
|
|
3261
|
+
],
|
|
3262
|
+
glob: [
|
|
3263
|
+
{
|
|
3264
|
+
detect: (content, diag) => diag.includes("sync") && content.includes("glob.sync("),
|
|
3265
|
+
transform: (content) => {
|
|
3266
|
+
let res = content.replace(/import\s+glob\s+from\s+['"]glob['"];?/g, "");
|
|
3267
|
+
res = `import { globSync } from "glob";
|
|
3268
|
+
` + res;
|
|
3269
|
+
return res.replace(/glob\.sync\(/g, "globSync(");
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
],
|
|
3273
|
+
mkdirp: [
|
|
3274
|
+
{
|
|
3275
|
+
detect: (content, diag) => diag.includes("sync") && content.includes("mkdirp.sync("),
|
|
3276
|
+
transform: (content) => {
|
|
3277
|
+
let res = content.replace(/import\s+mkdirp\s+from\s+['"]mkdirp['"];?/g, "");
|
|
3278
|
+
res = `import { mkdirpSync } from "mkdirp";
|
|
3279
|
+
` + res;
|
|
3280
|
+
return res.replace(/mkdirp\.sync\(/g, "mkdirpSync(");
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
]
|
|
3284
|
+
};
|
|
3285
|
+
function escapeRegex(str) {
|
|
3286
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3287
|
+
}
|
|
3288
|
+
function transformFileContent(content, packageName, diagnosticError, filePath) {
|
|
3289
|
+
let modified = false;
|
|
3290
|
+
let newContent = content;
|
|
3291
|
+
const migrations = KNOWN_MIGRATIONS[packageName];
|
|
3292
|
+
if (migrations) {
|
|
3293
|
+
for (const mig of migrations) {
|
|
3294
|
+
if (mig.detect(newContent, diagnosticError)) {
|
|
3295
|
+
const transformed = mig.transform(newContent);
|
|
3296
|
+
if (transformed !== newContent) {
|
|
3297
|
+
newContent = transformed;
|
|
3298
|
+
modified = true;
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
if (diagnosticError.includes("no default export") || diagnosticError.includes("has no exported member 'default'") || diagnosticError.includes("has no exported member default") || diagnosticError.includes("TS2614") || diagnosticError.includes("TS1192")) {
|
|
3304
|
+
const defaultImportRegex = new RegExp(
|
|
3305
|
+
`import\\s+([a-zA-Z0-9_$]+)\\s+from\\s+['"]${escapeRegex(packageName)}['"]`,
|
|
3306
|
+
"g"
|
|
3307
|
+
);
|
|
3308
|
+
if (defaultImportRegex.test(newContent)) {
|
|
3309
|
+
newContent = newContent.replace(
|
|
3310
|
+
defaultImportRegex,
|
|
3311
|
+
`import * as $1 from "${packageName}"`
|
|
3312
|
+
);
|
|
3313
|
+
modified = true;
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
if (diagnosticError.includes("can only be used with a default import") || diagnosticError.includes("only be used with a default import") || diagnosticError.includes("TS1259")) {
|
|
3317
|
+
const starImportRegex = new RegExp(
|
|
3318
|
+
`import\\s+\\*\\s+as\\s+([a-zA-Z0-9_$]+)\\s+from\\s+['"]${escapeRegex(packageName)}['"]`,
|
|
3319
|
+
"g"
|
|
3320
|
+
);
|
|
3321
|
+
if (starImportRegex.test(newContent)) {
|
|
3322
|
+
newContent = newContent.replace(
|
|
3323
|
+
starImportRegex,
|
|
3324
|
+
`import $1 from "${packageName}"`
|
|
3325
|
+
);
|
|
3326
|
+
modified = true;
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
const diagnostics = parseCompilerDiagnostics(diagnosticError);
|
|
3330
|
+
for (const diag of diagnostics) {
|
|
3331
|
+
if (diag.missingMember && diag.suggestedSymbol) {
|
|
3332
|
+
const missingMember = diag.missingMember;
|
|
3333
|
+
const suggestedSymbol = diag.suggestedSymbol;
|
|
3334
|
+
if (filePath && diag.filePath && filePath.endsWith(diag.filePath) && diag.line && diag.line > 0) {
|
|
3335
|
+
const fileLines = newContent.split("\n");
|
|
3336
|
+
const idx = diag.line - 1;
|
|
3337
|
+
if (idx >= 0 && idx < fileLines.length) {
|
|
3338
|
+
const line = fileLines[idx];
|
|
3339
|
+
const replaced = line.replace(new RegExp(`\\.${escapeRegex(missingMember)}\\b`, "g"), `.${suggestedSymbol}`).replace(new RegExp(`\\b${escapeRegex(missingMember)}\\b`, "g"), suggestedSymbol);
|
|
3340
|
+
if (replaced !== line) {
|
|
3341
|
+
fileLines[idx] = replaced;
|
|
3342
|
+
newContent = fileLines.join("\n");
|
|
3343
|
+
modified = true;
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
} else {
|
|
3347
|
+
const memberRegex = new RegExp(`\\.${escapeRegex(missingMember)}\\b`, "g");
|
|
3348
|
+
if (memberRegex.test(newContent)) {
|
|
3349
|
+
newContent = newContent.replace(memberRegex, `.${suggestedSymbol}`);
|
|
3350
|
+
modified = true;
|
|
3351
|
+
}
|
|
3352
|
+
const importPattern = new RegExp(
|
|
3353
|
+
`import\\s*\\{[^}]*\\b${escapeRegex(missingMember)}\\b[^}]*\\}\\s*from\\s*['"]${escapeRegex(packageName)}['"]`,
|
|
3354
|
+
"g"
|
|
3355
|
+
);
|
|
3356
|
+
if (importPattern.test(newContent)) {
|
|
3357
|
+
newContent = newContent.replace(
|
|
3358
|
+
importPattern,
|
|
3359
|
+
(match) => match.replace(new RegExp(`\\b${escapeRegex(missingMember)}\\b`, "g"), suggestedSymbol)
|
|
3360
|
+
);
|
|
3361
|
+
modified = true;
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
const lineDiags = diagnostics.filter(
|
|
3367
|
+
(d) => Boolean(filePath && d.filePath && filePath.endsWith(d.filePath) && typeof d.line === "number" && d.line > 0)
|
|
3368
|
+
).sort((a, b) => b.line - a.line);
|
|
3369
|
+
const processedLines = /* @__PURE__ */ new Set();
|
|
3370
|
+
for (const diag of lineDiags) {
|
|
3371
|
+
if (processedLines.has(diag.line)) continue;
|
|
3372
|
+
processedLines.add(diag.line);
|
|
3373
|
+
const fileLines = newContent.split("\n");
|
|
3374
|
+
const targetLineIdx = diag.line - 1;
|
|
3375
|
+
if (targetLineIdx >= 0 && targetLineIdx < fileLines.length) {
|
|
3376
|
+
const prevLine = targetLineIdx > 0 ? fileLines[targetLineIdx - 1] : "";
|
|
3377
|
+
if (!prevLine?.includes("@ts-expect-error") && !fileLines[targetLineIdx]?.includes("@ts-expect-error")) {
|
|
3378
|
+
const indent = fileLines[targetLineIdx]?.match(/^\s*/)?.[0] || "";
|
|
3379
|
+
fileLines.splice(
|
|
3380
|
+
targetLineIdx,
|
|
3381
|
+
0,
|
|
3382
|
+
`${indent}// @ts-expect-error [autoremediator-refactor]: breaking change in ${packageName}`
|
|
3383
|
+
);
|
|
3384
|
+
newContent = fileLines.join("\n");
|
|
3385
|
+
modified = true;
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
return { modified, newContent };
|
|
3390
|
+
}
|
|
3391
|
+
async function refactorConsumerCallSites(params) {
|
|
3392
|
+
const { packageName, diagnosticError, callSites } = params;
|
|
3393
|
+
if (callSites.length === 0) {
|
|
3394
|
+
return {
|
|
3395
|
+
applied: false,
|
|
3396
|
+
refactoredFiles: [],
|
|
3397
|
+
snapshots: {},
|
|
3398
|
+
error: `No consumer call sites found for "${packageName}".`
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
const snapshots = {};
|
|
3402
|
+
const refactoredFiles = [];
|
|
3403
|
+
try {
|
|
3404
|
+
for (const site of callSites) {
|
|
3405
|
+
const originalContent = site.sourceContent || readFileSync12(site.filePath, "utf8");
|
|
3406
|
+
snapshots[site.filePath] = originalContent;
|
|
3407
|
+
const { modified, newContent } = transformFileContent(
|
|
3408
|
+
originalContent,
|
|
3409
|
+
packageName,
|
|
3410
|
+
diagnosticError,
|
|
3411
|
+
site.filePath
|
|
3412
|
+
);
|
|
3413
|
+
if (modified && newContent !== originalContent) {
|
|
3414
|
+
writeFileSync3(site.filePath, newContent, "utf8");
|
|
3415
|
+
refactoredFiles.push(site.filePath);
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
if (refactoredFiles.length === 0) {
|
|
3419
|
+
return {
|
|
3420
|
+
applied: false,
|
|
3421
|
+
refactoredFiles: [],
|
|
3422
|
+
snapshots,
|
|
3423
|
+
error: "No automated codemod pattern matched the diagnostic error."
|
|
3424
|
+
};
|
|
3425
|
+
}
|
|
3426
|
+
return {
|
|
3427
|
+
applied: true,
|
|
3428
|
+
refactoredFiles,
|
|
3429
|
+
snapshots
|
|
3430
|
+
};
|
|
3431
|
+
} catch (err) {
|
|
3432
|
+
revertRefactorSnapshots(snapshots);
|
|
3433
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3434
|
+
return {
|
|
3435
|
+
applied: false,
|
|
3436
|
+
refactoredFiles: [],
|
|
3437
|
+
snapshots: {},
|
|
3438
|
+
error: `Failed to apply breaking change refactoring: ${message}`
|
|
3439
|
+
};
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
|
|
2476
3443
|
// src/remediation/tools/apply-version-bump.ts
|
|
2477
3444
|
var applyVersionBumpTool = defineTool({
|
|
2478
3445
|
description: "Update package.json to use the safe version of a vulnerable package and run the project's package manager install. In dry-run mode, only reports what would change.",
|
|
@@ -2485,10 +3452,12 @@ var applyVersionBumpTool = defineTool({
|
|
|
2485
3452
|
dryRun: z4.boolean().default(false).describe("If true, report changes but do not write"),
|
|
2486
3453
|
policy: z4.string().optional().describe("Optional path to .autoremediator policy file"),
|
|
2487
3454
|
runTests: z4.boolean().default(false).describe("If true, run test validation after applying the fix"),
|
|
3455
|
+
typecheck: z4.boolean().default(false).describe("If true, run typecheck validation after applying the fix"),
|
|
2488
3456
|
installMode: z4.enum(["standard", "prefer-offline", "deterministic"]).optional(),
|
|
2489
3457
|
installPreferOffline: z4.boolean().optional(),
|
|
2490
3458
|
enforceFrozenLockfile: z4.boolean().optional(),
|
|
2491
|
-
workspace: z4.string().optional()
|
|
3459
|
+
workspace: z4.string().optional(),
|
|
3460
|
+
breakingChangeRefactor: z4.boolean().optional().describe("If true, attempt agentic call-site refactoring when validation fails after major upgrade")
|
|
2492
3461
|
}),
|
|
2493
3462
|
execute: async ({
|
|
2494
3463
|
cwd,
|
|
@@ -2499,13 +3468,15 @@ var applyVersionBumpTool = defineTool({
|
|
|
2499
3468
|
dryRun,
|
|
2500
3469
|
policy,
|
|
2501
3470
|
runTests,
|
|
3471
|
+
typecheck,
|
|
2502
3472
|
installMode,
|
|
2503
3473
|
installPreferOffline,
|
|
2504
3474
|
enforceFrozenLockfile,
|
|
2505
|
-
workspace
|
|
3475
|
+
workspace,
|
|
3476
|
+
breakingChangeRefactor
|
|
2506
3477
|
}) => {
|
|
2507
3478
|
const pm = packageManager ?? detectPackageManager(cwd);
|
|
2508
|
-
|
|
3479
|
+
let pkgPath = join15(cwd, "package.json");
|
|
2509
3480
|
const loadedPolicy = loadPolicy(cwd, policy);
|
|
2510
3481
|
const commandConstraints = {
|
|
2511
3482
|
...loadedPolicy.constraints,
|
|
@@ -2515,9 +3486,6 @@ var applyVersionBumpTool = defineTool({
|
|
|
2515
3486
|
workspace: workspace ?? loadedPolicy.constraints?.workspace
|
|
2516
3487
|
};
|
|
2517
3488
|
const yarnMajor = pm === "yarn" ? await getYarnMajorVersion(cwd) : void 0;
|
|
2518
|
-
const installCommand = resolveInstallCommand(pm, commandConstraints, yarnMajor);
|
|
2519
|
-
const testCommand = resolveTestCommand(pm, commandConstraints);
|
|
2520
|
-
const dedupeCommand = resolveDedupeCommand(pm, commandConstraints);
|
|
2521
3489
|
if (!isPackageAllowed(loadedPolicy, packageName)) {
|
|
2522
3490
|
return {
|
|
2523
3491
|
packageName,
|
|
@@ -2530,7 +3498,7 @@ var applyVersionBumpTool = defineTool({
|
|
|
2530
3498
|
message: `Policy blocked changes for package "${packageName}".`
|
|
2531
3499
|
};
|
|
2532
3500
|
}
|
|
2533
|
-
const isMajorBump =
|
|
3501
|
+
const isMajorBump = semver3.valid(fromVersion) && semver3.valid(toVersion) && semver3.major(toVersion) > semver3.major(fromVersion);
|
|
2534
3502
|
if (isMajorBump && !loadedPolicy.allowMajorBumps) {
|
|
2535
3503
|
return {
|
|
2536
3504
|
packageName,
|
|
@@ -2543,9 +3511,10 @@ var applyVersionBumpTool = defineTool({
|
|
|
2543
3511
|
message: `Policy blocked major bump for "${packageName}" (${fromVersion} -> ${toVersion}).`
|
|
2544
3512
|
};
|
|
2545
3513
|
}
|
|
3514
|
+
pkgPath = join15(cwd, "package.json");
|
|
2546
3515
|
let pkgJson;
|
|
2547
3516
|
try {
|
|
2548
|
-
pkgJson = JSON.parse(
|
|
3517
|
+
pkgJson = JSON.parse(readFileSync13(pkgPath, "utf8"));
|
|
2549
3518
|
} catch {
|
|
2550
3519
|
return {
|
|
2551
3520
|
packageName,
|
|
@@ -2557,9 +3526,20 @@ var applyVersionBumpTool = defineTool({
|
|
|
2557
3526
|
message: `Could not read package.json at "${pkgPath}".`
|
|
2558
3527
|
};
|
|
2559
3528
|
}
|
|
2560
|
-
|
|
3529
|
+
let depField = ["dependencies", "devDependencies", "peerDependencies"].find(
|
|
2561
3530
|
(f) => pkgJson[f]?.[packageName] !== void 0
|
|
2562
3531
|
);
|
|
3532
|
+
if (!depField) {
|
|
3533
|
+
const declaringWorkspace = findDeclaringWorkspacePackage(cwd, packageName, workspace);
|
|
3534
|
+
if (declaringWorkspace) {
|
|
3535
|
+
pkgPath = declaringWorkspace.manifestPath;
|
|
3536
|
+
pkgJson = declaringWorkspace.manifest;
|
|
3537
|
+
depField = ["dependencies", "devDependencies", "peerDependencies"].find(
|
|
3538
|
+
(f) => pkgJson[f]?.[packageName] !== void 0
|
|
3539
|
+
);
|
|
3540
|
+
commandConstraints.workspace = workspace ?? declaringWorkspace.name;
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
2563
3543
|
if (!depField) {
|
|
2564
3544
|
return {
|
|
2565
3545
|
packageName,
|
|
@@ -2575,9 +3555,14 @@ var applyVersionBumpTool = defineTool({
|
|
|
2575
3555
|
const prefixMatch = currentRange.match(/^([~^]?)/);
|
|
2576
3556
|
const prefix = prefixMatch?.[1] ?? "";
|
|
2577
3557
|
const newRange = `${prefix}${toVersion}`;
|
|
3558
|
+
const effectiveInstallCommand = resolveInstallCommand(pm, commandConstraints, yarnMajor);
|
|
3559
|
+
const effectiveTestCommand = resolveTestCommand(pm, commandConstraints);
|
|
3560
|
+
const effectiveTypecheckCommand = resolveTypecheckCommand(pm, commandConstraints, cwd);
|
|
3561
|
+
const effectiveDedupeCommand = resolveDedupeCommand(pm, commandConstraints);
|
|
3562
|
+
const shouldTypecheck = typecheck || Boolean(commandConstraints.typecheck);
|
|
2578
3563
|
if (dryRun) {
|
|
2579
|
-
const installCmd =
|
|
2580
|
-
const testCmd =
|
|
3564
|
+
const installCmd = effectiveInstallCommand.join(" ");
|
|
3565
|
+
const testCmd = effectiveTestCommand.join(" ");
|
|
2581
3566
|
return {
|
|
2582
3567
|
packageName,
|
|
2583
3568
|
strategy: "version-bump",
|
|
@@ -2585,75 +3570,139 @@ var applyVersionBumpTool = defineTool({
|
|
|
2585
3570
|
toVersion,
|
|
2586
3571
|
applied: false,
|
|
2587
3572
|
dryRun: true,
|
|
2588
|
-
message: `[DRY RUN] Would update ${depField}.${packageName}: "${currentRange}" -> "${newRange}", then run ${installCmd}${runTests ? ` and ${testCmd}` : ""}${
|
|
3573
|
+
message: `[DRY RUN] Would update ${depField}.${packageName}: "${currentRange}" -> "${newRange}", then run ${installCmd}${runTests ? ` and ${testCmd}` : ""}${effectiveDedupeCommand.length > 0 ? ` and ${effectiveDedupeCommand.join(" ")} (best-effort)` : ""}.`
|
|
2589
3574
|
};
|
|
2590
3575
|
}
|
|
2591
3576
|
return withRepoLock(cwd, async () => {
|
|
2592
3577
|
pkgJson[depField][packageName] = newRange;
|
|
2593
|
-
|
|
3578
|
+
writeFileSync4(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
2594
3579
|
try {
|
|
2595
|
-
const [installCmd, ...installArgs] =
|
|
3580
|
+
const [installCmd, ...installArgs] = effectiveInstallCommand;
|
|
2596
3581
|
await safeExeca(installCmd, installArgs, {
|
|
2597
3582
|
cwd,
|
|
2598
3583
|
stdio: "pipe"
|
|
2599
3584
|
});
|
|
2600
3585
|
} catch (err) {
|
|
2601
3586
|
pkgJson[depField][packageName] = currentRange;
|
|
2602
|
-
|
|
3587
|
+
writeFileSync4(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
2603
3588
|
const message = err instanceof Error ? err.message : String(err);
|
|
2604
3589
|
return {
|
|
2605
3590
|
packageName,
|
|
2606
|
-
strategy: "version-bump",
|
|
3591
|
+
strategy: "version-bump",
|
|
3592
|
+
fromVersion,
|
|
3593
|
+
toVersion,
|
|
3594
|
+
applied: false,
|
|
3595
|
+
dryRun: false,
|
|
3596
|
+
unresolvedReason: "install-failed",
|
|
3597
|
+
message: `${effectiveInstallCommand.join(" ")} failed after updating "${packageName}" to ${toVersion}. Reverted. Error: ${message}`
|
|
3598
|
+
};
|
|
3599
|
+
}
|
|
3600
|
+
const shouldAttemptRefactor = Boolean(
|
|
3601
|
+
breakingChangeRefactor ?? loadedPolicy.constraints?.enableBreakingChangeRefactor
|
|
3602
|
+
);
|
|
3603
|
+
async function runValidationSteps() {
|
|
3604
|
+
if (runTests) {
|
|
3605
|
+
try {
|
|
3606
|
+
const [testCmd, ...testArgs] = effectiveTestCommand;
|
|
3607
|
+
await safeExeca(testCmd, testArgs, { cwd, stdio: "pipe" });
|
|
3608
|
+
} catch (err) {
|
|
3609
|
+
const execErr = err;
|
|
3610
|
+
const fullOutput = [execErr.stdout, execErr.stderr, execErr.message].filter(Boolean).join("\n");
|
|
3611
|
+
return {
|
|
3612
|
+
passed: false,
|
|
3613
|
+
error: fullOutput || (err instanceof Error ? err.message : String(err))
|
|
3614
|
+
};
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
if (shouldTypecheck && effectiveTypecheckCommand.length > 0) {
|
|
3618
|
+
try {
|
|
3619
|
+
const [tcCmd, ...tcArgs] = effectiveTypecheckCommand;
|
|
3620
|
+
await safeExeca(tcCmd, tcArgs, { cwd, stdio: "pipe" });
|
|
3621
|
+
} catch (err) {
|
|
3622
|
+
const execErr = err;
|
|
3623
|
+
const fullOutput = [execErr.stdout, execErr.stderr, execErr.message].filter(Boolean).join("\n");
|
|
3624
|
+
return {
|
|
3625
|
+
passed: false,
|
|
3626
|
+
isTypecheck: true,
|
|
3627
|
+
error: fullOutput || (err instanceof Error ? err.message : String(err))
|
|
3628
|
+
};
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
return { passed: true };
|
|
3632
|
+
}
|
|
3633
|
+
let validationResult = await runValidationSteps();
|
|
3634
|
+
let refactoredFilesList;
|
|
3635
|
+
if (!validationResult.passed && shouldAttemptRefactor) {
|
|
3636
|
+
const callSites = findPackageCallSites(cwd, packageName);
|
|
3637
|
+
if (callSites.length > 0) {
|
|
3638
|
+
const refactorAttempt = await refactorConsumerCallSites({
|
|
3639
|
+
cwd,
|
|
3640
|
+
packageName,
|
|
3641
|
+
fromVersion,
|
|
3642
|
+
toVersion,
|
|
3643
|
+
diagnosticError: validationResult.error || "",
|
|
3644
|
+
callSites
|
|
3645
|
+
});
|
|
3646
|
+
if (refactorAttempt.applied) {
|
|
3647
|
+
const postRefactorValidation = await runValidationSteps();
|
|
3648
|
+
if (postRefactorValidation.passed) {
|
|
3649
|
+
validationResult = { passed: true };
|
|
3650
|
+
refactoredFilesList = refactorAttempt.refactoredFiles;
|
|
3651
|
+
} else {
|
|
3652
|
+
revertRefactorSnapshots(refactorAttempt.snapshots);
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
if (!validationResult.passed) {
|
|
3658
|
+
pkgJson[depField][packageName] = currentRange;
|
|
3659
|
+
writeFileSync4(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
3660
|
+
try {
|
|
3661
|
+
const [rollbackCmd, ...rollbackArgs] = effectiveInstallCommand;
|
|
3662
|
+
await safeExeca(rollbackCmd, rollbackArgs, {
|
|
3663
|
+
cwd,
|
|
3664
|
+
stdio: "pipe"
|
|
3665
|
+
});
|
|
3666
|
+
} catch {
|
|
3667
|
+
}
|
|
3668
|
+
const failureMessage = validationResult.error || "Unknown validation error";
|
|
3669
|
+
const prefix2 = validationResult.isTypecheck ? `TypeCheck validation failed (${effectiveTypecheckCommand.join(" ")})` : `${effectiveTestCommand.join(" ")} failed`;
|
|
3670
|
+
return {
|
|
3671
|
+
packageName,
|
|
3672
|
+
strategy: "version-bump",
|
|
3673
|
+
fromVersion,
|
|
3674
|
+
toVersion,
|
|
3675
|
+
applied: false,
|
|
3676
|
+
dryRun: false,
|
|
3677
|
+
unresolvedReason: shouldAttemptRefactor ? "breaking-change-unresolved" : "validation-failed",
|
|
3678
|
+
message: `${prefix2} after upgrading "${packageName}" to ${toVersion}. Rolled back to ${currentRange}. Error: ${failureMessage}`
|
|
3679
|
+
};
|
|
3680
|
+
}
|
|
3681
|
+
if (refactoredFilesList && refactoredFilesList.length > 0) {
|
|
3682
|
+
return {
|
|
3683
|
+
packageName,
|
|
3684
|
+
strategy: "breaking-change-refactor",
|
|
2607
3685
|
fromVersion,
|
|
2608
3686
|
toVersion,
|
|
2609
|
-
applied:
|
|
3687
|
+
applied: true,
|
|
2610
3688
|
dryRun: false,
|
|
2611
|
-
|
|
2612
|
-
message:
|
|
3689
|
+
refactoredFiles: refactoredFilesList,
|
|
3690
|
+
message: `Upgraded "${packageName}" from ${fromVersion} to ${toVersion} and refactored ${refactoredFilesList.length} call-site file(s) across breaking API changes.`
|
|
2613
3691
|
};
|
|
2614
3692
|
}
|
|
2615
|
-
|
|
3693
|
+
let dedupeNote = "";
|
|
3694
|
+
if (effectiveDedupeCommand.length > 0) {
|
|
2616
3695
|
try {
|
|
2617
|
-
const [
|
|
2618
|
-
await safeExeca(
|
|
3696
|
+
const [dedupeCmd, ...dedupeArgs] = effectiveDedupeCommand;
|
|
3697
|
+
await safeExeca(dedupeCmd, dedupeArgs, {
|
|
2619
3698
|
cwd,
|
|
2620
3699
|
stdio: "pipe"
|
|
2621
3700
|
});
|
|
2622
3701
|
} catch (err) {
|
|
2623
|
-
pkgJson[depField][packageName] = currentRange;
|
|
2624
|
-
writeFileSync3(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
2625
|
-
try {
|
|
2626
|
-
const [rollbackCmd, ...rollbackArgs] = installCommand;
|
|
2627
|
-
await safeExeca(rollbackCmd, rollbackArgs, {
|
|
2628
|
-
cwd,
|
|
2629
|
-
stdio: "pipe"
|
|
2630
|
-
});
|
|
2631
|
-
} catch {
|
|
2632
|
-
}
|
|
2633
3702
|
const message = err instanceof Error ? err.message : String(err);
|
|
2634
|
-
|
|
2635
|
-
packageName,
|
|
2636
|
-
strategy: "version-bump",
|
|
2637
|
-
fromVersion,
|
|
2638
|
-
toVersion,
|
|
2639
|
-
applied: false,
|
|
2640
|
-
dryRun: false,
|
|
2641
|
-
unresolvedReason: "validation-failed",
|
|
2642
|
-
message: `${testCommand.join(" ")} failed after upgrading "${packageName}" to ${toVersion}. Rolled back to ${currentRange}. Error: ${message}`
|
|
2643
|
-
};
|
|
3703
|
+
dedupeNote = ` Dedupe warning: ${effectiveDedupeCommand.join(" ")} failed (${message}).`;
|
|
2644
3704
|
}
|
|
2645
3705
|
}
|
|
2646
|
-
let dedupeNote = "";
|
|
2647
|
-
try {
|
|
2648
|
-
const [dedupeCmd, ...dedupeArgs] = dedupeCommand;
|
|
2649
|
-
await safeExeca(dedupeCmd, dedupeArgs, {
|
|
2650
|
-
cwd,
|
|
2651
|
-
stdio: "pipe"
|
|
2652
|
-
});
|
|
2653
|
-
} catch (err) {
|
|
2654
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2655
|
-
dedupeNote = ` Dedupe warning: ${dedupeCommand.join(" ")} failed (${message}).`;
|
|
2656
|
-
}
|
|
2657
3706
|
return {
|
|
2658
3707
|
packageName,
|
|
2659
3708
|
strategy: "version-bump",
|
|
@@ -2661,7 +3710,7 @@ var applyVersionBumpTool = defineTool({
|
|
|
2661
3710
|
toVersion,
|
|
2662
3711
|
applied: true,
|
|
2663
3712
|
dryRun: false,
|
|
2664
|
-
message: `Successfully upgraded "${packageName}" from ${fromVersion} to ${toVersion}, ran ${
|
|
3713
|
+
message: `Successfully upgraded "${packageName}" from ${fromVersion} to ${toVersion} in ${depField}, then ran ${effectiveInstallCommand.join(" ")}${runTests ? ` and passed ${effectiveTestCommand.join(" ")}` : ""}.${dedupeNote}`
|
|
2665
3714
|
};
|
|
2666
3715
|
});
|
|
2667
3716
|
}
|
|
@@ -2669,9 +3718,9 @@ var applyVersionBumpTool = defineTool({
|
|
|
2669
3718
|
|
|
2670
3719
|
// src/remediation/tools/apply-package-override/index.ts
|
|
2671
3720
|
import { z as z5 } from "zod";
|
|
2672
|
-
import { join as
|
|
2673
|
-
import { existsSync as
|
|
2674
|
-
import
|
|
3721
|
+
import { join as join16 } from "path";
|
|
3722
|
+
import { existsSync as existsSync12, readFileSync as readFileSync14, writeFileSync as writeFileSync5 } from "fs";
|
|
3723
|
+
import semver4 from "semver";
|
|
2675
3724
|
|
|
2676
3725
|
// src/remediation/tools/apply-package-override/helpers.ts
|
|
2677
3726
|
async function collectDependencyTrace(cwd, pm, packageName, constraints) {
|
|
@@ -2813,9 +3862,9 @@ var applyPackageOverrideTool = defineTool({
|
|
|
2813
3862
|
workspace
|
|
2814
3863
|
}) => {
|
|
2815
3864
|
const pm = packageManager ?? detectPackageManager(cwd);
|
|
2816
|
-
const pkgPath =
|
|
2817
|
-
const denoJsonPath =
|
|
2818
|
-
const isDenoNative = pm === "deno" && !
|
|
3865
|
+
const pkgPath = join16(cwd, "package.json");
|
|
3866
|
+
const denoJsonPath = join16(cwd, "deno.json");
|
|
3867
|
+
const isDenoNative = pm === "deno" && !existsSync12(pkgPath);
|
|
2819
3868
|
const loadedPolicy = loadPolicy(cwd, policy);
|
|
2820
3869
|
const commandConstraints = {
|
|
2821
3870
|
...loadedPolicy.constraints,
|
|
@@ -2841,7 +3890,7 @@ var applyPackageOverrideTool = defineTool({
|
|
|
2841
3890
|
message: `Policy blocked changes for package "${packageName}".`
|
|
2842
3891
|
};
|
|
2843
3892
|
}
|
|
2844
|
-
const isMajorBump =
|
|
3893
|
+
const isMajorBump = semver4.valid(fromVersion) && semver4.valid(toVersion) && semver4.major(toVersion) > semver4.major(fromVersion);
|
|
2845
3894
|
if (isMajorBump && !loadedPolicy.allowMajorBumps) {
|
|
2846
3895
|
return {
|
|
2847
3896
|
packageName,
|
|
@@ -2857,7 +3906,7 @@ var applyPackageOverrideTool = defineTool({
|
|
|
2857
3906
|
if (isDenoNative) {
|
|
2858
3907
|
let denoJson;
|
|
2859
3908
|
try {
|
|
2860
|
-
denoJson = JSON.parse(
|
|
3909
|
+
denoJson = JSON.parse(readFileSync14(denoJsonPath, "utf8"));
|
|
2861
3910
|
} catch {
|
|
2862
3911
|
return {
|
|
2863
3912
|
packageName,
|
|
@@ -2903,13 +3952,13 @@ var applyPackageOverrideTool = defineTool({
|
|
|
2903
3952
|
}
|
|
2904
3953
|
return withRepoLock(cwd, async () => {
|
|
2905
3954
|
setDenoJsonImportValue(denoJson, overrideSelector, toVersion);
|
|
2906
|
-
|
|
3955
|
+
writeFileSync5(denoJsonPath, JSON.stringify(denoJson, null, 2) + "\n", "utf8");
|
|
2907
3956
|
try {
|
|
2908
3957
|
const [installCmd, ...installArgs] = installCommand;
|
|
2909
3958
|
await safeExeca(installCmd, installArgs, { cwd, stdio: "pipe" });
|
|
2910
3959
|
} catch (err) {
|
|
2911
3960
|
restoreDenoJsonImportValue(denoJson, overrideSelector, existingEntry);
|
|
2912
|
-
|
|
3961
|
+
writeFileSync5(denoJsonPath, JSON.stringify(denoJson, null, 2) + "\n", "utf8");
|
|
2913
3962
|
const message = err instanceof Error ? err.message : String(err);
|
|
2914
3963
|
return {
|
|
2915
3964
|
packageName,
|
|
@@ -2928,7 +3977,7 @@ var applyPackageOverrideTool = defineTool({
|
|
|
2928
3977
|
await safeExeca(testCmd, testArgs, { cwd, stdio: "pipe" });
|
|
2929
3978
|
} catch (err) {
|
|
2930
3979
|
restoreDenoJsonImportValue(denoJson, overrideSelector, existingEntry);
|
|
2931
|
-
|
|
3980
|
+
writeFileSync5(denoJsonPath, JSON.stringify(denoJson, null, 2) + "\n", "utf8");
|
|
2932
3981
|
try {
|
|
2933
3982
|
const [rollbackCmd, ...rollbackArgs] = installCommand;
|
|
2934
3983
|
await safeExeca(rollbackCmd, rollbackArgs, { cwd, stdio: "pipe" });
|
|
@@ -2960,7 +4009,7 @@ var applyPackageOverrideTool = defineTool({
|
|
|
2960
4009
|
}
|
|
2961
4010
|
let pkgJson;
|
|
2962
4011
|
try {
|
|
2963
|
-
pkgJson = JSON.parse(
|
|
4012
|
+
pkgJson = JSON.parse(readFileSync14(pkgPath, "utf8"));
|
|
2964
4013
|
} catch {
|
|
2965
4014
|
return {
|
|
2966
4015
|
packageName,
|
|
@@ -2990,13 +4039,13 @@ var applyPackageOverrideTool = defineTool({
|
|
|
2990
4039
|
}
|
|
2991
4040
|
return withRepoLock(cwd, async () => {
|
|
2992
4041
|
setOverrideValue(pkgJson, pm, overrideSelector, toVersion);
|
|
2993
|
-
|
|
4042
|
+
writeFileSync5(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
2994
4043
|
try {
|
|
2995
4044
|
const [installCmd, ...installArgs] = installCommand;
|
|
2996
4045
|
await safeExeca(installCmd, installArgs, { cwd, stdio: "pipe" });
|
|
2997
4046
|
} catch (err) {
|
|
2998
4047
|
restoreOverrideValue(pkgJson, pm, overrideSelector, previousValue);
|
|
2999
|
-
|
|
4048
|
+
writeFileSync5(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
3000
4049
|
const message = err instanceof Error ? err.message : String(err);
|
|
3001
4050
|
return {
|
|
3002
4051
|
packageName,
|
|
@@ -3015,7 +4064,7 @@ var applyPackageOverrideTool = defineTool({
|
|
|
3015
4064
|
await safeExeca(testCmd, testArgs, { cwd, stdio: "pipe" });
|
|
3016
4065
|
} catch (err) {
|
|
3017
4066
|
restoreOverrideValue(pkgJson, pm, overrideSelector, previousValue);
|
|
3018
|
-
|
|
4067
|
+
writeFileSync5(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
3019
4068
|
try {
|
|
3020
4069
|
const [rollbackCmd, ...rollbackArgs] = installCommand;
|
|
3021
4070
|
await safeExeca(rollbackCmd, rollbackArgs, { cwd, stdio: "pipe" });
|
|
@@ -3057,6 +4106,80 @@ var applyPackageOverrideTool = defineTool({
|
|
|
3057
4106
|
}
|
|
3058
4107
|
});
|
|
3059
4108
|
|
|
4109
|
+
// src/platform/supply-chain.ts
|
|
4110
|
+
async function verifyPackageTrust(packageName, targetVersion, policy) {
|
|
4111
|
+
if (!policy || !policy.minPublishAgeHours && !policy.blockInstallScripts && !policy.requireProvenance) {
|
|
4112
|
+
return { trusted: true };
|
|
4113
|
+
}
|
|
4114
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;
|
|
4115
|
+
let packument = null;
|
|
4116
|
+
try {
|
|
4117
|
+
const res = await httpClient({
|
|
4118
|
+
url,
|
|
4119
|
+
headers: { Accept: "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8" }
|
|
4120
|
+
});
|
|
4121
|
+
if (res.ok && res.data) {
|
|
4122
|
+
packument = res.data;
|
|
4123
|
+
}
|
|
4124
|
+
} catch (err) {
|
|
4125
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4126
|
+
return {
|
|
4127
|
+
trusted: false,
|
|
4128
|
+
reason: `Could not verify supply-chain trust for "${packageName}@${targetVersion}": ${msg}`
|
|
4129
|
+
};
|
|
4130
|
+
}
|
|
4131
|
+
if (!packument) {
|
|
4132
|
+
return {
|
|
4133
|
+
trusted: false,
|
|
4134
|
+
reason: `Package packument for "${packageName}" not found on registry.`
|
|
4135
|
+
};
|
|
4136
|
+
}
|
|
4137
|
+
let publishAgeHours;
|
|
4138
|
+
if (packument.time?.[targetVersion]) {
|
|
4139
|
+
const publishedAt = new Date(packument.time[targetVersion]).getTime();
|
|
4140
|
+
publishAgeHours = Math.max(0, (Date.now() - publishedAt) / (1e3 * 60 * 60));
|
|
4141
|
+
if (policy.minPublishAgeHours && publishAgeHours < policy.minPublishAgeHours) {
|
|
4142
|
+
return {
|
|
4143
|
+
trusted: false,
|
|
4144
|
+
publishAgeHours,
|
|
4145
|
+
reason: `Target release "${packageName}@${targetVersion}" was published only ${publishAgeHours.toFixed(1)}h ago (minimum required age is ${policy.minPublishAgeHours}h).`
|
|
4146
|
+
};
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
const versionData = packument.versions?.[targetVersion];
|
|
4150
|
+
const scripts = versionData?.scripts ?? {};
|
|
4151
|
+
const installScriptKeys = ["preinstall", "install", "postinstall"].filter(
|
|
4152
|
+
(k) => Boolean(scripts[k])
|
|
4153
|
+
);
|
|
4154
|
+
const hasInstallScripts = installScriptKeys.length > 0;
|
|
4155
|
+
if (policy.blockInstallScripts && hasInstallScripts) {
|
|
4156
|
+
return {
|
|
4157
|
+
trusted: false,
|
|
4158
|
+
publishAgeHours,
|
|
4159
|
+
hasInstallScripts: true,
|
|
4160
|
+
reason: `Target release "${packageName}@${targetVersion}" defines risky lifecycle scripts (${installScriptKeys.join(", ")}).`
|
|
4161
|
+
};
|
|
4162
|
+
}
|
|
4163
|
+
const hasProvenance = Boolean(
|
|
4164
|
+
versionData?.dist?.attestations?.provenance || versionData?.dist?.attestations?.url
|
|
4165
|
+
);
|
|
4166
|
+
if (policy.requireProvenance && !hasProvenance) {
|
|
4167
|
+
return {
|
|
4168
|
+
trusted: false,
|
|
4169
|
+
publishAgeHours,
|
|
4170
|
+
hasInstallScripts,
|
|
4171
|
+
hasProvenance: false,
|
|
4172
|
+
reason: `Target release "${packageName}@${targetVersion}" does not have cryptographic build provenance attestations.`
|
|
4173
|
+
};
|
|
4174
|
+
}
|
|
4175
|
+
return {
|
|
4176
|
+
trusted: true,
|
|
4177
|
+
publishAgeHours,
|
|
4178
|
+
hasInstallScripts,
|
|
4179
|
+
hasProvenance
|
|
4180
|
+
};
|
|
4181
|
+
}
|
|
4182
|
+
|
|
3060
4183
|
// src/remediation/local/primary-strategy.ts
|
|
3061
4184
|
async function resolvePrimaryResult(params) {
|
|
3062
4185
|
const { vulnerable, cwd, packageManager, dryRun, policy, runTests, constraints } = params;
|
|
@@ -3127,6 +4250,26 @@ async function resolvePrimaryResult(params) {
|
|
|
3127
4250
|
}
|
|
3128
4251
|
};
|
|
3129
4252
|
}
|
|
4253
|
+
const trust2 = await verifyPackageTrust(
|
|
4254
|
+
pkg.name,
|
|
4255
|
+
safeUpgrade2.safeVersion,
|
|
4256
|
+
constraints.supplyChainPolicy
|
|
4257
|
+
);
|
|
4258
|
+
if (!trust2.trusted) {
|
|
4259
|
+
return {
|
|
4260
|
+
steps: 1,
|
|
4261
|
+
result: {
|
|
4262
|
+
packageName: pkg.name,
|
|
4263
|
+
strategy: "none",
|
|
4264
|
+
fromVersion: pkg.version,
|
|
4265
|
+
applied: false,
|
|
4266
|
+
dryRun,
|
|
4267
|
+
unresolvedReason: "policy-blocked",
|
|
4268
|
+
vulnerableRange: vulnerable.affected.vulnerableRange,
|
|
4269
|
+
message: `Supply chain trust gate blocked "${pkg.name}@${safeUpgrade2.safeVersion}": ${trust2.reason}`
|
|
4270
|
+
}
|
|
4271
|
+
};
|
|
4272
|
+
}
|
|
3130
4273
|
const overrideResult = await applyPackageOverrideTool.execute({
|
|
3131
4274
|
cwd,
|
|
3132
4275
|
packageManager,
|
|
@@ -3182,6 +4325,26 @@ async function resolvePrimaryResult(params) {
|
|
|
3182
4325
|
}
|
|
3183
4326
|
};
|
|
3184
4327
|
}
|
|
4328
|
+
const trust = await verifyPackageTrust(
|
|
4329
|
+
pkg.name,
|
|
4330
|
+
safeUpgrade.safeVersion,
|
|
4331
|
+
constraints.supplyChainPolicy
|
|
4332
|
+
);
|
|
4333
|
+
if (!trust.trusted) {
|
|
4334
|
+
return {
|
|
4335
|
+
steps: 1,
|
|
4336
|
+
result: {
|
|
4337
|
+
packageName: pkg.name,
|
|
4338
|
+
strategy: "none",
|
|
4339
|
+
fromVersion: pkg.version,
|
|
4340
|
+
applied: false,
|
|
4341
|
+
dryRun,
|
|
4342
|
+
unresolvedReason: "policy-blocked",
|
|
4343
|
+
vulnerableRange: vulnerable.affected.vulnerableRange,
|
|
4344
|
+
message: `Supply chain trust gate blocked "${pkg.name}@${safeUpgrade.safeVersion}": ${trust.reason}`
|
|
4345
|
+
}
|
|
4346
|
+
};
|
|
4347
|
+
}
|
|
3185
4348
|
const applyResult = await applyVersionBumpTool.execute({
|
|
3186
4349
|
cwd,
|
|
3187
4350
|
packageManager,
|
|
@@ -3191,10 +4354,12 @@ async function resolvePrimaryResult(params) {
|
|
|
3191
4354
|
dryRun,
|
|
3192
4355
|
policy,
|
|
3193
4356
|
runTests,
|
|
4357
|
+
typecheck: constraints.typecheck,
|
|
3194
4358
|
installMode: constraints.installMode,
|
|
3195
4359
|
installPreferOffline: constraints.installPreferOffline,
|
|
3196
4360
|
enforceFrozenLockfile: constraints.enforceFrozenLockfile,
|
|
3197
|
-
workspace: constraints.workspace
|
|
4361
|
+
workspace: constraints.workspace,
|
|
4362
|
+
breakingChangeRefactor: constraints.enableBreakingChangeRefactor
|
|
3198
4363
|
});
|
|
3199
4364
|
return {
|
|
3200
4365
|
steps: 2,
|
|
@@ -3204,10 +4369,10 @@ async function resolvePrimaryResult(params) {
|
|
|
3204
4369
|
|
|
3205
4370
|
// src/remediation/tools/fetch-package-source.ts
|
|
3206
4371
|
import { z as z6 } from "zod";
|
|
3207
|
-
import { mkdir as mkdir2, mkdtemp, readdir as readdir2, readFile as
|
|
3208
|
-
import { join as
|
|
4372
|
+
import { mkdir as mkdir2, mkdtemp, readdir as readdir2, readFile as readFile4, rm as rm3 } from "fs/promises";
|
|
4373
|
+
import { join as join17 } from "path";
|
|
3209
4374
|
import { tmpdir } from "os";
|
|
3210
|
-
import { execa as
|
|
4375
|
+
import { execa as execa7 } from "execa";
|
|
3211
4376
|
var fetchPackageSourceTool = defineTool({
|
|
3212
4377
|
description: "Download package tarball from npm and extract source files for CVE analysis. Supports custom file patterns (default: *.js, *.ts).",
|
|
3213
4378
|
parameters: z6.object({
|
|
@@ -3225,25 +4390,25 @@ var fetchPackageSourceTool = defineTool({
|
|
|
3225
4390
|
if (safePatterns.length === 0) {
|
|
3226
4391
|
return { success: false, error: "No valid file patterns provided." };
|
|
3227
4392
|
}
|
|
3228
|
-
const tempBaseDir = await mkdtemp(
|
|
3229
|
-
const extractDir =
|
|
4393
|
+
const tempBaseDir = await mkdtemp(join17(tmpdir(), "autoremediator-pkg-"));
|
|
4394
|
+
const extractDir = join17(tempBaseDir, "out");
|
|
3230
4395
|
try {
|
|
3231
4396
|
const scopedName = packageName.split("/").pop();
|
|
3232
4397
|
const npmUrl = `https://registry.npmjs.org/${packageName}/-/${scopedName}-${version}.tgz`;
|
|
3233
4398
|
await mkdir2(tempBaseDir, { recursive: true });
|
|
3234
|
-
const tarballPath =
|
|
3235
|
-
await
|
|
4399
|
+
const tarballPath = join17(tempBaseDir, "package.tgz");
|
|
4400
|
+
await execa7("curl", ["-L", "-o", tarballPath, npmUrl]);
|
|
3236
4401
|
await mkdir2(extractDir, { recursive: true });
|
|
3237
|
-
await
|
|
4402
|
+
await execa7("tar", ["-xzf", tarballPath, "-C", extractDir]);
|
|
3238
4403
|
const extractedContents = await readdir2(extractDir);
|
|
3239
|
-
const packageRootDir = extractedContents.includes("package") ?
|
|
4404
|
+
const packageRootDir = extractedContents.includes("package") ? join17(extractDir, "package") : extractDir;
|
|
3240
4405
|
const sourceCode = {};
|
|
3241
4406
|
async function walkDir(dir, relativeBase) {
|
|
3242
4407
|
try {
|
|
3243
4408
|
const files = await readdir2(dir, { withFileTypes: true });
|
|
3244
4409
|
for (const file of files) {
|
|
3245
|
-
const fullPath =
|
|
3246
|
-
const relPath =
|
|
4410
|
+
const fullPath = join17(dir, file.name);
|
|
4411
|
+
const relPath = join17(relativeBase, file.name);
|
|
3247
4412
|
if (file.isDirectory()) {
|
|
3248
4413
|
if (!["node_modules", ".git", "dist", "build", "coverage", ".next", "out"].includes(
|
|
3249
4414
|
file.name
|
|
@@ -3257,7 +4422,7 @@ var fetchPackageSourceTool = defineTool({
|
|
|
3257
4422
|
});
|
|
3258
4423
|
if (matches) {
|
|
3259
4424
|
try {
|
|
3260
|
-
const content = await
|
|
4425
|
+
const content = await readFile4(fullPath, "utf8");
|
|
3261
4426
|
sourceCode[relPath] = content;
|
|
3262
4427
|
} catch {
|
|
3263
4428
|
}
|
|
@@ -3292,7 +4457,7 @@ var fetchPackageSourceTool = defineTool({
|
|
|
3292
4457
|
error: `Failed to fetch and extract package ${packageName}@${version}: ${message}`
|
|
3293
4458
|
};
|
|
3294
4459
|
} finally {
|
|
3295
|
-
await
|
|
4460
|
+
await rm3(tempBaseDir, { recursive: true, force: true });
|
|
3296
4461
|
}
|
|
3297
4462
|
}
|
|
3298
4463
|
});
|
|
@@ -3350,6 +4515,7 @@ Analyze the source code to:
|
|
|
3350
4515
|
2. Explain the root cause of the security issue
|
|
3351
4516
|
3. Propose a secure fix that addresses the vulnerability
|
|
3352
4517
|
4. Provide the complete fixed version of affected files
|
|
4518
|
+
5. Optionally provide a standalone Node.js reproduction/regression test script
|
|
3353
4519
|
|
|
3354
4520
|
## Response Format
|
|
3355
4521
|
Respond ONLY with valid JSON (no markdown, no extra text):
|
|
@@ -3360,7 +4526,8 @@ Respond ONLY with valid JSON (no markdown, no extra text):
|
|
|
3360
4526
|
"path/to/other.ts": "Complete fixed source code for this file"
|
|
3361
4527
|
},
|
|
3362
4528
|
"confidence": 0.95,
|
|
3363
|
-
"riskLevel": "medium"
|
|
4529
|
+
"riskLevel": "medium",
|
|
4530
|
+
"reproductionTest": "// Node.js script asserting the patch resolves the exploit/regression"
|
|
3364
4531
|
}
|
|
3365
4532
|
|
|
3366
4533
|
Important:
|
|
@@ -3371,7 +4538,7 @@ Important:
|
|
|
3371
4538
|
}
|
|
3372
4539
|
|
|
3373
4540
|
// src/remediation/tools/generate-patch/helpers.ts
|
|
3374
|
-
import { parseSync as
|
|
4541
|
+
import { parseSync as parseSync3 } from "oxc-parser";
|
|
3375
4542
|
import YAML from "yaml";
|
|
3376
4543
|
function parseLlmAnalysisResponse(text) {
|
|
3377
4544
|
try {
|
|
@@ -3394,7 +4561,7 @@ function validatePatchedCodeSyntax(filePath, code) {
|
|
|
3394
4561
|
const lowerPath = filePath.toLowerCase();
|
|
3395
4562
|
if (/\.[cm]?[jt]sx?$/i.test(lowerPath)) {
|
|
3396
4563
|
try {
|
|
3397
|
-
const res =
|
|
4564
|
+
const res = parseSync3(filePath, code);
|
|
3398
4565
|
if (res.errors && res.errors.length > 0) {
|
|
3399
4566
|
const errorMsg = res.errors.map((e) => e.message).join("; ");
|
|
3400
4567
|
return {
|
|
@@ -3640,6 +4807,7 @@ var generatePatchTool = defineTool({
|
|
|
3640
4807
|
success: true,
|
|
3641
4808
|
patches,
|
|
3642
4809
|
patchContent: patches[0]?.unifiedDiff,
|
|
4810
|
+
reproductionTest: analysis.reproductionTest,
|
|
3643
4811
|
llmProvider: provider,
|
|
3644
4812
|
llmModel: modelName,
|
|
3645
4813
|
latencyMs,
|
|
@@ -3664,17 +4832,126 @@ var generatePatchTool = defineTool({
|
|
|
3664
4832
|
|
|
3665
4833
|
// src/remediation/tools/apply-patch-file/index.ts
|
|
3666
4834
|
import { z as z8 } from "zod";
|
|
3667
|
-
import { mkdir as mkdir3, writeFile as
|
|
3668
|
-
import {
|
|
3669
|
-
import {
|
|
4835
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
4836
|
+
import { existsSync as existsSync14 } from "fs";
|
|
4837
|
+
import { isAbsolute as isAbsolute4, join as join19 } from "path";
|
|
4838
|
+
import { execa as execa9 } from "execa";
|
|
4839
|
+
|
|
4840
|
+
// src/remediation/proof-of-fix/runner.ts
|
|
4841
|
+
async function runProofOfFixPhase(options) {
|
|
4842
|
+
const { stage, testFilePath, cwd, timeoutMs = 15e3 } = options;
|
|
4843
|
+
const started = Date.now();
|
|
4844
|
+
const isTs = testFilePath.endsWith(".ts");
|
|
4845
|
+
const nodeArgs = isTs ? ["--experimental-strip-types", testFilePath] : [testFilePath];
|
|
4846
|
+
try {
|
|
4847
|
+
const result = await safeExeca("node", nodeArgs, {
|
|
4848
|
+
cwd,
|
|
4849
|
+
timeoutMs,
|
|
4850
|
+
stdio: "pipe"
|
|
4851
|
+
});
|
|
4852
|
+
const durationMs = Date.now() - started;
|
|
4853
|
+
const output = `${result.stdout || ""}
|
|
4854
|
+
${result.stderr || ""}`.trim();
|
|
4855
|
+
if (stage === "negative") {
|
|
4856
|
+
return {
|
|
4857
|
+
executed: true,
|
|
4858
|
+
passed: false,
|
|
4859
|
+
exitCode: 0,
|
|
4860
|
+
output,
|
|
4861
|
+
durationMs,
|
|
4862
|
+
error: "Exploit reproduction test passed on vulnerable target (failed to reproduce vulnerability)."
|
|
4863
|
+
};
|
|
4864
|
+
}
|
|
4865
|
+
return {
|
|
4866
|
+
executed: true,
|
|
4867
|
+
passed: true,
|
|
4868
|
+
exitCode: 0,
|
|
4869
|
+
output,
|
|
4870
|
+
durationMs
|
|
4871
|
+
};
|
|
4872
|
+
} catch (err) {
|
|
4873
|
+
const durationMs = Date.now() - started;
|
|
4874
|
+
const execaErr = err;
|
|
4875
|
+
const exitCode = typeof execaErr.exitCode === "number" ? execaErr.exitCode : 1;
|
|
4876
|
+
const errorMsg = execaErr.message || String(err);
|
|
4877
|
+
const output = [execaErr.stdout, execaErr.stderr, errorMsg].filter(Boolean).join("\n").trim();
|
|
4878
|
+
if (stage === "negative") {
|
|
4879
|
+
if (execaErr.timedOut || errorMsg.includes("timed out")) {
|
|
4880
|
+
return {
|
|
4881
|
+
executed: true,
|
|
4882
|
+
passed: false,
|
|
4883
|
+
exitCode,
|
|
4884
|
+
output,
|
|
4885
|
+
durationMs,
|
|
4886
|
+
error: "Exploit reproduction test timed out (execution hung)."
|
|
4887
|
+
};
|
|
4888
|
+
}
|
|
4889
|
+
const isHarnessFailure = exitCode === 127 || errorMsg.includes("ENOENT") || output.includes("bad option:") || output.includes("Cannot find module") && output.includes(testFilePath);
|
|
4890
|
+
if (isHarnessFailure) {
|
|
4891
|
+
return {
|
|
4892
|
+
executed: false,
|
|
4893
|
+
passed: false,
|
|
4894
|
+
exitCode,
|
|
4895
|
+
output,
|
|
4896
|
+
durationMs,
|
|
4897
|
+
error: `Reproduction test failed to execute (harness error): ${errorMsg}`
|
|
4898
|
+
};
|
|
4899
|
+
}
|
|
4900
|
+
return {
|
|
4901
|
+
executed: true,
|
|
4902
|
+
passed: true,
|
|
4903
|
+
exitCode,
|
|
4904
|
+
output,
|
|
4905
|
+
durationMs
|
|
4906
|
+
};
|
|
4907
|
+
}
|
|
4908
|
+
return {
|
|
4909
|
+
executed: true,
|
|
4910
|
+
passed: false,
|
|
4911
|
+
exitCode,
|
|
4912
|
+
output,
|
|
4913
|
+
durationMs,
|
|
4914
|
+
error: `Reproduction test failed on patched target: ${errorMsg}`
|
|
4915
|
+
};
|
|
4916
|
+
}
|
|
4917
|
+
}
|
|
4918
|
+
function assembleProofOfFixVerification(params) {
|
|
4919
|
+
const { reproductionTestPath, negativePhase, positivePhase, regressionPhase } = params;
|
|
4920
|
+
const regPhase = regressionPhase ?? {
|
|
4921
|
+
executed: false,
|
|
4922
|
+
passed: true
|
|
4923
|
+
};
|
|
4924
|
+
let status = "verified";
|
|
4925
|
+
let failureReason;
|
|
4926
|
+
if (!negativePhase.passed) {
|
|
4927
|
+
status = "failed";
|
|
4928
|
+
failureReason = negativePhase.error || "negative-phase-failed";
|
|
4929
|
+
} else if (!positivePhase.passed) {
|
|
4930
|
+
status = "failed";
|
|
4931
|
+
failureReason = positivePhase.error || "positive-phase-failed";
|
|
4932
|
+
} else if (regPhase.executed && !regPhase.passed) {
|
|
4933
|
+
status = "failed";
|
|
4934
|
+
failureReason = regPhase.error || "regression-phase-failed";
|
|
4935
|
+
}
|
|
4936
|
+
const durationMs = (negativePhase.durationMs || 0) + (positivePhase.durationMs || 0) + (regPhase.durationMs || 0);
|
|
4937
|
+
return {
|
|
4938
|
+
status,
|
|
4939
|
+
reproductionTestPath,
|
|
4940
|
+
negativePhase,
|
|
4941
|
+
positivePhase,
|
|
4942
|
+
regressionPhase: regPhase,
|
|
4943
|
+
durationMs,
|
|
4944
|
+
failureReason
|
|
4945
|
+
};
|
|
4946
|
+
}
|
|
3670
4947
|
|
|
3671
4948
|
// src/remediation/tools/apply-patch-file/helpers.ts
|
|
3672
|
-
import { existsSync as
|
|
3673
|
-
import { mkdtemp as mkdtemp2, readFile as
|
|
4949
|
+
import { existsSync as existsSync13 } from "fs";
|
|
4950
|
+
import { mkdtemp as mkdtemp2, readFile as readFile5, rm as rm4, writeFile as writeFile2 } from "fs/promises";
|
|
3674
4951
|
import { createHash } from "crypto";
|
|
3675
4952
|
import { tmpdir as tmpdir2 } from "os";
|
|
3676
|
-
import { join as
|
|
3677
|
-
import { execa as
|
|
4953
|
+
import { join as join18 } from "path";
|
|
4954
|
+
import { execa as execa8 } from "execa";
|
|
3678
4955
|
async function resolvePatchMode(packageManager, cwd) {
|
|
3679
4956
|
if (packageManager === "npm" || packageManager === "bun" || packageManager === "deno")
|
|
3680
4957
|
return "patch-package";
|
|
@@ -3705,13 +4982,13 @@ function computePatchIntegrity(patchContent) {
|
|
|
3705
4982
|
return `sha256:${hex}`;
|
|
3706
4983
|
}
|
|
3707
4984
|
async function writePatchManifest(manifestFilePath, artifact) {
|
|
3708
|
-
await
|
|
4985
|
+
await writeFile2(manifestFilePath, JSON.stringify(artifact, null, 2) + "\n", "utf8");
|
|
3709
4986
|
}
|
|
3710
4987
|
async function configurePatchPackagePostinstall(cwd, packageManager) {
|
|
3711
|
-
const pkgJsonPath =
|
|
4988
|
+
const pkgJsonPath = join18(cwd, "package.json");
|
|
3712
4989
|
let pkgJson;
|
|
3713
4990
|
try {
|
|
3714
|
-
pkgJson = JSON.parse(await
|
|
4991
|
+
pkgJson = JSON.parse(await readFile5(pkgJsonPath, "utf8"));
|
|
3715
4992
|
} catch {
|
|
3716
4993
|
return {
|
|
3717
4994
|
success: false,
|
|
@@ -3723,7 +5000,7 @@ async function configurePatchPackagePostinstall(cwd, packageManager) {
|
|
|
3723
5000
|
try {
|
|
3724
5001
|
const commands = getPackageManagerCommands(packageManager);
|
|
3725
5002
|
const [cmd, ...args] = commands.installDev("patch-package");
|
|
3726
|
-
await
|
|
5003
|
+
await execa8(cmd, args, {
|
|
3727
5004
|
cwd,
|
|
3728
5005
|
stdio: "pipe"
|
|
3729
5006
|
});
|
|
@@ -3744,13 +5021,13 @@ async function configurePatchPackagePostinstall(cwd, packageManager) {
|
|
|
3744
5021
|
} else if (!currentPostinstall) {
|
|
3745
5022
|
pkgJson.scripts.postinstall = patchApplyCmd;
|
|
3746
5023
|
}
|
|
3747
|
-
await
|
|
5024
|
+
await writeFile2(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
3748
5025
|
return { success: true };
|
|
3749
5026
|
}
|
|
3750
5027
|
async function capturePackageJsonSnapshot(cwd) {
|
|
3751
|
-
const path =
|
|
5028
|
+
const path = join18(cwd, "package.json");
|
|
3752
5029
|
try {
|
|
3753
|
-
const content = await
|
|
5030
|
+
const content = await readFile5(path, "utf8");
|
|
3754
5031
|
return { path, content };
|
|
3755
5032
|
} catch {
|
|
3756
5033
|
return void 0;
|
|
@@ -3766,19 +5043,19 @@ async function cleanupPatchArtifacts(params) {
|
|
|
3766
5043
|
installCommand,
|
|
3767
5044
|
rerunInstall
|
|
3768
5045
|
} = params;
|
|
3769
|
-
await
|
|
5046
|
+
await rm4(patchFilePath, { force: true }).catch(() => void 0);
|
|
3770
5047
|
if (manifestFilePath) {
|
|
3771
|
-
await
|
|
5048
|
+
await rm4(manifestFilePath, { force: true }).catch(() => void 0);
|
|
3772
5049
|
}
|
|
3773
5050
|
if (patchMode === "patch-package" && packageJsonSnapshot) {
|
|
3774
|
-
await
|
|
5051
|
+
await writeFile2(packageJsonSnapshot.path, packageJsonSnapshot.content, "utf8").catch(
|
|
3775
5052
|
() => void 0
|
|
3776
5053
|
);
|
|
3777
5054
|
}
|
|
3778
5055
|
if (!rerunInstall) return;
|
|
3779
5056
|
try {
|
|
3780
5057
|
const [installCmd, ...installArgs] = installCommand;
|
|
3781
|
-
await
|
|
5058
|
+
await execa8(installCmd, installArgs, {
|
|
3782
5059
|
cwd,
|
|
3783
5060
|
stdio: "pipe"
|
|
3784
5061
|
});
|
|
@@ -3792,7 +5069,7 @@ async function applyNativePatch(params) {
|
|
|
3792
5069
|
const createArgs = ["patch", packageSpec];
|
|
3793
5070
|
let patchDir;
|
|
3794
5071
|
try {
|
|
3795
|
-
const createResult = await
|
|
5072
|
+
const createResult = await execa8(createCommand, createArgs, {
|
|
3796
5073
|
cwd,
|
|
3797
5074
|
stdio: "pipe"
|
|
3798
5075
|
});
|
|
@@ -3820,17 +5097,17 @@ ${createResult.stderr}`);
|
|
|
3820
5097
|
error: `Could not determine native patch directory for ${packageSpec}.`
|
|
3821
5098
|
};
|
|
3822
5099
|
}
|
|
3823
|
-
const tempPatchDir = await mkdtemp2(
|
|
3824
|
-
const tempPatchFile =
|
|
5100
|
+
const tempPatchDir = await mkdtemp2(join18(tmpdir2(), "autoremediator-native-patch-"));
|
|
5101
|
+
const tempPatchFile = join18(tempPatchDir, "change.patch");
|
|
3825
5102
|
try {
|
|
3826
|
-
await
|
|
3827
|
-
await
|
|
5103
|
+
await writeFile2(tempPatchFile, patchContent, "utf8");
|
|
5104
|
+
await execa8("patch", ["-p1", "-i", tempPatchFile], {
|
|
3828
5105
|
cwd: patchDir,
|
|
3829
5106
|
stdio: "pipe"
|
|
3830
5107
|
});
|
|
3831
5108
|
const commitCommand = patchMode === "native-pnpm" ? "pnpm" : "yarn";
|
|
3832
5109
|
const commitArgs = patchMode === "native-pnpm" ? ["patch-commit", patchDir] : ["patch-commit", "-s", patchDir];
|
|
3833
|
-
await
|
|
5110
|
+
await execa8(commitCommand, commitArgs, {
|
|
3834
5111
|
cwd,
|
|
3835
5112
|
stdio: "pipe"
|
|
3836
5113
|
});
|
|
@@ -3845,19 +5122,19 @@ ${createResult.stderr}`);
|
|
|
3845
5122
|
error: `Failed to apply native patch for ${packageSpec}: ${err instanceof Error ? err.message : String(err)}`
|
|
3846
5123
|
};
|
|
3847
5124
|
} finally {
|
|
3848
|
-
await
|
|
5125
|
+
await rm4(tempPatchDir, { recursive: true, force: true });
|
|
3849
5126
|
}
|
|
3850
5127
|
return { success: true };
|
|
3851
5128
|
}
|
|
3852
5129
|
function extractPatchDirectory(output) {
|
|
3853
5130
|
const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
3854
5131
|
for (const line of lines) {
|
|
3855
|
-
if (
|
|
5132
|
+
if (existsSync13(line)) {
|
|
3856
5133
|
return line;
|
|
3857
5134
|
}
|
|
3858
5135
|
const tokens = line.split(/\s+/).map((token) => token.replace(/^['"]|['"]$/g, ""));
|
|
3859
5136
|
for (const token of tokens) {
|
|
3860
|
-
if (token.startsWith("/") &&
|
|
5137
|
+
if (token.startsWith("/") && existsSync13(token)) {
|
|
3861
5138
|
return token;
|
|
3862
5139
|
}
|
|
3863
5140
|
}
|
|
@@ -3867,7 +5144,7 @@ function extractPatchDirectory(output) {
|
|
|
3867
5144
|
async function validatePatchWithTests(cwd, testCommand) {
|
|
3868
5145
|
try {
|
|
3869
5146
|
const [cmd, ...args] = testCommand;
|
|
3870
|
-
const result = await
|
|
5147
|
+
const result = await execa8(cmd, args, {
|
|
3871
5148
|
cwd,
|
|
3872
5149
|
timeout: 6e4,
|
|
3873
5150
|
stdio: "pipe"
|
|
@@ -3927,6 +5204,11 @@ var applyPatchFileTool = defineTool({
|
|
|
3927
5204
|
enforceFrozenLockfile: z8.boolean().optional(),
|
|
3928
5205
|
workspace: z8.string().optional(),
|
|
3929
5206
|
validateWithTests: z8.boolean().optional().default(true).describe("Run package manager test command to validate patch doesn't break anything"),
|
|
5207
|
+
reproductionTest: z8.string().optional().describe("Optional synthesized reproduction test script verifying fix"),
|
|
5208
|
+
proofOfFix: z8.union([z8.boolean(), z8.string()]).optional().describe(
|
|
5209
|
+
"If true or script path/content, enforce dual-stage exploit reproduction (assert failure pre-patch, assert pass post-patch)"
|
|
5210
|
+
),
|
|
5211
|
+
requireProofOfFix: z8.boolean().optional().describe("If true, fail and roll back patch if proof-of-fix verification fails"),
|
|
3930
5212
|
dryRun: z8.boolean().optional().default(false).describe("If true, report but do not mutate files")
|
|
3931
5213
|
}).refine((value) => Boolean(value.patchContent || value.patches && value.patches.length > 0), {
|
|
3932
5214
|
message: "Either patchContent or patches must be provided"
|
|
@@ -3948,6 +5230,9 @@ var applyPatchFileTool = defineTool({
|
|
|
3948
5230
|
workspace,
|
|
3949
5231
|
riskLevel,
|
|
3950
5232
|
validateWithTests,
|
|
5233
|
+
reproductionTest,
|
|
5234
|
+
proofOfFix,
|
|
5235
|
+
requireProofOfFix,
|
|
3951
5236
|
dryRun
|
|
3952
5237
|
}) => {
|
|
3953
5238
|
try {
|
|
@@ -3998,7 +5283,7 @@ var applyPatchFileTool = defineTool({
|
|
|
3998
5283
|
};
|
|
3999
5284
|
}
|
|
4000
5285
|
const patchFileName = buildPatchFileName(packageName, vulnerableVersion);
|
|
4001
|
-
const patchFilePath =
|
|
5286
|
+
const patchFilePath = join19(cwd, patchesDir, patchFileName);
|
|
4002
5287
|
const manifestFilePath = `${patchFilePath}.json`;
|
|
4003
5288
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4004
5289
|
const baseArtifact = {
|
|
@@ -4037,9 +5322,9 @@ var applyPatchFileTool = defineTool({
|
|
|
4037
5322
|
}
|
|
4038
5323
|
return withRepoLock(cwd, async () => {
|
|
4039
5324
|
const packageJsonSnapshot = patchModeRequiresPackageJsonSnapshot(pm) ? await capturePackageJsonSnapshot(cwd) : void 0;
|
|
4040
|
-
const patchesDirPath =
|
|
5325
|
+
const patchesDirPath = join19(cwd, patchesDir);
|
|
4041
5326
|
await mkdir3(patchesDirPath, { recursive: true });
|
|
4042
|
-
await
|
|
5327
|
+
await writeFile3(patchFilePath, selectedPatch, "utf8");
|
|
4043
5328
|
validationPhases.push({
|
|
4044
5329
|
phase: "patch-write",
|
|
4045
5330
|
passed: true,
|
|
@@ -4060,6 +5345,101 @@ var applyPatchFileTool = defineTool({
|
|
|
4060
5345
|
});
|
|
4061
5346
|
artifact.validationPhases = validationPhases;
|
|
4062
5347
|
await writePatchManifest(manifestFilePath, artifact);
|
|
5348
|
+
let reproductionTestPath;
|
|
5349
|
+
let negativePhaseResult;
|
|
5350
|
+
if (typeof proofOfFix === "string" && proofOfFix.trim().length > 0) {
|
|
5351
|
+
const resolvedPath = isAbsolute4(proofOfFix) ? proofOfFix : join19(cwd, proofOfFix);
|
|
5352
|
+
if (existsSync14(resolvedPath)) {
|
|
5353
|
+
reproductionTestPath = resolvedPath;
|
|
5354
|
+
} else {
|
|
5355
|
+
const safeName = packageName.replace(/^@/, "").replace(/\//g, "+");
|
|
5356
|
+
reproductionTestPath = join19(patchesDirPath, `${safeName}+${vulnerableVersion}.test.cjs`);
|
|
5357
|
+
await writeFile3(reproductionTestPath, proofOfFix, "utf8");
|
|
5358
|
+
}
|
|
5359
|
+
} else if (reproductionTest) {
|
|
5360
|
+
const safeName = packageName.replace(/^@/, "").replace(/\//g, "+");
|
|
5361
|
+
reproductionTestPath = join19(patchesDirPath, `${safeName}+${vulnerableVersion}.test.cjs`);
|
|
5362
|
+
await writeFile3(reproductionTestPath, reproductionTest, "utf8");
|
|
5363
|
+
}
|
|
5364
|
+
const isProofOfFixActive = Boolean(proofOfFix || requireProofOfFix);
|
|
5365
|
+
if (isProofOfFixActive) {
|
|
5366
|
+
if (!reproductionTestPath) {
|
|
5367
|
+
if (requireProofOfFix) {
|
|
5368
|
+
await cleanupPatchArtifacts({
|
|
5369
|
+
cwd,
|
|
5370
|
+
patchFilePath,
|
|
5371
|
+
manifestFilePath,
|
|
5372
|
+
patchMode,
|
|
5373
|
+
packageJsonSnapshot,
|
|
5374
|
+
installCommand,
|
|
5375
|
+
rerunInstall: false
|
|
5376
|
+
});
|
|
5377
|
+
return {
|
|
5378
|
+
success: false,
|
|
5379
|
+
packageName,
|
|
5380
|
+
vulnerableVersion,
|
|
5381
|
+
applied: false,
|
|
5382
|
+
dryRun: false,
|
|
5383
|
+
message: "Proof-of-fix verification required, but no reproduction test or script was supplied.",
|
|
5384
|
+
patchFilePath,
|
|
5385
|
+
manifestFilePath,
|
|
5386
|
+
patchPath: patchFilePath,
|
|
5387
|
+
patchMode,
|
|
5388
|
+
postinstallConfigured: false,
|
|
5389
|
+
patchArtifact: {
|
|
5390
|
+
...artifact,
|
|
5391
|
+
applied: false,
|
|
5392
|
+
validationPhases
|
|
5393
|
+
},
|
|
5394
|
+
validationPhases,
|
|
5395
|
+
error: "Proof-of-fix verification required, but no reproduction test or script was supplied."
|
|
5396
|
+
};
|
|
5397
|
+
}
|
|
5398
|
+
} else {
|
|
5399
|
+
negativePhaseResult = await runProofOfFixPhase({
|
|
5400
|
+
stage: "negative",
|
|
5401
|
+
testFilePath: reproductionTestPath,
|
|
5402
|
+
cwd
|
|
5403
|
+
});
|
|
5404
|
+
validationPhases.push({
|
|
5405
|
+
phase: "proof-of-fix-negative",
|
|
5406
|
+
passed: negativePhaseResult.passed,
|
|
5407
|
+
message: negativePhaseResult.passed ? "Reproduction test reproduced vulnerability on unpatched code." : void 0,
|
|
5408
|
+
error: negativePhaseResult.passed ? void 0 : negativePhaseResult.error
|
|
5409
|
+
});
|
|
5410
|
+
if (!negativePhaseResult.passed && requireProofOfFix) {
|
|
5411
|
+
await cleanupPatchArtifacts({
|
|
5412
|
+
cwd,
|
|
5413
|
+
patchFilePath,
|
|
5414
|
+
manifestFilePath,
|
|
5415
|
+
patchMode,
|
|
5416
|
+
packageJsonSnapshot,
|
|
5417
|
+
installCommand,
|
|
5418
|
+
rerunInstall: false
|
|
5419
|
+
});
|
|
5420
|
+
return {
|
|
5421
|
+
success: false,
|
|
5422
|
+
packageName,
|
|
5423
|
+
vulnerableVersion,
|
|
5424
|
+
applied: false,
|
|
5425
|
+
dryRun: false,
|
|
5426
|
+
message: `Proof-of-fix negative phase failed: ${negativePhaseResult.error}`,
|
|
5427
|
+
patchFilePath,
|
|
5428
|
+
manifestFilePath,
|
|
5429
|
+
patchPath: patchFilePath,
|
|
5430
|
+
patchMode,
|
|
5431
|
+
postinstallConfigured: false,
|
|
5432
|
+
patchArtifact: {
|
|
5433
|
+
...artifact,
|
|
5434
|
+
applied: false,
|
|
5435
|
+
validationPhases
|
|
5436
|
+
},
|
|
5437
|
+
validationPhases,
|
|
5438
|
+
error: `Proof-of-fix negative phase failed: ${negativePhaseResult.error}`
|
|
5439
|
+
};
|
|
5440
|
+
}
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
4063
5443
|
const applyResult = patchMode === "patch-package" ? await configurePatchPackagePostinstall(cwd, pm) : await applyNativePatch({
|
|
4064
5444
|
cwd,
|
|
4065
5445
|
packageName,
|
|
@@ -4107,7 +5487,7 @@ var applyPatchFileTool = defineTool({
|
|
|
4107
5487
|
if (patchMode === "patch-package") {
|
|
4108
5488
|
try {
|
|
4109
5489
|
const [installCmd, ...installArgs] = installCommand;
|
|
4110
|
-
await
|
|
5490
|
+
await execa9(installCmd, installArgs, {
|
|
4111
5491
|
cwd,
|
|
4112
5492
|
stdio: "pipe"
|
|
4113
5493
|
});
|
|
@@ -4200,11 +5580,90 @@ var applyPatchFileTool = defineTool({
|
|
|
4200
5580
|
message: "Patch validation tests passed."
|
|
4201
5581
|
});
|
|
4202
5582
|
}
|
|
5583
|
+
let positivePhaseResult;
|
|
5584
|
+
if (reproductionTestPath) {
|
|
5585
|
+
if (isProofOfFixActive) {
|
|
5586
|
+
positivePhaseResult = await runProofOfFixPhase({
|
|
5587
|
+
stage: "positive",
|
|
5588
|
+
testFilePath: reproductionTestPath,
|
|
5589
|
+
cwd
|
|
5590
|
+
});
|
|
5591
|
+
validationPhases.push({
|
|
5592
|
+
phase: "proof-of-fix-positive",
|
|
5593
|
+
passed: positivePhaseResult.passed,
|
|
5594
|
+
message: positivePhaseResult.passed ? "Reproduction test passed on patched code (vulnerability mitigated)." : void 0,
|
|
5595
|
+
error: positivePhaseResult.passed ? void 0 : positivePhaseResult.error
|
|
5596
|
+
});
|
|
5597
|
+
if (!positivePhaseResult.passed && (requireProofOfFix || validateWithTests)) {
|
|
5598
|
+
await cleanupPatchArtifacts({
|
|
5599
|
+
cwd,
|
|
5600
|
+
patchFilePath,
|
|
5601
|
+
manifestFilePath,
|
|
5602
|
+
patchMode,
|
|
5603
|
+
packageJsonSnapshot,
|
|
5604
|
+
installCommand,
|
|
5605
|
+
rerunInstall: patchMode === "patch-package"
|
|
5606
|
+
});
|
|
5607
|
+
return {
|
|
5608
|
+
success: false,
|
|
5609
|
+
packageName,
|
|
5610
|
+
vulnerableVersion,
|
|
5611
|
+
applied: false,
|
|
5612
|
+
dryRun: false,
|
|
5613
|
+
message: `Proof-of-fix positive phase failed: ${positivePhaseResult.error}`,
|
|
5614
|
+
patchFilePath,
|
|
5615
|
+
manifestFilePath,
|
|
5616
|
+
patchPath: patchFilePath,
|
|
5617
|
+
patchMode,
|
|
5618
|
+
postinstallConfigured: false,
|
|
5619
|
+
patchArtifact: {
|
|
5620
|
+
...artifact,
|
|
5621
|
+
applied: false,
|
|
5622
|
+
validationPhases
|
|
5623
|
+
},
|
|
5624
|
+
validationPhases,
|
|
5625
|
+
error: `Proof-of-fix positive phase failed: ${positivePhaseResult.error}`
|
|
5626
|
+
};
|
|
5627
|
+
}
|
|
5628
|
+
} else {
|
|
5629
|
+
try {
|
|
5630
|
+
const reproRun = await execa9("node", [reproductionTestPath], { cwd, stdio: "pipe" });
|
|
5631
|
+
validationPhases.push({
|
|
5632
|
+
phase: "reproduction-test",
|
|
5633
|
+
passed: reproRun.exitCode === 0,
|
|
5634
|
+
message: "Reproduction test verified fix successfully."
|
|
5635
|
+
});
|
|
5636
|
+
} catch (err) {
|
|
5637
|
+
const reproErr = err instanceof Error ? err.message : String(err);
|
|
5638
|
+
validationPhases.push({
|
|
5639
|
+
phase: "reproduction-test",
|
|
5640
|
+
passed: false,
|
|
5641
|
+
error: `Reproduction test failed: ${reproErr}`
|
|
5642
|
+
});
|
|
5643
|
+
}
|
|
5644
|
+
}
|
|
5645
|
+
}
|
|
5646
|
+
let proofOfFixVerification;
|
|
5647
|
+
if (negativePhaseResult && positivePhaseResult) {
|
|
5648
|
+
proofOfFixVerification = assembleProofOfFixVerification({
|
|
5649
|
+
reproductionTestPath,
|
|
5650
|
+
negativePhase: negativePhaseResult,
|
|
5651
|
+
positivePhase: positivePhaseResult,
|
|
5652
|
+
regressionPhase: validationResult ? {
|
|
5653
|
+
executed: true,
|
|
5654
|
+
passed: validationResult.passed,
|
|
5655
|
+
error: validationResult.error
|
|
5656
|
+
} : void 0
|
|
5657
|
+
});
|
|
5658
|
+
}
|
|
4203
5659
|
const finalArtifact = {
|
|
4204
5660
|
...artifact,
|
|
4205
5661
|
applied: true,
|
|
4206
5662
|
dryRun: false,
|
|
4207
|
-
|
|
5663
|
+
reproductionTest,
|
|
5664
|
+
reproductionTestPath,
|
|
5665
|
+
validationPhases,
|
|
5666
|
+
proofOfFix: proofOfFixVerification
|
|
4208
5667
|
};
|
|
4209
5668
|
await writePatchManifest(manifestFilePath, finalArtifact);
|
|
4210
5669
|
return {
|
|
@@ -4221,7 +5680,8 @@ var applyPatchFileTool = defineTool({
|
|
|
4221
5680
|
postinstallConfigured: patchMode === "patch-package",
|
|
4222
5681
|
patchArtifact: finalArtifact,
|
|
4223
5682
|
validation: validationResult,
|
|
4224
|
-
validationPhases
|
|
5683
|
+
validationPhases,
|
|
5684
|
+
proofOfFix: proofOfFixVerification
|
|
4225
5685
|
};
|
|
4226
5686
|
});
|
|
4227
5687
|
} catch (err) {
|
|
@@ -4465,6 +5925,9 @@ async function tryLocalPatchFallback(params) {
|
|
|
4465
5925
|
workspace: params.workspace,
|
|
4466
5926
|
riskLevel: patchResult.riskLevel,
|
|
4467
5927
|
validateWithTests: params.runTests,
|
|
5928
|
+
reproductionTest: patchResult.reproductionTest,
|
|
5929
|
+
proofOfFix: params.proofOfFix,
|
|
5930
|
+
requireProofOfFix: params.requireProofOfFix,
|
|
4468
5931
|
dryRun: params.dryRun
|
|
4469
5932
|
});
|
|
4470
5933
|
steps += 1;
|
|
@@ -4513,8 +5976,14 @@ function resolveLocalRunOptions(options) {
|
|
|
4513
5976
|
const runTests = options.runTests ?? false;
|
|
4514
5977
|
const policy = options.policy ?? "";
|
|
4515
5978
|
const patchesDir = options.patchesDir || "./patches";
|
|
4516
|
-
const constraints = options.constraints ?? {};
|
|
4517
5979
|
const loadedPolicy = loadPolicy(cwd, options.policy);
|
|
5980
|
+
const constraints = {
|
|
5981
|
+
...options.constraints,
|
|
5982
|
+
typecheck: options.typecheck ?? options.constraints?.typecheck ?? loadedPolicy.constraints?.typecheck,
|
|
5983
|
+
supplyChainPolicy: options.supplyChainPolicy ?? options.constraints?.supplyChainPolicy ?? loadedPolicy.constraints?.supplyChainPolicy,
|
|
5984
|
+
enableBreakingChangeRefactor: options.breakingChangeRefactor ?? options.constraints?.enableBreakingChangeRefactor ?? loadedPolicy.constraints?.enableBreakingChangeRefactor,
|
|
5985
|
+
requireProofOfFix: options.requireProofOfFix ?? options.constraints?.requireProofOfFix ?? loadedPolicy.constraints?.requireProofOfFix
|
|
5986
|
+
};
|
|
4518
5987
|
const llmProvider = resolveProvider(options);
|
|
4519
5988
|
return {
|
|
4520
5989
|
cwd,
|
|
@@ -4545,12 +6014,15 @@ function resolveLocalRunOptions(options) {
|
|
|
4545
6014
|
regressionCheck: options.regressionCheck ?? false,
|
|
4546
6015
|
escalationGraph: options.escalationGraph ?? loadedPolicy.escalationGraph,
|
|
4547
6016
|
offlineIntelligence: options.offlineIntelligence ?? false,
|
|
4548
|
-
intelligenceSnapshotPath: options.intelligenceSnapshotPath
|
|
6017
|
+
intelligenceSnapshotPath: options.intelligenceSnapshotPath,
|
|
6018
|
+
proofOfFix: options.proofOfFix,
|
|
6019
|
+
requireProofOfFix: options.requireProofOfFix ?? constraints.requireProofOfFix,
|
|
6020
|
+
breakingChangeRefactor: options.breakingChangeRefactor ?? constraints.enableBreakingChangeRefactor
|
|
4549
6021
|
};
|
|
4550
6022
|
}
|
|
4551
6023
|
|
|
4552
6024
|
// src/remediation/local/vulnerability-match.ts
|
|
4553
|
-
import
|
|
6025
|
+
import semver5 from "semver";
|
|
4554
6026
|
function findVulnerablePackages(cveDetails, installedPackages) {
|
|
4555
6027
|
const vulnerablePackages = [];
|
|
4556
6028
|
for (const affected of cveDetails.affectedPackages) {
|
|
@@ -4559,10 +6031,10 @@ function findVulnerablePackages(cveDetails, installedPackages) {
|
|
|
4559
6031
|
if (affected.ecosystem !== "npm") continue;
|
|
4560
6032
|
const matches = installedPackages.filter((pkg) => pkg.name === affected.name);
|
|
4561
6033
|
for (const installed of matches) {
|
|
4562
|
-
if (!
|
|
6034
|
+
if (!semver5.valid(installed.version)) continue;
|
|
4563
6035
|
let isVulnerable = false;
|
|
4564
6036
|
try {
|
|
4565
|
-
isVulnerable =
|
|
6037
|
+
isVulnerable = semver5.satisfies(installed.version, affected.vulnerableRange, {
|
|
4566
6038
|
includePrerelease: false
|
|
4567
6039
|
});
|
|
4568
6040
|
} catch {
|
|
@@ -4713,7 +6185,9 @@ async function runLocalRemediationPipeline(cveId, options = {}) {
|
|
|
4713
6185
|
regressionCheck,
|
|
4714
6186
|
escalationGraph,
|
|
4715
6187
|
offlineIntelligence,
|
|
4716
|
-
intelligenceSnapshotPath
|
|
6188
|
+
intelligenceSnapshotPath,
|
|
6189
|
+
proofOfFix,
|
|
6190
|
+
requireProofOfFix
|
|
4717
6191
|
} = resolved;
|
|
4718
6192
|
const correlation = {
|
|
4719
6193
|
requestId: options.requestId,
|
|
@@ -4890,7 +6364,9 @@ async function runLocalRemediationPipeline(cveId, options = {}) {
|
|
|
4890
6364
|
installPreferOffline: constraints.installPreferOffline,
|
|
4891
6365
|
enforceFrozenLockfile: constraints.enforceFrozenLockfile,
|
|
4892
6366
|
workspace: constraints.workspace,
|
|
4893
|
-
vulnerableRange: vulnerable.affected.vulnerableRange
|
|
6367
|
+
vulnerableRange: vulnerable.affected.vulnerableRange,
|
|
6368
|
+
proofOfFix,
|
|
6369
|
+
requireProofOfFix
|
|
4894
6370
|
});
|
|
4895
6371
|
agentSteps += fallback.steps;
|
|
4896
6372
|
collectedResults.push(finalizePatchResult(fallback.result));
|
|
@@ -4905,7 +6381,7 @@ async function runLocalRemediationPipeline(cveId, options = {}) {
|
|
|
4905
6381
|
};
|
|
4906
6382
|
if (regressionCheck && primaryResult.applied && !dryRun && primaryResult.toVersion) {
|
|
4907
6383
|
try {
|
|
4908
|
-
if (
|
|
6384
|
+
if (semver6.satisfies(primaryResult.toVersion, vulnerable.affected.vulnerableRange, {
|
|
4909
6385
|
includePrerelease: false
|
|
4910
6386
|
})) {
|
|
4911
6387
|
primaryResult.regressionDetected = true;
|
|
@@ -4996,8 +6472,8 @@ function accumulateStepResults(params) {
|
|
|
4996
6472
|
}
|
|
4997
6473
|
|
|
4998
6474
|
// src/remediation/orchestration-prompt.ts
|
|
4999
|
-
import { existsSync as
|
|
5000
|
-
import { join as
|
|
6475
|
+
import { existsSync as existsSync15, readFileSync as readFileSync15 } from "fs";
|
|
6476
|
+
import { join as join20 } from "path";
|
|
5001
6477
|
function buildProviderAddendum(provider, personality = "balanced") {
|
|
5002
6478
|
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.";
|
|
5003
6479
|
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.";
|
|
@@ -5008,13 +6484,13 @@ Provider profile:
|
|
|
5008
6484
|
- ${personalityDirective}`;
|
|
5009
6485
|
}
|
|
5010
6486
|
function loadOrchestrationPrompt(ctx) {
|
|
5011
|
-
const promptPath =
|
|
6487
|
+
const promptPath = join20(
|
|
5012
6488
|
process.cwd(),
|
|
5013
6489
|
".github",
|
|
5014
6490
|
"instructions",
|
|
5015
6491
|
"orchestration.instructions.md"
|
|
5016
6492
|
);
|
|
5017
|
-
if (!
|
|
6493
|
+
if (!existsSync15(promptPath)) {
|
|
5018
6494
|
return `You are autoremediator, an agentic security remediation system for Node.js package dependencies.
|
|
5019
6495
|
Working directory: ${ctx.cwd}
|
|
5020
6496
|
Package manager: ${ctx.packageManager}
|
|
@@ -5040,7 +6516,7 @@ Fallback sequence (when neither version bump nor override can be applied):
|
|
|
5040
6516
|
|
|
5041
6517
|
Always respect dryRun and policy constraints.`;
|
|
5042
6518
|
}
|
|
5043
|
-
const template =
|
|
6519
|
+
const template = readFileSync15(promptPath, "utf8");
|
|
5044
6520
|
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(
|
|
5045
6521
|
"{{directDependenciesOnly}}",
|
|
5046
6522
|
String(ctx.constraints.directDependenciesOnly ?? false)
|
|
@@ -5053,81 +6529,17 @@ function createProgressEmitter(options) {
|
|
|
5053
6529
|
if (!options.onProgress) return;
|
|
5054
6530
|
options.onProgress({
|
|
5055
6531
|
stage,
|
|
5056
|
-
detail,
|
|
5057
|
-
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5058
|
-
provider: extra?.provider,
|
|
5059
|
-
model: extra?.model
|
|
5060
|
-
});
|
|
5061
|
-
};
|
|
5062
|
-
}
|
|
5063
|
-
|
|
5064
|
-
// src/remediation/tools/lookup-cve.ts
|
|
5065
|
-
import { z as z9 } from "zod";
|
|
5066
|
-
|
|
5067
|
-
// src/intelligence/sources/cisa-kev.ts
|
|
5068
|
-
var CISA_KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json";
|
|
5069
|
-
async function fetchCisaKevFeed() {
|
|
5070
|
-
try {
|
|
5071
|
-
const res = await httpClient({ url: CISA_KEV_URL });
|
|
5072
|
-
if (!res.ok) return void 0;
|
|
5073
|
-
return res.data;
|
|
5074
|
-
} catch {
|
|
5075
|
-
return void 0;
|
|
5076
|
-
}
|
|
5077
|
-
}
|
|
5078
|
-
function findKevEntry(feed, cveId) {
|
|
5079
|
-
if (!feed?.vulnerabilities?.length) return void 0;
|
|
5080
|
-
const normalized = cveId.toUpperCase();
|
|
5081
|
-
return feed.vulnerabilities.find((v) => v.cveID.toUpperCase() === normalized);
|
|
5082
|
-
}
|
|
5083
|
-
async function enrichWithCisaKev(details) {
|
|
5084
|
-
const feed = await fetchCisaKevFeed();
|
|
5085
|
-
const entry = findKevEntry(feed, details.id);
|
|
5086
|
-
if (!entry) return details;
|
|
5087
|
-
details.kev = {
|
|
5088
|
-
knownExploited: true,
|
|
5089
|
-
dateAdded: entry.dateAdded,
|
|
5090
|
-
dueDate: entry.dueDate,
|
|
5091
|
-
requiredAction: entry.requiredAction,
|
|
5092
|
-
knownRansomwareCampaignUse: entry.knownRansomwareCampaignUse
|
|
5093
|
-
};
|
|
5094
|
-
if (!details.references.includes(CISA_KEV_URL)) {
|
|
5095
|
-
details.references.push(CISA_KEV_URL);
|
|
5096
|
-
}
|
|
5097
|
-
return details;
|
|
5098
|
-
}
|
|
5099
|
-
|
|
5100
|
-
// src/intelligence/sources/epss.ts
|
|
5101
|
-
async function fetchEpss(cveId) {
|
|
5102
|
-
const { epssApi } = getIntelligenceSourceConfig();
|
|
5103
|
-
if (!epssApi) return void 0;
|
|
5104
|
-
try {
|
|
5105
|
-
const url = new URL(epssApi);
|
|
5106
|
-
url.searchParams.set("cve", cveId);
|
|
5107
|
-
const res = await httpClient({ url: url.toString() });
|
|
5108
|
-
if (!res.ok) return void 0;
|
|
5109
|
-
const body = res.data;
|
|
5110
|
-
return body.data?.[0];
|
|
5111
|
-
} catch {
|
|
5112
|
-
return void 0;
|
|
5113
|
-
}
|
|
5114
|
-
}
|
|
5115
|
-
async function enrichWithEpss(details) {
|
|
5116
|
-
const row = await fetchEpss(details.id);
|
|
5117
|
-
if (!row) return details;
|
|
5118
|
-
const score = Number.parseFloat(row.epss);
|
|
5119
|
-
const percentile = Number.parseFloat(row.percentile);
|
|
5120
|
-
if (!Number.isFinite(score) || !Number.isFinite(percentile)) {
|
|
5121
|
-
return details;
|
|
5122
|
-
}
|
|
5123
|
-
details.epss = {
|
|
5124
|
-
score,
|
|
5125
|
-
percentile,
|
|
5126
|
-
date: row.date
|
|
6532
|
+
detail,
|
|
6533
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6534
|
+
provider: extra?.provider,
|
|
6535
|
+
model: extra?.model
|
|
6536
|
+
});
|
|
5127
6537
|
};
|
|
5128
|
-
return details;
|
|
5129
6538
|
}
|
|
5130
6539
|
|
|
6540
|
+
// src/remediation/tools/lookup-cve.ts
|
|
6541
|
+
import { z as z9 } from "zod";
|
|
6542
|
+
|
|
5131
6543
|
// src/intelligence/sources/cve-services.ts
|
|
5132
6544
|
function pickEnglishDescription(container) {
|
|
5133
6545
|
if (!container?.descriptions?.length) return void 0;
|
|
@@ -5411,7 +6823,7 @@ var lookupCveTool = defineTool({
|
|
|
5411
6823
|
|
|
5412
6824
|
// src/remediation/tools/check-version-match.ts
|
|
5413
6825
|
import { z as z10 } from "zod";
|
|
5414
|
-
import
|
|
6826
|
+
import semver7 from "semver";
|
|
5415
6827
|
var affectedPackageSchema = z10.object({
|
|
5416
6828
|
name: z10.string(),
|
|
5417
6829
|
ecosystem: z10.literal("npm"),
|
|
@@ -5440,10 +6852,10 @@ var checkVersionMatchTool = defineTool({
|
|
|
5440
6852
|
(p) => p.name === affected.name
|
|
5441
6853
|
);
|
|
5442
6854
|
for (const installed of matches) {
|
|
5443
|
-
if (!
|
|
6855
|
+
if (!semver7.valid(installed.version)) continue;
|
|
5444
6856
|
let isVulnerable = false;
|
|
5445
6857
|
try {
|
|
5446
|
-
isVulnerable =
|
|
6858
|
+
isVulnerable = semver7.satisfies(installed.version, affected.vulnerableRange, {
|
|
5447
6859
|
includePrerelease: false
|
|
5448
6860
|
});
|
|
5449
6861
|
} catch {
|
|
@@ -5583,6 +6995,8 @@ function createRuntimeToolsForRun(options) {
|
|
|
5583
6995
|
installPreferOffline: constraints.installPreferOffline,
|
|
5584
6996
|
enforceFrozenLockfile: constraints.enforceFrozenLockfile,
|
|
5585
6997
|
workspace: constraints.workspace,
|
|
6998
|
+
typecheck: input.typecheck ?? options.typecheck ?? constraints.typecheck,
|
|
6999
|
+
breakingChangeRefactor: input.breakingChangeRefactor ?? options.breakingChangeRefactor ?? constraints.enableBreakingChangeRefactor,
|
|
5586
7000
|
dryRun: preview ? true : input.dryRun
|
|
5587
7001
|
})
|
|
5588
7002
|
};
|
|
@@ -5607,6 +7021,8 @@ function createRuntimeToolsForRun(options) {
|
|
|
5607
7021
|
installPreferOffline: constraints.installPreferOffline,
|
|
5608
7022
|
enforceFrozenLockfile: constraints.enforceFrozenLockfile,
|
|
5609
7023
|
workspace: constraints.workspace,
|
|
7024
|
+
proofOfFix: input.proofOfFix ?? options.proofOfFix,
|
|
7025
|
+
requireProofOfFix: input.requireProofOfFix ?? options.requireProofOfFix ?? constraints.requireProofOfFix,
|
|
5610
7026
|
dryRun: preview ? true : input.dryRun
|
|
5611
7027
|
})
|
|
5612
7028
|
};
|
|
@@ -5761,7 +7177,7 @@ async function runRemediationPipeline(cveId, options = {}) {
|
|
|
5761
7177
|
}
|
|
5762
7178
|
|
|
5763
7179
|
// src/api/change-request/index.ts
|
|
5764
|
-
import { execa as
|
|
7180
|
+
import { execa as execa10 } from "execa";
|
|
5765
7181
|
import { randomBytes } from "crypto";
|
|
5766
7182
|
function sanitizeBranchToken(value) {
|
|
5767
7183
|
return value.toLowerCase().replace(/[^a-z0-9._/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
@@ -5790,22 +7206,68 @@ function resolveProvider2(options) {
|
|
|
5790
7206
|
return options.provider ?? "github";
|
|
5791
7207
|
}
|
|
5792
7208
|
function resolveGrouping(options) {
|
|
5793
|
-
|
|
7209
|
+
const allowed = ["all", "per-cve", "per-package", "by-severity"];
|
|
7210
|
+
if (options.grouping && !allowed.includes(options.grouping)) {
|
|
5794
7211
|
throw new Error(
|
|
5795
|
-
|
|
7212
|
+
`Unsupported changeRequest.grouping: "${options.grouping}". Supported values: ${allowed.join(", ")}`
|
|
5796
7213
|
);
|
|
5797
7214
|
}
|
|
5798
|
-
return "all";
|
|
7215
|
+
return options.grouping ?? "all";
|
|
7216
|
+
}
|
|
7217
|
+
function groupReports(reports, grouping) {
|
|
7218
|
+
if (reports.length === 0) return [];
|
|
7219
|
+
if (grouping === "all") {
|
|
7220
|
+
return [reports];
|
|
7221
|
+
}
|
|
7222
|
+
if (grouping === "per-cve") {
|
|
7223
|
+
const map = /* @__PURE__ */ new Map();
|
|
7224
|
+
for (const report of reports) {
|
|
7225
|
+
const key = report.cveId.toUpperCase();
|
|
7226
|
+
const list = map.get(key) ?? [];
|
|
7227
|
+
list.push(report);
|
|
7228
|
+
map.set(key, list);
|
|
7229
|
+
}
|
|
7230
|
+
return Array.from(map.values());
|
|
7231
|
+
}
|
|
7232
|
+
if (grouping === "per-package") {
|
|
7233
|
+
const map = /* @__PURE__ */ new Map();
|
|
7234
|
+
for (const report of reports) {
|
|
7235
|
+
const pkgs = new Set(report.results.map((r) => r.packageName));
|
|
7236
|
+
if (pkgs.size === 0) {
|
|
7237
|
+
const list = map.get("misc") ?? [];
|
|
7238
|
+
list.push(report);
|
|
7239
|
+
map.set("misc", list);
|
|
7240
|
+
} else {
|
|
7241
|
+
for (const pkg of pkgs) {
|
|
7242
|
+
const list = map.get(pkg) ?? [];
|
|
7243
|
+
list.push(report);
|
|
7244
|
+
map.set(pkg, list);
|
|
7245
|
+
}
|
|
7246
|
+
}
|
|
7247
|
+
}
|
|
7248
|
+
return Array.from(map.values());
|
|
7249
|
+
}
|
|
7250
|
+
if (grouping === "by-severity") {
|
|
7251
|
+
const map = /* @__PURE__ */ new Map();
|
|
7252
|
+
for (const report of reports) {
|
|
7253
|
+
const sev = report.cveDetails?.severity ?? "UNKNOWN";
|
|
7254
|
+
const list = map.get(sev) ?? [];
|
|
7255
|
+
list.push(report);
|
|
7256
|
+
map.set(sev, list);
|
|
7257
|
+
}
|
|
7258
|
+
return Array.from(map.values());
|
|
7259
|
+
}
|
|
7260
|
+
return [reports];
|
|
5799
7261
|
}
|
|
5800
7262
|
async function detectCurrentBranch(cwd) {
|
|
5801
|
-
const result = await
|
|
7263
|
+
const result = await execa10("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
5802
7264
|
cwd,
|
|
5803
7265
|
stdio: "pipe"
|
|
5804
7266
|
});
|
|
5805
7267
|
return result.stdout.trim();
|
|
5806
7268
|
}
|
|
5807
7269
|
async function ensureRepoHasChanges(cwd) {
|
|
5808
|
-
const diff = await
|
|
7270
|
+
const diff = await execa10("git", ["status", "--porcelain"], {
|
|
5809
7271
|
cwd,
|
|
5810
7272
|
stdio: "pipe"
|
|
5811
7273
|
});
|
|
@@ -5834,7 +7296,7 @@ async function createGitHubRequest(params) {
|
|
|
5834
7296
|
if (params.draft) {
|
|
5835
7297
|
args.push("--draft");
|
|
5836
7298
|
}
|
|
5837
|
-
const result = await
|
|
7299
|
+
const result = await execa10("gh", args, {
|
|
5838
7300
|
cwd: params.cwd,
|
|
5839
7301
|
stdio: "pipe"
|
|
5840
7302
|
});
|
|
@@ -5859,7 +7321,7 @@ async function createGitLabRequest(params) {
|
|
|
5859
7321
|
if (params.draft) {
|
|
5860
7322
|
args.push("--draft");
|
|
5861
7323
|
}
|
|
5862
|
-
const result = await
|
|
7324
|
+
const result = await execa10("glab", args, {
|
|
5863
7325
|
cwd: params.cwd,
|
|
5864
7326
|
stdio: "pipe"
|
|
5865
7327
|
});
|
|
@@ -5869,64 +7331,68 @@ async function createChangeRequestsForReports(params) {
|
|
|
5869
7331
|
const { cwd, options, reports } = params;
|
|
5870
7332
|
const provider = resolveProvider2(options);
|
|
5871
7333
|
const grouping = resolveGrouping(options);
|
|
5872
|
-
const
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
const
|
|
7334
|
+
const groups = groupReports(reports, grouping);
|
|
7335
|
+
if (groups.length === 0) {
|
|
7336
|
+
return [];
|
|
7337
|
+
}
|
|
7338
|
+
const results = [];
|
|
7339
|
+
const hasChanges = await ensureRepoHasChanges(cwd).catch(() => false);
|
|
7340
|
+
const baseBranch = options.baseBranch ?? await detectCurrentBranch(cwd).catch(() => "main");
|
|
7341
|
+
const branchPrefix = options.branchPrefix ?? "autoremediator";
|
|
7342
|
+
const pushRemote = options.pushRemote ?? "origin";
|
|
7343
|
+
if (options.pushRemote && !/^[a-zA-Z0-9._-]+$/.test(options.pushRemote)) {
|
|
7344
|
+
throw new Error(`Invalid pushRemote value: ${options.pushRemote}`);
|
|
7345
|
+
}
|
|
7346
|
+
if (options.repository && !/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(options.repository)) {
|
|
7347
|
+
throw new Error(`Invalid repository format: ${options.repository}`);
|
|
7348
|
+
}
|
|
7349
|
+
for (const group of groups) {
|
|
7350
|
+
const plan = buildPlan(group);
|
|
7351
|
+
const titlePrefix = options.titlePrefix?.trim().slice(0, 200);
|
|
7352
|
+
const bodyFooter = options.bodyFooter ? options.bodyFooter.slice(0, 2e3) : void 0;
|
|
7353
|
+
const title = titlePrefix ? `${titlePrefix} ${plan.title}` : plan.title;
|
|
7354
|
+
const body = bodyFooter ? `${plan.body}
|
|
5877
7355
|
|
|
5878
7356
|
${bodyFooter}` : plan.body;
|
|
5879
|
-
try {
|
|
5880
|
-
const hasChanges = await ensureRepoHasChanges(cwd);
|
|
5881
7357
|
if (!hasChanges) {
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
];
|
|
7358
|
+
results.push({
|
|
7359
|
+
provider,
|
|
7360
|
+
grouping,
|
|
7361
|
+
repository: options.repository,
|
|
7362
|
+
branchName: "",
|
|
7363
|
+
title,
|
|
7364
|
+
body,
|
|
7365
|
+
created: false,
|
|
7366
|
+
cveIds: plan.cveIds,
|
|
7367
|
+
packageNames: plan.packageNames,
|
|
7368
|
+
error: "No repository changes to include in change request."
|
|
7369
|
+
});
|
|
7370
|
+
continue;
|
|
5896
7371
|
}
|
|
5897
|
-
const baseBranch = options.baseBranch ?? await detectCurrentBranch(cwd);
|
|
5898
|
-
const branchPrefix = options.branchPrefix ?? "autoremediator";
|
|
5899
7372
|
const branchName = toBranchName(branchPrefix, plan.cveIds);
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
target: baseBranch,
|
|
5924
|
-
title,
|
|
5925
|
-
body,
|
|
5926
|
-
draft: options.draft
|
|
5927
|
-
});
|
|
5928
|
-
return [
|
|
5929
|
-
{
|
|
7373
|
+
try {
|
|
7374
|
+
await execa10("git", ["checkout", "-b", branchName], { cwd, stdio: "pipe" });
|
|
7375
|
+
await execa10("git", ["add", "-A"], { cwd, stdio: "pipe" });
|
|
7376
|
+
await execa10("git", ["commit", "-m", title], { cwd, stdio: "pipe" });
|
|
7377
|
+
await execa10("git", ["push", "-u", pushRemote, branchName], { cwd, stdio: "pipe" });
|
|
7378
|
+
const url = provider === "github" ? await createGitHubRequest({
|
|
7379
|
+
cwd,
|
|
7380
|
+
repository: options.repository,
|
|
7381
|
+
head: branchName,
|
|
7382
|
+
base: baseBranch,
|
|
7383
|
+
title,
|
|
7384
|
+
body,
|
|
7385
|
+
draft: options.draft
|
|
7386
|
+
}) : await createGitLabRequest({
|
|
7387
|
+
cwd,
|
|
7388
|
+
repository: options.repository,
|
|
7389
|
+
source: branchName,
|
|
7390
|
+
target: baseBranch,
|
|
7391
|
+
title,
|
|
7392
|
+
body,
|
|
7393
|
+
draft: options.draft
|
|
7394
|
+
});
|
|
7395
|
+
results.push({
|
|
5930
7396
|
provider,
|
|
5931
7397
|
grouping,
|
|
5932
7398
|
repository: options.repository,
|
|
@@ -5938,11 +7404,13 @@ ${bodyFooter}` : plan.body;
|
|
|
5938
7404
|
url,
|
|
5939
7405
|
cveIds: plan.cveIds,
|
|
5940
7406
|
packageNames: plan.packageNames
|
|
7407
|
+
});
|
|
7408
|
+
if (groups.length > 1) {
|
|
7409
|
+
await execa10("git", ["checkout", baseBranch], { cwd, stdio: "pipe" }).catch(() => {
|
|
7410
|
+
});
|
|
5941
7411
|
}
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
return [
|
|
5945
|
-
{
|
|
7412
|
+
} catch (error) {
|
|
7413
|
+
results.push({
|
|
5946
7414
|
provider,
|
|
5947
7415
|
grouping,
|
|
5948
7416
|
repository: options.repository,
|
|
@@ -5954,9 +7422,10 @@ ${bodyFooter}` : plan.body;
|
|
|
5954
7422
|
cveIds: plan.cveIds,
|
|
5955
7423
|
packageNames: plan.packageNames,
|
|
5956
7424
|
error: error instanceof Error ? error.message : String(error)
|
|
5957
|
-
}
|
|
5958
|
-
|
|
7425
|
+
});
|
|
7426
|
+
}
|
|
5959
7427
|
}
|
|
7428
|
+
return results;
|
|
5960
7429
|
}
|
|
5961
7430
|
|
|
5962
7431
|
// src/api/context.ts
|
|
@@ -5984,13 +7453,17 @@ function resolveConstraints(options, cwd) {
|
|
|
5984
7453
|
installMode: options.constraints?.installMode ?? policy.constraints?.installMode ?? "deterministic",
|
|
5985
7454
|
installPreferOffline: options.constraints?.installPreferOffline ?? policy.constraints?.installPreferOffline,
|
|
5986
7455
|
enforceFrozenLockfile: options.constraints?.enforceFrozenLockfile ?? policy.constraints?.enforceFrozenLockfile,
|
|
5987
|
-
workspace: options.constraints?.workspace ?? policy.constraints?.workspace
|
|
7456
|
+
workspace: options.constraints?.workspace ?? policy.constraints?.workspace,
|
|
7457
|
+
typecheck: options.typecheck ?? options.constraints?.typecheck ?? policy.constraints?.typecheck,
|
|
7458
|
+
requireProofOfFix: options.requireProofOfFix ?? options.constraints?.requireProofOfFix ?? policy.constraints?.requireProofOfFix,
|
|
7459
|
+
enableBreakingChangeRefactor: options.breakingChangeRefactor ?? options.constraints?.enableBreakingChangeRefactor ?? policy.constraints?.enableBreakingChangeRefactor,
|
|
7460
|
+
supplyChainPolicy: options.supplyChainPolicy ?? options.constraints?.supplyChainPolicy ?? policy.constraints?.supplyChainPolicy
|
|
5988
7461
|
};
|
|
5989
7462
|
}
|
|
5990
7463
|
|
|
5991
7464
|
// src/platform/evidence.ts
|
|
5992
|
-
import { mkdirSync as mkdirSync3, writeFileSync as
|
|
5993
|
-
import { join as
|
|
7465
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
7466
|
+
import { join as join21 } from "path";
|
|
5994
7467
|
function createEvidenceLog(cwd, cveIds, context = {}) {
|
|
5995
7468
|
return {
|
|
5996
7469
|
runId: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
@@ -6022,10 +7495,10 @@ function finalizeEvidence(log) {
|
|
|
6022
7495
|
return log;
|
|
6023
7496
|
}
|
|
6024
7497
|
function writeEvidenceLog(cwd, log) {
|
|
6025
|
-
const dir =
|
|
7498
|
+
const dir = join21(cwd, ".autoremediator", "evidence");
|
|
6026
7499
|
mkdirSync3(dir, { recursive: true });
|
|
6027
|
-
const filePath =
|
|
6028
|
-
|
|
7500
|
+
const filePath = join21(dir, `${log.runId}.json`);
|
|
7501
|
+
writeFileSync6(filePath, JSON.stringify(log, null, 2) + "\n", "utf8");
|
|
6029
7502
|
return filePath;
|
|
6030
7503
|
}
|
|
6031
7504
|
|
|
@@ -6524,10 +7997,15 @@ function ciExitCode(summary) {
|
|
|
6524
7997
|
}
|
|
6525
7998
|
|
|
6526
7999
|
// src/api/remediate/validators.ts
|
|
8000
|
+
var CVE_ID_REGEX = /^CVE-\d{4}-\d{1,7}$/i;
|
|
8001
|
+
var GHSA_ID_REGEX = /^GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$/i;
|
|
8002
|
+
function isVulnerabilityId(id) {
|
|
8003
|
+
return CVE_ID_REGEX.test(id) || GHSA_ID_REGEX.test(id);
|
|
8004
|
+
}
|
|
6527
8005
|
function assertValidCveId(cveId) {
|
|
6528
|
-
if (
|
|
8006
|
+
if (!isVulnerabilityId(cveId)) {
|
|
6529
8007
|
throw new Error(
|
|
6530
|
-
`Invalid
|
|
8008
|
+
`Invalid vulnerability ID: "${cveId}". Expected format: CVE-YYYY-NNNNN (e.g. CVE-2021-23337) or GHSA-xxxx-xxxx-xxxx.`
|
|
6531
8009
|
);
|
|
6532
8010
|
}
|
|
6533
8011
|
}
|
|
@@ -6623,13 +8101,14 @@ async function planRemediation(cveId, options = {}) {
|
|
|
6623
8101
|
}
|
|
6624
8102
|
|
|
6625
8103
|
// src/scanner/parse-input.ts
|
|
6626
|
-
import { extname, resolve as
|
|
6627
|
-
import { readFileSync as
|
|
6628
|
-
import { execa as
|
|
8104
|
+
import { extname, resolve as resolve5 } from "path";
|
|
8105
|
+
import { readFileSync as readFileSync19 } from "fs";
|
|
8106
|
+
import { execa as execa11 } from "execa";
|
|
6629
8107
|
|
|
6630
8108
|
// src/scanner/adapters/npm-audit.ts
|
|
6631
|
-
import { readFileSync as
|
|
8109
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
6632
8110
|
var CVE_REGEX = /CVE-\d{4}-\d{1,7}/gi;
|
|
8111
|
+
var GHSA_REGEX = /GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}/gi;
|
|
6633
8112
|
function normalizeSeverity(raw) {
|
|
6634
8113
|
if (!raw) return "UNKNOWN";
|
|
6635
8114
|
const up = raw.toUpperCase();
|
|
@@ -6644,7 +8123,8 @@ function parseNpmAuditJsonFromString(content) {
|
|
|
6644
8123
|
for (const vuln of Object.values(report.vulnerabilities ?? {})) {
|
|
6645
8124
|
for (const viaEntry of vuln.via ?? []) {
|
|
6646
8125
|
const text = typeof viaEntry === "string" ? viaEntry : `${viaEntry.url ?? ""} ${viaEntry.name ?? ""}`;
|
|
6647
|
-
const
|
|
8126
|
+
const cveMatches = text.match(CVE_REGEX) ?? [];
|
|
8127
|
+
const matches = cveMatches.length > 0 ? cveMatches : text.match(GHSA_REGEX) ?? [];
|
|
6648
8128
|
for (const match of matches) {
|
|
6649
8129
|
const cveId = match.toUpperCase();
|
|
6650
8130
|
const key = `${cveId}:${vuln.name}`;
|
|
@@ -6662,13 +8142,14 @@ function parseNpmAuditJsonFromString(content) {
|
|
|
6662
8142
|
return findings;
|
|
6663
8143
|
}
|
|
6664
8144
|
function parseNpmAuditJsonFile(filePath) {
|
|
6665
|
-
const content =
|
|
8145
|
+
const content = readFileSync16(filePath, "utf8");
|
|
6666
8146
|
return parseNpmAuditJsonFromString(content);
|
|
6667
8147
|
}
|
|
6668
8148
|
|
|
6669
8149
|
// src/scanner/adapters/yarn-audit.ts
|
|
6670
|
-
import { readFileSync as
|
|
8150
|
+
import { readFileSync as readFileSync17 } from "fs";
|
|
6671
8151
|
var CVE_REGEX2 = /CVE-\d{4}-\d{1,7}/gi;
|
|
8152
|
+
var GHSA_REGEX2 = /GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}/gi;
|
|
6672
8153
|
function normalizeSeverity2(raw) {
|
|
6673
8154
|
if (!raw) return "UNKNOWN";
|
|
6674
8155
|
const up = raw.toUpperCase();
|
|
@@ -6693,7 +8174,8 @@ function parseYarnAuditJsonFromString(content) {
|
|
|
6693
8174
|
const packageName = advisory?.module_name;
|
|
6694
8175
|
const severity = normalizeSeverity2(advisory?.severity);
|
|
6695
8176
|
const text = `${advisory?.url ?? ""} ${(advisory?.cves ?? []).join(" ")}`;
|
|
6696
|
-
const
|
|
8177
|
+
const cveMatches = text.match(CVE_REGEX2) ?? [];
|
|
8178
|
+
const matches = cveMatches.length > 0 ? cveMatches : text.match(GHSA_REGEX2) ?? [];
|
|
6697
8179
|
for (const match of matches) {
|
|
6698
8180
|
const cveId = match.toUpperCase();
|
|
6699
8181
|
const key = `${cveId}:${packageName ?? ""}`;
|
|
@@ -6710,12 +8192,12 @@ function parseYarnAuditJsonFromString(content) {
|
|
|
6710
8192
|
return findings;
|
|
6711
8193
|
}
|
|
6712
8194
|
function parseYarnAuditJsonFile(filePath) {
|
|
6713
|
-
const content =
|
|
8195
|
+
const content = readFileSync17(filePath, "utf8");
|
|
6714
8196
|
return parseYarnAuditJsonFromString(content);
|
|
6715
8197
|
}
|
|
6716
8198
|
|
|
6717
8199
|
// src/scanner/adapters/sarif.ts
|
|
6718
|
-
import { readFileSync as
|
|
8200
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
6719
8201
|
var CVE_REGEX3 = /CVE-\d{4}-\d{1,7}/gi;
|
|
6720
8202
|
function extractPackageName(result) {
|
|
6721
8203
|
const pkg = result.properties?.["packageName"];
|
|
@@ -6753,7 +8235,7 @@ function parseSarifFromString(content) {
|
|
|
6753
8235
|
return findings;
|
|
6754
8236
|
}
|
|
6755
8237
|
function parseSarifFile(filePath) {
|
|
6756
|
-
const content =
|
|
8238
|
+
const content = readFileSync18(filePath, "utf8");
|
|
6757
8239
|
return parseSarifFromString(content);
|
|
6758
8240
|
}
|
|
6759
8241
|
|
|
@@ -6762,7 +8244,7 @@ function parseScanInput(filePath, format) {
|
|
|
6762
8244
|
if (filePath.includes("\0")) {
|
|
6763
8245
|
throw new Error("Invalid scan input path: path contains null bytes");
|
|
6764
8246
|
}
|
|
6765
|
-
const resolvedPath =
|
|
8247
|
+
const resolvedPath = resolve5(filePath);
|
|
6766
8248
|
const resolved = format === "auto" ? inferFormat(resolvedPath) : format;
|
|
6767
8249
|
if (resolved === "npm-audit") {
|
|
6768
8250
|
return parseNpmAuditJsonFile(resolvedPath);
|
|
@@ -6784,7 +8266,7 @@ async function parseScanInputFromAudit(params) {
|
|
|
6784
8266
|
}
|
|
6785
8267
|
const command = resolveAuditCommand(pm, { workspace: params.workspace });
|
|
6786
8268
|
const [cmd, ...args] = command;
|
|
6787
|
-
const result = await
|
|
8269
|
+
const result = await execa11(cmd, args, {
|
|
6788
8270
|
cwd: params.cwd,
|
|
6789
8271
|
stdio: "pipe",
|
|
6790
8272
|
reject: false
|
|
@@ -6838,7 +8320,7 @@ function inferFormat(filePath) {
|
|
|
6838
8320
|
const ext = extname(filePath).toLowerCase();
|
|
6839
8321
|
if (ext === ".sarif") return "sarif";
|
|
6840
8322
|
try {
|
|
6841
|
-
const content =
|
|
8323
|
+
const content = readFileSync19(filePath, "utf8");
|
|
6842
8324
|
const firstLine = content.split("\n").find((line) => line.trim().startsWith("{"));
|
|
6843
8325
|
if (firstLine) {
|
|
6844
8326
|
const parsed = JSON.parse(firstLine);
|
|
@@ -7249,14 +8731,14 @@ async function remediatePortfolio(targets, options = {}) {
|
|
|
7249
8731
|
}
|
|
7250
8732
|
|
|
7251
8733
|
// src/api/update-outdated/index.ts
|
|
7252
|
-
import { join as
|
|
7253
|
-
import { readFileSync as
|
|
7254
|
-
import { execa as
|
|
8734
|
+
import { join as join22 } from "path";
|
|
8735
|
+
import { readFileSync as readFileSync20, writeFileSync as writeFileSync7 } from "fs";
|
|
8736
|
+
import { execa as execa12 } from "execa";
|
|
7255
8737
|
async function applyBump(params) {
|
|
7256
|
-
const pkgPath =
|
|
8738
|
+
const pkgPath = join22(params.cwd, "package.json");
|
|
7257
8739
|
let pkgJson;
|
|
7258
8740
|
try {
|
|
7259
|
-
pkgJson = JSON.parse(
|
|
8741
|
+
pkgJson = JSON.parse(readFileSync20(pkgPath, "utf8"));
|
|
7260
8742
|
} catch {
|
|
7261
8743
|
return {
|
|
7262
8744
|
applied: false,
|
|
@@ -7278,13 +8760,13 @@ async function applyBump(params) {
|
|
|
7278
8760
|
const newRange = `${prefix}${params.toVersion}`;
|
|
7279
8761
|
return withRepoLock(params.cwd, async () => {
|
|
7280
8762
|
pkgJson[depField][params.packageName] = newRange;
|
|
7281
|
-
|
|
8763
|
+
writeFileSync7(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
7282
8764
|
try {
|
|
7283
8765
|
const [installCmd, ...installArgs] = params.installCommand;
|
|
7284
|
-
await
|
|
8766
|
+
await execa12(installCmd, installArgs, { cwd: params.cwd, stdio: "pipe" });
|
|
7285
8767
|
} catch (err) {
|
|
7286
8768
|
pkgJson[depField][params.packageName] = currentRange;
|
|
7287
|
-
|
|
8769
|
+
writeFileSync7(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
7288
8770
|
const message = err instanceof Error ? err.message : String(err);
|
|
7289
8771
|
return {
|
|
7290
8772
|
applied: false,
|
|
@@ -7294,13 +8776,13 @@ async function applyBump(params) {
|
|
|
7294
8776
|
if (params.runTests) {
|
|
7295
8777
|
try {
|
|
7296
8778
|
const [testCmd, ...testArgs] = params.testCommand;
|
|
7297
|
-
await
|
|
8779
|
+
await execa12(testCmd, testArgs, { cwd: params.cwd, stdio: "pipe" });
|
|
7298
8780
|
} catch (err) {
|
|
7299
8781
|
pkgJson[depField][params.packageName] = currentRange;
|
|
7300
|
-
|
|
8782
|
+
writeFileSync7(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
7301
8783
|
try {
|
|
7302
8784
|
const [rollbackCmd, ...rollbackArgs] = params.installCommand;
|
|
7303
|
-
await
|
|
8785
|
+
await execa12(rollbackCmd, rollbackArgs, { cwd: params.cwd, stdio: "pipe" });
|
|
7304
8786
|
} catch {
|
|
7305
8787
|
}
|
|
7306
8788
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -7501,131 +8983,169 @@ async function checkReachability(options) {
|
|
|
7501
8983
|
return assessPackageReachability(cwd, options.packageName, options.symbol);
|
|
7502
8984
|
}
|
|
7503
8985
|
|
|
7504
|
-
// src/
|
|
7505
|
-
import
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
}
|
|
7525
|
-
|
|
7526
|
-
if (enrich) {
|
|
8986
|
+
// src/scanner/delta-scan.ts
|
|
8987
|
+
import { existsSync as existsSync16, readFileSync as readFileSync21 } from "fs";
|
|
8988
|
+
import { join as join23 } from "path";
|
|
8989
|
+
import { execa as execa13 } from "execa";
|
|
8990
|
+
import semver9 from "semver";
|
|
8991
|
+
|
|
8992
|
+
// src/scanner/lockfile-diff.ts
|
|
8993
|
+
import YAML2 from "yaml";
|
|
8994
|
+
import semver8 from "semver";
|
|
8995
|
+
function parseLockfileDependencies(filename, content) {
|
|
8996
|
+
const result = /* @__PURE__ */ new Map();
|
|
8997
|
+
const add = (name, ver) => {
|
|
8998
|
+
if (!name || !ver) return;
|
|
8999
|
+
const cleanVer = semver8.clean(ver) || semver8.coerce(ver)?.version || ver;
|
|
9000
|
+
let set = result.get(name);
|
|
9001
|
+
if (!set) {
|
|
9002
|
+
set = /* @__PURE__ */ new Set();
|
|
9003
|
+
result.set(name, set);
|
|
9004
|
+
}
|
|
9005
|
+
set.add(cleanVer);
|
|
9006
|
+
};
|
|
9007
|
+
if (filename.endsWith("package-lock.json")) {
|
|
7527
9008
|
try {
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
9009
|
+
const data = JSON.parse(content);
|
|
9010
|
+
if (data.packages && typeof data.packages === "object") {
|
|
9011
|
+
for (const [key, pkg] of Object.entries(data.packages)) {
|
|
9012
|
+
if (!key || key === "" || typeof pkg !== "object" || !pkg) continue;
|
|
9013
|
+
const idx = key.lastIndexOf("node_modules/");
|
|
9014
|
+
if (idx !== -1) {
|
|
9015
|
+
const rawName = key.slice(idx + "node_modules/".length);
|
|
9016
|
+
const ver = pkg.version;
|
|
9017
|
+
add(rawName, ver);
|
|
9018
|
+
}
|
|
9019
|
+
}
|
|
9020
|
+
}
|
|
9021
|
+
if (data.dependencies && typeof data.dependencies === "object") {
|
|
9022
|
+
const walkDeps = (deps) => {
|
|
9023
|
+
for (const [name, info] of Object.entries(deps)) {
|
|
9024
|
+
if (!info || typeof info !== "object") continue;
|
|
9025
|
+
const depObj = info;
|
|
9026
|
+
add(name, depObj.version);
|
|
9027
|
+
if (depObj.dependencies && typeof depObj.dependencies === "object") {
|
|
9028
|
+
walkDeps(depObj.dependencies);
|
|
9029
|
+
}
|
|
9030
|
+
}
|
|
9031
|
+
};
|
|
9032
|
+
walkDeps(data.dependencies);
|
|
7543
9033
|
}
|
|
9034
|
+
} catch {
|
|
7544
9035
|
}
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
9036
|
+
} else if (filename.endsWith("pnpm-lock.yaml")) {
|
|
9037
|
+
try {
|
|
9038
|
+
const data = YAML2.parse(content);
|
|
9039
|
+
if (data?.packages && typeof data.packages === "object") {
|
|
9040
|
+
for (const [key, val] of Object.entries(data.packages)) {
|
|
9041
|
+
let rawKey = key.replace(/^\//, "");
|
|
9042
|
+
const parenIdx = rawKey.indexOf("(");
|
|
9043
|
+
if (parenIdx !== -1) {
|
|
9044
|
+
rawKey = rawKey.slice(0, parenIdx);
|
|
9045
|
+
}
|
|
9046
|
+
const lastAt = rawKey.lastIndexOf("@");
|
|
9047
|
+
if (lastAt > 0) {
|
|
9048
|
+
const name = val?.name ?? rawKey.slice(0, lastAt);
|
|
9049
|
+
const version = val?.version ?? rawKey.slice(lastAt + 1);
|
|
9050
|
+
add(name, version);
|
|
9051
|
+
}
|
|
9052
|
+
}
|
|
7551
9053
|
}
|
|
7552
|
-
|
|
7553
|
-
const
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
9054
|
+
if (data?.snapshots && typeof data.snapshots === "object") {
|
|
9055
|
+
for (const key of Object.keys(data.snapshots)) {
|
|
9056
|
+
let rawKey = key.replace(/^\//, "");
|
|
9057
|
+
const parenIdx = rawKey.indexOf("(");
|
|
9058
|
+
if (parenIdx !== -1) {
|
|
9059
|
+
rawKey = rawKey.slice(0, parenIdx);
|
|
9060
|
+
}
|
|
9061
|
+
const lastAt = rawKey.lastIndexOf("@");
|
|
9062
|
+
if (lastAt > 0) {
|
|
9063
|
+
const name = rawKey.slice(0, lastAt);
|
|
9064
|
+
const version = rawKey.slice(lastAt + 1);
|
|
9065
|
+
add(name, version);
|
|
9066
|
+
}
|
|
7557
9067
|
}
|
|
7558
|
-
} catch {
|
|
7559
9068
|
}
|
|
9069
|
+
} catch {
|
|
7560
9070
|
}
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
);
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
9071
|
+
} else if (filename.endsWith("yarn.lock")) {
|
|
9072
|
+
const lines = content.split("\n");
|
|
9073
|
+
let currentPackages = [];
|
|
9074
|
+
for (const line of lines) {
|
|
9075
|
+
if (line.startsWith("#") || line.trim() === "") continue;
|
|
9076
|
+
if (!line.startsWith(" ")) {
|
|
9077
|
+
currentPackages = [];
|
|
9078
|
+
const parts = line.split(",").map((p) => p.trim().replace(/^"/, "").replace(/":?$/, ""));
|
|
9079
|
+
for (const part of parts) {
|
|
9080
|
+
const lastAt = part.lastIndexOf("@");
|
|
9081
|
+
if (lastAt > 0) {
|
|
9082
|
+
currentPackages.push(part.slice(0, lastAt));
|
|
9083
|
+
}
|
|
9084
|
+
}
|
|
9085
|
+
} else {
|
|
9086
|
+
const match = line.match(/^\s+version\s+"?([^"\s]+)"?/);
|
|
9087
|
+
if (match && currentPackages.length > 0) {
|
|
9088
|
+
const ver = match[1];
|
|
9089
|
+
for (const pkg of currentPackages) {
|
|
9090
|
+
add(pkg, ver);
|
|
9091
|
+
}
|
|
9092
|
+
currentPackages = [];
|
|
9093
|
+
}
|
|
7572
9094
|
}
|
|
7573
9095
|
}
|
|
7574
|
-
entries.push({
|
|
7575
|
-
cveId: cveDetails.id,
|
|
7576
|
-
severity: cveDetails.severity,
|
|
7577
|
-
summary: cveDetails.summary,
|
|
7578
|
-
vulnerableRange: affected?.vulnerableRange,
|
|
7579
|
-
firstPatchedVersion: affected?.firstPatchedVersion,
|
|
7580
|
-
safeUpgradeVersion,
|
|
7581
|
-
epssScore,
|
|
7582
|
-
epssPercentile,
|
|
7583
|
-
inCisaKev,
|
|
7584
|
-
references: cveDetails.references
|
|
7585
|
-
});
|
|
7586
9096
|
}
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
9097
|
+
return result;
|
|
9098
|
+
}
|
|
9099
|
+
function computeLockfileDiff(filename, oldContent, newContent) {
|
|
9100
|
+
const changes = [];
|
|
9101
|
+
const oldDeps = oldContent ? parseLockfileDependencies(filename, oldContent) : /* @__PURE__ */ new Map();
|
|
9102
|
+
const newDeps = newContent ? parseLockfileDependencies(filename, newContent) : /* @__PURE__ */ new Map();
|
|
9103
|
+
const allPkgs = /* @__PURE__ */ new Set([...oldDeps.keys(), ...newDeps.keys()]);
|
|
9104
|
+
for (const pkg of allPkgs) {
|
|
9105
|
+
const oldVers = oldDeps.get(pkg) ?? /* @__PURE__ */ new Set();
|
|
9106
|
+
const newVers = newDeps.get(pkg) ?? /* @__PURE__ */ new Set();
|
|
9107
|
+
for (const nVer of newVers) {
|
|
9108
|
+
if (!oldVers.has(nVer)) {
|
|
9109
|
+
if (oldVers.size === 0) {
|
|
9110
|
+
changes.push({
|
|
9111
|
+
packageName: pkg,
|
|
9112
|
+
changeType: "added",
|
|
9113
|
+
newVersion: nVer,
|
|
9114
|
+
dependencyType: "transitive",
|
|
9115
|
+
manifestPath: filename
|
|
9116
|
+
});
|
|
9117
|
+
} else {
|
|
9118
|
+
const [firstOld] = oldVers;
|
|
9119
|
+
const cleanOld = semver8.clean(firstOld) || semver8.coerce(firstOld)?.version;
|
|
9120
|
+
const cleanNew = semver8.clean(nVer) || semver8.coerce(nVer)?.version;
|
|
9121
|
+
const isUpgrade = cleanOld && cleanNew && semver8.valid(cleanOld) && semver8.valid(cleanNew) ? semver8.gt(cleanNew, cleanOld) : true;
|
|
9122
|
+
changes.push({
|
|
9123
|
+
packageName: pkg,
|
|
9124
|
+
changeType: isUpgrade ? "upgraded" : "downgraded",
|
|
9125
|
+
oldVersion: firstOld,
|
|
9126
|
+
newVersion: nVer,
|
|
9127
|
+
dependencyType: "transitive",
|
|
9128
|
+
manifestPath: filename
|
|
9129
|
+
});
|
|
9130
|
+
}
|
|
9131
|
+
}
|
|
9132
|
+
}
|
|
9133
|
+
if (newVers.size === 0 && oldVers.size > 0) {
|
|
9134
|
+
for (const oVer of oldVers) {
|
|
9135
|
+
changes.push({
|
|
9136
|
+
packageName: pkg,
|
|
9137
|
+
changeType: "removed",
|
|
9138
|
+
oldVersion: oVer,
|
|
9139
|
+
dependencyType: "transitive",
|
|
9140
|
+
manifestPath: filename
|
|
9141
|
+
});
|
|
9142
|
+
}
|
|
7600
9143
|
}
|
|
7601
9144
|
}
|
|
7602
|
-
|
|
7603
|
-
if (isVulnerable) {
|
|
7604
|
-
const upgradeCandidates = entries.map((e) => e.safeUpgradeVersion).filter((v) => Boolean(v && semver7.valid(v))).sort((a, b) => semver7.compare(a, b));
|
|
7605
|
-
recommendedVersion = upgradeCandidates[upgradeCandidates.length - 1] || latestVersion;
|
|
7606
|
-
} else {
|
|
7607
|
-
recommendedVersion = version || latestVersion;
|
|
7608
|
-
}
|
|
7609
|
-
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.`;
|
|
7610
|
-
return {
|
|
7611
|
-
schemaVersion: "1.0",
|
|
7612
|
-
packageName,
|
|
7613
|
-
evaluatedVersion: version,
|
|
7614
|
-
isVulnerable,
|
|
7615
|
-
verdict,
|
|
7616
|
-
summary,
|
|
7617
|
-
vulnerabilities: entries,
|
|
7618
|
-
recommendedVersion,
|
|
7619
|
-
latestVersion,
|
|
7620
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7621
|
-
};
|
|
9145
|
+
return changes;
|
|
7622
9146
|
}
|
|
7623
9147
|
|
|
7624
9148
|
// src/scanner/delta-scan.ts
|
|
7625
|
-
import { existsSync as existsSync12, readFileSync as readFileSync17 } from "fs";
|
|
7626
|
-
import { join as join20 } from "path";
|
|
7627
|
-
import { execa as execa12 } from "execa";
|
|
7628
|
-
import semver8 from "semver";
|
|
7629
9149
|
var DEP_SECTIONS = [
|
|
7630
9150
|
"dependencies",
|
|
7631
9151
|
"devDependencies",
|
|
@@ -7635,7 +9155,7 @@ var DEP_SECTIONS = [
|
|
|
7635
9155
|
async function getChangedManifestPaths(cwd, baseRef) {
|
|
7636
9156
|
const manifests = /* @__PURE__ */ new Set();
|
|
7637
9157
|
try {
|
|
7638
|
-
const { stdout: diffFiles } = await
|
|
9158
|
+
const { stdout: diffFiles } = await execa13(
|
|
7639
9159
|
"git",
|
|
7640
9160
|
["diff", "--name-only", baseRef, "--", "package.json", "**/package.json"],
|
|
7641
9161
|
{ cwd, reject: false }
|
|
@@ -7646,7 +9166,7 @@ async function getChangedManifestPaths(cwd, baseRef) {
|
|
|
7646
9166
|
manifests.add(trimmed);
|
|
7647
9167
|
}
|
|
7648
9168
|
}
|
|
7649
|
-
const { stdout: untrackedFiles } = await
|
|
9169
|
+
const { stdout: untrackedFiles } = await execa13(
|
|
7650
9170
|
"git",
|
|
7651
9171
|
["ls-files", "--others", "--exclude-standard", "--", "package.json", "**/package.json"],
|
|
7652
9172
|
{ cwd, reject: false }
|
|
@@ -7658,7 +9178,7 @@ async function getChangedManifestPaths(cwd, baseRef) {
|
|
|
7658
9178
|
}
|
|
7659
9179
|
}
|
|
7660
9180
|
} catch {
|
|
7661
|
-
if (
|
|
9181
|
+
if (existsSync16(join23(cwd, "package.json"))) {
|
|
7662
9182
|
manifests.add("package.json");
|
|
7663
9183
|
}
|
|
7664
9184
|
}
|
|
@@ -7666,7 +9186,7 @@ async function getChangedManifestPaths(cwd, baseRef) {
|
|
|
7666
9186
|
}
|
|
7667
9187
|
async function getOldManifestContent(cwd, baseRef, relPath) {
|
|
7668
9188
|
try {
|
|
7669
|
-
const { stdout } = await
|
|
9189
|
+
const { stdout } = await execa13("git", ["show", `${baseRef}:${relPath}`], {
|
|
7670
9190
|
cwd,
|
|
7671
9191
|
reject: false
|
|
7672
9192
|
});
|
|
@@ -7677,10 +9197,10 @@ async function getOldManifestContent(cwd, baseRef, relPath) {
|
|
|
7677
9197
|
}
|
|
7678
9198
|
}
|
|
7679
9199
|
function getNewManifestContent(cwd, relPath) {
|
|
7680
|
-
const fullPath =
|
|
7681
|
-
if (!
|
|
9200
|
+
const fullPath = join23(cwd, relPath);
|
|
9201
|
+
if (!existsSync16(fullPath)) return null;
|
|
7682
9202
|
try {
|
|
7683
|
-
return JSON.parse(
|
|
9203
|
+
return JSON.parse(readFileSync21(fullPath, "utf8"));
|
|
7684
9204
|
} catch {
|
|
7685
9205
|
return null;
|
|
7686
9206
|
}
|
|
@@ -7701,9 +9221,9 @@ function computeDependencyDiff(relPath, oldPkg, newPkg) {
|
|
|
7701
9221
|
manifestPath: relPath
|
|
7702
9222
|
});
|
|
7703
9223
|
} else if (oldVer !== newVer) {
|
|
7704
|
-
const cleanOld =
|
|
7705
|
-
const cleanNew =
|
|
7706
|
-
const isUpgrade = cleanOld && cleanNew &&
|
|
9224
|
+
const cleanOld = semver9.clean(oldVer) || semver9.coerce(oldVer)?.version;
|
|
9225
|
+
const cleanNew = semver9.clean(newVer) || semver9.coerce(newVer)?.version;
|
|
9226
|
+
const isUpgrade = cleanOld && cleanNew && semver9.valid(cleanOld) && semver9.valid(cleanNew) ? semver9.gt(cleanNew, cleanOld) : true;
|
|
7707
9227
|
changes.push({
|
|
7708
9228
|
packageName: pkgName,
|
|
7709
9229
|
changeType: isUpgrade ? "upgraded" : "downgraded",
|
|
@@ -7728,22 +9248,89 @@ function computeDependencyDiff(relPath, oldPkg, newPkg) {
|
|
|
7728
9248
|
}
|
|
7729
9249
|
return changes;
|
|
7730
9250
|
}
|
|
9251
|
+
var LOCKFILE_PATTERNS = [
|
|
9252
|
+
"package-lock.json",
|
|
9253
|
+
"**/package-lock.json",
|
|
9254
|
+
"pnpm-lock.yaml",
|
|
9255
|
+
"**/pnpm-lock.yaml",
|
|
9256
|
+
"yarn.lock",
|
|
9257
|
+
"**/yarn.lock"
|
|
9258
|
+
];
|
|
9259
|
+
async function getChangedLockfilePaths(cwd, baseRef) {
|
|
9260
|
+
const lockfiles = /* @__PURE__ */ new Set();
|
|
9261
|
+
try {
|
|
9262
|
+
const { stdout: diffFiles } = await execa13(
|
|
9263
|
+
"git",
|
|
9264
|
+
["diff", "--name-only", baseRef, "--", ...LOCKFILE_PATTERNS],
|
|
9265
|
+
{ cwd, reject: false }
|
|
9266
|
+
);
|
|
9267
|
+
for (const f of diffFiles.split("\n")) {
|
|
9268
|
+
const trimmed = f.trim();
|
|
9269
|
+
if (trimmed && !trimmed.includes("node_modules") && (trimmed.endsWith("package-lock.json") || trimmed.endsWith("pnpm-lock.yaml") || trimmed.endsWith("yarn.lock"))) {
|
|
9270
|
+
lockfiles.add(trimmed);
|
|
9271
|
+
}
|
|
9272
|
+
}
|
|
9273
|
+
const { stdout: untrackedFiles } = await execa13(
|
|
9274
|
+
"git",
|
|
9275
|
+
["ls-files", "--others", "--exclude-standard", "--", ...LOCKFILE_PATTERNS],
|
|
9276
|
+
{ cwd, reject: false }
|
|
9277
|
+
);
|
|
9278
|
+
for (const f of untrackedFiles.split("\n")) {
|
|
9279
|
+
const trimmed = f.trim();
|
|
9280
|
+
if (trimmed && !trimmed.includes("node_modules") && (trimmed.endsWith("package-lock.json") || trimmed.endsWith("pnpm-lock.yaml") || trimmed.endsWith("yarn.lock"))) {
|
|
9281
|
+
lockfiles.add(trimmed);
|
|
9282
|
+
}
|
|
9283
|
+
}
|
|
9284
|
+
} catch {
|
|
9285
|
+
}
|
|
9286
|
+
return Array.from(lockfiles);
|
|
9287
|
+
}
|
|
9288
|
+
async function getFileContentAtRef(cwd, baseRef, relPath) {
|
|
9289
|
+
try {
|
|
9290
|
+
const { stdout } = await execa13("git", ["show", `${baseRef}:${relPath}`], {
|
|
9291
|
+
cwd,
|
|
9292
|
+
reject: false
|
|
9293
|
+
});
|
|
9294
|
+
return stdout || null;
|
|
9295
|
+
} catch {
|
|
9296
|
+
return null;
|
|
9297
|
+
}
|
|
9298
|
+
}
|
|
7731
9299
|
async function scanDelta(options = {}) {
|
|
7732
9300
|
const cwd = options.cwd ?? process.cwd();
|
|
7733
9301
|
const baseRef = options.baseRef ?? "HEAD";
|
|
7734
9302
|
const changedManifests = await getChangedManifestPaths(cwd, baseRef);
|
|
9303
|
+
const changedLockfiles = await getChangedLockfilePaths(cwd, baseRef);
|
|
7735
9304
|
const dependencyChanges = [];
|
|
9305
|
+
const directPkgs = /* @__PURE__ */ new Set();
|
|
7736
9306
|
for (const manifestRel of changedManifests) {
|
|
7737
9307
|
const oldPkg = await getOldManifestContent(cwd, baseRef, manifestRel);
|
|
7738
9308
|
const newPkg = getNewManifestContent(cwd, manifestRel);
|
|
7739
9309
|
const diffs = computeDependencyDiff(manifestRel, oldPkg, newPkg);
|
|
9310
|
+
for (const d of diffs) {
|
|
9311
|
+
directPkgs.add(d.packageName);
|
|
9312
|
+
}
|
|
7740
9313
|
dependencyChanges.push(...diffs);
|
|
7741
9314
|
}
|
|
9315
|
+
for (const lockfileRel of changedLockfiles) {
|
|
9316
|
+
const oldContent = await getFileContentAtRef(cwd, baseRef, lockfileRel);
|
|
9317
|
+
const fullPath = join23(cwd, lockfileRel);
|
|
9318
|
+
const newContent = existsSync16(fullPath) ? readFileSync21(fullPath, "utf8") : null;
|
|
9319
|
+
const lockDiffs = computeLockfileDiff(lockfileRel, oldContent, newContent);
|
|
9320
|
+
for (const change of lockDiffs) {
|
|
9321
|
+
if (!directPkgs.has(change.packageName)) {
|
|
9322
|
+
dependencyChanges.push(change);
|
|
9323
|
+
}
|
|
9324
|
+
}
|
|
9325
|
+
if (!changedManifests.includes(lockfileRel)) {
|
|
9326
|
+
changedManifests.push(lockfileRel);
|
|
9327
|
+
}
|
|
9328
|
+
}
|
|
7742
9329
|
const introducedFindings = [];
|
|
7743
9330
|
const resolvedFindings = [];
|
|
7744
9331
|
for (const change of dependencyChanges) {
|
|
7745
9332
|
if (change.changeType === "added" || change.changeType === "upgraded" || change.changeType === "downgraded") {
|
|
7746
|
-
const cleanVer =
|
|
9333
|
+
const cleanVer = semver9.coerce(change.newVersion)?.version ?? change.newVersion;
|
|
7747
9334
|
const evaluation = await evaluatePackage(change.packageName, {
|
|
7748
9335
|
version: cleanVer,
|
|
7749
9336
|
enrichIntelligence: true
|
|
@@ -7763,12 +9350,12 @@ async function scanDelta(options = {}) {
|
|
|
7763
9350
|
}
|
|
7764
9351
|
}
|
|
7765
9352
|
if (change.oldVersion && (change.changeType === "upgraded" || change.changeType === "removed")) {
|
|
7766
|
-
const cleanOldVer =
|
|
9353
|
+
const cleanOldVer = semver9.coerce(change.oldVersion)?.version ?? change.oldVersion;
|
|
7767
9354
|
const oldEval = await evaluatePackage(change.packageName, {
|
|
7768
9355
|
version: cleanOldVer,
|
|
7769
9356
|
enrichIntelligence: true
|
|
7770
9357
|
});
|
|
7771
|
-
const cleanNewVer = change.newVersion ?
|
|
9358
|
+
const cleanNewVer = change.newVersion ? semver9.coerce(change.newVersion)?.version ?? change.newVersion : void 0;
|
|
7772
9359
|
const newEval = cleanNewVer ? await evaluatePackage(change.packageName, {
|
|
7773
9360
|
version: cleanNewVer,
|
|
7774
9361
|
enrichIntelligence: false
|
|
@@ -7931,9 +9518,9 @@ function toSarifOutput(report) {
|
|
|
7931
9518
|
}
|
|
7932
9519
|
|
|
7933
9520
|
// src/version.ts
|
|
7934
|
-
import { readFileSync as
|
|
9521
|
+
import { readFileSync as readFileSync22 } from "fs";
|
|
7935
9522
|
function readPackageVersion() {
|
|
7936
|
-
const raw =
|
|
9523
|
+
const raw = readFileSync22(new URL("../package.json", import.meta.url), "utf8");
|
|
7937
9524
|
const metadata = JSON.parse(raw);
|
|
7938
9525
|
if (!metadata.version) {
|
|
7939
9526
|
throw new Error("packages/core/package.json is missing a version field.");
|
|
@@ -7958,13 +9545,17 @@ export {
|
|
|
7958
9545
|
listPatchArtifacts,
|
|
7959
9546
|
inspectPatchArtifact,
|
|
7960
9547
|
validatePatchArtifact,
|
|
9548
|
+
evaluatePackage,
|
|
9549
|
+
reconcilePatchArtifacts,
|
|
7961
9550
|
remediate,
|
|
7962
9551
|
planRemediation,
|
|
9552
|
+
parseScanInput,
|
|
9553
|
+
parseScanInputFromAudit,
|
|
9554
|
+
uniqueCveIds,
|
|
7963
9555
|
remediateFromScan,
|
|
7964
9556
|
remediatePortfolio,
|
|
7965
9557
|
updateOutdated,
|
|
7966
9558
|
checkReachability,
|
|
7967
|
-
evaluatePackage,
|
|
7968
9559
|
scanDelta,
|
|
7969
9560
|
submitRemediateJob,
|
|
7970
9561
|
submitScanJob,
|