@profullstack/threatcrush 0.11.3 → 0.11.4

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
@@ -2150,6 +2150,9 @@ bus.setMaxListeners(50);
2150
2150
  var import_better_sqlite3 = __toESM(require("better-sqlite3"));
2151
2151
  var db = null;
2152
2152
  var dbUnavailable = false;
2153
+ function isStateDbAvailable() {
2154
+ return db !== null;
2155
+ }
2153
2156
  function initStateDB(dbPath = "/var/lib/threatcrush/state.db") {
2154
2157
  if (db) return db;
2155
2158
  if (dbUnavailable) {
@@ -2427,7 +2430,8 @@ var IpcServer = class {
2427
2430
  socket: PATHS.socket
2428
2431
  },
2429
2432
  modules: this.moduleHost.summary(),
2430
- counters: { ...this.counters }
2433
+ counters: { ...this.counters },
2434
+ stateDb: isStateDbAvailable()
2431
2435
  };
2432
2436
  return this.send(client, { id: req.id, ok: true, result: status });
2433
2437
  }
@@ -4979,7 +4983,12 @@ var CODE_RULES = [
4979
4983
  severity: "critical",
4980
4984
  languages: ["javascript", "typescript"],
4981
4985
  pattern: /\b(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+|[a-zA-Z_$][\w$]*\s*\+)/,
4982
- constantInterpolationGuard: true
4986
+ constantInterpolationGuard: true,
4987
+ // Interpolation is common in locally run CLI and installer code. Report
4988
+ // command execution as an injection vulnerability only when the nearby
4989
+ // code shows a caller-controlled source, rather than treating every
4990
+ // internally assembled command as attacker input.
4991
+ needsContext: true
4983
4992
  },
