@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/index.js CHANGED
@@ -9580,111 +9580,7 @@ function workerId() {
9580
9580
  return `${import_node_os3.default.hostname()}/${process.pid}`;
9581
9581
  }
9582
9582
 
9583
- // src/scan/dependencies.ts
9584
- var import_node_fs5 = require("fs");
9585
- var import_node_path2 = require("path");
9586
- var LOCKFILES = [
9587
- { file: "package-lock.json", ecosystem: "npm" },
9588
- { file: "pnpm-lock.yaml", ecosystem: "npm" },
9589
- { file: "yarn.lock", ecosystem: "npm" },
9590
- { file: "requirements.txt", ecosystem: "PyPI" },
9591
- { file: "Pipfile.lock", ecosystem: "PyPI" }
9592
- ];
9593
- var MAX_DEPS_PER_LOCKFILE = 50;
9594
- async function scanDependencies(targetPath) {
9595
- const findings = [];
9596
- for (const { file, ecosystem } of LOCKFILES) {
9597
- const lockPath = (0, import_node_path2.join)(targetPath, file);
9598
- if (!(0, import_node_fs5.existsSync)(lockPath)) continue;
9599
- let deps;
9600
- try {
9601
- deps = parseDependencies(lockPath, file);
9602
- } catch {
9603
- continue;
9604
- }
9605
- for (const dep of deps.slice(0, MAX_DEPS_PER_LOCKFILE)) {
9606
- let vulns;
9607
- try {
9608
- vulns = await queryOsv(dep.name, dep.version, ecosystem);
9609
- } catch {
9610
- continue;
9611
- }
9612
- for (const vuln of vulns) {
9613
- const cvss = vuln.severity?.find((entry) => entry.type === "CVSS_V3")?.score;
9614
- findings.push({
9615
- ruleId: "dependency-known-vulnerability",
9616
- title: "Dependency CVE",
9617
- file,
9618
- line: 1,
9619
- severity: severityFromCvss(cvss),
9620
- confidence: "evidence",
9621
- message: `${dep.name}@${dep.version}: ${vuln.summary ?? vuln.id}`,
9622
- consequence: "A published advisory exists for the exact version resolved in this lockfile.",
9623
- excerpt: `${vuln.id}${cvss ? ` (CVSS: ${cvss})` : ""}`,
9624
- category: "dependency"
9625
- });
9626
- }
9627
- }
9628
- }
9629
- return findings;
9630
- }
9631
- function severityFromCvss(score) {
9632
- if (!score) return "medium";
9633
- const value = Number.parseFloat(score);
9634
- if (Number.isNaN(value)) return "medium";
9635
- if (value >= 9) return "critical";
9636
- if (value >= 7) return "high";
9637
- if (value >= 4) return "medium";
9638
- return "low";
9639
- }
9640
- function parseDependencies(lockPath, filename) {
9641
- const deps = [];
9642
- if (filename === "package-lock.json") {
9643
- const lock = JSON.parse((0, import_node_fs5.readFileSync)(lockPath, "utf-8"));
9644
- const packages = lock.packages ?? lock.dependencies ?? {};
9645
- for (const [key, value] of Object.entries(packages)) {
9646
- const name = key.replace(/^node_modules\//, "");
9647
- const version = value?.version;
9648
- if (name && version && !name.startsWith(".")) deps.push({ name, version });
9649
- }
9650
- return deps;
9651
- }
9652
- if (filename === "requirements.txt") {
9653
- for (const line of (0, import_node_fs5.readFileSync)(lockPath, "utf-8").split("\n")) {
9654
- const match = /^([a-zA-Z0-9_.-]+)==([0-9.]+)/.exec(line);
9655
- if (match?.[1] && match[2]) deps.push({ name: match[1], version: match[2] });
9656
- }
9657
- }
9658
- return deps;
9659
- }
9660
- function isValidPackageName(name) {
9661
- return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
9662
- }
9663
- function isValidVersion(version) {
9664
- return /^[0-9a-zA-Z._\-+]{1,50}$/.test(version);
9665
- }
9666
- async function queryOsv(name, version, ecosystem) {
9667
- if (!isValidPackageName(name) || !isValidVersion(version)) return [];
9668
- try {
9669
- const response = await fetch("https://api.osv.dev/v1/query", {
9670
- method: "POST",
9671
- headers: { "Content-Type": "application/json" },
9672
- body: JSON.stringify({ package: { name, ecosystem }, version }),
9673
- signal: AbortSignal.timeout(5e3)
9674
- });
9675
- if (!response.ok) return [];
9676
- const data = await response.json();
9677
- return data.vulns ?? [];
9678
- } catch {
9679
- return [];
9680
- }
9681
- }
9682
-
9683
- // src/scan/engine.ts
9684
- var import_node_fs6 = require("fs");
9685
- var import_node_path3 = require("path");
9686
-
9687
- // src/scan/types.ts
9583
+ // ../../packages/scan/src/types.ts
9688
9584
  var SEVERITY_ORDER = ["info", "low", "medium", "high", "critical"];
9689
9585
  function severityRank(severity) {
9690
9586
  const index = SEVERITY_ORDER.indexOf(severity);
@@ -9695,7 +9591,7 @@ function severityFor(declared, confidence) {
9695
9591
  return severityRank(declared) > severityRank("medium") ? "medium" : declared;
9696
9592
  }
9697
9593
 
9698
- // src/scan/code-rules.ts
9594
+ // ../../packages/scan/src/code-rules.ts
9699
9595
  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*\(/;
9700
9596
  var UNTRUSTED_PY = /\brequest\b|\bparams\b|\bflask\b|\bsys\.argv\b|\bos\.environ\b\s*\[/;
9701
9597
  var UNTRUSTED_RB = /\bparams\s*\[|\brequest\b|\bcookies\s*\[/;
@@ -10079,6 +9975,7 @@ var CODE_RULES = [
10079
9975
  },
10080
9976
  {
10081
9977
  id: "tls-verification-disabled",
9978
+ inherent: true,
10082
9979
  title: "TLS certificate verification disabled",
10083
9980
  consequence: "Every connection made this way is trivially interceptable; the encryption is decorative.",
10084
9981
  cwe: "CWE-295",
@@ -10172,6 +10069,7 @@ var CODE_RULES = [
10172
10069
  // of privileged work actually happens, and they run as whoever invoked them.
10173
10070
  {
10174
10071
  id: "sh-remote-script-execution",
10072
+ inherent: true,
10175
10073
  title: "network output piped into a shell",
10176
10074
  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.",
10177
10075
  cwe: "CWE-494",
@@ -10227,6 +10125,7 @@ var CODE_RULES = [
10227
10125
  },
10228
10126
  {
10229
10127
  id: "sh-insecure-transport-flag",
10128
+ inherent: true,
10230
10129
  title: "certificate verification disabled",
10231
10130
  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.",
10232
10131
  cwe: "CWE-295",
@@ -10241,6 +10140,7 @@ var CODE_RULES = [
10241
10140
  },
10242
10141
  {
10243
10142
  id: "sh-plaintext-download",
10143
+ inherent: true,
10244
10144
  title: "download over plain HTTP",
10245
10145
  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.",
10246
10146
  cwe: "CWE-319",
@@ -10252,6 +10152,7 @@ var CODE_RULES = [
10252
10152
  },
10253
10153
  {
10254
10154
  id: "sh-world-writable-permissions",
10155
+ inherent: true,
10255
10156
  title: "world-writable permissions",
10256
10157
  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.",
10257
10158
  cwe: "CWE-732",
@@ -10369,6 +10270,91 @@ var CODE_RULES = [
10369
10270
  languages: ["php"],
10370
10271
  pattern: /\bextract\s*\(\s*\$_(?:GET|POST|REQUEST|COOKIE)\b|\bimport_request_variables\s*\(/,
10371
10272
  guard: false
10273
+ },
10274
+ // ── Java and Go: classes the other languages already had ─────────────────
10275
+ //
10276
+ // Command injection, SSRF and path traversal were implemented for JavaScript
10277
+ // and, in part, for Go, and never for Java — so the same defect in the same
10278
+ // codebase was reported or not depending on which file it lived in. TLS
10279
+ // verification, weak hashing and insecure randomness are deliberately absent
10280
+ // here: `tls-verification-disabled`, `weak-hash-on-credential` and
10281
+ // `insecure-randomness-for-secret` are language-agnostic and already cover
10282
+ // both, including Go's `InsecureSkipVerify` and Java's `MessageDigest`.
10283
+ {
10284
+ id: "java-runtime-exec-concatenation",
10285
+ title: "Runtime.exec with a concatenated command",
10286
+ 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.",
10287
+ cwe: "CWE-78",
10288
+ severity: "critical",
10289
+ languages: ["java"],
10290
+ // The array form — `exec(new String[]{"git", arg})` — passes argv and is
10291
+ // the fix, so it is not matched: the `+` has to be inside the string
10292
+ // argument for this to fire.
10293
+ pattern: /\b(?:Runtime\s*\.\s*getRuntime\s*\(\s*\)\s*\.\s*exec|ProcessBuilder)\s*\(\s*(?:"[^"\n]*"\s*\+|\w+\s*\+\s*")/
10294
+ },
10295
+ {
10296
+ id: "java-ssrf-outbound-request",
10297
+ title: "outbound request to a computed URL",
10298
+ 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.",
10299
+ cwe: "CWE-918",
10300
+ severity: "high",
10301
+ languages: ["java"],
10302
+ 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)]/,
10303
+ needsContext: true
10304
+ },
10305
+ {
10306
+ id: "java-request-path-traversal",
10307
+ title: "file path built from request data",
10308
+ 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.",
10309
+ cwe: "CWE-22",
10310
+ severity: "high",
10311
+ languages: ["java"],
10312
+ 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]*\+/,
10313
+ // `getCanonicalPath().startsWith(base)` is the check that makes this safe,
10314
+ // and it is normally a line or two below the construction.
10315
+ guard: /getCanonicalPath|toRealPath|normalize\s*\(\s*\)|\bstartsWith\s*\(/,
10316
+ guardForward: 4,
10317
+ needsContext: true
10318
+ },
10319
+ {
10320
+ id: "java-broken-cipher",
10321
+ inherent: true,
10322
+ title: "broken cipher or ECB mode",
10323
+ 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.",
10324
+ cwe: "CWE-327",
10325
+ severity: "high",
10326
+ languages: ["java"],
10327
+ // Bare `"AES"` is included: the JCE resolves it to `AES/ECB/PKCS5Padding`,
10328
+ // so the default is the mode this rule exists to catch.
10329
+ 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*\)/,
10330
+ guard: false
10331
+ },
10332
+ {
10333
+ id: "go-request-path-traversal",
10334
+ title: "file path built from request data",
10335
+ consequence: "A `../` sequence in the value walks out of the intended directory, and the handler serves or writes whatever it reaches.",
10336
+ cwe: "CWE-22",
10337
+ severity: "high",
10338
+ languages: ["go"],
10339
+ 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)/,
10340
+ // `filepath.Clean` alone does not bound the result to a directory, so it is
10341
+ // not a guard here — the containment check is.
10342
+ guard: /\bstrings\s*\.\s*HasPrefix\s*\(|\bfilepath\s*\.\s*Rel\s*\(|\bfs\s*\.\s*ValidPath\s*\(|\bhttp\s*\.\s*Dir\b/,
10343
+ guardBack: 5,
10344
+ guardForward: 3,
10345
+ needsContext: true
10346
+ },
10347
+ {
10348
+ id: "go-template-escaping-bypass",
10349
+ title: "value marked as pre-escaped HTML",
10350
+ 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.",
10351
+ cwe: "CWE-79",
10352
+ severity: "high",
10353
+ languages: ["go"],
10354
+ // A conversion of a *variable*. `template.HTML("<br>")` on a literal is a
10355
+ // constant the author wrote and is not a finding.
10356
+ pattern: /\btemplate\s*\.\s*(?:HTML|JS|CSS|HTMLAttr|URL|Srcset)\s*\(\s*(?!\s*[`"])/,
10357
+ needsContext: true
10372
10358
  }
10373
10359
  ];
10374
10360
  var COMMENT_PREFIX = /^\s*(?:\/\/|\/\*|\*|#|--|<!--)/;
@@ -10419,6 +10405,9 @@ function fileTextOf(lines) {
10419
10405
  }
10420
10406
  return text;
10421
10407
  }
10408
+ function withoutSingleQuoted(text) {
10409
+ return text.replace(/'[^'\n]*'/g, "''");
10410
+ }
10422
10411
  function evaluateRule(rule, ctx) {
10423
10412
  if (rule.languages && !rule.languages.includes(ctx.language)) return null;
10424
10413
  const line = ctx.lines[ctx.index] ?? "";
@@ -10433,13 +10422,15 @@ function evaluateRule(rule, ctx) {
10433
10422
  const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
10434
10423
  if (guard && (guard.test(line) || guard.test(context))) return null;
10435
10424
  const untrusted = untrustedPatternFor(ctx.language);
10436
- const contextual = untrusted.test(line) || untrusted.test(context);
10425
+ const probeLine = ctx.language === "shell" ? withoutSingleQuoted(line) : line;
10426
+ const probeContext = ctx.language === "shell" ? withoutSingleQuoted(context) : context;
10427
+ const contextual = untrusted.test(probeLine) || untrusted.test(probeContext);
10437
10428
  if (rule.needsContext && !contextual) return null;
10438
- const confidence = contextual ? "contextual" : "pattern";
10429
+ const confidence = rule.inherent ? "evidence" : contextual ? "contextual" : "pattern";
10439
10430
  return { rule, confidence, severity: severityFor(rule.severity, confidence) };
10440
10431
  }
10441
10432
 
10442
- // src/scan/manifest-rules.ts
10433
+ // ../../packages/scan/src/manifest-rules.ts
10443
10434
  var POPULAR_NPM = [
10444
10435
  "react",
10445
10436
  "react-dom",
@@ -10714,7 +10705,7 @@ function scanRequirementsTxt(text) {
10714
10705
  return findings;
10715
10706
  }
10716
10707
 
10717
- // src/scan/secret-rules.ts
10708
+ // ../../packages/scan/src/secret-rules.ts
10718
10709
  var SECRET_RULES = [
10719
10710
  {
10720
10711
  id: "secret-aws-access-key",
@@ -10897,7 +10888,15 @@ var SENSITIVE_FILES = [
10897
10888
  // match. Reporting the file itself trades a real finding for a chore.
10898
10889
  ];
10899
10890
 
10900
- // src/scan/engine.ts
10891
+ // ../../packages/scan/src/text.ts
10892
+ function baseNameOf(path) {
10893
+ return path.slice(path.lastIndexOf("/") + 1);
10894
+ }
10895
+ function extensionOf(path) {
10896
+ const base = baseNameOf(path);
10897
+ const dot = base.lastIndexOf(".");
10898
+ return dot <= 0 ? "" : base.slice(dot);
10899
+ }
10901
10900
  var SKIP_DIRS = /* @__PURE__ */ new Set([
10902
10901
  "node_modules",
10903
10902
  ".git",
@@ -11006,7 +11005,7 @@ var LANGUAGE_BY_EXTENSION = {
11006
11005
  };
11007
11006
  function languageOf(filename) {
11008
11007
  if (filename.startsWith(".env") || filename.endsWith(".env")) return "config";
11009
- return LANGUAGE_BY_EXTENSION[(0, import_node_path3.extname)(filename).toLowerCase()] ?? "other";
11008
+ return LANGUAGE_BY_EXTENSION[extensionOf(filename).toLowerCase()] ?? "other";
11010
11009
  }
11011
11010
  var LANGUAGE_BY_INTERPRETER = {
11012
11011
  sh: "shell",
@@ -11026,10 +11025,12 @@ var LANGUAGE_BY_INTERPRETER = {
11026
11025
  php: "php"
11027
11026
  };
11028
11027
  function languageOfShebang(firstLine) {
11029
- const match = /^#!\s*(\S+)(?:\s+(\S+))?/.exec(firstLine);
11028
+ const match = /^#!\s*(\S+)(?:\s+(.+?))?\s*$/.exec(firstLine);
11030
11029
  if (!match) return null;
11031
- const command = (0, import_node_path3.basename)(match[1]);
11032
- const name = command === "env" && match[2] ? (0, import_node_path3.basename)(match[2]) : command;
11030
+ const command = baseNameOf(match[1]);
11031
+ const args = match[2]?.trim().split(/\s+/) ?? [];
11032
+ const splitString = args[0] === "-S" || args[0] === "--split-string";
11033
+ const name = command === "env" ? baseNameOf(args[splitString ? 1 : 0] ?? "") : command;
11033
11034
  const exact = LANGUAGE_BY_INTERPRETER[name];
11034
11035
  if (exact) return exact;
11035
11036
  const stripped = name.replace(/[\d.]+$/, "");
@@ -11131,6 +11132,15 @@ function scanManifest(relativePath, filename, text) {
11131
11132
  category: "manifest"
11132
11133
  }));
11133
11134
  }
11135
+ function meetsFailThreshold(findings, threshold) {
11136
+ if (threshold.length === 0) return false;
11137
+ const floor = Math.min(...threshold.map(severityRank));
11138
+ return findings.some((finding) => severityRank(finding.severity) >= floor);
11139
+ }
11140
+
11141
+ // ../../packages/scan/src/node/walk.ts
11142
+ var import_node_fs5 = require("fs");
11143
+ var import_node_path2 = require("path");
11134
11144
  function scanPath(targetPath, options = {}) {
11135
11145
  const maxFileBytes = options.maxFileBytes ?? 1024 * 1024;
11136
11146
  const allowed = options.categories ? new Set(options.categories) : null;
@@ -11140,15 +11150,15 @@ function scanPath(targetPath, options = {}) {
11140
11150
  let suppressed = 0;
11141
11151
  const rootIsDirectory = (() => {
11142
11152
  try {
11143
- return (0, import_node_fs6.statSync)(targetPath).isDirectory();
11153
+ return (0, import_node_fs5.statSync)(targetPath).isDirectory();
11144
11154
  } catch {
11145
11155
  return true;
11146
11156
  }
11147
11157
  })();
11148
- const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path3.dirname)(targetPath);
11158
+ const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path2.dirname)(targetPath);
11149
11159
  const scanFile = (fullPath, filename) => {
11150
11160
  const relativePath = toRelative(walkRoot, fullPath);
11151
- const extension = (0, import_node_path3.extname)(filename).toLowerCase();
11161
+ const extension = (0, import_node_path2.extname)(filename).toLowerCase();
11152
11162
  const isManifest = filename === "package.json" || filename === "requirements.txt";
11153
11163
  const scannable = SCAN_EXTENSIONS.has(extension) || filename.startsWith(".env");
11154
11164
  const mayDeclareInterpreter = !scannable && !isManifest && extension === "";
@@ -11160,26 +11170,26 @@ function scanPath(targetPath, options = {}) {
11160
11170
  let handle;
11161
11171
  let declared = null;
11162
11172
  try {
11163
- handle = (0, import_node_fs6.openSync)(fullPath, "r");
11173
+ handle = (0, import_node_fs5.openSync)(fullPath, "r");
11164
11174
  } catch {
11165
11175
  unreadable.push(relativePath);
11166
11176
  return;
11167
11177
  }
11168
11178
  try {
11169
- if ((0, import_node_fs6.fstatSync)(handle).size > maxFileBytes) return;
11179
+ if ((0, import_node_fs5.fstatSync)(handle).size > maxFileBytes) return;
11170
11180
  if (mayDeclareInterpreter) {
11171
11181
  const prefix = Buffer.alloc(128);
11172
- const read = (0, import_node_fs6.readSync)(handle, prefix, 0, prefix.length, 0);
11182
+ const read = (0, import_node_fs5.readSync)(handle, prefix, 0, prefix.length, 0);
11173
11183
  declared = languageOfShebang(prefix.subarray(0, read).toString("utf-8").split("\n", 1)[0] ?? "");
11174
11184
  if (!declared) return;
11175
11185
  }
11176
- text = (0, import_node_fs6.readFileSync)(handle, "utf-8");
11186
+ text = (0, import_node_fs5.readFileSync)(handle, "utf-8");
11177
11187
  } catch {
11178
11188
  unreadable.push(relativePath);
11179
11189
  return;
11180
11190
  } finally {
11181
11191
  try {
11182
- (0, import_node_fs6.closeSync)(handle);
11192
+ (0, import_node_fs5.closeSync)(handle);
11183
11193
  } catch {
11184
11194
  }
11185
11195
  }
@@ -11196,13 +11206,13 @@ function scanPath(targetPath, options = {}) {
11196
11206
  const walk = (currentPath) => {
11197
11207
  let entries;
11198
11208
  try {
11199
- entries = (0, import_node_fs6.readdirSync)(currentPath, { withFileTypes: true });
11209
+ entries = (0, import_node_fs5.readdirSync)(currentPath, { withFileTypes: true });
11200
11210
  } catch {
11201
11211
  unreadable.push(toRelative(walkRoot, currentPath));
11202
11212
  return;
11203
11213
  }
11204
11214
  for (const entry of entries) {
11205
- const fullPath = (0, import_node_path3.join)(currentPath, entry.name);
11215
+ const fullPath = (0, import_node_path2.join)(currentPath, entry.name);
11206
11216
  if (entry.isDirectory()) {
11207
11217
  if (SKIP_DIRS.has(entry.name)) continue;
11208
11218
  walk(fullPath);
@@ -11215,7 +11225,7 @@ function scanPath(targetPath, options = {}) {
11215
11225
  if (rootIsDirectory) {
11216
11226
  walk(targetPath);
11217
11227
  } else {
11218
- scanFile(targetPath, (0, import_node_path3.basename)(targetPath));
11228
+ scanFile(targetPath, (0, import_node_path2.basename)(targetPath));
11219
11229
  }
11220
11230
  const filtered = allowed ? findings.filter((f) => allowed.has(f.category)) : findings;
11221
11231
  filtered.sort(
@@ -11246,16 +11256,111 @@ function recordSensitiveFile(filename, relativePath, sink, fileFindings) {
11246
11256
  }
11247
11257
  }
11248
11258
  function toRelative(base, target) {
11249
- const rel = (0, import_node_path3.relative)(base, target);
11250
- return (rel === "" ? target : rel).split(import_node_path3.sep).join("/");
11259
+ const rel = (0, import_node_path2.relative)(base, target);
11260
+ return (rel === "" ? target : rel).split(import_node_path2.sep).join("/");
11251
11261
  }
11252
- function meetsFailThreshold(findings, threshold) {
11253
- if (threshold.length === 0) return false;
11254
- const floor = Math.min(...threshold.map(severityRank));
11255
- return findings.some((finding) => severityRank(finding.severity) >= floor);
11262
+
11263
+ // ../../packages/scan/src/node/dependencies.ts
11264
+ var import_node_fs6 = require("fs");
11265
+ var import_node_path3 = require("path");
11266
+ var LOCKFILES = [
11267
+ { file: "package-lock.json", ecosystem: "npm" },
11268
+ { file: "pnpm-lock.yaml", ecosystem: "npm" },
11269
+ { file: "yarn.lock", ecosystem: "npm" },
11270
+ { file: "requirements.txt", ecosystem: "PyPI" },
11271
+ { file: "Pipfile.lock", ecosystem: "PyPI" }
11272
+ ];
11273
+ var MAX_DEPS_PER_LOCKFILE = 50;
11274
+ async function scanDependencies(targetPath) {
11275
+ const findings = [];
11276
+ for (const { file, ecosystem } of LOCKFILES) {
11277
+ const lockPath = (0, import_node_path3.join)(targetPath, file);
11278
+ if (!(0, import_node_fs6.existsSync)(lockPath)) continue;
11279
+ let deps;
11280
+ try {
11281
+ deps = parseDependencies(lockPath, file);
11282
+ } catch {
11283
+ continue;
11284
+ }
11285
+ for (const dep of deps.slice(0, MAX_DEPS_PER_LOCKFILE)) {
11286
+ let vulns;
11287
+ try {
11288
+ vulns = await queryOsv(dep.name, dep.version, ecosystem);
11289
+ } catch {
11290
+ continue;
11291
+ }
11292
+ for (const vuln of vulns) {
11293
+ const cvss = vuln.severity?.find((entry) => entry.type === "CVSS_V3")?.score;
11294
+ findings.push({
11295
+ ruleId: "dependency-known-vulnerability",
11296
+ title: "Dependency CVE",
11297
+ file,
11298
+ line: 1,
11299
+ severity: severityFromCvss(cvss),
11300
+ confidence: "evidence",
11301
+ message: `${dep.name}@${dep.version}: ${vuln.summary ?? vuln.id}`,
11302
+ consequence: "A published advisory exists for the exact version resolved in this lockfile.",
11303
+ excerpt: `${vuln.id}${cvss ? ` (CVSS: ${cvss})` : ""}`,
11304
+ category: "dependency"
11305
+ });
11306
+ }
11307
+ }
11308
+ }
11309
+ return findings;
11310
+ }
11311
+ function severityFromCvss(score) {
11312
+ if (!score) return "medium";
11313
+ const value = Number.parseFloat(score);
11314
+ if (Number.isNaN(value)) return "medium";
11315
+ if (value >= 9) return "critical";
11316
+ if (value >= 7) return "high";
11317
+ if (value >= 4) return "medium";
11318
+ return "low";
11319
+ }
11320
+ function parseDependencies(lockPath, filename) {
11321
+ const deps = [];
11322
+ if (filename === "package-lock.json") {
11323
+ const lock = JSON.parse((0, import_node_fs6.readFileSync)(lockPath, "utf-8"));
11324
+ const packages = lock.packages ?? lock.dependencies ?? {};
11325
+ for (const [key, value] of Object.entries(packages)) {
11326
+ const name = key.replace(/^node_modules\//, "");
11327
+ const version = value?.version;
11328
+ if (name && version && !name.startsWith(".")) deps.push({ name, version });
11329
+ }
11330
+ return deps;
11331
+ }
11332
+ if (filename === "requirements.txt") {
11333
+ for (const line of (0, import_node_fs6.readFileSync)(lockPath, "utf-8").split("\n")) {
11334
+ const match = /^([a-zA-Z0-9_.-]+)==([0-9.]+)/.exec(line);
11335
+ if (match?.[1] && match[2]) deps.push({ name: match[1], version: match[2] });
11336
+ }
11337
+ }
11338
+ return deps;
11339
+ }
11340
+ function isValidPackageName(name) {
11341
+ return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
11342
+ }
11343
+ function isValidVersion(version) {
11344
+ return /^[0-9a-zA-Z._\-+]{1,50}$/.test(version);
11345
+ }
11346
+ async function queryOsv(name, version, ecosystem) {
11347
+ if (!isValidPackageName(name) || !isValidVersion(version)) return [];
11348
+ try {
11349
+ const response = await fetch("https://api.osv.dev/v1/query", {
11350
+ method: "POST",
11351
+ headers: { "Content-Type": "application/json" },
11352
+ body: JSON.stringify({ package: { name, ecosystem }, version }),
11353
+ signal: AbortSignal.timeout(5e3)
11354
+ });
11355
+ if (!response.ok) return [];
11356
+ const data = await response.json();
11357
+ return data.vulns ?? [];
11358
+ } catch {
11359
+ return [];
11360
+ }
11256
11361
  }
11257
11362
 
11258
- // src/scan/sarif.ts
11363
+ // ../../packages/scan/src/node/sarif.ts
11259
11364
  var import_node_crypto = require("crypto");
11260
11365
  var import_node_path4 = require("path");
11261
11366
  var FINGERPRINT_KEY = "threatcrush/contentHash/v1";