Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions malware-scan/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ objects without checking anything out.

| Rule | Severity | Catches |
|------|----------|---------|
| `WHITESPACE_PADDING` | high | Code after 150+ spaces/tabs on one line (hidden off-screen) |
| `GLOBAL_REQUIRE` | high | `require` assigned onto `global` |
| `WHITESPACE_PADDING` | high in config files, or when code (200+ chars, or `require(`, `global[`, `eval(` and similar) follows; otherwise low | Code after 150+ spaces/tabs on one line (hidden off-screen). Aligned comments in vendor bundles and `.d.ts` files are low, so hidden unless you pass `--min-severity low` |
| `GLOBAL_REQUIRE` | high | The `require` function itself assigned onto `global` (`global['r'] = require`). Storing the result of a call, like `global.x = require('y')`, is not flagged |
| `GLOBAL_MARKER` | high | The campaign's string marker assigned onto `global` |
| `OBFUSCATOR_ID` | high | `_$_` + 4 hex identifiers from the obfuscator |
| `LIFECYCLE_SCRIPT` | high | `preinstall`/`postinstall`/`prepare` npm scripts that download or pipe to a shell |
Expand Down
15 changes: 14 additions & 1 deletion malware-scan/scan_malicious.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
("WHITESPACE_PADDING", "high", re.compile(r"\S[ \t]{150,}\S"),
"code hidden after a long run of whitespace (pushed off-screen)"),
("GLOBAL_REQUIRE", "high",
re.compile(r"global\s*(\[[^=;]{1,60}\]|\.\w+)\s*=\s*require\b"),
re.compile(r"global\s*(\[[^=;]{1,60}\]|\.\w+)\s*=\s*require\b(?!\s*\()"),
"require() exported onto the global object"),
("GLOBAL_MARKER", "high",
re.compile(r"""global\s*(\.\s*o|\[\s*['"](!|_V|o|r)['"]\s*\])\s*=\s*['"]"""),
Expand All @@ -66,6 +66,13 @@
("EVAL", "low", re.compile(r"\beval\s*\("), "eval() call"),
]

# Text after a whitespace run that looks like hidden code rather than formatting
# (aligned comments/tables in vendor bundles and .d.ts files end in a word or a "|").
HIDDEN_CODE = re.compile(
r"global\s*[\[.]|require\s*\(|\beval\s*\(|Function\s*\(|fromCharCode|child_process"
r"|_\$_[0-9a-fA-F]{4}|\bimport\s*\(|https?://|\batob\s*\(|Buffer\.from")
HIDDEN_CODE_MIN = 200 # payloads hide thousands of characters after the padding

SUSPICIOUS_CMD = re.compile(
r"curl|wget|node\s+-e|base64|powershell|Invoke-|\|\s*(sh|bash)\b|https?://")
LIFECYCLE = ("preinstall", "install", "postinstall", "prepare", "prepublish")
Expand Down Expand Up @@ -180,6 +187,12 @@ def scan_text(path, text, real_path=None):
for rid, sev, rx, desc in RULES:
m = rx.search(line)
if m:
if rid == "WHITESPACE_PADDING" and not is_config:
tail = line[m.end() - 1:].strip()
is_comment = line.lstrip().startswith(("*", "//", "/*", "#"))
if not HIDDEN_CODE.search(tail) and (is_comment or len(tail) < HIDDEN_CODE_MIN):
sev = "low"
desc = "long run of whitespace followed by short text (likely formatting)"
if is_config and sev == "medium":
sev = "high"
findings.append((sev, rid, path, n, desc, preview(line, m.start())))
Expand Down
Loading