@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 +139 -1
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +155 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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;
|