Your SBOM Is a Compliance Artifact. Make It a Build Gate: Translating PCI DSS 4.0.1's Supply-Chain Requirements Into Enforceable CI

Part 4 of a 4-part series on the software supply chain and your CDE.
- Translating CI/CD Identity into Terraform - kill the standing credentials.
- Zero-CVE Worms in Your CDE Pipeline - why your CVE program cannot see the worm.
- SLSA Was On, and It Still Shipped Malware - the provenance control, done honestly.
- Your SBOM Is a Compliance Artifact. Make It a Build Gate (this piece) - mapping it all to PCI 4.0.1.
"Maintain an inventory of your software components" is a requirement that fits comfortably in a spreadsheet, refreshed quarterly and produced at assessment time. A spreadsheet would not have stopped any of the attacks that defined the last three weeks. An inventory you read after the build is a record of what already shipped. The control PCI 4.0.1 is actually reaching for is the one that runs before the artifact leaves your pipeline and refuses to let a poisoned dependency through.
The good news for anyone who has been told supply-chain compliance is a paperwork problem: the enforcement points already exist, they live in your CI config, and you can write them as code. Here is how the June 2026 npm wave maps to three PCI DSS 4.0.1 requirements, and how to turn each requirement into a gate instead of a gesture.
What actually happened, and why your inventory didn't catch it
On June 16, 2026, an attacker published a clean version of a small date-handling package called easy-day-js. Twelve hours later they flipped it: version 1.11.22 carried a postinstall payload. Then, in a burst between roughly 01:00 and 02:40 UTC on June 17, they republished around 140 packages across the mastra and @mastra npm scopes, and into each one they added a single innocuous-looking line to package.json:
"easy-day-js": "^1.11.21"Read the pinned version and you see a harmless 1.11.21. The caret range resolved it to 1.11.22, the weaponized release tagged latest. As SafeDep's teardown put it: "Audit the pinned 1.11.21 and you read a harmless date library. The payload rides in on 1.11.22." Your SBOM, if it captured the manifest, recorded the safe number.
The access was almost boring. The compromised account, ehindero, belonged to a former contributor who had not published since early 2025. npm does not expire scope permissions on inactivity, so one stale maintainer credential was enough to publish to the entire scope. No zero-day. A login.
What ran next is the part your CDE cares about. The postinstall hook executed a 4,572-byte dropper that set NODE_TLS_REJECT_UNAUTHORIZED = '0' to disable TLS verification, pulled a second stage from a hardcoded IP, spawned it as a detached background process, and deleted itself. The second stage hunted 166 cryptocurrency-wallet browser extensions, copied browser history databases, and on developer and CI machines reached for exactly the credentials a build host carries. Microsoft's guidance was blunt about the install-time control that breaks this: run installs with --ignore-scripts, and rotate "any credentials, tokens, or API keys that may have been present on systems."
This was not an isolated event. Two weeks earlier, Red Hat disclosed (RHSB-2026-006) that a compromised GitHub account had injected malicious code into frontend JavaScript libraries in the @redhat-cloud-services namespace. And the macro picture backs the trend: the 2026 Verizon DBIR reported vulnerability exploitation overtaking stolen credentials as the number-one initial-access vector, at 31 percent of breaches, the first time in the report's nineteen-year history that credential abuse has been knocked off the top spot. The dependency graph is now a primary attack surface, and "we keep an inventory" is not a control on that surface. It is a description of one.
Three PCI DSS 4.0.1 requirements speak directly to this. Each has a paper reading and an engineering reading. The engineering reading is the one that would have caught Mastra.
6.3.2: the inventory is the input to a gate, not the output of a meeting
What it asks for: an inventory of bespoke and custom software and the third-party software components incorporated into it, maintained to facilitate vulnerability and patch management.
The paper reading: generate an SBOM, store it, show it at assessment time.
The engineering reading: generate the SBOM in the pipeline, on every build, and feed it straight into a policy check that fails the build on a known-bad or unverified component. The inventory stops being a document you maintain and becomes a value your pipeline produces and immediately acts on.
# .github/workflows/build.yml: SBOM as a build-time artifact AND a gate
- uses: actions/checkout@v4
- run: npm ci --ignore-scripts # resolve the tree first, with no lifecycle-hook execution
- name: Generate SBOM (CycloneDX) on every build
run: npx @cyclonedx/cyclonedx-npm --output-file sbom.json
- name: Fail the build on known-vulnerable or unverified components
run: |
# 1. The inventory exists as a versioned artifact (6.3.2 evidence).
# 2. It is also the input to a hard policy decision, right here.
osv-scanner --sbom=sbom.json # exits nonzero when any vuln is found
- name: Require a PRESENT provenance attestation on every dependency
run: |
# --include-attestations is load-bearing: plain --json returns only
# {"invalid":[],"missing":[]} counts about REGISTRY SIGNATURES and carries
# no attestation data at all.
npm audit signatures --json --include-attestations > sig.json
node ci/require-provenance.js sig.json # exits 1 when any attestation is MISSINGThat last step is the one that mattered in June, and the obvious one-liner is not enough. npm audit signatures verifies the npm registry signature on everything it fetched and validates any provenance attestations that are present. It does not, by default, fail simply because provenance is absent. The legitimate Mastra releases shipped from CI with SLSA provenance attestations; the malicious republishes dropped them, the publisher field changed from the CI identity to a personal email, and provenance: yes became provenance: no. Those poisoned packages still carried valid registry signatures, so the bare command would have passed them.
You have to turn "has an attestation" into a hard requirement. That is what the require-provenance.js step above does, and the obvious implementation of it does not work. Plain --json returns only {"invalid":[],"missing":[]}, two arrays that describe registry signature failures and contain no attestation data whatsoever. You need --json --include-attestations, which adds a verified array listing the packages that do carry provenance. Two sharp edges there, both of which will cost you an afternoon if you meet them in CI instead of here. That verified array is npm 11 or newer: on npm 10, which is still what node:22 ships, --include-attestations returns the same two arrays and no verified key, so a script that reaches for it dies on undefined. And npm never emits the complement, the list of packages without attestations, so you have to derive it yourself: resolve the installed set from package-lock.json, subtract the verified names, and exit nonzero on the remainder. There is still no native npm flag for any of this.
// ci/require-provenance.js - requires npm 11+ for the `verified` array
const fs = require('fs');
const audit = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
const lock = JSON.parse(fs.readFileSync('package-lock.json', 'utf8'));
const attested = new Set(audit.verified.map(p => `${p.name}@${p.version}`));
const installed = [...new Set(
Object.entries(lock.packages)
.filter(([path, meta]) => path.startsWith('node_modules/') && meta.version && !meta.link)
.map(([path, meta]) =>
`${path.slice(path.lastIndexOf('node_modules/') + 13)}@${meta.version}`)
)];
const missing = installed.filter(id => !attested.has(id)).sort();
console.log(`${installed.length} packages, ${attested.size} attested, ${missing.length} unattested`);
missing.slice(0, 3).forEach(id => console.error(` no provenance attestation: ${id}`));
if (missing.length > 3) console.error(` ...and ${missing.length - 3} more`);
process.exit(missing.length ? 1 : 0);Here it is running. Two packages, one published with provenance and one without, in a clean container:
docker run --rm -it node:22-alpine sh
npm i -g npm@11
npm init -y && npm install --ignore-scripts \
@sigstore/[email protected] [email protected]
npm audit signatures
audited 2 packages in 0s
2 packages have verified registry signatures
1 package has a verified attestation
echo $?
0
npm audit signatures --json --include-attestations > sig.json
node require-provenance.js sig.json
2 packages, 1 attested, 1 unattested
no provenance attestation: [email protected]
echo $?
1Read the two exit codes. Both packages carry valid registry signatures. Only one carries provenance. npm audit signatures sees the difference, prints it in plain English, and exits 0 anyway. The gate reads the same data and exits 1. That gap between what the tooling reports and what it enforces is precisely the gap the Mastra republishes walked through.
Part 3 of this series walks that gate, and the organizational versions of it (a pull-through proxy or admission controller that enforces the policy where a developer cannot skip it), in full.
The attestation existed as a signal. It was never wired to a gate.
6.4.3 (and 11.6.1): your payment page is downstream of your dependency graph
What it asks for: manage all payment-page scripts that load and execute in the consumer's browser. Confirm each script is authorized, assure its integrity, and maintain an inventory with written justification. Its sibling, 11.6.1, wants a tamper-detection mechanism on the payment page itself.
6.4.3 is usually associated with Magecart-style skimming injected at the edge. Look at what Red Hat's incident actually poisoned: frontend JavaScript libraries. The path from a compromised npm package to a malicious <script> on your checkout page is not hypothetical, it is a npm install followed by a webpack build followed by a deploy. 6.3.2 governs the component going in; 6.4.3 governs the script coming out. They are the two ends of one supply chain, and PCI 4.0.1 made both mandatory (DESV-graduated) as of March 31, 2025 for a reason.
Enforce integrity at both ends. Subresource Integrity pins the hash of an externally loaded script so a swapped file fails to execute:
<!-- A third-party script you control the version of: pin it,
or the browser refuses to run it -->
<script src="https://cdn.example.com/analytics/4.2.1/a.js"
integrity="sha384-<pinned-hash>"
crossorigin="anonymous"></script>One caveat shapes how you evidence this control: some payment scripts cannot be SRI-pinned at all. A major processor's drop-in JS is typically served from the processor's own CDN and updated continuously so they can push fraud-detection changes, which means it is a deliberately moving target. Pinning a hash against one either breaks checkout on the next update or freezes you on a stale build. Whether a given processor supports pinning or self-hosting is their call and it changes, so read their current integration guidance rather than ours. The question is what you do when the answer is that you cannot pin this one.
That is not a gap in your program. It is the reason 6.4.3 is written as authorize, justify, inventory rather than hash everything: the requirement anticipates scripts you cannot pin. Where you cannot pin, the control is met by the other three legs: the script is on an explicit allow-list, the business justification is written down, the version and source are inventoried, and 11.6.1 tamper detection watches the page for anything that appears outside that list. Pin what you can pin. Authorize and monitor what you cannot.
And the inventory-with-justification that 6.4.3 demands is, again, better as a checked-in artifact than a Confluence page. A small allow-list of authorized payment-page scripts, validated in CI against what the build actually emits, turns "we maintain a list" into "the build fails if an unlisted script appears on the payment page." A reporting Content-Security-Policy gives you the 11.6.1 tamper signal as a side effect: any script the page tries to load that you did not authorize generates a violation report you can alert on.
12.8: your build platform is a TPSP, and the public registry is not
What it asks for: a list of third-party service providers (12.8.1), written agreements (12.8.2), due diligence before engagement (12.8.3), and monitoring of their compliance posture (12.8.4-12.8.5).
Start with what does not belong on that list. The public npm registry is not a TPSP, and neither is a pseudonymous package maintainer. Requirement 12.8 is built on a contractual relationship: 12.8.2 wants a written agreement, 12.8.3 wants due diligence before engagement, 12.8.4 wants you to monitor their compliance status. None of that machinery has anywhere to attach on registry.npmjs.org. You cannot sign an MSA with it, and you will not get a SOC 2 report from a contributor who stopped logging in eighteen months ago. Open-source dependencies belong to 6.3.2. That is the requirement written for them.
What does belong on the 12.8 list is the set of contracted platforms your build actually runs on and pulls through: your CI/CD provider (GitHub, GitLab, CircleCI), your artifact registry or pull-through proxy vendor (Artifactory, Cloudsmith, GitHub Packages), and your source-control host. These are vendors you have an agreement with, they can demonstrably affect the security of your CDE, and they are exactly the systems the June wave was harvesting credentials from. Scope those under 12.8, do the diligence, and monitor them.
That leaves a real question 12.8 does not answer: what do you do about the untrusted upstream you pull from at 2 a.m.? The answer is not a vendor questionnaire. It is a technical constraint, enforced at the boundary you do control, which happens to be the proxy vendor you just scoped under 12.8. Constrain what the relationship is allowed to do.
The single highest-leverage move is to delete the long-lived publish token from the equation entirely, because that is the credential class these attacks keep cashing in. npm's trusted publishing (GA July 2025) lets CI authenticate to the registry with a short-lived OIDC token scoped to one workflow, with no NPM_TOKEN stored anywhere, and it auto-generates the provenance attestation that section 6.3.2's gate then verifies:
# Publishing job: no stored token, OIDC-minted credential, automatic provenance
permissions:
id-token: write # mint the OIDC token
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
- run: npm ci --ignore-scripts # same default-deny rule as every other job
- run: npm audit signatures --json --include-attestations > sig.json
- run: node ci/require-provenance.js sig.json # do not sign what you did not verify
- run: npm publish # trusted publishing: no NPM_TOKEN, provenance attached by defaultOne caveat on that last line. Provenance really is automatic under trusted publishing, with no --provenance flag required, but only when the package is public and built from a public repository. Publish a private package, or build from a private repo, and no attestation is generated no matter how correct the rest of the workflow is. Provenance generation is also not supported on CircleCI. If either applies to you, the gate in section 6.3.2 is verifying an attestation your own pipeline never produced.
Then enforce the posture at the org level so the keyless path is the only path. npm lets an organization require 2FA and disallow tokens, so publish authority flows exclusively through CI's OIDC identity and there is no static secret in any repo for the next worm to harvest. On the install side, npm v12 shipped on July 8, 2026 and finally makes the install-time controls default-deny: allowScripts is off, so preinstall, install, postinstall, and prepare scripts from dependencies no longer run unless you explicitly approve them via npm approve-scripts (approvals are recorded in package.json, so commit them). Git and remote-URL dependencies are likewise blocked unless permitted. Note the sharp edge for CI: this also blocks the implicit node-gyp rebuild for any package with a binding.gyp, so native modules are the thing most likely to break on upgrade.
If you are still on v11, set the policy now rather than discovering it at upgrade time:
# .npmrc: default-deny install scripts; pin your OWN scope to a trusted source
ignore-scripts=true
@yourco:registry=https://your-proxy.internal/npm/Substitute your real internal scope. The scope-level registry assignment is your answer to dependency confusion: a package claiming an internal scope resolves only from the source you named, not from whatever public mirror answers first. Run an install on npm 11.16.0 or newer first, since it emits warnings for exactly what v12 will refuse.
Continuous compliance: the evidence is a pipeline artifact, not a quarterly scramble
Every control above has one thing in common. The SBOM, the provenance verification log, the SRI manifest, the CSP violation reports, the signed publish record: each one is produced automatically, on every build, and each one is exactly the evidence an assessor asks for. You are not assembling a binder the week before the assessment. The pipeline emits the binder as a byproduct of refusing to ship unverified code.
That is the difference PCI 4.0.1 is quietly pushing the whole standard toward. The customized-approach framing and the targeted-risk-analysis language all reward programs that can show a control operating continuously over one that can show a control documented. Supply-chain security is the cleanest place to demonstrate it, because the artifacts are machine-generated and timestamped by construction.
A decision checklist for SaaS and Stripe-integrated teams
If you offload your payment page to a processor and assume your dependency risk went with it, walk this list before your next assessment:
- Does your frontend build pull any package that renders on the payment page? If yes, your dependency graph is upstream of your payment page, and those scripts are what 6.4.3 and 11.6.1 are written about. SRI-pin every script you can pin, and validate the payment page's emitted script set in CI against an authorized allow-list.
- Do your CI runners hold a long-lived
NPM_TOKEN, cloud key, or registry credential? If yes, that is the artifact June's worms were built to collect. Move to OIDC trusted publishing and federated cloud identity; store no static publish secret. - Does
npm installrun dependency lifecycle scripts in your pipeline today? npm v12 made default-deny the default as of July 8, 2026, so this is now an upgrade-readiness question rather than a policy one. Install on 11.16.0+ first, review the warnings, and approve the handful you actually need withnpm approve-scripts. - Is your SBOM read by a human or by a gate? If a human, it is documentation. Wire it to
osv-scannerand to a provenance check that fails on a missing attestation, not just an invalid one, so an unverified component fails the build. - Is your CI/CD platform and artifact proxy on your 12.8 TPSP list? Those are contracted vendors that can affect CDE security, and they are frequently missing. Do not put the public npm registry on that list; it is not a TPSP, and dependencies belong under 6.3.2. The control for the untrusted upstream is technical: keyless publish, provenance verification, registry pinning, default-deny scripts.
The takeaway
The June 2026 npm attacks did not defeat anyone's inventory. They walked right past it, because an inventory is a record and an attack is an event, and a record cannot stop an event. PCI DSS 4.0.1's supply-chain requirements are worth far more read as engineering than as documentation: take 6.3.2, 6.4.3, and 12.8 as instructions to build gates, and they become the difference between a poisoned dependency that fails your build at 01:20 UTC and one that ships to your payment page.
Translate the controls into CI policy, let the pipeline produce the evidence, and the next time a maintainer credential gets reused to push 140 backdoored packages, your build reads the missing provenance, exits nonzero, and the assessment writes itself.
We spend our days inside cloud-native cardholder data environments, reading Terraform and CI config alongside the engineers who wrote it. If your supply-chain compliance is still a spreadsheet, we can help you turn 6.3.2, 6.4.3 and 12.8 into build gates that produce their own evidence.
Sources
- SafeDep - Mastra npm Scope Takeover Supply Chain Attack
- Microsoft Security - Postinstall Payload Inside the Mastra npm Supply-Chain Compromise
- Red Hat - RHSB-2026-006: npm Supply Chain Compromise (@redhat-cloud-services)
- BleepingComputer - GitHub Announces npm Security Changes to Tackle Supply-Chain Attacks (npm v12)
- GitHub Changelog - npm Trusted Publishing with OIDC Is Generally Available
- npm Docs - Trusted Publishers
- NCSC - Software Supply Chain Attacks: Check Your Dependencies
- CPO Magazine - Verizon 2026 DBIR: Vulnerability Exploitation Leaps Ahead of Stolen Credentials
- GitHub Changelog - Upcoming Breaking Changes for npm v12
- The Hacker News - npm 12 Disables Install Scripts by Default to Reduce Supply Chain Risk
- npm Docs - npm-audit (audit signatures)
- PCI DSS v4.0.1 - Requirements 6.3.2, 6.4.3, 11.6.1, and 12.8.1-12.8.5
Christopher Callas
Christopher is the Principal at Arbure Inc., leading strategic and technical initiatives that shape the firm's cybersecurity consulting services. With over a decade of experience, he has built a reputation for delivering tailored security solutions that align with business objectives while addressing modern threats. His expertise spans cloud security, compliance, and risk management, guiding organizations through complex regulatory landscapes and securing multi-cloud environments.