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
6 changes: 4 additions & 2 deletions malware-scan/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ objects without checking anything out.
|------|----------|---------|
| `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` |
| `GLOBAL_MARKER` | high | The campaign's string marker assigned onto `global` (`global.o`, `global['!']`, `global['_V']`, and `global.i` set to a `5-…-du` or `A8-…` value) |
| `SIGNATURE_STRING` | high | The published PolinRider obfuscator signatures `rmcej%otb%` and `Cot%3t=shtP` |
| `PROPAGATION_SCRIPT` | high | A committed `temp_auto_push.bat`, the script the campaign uses to amend commits with a rewound clock and force-push them |
| `OBFUSCATOR_ID` | high | `_$_` + 4 hex identifiers from the obfuscator |
| `LIFECYCLE_SCRIPT` | high | `preinstall`/`postinstall`/`prepare` npm scripts that download or pipe to a shell |
| `VSCODE_FOLDEROPEN` | high / medium | `.vscode/tasks.json` tasks that auto-run when the folder is opened (high if they download/execute) |
Expand All @@ -34,7 +36,7 @@ Only **high** findings fail the scan. On a clean checkout of `vitejs/vite` (2,17
reports 0 high and 3 legitimate medium findings.

Scanned: `.js .cjs .mjs .jsx .ts .tsx .json .vue .svelte .html .py .sh .yml .yaml`, `*.config.*`,
dotfile configs, and extensionless hooks in `.husky/` and `.githooks/`.
dotfile configs, extensionless hooks in `.husky/` and `.githooks/`, and any file named `temp_auto_push.bat`.
Skipped everywhere: lockfiles, `*.map`, `*.min.js`.
Git modes (default, `--staged`, `--all-branches`) scan every tracked path, including committed
`vendor/`, `dist/`, `build/` and `node_modules/`. Payloads have been found in a committed
Expand Down
16 changes: 13 additions & 3 deletions malware-scan/scan_malicious.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
HOOK_DIR_RE = re.compile(r"(^|/)\.(husky|githooks)/[^/]+$")
VSCODE_TASKS_RE = re.compile(r"(^|/)\.vscode/tasks\.json$")
CLAUDE_SETTINGS_RE = re.compile(r"(^|/)\.claude/settings(\.local)?\.json$")
# Windows script the campaign uses to amend commits with a rewound clock and force-push.
PROPAGATION_RE = re.compile(r"(^|/)temp_auto_push\.bat$", re.I)

# (rule id, severity, regex, description)
RULES = [
Expand All @@ -51,8 +53,12 @@
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*['"]"""),
"known campaign marker (global.o / global['!'] string assignment)"),
re.compile(r"""global\s*(\.\s*o|\[\s*['"](!|_V|o|r)['"]\s*\])\s*=\s*['"]"""
r"""|global\s*\.\s*i\s*=\s*['"](\d+-\d+(-\d+)?-du|A8-)"""),
"known campaign marker (global.o / global.i / global['!'] string assignment)"),
# [b] and [P] stop this file from matching its own pattern.
("SIGNATURE_STRING", "high", re.compile(r"rmcej%ot[b]%|Cot%3t=sht[P]"),
"published PolinRider obfuscator signature string"),
("OBFUSCATOR_ID", "high", re.compile(r"_\$_[0-9a-fA-F]{4}\b"),
"obfuscator-generated identifier (_$_ followed by 4 hex chars)"),
("CHAR127_DECODER", "medium", re.compile(r"fromCharCode\(\s*127\s*\)"),
Expand Down Expand Up @@ -89,7 +95,8 @@ def should_scan(path, skip_dirs=frozenset()):
if any(part in skip_dirs for part in parts[:-1]) or p.endswith(SKIP_SUFFIX):
return False
return (os.path.splitext(p)[1].lower() in TEXT_EXT
or bool(CONFIG_RE.search(p)) or bool(HOOK_DIR_RE.search(p)))
or bool(CONFIG_RE.search(p)) or bool(HOOK_DIR_RE.search(p))
or bool(PROPAGATION_RE.search(p)))


def preview(line, start):
Expand Down Expand Up @@ -143,6 +150,9 @@ def walk_strings(obj):
def scan_structured(path, real, text):
"""Checks for files that trigger code automatically (npm, VS Code, Claude Code)."""
out = []
if PROPAGATION_RE.search(real):
out.append(("high", "PROPAGATION_SCRIPT", path, 0,
"temp_auto_push.bat: the campaign's commit-amend and force-push script", ""))
if real.endswith("package.json"):
data = load_json(text)
scripts = (data or {}).get("scripts", {}) if isinstance(data, dict) else {}
Expand Down
Loading