Supply Chain Attack Detection
Supply chain attack detection in CI/CD. Build-time egress, dependency drift, and artifact provenance, with Sigma and SPL analytics plus hermetic-build hardening.
Supply chain attack detection is difficult for one structural reason: the malicious code arrives through a trusted path, and you extend near-total trust to your own build output. No perimeter is crossed, no exploit fires, and no anomaly appears at runtime, because from the system’s point of view nothing abnormal happened. Your pipeline compiled a dependency and shipped it, exactly as designed. This guide moves detection to where the compromise is actually visible, which is the build boundary rather than the dependency list.
Supply chain compromise maps to MITRE ATT&CK T1195.001 — Compromise Software Dependencies and Development Tools and T1195.002 — Compromise Software Supply Chain. It is the technique class where prevention and detection diverge most sharply, and where the usual tooling answer is the weakest.
What is a supply chain attack?
A supply chain attack compromises software before it reaches you. Rather than attacking your running systems, the adversary poisons something upstream that you already trust: a dependency, a build tool, an artifact registry, a signing key, or an update channel. The malicious code then travels through your own pipeline and inherits your own trust.
That inheritance is the entire problem. Your production environment does not treat your build output as untrusted input. It executes it with full privilege, because it is your software. Every control designed to keep bad code out of production is oriented toward external input, and this code arrives as internal output.
The attack surface is wider than most teams enumerate:
| Surface | What the attacker compromises | Real-world precedent |
|---|---|---|
| Direct dependency | A package you explicitly require | event-stream, ua-parser-js |
| Transitive dependency | A dependency of a dependency | The typical npm blast radius |
| Upstream source tarball | The released archive, not the git repo | xz-utils (CVE-2024-3094) |
| Build system | The compiler, build tool, or agent | SolarWinds SUNBURST |
| CI/CD credentials | Tokens with publish or deploy rights | Codecov bash uploader |
| Artifact registry | The stored, already-built package | Registry account takeover |
| Update channel | The delivery mechanism itself | Signed-update abuse |
| Developer tooling | IDE extensions, local toolchains | Malicious extension packages |
Two rows deserve emphasis because they defeat the most common assumptions. The upstream source tarball row breaks “we reviewed the repository,” since the released archive need not match the repository. And the CI/CD credentials row breaks “we review all code,” since an attacker holding a publish token does not need to touch your source at all.
What the xz-utils backdoor teaches about detection
CVE-2024-3094 is the clearest case study available, and it is instructive precisely because every conventional control failed.
The compromise reached xz-utils versions 5.6.0 and 5.6.1 through a maintainer who had spent roughly two years building legitimate contribution history before acting. The malicious payload was not in the git repository. It was embedded in the release tarballs as a disguised test fixture, extracted during the build process by modified build scripts, and linked into liblzma. On distributions where systemd links liblzma into sshd, the result was a backdoor in remote authentication on a CVSS 10.0 rating.
Consider which controls that defeats:
- Repository review failed, because the payload was not in the repository.
- Dependency scanning failed, because the package was not known-bad and the version was the current official release.
- Signature verification failed, because the releases were signed correctly by the legitimate maintainer.
- Code review failed, because the malicious step lived in build scripting and an opaque binary test fixture.
- Reputation and maintainer trust failed, because the trust was genuine and patiently earned.
What actually found it was a performance anomaly. An engineer investigating roughly half a second of unexplained latency in SSH logins, plus unusual CPU consumption by sshd, followed the oddity to its source. Detection came from someone noticing that a system behaved differently than it should, not from a security tool reporting a finding.
What telemetry do you need?
Build-boundary detection requires telemetry most organisations do not collect, because build agents are typically treated as infrastructure rather than as endpoints worth monitoring.
| Requirement | Why it matters | Common failure |
|---|---|---|
| Process execution on build agents | Catches install-time script execution | Agents run without EDR |
| Network connection logs from build agents | The single strongest signal available | Egress unmonitored and unrestricted |
| Package resolution logs with source registry | Detects registry substitution | Resolver output discarded |
| Lockfile diffs per build | Detects unpinned or drifted dependencies | Lockfile not enforced in CI |
| Artifact publish events with source commit | Enables provenance correlation | Publish not tied to a commit SHA |
| Build agent image and lifetime | Ephemeral agents limit persistence | Long-lived reusable agents |
| Signing key usage audit | Detects out-of-band signing | Key usage not logged |
The first two are the highest-value items on this list and the most commonly missing. A build agent is an endpoint that executes arbitrary third-party code by design, which arguably makes it the most interesting endpoint you own. Most estates monitor developer laptops far more closely than the machine that compiles and signs production artifacts.
How to detect supply chain attacks
Three analytics covering the three moments where a compromise becomes observable: during build, during dependency resolution, and at publish.
Unexpected network egress from a build agent
A build agent has a small, stable, enumerable set of destinations: your source host, your package registries, and your artifact store. Anything else is worth an alert, and this is the detection that would have caught most historical CI/CD compromises. Where the destination is unknown rather than merely unapproved, the interval-regularity scoring in C2 beaconing detection tells you whether the agent is exfiltrating once or has been enrolled into an implant’s callback schedule.
title: Unexpected Network Egress From a Build Agent
id: 3b7e2c94-6a15-4f83-b0d2-9e4c1a7f5b8e
status: experimental
description: >
Detects a CI/CD build agent making a network connection to a destination outside
the approved set of source hosts, package registries, and artifact stores.
Build-time exfiltration and second-stage payload retrieval both surface here.
references:
- https://attack.mitre.org/techniques/T1195/002/
- https://nvd.nist.gov/vuln/detail/CVE-2024-3094
author: Colson
date: 2026/07/28
logsource:
category: network_connection
product: linux
detection:
build_agent:
Image|startswith:
- '/opt/ci/'
- '/usr/local/bin/build'
ParentImage|contains:
- 'runner'
- 'buildkitd'
approved_destinations:
DestinationHostname|endswith:
- 'registry.npmjs.org'
- 'pypi.org'
- 'proxy.golang.org'
- 'repo.maven.apache.org'
- 'crates.io'
- 'github.com'
internal_ranges:
DestinationIp|cidr:
- '10.0.0.0/8'
- '172.16.0.0/12'
- '192.168.0.0/16'
condition: build_agent and not (approved_destinations or internal_ranges)
fields:
- Image
- CommandLine
- DestinationHostname
- DestinationIp
- DestinationPort
- User
falsepositives:
- A new legitimate dependency source added without updating the allowlist
- Vendor telemetry or licence-check callbacks from build tooling
- Container base image pulls from an unlisted mirror
level: high Populate the allowlist from your own thirty-day baseline rather than from this template, because the correct list is specific to your stack. The rule’s value comes from the allowlist being genuinely complete and genuinely small, which is achievable for a build agent in a way it never is for a workstation.
Dependency resolved outside the lockfile
A lockfile is a statement about exactly which versions, from exactly which registries, should be installed. When resolution departs from that statement, something has changed that nobody declared.
index=cicd sourcetype=build:dependency_resolution
| eval registry_host = lower(replace(resolved_url, "^https?://([^/]+)/.*", "\1"))
| lookup approved_registries host AS registry_host OUTPUT approved
| lookup lockfile_expected package AS package_name version AS resolved_version
OUTPUT expected_version, expected_registry
| eval anomaly = case(
isnull(approved), "UNAPPROVED_REGISTRY",
isnull(expected_version), "NOT_IN_LOCKFILE",
resolved_version != expected_version, "VERSION_DRIFT",
registry_host != expected_registry, "REGISTRY_SUBSTITUTION",
1==1, null())
| where isnotnull(anomaly)
| stats count AS occurrences,
values(resolved_version) AS versions,
values(registry_host) AS registries,
values(build_id) AS builds
by repo, package_name, anomaly
| sort - occurrences REGISTRY_SUBSTITUTION is the dependency-confusion case and deserves its own alert route. An
internal package name that suddenly resolves from a public registry means an attacker published
a higher version publicly and your resolver preferred it. NOT_IN_LOCKFILE frequently indicates
a build that is not actually reproducible, which is a supply chain weakness independent of any
active attack.
Artifact published without a matching source commit
The last observable moment is publish. Every artifact in your registry should trace to a commit, a build, and a pipeline run. One that does not was built somewhere you are not watching.
index=cicd sourcetype=artifact:publish
| join type=left build_id
[ search index=cicd sourcetype=build:complete
| fields build_id, source_commit, pipeline_id, agent_id ]
| eval provenance_gap = case(
isnull(source_commit), "NO_SOURCE_COMMIT",
isnull(pipeline_id), "NO_PIPELINE_RECORD",
publish_actor != "ci-service-account", "HUMAN_PUBLISH",
1==1, null())
| where isnotnull(provenance_gap)
| table _time, artifact_name, artifact_version, publish_actor,
source_commit, pipeline_id, agent_id, provenance_gap
| sort - _time HUMAN_PUBLISH should be near-zero in a mature pipeline and is worth alerting on regardless of
intent, because a human with publish rights is a stolen credential away from being an attacker
with publish rights. This is the analytic that would surface a compromised publish token being
used outside CI entirely.
Which false positives will you actually see?
| False positive | Which rule it hits | Why it happens | Resolution |
|---|---|---|---|
| New legitimate dependency source | Build egress | Team adds a registry, allowlist not updated | Make allowlist changes part of the dependency PR |
| Vendor licence callbacks | Build egress | Build tooling phones home | Allowlist the specific vendor endpoint, not the ASN |
| Container base image mirror | Build egress | Pull from an unlisted mirror | Pin and allowlist the mirror explicitly |
| Transitive version float | Lockfile drift | A range spec resolves higher | Pin transitives, enable lockfile enforcement |
| Legitimate emergency hotfix publish | Provenance gap | A human published under incident pressure | Keep the alert, document the exception |
| Monorepo internal packages | Registry substitution | Internal names resolve locally | Scope internal namespaces explicitly |
| Build cache restore | Provenance gap | Cached artifact has no fresh commit | Record the originating commit in cache metadata |
The lockfile-drift row is the one that reveals the most about an organisation. Persistent version float means builds are not reproducible, and non-reproducible builds make every other control on this list weaker, because you cannot prove what you shipped.
How do you triage a suspected supply chain compromise?
Blast radius here is larger than in most incident classes, because the compromise propagates through everything the pipeline built. Work in this order.
- Freeze the pipeline. Stop publishing before you investigate. Every additional build is a potential additional distribution of the compromise.
- Identify the first suspicious build. Establish the earliest build showing the anomaly; this bounds everything downstream.
- Enumerate every artifact built since that point. This is your blast radius, and it is usually larger than the initial estimate.
- Determine where those artifacts deployed. Including to customers, if you distribute software. Notification obligations may attach here.
- Pull the build agent’s full process and network history. Establish what executed and where it connected. Ephemeral agents make this a race against instance teardown.
- Rotate every credential the pipeline could reach. Publish tokens, signing keys, cloud roles, and deploy credentials. Assume all were readable during the compromise window.
- Rebuild from a known-good commit on a fresh, isolated agent. Never reuse the suspect agent image.
- Compare the rebuilt artifact against the published one. A byte-level difference confirms build-time injection and is the strongest evidence you will get.
- Preserve the malicious artifact for analysis before purging registries, and write YARA rules from it to sweep for other copies.
Step 8 is the one worth designing for in advance. If your builds are reproducible, this comparison is decisive and takes minutes. If they are not, you will spend days arguing about whether a difference is meaningful.
How to test your supply chain detection
In a lab pipeline you own, with no production credentials in scope:
- Add a benign build step that connects to an unlisted external host. Confirm the egress rule fires and identifies the build and commit.
- Publish an internal-named package to a private test registry at a higher version. Confirm
REGISTRY_SUBSTITUTIONfires before the resolver prefers it in a real build. - Modify a lockfile entry out of band and run a build. Confirm
VERSION_DRIFTfires. - Publish an artifact manually with a human account. Confirm
HUMAN_PUBLISHfires. - Run two builds from the same commit and diff the artifacts. If they differ, your builds are not reproducible and step 8 of the runbook will not work when you need it.
- Confirm the egress rule still fires when the build runs inside a container, which is where host-level network monitoring most often loses visibility.
Item 5 is a test of your architecture rather than your detection, and it is the most valuable one on the list.
How to prevent supply chain attacks
Beyond the build environment, five controls carry the most weight:
- Pin everything, including transitives. A version range is a standing invitation for an upstream account takeover to reach you automatically.
- Scope internal namespaces. Dependency confusion works only where an internal name can resolve publicly. Scoped namespaces plus explicit registry pinning close it entirely.
- Generate and verify provenance. Sign artifacts and verify at deploy time that the artifact came from the expected commit, built by the expected pipeline. SLSA formalises this.
- Separate build identity from publish identity. A compromised build agent should not hold publish rights, and publish should require a distinct credential.
- Vendor and mirror critical dependencies. For dependencies whose compromise would be catastrophic, hold your own reviewed copy rather than resolving upstream on every build.
Which supply chain control should you deploy first?
| Control | Stops | Effort | Reproducibility gain | Deploy when |
|---|---|---|---|---|
| Lockfile enforcement in CI | Silent version drift | Low | High | Always, first |
| Build agent egress monitoring | Build-time exfiltration | Low | None | Always, second |
| Ephemeral build agents | Persistence on agents | Medium | Medium | Always |
| Scoped internal namespaces | Dependency confusion | Low | None | You have private packages |
| Hermetic network-isolated builds | Most build-time execution | High | Very high | You control the build tool |
| Artifact signing + provenance | Registry tampering | Medium | High | You distribute software |
| Vendored critical dependencies | Upstream takeover | High | High | A dependency is load-bearing |
Lockfile enforcement and egress monitoring together are cheap, fast, and cover a disproportionate share of realistic attack paths. Hermetic builds are the endgame and are worth the effort for anything whose compromise would reach customers.
Common supply chain detection mistakes
- Treating dependency scanning as detection. It finds known-bad, and novel compromise is by definition not yet known.
- Leaving build agents unmonitored. They execute arbitrary third-party code and often hold the most valuable credentials you have.
- Reviewing the repository and trusting the tarball. The xz backdoor lived only in the release archive.
- Allowing unrestricted build egress. This is the single control that would have blocked most historical CI/CD compromises.
- Using long-lived reusable build agents. They let a one-build compromise become permanent.
- Giving build identity publish rights. One compromise then yields both capability sets.
- Ignoring transitive pinning. Direct dependencies are the minority of your real tree.
- Never testing reproducibility. You discover it does not work during the incident.
Supply chain attack detection checklist
- Deploy process and network monitoring on every build agent, treated as a high-value endpoint.
- Build a thirty-day baseline of build-agent egress and alert on anything outside it.
- Enforce lockfiles in CI so a build fails rather than silently resolving something new.
- Pin transitive dependencies, not only direct ones.
- Log package resolution including the source registry for every dependency.
- Alert on internal package names resolving from public registries.
- Scope internal namespaces so public resolution is impossible by construction.
- Tie every artifact publish to a source commit, a pipeline run, and a build agent.
- Alert on any publish performed by a human identity.
- Make build agents ephemeral and single-use.
- Separate the build identity from the publish identity.
- Move to hermetic network-isolated builds for anything customer-facing.
- Generate signed provenance attestations and verify them at deploy time.
- Test build reproducibility regularly, before you need it in an incident.
- Rehearse the freeze-and-rebuild runbook, especially the artifact comparison at step 8.
The takeaway
Supply chain attack detection fails when it is aimed at the dependency list and works when it is aimed at the build boundary. Watch what your pipeline does: where a build agent connects, which registry a dependency actually came from, and whether a published artifact traces to a real commit. Then remove the attack paths structurally with hermetic builds, full transitive pinning, ephemeral agents, and verified provenance. Continue with secrets detection, Kubernetes security events to prioritize, and YARA rules for incident response, or browse the full Defensive Research pillar.
Training & tools referenced
Disclosure: Some links below are affiliate links. If you buy through them, darkpwn may earn a commission at no extra cost to you. We only recommend training and tools we actually use in our own lab, and affiliate links never influence editorial coverage.
- TryHackMeAuthorized labs to practice CI/CD and supply chain security analysisSecurity TrainingStart training
- PluralsightDevSecOps and secure build pipeline training pathsSecurity TrainingBrowse courses
Frequently asked questions
What is a supply chain attack?
A supply chain attack compromises software before it reaches you, by poisoning a dependency, a build system, an artifact registry, or an update channel rather than attacking your running systems directly. The malicious code arrives through a trusted path and inherits whatever trust you extend to your own build output, which is usually total.
How do you detect supply chain attacks?
Detect them at the build boundary rather than in the dependency list. The strongest signals are unexpected network egress from a build agent, a dependency resolved outside the lockfile or from an unexpected registry, and a published artifact with no matching source commit. These catch compromise regardless of whether the malicious package is already known.
Why is dependency scanning not enough?
Scanners compare your dependencies against databases of known-bad packages, so they are structurally blind to anything not yet published as malicious. The xz-utils backdoor sat in official release tarballs for weeks with a clean scanning record. Scanning is a necessary hygiene control and a poor detection control, because it only ever finds what is already public.
What is SLSA and does it help?
SLSA (Supply-chain Levels for Software Artifacts) is an OpenSSF framework defining build integrity levels. It helps substantially because its requirements — scripted builds, provenance generation, and hardened isolated build platforms — attack the exact gap that dependency scanning cannot cover, namely whether the artifact you shipped actually came from the source you think it did.
What is dependency confusion?
Dependency confusion exploits resolvers that check public registries alongside private ones. An attacker publishes a package to a public registry using the same name as your internal package with a higher version number, and the resolver prefers the public copy. The fix is scoped namespaces plus explicit registry pinning so internal names never resolve publicly.