4984
4993
  {
4985
4994
  id: "py-shell-command-string",
@@ -5117,7 +5126,8 @@ var CODE_RULES = [
5117
5126
  * and the negative lookahead succeeds — reinstating every finding the
5118
5127
  * exemption was written to remove.
5119
5128
  */
5120
- pattern: /\bdangerouslySetInnerHTML\s*=|\.\s*(?:innerHTML|outerHTML)\s*=(?!\s*(?:'[^'\\\n]*'|"[^"\\\n]*"|`[^`$\\\n]*`)\s*(?:;|$))|\bdocument\s*\.\s*write(?:ln)?\s*\(|\.\s*insertAdjacentHTML\s*\(/
5129
+ pattern: /\bdangerouslySetInnerHTML\s*=|\.\s*(?:innerHTML|outerHTML)\s*=(?!\s*(?:'[^'\\\n]*'|"[^"\\\n]*"|`[^`$\\\n]*`)\s*(?:;|$))|\bdocument\s*\.\s*write(?:ln)?\s*\(|\.\s*insertAdjacentHTML\s*\(/,
5130
+ sanitizedHtmlGuard: true
5121
5131
  },
5122
5132
  {
5123
5133
  id: "java-html-writer-concatenation",
@@ -5196,7 +5206,8 @@ var CODE_RULES = [
5196
5206
  severity: "medium",
5197
5207
  languages: ["javascript", "typescript"],
5198
5208
  pattern: /\b(?:res|response)\s*\.\s*redirect\s*\(\s*[a-zA-Z_$][\w$]*\s*\)|\bwindow\s*\.\s*location(?:\s*\.\s*(?:href|replace))?\s*(?:=\s*[a-zA-Z_$]|\(\s*[a-zA-Z_$][\w$]*\s*\))/,
5199
- needsContext: true
5209
+ needsContext: true,
5210
+ safeRedirectGuard: true
5200
5211
  },
5201
5212
  // ── Deserialisation ──────────────────────────────────────────────────────
5202
5213
  {
@@ -5396,7 +5407,7 @@ var CODE_RULES = [
5396
5407
  // the pattern is written. `other` also covers C++, Rust and Zig, which
5397
5408
  // share the cast-then-deref spelling.
5398
5409
  languages: ["javascript", "typescript", "python", "ruby", "go", "java", "php"],
5399
- pattern: /\([^)\n]*[+*]\s*\)\s*[+*]|\([^)\n]*\{\d+,\}\s*\)\s*[+*{]/
5410
+ pattern: /(?:^|[=(:,]\s*)\/(?:\\.|[^/\\\n])*\([^()\n]*[+*][^()\n]*\)\s*(?:[+*]|\{\d+,\})|\b(?:new\s+RegExp|RegExp|re\.compile|regexp\.(?:Compile|MustCompile)|Pattern\.compile)\s*\(\s*(?:r)?["'][^"'\n]*\([^()\n]*[+*][^()\n]*\)\s*(?:[+*]|\{\d+,\})/
5400
5411
  },
5401
5412
  // ── Temporary files ──────────────────────────────────────────────────────
5402
5413
  {
@@ -5405,10 +5416,13 @@ var CODE_RULES = [
5405
5416
  consequence: "A predictable name in a world-writable directory is a symlink attack: an attacker pre-creates the path and your process writes through it.",
5406
5417
  cwe: "CWE-377",
5407
5418
  severity: "medium",
5408
- // A hardcoded path under /tmp is the finding whether or not it is
5409
- // formatted: `"/tmp/application.log.tmp"` is worse than the PID-based one,
5410
- // because every process on the host can predict it exactly.
5411
- pattern: /\btempfile\.mktemp\s*\(|\bos\.tmpnam\s*\(|['"]\/tmp\/[^'"\n]+['"]|['"]\/tmp\/[^'"\n]*\{|\bFile\.createTempFile\s*\(/
5419
+ // `File.createTempFile` is specifically the safe Java API, and a string
5420
+ // containing `/tmp/` is not necessarily a write. Detect unsafe name
5421
+ // generators and actual writes to a literal temp path instead. Shell
5422
+ // redirections are handled by the shell-specific rule below.
5423
+ pattern: new RegExp(
5424
+ String.raw`\btempfile\.mktemp\s*\(|\bos\.tmpnam\s*\(|\b(?:writeFile(?:Sync)?|appendFile(?:Sync)?|createWriteStream|FileOutputStream|FileWriter|os\.OpenFile|open)\s*\([^\n)]*['"]\/tmp\/[^'"\n]+['"]`
5425
+ )
5412
5426
  },
5413
5427
  // ── Information exposure ─────────────────────────────────────────────────
5414
5428
  {
@@ -5461,7 +5475,6 @@ var CODE_RULES = [
5461
5475
  // of privileged work actually happens, and they run as whoever invoked them.
5462
5476
  {
5463
5477
  id: "sh-remote-script-execution",
5464
- inherent: true,
5465
5478
  title: "network output piped into a shell",
5466
5479
  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.",
5467
5480
  cwe: "CWE-494",
@@ -5470,9 +5483,13 @@ var CODE_RULES = [
5470
5483
  // The pipe must be the *next* thing: `curl -o f url && sh f` is a different
5471
5484
  // (and checkable) shape, and `curl url | jq` is not an execution at all.
5472
5485
  pattern: /\b(?:curl|wget)\b[^|\n]*\|\s*(?:sudo\s+(?:-\S+\s+)*)?(?:\/bin\/|\/usr\/bin\/)?(?:ba|da|k|z|a)?sh\b/,
5473
- // Nothing on this line can exonerate it. Integrity checking happens in a
5474
- // separate step by construction, so a window guard would only mislead.
5475
- guard: false
5486
+ // This is a supply-chain review item, not proof that the repository is
5487
+ // compromised. A literal HTTPS installer URL is capped at medium; it only
5488
+ // escalates when nearby evidence shows that input can choose the download.
5489
+ guard: false,
5490
+ // Installer instructions printed by the script do not execute. Matching
5491
+ // them made a script report its own documentation as a network pipeline.
5492
+ lineGuard: /^\s*(?:echo|printf|say|info|warn|error)\s+["'][^"'\n]*\b(?:curl|wget)\b/
5476
5493
  },
5477
5494
  {
5478
5495
  id: "sh-eval-expansion",
@@ -6048,6 +6065,57 @@ function isConstantString(name, fileText) {
6048
6065
  `\\bconst\\s+${escaped}\\s*(?::[^=\\n]+)?=\\s*(?:'[^'\\n]*'|"[^"\\n]*"|\`[^\`$\\n]*\`)`
6049
6066
  ).test(fileText);
6050
6067
  }
6068
+ function hasSanitizedHtmlValue(line, fileText) {
6069
+ const direct = /\b__html\s*:\s*(?:[A-Za-z_$][\w$]*\s*\.\s*)?(?:sanitize\w*|escape\w*|serializeJsonForHtml|renderSanitizedMarkdown)\s*\(/i;
6070
+ if (direct.test(line)) return true;
6071
+ const value = /\b__html\s*:\s*([A-Za-z_$][\w$]*)\b/.exec(line)?.[1];
6072
+ if (!value) return false;
6073
+ const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6074
+ return new RegExp(
6075
+ `\\bconst\\s+${escaped}\\s*=[^\\n;]*(?:[A-Za-z_$][\\w$]*\\s*\\.\\s*)?(?:sanitize\\w*|escape\\w*|serializeJsonForHtml|renderSanitizedMarkdown)\\s*\\(`,
6076
+ "i"
6077
+ ).test(fileText);
6078
+ }
6079
+ function isIdentifierStart(char) {
6080
+ if (!char) return false;
6081
+ const code = char.charCodeAt(0);
6082
+ return char === "$" || char === "_" || code >= 65 && code <= 90 || code >= 97 && code <= 122;
6083
+ }
6084
+ function isIdentifierPart(char) {
6085
+ if (isIdentifierStart(char)) return true;
6086
+ if (!char) return false;
6087
+ const code = char.charCodeAt(0);
6088
+ return code >= 48 && code <= 57;
6089
+ }
6090
+ function identifierAfter(text, start) {
6091
+ let index = start;
6092
+ while (text[index] === " " || text[index] === " ") index += 1;
6093
+ if (!isIdentifierStart(text[index])) return null;
6094
+ const from = index;
6095
+ while (isIdentifierPart(text[index])) index += 1;
6096
+ return text.slice(from, index);
6097
+ }
6098
+ function hasSafeRedirectValue(line, fileText) {
6099
+ const location = line.indexOf("window.location");
6100
+ const redirect = line.indexOf(".redirect");
6101
+ const valueStart = location >= 0 ? line.indexOf("=", location) + 1 : redirect >= 0 ? line.indexOf("(", redirect) + 1 : 0;
6102
+ if (valueStart === 0) return false;
6103
+ const value = identifierAfter(line, valueStart);
6104
+ if (!value) return false;
6105
+ const declaration = `const ${value}`;
6106
+ for (const candidate of fileText.split("\n")) {
6107
+ const at = candidate.indexOf(declaration);
6108
+ if (at === -1 || isIdentifierPart(candidate[at - 1]) || isIdentifierPart(candidate[at + declaration.length])) {
6109
+ continue;
6110
+ }
6111
+ const equals = candidate.indexOf("=", at + declaration.length);
6112
+ const initializer = candidate.slice(equals + 1);
6113
+ if (equals !== -1 && ["safeRedirectPath(", "safeRedirectUrl(", "safeRedirectURL("].some((call) => initializer.includes(call))) {
6114
+ return true;
6115
+ }
6116
+ }
6117
+ return false;
6118
+ }
6051
6119
  function interpolationsAreConstant(line, fileText) {
6052
6120
  const found = interpolations(line);
6053
6121
  if (!found) return false;
@@ -6112,6 +6180,12 @@ function evaluateRule(rule, ctx) {
6112
6180
  if (rule.constantInterpolationGuard && interpolationsAreConstant(line, fileTextOf(ctx.lines))) {
6113
6181
  return null;
6114
6182
  }
6183
+ if (rule.sanitizedHtmlGuard && hasSanitizedHtmlValue(line, fileTextOf(ctx.lines))) {
6184
+ return null;
6185
+ }
6186
+ if (rule.safeRedirectGuard && hasSafeRedirectValue(line, fileTextOf(ctx.lines))) {
6187
+ return null;
6188
+ }
6115
6189
  if (rule.filledBeforeUseGuard && bufferFilledBeforeUse(ctx)) return null;
6116
6190
  const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
6117
6191
  if (guard && (guard.test(line) || guard.test(context))) return null;
@@ -6477,6 +6551,10 @@ function detectTyposquat(name, ecosystem) {
6477
6551
  return null;
6478
6552
  }
6479
6553
  var LIFECYCLE_SCRIPTS = ["preinstall", "install", "postinstall", "prepare", "prepublish"];
6554
+ function isRiskyLifecycleScript(body) {
6555
+ if (typeof body !== "string") return false;
6556
+ return /\b(?:curl|wget)\b[^\n|]*\|\s*(?:\w*sh|bash|node|python)\b|\b(?:curl|wget)\b[^\n]*(?:https?:|--upload-file)|\b(?:node|python|ruby|perl)\s+-e\b|\b(?:base64|openssl)\b[^\n]*(?:-d|--decode)|\beval\s+|\bchmod\s+(?:\S+\s+)*(?:777|a\+rwx)\b/i.test(body);
6557
+ }
6480
6558
  function scanPackageJson(text) {
6481
6559
  const findings = [];
6482
6560
  const lines = text.split("\n");
@@ -6528,13 +6606,14 @@ function scanPackageJson(text) {
6528
6606
  if (scripts && typeof scripts === "object") {
6529
6607
  for (const [name, body] of Object.entries(scripts)) {
6530
6608
  if (!LIFECYCLE_SCRIPTS.includes(name)) continue;
6609
+ if (!isRiskyLifecycleScript(body)) continue;
6531
6610
  findings.push({
6532
- ruleId: "manifest-install-lifecycle-script",
6533
- title: "install-time lifecycle script",
6611
+ ruleId: "manifest-risky-install-lifecycle-script",
6612
+ title: "risky install-time lifecycle script",
6534
6613
  line: lineOf(name),
6535
6614
  severity: "medium",
6536
6615
  cwe: "CWE-506",
6537
- message: `"${name}" runs automatically on install: ${String(body).slice(0, 120)}`,
6616
+ message: `"${name}" runs automatically on install and contains a network or code-execution primitive: ${String(body).slice(0, 120)}`,
6538
6617
  consequence: "Lifecycle scripts run with the installing user\u2019s privileges and network access, before any code is reviewed. It is the execution vector every notable npm compromise has used.",
6539
6618
  excerpt: (lines[lineOf(name) - 1] ?? "").trim()
6540
6619
  });