Resource
malwarecybersecurity

Malware Audit & Cleanup Playbook (JavaScript / Node projects)

October 2, 2026
Resource

# Malware Audit & Cleanup Playbook (JavaScript / Node projects)
Copy this file into any project and either follow it by hand or give it to Claude Code with:
> Follow `malware-audit-playbook.md` in this project. Obey the Safety Contract in section 0 exactly: remove only the malware, delete nothing else, commit/push nothing, and finish with the instructions for me.
Written from a real incident (Oct 2026): an obfuscated payload was appended to `postcss.config.mjs`, a deploy script and other config files, launched hidden background `node` processes, tried to contact a remote server, and spread to ~10 projects on one machine.
---
## 0. SAFETY CONTRACT (binding for Claude Code, Cursor, any AI agent, and scripts)
The goal of an automated run is narrow: **neutralise the malware and report.** Everything else is left to the human, with written instructions.
### 0.1 The agent MAY (automatically)
| Action | Condition ||--------|-----------|| **Read / search** files, `git log`, `git show`, `git status`, process list, `lsof`, launch-agent listings | Always; read-only || **Run the read-only scanner** (§3.2) | Always || **Stop a running payload process** (`kill <PID>`, never `kill -9` first, never `killall node`) | Only a PID whose command line matches a payload marker (§1.1), after printing its PID, start time, working directory and command (first 120 chars) || **Delete the malware itself** — (a) remove the payload code from an infected file, by restoring that single file from the last clean commit or by cutting the payload off the end of the line (§5); (b) delete a file that consists *only* of malware (a dropper that is not in any clean commit and has no legitimate content) | Only what the scanner/inspection confirmed as malware (MARKER or HIDDEN). **No backup copy of the malware is kept** — don't create quarantine folders or `.infected` copies. Show the user the file list and the `git diff --stat` afterwards. Legitimate code in the same file is preserved || **Re-run the scanner** to verify | Always || **Write the final report** (§10) and the **instructions for the user** (§0.4) | Always |
### 0.2 The agent MUST NOT (without the user typing an explicit "yes" for that exact action in the chat)
-**Delete anything other than the confirmed malware itself (§0.3)**: no `rm`, `rm -rf`, `git clean`, `git rm`, `find -delete`, `trash`, `mv` to trash — not files, folders, `node_modules`, build output, logs, launch agents, cron entries, extensions, or apps. The only deletions allowed are the payload code and malware-only files confirmed under §0.3. If anything else *should* be deleted, put it in the user instructions (§0.4) instead.-**Rewrite or discard history/work**: no `git reset --hard`, `git checkout -- .` / `git restore .` on the whole tree, `git stash drop`, `git rebase`, `git filter-repo`, `git push --force`, branch deletion.-**Commit, push, tag, open PRs, or change remotes.** Leave the cleaned files as an uncommitted change for the human to review.-**Touch any file that is not confirmed infected** — no "while I'm here" fixes, formatting, dependency upgrades, lockfile edits.-**Run project code**: `npm/yarn/pnpm install|ci|run`, `dev`, `build`, `start`, tests, deploy scripts, config files, or the payload (it can re-infect and exfiltrate).-**Change credentials or accounts**: no logging out/in, no token/key creation or revocation, no editing `.env*`, no reading `.env` contents into the chat.-**Modify system persistence or software**: no editing/unloading launch agents/daemons, cron, shell rc files, login items; no uninstalling apps or extensions; no `sudo`.-**Scan or modify other projects/directories** than the one it was asked to work in, unless the user names them.-**Deobfuscate by executing**, or paste payload contents, IPs-as-clickable-links, tokens or secrets into the chat. Describe the payload; don't reproduce it.-**Continue after a surprise.** If the situation doesn't match this playbook (unexpected files, scanner errors, a process that won't die, a file with both malware and real code you can't separate), **stop and report** instead of improvising.
### 0.3 How malware is deleted (no quarantine, no backups of the payload)
The malware is **deleted, not preserved**. Do not copy it to a quarantine folder, rename it to `.infected`, or paste it anywhere. Recovery of the legitimate content comes from git history, which still has the clean versions.
1.**Confirm it is malware** (scanner MARKER/HIDDEN, or you inspected the long line's start with `awk ... substr`).2.**Make sure the legitimate part is recoverable**: `git status` shows the file's real code is in git (last clean commit exists), or the legitimate lines are visible before the payload. If neither is true — e.g. the file is untracked, has uncommitted legitimate edits mixed with the payload, or you can't tell where the real code ends — **stop and ask** instead of deleting.3.**Delete only the payload**:- Preferred: restore the single file from the last clean commit (§5.1) and re-apply any legitimate edits the entry commit made.- Otherwise: cut the payload text off the line, keeping the legitimate code (§5.2).- If the whole file is malware (no legitimate content, not in any clean commit): delete that one file.4.**Verify** with the scanner that the markers are gone and that the file's size/line count matches a normal version.5. Leave the change **uncommitted** for the user to review (§0.4).
### 0.4 Final output the agent must give the user (always)
End every run with a message in this shape (plain language, no payload contents):
```MALWARE AUDIT RESULT — <project>
STATUS: INFECTED / CLEAN / NEEDS YOUR DECISION
WHAT I DID (automatically): - Stopped process(es): <PID, started, folder it ran from> (or "none found") - Deleted the malware from: <file list> (legitimate code kept; clean versions are in git history <hash>) - Re-ran the scanner: clean / still flagged <files>
WHAT I FOUND: - Infected files: ... - Entry commit: <hash, date, subject>; pushed to remote: yes/no - Last clean commit: <hash> - Persistence found: none / <details, NOT changed> - Suspicious package scripts / extensions: ...
WHAT I DID NOT DO (on purpose): delete files, commit, push, install, run the project, touch credentials or system settings.
WHAT YOU NEED TO DO (in order): 1. Review the change: git diff --stat and git diff -- <files> 2. (If happy) commit it: git add -p && git commit -m "Remove injected malware from <files>" — then push only after step 5. 3. Reinstall dependencies safely: <exact commands for this package manager with --ignore-scripts> 4. Check the other projects on this machine (run this playbook in each) — these also contain markers: <list if known> 5. Rotate credentials (see section 7), AFTER the machine is clean: <specific list for this project: .env keys, cloud tokens, GitHub tokens, SSH keys...> 6. Tell collaborators if the malware was pushed to a shared branch; they must scan and rotate too. 7. Optional hardening: pre-commit guard (§8), editor whitespace/wrap settings, run unknown code in a VM.
QUESTIONS FOR YOU: <anything that needed a decision, e.g. file mixes real code and payload>```
### 0.5 General rules
1.**Never execute, import, `eval` or "test run" the suspicious code.** Read files with `cat`, `head -c`, `grep`, `git show`.2.**Don't "deobfuscate by running it".** Analysis is static only; it isn't needed for this playbook.3.**Stop processes before cleaning files**, otherwise they can re-infect the files you just fixed.4.**Assume secrets on the machine were exposed** if any infected file was ever executed (§7) — tell the user; don't act on credentials yourself.5. Don't print `.env` values, tokens or payload bodies into chats, tickets or public posts.6. Shell caveat (macOS zsh): an unmatched glob like `--include=*.js` aborts the command with `no matches found`. Quote globs or use `find`/`grep -r --include='*.js'`.
---
## 1. What you are looking for
### 1.1 Known indicators (this campaign)
| Indicator | Where it appears ||-----------|------------------|| String `5-2-326-du` (campaign/build tag; also seen as `global.i='5-2-326-du'` or `global.o='5-2-326-du'`) | Payload, and the command line of the running process || `global.r=require` / `global[...]= require` / `global.m=module` | Payload header || `global.e='NPM'` | Payload variant || `For only test` comment | Payload variant || `var _$_xxxx=(function(n,c){...` (identifiers like `_$_1aa7`, `_$_40e4`) | Payload (string-array decoder) || `String.fromCharCode(127)` + `.split('%').join(` | Payload decoder || `createRequire(import.meta.url)` lines **added at the top of an ES-module file that never needed them** (`const require = createRequire(...)`) | Injector adds them so `require` works in `.mjs` || Outbound HTTPS from a `node` process to `198.105.127[.]210` | Network || Process: `node -e global.i='...';global.r=require;...` with **parent PID 1** | Process list |
> The attacker can change these strings between variants. **Don't rely on the markers alone** — also run the heuristic checks in §1.2.
### 1.2 Heuristic indicators (survive variants)
- A config / script file with **one very long line (> 1,000 characters)**, especially a file that is normally < 30 lines.- A line that contains **a run of 80+ spaces or tabs followed by non-whitespace code** (the payload is hidden off-screen to the right).- A file whose last line is `main();` / `export default config;` / `module.exports = ...;` followed by lots of trailing spaces and then more code on the **same line**.-`eval(`, `new Function(`, `Function("return this")()`, `global[...] = require`, `child_process` + `detached: true` + `unref()` in a file that has no reason for them (CSS/PostCSS/Babel/Next/Tailwind/ESLint/Metro/Jest config).- Large hex/base64/obfuscated string tables (`\x25`, `\x23\x31`, `String.fromCharCode`) in config files.- Files modified recently that you didn't edit (`git status` shows `M` on config files after a clean checkout).
### 1.3 Files this campaign targets (check these first, in every project root and `scripts/`)
```postcss.config.js|mjs|cjs tailwind.config.* next.config.*babel.config.* .babelrc.js metro.config.js react-native.config.jsvite.config.* webpack.config.* rollup.config.* eslint.config.* .eslintrc.jsjest.config.* vitest.config.* svelte.config.* nuxt.config.* astro.config.*gulpfile.js Gruntfile.js scripts/*.mjs|js|ts any file named in package.json "scripts"```Configs are favored because they run automatically on `dev` / `build` / `start`.
---
## 2. Step 1 — Stop active infection (do this first; agent may kill only marker-matching PIDs, §0.1)
```bash# Look for payload processes (macOS/Linux). The marker is the campaign tag; the generic patterns catch variants.ps -axopid,ppid,lstart,command | grep -E"node .*-e .*(global\.|_\\\$_|require=|5-2-326)" | grep -vgrep
# Any node process whose parent is launchd/init (PPID 1) is suspicious — normal dev servers have a shell/IDE parent.ps -axopid,ppid,lstart,command | awk '$2==1' | grep -inode | grep -vgrep```
For each suspicious PID, record evidence **before** killing:
```bashps -opid,ppid,lstart,command-p <PID> | cut -c1-200lsof -p <PID> | grep -E"cwd|TCP|UDP"# working dir = which project launched it; TCP = who it talks to```
Then stop **only** processes that matched a marker, and re-check:
```bashkill <PID> # use kill -9 only if it survivessleep 1; ps -axopid,command | grep -E"global\.|5-2-326" | grep -vgrep || echo "none left"```
If a process refuses to die, or it doesn't match a marker but looks suspicious: **do not escalate** (`kill -9`, `sudo`) — list it in the user instructions. The user may turn Wi-Fi off while cleaning.
---
## 3. Step 2 — Scan the project (read-only)
### 3.1 Quick marker search (excludes `node_modules` and `.git`; also scans them in 3.3)
```bashcd /path/to/projectgrep -rIlF"5-2-326-du".--exclude-dir=node_modules--exclude-dir=.gitgrep -rIlE'global\[_\$_|_\$_[0-9a-f]{4}=\(function|global\.r=require|For only test'.--exclude-dir=node_modules--exclude-dir=.git```
### 3.2 Heuristic scan script (catches variants)
Save as `scan-malware.sh`, `chmod +x`, run from the project root. It is read-only.
```bash#!/usr/bin/env bash# Read-only scanner for the hidden-payload-in-config-file malware pattern.# Usage: ./scan-malware.sh [dir] (default: .)set -uROOT="${1:-.}"hits=0note() { printf '%s\n'"$*"; hits=$((hits+1)); }
# Build output / generated dirs are skipped (compiled code has long lines by design); scan SOURCE and config.EXCL=(-not-path'*/node_modules/*'-not-path'*/.git/*'-not-path'*/.next/*'-not-path'*/dist/*'-not-path'*/build/*'-not-path'*/out/*'-not-path'*/.yarn/*'-not-path'*/coverage/*'-not-path'*/.vercel/*'-not-path'*/.output/*'-not-path'*/.turbo/*'-not-path'*/functions/lib/*'-not-path'*/.expo/*'-not-path'*/android/app/build/*'-not-path'*/ios/Pods/*')XDIRS=(--exclude-dir=node_modules--exclude-dir=.git--exclude-dir=.next--exclude-dir=dist--exclude-dir=build--exclude-dir=out--exclude-dir=coverage--exclude-dir=.vercel--exclude-dir=.output--exclude-dir=.turbo--exclude-dir=.expo--exclude-dir=Pods)CODE=(--include='*.js'--include='*.mjs'--include='*.cjs'--include='*.ts'--include='*.mts'--include='*.cts'--include='*.jsx'--include='*.tsx'--include='*.json'--include='*.sh')
echo "== 1. Known markers"while IFS= read -rf; do note "MARKER $f"; done < <( grep -rIlE -e '5-2-326-du' -e 'global\[_\$_' -e '_\$_[0-9a-f]{4}=\(function' -e 'For only test' -e "global\.[a-z]='[0-9]+-[0-9]+-[0-9]+" \ "${CODE[@]}" "${XDIRS[@]}" "$ROOT" 2>/dev/null)
echo "== 2. Very long lines (>1000 chars) in source/config files"while IFS= read -r f; doif awk 'length($0) > 1000 { found=1; exit } END { exit !found }' "$f" 2>/dev/null; then note "LONGLINE $f (longest: $(awk '{ if (length($0)>m) m=length($0) } END{print m}' "$f") chars, $(wc -l <"$f") lines)"fidone< <(find "$ROOT" -type f \( -name '*.js' -o -name '*.mjs' -o -name '*.cjs' -o -name '*.ts' -o -name '*.mts' -o -name '*.cts' -o -name '*.jsx' -o -name '*.tsx' \) "${EXCL[@]}" -size -2000k 2>/dev/null \ | grep -v -E '\.min\.|\.bundle\.|\.d\.ts$|/public/|/vendor/')
echo "== 3. Lines with 80+ whitespace followed by code (hidden-to-the-right payload)"while IFS= read -r f; do note "HIDDEN $f"; done< <( grep -rIlE -e '[;})][[:space:]]{80,}[^[:space:]]' "${CODE[@]}" "${XDIRS[@]}" "$ROOT" 2>/dev/null)
echo "== 4. Suspicious primitives in config/script files"while IFS= read -r f; doif grep -qE 'eval\(|new Function\(|Function\(["'\'']return this|detached: *true|\.unref\(\)|String\.fromCharCode\(127\)|\\x25.*\\x23\\x31' "$f" 2>/dev/null; then note "PRIMITIVE $f"fidone< <(find "$ROOT" -maxdepth 3 -type f \( -name '*.config.*' -o -name '.babelrc*' -o -name '.eslintrc*' -o -name 'gulpfile*' -o -name 'Gruntfile*' -o -path '*/scripts/*' \) "${EXCL[@]}" 2>/dev/null)
echo "== 5. Lifecycle scripts in package.json files (preinstall/postinstall/prepare/install)"while IFS= read -r f; doif grep -qE '"(preinstall|install|postinstall|prepare|prepublish|preprepare|postprepare)"[[:space:]]*:' "$f" 2>/dev/null; then echo " $f:"; grep -nE '"(preinstall|install|postinstall|prepare|prepublish)"[[:space:]]*:' "$f" | sed 's/^/ /'fidone< <(find "$ROOT" -maxdepth 4 -name package.json -not -path '*/node_modules/*' 2>/dev/null)
echo "== 6. Git: files modified but not committed (config files you did not touch?)"if git -C "$ROOT" rev-parse --git-dir >/dev/null 2>&1; then git -C "$ROOT" status --short | head -50; else echo " (not a git repo)"; fi
echoif [ "$hits" -eq0 ]; then echo "RESULT: no indicators found (still review section 5 output above)."; else echo "RESULT: $hits suspicious finding(s). Do NOT run build/dev/install until reviewed."; exit 2; fi```
This scanner skips build output (`out/`, `dist/`, `.next/`, `functions/lib/`…). If a project *commits* build output, also scan it manually — the payload can live there too. Don't scan this playbook/report files themselves (they quote the markers).
Interpretation: **MARKER** = confirmed. **HIDDEN** = almost certainly confirmed. **LONGLINE / PRIMITIVE** = inspect by hand (minified vendored files are the usual false positives). Section 5 output is informational: a `postinstall` you don't recognise deserves a look.
### 3.3 Inspect a flagged file safely (never run it)
```bashwc -lcFILE# a 9-line config that is 8 KB+ is a red flagawk '{ print NR": "length($0)" chars" }'FILE | sort -t:-k2-n-r | head -3# find the long linehead -c600FILE# beginning (normal code)awk 'length($0) > 1000 { print NR": "substr($0, 1, 120) " ... [" length($0) " chars]" }'FILE# show only the START of long linescat -AFILE | cut -c1-160 | head # makes trailing spaces visible as nothing / ^M for CRLF```
### 3.4 Scan dependencies and tooling (the likely entry points)
```bash# Installed packagesgrep -rIlF"5-2-326-du"node_modules 2>/dev/null | head # may take a while on big trees# Lockfile: packages you don't recognise, or recently added onesgit log-p--follow--package.jsonyarn.lockpackage-lock.jsonpnpm-lock.yaml | grep -E'^[+-].*"?(version|resolved)' | head -80npm audit# or: yarn audit / pnpm audit# Global toolinggrep -rIlF"5-2-326-du"~/.nvm~/.npm~/.config~/.cursor/extensions~/.vscode/extensions 2>/dev/null | head```
---
## 4. Step 3 — Establish when and how it got in (git forensics)
### 4.1 Which commit introduced it
For each infected file, find the first commit containing a marker (adjust the marker if you found a variant):
```bashFILE=postcss.config.mjsfor c in $(git log--format=%h--all--"$FILE"); do n=$(git show"$c:$FILE" 2>/dev/null | grep -cE'5-2-326-du|global\[_\$_|_\$_[0-9a-f]{4}=\(function') echo "$c $(git log -1 --format='%ad | %an <%ae>|%s' --date=iso "$c") | payload_lines=$n"done```
The oldest commit with `payload_lines>0` is the entry commit; the one before it is the last clean version.
Other useful checks:
```bashgit show <entry-commit> --stat# what else changed (injected code often rides inside a real commit)git log--all--format='%h author=%an <%ae> committer=%cn <%ce>%ad' --date=iso -- <file>git branch -a; git stash list # payload on other branches/stashes?git rev-parse HEAD origin/main # already pushed?git branch -r --contains <entry-commit> # which remote branches contain itgit grep -lF "5-2-326-du" $(git rev-list --all) | sed 's/^[0-9a-f]*://' | sort | uniq -c # every file in all history```
Note: commit author/committer are **self-reported** and prove nothing about who actually injected the code. The usual story: malware on the dev machine modified files, and the developer committed them with `git add -A`.
### 4.2 Timeline questions to answer
- When did the first infected commit land? What was installed/cloned/opened in the days before (new npm packages, editor extensions, a "test project" from a recruiter/client, a downloaded template)?- Which projects were last built/run **after** the infection date? Those machines/CI runners executed the payload.- Did CI or a deploy pipeline run an infected file? If yes, treat CI secrets as exposed too.
---
## 5. Step 4 — Delete the malware (agent: the malware only, §0.3; everything else is user instructions)
### 5.1 Restore from the last clean commit (preferred)
Per §0.3, for each confirmed-infected file, **one file at a time**:
```bashgit show <last-clean-commit>:path/to/file > path/to/file# repeat per infected filegit diff <last-clean-commit> HEAD--path/to/file | cut -c1-120 | grep -E'^[-+]' | grep -v'^+++\|^---'```
Check the second command: if the entry commit also made **legitimate** edits to that file, re-apply them by hand (don't restore blindly). Injected lines to remove typically include `import { createRequire } from 'module'; const require = createRequire(import.meta.url);` if the file didn't have them before.
### 5.2 If there is no clean commit (or not a git repo)
Open the file, keep everything up to the real end of the code, and delete the rest of the long line. Re-check with `wc -c` (size should drop dramatically) and the scanner.
### 5.3 Re-verify
```bash./scan-malware.sh # expect: no indicatorsgit diff--stat# only the files you meant to changeps -axopid,command | grep -E"global\.|5-2-326" | grep -vgrep || echo "no payload processes"```
### 5.4 Reinstall dependencies cleanly (USER ACTION — the agent must not run or delete anything here; put it in the instructions)
```bashrm -rfnode_modules.nextdistbuild# the USER runs this, not the agent# keep the lockfile; review its diff first (§3.4)npm ci--ignore-scripts# or: yarn install --frozen-lockfile --ignore-scripts / pnpm i --frozen-lockfile --ignore-scripts# then rebuild only what's needed, after reading package scripts```
### 5.5 Commit the cleanup (USER ACTION — the agent must not commit or push)
```bashgit add-p# review hunks; do NOT use git add -Agit commit-m"Remove injected malware payload from <files>"git push# only after secrets are rotated and the user is happy```
The payload **remains in git history** after this. To purge it you'd need a history rewrite (`git filter-repo`) plus force-push and everyone re-cloning — usually not worth it; instead rotate secrets and make sure no one runs old commits. Decide with the owner.
---
## 6. Step 5 — Persistence & machine checks (READ-ONLY for the agent; report findings, change nothing)
```bash# Launch items — look for unfamiliar names, or ones that run node/sh/curlls -la~/Library/LaunchAgents/Library/LaunchAgents/Library/LaunchDaemons 2>/dev/nullgrep -lE"node|curl|bash -c|osascript|base64"~/Library/LaunchAgents/*.plist/Library/LaunchAgents/*.plist/Library/LaunchDaemons/*.plist 2>/dev/nullcrontab -l# Shell startup filesgrep -nE"node -e|curl .*\|.*(ba)?sh|base64|eval|5-2-326"~/.zshrc~/.zprofile~/.zshenv~/.bash_profile~/.bashrc~/.profile 2>/dev/null# Login items / remote access tools (confirm each is yours): System Settings → General → Login Items; AnyDesk/TeamViewer/etc.# Network: any node process talking outlsof -nP-iTCP-sTCP:ESTABLISHED,SYN_SENT | grep -inode# Browser extensions & recently installed apps/extensions: Cursor/VS Code extensions list, Chrome extensionsls -lt~/.cursor/extensions~/.vscode/extensions 2>/dev/null | head -20```
In this incident no persistence was found: the payload only came back when an infected file was executed.
### Scan every project on the machine
```bash# Markersgrep -rIlF"5-2-326-du"~/Desktop~/Documents~/Projects~/code~/dev 2>/dev/null--exclude-dir=node_modules--exclude-dir=.git# Heuristic (long lines in config files)find ~/Desktop~/Documents-typef \( -name'*.config.*'-o-name'.babelrc*' \) -not-path'*/node_modules/*'-not-path'*/.git/*'-execawk'length($0)>1000 {print FILENAME": line "NR" ("length($0)" chars)"; exit}'{} \; 2>/dev/null```
Run `scan-malware.sh` in each project that turns up.
---
## 7. Step 6 — Treat credentials as compromised (USER ACTION — the agent only lists what to rotate)
If any infected file was executed (build, dev server, deploy script, tests), assume the machine's secrets were readable. Rotate **after** the machine is clean (otherwise the new secrets leak too).
| Area | Action ||------|--------|| Git hosting | Revoke GitHub/GitLab personal access tokens, SSH keys, deploy keys, OAuth app grants; check recent activity/audit log, new collaborators, Actions secrets, unfamiliar commits/branches || Cloud / Firebase / Vercel / AWS / GCP | `firebase logout` + re-login; rotate service-account keys; revoke CLI tokens (`firebase login:list`); check IAM for new members/keys || API keys in `.env*`, `functions/.env`, CI secrets | Regenerate every key (OpenAI, Gemini, OpenRouter, Meta/Threads/LinkedIn/X/Telegram, Stripe, SendGrid, DB URLs…) || Package registries | Revoke npm tokens; check for unexpected published versions || Browser | Log out of all sessions on sensitive sites; change passwords stored in the browser; revoke active sessions (Google, GitHub, banking) || Password manager | Rotate anything accessed on this machine; check for vault access from new devices || Crypto wallets | Move funds to a fresh wallet created on a clean device || SSH | Generate new keys; remove old public keys from servers and GitHub || Databases | Rotate passwords; review for unusual access / new users |
Then review: new GitHub tokens/deploy keys created recently, unexpected Firebase/Cloud users, unfamiliar sign-ins on Google/GitHub, billing spikes on API providers.
---
## 8. Step 7 — Prevent recurrence (suggestions for the user; the agent installs/changes nothing unless told to)
1.**Find and remove the entry point**: the package, extension, repo or download that was opened just before the first infected commit. Warn teammates who may have it.2.**Untrusted code → isolated environment** (VM, container, Codespaces, throwaway laptop). Never on the main machine with logged-in accounts. Be especially wary of recruiter/client "coding tests".3. Use `--ignore-scripts` when installing unfamiliar projects: `npm i --ignore-scripts`, `yarn --ignore-scripts`, `pnpm i --ignore-scripts`. Set `ignore-scripts=true` in `.npmrc` for sensitive machines.4.**Editor setup**: enable word wrap and render whitespace so hidden right-hand code is visible (VS Code/Cursor: `"editor.wordWrap": "on"`, `"editor.renderWhitespace": "boundary"`; keep minimap on).5.**Pre-commit / CI guard** (blocks the pattern):
```bash# .git/hooks/pre-commit (chmod +x) — or run the same logic in CI#!/usr/bin/env bashbad=0for f in $(git diff--cached--name-only--diff-filter=ACM | grep -E'\.(js|mjs|cjs|ts|mts|cts|jsx|tsx)$'); doif git show":$f" | awk 'length($0)>1000{f=1} END{exit !f}'; then echo "✖ $f has a line >1000 chars"; bad=1; fiif git show":$f" | grep -qE'5-2-326-du|global\[_\$_|_\$_[0-9a-f]{4}=\(function|[;})][[:space:]]{80,}[^[:space:]]'; then echo "✖ $f matches malware pattern"; bad=1; fidone[ $bad -eq 1 ] && { echo "Commit blocked. Inspect the file(s) above."; exit 1; }exit 0 ```6.**Never `git add -A` blindly.** Use `git add -p` and read the diff, including config files. Add `git diff --stat` to your commit routine.7. Protect `main`: required PR reviews, required status checks (CI runs the scanner), signed commits if possible.8. Dependencies: commit lockfiles; review lockfile diffs; run `npm audit`; use Socket/Snyk/Dependabot; watch for typosquats; pin and review new packages and their install scripts before adding.9. Editor extensions only from verified publishers; review ones with broad permissions; remove unused ones.10. Secrets hygiene: short-lived tokens, secret managers instead of long-lived `.env` files on dev laptops, separate browser profile for admin/cloud consoles, hardware-key/passkey 2FA.11. Endpoint protection + OS updates; consider an outbound firewall (Little Snitch / LuLu) so unknown `node` network calls are visible.
---
## 9. Decision guide
| Situation | Conclusion | Action ||-----------|------------|--------|| Markers/hidden-line found, **file never executed** (e.g. just cloned) | Infected repo, machine not exposed | Don't run it; delete the clone or clean per §5; report the source || Markers found **and** an infected file was run (dev/build/deploy/tests) | Machine exposed | §2 → §5 → §6 → §7; rotate all secrets || Process with `global.i=…`/PPID 1 found | Payload ran | Same as above; also check which project's `cwd` it ran from || Only LONGLINE hits in minified/vendored files | Probably false positive | Verify the file is third-party and unchanged vs. upstream || Payload found in git history but not in current files | Already removed | Confirm HEAD clean; still rotate if it ever ran || Infected file is on `origin/main` | Others may have it | Tell collaborators; they must scan & rotate too |
---
## 10. Report template (fill in per project)
```Project: Date audited:Auditor:Infected files: (path, size before/after)Entry commit: (hash, date, subject) — pushed to remote? Y/NLast clean commit:Processes found/killed: (PID, start time, cwd, remote IP:port)Persistence found: (none / details)Other projects infected: (list)Entry point (suspected): (package / extension / repo / download)Files restored/changed: (list; legit edits re-applied? Y/N)Verification: scanner clean Y/N · no payload processes Y/N · no outbound connections Y/NSecrets rotated: (list + date)Open items:```
---
## 11. Notes on interpretation
- The payload was **not** statically analysed in the original incident, so don't claim exactly what it stole. It matches the profile of a developer-targeted infostealer / remote-access tool seen in fake-recruiter "coding test" campaigns; treat all local credentials as exposed.- Defang IPs/URLs (`198.105.127[.]210`) when sharing publicly.- If a variant appears with different markers, rely on the heuristics (§1.2, scanner sections 2–4), then add the new markers to your copy of this playbook.

Written by

BK

Badal Khatri

AI Engineer & Architect

[ Contact ]

Let's Start A Project Together

Email Me

badal.khatri0924@gmail.com

Location

Ahmedabad, India / Remote

Send a Message