Blog// 18 MIN READ

A Signed Build Can Still Be Malicious: What UNC6780 Teaches AI Supply Chains

UNC6780 published malicious packages with valid SLSA attestations. Learn what provenance proves and how to build a safer AI release gate.

EVIDENCE → INTERPRETATION → DECISION

Google Threat Intelligence Group reported a supply-chain failure that should change how teams review AI-generated code. UNC6780 stole short-lived publishing identity from CI runners, then released compromised packages with valid SLSA Build Level 3 attestations. The signature worked. The provenance worked. The release was still malicious.

The incident does not show that provenance is useless. It shows that teams often ask provenance to answer a question it was never designed to answer. A build attestation can tell you where, when and how an artefact was produced. It cannot decide whether the source was hostile, the workflow had been changed by an attacker, or the package should be allowed into a privileged AI development environment.

This field note separates those claims and turns the incident into a release gate that an engineering team can test. The core rule is simple: signed is an evidence state, not a safety verdict.

Answer first: yes, a signed SLSA build can be malicious. A valid attestation protects the integrity of recorded provenance. Safety also requires an authorised publisher, reviewed source, a trusted build path, behaviour checks and a policy decision tied to the intended deployment.

What Google observed

Google published its GTIG AI Threat Tracker on 8 September 2026. The report attributes a series of large software supply-chain compromises to the financially motivated group UNC6780, also known as TeamPCP. Its targets included PyPI, npm and Docker Hub, as well as development environments that use AI coding tools.

This was not a theoretical paper exercise. Independent analysis by Cloud Security Alliance research and the vendors who tracked the campaign described it as the first documented npm worm to produce packages carrying valid SLSA Build Level 3 provenance attestations. The same group hit PyPI with Python-specific persistence, and it compromised Docker Hub images as well. By the time public analysis settled, the campaign had published a malicious package that reached roughly two million installs in about eleven minutes under a verified publisher identity, and it had exfiltrated thousands of internal GitHub repositories, including infrastructure used by GitHub’s own ecosystem-facing services.

One part of the report deserves close attention. GTIG says DUSTMAKER malware could detect when it was running inside a CI/CD environment, extract OpenID Connect tokens from GitHub Actions runner memory and use them to act as a trusted publisher. The resulting compromised packages carried valid, cryptographically signed SLSA Build Level 3 attestations. According to GTIG, packages released with those valid tokens could pass automated trust checks used by AI coding agents.

The same campaign worked on several neighbouring control surfaces. Attackers placed files in hidden workspace directories used by IDEs and coding assistants, including .claude/, .vscode/ and .cursor/. Malicious configuration could trigger commands when a workspace opened. Hostile comments in JavaScript loaders were also designed to make LLM-based scanners refuse or skip inspection while the code below remained available to execute.

These are connected failures. The attacker did not need to defeat one perfect security product. They shaped the evidence each automated component consumed: the registry saw a trusted publisher, the coding agent saw a signed package, the IDE saw ordinary project configuration, and the scanner saw text that could divert its analysis.

The attack chain, step by step

To understand why the provenance was valid, you need to see the whole chain. Public analysis of the Shai-Hulud lineage that precedes UNC6780’s later waves reconstructed it as follows.

Step 1: initial access through a CI workflow misconfiguration. The campaign’s first foothold into a major maintainer’s release pipeline came from the way the workflow handled pull requests from forks. The workflow used the pull_request_target trigger with write access to the base repository’s cache. That combination is dangerous by design: pull_request_target runs with the base branch’s secrets and permissions, but an attacker can control the code paths that process the PR. The attacker opened a pull request from a fork and used the misconfiguration to inject code into a shared cache partition.

Step 2: cache poisoning reaches the release build. A later legitimate release workflow read from that shared cache. Because the poisoned entries were already present, the trusted build consumed attacker-influenced content. This is why “the build environment is isolated” is only part of the story: if the build ingests a cache that another tenant could populate, the isolation boundary has a hole in it.

Step 3: OIDC token extraction from runner memory. Once the attacker’s code was executing inside the release workflow, it extracted an OpenID Connect token from the GitHub Actions runner process’s memory. Public analysis described the mechanism as reading process memory via /proc/<pid>/mem on the runner. The token was short-lived, but it was alive at that moment, and the attacker could use it before it expired.

