Prefetch Technologies // Keeping your cache lines cozy

Layered defenses against software supply chain attacks

Software supply chain attacks target the dependencies, build systems, and publishing pipelines that produce the software we run, instead of the application itself. A single compromised package can reach millions of systems before anyone notices, and once it lands, it runs with the same trust and privileges as the rest of your code. The diagram below shows where these attacks typically land along the chain:

  source      →    build       →    registry      →    install       →    runtime
  ──────           ─────             ────────           ───────            ───────
  tampered         pipeline          account            malicious          stolen
  commits          compromise        takeover           package            credentials

Defending against this requires controls at every one of those stages.

Recent supply chain compromises

Two incidents from earlier this year show just how quickly a compromised pipeline can lead to large scale security incidents:

  • LiteLLM (March 2026): A compromised CI/CD pipeline let attackers ship malicious releases of litellm, a widely used AI infrastructure library, that harvested cloud credentials and API keys on install.
  • PyTorch Lightning (April 2026): A hijacked publisher account let attackers push malicious PyPI releases of lightning that stole developer tokens and spread the compromise into downstream repositories.

Both followed the same playbook: compromise a trusted publishing channel and run malicious code automatically on install. That's what makes supply chain attacks so dangerous. A routine pip install or npm install can become a nightmare for organizations blindly trusting the latest upstream packages.

The CNCF secure software factory model

The CNCF's TAG Security working group published a Secure Software Factory whitepaper that lays out a reference architecture for producing software with verifiable provenance at every stage. Instead of trusting a package because it showed up on PyPI or npm, the methodology asks you to verify how it got there: who committed the source, how it was built, what tests it passed, and whether the published artifact actually matches that source.

The core idea is that each stage of the factory (source control, build, packaging, and distribution) produces signed attestations that the next stage checks before proceeding. A break anywhere in that chain becomes visible right away, instead of surfacing months later when you show up on hacker news.

Defense levels

In practice I lean on five layers to protect my pipelines and help me identify questionable code:

  • Signing: Cosign signs most of the artifacts I publish, so consumers can verify they actually came from my pipeline and weren't tampered with in transit.
  • Scanning: Trivy scans every artifact for known vulnerabilities, and blocks releases if it detects any critical or high vulnerabilities.
  • Release age: Package tooling holds new releases back for a set number of days, giving the ecosystem time to catch a bad one, unless a scanner flags a critical fix that needs the newer version sooner.
  • Locking: Every dependency, transitive ones included, is pinned to an exact, reviewed version, and CI installs only from that pinned set rather than whatever happens to be newest that day.
  • LLM review: An LLM scans freshly pulled modules before the build step and fails the build on questionable or insecure code that pattern-based scanners miss.

Each layer covers a gap the others don't. Signing proves provenance, scanning catches known-bad code, the release-age window buys time for the ecosystem to catch what scanners haven't found yet, locking keeps builds reproducible, and LLM review catches the questionable or insecure code that none of the other layers are built to recognize.

The remainder of the article will show specific tools and processes I've incorporated into my daily workflow. Since the security landscape is constantly changing my tooling and processes are constantly evolving to handle current and future threats.

Signing artifacts with cosign

Cosign is part of the sigstore project. It signs and verifies container images, binaries, packages, etc., and it can attach attestations and SBOMs (Software Bills of Materials, an inventory of every component and dependency that went into an artifact) to them.

To sign an artifact with a key pair, you can first generate one with the cosign generate-key-pair option:

$ cosign generate-key-pair

This writes cosign.key (an encrypted private key) and cosign.pub (the matching public key) to disk. The following command signs the artifact with the private key and writes the signature to a local .sig file alongside it:

$ cosign sign-blob --key cosign.key myartifact.tar.gz --output-signature myartifact.tar.gz.sig

To check the artifact against the signature you can use the "verify-blog" option:

$ cosign verify-blob --key cosign.pub --signature myartifact.tar.gz.sig myartifact.tar.gz

The cosign verify-blob option exits non-zero if the artifact's signature doesn't match the public key. Cosign also has a "keyless" mode worth knowing about: instead of a key pair you generate and guard yourself, it ties the signature to an OIDC identity, such as your CI provider, so there's no private key to store, rotate, or leak in the first place. Of course the key hasn't vanished, you've just handed the job of guarding it to your CI or cloud provider.

Security scanning with trivy

Trivy is a fast vulnerability scanner that can check a variety of assets, from container images to filesystems and source repositories. I run it as a hard gate in my pipelines. Any critical or high findings fails the pipeline and generates a notification to let me know I have additional work to do.

Here's a sample trivy command line to scan a Python codebase and produce human readable output:

$ trivy fs \
    --scanners vuln \
    --skip-db-update \
    --ignore-unfixed \
    --severity HIGH,CRITICAL \
    --exit-code 1 \
    --format table \
    .

The --exit-code option is the key to failing pipelines when high or critical vulnerabilities are detected. Also notice the --skip-db-update flag. A build gate shouldn't depend on reaching the internet to refresh trivy's vulnerability database mid-build (unless that is something your organization requires). I refresh my database daily with a scheduled job that runs trivy with the --download-db-only option:

