How to Detect Compromised Packages with publisherChanged and versionAnomaly
Catch Axios-style maintainer-takeover drops, semver regressions, multi-major skips, and backdated publishes before they reach your builds.
Overview
Two of the highest-signal supply-chain attack patterns leave identifiable metadata fingerprints on the publish event itself:
- Maintainer account takeover — the attacker publishes from a
never-before-seen npm user, email, or Maven developer ID. The
publisherChangedcondition diffs the incoming version’s publisher set against the last persisted version’s set. - Version-number games — the Axios v1.14.1 → v0.30.4 drop flipped the
major to make the malicious release look “old”, and subsequent variants
skipped majors or backdated the publish timestamp. The
versionAnomalycondition detects all three shapes.
Both conditions run inside the sync-check budget and persist their results
to package_metadata so the BOM can show you the history.
Prerequisites
- Admin or Manager role in Chainsaw
- At least one of: npm, PyPI, RubyGems, NuGet, Maven, Gradle repositories proxied through Chainsaw for long enough to accumulate a baseline
Step 1: Let Chainsaw Learn the Baseline
The publisherChanged signal requires at least one prior persisted version
of the package. Give Chainsaw a week or two of normal traffic before
turning the block on — otherwise the first install of every package looks
like a publisher change and nothing passes.
Step 2: Create the publisherChanged Policy
- Navigate to Policies → Create Policy
- Name:
Warn — publisher changed - Condition:
publisherChanged = true - Action: Warn (start in monitor mode)
- Scope: All repositories
After ~14 days, review the violations. Legitimate publisher changes (a
maintainer handing the project off, a bot account being rotated) will show
up alongside the attack signal; add those to an allowlist by package
before promoting the policy to Block.
publisherChanged with trustScoreMin and packageAge — a genuine
handoff still has a reasonable trust score and usually isn’t published
within 24 hours of the previous release, while an account-takeover drop
often fails both additional checks.Step 3: Create the versionAnomaly Policy
versionAnomaly fires when any of three sequence flags are set on the
incoming version:
| Flag | What it catches | Example |
|---|---|---|
semver_regression | Major version flips backwards | 1.14.x → 0.30.4 |
major_skip | 2+ majors skipped with no intermediate release | 1.x → 3.x directly |
timestamp_regression | Publish time is before a previous version’s publish time | Backdated to look “older” |
Create a policy that narrows to the highest-signal flags first:
- Name:
Block — version anomaly (regression + major skip) - Condition:
versionAnomaly = true - Kinds filter:
versionAnomalyKinds = [semver_regression, major_skip] - Action: Block
- Scope: Production repositories
Leaving timestamp_regression out of the block condition is intentional —
some upstream mirrors reclock publish timestamps for infrastructure
reasons. Track it in a separate warn-mode policy while you triage the
signal.
Step 4: Ecosystem Support
| Ecosystem | publisherChanged | versionAnomaly |
|---|---|---|
| npm | ✅ maintainers[].name | ✅ |
| PyPI | ✅ info.author_email + info.maintainer_email | ✅ |
| RubyGems | ✅ authors | ✅ |
| NuGet | ✅ authors | ✅ |
| Maven / Gradle | ✅ POM developers[].id / <email> | ✅ |
| Go | ❌ (no per-version publisher field) | ✅ |
| Swift | ❌ | ✅ |
| HuggingFace | ❌ | ✅ |
| Cargo | ❌ | ✅ |
| Composer | ❌ | ✅ |
| CocoaPods | ❌ | ⚠️ (no release-date for timestamp_regression) |
| Docker | ❌ | ❌ (tags not SemVer) |
| APT / Yum / DNF | ❌ | ❌ (non-SemVer versioning) |
For ecosystems that lack publisherChanged support, pair versionAnomaly
with trustScoreMin and the provenance rule for that ecosystem.
Step 5: CLI Verification
chainsaw pkg scan npm/axios \
--version 0.30.4 \
--with-publisher-change \
--with-version-anomaly
Output includes publisher_set_diff, version_anomaly_flags, and the
persisted diff against the previous version.
Step 6: Investigate a Detection
When publisherChanged fires, open the package in the BOM and review:
- The prior publisher set vs. the incoming set
- Whether the new publisher has recent activity on other packages
(cross-reference with
publishVelocityAnomaly) - The version-number shape (cross-reference with
versionAnomaly) - The trust score delta since the last version
Three of the four signals lighting up at once is a high-confidence account-takeover. Block the version, open an incident, and quarantine any repos that installed it during the detection window.