@profullstack/threatcrush 0.7.0 → 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",
@@ -5147,6 +5048,7 @@ var CODE_RULES = [
5147
5048
  },
5148
5049
  {
5149
5050
  id: "java-broken-cipher",
5051
+ inherent: true,
5150
5052
  title: "broken cipher or ECB mode",
5151
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.",
5152
5054
  cwe: "CWE-327",
@@ -5233,6 +5135,9 @@ function fileTextOf(lines) {
5233
5135
  }
5234
5136
  return text;
5235
5137
  }
5138
+ function withoutSingleQuoted(text) {
5139
+ return text.replace(/'[^'\n]*'/g, "''");
5140
+ }
5236
5141
  function evaluateRule(rule, ctx) {
5237
5142
  if (rule.languages && !rule.languages.includes(ctx.language)) return null;
5238
5143
  const line = ctx.lines[ctx.index] ?? "";
@@ -5247,13 +5152,15 @@ function evaluateRule(rule, ctx) {
5247
5152
  const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
5248
5153
  if (guard && (guard.test(line) || guard.test(context))) return null;
5249
5154
  const untrusted = untrustedPatternFor(ctx.language);
5250
- 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);
5251
5158
  if (rule.needsContext && !contextual) return null;
5252
- const confidence = contextual ? "contextual" : "pattern";
5159
+ const confidence = rule.inherent ? "evidence" : contextual ? "contextual" : "pattern";
5253
5160
  return { rule, confidence, severity: severityFor(rule.severity, confidence) };
5254
5161
  }
5255
5162
 
