autoremediator 0.14.0 → 0.14.1

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.
@@ -613,13 +613,21 @@ function validatePatchDiff(patchContent) {
613
613
  // src/api/patches/helpers.ts
614
614
  import { existsSync as existsSync3 } from "fs";
615
615
  import { readFile } from "fs/promises";
616
- import { isAbsolute, resolve } from "path";
616
+ import { isAbsolute, resolve, sep } from "path";
617
617
  var DEFAULT_PATCHES_DIR = "./patches";
618
618
  function resolvePatchesDir(cwd, patchesDir = DEFAULT_PATCHES_DIR) {
619
619
  return isAbsolute(patchesDir) ? patchesDir : resolve(cwd, patchesDir);
620
620
  }
621
621
  function resolveArtifactPath(cwd, patchFilePath) {
622
- return isAbsolute(patchFilePath) ? patchFilePath : resolve(cwd, patchFilePath);
622
+ const resolved = isAbsolute(patchFilePath) ? patchFilePath : resolve(cwd, patchFilePath);
623
+ if (!resolved.endsWith(".patch")) {
624
+ throw new Error(`patchFilePath must point to a .patch file: ${patchFilePath}`);
625
+ }
626
+ const patchesRoot = resolvePatchesDir(cwd);
627
+ if (!resolved.startsWith(patchesRoot + sep)) {
628
+ throw new Error(`patchFilePath must be inside the patches directory: ${patchFilePath}`);
629
+ }
630
+ return resolved;
623
631
  }
624
632
  async function readManifest(manifestFilePath) {
625
633
  if (!existsSync3(manifestFilePath)) {
@@ -627,7 +635,11 @@ async function readManifest(manifestFilePath) {
627
635
  }
628
636
  try {
629
637
  const raw = await readFile(manifestFilePath, "utf8");
630
- return JSON.parse(raw);
638
+ const parsed = JSON.parse(raw);
639
+ if (parsed === null || typeof parsed !== "object" || parsed.schemaVersion !== "1.0" || typeof parsed.packageName !== "string" || typeof parsed.vulnerableVersion !== "string" || typeof parsed.patchFilePath !== "string" || typeof parsed.patchFileName !== "string" || typeof parsed.applied !== "boolean" || typeof parsed.dryRun !== "boolean" || typeof parsed.generatedAt !== "string") {
640
+ return void 0;
641
+ }
642
+ return parsed;
631
643
  } catch {
632
644
  return void 0;
633
645
  }
@@ -1062,6 +1074,11 @@ async function loadRemoteFactory() {
1062
1074
  "AUTOREMEDIATOR_REMOTE_CLIENT_MODULE is required for remote provider model loading."
1063
1075
  );
1064
1076
  }
1077
+ if (moduleName.startsWith("./") || moduleName.startsWith("../") || moduleName.startsWith("/") || moduleName.startsWith("file:")) {
1078
+ throw new Error(
1079
+ `AUTOREMEDIATOR_REMOTE_CLIENT_MODULE must be a package name, not a file path: ${moduleName}`
1080
+ );
1081
+ }
1065
1082
  const loaded = await import(moduleName);
1066
1083
  const factory = loaded[exportName];
1067
1084
  if (typeof factory !== "function") {
@@ -1223,7 +1240,15 @@ async function httpClient(request) {
1223
1240
  }
1224
1241
  try {
1225
1242
  const res = await requestWithTimeout(url, init, timeout);
1243
+ const MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
1244
+ const contentLength = res.headers?.get?.("content-length") ?? null;
1245
+ if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_BYTES) {
1246
+ throw new HttpError(`Response too large (content-length: ${contentLength})`, "HTTP_ERROR");
1247
+ }
1226
1248
  const text = await res.text();
1249
+ if (Buffer.byteLength(text) > MAX_RESPONSE_BYTES) {
1250
+ throw new HttpError("Response body exceeds 10 MB limit", "HTTP_ERROR");
1251
+ }
1227
1252
  let data;
1228
1253
  try {
1229
1254
  data = text ? JSON.parse(text) : {};
@@ -1862,7 +1887,7 @@ import semver2 from "semver";
1862
1887
  import { mkdir, rm } from "fs/promises";
1863
1888
  import { join as join10 } from "path";
1864
1889
  async function sleep(ms) {
1865
- await new Promise((resolve2) => setTimeout(resolve2, ms));
1890
+ await new Promise((resolve3) => setTimeout(resolve3, ms));
1866
1891
  }
1867
1892
  async function acquireRepoLock(cwd, options = {}) {
1868
1893
  const timeoutMs = options.timeoutMs ?? 15e3;
@@ -2621,8 +2646,9 @@ async function resolvePrimaryResult(params) {
2621
2646
 
2622
2647
  // src/remediation/tools/fetch-package-source.ts
2623
2648
  import { z as z6 } from "zod";
2624
- import { mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rm as rm2 } from "fs/promises";
2649
+ import { mkdir as mkdir2, mkdtemp, readdir as readdir2, readFile as readFile3, rm as rm2 } from "fs/promises";
2625
2650
  import { join as join13 } from "path";
2651
+ import { tmpdir } from "os";
2626
2652
  import { execa as execa8 } from "execa";
2627
2653
  var fetchPackageSourceTool = defineTool({
2628
2654
  description: "Download package tarball from npm and extract source files for CVE analysis. Supports custom file patterns (default: *.js, *.ts).",
@@ -2638,10 +2664,20 @@ var fetchPackageSourceTool = defineTool({
2638
2664
  version,
2639
2665
  filePatterns
2640
2666
  }) => {
2641
- const tempBaseDir = `/tmp/autoremediator-pkg-${Date.now()}`;
2667
+ if (!/^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i.test(packageName)) {
2668
+ return { success: false, error: `Invalid package name: ${packageName}` };
2669
+ }
2670
+ const safePatterns = (filePatterns ?? ["*.js", "*.ts"]).filter(
2671
+ (p) => /^[a-zA-Z0-9._/*?-]+$/.test(p)
2672
+ );
2673
+ if (safePatterns.length === 0) {
2674
+ return { success: false, error: "No valid file patterns provided." };
2675
+ }
2676
+ const tempBaseDir = await mkdtemp(join13(tmpdir(), "autoremediator-pkg-"));
2642
2677
  const extractDir = join13(tempBaseDir, "out");
2643
2678
  try {
2644
- const npmUrl = `https://registry.npmjs.org/${packageName}/-/${packageName.split("/").pop()}-${version}.tgz`;
2679
+ const scopedName = packageName.split("/").pop();
2680
+ const npmUrl = `https://registry.npmjs.org/${packageName}/-/${scopedName}-${version}.tgz`;
2645
2681
  await mkdir2(tempBaseDir, { recursive: true });
2646
2682
  const tarballPath = join13(tempBaseDir, "package.tgz");
2647
2683
  await execa8("curl", ["-L", "-o", tarballPath, npmUrl]);
@@ -2669,7 +2705,7 @@ var fetchPackageSourceTool = defineTool({
2669
2705
  await walkDir(fullPath, relPath);
2670
2706
  }
2671
2707
  } else if (file.isFile()) {
2672
- const matches = filePatterns.some((pattern) => {
2708
+ const matches = safePatterns.some((pattern) => {
2673
2709
  const regex = new RegExp(
2674
2710
  `^${pattern.replace(/\*/g, ".*").replace(/\./g, "\\.")}$`
2675
2711
  );
@@ -2691,7 +2727,7 @@ var fetchPackageSourceTool = defineTool({
2691
2727
  if (Object.keys(sourceCode).length === 0) {
2692
2728
  return {
2693
2729
  success: false,
2694
- error: `No source files matching patterns [${filePatterns.join(", ")}] found in ${packageName}@${version}. Download succeeded but extraction yielded no matching files.`
2730
+ error: `No source files matching patterns [${safePatterns.join(", ")}] found in ${packageName}@${version}. Download succeeded but extraction yielded no matching files.`
2695
2731
  };
2696
2732
  }
2697
2733
  return {
@@ -3032,9 +3068,9 @@ import { execa as execa10 } from "execa";
3032
3068
 
3033
3069
  // src/remediation/tools/apply-patch-file/helpers.ts
3034
3070
  import { existsSync as existsSync8 } from "fs";
3035
- import { mkdtemp, readFile as readFile4, rm as rm3, writeFile } from "fs/promises";
3071
+ import { mkdtemp as mkdtemp2, readFile as readFile4, rm as rm3, writeFile } from "fs/promises";
3036
3072
  import { createHash } from "crypto";
3037
- import { tmpdir } from "os";
3073
+ import { tmpdir as tmpdir2 } from "os";
3038
3074
  import { join as join14 } from "path";
3039
3075
  import { execa as execa9 } from "execa";
3040
3076
  async function resolvePatchMode(packageManager, cwd) {
@@ -3179,7 +3215,7 @@ ${createResult.stderr}`);
3179
3215
  error: `Could not determine native patch directory for ${packageSpec}.`
3180
3216
  };
3181
3217
  }
3182
- const tempPatchDir = await mkdtemp(join14(tmpdir(), "autoremediator-native-patch-"));
3218
+ const tempPatchDir = await mkdtemp2(join14(tmpdir2(), "autoremediator-native-patch-"));
3183
3219
  const tempPatchFile = join14(tempPatchDir, "change.patch");
3184
3220
  try {
3185
3221
  await writeFile(tempPatchFile, patchContent, "utf8");
@@ -4661,6 +4697,13 @@ async function enrichWithOssfScorecard(details) {
4661
4697
  async function probeFeed(url, cveId, token) {
4662
4698
  try {
4663
4699
  const feedUrl = new URL(url);
4700
+ const hostname = feedUrl.hostname.toLowerCase();
4701
+ if (hostname === "localhost" || /^127\./.test(hostname) || /^10\./.test(hostname) || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname) || /^192\.168\./.test(hostname) || hostname === "0.0.0.0" || /^169\.254\./.test(hostname) || hostname === "[::1]" || hostname === "::1") {
4702
+ return void 0;
4703
+ }
4704
+ if (feedUrl.protocol !== "https:" && feedUrl.protocol !== "http:") {
4705
+ return void 0;
4706
+ }
4664
4707
  feedUrl.searchParams.set("cve", cveId);
4665
4708
  const headers = {};
4666
4709
  if (token) headers.Authorization = `Bearer ${token}`;
@@ -5091,6 +5134,7 @@ async function runRemediationPipeline(cveId, options = {}) {
5091
5134
 
5092
5135
  // src/api/change-request/index.ts
5093
5136
  import { execa as execa11 } from "execa";
5137
+ import { randomBytes } from "crypto";
5094
5138
  function sanitizeBranchToken(value) {
5095
5139
  return value.toLowerCase().replace(/[^a-z0-9._/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
5096
5140
  }
@@ -5143,7 +5187,7 @@ async function ensureRepoHasChanges(cwd) {
5143
5187
  }
5144
5188
  function toBranchName(prefix, cveIds) {
5145
5189
  const token = sanitizeBranchToken(cveIds.join("-") || "remediation");
5146
- return `${sanitizeBranchToken(prefix)}/${token}-${Date.now()}`;
5190
+ return `${sanitizeBranchToken(prefix)}/${token}-${randomBytes(6).toString("hex")}`;
5147
5191
  }
5148
5192
  async function createGitHubRequest(params) {
5149
5193
  const args = ["pr", "create", "--head", params.head, "--base", params.base, "--title", params.title, "--body", params.body];
@@ -5189,11 +5233,12 @@ async function createChangeRequestsForReports(params) {
5189
5233
  const provider = resolveProvider2(options);
5190
5234
  const grouping = resolveGrouping(options);
5191
5235
  const plan = buildPlan(reports);
5192
- const titlePrefix = options.titlePrefix?.trim();
5236
+ const titlePrefix = options.titlePrefix?.trim().slice(0, 200);
5237
+ const bodyFooter = options.bodyFooter ? options.bodyFooter.slice(0, 2e3) : void 0;
5193
5238
  const title = titlePrefix ? `${titlePrefix} ${plan.title}` : plan.title;
5194
- const body = options.bodyFooter ? `${plan.body}
5239
+ const body = bodyFooter ? `${plan.body}
5195
5240
 
5196
- ${options.bodyFooter}` : plan.body;
5241
+ ${bodyFooter}` : plan.body;
5197
5242
  try {
5198
5243
  const hasChanges = await ensureRepoHasChanges(cwd);
5199
5244
  if (!hasChanges) {
@@ -5216,6 +5261,12 @@ ${options.bodyFooter}` : plan.body;
5216
5261
  const branchPrefix = options.branchPrefix ?? "autoremediator";
5217
5262
  const branchName = toBranchName(branchPrefix, plan.cveIds);
5218
5263
  const pushRemote = options.pushRemote ?? "origin";
5264
+ if (options.pushRemote && !/^[a-zA-Z0-9._-]+$/.test(options.pushRemote)) {
5265
+ throw new Error(`Invalid pushRemote value: ${options.pushRemote}`);
5266
+ }
5267
+ if (options.repository && !/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(options.repository)) {
5268
+ throw new Error(`Invalid repository format: ${options.repository}`);
5269
+ }
5219
5270
  await execa11("git", ["checkout", "-b", branchName], { cwd, stdio: "pipe" });
5220
5271
  await execa11("git", ["add", "-A"], { cwd, stdio: "pipe" });
5221
5272
  await execa11("git", ["commit", "-m", title], { cwd, stdio: "pipe" });
@@ -5903,13 +5954,13 @@ async function planRemediation(cveId, options = {}) {
5903
5954
  }
5904
5955
 
5905
5956
  // src/scanner/parse-input.ts
5906
- import { extname as extname2 } from "path";
5957
+ import { extname as extname2, resolve as resolve2 } from "path";
5907
5958
  import { readFileSync as readFileSync14 } from "fs";
5908
5959
  import { execa as execa12 } from "execa";
5909
5960
 
5910
5961
  // src/scanner/adapters/npm-audit.ts
5911
5962
  import { readFileSync as readFileSync11 } from "fs";
5912
- var CVE_REGEX = /CVE-\d{4}-\d+/gi;
5963
+ var CVE_REGEX = /CVE-\d{4}-\d{1,7}/gi;
5913
5964
  function normalizeSeverity(raw) {
5914
5965
  if (!raw) return "UNKNOWN";
5915
5966
  const up = raw.toUpperCase();
@@ -5948,7 +5999,7 @@ function parseNpmAuditJsonFile(filePath) {
5948
5999
 
5949
6000
  // src/scanner/adapters/yarn-audit.ts
5950
6001
  import { readFileSync as readFileSync12 } from "fs";
5951
- var CVE_REGEX2 = /CVE-\d{4}-\d+/gi;
6002
+ var CVE_REGEX2 = /CVE-\d{4}-\d{1,7}/gi;
5952
6003
  function normalizeSeverity2(raw) {
5953
6004
  if (!raw) return "UNKNOWN";
5954
6005
  const up = raw.toUpperCase();
@@ -5996,7 +6047,7 @@ function parseYarnAuditJsonFile(filePath) {
5996
6047
 
5997
6048
  // src/scanner/adapters/sarif.ts
5998
6049
  import { readFileSync as readFileSync13 } from "fs";
5999
- var CVE_REGEX3 = /CVE-\d{4}-\d+/gi;
6050
+ var CVE_REGEX3 = /CVE-\d{4}-\d{1,7}/gi;
6000
6051
  function extractPackageName(result) {
6001
6052
  const pkg = result.properties?.["packageName"];
6002
6053
  return typeof pkg === "string" ? pkg : void 0;
@@ -6005,9 +6056,15 @@ function parseSarifFromString(content) {
6005
6056
  const report = JSON.parse(content);
6006
6057
  const findings = [];
6007
6058
  const seen = /* @__PURE__ */ new Set();
6008
- for (const run of report.runs ?? []) {
6059
+ const MAX_RUNS = 100;
6060
+ const MAX_TOTAL_RESULTS = 1e4;
6061
+ let totalResults = 0;
6062
+ for (const run of (report.runs ?? []).slice(0, MAX_RUNS)) {
6009
6063
  for (const result of run.results ?? []) {
6010
- const combined = `${result.ruleId ?? ""} ${result.message?.text ?? ""}`;
6064
+ if (totalResults++ >= MAX_TOTAL_RESULTS) break;
6065
+ const ruleId = (result.ruleId ?? "").slice(0, 1024);
6066
+ const messageText = (result.message?.text ?? "").slice(0, 4096);
6067
+ const combined = `${ruleId} ${messageText}`;
6011
6068
  const matches = combined.match(CVE_REGEX3) ?? [];
6012
6069
  for (const match of matches) {
6013
6070
  const cveId = match.toUpperCase();
@@ -6033,15 +6090,19 @@ function parseSarifFile(filePath) {
6033
6090
 
6034
6091
  // src/scanner/parse-input.ts
6035
6092
  function parseScanInput(filePath, format) {
6036
- const resolved = format === "auto" ? inferFormat(filePath) : format;
6093
+ if (filePath.includes("\0")) {
6094
+ throw new Error("Invalid scan input path: path contains null bytes");
6095
+ }
6096
+ const resolvedPath = resolve2(filePath);
6097
+ const resolved = format === "auto" ? inferFormat(resolvedPath) : format;
6037
6098
  if (resolved === "npm-audit") {
6038
- return parseNpmAuditJsonFile(filePath);
6099
+ return parseNpmAuditJsonFile(resolvedPath);
6039
6100
  }
6040
6101
  if (resolved === "yarn-audit") {
6041
- return parseYarnAuditJsonFile(filePath);
6102
+ return parseYarnAuditJsonFile(resolvedPath);
6042
6103
  }
6043
6104
  if (resolved === "sarif") {
6044
- return parseSarifFile(filePath);
6105
+ return parseSarifFile(resolvedPath);
6045
6106
  }
6046
6107
  throw new Error(`Unsupported input format: ${resolved}`);
6047
6108
  }
@@ -6840,4 +6901,3 @@ export {
6840
6901
  updateOutdated,
6841
6902
  PACKAGE_VERSION
6842
6903
  };
6843
- //# sourceMappingURL=chunk-3NNNFJLV.js.map
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  toSarifOutput,
13
13
  updateOutdated,
14
14
  validatePatchArtifact
15
- } from "./chunk-3NNNFJLV.js";
15
+ } from "./chunk-IXCNMTOO.js";
16
16
 
17
17
  // src/cli/index.ts
18
18
  import { fileURLToPath } from "url";
@@ -507,7 +507,7 @@ async function runPortfolio(targetsFilePath, opts) {
507
507
 
508
508
  // src/cli/types.ts
509
509
  function isCveId(value) {
510
- return /^CVE-\d{4}-\d+$/i.test(value);
510
+ return /^CVE-\d{4}-\d{1,7}$/i.test(value);
511
511
  }
512
512
 
513
513
  // src/cli/validators.ts
@@ -727,4 +727,3 @@ if (isMainModule()) {
727
727
  export {
728
728
  createProgram2 as createProgram
729
729
  };
730
- //# sourceMappingURL=cli.js.map
package/dist/index.js CHANGED
@@ -1007,13 +1007,21 @@ function validatePatchDiff(patchContent) {
1007
1007
  // src/api/patches/helpers.ts
1008
1008
  import { existsSync as existsSync3 } from "fs";
1009
1009
  import { readFile } from "fs/promises";
1010
- import { isAbsolute, resolve } from "path";
1010
+ import { isAbsolute, resolve, sep } from "path";
1011
1011
  var DEFAULT_PATCHES_DIR = "./patches";
1012
1012
  function resolvePatchesDir(cwd, patchesDir = DEFAULT_PATCHES_DIR) {
1013
1013
  return isAbsolute(patchesDir) ? patchesDir : resolve(cwd, patchesDir);
1014
1014
  }
1015
1015
  function resolveArtifactPath(cwd, patchFilePath) {
1016
- return isAbsolute(patchFilePath) ? patchFilePath : resolve(cwd, patchFilePath);
1016
+ const resolved = isAbsolute(patchFilePath) ? patchFilePath : resolve(cwd, patchFilePath);
1017
+ if (!resolved.endsWith(".patch")) {
1018
+ throw new Error(`patchFilePath must point to a .patch file: ${patchFilePath}`);
1019
+ }
1020
+ const patchesRoot = resolvePatchesDir(cwd);
1021
+ if (!resolved.startsWith(patchesRoot + sep)) {
1022
+ throw new Error(`patchFilePath must be inside the patches directory: ${patchFilePath}`);
1023
+ }
1024
+ return resolved;
1017
1025
  }
1018
1026
  async function readManifest(manifestFilePath) {
1019
1027
  if (!existsSync3(manifestFilePath)) {
@@ -1021,7 +1029,11 @@ async function readManifest(manifestFilePath) {
1021
1029
  }
1022
1030
  try {
1023
1031
  const raw = await readFile(manifestFilePath, "utf8");
1024
- return JSON.parse(raw);
1032
+ const parsed = JSON.parse(raw);
1033
+ if (parsed === null || typeof parsed !== "object" || parsed.schemaVersion !== "1.0" || typeof parsed.packageName !== "string" || typeof parsed.vulnerableVersion !== "string" || typeof parsed.patchFilePath !== "string" || typeof parsed.patchFileName !== "string" || typeof parsed.applied !== "boolean" || typeof parsed.dryRun !== "boolean" || typeof parsed.generatedAt !== "string") {
1034
+ return void 0;
1035
+ }
1036
+ return parsed;
1025
1037
  } catch {
1026
1038
  return void 0;
1027
1039
  }
@@ -1456,6 +1468,11 @@ async function loadRemoteFactory() {
1456
1468
  "AUTOREMEDIATOR_REMOTE_CLIENT_MODULE is required for remote provider model loading."
1457
1469
  );
1458
1470
  }
1471
+ if (moduleName.startsWith("./") || moduleName.startsWith("../") || moduleName.startsWith("/") || moduleName.startsWith("file:")) {
1472
+ throw new Error(
1473
+ `AUTOREMEDIATOR_REMOTE_CLIENT_MODULE must be a package name, not a file path: ${moduleName}`
1474
+ );
1475
+ }
1459
1476
  const loaded = await import(moduleName);
1460
1477
  const factory = loaded[exportName];
1461
1478
  if (typeof factory !== "function") {
@@ -1617,7 +1634,15 @@ async function httpClient(request) {
1617
1634
  }
1618
1635
  try {
1619
1636
  const res = await requestWithTimeout(url, init, timeout);
1637
+ const MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
1638
+ const contentLength = res.headers?.get?.("content-length") ?? null;
1639
+ if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_BYTES) {
1640
+ throw new HttpError(`Response too large (content-length: ${contentLength})`, "HTTP_ERROR");
1641
+ }
1620
1642
  const text = await res.text();
1643
+ if (Buffer.byteLength(text) > MAX_RESPONSE_BYTES) {
1644
+ throw new HttpError("Response body exceeds 10 MB limit", "HTTP_ERROR");
1645
+ }
1621
1646
  let data;
1622
1647
  try {
1623
1648
  data = text ? JSON.parse(text) : {};
@@ -2256,7 +2281,7 @@ import semver2 from "semver";
2256
2281
  import { mkdir, rm } from "fs/promises";
2257
2282
  import { join as join10 } from "path";
2258
2283
  async function sleep(ms) {
2259
- await new Promise((resolve2) => setTimeout(resolve2, ms));
2284
+ await new Promise((resolve3) => setTimeout(resolve3, ms));
2260
2285
  }
2261
2286
  async function acquireRepoLock(cwd, options = {}) {
2262
2287
  const timeoutMs = options.timeoutMs ?? 15e3;
@@ -3015,8 +3040,9 @@ async function resolvePrimaryResult(params) {
3015
3040
 
3016
3041
  // src/remediation/tools/fetch-package-source.ts
3017
3042
  import { z as z6 } from "zod";
3018
- import { mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rm as rm2 } from "fs/promises";
3043
+ import { mkdir as mkdir2, mkdtemp, readdir as readdir2, readFile as readFile3, rm as rm2 } from "fs/promises";
3019
3044
  import { join as join13 } from "path";
3045
+ import { tmpdir } from "os";
3020
3046
  import { execa as execa8 } from "execa";
3021
3047
  var fetchPackageSourceTool = defineTool({
3022
3048
  description: "Download package tarball from npm and extract source files for CVE analysis. Supports custom file patterns (default: *.js, *.ts).",
@@ -3032,10 +3058,20 @@ var fetchPackageSourceTool = defineTool({
3032
3058
  version,
3033
3059
  filePatterns
3034
3060
  }) => {
3035
- const tempBaseDir = `/tmp/autoremediator-pkg-${Date.now()}`;
3061
+ if (!/^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i.test(packageName)) {
3062
+ return { success: false, error: `Invalid package name: ${packageName}` };
3063
+ }
3064
+ const safePatterns = (filePatterns ?? ["*.js", "*.ts"]).filter(
3065
+ (p) => /^[a-zA-Z0-9._/*?-]+$/.test(p)
3066
+ );
3067
+ if (safePatterns.length === 0) {
3068
+ return { success: false, error: "No valid file patterns provided." };
3069
+ }
3070
+ const tempBaseDir = await mkdtemp(join13(tmpdir(), "autoremediator-pkg-"));
3036
3071
  const extractDir = join13(tempBaseDir, "out");
3037
3072
  try {
3038
- const npmUrl = `https://registry.npmjs.org/${packageName}/-/${packageName.split("/").pop()}-${version}.tgz`;
3073
+ const scopedName = packageName.split("/").pop();
3074
+ const npmUrl = `https://registry.npmjs.org/${packageName}/-/${scopedName}-${version}.tgz`;
3039
3075
  await mkdir2(tempBaseDir, { recursive: true });
3040
3076
  const tarballPath = join13(tempBaseDir, "package.tgz");
3041
3077
  await execa8("curl", ["-L", "-o", tarballPath, npmUrl]);
@@ -3063,7 +3099,7 @@ var fetchPackageSourceTool = defineTool({
3063
3099
  await walkDir(fullPath, relPath);
3064
3100
  }
3065
3101
  } else if (file.isFile()) {
3066
- const matches = filePatterns.some((pattern) => {
3102
+ const matches = safePatterns.some((pattern) => {
3067
3103
  const regex = new RegExp(
3068
3104
  `^${pattern.replace(/\*/g, ".*").replace(/\./g, "\\.")}$`
3069
3105
  );
@@ -3085,7 +3121,7 @@ var fetchPackageSourceTool = defineTool({
3085
3121
  if (Object.keys(sourceCode).length === 0) {
3086
3122
  return {
3087
3123
  success: false,
3088
- error: `No source files matching patterns [${filePatterns.join(", ")}] found in ${packageName}@${version}. Download succeeded but extraction yielded no matching files.`
3124
+ error: `No source files matching patterns [${safePatterns.join(", ")}] found in ${packageName}@${version}. Download succeeded but extraction yielded no matching files.`
3089
3125
  };
3090
3126
  }
3091
3127
  return {
@@ -3426,9 +3462,9 @@ import { execa as execa10 } from "execa";
3426
3462
 
3427
3463
  // src/remediation/tools/apply-patch-file/helpers.ts
3428
3464
  import { existsSync as existsSync8 } from "fs";
3429
- import { mkdtemp, readFile as readFile4, rm as rm3, writeFile } from "fs/promises";
3465
+ import { mkdtemp as mkdtemp2, readFile as readFile4, rm as rm3, writeFile } from "fs/promises";
3430
3466
  import { createHash } from "crypto";
3431
- import { tmpdir } from "os";
3467
+ import { tmpdir as tmpdir2 } from "os";
3432
3468
  import { join as join14 } from "path";
3433
3469
  import { execa as execa9 } from "execa";
3434
3470
  async function resolvePatchMode(packageManager, cwd) {
@@ -3573,7 +3609,7 @@ ${createResult.stderr}`);
3573
3609
  error: `Could not determine native patch directory for ${packageSpec}.`
3574
3610
  };
3575
3611
  }
3576
- const tempPatchDir = await mkdtemp(join14(tmpdir(), "autoremediator-native-patch-"));
3612
+ const tempPatchDir = await mkdtemp2(join14(tmpdir2(), "autoremediator-native-patch-"));
3577
3613
  const tempPatchFile = join14(tempPatchDir, "change.patch");
3578
3614
  try {
3579
3615
  await writeFile(tempPatchFile, patchContent, "utf8");
@@ -5055,6 +5091,13 @@ async function enrichWithOssfScorecard(details) {
5055
5091
  async function probeFeed(url, cveId, token) {
5056
5092
  try {
5057
5093
  const feedUrl = new URL(url);
5094
+ const hostname = feedUrl.hostname.toLowerCase();
5095
+ if (hostname === "localhost" || /^127\./.test(hostname) || /^10\./.test(hostname) || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname) || /^192\.168\./.test(hostname) || hostname === "0.0.0.0" || /^169\.254\./.test(hostname) || hostname === "[::1]" || hostname === "::1") {
5096
+ return void 0;
5097
+ }
5098
+ if (feedUrl.protocol !== "https:" && feedUrl.protocol !== "http:") {
5099
+ return void 0;
5100
+ }
5058
5101
  feedUrl.searchParams.set("cve", cveId);
5059
5102
  const headers = {};
5060
5103
  if (token) headers.Authorization = `Bearer ${token}`;
@@ -5485,6 +5528,7 @@ async function runRemediationPipeline(cveId, options = {}) {
5485
5528
 
5486
5529
  // src/api/change-request/index.ts
5487
5530
  import { execa as execa11 } from "execa";
5531
+ import { randomBytes } from "crypto";
5488
5532
  function sanitizeBranchToken(value) {
5489
5533
  return value.toLowerCase().replace(/[^a-z0-9._/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
5490
5534
  }
@@ -5537,7 +5581,7 @@ async function ensureRepoHasChanges(cwd) {
5537
5581
  }
5538
5582
  function toBranchName(prefix, cveIds) {
5539
5583
  const token = sanitizeBranchToken(cveIds.join("-") || "remediation");
5540
- return `${sanitizeBranchToken(prefix)}/${token}-${Date.now()}`;
5584
+ return `${sanitizeBranchToken(prefix)}/${token}-${randomBytes(6).toString("hex")}`;
5541
5585
  }
5542
5586
  async function createGitHubRequest(params) {
5543
5587
  const args = ["pr", "create", "--head", params.head, "--base", params.base, "--title", params.title, "--body", params.body];
@@ -5583,11 +5627,12 @@ async function createChangeRequestsForReports(params) {
5583
5627
  const provider = resolveProvider2(options);
5584
5628
  const grouping = resolveGrouping(options);
5585
5629
  const plan = buildPlan(reports);
5586
- const titlePrefix = options.titlePrefix?.trim();
5630
+ const titlePrefix = options.titlePrefix?.trim().slice(0, 200);
5631
+ const bodyFooter = options.bodyFooter ? options.bodyFooter.slice(0, 2e3) : void 0;
5587
5632
  const title = titlePrefix ? `${titlePrefix} ${plan.title}` : plan.title;
5588
- const body = options.bodyFooter ? `${plan.body}
5633
+ const body = bodyFooter ? `${plan.body}
5589
5634
 
5590
- ${options.bodyFooter}` : plan.body;
5635
+ ${bodyFooter}` : plan.body;
5591
5636
  try {
5592
5637
  const hasChanges = await ensureRepoHasChanges(cwd);
5593
5638
  if (!hasChanges) {
@@ -5610,6 +5655,12 @@ ${options.bodyFooter}` : plan.body;
5610
5655
  const branchPrefix = options.branchPrefix ?? "autoremediator";
5611
5656
  const branchName = toBranchName(branchPrefix, plan.cveIds);
5612
5657
  const pushRemote = options.pushRemote ?? "origin";
5658
+ if (options.pushRemote && !/^[a-zA-Z0-9._-]+$/.test(options.pushRemote)) {
5659
+ throw new Error(`Invalid pushRemote value: ${options.pushRemote}`);
5660
+ }
5661
+ if (options.repository && !/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(options.repository)) {
5662
+ throw new Error(`Invalid repository format: ${options.repository}`);
5663
+ }
5613
5664
  await execa11("git", ["checkout", "-b", branchName], { cwd, stdio: "pipe" });
5614
5665
  await execa11("git", ["add", "-A"], { cwd, stdio: "pipe" });
5615
5666
  await execa11("git", ["commit", "-m", title], { cwd, stdio: "pipe" });
@@ -5966,13 +6017,13 @@ async function planRemediation(cveId, options = {}) {
5966
6017
  }
5967
6018
 
5968
6019
  // src/scanner/parse-input.ts
5969
- import { extname as extname2 } from "path";
6020
+ import { extname as extname2, resolve as resolve2 } from "path";
5970
6021
  import { readFileSync as readFileSync14 } from "fs";
5971
6022
  import { execa as execa12 } from "execa";
5972
6023
 
5973
6024
  // src/scanner/adapters/npm-audit.ts
5974
6025
  import { readFileSync as readFileSync11 } from "fs";
5975
- var CVE_REGEX = /CVE-\d{4}-\d+/gi;
6026
+ var CVE_REGEX = /CVE-\d{4}-\d{1,7}/gi;
5976
6027
  function normalizeSeverity(raw) {
5977
6028
  if (!raw) return "UNKNOWN";
5978
6029
  const up = raw.toUpperCase();
@@ -6011,7 +6062,7 @@ function parseNpmAuditJsonFile(filePath) {
6011
6062
 
6012
6063
  // src/scanner/adapters/yarn-audit.ts
6013
6064
  import { readFileSync as readFileSync12 } from "fs";
6014
- var CVE_REGEX2 = /CVE-\d{4}-\d+/gi;
6065
+ var CVE_REGEX2 = /CVE-\d{4}-\d{1,7}/gi;
6015
6066
  function normalizeSeverity2(raw) {
6016
6067
  if (!raw) return "UNKNOWN";
6017
6068
  const up = raw.toUpperCase();
@@ -6059,7 +6110,7 @@ function parseYarnAuditJsonFile(filePath) {
6059
6110
 
6060
6111
  // src/scanner/adapters/sarif.ts
6061
6112
  import { readFileSync as readFileSync13 } from "fs";
6062
- var CVE_REGEX3 = /CVE-\d{4}-\d+/gi;
6113
+ var CVE_REGEX3 = /CVE-\d{4}-\d{1,7}/gi;
6063
6114
  function extractPackageName(result) {
6064
6115
  const pkg = result.properties?.["packageName"];
6065
6116
  return typeof pkg === "string" ? pkg : void 0;
@@ -6068,9 +6119,15 @@ function parseSarifFromString(content) {
6068
6119
  const report = JSON.parse(content);
6069
6120
  const findings = [];
6070
6121
  const seen = /* @__PURE__ */ new Set();
6071
- for (const run of report.runs ?? []) {
6122
+ const MAX_RUNS = 100;
6123
+ const MAX_TOTAL_RESULTS = 1e4;
6124
+ let totalResults = 0;
6125
+ for (const run of (report.runs ?? []).slice(0, MAX_RUNS)) {
6072
6126
  for (const result of run.results ?? []) {
6073
- const combined = `${result.ruleId ?? ""} ${result.message?.text ?? ""}`;
6127
+ if (totalResults++ >= MAX_TOTAL_RESULTS) break;
6128
+ const ruleId = (result.ruleId ?? "").slice(0, 1024);
6129
+ const messageText = (result.message?.text ?? "").slice(0, 4096);
6130
+ const combined = `${ruleId} ${messageText}`;
6074
6131
  const matches = combined.match(CVE_REGEX3) ?? [];
6075
6132
  for (const match of matches) {
6076
6133
  const cveId = match.toUpperCase();
@@ -6096,15 +6153,19 @@ function parseSarifFile(filePath) {
6096
6153
 
6097
6154
  // src/scanner/parse-input.ts
6098
6155
  function parseScanInput(filePath, format) {
6099
- const resolved = format === "auto" ? inferFormat(filePath) : format;
6156
+ if (filePath.includes("\0")) {
6157
+ throw new Error("Invalid scan input path: path contains null bytes");
6158
+ }
6159
+ const resolvedPath = resolve2(filePath);
6160
+ const resolved = format === "auto" ? inferFormat(resolvedPath) : format;
6100
6161
  if (resolved === "npm-audit") {
6101
- return parseNpmAuditJsonFile(filePath);
6162
+ return parseNpmAuditJsonFile(resolvedPath);
6102
6163
  }
6103
6164
  if (resolved === "yarn-audit") {
6104
- return parseYarnAuditJsonFile(filePath);
6165
+ return parseYarnAuditJsonFile(resolvedPath);
6105
6166
  }
6106
6167
  if (resolved === "sarif") {
6107
- return parseSarifFile(filePath);
6168
+ return parseSarifFile(resolvedPath);
6108
6169
  }
6109
6170
  throw new Error(`Unsupported input format: ${resolved}`);
6110
6171
  }