@profullstack/threatcrush 0.7.2 → 0.9.0

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
@@ -4728,6 +4728,56 @@ var CODE_RULES = [
4728
4728
  severity: "high",
4729
4729
  pattern: /(?:token|secret|password|salt|nonce|session|otp|reset|apikey|api_key)[\w]*\s*[:=][^;\n]{0,60}(?:Math\s*\.\s*random\s*\(|\brandom\s*\.\s*(?:random|randint|choice)\s*\(|\brand\s*\()/i
4730
4730
  },
4731
+ // ── Weak crypto: Python ──────────────────────────────────────────────────
4732
+ //
4733
+ // The generic rules above catch the credential case on the matched line.
4734
+ // These cover what they miss in Python, where the security role of a value is
4735
+ // set by the enclosing function rather than a same-line assignment — a
4736
+ // `random`-drawn token that is *returned*, an MD5 used to *verify* an
4737
+ // artifact — and the broken ciphers, which have no safe use at all.
4738
+ {
4739
+ id: "py-broken-cipher",
4740
+ title: "broken cipher or ECB mode",
4741
+ consequence: "DES, RC2, RC4 and Blowfish are broken or too small to rely on, and ECB encrypts identical plaintext blocks to identical ciphertext, so structure in the data survives encryption. None of them provides the confidentiality their use implies.",
4742
+ cwe: "CWE-327",
4743
+ severity: "high",
4744
+ languages: ["python"],
4745
+ // PyCryptodome/PyCrypto constructors. The mode matters only for AES, whose
4746
+ // safe modes (GCM, CTR, CBC) are common — so AES matches solely on ECB,
4747
+ // while DES/RC4/Blowfish are broken by the algorithm regardless of mode.
4748
+ pattern: /\b(?:DES|DES3|ARC2|RC2|ARC4|RC4|Blowfish|XOR)\s*\.\s*new\s*\(|\bAES\s*\.\s*new\s*\([^)\n]*\bMODE_ECB\b/,
4749
+ inherent: true,
4750
+ guard: false
4751
+ },
4752
+ {
4753
+ id: "py-weak-hash",
4754
+ title: "broken hash algorithm",
4755
+ consequence: "MD5 and SHA-1 have practical collisions, so a digest used for integrity or a signature can be forged to match a value the code trusts.",
4756
+ cwe: "CWE-327",
4757
+ severity: "medium",
4758
+ languages: ["python"],
4759
+ pattern: /\bhashlib\s*\.\s*(?:md5|sha1)\s*\(/,
4760
+ // Python 3.9+ marks a non-security digest — a cache key, an ETag — with
4761
+ // `usedforsecurity=False`, which is exactly the "this MD5 is not a security
4762
+ // claim" signal, so it exempts the line rather than being flagged.
4763
+ lineGuard: /usedforsecurity\s*=\s*False/
4764
+ },
4765
+ {
4766
+ id: "py-predictable-random-seed",
4767
+ title: "PRNG seeded from a predictable value",
4768
+ consequence: "Seeding `random` from the clock or the process id makes its whole sequence reproducible, so anything drawn from it afterwards \u2014 a token, an id, a shuffle \u2014 can be regenerated by guessing the seed.",
4769
+ cwe: "CWE-338",
4770
+ severity: "high",
4771
+ languages: ["python"],
4772
+ // A time- or pid-derived seed, which is the predictable kind. A fixed
4773
+ // integer seed (`random.seed(42)`) is deliberate reproducibility for tests
4774
+ // and simulations, so it is left alone. This needs no credential context:
4775
+ // seeding the global PRNG from the clock is a weakness on its own terms,
4776
+ // which is why the enclosing function's name — that this engine does not
4777
+ // read as evidence anyway — is not consulted.
4778
+ pattern: /\brandom\s*\.\s*seed\s*\([^)\n]*(?:time\s*\.\s*time|datetime|\.\s*now\s*\(|getpid)/,
4779
+ inherent: true
4780
+ },
4731
4781
  {
4732
4782
  id: "redos-nested-quantifier",
4733
4783
  title: "regex with nested unbounded quantifiers",
@@ -5866,6 +5916,81 @@ function scanManifest(relativePath, filename, text) {
5866
5916
  // ../../packages/scan/src/node/walk.ts
5867
5917
  var import_node_fs10 = require("fs");
5868
5918
  var import_node_path5 = require("path");
5919
+ function compileExcludes(patterns) {
5920
+ const matchers = patterns.map((p) => p.trim()).filter((p) => p.length > 0 && !p.startsWith("#")).map(compilePattern);
5921
+ if (matchers.length === 0) return () => false;
5922
+ return (relPath) => {
5923
+ const segs = relPath.split("/");
5924
+ return matchers.some((m) => m(segs));
5925
+ };
5926
+ }
5927
+ var GLOBSTAR = /* @__PURE__ */ Symbol("globstar");
5928
+ function compilePattern(pattern) {
5929
+ const withoutLead = pattern.replace(/^\.?\//, "");
5930
+ let end = withoutLead.length;
5931
+ while (end > 0 && withoutLead[end - 1] === "/") end -= 1;
5932
+ const p = withoutLead.slice(0, end);
5933
+ if (!p.includes("/")) {
5934
+ if (p === "**") return () => true;
5935
+ const rx = segToRegExp(p);
5936
+ return (segs) => segs.some((s) => rx.test(s));
5937
+ }
5938
+ const raw = p.split("/");
5939
+ const trailingGlobstar = raw[raw.length - 1] === "**";
5940
+ const core = trailingGlobstar ? raw.slice(0, -1) : raw;
5941
+ const parts = core.map((s) => s === "**" ? GLOBSTAR : segToRegExp(s));
5942
+ return (segs) => {
5943
+ const end2 = matchPrefix(parts, segs);
5944
+ if (end2 < 0) return false;
5945
+ return trailingGlobstar ? end2 < segs.length : true;
5946
+ };
5947
+ }
5948
+ function segToRegExp(seg) {
5949
+ let body = "";
5950
+ for (let i = 0; i < seg.length; i += 1) {
5951
+ const c = seg[i];
5952
+ if (c === "*") {
5953
+ while (seg[i + 1] === "*") i += 1;
5954
+ body += "[^/]*";
5955
+ } else if (c === "?") {
5956
+ body += "[^/]";
5957
+ } else {
5958
+ body += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
5959
+ }
5960
+ }
5961
+ return new RegExp(`^${body}$`);
5962
+ }
5963
+ function matchPrefix(parts, segs) {
5964
+ let pi = 0;
5965
+ let si = 0;
5966
+ let star = -1;
5967
+ let starSi = -1;
5968
+ while (pi < parts.length) {
5969
+ const part = parts[pi];
5970
+ if (part === GLOBSTAR) {
5971
+ star = pi;
5972
+ starSi = si;
5973
+ pi += 1;
5974
+ } else if (si < segs.length && part.test(segs[si])) {
5975
+ pi += 1;
5976
+ si += 1;
5977
+ } else if (star !== -1 && starSi < segs.length) {
5978
+ starSi += 1;
5979
+ si = starSi;
5980
+ pi = star + 1;
5981
+ } else {
5982
+ return -1;
5983
+ }
5984
+ }
5985
+ return si;
5986
+ }
5987
+ function readIgnoreFile(root) {
5988
+ try {
5989
+ return (0, import_node_fs10.readFileSync)((0, import_node_path5.join)(root, ".threatcrushignore"), "utf-8").split("\n");
5990
+ } catch {
5991
+ return [];
5992
+ }
5993
+ }
5869
5994
  function scanPath(targetPath, options = {}) {
5870
5995
  const maxFileBytes = options.maxFileBytes ?? 1024 * 1024;
5871
5996
  const allowed = options.categories ? new Set(options.categories) : null;
@@ -5873,6 +5998,7 @@ function scanPath(targetPath, options = {}) {
5873
5998
  const unreadable = [];
5874
5999
  let filesScanned = 0;
5875
6000
  let suppressed = 0;
6001
+ let excluded = 0;
5876
6002
  const rootIsDirectory = (() => {
5877
6003
  try {
5878
6004
  return (0, import_node_fs10.statSync)(targetPath).isDirectory();
@@ -5881,6 +6007,7 @@ function scanPath(targetPath, options = {}) {
5881
6007
  }
5882
6008
  })();
5883
6009
  const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path5.dirname)(targetPath);
6010
+ const isExcluded = compileExcludes([...options.exclude ?? [], ...readIgnoreFile(walkRoot)]);
5884
6011
  const scanFile = (fullPath, filename) => {
5885
6012
  const relativePath = toRelative(walkRoot, fullPath);
5886
6013
  const extension = (0, import_node_path5.extname)(filename).toLowerCase();
@@ -5938,17 +6065,28 @@ function scanPath(targetPath, options = {}) {
5938
6065
  }
5939
6066
  for (const entry of entries) {
5940
6067
  const fullPath = (0, import_node_path5.join)(currentPath, entry.name);
6068
+ const relativePath = toRelative(walkRoot, fullPath);
5941
6069
  if (entry.isDirectory()) {
5942
6070
  if (SKIP_DIRS.has(entry.name)) continue;
6071
+ if (isExcluded(relativePath)) {
6072
+ excluded += 1;
6073
+ continue;
6074
+ }
5943
6075
  walk(fullPath);
5944
6076
  continue;
5945
6077
  }
5946
6078
  if (!entry.isFile()) continue;
6079
+ if (isExcluded(relativePath)) {
6080
+ excluded += 1;
6081
+ continue;
6082
+ }
5947
6083
  scanFile(fullPath, entry.name);
5948
6084
  }
5949
6085
  };
5950
6086
  if (rootIsDirectory) {
5951
6087
  walk(targetPath);
6088
+ } else if (isExcluded(toRelative(walkRoot, targetPath))) {
6089
+ excluded += 1;
5952
6090
  } else {
5953
6091
  scanFile(targetPath, (0, import_node_path5.basename)(targetPath));
5954
6092
  }
@@ -5956,7 +6094,7 @@ function scanPath(targetPath, options = {}) {
5956
6094
  filtered.sort(
5957
6095
  (a, b) => severityRank(b.severity) - severityRank(a.severity) || a.file.localeCompare(b.file) || a.line - b.line
5958
6096
  );
5959
- return { findings: filtered, filesScanned, unreadable, suppressed, root: walkRoot };
6097
+ return { findings: filtered, filesScanned, unreadable, suppressed, excluded, root: walkRoot };
5960
6098
  }
5961
6099
  function recordSensitiveFile(filename, relativePath, sink, fileFindings) {
5962
6100
  if (fileFindings.length > 0) return;