diff --git a/malware-scan/README.md b/malware-scan/README.md index 9fa9bdc..ef4694e 100644 --- a/malware-scan/README.md +++ b/malware-scan/README.md @@ -35,7 +35,11 @@ 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/`. -Skipped: `node_modules`, `vendor`, `dist`, `build`, `.next`, lockfiles, `*.min.js`. +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 +`vendor/` folder. The on-disk walk (`--path`) also skips untracked install and cache folders: +`node_modules`, `.next`, `.nuxt`, `coverage`, `.venv`/`venv`, `__pycache__`, `.turbo`, `.cache`. ## 1. CI gate (every project) diff --git a/malware-scan/scan_malicious.py b/malware-scan/scan_malicious.py index 5bbcf72..8f4d77d 100755 --- a/malware-scan/scan_malicious.py +++ b/malware-scan/scan_malicious.py @@ -25,9 +25,13 @@ TEXT_EXT = {".js", ".cjs", ".mjs", ".jsx", ".ts", ".tsx", ".json", ".vue", ".svelte", ".html", ".py", ".sh", ".yml", ".yaml"} -# Directories never descended into / never reported. -SKIP_DIRS = {"node_modules", ".git", "dist", "build", ".next", ".nuxt", "vendor", - "coverage", ".venv", "venv", "__pycache__", ".turbo", ".cache"} +# Directories skipped by the on-disk walk (--path) only: installed or generated +# content that is normally untracked. Git modes scan every tracked path, because a +# committed vendor/, dist/ or build/ folder is as attacker-writable as src/. +# vendor/, dist/ and build/ are deliberately NOT listed: repos commit them (e.g. +# WordPress plugins ship vendor/) and payloads have been found there. +DISK_SKIP_DIRS = {"node_modules", ".git", ".next", ".nuxt", "coverage", ".venv", + "venv", "__pycache__", ".turbo", ".cache"} SKIP_SUFFIX = (".min.js", ".map", "package-lock.json", "yarn.lock", "pnpm-lock.yaml") MAX_BYTES = 3_000_000 @@ -72,10 +76,10 @@ def norm(path): return path.replace("\\", "/") -def should_scan(path): +def should_scan(path, skip_dirs=frozenset()): p = norm(path) parts = p.split("/") - if any(part in SKIP_DIRS for part in parts[:-1]) or p.endswith(SKIP_SUFFIX): + 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))) @@ -210,11 +214,11 @@ def iter_tracked(): def iter_disk(root): for d, dirs, files in os.walk(root): - dirs[:] = [x for x in dirs if x not in SKIP_DIRS] + dirs[:] = [x for x in dirs if x not in DISK_SKIP_DIRS] for f in files: full = os.path.join(d, f) rel = norm(os.path.relpath(full, root)) - if should_scan(rel): + if should_scan(rel, DISK_SKIP_DIRS): try: text = read_file(full) except OSError: