Dead-code and obfuscation analyzer for JavaScript and TypeScript, with an optional hardened Docker sandbox for dynamic behavioral analysis of suspicious scripts.
Originally built to triage JS malware samples - packed payloads, eval layers, char-code arrays, base64 staging, smart-contract-hosted payloads - but it works just as well as a plain dead-code finder on regular source trees.
Warning
examples/ contains real, live malware and ad-fraud samples. The files under examples/dom01/, examples/etherhiding/artifacts/, examples/clickfix-xloader/artifacts/, examples/propellerads-sfp/artifacts/, examples/deadcode01/, and examples/deadcode02/ are deobfuscation fixtures and reverse-engineering walkthroughs that ship inert payloads (.js, .b64, .hex, .ps1, .txt) as data files. They will not execute unless you deliberately run them. Do not node, bash, or eval any file under examples/. Do not paste the contents of any clipboard-payload.txt or clipboard-command.txt into a shell.
If you want a code-only clone with no payloads, use a sparse checkout:
git clone --filter=blob:none --no-checkout https://github.com/sresarehumantoo/reaper.git
cd reaper
git sparse-checkout init --cone
git sparse-checkout set src scripts docker
git checkoutSee examples/etherhiding/README.md for the analysis walkthrough and examples/etherhiding/REPORT.md for the full report.
Static analysis (Babel-based AST):
- Unused imports, variables, functions, and exports
- Unreachable code after
return/throw - Dead branches via constant folding (
if (false),1 === 2, etc.) - Obfuscation patterns:
eval,new Function,setTimeout("..."),atob,String.fromCharCode(...), bracket access to['eval']/['constructor'], high-entropy string literals, hex/unicode escape density - Cross-scope reachability: call-graph BFS from auto-detected or user-supplied entry points
- Eval-aware scope capture - intercepts
eval,Function(including.constructor.constructorand async/generator variants) and string timers in an isolated child process, and recursively analyses the inner layers p,a,c,k,e,rstatic unpack + string folding inside dead function bodies (recovers constant strings from code that won't run)- obfuscator.io string-array rewriter - detects the array-fn + decoder + IIFE-shuffle + wrapper-fn pattern (including nested wrappers), boots the decoder in an isolated child process (
--frozen-intrinsics, 128 MB heap, hard timeout, stripped env), inlines enclosing-scope const lookups, and substitutes every wrapper call with its plaintext string. Output is a fully rewritten.deobf.js - Generic constant-folding pass - a fixpoint partial-evaluator that collapses the mechanical transforms obfuscators rely on beyond the string-array:
atob/unescape/decodeURIComponent,String.fromCharCode,parseInt/Number, string concat andArray.join, pure-literal arithmetic (^ & | << >> + - * / %),!0/![]/!![]/voidtruthiness, andobj["x"]→obj.x. Runs after the string-array rewrite during--rewrite(disable with--no-fold) - XOR-loop decoder recovery - detects functions of the form
for (i) out += fromCharCode(s.charCodeAt(i) ^ k.charCodeAt(i % k.length))and, when callers pass string-literal arguments, statically recovers the plaintext into the finding - AAEncode/JJEncode detection - flags the katakana-heavy ASCII-art encoding family. Recovery requires execution; route through
scripts/analyze.shor--reachability - IOC extraction (
--iocs) - pulls a broad indicator set out of any input: URLs, bare domains (public-suffix-correct viatldts), IPv4/IPv6, EVM/BTC/XMR addresses, EVM function selectors, Telegram bot tokens, Discord webhooks, AWS keys, JWTs, PEM private keys, Windows paths, registry keys, suspicious LOLBin/PowerShell command lines, base64 blobs, high-entropy strings, and emails. Base64 blobs andatob()arguments are decoded and re-scanned so one-layer-nested indicators surface (via:base64). Each indicator carries a context hint (prop:data,arg-of:fetch,init:varName) and--defangrenders network indicators safely (hxxp://,evil[.]com) - One-shot triage (
--triage) - chains HTML extraction → string-array rewrite → constant-fold → obfuscation/encoded findings → IOC extraction into a single per-input report with a SHA-256, deobfuscation stats, and a coarseclean/suspicious/maliciousverdict
HTML / data-URI ingestion:
.htmlinputs are scanned for inline<script>blocks anddata:text/javascript;base64,...URIs; each script becomes a virtual sub-file the analyzers process independently- Common in real-world DOM dumps where the malicious payload is smuggled as a base64 data URI in a
<script src=...>
Dynamic analysis (Docker sandbox):
node:24-alpinecontainer, non-root uid 1001, all caps dropped,no-new-privileges--network none, 256 MB memory cap, 0.5 CPU, read-only FS,noexectmpfs- Pre-loaded monitoring shim logs
eval/new Function/setTimeout(string)calls,require()s, env-var access,fetch/net/http/fscalls as[REAPER]JSON lines on stderr - Hard wall-clock timeout,
child_process/cluster/worker_threadsblocked - Runtime modes:
--observe-network- real egress stays blocked but a stubfetch/httpresponder is installed so the script proceeds past network calls; you see the URL/method/body it would have used--block-eval-eval/Functionthrow after logging--block-fs- file-system writes throw after logging
From npm:
npm install -g @sresarehumantoo/reaper
reaper "src/**/*.js"From source (development):
make # installs deps, typechecks, compiles to dist/
# or run reaper from source without building:
npx tsx src/cli.ts <pattern>With Docker only (no Node install), build the static-analyzer image once and run the CLI from it:
make image # or: docker build -t reaper .
docker run --rm --network none --read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--cap-drop ALL --security-opt no-new-privileges \
-v "$PWD:/work:ro" \
reaper suspicious.html --triagePaths are relative to the mounted directory (/work). The flags above are the recommended way to analyze an untrusted sample, since static analysis runs the string-array decoder and eval capture on sample code (see SECURITY.md). --rewrite needs a writable output mount; add --user "$(id -u):$(id -g)" so the files are owned by you:
docker run --rm --network none --read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--cap-drop ALL --security-opt no-new-privileges \
--user "$(id -u):$(id -g)" \
-v "$PWD:/work:ro" -v "$PWD/out:/out" \
reaper suspicious.html --rewrite /outThis image runs reaper itself. It is separate from the dynamic sandbox in docker/, which executes a sample under the monitoring shim (see "Full pipeline" below).
make help lists every available target. The common ones: make build, make typecheck, make image (build the static-analyzer image), make sandbox (build the dynamic sandbox image), make demo (deobfuscate the bundled EtherHiding fixture end-to-end), make ci (typecheck + artifact hash verification).
# Default scan - all analyzers on
reaper "src/**/*.ts"
# Scan a captured DOM (extracts inline + data: URI scripts automatically)
reaper page.html
# JSON output
reaper malware.js --format json --output report.json
# Function inventory + reduction report
reaper packed.js --analyze
# Cross-scope reachability (eval-aware) with auto-detected entry points
reaper packed.js --reachability
# Reachability with explicit entry points
reaper malware.js --reachability --entry sendCode,init
# Disable specific analyzers
reaper "src/**/*.js" --no-obfuscation --no-dead-branchesExit codes compose with CI: 0 means nothing found, 1 means something was found (findings, dead code, IOCs, or a suspicious/unknown triage verdict) or an input failed to read or parse, and --triage exits 2 when any unit is malicious. Pick one mode per run; --format sarif is available for the default scan only.
# Deobfuscate → findings → IOCs → verdict, in a single report
reaper suspicious.html --triage
# Machine-readable, defanged, for a pipeline / ticket
reaper suspicious.js --triage --format json --defang --output triage.json--triage runs the whole static chain over each input (HTML extraction → string-array rewrite → constant-fold → obfuscation/encoded findings → IOC extraction) and prints a per-unit report — SHA-256, deobfuscation stats, findings, IOCs — plus a coarse clean / suspicious / malicious verdict with the reasons that drove it. Exit code is non-zero when anything scores above clean.
# HTML → b64 → string-array rewrite → constant-fold, plaintext written to out/
reaper page.html --rewrite out/
# Same for raw JS
reaper obfuscated.js --rewrite out/
# Skip the constant-folding pass (string-array rewrite only)
reaper obfuscated.js --rewrite out/ --no-foldAfter the obfuscator.io string-array rewrite, a generic constant-folding pass collapses the remaining mechanical obfuscation — atob/unescape/decodeURIComponent, String.fromCharCode, parseInt/Number, string concatenation and Array.join, pure-literal arithmetic, !0/![]/!![] truthiness, and obj["x"] → obj.x — so the output reads like source. For each input the rewriter reports substitutions and folds; outputs are <name>.deobf.js (or <name>.js pass-through when nothing was recovered).
# Indicators of compromise, with context hints
reaper sample.js --iocs
# Defanged + machine-readable JSON for downstream pipelines / tickets
reaper sample.js --iocs --defang --format json --output iocs.jsonIndicator families: URLs, bare domains (public-suffix-correct via tldts), IPv4/IPv6, EVM addresses + selectors, BTC/XMR addresses, Telegram bot tokens, Discord webhooks, AWS access keys, JWTs, PEM private keys, Windows paths, registry keys, suspicious LOLBin/PowerShell command lines, base64 blobs, high-entropy strings, and emails — each with a context hint (prop:data, arg-of:fetch, init:varName, via:base64). Base64 blobs and atob() arguments are decoded and re-scanned, so one-layer-nested indicators surface too. --defang rewrites network indicators safely (hxxp://, evil[.]com).
For best recall, run --rewrite/--triage first so IOCs hidden behind a string-array decoder or an atob() are visible.
reaper "src/**/*.js" --format sarif --output reaper.sarifThe result is a SARIF 2.1.0 document. Upload it from a workflow with github/codeql-action/upload-sarif to get findings rendered in the repo's Code Scanning tab.
# Static-only
./scripts/analyze.sh suspicious.js --static-only
# Static + dynamic with default constraints (network blocked, hard timeout)
./scripts/analyze.sh malware.js --timeout 30 --output-dir ./reports
# Observe network attempts without real egress (logs URL/method/body of every fetch)
./scripts/analyze.sh etherhiding.js --observe-network --timeout 20
# Log what eval would have executed without actually running it
./scripts/analyze.sh packed.js --observe-network --block-evalThe pipeline script runs the static analyzer, then builds and runs the Docker sandbox image (reaper-sandbox:latest) against the target.
See the warning at the top of this README before opening files under examples/.
examples/etherhiding/- full walkthrough of an in-the-wild EtherHiding + ClickFix sample. The directory contains the minimal HTML fixture, every intermediate stage fetched from BSC testnet contract storage, both OS-specific clipboard payloads, the deobfuscated plaintext of every JavaScript stage, and a step-by-stepREADME.mdyou can follow with no network access. Seeexamples/etherhiding/REPORT.mdfor the analysis report and IOCs.examples/propellerads-sfp/- full walkthrough of a PropellerAds-family smart popunder / click-hijack tag (sfp.js, build2026.5.0). The 97 KB obfuscator.io-obfuscated payload, reaper's plaintext rewrite (2 355/2 355 substitutions), the extracted publisher/network bootstrap config, and an IOC list. Motivated the alias / IIFE-nested / simple-subtract extensions to the string-array rewriter. Seeexamples/propellerads-sfp/REPORT.mdfor the analysis report and IOCs.examples/dom01/- original DOM dump (compromised WordPress page) from which the EtherHiding sample was extracted.examples/deadcode01/- real-world obfuscated sample (sendCode.js) plus its companion files. Good test for reachability and eval-layer capture.examples/deadcode02/- smallp,a,c,k,e,r-packed flag. Tryreaper examples/deadcode02/flag.js --reachability.
Quick end-to-end against the EtherHiding fixture:
# 1. Static deobfuscation - recovers the fetch URL, contract address, selector
reaper examples/etherhiding/sample.html --rewrite /tmp/out
# 2. Dynamic run with observe-network - captures the C2 request without egress
./scripts/analyze.sh examples/etherhiding/artifacts/stage1/payload.deobf.js \
--dynamic-only --observe-network --timeout 8
# 3. (Optional) Re-fetch the next stage from the live contract
./examples/etherhiding/fetch-evm-payload.mjs 0xA1decFB75C8C0CA28C10517ce56B710baf727d2e \
--out /tmp/dispatcher.jsExpected output from step 2 includes a [REAPER] {"category":"fetch","detail":{"url":"https://bsc-testnet-rpc.publicnode.com/","method":"POST", ...}} line containing the JSON-RPC eth_call body.
src/
cli.ts # commander entrypoint
parser/
index.ts # @babel/parser wrapper
html.ts # .html input → <script> / data: URI / event-handler subfiles + remote refs
analyzers/
imports.ts # unused imports
references.ts # unused vars / functions
unreachable.ts # code after return/throw
branches.ts # constant-folded dead branches
obfuscation.ts # eval, Function, atob, fromCharCode, entropy
encoded.ts # XOR-loop decoder recovery, AAEncode/JJEncode detection
reachability.ts # top-level cross-scope reachability analyzer
evalscope.ts # eval interception → captured inner-layer sources
isolate.ts # the one hardened child-process path for running sample code
*-worker.cjs # dependency-free workers isolate.ts spawns (copied to dist/)
packer.ts # p,a,c,k,e,r detection + static unpack
stringarray.ts # obfuscator.io string-array detect + static rewrite
constfold.ts # generic constant-folding / partial-evaluation pass
iocs.ts # indicator-of-compromise extractor
strfold.ts # constant-string folding inside dead bodies
functions.ts # function metadata extraction
triage.ts # one-shot deobfuscate → findings → IOCs → verdict
util.ts # shared traverse/generate interop, entropy, defang
types.ts # shared Finding / option types
graph/
callgraph.ts # build call graph from AST
reachability.ts # BFS over the graph, entry-point detection
reporter/
console.ts # default human-readable output
json.ts # JSON output
sarif.ts # SARIF 2.1.0 output
iocs.ts # --iocs report
triage.ts # --triage report
analysis.ts # --analyze inventory report
reachability.ts # --reachability report
Dockerfile # static-analyzer image: the reaper CLI without a local Node install
docker/
Dockerfile # hardened dynamic sandbox image
runner.js # --require shim - logs eval/fetch/fs/http, supports observe/block modes
scripts/
analyze.sh # combined static + dynamic pipeline
examples/ # sample inputs (incl. etherhiding/)
- Node.js 22.18+ (or 24.11+), or Docker to run the static-analyzer image instead
- Docker for the dynamic pipeline (
scripts/analyze.sh)
MIT. See LICENSE.