@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 +95 -16
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +4941 -649
- package/dist/index.js.map +1 -1
- package/package.json +3 -9
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:
|
|
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
|
-
//
|
|
5409
|
-
//
|
|
5410
|
-
//
|
|
5411
|
-
|
|
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
|
-
//
|
|
5474
|
-
//
|
|
5475
|
-
|
|
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
|
});
|