How to Detect Shai-Hulud-Style Worm Bursts
Catch attackers auto-publishing hundreds of packages from a compromised token with the publishVelocityAnomaly condition.
Overview
Shai-Hulud-style attacks compromise a publishing token and then use it to publish hundreds of package versions in a single burst — either new typosquats, or trojanised variants of existing packages the attacker has push rights to. The signature is a publisher pushing an implausible number of versions in a short time window.
The publishVelocityAnomaly condition counts versions published by the
incoming version’s publisher across any package in the trailing 24
hours. If the count exceeds publishVelocityThreshold24h (default
policy.DefaultPublishVelocityThreshold24h = 20), the condition fires.
The count runs against the package_metadata.publisher_set GIN index and
stays under the 20ms sync-check budget even at tens of millions of rows.
Prerequisites
- Admin or Manager role in Chainsaw
- At least one of: npm, PyPI, RubyGems, NuGet repositories proxied through Chainsaw with enough historical traffic that the publisher_set index is populated
Step 1: Understand the Threshold
A threshold of 20 versions / 24 hours is a conservative default that catches the Shai-Hulud and August 2025 typosquat-worm shapes without false-positiving on legitimate burst publishers:
- A bot-driven monorepo publisher (Babel, React, NX) typically pushes 5–15 versions in a release window
- A framework like Next.js or Kubernetes publishes ~10 versions in a major-release day
- An individual maintainer almost never exceeds 20 versions / 24h unless something is very wrong
Tune it via policy if your environment has a different baseline:
conditions:
publishVelocityAnomaly: true
publishVelocityThreshold24h: 50 # raise for orgs that host large monorepos
Step 2: Create the Policy
- Navigate to Policies → Create Policy
- Name:
Block — publish velocity anomaly - Condition:
publishVelocityAnomaly = true - Action: Block
- Scope: All repositories
- Precedence: High
publisherChanged — a Shai-Hulud burst usually
also exhibits a maintainer-set change on the incoming version, so both
signals firing together is a very strong indicator. Both signals alone
have a reasonable false-positive rate; both together are near zero.Step 3: Ecosystem Support
| Ecosystem | Support |
|---|---|
| npm / PyPI / RubyGems / NuGet | ✅ |
| Maven / Gradle | ⚠️ Developer IDs are optional in POM/metadata — some publishers have empty publisher sets and can’t be velocity-counted |
| Go / Swift / HuggingFace / Cargo / Composer / CocoaPods / Docker / APT / Yum / DNF | ❌ (no reliable per-version publisher metadata) |
For unsupported ecosystems, rely on the combination of
publisherChanged, versionAnomaly, and trustScoreMin to cover the
same attack surface from a different angle.
Step 4: Respond to a Detection
When this fires, it almost always means something significant is in progress upstream. Response playbook:
- Identify the publisher — the BOM shows the triggering publisher
in
publisher_set - Query the audit log — list every package this publisher has touched in the last 24 hours
- Quarantine — any of those packages that reached a repo during the detection window
- Cross-check — pair the publisher with trust score, repo ownership
match, and any
publisherChangedfindings in the same window - Escalate — if the publisher also shows
publisherChangedhits, the token is compromised; contact the ecosystem’s security team
Step 5: CLI Verification
chainsaw pkg scan npm/@some-publisher/some-package \
--version 4.2.1 \
--with-publish-velocity
Output includes the 24h rolling count and the threshold in effect.