Threat bulletin 01Moving targets / local defense

Your lockfile trusts a moving target.

One poisoned release or repointed CI tag can inherit developer credentials and pipeline tokens. Supply Core quarantines fresh packages, verifies integrity, and rejects mutable references before they reach the build.

Five failures that collapsed the time between publish and compromise.

Not hypothetical. Recurring.

  1. 01

    MAY 2026

    Mini Shai-Hulud / TanStack

    42 packages, 84 artifacts. @tanstack/react-router at ~12M weekly downloads. PR → cache poisoning → OIDC token pulled from runner memory → publish. First npm attack carrying valid SLSA provenance.

    Filed / StepSecurity postmortem

    SLSA alone is dead as a defense.

    Age quarantine blocks the fresh publish; provenance on a poisoned artifact changes nothing.

  2. 02

    2025–2026

    Shai-Hulud v1 / v2 / v3

    Self-replicating npm worm spreading through pre/postinstall scripts. Destructive payload triggers on token revocation — a naïve “revoke first” incident response burns the host.

    Filed / Public incident writeups

    Install-time execution is the blast radius.

    Frozen fallback installs a known-good version instead of the fresh, worm-carried release.

  3. 03

    2026

    PyTorch Lightning / axios / LiteLLM

    Compromised releases were being pulled within 6–42 minutes of publish. Registry-side detection and takedown cannot move faster than CI.

    Filed / Public incident writeups

    Publish-to-pull windows are measured in minutes.

    Publish-age policy treats every too-fresh release as hostile until it clears quarantine.

  4. 04

    NOV 2025

    shanhai666 / NuGet

    9 packages, ~9.5k downloads, ~99% functional cover, time bombs scheduled for 2027 and 2028. Sharp7Extend typosquats the Siemens S7 PLC library. A slow-burn class that defeats install-time scanning.

    Filed / Public disclosure writeup

    Malware that waits years looks clean today.

    OSV advisory checks and contentHash verification on packages.lock.json catch drift when the record catches up.

  5. 05

    MAR–APR 2026

    Trivy + Bitwarden CLI / TeamPCP

    Same actor as the TanStack attack. Entry was pull_request_target plus a tag force-push — not a dependency at all. The defense surface is bigger than dependencies.

    Filed / StepSecurity postmortem

    Mutable CI references are a supply chain too.

    scan-actions and scan-pipelines reject anything not pinned to a full 40-character SHA.

A mutable reference enters CI. The build stops.

scan / actionsexit status / 2
$cargo run -- scan-actions . --annotations
scanning .github/workflows/*.yml
BLOCKactions/checkout@v4
reference is not pinned to a full 40-character SHA

Mechanism / package quarantine

  1. 01Fresh release appears on the registry.
  2. 02Age, integrity, and OSV are checked.
  3. 03Release outside policy is held.
  4. 04Known-good satisfying version keeps CI moving.
Quarantined

Specific controls. Explicit scope.

Every row names the file, policy, and command. No “supports everything” claim.

TargetInputChecksCommand
01npmpackage.jsonage quarantine · integrity · OSV · frozen fallbacksnapshot-npm
02PyPIrequirements.txt exact pinsage · sha256 · hash validation · OSVsnapshot-pip
03NuGetpackages.lock.jsonage · contentHash verification · OSVsnapshot-nuget
04GitHub Actions.github/workflows/*full 40-character SHA pinningscan-actions
05Azure Pipelinesazure-pipelines*.ymltask and external checkout pinningscan-pipelines
06DockerDockerfile and composestatic sha256 digest pinningscan-docker

One local system. Six refusal points. Signal moves left to right, then returns to the machine.

One firewall. Six layers.

  1. 01

    LOCK THE REFERENCES

    Reject mutable infrastructure

    GitHub Actions must resolve to full 40-character SHAs. Docker images must carry sha256 digests. Azure Pipelines get task and checkout pinning checks.

    $ scan-actions · scan-pipelines · scan-docker

    EXIT 2 / BLOCK CI
    ↓
  2. 02

    RESOLVE THE PACKAGE

    Verify what the lockfile names

    npm, exact-pin PyPI, and NuGet snapshots check registry age, package integrity, and OSV advisories before trust is granted.

    $ snapshot-npm · snapshot-pip · snapshot-nuget

    ALLOW / WARN / HOLD
    ↓
  3. 03

    CONTROL INSTALLATION

    Quarantine the fresh release

    The local npm proxy holds versions inside policy. When possible, it serves the latest known-good frozen version that still satisfies the requested range.

    $ docker compose up --build

    QUARANTINE / FALL BACK
    ↓
  4. 04

    INSPECT THE BYTES

    Scan before advisories exist

    Verified archives pass through built-in malware and AI-agent rules. Optional GuardDog adds install-hook, credential-access, exfiltration, and obfuscation heuristics.

    $ scan-package --guarddog

    SCORE / BLOCK / REVIEW
    ↓
  5. 05

    WATCH THE MACHINE

    Re-evaluate every checkout daily

    The macOS radar discovers repositories, inventories supported lockfiles, batches OSV checks with a local cache, and writes a morning report.

    $ install_machine_eval.py --home-scope

    REPORT / RETRY HOLDS
    ↓
  6. 06

    SHARE THE SIGNAL — OPTIONAL

    Publish status, not source code

    The self-hosted server accepts authenticated snapshots and exposes read-only status JSON and a live SVG card. Repository paths, ranges, and file contents stay local.

    $ serve · /api/v1/status

    REMOTE SCAN / STATUS

Your machine is the firewall.

  • 01Package policy runs through a local caching proxy.
  • 02A daily macOS radar inventories lockfiles and checks OSV.dev with a local 24-hour cache.
  • 03No SaaS account, cloud dashboard, agent daemon, or telemetry.
  • 04An optional self-hosted service publishes status without source code.

Security software should survive your inspection.

Read the source, review the threat model, and run the checks locally. Trust is earned in code, not a dashboard badge.