Why malware-detector?
Malicious code rarely shows up looking malicious. It arrives as a "telemetry" helper, a postinstall script in a dependency, or one long minified line in a file nobody reviews. By the time it runs, it has already read your .env, your cloud credentials or your users' cookies.
malware-detector looks for that behaviour before the code runs. Point it at a directory or a package and it tells you, in a few seconds, whether anything in it acts like malware.
Who it's for
- Developers who install packages every day and want a quick check before
npm installorpip install. - Maintainers who review outside contributions and want a second pair of eyes on every pull request.
- Security and platform teams who need a malware gate in CI that runs on any provider, including air-gapped and self-hosted runners.
- Agencies and freelancers who take over existing codebases (often PHP) and need to know whether a webshell is hiding in them.
What makes it different
It looks for malicious behaviour, not just bugs
Most code scanners look for vulnerabilities: mistakes an attacker could exploit. malware-detector looks for intent: code that is already trying to steal, hide or execute something. Its rules track how data moves. A secret flowing into an HTTP request, or a decoded string flowing into eval, is a much stronger signal than the presence of eval on its own.
It combines engines instead of betting on one
No single engine catches everything. Each one sees a different part of the problem:
| Engine | What it is good at |
|---|---|
| Semgrep + our rule pack | Structure and data flow in source code (taint from secrets to the network) |
| Built-in heuristics | Packed and obfuscated lines that have no useful structure to parse |
| GuardDog | Package metadata and known malicious package behaviours on npm and PyPI |
| malcontent (optional) | YARA signatures for binaries, archives and heavily obfuscated webshells |
malware-detector runs them together, merges overlapping findings, ranks everything by severity and gives you one report, one ignore file and one exit code. On a real-world corpus, the combined setup catches more malware than any single engine (see the benchmark).
Your code stays on your machine
There is no service behind it. Scans run locally or on your CI runner, with no account, token or upload. The Docker image runs with a read-only mount and needs no network access, so you can safely scan code you don't trust.
It fits the workflow you already have
- A
--stagedmode that finishes in seconds for pre-commit hooks. - A
--changedmode for pre-push. - SARIF output that shows up in GitHub's Security → Code scanning tab.
- JSON output for dashboards and scripts.
- Exit codes that any CI understands:
0clean,1findings,2usage error,3engine error.
How it compares
| malware-detector | Dependabot / npm audit | Generic SAST linters | |
|---|---|---|---|
| Detects new, unreported malware | ✅ | ❌ (known CVEs only) | ⚠️ rarely |
| Scans your own source | ✅ | ❌ | ✅ |
| Scans third-party packages | ✅ | ✅ | ❌ |
| Works offline / any CI | ✅ | ⚠️ GitHub / online | varies |
| JS, TS, Python and PHP in one tool | ✅ | per-ecosystem | varies |
These tools work well together. npm audit tells you a package has a known vulnerability. malware-detector tells you a package, or your own code, behaves like malware even when nobody has reported it yet.
What it is not
Being clear about limits makes the tool easier to trust:
- It doesn't run your code. It is a static analyser, so it can't observe behaviour that only appears at runtime, such as code downloaded later.
- Heavily obfuscated PHP webshells are the hardest case. Turn on
--with-malcontentif PHP is a priority for you. - A finding is a signal, not a verdict. Low-confidence rules report at
MEDIUMso you can review them. Use--min-severity HIGHif you only want to see the strongest signals.
Ready? Run your first scan →