Defensive Research

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.

A dark chain of cyan-lit package nodes with one corrupted red link, representing supply chain attack detection

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:

SurfaceWhat the attacker compromisesReal-world precedent
Direct dependencyA package you explicitly requireevent-stream, ua-parser-js
Transitive dependencyA dependency of a dependencyThe typical npm blast radius
Upstream source tarballThe released archive, not the git repoxz-utils (CVE-2024-3094)
Build systemThe compiler, build tool, or agentSolarWinds SUNBURST
CI/CD credentialsTokens with publish or deploy rightsCodecov bash uploader
Artifact registryThe stored, already-built packageRegistry account takeover
Update channelThe delivery mechanism itselfSigned-update abuse
Developer toolingIDE extensions, local toolchainsMalicious 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.

RequirementWhy it mattersCommon failure
Process execution on build agentsCatches install-time script executionAgents run without EDR
Network connection logs from build agentsThe single strongest signal availableEgress unmonitored and unrestricted
Package resolution logs with source registryDetects registry substitutionResolver output discarded
Lockfile diffs per buildDetects unpinned or drifted dependenciesLockfile not enforced in CI
Artifact publish events with source commitEnables provenance correlationPublish not tied to a commit SHA
Build agent image and lifetimeEphemeral agents limit persistenceLong-lived reusable agents
Signing key usage auditDetects out-of-band signingKey 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.

Sigma Unexpected Network Egress From a Build Agent
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.

SPL Dependency Resolved Outside the Lockfile or From a New Registry
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.

SPL Artifact Published Without a Matching Source Commit
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 positiveWhich rule it hitsWhy it happensResolution
New legitimate dependency sourceBuild egressTeam adds a registry, allowlist not updatedMake allowlist changes part of the dependency PR
Vendor licence callbacksBuild egressBuild tooling phones homeAllowlist the specific vendor endpoint, not the ASN
Container base image mirrorBuild egressPull from an unlisted mirrorPin and allowlist the mirror explicitly
Transitive version floatLockfile driftA range spec resolves higherPin transitives, enable lockfile enforcement
Legitimate emergency hotfix publishProvenance gapA human published under incident pressureKeep the alert, document the exception
Monorepo internal packagesRegistry substitutionInternal names resolve locallyScope internal namespaces explicitly
Build cache restoreProvenance gapCached artifact has no fresh commitRecord 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.

  1. Freeze the pipeline. Stop publishing before you investigate. Every additional build is a potential additional distribution of the compromise.
  2. Identify the first suspicious build. Establish the earliest build showing the anomaly; this bounds everything downstream.
  3. Enumerate every artifact built since that point. This is your blast radius, and it is usually larger than the initial estimate.
  4. Determine where those artifacts deployed. Including to customers, if you distribute software. Notification obligations may attach here.
  5. 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.
  6. Rotate every credential the pipeline could reach. Publish tokens, signing keys, cloud roles, and deploy credentials. Assume all were readable during the compromise window.
  7. Rebuild from a known-good commit on a fresh, isolated agent. Never reuse the suspect agent image.
  8. Compare the rebuilt artifact against the published one. A byte-level difference confirms build-time injection and is the strongest evidence you will get.
  9. 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:

  1. Add a benign build step that connects to an unlisted external host. Confirm the egress rule fires and identifies the build and commit.
  2. Publish an internal-named package to a private test registry at a higher version. Confirm REGISTRY_SUBSTITUTION fires before the resolver prefers it in a real build.
  3. Modify a lockfile entry out of band and run a build. Confirm VERSION_DRIFT fires.
  4. Publish an artifact manually with a human account. Confirm HUMAN_PUBLISH fires.
  5. 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.
  6. 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?

ControlStopsEffortReproducibility gainDeploy when
Lockfile enforcement in CISilent version driftLowHighAlways, first
Build agent egress monitoringBuild-time exfiltrationLowNoneAlways, second
Ephemeral build agentsPersistence on agentsMediumMediumAlways
Scoped internal namespacesDependency confusionLowNoneYou have private packages
Hermetic network-isolated buildsMost build-time executionHighVery highYou control the build tool
Artifact signing + provenanceRegistry tamperingMediumHighYou distribute software
Vendored critical dependenciesUpstream takeoverHighHighA 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

  1. Deploy process and network monitoring on every build agent, treated as a high-value endpoint.
  2. Build a thirty-day baseline of build-agent egress and alert on anything outside it.
  3. Enforce lockfiles in CI so a build fails rather than silently resolving something new.
  4. Pin transitive dependencies, not only direct ones.
  5. Log package resolution including the source registry for every dependency.
  6. Alert on internal package names resolving from public registries.
  7. Scope internal namespaces so public resolution is impossible by construction.
  8. Tie every artifact publish to a source commit, a pipeline run, and a build agent.
  9. Alert on any publish performed by a human identity.
  10. Make build agents ephemeral and single-use.
  11. Separate the build identity from the publish identity.
  12. Move to hermetic network-isolated builds for anything customer-facing.
  13. Generate signed provenance attestations and verify them at deploy time.
  14. Test build reproducibility regularly, before you need it in an incident.
  15. 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 Training
    Start training
  • PluralsightDevSecOps and secure build pipeline training pathsSecurity Training
    Browse 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.