autoremediator 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/api/change-request/index.d.ts +2 -1
- package/dist/api/change-request/index.d.ts.map +1 -1
- package/dist/api/delta-scan/delta-scan.test.d.ts +2 -0
- package/dist/api/delta-scan/delta-scan.test.d.ts.map +1 -0
- package/dist/api/delta-scan/index.d.ts +5 -0
- package/dist/api/delta-scan/index.d.ts.map +1 -0
- package/dist/api/evaluate/evaluate.test.d.ts +2 -0
- package/dist/api/evaluate/evaluate.test.d.ts.map +1 -0
- package/dist/api/evaluate/index.d.ts +5 -0
- package/dist/api/evaluate/index.d.ts.map +1 -0
- package/dist/api/index.d.ts +5 -2
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/patches/index.d.ts +1 -0
- package/dist/api/patches/index.d.ts.map +1 -1
- package/dist/api/patches/reconcile.d.ts +7 -0
- package/dist/api/patches/reconcile.d.ts.map +1 -0
- package/dist/api/patches/reconcile.test.d.ts +2 -0
- package/dist/api/patches/reconcile.test.d.ts.map +1 -0
- package/dist/api/reachability/index.d.ts +3 -0
- package/dist/api/reachability/index.d.ts.map +1 -0
- package/dist/api/reachability/reachability.test.d.ts +2 -0
- package/dist/api/reachability/reachability.test.d.ts.map +1 -0
- package/dist/api/remediate/validators.d.ts +3 -0
- package/dist/api/remediate/validators.d.ts.map +1 -1
- package/dist/api/reporting.d.ts.map +1 -1
- package/dist/api/vex/index.d.ts +1 -1
- package/dist/api/vex/index.d.ts.map +1 -1
- package/dist/{chunk-CQXNIRHT.js → chunk-FXOUJDQ3.js} +2003 -756
- package/dist/cli/program.d.ts.map +1 -1
- package/dist/cli/runners.d.ts +22 -0
- package/dist/cli/runners.d.ts.map +1 -1
- package/dist/cli/triage.d.ts +7 -0
- package/dist/cli/triage.d.ts.map +1 -0
- package/dist/cli/types.d.ts +2 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli.js +365 -3
- package/dist/index.js +1967 -721
- package/dist/index.js.map +1 -1
- package/dist/intelligence/index.d.ts +1 -1
- package/dist/intelligence/index.d.ts.map +1 -1
- package/dist/intelligence/sources/github-advisory.d.ts +8 -0
- package/dist/intelligence/sources/github-advisory.d.ts.map +1 -1
- package/dist/intelligence/sources/osv.d.ts +4 -0
- package/dist/intelligence/sources/osv.d.ts.map +1 -1
- package/dist/mcp/server.d.ts +106 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +100 -1
- package/dist/openapi/rbac.d.ts +2 -2
- package/dist/openapi/rbac.d.ts.map +1 -1
- package/dist/openapi/server.d.ts.map +1 -1
- package/dist/openapi/server.js +13 -6
- package/dist/platform/package-manager/index.d.ts +1 -0
- package/dist/platform/package-manager/index.d.ts.map +1 -1
- package/dist/platform/supply-chain.d.ts +10 -0
- package/dist/platform/supply-chain.d.ts.map +1 -0
- package/dist/platform/supply-chain.test.d.ts +2 -0
- package/dist/platform/supply-chain.test.d.ts.map +1 -0
- package/dist/platform/types.d.ts +121 -4
- package/dist/platform/types.d.ts.map +1 -1
- package/dist/platform/workspace.d.ts +28 -0
- package/dist/platform/workspace.d.ts.map +1 -0
- package/dist/platform/workspace.test.d.ts +2 -0
- package/dist/platform/workspace.test.d.ts.map +1 -0
- package/dist/remediation/evaluate-package.d.ts +3 -0
- package/dist/remediation/evaluate-package.d.ts.map +1 -0
- package/dist/remediation/local/options.d.ts.map +1 -1
- package/dist/remediation/local/primary-strategy.d.ts +3 -1
- package/dist/remediation/local/primary-strategy.d.ts.map +1 -1
- package/dist/remediation/runtime-tools.d.ts +1 -1
- package/dist/remediation/runtime-tools.d.ts.map +1 -1
- package/dist/remediation/strategies/patch-synthesis-prompt.d.ts.map +1 -1
- package/dist/remediation/tools/apply-patch-file/helpers.d.ts.map +1 -1
- package/dist/remediation/tools/apply-patch-file/index.d.ts +1 -0
- package/dist/remediation/tools/apply-patch-file/index.d.ts.map +1 -1
- package/dist/remediation/tools/apply-version-bump.d.ts +1 -0
- package/dist/remediation/tools/apply-version-bump.d.ts.map +1 -1
- package/dist/remediation/tools/check-inventory.d.ts.map +1 -1
- package/dist/remediation/tools/check-reachability.d.ts +6 -1
- package/dist/remediation/tools/check-reachability.d.ts.map +1 -1
- package/dist/remediation/tools/generate-patch/helpers.d.ts +1 -0
- package/dist/remediation/tools/generate-patch/helpers.d.ts.map +1 -1
- package/dist/remediation/tools/generate-patch/index.d.ts +1 -0
- package/dist/remediation/tools/generate-patch/index.d.ts.map +1 -1
- package/dist/scanner/adapters/npm-audit.d.ts.map +1 -1
- package/dist/scanner/adapters/yarn-audit.d.ts.map +1 -1
- package/dist/scanner/delta-scan.d.ts +3 -0
- package/dist/scanner/delta-scan.d.ts.map +1 -0
- package/dist/scanner/lockfile-diff.d.ts +10 -0
- package/dist/scanner/lockfile-diff.d.ts.map +1 -0
- package/dist/scanner/lockfile-diff.test.d.ts +2 -0
- package/dist/scanner/lockfile-diff.test.d.ts.map +1 -0
- package/package.json +7 -9
|
@@ -463,12 +463,12 @@ import { readdir, readFile as readFile2, stat } from "fs/promises";
|
|
|
463
463
|
import { join as join5 } from "path";
|
|
464
464
|
|
|
465
465
|
// src/remediation/strategies/patch-utils.ts
|
|
466
|
-
import { existsSync as existsSync3, mkdirSync, writeFileSync, readFileSync as
|
|
466
|
+
import { existsSync as existsSync3, mkdirSync, writeFileSync, readFileSync as readFileSync3 } from "fs";
|
|
467
467
|
import { join as join4 } from "path";
|
|
468
468
|
import { execa as execa2 } from "execa";
|
|
469
469
|
|
|
470
470
|
// src/platform/package-manager/index.ts
|
|
471
|
-
import { existsSync as existsSync2 } from "fs";
|
|
471
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
472
472
|
import { join as join3 } from "path";
|
|
473
473
|
import { execa } from "execa";
|
|
474
474
|
|
|
@@ -661,6 +661,32 @@ function resolveTestCommand(pm, constraints) {
|
|
|
661
661
|
const base = pm === "pnpm" ? ["pnpm", "test"] : pm === "yarn" ? ["yarn", "test"] : pm === "bun" ? ["bun", "test"] : pm === "deno" ? ["deno", "test"] : ["npm", "test"];
|
|
662
662
|
return withWorkspace(base, pm, constraints?.workspace);
|
|
663
663
|
}
|
|
664
|
+
function resolveTypecheckCommand(pm, constraints, cwd = process.cwd()) {
|
|
665
|
+
const pkgJsonPath = join3(cwd, "package.json");
|
|
666
|
+
let hasTypecheckScript = false;
|
|
667
|
+
const hasTsConfig = existsSync2(join3(cwd, "tsconfig.json"));
|
|
668
|
+
if (existsSync2(pkgJsonPath)) {
|
|
669
|
+
try {
|
|
670
|
+
const pkg = JSON.parse(readFileSync2(pkgJsonPath, "utf8"));
|
|
671
|
+
if (pkg.scripts?.typecheck) {
|
|
672
|
+
hasTypecheckScript = true;
|
|
673
|
+
}
|
|
674
|
+
} catch {
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (hasTypecheckScript) {
|
|
678
|
+
const base = [pm, "run", "typecheck"];
|
|
679
|
+
return withWorkspace(base, pm, constraints?.workspace);
|
|
680
|
+
}
|
|
681
|
+
if (hasTsConfig) {
|
|
682
|
+
if (pm === "deno") {
|
|
683
|
+
return withWorkspace(["deno", "check"], pm, constraints?.workspace);
|
|
684
|
+
}
|
|
685
|
+
const base = ["npx", "tsc", "--noEmit"];
|
|
686
|
+
return withWorkspace(base, pm, constraints?.workspace);
|
|
687
|
+
}
|
|
688
|
+
return [];
|
|
689
|
+
}
|
|
664
690
|
function resolveAuditCommand(pm, constraints) {
|
|
665
691
|
if (pm === "deno") {
|
|
666
692
|
throw new Error(
|
|
@@ -905,12 +931,12 @@ function defineTool(config) {
|
|
|
905
931
|
|
|
906
932
|
// src/remediation/tools/check-inventory.ts
|
|
907
933
|
import { z } from "zod";
|
|
908
|
-
import { readFileSync as
|
|
909
|
-
import { join as
|
|
934
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
935
|
+
import { join as join8 } from "path";
|
|
910
936
|
import { execa as execa3 } from "execa";
|
|
911
937
|
|
|
912
938
|
// src/platform/policy.ts
|
|
913
|
-
import { existsSync as existsSync6, readFileSync as
|
|
939
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
|
|
914
940
|
import { join as join6 } from "path";
|
|
915
941
|
import { parse as yamlParse } from "yaml";
|
|
916
942
|
var DEFAULT_POLICY = {
|
|
@@ -945,7 +971,7 @@ function loadPolicy(cwd, explicitPath) {
|
|
|
945
971
|
const candidate = explicitPath ?? join6(cwd, ".github", "autoremediator.yml");
|
|
946
972
|
if (!existsSync6(candidate)) return DEFAULT_POLICY;
|
|
947
973
|
try {
|
|
948
|
-
const parsed = yamlParse(
|
|
974
|
+
const parsed = yamlParse(readFileSync4(candidate, "utf8"));
|
|
949
975
|
return {
|
|
950
976
|
allowMajorBumps: parsed.allowMajorBumps ?? DEFAULT_POLICY.allowMajorBumps,
|
|
951
977
|
denyPackages: parsed.denyPackages ?? DEFAULT_POLICY.denyPackages,
|
|
@@ -1006,7 +1032,7 @@ function isActiveSuppression(suppression) {
|
|
|
1006
1032
|
}
|
|
1007
1033
|
function loadSuppressionsFile(filePath) {
|
|
1008
1034
|
try {
|
|
1009
|
-
const content =
|
|
1035
|
+
const content = readFileSync4(filePath, "utf8");
|
|
1010
1036
|
const parsed = yamlParse(content);
|
|
1011
1037
|
return Array.isArray(parsed?.suppressions) ? parsed.suppressions : [];
|
|
1012
1038
|
} catch {
|
|
@@ -1032,6 +1058,140 @@ function checkSlaBreach(cveId, severity, publishedAt, slaPolicy) {
|
|
|
1032
1058
|
};
|
|
1033
1059
|
}
|
|
1034
1060
|
|
|
1061
|
+
// src/platform/workspace.ts
|
|
1062
|
+
import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync5, statSync } from "fs";
|
|
1063
|
+
import { join as join7, resolve as resolve2, relative } from "path";
|
|
1064
|
+
import yaml from "yaml";
|
|
1065
|
+
function findWorkspaceRoot(startDir) {
|
|
1066
|
+
let curr = resolve2(startDir);
|
|
1067
|
+
while (true) {
|
|
1068
|
+
if (existsSync7(join7(curr, "pnpm-workspace.yaml"))) return curr;
|
|
1069
|
+
const pkgPath = join7(curr, "package.json");
|
|
1070
|
+
if (existsSync7(pkgPath)) {
|
|
1071
|
+
try {
|
|
1072
|
+
const parsed = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
1073
|
+
if (parsed.workspaces) return curr;
|
|
1074
|
+
} catch {
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const parent = resolve2(curr, "..");
|
|
1078
|
+
if (parent === curr) break;
|
|
1079
|
+
curr = parent;
|
|
1080
|
+
}
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
function findWorkspacePackages(cwd) {
|
|
1084
|
+
const rootCwd = findWorkspaceRoot(cwd) ?? resolve2(cwd);
|
|
1085
|
+
const packages = [];
|
|
1086
|
+
const packageGlobs = [];
|
|
1087
|
+
const pnpmWorkspaceFile = join7(rootCwd, "pnpm-workspace.yaml");
|
|
1088
|
+
if (existsSync7(pnpmWorkspaceFile)) {
|
|
1089
|
+
try {
|
|
1090
|
+
const content = readFileSync5(pnpmWorkspaceFile, "utf8");
|
|
1091
|
+
const parsed = yaml.parse(content);
|
|
1092
|
+
if (Array.isArray(parsed?.packages)) {
|
|
1093
|
+
packageGlobs.push(...parsed.packages);
|
|
1094
|
+
}
|
|
1095
|
+
} catch {
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
const rootPkgFile = join7(rootCwd, "package.json");
|
|
1099
|
+
if (existsSync7(rootPkgFile)) {
|
|
1100
|
+
try {
|
|
1101
|
+
const rootPkg = JSON.parse(readFileSync5(rootPkgFile, "utf8"));
|
|
1102
|
+
if (Array.isArray(rootPkg.workspaces)) {
|
|
1103
|
+
packageGlobs.push(...rootPkg.workspaces);
|
|
1104
|
+
} else if (Array.isArray(rootPkg.workspaces?.packages)) {
|
|
1105
|
+
packageGlobs.push(...rootPkg.workspaces.packages);
|
|
1106
|
+
}
|
|
1107
|
+
} catch {
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
const denoJsonFile = join7(rootCwd, "deno.json");
|
|
1111
|
+
if (existsSync7(denoJsonFile)) {
|
|
1112
|
+
try {
|
|
1113
|
+
const denoJson = JSON.parse(readFileSync5(denoJsonFile, "utf8"));
|
|
1114
|
+
if (Array.isArray(denoJson.workspace)) {
|
|
1115
|
+
packageGlobs.push(...denoJson.workspace);
|
|
1116
|
+
}
|
|
1117
|
+
} catch {
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
if (packageGlobs.length === 0) {
|
|
1121
|
+
return packages;
|
|
1122
|
+
}
|
|
1123
|
+
const seenDirs = /* @__PURE__ */ new Set();
|
|
1124
|
+
for (const glob of packageGlobs) {
|
|
1125
|
+
const cleanGlob = glob.trim().replace(/\/+$/, "");
|
|
1126
|
+
if (!cleanGlob || cleanGlob.startsWith("!")) continue;
|
|
1127
|
+
if (cleanGlob.endsWith("/*")) {
|
|
1128
|
+
const parentDir = join7(rootCwd, cleanGlob.slice(0, -2));
|
|
1129
|
+
if (!existsSync7(parentDir)) continue;
|
|
1130
|
+
try {
|
|
1131
|
+
const entries = readdirSync(parentDir);
|
|
1132
|
+
for (const entry of entries) {
|
|
1133
|
+
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
1134
|
+
const subDir = join7(parentDir, entry);
|
|
1135
|
+
try {
|
|
1136
|
+
if (statSync(subDir).isDirectory()) {
|
|
1137
|
+
addWorkspacePackage(rootCwd, subDir, packages, seenDirs);
|
|
1138
|
+
}
|
|
1139
|
+
} catch {
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
} catch {
|
|
1143
|
+
}
|
|
1144
|
+
} else {
|
|
1145
|
+
const directDir = join7(rootCwd, cleanGlob);
|
|
1146
|
+
if (existsSync7(directDir)) {
|
|
1147
|
+
try {
|
|
1148
|
+
if (statSync(directDir).isDirectory()) {
|
|
1149
|
+
addWorkspacePackage(rootCwd, directDir, packages, seenDirs);
|
|
1150
|
+
}
|
|
1151
|
+
} catch {
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
return packages;
|
|
1157
|
+
}
|
|
1158
|
+
function addWorkspacePackage(rootCwd, dir, out, seen) {
|
|
1159
|
+
const norm = resolve2(dir);
|
|
1160
|
+
if (seen.has(norm)) return;
|
|
1161
|
+
seen.add(norm);
|
|
1162
|
+
const manifestPath = join7(dir, "package.json");
|
|
1163
|
+
if (!existsSync7(manifestPath)) return;
|
|
1164
|
+
try {
|
|
1165
|
+
const content = readFileSync5(manifestPath, "utf8");
|
|
1166
|
+
const manifest = JSON.parse(content);
|
|
1167
|
+
out.push({
|
|
1168
|
+
name: manifest.name ?? relative(rootCwd, dir),
|
|
1169
|
+
relativeDir: relative(rootCwd, dir),
|
|
1170
|
+
absoluteDir: norm,
|
|
1171
|
+
manifestPath,
|
|
1172
|
+
manifest
|
|
1173
|
+
});
|
|
1174
|
+
} catch {
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
function findDeclaringWorkspacePackage(cwd, dependencyName, filterWorkspace) {
|
|
1178
|
+
const workspaces = findWorkspacePackages(cwd);
|
|
1179
|
+
if (workspaces.length === 0) return null;
|
|
1180
|
+
const matchesFilter = (wp) => {
|
|
1181
|
+
if (!filterWorkspace) return true;
|
|
1182
|
+
const filter = filterWorkspace.trim();
|
|
1183
|
+
return wp.name === filter || wp.relativeDir === filter || wp.relativeDir.replace(/\\/g, "/") === filter.replace(/\\/g, "/");
|
|
1184
|
+
};
|
|
1185
|
+
for (const wp of workspaces) {
|
|
1186
|
+
if (!matchesFilter(wp)) continue;
|
|
1187
|
+
const isDeclared = Boolean(wp.manifest.dependencies?.[dependencyName]) || Boolean(wp.manifest.devDependencies?.[dependencyName]) || Boolean(wp.manifest.peerDependencies?.[dependencyName]);
|
|
1188
|
+
if (isDeclared) {
|
|
1189
|
+
return wp;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1035
1195
|
// src/remediation/tools/check-inventory.ts
|
|
1036
1196
|
var checkInventoryTool = defineTool({
|
|
1037
1197
|
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 +1209,7 @@ var checkInventoryTool = defineTool({
|
|
|
1049
1209
|
}) => {
|
|
1050
1210
|
let pkgJson;
|
|
1051
1211
|
try {
|
|
1052
|
-
pkgJson = JSON.parse(
|
|
1212
|
+
pkgJson = JSON.parse(readFileSync6(join8(cwd, "package.json"), "utf8"));
|
|
1053
1213
|
} catch {
|
|
1054
1214
|
return {
|
|
1055
1215
|
packages: [],
|
|
@@ -1081,7 +1241,8 @@ var checkInventoryTool = defineTool({
|
|
|
1081
1241
|
}
|
|
1082
1242
|
const packages = [];
|
|
1083
1243
|
for (const [name, version] of installedVersions.entries()) {
|
|
1084
|
-
const
|
|
1244
|
+
const declaringWorkspace = findDeclaringWorkspacePackage(cwd, name, workspace);
|
|
1245
|
+
const isDirect = Boolean(pkgJson.dependencies?.[name]) || Boolean(pkgJson.devDependencies?.[name]) || Boolean(pkgJson.peerDependencies?.[name]) || Boolean(declaringWorkspace);
|
|
1085
1246
|
packages.push({
|
|
1086
1247
|
name,
|
|
1087
1248
|
version,
|
|
@@ -1204,173 +1365,8 @@ async function validatePatchArtifact(patchFilePath, options = {}) {
|
|
|
1204
1365
|
};
|
|
1205
1366
|
}
|
|
1206
1367
|
|
|
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";
|
|
1368
|
+
// src/remediation/evaluate-package.ts
|
|
1369
|
+
import semver2 from "semver";
|
|
1374
1370
|
|
|
1375
1371
|
// src/platform/http-client.ts
|
|
1376
1372
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
@@ -1458,6 +1454,37 @@ async function fetchOsvVuln(cveId) {
|
|
|
1458
1454
|
throw err;
|
|
1459
1455
|
}
|
|
1460
1456
|
}
|
|
1457
|
+
async function queryOsvPackage(packageName, version) {
|
|
1458
|
+
const url = `${OSV_BASE}/query`;
|
|
1459
|
+
const body = {
|
|
1460
|
+
package: {
|
|
1461
|
+
name: packageName,
|
|
1462
|
+
ecosystem: "npm"
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
if (version) {
|
|
1466
|
+
body.version = version;
|
|
1467
|
+
}
|
|
1468
|
+
try {
|
|
1469
|
+
const res = await httpClient({
|
|
1470
|
+
url,
|
|
1471
|
+
method: "POST",
|
|
1472
|
+
body,
|
|
1473
|
+
headers: { "Content-Type": "application/json" }
|
|
1474
|
+
});
|
|
1475
|
+
if (res.status === 404) return [];
|
|
1476
|
+
if (!res.ok) {
|
|
1477
|
+
throw new Error(`OSV API error ${res.status} for query "${packageName}": ${res.text}`);
|
|
1478
|
+
}
|
|
1479
|
+
const data = res.data;
|
|
1480
|
+
return data.vulns ?? [];
|
|
1481
|
+
} catch (err) {
|
|
1482
|
+
if (err instanceof Error) {
|
|
1483
|
+
throw new Error(`OSV API error for query "${packageName}": ${err.message}`);
|
|
1484
|
+
}
|
|
1485
|
+
throw err;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1461
1488
|
function osvEventsToSemverRange(events) {
|
|
1462
1489
|
const parts = [];
|
|
1463
1490
|
for (const event of events) {
|
|
@@ -1521,27 +1548,762 @@ async function lookupCveOsv(cveId) {
|
|
|
1521
1548
|
return parseOsvVuln(vuln);
|
|
1522
1549
|
}
|
|
1523
1550
|
|
|
1524
|
-
// src/
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1551
|
+
// src/platform/config.ts
|
|
1552
|
+
function resolveProvider(options = {}) {
|
|
1553
|
+
const raw = options.llmProvider ?? process.env.AUTOREMEDIATOR_LLM_PROVIDER ?? "remote";
|
|
1554
|
+
if (raw !== "remote" && raw !== "local") {
|
|
1555
|
+
throw new Error(
|
|
1556
|
+
`Unsupported LLM provider "${raw}". Set AUTOREMEDIATOR_LLM_PROVIDER to "remote" or "local".`
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
return raw;
|
|
1560
|
+
}
|
|
1561
|
+
function resolveModelName(provider, options = {}, routing = {}) {
|
|
1562
|
+
if (options.model) return options.model;
|
|
1563
|
+
if (process.env.AUTOREMEDIATOR_MODEL) return process.env.AUTOREMEDIATOR_MODEL;
|
|
1564
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1565
|
+
const policy = loadPolicy(cwd, options.policy);
|
|
1566
|
+
const providerEnvModel = provider === "remote" ? process.env.AUTOREMEDIATOR_MODEL_REMOTE : process.env.AUTOREMEDIATOR_MODEL_LOCAL;
|
|
1567
|
+
if (providerEnvModel) return providerEnvModel;
|
|
1568
|
+
const policyPinnedModel = provider === "remote" ? policy.modelDefaults?.remote : policy.modelDefaults?.local;
|
|
1569
|
+
if (policyPinnedModel) return policyPinnedModel;
|
|
1570
|
+
const defaults = {
|
|
1571
|
+
remote: "remote-default",
|
|
1572
|
+
local: "local"
|
|
1573
|
+
};
|
|
1574
|
+
const routingEnabled = options.dynamicModelRouting ?? policy.dynamicModelRouting ?? false;
|
|
1575
|
+
const threshold = options.dynamicRoutingThresholdChars ?? policy.dynamicRoutingThresholdChars ?? 18e3;
|
|
1576
|
+
if (provider === "remote" && routingEnabled && typeof routing.inputChars === "number" && routing.inputChars >= threshold) {
|
|
1577
|
+
return process.env.AUTOREMEDIATOR_MODEL_REMOTE_LARGE ?? "remote-large";
|
|
1578
|
+
}
|
|
1579
|
+
return defaults[provider];
|
|
1580
|
+
}
|
|
1581
|
+
async function loadRemoteFactory() {
|
|
1582
|
+
const moduleName = process.env.AUTOREMEDIATOR_REMOTE_CLIENT_MODULE;
|
|
1583
|
+
const exportName = process.env.AUTOREMEDIATOR_REMOTE_CLIENT_FACTORY ?? "createRemoteClient";
|
|
1584
|
+
if (!moduleName) {
|
|
1585
|
+
throw new Error(
|
|
1586
|
+
"AUTOREMEDIATOR_REMOTE_CLIENT_MODULE is required for remote provider model loading."
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
if (moduleName.startsWith("./") || moduleName.startsWith("../") || moduleName.startsWith("/") || moduleName.startsWith("file:")) {
|
|
1590
|
+
throw new Error(
|
|
1591
|
+
`AUTOREMEDIATOR_REMOTE_CLIENT_MODULE must be a package name, not a file path: ${moduleName}`
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
const loaded = await import(moduleName);
|
|
1595
|
+
const factory = loaded[exportName];
|
|
1596
|
+
if (typeof factory !== "function") {
|
|
1597
|
+
throw new Error(
|
|
1598
|
+
`Remote client factory "${exportName}" was not found in module "${moduleName}".`
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
return factory;
|
|
1602
|
+
}
|
|
1603
|
+
async function createModel(options = {}, routing = {}) {
|
|
1604
|
+
const provider = resolveProvider(options);
|
|
1605
|
+
if (provider === "local") {
|
|
1606
|
+
throw new Error(
|
|
1607
|
+
"Local provider does not create a language model. Use the deterministic pipeline path instead."
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
const apiKey = process.env.AUTOREMEDIATOR_REMOTE_API_KEY;
|
|
1611
|
+
if (!apiKey) {
|
|
1612
|
+
throw new Error(
|
|
1613
|
+
"AUTOREMEDIATOR_REMOTE_API_KEY environment variable is required for remote provider."
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
const modelName = resolveModelName(provider, options, routing);
|
|
1617
|
+
const createRemoteClient = await loadRemoteFactory();
|
|
1618
|
+
const remoteClient = createRemoteClient({ apiKey });
|
|
1619
|
+
return remoteClient(modelName);
|
|
1620
|
+
}
|
|
1621
|
+
function getPatchConfidenceThreshold(provider, safetyProfile = "relaxed", riskLevel = "medium", overrides) {
|
|
1622
|
+
const relaxed = {
|
|
1623
|
+
remote: { low: 0.65, medium: 0.7, high: 0.8 },
|
|
1624
|
+
local: { low: 0.65, medium: 0.7, high: 0.8 }
|
|
1625
|
+
};
|
|
1626
|
+
const strict = {
|
|
1627
|
+
remote: { low: 0.8, medium: 0.85, high: 0.9 },
|
|
1628
|
+
local: { low: 0.8, medium: 0.85, high: 0.9 }
|
|
1629
|
+
};
|
|
1630
|
+
const baseThreshold = safetyProfile === "strict" ? strict[provider][riskLevel] : relaxed[provider][riskLevel];
|
|
1631
|
+
const override = overrides?.[riskLevel];
|
|
1632
|
+
if (typeof override === "number" && Number.isFinite(override)) {
|
|
1633
|
+
return Math.max(0, Math.min(1, override));
|
|
1634
|
+
}
|
|
1635
|
+
return baseThreshold;
|
|
1636
|
+
}
|
|
1637
|
+
function estimateModelCostUsd(params) {
|
|
1638
|
+
const inputTokens = params.promptChars / 4;
|
|
1639
|
+
const outputTokens = params.completionChars / 4;
|
|
1640
|
+
const pricePerThousand = params.provider === "remote" ? { input: 6e-3, output: 0.02 } : { input: 0, output: 0 };
|
|
1641
|
+
return inputTokens / 1e3 * pricePerThousand.input + outputTokens / 1e3 * pricePerThousand.output;
|
|
1642
|
+
}
|
|
1643
|
+
function getNvdConfig() {
|
|
1644
|
+
return {
|
|
1645
|
+
apiKey: process.env.AUTOREMEDIATOR_NVD_API_KEY
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
function getGitHubToken() {
|
|
1649
|
+
return process.env.GITHUB_TOKEN;
|
|
1650
|
+
}
|
|
1651
|
+
function getIntelligenceSourceConfig() {
|
|
1652
|
+
return {
|
|
1653
|
+
gitLabAdvisoryApi: process.env.AUTOREMEDIATOR_GITLAB_ADVISORY_API ?? "https://advisories.gitlab.com/api/v1/advisories",
|
|
1654
|
+
certCcSearchUrl: process.env.AUTOREMEDIATOR_CERTCC_SEARCH_URL ?? "https://www.kb.cert.org/vuls/search",
|
|
1655
|
+
epssApi: process.env.AUTOREMEDIATOR_EPSS_API ?? "https://api.first.org/data/v1/epss",
|
|
1656
|
+
cveServicesApi: process.env.AUTOREMEDIATOR_CVE_SERVICES_API ?? "https://cveawg.mitre.org/api/cve",
|
|
1657
|
+
depsDevApi: process.env.AUTOREMEDIATOR_DEPSDEV_API ?? "https://api.deps.dev/v3",
|
|
1658
|
+
scorecardApi: process.env.AUTOREMEDIATOR_SCORECARD_API ?? "https://api.securityscorecards.dev",
|
|
1659
|
+
vendorAdvisoryFeeds: (process.env.AUTOREMEDIATOR_VENDOR_ADVISORY_FEEDS ?? "").split(",").map((v) => v.trim()).filter(Boolean),
|
|
1660
|
+
commercialFeeds: (process.env.AUTOREMEDIATOR_COMMERCIAL_FEEDS ?? "").split(",").map((v) => v.trim()).filter(Boolean),
|
|
1661
|
+
commercialFeedToken: process.env.AUTOREMEDIATOR_COMMERCIAL_FEED_TOKEN
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// src/intelligence/sources/epss.ts
|
|
1666
|
+
async function fetchEpss(cveId) {
|
|
1667
|
+
const { epssApi } = getIntelligenceSourceConfig();
|
|
1668
|
+
if (!epssApi) return void 0;
|
|
1669
|
+
try {
|
|
1670
|
+
const url = new URL(epssApi);
|
|
1671
|
+
url.searchParams.set("cve", cveId);
|
|
1672
|
+
const res = await httpClient({ url: url.toString() });
|
|
1673
|
+
if (!res.ok) return void 0;
|
|
1674
|
+
const body = res.data;
|
|
1675
|
+
return body.data?.[0];
|
|
1676
|
+
} catch {
|
|
1677
|
+
return void 0;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
async function enrichWithEpss(details) {
|
|
1681
|
+
const row = await fetchEpss(details.id);
|
|
1682
|
+
if (!row) return details;
|
|
1683
|
+
const score = Number.parseFloat(row.epss);
|
|
1684
|
+
const percentile = Number.parseFloat(row.percentile);
|
|
1685
|
+
if (!Number.isFinite(score) || !Number.isFinite(percentile)) {
|
|
1686
|
+
return details;
|
|
1687
|
+
}
|
|
1688
|
+
details.epss = {
|
|
1689
|
+
score,
|
|
1690
|
+
percentile,
|
|
1691
|
+
date: row.date
|
|
1692
|
+
};
|
|
1693
|
+
return details;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// src/intelligence/sources/cisa-kev.ts
|
|
1697
|
+
var CISA_KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json";
|
|
1698
|
+
async function fetchCisaKevFeed() {
|
|
1699
|
+
try {
|
|
1700
|
+
const res = await httpClient({ url: CISA_KEV_URL });
|
|
1701
|
+
if (!res.ok) return void 0;
|
|
1702
|
+
return res.data;
|
|
1703
|
+
} catch {
|
|
1704
|
+
return void 0;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
function findKevEntry(feed, cveId) {
|
|
1708
|
+
if (!feed?.vulnerabilities?.length) return void 0;
|
|
1709
|
+
const normalized = cveId.toUpperCase();
|
|
1710
|
+
return feed.vulnerabilities.find((v) => v.cveID.toUpperCase() === normalized);
|
|
1711
|
+
}
|
|
1712
|
+
async function enrichWithCisaKev(details) {
|
|
1713
|
+
const feed = await fetchCisaKevFeed();
|
|
1714
|
+
const entry = findKevEntry(feed, details.id);
|
|
1715
|
+
if (!entry) return details;
|
|
1716
|
+
details.kev = {
|
|
1717
|
+
knownExploited: true,
|
|
1718
|
+
dateAdded: entry.dateAdded,
|
|
1719
|
+
dueDate: entry.dueDate,
|
|
1720
|
+
requiredAction: entry.requiredAction,
|
|
1721
|
+
knownRansomwareCampaignUse: entry.knownRansomwareCampaignUse
|
|
1722
|
+
};
|
|
1723
|
+
if (!details.references.includes(CISA_KEV_URL)) {
|
|
1724
|
+
details.references.push(CISA_KEV_URL);
|
|
1725
|
+
}
|
|
1726
|
+
return details;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// src/intelligence/sources/registry.ts
|
|
1730
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
1731
|
+
import { join as join9 } from "path";
|
|
1732
|
+
import { execa as execa4 } from "execa";
|
|
1733
|
+
import semver from "semver";
|
|
1734
|
+
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
1735
|
+
async function fetchPackageVersions(packageName) {
|
|
1736
|
+
const url = `${NPM_REGISTRY}/${encodeURIComponent(packageName)}`;
|
|
1737
|
+
try {
|
|
1738
|
+
const res = await httpClient({ url });
|
|
1739
|
+
if (res.status === 404) return [];
|
|
1740
|
+
if (!res.ok) {
|
|
1741
|
+
throw new Error(`npm registry error ${res.status} for "${packageName}": ${res.text}`);
|
|
1742
|
+
}
|
|
1743
|
+
const data = res.data;
|
|
1744
|
+
return Object.keys(data.versions);
|
|
1745
|
+
} catch (err) {
|
|
1746
|
+
if (err instanceof Error && err.message.includes("registry error")) {
|
|
1747
|
+
throw err;
|
|
1748
|
+
}
|
|
1749
|
+
return [];
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
async function findSafeUpgradeVersion(packageName, installedVersion, firstPatchedVersion, vulnerableRange) {
|
|
1753
|
+
const resolution = await resolveSafeUpgradeVersion(
|
|
1754
|
+
packageName,
|
|
1755
|
+
installedVersion,
|
|
1756
|
+
firstPatchedVersion,
|
|
1757
|
+
vulnerableRange
|
|
1758
|
+
);
|
|
1759
|
+
return resolution.safeVersion;
|
|
1760
|
+
}
|
|
1761
|
+
async function resolveSafeUpgradeVersion(packageName, installedVersion, firstPatchedVersion, vulnerableRange) {
|
|
1762
|
+
const versions = await fetchPackageVersions(packageName);
|
|
1763
|
+
if (!versions.length) {
|
|
1764
|
+
return {
|
|
1765
|
+
candidates: {},
|
|
1766
|
+
majorOnlyFixAvailable: false
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
const installed = semver.parse(installedVersion);
|
|
1770
|
+
const candidates = versions.filter((v) => semver.valid(v) && semver.gte(v, firstPatchedVersion)).filter((v) => {
|
|
1771
|
+
if (!vulnerableRange) return true;
|
|
1772
|
+
try {
|
|
1773
|
+
return !semver.satisfies(v, vulnerableRange, { includePrerelease: false });
|
|
1774
|
+
} catch {
|
|
1775
|
+
return true;
|
|
1776
|
+
}
|
|
1777
|
+
}).sort(semver.compare);
|
|
1778
|
+
if (!candidates.length) {
|
|
1779
|
+
return {
|
|
1780
|
+
candidates: {},
|
|
1781
|
+
majorOnlyFixAvailable: false
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
const categorizedCandidates = {};
|
|
1785
|
+
for (const candidate of candidates) {
|
|
1786
|
+
const level = classifyUpgradeLevel(installedVersion, candidate);
|
|
1787
|
+
if (!level) continue;
|
|
1788
|
+
if (!categorizedCandidates[level]) {
|
|
1789
|
+
categorizedCandidates[level] = candidate;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
const safeVersion = categorizedCandidates.patch ?? categorizedCandidates.minor ?? categorizedCandidates.major;
|
|
1793
|
+
if (!safeVersion) {
|
|
1794
|
+
return {
|
|
1795
|
+
candidates: categorizedCandidates,
|
|
1796
|
+
majorOnlyFixAvailable: false
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
const upgradeLevel = classifyUpgradeLevel(installedVersion, safeVersion);
|
|
1800
|
+
const majorOnlyFixAvailable = !categorizedCandidates.patch && !categorizedCandidates.minor && Boolean(categorizedCandidates.major);
|
|
1801
|
+
if (!installed || !upgradeLevel) {
|
|
1802
|
+
return {
|
|
1803
|
+
safeVersion,
|
|
1804
|
+
upgradeLevel,
|
|
1805
|
+
candidates: categorizedCandidates,
|
|
1806
|
+
majorOnlyFixAvailable
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
return {
|
|
1810
|
+
safeVersion,
|
|
1811
|
+
upgradeLevel,
|
|
1812
|
+
candidates: categorizedCandidates,
|
|
1813
|
+
majorOnlyFixAvailable
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
function classifyUpgradeLevel(installedVersion, candidateVersion) {
|
|
1817
|
+
const installed = semver.parse(installedVersion);
|
|
1818
|
+
const candidate = semver.parse(candidateVersion);
|
|
1819
|
+
if (!installed || !candidate) return void 0;
|
|
1820
|
+
if (candidate.major > installed.major) return "major";
|
|
1821
|
+
if (candidate.minor > installed.minor) return "minor";
|
|
1822
|
+
if (candidate.patch > installed.patch || candidate.version === installed.version) {
|
|
1823
|
+
return "patch";
|
|
1824
|
+
}
|
|
1825
|
+
return void 0;
|
|
1826
|
+
}
|
|
1827
|
+
async function queryOutdatedPackages(cwd, options = {}) {
|
|
1828
|
+
const pm = options.packageManager ?? detectPackageManager(cwd);
|
|
1829
|
+
let stdout;
|
|
1830
|
+
try {
|
|
1831
|
+
const args = pm === "pnpm" ? ["outdated", "--json"] : ["outdated", "--json"];
|
|
1832
|
+
const yarnArgs = pm === "yarn" ? ["outdated", "--json"] : void 0;
|
|
1833
|
+
const finalArgs = yarnArgs ?? args;
|
|
1834
|
+
const result2 = await execa4(pm, finalArgs, {
|
|
1835
|
+
cwd,
|
|
1836
|
+
reject: false,
|
|
1837
|
+
stdio: "pipe"
|
|
1838
|
+
});
|
|
1839
|
+
if (result2.exitCode !== 0 && result2.exitCode !== 1) {
|
|
1840
|
+
const stderr = typeof result2.stderr === "string" ? result2.stderr : "";
|
|
1841
|
+
if (!result2.stdout) {
|
|
1842
|
+
throw new Error(
|
|
1843
|
+
`${pm} outdated --json failed (exit ${result2.exitCode}): ${stderr.slice(0, 500)}`
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
stdout = typeof result2.stdout === "string" ? result2.stdout : "";
|
|
1848
|
+
} catch (err) {
|
|
1849
|
+
if (err instanceof Error && !err.message.includes("outdated --json failed")) {
|
|
1850
|
+
throw new Error(`Failed to query outdated packages: ${err.message}`);
|
|
1851
|
+
}
|
|
1852
|
+
throw err;
|
|
1853
|
+
}
|
|
1854
|
+
if (!stdout.trim()) {
|
|
1855
|
+
return /* @__PURE__ */ new Map();
|
|
1856
|
+
}
|
|
1857
|
+
const rawEntries = {};
|
|
1858
|
+
if (pm === "yarn") {
|
|
1859
|
+
for (const line of stdout.split("\n")) {
|
|
1860
|
+
const trimmed = line.trim();
|
|
1861
|
+
if (!trimmed) continue;
|
|
1862
|
+
try {
|
|
1863
|
+
const parsed = JSON.parse(trimmed);
|
|
1864
|
+
if (parsed.type === "table" && parsed.data) {
|
|
1865
|
+
const tableData = parsed.data;
|
|
1866
|
+
const head = tableData.head ?? [];
|
|
1867
|
+
const currentIdx = head.indexOf("Current");
|
|
1868
|
+
const wantedIdx = head.indexOf("Wanted");
|
|
1869
|
+
const latestIdx = head.indexOf("Latest");
|
|
1870
|
+
for (const row of tableData.body ?? []) {
|
|
1871
|
+
const name = row[0];
|
|
1872
|
+
if (!name) continue;
|
|
1873
|
+
rawEntries[name] = {
|
|
1874
|
+
current: row[currentIdx] ?? "",
|
|
1875
|
+
wanted: row[wantedIdx] ?? "",
|
|
1876
|
+
latest: row[latestIdx] ?? "",
|
|
1877
|
+
type: row[head.indexOf("Package Type")] ?? "dependencies"
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
} catch {
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
} else {
|
|
1885
|
+
try {
|
|
1886
|
+
const parsed = JSON.parse(stdout);
|
|
1887
|
+
if (Array.isArray(parsed)) {
|
|
1888
|
+
for (const entry of parsed) {
|
|
1889
|
+
const name = entry.name ?? entry.packageName;
|
|
1890
|
+
if (!name) continue;
|
|
1891
|
+
rawEntries[name] = {
|
|
1892
|
+
current: entry.current,
|
|
1893
|
+
wanted: entry.wanted,
|
|
1894
|
+
latest: entry.latest,
|
|
1895
|
+
type: entry.dependencyType
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
} else {
|
|
1899
|
+
Object.assign(rawEntries, parsed);
|
|
1900
|
+
}
|
|
1901
|
+
} catch {
|
|
1902
|
+
throw new Error(`Could not parse ${pm} outdated --json output.`);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
let directDeps;
|
|
1906
|
+
try {
|
|
1907
|
+
const pkgRaw = JSON.parse(readFileSync7(join9(cwd, "package.json"), "utf8"));
|
|
1908
|
+
directDeps = /* @__PURE__ */ new Set([
|
|
1909
|
+
...Object.keys(pkgRaw.dependencies ?? {}),
|
|
1910
|
+
...Object.keys(pkgRaw.devDependencies ?? {})
|
|
1911
|
+
]);
|
|
1912
|
+
} catch {
|
|
1913
|
+
directDeps = /* @__PURE__ */ new Set();
|
|
1914
|
+
}
|
|
1915
|
+
const result = /* @__PURE__ */ new Map();
|
|
1916
|
+
for (const [name, entry] of Object.entries(rawEntries)) {
|
|
1917
|
+
const currentVersion = entry.current ?? "";
|
|
1918
|
+
const wantedVersion = entry.wanted ?? currentVersion;
|
|
1919
|
+
const latestVersion = entry.latest ?? currentVersion;
|
|
1920
|
+
if (!currentVersion || !latestVersion) continue;
|
|
1921
|
+
const dependencyScope = directDeps.has(name) ? "direct" : "transitive";
|
|
1922
|
+
if (!options.includeTransitive && dependencyScope === "transitive") continue;
|
|
1923
|
+
const isMajorBump = semver.valid(currentVersion) !== null && semver.valid(latestVersion) !== null && semver.major(latestVersion) > semver.major(currentVersion);
|
|
1924
|
+
result.set(name, {
|
|
1925
|
+
currentVersion,
|
|
1926
|
+
wantedVersion,
|
|
1927
|
+
latestVersion,
|
|
1928
|
+
isMajorBump,
|
|
1929
|
+
dependencyScope
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
return result;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
// src/remediation/evaluate-package.ts
|
|
1936
|
+
async function evaluatePackage(packageName, options = {}) {
|
|
1937
|
+
const version = options.version?.trim();
|
|
1938
|
+
const enrich = options.enrichIntelligence !== false;
|
|
1939
|
+
let versions = [];
|
|
1940
|
+
let latestVersion;
|
|
1941
|
+
try {
|
|
1942
|
+
versions = await fetchPackageVersions(packageName);
|
|
1943
|
+
if (versions.length > 0) {
|
|
1944
|
+
const valid = versions.filter((v) => semver2.valid(v)).sort((a, b) => semver2.compare(a, b));
|
|
1945
|
+
latestVersion = valid[valid.length - 1];
|
|
1946
|
+
}
|
|
1947
|
+
} catch {
|
|
1948
|
+
}
|
|
1949
|
+
let rawVulns = [];
|
|
1950
|
+
try {
|
|
1951
|
+
rawVulns = await queryOsvPackage(packageName, version);
|
|
1952
|
+
} catch {
|
|
1953
|
+
rawVulns = [];
|
|
1954
|
+
}
|
|
1955
|
+
let kevFeed = null;
|
|
1956
|
+
if (enrich) {
|
|
1957
|
+
try {
|
|
1958
|
+
kevFeed = await fetchCisaKevFeed();
|
|
1959
|
+
} catch {
|
|
1960
|
+
kevFeed = null;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
const entries = [];
|
|
1964
|
+
for (const raw of rawVulns) {
|
|
1965
|
+
const cveDetails = parseOsvVuln(raw);
|
|
1966
|
+
const affected = cveDetails.affectedPackages.find(
|
|
1967
|
+
(p) => p.name.toLowerCase() === packageName.toLowerCase()
|
|
1968
|
+
);
|
|
1969
|
+
if (version && affected?.vulnerableRange) {
|
|
1970
|
+
const cleanVer = semver2.clean(version) || version;
|
|
1971
|
+
if (semver2.valid(cleanVer) && !semver2.satisfies(cleanVer, affected.vulnerableRange)) {
|
|
1972
|
+
continue;
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
let inCisaKev = false;
|
|
1976
|
+
let epssScore;
|
|
1977
|
+
let epssPercentile;
|
|
1978
|
+
if (enrich && cveDetails.id.startsWith("CVE-")) {
|
|
1979
|
+
if (kevFeed) {
|
|
1980
|
+
inCisaKev = Boolean(findKevEntry(kevFeed, cveDetails.id));
|
|
1981
|
+
}
|
|
1982
|
+
try {
|
|
1983
|
+
const epss = await fetchEpss(cveDetails.id);
|
|
1984
|
+
if (epss) {
|
|
1985
|
+
epssScore = Number.parseFloat(epss.epss);
|
|
1986
|
+
epssPercentile = Number.parseFloat(epss.percentile);
|
|
1987
|
+
}
|
|
1988
|
+
} catch {
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
let safeUpgradeVersion;
|
|
1992
|
+
if (affected?.firstPatchedVersion) {
|
|
1993
|
+
try {
|
|
1994
|
+
const safe = await findSafeUpgradeVersion(
|
|
1995
|
+
packageName,
|
|
1996
|
+
version || "0.0.0",
|
|
1997
|
+
affected.firstPatchedVersion
|
|
1998
|
+
);
|
|
1999
|
+
safeUpgradeVersion = safe ?? affected.firstPatchedVersion;
|
|
2000
|
+
} catch {
|
|
2001
|
+
safeUpgradeVersion = affected.firstPatchedVersion;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
entries.push({
|
|
2005
|
+
cveId: cveDetails.id,
|
|
2006
|
+
severity: cveDetails.severity,
|
|
2007
|
+
summary: cveDetails.summary,
|
|
2008
|
+
vulnerableRange: affected?.vulnerableRange,
|
|
2009
|
+
firstPatchedVersion: affected?.firstPatchedVersion,
|
|
2010
|
+
safeUpgradeVersion,
|
|
2011
|
+
epssScore,
|
|
2012
|
+
epssPercentile,
|
|
2013
|
+
inCisaKev,
|
|
2014
|
+
references: cveDetails.references
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
const isVulnerable = entries.length > 0;
|
|
2018
|
+
let verdict = "safe";
|
|
2019
|
+
if (isVulnerable) {
|
|
2020
|
+
const hasKevOrHighEpss = entries.some((e) => e.inCisaKev || e.epssScore && e.epssScore > 0.5);
|
|
2021
|
+
const hasHighOrCritical = entries.some(
|
|
2022
|
+
(e) => e.severity === "CRITICAL" || e.severity === "HIGH"
|
|
2023
|
+
);
|
|
2024
|
+
if (hasKevOrHighEpss) {
|
|
2025
|
+
verdict = "actively-exploited";
|
|
2026
|
+
} else if (hasHighOrCritical) {
|
|
2027
|
+
verdict = "vulnerable";
|
|
2028
|
+
} else {
|
|
2029
|
+
verdict = "caution";
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
let recommendedVersion;
|
|
2033
|
+
if (isVulnerable) {
|
|
2034
|
+
const upgradeCandidates = entries.map((e) => e.safeUpgradeVersion).filter((v) => Boolean(v && semver2.valid(v))).sort((a, b) => semver2.compare(a, b));
|
|
2035
|
+
recommendedVersion = upgradeCandidates[upgradeCandidates.length - 1] || latestVersion;
|
|
2036
|
+
} else {
|
|
2037
|
+
recommendedVersion = version || latestVersion;
|
|
2038
|
+
}
|
|
2039
|
+
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.`;
|
|
2040
|
+
return {
|
|
2041
|
+
schemaVersion: "1.0",
|
|
2042
|
+
packageName,
|
|
2043
|
+
evaluatedVersion: version,
|
|
2044
|
+
isVulnerable,
|
|
2045
|
+
verdict,
|
|
2046
|
+
summary,
|
|
2047
|
+
vulnerabilities: entries,
|
|
2048
|
+
recommendedVersion,
|
|
2049
|
+
latestVersion,
|
|
2050
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// src/api/patches/reconcile.ts
|
|
2055
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2056
|
+
import { readFile as readFile3, rm, writeFile } from "fs/promises";
|
|
2057
|
+
import { join as join10 } from "path";
|
|
2058
|
+
import { execa as execa5 } from "execa";
|
|
2059
|
+
async function reconcilePatchArtifacts(options = {}) {
|
|
2060
|
+
const cwd = options.cwd ?? process.cwd();
|
|
2061
|
+
const pm = options.packageManager ?? detectPackageManager(cwd);
|
|
2062
|
+
const summaries = await listPatchArtifacts(options);
|
|
2063
|
+
const results = [];
|
|
2064
|
+
for (const summary of summaries) {
|
|
2065
|
+
const inspection = await inspectPatchArtifact(summary.patchFilePath, options);
|
|
2066
|
+
let packageName = inspection.packageName;
|
|
2067
|
+
let vulnerableVersion = inspection.vulnerableVersion;
|
|
2068
|
+
const cveId = inspection.cveId;
|
|
2069
|
+
if (!packageName && summary.patchFileName) {
|
|
2070
|
+
const basename = summary.patchFileName.replace(/\.patch$/, "");
|
|
2071
|
+
const plusIdx = basename.lastIndexOf("+");
|
|
2072
|
+
if (plusIdx > 0) {
|
|
2073
|
+
let rawPkg = basename.slice(0, plusIdx);
|
|
2074
|
+
vulnerableVersion = basename.slice(plusIdx + 1);
|
|
2075
|
+
if (rawPkg.includes("+")) {
|
|
2076
|
+
rawPkg = "@" + rawPkg.replace("+", "/");
|
|
2077
|
+
}
|
|
2078
|
+
packageName = rawPkg;
|
|
2079
|
+
} else {
|
|
2080
|
+
packageName = basename;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
if (!packageName) {
|
|
2084
|
+
results.push({
|
|
2085
|
+
patchFilePath: summary.patchFilePath,
|
|
2086
|
+
packageName: "unknown",
|
|
2087
|
+
cveId,
|
|
2088
|
+
status: "patch-retained",
|
|
2089
|
+
reason: "Cannot determine package name from patch artifact."
|
|
2090
|
+
});
|
|
2091
|
+
continue;
|
|
2092
|
+
}
|
|
2093
|
+
let safeUpgradeVersion;
|
|
2094
|
+
try {
|
|
2095
|
+
const evaluation = await evaluatePackage(packageName, {
|
|
2096
|
+
version: vulnerableVersion,
|
|
2097
|
+
enrichIntelligence: false
|
|
2098
|
+
});
|
|
2099
|
+
if (cveId) {
|
|
2100
|
+
const vuln = evaluation.vulnerabilities.find(
|
|
2101
|
+
(v) => v.cveId.toLowerCase() === cveId.toLowerCase()
|
|
2102
|
+
);
|
|
2103
|
+
if (vuln?.safeUpgradeVersion) {
|
|
2104
|
+
safeUpgradeVersion = vuln.safeUpgradeVersion;
|
|
2105
|
+
}
|
|
2106
|
+
} else {
|
|
2107
|
+
safeUpgradeVersion = evaluation.vulnerabilities.find(
|
|
2108
|
+
(v) => v.safeUpgradeVersion
|
|
2109
|
+
)?.safeUpgradeVersion;
|
|
2110
|
+
}
|
|
2111
|
+
} catch {
|
|
2112
|
+
}
|
|
2113
|
+
if (!safeUpgradeVersion) {
|
|
2114
|
+
results.push({
|
|
2115
|
+
patchFilePath: summary.patchFilePath,
|
|
2116
|
+
packageName,
|
|
2117
|
+
cveId,
|
|
2118
|
+
installedVersion: vulnerableVersion,
|
|
2119
|
+
status: "patch-retained",
|
|
2120
|
+
reason: "No safe upstream release exists yet; patch remains necessary."
|
|
2121
|
+
});
|
|
2122
|
+
continue;
|
|
2123
|
+
}
|
|
2124
|
+
if (!options.autoApply) {
|
|
2125
|
+
results.push({
|
|
2126
|
+
patchFilePath: summary.patchFilePath,
|
|
2127
|
+
packageName,
|
|
2128
|
+
cveId,
|
|
2129
|
+
installedVersion: vulnerableVersion,
|
|
2130
|
+
safeUpstreamVersion: safeUpgradeVersion,
|
|
2131
|
+
status: "safe-version-available",
|
|
2132
|
+
reason: `Safe upstream version ${safeUpgradeVersion} is available. Re-run with autoApply: true or --auto-apply to upgrade.`
|
|
2133
|
+
});
|
|
2134
|
+
continue;
|
|
2135
|
+
}
|
|
2136
|
+
try {
|
|
2137
|
+
const pkgJsonPath = join10(cwd, "package.json");
|
|
2138
|
+
if (existsSync8(pkgJsonPath)) {
|
|
2139
|
+
const pkgJson = JSON.parse(await readFile3(pkgJsonPath, "utf8"));
|
|
2140
|
+
let updated = false;
|
|
2141
|
+
const depSections = [
|
|
2142
|
+
"dependencies",
|
|
2143
|
+
"devDependencies",
|
|
2144
|
+
"peerDependencies",
|
|
2145
|
+
"optionalDependencies"
|
|
2146
|
+
];
|
|
2147
|
+
for (const depSection of depSections) {
|
|
2148
|
+
if (pkgJson[depSection] && pkgJson[depSection][packageName]) {
|
|
2149
|
+
pkgJson[depSection][packageName] = `^${safeUpgradeVersion}`;
|
|
2150
|
+
updated = true;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
if (pkgJson.pnpm?.patchedDependencies) {
|
|
2154
|
+
for (const key of Object.keys(pkgJson.pnpm.patchedDependencies)) {
|
|
2155
|
+
if (key.startsWith(`${packageName}@`)) {
|
|
2156
|
+
delete pkgJson.pnpm.patchedDependencies[key];
|
|
2157
|
+
updated = true;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
if (updated) {
|
|
2162
|
+
await writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
await rm(summary.patchFilePath, { force: true });
|
|
2166
|
+
if (inspection.manifestFilePath) {
|
|
2167
|
+
await rm(inspection.manifestFilePath, { force: true });
|
|
2168
|
+
}
|
|
2169
|
+
const installCmd = resolveInstallCommand(pm);
|
|
2170
|
+
await execa5(installCmd[0], installCmd.slice(1), { cwd, reject: false });
|
|
2171
|
+
if (options.runTests) {
|
|
2172
|
+
const testCmd = resolveTestCommand(pm);
|
|
2173
|
+
await execa5(testCmd[0], testCmd.slice(1), { cwd, reject: false });
|
|
2174
|
+
}
|
|
2175
|
+
results.push({
|
|
2176
|
+
patchFilePath: summary.patchFilePath,
|
|
2177
|
+
packageName,
|
|
2178
|
+
cveId,
|
|
2179
|
+
installedVersion: vulnerableVersion,
|
|
2180
|
+
safeUpstreamVersion: safeUpgradeVersion,
|
|
2181
|
+
status: "upgraded",
|
|
2182
|
+
reason: `Successfully upgraded to ${safeUpgradeVersion} and retired patch artifact.`
|
|
2183
|
+
});
|
|
2184
|
+
} catch (err) {
|
|
2185
|
+
results.push({
|
|
2186
|
+
patchFilePath: summary.patchFilePath,
|
|
2187
|
+
packageName,
|
|
2188
|
+
cveId,
|
|
2189
|
+
installedVersion: vulnerableVersion,
|
|
2190
|
+
safeUpstreamVersion: safeUpgradeVersion,
|
|
2191
|
+
status: "error",
|
|
2192
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
const reconciledCount = results.filter((r) => r.status === "upgraded").length;
|
|
2197
|
+
const retainedCount = results.filter(
|
|
2198
|
+
(r) => r.status === "patch-retained" || r.status === "safe-version-available"
|
|
2199
|
+
).length;
|
|
2200
|
+
return {
|
|
2201
|
+
reconciled: results,
|
|
2202
|
+
reconciledCount,
|
|
2203
|
+
retainedCount
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// src/platform/idempotency.ts
|
|
2208
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "fs";
|
|
2209
|
+
import { join as join11 } from "path";
|
|
2210
|
+
var DEFAULT_INDEX = {
|
|
2211
|
+
schemaVersion: "1.0",
|
|
2212
|
+
entries: {}
|
|
2213
|
+
};
|
|
2214
|
+
function indexFilePath(cwd) {
|
|
2215
|
+
return join11(cwd, ".autoremediator", "state", "idempotency.json");
|
|
2216
|
+
}
|
|
2217
|
+
function entryKey(idempotencyKey, cveId) {
|
|
2218
|
+
return `${idempotencyKey}::${cveId.toUpperCase()}`;
|
|
2219
|
+
}
|
|
2220
|
+
function loadIndex(cwd) {
|
|
2221
|
+
const filePath = indexFilePath(cwd);
|
|
2222
|
+
if (!existsSync9(filePath)) return DEFAULT_INDEX;
|
|
2223
|
+
try {
|
|
2224
|
+
const parsed = JSON.parse(readFileSync8(filePath, "utf8"));
|
|
2225
|
+
if (parsed && parsed.schemaVersion === "1.0" && parsed.entries) {
|
|
2226
|
+
return parsed;
|
|
2227
|
+
}
|
|
2228
|
+
return DEFAULT_INDEX;
|
|
2229
|
+
} catch {
|
|
2230
|
+
return DEFAULT_INDEX;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
function saveIndex(cwd, index) {
|
|
2234
|
+
const filePath = indexFilePath(cwd);
|
|
2235
|
+
mkdirSync2(join11(cwd, ".autoremediator", "state"), { recursive: true });
|
|
2236
|
+
writeFileSync2(filePath, JSON.stringify(index, null, 2) + "\n", "utf8");
|
|
2237
|
+
}
|
|
2238
|
+
function readIdempotentReport(cwd, idempotencyKey, cveId) {
|
|
2239
|
+
const index = loadIndex(cwd);
|
|
2240
|
+
const key = entryKey(idempotencyKey, cveId);
|
|
2241
|
+
return index.entries[key]?.report;
|
|
2242
|
+
}
|
|
2243
|
+
function storeIdempotentReport(cwd, idempotencyKey, cveId, report) {
|
|
2244
|
+
const index = loadIndex(cwd);
|
|
2245
|
+
const key = entryKey(idempotencyKey, cveId);
|
|
2246
|
+
index.entries[key] = {
|
|
2247
|
+
key: idempotencyKey,
|
|
2248
|
+
cveId: cveId.toUpperCase(),
|
|
2249
|
+
report,
|
|
2250
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2251
|
+
};
|
|
2252
|
+
saveIndex(cwd, index);
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
// src/remediation/pipeline.ts
|
|
2256
|
+
import { generateText as generateText2 } from "ai";
|
|
2257
|
+
|
|
2258
|
+
// src/remediation/local/run.ts
|
|
2259
|
+
import semver6 from "semver";
|
|
2260
|
+
|
|
2261
|
+
// src/intelligence/sources/github-advisory.ts
|
|
2262
|
+
var GITHUB_API_BASE = "https://api.github.com/advisories";
|
|
2263
|
+
async function fetchGhAdvisories(cveId) {
|
|
2264
|
+
const token = getGitHubToken();
|
|
2265
|
+
const headers = {
|
|
2266
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
2267
|
+
"User-Agent": "autoremediator"
|
|
2268
|
+
};
|
|
2269
|
+
if (token) {
|
|
2270
|
+
headers["Authorization"] = `token ${token}`;
|
|
2271
|
+
}
|
|
2272
|
+
const normalized = cveId.trim();
|
|
2273
|
+
if (normalized.toUpperCase().startsWith("GHSA-")) {
|
|
2274
|
+
const url2 = `${GITHUB_API_BASE}/${encodeURIComponent(normalized)}`;
|
|
2275
|
+
try {
|
|
2276
|
+
const res = await httpClient({ url: url2, headers });
|
|
2277
|
+
if (!res.ok) {
|
|
2278
|
+
if (res.status === 404) return [];
|
|
2279
|
+
throw new Error(`HTTP ${res.status}: ${res.text}`);
|
|
2280
|
+
}
|
|
2281
|
+
return res.data ? [res.data] : [];
|
|
2282
|
+
} catch (err) {
|
|
2283
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
2284
|
+
console.warn(
|
|
2285
|
+
`[autoremediator] GitHub Advisory API error for ${cveId}: ${errorMsg} \u2014 skipping.`
|
|
2286
|
+
);
|
|
2287
|
+
return [];
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
const url = new URL(GITHUB_API_BASE);
|
|
2291
|
+
url.searchParams.set("cve_id", cveId);
|
|
2292
|
+
url.searchParams.set("ecosystem", "npm");
|
|
2293
|
+
url.searchParams.set("type", "reviewed");
|
|
2294
|
+
url.searchParams.set("per_page", "10");
|
|
2295
|
+
try {
|
|
2296
|
+
const res = await httpClient({
|
|
2297
|
+
url: url.toString(),
|
|
2298
|
+
headers
|
|
2299
|
+
});
|
|
2300
|
+
if (!res.ok) {
|
|
2301
|
+
if (res.status === 404) return [];
|
|
2302
|
+
throw new Error(`HTTP ${res.status}: ${res.text}`);
|
|
2303
|
+
}
|
|
2304
|
+
return Array.isArray(res.data) ? res.data : [];
|
|
2305
|
+
} catch (err) {
|
|
2306
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1545
2307
|
console.warn(
|
|
1546
2308
|
`[autoremediator] GitHub Advisory API error for ${cveId}: ${errorMsg} \u2014 skipping.`
|
|
1547
2309
|
);
|
|
@@ -1633,9 +2395,9 @@ async function enrichWithNvd(details) {
|
|
|
1633
2395
|
}
|
|
1634
2396
|
|
|
1635
2397
|
// src/intelligence/sources/snapshot.ts
|
|
1636
|
-
import { readFileSync as
|
|
2398
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
1637
2399
|
function loadIntelligenceSnapshot(filePath) {
|
|
1638
|
-
const raw =
|
|
2400
|
+
const raw = readFileSync9(filePath, "utf8");
|
|
1639
2401
|
const parsed = JSON.parse(raw);
|
|
1640
2402
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1641
2403
|
throw new Error(`Intelligence snapshot at ${filePath} must be a JSON object keyed by CVE ID.`);
|
|
@@ -1647,8 +2409,8 @@ function lookupSnapshotCve(snapshot, cveId) {
|
|
|
1647
2409
|
}
|
|
1648
2410
|
|
|
1649
2411
|
// src/remediation/tools/check-reachability.ts
|
|
1650
|
-
import { existsSync as
|
|
1651
|
-
import { isAbsolute as isAbsolute2, join as
|
|
2412
|
+
import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync10, statSync as statSync2 } from "fs";
|
|
2413
|
+
import { isAbsolute as isAbsolute2, join as join12, resolve as resolve3 } from "path";
|
|
1652
2414
|
import { parseSync } from "oxc-parser";
|
|
1653
2415
|
import { z as z2 } from "zod";
|
|
1654
2416
|
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"]);
|
|
@@ -1664,16 +2426,16 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
1664
2426
|
]);
|
|
1665
2427
|
function loadTsconfigPaths(cwd) {
|
|
1666
2428
|
for (const configName of ["tsconfig.json", "jsconfig.json"]) {
|
|
1667
|
-
const fullPath =
|
|
1668
|
-
if (!
|
|
2429
|
+
const fullPath = join12(cwd, configName);
|
|
2430
|
+
if (!existsSync10(fullPath)) continue;
|
|
1669
2431
|
try {
|
|
1670
|
-
const raw =
|
|
2432
|
+
const raw = readFileSync10(fullPath, "utf8");
|
|
1671
2433
|
const cleaned = raw.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "");
|
|
1672
2434
|
const parsed = JSON.parse(cleaned);
|
|
1673
2435
|
const compilerOptions = parsed.compilerOptions;
|
|
1674
2436
|
if (compilerOptions?.paths) {
|
|
1675
2437
|
return {
|
|
1676
|
-
baseUrl: compilerOptions.baseUrl ?
|
|
2438
|
+
baseUrl: compilerOptions.baseUrl ? resolve3(cwd, compilerOptions.baseUrl) : cwd,
|
|
1677
2439
|
paths: compilerOptions.paths
|
|
1678
2440
|
};
|
|
1679
2441
|
}
|
|
@@ -1688,14 +2450,14 @@ function resolveAliasedPath(importSource, config) {
|
|
|
1688
2450
|
}
|
|
1689
2451
|
for (const [pattern, replacements] of Object.entries(config.paths)) {
|
|
1690
2452
|
if (pattern === importSource && replacements[0]) {
|
|
1691
|
-
return
|
|
2453
|
+
return resolve3(config.baseUrl, replacements[0]);
|
|
1692
2454
|
}
|
|
1693
2455
|
if (pattern.endsWith("/*")) {
|
|
1694
2456
|
const prefix = pattern.slice(0, -2);
|
|
1695
2457
|
if (importSource.startsWith(prefix)) {
|
|
1696
2458
|
const suffix = importSource.slice(prefix.length);
|
|
1697
2459
|
const replacement = replacements[0]?.slice(0, -2) ?? "";
|
|
1698
|
-
return
|
|
2460
|
+
return resolve3(config.baseUrl, replacement + suffix);
|
|
1699
2461
|
}
|
|
1700
2462
|
}
|
|
1701
2463
|
}
|
|
@@ -1704,16 +2466,16 @@ function resolveAliasedPath(importSource, config) {
|
|
|
1704
2466
|
function* walkSourceFiles(dir) {
|
|
1705
2467
|
let entries;
|
|
1706
2468
|
try {
|
|
1707
|
-
entries =
|
|
2469
|
+
entries = readdirSync2(dir);
|
|
1708
2470
|
} catch {
|
|
1709
2471
|
return;
|
|
1710
2472
|
}
|
|
1711
2473
|
for (const entry of entries) {
|
|
1712
2474
|
if (IGNORED_DIRS.has(entry)) continue;
|
|
1713
|
-
const full =
|
|
2475
|
+
const full = join12(dir, entry);
|
|
1714
2476
|
let stat2;
|
|
1715
2477
|
try {
|
|
1716
|
-
stat2 =
|
|
2478
|
+
stat2 = statSync2(full);
|
|
1717
2479
|
} catch {
|
|
1718
2480
|
continue;
|
|
1719
2481
|
}
|
|
@@ -1746,12 +2508,36 @@ function walkAst(node, visitor) {
|
|
|
1746
2508
|
walkAst(child, visitor);
|
|
1747
2509
|
}
|
|
1748
2510
|
}
|
|
1749
|
-
} else if (value && typeof value === "object" && "type" in value) {
|
|
1750
|
-
walkAst(value, visitor);
|
|
2511
|
+
} else if (value && typeof value === "object" && "type" in value) {
|
|
2512
|
+
walkAst(value, visitor);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
function toRootPackageName(importSource) {
|
|
2517
|
+
if (importSource.startsWith("@")) {
|
|
2518
|
+
const parts = importSource.split("/");
|
|
2519
|
+
return parts.slice(0, 2).join("/");
|
|
2520
|
+
}
|
|
2521
|
+
return importSource.split("/")[0] ?? importSource;
|
|
2522
|
+
}
|
|
2523
|
+
function findTransitiveDependencyPath(cwd, targetPackage, importedDirectPackages) {
|
|
2524
|
+
for (const directPkg of importedDirectPackages) {
|
|
2525
|
+
const manifestPath = join12(cwd, "node_modules", directPkg, "package.json");
|
|
2526
|
+
if (!existsSync10(manifestPath)) continue;
|
|
2527
|
+
try {
|
|
2528
|
+
const manifest = JSON.parse(readFileSync10(manifestPath, "utf8"));
|
|
2529
|
+
if (manifest.dependencies && manifest.dependencies[targetPackage]) {
|
|
2530
|
+
return {
|
|
2531
|
+
directDependency: directPkg,
|
|
2532
|
+
version: manifest.dependencies[targetPackage]
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
} catch {
|
|
1751
2536
|
}
|
|
1752
2537
|
}
|
|
2538
|
+
return null;
|
|
1753
2539
|
}
|
|
1754
|
-
function extractPackageReferences(filename, content, packageName, tsconfig) {
|
|
2540
|
+
function extractPackageReferences(filename, content, packageName, tsconfig, outImportedDirectPackages) {
|
|
1755
2541
|
const results = [];
|
|
1756
2542
|
let parseResult;
|
|
1757
2543
|
try {
|
|
@@ -1763,6 +2549,9 @@ function extractPackageReferences(filename, content, packageName, tsconfig) {
|
|
|
1763
2549
|
walkAst(parseResult.program, (node) => {
|
|
1764
2550
|
if (node.type === "ImportDeclaration") {
|
|
1765
2551
|
const source = node.source;
|
|
2552
|
+
if (source?.value && !source.value.startsWith(".") && !isAbsolute2(source.value)) {
|
|
2553
|
+
outImportedDirectPackages?.add(toRootPackageName(source.value));
|
|
2554
|
+
}
|
|
1766
2555
|
if (source?.value && matchesPackage(source.value, packageName, tsconfig)) {
|
|
1767
2556
|
const symbols = [];
|
|
1768
2557
|
const importedIdentifiers = /* @__PURE__ */ new Map();
|
|
@@ -1797,6 +2586,9 @@ function extractPackageReferences(filename, content, packageName, tsconfig) {
|
|
|
1797
2586
|
}
|
|
1798
2587
|
if (node.type === "ImportExpression") {
|
|
1799
2588
|
const source = node.source;
|
|
2589
|
+
if (source?.value && !source.value.startsWith(".") && !isAbsolute2(source.value)) {
|
|
2590
|
+
outImportedDirectPackages?.add(toRootPackageName(source.value));
|
|
2591
|
+
}
|
|
1800
2592
|
if (source?.value && matchesPackage(source.value, packageName, tsconfig)) {
|
|
1801
2593
|
results.push({
|
|
1802
2594
|
matchType: "dynamic-import",
|
|
@@ -1808,6 +2600,9 @@ function extractPackageReferences(filename, content, packageName, tsconfig) {
|
|
|
1808
2600
|
}
|
|
1809
2601
|
if (node.type === "CallExpression" && node.callee?.name === "require" && Array.isArray(node.arguments) && node.arguments.length > 0) {
|
|
1810
2602
|
const firstArg = node.arguments[0];
|
|
2603
|
+
if (firstArg?.value && typeof firstArg.value === "string" && !firstArg.value.startsWith(".") && !isAbsolute2(firstArg.value)) {
|
|
2604
|
+
outImportedDirectPackages?.add(toRootPackageName(firstArg.value));
|
|
2605
|
+
}
|
|
1811
2606
|
if (firstArg?.value && typeof firstArg.value === "string" && matchesPackage(firstArg.value, packageName, tsconfig)) {
|
|
1812
2607
|
results.push({
|
|
1813
2608
|
matchType: "require",
|
|
@@ -1819,6 +2614,9 @@ function extractPackageReferences(filename, content, packageName, tsconfig) {
|
|
|
1819
2614
|
}
|
|
1820
2615
|
if (node.type === "ExportNamedDeclaration" || node.type === "ExportAllDeclaration") {
|
|
1821
2616
|
const source = node.source;
|
|
2617
|
+
if (source?.value && !source.value.startsWith(".") && !isAbsolute2(source.value)) {
|
|
2618
|
+
outImportedDirectPackages?.add(toRootPackageName(source.value));
|
|
2619
|
+
}
|
|
1822
2620
|
if (source?.value && matchesPackage(source.value, packageName, tsconfig)) {
|
|
1823
2621
|
const symbols = [];
|
|
1824
2622
|
if (node.type === "ExportAllDeclaration") {
|
|
@@ -1895,15 +2693,16 @@ function assessPackageReachability(cwd, packageName, targetSymbol) {
|
|
|
1895
2693
|
let hasReExport = false;
|
|
1896
2694
|
let totalInvocations = 0;
|
|
1897
2695
|
let hasPackageImport = false;
|
|
2696
|
+
const importedDirectPackages = /* @__PURE__ */ new Set();
|
|
1898
2697
|
for (const filePath of walkSourceFiles(cwd)) {
|
|
1899
2698
|
scanned++;
|
|
1900
2699
|
let content;
|
|
1901
2700
|
try {
|
|
1902
|
-
content =
|
|
2701
|
+
content = readFileSync10(filePath, "utf8");
|
|
1903
2702
|
} catch {
|
|
1904
2703
|
continue;
|
|
1905
2704
|
}
|
|
1906
|
-
const refs = extractPackageReferences(filePath, content, packageName, tsconfig);
|
|
2705
|
+
const refs = extractPackageReferences(filePath, content, packageName, tsconfig, importedDirectPackages);
|
|
1907
2706
|
if (refs.length > 0) {
|
|
1908
2707
|
hasPackageImport = true;
|
|
1909
2708
|
}
|
|
@@ -1946,6 +2745,16 @@ function assessPackageReachability(cwd, packageName, targetSymbol) {
|
|
|
1946
2745
|
};
|
|
1947
2746
|
}
|
|
1948
2747
|
if (!hasPackageImport) {
|
|
2748
|
+
const transitivePath = findTransitiveDependencyPath(cwd, packageName, importedDirectPackages);
|
|
2749
|
+
if (transitivePath) {
|
|
2750
|
+
return {
|
|
2751
|
+
packageName,
|
|
2752
|
+
status: "reachable",
|
|
2753
|
+
reason: `Package '${packageName}' is a transitive dependency of imported package '${transitivePath.directDependency}'`,
|
|
2754
|
+
reachabilityBasis: "transitive-path-found",
|
|
2755
|
+
evidence: []
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
1949
2758
|
return {
|
|
1950
2759
|
packageName,
|
|
1951
2760
|
status: "not-reachable",
|
|
@@ -2074,261 +2883,64 @@ async function runSecOpsPreflight(normalizedId, cveDetails, opts) {
|
|
|
2074
2883
|
id: cveDetails.id,
|
|
2075
2884
|
kev: cveDetails.kev,
|
|
2076
2885
|
epss: cveDetails.epss
|
|
2077
|
-
},
|
|
2078
|
-
policy: { exploitSignalOverride: opts.exploitSignalOverride }
|
|
2079
|
-
});
|
|
2080
|
-
if (result.exploitSignalTriggered) {
|
|
2081
|
-
exploitSignalTriggered = true;
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
let slaBreaches;
|
|
2085
|
-
if (opts.slaCheck && opts.slaPolicy && cveDetails.publishedAt) {
|
|
2086
|
-
const breach = checkSlaBreach(
|
|
2087
|
-
normalizedId,
|
|
2088
|
-
cveDetails.severity,
|
|
2089
|
-
cveDetails.publishedAt,
|
|
2090
|
-
opts.slaPolicy
|
|
2091
|
-
);
|
|
2092
|
-
if (breach) {
|
|
2093
|
-
slaBreaches = [breach];
|
|
2094
|
-
}
|
|
2095
|
-
}
|
|
2096
|
-
return { suppressed: false, exploitSignalTriggered, slaBreaches };
|
|
2097
|
-
}
|
|
2098
|
-
|
|
2099
|
-
// src/remediation/local/sbom.ts
|
|
2100
|
-
function buildSbom(packages, vulnerableNames, results) {
|
|
2101
|
-
const statusByPackage = /* @__PURE__ */ new Map();
|
|
2102
|
-
for (const result of results) {
|
|
2103
|
-
if (!vulnerableNames.has(result.packageName)) continue;
|
|
2104
|
-
if (result.suppressedBy) {
|
|
2105
|
-
statusByPackage.set(result.packageName, "suppressed");
|
|
2106
|
-
} else if (!result.applied && result.strategy === "none") {
|
|
2107
|
-
statusByPackage.set(result.packageName, "skipped");
|
|
2108
|
-
} else if (result.applied) {
|
|
2109
|
-
statusByPackage.set(result.packageName, "patched");
|
|
2110
|
-
} else {
|
|
2111
|
-
statusByPackage.set(result.packageName, "unpatched");
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2114
|
-
return packages.map((pkg) => {
|
|
2115
|
-
const isVulnerable = vulnerableNames.has(pkg.name);
|
|
2116
|
-
const entry = {
|
|
2117
|
-
name: pkg.name,
|
|
2118
|
-
version: pkg.version,
|
|
2119
|
-
scope: pkg.type === "direct" ? "direct" : "transitive"
|
|
2120
|
-
};
|
|
2121
|
-
if (isVulnerable) {
|
|
2122
|
-
entry.status = statusByPackage.get(pkg.name) ?? "unpatched";
|
|
2123
|
-
}
|
|
2124
|
-
return entry;
|
|
2125
|
-
});
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
// src/intelligence/sources/registry.ts
|
|
2129
|
-
import { readFileSync as readFileSync8 } from "fs";
|
|
2130
|
-
import { join as join10 } from "path";
|
|
2131
|
-
import { execa as execa4 } from "execa";
|
|
2132
|
-
import semver from "semver";
|
|
2133
|
-
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
2134
|
-
async function fetchPackageVersions(packageName) {
|
|
2135
|
-
const url = `${NPM_REGISTRY}/${encodeURIComponent(packageName)}`;
|
|
2136
|
-
try {
|
|
2137
|
-
const res = await httpClient({ url });
|
|
2138
|
-
if (res.status === 404) return [];
|
|
2139
|
-
if (!res.ok) {
|
|
2140
|
-
throw new Error(`npm registry error ${res.status} for "${packageName}": ${res.text}`);
|
|
2141
|
-
}
|
|
2142
|
-
const data = res.data;
|
|
2143
|
-
return Object.keys(data.versions);
|
|
2144
|
-
} catch (err) {
|
|
2145
|
-
if (err instanceof Error && err.message.includes("registry error")) {
|
|
2146
|
-
throw err;
|
|
2147
|
-
}
|
|
2148
|
-
return [];
|
|
2149
|
-
}
|
|
2150
|
-
}
|
|
2151
|
-
async function resolveSafeUpgradeVersion(packageName, installedVersion, firstPatchedVersion, vulnerableRange) {
|
|
2152
|
-
const versions = await fetchPackageVersions(packageName);
|
|
2153
|
-
if (!versions.length) {
|
|
2154
|
-
return {
|
|
2155
|
-
candidates: {},
|
|
2156
|
-
majorOnlyFixAvailable: false
|
|
2157
|
-
};
|
|
2158
|
-
}
|
|
2159
|
-
const installed = semver.parse(installedVersion);
|
|
2160
|
-
const candidates = versions.filter((v) => semver.valid(v) && semver.gte(v, firstPatchedVersion)).filter((v) => {
|
|
2161
|
-
if (!vulnerableRange) return true;
|
|
2162
|
-
try {
|
|
2163
|
-
return !semver.satisfies(v, vulnerableRange, { includePrerelease: false });
|
|
2164
|
-
} catch {
|
|
2165
|
-
return true;
|
|
2166
|
-
}
|
|
2167
|
-
}).sort(semver.compare);
|
|
2168
|
-
if (!candidates.length) {
|
|
2169
|
-
return {
|
|
2170
|
-
candidates: {},
|
|
2171
|
-
majorOnlyFixAvailable: false
|
|
2172
|
-
};
|
|
2173
|
-
}
|
|
2174
|
-
const categorizedCandidates = {};
|
|
2175
|
-
for (const candidate of candidates) {
|
|
2176
|
-
const level = classifyUpgradeLevel(installedVersion, candidate);
|
|
2177
|
-
if (!level) continue;
|
|
2178
|
-
if (!categorizedCandidates[level]) {
|
|
2179
|
-
categorizedCandidates[level] = candidate;
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
2182
|
-
const safeVersion = categorizedCandidates.patch ?? categorizedCandidates.minor ?? categorizedCandidates.major;
|
|
2183
|
-
if (!safeVersion) {
|
|
2184
|
-
return {
|
|
2185
|
-
candidates: categorizedCandidates,
|
|
2186
|
-
majorOnlyFixAvailable: false
|
|
2187
|
-
};
|
|
2188
|
-
}
|
|
2189
|
-
const upgradeLevel = classifyUpgradeLevel(installedVersion, safeVersion);
|
|
2190
|
-
const majorOnlyFixAvailable = !categorizedCandidates.patch && !categorizedCandidates.minor && Boolean(categorizedCandidates.major);
|
|
2191
|
-
if (!installed || !upgradeLevel) {
|
|
2192
|
-
return {
|
|
2193
|
-
safeVersion,
|
|
2194
|
-
upgradeLevel,
|
|
2195
|
-
candidates: categorizedCandidates,
|
|
2196
|
-
majorOnlyFixAvailable
|
|
2197
|
-
};
|
|
2198
|
-
}
|
|
2199
|
-
return {
|
|
2200
|
-
safeVersion,
|
|
2201
|
-
upgradeLevel,
|
|
2202
|
-
candidates: categorizedCandidates,
|
|
2203
|
-
majorOnlyFixAvailable
|
|
2204
|
-
};
|
|
2205
|
-
}
|
|
2206
|
-
function classifyUpgradeLevel(installedVersion, candidateVersion) {
|
|
2207
|
-
const installed = semver.parse(installedVersion);
|
|
2208
|
-
const candidate = semver.parse(candidateVersion);
|
|
2209
|
-
if (!installed || !candidate) return void 0;
|
|
2210
|
-
if (candidate.major > installed.major) return "major";
|
|
2211
|
-
if (candidate.minor > installed.minor) return "minor";
|
|
2212
|
-
if (candidate.patch > installed.patch || candidate.version === installed.version) {
|
|
2213
|
-
return "patch";
|
|
2214
|
-
}
|
|
2215
|
-
return void 0;
|
|
2216
|
-
}
|
|
2217
|
-
async function queryOutdatedPackages(cwd, options = {}) {
|
|
2218
|
-
const pm = options.packageManager ?? detectPackageManager(cwd);
|
|
2219
|
-
let stdout;
|
|
2220
|
-
try {
|
|
2221
|
-
const args = pm === "pnpm" ? ["outdated", "--json"] : ["outdated", "--json"];
|
|
2222
|
-
const yarnArgs = pm === "yarn" ? ["outdated", "--json"] : void 0;
|
|
2223
|
-
const finalArgs = yarnArgs ?? args;
|
|
2224
|
-
const result2 = await execa4(pm, finalArgs, {
|
|
2225
|
-
cwd,
|
|
2226
|
-
reject: false,
|
|
2227
|
-
stdio: "pipe"
|
|
2228
|
-
});
|
|
2229
|
-
if (result2.exitCode !== 0 && result2.exitCode !== 1) {
|
|
2230
|
-
const stderr = typeof result2.stderr === "string" ? result2.stderr : "";
|
|
2231
|
-
if (!result2.stdout) {
|
|
2232
|
-
throw new Error(
|
|
2233
|
-
`${pm} outdated --json failed (exit ${result2.exitCode}): ${stderr.slice(0, 500)}`
|
|
2234
|
-
);
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
stdout = typeof result2.stdout === "string" ? result2.stdout : "";
|
|
2238
|
-
} catch (err) {
|
|
2239
|
-
if (err instanceof Error && !err.message.includes("outdated --json failed")) {
|
|
2240
|
-
throw new Error(`Failed to query outdated packages: ${err.message}`);
|
|
2241
|
-
}
|
|
2242
|
-
throw err;
|
|
2243
|
-
}
|
|
2244
|
-
if (!stdout.trim()) {
|
|
2245
|
-
return /* @__PURE__ */ new Map();
|
|
2246
|
-
}
|
|
2247
|
-
const rawEntries = {};
|
|
2248
|
-
if (pm === "yarn") {
|
|
2249
|
-
for (const line of stdout.split("\n")) {
|
|
2250
|
-
const trimmed = line.trim();
|
|
2251
|
-
if (!trimmed) continue;
|
|
2252
|
-
try {
|
|
2253
|
-
const parsed = JSON.parse(trimmed);
|
|
2254
|
-
if (parsed.type === "table" && parsed.data) {
|
|
2255
|
-
const tableData = parsed.data;
|
|
2256
|
-
const head = tableData.head ?? [];
|
|
2257
|
-
const currentIdx = head.indexOf("Current");
|
|
2258
|
-
const wantedIdx = head.indexOf("Wanted");
|
|
2259
|
-
const latestIdx = head.indexOf("Latest");
|
|
2260
|
-
for (const row of tableData.body ?? []) {
|
|
2261
|
-
const name = row[0];
|
|
2262
|
-
if (!name) continue;
|
|
2263
|
-
rawEntries[name] = {
|
|
2264
|
-
current: row[currentIdx] ?? "",
|
|
2265
|
-
wanted: row[wantedIdx] ?? "",
|
|
2266
|
-
latest: row[latestIdx] ?? "",
|
|
2267
|
-
type: row[head.indexOf("Package Type")] ?? "dependencies"
|
|
2268
|
-
};
|
|
2269
|
-
}
|
|
2270
|
-
}
|
|
2271
|
-
} catch {
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
} else {
|
|
2275
|
-
try {
|
|
2276
|
-
const parsed = JSON.parse(stdout);
|
|
2277
|
-
if (Array.isArray(parsed)) {
|
|
2278
|
-
for (const entry of parsed) {
|
|
2279
|
-
const name = entry.name ?? entry.packageName;
|
|
2280
|
-
if (!name) continue;
|
|
2281
|
-
rawEntries[name] = {
|
|
2282
|
-
current: entry.current,
|
|
2283
|
-
wanted: entry.wanted,
|
|
2284
|
-
latest: entry.latest,
|
|
2285
|
-
type: entry.dependencyType
|
|
2286
|
-
};
|
|
2287
|
-
}
|
|
2288
|
-
} else {
|
|
2289
|
-
Object.assign(rawEntries, parsed);
|
|
2290
|
-
}
|
|
2291
|
-
} catch {
|
|
2292
|
-
throw new Error(`Could not parse ${pm} outdated --json output.`);
|
|
2293
|
-
}
|
|
2294
|
-
}
|
|
2295
|
-
let directDeps;
|
|
2296
|
-
try {
|
|
2297
|
-
const pkgRaw = JSON.parse(readFileSync8(join10(cwd, "package.json"), "utf8"));
|
|
2298
|
-
directDeps = /* @__PURE__ */ new Set([
|
|
2299
|
-
...Object.keys(pkgRaw.dependencies ?? {}),
|
|
2300
|
-
...Object.keys(pkgRaw.devDependencies ?? {})
|
|
2301
|
-
]);
|
|
2302
|
-
} catch {
|
|
2303
|
-
directDeps = /* @__PURE__ */ new Set();
|
|
2304
|
-
}
|
|
2305
|
-
const result = /* @__PURE__ */ new Map();
|
|
2306
|
-
for (const [name, entry] of Object.entries(rawEntries)) {
|
|
2307
|
-
const currentVersion = entry.current ?? "";
|
|
2308
|
-
const wantedVersion = entry.wanted ?? currentVersion;
|
|
2309
|
-
const latestVersion = entry.latest ?? currentVersion;
|
|
2310
|
-
if (!currentVersion || !latestVersion) continue;
|
|
2311
|
-
const dependencyScope = directDeps.has(name) ? "direct" : "transitive";
|
|
2312
|
-
if (!options.includeTransitive && dependencyScope === "transitive") continue;
|
|
2313
|
-
const isMajorBump = semver.valid(currentVersion) !== null && semver.valid(latestVersion) !== null && semver.major(latestVersion) > semver.major(currentVersion);
|
|
2314
|
-
result.set(name, {
|
|
2315
|
-
currentVersion,
|
|
2316
|
-
wantedVersion,
|
|
2317
|
-
latestVersion,
|
|
2318
|
-
isMajorBump,
|
|
2319
|
-
dependencyScope
|
|
2886
|
+
},
|
|
2887
|
+
policy: { exploitSignalOverride: opts.exploitSignalOverride }
|
|
2320
2888
|
});
|
|
2889
|
+
if (result.exploitSignalTriggered) {
|
|
2890
|
+
exploitSignalTriggered = true;
|
|
2891
|
+
}
|
|
2321
2892
|
}
|
|
2322
|
-
|
|
2893
|
+
let slaBreaches;
|
|
2894
|
+
if (opts.slaCheck && opts.slaPolicy && cveDetails.publishedAt) {
|
|
2895
|
+
const breach = checkSlaBreach(
|
|
2896
|
+
normalizedId,
|
|
2897
|
+
cveDetails.severity,
|
|
2898
|
+
cveDetails.publishedAt,
|
|
2899
|
+
opts.slaPolicy
|
|
2900
|
+
);
|
|
2901
|
+
if (breach) {
|
|
2902
|
+
slaBreaches = [breach];
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
return { suppressed: false, exploitSignalTriggered, slaBreaches };
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
// src/remediation/local/sbom.ts
|
|
2909
|
+
function buildSbom(packages, vulnerableNames, results) {
|
|
2910
|
+
const statusByPackage = /* @__PURE__ */ new Map();
|
|
2911
|
+
for (const result of results) {
|
|
2912
|
+
if (!vulnerableNames.has(result.packageName)) continue;
|
|
2913
|
+
if (result.suppressedBy) {
|
|
2914
|
+
statusByPackage.set(result.packageName, "suppressed");
|
|
2915
|
+
} else if (!result.applied && result.strategy === "none") {
|
|
2916
|
+
statusByPackage.set(result.packageName, "skipped");
|
|
2917
|
+
} else if (result.applied) {
|
|
2918
|
+
statusByPackage.set(result.packageName, "patched");
|
|
2919
|
+
} else {
|
|
2920
|
+
statusByPackage.set(result.packageName, "unpatched");
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
return packages.map((pkg) => {
|
|
2924
|
+
const isVulnerable = vulnerableNames.has(pkg.name);
|
|
2925
|
+
const entry = {
|
|
2926
|
+
name: pkg.name,
|
|
2927
|
+
version: pkg.version,
|
|
2928
|
+
scope: pkg.type === "direct" ? "direct" : "transitive"
|
|
2929
|
+
};
|
|
2930
|
+
if (isVulnerable) {
|
|
2931
|
+
entry.status = statusByPackage.get(pkg.name) ?? "unpatched";
|
|
2932
|
+
}
|
|
2933
|
+
return entry;
|
|
2934
|
+
});
|
|
2323
2935
|
}
|
|
2324
2936
|
|
|
2325
2937
|
// src/remediation/tools/apply-version-bump.ts
|
|
2326
2938
|
import { z as z4 } from "zod";
|
|
2327
|
-
import { join as
|
|
2328
|
-
import { readFileSync as
|
|
2939
|
+
import { join as join14 } from "path";
|
|
2940
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync3 } from "fs";
|
|
2329
2941
|
|
|
2330
2942
|
// src/platform/safe-exec.ts
|
|
2331
|
-
import { execa as
|
|
2943
|
+
import { execa as execa6 } from "execa";
|
|
2332
2944
|
var DANGEROUS_ENV_VARS = /* @__PURE__ */ new Set([
|
|
2333
2945
|
"NODE_OPTIONS",
|
|
2334
2946
|
"NODE_PATH",
|
|
@@ -2377,7 +2989,7 @@ function safeExeca(file, args = [], options = {}) {
|
|
|
2377
2989
|
options.env ?? process.env,
|
|
2378
2990
|
options.allowNodeOptions
|
|
2379
2991
|
);
|
|
2380
|
-
return
|
|
2992
|
+
return execa6(file, args, {
|
|
2381
2993
|
...options,
|
|
2382
2994
|
timeout,
|
|
2383
2995
|
env: sanitizedEnv
|
|
@@ -2385,19 +2997,19 @@ function safeExeca(file, args = [], options = {}) {
|
|
|
2385
2997
|
}
|
|
2386
2998
|
|
|
2387
2999
|
// src/remediation/tools/apply-version-bump.ts
|
|
2388
|
-
import
|
|
3000
|
+
import semver3 from "semver";
|
|
2389
3001
|
|
|
2390
3002
|
// src/platform/repo-lock.ts
|
|
2391
|
-
import { mkdir, rm } from "fs/promises";
|
|
2392
|
-
import { join as
|
|
3003
|
+
import { mkdir, rm as rm2 } from "fs/promises";
|
|
3004
|
+
import { join as join13 } from "path";
|
|
2393
3005
|
async function sleep(ms) {
|
|
2394
|
-
await new Promise((
|
|
3006
|
+
await new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
2395
3007
|
}
|
|
2396
3008
|
async function acquireRepoLock(cwd, options = {}) {
|
|
2397
3009
|
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
2398
3010
|
const retryDelayMs = options.retryDelayMs ?? 125;
|
|
2399
|
-
const lockRoot =
|
|
2400
|
-
const lockPath =
|
|
3011
|
+
const lockRoot = join13(cwd, ".autoremediator", "locks");
|
|
3012
|
+
const lockPath = join13(cwd, ".autoremediator", "locks", "remediation.lock");
|
|
2401
3013
|
const startedAt = Date.now();
|
|
2402
3014
|
await mkdir(lockRoot, { recursive: true });
|
|
2403
3015
|
while (true) {
|
|
@@ -2406,7 +3018,7 @@ async function acquireRepoLock(cwd, options = {}) {
|
|
|
2406
3018
|
return {
|
|
2407
3019
|
lockPath,
|
|
2408
3020
|
release: async () => {
|
|
2409
|
-
await
|
|
3021
|
+
await rm2(lockPath, { recursive: true, force: true });
|
|
2410
3022
|
}
|
|
2411
3023
|
};
|
|
2412
3024
|
} catch {
|
|
@@ -2438,6 +3050,7 @@ var applyVersionBumpTool = defineTool({
|
|
|
2438
3050
|
dryRun: z4.boolean().default(false).describe("If true, report changes but do not write"),
|
|
2439
3051
|
policy: z4.string().optional().describe("Optional path to .autoremediator policy file"),
|
|
2440
3052
|
runTests: z4.boolean().default(false).describe("If true, run test validation after applying the fix"),
|
|
3053
|
+
typecheck: z4.boolean().default(false).describe("If true, run typecheck validation after applying the fix"),
|
|
2441
3054
|
installMode: z4.enum(["standard", "prefer-offline", "deterministic"]).optional(),
|
|
2442
3055
|
installPreferOffline: z4.boolean().optional(),
|
|
2443
3056
|
enforceFrozenLockfile: z4.boolean().optional(),
|
|
@@ -2452,13 +3065,14 @@ var applyVersionBumpTool = defineTool({
|
|
|
2452
3065
|
dryRun,
|
|
2453
3066
|
policy,
|
|
2454
3067
|
runTests,
|
|
3068
|
+
typecheck,
|
|
2455
3069
|
installMode,
|
|
2456
3070
|
installPreferOffline,
|
|
2457
3071
|
enforceFrozenLockfile,
|
|
2458
3072
|
workspace
|
|
2459
3073
|
}) => {
|
|
2460
3074
|
const pm = packageManager ?? detectPackageManager(cwd);
|
|
2461
|
-
|
|
3075
|
+
let pkgPath = join14(cwd, "package.json");
|
|
2462
3076
|
const loadedPolicy = loadPolicy(cwd, policy);
|
|
2463
3077
|
const commandConstraints = {
|
|
2464
3078
|
...loadedPolicy.constraints,
|
|
@@ -2468,9 +3082,6 @@ var applyVersionBumpTool = defineTool({
|
|
|
2468
3082
|
workspace: workspace ?? loadedPolicy.constraints?.workspace
|
|
2469
3083
|
};
|
|
2470
3084
|
const yarnMajor = pm === "yarn" ? await getYarnMajorVersion(cwd) : void 0;
|
|
2471
|
-
const installCommand = resolveInstallCommand(pm, commandConstraints, yarnMajor);
|
|
2472
|
-
const testCommand = resolveTestCommand(pm, commandConstraints);
|
|
2473
|
-
const dedupeCommand = resolveDedupeCommand(pm, commandConstraints);
|
|
2474
3085
|
if (!isPackageAllowed(loadedPolicy, packageName)) {
|
|
2475
3086
|
return {
|
|
2476
3087
|
packageName,
|
|
@@ -2483,7 +3094,7 @@ var applyVersionBumpTool = defineTool({
|
|
|
2483
3094
|
message: `Policy blocked changes for package "${packageName}".`
|
|
2484
3095
|
};
|
|
2485
3096
|
}
|
|
2486
|
-
const isMajorBump =
|
|
3097
|
+
const isMajorBump = semver3.valid(fromVersion) && semver3.valid(toVersion) && semver3.major(toVersion) > semver3.major(fromVersion);
|
|
2487
3098
|
if (isMajorBump && !loadedPolicy.allowMajorBumps) {
|
|
2488
3099
|
return {
|
|
2489
3100
|
packageName,
|
|
@@ -2496,9 +3107,10 @@ var applyVersionBumpTool = defineTool({
|
|
|
2496
3107
|
message: `Policy blocked major bump for "${packageName}" (${fromVersion} -> ${toVersion}).`
|
|
2497
3108
|
};
|
|
2498
3109
|
}
|
|
3110
|
+
pkgPath = join14(cwd, "package.json");
|
|
2499
3111
|
let pkgJson;
|
|
2500
3112
|
try {
|
|
2501
|
-
pkgJson = JSON.parse(
|
|
3113
|
+
pkgJson = JSON.parse(readFileSync11(pkgPath, "utf8"));
|
|
2502
3114
|
} catch {
|
|
2503
3115
|
return {
|
|
2504
3116
|
packageName,
|
|
@@ -2510,9 +3122,20 @@ var applyVersionBumpTool = defineTool({
|
|
|
2510
3122
|
message: `Could not read package.json at "${pkgPath}".`
|
|
2511
3123
|
};
|
|
2512
3124
|
}
|
|
2513
|
-
|
|
3125
|
+
let depField = ["dependencies", "devDependencies", "peerDependencies"].find(
|
|
2514
3126
|
(f) => pkgJson[f]?.[packageName] !== void 0
|
|
2515
3127
|
);
|
|
3128
|
+
if (!depField) {
|
|
3129
|
+
const declaringWorkspace = findDeclaringWorkspacePackage(cwd, packageName, workspace);
|
|
3130
|
+
if (declaringWorkspace) {
|
|
3131
|
+
pkgPath = declaringWorkspace.manifestPath;
|
|
3132
|
+
pkgJson = declaringWorkspace.manifest;
|
|
3133
|
+
depField = ["dependencies", "devDependencies", "peerDependencies"].find(
|
|
3134
|
+
(f) => pkgJson[f]?.[packageName] !== void 0
|
|
3135
|
+
);
|
|
3136
|
+
commandConstraints.workspace = workspace ?? declaringWorkspace.name;
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
2516
3139
|
if (!depField) {
|
|
2517
3140
|
return {
|
|
2518
3141
|
packageName,
|
|
@@ -2528,9 +3151,14 @@ var applyVersionBumpTool = defineTool({
|
|
|
2528
3151
|
const prefixMatch = currentRange.match(/^([~^]?)/);
|
|
2529
3152
|
const prefix = prefixMatch?.[1] ?? "";
|
|
2530
3153
|
const newRange = `${prefix}${toVersion}`;
|
|
3154
|
+
const effectiveInstallCommand = resolveInstallCommand(pm, commandConstraints, yarnMajor);
|
|
3155
|
+
const effectiveTestCommand = resolveTestCommand(pm, commandConstraints);
|
|
3156
|
+
const effectiveTypecheckCommand = resolveTypecheckCommand(pm, commandConstraints, cwd);
|
|
3157
|
+
const effectiveDedupeCommand = resolveDedupeCommand(pm, commandConstraints);
|
|
3158
|
+
const shouldTypecheck = typecheck || Boolean(commandConstraints.typecheck);
|
|
2531
3159
|
if (dryRun) {
|
|
2532
|
-
const installCmd =
|
|
2533
|
-
const testCmd =
|
|
3160
|
+
const installCmd = effectiveInstallCommand.join(" ");
|
|
3161
|
+
const testCmd = effectiveTestCommand.join(" ");
|
|
2534
3162
|
return {
|
|
2535
3163
|
packageName,
|
|
2536
3164
|
strategy: "version-bump",
|
|
@@ -2538,14 +3166,14 @@ var applyVersionBumpTool = defineTool({
|
|
|
2538
3166
|
toVersion,
|
|
2539
3167
|
applied: false,
|
|
2540
3168
|
dryRun: true,
|
|
2541
|
-
message: `[DRY RUN] Would update ${depField}.${packageName}: "${currentRange}" -> "${newRange}", then run ${installCmd}${runTests ? ` and ${testCmd}` : ""}${
|
|
3169
|
+
message: `[DRY RUN] Would update ${depField}.${packageName}: "${currentRange}" -> "${newRange}", then run ${installCmd}${runTests ? ` and ${testCmd}` : ""}${effectiveDedupeCommand.length > 0 ? ` and ${effectiveDedupeCommand.join(" ")} (best-effort)` : ""}.`
|
|
2542
3170
|
};
|
|
2543
3171
|
}
|
|
2544
3172
|
return withRepoLock(cwd, async () => {
|
|
2545
3173
|
pkgJson[depField][packageName] = newRange;
|
|
2546
3174
|
writeFileSync3(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
2547
3175
|
try {
|
|
2548
|
-
const [installCmd, ...installArgs] =
|
|
3176
|
+
const [installCmd, ...installArgs] = effectiveInstallCommand;
|
|
2549
3177
|
await safeExeca(installCmd, installArgs, {
|
|
2550
3178
|
cwd,
|
|
2551
3179
|
stdio: "pipe"
|
|
@@ -2562,12 +3190,12 @@ var applyVersionBumpTool = defineTool({
|
|
|
2562
3190
|
applied: false,
|
|
2563
3191
|
dryRun: false,
|
|
2564
3192
|
unresolvedReason: "install-failed",
|
|
2565
|
-
message: `${
|
|
3193
|
+
message: `${effectiveInstallCommand.join(" ")} failed after updating "${packageName}" to ${toVersion}. Reverted. Error: ${message}`
|
|
2566
3194
|
};
|
|
2567
3195
|
}
|
|
2568
3196
|
if (runTests) {
|
|
2569
3197
|
try {
|
|
2570
|
-
const [testCmd, ...testArgs] =
|
|
3198
|
+
const [testCmd, ...testArgs] = effectiveTestCommand;
|
|
2571
3199
|
await safeExeca(testCmd, testArgs, {
|
|
2572
3200
|
cwd,
|
|
2573
3201
|
stdio: "pipe"
|
|
@@ -2576,7 +3204,38 @@ var applyVersionBumpTool = defineTool({
|
|
|
2576
3204
|
pkgJson[depField][packageName] = currentRange;
|
|
2577
3205
|
writeFileSync3(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
2578
3206
|
try {
|
|
2579
|
-
const [rollbackCmd, ...rollbackArgs] =
|
|
3207
|
+
const [rollbackCmd, ...rollbackArgs] = effectiveInstallCommand;
|
|
3208
|
+
await safeExeca(rollbackCmd, rollbackArgs, {
|
|
3209
|
+
cwd,
|
|
3210
|
+
stdio: "pipe"
|
|
3211
|
+
});
|
|
3212
|
+
} catch {
|
|
3213
|
+
}
|
|
3214
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3215
|
+
return {
|
|
3216
|
+
packageName,
|
|
3217
|
+
strategy: "version-bump",
|
|
3218
|
+
fromVersion,
|
|
3219
|
+
toVersion,
|
|
3220
|
+
applied: false,
|
|
3221
|
+
dryRun: false,
|
|
3222
|
+
unresolvedReason: "validation-failed",
|
|
3223
|
+
message: `${effectiveTestCommand.join(" ")} failed after upgrading "${packageName}" to ${toVersion}. Rolled back to ${currentRange}. Error: ${message}`
|
|
3224
|
+
};
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
if (shouldTypecheck && effectiveTypecheckCommand.length > 0) {
|
|
3228
|
+
try {
|
|
3229
|
+
const [tcCmd, ...tcArgs] = effectiveTypecheckCommand;
|
|
3230
|
+
await safeExeca(tcCmd, tcArgs, {
|
|
3231
|
+
cwd,
|
|
3232
|
+
stdio: "pipe"
|
|
3233
|
+
});
|
|
3234
|
+
} catch (err) {
|
|
3235
|
+
pkgJson[depField][packageName] = currentRange;
|
|
3236
|
+
writeFileSync3(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
3237
|
+
try {
|
|
3238
|
+
const [rollbackCmd, ...rollbackArgs] = effectiveInstallCommand;
|
|
2580
3239
|
await safeExeca(rollbackCmd, rollbackArgs, {
|
|
2581
3240
|
cwd,
|
|
2582
3241
|
stdio: "pipe"
|
|
@@ -2592,20 +3251,22 @@ var applyVersionBumpTool = defineTool({
|
|
|
2592
3251
|
applied: false,
|
|
2593
3252
|
dryRun: false,
|
|
2594
3253
|
unresolvedReason: "validation-failed",
|
|
2595
|
-
message:
|
|
3254
|
+
message: `TypeCheck validation failed (${effectiveTypecheckCommand.join(" ")}) after upgrading "${packageName}" to ${toVersion}. Rolled back to ${currentRange}. Error: ${message}`
|
|
2596
3255
|
};
|
|
2597
3256
|
}
|
|
2598
3257
|
}
|
|
2599
3258
|
let dedupeNote = "";
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
3259
|
+
if (effectiveDedupeCommand.length > 0) {
|
|
3260
|
+
try {
|
|
3261
|
+
const [dedupeCmd, ...dedupeArgs] = effectiveDedupeCommand;
|
|
3262
|
+
await safeExeca(dedupeCmd, dedupeArgs, {
|
|
3263
|
+
cwd,
|
|
3264
|
+
stdio: "pipe"
|
|
3265
|
+
});
|
|
3266
|
+
} catch (err) {
|
|
3267
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3268
|
+
dedupeNote = ` Dedupe warning: ${effectiveDedupeCommand.join(" ")} failed (${message}).`;
|
|
3269
|
+
}
|
|
2609
3270
|
}
|
|
2610
3271
|
return {
|
|
2611
3272
|
packageName,
|
|
@@ -2614,7 +3275,7 @@ var applyVersionBumpTool = defineTool({
|
|
|
2614
3275
|
toVersion,
|
|
2615
3276
|
applied: true,
|
|
2616
3277
|
dryRun: false,
|
|
2617
|
-
message: `Successfully upgraded "${packageName}" from ${fromVersion} to ${toVersion}, ran ${
|
|
3278
|
+
message: `Successfully upgraded "${packageName}" from ${fromVersion} to ${toVersion} in ${depField}, then ran ${effectiveInstallCommand.join(" ")}${runTests ? ` and passed ${effectiveTestCommand.join(" ")}` : ""}.${dedupeNote}`
|
|
2618
3279
|
};
|
|
2619
3280
|
});
|
|
2620
3281
|
}
|
|
@@ -2622,9 +3283,9 @@ var applyVersionBumpTool = defineTool({
|
|
|
2622
3283
|
|
|
2623
3284
|
// src/remediation/tools/apply-package-override/index.ts
|
|
2624
3285
|
import { z as z5 } from "zod";
|
|
2625
|
-
import { join as
|
|
2626
|
-
import { existsSync as
|
|
2627
|
-
import
|
|
3286
|
+
import { join as join15 } from "path";
|
|
3287
|
+
import { existsSync as existsSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
|
|
3288
|
+
import semver4 from "semver";
|
|
2628
3289
|
|
|
2629
3290
|
// src/remediation/tools/apply-package-override/helpers.ts
|
|
2630
3291
|
async function collectDependencyTrace(cwd, pm, packageName, constraints) {
|
|
@@ -2766,9 +3427,9 @@ var applyPackageOverrideTool = defineTool({
|
|
|
2766
3427
|
workspace
|
|
2767
3428
|
}) => {
|
|
2768
3429
|
const pm = packageManager ?? detectPackageManager(cwd);
|
|
2769
|
-
const pkgPath =
|
|
2770
|
-
const denoJsonPath =
|
|
2771
|
-
const isDenoNative = pm === "deno" && !
|
|
3430
|
+
const pkgPath = join15(cwd, "package.json");
|
|
3431
|
+
const denoJsonPath = join15(cwd, "deno.json");
|
|
3432
|
+
const isDenoNative = pm === "deno" && !existsSync11(pkgPath);
|
|
2772
3433
|
const loadedPolicy = loadPolicy(cwd, policy);
|
|
2773
3434
|
const commandConstraints = {
|
|
2774
3435
|
...loadedPolicy.constraints,
|
|
@@ -2794,7 +3455,7 @@ var applyPackageOverrideTool = defineTool({
|
|
|
2794
3455
|
message: `Policy blocked changes for package "${packageName}".`
|
|
2795
3456
|
};
|
|
2796
3457
|
}
|
|
2797
|
-
const isMajorBump =
|
|
3458
|
+
const isMajorBump = semver4.valid(fromVersion) && semver4.valid(toVersion) && semver4.major(toVersion) > semver4.major(fromVersion);
|
|
2798
3459
|
if (isMajorBump && !loadedPolicy.allowMajorBumps) {
|
|
2799
3460
|
return {
|
|
2800
3461
|
packageName,
|
|
@@ -2810,7 +3471,7 @@ var applyPackageOverrideTool = defineTool({
|
|
|
2810
3471
|
if (isDenoNative) {
|
|
2811
3472
|
let denoJson;
|
|
2812
3473
|
try {
|
|
2813
|
-
denoJson = JSON.parse(
|
|
3474
|
+
denoJson = JSON.parse(readFileSync12(denoJsonPath, "utf8"));
|
|
2814
3475
|
} catch {
|
|
2815
3476
|
return {
|
|
2816
3477
|
packageName,
|
|
@@ -2913,7 +3574,7 @@ var applyPackageOverrideTool = defineTool({
|
|
|
2913
3574
|
}
|
|
2914
3575
|
let pkgJson;
|
|
2915
3576
|
try {
|
|
2916
|
-
pkgJson = JSON.parse(
|
|
3577
|
+
pkgJson = JSON.parse(readFileSync12(pkgPath, "utf8"));
|
|
2917
3578
|
} catch {
|
|
2918
3579
|
return {
|
|
2919
3580
|
packageName,
|
|
@@ -3010,6 +3671,76 @@ var applyPackageOverrideTool = defineTool({
|
|
|
3010
3671
|
}
|
|
3011
3672
|
});
|
|
3012
3673
|
|
|
3674
|
+
// src/platform/supply-chain.ts
|
|
3675
|
+
async function verifyPackageTrust(packageName, targetVersion, policy) {
|
|
3676
|
+
if (!policy || !policy.minPublishAgeHours && !policy.blockInstallScripts && !policy.requireProvenance) {
|
|
3677
|
+
return { trusted: true };
|
|
3678
|
+
}
|
|
3679
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;
|
|
3680
|
+
let packument = null;
|
|
3681
|
+
try {
|
|
3682
|
+
const res = await httpClient({
|
|
3683
|
+
url,
|
|
3684
|
+
headers: { Accept: "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8" }
|
|
3685
|
+
});
|
|
3686
|
+
if (res.ok && res.data) {
|
|
3687
|
+
packument = res.data;
|
|
3688
|
+
}
|
|
3689
|
+
} catch (err) {
|
|
3690
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3691
|
+
return {
|
|
3692
|
+
trusted: false,
|
|
3693
|
+
reason: `Could not verify supply-chain trust for "${packageName}@${targetVersion}": ${msg}`
|
|
3694
|
+
};
|
|
3695
|
+
}
|
|
3696
|
+
if (!packument) {
|
|
3697
|
+
return {
|
|
3698
|
+
trusted: false,
|
|
3699
|
+
reason: `Package packument for "${packageName}" not found on registry.`
|
|
3700
|
+
};
|
|
3701
|
+
}
|
|
3702
|
+
let publishAgeHours;
|
|
3703
|
+
if (packument.time?.[targetVersion]) {
|
|
3704
|
+
const publishedAt = new Date(packument.time[targetVersion]).getTime();
|
|
3705
|
+
publishAgeHours = Math.max(0, (Date.now() - publishedAt) / (1e3 * 60 * 60));
|
|
3706
|
+
if (policy.minPublishAgeHours && publishAgeHours < policy.minPublishAgeHours) {
|
|
3707
|
+
return {
|
|
3708
|
+
trusted: false,
|
|
3709
|
+
publishAgeHours,
|
|
3710
|
+
reason: `Target release "${packageName}@${targetVersion}" was published only ${publishAgeHours.toFixed(1)}h ago (minimum required age is ${policy.minPublishAgeHours}h).`
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
const versionData = packument.versions?.[targetVersion];
|
|
3715
|
+
const scripts = versionData?.scripts ?? {};
|
|
3716
|
+
const installScriptKeys = ["preinstall", "install", "postinstall"].filter((k) => Boolean(scripts[k]));
|
|
3717
|
+
const hasInstallScripts = installScriptKeys.length > 0;
|
|
3718
|
+
if (policy.blockInstallScripts && hasInstallScripts) {
|
|
3719
|
+
return {
|
|
3720
|
+
trusted: false,
|
|
3721
|
+
publishAgeHours,
|
|
3722
|
+
hasInstallScripts: true,
|
|
3723
|
+
reason: `Target release "${packageName}@${targetVersion}" defines risky lifecycle scripts (${installScriptKeys.join(", ")}).`
|
|
3724
|
+
};
|
|
3725
|
+
}
|
|
3726
|
+
const hasProvenance = Boolean(versionData?.dist?.attestations?.provenance || versionData?.dist?.attestations?.url);
|
|
3727
|
+
if (policy.requireProvenance && !hasProvenance) {
|
|
3728
|
+
return {
|
|
3729
|
+
trusted: false,
|
|
3730
|
+
publishAgeHours,
|
|
3731
|
+
hasInstallScripts,
|
|
3732
|
+
hasProvenance: false,
|
|
3733
|
+
reason: `Target release "${packageName}@${targetVersion}" does not have cryptographic build provenance attestations.`
|
|
3734
|
+
};
|
|
3735
|
+
}
|
|
3736
|
+
return {
|
|
3737
|
+
trusted: true,
|
|
3738
|
+
publishAgeHours,
|
|
3739
|
+
hasInstallScripts,
|
|
3740
|
+
hasProvenance
|
|
3741
|
+
};
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3013
3744
|
// src/remediation/local/primary-strategy.ts
|
|
3014
3745
|
async function resolvePrimaryResult(params) {
|
|
3015
3746
|
const { vulnerable, cwd, packageManager, dryRun, policy, runTests, constraints } = params;
|
|
@@ -3080,6 +3811,22 @@ async function resolvePrimaryResult(params) {
|
|
|
3080
3811
|
}
|
|
3081
3812
|
};
|
|
3082
3813
|
}
|
|
3814
|
+
const trust2 = await verifyPackageTrust(pkg.name, safeUpgrade2.safeVersion, constraints.supplyChainPolicy);
|
|
3815
|
+
if (!trust2.trusted) {
|
|
3816
|
+
return {
|
|
3817
|
+
steps: 1,
|
|
3818
|
+
result: {
|
|
3819
|
+
packageName: pkg.name,
|
|
3820
|
+
strategy: "none",
|
|
3821
|
+
fromVersion: pkg.version,
|
|
3822
|
+
applied: false,
|
|
3823
|
+
dryRun,
|
|
3824
|
+
unresolvedReason: "policy-blocked",
|
|
3825
|
+
vulnerableRange: vulnerable.affected.vulnerableRange,
|
|
3826
|
+
message: `Supply chain trust gate blocked "${pkg.name}@${safeUpgrade2.safeVersion}": ${trust2.reason}`
|
|
3827
|
+
}
|
|
3828
|
+
};
|
|
3829
|
+
}
|
|
3083
3830
|
const overrideResult = await applyPackageOverrideTool.execute({
|
|
3084
3831
|
cwd,
|
|
3085
3832
|
packageManager,
|
|
@@ -3135,6 +3882,22 @@ async function resolvePrimaryResult(params) {
|
|
|
3135
3882
|
}
|
|
3136
3883
|
};
|
|
3137
3884
|
}
|
|
3885
|
+
const trust = await verifyPackageTrust(pkg.name, safeUpgrade.safeVersion, constraints.supplyChainPolicy);
|
|
3886
|
+
if (!trust.trusted) {
|
|
3887
|
+
return {
|
|
3888
|
+
steps: 1,
|
|
3889
|
+
result: {
|
|
3890
|
+
packageName: pkg.name,
|
|
3891
|
+
strategy: "none",
|
|
3892
|
+
fromVersion: pkg.version,
|
|
3893
|
+
applied: false,
|
|
3894
|
+
dryRun,
|
|
3895
|
+
unresolvedReason: "policy-blocked",
|
|
3896
|
+
vulnerableRange: vulnerable.affected.vulnerableRange,
|
|
3897
|
+
message: `Supply chain trust gate blocked "${pkg.name}@${safeUpgrade.safeVersion}": ${trust.reason}`
|
|
3898
|
+
}
|
|
3899
|
+
};
|
|
3900
|
+
}
|
|
3138
3901
|
const applyResult = await applyVersionBumpTool.execute({
|
|
3139
3902
|
cwd,
|
|
3140
3903
|
packageManager,
|
|
@@ -3144,6 +3907,7 @@ async function resolvePrimaryResult(params) {
|
|
|
3144
3907
|
dryRun,
|
|
3145
3908
|
policy,
|
|
3146
3909
|
runTests,
|
|
3910
|
+
typecheck: constraints.typecheck,
|
|
3147
3911
|
installMode: constraints.installMode,
|
|
3148
3912
|
installPreferOffline: constraints.installPreferOffline,
|
|
3149
3913
|
enforceFrozenLockfile: constraints.enforceFrozenLockfile,
|
|
@@ -3157,10 +3921,10 @@ async function resolvePrimaryResult(params) {
|
|
|
3157
3921
|
|
|
3158
3922
|
// src/remediation/tools/fetch-package-source.ts
|
|
3159
3923
|
import { z as z6 } from "zod";
|
|
3160
|
-
import { mkdir as mkdir2, mkdtemp, readdir as readdir2, readFile as
|
|
3161
|
-
import { join as
|
|
3924
|
+
import { mkdir as mkdir2, mkdtemp, readdir as readdir2, readFile as readFile4, rm as rm3 } from "fs/promises";
|
|
3925
|
+
import { join as join16 } from "path";
|
|
3162
3926
|
import { tmpdir } from "os";
|
|
3163
|
-
import { execa as
|
|
3927
|
+
import { execa as execa7 } from "execa";
|
|
3164
3928
|
var fetchPackageSourceTool = defineTool({
|
|
3165
3929
|
description: "Download package tarball from npm and extract source files for CVE analysis. Supports custom file patterns (default: *.js, *.ts).",
|
|
3166
3930
|
parameters: z6.object({
|
|
@@ -3178,25 +3942,25 @@ var fetchPackageSourceTool = defineTool({
|
|
|
3178
3942
|
if (safePatterns.length === 0) {
|
|
3179
3943
|
return { success: false, error: "No valid file patterns provided." };
|
|
3180
3944
|
}
|
|
3181
|
-
const tempBaseDir = await mkdtemp(
|
|
3182
|
-
const extractDir =
|
|
3945
|
+
const tempBaseDir = await mkdtemp(join16(tmpdir(), "autoremediator-pkg-"));
|
|
3946
|
+
const extractDir = join16(tempBaseDir, "out");
|
|
3183
3947
|
try {
|
|
3184
3948
|
const scopedName = packageName.split("/").pop();
|
|
3185
3949
|
const npmUrl = `https://registry.npmjs.org/${packageName}/-/${scopedName}-${version}.tgz`;
|
|
3186
3950
|
await mkdir2(tempBaseDir, { recursive: true });
|
|
3187
|
-
const tarballPath =
|
|
3188
|
-
await
|
|
3951
|
+
const tarballPath = join16(tempBaseDir, "package.tgz");
|
|
3952
|
+
await execa7("curl", ["-L", "-o", tarballPath, npmUrl]);
|
|
3189
3953
|
await mkdir2(extractDir, { recursive: true });
|
|
3190
|
-
await
|
|
3954
|
+
await execa7("tar", ["-xzf", tarballPath, "-C", extractDir]);
|
|
3191
3955
|
const extractedContents = await readdir2(extractDir);
|
|
3192
|
-
const packageRootDir = extractedContents.includes("package") ?
|
|
3956
|
+
const packageRootDir = extractedContents.includes("package") ? join16(extractDir, "package") : extractDir;
|
|
3193
3957
|
const sourceCode = {};
|
|
3194
3958
|
async function walkDir(dir, relativeBase) {
|
|
3195
3959
|
try {
|
|
3196
3960
|
const files = await readdir2(dir, { withFileTypes: true });
|
|
3197
3961
|
for (const file of files) {
|
|
3198
|
-
const fullPath =
|
|
3199
|
-
const relPath =
|
|
3962
|
+
const fullPath = join16(dir, file.name);
|
|
3963
|
+
const relPath = join16(relativeBase, file.name);
|
|
3200
3964
|
if (file.isDirectory()) {
|
|
3201
3965
|
if (!["node_modules", ".git", "dist", "build", "coverage", ".next", "out"].includes(
|
|
3202
3966
|
file.name
|
|
@@ -3210,7 +3974,7 @@ var fetchPackageSourceTool = defineTool({
|
|
|
3210
3974
|
});
|
|
3211
3975
|
if (matches) {
|
|
3212
3976
|
try {
|
|
3213
|
-
const content = await
|
|
3977
|
+
const content = await readFile4(fullPath, "utf8");
|
|
3214
3978
|
sourceCode[relPath] = content;
|
|
3215
3979
|
} catch {
|
|
3216
3980
|
}
|
|
@@ -3245,7 +4009,7 @@ var fetchPackageSourceTool = defineTool({
|
|
|
3245
4009
|
error: `Failed to fetch and extract package ${packageName}@${version}: ${message}`
|
|
3246
4010
|
};
|
|
3247
4011
|
} finally {
|
|
3248
|
-
await
|
|
4012
|
+
await rm3(tempBaseDir, { recursive: true, force: true });
|
|
3249
4013
|
}
|
|
3250
4014
|
}
|
|
3251
4015
|
});
|
|
@@ -3303,6 +4067,7 @@ Analyze the source code to:
|
|
|
3303
4067
|
2. Explain the root cause of the security issue
|
|
3304
4068
|
3. Propose a secure fix that addresses the vulnerability
|
|
3305
4069
|
4. Provide the complete fixed version of affected files
|
|
4070
|
+
5. Optionally provide a standalone Node.js reproduction/regression test script
|
|
3306
4071
|
|
|
3307
4072
|
## Response Format
|
|
3308
4073
|
Respond ONLY with valid JSON (no markdown, no extra text):
|
|
@@ -3313,7 +4078,8 @@ Respond ONLY with valid JSON (no markdown, no extra text):
|
|
|
3313
4078
|
"path/to/other.ts": "Complete fixed source code for this file"
|
|
3314
4079
|
},
|
|
3315
4080
|
"confidence": 0.95,
|
|
3316
|
-
"riskLevel": "medium"
|
|
4081
|
+
"riskLevel": "medium",
|
|
4082
|
+
"reproductionTest": "// Node.js script asserting the patch resolves the exploit/regression"
|
|
3317
4083
|
}
|
|
3318
4084
|
|
|
3319
4085
|
Important:
|
|
@@ -3593,6 +4359,7 @@ var generatePatchTool = defineTool({
|
|
|
3593
4359
|
success: true,
|
|
3594
4360
|
patches,
|
|
3595
4361
|
patchContent: patches[0]?.unifiedDiff,
|
|
4362
|
+
reproductionTest: analysis.reproductionTest,
|
|
3596
4363
|
llmProvider: provider,
|
|
3597
4364
|
llmModel: modelName,
|
|
3598
4365
|
latencyMs,
|
|
@@ -3617,17 +4384,17 @@ var generatePatchTool = defineTool({
|
|
|
3617
4384
|
|
|
3618
4385
|
// src/remediation/tools/apply-patch-file/index.ts
|
|
3619
4386
|
import { z as z8 } from "zod";
|
|
3620
|
-
import { mkdir as mkdir3, writeFile as
|
|
3621
|
-
import { join as
|
|
3622
|
-
import { execa as
|
|
4387
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
4388
|
+
import { join as join18 } from "path";
|
|
4389
|
+
import { execa as execa9 } from "execa";
|
|
3623
4390
|
|
|
3624
4391
|
// src/remediation/tools/apply-patch-file/helpers.ts
|
|
3625
|
-
import { existsSync as
|
|
3626
|
-
import { mkdtemp as mkdtemp2, readFile as
|
|
4392
|
+
import { existsSync as existsSync12 } from "fs";
|
|
4393
|
+
import { mkdtemp as mkdtemp2, readFile as readFile5, rm as rm4, writeFile as writeFile2 } from "fs/promises";
|
|
3627
4394
|
import { createHash } from "crypto";
|
|
3628
4395
|
import { tmpdir as tmpdir2 } from "os";
|
|
3629
|
-
import { join as
|
|
3630
|
-
import { execa as
|
|
4396
|
+
import { join as join17 } from "path";
|
|
4397
|
+
import { execa as execa8 } from "execa";
|
|
3631
4398
|
async function resolvePatchMode(packageManager, cwd) {
|
|
3632
4399
|
if (packageManager === "npm" || packageManager === "bun" || packageManager === "deno")
|
|
3633
4400
|
return "patch-package";
|
|
@@ -3658,13 +4425,13 @@ function computePatchIntegrity(patchContent) {
|
|
|
3658
4425
|
return `sha256:${hex}`;
|
|
3659
4426
|
}
|
|
3660
4427
|
async function writePatchManifest(manifestFilePath, artifact) {
|
|
3661
|
-
await
|
|
4428
|
+
await writeFile2(manifestFilePath, JSON.stringify(artifact, null, 2) + "\n", "utf8");
|
|
3662
4429
|
}
|
|
3663
4430
|
async function configurePatchPackagePostinstall(cwd, packageManager) {
|
|
3664
|
-
const pkgJsonPath =
|
|
4431
|
+
const pkgJsonPath = join17(cwd, "package.json");
|
|
3665
4432
|
let pkgJson;
|
|
3666
4433
|
try {
|
|
3667
|
-
pkgJson = JSON.parse(await
|
|
4434
|
+
pkgJson = JSON.parse(await readFile5(pkgJsonPath, "utf8"));
|
|
3668
4435
|
} catch {
|
|
3669
4436
|
return {
|
|
3670
4437
|
success: false,
|
|
@@ -3676,7 +4443,7 @@ async function configurePatchPackagePostinstall(cwd, packageManager) {
|
|
|
3676
4443
|
try {
|
|
3677
4444
|
const commands = getPackageManagerCommands(packageManager);
|
|
3678
4445
|
const [cmd, ...args] = commands.installDev("patch-package");
|
|
3679
|
-
await
|
|
4446
|
+
await execa8(cmd, args, {
|
|
3680
4447
|
cwd,
|
|
3681
4448
|
stdio: "pipe"
|
|
3682
4449
|
});
|
|
@@ -3697,13 +4464,13 @@ async function configurePatchPackagePostinstall(cwd, packageManager) {
|
|
|
3697
4464
|
} else if (!currentPostinstall) {
|
|
3698
4465
|
pkgJson.scripts.postinstall = patchApplyCmd;
|
|
3699
4466
|
}
|
|
3700
|
-
await
|
|
4467
|
+
await writeFile2(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
3701
4468
|
return { success: true };
|
|
3702
4469
|
}
|
|
3703
4470
|
async function capturePackageJsonSnapshot(cwd) {
|
|
3704
|
-
const path =
|
|
4471
|
+
const path = join17(cwd, "package.json");
|
|
3705
4472
|
try {
|
|
3706
|
-
const content = await
|
|
4473
|
+
const content = await readFile5(path, "utf8");
|
|
3707
4474
|
return { path, content };
|
|
3708
4475
|
} catch {
|
|
3709
4476
|
return void 0;
|
|
@@ -3719,19 +4486,19 @@ async function cleanupPatchArtifacts(params) {
|
|
|
3719
4486
|
installCommand,
|
|
3720
4487
|
rerunInstall
|
|
3721
4488
|
} = params;
|
|
3722
|
-
await
|
|
4489
|
+
await rm4(patchFilePath, { force: true }).catch(() => void 0);
|
|
3723
4490
|
if (manifestFilePath) {
|
|
3724
|
-
await
|
|
4491
|
+
await rm4(manifestFilePath, { force: true }).catch(() => void 0);
|
|
3725
4492
|
}
|
|
3726
4493
|
if (patchMode === "patch-package" && packageJsonSnapshot) {
|
|
3727
|
-
await
|
|
4494
|
+
await writeFile2(packageJsonSnapshot.path, packageJsonSnapshot.content, "utf8").catch(
|
|
3728
4495
|
() => void 0
|
|
3729
4496
|
);
|
|
3730
4497
|
}
|
|
3731
4498
|
if (!rerunInstall) return;
|
|
3732
4499
|
try {
|
|
3733
4500
|
const [installCmd, ...installArgs] = installCommand;
|
|
3734
|
-
await
|
|
4501
|
+
await execa8(installCmd, installArgs, {
|
|
3735
4502
|
cwd,
|
|
3736
4503
|
stdio: "pipe"
|
|
3737
4504
|
});
|
|
@@ -3745,7 +4512,7 @@ async function applyNativePatch(params) {
|
|
|
3745
4512
|
const createArgs = ["patch", packageSpec];
|
|
3746
4513
|
let patchDir;
|
|
3747
4514
|
try {
|
|
3748
|
-
const createResult = await
|
|
4515
|
+
const createResult = await execa8(createCommand, createArgs, {
|
|
3749
4516
|
cwd,
|
|
3750
4517
|
stdio: "pipe"
|
|
3751
4518
|
});
|
|
@@ -3773,17 +4540,17 @@ ${createResult.stderr}`);
|
|
|
3773
4540
|
error: `Could not determine native patch directory for ${packageSpec}.`
|
|
3774
4541
|
};
|
|
3775
4542
|
}
|
|
3776
|
-
const tempPatchDir = await mkdtemp2(
|
|
3777
|
-
const tempPatchFile =
|
|
4543
|
+
const tempPatchDir = await mkdtemp2(join17(tmpdir2(), "autoremediator-native-patch-"));
|
|
4544
|
+
const tempPatchFile = join17(tempPatchDir, "change.patch");
|
|
3778
4545
|
try {
|
|
3779
|
-
await
|
|
3780
|
-
await
|
|
4546
|
+
await writeFile2(tempPatchFile, patchContent, "utf8");
|
|
4547
|
+
await execa8("patch", ["-p1", "-i", tempPatchFile], {
|
|
3781
4548
|
cwd: patchDir,
|
|
3782
4549
|
stdio: "pipe"
|
|
3783
4550
|
});
|
|
3784
4551
|
const commitCommand = patchMode === "native-pnpm" ? "pnpm" : "yarn";
|
|
3785
4552
|
const commitArgs = patchMode === "native-pnpm" ? ["patch-commit", patchDir] : ["patch-commit", "-s", patchDir];
|
|
3786
|
-
await
|
|
4553
|
+
await execa8(commitCommand, commitArgs, {
|
|
3787
4554
|
cwd,
|
|
3788
4555
|
stdio: "pipe"
|
|
3789
4556
|
});
|
|
@@ -3798,19 +4565,19 @@ ${createResult.stderr}`);
|
|
|
3798
4565
|
error: `Failed to apply native patch for ${packageSpec}: ${err instanceof Error ? err.message : String(err)}`
|
|
3799
4566
|
};
|
|
3800
4567
|
} finally {
|
|
3801
|
-
await
|
|
4568
|
+
await rm4(tempPatchDir, { recursive: true, force: true });
|
|
3802
4569
|
}
|
|
3803
4570
|
return { success: true };
|
|
3804
4571
|
}
|
|
3805
4572
|
function extractPatchDirectory(output) {
|
|
3806
4573
|
const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
3807
4574
|
for (const line of lines) {
|
|
3808
|
-
if (
|
|
4575
|
+
if (existsSync12(line)) {
|
|
3809
4576
|
return line;
|
|
3810
4577
|
}
|
|
3811
4578
|
const tokens = line.split(/\s+/).map((token) => token.replace(/^['"]|['"]$/g, ""));
|
|
3812
4579
|
for (const token of tokens) {
|
|
3813
|
-
if (token.startsWith("/") &&
|
|
4580
|
+
if (token.startsWith("/") && existsSync12(token)) {
|
|
3814
4581
|
return token;
|
|
3815
4582
|
}
|
|
3816
4583
|
}
|
|
@@ -3820,7 +4587,7 @@ function extractPatchDirectory(output) {
|
|
|
3820
4587
|
async function validatePatchWithTests(cwd, testCommand) {
|
|
3821
4588
|
try {
|
|
3822
4589
|
const [cmd, ...args] = testCommand;
|
|
3823
|
-
const result = await
|
|
4590
|
+
const result = await execa8(cmd, args, {
|
|
3824
4591
|
cwd,
|
|
3825
4592
|
timeout: 6e4,
|
|
3826
4593
|
stdio: "pipe"
|
|
@@ -3830,7 +4597,8 @@ async function validatePatchWithTests(cwd, testCommand) {
|
|
|
3830
4597
|
output: result.stdout
|
|
3831
4598
|
};
|
|
3832
4599
|
} catch (err) {
|
|
3833
|
-
const
|
|
4600
|
+
const stdoutVal = typeof err === "object" && err !== null && "stdout" in err ? err.stdout : "";
|
|
4601
|
+
const errorOutput = typeof stdoutVal === "string" ? stdoutVal : Buffer.isBuffer(stdoutVal) ? stdoutVal.toString("utf8") : "";
|
|
3834
4602
|
const failedTests = extractFailedTests(errorOutput);
|
|
3835
4603
|
return {
|
|
3836
4604
|
passed: false,
|
|
@@ -3879,6 +4647,7 @@ var applyPatchFileTool = defineTool({
|
|
|
3879
4647
|
enforceFrozenLockfile: z8.boolean().optional(),
|
|
3880
4648
|
workspace: z8.string().optional(),
|
|
3881
4649
|
validateWithTests: z8.boolean().optional().default(true).describe("Run package manager test command to validate patch doesn't break anything"),
|
|
4650
|
+
reproductionTest: z8.string().optional().describe("Optional synthesized reproduction test script verifying fix"),
|
|
3882
4651
|
dryRun: z8.boolean().optional().default(false).describe("If true, report but do not mutate files")
|
|
3883
4652
|
}).refine((value) => Boolean(value.patchContent || value.patches && value.patches.length > 0), {
|
|
3884
4653
|
message: "Either patchContent or patches must be provided"
|
|
@@ -3900,6 +4669,7 @@ var applyPatchFileTool = defineTool({
|
|
|
3900
4669
|
workspace,
|
|
3901
4670
|
riskLevel,
|
|
3902
4671
|
validateWithTests,
|
|
4672
|
+
reproductionTest,
|
|
3903
4673
|
dryRun
|
|
3904
4674
|
}) => {
|
|
3905
4675
|
try {
|
|
@@ -3950,7 +4720,7 @@ var applyPatchFileTool = defineTool({
|
|
|
3950
4720
|
};
|
|
3951
4721
|
}
|
|
3952
4722
|
const patchFileName = buildPatchFileName(packageName, vulnerableVersion);
|
|
3953
|
-
const patchFilePath =
|
|
4723
|
+
const patchFilePath = join18(cwd, patchesDir, patchFileName);
|
|
3954
4724
|
const manifestFilePath = `${patchFilePath}.json`;
|
|
3955
4725
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3956
4726
|
const baseArtifact = {
|
|
@@ -3989,9 +4759,9 @@ var applyPatchFileTool = defineTool({
|
|
|
3989
4759
|
}
|
|
3990
4760
|
return withRepoLock(cwd, async () => {
|
|
3991
4761
|
const packageJsonSnapshot = patchModeRequiresPackageJsonSnapshot(pm) ? await capturePackageJsonSnapshot(cwd) : void 0;
|
|
3992
|
-
const patchesDirPath =
|
|
4762
|
+
const patchesDirPath = join18(cwd, patchesDir);
|
|
3993
4763
|
await mkdir3(patchesDirPath, { recursive: true });
|
|
3994
|
-
await
|
|
4764
|
+
await writeFile3(patchFilePath, selectedPatch, "utf8");
|
|
3995
4765
|
validationPhases.push({
|
|
3996
4766
|
phase: "patch-write",
|
|
3997
4767
|
passed: true,
|
|
@@ -4059,7 +4829,7 @@ var applyPatchFileTool = defineTool({
|
|
|
4059
4829
|
if (patchMode === "patch-package") {
|
|
4060
4830
|
try {
|
|
4061
4831
|
const [installCmd, ...installArgs] = installCommand;
|
|
4062
|
-
await
|
|
4832
|
+
await execa9(installCmd, installArgs, {
|
|
4063
4833
|
cwd,
|
|
4064
4834
|
stdio: "pipe"
|
|
4065
4835
|
});
|
|
@@ -4152,10 +4922,33 @@ var applyPatchFileTool = defineTool({
|
|
|
4152
4922
|
message: "Patch validation tests passed."
|
|
4153
4923
|
});
|
|
4154
4924
|
}
|
|
4925
|
+
let reproductionTestPath;
|
|
4926
|
+
if (reproductionTest) {
|
|
4927
|
+
const safeName = packageName.replace(/^@/, "").replace(/\//g, "+");
|
|
4928
|
+
reproductionTestPath = join18(patchesDirPath, `${safeName}+${vulnerableVersion}.test.cjs`);
|
|
4929
|
+
await writeFile3(reproductionTestPath, reproductionTest, "utf8");
|
|
4930
|
+
try {
|
|
4931
|
+
const reproRun = await execa9("node", [reproductionTestPath], { cwd, stdio: "pipe" });
|
|
4932
|
+
validationPhases.push({
|
|
4933
|
+
phase: "reproduction-test",
|
|
4934
|
+
passed: reproRun.exitCode === 0,
|
|
4935
|
+
message: "Reproduction test verified fix successfully."
|
|
4936
|
+
});
|
|
4937
|
+
} catch (err) {
|
|
4938
|
+
const reproErr = err instanceof Error ? err.message : String(err);
|
|
4939
|
+
validationPhases.push({
|
|
4940
|
+
phase: "reproduction-test",
|
|
4941
|
+
passed: false,
|
|
4942
|
+
error: `Reproduction test failed: ${reproErr}`
|
|
4943
|
+
});
|
|
4944
|
+
}
|
|
4945
|
+
}
|
|
4155
4946
|
const finalArtifact = {
|
|
4156
4947
|
...artifact,
|
|
4157
4948
|
applied: true,
|
|
4158
4949
|
dryRun: false,
|
|
4950
|
+
reproductionTest,
|
|
4951
|
+
reproductionTestPath,
|
|
4159
4952
|
validationPhases
|
|
4160
4953
|
};
|
|
4161
4954
|
await writePatchManifest(manifestFilePath, finalArtifact);
|
|
@@ -4465,7 +5258,11 @@ function resolveLocalRunOptions(options) {
|
|
|
4465
5258
|
const runTests = options.runTests ?? false;
|
|
4466
5259
|
const policy = options.policy ?? "";
|
|
4467
5260
|
const patchesDir = options.patchesDir || "./patches";
|
|
4468
|
-
const constraints =
|
|
5261
|
+
const constraints = {
|
|
5262
|
+
...options.constraints,
|
|
5263
|
+
typecheck: options.typecheck ?? options.constraints?.typecheck,
|
|
5264
|
+
supplyChainPolicy: options.supplyChainPolicy ?? options.constraints?.supplyChainPolicy
|
|
5265
|
+
};
|
|
4469
5266
|
const loadedPolicy = loadPolicy(cwd, options.policy);
|
|
4470
5267
|
const llmProvider = resolveProvider(options);
|
|
4471
5268
|
return {
|
|
@@ -4502,7 +5299,7 @@ function resolveLocalRunOptions(options) {
|
|
|
4502
5299
|
}
|
|
4503
5300
|
|
|
4504
5301
|
// src/remediation/local/vulnerability-match.ts
|
|
4505
|
-
import
|
|
5302
|
+
import semver5 from "semver";
|
|
4506
5303
|
function findVulnerablePackages(cveDetails, installedPackages) {
|
|
4507
5304
|
const vulnerablePackages = [];
|
|
4508
5305
|
for (const affected of cveDetails.affectedPackages) {
|
|
@@ -4511,10 +5308,10 @@ function findVulnerablePackages(cveDetails, installedPackages) {
|
|
|
4511
5308
|
if (affected.ecosystem !== "npm") continue;
|
|
4512
5309
|
const matches = installedPackages.filter((pkg) => pkg.name === affected.name);
|
|
4513
5310
|
for (const installed of matches) {
|
|
4514
|
-
if (!
|
|
5311
|
+
if (!semver5.valid(installed.version)) continue;
|
|
4515
5312
|
let isVulnerable = false;
|
|
4516
5313
|
try {
|
|
4517
|
-
isVulnerable =
|
|
5314
|
+
isVulnerable = semver5.satisfies(installed.version, affected.vulnerableRange, {
|
|
4518
5315
|
includePrerelease: false
|
|
4519
5316
|
});
|
|
4520
5317
|
} catch {
|
|
@@ -4857,7 +5654,7 @@ async function runLocalRemediationPipeline(cveId, options = {}) {
|
|
|
4857
5654
|
};
|
|
4858
5655
|
if (regressionCheck && primaryResult.applied && !dryRun && primaryResult.toVersion) {
|
|
4859
5656
|
try {
|
|
4860
|
-
if (
|
|
5657
|
+
if (semver6.satisfies(primaryResult.toVersion, vulnerable.affected.vulnerableRange, {
|
|
4861
5658
|
includePrerelease: false
|
|
4862
5659
|
})) {
|
|
4863
5660
|
primaryResult.regressionDetected = true;
|
|
@@ -4948,8 +5745,8 @@ function accumulateStepResults(params) {
|
|
|
4948
5745
|
}
|
|
4949
5746
|
|
|
4950
5747
|
// src/remediation/orchestration-prompt.ts
|
|
4951
|
-
import { existsSync as
|
|
4952
|
-
import { join as
|
|
5748
|
+
import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
|
|
5749
|
+
import { join as join19 } from "path";
|
|
4953
5750
|
function buildProviderAddendum(provider, personality = "balanced") {
|
|
4954
5751
|
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.";
|
|
4955
5752
|
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.";
|
|
@@ -4960,13 +5757,13 @@ Provider profile:
|
|
|
4960
5757
|
- ${personalityDirective}`;
|
|
4961
5758
|
}
|
|
4962
5759
|
function loadOrchestrationPrompt(ctx) {
|
|
4963
|
-
const promptPath =
|
|
5760
|
+
const promptPath = join19(
|
|
4964
5761
|
process.cwd(),
|
|
4965
5762
|
".github",
|
|
4966
5763
|
"instructions",
|
|
4967
5764
|
"orchestration.instructions.md"
|
|
4968
5765
|
);
|
|
4969
|
-
if (!
|
|
5766
|
+
if (!existsSync13(promptPath)) {
|
|
4970
5767
|
return `You are autoremediator, an agentic security remediation system for Node.js package dependencies.
|
|
4971
5768
|
Working directory: ${ctx.cwd}
|
|
4972
5769
|
Package manager: ${ctx.packageManager}
|
|
@@ -4979,107 +5776,43 @@ Prefer version bump: ${String(ctx.constraints.preferVersionBump ?? false)}
|
|
|
4979
5776
|
|
|
4980
5777
|
Required sequence:
|
|
4981
5778
|
1. lookup-cve
|
|
4982
|
-
2. check-inventory
|
|
4983
|
-
3. check-version-match
|
|
4984
|
-
4. find-fixed-version
|
|
4985
|
-
5. apply-version-bump
|
|
4986
|
-
6. apply-package-override
|
|
4987
|
-
|
|
4988
|
-
Fallback sequence (when neither version bump nor override can be applied):
|
|
4989
|
-
1. fetch-package-source
|
|
4990
|
-
2. generate-patch
|
|
4991
|
-
3. apply-patch-file
|
|
4992
|
-
|
|
4993
|
-
Always respect dryRun and policy constraints.`;
|
|
4994
|
-
}
|
|
4995
|
-
const template = readFileSync11(promptPath, "utf8");
|
|
4996
|
-
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(
|
|
4997
|
-
"{{directDependenciesOnly}}",
|
|
4998
|
-
String(ctx.constraints.directDependenciesOnly ?? false)
|
|
4999
|
-
).replaceAll("{{preferVersionBump}}", String(ctx.constraints.preferVersionBump ?? false)) + buildProviderAddendum(ctx.llmProvider, ctx.modelPersonality);
|
|
5000
|
-
}
|
|
5001
|
-
|
|
5002
|
-
// src/remediation/pipeline-runtime/progress.ts
|
|
5003
|
-
function createProgressEmitter(options) {
|
|
5004
|
-
return (stage, detail, extra) => {
|
|
5005
|
-
if (!options.onProgress) return;
|
|
5006
|
-
options.onProgress({
|
|
5007
|
-
stage,
|
|
5008
|
-
detail,
|
|
5009
|
-
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5010
|
-
provider: extra?.provider,
|
|
5011
|
-
model: extra?.model
|
|
5012
|
-
});
|
|
5013
|
-
};
|
|
5014
|
-
}
|
|
5015
|
-
|
|
5016
|
-
// src/remediation/tools/lookup-cve.ts
|
|
5017
|
-
import { z as z9 } from "zod";
|
|
5018
|
-
|
|
5019
|
-
// src/intelligence/sources/cisa-kev.ts
|
|
5020
|
-
var CISA_KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json";
|
|
5021
|
-
async function fetchCisaKevFeed() {
|
|
5022
|
-
try {
|
|
5023
|
-
const res = await httpClient({ url: CISA_KEV_URL });
|
|
5024
|
-
if (!res.ok) return void 0;
|
|
5025
|
-
return res.data;
|
|
5026
|
-
} catch {
|
|
5027
|
-
return void 0;
|
|
5028
|
-
}
|
|
5029
|
-
}
|
|
5030
|
-
function findKevEntry(feed, cveId) {
|
|
5031
|
-
if (!feed?.vulnerabilities?.length) return void 0;
|
|
5032
|
-
const normalized = cveId.toUpperCase();
|
|
5033
|
-
return feed.vulnerabilities.find((v) => v.cveID.toUpperCase() === normalized);
|
|
5034
|
-
}
|
|
5035
|
-
async function enrichWithCisaKev(details) {
|
|
5036
|
-
const feed = await fetchCisaKevFeed();
|
|
5037
|
-
const entry = findKevEntry(feed, details.id);
|
|
5038
|
-
if (!entry) return details;
|
|
5039
|
-
details.kev = {
|
|
5040
|
-
knownExploited: true,
|
|
5041
|
-
dateAdded: entry.dateAdded,
|
|
5042
|
-
dueDate: entry.dueDate,
|
|
5043
|
-
requiredAction: entry.requiredAction,
|
|
5044
|
-
knownRansomwareCampaignUse: entry.knownRansomwareCampaignUse
|
|
5045
|
-
};
|
|
5046
|
-
if (!details.references.includes(CISA_KEV_URL)) {
|
|
5047
|
-
details.references.push(CISA_KEV_URL);
|
|
5048
|
-
}
|
|
5049
|
-
return details;
|
|
5050
|
-
}
|
|
5779
|
+
2. check-inventory
|
|
5780
|
+
3. check-version-match
|
|
5781
|
+
4. find-fixed-version
|
|
5782
|
+
5. apply-version-bump
|
|
5783
|
+
6. apply-package-override
|
|
5051
5784
|
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
url.searchParams.set("cve", cveId);
|
|
5059
|
-
const res = await httpClient({ url: url.toString() });
|
|
5060
|
-
if (!res.ok) return void 0;
|
|
5061
|
-
const body = res.data;
|
|
5062
|
-
return body.data?.[0];
|
|
5063
|
-
} catch {
|
|
5064
|
-
return void 0;
|
|
5785
|
+
Fallback sequence (when neither version bump nor override can be applied):
|
|
5786
|
+
1. fetch-package-source
|
|
5787
|
+
2. generate-patch
|
|
5788
|
+
3. apply-patch-file
|
|
5789
|
+
|
|
5790
|
+
Always respect dryRun and policy constraints.`;
|
|
5065
5791
|
}
|
|
5792
|
+
const template = readFileSync13(promptPath, "utf8");
|
|
5793
|
+
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(
|
|
5794
|
+
"{{directDependenciesOnly}}",
|
|
5795
|
+
String(ctx.constraints.directDependenciesOnly ?? false)
|
|
5796
|
+
).replaceAll("{{preferVersionBump}}", String(ctx.constraints.preferVersionBump ?? false)) + buildProviderAddendum(ctx.llmProvider, ctx.modelPersonality);
|
|
5066
5797
|
}
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5798
|
+
|
|
5799
|
+
// src/remediation/pipeline-runtime/progress.ts
|
|
5800
|
+
function createProgressEmitter(options) {
|
|
5801
|
+
return (stage, detail, extra) => {
|
|
5802
|
+
if (!options.onProgress) return;
|
|
5803
|
+
options.onProgress({
|
|
5804
|
+
stage,
|
|
5805
|
+
detail,
|
|
5806
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5807
|
+
provider: extra?.provider,
|
|
5808
|
+
model: extra?.model
|
|
5809
|
+
});
|
|
5079
5810
|
};
|
|
5080
|
-
return details;
|
|
5081
5811
|
}
|
|
5082
5812
|
|
|
5813
|
+
// src/remediation/tools/lookup-cve.ts
|
|
5814
|
+
import { z as z9 } from "zod";
|
|
5815
|
+
|
|
5083
5816
|
// src/intelligence/sources/cve-services.ts
|
|
5084
5817
|
function pickEnglishDescription(container) {
|
|
5085
5818
|
if (!container?.descriptions?.length) return void 0;
|
|
@@ -5363,7 +6096,7 @@ var lookupCveTool = defineTool({
|
|
|
5363
6096
|
|
|
5364
6097
|
// src/remediation/tools/check-version-match.ts
|
|
5365
6098
|
import { z as z10 } from "zod";
|
|
5366
|
-
import
|
|
6099
|
+
import semver7 from "semver";
|
|
5367
6100
|
var affectedPackageSchema = z10.object({
|
|
5368
6101
|
name: z10.string(),
|
|
5369
6102
|
ecosystem: z10.literal("npm"),
|
|
@@ -5392,10 +6125,10 @@ var checkVersionMatchTool = defineTool({
|
|
|
5392
6125
|
(p) => p.name === affected.name
|
|
5393
6126
|
);
|
|
5394
6127
|
for (const installed of matches) {
|
|
5395
|
-
if (!
|
|
6128
|
+
if (!semver7.valid(installed.version)) continue;
|
|
5396
6129
|
let isVulnerable = false;
|
|
5397
6130
|
try {
|
|
5398
|
-
isVulnerable =
|
|
6131
|
+
isVulnerable = semver7.satisfies(installed.version, affected.vulnerableRange, {
|
|
5399
6132
|
includePrerelease: false
|
|
5400
6133
|
});
|
|
5401
6134
|
} catch {
|
|
@@ -5713,7 +6446,7 @@ async function runRemediationPipeline(cveId, options = {}) {
|
|
|
5713
6446
|
}
|
|
5714
6447
|
|
|
5715
6448
|
// src/api/change-request/index.ts
|
|
5716
|
-
import { execa as
|
|
6449
|
+
import { execa as execa10 } from "execa";
|
|
5717
6450
|
import { randomBytes } from "crypto";
|
|
5718
6451
|
function sanitizeBranchToken(value) {
|
|
5719
6452
|
return value.toLowerCase().replace(/[^a-z0-9._/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
@@ -5742,22 +6475,68 @@ function resolveProvider2(options) {
|
|
|
5742
6475
|
return options.provider ?? "github";
|
|
5743
6476
|
}
|
|
5744
6477
|
function resolveGrouping(options) {
|
|
5745
|
-
|
|
6478
|
+
const allowed = ["all", "per-cve", "per-package", "by-severity"];
|
|
6479
|
+
if (options.grouping && !allowed.includes(options.grouping)) {
|
|
5746
6480
|
throw new Error(
|
|
5747
|
-
|
|
6481
|
+
`Unsupported changeRequest.grouping: "${options.grouping}". Supported values: ${allowed.join(", ")}`
|
|
5748
6482
|
);
|
|
5749
6483
|
}
|
|
5750
|
-
return "all";
|
|
6484
|
+
return options.grouping ?? "all";
|
|
6485
|
+
}
|
|
6486
|
+
function groupReports(reports, grouping) {
|
|
6487
|
+
if (reports.length === 0) return [];
|
|
6488
|
+
if (grouping === "all") {
|
|
6489
|
+
return [reports];
|
|
6490
|
+
}
|
|
6491
|
+
if (grouping === "per-cve") {
|
|
6492
|
+
const map = /* @__PURE__ */ new Map();
|
|
6493
|
+
for (const report of reports) {
|
|
6494
|
+
const key = report.cveId.toUpperCase();
|
|
6495
|
+
const list = map.get(key) ?? [];
|
|
6496
|
+
list.push(report);
|
|
6497
|
+
map.set(key, list);
|
|
6498
|
+
}
|
|
6499
|
+
return Array.from(map.values());
|
|
6500
|
+
}
|
|
6501
|
+
if (grouping === "per-package") {
|
|
6502
|
+
const map = /* @__PURE__ */ new Map();
|
|
6503
|
+
for (const report of reports) {
|
|
6504
|
+
const pkgs = new Set(report.results.map((r) => r.packageName));
|
|
6505
|
+
if (pkgs.size === 0) {
|
|
6506
|
+
const list = map.get("misc") ?? [];
|
|
6507
|
+
list.push(report);
|
|
6508
|
+
map.set("misc", list);
|
|
6509
|
+
} else {
|
|
6510
|
+
for (const pkg of pkgs) {
|
|
6511
|
+
const list = map.get(pkg) ?? [];
|
|
6512
|
+
list.push(report);
|
|
6513
|
+
map.set(pkg, list);
|
|
6514
|
+
}
|
|
6515
|
+
}
|
|
6516
|
+
}
|
|
6517
|
+
return Array.from(map.values());
|
|
6518
|
+
}
|
|
6519
|
+
if (grouping === "by-severity") {
|
|
6520
|
+
const map = /* @__PURE__ */ new Map();
|
|
6521
|
+
for (const report of reports) {
|
|
6522
|
+
const sev = report.cveDetails?.severity ?? "UNKNOWN";
|
|
6523
|
+
const list = map.get(sev) ?? [];
|
|
6524
|
+
list.push(report);
|
|
6525
|
+
map.set(sev, list);
|
|
6526
|
+
}
|
|
6527
|
+
return Array.from(map.values());
|
|
6528
|
+
}
|
|
6529
|
+
return [reports];
|
|
5751
6530
|
}
|
|
5752
6531
|
async function detectCurrentBranch(cwd) {
|
|
5753
|
-
const result = await
|
|
6532
|
+
const result = await execa10("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
5754
6533
|
cwd,
|
|
5755
6534
|
stdio: "pipe"
|
|
5756
6535
|
});
|
|
5757
6536
|
return result.stdout.trim();
|
|
5758
6537
|
}
|
|
5759
6538
|
async function ensureRepoHasChanges(cwd) {
|
|
5760
|
-
const diff = await
|
|
6539
|
+
const diff = await execa10("git", ["status", "--porcelain"], {
|
|
5761
6540
|
cwd,
|
|
5762
6541
|
stdio: "pipe"
|
|
5763
6542
|
});
|
|
@@ -5786,7 +6565,7 @@ async function createGitHubRequest(params) {
|
|
|
5786
6565
|
if (params.draft) {
|
|
5787
6566
|
args.push("--draft");
|
|
5788
6567
|
}
|
|
5789
|
-
const result = await
|
|
6568
|
+
const result = await execa10("gh", args, {
|
|
5790
6569
|
cwd: params.cwd,
|
|
5791
6570
|
stdio: "pipe"
|
|
5792
6571
|
});
|
|
@@ -5811,7 +6590,7 @@ async function createGitLabRequest(params) {
|
|
|
5811
6590
|
if (params.draft) {
|
|
5812
6591
|
args.push("--draft");
|
|
5813
6592
|
}
|
|
5814
|
-
const result = await
|
|
6593
|
+
const result = await execa10("glab", args, {
|
|
5815
6594
|
cwd: params.cwd,
|
|
5816
6595
|
stdio: "pipe"
|
|
5817
6596
|
});
|
|
@@ -5821,64 +6600,68 @@ async function createChangeRequestsForReports(params) {
|
|
|
5821
6600
|
const { cwd, options, reports } = params;
|
|
5822
6601
|
const provider = resolveProvider2(options);
|
|
5823
6602
|
const grouping = resolveGrouping(options);
|
|
5824
|
-
const
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
const
|
|
6603
|
+
const groups = groupReports(reports, grouping);
|
|
6604
|
+
if (groups.length === 0) {
|
|
6605
|
+
return [];
|
|
6606
|
+
}
|
|
6607
|
+
const results = [];
|
|
6608
|
+
const hasChanges = await ensureRepoHasChanges(cwd).catch(() => false);
|
|
6609
|
+
const baseBranch = options.baseBranch ?? await detectCurrentBranch(cwd).catch(() => "main");
|
|
6610
|
+
const branchPrefix = options.branchPrefix ?? "autoremediator";
|
|
6611
|
+
const pushRemote = options.pushRemote ?? "origin";
|
|
6612
|
+
if (options.pushRemote && !/^[a-zA-Z0-9._-]+$/.test(options.pushRemote)) {
|
|
6613
|
+
throw new Error(`Invalid pushRemote value: ${options.pushRemote}`);
|
|
6614
|
+
}
|
|
6615
|
+
if (options.repository && !/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(options.repository)) {
|
|
6616
|
+
throw new Error(`Invalid repository format: ${options.repository}`);
|
|
6617
|
+
}
|
|
6618
|
+
for (const group of groups) {
|
|
6619
|
+
const plan = buildPlan(group);
|
|
6620
|
+
const titlePrefix = options.titlePrefix?.trim().slice(0, 200);
|
|
6621
|
+
const bodyFooter = options.bodyFooter ? options.bodyFooter.slice(0, 2e3) : void 0;
|
|
6622
|
+
const title = titlePrefix ? `${titlePrefix} ${plan.title}` : plan.title;
|
|
6623
|
+
const body = bodyFooter ? `${plan.body}
|
|
5829
6624
|
|
|
5830
6625
|
${bodyFooter}` : plan.body;
|
|
5831
|
-
try {
|
|
5832
|
-
const hasChanges = await ensureRepoHasChanges(cwd);
|
|
5833
6626
|
if (!hasChanges) {
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
];
|
|
6627
|
+
results.push({
|
|
6628
|
+
provider,
|
|
6629
|
+
grouping,
|
|
6630
|
+
repository: options.repository,
|
|
6631
|
+
branchName: "",
|
|
6632
|
+
title,
|
|
6633
|
+
body,
|
|
6634
|
+
created: false,
|
|
6635
|
+
cveIds: plan.cveIds,
|
|
6636
|
+
packageNames: plan.packageNames,
|
|
6637
|
+
error: "No repository changes to include in change request."
|
|
6638
|
+
});
|
|
6639
|
+
continue;
|
|
5848
6640
|
}
|
|
5849
|
-
const baseBranch = options.baseBranch ?? await detectCurrentBranch(cwd);
|
|
5850
|
-
const branchPrefix = options.branchPrefix ?? "autoremediator";
|
|
5851
6641
|
const branchName = toBranchName(branchPrefix, plan.cveIds);
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
target: baseBranch,
|
|
5876
|
-
title,
|
|
5877
|
-
body,
|
|
5878
|
-
draft: options.draft
|
|
5879
|
-
});
|
|
5880
|
-
return [
|
|
5881
|
-
{
|
|
6642
|
+
try {
|
|
6643
|
+
await execa10("git", ["checkout", "-b", branchName], { cwd, stdio: "pipe" });
|
|
6644
|
+
await execa10("git", ["add", "-A"], { cwd, stdio: "pipe" });
|
|
6645
|
+
await execa10("git", ["commit", "-m", title], { cwd, stdio: "pipe" });
|
|
6646
|
+
await execa10("git", ["push", "-u", pushRemote, branchName], { cwd, stdio: "pipe" });
|
|
6647
|
+
const url = provider === "github" ? await createGitHubRequest({
|
|
6648
|
+
cwd,
|
|
6649
|
+
repository: options.repository,
|
|
6650
|
+
head: branchName,
|
|
6651
|
+
base: baseBranch,
|
|
6652
|
+
title,
|
|
6653
|
+
body,
|
|
6654
|
+
draft: options.draft
|
|
6655
|
+
}) : await createGitLabRequest({
|
|
6656
|
+
cwd,
|
|
6657
|
+
repository: options.repository,
|
|
6658
|
+
source: branchName,
|
|
6659
|
+
target: baseBranch,
|
|
6660
|
+
title,
|
|
6661
|
+
body,
|
|
6662
|
+
draft: options.draft
|
|
6663
|
+
});
|
|
6664
|
+
results.push({
|
|
5882
6665
|
provider,
|
|
5883
6666
|
grouping,
|
|
5884
6667
|
repository: options.repository,
|
|
@@ -5890,11 +6673,13 @@ ${bodyFooter}` : plan.body;
|
|
|
5890
6673
|
url,
|
|
5891
6674
|
cveIds: plan.cveIds,
|
|
5892
6675
|
packageNames: plan.packageNames
|
|
6676
|
+
});
|
|
6677
|
+
if (groups.length > 1) {
|
|
6678
|
+
await execa10("git", ["checkout", baseBranch], { cwd, stdio: "pipe" }).catch(() => {
|
|
6679
|
+
});
|
|
5893
6680
|
}
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
return [
|
|
5897
|
-
{
|
|
6681
|
+
} catch (error) {
|
|
6682
|
+
results.push({
|
|
5898
6683
|
provider,
|
|
5899
6684
|
grouping,
|
|
5900
6685
|
repository: options.repository,
|
|
@@ -5906,9 +6691,10 @@ ${bodyFooter}` : plan.body;
|
|
|
5906
6691
|
cveIds: plan.cveIds,
|
|
5907
6692
|
packageNames: plan.packageNames,
|
|
5908
6693
|
error: error instanceof Error ? error.message : String(error)
|
|
5909
|
-
}
|
|
5910
|
-
|
|
6694
|
+
});
|
|
6695
|
+
}
|
|
5911
6696
|
}
|
|
6697
|
+
return results;
|
|
5912
6698
|
}
|
|
5913
6699
|
|
|
5914
6700
|
// src/api/context.ts
|
|
@@ -5942,7 +6728,7 @@ function resolveConstraints(options, cwd) {
|
|
|
5942
6728
|
|
|
5943
6729
|
// src/platform/evidence.ts
|
|
5944
6730
|
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
5945
|
-
import { join as
|
|
6731
|
+
import { join as join20 } from "path";
|
|
5946
6732
|
function createEvidenceLog(cwd, cveIds, context = {}) {
|
|
5947
6733
|
return {
|
|
5948
6734
|
runId: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
@@ -5974,9 +6760,9 @@ function finalizeEvidence(log) {
|
|
|
5974
6760
|
return log;
|
|
5975
6761
|
}
|
|
5976
6762
|
function writeEvidenceLog(cwd, log) {
|
|
5977
|
-
const dir =
|
|
6763
|
+
const dir = join20(cwd, ".autoremediator", "evidence");
|
|
5978
6764
|
mkdirSync3(dir, { recursive: true });
|
|
5979
|
-
const filePath =
|
|
6765
|
+
const filePath = join20(dir, `${log.runId}.json`);
|
|
5980
6766
|
writeFileSync5(filePath, JSON.stringify(log, null, 2) + "\n", "utf8");
|
|
5981
6767
|
return filePath;
|
|
5982
6768
|
}
|
|
@@ -6268,6 +7054,24 @@ function buildResultSimulation(report, result, options) {
|
|
|
6268
7054
|
const dependencyScope = result.dependencyScope ?? dependencyScopeLookup.get(result.packageName);
|
|
6269
7055
|
const plannedMutations = buildPlannedMutations(result, options.runTests === true);
|
|
6270
7056
|
const wouldMutate = plannedMutations.length > 0;
|
|
7057
|
+
let unifiedDiff;
|
|
7058
|
+
if (result.patchArtifact?.patchFilePath) {
|
|
7059
|
+
unifiedDiff = `--- ${result.patchArtifact.patchFilePath}
|
|
7060
|
+
+++ ${result.patchArtifact.patchFilePath}`;
|
|
7061
|
+
} else if (result.strategy === "version-bump" && result.toVersion) {
|
|
7062
|
+
unifiedDiff = `--- package.json
|
|
7063
|
+
+++ package.json
|
|
7064
|
+
@@ -1,3 +1,3 @@
|
|
7065
|
+
- "${result.packageName}": "${result.fromVersion}"
|
|
7066
|
+
+ "${result.packageName}": "^${result.toVersion}"`;
|
|
7067
|
+
} else if (result.strategy === "override" && result.toVersion) {
|
|
7068
|
+
unifiedDiff = `--- package.json
|
|
7069
|
+
+++ package.json
|
|
7070
|
+
@@ -1,3 +1,3 @@
|
|
7071
|
+
+ "overrides": {
|
|
7072
|
+
+ "${result.packageName}": "${result.toVersion}"
|
|
7073
|
+
+ }`;
|
|
7074
|
+
}
|
|
6271
7075
|
return {
|
|
6272
7076
|
mode,
|
|
6273
7077
|
wouldMutate,
|
|
@@ -6278,7 +7082,8 @@ function buildResultSimulation(report, result, options) {
|
|
|
6278
7082
|
dependencyScope,
|
|
6279
7083
|
wouldMutate,
|
|
6280
7084
|
runTests: options.runTests === true
|
|
6281
|
-
})
|
|
7085
|
+
}),
|
|
7086
|
+
unifiedDiff
|
|
6282
7087
|
};
|
|
6283
7088
|
}
|
|
6284
7089
|
function buildSimulationSummary(reports) {
|
|
@@ -6457,10 +7262,15 @@ function ciExitCode(summary) {
|
|
|
6457
7262
|
}
|
|
6458
7263
|
|
|
6459
7264
|
// src/api/remediate/validators.ts
|
|
7265
|
+
var CVE_ID_REGEX = /^CVE-\d{4}-\d{1,7}$/i;
|
|
7266
|
+
var GHSA_ID_REGEX = /^GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$/i;
|
|
7267
|
+
function isVulnerabilityId(id) {
|
|
7268
|
+
return CVE_ID_REGEX.test(id) || GHSA_ID_REGEX.test(id);
|
|
7269
|
+
}
|
|
6460
7270
|
function assertValidCveId(cveId) {
|
|
6461
|
-
if (
|
|
7271
|
+
if (!isVulnerabilityId(cveId)) {
|
|
6462
7272
|
throw new Error(
|
|
6463
|
-
`Invalid
|
|
7273
|
+
`Invalid vulnerability ID: "${cveId}". Expected format: CVE-YYYY-NNNNN (e.g. CVE-2021-23337) or GHSA-xxxx-xxxx-xxxx.`
|
|
6464
7274
|
);
|
|
6465
7275
|
}
|
|
6466
7276
|
}
|
|
@@ -6556,13 +7366,14 @@ async function planRemediation(cveId, options = {}) {
|
|
|
6556
7366
|
}
|
|
6557
7367
|
|
|
6558
7368
|
// src/scanner/parse-input.ts
|
|
6559
|
-
import { extname, resolve as
|
|
6560
|
-
import { readFileSync as
|
|
6561
|
-
import { execa as
|
|
7369
|
+
import { extname, resolve as resolve4 } from "path";
|
|
7370
|
+
import { readFileSync as readFileSync17 } from "fs";
|
|
7371
|
+
import { execa as execa11 } from "execa";
|
|
6562
7372
|
|
|
6563
7373
|
// src/scanner/adapters/npm-audit.ts
|
|
6564
|
-
import { readFileSync as
|
|
7374
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
6565
7375
|
var CVE_REGEX = /CVE-\d{4}-\d{1,7}/gi;
|
|
7376
|
+
var GHSA_REGEX = /GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}/gi;
|
|
6566
7377
|
function normalizeSeverity(raw) {
|
|
6567
7378
|
if (!raw) return "UNKNOWN";
|
|
6568
7379
|
const up = raw.toUpperCase();
|
|
@@ -6577,7 +7388,8 @@ function parseNpmAuditJsonFromString(content) {
|
|
|
6577
7388
|
for (const vuln of Object.values(report.vulnerabilities ?? {})) {
|
|
6578
7389
|
for (const viaEntry of vuln.via ?? []) {
|
|
6579
7390
|
const text = typeof viaEntry === "string" ? viaEntry : `${viaEntry.url ?? ""} ${viaEntry.name ?? ""}`;
|
|
6580
|
-
const
|
|
7391
|
+
const cveMatches = text.match(CVE_REGEX) ?? [];
|
|
7392
|
+
const matches = cveMatches.length > 0 ? cveMatches : text.match(GHSA_REGEX) ?? [];
|
|
6581
7393
|
for (const match of matches) {
|
|
6582
7394
|
const cveId = match.toUpperCase();
|
|
6583
7395
|
const key = `${cveId}:${vuln.name}`;
|
|
@@ -6595,13 +7407,14 @@ function parseNpmAuditJsonFromString(content) {
|
|
|
6595
7407
|
return findings;
|
|
6596
7408
|
}
|
|
6597
7409
|
function parseNpmAuditJsonFile(filePath) {
|
|
6598
|
-
const content =
|
|
7410
|
+
const content = readFileSync14(filePath, "utf8");
|
|
6599
7411
|
return parseNpmAuditJsonFromString(content);
|
|
6600
7412
|
}
|
|
6601
7413
|
|
|
6602
7414
|
// src/scanner/adapters/yarn-audit.ts
|
|
6603
|
-
import { readFileSync as
|
|
7415
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
6604
7416
|
var CVE_REGEX2 = /CVE-\d{4}-\d{1,7}/gi;
|
|
7417
|
+
var GHSA_REGEX2 = /GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}/gi;
|
|
6605
7418
|
function normalizeSeverity2(raw) {
|
|
6606
7419
|
if (!raw) return "UNKNOWN";
|
|
6607
7420
|
const up = raw.toUpperCase();
|
|
@@ -6626,7 +7439,8 @@ function parseYarnAuditJsonFromString(content) {
|
|
|
6626
7439
|
const packageName = advisory?.module_name;
|
|
6627
7440
|
const severity = normalizeSeverity2(advisory?.severity);
|
|
6628
7441
|
const text = `${advisory?.url ?? ""} ${(advisory?.cves ?? []).join(" ")}`;
|
|
6629
|
-
const
|
|
7442
|
+
const cveMatches = text.match(CVE_REGEX2) ?? [];
|
|
7443
|
+
const matches = cveMatches.length > 0 ? cveMatches : text.match(GHSA_REGEX2) ?? [];
|
|
6630
7444
|
for (const match of matches) {
|
|
6631
7445
|
const cveId = match.toUpperCase();
|
|
6632
7446
|
const key = `${cveId}:${packageName ?? ""}`;
|
|
@@ -6643,12 +7457,12 @@ function parseYarnAuditJsonFromString(content) {
|
|
|
6643
7457
|
return findings;
|
|
6644
7458
|
}
|
|
6645
7459
|
function parseYarnAuditJsonFile(filePath) {
|
|
6646
|
-
const content =
|
|
7460
|
+
const content = readFileSync15(filePath, "utf8");
|
|
6647
7461
|
return parseYarnAuditJsonFromString(content);
|
|
6648
7462
|
}
|
|
6649
7463
|
|
|
6650
7464
|
// src/scanner/adapters/sarif.ts
|
|
6651
|
-
import { readFileSync as
|
|
7465
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
6652
7466
|
var CVE_REGEX3 = /CVE-\d{4}-\d{1,7}/gi;
|
|
6653
7467
|
function extractPackageName(result) {
|
|
6654
7468
|
const pkg = result.properties?.["packageName"];
|
|
@@ -6686,7 +7500,7 @@ function parseSarifFromString(content) {
|
|
|
6686
7500
|
return findings;
|
|
6687
7501
|
}
|
|
6688
7502
|
function parseSarifFile(filePath) {
|
|
6689
|
-
const content =
|
|
7503
|
+
const content = readFileSync16(filePath, "utf8");
|
|
6690
7504
|
return parseSarifFromString(content);
|
|
6691
7505
|
}
|
|
6692
7506
|
|
|
@@ -6695,7 +7509,7 @@ function parseScanInput(filePath, format) {
|
|
|
6695
7509
|
if (filePath.includes("\0")) {
|
|
6696
7510
|
throw new Error("Invalid scan input path: path contains null bytes");
|
|
6697
7511
|
}
|
|
6698
|
-
const resolvedPath =
|
|
7512
|
+
const resolvedPath = resolve4(filePath);
|
|
6699
7513
|
const resolved = format === "auto" ? inferFormat(resolvedPath) : format;
|
|
6700
7514
|
if (resolved === "npm-audit") {
|
|
6701
7515
|
return parseNpmAuditJsonFile(resolvedPath);
|
|
@@ -6706,7 +7520,7 @@ function parseScanInput(filePath, format) {
|
|
|
6706
7520
|
if (resolved === "sarif") {
|
|
6707
7521
|
return parseSarifFile(resolvedPath);
|
|
6708
7522
|
}
|
|
6709
|
-
throw new Error(`Unsupported input format: ${resolved}`);
|
|
7523
|
+
throw new Error(`Unsupported input format: ${String(resolved)}`);
|
|
6710
7524
|
}
|
|
6711
7525
|
async function parseScanInputFromAudit(params) {
|
|
6712
7526
|
const pm = params.packageManager ?? detectPackageManager(params.cwd);
|
|
@@ -6717,7 +7531,7 @@ async function parseScanInputFromAudit(params) {
|
|
|
6717
7531
|
}
|
|
6718
7532
|
const command = resolveAuditCommand(pm, { workspace: params.workspace });
|
|
6719
7533
|
const [cmd, ...args] = command;
|
|
6720
|
-
const result = await
|
|
7534
|
+
const result = await execa11(cmd, args, {
|
|
6721
7535
|
cwd: params.cwd,
|
|
6722
7536
|
stdio: "pipe",
|
|
6723
7537
|
reject: false
|
|
@@ -6771,7 +7585,7 @@ function inferFormat(filePath) {
|
|
|
6771
7585
|
const ext = extname(filePath).toLowerCase();
|
|
6772
7586
|
if (ext === ".sarif") return "sarif";
|
|
6773
7587
|
try {
|
|
6774
|
-
const content =
|
|
7588
|
+
const content = readFileSync17(filePath, "utf8");
|
|
6775
7589
|
const firstLine = content.split("\n").find((line) => line.trim().startsWith("{"));
|
|
6776
7590
|
if (firstLine) {
|
|
6777
7591
|
const parsed = JSON.parse(firstLine);
|
|
@@ -7182,14 +7996,14 @@ async function remediatePortfolio(targets, options = {}) {
|
|
|
7182
7996
|
}
|
|
7183
7997
|
|
|
7184
7998
|
// src/api/update-outdated/index.ts
|
|
7185
|
-
import { join as
|
|
7186
|
-
import { readFileSync as
|
|
7187
|
-
import { execa as
|
|
7999
|
+
import { join as join21 } from "path";
|
|
8000
|
+
import { readFileSync as readFileSync18, writeFileSync as writeFileSync6 } from "fs";
|
|
8001
|
+
import { execa as execa12 } from "execa";
|
|
7188
8002
|
async function applyBump(params) {
|
|
7189
|
-
const pkgPath =
|
|
8003
|
+
const pkgPath = join21(params.cwd, "package.json");
|
|
7190
8004
|
let pkgJson;
|
|
7191
8005
|
try {
|
|
7192
|
-
pkgJson = JSON.parse(
|
|
8006
|
+
pkgJson = JSON.parse(readFileSync18(pkgPath, "utf8"));
|
|
7193
8007
|
} catch {
|
|
7194
8008
|
return {
|
|
7195
8009
|
applied: false,
|
|
@@ -7214,7 +8028,7 @@ async function applyBump(params) {
|
|
|
7214
8028
|
writeFileSync6(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
7215
8029
|
try {
|
|
7216
8030
|
const [installCmd, ...installArgs] = params.installCommand;
|
|
7217
|
-
await
|
|
8031
|
+
await execa12(installCmd, installArgs, { cwd: params.cwd, stdio: "pipe" });
|
|
7218
8032
|
} catch (err) {
|
|
7219
8033
|
pkgJson[depField][params.packageName] = currentRange;
|
|
7220
8034
|
writeFileSync6(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
@@ -7227,13 +8041,13 @@ async function applyBump(params) {
|
|
|
7227
8041
|
if (params.runTests) {
|
|
7228
8042
|
try {
|
|
7229
8043
|
const [testCmd, ...testArgs] = params.testCommand;
|
|
7230
|
-
await
|
|
8044
|
+
await execa12(testCmd, testArgs, { cwd: params.cwd, stdio: "pipe" });
|
|
7231
8045
|
} catch (err) {
|
|
7232
8046
|
pkgJson[depField][params.packageName] = currentRange;
|
|
7233
8047
|
writeFileSync6(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
|
|
7234
8048
|
try {
|
|
7235
8049
|
const [rollbackCmd, ...rollbackArgs] = params.installCommand;
|
|
7236
|
-
await
|
|
8050
|
+
await execa12(rollbackCmd, rollbackArgs, { cwd: params.cwd, stdio: "pipe" });
|
|
7237
8051
|
} catch {
|
|
7238
8052
|
}
|
|
7239
8053
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -7428,6 +8242,432 @@ async function updateOutdated(options = {}) {
|
|
|
7428
8242
|
};
|
|
7429
8243
|
}
|
|
7430
8244
|
|
|
8245
|
+
// src/api/reachability/index.ts
|
|
8246
|
+
async function checkReachability(options) {
|
|
8247
|
+
const cwd = options.cwd ?? process.cwd();
|
|
8248
|
+
return assessPackageReachability(cwd, options.packageName, options.symbol);
|
|
8249
|
+
}
|
|
8250
|
+
|
|
8251
|
+
// src/scanner/delta-scan.ts
|
|
8252
|
+
import { existsSync as existsSync14, readFileSync as readFileSync19 } from "fs";
|
|
8253
|
+
import { join as join22 } from "path";
|
|
8254
|
+
import { execa as execa13 } from "execa";
|
|
8255
|
+
import semver9 from "semver";
|
|
8256
|
+
|
|
8257
|
+
// src/scanner/lockfile-diff.ts
|
|
8258
|
+
import YAML2 from "yaml";
|
|
8259
|
+
import semver8 from "semver";
|
|
8260
|
+
function parseLockfileDependencies(filename, content) {
|
|
8261
|
+
const result = /* @__PURE__ */ new Map();
|
|
8262
|
+
const add = (name, ver) => {
|
|
8263
|
+
if (!name || !ver) return;
|
|
8264
|
+
const cleanVer = semver8.clean(ver) || semver8.coerce(ver)?.version || ver;
|
|
8265
|
+
let set = result.get(name);
|
|
8266
|
+
if (!set) {
|
|
8267
|
+
set = /* @__PURE__ */ new Set();
|
|
8268
|
+
result.set(name, set);
|
|
8269
|
+
}
|
|
8270
|
+
set.add(cleanVer);
|
|
8271
|
+
};
|
|
8272
|
+
if (filename.endsWith("package-lock.json")) {
|
|
8273
|
+
try {
|
|
8274
|
+
const data = JSON.parse(content);
|
|
8275
|
+
if (data.packages && typeof data.packages === "object") {
|
|
8276
|
+
for (const [key, pkg] of Object.entries(data.packages)) {
|
|
8277
|
+
if (!key || key === "" || typeof pkg !== "object" || !pkg) continue;
|
|
8278
|
+
const idx = key.lastIndexOf("node_modules/");
|
|
8279
|
+
if (idx !== -1) {
|
|
8280
|
+
const rawName = key.slice(idx + "node_modules/".length);
|
|
8281
|
+
const ver = pkg.version;
|
|
8282
|
+
add(rawName, ver);
|
|
8283
|
+
}
|
|
8284
|
+
}
|
|
8285
|
+
}
|
|
8286
|
+
if (data.dependencies && typeof data.dependencies === "object") {
|
|
8287
|
+
const walkDeps = (deps) => {
|
|
8288
|
+
for (const [name, info] of Object.entries(deps)) {
|
|
8289
|
+
if (!info || typeof info !== "object") continue;
|
|
8290
|
+
const depObj = info;
|
|
8291
|
+
add(name, depObj.version);
|
|
8292
|
+
if (depObj.dependencies && typeof depObj.dependencies === "object") {
|
|
8293
|
+
walkDeps(depObj.dependencies);
|
|
8294
|
+
}
|
|
8295
|
+
}
|
|
8296
|
+
};
|
|
8297
|
+
walkDeps(data.dependencies);
|
|
8298
|
+
}
|
|
8299
|
+
} catch {
|
|
8300
|
+
}
|
|
8301
|
+
} else if (filename.endsWith("pnpm-lock.yaml")) {
|
|
8302
|
+
try {
|
|
8303
|
+
const data = YAML2.parse(content);
|
|
8304
|
+
if (data?.packages && typeof data.packages === "object") {
|
|
8305
|
+
for (const [key, val] of Object.entries(data.packages)) {
|
|
8306
|
+
let rawKey = key.replace(/^\//, "");
|
|
8307
|
+
const parenIdx = rawKey.indexOf("(");
|
|
8308
|
+
if (parenIdx !== -1) {
|
|
8309
|
+
rawKey = rawKey.slice(0, parenIdx);
|
|
8310
|
+
}
|
|
8311
|
+
const lastAt = rawKey.lastIndexOf("@");
|
|
8312
|
+
if (lastAt > 0) {
|
|
8313
|
+
const name = val?.name ?? rawKey.slice(0, lastAt);
|
|
8314
|
+
const version = val?.version ?? rawKey.slice(lastAt + 1);
|
|
8315
|
+
add(name, version);
|
|
8316
|
+
}
|
|
8317
|
+
}
|
|
8318
|
+
}
|
|
8319
|
+
if (data?.snapshots && typeof data.snapshots === "object") {
|
|
8320
|
+
for (const key of Object.keys(data.snapshots)) {
|
|
8321
|
+
let rawKey = key.replace(/^\//, "");
|
|
8322
|
+
const parenIdx = rawKey.indexOf("(");
|
|
8323
|
+
if (parenIdx !== -1) {
|
|
8324
|
+
rawKey = rawKey.slice(0, parenIdx);
|
|
8325
|
+
}
|
|
8326
|
+
const lastAt = rawKey.lastIndexOf("@");
|
|
8327
|
+
if (lastAt > 0) {
|
|
8328
|
+
const name = rawKey.slice(0, lastAt);
|
|
8329
|
+
const version = rawKey.slice(lastAt + 1);
|
|
8330
|
+
add(name, version);
|
|
8331
|
+
}
|
|
8332
|
+
}
|
|
8333
|
+
}
|
|
8334
|
+
} catch {
|
|
8335
|
+
}
|
|
8336
|
+
} else if (filename.endsWith("yarn.lock")) {
|
|
8337
|
+
const lines = content.split("\n");
|
|
8338
|
+
let currentPackages = [];
|
|
8339
|
+
for (const line of lines) {
|
|
8340
|
+
if (line.startsWith("#") || line.trim() === "") continue;
|
|
8341
|
+
if (!line.startsWith(" ")) {
|
|
8342
|
+
currentPackages = [];
|
|
8343
|
+
const parts = line.split(",").map((p) => p.trim().replace(/^"/, "").replace(/":?$/, ""));
|
|
8344
|
+
for (const part of parts) {
|
|
8345
|
+
const lastAt = part.lastIndexOf("@");
|
|
8346
|
+
if (lastAt > 0) {
|
|
8347
|
+
currentPackages.push(part.slice(0, lastAt));
|
|
8348
|
+
}
|
|
8349
|
+
}
|
|
8350
|
+
} else {
|
|
8351
|
+
const match = line.match(/^\s+version\s+"?([^"\s]+)"?/);
|
|
8352
|
+
if (match && currentPackages.length > 0) {
|
|
8353
|
+
const ver = match[1];
|
|
8354
|
+
for (const pkg of currentPackages) {
|
|
8355
|
+
add(pkg, ver);
|
|
8356
|
+
}
|
|
8357
|
+
currentPackages = [];
|
|
8358
|
+
}
|
|
8359
|
+
}
|
|
8360
|
+
}
|
|
8361
|
+
}
|
|
8362
|
+
return result;
|
|
8363
|
+
}
|
|
8364
|
+
function computeLockfileDiff(filename, oldContent, newContent) {
|
|
8365
|
+
const changes = [];
|
|
8366
|
+
const oldDeps = oldContent ? parseLockfileDependencies(filename, oldContent) : /* @__PURE__ */ new Map();
|
|
8367
|
+
const newDeps = newContent ? parseLockfileDependencies(filename, newContent) : /* @__PURE__ */ new Map();
|
|
8368
|
+
const allPkgs = /* @__PURE__ */ new Set([...oldDeps.keys(), ...newDeps.keys()]);
|
|
8369
|
+
for (const pkg of allPkgs) {
|
|
8370
|
+
const oldVers = oldDeps.get(pkg) ?? /* @__PURE__ */ new Set();
|
|
8371
|
+
const newVers = newDeps.get(pkg) ?? /* @__PURE__ */ new Set();
|
|
8372
|
+
for (const nVer of newVers) {
|
|
8373
|
+
if (!oldVers.has(nVer)) {
|
|
8374
|
+
if (oldVers.size === 0) {
|
|
8375
|
+
changes.push({
|
|
8376
|
+
packageName: pkg,
|
|
8377
|
+
changeType: "added",
|
|
8378
|
+
newVersion: nVer,
|
|
8379
|
+
dependencyType: "transitive",
|
|
8380
|
+
manifestPath: filename
|
|
8381
|
+
});
|
|
8382
|
+
} else {
|
|
8383
|
+
const [firstOld] = oldVers;
|
|
8384
|
+
const cleanOld = semver8.clean(firstOld) || semver8.coerce(firstOld)?.version;
|
|
8385
|
+
const cleanNew = semver8.clean(nVer) || semver8.coerce(nVer)?.version;
|
|
8386
|
+
const isUpgrade = cleanOld && cleanNew && semver8.valid(cleanOld) && semver8.valid(cleanNew) ? semver8.gt(cleanNew, cleanOld) : true;
|
|
8387
|
+
changes.push({
|
|
8388
|
+
packageName: pkg,
|
|
8389
|
+
changeType: isUpgrade ? "upgraded" : "downgraded",
|
|
8390
|
+
oldVersion: firstOld,
|
|
8391
|
+
newVersion: nVer,
|
|
8392
|
+
dependencyType: "transitive",
|
|
8393
|
+
manifestPath: filename
|
|
8394
|
+
});
|
|
8395
|
+
}
|
|
8396
|
+
}
|
|
8397
|
+
}
|
|
8398
|
+
if (newVers.size === 0 && oldVers.size > 0) {
|
|
8399
|
+
for (const oVer of oldVers) {
|
|
8400
|
+
changes.push({
|
|
8401
|
+
packageName: pkg,
|
|
8402
|
+
changeType: "removed",
|
|
8403
|
+
oldVersion: oVer,
|
|
8404
|
+
dependencyType: "transitive",
|
|
8405
|
+
manifestPath: filename
|
|
8406
|
+
});
|
|
8407
|
+
}
|
|
8408
|
+
}
|
|
8409
|
+
}
|
|
8410
|
+
return changes;
|
|
8411
|
+
}
|
|
8412
|
+
|
|
8413
|
+
// src/scanner/delta-scan.ts
|
|
8414
|
+
var DEP_SECTIONS = [
|
|
8415
|
+
"dependencies",
|
|
8416
|
+
"devDependencies",
|
|
8417
|
+
"peerDependencies",
|
|
8418
|
+
"optionalDependencies"
|
|
8419
|
+
];
|
|
8420
|
+
async function getChangedManifestPaths(cwd, baseRef) {
|
|
8421
|
+
const manifests = /* @__PURE__ */ new Set();
|
|
8422
|
+
try {
|
|
8423
|
+
const { stdout: diffFiles } = await execa13(
|
|
8424
|
+
"git",
|
|
8425
|
+
["diff", "--name-only", baseRef, "--", "package.json", "**/package.json"],
|
|
8426
|
+
{ cwd, reject: false }
|
|
8427
|
+
);
|
|
8428
|
+
for (const f of diffFiles.split("\n")) {
|
|
8429
|
+
const trimmed = f.trim();
|
|
8430
|
+
if (trimmed && trimmed.endsWith("package.json") && !trimmed.includes("node_modules")) {
|
|
8431
|
+
manifests.add(trimmed);
|
|
8432
|
+
}
|
|
8433
|
+
}
|
|
8434
|
+
const { stdout: untrackedFiles } = await execa13(
|
|
8435
|
+
"git",
|
|
8436
|
+
["ls-files", "--others", "--exclude-standard", "--", "package.json", "**/package.json"],
|
|
8437
|
+
{ cwd, reject: false }
|
|
8438
|
+
);
|
|
8439
|
+
for (const f of untrackedFiles.split("\n")) {
|
|
8440
|
+
const trimmed = f.trim();
|
|
8441
|
+
if (trimmed && trimmed.endsWith("package.json") && !trimmed.includes("node_modules")) {
|
|
8442
|
+
manifests.add(trimmed);
|
|
8443
|
+
}
|
|
8444
|
+
}
|
|
8445
|
+
} catch {
|
|
8446
|
+
if (existsSync14(join22(cwd, "package.json"))) {
|
|
8447
|
+
manifests.add("package.json");
|
|
8448
|
+
}
|
|
8449
|
+
}
|
|
8450
|
+
return Array.from(manifests);
|
|
8451
|
+
}
|
|
8452
|
+
async function getOldManifestContent(cwd, baseRef, relPath) {
|
|
8453
|
+
try {
|
|
8454
|
+
const { stdout } = await execa13("git", ["show", `${baseRef}:${relPath}`], {
|
|
8455
|
+
cwd,
|
|
8456
|
+
reject: false
|
|
8457
|
+
});
|
|
8458
|
+
if (!stdout || !stdout.trim()) return null;
|
|
8459
|
+
return JSON.parse(stdout);
|
|
8460
|
+
} catch {
|
|
8461
|
+
return null;
|
|
8462
|
+
}
|
|
8463
|
+
}
|
|
8464
|
+
function getNewManifestContent(cwd, relPath) {
|
|
8465
|
+
const fullPath = join22(cwd, relPath);
|
|
8466
|
+
if (!existsSync14(fullPath)) return null;
|
|
8467
|
+
try {
|
|
8468
|
+
return JSON.parse(readFileSync19(fullPath, "utf8"));
|
|
8469
|
+
} catch {
|
|
8470
|
+
return null;
|
|
8471
|
+
}
|
|
8472
|
+
}
|
|
8473
|
+
function computeDependencyDiff(relPath, oldPkg, newPkg) {
|
|
8474
|
+
const changes = [];
|
|
8475
|
+
for (const section of DEP_SECTIONS) {
|
|
8476
|
+
const oldDeps = oldPkg?.[section] ?? {};
|
|
8477
|
+
const newDeps = newPkg?.[section] ?? {};
|
|
8478
|
+
for (const [pkgName, newVer] of Object.entries(newDeps)) {
|
|
8479
|
+
const oldVer = oldDeps[pkgName];
|
|
8480
|
+
if (!oldVer) {
|
|
8481
|
+
changes.push({
|
|
8482
|
+
packageName: pkgName,
|
|
8483
|
+
changeType: "added",
|
|
8484
|
+
newVersion: newVer,
|
|
8485
|
+
dependencyType: section,
|
|
8486
|
+
manifestPath: relPath
|
|
8487
|
+
});
|
|
8488
|
+
} else if (oldVer !== newVer) {
|
|
8489
|
+
const cleanOld = semver9.clean(oldVer) || semver9.coerce(oldVer)?.version;
|
|
8490
|
+
const cleanNew = semver9.clean(newVer) || semver9.coerce(newVer)?.version;
|
|
8491
|
+
const isUpgrade = cleanOld && cleanNew && semver9.valid(cleanOld) && semver9.valid(cleanNew) ? semver9.gt(cleanNew, cleanOld) : true;
|
|
8492
|
+
changes.push({
|
|
8493
|
+
packageName: pkgName,
|
|
8494
|
+
changeType: isUpgrade ? "upgraded" : "downgraded",
|
|
8495
|
+
oldVersion: oldVer,
|
|
8496
|
+
newVersion: newVer,
|
|
8497
|
+
dependencyType: section,
|
|
8498
|
+
manifestPath: relPath
|
|
8499
|
+
});
|
|
8500
|
+
}
|
|
8501
|
+
}
|
|
8502
|
+
for (const [pkgName, oldVer] of Object.entries(oldDeps)) {
|
|
8503
|
+
if (!(pkgName in newDeps)) {
|
|
8504
|
+
changes.push({
|
|
8505
|
+
packageName: pkgName,
|
|
8506
|
+
changeType: "removed",
|
|
8507
|
+
oldVersion: oldVer,
|
|
8508
|
+
dependencyType: section,
|
|
8509
|
+
manifestPath: relPath
|
|
8510
|
+
});
|
|
8511
|
+
}
|
|
8512
|
+
}
|
|
8513
|
+
}
|
|
8514
|
+
return changes;
|
|
8515
|
+
}
|
|
8516
|
+
var LOCKFILE_PATTERNS = [
|
|
8517
|
+
"package-lock.json",
|
|
8518
|
+
"**/package-lock.json",
|
|
8519
|
+
"pnpm-lock.yaml",
|
|
8520
|
+
"**/pnpm-lock.yaml",
|
|
8521
|
+
"yarn.lock",
|
|
8522
|
+
"**/yarn.lock"
|
|
8523
|
+
];
|
|
8524
|
+
async function getChangedLockfilePaths(cwd, baseRef) {
|
|
8525
|
+
const lockfiles = /* @__PURE__ */ new Set();
|
|
8526
|
+
try {
|
|
8527
|
+
const { stdout: diffFiles } = await execa13(
|
|
8528
|
+
"git",
|
|
8529
|
+
["diff", "--name-only", baseRef, "--", ...LOCKFILE_PATTERNS],
|
|
8530
|
+
{ cwd, reject: false }
|
|
8531
|
+
);
|
|
8532
|
+
for (const f of diffFiles.split("\n")) {
|
|
8533
|
+
const trimmed = f.trim();
|
|
8534
|
+
if (trimmed && !trimmed.includes("node_modules") && (trimmed.endsWith("package-lock.json") || trimmed.endsWith("pnpm-lock.yaml") || trimmed.endsWith("yarn.lock"))) {
|
|
8535
|
+
lockfiles.add(trimmed);
|
|
8536
|
+
}
|
|
8537
|
+
}
|
|
8538
|
+
const { stdout: untrackedFiles } = await execa13(
|
|
8539
|
+
"git",
|
|
8540
|
+
["ls-files", "--others", "--exclude-standard", "--", ...LOCKFILE_PATTERNS],
|
|
8541
|
+
{ cwd, reject: false }
|
|
8542
|
+
);
|
|
8543
|
+
for (const f of untrackedFiles.split("\n")) {
|
|
8544
|
+
const trimmed = f.trim();
|
|
8545
|
+
if (trimmed && !trimmed.includes("node_modules") && (trimmed.endsWith("package-lock.json") || trimmed.endsWith("pnpm-lock.yaml") || trimmed.endsWith("yarn.lock"))) {
|
|
8546
|
+
lockfiles.add(trimmed);
|
|
8547
|
+
}
|
|
8548
|
+
}
|
|
8549
|
+
} catch {
|
|
8550
|
+
}
|
|
8551
|
+
return Array.from(lockfiles);
|
|
8552
|
+
}
|
|
8553
|
+
async function getFileContentAtRef(cwd, baseRef, relPath) {
|
|
8554
|
+
try {
|
|
8555
|
+
const { stdout } = await execa13("git", ["show", `${baseRef}:${relPath}`], {
|
|
8556
|
+
cwd,
|
|
8557
|
+
reject: false
|
|
8558
|
+
});
|
|
8559
|
+
return stdout || null;
|
|
8560
|
+
} catch {
|
|
8561
|
+
return null;
|
|
8562
|
+
}
|
|
8563
|
+
}
|
|
8564
|
+
async function scanDelta(options = {}) {
|
|
8565
|
+
const cwd = options.cwd ?? process.cwd();
|
|
8566
|
+
const baseRef = options.baseRef ?? "HEAD";
|
|
8567
|
+
const changedManifests = await getChangedManifestPaths(cwd, baseRef);
|
|
8568
|
+
const changedLockfiles = await getChangedLockfilePaths(cwd, baseRef);
|
|
8569
|
+
const dependencyChanges = [];
|
|
8570
|
+
const directPkgs = /* @__PURE__ */ new Set();
|
|
8571
|
+
for (const manifestRel of changedManifests) {
|
|
8572
|
+
const oldPkg = await getOldManifestContent(cwd, baseRef, manifestRel);
|
|
8573
|
+
const newPkg = getNewManifestContent(cwd, manifestRel);
|
|
8574
|
+
const diffs = computeDependencyDiff(manifestRel, oldPkg, newPkg);
|
|
8575
|
+
for (const d of diffs) {
|
|
8576
|
+
directPkgs.add(d.packageName);
|
|
8577
|
+
}
|
|
8578
|
+
dependencyChanges.push(...diffs);
|
|
8579
|
+
}
|
|
8580
|
+
for (const lockfileRel of changedLockfiles) {
|
|
8581
|
+
const oldContent = await getFileContentAtRef(cwd, baseRef, lockfileRel);
|
|
8582
|
+
const fullPath = join22(cwd, lockfileRel);
|
|
8583
|
+
const newContent = existsSync14(fullPath) ? readFileSync19(fullPath, "utf8") : null;
|
|
8584
|
+
const lockDiffs = computeLockfileDiff(lockfileRel, oldContent, newContent);
|
|
8585
|
+
for (const change of lockDiffs) {
|
|
8586
|
+
if (!directPkgs.has(change.packageName)) {
|
|
8587
|
+
dependencyChanges.push(change);
|
|
8588
|
+
}
|
|
8589
|
+
}
|
|
8590
|
+
if (!changedManifests.includes(lockfileRel)) {
|
|
8591
|
+
changedManifests.push(lockfileRel);
|
|
8592
|
+
}
|
|
8593
|
+
}
|
|
8594
|
+
const introducedFindings = [];
|
|
8595
|
+
const resolvedFindings = [];
|
|
8596
|
+
for (const change of dependencyChanges) {
|
|
8597
|
+
if (change.changeType === "added" || change.changeType === "upgraded" || change.changeType === "downgraded") {
|
|
8598
|
+
const cleanVer = semver9.coerce(change.newVersion)?.version ?? change.newVersion;
|
|
8599
|
+
const evaluation = await evaluatePackage(change.packageName, {
|
|
8600
|
+
version: cleanVer,
|
|
8601
|
+
enrichIntelligence: true
|
|
8602
|
+
});
|
|
8603
|
+
for (const vuln of evaluation.vulnerabilities) {
|
|
8604
|
+
introducedFindings.push({
|
|
8605
|
+
packageName: change.packageName,
|
|
8606
|
+
cveId: vuln.cveId,
|
|
8607
|
+
severity: vuln.severity,
|
|
8608
|
+
summary: vuln.summary,
|
|
8609
|
+
deltaType: "introduced",
|
|
8610
|
+
installedVersion: change.newVersion,
|
|
8611
|
+
safeUpgradeVersion: vuln.safeUpgradeVersion,
|
|
8612
|
+
inCisaKev: vuln.inCisaKev,
|
|
8613
|
+
epssScore: vuln.epssScore
|
|
8614
|
+
});
|
|
8615
|
+
}
|
|
8616
|
+
}
|
|
8617
|
+
if (change.oldVersion && (change.changeType === "upgraded" || change.changeType === "removed")) {
|
|
8618
|
+
const cleanOldVer = semver9.coerce(change.oldVersion)?.version ?? change.oldVersion;
|
|
8619
|
+
const oldEval = await evaluatePackage(change.packageName, {
|
|
8620
|
+
version: cleanOldVer,
|
|
8621
|
+
enrichIntelligence: true
|
|
8622
|
+
});
|
|
8623
|
+
const cleanNewVer = change.newVersion ? semver9.coerce(change.newVersion)?.version ?? change.newVersion : void 0;
|
|
8624
|
+
const newEval = cleanNewVer ? await evaluatePackage(change.packageName, {
|
|
8625
|
+
version: cleanNewVer,
|
|
8626
|
+
enrichIntelligence: false
|
|
8627
|
+
}) : { vulnerabilities: [] };
|
|
8628
|
+
const newCveSet = new Set(newEval.vulnerabilities.map((v) => v.cveId));
|
|
8629
|
+
for (const oldVuln of oldEval.vulnerabilities) {
|
|
8630
|
+
if (!newCveSet.has(oldVuln.cveId)) {
|
|
8631
|
+
resolvedFindings.push({
|
|
8632
|
+
packageName: change.packageName,
|
|
8633
|
+
cveId: oldVuln.cveId,
|
|
8634
|
+
severity: oldVuln.severity,
|
|
8635
|
+
summary: oldVuln.summary,
|
|
8636
|
+
deltaType: "resolved",
|
|
8637
|
+
installedVersion: change.oldVersion,
|
|
8638
|
+
safeUpgradeVersion: oldVuln.safeUpgradeVersion,
|
|
8639
|
+
inCisaKev: oldVuln.inCisaKev,
|
|
8640
|
+
epssScore: oldVuln.epssScore
|
|
8641
|
+
});
|
|
8642
|
+
}
|
|
8643
|
+
}
|
|
8644
|
+
}
|
|
8645
|
+
}
|
|
8646
|
+
let netRiskVerdict = "neutral";
|
|
8647
|
+
let status = "ok";
|
|
8648
|
+
if (introducedFindings.length > 0) {
|
|
8649
|
+
netRiskVerdict = "degraded";
|
|
8650
|
+
status = "degraded";
|
|
8651
|
+
} else if (resolvedFindings.length > 0) {
|
|
8652
|
+
netRiskVerdict = "improved";
|
|
8653
|
+
status = "improved";
|
|
8654
|
+
}
|
|
8655
|
+
const summary = introducedFindings.length > 0 ? `Delta scan against ${baseRef} found ${introducedFindings.length} newly introduced vulnerability(ies) across ${dependencyChanges.length} dependency change(s) (net risk: degraded).` : resolvedFindings.length > 0 ? `Delta scan against ${baseRef} resolved ${resolvedFindings.length} vulnerability(ies) across ${dependencyChanges.length} dependency change(s) (net risk: improved).` : `Delta scan against ${baseRef} found 0 new vulnerabilities across ${dependencyChanges.length} dependency change(s) (net risk: neutral).`;
|
|
8656
|
+
return {
|
|
8657
|
+
schemaVersion: "1.0",
|
|
8658
|
+
status,
|
|
8659
|
+
baseRef,
|
|
8660
|
+
targetRef: "working-tree",
|
|
8661
|
+
changedManifests,
|
|
8662
|
+
dependencyChanges,
|
|
8663
|
+
introducedFindings,
|
|
8664
|
+
resolvedFindings,
|
|
8665
|
+
netRiskVerdict,
|
|
8666
|
+
summary,
|
|
8667
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8668
|
+
};
|
|
8669
|
+
}
|
|
8670
|
+
|
|
7431
8671
|
// src/api/jobs/index.ts
|
|
7432
8672
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
7433
8673
|
|
|
@@ -7543,9 +8783,9 @@ function toSarifOutput(report) {
|
|
|
7543
8783
|
}
|
|
7544
8784
|
|
|
7545
8785
|
// src/version.ts
|
|
7546
|
-
import { readFileSync as
|
|
8786
|
+
import { readFileSync as readFileSync20 } from "fs";
|
|
7547
8787
|
function readPackageVersion() {
|
|
7548
|
-
const raw =
|
|
8788
|
+
const raw = readFileSync20(new URL("../package.json", import.meta.url), "utf8");
|
|
7549
8789
|
const metadata = JSON.parse(raw);
|
|
7550
8790
|
if (!metadata.version) {
|
|
7551
8791
|
throw new Error("packages/core/package.json is missing a version field.");
|
|
@@ -7570,11 +8810,18 @@ export {
|
|
|
7570
8810
|
listPatchArtifacts,
|
|
7571
8811
|
inspectPatchArtifact,
|
|
7572
8812
|
validatePatchArtifact,
|
|
8813
|
+
evaluatePackage,
|
|
8814
|
+
reconcilePatchArtifacts,
|
|
7573
8815
|
remediate,
|
|
7574
8816
|
planRemediation,
|
|
8817
|
+
parseScanInput,
|
|
8818
|
+
parseScanInputFromAudit,
|
|
8819
|
+
uniqueCveIds,
|
|
7575
8820
|
remediateFromScan,
|
|
7576
8821
|
remediatePortfolio,
|
|
7577
8822
|
updateOutdated,
|
|
8823
|
+
checkReachability,
|
|
8824
|
+
scanDelta,
|
|
7578
8825
|
submitRemediateJob,
|
|
7579
8826
|
submitScanJob,
|
|
7580
8827
|
submitPortfolioJob,
|