How to Detect Shai-Hulud-Style Worm Bursts

Intermediate 15 minutes Security Engineers Security & Policy

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

  1. Navigate to Policies → Create Policy
  2. Name: Block — publish velocity anomaly
  3. Condition: publishVelocityAnomaly = true
  4. Action: Block
  5. Scope: All repositories
  6. Precedence: High
Pair this condition with 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

EcosystemSupport
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:

  1. Identify the publisher — the BOM shows the triggering publisher in publisher_set
  2. Query the audit log — list every package this publisher has touched in the last 24 hours
  3. Quarantine — any of those packages that reached a repo during the detection window
  4. Cross-check — pair the publisher with trust score, repo ownership match, and any publisherChanged findings in the same window
  5. Escalate — if the publisher also shows publisherChanged hits, 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.

Next Steps