Step 4: exchange with Sigstore’s Fulcio for a signing certificate. The OIDC token is presented to Sigstore’s Fulcio certificate authority, which issues a short-lived code-signing certificate bound to the identity claims in the token. The attacker now holds a legitimate signing key with a certificate that names the compromised maintainer’s repository and workflow as the subject.

Step 5: valid SLSA Build Level 3 attestation. With that certificate, the attacker could sign a provenance attestation for the malicious package. The attestation described a build that genuinely ran in GitHub Actions and produced the named digest. Every cryptographic check downstream passed, because the authority used to sign was real, even though the workflow had been compromised.

Step 6: publication under a trusted publisher. The OIDC token also satisfied PyPI’s trusted-publisher exchange for npm and PyPI respectively, so the package was published under a verified publisher identity. Tooling displayed green provenance badges. AI coding agents that checked signatures and provenance saw a clean result.

Later waves escalated further. Analysts documented the group calling Sigstore’s Fulcio and Rekor services in real time to obtain signing certificates, rather than only stealing tokens already present in a runner. That removed even the need to compromise a specific build first. Supply-chain scanners displayed valid provenance on malicious artifacts.

The important detail is that none of these steps broke the cryptography. The signatures were genuine. The attacker simply controlled the identity that was allowed to ask for them, or stole it from a process that was already inside the trusted workflow. Provenance attests to what happened. It does not attest to who decided that it should happen.

The question provenance actually answers

The SLSA v1.2 provenance specification defines provenance as verifiable information describing where, when and how an artefact was produced. Build provenance links an output back to the source and build process. That evidence lets a consumer compare the build against expectations and, where practical, reproduce it.

SLSA’s build requirements make the trust boundary explicit. Higher build levels strengthen completeness, authenticity and resistance to tampering. They help establish that the recorded builder produced the named digest under the recorded conditions.

None of that means “the code is benign”. If an attacker controls the source revision or obtains the identity that is permitted to publish, a strong build platform can produce accurate evidence about a bad build. The attestation may be cryptographically sound because the compromised workflow genuinely requested it. Provenance has not lied. The organisation’s policy has trusted an insufficient set of facts.

Trust stack separating publisher identity, source review, build provenance, behaviour analysis and deployment policy for signed software packages.
Original model: provenance is one layer of a release decision. Each layer answers a different question, and no signature collapses them into one verdict.

Reading a provenance attestation

To know what you can infer, you need to know what an attestation contains. The SLSA provenance predicate is an in-toto statement with a predicateType of https://slsa.dev/provenance/v1. Two structures dominate it: buildDefinition and runDetails.

Annotated SLSA provenance predicate showing what the subject, buildDefinition, externalParameters, internalParameters, resolvedDependencies, runDetails, builder.id, metadata and byproducts each prove, and what they cannot.
Original annotated diagram: the fields inside an SLSA provenance predicate and the claim each one actually supports.

buildDefinition.buildType is a URI that identifies how to interpret the build. externalParameters are the interface to the build, and per the specification they are untrusted: they MUST be included and MUST be verified downstream. At SLSA Build Level 3, external parameters MUST be complete, meaning there is no additional mechanism for an external party to influence the build without it being recorded. internalParameters are trusted platform internals, optional, and useful for debugging and reproduction. resolvedDependencies lists the artefacts needed at build time; completeness is best effort even at Level 3.

runDetails.builder.id is the single most important field. It is intended to be the sole determiner of the SLSA Build level, and it identifies the transitive closure of the trusted build platform. If a platform has modes with different security properties, each mode must have a different builder.id and ideally a different signer. The metadata block carries invocationId, startedOn and finishedOn, which let you find logs. byproducts can reference diagnostic artefacts for incident response.

The verification consequence is concrete: you should accept only a specific signer-builder pair for your policy, and you should reject unrecognised or unexpected fields in externalParameters. A verifier that accepts “any valid signature from GitHub Actions” has already lost most of the value of provenance, because it has collapsed every builder mode into one trust decision.

What Build Level 3 actually requires

