Vulnerability gating
Block by CVE, CVSS score, EPSS exploit probability, or CISA KEV membership. Combine freely; every operator is composable inside a single rule.
Policy
A failing package is refused before it reaches the build — on rules you write. Compose vulnerability, license, version, provenance, and client-context rules declaratively, then layer in the supply-chain attack signals CVE-based scanners miss. Every rule ships in monitor mode first, flips to block or quarantine when you've seen the data, and leaves a structured audit record on every decision.
Core rule families
Block by CVE, CVSS score, EPSS exploit probability, or CISA KEV membership. Combine freely; every operator is composable inside a single rule.
Allow, warn, or block by SPDX identifier. Separate policy for direct and transitive dependencies. SPDX-flavoured expressions for the edge cases.
Pin minimum versions, block deprecated or EOL releases, enforce semver ranges, require a minimum package age, or set a per-version cooldown that holds every fresh release for a window while the ecosystem has time to react.
Require npm provenance, Sigstore signatures, Go's sum.golang.org, PGP for Maven and RubyGems, or InRelease/repomd hash-chain for APT/Yum/DNF. Trust roots are configurable.
Different policy for prod vs dev, per repository, per CI job, per geographic region. Tight where it matters, permissive where it doesn't.
Every exception carries a reviewer, a reason, and an expiry date — no permanent waivers by default. Exceptions are VEX-aware: chainsaw exception create accepts --cve, --decision (not_affected / affected / fixed / under_investigation), and --vex-note, so the audit log answers vendor questionnaires directly.
Supply-chain attack signals
Each signal evaluates at install time against the artifact, the registry metadata, and the publisher history. Combine them with vulnerability, license, version, and provenance rules in the same policy. Start in monitor mode, tune exceptions, flip to enforce one rule at a time.
hasInstallScript · installScriptFetchesRemote Flags packages whose lifecycle hooks — preinstall/postinstall in npm, setup.py extensions in pip, build.rs in Cargo, Composer hooks — run remote fetches or decode base64 payloads. Refuses the install before the hook fires.
cooldownDays Quarantines any version published inside your cooldown window — a few hours to a few days — on the install path, so a poisoned release sits unreachable while the ecosystem, the malware feeds, and the publisher-anomaly signals catch up. Package age asks how old the package is; cooldown asks how old this version is, which is the question the Axios, Chalk, and Mastra takeovers turned on: a brand-new release of a long-trusted package. Runs on every registry that exposes a per-version publish date — the same coverage as the minimum-age rule. Absent date metadata fails open, never quarantining on a guess.
publisherChanged Compares the current maintainer set against the package's history. A surprise publisher on a popular dependency refuses on the install path pending review. The account-takeover class — a hijacked maintainer pushing a tainted version of a package you already trust — is the live threat right now, ahead of the typosquats that catch the easier, older mistakes. Works on npm, PyPI, RubyGems, NuGet, and Maven.
versionAnomaly · versionAnomalyKinds Catches semver regressions, multi-major skips, and backdated publish timestamps used to sneak a compromised version under a higher constraint. Narrow by kind: semver_regression, major_skip, timestamp_regression.
publishVelocityAnomaly A rolling 24-hour counter per publisher. When one compromised account starts pushing dozens of tainted versions in a day, the burst trips the rule before your build runs. Threshold is tunable.
reservedNamespaces Dependency confusion works because attackers publish your internal package names on public registries first. Reserved-namespace packs block the attack class up front. One-click apply of curated starters per ecosystem.
isSuspectedTyposquat Blocks the install when the requested name is a near-miss of a popular package. BK-tree, homoglyph, and word-reorder matchers run against curated seed lists. Fifteen ecosystems including Go, CocoaPods, and GitHub Actions. Dart/pub gets CVE and typosquat coverage only — no malicious-package prevention.
isKnownMalicious (docker) A Docker-native malware index matched by digest and by name-plus-tag. Closes the container-image gap that public SCA feeds miss. Lookup is constant-time; a hit drops the trust score to -100.
Container image analysis Walks every image layer with Trivy under the hood. Vulnerabilities hidden beneath a clean top-level manifest are blocked at install; a clean tag no longer guarantees a clean image.
hasProvenance (apt/yum/dnf) InRelease verification for APT, repomd.xml.asc for Yum/DNF. A mirror that tampers with a package between publish and fetch fails the chain. Debian and Fedora keyrings ship embedded.
distroCVE (alpine/debian/rhel/oracle) Native CVE detectors for Alpine, Debian, Red Hat, and Oracle Linux — distinct from upstream OSV, with each distro stream on its own update cadence. A vendor advisory becomes a block-list entry the same hour the distro publishes it.
isKnownMalicious (huggingface) Bundled HF-native coordinate-match feed shipped in-process. Closes the gap where public SCA indexes lag on model-repo malware. Lookup is constant-time at resolve.
trustScoreMin Unmaintained repos with an active npm publisher are a compromise waiting to happen. Each package gets a composite trust score folding in repo activity, ownership match, license, age, version count, and checksum presence. Set the floor you're comfortable with.
checksum mode Every upstream fetch is audited against the declared hash. Pick log, quarantine, or block mode per ecosystem. Distinguishes a real mismatch from an upstream that never published a hash.
Where refusals surface
When a rule fires, the decision and its reason land in Findings. Severity cards summarize the queue; the policy-blocked findings below are the ones to triage first.
Coverage matrix
One row per attack class. The detection mechanism column names the module that runs the check; the data source column names the feed or signal it pulls from. Honest gaps — if a column is empty for an ecosystem, it's empty in the product.
| Attack class | Detection mechanism | Ecosystems | Data source |
|---|---|---|---|
| Known vulnerability (CVE) | Vulnerability gate matches CVE IDs and scores by CVSS, EPSS exploit probability, and CISA KEV membership. KEV cross-reference runs after the CVE merge so known-exploited issues sort to the top. | All 16 ecosystems | OSV, GHSA, NVD, FIRST EPSS, CISA KEV, Aqua Trivy DB |
| Typosquatting | BK-tree, homoglyph, and word-reorder matchers against curated popular-package seeds. Low-risk gate on APT/Yum/DNF. | 15 ecosystems (BK-tree); APT/Yum/DNF low-risk gate | Curated popular-package seeds per ecosystem |
| Dependency confusion | Reserved-namespace packs block public-registry publishes of your internal names. The Birsan pattern is refused up front, no per-package allow-listing. | Universal (customer-declared namespaces) | Customer-declared internal namespaces + public-registry name resolution |
| Known malware | Digest and name+tag match against the OpenSSF malicious-package and malware indexes, plus a bundled Docker and Hugging Face malware feed. | npm, PyPI, RubyGems, Cargo, Packagist, NuGet, Hugging Face, Docker, Swift | OpenSSF malicious-packages, OpenSSF malware feed, Docker malware feed, GHSA (Swift) |
| Hidden Unicode / Trojan Source | Refuses packages carrying zero-width, bidi-override, or Unicode-tag characters (GlassWorm, Trojan Source). Bounded scan: 500 files / 50 MiB per artifact. | All artifact-bearing ecosystems | Static source scan (no external feed required) |
| Install-script exfiltration | PhantomRaven-style detection flags lifecycle hooks that fetch remote payloads or shell out — curl/wget, urllib, requests.get, subprocess, child_process.exec, eval. | npm, PyPI (setup.py / pyproject), RubyGems, Cargo, Composer | Static + AST analysis of declared lifecycle scripts |
| Maintainer-account takeover | Compares the current maintainer set against publish history (Axios-style ATO); surprise publishers block pending review. Maintainer tenure / reputation / account history feed the verdict. | npm, PyPI, RubyGems, NuGet, Maven/Gradle | Per-registry publisher history + maintainer reputation feeds |
| Publish-velocity worm bursts | Rolling 24-hour publish-velocity counter per publisher trips on Shai-Hulud-style worm bursts when one account pushes dozens of tainted versions. | npm, PyPI, RubyGems, NuGet, Maven/Gradle | Per-publisher publish-rate telemetry from registry events |
| Broken provenance / unsigned artifact | SLSA / Sigstore chain verification per ecosystem, with a post-merge signature-verify pass that projects the provenance verdict for closure. | npm, PyPI, Maven/Gradle, RubyGems, Go, NuGet, Cargo, Docker, Swift, CocoaPods, Hugging Face, APT, Yum, DNF | SLSA attestations, Sigstore transparency log |
| Tampered artifact (checksum mismatch) | Declared-versus-actual hash verification with log / quarantine / block outcomes per policy. | npm, PyPI, RubyGems, Composer, Maven, Gradle, NuGet, Cargo | Registry-declared hashes + computed artifact digest |
| Manifest confusion / lockfile-only artifact | Format-specific mismatch detection (PyPI wheel-vs-sdist sub-provider) plus shrinkwrap / lock-file-only artifacts that ship without source. | npm (shrinkwrap), PyPI (wheel/sdist), plus cross-format manifest checks | Static manifest + lockfile inspection |
| Malicious package behaviour (code smell) | Nine static sub-detectors on the artifact bytes — eval, network, shell, filesystem, env-var access, native binary, high-entropy strings, URL strings, minified code. npm-only capability scanner emits first-class cap.* signals behind a flag. | All artifact-bearing ecosystems (capability scan: npm) | Static analysis of package artifact contents (not user source) |
| Trivial / decoy package | Flags trivial_package and too_many_files heuristics that mark empty decoys or grossly over-stuffed artifacts. | All artifact-bearing ecosystems | Artifact structure heuristics |
| Malicious AI model / agent artifact | Three sub-providers detect unsafe pickle opcodes — unwrapping zip-container .pt/.bin checkpoints and modeling the pickle stack + memo so nested and obfuscated payloads are caught — plus mismatched model cards and agent-tool artifacts; a post-merge pass verifies the agent-tool artifact. | Hugging Face / model registries | Static model-artifact inspection |
| Repo liveness / ownership mismatch | Repolink probes the upstream source-repository URL; Repository Trust Traits check liveness, ownership match, non-existent author, suspicious-star bursts, and first-time collaborators. | All ecosystems with a linkable upstream repo | Upstream repository metadata + community signals |
| Abandoned / unpopular dependency | Deprecated / archived / stale detection (OpenSSF Scorecard-adjacent) reads the repolink output; download-count fetch powers maint.unpopular_package with a fail-open SevUnknown when the count can't be fetched. Registry metadata supplies publish date, licences, maintainers. | npm, PyPI (download counts); all ecosystems (metadata + maintenance) | OpenSSF Scorecard signals, npm/PyPI weekly downloads, registry manifests |
Internal & private packages
Chainsaw treats internal packages as first-class. You don't have to choose between running a private registry and running policy on what flows through it.
Declare the namespaces your org owns — @acme/* on npm,
com.acme.* on Maven, acme. on PyPI — and Chainsaw
refuses any public-registry publish that tries to squat them. Closes the
Birsan dependency-confusion class up front, no per-package allow-listing.
Point Chainsaw at your internal registry (JFrog Artifactory, Sonatype Nexus, Cloudsmith, GitHub Packages, AWS CodeArtifact) the same way you point it at npm or PyPI. The proxy authenticates outbound, applies the same policy, and caches results. Configurable per ecosystem; turn it off for any registry you don't want scanned.
Internal-package vulns surface on the same install-path audit row as public-package vulns — by repo, by CI job, by rule. If your internal package ships a vulnerable transitive (which is where most internal CVEs hide), the transitive-risk module walks the closure and the result lands in the inventory under the internal name.
Vendored & bundled dependencies
Some teams check vendored copies into the repo — vendor/ in Go,
third_party/ in monorepos, bundled wheels in Python projects, fat
JARs on the JVM. Chainsaw's SBOM walks those directories during inventory
generation, attributes findings back to the original upstream package, and
records the vendored copy alongside the install-path record so a single CVE
query catches both. Scan depth is configurable per repo — set the max walk
depth, exclude a directory, or skip vendored scanning entirely if your
compliance posture doesn't need it.
Dependencies pulled straight from a Git URL or HTTP archive instead of a
registry never pass through publisher history, checksums, or provenance — so
Chainsaw flags them with the first-class supply-chain signals
sc.git_url_dependency and sc.http_url_dependency.
You decide the verdict: log them, require an exception with an owner and
expiry, or block the install outright. Either way the non-registry source
shows up on the same audit row as everything else, so a
git+https://… backdoor can't slip in unseen.
Ready to roll out?
Start free in monitor mode. See what would be refused, then flip to enforce when you've seen the data.