@profullstack/threatcrush 0.5.1 → 0.6.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 +275 -3
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +275 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/daemon.js
CHANGED
|
@@ -4431,8 +4431,14 @@ var UNTRUSTED_PY = /\brequest\b|\bparams\b|\bflask\b|\bsys\.argv\b|\bos\.environ
|
|
|
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*\(/;
|
|
4433
4433
|
var UNTRUSTED_JAVA = /\bgetParameter\s*\(|\bgetQueryString\s*\(|\bgetHeader\s*\(|\bgetInputStream\s*\(|\bgetCookies\s*\(|\b@RequestParam\b|\b@PathVariable\b/;
|
|
4434
|
+
var UNTRUSTED_SH = /\$\{?[1-9]\d*\b|\$[@*]|\$\{@\}|\bread\s+(?:-\S+\s+)*[A-Za-z_]\w*|\$\{?REPLY\b|\$\{?QUERY_STRING\b/;
|
|
4435
|
+
var UNTRUSTED_PHP = /\$_(?:GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|\bphp:\/\/input\b|\bgetallheaders\s*\(/;
|
|
4434
4436
|
function untrustedPatternFor(language) {
|
|
4435
4437
|
switch (language) {
|
|
4438
|
+
case "php":
|
|
4439
|
+
return UNTRUSTED_PHP;
|
|
4440
|
+
case "shell":
|
|
4441
|
+
return UNTRUSTED_SH;
|
|
4436
4442
|
case "python":
|
|
4437
4443
|
return UNTRUSTED_PY;
|
|
4438
4444
|
case "ruby":
|
|
@@ -4457,6 +4463,7 @@ var GENERIC_GUARD = (
|
|
|
4457
4463
|
/\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
4464
|
);
|
|
4459
4465
|
var XXE_GUARD = /FEATURE_SECURE_PROCESSING|setExpandEntityReferences|disallow-doctype-decl|external-general-entities|external-parameter-entities|setXIncludeAware\s*\(\s*false/;
|
|
4466
|
+
var XML_PARSING_FILE = /\b(?:javax\.xml|org\.xml\.sax|org\.w3c\.dom|org\.jdom2?|org\.dom4j|XmlPullParser|DocumentBuilderFactory|DocumentBuilder|SAXParserFactory|SAXParser|XMLInputFactory|XMLReaderFactory|XMLReader|SAXBuilder|SAXReader)\b/;
|
|
4460
4467
|
var CODE_SINK = /\bglobalThis\s*\[|\bconstructor\b|\beval\b|\bFunction\b|\brun\s*\(|\bvm\s*\.\s*run/;
|
|
4461
4468
|
var EXFIL_SINK = /\bconsole\s*\.\s*(?:log|debug|info|warn|error)\s*\(|\bfetch\s*\(|\baxios\b|\brequest\s*\(|\.\s*send\s*\(/;
|
|
4462
4469
|
var SQL_KEYWORDS = "SELECT|INSERT\\s+INTO|INSERT|UPDATE|DELETE\\s+FROM|DELETE|DROP|UNION\\s+SELECT";
|
|
@@ -4731,6 +4738,16 @@ var CODE_RULES = [
|
|
|
4731
4738
|
pattern: /\byaml\s*\.\s*load\s*\((?![^)]*safe)|\bloadAll\s*\([^)]*unsafe/i
|
|
4732
4739
|
},
|
|
4733
4740
|
// ── XML external entities ────────────────────────────────────────────────
|
|
4741
|
+
//
|
|
4742
|
+
// Evidence that a file parses XML at all. The XXE rules match on receiver
|
|
4743
|
+
// *names* — `builder.parse(is)` is the shape the vulnerability actually takes,
|
|
4744
|
+
// and the declared type is rarely on that line — so without this the pattern
|
|
4745
|
+
// reads any `.parse()` on anything suffixed Builder, Parser or Reader as XML.
|
|
4746
|
+
// In practice that meant a hostname-mask parser (`HostMask.Parser.parse(...)`)
|
|
4747
|
+
// was reported as CWE-611 at high severity.
|
|
4748
|
+
//
|
|
4749
|
+
// An import is the cheapest honest signal: a file that parses XML says so at
|
|
4750
|
+
// the top, and one that never mentions XML is not parsing it.
|
|
4734
4751
|
{
|
|
4735
4752
|
id: "java-xxe-parser-defaults",
|
|
4736
4753
|
title: "XML parser left on its insecure defaults",
|
|
@@ -4751,7 +4768,10 @@ var CODE_RULES = [
|
|
|
4751
4768
|
severity: "high",
|
|
4752
4769
|
languages: ["java"],
|
|
4753
4770
|
// Receiver-qualified so `LocalDate.parse(s)` and friends stay out of it.
|
|
4771
|
+
// The suffix alone is not enough — plenty of parsers parse things that are
|
|
4772
|
+
// not XML — so `fileRequires` decides whether the file is in scope at all.
|
|
4754
4773
|
pattern: /\b\w*(?:[Bb]uilder|[Pp]arser|[Rr]eader)\s*\.\s*parse\s*\(/,
|
|
4774
|
+
fileRequires: XML_PARSING_FILE,
|
|
4755
4775
|
guard: XXE_GUARD,
|
|
4756
4776
|
guardBack: 6,
|
|
4757
4777
|
guardForward: 4
|
|
@@ -4873,6 +4893,212 @@ var CODE_RULES = [
|
|
|
4873
4893
|
severity: "high",
|
|
4874
4894
|
languages: ["javascript", "typescript"],
|
|
4875
4895
|
pattern: /\[\s*['"]__proto__['"]\s*\]|\bObject\s*\.\s*assign\s*\(\s*[\w.$]*\.prototype\b|\.\s*__proto__\s*=/
|
|
4896
|
+
},
|
|
4897
|
+
// ── Shell ────────────────────────────────────────────────────────────────
|
|
4898
|
+
//
|
|
4899
|
+
// `shell` was a language the type system knew about and no rule targeted, so
|
|
4900
|
+
// a repository written entirely in bash got secret detection and nothing
|
|
4901
|
+
// else. Installers, CI helpers and packaging scripts are where a great deal
|
|
4902
|
+
// of privileged work actually happens, and they run as whoever invoked them.
|
|
4903
|
+
{
|
|
4904
|
+
id: "sh-remote-script-execution",
|
|
4905
|
+
title: "network output piped into a shell",
|
|
4906
|
+
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.",
|
|
4907
|
+
cwe: "CWE-494",
|
|
4908
|
+
severity: "high",
|
|
4909
|
+
languages: ["shell"],
|
|
4910
|
+
// The pipe must be the *next* thing: `curl -o f url && sh f` is a different
|
|
4911
|
+
// (and checkable) shape, and `curl url | jq` is not an execution at all.
|
|
4912
|
+
pattern: /\b(?:curl|wget)\b[^|\n]*\|\s*(?:sudo\s+(?:-\S+\s+)*)?(?:\/bin\/|\/usr\/bin\/)?(?:ba|da|k|z|a)?sh\b/,
|
|
4913
|
+
// Nothing on this line can exonerate it. Integrity checking happens in a
|
|
4914
|
+
// separate step by construction, so a window guard would only mislead.
|
|
4915
|
+
guard: false
|
|
4916
|
+
},
|
|
4917
|
+
{
|
|
4918
|
+
id: "sh-eval-expansion",
|
|
4919
|
+
title: "eval on an expanded string",
|
|
4920
|
+
consequence: "The expansion is re-parsed as shell source, so a `;` or `$(\u2026)` anywhere in the value runs as a command rather than arriving as data.",
|
|
4921
|
+
cwe: "CWE-78",
|
|
4922
|
+
severity: "high",
|
|
4923
|
+
languages: ["shell"],
|
|
4924
|
+
// Matched against eval's *argument*, not against the rest of the line.
|
|
4925
|
+
//
|
|
4926
|
+
// `\beval\b.*\$` looks equivalent and is not: bash's ordinary dynamic-range
|
|
4927
|
+
// idiom, `eval echo {$((k + 1))..$((k + n))}`, contains `$k` inside the
|
|
4928
|
+
// arithmetic, so a line-wide search finds an expansion in a construct that
|
|
4929
|
+
// cannot carry a command — `$((…))` is parsed as an expression, where a `;`
|
|
4930
|
+
// is a syntax error rather than a second command. That spelling reported
|
|
4931
|
+
// 355 findings in one 3,500-line script, all of them the same safe loop.
|
|
4932
|
+
//
|
|
4933
|
+
// What is left is the form where eval is handed a value directly —
|
|
4934
|
+
// `eval "$cmd"`, `eval $cmd`, `eval "$(…)"` — which is the shape that
|
|
4935
|
+
// actually re-parses untrusted text as source. `eval echo $x` is not
|
|
4936
|
+
// covered; catching it without also catching the range idiom needs to know
|
|
4937
|
+
// which expansions are arithmetic, which is parsing, not matching.
|
|
4938
|
+
pattern: /\beval\s+(?:-\S+\s+)*(?:"\s*)?\$(?:\{?[A-Za-z_]\w*|\((?!\())/,
|
|
4939
|
+
// The shell-init idiom `eval "$(tool init -)"` is the documented interface
|
|
4940
|
+
// of most version managers. It is still eval of program output, but the
|
|
4941
|
+
// program is a fixed local binary, and flagging it reports every developer
|
|
4942
|
+
// dotfile in existence.
|
|
4943
|
+
lineGuard: /\beval\s+"?\$\(\s*(?:ssh-agent|dircolors|direnv|rbenv|pyenv|nodenv|goenv|jenv|tfenv|opam|luarocks|conda|mamba|zoxide|starship|mise|asdf|fnm|nvm|brew|thefuck|register-python-argcomplete|_\w+_completion)\b/
|
|
4944
|
+
},
|
|
4945
|
+
{
|
|
4946
|
+
id: "sh-unquoted-expansion-destructive",
|
|
4947
|
+
title: "unquoted expansion in a destructive command",
|
|
4948
|
+
consequence: "An unquoted expansion is word-split and glob-expanded before the command sees it. A value with a space removes two paths instead of one; an empty value removes the argument entirely, which is how `rm -rf $DIR/` becomes `rm -rf /`.",
|
|
4949
|
+
cwe: "CWE-78",
|
|
4950
|
+
severity: "high",
|
|
4951
|
+
languages: ["shell"],
|
|
4952
|
+
// `[^"'\n]*?` cannot cross a quote, so `rm -rf "$dir"` — the correct form —
|
|
4953
|
+
// never reaches the `$` and never matches. Only a genuinely bare expansion
|
|
4954
|
+
// does. Restricted to recursive/forced removal: a bare `$f` in `rm $f` is
|
|
4955
|
+
// sloppy, but it is not the shape that erases a filesystem.
|
|
4956
|
+
pattern: /\brm\s+(?:-[a-zA-Z-]*[rRf][a-zA-Z-]*\s+)+[^"'\n]*?\$\{?[A-Za-z_]/
|
|
4957
|
+
},
|
|
4958
|
+
{
|
|
4959
|
+
id: "sh-insecure-transport-flag",
|
|
4960
|
+
title: "certificate verification disabled",
|
|
4961
|
+
consequence: "Anyone positioned between this host and the server can substitute the response. When the response is a package, a key or a script, that is remote code execution with the transport doing nothing to stop it.",
|
|
4962
|
+
cwe: "CWE-295",
|
|
4963
|
+
severity: "high",
|
|
4964
|
+
languages: ["shell"],
|
|
4965
|
+
pattern: /\b(?:curl|wget)\b[^\n|]*(?:\s-k(?=\s|$)|\s--insecure\b|\s--no-check-certificate\b)/,
|
|
4966
|
+
// Over plain HTTP there is no certificate to skip, so the flag is inert and
|
|
4967
|
+
// this rule has nothing to say — `sh-plaintext-download` is the finding
|
|
4968
|
+
// that fits. Reporting both put two entries on one line, one of which
|
|
4969
|
+
// recommended a fix that would change nothing.
|
|
4970
|
+
lineGuard: /^(?!.*https:\/\/).*\bhttp:\/\//
|
|
4971
|
+
},
|
|
4972
|
+
{
|
|
4973
|
+
id: "sh-plaintext-download",
|
|
4974
|
+
title: "download over plain HTTP",
|
|
4975
|
+
consequence: "The response arrives unauthenticated over a channel any intermediary can rewrite. Where the payload is an archive, a package list or a key, substituting it is straightforward and leaves nothing for the script to notice.",
|
|
4976
|
+
cwe: "CWE-319",
|
|
4977
|
+
severity: "high",
|
|
4978
|
+
languages: ["shell"],
|
|
4979
|
+
pattern: /\b(?:curl|wget)\b[^\n|]*\bhttp:\/\//,
|
|
4980
|
+
// Loopback and link-local are not carried over a network anyone can sit on.
|
|
4981
|
+
lineGuard: /http:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\]|169\.254\.|host\.docker\.internal)\b/
|
|
4982
|
+
},
|
|
4983
|
+
{
|
|
4984
|
+
id: "sh-world-writable-permissions",
|
|
4985
|
+
title: "world-writable permissions",
|
|
4986
|
+
consequence: "Any local account can rewrite the file. If it is a script, a config or anything on a privileged path, the next process to read it runs someone else\u2019s content.",
|
|
4987
|
+
cwe: "CWE-732",
|
|
4988
|
+
severity: "medium",
|
|
4989
|
+
languages: ["shell"],
|
|
4990
|
+
pattern: /\bchmod\s+(?:-[a-zA-Z-]+\s+)*(?:0?777|a\+rwx|ugo\+rwx|a=rwx)\b/
|
|
4991
|
+
},
|
|
4992
|
+
{
|
|
4993
|
+
id: "sh-predictable-temp-path",
|
|
4994
|
+
title: "predictable temporary file",
|
|
4995
|
+
consequence: "The name is guessable, so a local attacker can create it first \u2014 as a symlink to somewhere that matters \u2014 and the script writes through it with its own privileges.",
|
|
4996
|
+
cwe: "CWE-377",
|
|
4997
|
+
severity: "medium",
|
|
4998
|
+
languages: ["shell"],
|
|
4999
|
+
// Redirection or an explicit write into a literal `/tmp` path. A `$$` or
|
|
5000
|
+
// `$RANDOM` suffix is still predictable, so it is not treated as a fix;
|
|
5001
|
+
// `mktemp` is, and it is the guard below.
|
|
5002
|
+
pattern: /(?:>{1,2}\s*|\b(?:tee|touch|cp|mv|install)\s+(?:-\S+\s+)*)\/tmp\/[\w.$-]+/,
|
|
5003
|
+
guard: /\bmktemp\b/,
|
|
5004
|
+
guardBack: 6,
|
|
5005
|
+
guardForward: 2
|
|
5006
|
+
},
|
|
5007
|
+
// ── PHP ──────────────────────────────────────────────────────────────────
|
|
5008
|
+
//
|
|
5009
|
+
// `php` was the second language in `ScanLanguage` that no rule targeted, for
|
|
5010
|
+
// the same reason `shell` was: nothing checks the two lists against each
|
|
5011
|
+
// other. `LANGUAGE_COVERAGE` in the tests does now.
|
|
5012
|
+
{
|
|
5013
|
+
id: "php-sql-interpolation",
|
|
5014
|
+
title: "SQL assembled by interpolation",
|
|
5015
|
+
consequence: "PHP interpolates variables inside double-quoted strings, so the value is part of the statement before the driver ever sees it. A quote in the value ends the literal and the rest is parsed as SQL.",
|
|
5016
|
+
cwe: "CWE-89",
|
|
5017
|
+
severity: "critical",
|
|
5018
|
+
languages: ["php"],
|
|
5019
|
+
// Interpolation (`"… $id …"`, `"… {$id} …"`) or concatenation onto a SQL
|
|
5020
|
+
// string. A prepared statement passes placeholders and binds separately,
|
|
5021
|
+
// so its query string contains neither.
|
|
5022
|
+
pattern: new RegExp(
|
|
5023
|
+
`\\b(?:mysqli_query|mysql_query|pg_query|->\\s*(?:query|exec|unprepared))\\s*\\([^)]*(?:"[^"\\n]*(?:${SQL_KEYWORDS})\\b[^"\\n]*(?:\\$\\w+|\\{\\$)|${SQL_STRING}\\s*\\.)`,
|
|
5024
|
+
"i"
|
|
5025
|
+
)
|
|
5026
|
+
},
|
|
5027
|
+
{
|
|
5028
|
+
id: "php-shell-exec-interpolation",
|
|
5029
|
+
title: "shell command built from a variable",
|
|
5030
|
+
consequence: "The string is handed to `/bin/sh`, which interprets `;`, `|` and `$(\u2026)` in whatever the variable held.",
|
|
5031
|
+
cwe: "CWE-78",
|
|
5032
|
+
severity: "critical",
|
|
5033
|
+
languages: ["php"],
|
|
5034
|
+
pattern: /\b(?:exec|system|shell_exec|passthru|popen|proc_open|pcntl_exec)\s*\(\s*(?:"[^"\n]*(?:\$\w+|\{\$)|'[^'\n]*'\s*\.|\$\w+\s*\.)/,
|
|
5035
|
+
// `escapeshellarg`/`escapeshellcmd` are the correct answer and are usually
|
|
5036
|
+
// applied inline, so a line-scoped guard is the right shape.
|
|
5037
|
+
lineGuard: /\bescapeshell(?:arg|cmd)\s*\(/
|
|
5038
|
+
},
|
|
5039
|
+
{
|
|
5040
|
+
id: "php-dynamic-code-execution",
|
|
5041
|
+
title: "dynamic code execution",
|
|
5042
|
+
consequence: "Whatever reaches this call is executed as PHP source with the privileges of the web process.",
|
|
5043
|
+
cwe: "CWE-95",
|
|
5044
|
+
severity: "critical",
|
|
5045
|
+
languages: ["php"],
|
|
5046
|
+
pattern: /\b(?:eval|assert|create_function)\s*\(\s*(?:\$\w+|"[^"\n]*(?:\$\w+|\{\$))/,
|
|
5047
|
+
guard: false
|
|
5048
|
+
},
|
|
5049
|
+
{
|
|
5050
|
+
id: "php-dynamic-file-inclusion",
|
|
5051
|
+
title: "include path built from a variable",
|
|
5052
|
+
consequence: "The included file is executed, not read. A traversal sequence reaches any file the process can open, and with a remote wrapper enabled the path need not be local at all.",
|
|
5053
|
+
cwe: "CWE-98",
|
|
5054
|
+
severity: "critical",
|
|
5055
|
+
languages: ["php"],
|
|
5056
|
+
pattern: /\b(?:include|include_once|require|require_once)\s*(?:\(\s*)?(?:\$\w+|"[^"\n]*(?:\$\w+|\{\$)|'[^'\n]*'\s*\.)/,
|
|
5057
|
+
// A basename-and-allow-list is the standard fix; `basename` alone still
|
|
5058
|
+
// leaves the extension open, but it defeats traversal, which is the part
|
|
5059
|
+
// this rule is about.
|
|
5060
|
+
lineGuard: /\bbasename\s*\(/
|
|
5061
|
+
},
|
|
5062
|
+
{
|
|
5063
|
+
id: "php-unserialize-untrusted",
|
|
5064
|
+
title: "unserialize on request data",
|
|
5065
|
+
consequence: "PHP object deserialisation instantiates classes and runs their magic methods. With a suitable class in scope this is remote code execution, and no `unserialize` option short of `allowed_classes => false` prevents it.",
|
|
5066
|
+
cwe: "CWE-502",
|
|
5067
|
+
severity: "critical",
|
|
5068
|
+
languages: ["php"],
|
|
5069
|
+
pattern: /\bunserialize\s*\(\s*(?:\$_(?:GET|POST|REQUEST|COOKIE)\b|\$\w+)/,
|
|
5070
|
+
// The key is an array key and is normally quoted: `['allowed_classes' => false]`.
|
|
5071
|
+
lineGuard: /["']?allowed_classes["']?\s*=>\s*(?:false|\[)/,
|
|
5072
|
+
needsContext: true
|
|
5073
|
+
},
|
|
5074
|
+
{
|
|
5075
|
+
id: "php-unescaped-output",
|
|
5076
|
+
title: "request data echoed without escaping",
|
|
5077
|
+
consequence: "The value is written into the response verbatim, so markup in it becomes markup in the page \u2014 script that runs with the victim\u2019s session.",
|
|
5078
|
+
cwe: "CWE-79",
|
|
5079
|
+
severity: "high",
|
|
5080
|
+
languages: ["php"],
|
|
5081
|
+
pattern: /\b(?:echo|print)\s+[^;\n]*\$_(?:GET|POST|REQUEST|COOKIE|SERVER)\b|<\?=\s*\$_(?:GET|POST|REQUEST|COOKIE)\b/
|
|
5082
|
+
},
|
|
5083
|
+
{
|
|
5084
|
+
id: "php-request-path-traversal",
|
|
5085
|
+
title: "file path taken from the request",
|
|
5086
|
+
consequence: "A `../` sequence in the value walks out of the intended directory, and the process reads or writes wherever it lands.",
|
|
5087
|
+
cwe: "CWE-22",
|
|
5088
|
+
severity: "high",
|
|
5089
|
+
languages: ["php"],
|
|
5090
|
+
pattern: /\b(?:file_get_contents|file_put_contents|fopen|readfile|unlink|copy|rename|opendir|scandir)\s*\(\s*[^)\n]*\$_(?:GET|POST|REQUEST|COOKIE)\b/,
|
|
5091
|
+
lineGuard: /\bbasename\s*\(|\brealpath\s*\(/
|
|
5092
|
+
},
|
|
5093
|
+
{
|
|
5094
|
+
id: "php-variable-injection",
|
|
5095
|
+
title: "request data expanded into local variables",
|
|
5096
|
+
consequence: "`extract` creates a variable per key, so a request can overwrite any local already in scope \u2014 including the one a subsequent authorisation check reads.",
|
|
5097
|
+
cwe: "CWE-621",
|
|
5098
|
+
severity: "high",
|
|
5099
|
+
languages: ["php"],
|
|
5100
|
+
pattern: /\bextract\s*\(\s*\$_(?:GET|POST|REQUEST|COOKIE)\b|\bimport_request_variables\s*\(/,
|
|
5101
|
+
guard: false
|
|
4876
5102
|
}
|
|
4877
5103
|
];
|
|
4878
5104
|
var COMMENT_PREFIX = /^\s*(?:\/\/|\/\*|\*|#|--|<!--)/;
|
|
@@ -4914,11 +5140,21 @@ function windowText(lines, index, back, forward, prose) {
|
|
|
4914
5140
|
}
|
|
4915
5141
|
return collected.join("\n");
|
|
4916
5142
|
}
|
|
5143
|
+
var FILE_TEXT = /* @__PURE__ */ new WeakMap();
|
|
5144
|
+
function fileTextOf(lines) {
|
|
5145
|
+
let text = FILE_TEXT.get(lines);
|
|
5146
|
+
if (text === void 0) {
|
|
5147
|
+
text = lines.join("\n");
|
|
5148
|
+
FILE_TEXT.set(lines, text);
|
|
5149
|
+
}
|
|
5150
|
+
return text;
|
|
5151
|
+
}
|
|
4917
5152
|
function evaluateRule(rule, ctx) {
|
|
4918
5153
|
if (rule.languages && !rule.languages.includes(ctx.language)) return null;
|
|
4919
5154
|
const line = ctx.lines[ctx.index] ?? "";
|
|
4920
5155
|
if (isComment(line) || ctx.prose?.has(ctx.index)) return null;
|
|
4921
5156
|
if (!rule.pattern.test(line)) return null;
|
|
5157
|
+
if (rule.fileRequires && !rule.fileRequires.test(fileTextOf(ctx.lines))) return null;
|
|
4922
5158
|
const back = rule.guardBack ?? 8;
|
|
4923
5159
|
const forward = rule.guardForward ?? 0;
|
|
4924
5160
|
const context = windowText(ctx.lines, ctx.index, back, forward, ctx.prose);
|
|
@@ -5089,7 +5325,8 @@ function editDistance(a, b, cap = 3) {
|
|
|
5089
5325
|
}
|
|
5090
5326
|
function detectTyposquat(name, ecosystem) {
|
|
5091
5327
|
const popular = ecosystem === "npm" ? POPULAR_NPM : POPULAR_PYPI;
|
|
5092
|
-
const lower = name.toLowerCase()
|
|
5328
|
+
const lower = name.toLowerCase();
|
|
5329
|
+
if (/^@[^/]+\//.test(lower)) return null;
|
|
5093
5330
|
if (popular.includes(lower)) return null;
|
|
5094
5331
|
if (lower.length < 4) return null;
|
|
5095
5332
|
const normalized = normalizeName(lower);
|
|
@@ -5501,6 +5738,33 @@ function languageOf(filename) {
|
|
|
5501
5738
|
if (filename.startsWith(".env") || filename.endsWith(".env")) return "config";
|
|
5502
5739
|
return LANGUAGE_BY_EXTENSION[(0, import_node_path6.extname)(filename).toLowerCase()] ?? "other";
|
|
5503
5740
|
}
|
|
5741
|
+
var LANGUAGE_BY_INTERPRETER = {
|
|
5742
|
+
sh: "shell",
|
|
5743
|
+
bash: "shell",
|
|
5744
|
+
zsh: "shell",
|
|
5745
|
+
dash: "shell",
|
|
5746
|
+
ksh: "shell",
|
|
5747
|
+
ash: "shell",
|
|
5748
|
+
python: "python",
|
|
5749
|
+
python2: "python",
|
|
5750
|
+
python3: "python",
|
|
5751
|
+
ruby: "ruby",
|
|
5752
|
+
node: "javascript",
|
|
5753
|
+
nodejs: "javascript",
|
|
5754
|
+
deno: "javascript",
|
|
5755
|
+
bun: "javascript",
|
|
5756
|
+
php: "php"
|
|
5757
|
+
};
|
|
5758
|
+
function languageOfShebang(firstLine) {
|
|
5759
|
+
const match = /^#!\s*(\S+)(?:\s+(\S+))?/.exec(firstLine);
|
|
5760
|
+
if (!match) return null;
|
|
5761
|
+
const command = (0, import_node_path6.basename)(match[1]);
|
|
5762
|
+
const name = command === "env" && match[2] ? (0, import_node_path6.basename)(match[2]) : command;
|
|
5763
|
+
const exact = LANGUAGE_BY_INTERPRETER[name];
|
|
5764
|
+
if (exact) return exact;
|
|
5765
|
+
const stripped = name.replace(/[\d.]+$/, "");
|
|
5766
|
+
return (stripped ? LANGUAGE_BY_INTERPRETER[stripped] : void 0) ?? null;
|
|
5767
|
+
}
|
|
5504
5768
|
var SUPPRESS_NEXT = /threatcrush-disable-next-line(?:\s+([\w-]+))?/;
|
|
5505
5769
|
var SUPPRESS_LINE = /threatcrush-disable-line(?:\s+([\w-]+))?/;
|
|
5506
5770
|
function collectSuppressions(lines) {
|
|
@@ -5617,12 +5881,14 @@ function scanPath(targetPath, options = {}) {
|
|
|
5617
5881
|
const extension = (0, import_node_path6.extname)(filename).toLowerCase();
|
|
5618
5882
|
const isManifest = filename === "package.json" || filename === "requirements.txt";
|
|
5619
5883
|
const scannable = SCAN_EXTENSIONS.has(extension) || filename.startsWith(".env");
|
|
5620
|
-
|
|
5884
|
+
const mayDeclareInterpreter = !scannable && !isManifest && extension === "";
|
|
5885
|
+
if (!scannable && !isManifest && !mayDeclareInterpreter) {
|
|
5621
5886
|
recordSensitiveFile(filename, relativePath, findings, []);
|
|
5622
5887
|
return;
|
|
5623
5888
|
}
|
|
5624
5889
|
let text;
|
|
5625
5890
|
let handle;
|
|
5891
|
+
let declared = null;
|
|
5626
5892
|
try {
|
|
5627
5893
|
handle = (0, import_node_fs11.openSync)(fullPath, "r");
|
|
5628
5894
|
} catch {
|
|
@@ -5631,6 +5897,12 @@ function scanPath(targetPath, options = {}) {
|
|
|
5631
5897
|
}
|
|
5632
5898
|
try {
|
|
5633
5899
|
if ((0, import_node_fs11.fstatSync)(handle).size > maxFileBytes) return;
|
|
5900
|
+
if (mayDeclareInterpreter) {
|
|
5901
|
+
const prefix = Buffer.alloc(128);
|
|
5902
|
+
const read = (0, import_node_fs11.readSync)(handle, prefix, 0, prefix.length, 0);
|
|
5903
|
+
declared = languageOfShebang(prefix.subarray(0, read).toString("utf-8").split("\n", 1)[0] ?? "");
|
|
5904
|
+
if (!declared) return;
|
|
5905
|
+
}
|
|
5634
5906
|
text = (0, import_node_fs11.readFileSync)(handle, "utf-8");
|
|
5635
5907
|
} catch {
|
|
5636
5908
|
unreadable.push(relativePath);
|
|
@@ -5645,7 +5917,7 @@ function scanPath(targetPath, options = {}) {
|
|
|
5645
5917
|
options.onFile?.(relativePath);
|
|
5646
5918
|
suppressed += collectSuppressions(text.split("\n")).count;
|
|
5647
5919
|
const fileFindings = [
|
|
5648
|
-
...scanText(relativePath, text, languageOf(filename)),
|
|
5920
|
+
...scanText(relativePath, text, declared ?? languageOf(filename)),
|
|
5649
5921
|
...isManifest ? scanManifest(relativePath, filename, text) : []
|
|
5650
5922
|
];
|
|
5651
5923
|
findings.push(...fileFindings);
|