Skip to content

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.

Updated 02 Nov 2026Checked against cli@2449840, malscan-engine@3dfe895

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_only rules such as bare docker or a remote npx) is evidence only there; in the primary manifest it is dropped or recorded as context.
  • Weight. A pattern rule with override_gate or 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 .pth file written with an import line, setup.py reading ~/.aws/credentials and making a request, build.rs rewriting ~/.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.
Edit this pageSources: malscan-engine/detect/model.go, malscan-engine/detect/engine.go, malscan-engine/detect/patterns.go, malscan-engine/detect/qualify.go, malscan-engine/iocscan/matcher.go, malscan-engine/iocscan/scan.go, malscan-engine/allow/allow.go, cli/cmd/malscan.go, cli/cmd/malscan_engine.go
esc
↑ ↓ move↵ openSearches in your browser. A key like autonomy.max or a code like threshold_required jumps straight to it.