Most coverage treats “Build Level 3” as a badge. The SLSA build requirements define it as a precise ladder, and each rung is an assumption you are allowed to make.

Property Level 1 Level 2 Level 3
Provenance exists Yes Yes Yes
Provenance authentic (signed, tamper-evident) No Yes Yes
Provenance unforgeable (signing key in a secure management system, inaccessible to build steps) No No Yes
Isolation strength Hosted Hosted Isolated
Ephemeral environment per build No No Yes
Resists cache poisoning and cross-build influence No No Yes

At Level 3, the build platform MUST guarantee that a build cannot access the provenance signing key, that overlapping builds cannot influence each other’s memory, that each build runs in an ephemeral environment, and that one build cannot inject false entries into a cache used by another build. Those are real, strong guarantees about the build platform.

Here is the boundary the UNC6780 campaign crossed. SLSA Build Level 3 guarantees that a well-intentioned build runs securely, and that the provenance cannot be forged by a tenant of the platform. It explicitly does not prevent a producer from running a risky or insecure build. It does not require the build platform to verify that the source is benign, that the workflow was not changed by an attacker before the build started, or that the person or process authorising the build was authorised to do so. The spec says it plainly: a self-hosted runner or a remote execution service can sit outside the trust boundary, and the provenance can still be valid Level 3.

So a valid Level 3 attestation is strong evidence that the named builder really did build that digest, in an isolated environment, without the signing key being exposed. It says almost nothing about whether the source revision was safe, whether the workflow that produced it had been modified upstream, or whether the identity that triggered the build had been stolen.

How short-lived OIDC trust became publishing authority

Trusted publishing removes long-lived registry secrets from CI. A workflow requests an OIDC token from its identity provider, presents that token to the package registry and receives short-lived publishing authority. This is a real security improvement because there is no static API key waiting in a repository secret for months.

Short-lived does not mean harmless. The PyPI Trusted Publishers security model says OIDC tokens remain sensitive material and that a compromised CI workflow can be equivalent to credential compromise. PyPI also states that Trusted Publishing does not assert the safety of the code or the trustworthiness of its authors.

GitHub’s OIDC reference shows why claim design matters. Trust policies can bind tokens to a repository, branch, environment and other properties. A weak policy that accepts a broad repository context may grant too much. A tighter policy narrows who can obtain the credential, but it still assumes that the authorised workflow and runner are behaving as expected.

UNC6780 attacked that assumption at runtime. If malware is already inside a runner and can retrieve a token from memory, the token’s short lifetime may still be long enough to publish. The registry sees an identity statement that is valid at that moment. It does not see the compromise that preceded the request.

The defensive shift is to stop treating “token came from the right repository” as “the right repository was uncompromised”. Bind the publishing decision to the claims you can actually verify at decision time, and treat the token as evidence of an identity, never as evidence of integrity.

Four claims that must stay separate

I would make a release gate evaluate four independent claims. This prevents an AI agent, registry policy or human reviewer from turning one green check into blanket trust.

Claim Evidence What it does not prove Fail action
Publisher identity OIDC issuer, audience, subject, repository ID, workflow and environment That the runner or workflow was uncompromised Reject the release and revoke the trust binding
Expected source Protected branch, reviewed commit, immutable repository identity and approved dependency change That the build used only those inputs Quarantine the artefact and block promotion
Known build Signed provenance, builder identity, digest, parameters and resolved dependencies That the resulting behaviour is safe Require a matching policy and reproducible evidence
Acceptable behaviour Sandbox execution, capability diff, network policy, malware analysis and negative tests That a later deployment context is equally safe Deny installation or constrain capabilities

The gate should fail closed. If the identity service, policy service, provenance verifier or analysis sandbox is unavailable, the package waits. A timeout is not permission to install. This matters most for AI coding agents because they can turn a dependency suggestion into a filesystem change, build invocation or deployment without the pause a human normally introduces.

Why AI coding agents change the exposure

Traditional dependency risk assumes a person or build system selects a package. An AI coding agent adds another decision-maker that reads project text, chooses tools and may install what appears to satisfy a task. The package name, repository description, README, workspace configuration and scanner output all become inputs to that decision.