5256
- // src/scan/manifest-rules.ts
5163
+ // ../../packages/scan/src/manifest-rules.ts
5257
5164
  var POPULAR_NPM = [
5258
5165
  "react",
5259
5166
  "react-dom",
@@ -5528,7 +5435,7 @@ function scanRequirementsTxt(text) {
5528
5435
  return findings;
5529
5436
  }
5530
5437
 
5531
- // src/scan/secret-rules.ts
5438
+ // ../../packages/scan/src/secret-rules.ts
5532
5439
  var SECRET_RULES = [
5533
5440
  {
5534
5441
  id: "secret-aws-access-key",
@@ -5711,7 +5618,15 @@ var SENSITIVE_FILES = [
5711
5618
  // match. Reporting the file itself trades a real finding for a chore.
5712
5619
  ];
5713
5620
 
5714
- // 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
+ }
5715
5630
  var SKIP_DIRS = /* @__PURE__ */ new Set([
5716
5631
  "node_modules",
5717
5632
  ".git",
@@ -5820,7 +5735,7 @@ var LANGUAGE_BY_EXTENSION = {
5820
5735
  };
5821
5736
  function languageOf(filename) {
5822
5737
  if (filename.startsWith(".env") || filename.endsWith(".env")) return "config";
5823
- return LANGUAGE_BY_EXTENSION[(0, import_node_path6.extname)(filename).toLowerCase()] ?? "other";
5738
+ return LANGUAGE_BY_EXTENSION[extensionOf(filename).toLowerCase()] ?? "other";
5824
5739
  }
5825
5740
  var LANGUAGE_BY_INTERPRETER = {
5826
5741
  sh: "shell",
@@ -5840,10 +5755,12 @@ var LANGUAGE_BY_INTERPRETER = {
5840
5755
  php: "php"
5841
5756
  };
5842
5757
  function languageOfShebang(firstLine) {
5843
- const match = /^#!\s*(\S+)(?:\s+(\S+))?/.exec(firstLine);
5758
+ const match = /^#!\s*(\S+)(?:\s+(.+?))?\s*$/.exec(firstLine);
5844
5759
  if (!match) return null;
5845
- const command = (0, import_node_path6.basename)(match[1]);
5846
- 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;
5847
5764
  const exact = LANGUAGE_BY_INTERPRETER[name];
5848
5765
  if (exact) return exact;
5849
5766
  const stripped = name.replace(/[\d.]+$/, "");
@@ -5945,6 +5862,10 @@ function scanManifest(relativePath, filename, text) {
5945
5862
  category: "manifest"
5946
5863
  }));
5947
5864
  }
5865
+
5866
+ // ../../packages/scan/src/node/walk.ts
5867
+ var import_node_fs10 = require("fs");
5868
+ var import_node_path5 = require("path");
5948
5869
  function scanPath(targetPath, options = {}) {
5949
5870
  const maxFileBytes = options.maxFileBytes ?? 1024 * 1024;
5950
5871
  const allowed = options.categories ? new Set(options.categories) : null;
@@ -5954,15 +5875,15 @@ function scanPath(targetPath, options = {}) {
5954
5875
  let suppressed = 0;
5955
5876
  const rootIsDirectory = (() => {
5956
5877
  try {
5957
- return (0, import_node_fs11.statSync)(targetPath).isDirectory();
5878
+ return (0, import_node_fs10.statSync)(targetPath).isDirectory();
5958
5879
  } catch {
5959
5880
  return true;
5960
5881
  }
5961
5882
  })();
5962
- const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path6.dirname)(targetPath);
5883
+ const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path5.dirname)(targetPath);
5963
5884
  const scanFile = (fullPath, filename) => {
5964
5885
  const relativePath = toRelative(walkRoot, fullPath);
5965
- const extension = (0, import_node_path6.extname)(filename).toLowerCase();
5886
+ const extension = (0, import_node_path5.extname)(filename).toLowerCase();
5966
5887
  const isManifest = filename === "package.json" || filename === "requirements.txt";
5967
5888
  const scannable = SCAN_EXTENSIONS.has(extension) || filename.startsWith(".env");
5968
5889
  const mayDeclareInterpreter = !scannable && !isManifest && extension === "";
@@ -5974,26 +5895,26 @@ function scanPath(targetPath, options = {}) {
5974
5895
  let handle;
5975
5896
  let declared = null;
5976
5897
  try {
5977
- handle = (0, import_node_fs11.openSync)(fullPath, "r");
5898
+ handle = (0, import_node_fs10.openSync)(fullPath, "r");
5978
5899
  } catch {
5979
5900
  unreadable.push(relativePath);
5980
5901
  return;
5981
5902
  }
5982
5903
  try {
5983
- if ((0, import_node_fs11.fstatSync)(handle).size > maxFileBytes) return;
5904
+ if ((0, import_node_fs10.fstatSync)(handle).size > maxFileBytes) return;
5984
5905
  if (mayDeclareInterpreter) {
5985
5906
  const prefix = Buffer.alloc(128);
5986
- 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);
5987
5908
  declared = languageOfShebang(prefix.subarray(0, read).toString("utf-8").split("\n", 1)[0] ?? "");
5988
5909
  if (!declared) return;
5989
5910
  }
5990
- text = (0, import_node_fs11.readFileSync)(handle, "utf-8");
5911
+ text = (0, import_node_fs10.readFileSync)(handle, "utf-8");
5991
5912
  } catch {
5992
5913
  unreadable.push(relativePath);
5993
5914
  return;
5994
5915
  } finally {
5995
5916
  try {
5996
- (0, import_node_fs11.closeSync)(handle);
5917
+ (0, import_node_fs10.closeSync)(handle);
5997
5918
  } catch {
5998
5919
  }
5999
5920
  }
@@ -6010,13 +5931,13 @@ function scanPath(targetPath, options = {}) {
6010
5931
  const walk = (currentPath) => {
6011
5932
  let entries;
6012
5933
  try {
6013
- entries = (0, import_node_fs11.readdirSync)(currentPath, { withFileTypes: true });
5934
+ entries = (0, import_node_fs10.readdirSync)(currentPath, { withFileTypes: true });
6014
5935
  } catch {
6015
5936
  unreadable.push(toRelative(walkRoot, currentPath));
6016
5937
  return;
6017
5938
  }
6018
5939
  for (const entry of entries) {
6019
- const fullPath = (0, import_node_path6.join)(currentPath, entry.name);
5940
+ const fullPath = (0, import_node_path5.join)(currentPath, entry.name);
6020
5941
  if (entry.isDirectory()) {
6021
5942
  if (SKIP_DIRS.has(entry.name)) continue;
6022
5943
  walk(fullPath);
@@ -6029,7 +5950,7 @@ function scanPath(targetPath, options = {}) {
6029
5950
  if (rootIsDirectory) {
6030
5951
  walk(targetPath);
6031
5952
  } else {
6032
- scanFile(targetPath, (0, import_node_path6.basename)(targetPath));
5953
+ scanFile(targetPath, (0, import_node_path5.basename)(targetPath));
6033
5954
  }
6034
5955
  const filtered = allowed ? findings.filter((f) => allowed.has(f.category)) : findings;
6035
5956
  filtered.sort(
@@ -6060,11 +5981,111 @@ function recordSensitiveFile(filename, relativePath, sink, fileFindings) {
6060
5981
  }
6061
5982
  }
6062
5983
  function toRelative(base, target) {
6063
- const rel = (0, import_node_path6.relative)(base, target);
6064
- 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
+ }
6065
6086
  }
6066
6087
 
6067
- // src/scan/sarif.ts
6088
+ // ../../packages/scan/src/node/sarif.ts
6068
6089
  var import_node_crypto = require("crypto");
6069
6090
  var import_node_path7 = require("path");
6070
6091