Detection model
How malscan classes findings as evidence, trigger or context, why the same command can be benign or malicious, and the false-positive controls that keep a README link from condemning a package.
malscan does not score packages on a trust scale. Every finding is one of three classes, and the class alone decides the verdict.
| Class | Meaning | Severity in SARIF | Makes the package malicious |
|---|---|---|---|
evidence |
A factual malicious indicator: download-and-execute, a reverse or bind shell, credential exfiltration, a Tor .onion C2, a known-bad hash, a known-bad host referenced from executed code. |
critical (an IOC hit takes the indicator’s own severity) |
Yes, on its own |
trigger |
A corroborating signal: a high-entropy heredoc, an install hook that decodes and opens a connection. | medium |
Only combined with an ownership or identity change, which a local scan has no data for |
context |
Risk metadata or a weak signal: a plain-HTTP source, a long base64 or hex blob, an IDN host in a URL, an IOC in a test fixture. | low |
Never |
In a local vulnetix malscan, then, a package is MALICIOUS when it has at least one evidence finding, and SUSPICIOUS when it has only triggers or context.
The same command, two intents
A primitive is not a verdict. What decides is where it runs and what its result reaches.
| Behaviour | Benign | Malicious |
|---|---|---|
postinstall script |
node-gyp rebuild or prebuild-install building a native addon |
curl … | bash, or a hook that decodes a blob and opens a network connection |
| Reading the environment | One named variable (process.env.PORT) |
The whole environment, or ~/.aws, ~/.ssh, ~/.npmrc, followed by egress |
| base64 decode | An icon or certificate decoded into data | A payload decoded into eval or exec |
| Remote download | A pinned, checksum-verified binary from a vendor CDN | An unpinned blob from a raw IP or paste site piped into an interpreter |
The engine encodes this in three ways:
- Surface. Install scripts (npm lifecycle scripts,
build.rs,extconf.rb) auto-execute, so the install-script rules treat them as an execution surface. A dual-use command (hook_onlyrules such as baredockeror a remotenpx) is evidence only there; in the primary manifest it is dropped or recorded as context. - Weight. A pattern rule with
override_gateor at least 40 points is evidence; below that it is context. See Pattern rules. - Sequence. The behavioural detectors look for a capability and a threat indicator together: a
.pthfile written with animportline,setup.pyreading~/.aws/credentialsand making a request,build.rsrewriting~/.cargo/config.toml. See Detectors.
Where an indicator sits
The IOC scan matches every file in an install directory. The same C2 address is treated differently depending on the file:
| File | Treatment |
|---|---|
Markdown (.md, .markdown, .mdx, .mkd, .mdown) |
Not scanned: documentation names endpoints as examples. |
Source maps (.map) |
Hits suppressed: generated debug data, never executed. |
Test files and fixtures (/test/, /tests/, /__tests__/, /__fixtures__/, /fixtures/, /spec/, /__mocks__/, *.test.*, *.spec.*, *_test.go, *.snap, conftest.py) |
Demoted to context. |
Examples (/example/, /examples/, /sample/, /samples/, /demo/, /demos/) |
Demoted to context. |
Bundles (/dist/, *.min.js, *.min.mjs, *.min.cjs, *.min.css) |
Demoted to context. IPv4 matching is off on minified lines of 2,000 characters or more. |
| Everything else, including binaries | Kept as evidence. |
Only evidence-tier IOC hits mark a package malicious.
False-positive controls
- Shared allowlist. Reserved and documentation IP ranges, localhost, and well-known registry, CDN, standards and documentation domains are never treated as indicators. The same list (
malscan-engine/allow) filters the feeds when definitions are fetched, so no layer can drift from another. - Feed hygiene. CIDR ranges wider than one host are not loaded as indicators.
- Community curation. An indicator any organisation has marked false positive in the console is dropped before the report. See Curation and indicators.
- Capability overrides. A noisy detector can be turned off per ecosystem with
--catalog. See Detectors.
What it does not do
- It does not run code. Every check is static.
- It inspects installed files, not your own source. Your code is covered by SAST and secret detection.
- Detectors that need registry metadata (maintainer changes, votes and stars, ownership transfers, diffs against the previous release) and the name checks (typosquats, impersonation, which only apply to AUR packages) have no input in a local scan and do not fire. The VDB’s malware pipeline runs them when it builds advisories, and the known-malicious gate reports what it finds.