This widens the attack surface in three ways. First, agents can discover and install unfamiliar packages faster than a reviewer can investigate them. Second, project-local instruction files may alter how an agent interprets the workspace. Third, an LLM-based scanner can be influenced by the same untrusted content it is supposed to judge.

UNC6780 exploited all three. Hidden configuration in .claude/, .vscode/ and .cursor/ directories could execute commands when the workspace opened, which means the “install a dependency” task and the “run setup commands” task are the same decision. Hostile comments in JavaScript loaders were crafted so that an LLM-based scanner would refuse or skip analysis of the file, while the executable code below the comment remained intact. The attacker was not trying to beat a human reviewer; they were shaping what the automated reviewers would report.

A signed artefact is especially persuasive in this environment because automated systems prefer clean binary signals. Signature valid. Provenance present. Trusted publisher matched. Those checks are useful, but they become dangerous when the final policy treats them as a substitute for source and behaviour review.

The right architecture keeps package content outside the trust boundary. Metadata can inform the decision, but neither README prose nor project instructions should be able to grant the agent more capability. Installation, script execution, network access and secret access need separate policy decisions.

Build a four-claim release gate

Four-claim release gate checking identity, source, build and behaviour before an AI coding agent can install a package.
Original release gate: installation proceeds only when identity, source, build and behaviour evidence agree with the deployment policy.

Start by defining the protected action. For an AI coding assistant, “add dependency” is too vague. Name the actual state change: resolve a package version, write it to a lockfile, run lifecycle scripts, execute its code during tests, include it in a production image or deploy it with access to customer data.

Then bind evidence to that action:

  1. Pin the identity: require the expected OIDC issuer, audience, immutable repository identity, workflow path, protected environment and release ref. Alert on claim drift.
  2. Pin the source: accept only a reviewed commit from a protected branch. Require change approval for workflow files, agent instructions and dependency manifests.
  3. Verify the build: check the artefact digest, builder identity, provenance signature, external parameters and resolved dependencies against policy. Do not stop at “attestation exists”.
  4. Test behaviour: install and exercise the package in a disposable environment with no production credentials. Record process creation, filesystem writes, network destinations and requested permissions.
  5. Constrain deployment: grant only the capability needed by the consuming application. A package used for tokenisation should not inherit cloud credentials or unrestricted outbound access.
  6. Preserve the decision: store the source revision, artefact digest, attestation, analysis result and policy version together. A later incident should be reconstructable without trusting mutable registry pages.

The order matters. Behavioural analysis cannot repair a publisher identity mismatch. A valid source commit cannot excuse a builder outside policy. Passing all four claims at release time also does not create permanent trust, so deployments need a way to revoke an artefact and trace where its digest is running.

Test the gate with evidence failures

A release policy is only real after its denial paths work. Build a small suite around representative packages and synthetic credentials. The tests should not need live malware.

  • Present valid provenance from the wrong workflow or repository identity. The release must stop.
  • Present expected provenance for a commit that lacks required review. The release must stop.
  • Change the lockfile after approval while keeping the source repository trusted. The digest comparison must catch it.
  • Return a timeout from the provenance service or policy engine. The agent must not install by default.
  • Use a benign test package that attempts an undeclared network connection during installation. The sandbox should record and block it.
  • Place untrusted instructions in a fixture’s IDE configuration. The coding agent must not convert them into command authority.
  • Feed the scanner content designed to trigger a refusal while harmless executable test code sits below it. The inspection pipeline should isolate content classification from code analysis.

Record the denial reason in terms an engineer can act on: “publisher workflow mismatch” is useful; “untrusted package” is not. Good telemetry names which claim failed, the evidence observed and the exact protected action that was denied.

A 60-minute review for an engineering team

  1. 0 to 10 minutes: choose one package that an AI coding agent may install and list every state change that follows installation.
  2. 10 to 20 minutes: inspect the trusted-publisher binding. Write down the accepted issuer, repository, workflow, environment, ref and audience.
  3. 20 to 30 minutes: take one released digest and verify its provenance. Compare builder, source and parameters with the release policy.
  4. 30 to 40 minutes: identify where project content can instruct the coding agent or scanner. Include hidden IDE directories and repository-level agent files.
  5. 40 to 50 minutes: run one negative test for each of the four claims. Confirm the gate denies rather than warns and continues.
  6. 50 to 60 minutes: write the revocation path. Find every build, cache and production image that would need removal if the digest became untrusted.

