@profullstack/threatcrush 0.3.0 → 0.5.1
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 +32 -5
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +32 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/daemon.js
CHANGED
|
@@ -4426,7 +4426,7 @@ function severityFor(declared, confidence) {
|
|
|
4426
4426
|
}
|
|
4427
4427
|
|
|
4428
4428
|
// src/scan/code-rules.ts
|
|
4429
|
-
var UNTRUSTED_JS = /\b(?:req|request|ctx|context)\s*\.\s*(?:body|query|params|param|headers|cookies|url|files)\b|\bprocess\.argv\b|\bwindow\.location\b|\bdocument\.location\b|\blocation\.(?:search|hash|href)\b|\bsearchParams\b|\bgetParameter\s*\(|\bgetQueryString\s*\(|\bgetInputStream\s*\(/;
|
|
4429
|
+
var UNTRUSTED_JS = /\b(?:req|request|ctx|context)\s*\.\s*(?:body|query|params|param|headers|cookies|url|files)\b|\bprocess\.argv\b|\bwindow\.location\b|\bdocument\.location\b|\blocation\.(?:search|hash|href)\b|\bsearchParams\s*\.\s*(?:get|getAll|has|entries|keys|values|forEach)\b|\bgetParameter\s*\(|\bgetQueryString\s*\(|\bgetInputStream\s*\(/;
|
|
4430
4430
|
var UNTRUSTED_PY = /\brequest\b|\bparams\b|\bflask\b|\bsys\.argv\b|\bos\.environ\b\s*\[/;
|
|
4431
4431
|
var UNTRUSTED_RB = /\bparams\s*\[|\brequest\b|\bcookies\s*\[/;
|
|
4432
4432
|
var UNTRUSTED_GO = /\br\s*\.\s*(?:URL|Form|Body|Header|PostForm)\b|\bFormValue\s*\(|\bQuery\s*\(\s*\)\s*\.\s*Get\s*\(|\bmux\.Vars\s*\(/;
|
|
@@ -4445,7 +4445,17 @@ function untrustedPatternFor(language) {
|
|
|
4445
4445
|
return UNTRUSTED_JS;
|
|
4446
4446
|
}
|
|
4447
4447
|
}
|
|
4448
|
-
var GENERIC_GUARD =
|
|
4448
|
+
var GENERIC_GUARD = (
|
|
4449
|
+
// `esc(`, `aEsc(`, `htmlEscape(`, `escapeHtml(` — the escaper is almost
|
|
4450
|
+
// never *named* `escapeHtml` in real code. It gets aliased to something
|
|
4451
|
+
// short because it is called on nearly every interpolation, so matching only
|
|
4452
|
+
// the long spellings reported the codebases that escape most rigorously.
|
|
4453
|
+
//
|
|
4454
|
+
// The identifier must END at the escaper (with at most a known output-context
|
|
4455
|
+
// suffix). An earlier, looser form also matched `describe(`, which would have
|
|
4456
|
+
// silenced findings across every test file in every repository.
|
|
4457
|
+
/\ballow(?:ed|list|_list|ed_hosts)?\b|\bwhitelist\b|\b\w{0,6}[Ee]sc(?:ape)?(?:[Hh]tml|HTML|[Xx]ml|XML|[Ss]ql|[Aa]ttr|[Jj]s|[Uu]ri|[Uu]rl)?\s*\(|\bhtml_escape\b|\bhtmlspecialchars\s*\(|\bsanitiz\w*\b|\bencoded\b|\brealpath\b|\bcommonpath\b|\bresolve\(\)\.startsWith\b|\bprocess\.env\b|\bos\.environ\b|\bgetenv\b|\bENV\s*\[|setObjectInputFilter|ObjectInputFilter/i
|
|
4458
|
+
);
|
|
4449
4459
|
var XXE_GUARD = /FEATURE_SECURE_PROCESSING|setExpandEntityReferences|disallow-doctype-decl|external-general-entities|external-parameter-entities|setXIncludeAware\s*\(\s*false/;
|
|
4450
4460
|
var CODE_SINK = /\bglobalThis\s*\[|\bconstructor\b|\beval\b|\bFunction\b|\brun\s*\(|\bvm\s*\.\s*run/;
|
|
4451
4461
|
var EXFIL_SINK = /\bconsole\s*\.\s*(?:log|debug|info|warn|error)\s*\(|\bfetch\s*\(|\baxios\b|\brequest\s*\(|\.\s*send\s*\(/;
|
|
@@ -4596,7 +4606,17 @@ var CODE_RULES = [
|
|
|
4596
4606
|
cwe: "CWE-79",
|
|
4597
4607
|
severity: "high",
|
|
4598
4608
|
languages: ["javascript", "typescript"],
|
|
4599
|
-
pattern: /\bdangerouslySetInnerHTML\s*=|\.\s*(?:innerHTML|outerHTML)\s*=\s*(?!\s*['"`]\s*['"`]\s*;?\s*$)|\bdocument\s*\.\s*write(?:ln)?\s*\(|\.\s*insertAdjacentHTML\s*\(
|
|
4609
|
+
pattern: /\bdangerouslySetInnerHTML\s*=|\.\s*(?:innerHTML|outerHTML)\s*=\s*(?!\s*['"`]\s*['"`]\s*;?\s*$)|\bdocument\s*\.\s*write(?:ln)?\s*\(|\.\s*insertAdjacentHTML\s*\(/,
|
|
4610
|
+
/**
|
|
4611
|
+
* A whole-statement assignment of a string with no interpolation and no
|
|
4612
|
+
* concatenation carries no data, so it cannot carry attacker data. This
|
|
4613
|
+
* was the single largest source of noise: a codebase that builds its UI
|
|
4614
|
+
* with innerHTML reports every static heading and spinner as XSS, and a
|
|
4615
|
+
* rule that flags 40 safe lines to catch one real one gets switched off.
|
|
4616
|
+
*
|
|
4617
|
+
* Line-scoped on purpose — see `lineGuard`.
|
|
4618
|
+
*/
|
|
4619
|
+
lineGuard: /(?:innerHTML|outerHTML)\s*=\s*(?:'[^'\\]*'|"[^"\\]*"|`[^`$\\]*`)\s*;?\s*$/
|
|
4600
4620
|
},
|
|
4601
4621
|
{
|
|
4602
4622
|
id: "java-html-writer-concatenation",
|
|
@@ -4903,6 +4923,7 @@ function evaluateRule(rule, ctx) {
|
|
|
4903
4923
|
const forward = rule.guardForward ?? 0;
|
|
4904
4924
|
const context = windowText(ctx.lines, ctx.index, back, forward, ctx.prose);
|
|
4905
4925
|
if (rule.requires && !rule.requires.test(context)) return null;
|
|
4926
|
+
if (rule.lineGuard?.test(line)) return null;
|
|
4906
4927
|
const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
|
|
4907
4928
|
if (guard && (guard.test(line) || guard.test(context))) return null;
|
|
4908
4929
|
const untrusted = untrustedPatternFor(ctx.language);
|
|
@@ -5504,10 +5525,15 @@ function isSuppressed(suppressions, index, ruleId) {
|
|
|
5504
5525
|
if (!rules) return false;
|
|
5505
5526
|
return rules.has("*") || rules.has(ruleId);
|
|
5506
5527
|
}
|
|
5528
|
+
function isTestPath(relativePath) {
|
|
5529
|
+
const p = relativePath.replace(/\\/g, "/");
|
|
5530
|
+
return /(?:^|\/)(?:tests?|__tests__|__mocks__|spec|specs|fixtures?|mocks?|e2e|testdata)\//i.test(p) || /(?:^|\/)(?:test|conftest)_[^/]+$/i.test(p) || /[._-](?:test|spec)\.[a-z]+$/i.test(p) || /_test\.[a-z]+$/i.test(p);
|
|
5531
|
+
}
|
|
5507
5532
|
function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
5508
5533
|
const findings = [];
|
|
5509
5534
|
const lines = text.split("\n");
|
|
5510
5535
|
const suppressions = collectSuppressions(lines);
|
|
5536
|
+
const inTests = isTestPath(relativePath);
|
|
5511
5537
|
lines.forEach((line, index) => {
|
|
5512
5538
|
for (const rule of SECRET_RULES) {
|
|
5513
5539
|
const match = rule.pattern.exec(line);
|
|
@@ -5519,10 +5545,11 @@ function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
|
5519
5545
|
title: rule.name,
|
|
5520
5546
|
file: relativePath,
|
|
5521
5547
|
line: index + 1,
|
|
5522
|
-
|
|
5548
|
+
// Reported but not blocking in tests — see isTestPath.
|
|
5549
|
+
severity: inTests ? "low" : rule.severity,
|
|
5523
5550
|
// A matched credential format is the finding, not a proxy for one.
|
|
5524
5551
|
confidence: "evidence",
|
|
5525
|
-
message: `Possible ${rule.name} detected`,
|
|
5552
|
+
message: inTests ? `Possible ${rule.name} detected in a test file \u2014 usually a fixture, still worth confirming it is not a live credential` : `Possible ${rule.name} detected`,
|
|
5526
5553
|
consequence: rule.consequence,
|
|
5527
5554
|
cwe: rule.cwe,
|
|
5528
5555
|
excerpt: redactSecret(line.trim()).slice(0, 200),
|