@profullstack/threatcrush 0.6.2 → 0.7.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.
package/dist/daemon.js CHANGED
@@ -4310,111 +4310,7 @@ function workerId() {
4310
4310
  return `${import_node_os4.default.hostname()}/${process.pid}`;
4311
4311
  }
4312
4312
 
4313
- // src/scan/dependencies.ts
4314
- var import_node_fs10 = require("fs");
4315
- var import_node_path5 = require("path");
4316
- var LOCKFILES = [
4317
- { file: "package-lock.json", ecosystem: "npm" },
4318
- { file: "pnpm-lock.yaml", ecosystem: "npm" },
4319
- { file: "yarn.lock", ecosystem: "npm" },
4320
- { file: "requirements.txt", ecosystem: "PyPI" },
4321
- { file: "Pipfile.lock", ecosystem: "PyPI" }
4322
- ];
4323
- var MAX_DEPS_PER_LOCKFILE = 50;
4324
- async function scanDependencies(targetPath) {
4325
- const findings = [];
4326
- for (const { file, ecosystem } of LOCKFILES) {
4327
- const lockPath = (0, import_node_path5.join)(targetPath, file);
4328
- if (!(0, import_node_fs10.existsSync)(lockPath)) continue;
4329
- let deps;
4330
- try {
4331
- deps = parseDependencies(lockPath, file);
4332
- } catch {
4333
- continue;
4334
- }
4335
- for (const dep of deps.slice(0, MAX_DEPS_PER_LOCKFILE)) {
4336
- let vulns;
4337
- try {
4338
- vulns = await queryOsv(dep.name, dep.version, ecosystem);
4339
- } catch {
4340
- continue;
4341
- }
4342
- for (const vuln of vulns) {
4343
- const cvss = vuln.severity?.find((entry) => entry.type === "CVSS_V3")?.score;
4344
- findings.push({
4345
- ruleId: "dependency-known-vulnerability",
4346
- title: "Dependency CVE",
4347
- file,
4348
- line: 1,
4349
- severity: severityFromCvss(cvss),
4350
- confidence: "evidence",
4351
- message: `${dep.name}@${dep.version}: ${vuln.summary ?? vuln.id}`,
4352
- consequence: "A published advisory exists for the exact version resolved in this lockfile.",
4353
- excerpt: `${vuln.id}${cvss ? ` (CVSS: ${cvss})` : ""}`,
4354
- category: "dependency"
4355
- });
4356
- }
4357
- }
4358
- }
4359
- return findings;
4360
- }
4361
- function severityFromCvss(score) {
4362
- if (!score) return "medium";
4363
- const value = Number.parseFloat(score);
4364
- if (Number.isNaN(value)) return "medium";
4365
- if (value >= 9) return "critical";
4366
- if (value >= 7) return "high";
4367
- if (value >= 4) return "medium";
4368
- return "low";
4369
- }
4370
- function parseDependencies(lockPath, filename) {
4371
- const deps = [];
4372
- if (filename === "package-lock.json") {
4373
- const lock = JSON.parse((0, import_node_fs10.readFileSync)(lockPath, "utf-8"));
4374
- const packages = lock.packages ?? lock.dependencies ?? {};
4375
- for (const [key, value] of Object.entries(packages)) {
4376
- const name = key.replace(/^node_modules\//, "");
4377
- const version = value?.version;
4378
- if (name && version && !name.startsWith(".")) deps.push({ name, version });
4379
- }
4380
- return deps;
4381
- }
4382
- if (filename === "requirements.txt") {
4383
- for (const line of (0, import_node_fs10.readFileSync)(lockPath, "utf-8").split("\n")) {
4384
- const match = /^([a-zA-Z0-9_.-]+)==([0-9.]+)/.exec(line);
4385
- if (match?.[1] && match[2]) deps.push({ name: match[1], version: match[2] });
4386
- }
4387
- }
4388
- return deps;
4389
- }
4390
- function isValidPackageName(name) {
4391
- return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
4392
- }
4393
- function isValidVersion(version) {
4394
- return /^[0-9a-zA-Z._\-+]{1,50}$/.test(version);
4395
- }
4396
- async function queryOsv(name, version, ecosystem) {
4397
- if (!isValidPackageName(name) || !isValidVersion(version)) return [];
4398
- try {
4399
- const response = await fetch("https://api.osv.dev/v1/query", {
4400
- method: "POST",
4401
- headers: { "Content-Type": "application/json" },
4402
- body: JSON.stringify({ package: { name, ecosystem }, version }),
4403
- signal: AbortSignal.timeout(5e3)
4404
- });
4405
- if (!response.ok) return [];
4406
- const data = await response.json();
4407
- return data.vulns ?? [];
4408
- } catch {
4409
- return [];
4410
- }
4411
- }
4412
-
4413
- // src/scan/engine.ts
4414
- var import_node_fs11 = require("fs");
4415
- var import_node_path6 = require("path");
4416
-
4417
- // src/scan/types.ts
4313
+ // ../../packages/scan/src/types.ts
4418
4314
  var SEVERITY_ORDER = ["info", "low", "medium", "high", "critical"];
4419
4315
  function severityRank(severity) {
4420
4316
  const index = SEVERITY_ORDER.indexOf(severity);
@@ -4425,7 +4321,7 @@ function severityFor(declared, confidence) {
4425
4321
  return severityRank(declared) > severityRank("medium") ? "medium" : declared;
4426
4322
  }
4427
4323
 
4428
- // src/scan/code-rules.ts
4324
+ // ../../packages/scan/src/code-rules.ts
4429
4325
  var UNTRUSTED_JS = /\b(?:req|request|ctx|context)\s*\.\s*(?:body|query|params|param|headers|cookies|url|files)\b|\bprocess\.argv\b|\bwindow\.location\b|\bdocument\.location\b|\blocation\.(?:search|hash|href)\b|\bsearchParams\s*\.\s*(?:get|getAll|has|entries|keys|values|forEach)\b|\bgetParameter\s*\(|\bgetQueryString\s*\(|\bgetInputStream\s*\(/;
4430
4326
  var UNTRUSTED_PY = /\brequest\b|\bparams\b|\bflask\b|\bsys\.argv\b|\bos\.environ\b\s*\[/;
4431
4327
  var UNTRUSTED_RB = /\bparams\s*\[|\brequest\b|\bcookies\s*\[/;
@@ -4809,6 +4705,7 @@ var CODE_RULES = [
4809
4705
  },
4810
4706
  {
4811
4707
  id: "tls-verification-disabled",
4708
+ inherent: true,
4812
4709
  title: "TLS certificate verification disabled",
4813
4710
  consequence: "Every connection made this way is trivially interceptable; the encryption is decorative.",
4814
4711
  cwe: "CWE-295",
@@ -4902,6 +4799,7 @@ var CODE_RULES = [
4902
4799
  // of privileged work actually happens, and they run as whoever invoked them.
4903
4800
  {
4904
4801
  id: "sh-remote-script-execution",
4802
+ inherent: true,
4905
4803
  title: "network output piped into a shell",
4906
4804
  consequence: "Whatever that URL serves at the moment this runs is executed as the invoking user. There is no version, no signature, and no review \u2014 a compromise of the host, or anyone able to answer for it, is a compromise of every machine that runs the script.",
4907
4805
  cwe: "CWE-494",
@@ -4957,6 +4855,7 @@ var CODE_RULES = [
4957
4855
  },
4958
4856
  {
4959
4857
  id: "sh-insecure-transport-flag",
4858
+ inherent: true,
4960
4859
  title: "certificate verification disabled",
4961
4860
  consequence: "Anyone positioned between this host and the server can substitute the response. When the response is a package, a key or a script, that is remote code execution with the transport doing nothing to stop it.",
4962
4861
  cwe: "CWE-295",
@@ -4971,6 +4870,7 @@ var CODE_RULES = [
4971
4870
  },
4972
4871
  {
4973
4872
  id: "sh-plaintext-download",
4873
+ inherent: true,
4974
4874
  title: "download over plain HTTP",
4975
4875
  consequence: "The response arrives unauthenticated over a channel any intermediary can rewrite. Where the payload is an archive, a package list or a key, substituting it is straightforward and leaves nothing for the script to notice.",
4976
4876
  cwe: "CWE-319",
@@ -4982,6 +4882,7 @@ var CODE_RULES = [
4982
4882
  },
4983
4883
  {
4984
4884
  id: "sh-world-writable-permissions",
4885
+ inherent: true,
4985
4886
  title: "world-writable permissions",
4986
4887
  consequence: "Any local account can rewrite the file. If it is a script, a config or anything on a privileged path, the next process to read it runs someone else\u2019s content.",
4987
4888
  cwe: "CWE-732",
@@ -5099,6 +5000,91 @@ var CODE_RULES = [
5099
5000
  languages: ["php"],
5100
5001
  pattern: /\bextract\s*\(\s*\$_(?:GET|POST|REQUEST|COOKIE)\b|\bimport_request_variables\s*\(/,
5101
5002
  guard: false
5003
+ },
5004
+ // ── Java and Go: classes the other languages already had ─────────────────
5005
+ //
5006
+ // Command injection, SSRF and path traversal were implemented for JavaScript
5007
+ // and, in part, for Go, and never for Java — so the same defect in the same
5008
+ // codebase was reported or not depending on which file it lived in. TLS
5009
+ // verification, weak hashing and insecure randomness are deliberately absent
5010
+ // here: `tls-verification-disabled`, `weak-hash-on-credential` and
5011
+ // `insecure-randomness-for-secret` are language-agnostic and already cover
5012
+ // both, including Go's `InsecureSkipVerify` and Java's `MessageDigest`.
5013
+ {
5014
+ id: "java-runtime-exec-concatenation",
5015
+ title: "Runtime.exec with a concatenated command",
5016
+ consequence: "The single-string form of `exec` is split on whitespace and handed to the OS. A value carrying a space becomes extra arguments, and where a shell is invoked, `;` and `$(\u2026)` become extra commands.",
5017
+ cwe: "CWE-78",
5018
+ severity: "critical",
5019
+ languages: ["java"],
5020
+ // The array form — `exec(new String[]{"git", arg})` — passes argv and is
5021
+ // the fix, so it is not matched: the `+` has to be inside the string
5022
+ // argument for this to fire.
5023
+ pattern: /\b(?:Runtime\s*\.\s*getRuntime\s*\(\s*\)\s*\.\s*exec|ProcessBuilder)\s*\(\s*(?:"[^"\n]*"\s*\+|\w+\s*\+\s*")/
5024
+ },
5025
+ {
5026
+ id: "java-ssrf-outbound-request",
5027
+ title: "outbound request to a computed URL",
5028
+ consequence: "The destination is chosen by the caller, so the request can be aimed at internal services and cloud metadata endpoints that are reachable from this host and from nowhere else.",
5029
+ cwe: "CWE-918",
5030
+ severity: "high",
5031
+ languages: ["java"],
5032
+ pattern: /\bnew\s+URL\s*\(\s*(?!\s*"[a-z]+:\/\/[^"\n]*"\s*\))[^)\n]*\w|\bHttpRequest\s*\.\s*newBuilder\s*\(\s*\)\s*\.\s*uri\s*\(\s*URI\s*\.\s*create\s*\(\s*[^"\n)]/,
5033
+ needsContext: true
5034
+ },
5035
+ {
5036
+ id: "java-request-path-traversal",
5037
+ title: "file path built from request data",
5038
+ consequence: "A `../` sequence in the value walks out of the intended directory. The process then reads or writes wherever it lands, with its own privileges.",
5039
+ cwe: "CWE-22",
5040
+ severity: "high",
5041
+ languages: ["java"],
5042
+ pattern: /\b(?:new\s+File|new\s+FileInputStream|new\s+FileOutputStream|Paths\s*\.\s*get|Files\s*\.\s*(?:readAllBytes|newInputStream|newOutputStream|copy|delete))\s*\([^)\n]*\+/,
5043
+ // `getCanonicalPath().startsWith(base)` is the check that makes this safe,
5044
+ // and it is normally a line or two below the construction.
5045
+ guard: /getCanonicalPath|toRealPath|normalize\s*\(\s*\)|\bstartsWith\s*\(/,
5046
+ guardForward: 4,
5047
+ needsContext: true
5048
+ },
5049
+ {
5050
+ id: "java-broken-cipher",
5051
+ inherent: true,
5052
+ title: "broken cipher or ECB mode",
5053
+ consequence: "DES, RC2, RC4 and Blowfish are broken or too small to rely on. ECB encrypts identical plaintext blocks to identical ciphertext blocks, so structure in the data survives encryption and is readable straight off the ciphertext.",
5054
+ cwe: "CWE-327",
5055
+ severity: "high",
5056
+ languages: ["java"],
5057
+ // Bare `"AES"` is included: the JCE resolves it to `AES/ECB/PKCS5Padding`,
5058
+ // so the default is the mode this rule exists to catch.
5059
+ pattern: /\bCipher\s*\.\s*getInstance\s*\(\s*"(?:DES|DESede|RC2|RC4|ARCFOUR|Blowfish)(?:\/|")|\bCipher\s*\.\s*getInstance\s*\(\s*"[^"\n]*\/ECB\/|\bCipher\s*\.\s*getInstance\s*\(\s*"AES"\s*\)/,
5060
+ guard: false
5061
+ },
5062
+ {
5063
+ id: "go-request-path-traversal",
5064
+ title: "file path built from request data",
5065
+ consequence: "A `../` sequence in the value walks out of the intended directory, and the handler serves or writes whatever it reaches.",
5066
+ cwe: "CWE-22",
5067
+ severity: "high",
5068
+ languages: ["go"],
5069
+ pattern: /\b(?:os\s*\.\s*(?:Open|OpenFile|ReadFile|Create|Remove|WriteFile)|ioutil\s*\.\s*(?:ReadFile|WriteFile)|http\s*\.\s*ServeFile)\s*\([^)\n]*(?:r\s*\.\s*URL|FormValue|Query\s*\(\s*\)\s*\.\s*Get|mux\s*\.\s*Vars|\bfilepath\s*\.\s*Join\s*\([^)\n]*\w)/,
5070
+ // `filepath.Clean` alone does not bound the result to a directory, so it is
5071
+ // not a guard here — the containment check is.
5072
+ guard: /\bstrings\s*\.\s*HasPrefix\s*\(|\bfilepath\s*\.\s*Rel\s*\(|\bfs\s*\.\s*ValidPath\s*\(|\bhttp\s*\.\s*Dir\b/,
5073
+ guardBack: 5,
5074
+ guardForward: 3,
5075
+ needsContext: true
5076
+ },
5077
+ {
5078
+ id: "go-template-escaping-bypass",
5079
+ title: "value marked as pre-escaped HTML",
5080
+ consequence: "`template.HTML` tells `html/template` the value is already safe, which switches off the contextual escaping that makes the package worth using. Markup in the value reaches the page intact.",
5081
+ cwe: "CWE-79",
5082
+ severity: "high",
5083
+ languages: ["go"],
5084
+ // A conversion of a *variable*. `template.HTML("<br>")` on a literal is a
5085
+ // constant the author wrote and is not a finding.
5086
+ pattern: /\btemplate\s*\.\s*(?:HTML|JS|CSS|HTMLAttr|URL|Srcset)\s*\(\s*(?!\s*[`"])/,
5087
+ needsContext: true
5102
5088
  }
5103
5089
  ];
5104
5090
  var COMMENT_PREFIX = /^\s*(?:\/\/|\/\*|\*|#|--|<!--)/;
@@ -5149,6 +5135,9 @@ function fileTextOf(lines) {
5149
5135
  }
5150
5136
  return text;
5151
5137
  }
5138
+ function withoutSingleQuoted(text) {
5139
+ return text.replace(/'[^'\n]*'/g, "''");
5140
+ }
5152
5141
  function evaluateRule(rule, ctx) {
5153
5142
  if (rule.languages && !rule.languages.includes(ctx.language)) return null;
5154
5143
  const line = ctx.lines[ctx.index] ?? "";
@@ -5163,13 +5152,15 @@ function evaluateRule(rule, ctx) {
5163
5152
  const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
5164
5153
  if (guard && (guard.test(line) || guard.test(context))) return null;
5165
5154
  const untrusted = untrustedPatternFor(ctx.language);
5166
- const contextual = untrusted.test(line) || untrusted.test(context);
5155
+ const probeLine = ctx.language === "shell" ? withoutSingleQuoted(line) : line;
5156
+ const probeContext = ctx.language === "shell" ? withoutSingleQuoted(context) : context;
5157
+ const contextual = untrusted.test(probeLine) || untrusted.test(probeContext);
5167
5158
  if (rule.needsContext && !contextual) return null;
5168
- const confidence = contextual ? "contextual" : "pattern";
5159
+ const confidence = rule.inherent ? "evidence" : contextual ? "contextual" : "pattern";
5169
5160
  return { rule, confidence, severity: severityFor(rule.severity, confidence) };
5170
5161
  }
5171
5162
 
5172
- // src/scan/manifest-rules.ts
5163
+ // ../../packages/scan/src/manifest-rules.ts
5173
5164
  var POPULAR_NPM = [
5174
5165
  "react",
5175
5166
  "react-dom",
@@ -5444,7 +5435,7 @@ function scanRequirementsTxt(text) {
5444
5435
  return findings;
5445
5436
  }
5446
5437
 
5447
- // src/scan/secret-rules.ts
5438
+ // ../../packages/scan/src/secret-rules.ts
5448
5439
  var SECRET_RULES = [
5449
5440
  {
5450
5441
  id: "secret-aws-access-key",
@@ -5627,7 +5618,15 @@ var SENSITIVE_FILES = [
5627
5618
  // match. Reporting the file itself trades a real finding for a chore.
5628
5619
  ];
5629
5620
 
5630
- // src/scan/engine.ts
5621
+ // ../../packages/scan/src/text.ts
5622
+ function baseNameOf(path) {
5623
+ return path.slice(path.lastIndexOf("/") + 1);
5624
+ }
5625
+ function extensionOf(path) {
5626
+ const base = baseNameOf(path);
5627
+ const dot = base.lastIndexOf(".");
5628
+ return dot <= 0 ? "" : base.slice(dot);
5629
+ }
5631
5630
  var SKIP_DIRS = /* @__PURE__ */ new Set([
5632
5631
  "node_modules",
5633
5632
  ".git",
@@ -5736,7 +5735,7 @@ var LANGUAGE_BY_EXTENSION = {
5736
5735
  };
5737
5736
  function languageOf(filename) {
5738
5737
  if (filename.startsWith(".env") || filename.endsWith(".env")) return "config";
5739
- return LANGUAGE_BY_EXTENSION[(0, import_node_path6.extname)(filename).toLowerCase()] ?? "other";
5738
+ return LANGUAGE_BY_EXTENSION[extensionOf(filename).toLowerCase()] ?? "other";
5740
5739
  }
5741
5740
  var LANGUAGE_BY_INTERPRETER = {
5742
5741
  sh: "shell",
@@ -5756,10 +5755,12 @@ var LANGUAGE_BY_INTERPRETER = {
5756
5755
  php: "php"
5757
5756
  };
5758
5757
  function languageOfShebang(firstLine) {
5759
- const match = /^#!\s*(\S+)(?:\s+(\S+))?/.exec(firstLine);
5758
+ const match = /^#!\s*(\S+)(?:\s+(.+?))?\s*$/.exec(firstLine);
5760
5759
  if (!match) return null;
5761
- const command = (0, import_node_path6.basename)(match[1]);
5762
- const name = command === "env" && match[2] ? (0, import_node_path6.basename)(match[2]) : command;
5760
+ const command = baseNameOf(match[1]);
5761
+ const args = match[2]?.trim().split(/\s+/) ?? [];
5762
+ const splitString = args[0] === "-S" || args[0] === "--split-string";
5763
+ const name = command === "env" ? baseNameOf(args[splitString ? 1 : 0] ?? "") : command;
5763
5764
  const exact = LANGUAGE_BY_INTERPRETER[name];
5764
5765
  if (exact) return exact;
5765
5766
  const stripped = name.replace(/[\d.]+$/, "");
@@ -5861,6 +5862,10 @@ function scanManifest(relativePath, filename, text) {
5861
5862
  category: "manifest"
5862
5863
  }));
5863
5864
  }
5865
+
5866
+ // ../../packages/scan/src/node/walk.ts
5867
+ var import_node_fs10 = require("fs");
5868
+ var import_node_path5 = require("path");
5864
5869
  function scanPath(targetPath, options = {}) {
5865
5870
  const maxFileBytes = options.maxFileBytes ?? 1024 * 1024;
5866
5871
  const allowed = options.categories ? new Set(options.categories) : null;
@@ -5870,15 +5875,15 @@ function scanPath(targetPath, options = {}) {
5870
5875
  let suppressed = 0;
5871
5876
  const rootIsDirectory = (() => {
5872
5877
  try {
5873
- return (0, import_node_fs11.statSync)(targetPath).isDirectory();
5878
+ return (0, import_node_fs10.statSync)(targetPath).isDirectory();
5874
5879
  } catch {
5875
5880
  return true;
5876
5881
  }
5877
5882
  })();
5878
- const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path6.dirname)(targetPath);
5883
+ const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path5.dirname)(targetPath);
5879
5884
  const scanFile = (fullPath, filename) => {
5880
5885
  const relativePath = toRelative(walkRoot, fullPath);
5881
- const extension = (0, import_node_path6.extname)(filename).toLowerCase();
5886
+ const extension = (0, import_node_path5.extname)(filename).toLowerCase();
5882
5887
  const isManifest = filename === "package.json" || filename === "requirements.txt";
5883
5888
  const scannable = SCAN_EXTENSIONS.has(extension) || filename.startsWith(".env");
5884
5889
  const mayDeclareInterpreter = !scannable && !isManifest && extension === "";
@@ -5890,26 +5895,26 @@ function scanPath(targetPath, options = {}) {
5890
5895
  let handle;
5891
5896
  let declared = null;
5892
5897
  try {
5893
- handle = (0, import_node_fs11.openSync)(fullPath, "r");
5898
+ handle = (0, import_node_fs10.openSync)(fullPath, "r");
5894
5899
  } catch {
5895
5900
  unreadable.push(relativePath);
5896
5901
  return;
5897
5902
  }
5898
5903
  try {
5899
- if ((0, import_node_fs11.fstatSync)(handle).size > maxFileBytes) return;
5904
+ if ((0, import_node_fs10.fstatSync)(handle).size > maxFileBytes) return;
5900
5905
  if (mayDeclareInterpreter) {
5901
5906
  const prefix = Buffer.alloc(128);
5902
- const read = (0, import_node_fs11.readSync)(handle, prefix, 0, prefix.length, 0);
5907
+ const read = (0, import_node_fs10.readSync)(handle, prefix, 0, prefix.length, 0);
5903
5908
  declared = languageOfShebang(prefix.subarray(0, read).toString("utf-8").split("\n", 1)[0] ?? "");
5904
5909
  if (!declared) return;
5905
5910
  }
5906
- text = (0, import_node_fs11.readFileSync)(handle, "utf-8");
5911
+ text = (0, import_node_fs10.readFileSync)(handle, "utf-8");
5907
5912
  } catch {
5908
5913
  unreadable.push(relativePath);
5909
5914
  return;
5910
5915
  } finally {
5911
5916
  try {
5912
- (0, import_node_fs11.closeSync)(handle);
5917
+ (0, import_node_fs10.closeSync)(handle);
5913
5918
  } catch {
5914
5919
  }
5915
5920
  }
@@ -5926,13 +5931,13 @@ function scanPath(targetPath, options = {}) {
5926
5931
  const walk = (currentPath) => {
5927
5932
  let entries;
5928
5933
  try {
5929
- entries = (0, import_node_fs11.readdirSync)(currentPath, { withFileTypes: true });
5934
+ entries = (0, import_node_fs10.readdirSync)(currentPath, { withFileTypes: true });
5930
5935
  } catch {
5931
5936
  unreadable.push(toRelative(walkRoot, currentPath));
5932
5937
  return;
5933
5938
  }
5934
5939
  for (const entry of entries) {
5935
- const fullPath = (0, import_node_path6.join)(currentPath, entry.name);
5940
+ const fullPath = (0, import_node_path5.join)(currentPath, entry.name);
5936
5941
  if (entry.isDirectory()) {
5937
5942
  if (SKIP_DIRS.has(entry.name)) continue;
5938
5943
  walk(fullPath);
@@ -5945,7 +5950,7 @@ function scanPath(targetPath, options = {}) {
5945
5950
  if (rootIsDirectory) {
5946
5951
  walk(targetPath);
5947
5952
  } else {
5948
- scanFile(targetPath, (0, import_node_path6.basename)(targetPath));
5953
+ scanFile(targetPath, (0, import_node_path5.basename)(targetPath));
5949
5954
  }
5950
5955
  const filtered = allowed ? findings.filter((f) => allowed.has(f.category)) : findings;
5951
5956
  filtered.sort(
@@ -5976,11 +5981,111 @@ function recordSensitiveFile(filename, relativePath, sink, fileFindings) {
5976
5981
  }
5977
5982
  }
5978
5983
  function toRelative(base, target) {
5979
- const rel = (0, import_node_path6.relative)(base, target);
5980
- return (rel === "" ? target : rel).split(import_node_path6.sep).join("/");
5984
+ const rel = (0, import_node_path5.relative)(base, target);
5985
+ return (rel === "" ? target : rel).split(import_node_path5.sep).join("/");
5986
+ }
5987
+
5988
+ // ../../packages/scan/src/node/dependencies.ts
5989
+ var import_node_fs11 = require("fs");
5990
+ var import_node_path6 = require("path");
5991
+ var LOCKFILES = [
5992
+ { file: "package-lock.json", ecosystem: "npm" },
5993
+ { file: "pnpm-lock.yaml", ecosystem: "npm" },
5994
+ { file: "yarn.lock", ecosystem: "npm" },
5995
+ { file: "requirements.txt", ecosystem: "PyPI" },
5996
+ { file: "Pipfile.lock", ecosystem: "PyPI" }
5997
+ ];
5998
+ var MAX_DEPS_PER_LOCKFILE = 50;
5999
+ async function scanDependencies(targetPath) {
6000
+ const findings = [];
6001
+ for (const { file, ecosystem } of LOCKFILES) {
6002
+ const lockPath = (0, import_node_path6.join)(targetPath, file);
6003
+ if (!(0, import_node_fs11.existsSync)(lockPath)) continue;
6004
+ let deps;
6005
+ try {
6006
+ deps = parseDependencies(lockPath, file);
6007
+ } catch {
6008
+ continue;
6009
+ }
6010
+ for (const dep of deps.slice(0, MAX_DEPS_PER_LOCKFILE)) {
6011
+ let vulns;
6012
+ try {
6013
+ vulns = await queryOsv(dep.name, dep.version, ecosystem);
6014
+ } catch {
6015
+ continue;
6016
+ }
6017
+ for (const vuln of vulns) {
6018
+ const cvss = vuln.severity?.find((entry) => entry.type === "CVSS_V3")?.score;
6019
+ findings.push({
6020
+ ruleId: "dependency-known-vulnerability",
6021
+ title: "Dependency CVE",
6022
+ file,
6023
+ line: 1,
6024
+ severity: severityFromCvss(cvss),
6025
+ confidence: "evidence",
6026
+ message: `${dep.name}@${dep.version}: ${vuln.summary ?? vuln.id}`,
6027
+ consequence: "A published advisory exists for the exact version resolved in this lockfile.",
6028
+ excerpt: `${vuln.id}${cvss ? ` (CVSS: ${cvss})` : ""}`,
6029
+ category: "dependency"
6030
+ });
6031
+ }
6032
+ }
6033
+ }
6034
+ return findings;
6035
+ }
6036
+ function severityFromCvss(score) {
6037
+ if (!score) return "medium";
6038
+ const value = Number.parseFloat(score);
6039
+ if (Number.isNaN(value)) return "medium";
6040
+ if (value >= 9) return "critical";
6041
+ if (value >= 7) return "high";
6042
+ if (value >= 4) return "medium";
6043
+ return "low";
6044
+ }
6045
+ function parseDependencies(lockPath, filename) {
6046
+ const deps = [];
6047
+ if (filename === "package-lock.json") {
6048
+ const lock = JSON.parse((0, import_node_fs11.readFileSync)(lockPath, "utf-8"));
6049
+ const packages = lock.packages ?? lock.dependencies ?? {};
6050
+ for (const [key, value] of Object.entries(packages)) {
6051
+ const name = key.replace(/^node_modules\//, "");
6052
+ const version = value?.version;
6053
+ if (name && version && !name.startsWith(".")) deps.push({ name, version });
6054
+ }
6055
+ return deps;
6056
+ }
6057
+ if (filename === "requirements.txt") {
6058
+ for (const line of (0, import_node_fs11.readFileSync)(lockPath, "utf-8").split("\n")) {
6059
+ const match = /^([a-zA-Z0-9_.-]+)==([0-9.]+)/.exec(line);
6060
+ if (match?.[1] && match[2]) deps.push({ name: match[1], version: match[2] });
6061
+ }
6062
+ }
6063
+ return deps;
6064
+ }
6065
+ function isValidPackageName(name) {
6066
+ return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
6067
+ }
6068
+ function isValidVersion(version) {
6069
+ return /^[0-9a-zA-Z._\-+]{1,50}$/.test(version);
6070
+ }
6071
+ async function queryOsv(name, version, ecosystem) {
6072
+ if (!isValidPackageName(name) || !isValidVersion(version)) return [];
6073
+ try {
6074
+ const response = await fetch("https://api.osv.dev/v1/query", {
6075
+ method: "POST",
6076
+ headers: { "Content-Type": "application/json" },
6077
+ body: JSON.stringify({ package: { name, ecosystem }, version }),
6078
+ signal: AbortSignal.timeout(5e3)
6079
+ });
6080
+ if (!response.ok) return [];
6081
+ const data = await response.json();
6082
+ return data.vulns ?? [];
6083
+ } catch {
6084
+ return [];
6085
+ }
5981
6086
  }
5982
6087
 
5983
- // src/scan/sarif.ts
6088
+ // ../../packages/scan/src/node/sarif.ts
5984
6089
  var import_node_crypto = require("crypto");
5985
6090
  var import_node_path7 = require("path");
5986
6091