$ trivy image --download-db-only

Using uv to limit package installations based on date

uv is a fast Python package and project manager from Astral that handles dependency resolution, virtual environments, and installs, the work pip, pip-tools, and virtualenv used to split between them. It can also resolve dependencies as of a fixed point in time with --exclude-newer, ignoring any package version published after that timestamp. This is helpful because most malicious releases get caught and yanked within days of going out, so if your lockfile only ever resolves to packages that are a week or two old, you give the ecosystem time to catch the bad release before it ever reaches your build.

This sets the cutoff once, in an environment variable, so every uv command in the session honors it:

$ export UV_EXCLUDE_NEWER="2026-05-23T00:00:00Z"

This re-resolves the lockfile against that cutoff, swapping out anything published after it for the newest version available before it:

$ uv sync --exclude-newer "$UV_EXCLUDE_NEWER"

When a critical fix needs to ship immediately, you don't want to wait out that window. Override it for the one package that needs the newer release, rather than moving the whole project's cutoff forward:

$ uv add "requests==2.32.4" --exclude-newer "2026-06-06T00:00:00Z"

Using npm to limit packages based on date

npm is a package manager for Node.js, handling dependency installs, lockfiles, and publishing for the JavaScript ecosystem. The frontend takes the same approach as uv with npm's min-release-age setting, which refuses to install a package version until it has been published for at least the given number of days:

$ egrep '(strict|min)' myproject/.npmrc
engine-strict=true
min-release-age=10

engine-strict turns the engines field in package.json from a suggestion into a hard requirement: if the installed Node or npm version doesn't satisfy it, npm install fails instead of just printing a warning. It's not a supply chain control on its own, but it keeps installs from running on an unexpected runtime, which is one less variable when you're trying to reason about what a build actually did.

The same escape hatch applies here too. When a critical vulnerability needs an immediate patch, override the minimum age for that one install instead of relaxing it for the whole project:

$ npm install lodash@4.17.21 --min-release-age=0

Locking dependencies to known-good versions

I commit the lock files from uv (uv.lock) and npm (package-lock.json) to version control and install from them in CI, so every build resolves to the exact, reviewed versions I expect rather than whatever happens to be newest catch of the day. Both files pin every dependency, transitive ones included, to an exact version and hash, no matter how loose the ranges in pyproject.toml or package.json are.

On the Python side, uv sync --locked installs exactly what's recorded in uv.lock and refuses to proceed if pyproject.toml has drifted from it:

$ uv sync --locked

The npm equivalent is npm ci, which does a clean install straight from package-lock.json: it wipes the node_modules directory, installs the precise versions and hashes the lockfile records, and fails outright if package.json and the lockfile disagree. Worth noting, npm ci doesn't write or update the lockfile itself, that's the job of npm install.

I like that both commands fail rather than quietly re-resolving when the lockfile is out of date. A drifted dependency shows up as a build failure, not a future hacker news article.

Bumping a package on purpose is the mirror image. uv add and npm install resolve the new version, rewrite the lockfile to match, and produce a diff that shows exactly what changed, transitive dependencies included:

$ uv add "requests==2.32.4"
Resolved 6 packages in 6ms
Uninstalled 1 package in 0.86ms
Installed 1 package in 3ms
 - requests==2.34.1
 + requests==2.34.2
$ npm install express@5.2.1
changed 1 package, and audited 67 packages in 597ms

24 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

That's the same uv add and npm install from the emergency overrides earlier, just with extra flags tacked on. Even a rushed patch ends up going through the same reviewable, lockfile-updating path as a routine bump.

Using LLMs as a static analysis gate

I'm experimenting with another layer: running an LLM over pulled modules after install but before the build step. LLMs turn out to be remarkably good at static analysis, and with the right guardrails they can flag questionable or insecure code. The goal is if the LLM raises a concern about a freshly pulled dependency, the build stops before that code ever reaches a runtime. Then it notifies me so I can dig further and either whitelist the flagged software or spend time analyzing code to see what is questionable.

Defense in depth in the age of AI

AI has changed the security landscape in ways that make defense in depth more important than ever. Tooling now writes code, reviews code, and increasingly helps pick which packages to pull in, which means a compromised dependency can spread through a codebase faster and with less human review than it used to.

None of the controls above (provenance attestation, signed artifacts, vulnerability gates, locked dependencies, or release-age limits) stops every attack on its own. Layered together, they buy time, add friction, and create the kind of visibility that turns a silent compromise into a caught one.

Honestly, this post barely scratched the surface. We didn't get into identity policies, zero trust concepts, logging, etc. In my opinion AI is going to make this problem a whole lot worse before it gets better, the same tooling that helps us write and review code faster also lowers the bar for attackers to slip something past us.

I have a lot more to learn on this topic, and there are definitely more steps that can be taken to help protect your software supply chain. When I get more time I'm hoping to read Supply Chain Software Security: AI, IoT, and Application Security . I want to learn a TON more about this topic. Knowledge and adding as many defensive steps as possible will help me sleep better at night.

Resources: