@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/index.js
CHANGED
|
@@ -9998,6 +9998,56 @@ var CODE_RULES = [
|
|
|
9998
9998
|
severity: "high",
|
|
9999
9999
|
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
|
|
10000
10000
|
},
|
|
10001
|
+
// ── Weak crypto: Python ──────────────────────────────────────────────────
|
|
10002
|
+
//
|
|
10003
|
+
// The generic rules above catch the credential case on the matched line.
|
|
10004
|
+
// These cover what they miss in Python, where the security role of a value is
|
|
10005
|
+
// set by the enclosing function rather than a same-line assignment — a
|
|
10006
|
+
// `random`-drawn token that is *returned*, an MD5 used to *verify* an
|
|
10007
|
+
// artifact — and the broken ciphers, which have no safe use at all.
|
|
10008
|
+
{
|
|
10009
|
+
id: "py-broken-cipher",
|
|
10010
|
+
title: "broken cipher or ECB mode",
|
|
10011
|
+
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.",
|
|
10012
|
+
cwe: "CWE-327",
|
|
10013
|
+
severity: "high",
|
|
10014
|
+
languages: ["python"],
|
|
10015
|
+
// PyCryptodome/PyCrypto constructors. The mode matters only for AES, whose
|
|
10016
|
+
// safe modes (GCM, CTR, CBC) are common — so AES matches solely on ECB,
|
|
10017
|
+
// while DES/RC4/Blowfish are broken by the algorithm regardless of mode.
|
|
10018
|
+
pattern: /\b(?:DES|DES3|ARC2|RC2|ARC4|RC4|Blowfish|XOR)\s*\.\s*new\s*\(|\bAES\s*\.\s*new\s*\([^)\n]*\bMODE_ECB\b/,
|
|
10019
|
+
inherent: true,
|
|
10020
|
+
guard: false
|
|
10021
|
+
},
|
|
10022
|
+
{
|
|
10023
|
+
id: "py-weak-hash",
|
|
10024
|
+
title: "broken hash algorithm",
|
|
10025
|
+
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.",
|
|
10026
|
+
cwe: "CWE-327",
|
|
10027
|
+
severity: "medium",
|
|
10028
|
+
languages: ["python"],
|
|
10029
|
+
pattern: /\bhashlib\s*\.\s*(?:md5|sha1)\s*\(/,
|
|
10030
|
+
// Python 3.9+ marks a non-security digest — a cache key, an ETag — with
|
|
10031
|
+
// `usedforsecurity=False`, which is exactly the "this MD5 is not a security
|
|
10032
|
+
// claim" signal, so it exempts the line rather than being flagged.
|
|
10033
|
+
lineGuard: /usedforsecurity\s*=\s*False/
|
|
10034
|
+
},
|
|
10035
|
+
{
|
|
10036
|
+
id: "py-predictable-random-seed",
|
|
10037
|
+
title: "PRNG seeded from a predictable value",
|
|
10038
|
+
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.",
|
|
10039
|
+
cwe: "CWE-338",
|
|
10040
|
+
severity: "high",
|
|
10041
|
+
languages: ["python"],
|
|
10042
|
+
// A time- or pid-derived seed, which is the predictable kind. A fixed
|
|
10043
|
+
// integer seed (`random.seed(42)`) is deliberate reproducibility for tests
|
|
10044
|
+
// and simulations, so it is left alone. This needs no credential context:
|
|
10045
|
+
// seeding the global PRNG from the clock is a weakness on its own terms,
|
|
10046
|
+
// which is why the enclosing function's name — that this engine does not
|
|
10047
|
+
// read as evidence anyway — is not consulted.
|
|
10048
|
+
pattern: /\brandom\s*\.\s*seed\s*\([^)\n]*(?:time\s*\.\s*time|datetime|\.\s*now\s*\(|getpid)/,
|
|
10049
|
+
inherent: true
|
|
10050
|
+
},
|
|
10001
10051
|
{
|
|
10002
10052
|
id: "redos-nested-quantifier",
|
|
10003
10053
|
title: "regex with nested unbounded quantifiers",
|
|
@@ -11141,6 +11191,81 @@ function meetsFailThreshold(findings, threshold) {
|
|
|
11141
11191
|
// ../../packages/scan/src/node/walk.ts
|
|
11142
11192
|
var import_node_fs5 = require("fs");
|
|
11143
11193
|
var import_node_path2 = require("path");
|
|
11194
|
+
function compileExcludes(patterns) {
|
|
11195
|
+
const matchers = patterns.map((p) => p.trim()).filter((p) => p.length > 0 && !p.startsWith("#")).map(compilePattern);
|
|
11196
|
+
if (matchers.length === 0) return () => false;
|
|
11197
|
+
return (relPath) => {
|
|
11198
|
+
const segs = relPath.split("/");
|
|
11199
|
+
return matchers.some((m) => m(segs));
|
|
11200
|
+
};
|
|
11201
|
+
}
|
|
11202
|
+
var GLOBSTAR = /* @__PURE__ */ Symbol("globstar");
|
|
11203
|
+
function compilePattern(pattern) {
|
|
11204
|
+
const withoutLead = pattern.replace(/^\.?\//, "");
|
|
11205
|
+
let end = withoutLead.length;
|
|
11206
|
+
while (end > 0 && withoutLead[end - 1] === "/") end -= 1;
|
|
11207
|
+
const p = withoutLead.slice(0, end);
|
|
11208
|
+
if (!p.includes("/")) {
|
|
11209
|
+
if (p === "**") return () => true;
|
|
11210
|
+
const rx = segToRegExp(p);
|
|
11211
|
+
return (segs) => segs.some((s) => rx.test(s));
|
|
11212
|
+
}
|
|
11213
|
+
const raw = p.split("/");
|
|
11214
|
+
const trailingGlobstar = raw[raw.length - 1] === "**";
|
|
11215
|
+
const core = trailingGlobstar ? raw.slice(0, -1) : raw;
|
|
11216
|
+
const parts = core.map((s) => s === "**" ? GLOBSTAR : segToRegExp(s));
|
|
11217
|
+
return (segs) => {
|
|
11218
|
+
const end2 = matchPrefix(parts, segs);
|
|
11219
|
+
if (end2 < 0) return false;
|
|
11220
|
+
return trailingGlobstar ? end2 < segs.length : true;
|
|
11221
|
+
};
|
|
11222
|
+
}
|
|
11223
|
+
function segToRegExp(seg) {
|
|
11224
|
+
let body = "";
|
|
11225
|
+
for (let i = 0; i < seg.length; i += 1) {
|
|
11226
|
+
const c = seg[i];
|
|
11227
|
+
if (c === "*") {
|
|
11228
|
+
while (seg[i + 1] === "*") i += 1;
|
|
11229
|
+
body += "[^/]*";
|
|
11230
|
+
} else if (c === "?") {
|
|
11231
|
+
body += "[^/]";
|
|
11232
|
+
} else {
|
|
11233
|
+
body += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
11234
|
+
}
|
|
11235
|
+
}
|
|
11236
|
+
return new RegExp(`^${body}$`);
|
|
11237
|
+
}
|
|
11238
|
+
function matchPrefix(parts, segs) {
|
|
11239
|
+
let pi = 0;
|
|
11240
|
+
let si = 0;
|
|
11241
|
+
let star = -1;
|
|
11242
|
+
let starSi = -1;
|
|
11243
|
+
while (pi < parts.length) {
|
|
11244
|
+
const part = parts[pi];
|
|
11245
|
+
if (part === GLOBSTAR) {
|
|
11246
|
+
star = pi;
|
|
11247
|
+
starSi = si;
|
|
11248
|
+
pi += 1;
|
|
11249
|
+
} else if (si < segs.length && part.test(segs[si])) {
|
|
11250
|
+
pi += 1;
|
|
11251
|
+
si += 1;
|
|
11252
|
+
} else if (star !== -1 && starSi < segs.length) {
|
|
11253
|
+
starSi += 1;
|
|
11254
|
+
si = starSi;
|
|
11255
|
+
pi = star + 1;
|
|
11256
|
+
} else {
|
|
11257
|
+
return -1;
|
|
11258
|
+
}
|
|
11259
|
+
}
|
|
11260
|
+
return si;
|
|
11261
|
+
}
|
|
11262
|
+
function readIgnoreFile(root) {
|
|
11263
|
+
try {
|
|
11264
|
+
return (0, import_node_fs5.readFileSync)((0, import_node_path2.join)(root, ".threatcrushignore"), "utf-8").split("\n");
|
|
11265
|
+
} catch {
|
|
11266
|
+
return [];
|
|
11267
|
+
}
|
|
11268
|
+
}
|
|
11144
11269
|
function scanPath(targetPath, options = {}) {
|
|
11145
11270
|
const maxFileBytes = options.maxFileBytes ?? 1024 * 1024;
|
|
11146
11271
|
const allowed = options.categories ? new Set(options.categories) : null;
|
|
@@ -11148,6 +11273,7 @@ function scanPath(targetPath, options = {}) {
|
|
|
11148
11273
|
const unreadable = [];
|
|
11149
11274
|
let filesScanned = 0;
|
|
11150
11275
|
let suppressed = 0;
|
|
11276
|
+
let excluded = 0;
|
|
11151
11277
|
const rootIsDirectory = (() => {
|
|
11152
11278
|
try {
|
|
11153
11279
|
return (0, import_node_fs5.statSync)(targetPath).isDirectory();
|
|
@@ -11156,6 +11282,7 @@ function scanPath(targetPath, options = {}) {
|
|
|
11156
11282
|
}
|
|
11157
11283
|
})();
|
|
11158
11284
|
const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path2.dirname)(targetPath);
|
|
11285
|
+
const isExcluded = compileExcludes([...options.exclude ?? [], ...readIgnoreFile(walkRoot)]);
|
|
11159
11286
|
const scanFile = (fullPath, filename) => {
|
|
11160
11287
|
const relativePath = toRelative(walkRoot, fullPath);
|
|
11161
11288
|
const extension = (0, import_node_path2.extname)(filename).toLowerCase();
|
|
@@ -11213,17 +11340,28 @@ function scanPath(targetPath, options = {}) {
|
|
|
11213
11340
|
}
|
|
11214
11341
|
for (const entry of entries) {
|
|
11215
11342
|
const fullPath = (0, import_node_path2.join)(currentPath, entry.name);
|
|
11343
|
+
const relativePath = toRelative(walkRoot, fullPath);
|
|
11216
11344
|
if (entry.isDirectory()) {
|
|
11217
11345
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
11346
|
+
if (isExcluded(relativePath)) {
|
|
11347
|
+
excluded += 1;
|
|
11348
|
+
continue;
|
|
11349
|
+
}
|
|
11218
11350
|
walk(fullPath);
|
|
11219
11351
|
continue;
|
|
11220
11352
|
}
|
|
11221
11353
|
if (!entry.isFile()) continue;
|
|
11354
|
+
if (isExcluded(relativePath)) {
|
|
11355
|
+
excluded += 1;
|
|
11356
|
+
continue;
|
|
11357
|
+
}
|
|
11222
11358
|
scanFile(fullPath, entry.name);
|
|
11223
11359
|
}
|
|
11224
11360
|
};
|
|
11225
11361
|
if (rootIsDirectory) {
|
|
11226
11362
|
walk(targetPath);
|
|
11363
|
+
} else if (isExcluded(toRelative(walkRoot, targetPath))) {
|
|
11364
|
+
excluded += 1;
|
|
11227
11365
|
} else {
|
|
11228
11366
|
scanFile(targetPath, (0, import_node_path2.basename)(targetPath));
|
|
11229
11367
|
}
|
|
@@ -11231,7 +11369,7 @@ function scanPath(targetPath, options = {}) {
|
|
|
11231
11369
|
filtered.sort(
|
|
11232
11370
|
(a, b) => severityRank(b.severity) - severityRank(a.severity) || a.file.localeCompare(b.file) || a.line - b.line
|
|
11233
11371
|
);
|
|
11234
|
-
return { findings: filtered, filesScanned, unreadable, suppressed, root: walkRoot };
|
|
11372
|
+
return { findings: filtered, filesScanned, unreadable, suppressed, excluded, root: walkRoot };
|
|
11235
11373
|
}
|
|
11236
11374
|
function recordSensitiveFile(filename, relativePath, sink, fileFindings) {
|
|
11237
11375
|
if (fileFindings.length > 0) return;
|
|
@@ -11580,6 +11718,7 @@ async function scanCommand(targetPath, options = {}) {
|
|
|
11580
11718
|
try {
|
|
11581
11719
|
let seen = 0;
|
|
11582
11720
|
const report = scanPath(targetPath, {
|
|
11721
|
+
exclude: options.exclude,
|
|
11583
11722
|
onFile: () => {
|
|
11584
11723
|
seen += 1;
|
|
11585
11724
|
if (spinner) spinner.text = `Scanning files... (${seen} files)`;
|
|
@@ -11595,6 +11734,7 @@ async function scanCommand(targetPath, options = {}) {
|
|
|
11595
11734
|
filesScanned: report.filesScanned,
|
|
11596
11735
|
unreadable: report.unreadable,
|
|
11597
11736
|
suppressed: report.suppressed,
|
|
11737
|
+
excluded: report.excluded,
|
|
11598
11738
|
root: report.root
|
|
11599
11739
|
};
|
|
11600
11740
|
} catch (err) {
|
|
@@ -11614,6 +11754,13 @@ async function scanCommand(targetPath, options = {}) {
|
|
|
11614
11754
|
for (const path of outcome.unreadable) say(source_default.gray(` ${path}`));
|
|
11615
11755
|
}
|
|
11616
11756
|
}
|
|
11757
|
+
if (outcome.excluded > 0) {
|
|
11758
|
+
say(
|
|
11759
|
+
source_default.gray(
|
|
11760
|
+
` \xB7 ${outcome.excluded} path(s) excluded by --exclude or .threatcrushignore`
|
|
11761
|
+
)
|
|
11762
|
+
);
|
|
11763
|
+
}
|
|
11617
11764
|
if (outcome.suppressed > 0) {
|
|
11618
11765
|
say(
|
|
11619
11766
|
source_default.gray(
|
|
@@ -17245,7 +17392,12 @@ program2.command("scan").description("Scan codebase for vulnerabilities and secr
|
|
|
17245
17392
|
).option(
|
|
17246
17393
|
"--path-prefix <prefix>",
|
|
17247
17394
|
"prepend this to SARIF file URIs \u2014 use when the scan root is not the repository root"
|
|
17248
|
-
).option("--deps", "also query OSV.dev for advisories against lockfile versions (network)").option(
|
|
17395
|
+
).option("--deps", "also query OSV.dev for advisories against lockfile versions (network)").option(
|
|
17396
|
+
"--exclude <glob>",
|
|
17397
|
+
"skip paths matching this glob (repeatable); merged with a .threatcrushignore at the scan root",
|
|
17398
|
+
(value, previous) => [...previous, value],
|
|
17399
|
+
[]
|
|
17400
|
+
).option("-v, --verbose", "list the paths that could not be read").action(async (targetPath, opts) => {
|
|
17249
17401
|
const format = (opts.format ?? "text").toLowerCase();
|
|
17250
17402
|
if (!["text", "json", "sarif"].includes(format)) {
|
|
17251
17403
|
console.error(source_default.red(`Unknown --format "${opts.format}" (expected text, json, or sarif)`));
|
|
@@ -17264,6 +17416,7 @@ program2.command("scan").description("Scan codebase for vulnerabilities and secr
|
|
|
17264
17416
|
failOn,
|
|
17265
17417
|
pathPrefix: opts.pathPrefix,
|
|
17266
17418
|
dependencies: opts.deps,
|
|
17419
|
+
exclude: opts.exclude,
|
|
17267
17420
|
verbose: opts.verbose
|
|
17268
17421
|
});
|
|
17269
17422
|
});
|