@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/index.js
CHANGED
|
@@ -9701,8 +9701,14 @@ var UNTRUSTED_PY = /\brequest\b|\bparams\b|\bflask\b|\bsys\.argv\b|\bos\.environ
|
|
|
9701
9701
|
var UNTRUSTED_RB = /\bparams\s*\[|\brequest\b|\bcookies\s*\[/;
|
|
9702
9702
|
var UNTRUSTED_GO = /\br\s*\.\s*(?:URL|Form|Body|Header|PostForm)\b|\bFormValue\s*\(|\bQuery\s*\(\s*\)\s*\.\s*Get\s*\(|\bmux\.Vars\s*\(/;
|
|
9703
9703
|
var UNTRUSTED_JAVA = /\bgetParameter\s*\(|\bgetQueryString\s*\(|\bgetHeader\s*\(|\bgetInputStream\s*\(|\bgetCookies\s*\(|\b@RequestParam\b|\b@PathVariable\b/;
|
|
9704
|
+
var UNTRUSTED_SH = /\$\{?[1-9]\d*\b|\$[@*]|\$\{@\}|\bread\s+(?:-\S+\s+)*[A-Za-z_]\w*|\$\{?REPLY\b|\$\{?QUERY_STRING\b/;
|
|
9705
|
+
var UNTRUSTED_PHP = /\$_(?:GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|\bphp:\/\/input\b|\bgetallheaders\s*\(/;
|
|
9704
9706
|
function untrustedPatternFor(language) {
|
|
9705
9707
|
switch (language) {
|
|
9708
|
+
case "php":
|
|
9709
|
+
return UNTRUSTED_PHP;
|
|
9710
|
+
case "shell":
|
|
9711
|
+
return UNTRUSTED_SH;
|
|
9706
9712
|
case "python":
|
|
9707
9713
|
return UNTRUSTED_PY;
|
|
9708
9714
|
case "ruby":
|
|
@@ -9727,6 +9733,7 @@ var GENERIC_GUARD = (
|
|
|
9727
9733
|
/\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
|
|
9728
9734
|
);
|
|
9729
9735
|
var XXE_GUARD = /FEATURE_SECURE_PROCESSING|setExpandEntityReferences|disallow-doctype-decl|external-general-entities|external-parameter-entities|setXIncludeAware\s*\(\s*false/;
|
|
9736
|
+
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/;
|
|
9730
9737
|
var CODE_SINK = /\bglobalThis\s*\[|\bconstructor\b|\beval\b|\bFunction\b|\brun\s*\(|\bvm\s*\.\s*run/;
|
|
9731
9738
|
var EXFIL_SINK = /\bconsole\s*\.\s*(?:log|debug|info|warn|error)\s*\(|\bfetch\s*\(|\baxios\b|\brequest\s*\(|\.\s*send\s*\(/;
|
|
9732
9739
|
var SQL_KEYWORDS = "SELECT|INSERT\\s+INTO|INSERT|UPDATE|DELETE\\s+FROM|DELETE|DROP|UNION\\s+SELECT";
|
|
@@ -10001,6 +10008,16 @@ var CODE_RULES = [
|
|
|
10001
10008
|
pattern: /\byaml\s*\.\s*load\s*\((?![^)]*safe)|\bloadAll\s*\([^)]*unsafe/i
|
|
10002
10009
|
},
|
|
10003
10010
|
// ── XML external entities ────────────────────────────────────────────────
|
|
10011
|
+
//
|
|
10012
|
+
// Evidence that a file parses XML at all. The XXE rules match on receiver
|
|
10013
|
+
// *names* — `builder.parse(is)` is the shape the vulnerability actually takes,
|
|
10014
|
+
// and the declared type is rarely on that line — so without this the pattern
|
|
10015
|
+
// reads any `.parse()` on anything suffixed Builder, Parser or Reader as XML.
|
|
10016
|
+
// In practice that meant a hostname-mask parser (`HostMask.Parser.parse(...)`)
|
|
10017
|
+
// was reported as CWE-611 at high severity.
|
|
10018
|
+
//
|
|
10019
|
+
// An import is the cheapest honest signal: a file that parses XML says so at
|
|
10020
|
+
// the top, and one that never mentions XML is not parsing it.
|
|
10004
10021
|
{
|
|
10005
10022
|
id: "java-xxe-parser-defaults",
|
|
10006
10023
|
title: "XML parser left on its insecure defaults",
|
|
@@ -10021,7 +10038,10 @@ var CODE_RULES = [
|
|
|
10021
10038
|
severity: "high",
|
|
10022
10039
|
languages: ["java"],
|
|
10023
10040
|
// Receiver-qualified so `LocalDate.parse(s)` and friends stay out of it.
|
|
10041
|
+
// The suffix alone is not enough — plenty of parsers parse things that are
|
|
10042
|
+
// not XML — so `fileRequires` decides whether the file is in scope at all.
|
|
10024
10043
|
pattern: /\b\w*(?:[Bb]uilder|[Pp]arser|[Rr]eader)\s*\.\s*parse\s*\(/,
|
|
10044
|
+
fileRequires: XML_PARSING_FILE,
|
|
10025
10045
|
guard: XXE_GUARD,
|
|
10026
10046
|
guardBack: 6,
|
|
10027
10047
|
guardForward: 4
|
|
@@ -10143,6 +10163,212 @@ var CODE_RULES = [
|
|
|
10143
10163
|
severity: "high",
|
|
10144
10164
|
languages: ["javascript", "typescript"],
|
|
10145
10165
|
pattern: /\[\s*['"]__proto__['"]\s*\]|\bObject\s*\.\s*assign\s*\(\s*[\w.$]*\.prototype\b|\.\s*__proto__\s*=/
|
|
10166
|
+
},
|
|
10167
|
+
// ── Shell ────────────────────────────────────────────────────────────────
|
|
10168
|
+
//
|
|
10169
|
+
// `shell` was a language the type system knew about and no rule targeted, so
|
|
10170
|
+
// a repository written entirely in bash got secret detection and nothing
|
|
10171
|
+
// else. Installers, CI helpers and packaging scripts are where a great deal
|
|
10172
|
+
// of privileged work actually happens, and they run as whoever invoked them.
|
|
10173
|
+
{
|
|
10174
|
+
id: "sh-remote-script-execution",
|
|
10175
|
+
title: "network output piped into a shell",
|
|
10176
|
+
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.",
|
|
10177
|
+
cwe: "CWE-494",
|
|
10178
|
+
severity: "high",
|
|
10179
|
+
languages: ["shell"],
|
|
10180
|
+
// The pipe must be the *next* thing: `curl -o f url && sh f` is a different
|
|
10181
|
+
// (and checkable) shape, and `curl url | jq` is not an execution at all.
|
|
10182
|
+
pattern: /\b(?:curl|wget)\b[^|\n]*\|\s*(?:sudo\s+(?:-\S+\s+)*)?(?:\/bin\/|\/usr\/bin\/)?(?:ba|da|k|z|a)?sh\b/,
|
|
10183
|
+
// Nothing on this line can exonerate it. Integrity checking happens in a
|
|
10184
|
+
// separate step by construction, so a window guard would only mislead.
|
|
10185
|
+
guard: false
|
|
10186
|
+
},
|
|
10187
|
+
{
|
|
10188
|
+
id: "sh-eval-expansion",
|
|
10189
|
+
title: "eval on an expanded string",
|
|
10190
|
+
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.",
|
|
10191
|
+
cwe: "CWE-78",
|
|
10192
|
+
severity: "high",
|
|
10193
|
+
languages: ["shell"],
|
|
10194
|
+
// Matched against eval's *argument*, not against the rest of the line.
|
|
10195
|
+
//
|
|
10196
|
+
// `\beval\b.*\$` looks equivalent and is not: bash's ordinary dynamic-range
|
|
10197
|
+
// idiom, `eval echo {$((k + 1))..$((k + n))}`, contains `$k` inside the
|
|
10198
|
+
// arithmetic, so a line-wide search finds an expansion in a construct that
|
|
10199
|
+
// cannot carry a command — `$((…))` is parsed as an expression, where a `;`
|
|
10200
|
+
// is a syntax error rather than a second command. That spelling reported
|
|
10201
|
+
// 355 findings in one 3,500-line script, all of them the same safe loop.
|
|
10202
|
+
//
|
|
10203
|
+
// What is left is the form where eval is handed a value directly —
|
|
10204
|
+
// `eval "$cmd"`, `eval $cmd`, `eval "$(…)"` — which is the shape that
|
|
10205
|
+
// actually re-parses untrusted text as source. `eval echo $x` is not
|
|
10206
|
+
// covered; catching it without also catching the range idiom needs to know
|
|
10207
|
+
// which expansions are arithmetic, which is parsing, not matching.
|
|
10208
|
+
pattern: /\beval\s+(?:-\S+\s+)*(?:"\s*)?\$(?:\{?[A-Za-z_]\w*|\((?!\())/,
|
|
10209
|
+
// The shell-init idiom `eval "$(tool init -)"` is the documented interface
|
|
10210
|
+
// of most version managers. It is still eval of program output, but the
|
|
10211
|
+
// program is a fixed local binary, and flagging it reports every developer
|
|
10212
|
+
// dotfile in existence.
|
|
10213
|
+
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/
|
|
10214
|
+
},
|
|
10215
|
+
{
|
|
10216
|
+
id: "sh-unquoted-expansion-destructive",
|
|
10217
|
+
title: "unquoted expansion in a destructive command",
|
|
10218
|
+
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 /`.",
|
|
10219
|
+
cwe: "CWE-78",
|
|
10220
|
+
severity: "high",
|
|
10221
|
+
languages: ["shell"],
|
|
10222
|
+
// `[^"'\n]*?` cannot cross a quote, so `rm -rf "$dir"` — the correct form —
|
|
10223
|
+
// never reaches the `$` and never matches. Only a genuinely bare expansion
|
|
10224
|
+
// does. Restricted to recursive/forced removal: a bare `$f` in `rm $f` is
|
|
10225
|
+
// sloppy, but it is not the shape that erases a filesystem.
|
|
10226
|
+
pattern: /\brm\s+(?:-[a-zA-Z-]*[rRf][a-zA-Z-]*\s+)+[^"'\n]*?\$\{?[A-Za-z_]/
|
|
10227
|
+
},
|
|
10228
|
+
{
|
|
10229
|
+
id: "sh-insecure-transport-flag",
|
|
10230
|
+
title: "certificate verification disabled",
|
|
10231
|
+
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.",
|
|
10232
|
+
cwe: "CWE-295",
|
|
10233
|
+
severity: "high",
|
|
10234
|
+
languages: ["shell"],
|
|
10235
|
+
pattern: /\b(?:curl|wget)\b[^\n|]*(?:\s-k(?=\s|$)|\s--insecure\b|\s--no-check-certificate\b)/,
|
|
10236
|
+
// Over plain HTTP there is no certificate to skip, so the flag is inert and
|
|
10237
|
+
// this rule has nothing to say — `sh-plaintext-download` is the finding
|
|
10238
|
+
// that fits. Reporting both put two entries on one line, one of which
|
|
10239
|
+
// recommended a fix that would change nothing.
|
|
10240
|
+
lineGuard: /^(?!.*https:\/\/).*\bhttp:\/\//
|
|
10241
|
+
},
|
|
10242
|
+
{
|
|
10243
|
+
id: "sh-plaintext-download",
|
|
10244
|
+
title: "download over plain HTTP",
|
|
10245
|
+
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.",
|
|
10246
|
+
cwe: "CWE-319",
|
|
10247
|
+
severity: "high",
|
|
10248
|
+
languages: ["shell"],
|
|
10249
|
+
pattern: /\b(?:curl|wget)\b[^\n|]*\bhttp:\/\//,
|
|
10250
|
+
// Loopback and link-local are not carried over a network anyone can sit on.
|
|
10251
|
+
lineGuard: /http:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\]|169\.254\.|host\.docker\.internal)\b/
|
|
10252
|
+
},
|
|
10253
|
+
{
|
|
10254
|
+
id: "sh-world-writable-permissions",
|
|
10255
|
+
title: "world-writable permissions",
|
|
10256
|
+
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.",
|
|
10257
|
+
cwe: "CWE-732",
|
|
10258
|
+
severity: "medium",
|
|
10259
|
+
languages: ["shell"],
|
|
10260
|
+
pattern: /\bchmod\s+(?:-[a-zA-Z-]+\s+)*(?:0?777|a\+rwx|ugo\+rwx|a=rwx)\b/
|
|
10261
|
+
},
|
|
10262
|
+
{
|
|
10263
|
+
id: "sh-predictable-temp-path",
|
|
10264
|
+
title: "predictable temporary file",
|
|
10265
|
+
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.",
|
|
10266
|
+
cwe: "CWE-377",
|
|
10267
|
+
severity: "medium",
|
|
10268
|
+
languages: ["shell"],
|
|
10269
|
+
// Redirection or an explicit write into a literal `/tmp` path. A `$$` or
|
|
10270
|
+
// `$RANDOM` suffix is still predictable, so it is not treated as a fix;
|
|
10271
|
+
// `mktemp` is, and it is the guard below.
|
|
10272
|
+
pattern: /(?:>{1,2}\s*|\b(?:tee|touch|cp|mv|install)\s+(?:-\S+\s+)*)\/tmp\/[\w.$-]+/,
|
|
10273
|
+
guard: /\bmktemp\b/,
|
|
10274
|
+
guardBack: 6,
|
|
10275
|
+
guardForward: 2
|
|
10276
|
+
},
|
|
10277
|
+
// ── PHP ──────────────────────────────────────────────────────────────────
|
|
10278
|
+
//
|
|
10279
|
+
// `php` was the second language in `ScanLanguage` that no rule targeted, for
|
|
10280
|
+
// the same reason `shell` was: nothing checks the two lists against each
|
|
10281
|
+
// other. `LANGUAGE_COVERAGE` in the tests does now.
|
|
10282
|
+
{
|
|
10283
|
+
id: "php-sql-interpolation",
|
|
10284
|
+
title: "SQL assembled by interpolation",
|
|
10285
|
+
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.",
|
|
10286
|
+
cwe: "CWE-89",
|
|
10287
|
+
severity: "critical",
|
|
10288
|
+
languages: ["php"],
|
|
10289
|
+
// Interpolation (`"… $id …"`, `"… {$id} …"`) or concatenation onto a SQL
|
|
10290
|
+
// string. A prepared statement passes placeholders and binds separately,
|
|
10291
|
+
// so its query string contains neither.
|
|
10292
|
+
pattern: new RegExp(
|
|
10293
|
+
`\\b(?:mysqli_query|mysql_query|pg_query|->\\s*(?:query|exec|unprepared))\\s*\\([^)]*(?:"[^"\\n]*(?:${SQL_KEYWORDS})\\b[^"\\n]*(?:\\$\\w+|\\{\\$)|${SQL_STRING}\\s*\\.)`,
|
|
10294
|
+
"i"
|
|
10295
|
+
)
|
|
10296
|
+
},
|
|
10297
|
+
{
|
|
10298
|
+
id: "php-shell-exec-interpolation",
|
|
10299
|
+
title: "shell command built from a variable",
|
|
10300
|
+
consequence: "The string is handed to `/bin/sh`, which interprets `;`, `|` and `$(\u2026)` in whatever the variable held.",
|
|
10301
|
+
cwe: "CWE-78",
|
|
10302
|
+
severity: "critical",
|
|
10303
|
+
languages: ["php"],
|
|
10304
|
+
pattern: /\b(?:exec|system|shell_exec|passthru|popen|proc_open|pcntl_exec)\s*\(\s*(?:"[^"\n]*(?:\$\w+|\{\$)|'[^'\n]*'\s*\.|\$\w+\s*\.)/,
|
|
10305
|
+
// `escapeshellarg`/`escapeshellcmd` are the correct answer and are usually
|
|
10306
|
+
// applied inline, so a line-scoped guard is the right shape.
|
|
10307
|
+
lineGuard: /\bescapeshell(?:arg|cmd)\s*\(/
|
|
10308
|
+
},
|
|
10309
|
+
{
|
|
10310
|
+
id: "php-dynamic-code-execution",
|
|
10311
|
+
title: "dynamic code execution",
|
|
10312
|
+
consequence: "Whatever reaches this call is executed as PHP source with the privileges of the web process.",
|
|
10313
|
+
cwe: "CWE-95",
|
|
10314
|
+
severity: "critical",
|
|
10315
|
+
languages: ["php"],
|
|
10316
|
+
pattern: /\b(?:eval|assert|create_function)\s*\(\s*(?:\$\w+|"[^"\n]*(?:\$\w+|\{\$))/,
|
|
10317
|
+
guard: false
|
|
10318
|
+
},
|
|
10319
|
+
{
|
|
10320
|
+
id: "php-dynamic-file-inclusion",
|
|
10321
|
+
title: "include path built from a variable",
|
|
10322
|
+
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.",
|
|
10323
|
+
cwe: "CWE-98",
|
|
10324
|
+
severity: "critical",
|
|
10325
|
+
languages: ["php"],
|
|
10326
|
+
pattern: /\b(?:include|include_once|require|require_once)\s*(?:\(\s*)?(?:\$\w+|"[^"\n]*(?:\$\w+|\{\$)|'[^'\n]*'\s*\.)/,
|
|
10327
|
+
// A basename-and-allow-list is the standard fix; `basename` alone still
|
|
10328
|
+
// leaves the extension open, but it defeats traversal, which is the part
|
|
10329
|
+
// this rule is about.
|
|
10330
|
+
lineGuard: /\bbasename\s*\(/
|
|
10331
|
+
},
|
|
10332
|
+
{
|
|
10333
|
+
id: "php-unserialize-untrusted",
|
|
10334
|
+
title: "unserialize on request data",
|
|
10335
|
+
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.",
|
|
10336
|
+
cwe: "CWE-502",
|
|
10337
|
+
severity: "critical",
|
|
10338
|
+
languages: ["php"],
|
|
10339
|
+
pattern: /\bunserialize\s*\(\s*(?:\$_(?:GET|POST|REQUEST|COOKIE)\b|\$\w+)/,
|
|
10340
|
+
// The key is an array key and is normally quoted: `['allowed_classes' => false]`.
|
|
10341
|
+
lineGuard: /["']?allowed_classes["']?\s*=>\s*(?:false|\[)/,
|
|
10342
|
+
needsContext: true
|
|
10343
|
+
},
|
|
10344
|
+
{
|
|
10345
|
+
id: "php-unescaped-output",
|
|
10346
|
+
title: "request data echoed without escaping",
|
|
10347
|
+
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.",
|
|
10348
|
+
cwe: "CWE-79",
|
|
10349
|
+
severity: "high",
|
|
10350
|
+
languages: ["php"],
|
|
10351
|
+
pattern: /\b(?:echo|print)\s+[^;\n]*\$_(?:GET|POST|REQUEST|COOKIE|SERVER)\b|<\?=\s*\$_(?:GET|POST|REQUEST|COOKIE)\b/
|
|
10352
|
+
},
|
|
10353
|
+
{
|
|
10354
|
+
id: "php-request-path-traversal",
|
|
10355
|
+
title: "file path taken from the request",
|
|
10356
|
+
consequence: "A `../` sequence in the value walks out of the intended directory, and the process reads or writes wherever it lands.",
|
|
10357
|
+
cwe: "CWE-22",
|
|
10358
|
+
severity: "high",
|
|
10359
|
+
languages: ["php"],
|
|
10360
|
+
pattern: /\b(?:file_get_contents|file_put_contents|fopen|readfile|unlink|copy|rename|opendir|scandir)\s*\(\s*[^)\n]*\$_(?:GET|POST|REQUEST|COOKIE)\b/,
|
|
10361
|
+
lineGuard: /\bbasename\s*\(|\brealpath\s*\(/
|
|
10362
|
+
},
|
|
10363
|
+
{
|
|
10364
|
+
id: "php-variable-injection",
|
|
10365
|
+
title: "request data expanded into local variables",
|
|
10366
|
+
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.",
|
|
10367
|
+
cwe: "CWE-621",
|
|
10368
|
+
severity: "high",
|
|
10369
|
+
languages: ["php"],
|
|
10370
|
+
pattern: /\bextract\s*\(\s*\$_(?:GET|POST|REQUEST|COOKIE)\b|\bimport_request_variables\s*\(/,
|
|
10371
|
+
guard: false
|
|
10146
10372
|
}
|
|
10147
10373
|
];
|
|
10148
10374
|
var COMMENT_PREFIX = /^\s*(?:\/\/|\/\*|\*|#|--|<!--)/;
|
|
@@ -10184,11 +10410,21 @@ function windowText(lines, index, back, forward, prose) {
|
|
|
10184
10410
|
}
|
|
10185
10411
|
return collected.join("\n");
|
|
10186
10412
|
}
|
|
10413
|
+
var FILE_TEXT = /* @__PURE__ */ new WeakMap();
|
|
10414
|
+
function fileTextOf(lines) {
|
|
10415
|
+
let text = FILE_TEXT.get(lines);
|
|
10416
|
+
if (text === void 0) {
|
|
10417
|
+
text = lines.join("\n");
|
|
10418
|
+
FILE_TEXT.set(lines, text);
|
|
10419
|
+
}
|
|
10420
|
+
return text;
|
|
10421
|
+
}
|
|
10187
10422
|
function evaluateRule(rule, ctx) {
|
|
10188
10423
|
if (rule.languages && !rule.languages.includes(ctx.language)) return null;
|
|
10189
10424
|
const line = ctx.lines[ctx.index] ?? "";
|
|
10190
10425
|
if (isComment(line) || ctx.prose?.has(ctx.index)) return null;
|
|
10191
10426
|
if (!rule.pattern.test(line)) return null;
|
|
10427
|
+
if (rule.fileRequires && !rule.fileRequires.test(fileTextOf(ctx.lines))) return null;
|
|
10192
10428
|
const back = rule.guardBack ?? 8;
|
|
10193
10429
|
const forward = rule.guardForward ?? 0;
|
|
10194
10430
|
const context = windowText(ctx.lines, ctx.index, back, forward, ctx.prose);
|
|
@@ -10359,7 +10595,8 @@ function editDistance(a, b, cap = 3) {
|
|
|
10359
10595
|
}
|
|
10360
10596
|
function detectTyposquat(name, ecosystem) {
|
|
10361
10597
|
const popular = ecosystem === "npm" ? POPULAR_NPM : POPULAR_PYPI;
|
|
10362
|
-
const lower = name.toLowerCase()
|
|
10598
|
+
const lower = name.toLowerCase();
|
|
10599
|
+
if (/^@[^/]+\//.test(lower)) return null;
|
|
10363
10600
|
if (popular.includes(lower)) return null;
|
|
10364
10601
|
if (lower.length < 4) return null;
|
|
10365
10602
|
const normalized = normalizeName(lower);
|
|
@@ -10771,6 +11008,33 @@ function languageOf(filename) {
|
|
|
10771
11008
|
if (filename.startsWith(".env") || filename.endsWith(".env")) return "config";
|
|
10772
11009
|
return LANGUAGE_BY_EXTENSION[(0, import_node_path3.extname)(filename).toLowerCase()] ?? "other";
|
|
10773
11010
|
}
|
|
11011
|
+
var LANGUAGE_BY_INTERPRETER = {
|
|
11012
|
+
sh: "shell",
|
|
11013
|
+
bash: "shell",
|
|
11014
|
+
zsh: "shell",
|
|
11015
|
+
dash: "shell",
|
|
11016
|
+
ksh: "shell",
|
|
11017
|
+
ash: "shell",
|
|
11018
|
+
python: "python",
|
|
11019
|
+
python2: "python",
|
|
11020
|
+
python3: "python",
|
|
11021
|
+
ruby: "ruby",
|
|
11022
|
+
node: "javascript",
|
|
11023
|
+
nodejs: "javascript",
|
|
11024
|
+
deno: "javascript",
|
|
11025
|
+
bun: "javascript",
|
|
11026
|
+
php: "php"
|
|
11027
|
+
};
|
|
11028
|
+
function languageOfShebang(firstLine) {
|
|
11029
|
+
const match = /^#!\s*(\S+)(?:\s+(\S+))?/.exec(firstLine);
|
|
11030
|
+
if (!match) return null;
|
|
11031
|
+
const command = (0, import_node_path3.basename)(match[1]);
|
|
11032
|
+
const name = command === "env" && match[2] ? (0, import_node_path3.basename)(match[2]) : command;
|
|
11033
|
+
const exact = LANGUAGE_BY_INTERPRETER[name];
|
|
11034
|
+
if (exact) return exact;
|
|
11035
|
+
const stripped = name.replace(/[\d.]+$/, "");
|
|
11036
|
+
return (stripped ? LANGUAGE_BY_INTERPRETER[stripped] : void 0) ?? null;
|
|
11037
|
+
}
|
|
10774
11038
|
var SUPPRESS_NEXT = /threatcrush-disable-next-line(?:\s+([\w-]+))?/;
|
|
10775
11039
|
var SUPPRESS_LINE = /threatcrush-disable-line(?:\s+([\w-]+))?/;
|
|
10776
11040
|
function collectSuppressions(lines) {
|
|
@@ -10887,12 +11151,14 @@ function scanPath(targetPath, options = {}) {
|
|
|
10887
11151
|
const extension = (0, import_node_path3.extname)(filename).toLowerCase();
|
|
10888
11152
|
const isManifest = filename === "package.json" || filename === "requirements.txt";
|
|
10889
11153
|
const scannable = SCAN_EXTENSIONS.has(extension) || filename.startsWith(".env");
|
|
10890
|
-
|
|
11154
|
+
const mayDeclareInterpreter = !scannable && !isManifest && extension === "";
|
|
11155
|
+
if (!scannable && !isManifest && !mayDeclareInterpreter) {
|
|
10891
11156
|
recordSensitiveFile(filename, relativePath, findings, []);
|
|
10892
11157
|
return;
|
|
10893
11158
|
}
|
|
10894
11159
|
let text;
|
|
10895
11160
|
let handle;
|
|
11161
|
+
let declared = null;
|
|
10896
11162
|
try {
|
|
10897
11163
|
handle = (0, import_node_fs6.openSync)(fullPath, "r");
|
|
10898
11164
|
} catch {
|
|
@@ -10901,6 +11167,12 @@ function scanPath(targetPath, options = {}) {
|
|
|
10901
11167
|
}
|
|
10902
11168
|
try {
|
|
10903
11169
|
if ((0, import_node_fs6.fstatSync)(handle).size > maxFileBytes) return;
|
|
11170
|
+
if (mayDeclareInterpreter) {
|
|
11171
|
+
const prefix = Buffer.alloc(128);
|
|
11172
|
+
const read = (0, import_node_fs6.readSync)(handle, prefix, 0, prefix.length, 0);
|
|
11173
|
+
declared = languageOfShebang(prefix.subarray(0, read).toString("utf-8").split("\n", 1)[0] ?? "");
|
|
11174
|
+
if (!declared) return;
|
|
11175
|
+
}
|
|
10904
11176
|
text = (0, import_node_fs6.readFileSync)(handle, "utf-8");
|
|
10905
11177
|
} catch {
|
|
10906
11178
|
unreadable.push(relativePath);
|
|
@@ -10915,7 +11187,7 @@ function scanPath(targetPath, options = {}) {
|
|
|
10915
11187
|
options.onFile?.(relativePath);
|
|
10916
11188
|
suppressed += collectSuppressions(text.split("\n")).count;
|
|
10917
11189
|
const fileFindings = [
|
|
10918
|
-
...scanText(relativePath, text, languageOf(filename)),
|
|
11190
|
+
...scanText(relativePath, text, declared ?? languageOf(filename)),
|
|
10919
11191
|
...isManifest ? scanManifest(relativePath, filename, text) : []
|
|
10920
11192
|
];
|
|
10921
11193
|
findings.push(...fileFindings);
|