The output should be small: one policy, four evidence checks, a denial log and a revocation query. If the team cannot reconstruct why a package was installed, adding another signature will not fix the decision.

What to log without creating another secret store

Release telemetry should retain identity and integrity evidence, not copy sensitive tokens. Store the OIDC claim set needed for policy review, the token issuer and key identifier, but never the bearer token itself. Link that record to the immutable artefact digest, source commit, workflow revision, builder identity, analysis result and policy version.

For AI coding agents, preserve the requested dependency, the user-approved task and the tool call that attempted installation. Keep untrusted repository text separate from the policy decision. If a later investigation shows the agent followed a workspace instruction, responders need to know whether that text changed the plan, the tool arguments or the final state.

Watch for releases from new workflows, changes to trusted-publisher configuration, unexpected id-token: write grants, runner processes reading OIDC environment variables, deleted workflow logs and newly created hidden IDE configuration. Each signal is weak alone. Together they can expose the path from developer compromise to signed release.

Keep the evidence and the limits together

GTIG’s report is threat intelligence, not a complete forensic record available for independent reproduction. It names observed techniques and affected ecosystems, but organisations should not convert every detail into a prevalence estimate. The report also says Google took action against associated assets and used the findings to improve model and classifier defences.

The safer conclusion is narrower. Real attackers are shaping CI identity, package provenance, AI workspace configuration and LLM scanner behaviour as one connected system. Controls that evaluate those surfaces independently will miss the composed path.

SLSA remains valuable. Trusted publishing remains valuable. AI-assisted review remains valuable. Their output becomes dependable only when a policy knows the claim each control supports and refuses to infer the rest. The question to ask of any green check is not “did it pass?” It is “which of the four claims does this check actually support, and which three did it leave open?”

Frequently asked questions

Can a package with valid SLSA provenance contain malware?

Yes. SLSA provenance describes how an artefact was produced and strengthens the integrity of that record. If an attacker controls the authorised source, workflow or publishing identity, the build can produce valid provenance for malicious content.

Does SLSA Build Level 3 certify that software is safe?

No. Build Level 3 strengthens the build platform and provenance guarantees: the provenance is unforgeable, the environment is isolated and ephemeral, and cache poisoning is resisted. It does not replace source review, vulnerability analysis, malware detection, runtime policy or a deployment-specific risk decision.

How did UNC6780 produce valid signatures without breaking the cryptography?

It did not break the cryptography. It compromised a release workflow or extracted an OIDC token from runner memory via process memory access, exchanged the token with Sigstore Fulcio for a legitimate signing certificate, and used that certificate to sign provenance for the malicious package. The signature was real; the authority behind it had been stolen.

What is OIDC trusted publishing?

It lets a CI workflow exchange a short-lived identity token for permission to publish a package, avoiding a long-lived registry key. The workflow, runner and trust conditions still need protection because a stolen token can be used during its validity window.

What should an AI coding agent verify before installing a package?

The agent’s surrounding policy should verify publisher identity, reviewed source, build provenance and acceptable behaviour, then bind approval to the exact version, digest and intended use. Package content must not be able to approve itself.

What is the first control to add?

Put package installation behind a fail-closed tool policy that verifies the immutable digest and expected publisher context. Then add source and behaviour evidence so the signature is never treated as the whole decision.

UNC6780’s useful warning is not that signatures failed. The signed evidence described a build produced through authority the attacker had stolen. Security teams need to decide whether that authority, source and behaviour belong together before an agent turns an artefact into running code.

For the related action-boundary method, read Prompt Injection Is a Search Problem. For the control-plane view, see Your AI Gateway Is a Privileged Control Plane.

One next action

If you run an agent that executes tools, AgentCrash is an open-source harness that crash-tests it in a synthetic workplace: synthetic tools, visual evidence and regression tests. It is built for exactly this problem, turning a containment claim into something you can run against the system instead of arguing about it in review.