There is a structural blind spot in how most organizations handle software supply chain risk: we treat CVE publication as the starting gun for detection. For traditional vulnerabilities, that cadence is survivable — researchers disclose, a CVE is minted, scanners ingest it, and you patch on a prioritized schedule. For compromised packages in public registries, that model collapses. When a maintainer account is phished and a malicious version of a trusted npm or PyPI package is published, the malware is live at publication time. Developer machines pull it in on their next install. CI/CD runners execute it during builds. By the time a CVE or registry takedown notice exists, the credential theft and lateral movement are already done.
This is the threat model behind Wiz's recent write-up on supply chain zero days — building detection pipelines that flag compromised packages at the moment of publication rather than waiting for formal vulnerability disclosure. The lesson for defenders isn't vendor-specific: your exposure window for a poisoned dependency is measured in the hours between publication and takedown, and none of your CVE-driven tooling sees it. If your software composition analysis (SCA) stack is the only thing watching your dependency graph, you are blind during the exact window that matters.
Why the Publication-to-Detection Gap Is So Dangerous
Consider the kill chain of a typical registry compromise:
- Maintainer account takeover — phishing, session token theft, or credential stuffing against a maintainer of a widely-downloaded package.
- Malicious version publication — the attacker pushes a new patch version (e.g.,
1.4.1→1.4.2) with an obfuscated payload embedded in a lifecycle script (preinstall,postinstall) or in source that executes on import. - Automated ingestion — developer laptops, CI runners, and build systems with loose version pinning (
^ranges inpackage.json, unpinnedrequirements.txt) pull the malicious version within minutes. - Execution and staging — the payload harvests environment variables (CI secrets, cloud credentials,
NPM_TOKEN,AWS_*,GITHUB_TOKEN), SSH keys, and.npmrc/.pypircauth files, then exfiltrates to attacker infrastructure — often disguised as telemetry endpoints or legitimate-looking domains. - Takedown — eventually. The registry or community flags the package hours to days later. A CVE may never be issued at all; many registry compromises are handled as security advisories only.
Notice what never appears in that chain: a CVE identifier your scanner can match against. NVD enrichment can lag weeks. Your only reliable detection surfaces are endpoint telemetry on developer machines and build runners, egress network monitoring, and pipeline-time package analysis.
What "Detection at Publication" Actually Looks Like
The approach Wiz describes — and the one you should replicate regardless of tooling vendor — rests on continuous registry monitoring rather than periodic scanning:
- Registry delta monitoring: Diff every newly published package version against its predecessor. Flag anomalous additions: new install scripts where none existed, sudden obfuscated/minified blobs, newly added network calls, entropy spikes in previously plain source files, and maintainer/metadata changes coinciding with publication.
- Behavioral sandboxing: Execute the package's install lifecycle in an instrumented sandbox and record child process spawns, filesystem writes outside the package directory, and network connections. A
postinstallscript that spawnscurl, reads~/.aws/credentials, or beacons to a freshly-registered domain is malicious until proven otherwise. - Dependency graph correlation: Knowing a package is bad is half the battle — you must know which assets installed it and when. This requires an accurate, continuously refreshed inventory of developer workstations, CI runners, and container images mapped to resolved dependency versions (not just declared ranges).
- Pre-CVE alerting: Feed these signals into your cloud detection and response layer (Wiz Defend, in the source example) so SOC analysts get an alert tied to specific affected workloads before any public advisory exists.
If you can't stand up registry diffing yourself, the defensive floor is endpoint and network detection for the behaviors these payloads universally exhibit.
Detection & Response
The detections below target the behavioral constants of malicious package execution: package managers spawning interpreters or shells, install scripts reaching for credential stores, and egress from build infrastructure to unusual destinations. These are tuned to fire on the payload behaviors common across recent registry compromises — not on any single indicator of compromise that will rot in a week.
Sigma Rules
---
title: Package Manager Spawning Script Interpreter or Shell
id: 8f2c4e1a-7b3d-4f5e-9a21-6c8d0e2b4f7a
status: experimental
description: Detects npm, npx, pip, or yarn spawning shell or script interpreter child processes, consistent with malicious preinstall/postinstall lifecycle scripts in compromised registry packages.
references:
- https://www.wiz.io/blog/detect-supply-chain-zero-days-at-publication
- https://attack.mitre.org/techniques/T1195/002/
author: Security Arsenal
date: 2026/02/14
tags:
- attack.initial_access
- attack.t1195.002
- attack.execution
logsource:
category: process_creation
product: windows
detection:
selection_parent:
ParentImage|endswith:
- '\node.exe'
- '\npm.exe'
- '\npm.cmd'
- '\npx.cmd'
- '\yarn.exe'
- '\pip.exe'
selection_child:
Image|endswith:
- '\powershell.exe'
- '\pwsh.exe'
- '\cmd.exe'
- '\wscript.exe'
- '\cscript.exe'
- '\curl.exe'
- '\certutil.exe'
condition: selection_parent and selection_child
falsepositives:
- Build tooling with native compilation steps (node-gyp) spawning cmd.exe
- Legitimate postinstall scripts in enterprise-vetted packages
level: high
---
title: Credential Store Access by Package Manager Process
id: 2d7b9f3e-4a1c-4e8b-b5d2-9f0a3c6e1d84
status: experimental
description: Detects node, npm, pip, or python processes reading credential and token files commonly harvested by malicious package install scripts, including npmrc, AWS, Azure, and SSH key material.
references:
- https://www.wiz.io/blog/detect-supply-chain-zero-days-at-publication
- https://attack.mitre.org/techniques/T1552/001/
author: Security Arsenal
date: 2026/02/14
tags:
- attack.credential_access
- attack.t1552.001
logsource:
category: file_event
product: windows
detection:
selection_image:
Image|endswith:
- '\node.exe'
- '\python.exe'
- '\npm.cmd'
selection_target:
TargetFilename|contains:
- '\.npmrc'
- '\.pypirc'
- '\.aws\credentials'
- '\.azure\'
- '\.ssh\id_'
- '\.git-credentials'
- '\.docker\config.json'
- '\kube\config'
condition: selection_image and selection_target
falsepositives:
- npm CLI reading .npmrc during authenticated publish/install operations — correlate with whether a publish was expected
- AWS SDK initialization on developer machines — tune by host role
level: high
---
title: Package Manager Egress to Non-Registry Destination
id: 5c1e8a4d-9f2b-4d7a-a3e6-1b8c4f0d2e97
status: experimental
description: Detects node or python processes establishing outbound connections to destinations outside expected package registry and artifact endpoints, consistent with exfiltration by malicious install scripts from developer or CI build hosts.
references:
- https://www.wiz.io/blog/detect-supply-chain-zero-days-at-publication
- https://attack.mitre.org/techniques/T1041/
author: Security Arsenal
date: 2026/02/14
tags:
- attack.exfiltration
- attack.t1041
- attack.command_and_control
logsource:
category: network_connection
product: windows
detection:
selection_image:
Image|endswith:
- '\node.exe'
- '\python.exe'
filter_registries:
DestinationHostname|contains:
- 'npmjs.org'
- 'npmjs.com'
- 'pypi.org'
- 'pythonhosted.org'
- 'github.com'
- 'githubusercontent.com'
- 'yarnpkg.com'
- 'azure.com'
- 'microsoft.com'
filter_local:
DestinationIp|startswith:
- '10.'
- '192.168.'
- '172.16.'
condition: selection_image and not 1 of filter_*
falsepositives:
- Development servers and test frameworks — scope this rule to CI/CD runner and build host subnets first
- Internal artifact mirrors — add them to the registry filter
level: medium
KQL — Microsoft Sentinel / Defender
This hunt identifies developer workstations and build agents where a package install was followed within a short window by credential-file access and outbound network activity from the same interpreter process — the signature sequence of a malicious install script executing its payload.
let Lookback = 7d;
let SuspiciousParents = dynamic(["node.exe", "npm.cmd", "npx.cmd", "python.exe", "pip.exe"]);
let CredPaths = dynamic(["\\.npmrc", "\\.pypirc", "\\.aws\\credentials", "\\.ssh\\", "\\.git-credentials", "\\.docker\\config.json"]);
let Installs =
DeviceProcessEvents
| where TimeGenerated > ago(Lookback)
| where FileName in~ ("npm.exe", "npm.cmd", "pip.exe", "yarn.exe")
or ProcessCommandLine has_any ("npm install", "npm ci", "pip install", "yarn add")
| project InstallTime=TimeGenerated, DeviceName, DeviceId, AccountName, InstallCmd=ProcessCommandLine;
let CredAccess =
DeviceFileEvents
| where TimeGenerated > ago(Lookback)
| where InitiatingProcessFileName in~ (SuspiciousParents)
| where FolderPath has_any (CredPaths)
| project CredTime=TimeGenerated, DeviceId, InitiatingProcessFileName, FolderPath, InitiatingProcessCommandLine;
let Egress =
DeviceNetworkEvents
| where TimeGenerated > ago(Lookback)
| where InitiatingProcessFileName in~ (SuspiciousParents)
| where not(RemoteUrl has_any ("npmjs.org", "npmjs.com", "pypi.org", "pythonhosted.org", "github.com", "githubusercontent.com", "yarnpkg.com"))
| where not(RemoteIP startswith "10." or RemoteIP startswith "192.168." or RemoteIP startswith "172.16.")
| project NetTime=TimeGenerated, DeviceId, InitiatingProcessFileName, RemoteIP, RemoteUrl, RemotePort;
Installs
| join kind=inner CredAccess on DeviceId
| where CredTime between (InstallTime .. (InstallTime + 30m))
| join kind=leftouter Egress on DeviceId and InitiatingProcessFileName
| where isnull(NetTime) or NetTime between (InstallTime .. (InstallTime + 60m))
| project InstallTime, DeviceName, AccountName, InstallCmd, InitiatingProcessCommandLine, FolderPath, RemoteIP, RemoteUrl
| order by InstallTime desc
For environments ingesting Linux CI runner telemetry via Syslog/CEF, pivot the same logic against the Syslog table: hunt for node or python process execution immediately following package manager activity on build hosts, then correlate with CommonSecurityLog egress from those hosts to non-registry destinations.
Velociraptor VQL
Use this hunt across developer endpoints and build runners to find interpreter processes spawned by package managers — the live execution artifact of a malicious lifecycle script — along with their command lines and network state for triage.
-- Hunt for shells/interpreters spawned by package managers (malicious install script indicator)
SELECT Pid,
Ppid,
Name,
Exe,
CommandLine,
Username,
CreateTime,
get_member(field='Exe') AS ProcessPath
FROM pslist()
WHERE Name =~ '(?i)(powershell|pwsh|cmd|wscript|cscript|bash|sh|curl|wget)'
AND (
SELECT Name
FROM pslist(pid=Ppid)
) =~ '(?i)(node|npm|npx|yarn|pip|python)'
-- Enumerate recent package install artifacts for timeline reconstruction
SELECT FullPath,
Mtime,
Size,
parse_json_array(data=read_file(filename=FullPath)) AS LockfileSample
FROM glob(globs=['C:\\Users\\*\\*\\package-lock.json',
'C:\\Users\\*\\*\\node_modules\\.package-lock.json',
'/home/*/*/package-lock.json',
'/opt/**/package-lock.json'])
WHERE Mtime > now() - 604800
ORDER BY Mtime DESC
Correlate Mtime on recently-written lockfiles with the process execution timestamps from the first hunt to answer the critical IR question: which package version was installed, on which hosts, and did its install script execute?
Remediation & Hardening Script
Run this on developer workstations and CI runners to audit for high-risk dependency hygiene issues: unpinned version ranges, packages with install scripts in the dependency tree, and recently-changed lockfiles. Pair it with your inventory of known-bad package versions when a compromise is disclosed.
#!/usr/bin/env bash
# Supply-chain compromise triage: audit Node.js dependency hygiene on a host
set -euo pipefail
echo "=== [1] Scanning for package.json files with loose version ranges ==="
find /home /opt /srv -name "package.json" -not -path "*/node_modules/*" 2>/dev/null | while read -r f; do
if grep -qE '"\^|"~|"\*|">=' "$f"; then
echo "LOOSE RANGES: $f"
fi
done
echo "=== [2] Enumerating installed packages that carry install scripts ==="
find /home /opt /srv -type d -name "node_modules" 2>/dev/null | while read -r nm; do
find "$nm" -maxdepth 3 -name "package.json" 2>/dev/null | while read -r p; do
if grep -qE '"(preinstall|install|postinstall)"' "$p"; then
pkg=$(grep -oE '"name"[^,]*' "$p" | head -1)
echo "INSTALL SCRIPT: $p ($pkg)"
fi
done
done
echo "=== [3] Lockfiles modified in the last 72 hours (possible recent install) ==="
find /home /opt /srv \( -name "package-lock.json" -o -name "yarn.lock" -o -name "requirements.txt" \) -mtime -3 2>/dev/null
echo "=== [4] Checking for credential files commonly targeted by install-script payloads ==="
for f in ~/.npmrc ~/.pypirc ~/.aws/credentials ~/.git-credentials ~/.docker/config.json; do
[ -f "$f" ] && echo "PRESENT (verify no unexpected access): $f"
done
echo "=== [5] Recent node/python processes with network-adjacent command lines ==="
ps -eo pid,ppid,user,etime,cmd | grep -E '(node|python)' | grep -iE '(curl|wget|http|base64|eval)' | grep -v grep || echo "None found"
echo "=== Triage complete. Cross-reference findings against known-bad package/version lists from your registry monitoring or vendor advisories. ==="
On Windows developer endpoints, the equivalent audit is: enumerate package.json files for loose ranges, check Get-ChildItem against node_modules for packages declaring install scripts, and review recent package-lock.json modification times — wrap it in your RCM/EDR live-response tooling rather than asking developers to self-report.
Remediation and Long-Term Hardening
There is no patch for a poisoned registry package — remediation is about exposure reduction, speed, and response readiness:
- Pin and lock everything. Eliminate
^and~ranges in production builds. Commit lockfiles (package-lock.json,poetry.lock,Pipfile.lock) and enforce--frozen-lockfile/npm ciin CI so builds resolve only to reviewed, hashed versions. This converts your exposure window from "every install" to "first install after a version bump you chose to take." - Delay new version adoption. Configure your artifact proxy (Artifactory, Nexus, GitHub Packages, CodeArtifact) to quarantine package versions younger than 24–72 hours unless explicitly allowlisted. Most registry compromises are detected and pulled within that window; a cooling-off period means the malicious version is gone before you ever fetch it.
- Disable lifecycle scripts by default in CI. Run
npm ci --ignore-scripts(and setignore-scripts=truein project.npmrc) for dependencies, explicitly re-enabling scripts only for the small set of packages that genuinely need native builds. This kills the most common payload delivery mechanism outright. - Segment and egress-filter build runners. CI runners hold the crown jewels — cloud deploy credentials, signing keys, registry tokens. They should have no route to the internet except an allowlisted set of registry and artifact endpoints. Exfiltration-dependent payloads die at the firewall.
- Short-lived, scoped CI credentials. Replace long-lived
NPM_TOKENand cloud keys on runners with OIDC-based workload identity (e.g., GitHub Actions OIDC to AWS) so that a harvested environment yields nothing reusable. - Maintain a real dependency-to-asset inventory. When a compromise is disclosed, "are we affected?" must be a query, not a project. Continuously map resolved dependency versions to hosts, images, and pipelines.
- Operationalize pre-CVE intelligence. Subscribe to registry security advisories, OpenSSF and Socket/GitHub malware feeds, and vendor publication-time detection (as described in the Wiz research). Route these into your SIEM with asset correlation so your SOC is working the alert during the exposure window — not reading about it in a disclosure blog next month.
The Bottom Line
The supply chain zero-day problem is fundamentally a timing problem. Attackers get execution at publication; defenders historically get awareness at disclosure — and in many registry compromises, formal CVEs never arrive at all. Closing that gap requires shifting detection left: registry-level monitoring, behavioral detection on developer and build endpoints, and dependency hygiene that shrinks the window in which a poisoned package can execute in your environment. The organizations that weather the next registry compromise will be the ones whose telemetry — not the attacker's announcement — told them they were exposed.
Related Resources
Security Arsenal Penetration Testing Services AlertMonitor Platform Book a SOC Assessment vulnerability-management Intel Hub
Is your security operations ready?
Get a free SOC assessment or see how AlertMonitor cuts through alert noise with automated triage.