Start by Deleting Everything You Didn't Write
Take a production SaaS application — one that serves real customers, processes payments, stores records, and gets deployed several times a week. Now run a deletion pass over it. Not a refactor. A removal.
Delete the operating system. Delete the language runtime. Delete the web framework and the frontend framework. Delete every package the team declared as a dependency, and then every package those packages pulled in behind them. Delete the container base image and every system library inside it. Delete the database engine. Delete the CI runner and every workflow action it invokes. Delete the managed queue, the object store, the load balancer, the certificate manager. Delete the monitoring agent, the authentication SDK, the payment SDK, the analytics library, the feature-flag client. Delete the compiler, the bundler, the package manager, the linter that rewrites code on commit. Delete the cryptography library. Delete any model weights or inference libraries. Delete the browser the customer uses to reach the thing.
What is left?
Something real is left, and it is the most valuable part: the rules that describe how this particular business works. Pricing logic. Entitlement checks. The state machine that governs an order, a claim, a shipment, a lesson plan. The specific way this product decides what a user is allowed to see. The workflows nobody else has, because nobody else has this company's customers.
But that residue does not run. It is a set of instructions written in a language whose interpreter you just deleted, calling functions you just deleted, listening on a socket managed by a kernel you just deleted, reading from a database you just deleted. It is a schematic without a machine.
This is not a criticism of anyone's engineering. It is the shape of the trade the industry made decades ago and has been compounding ever since. A team of twenty can ship software that would have required a thousand people in 1995 because almost none of the machine has to be built. It is assembled. That assembly — the whole path from a maintainer's commit somewhere on the internet to a running process in your production account — is the software supply chain.
Resist the urge to put a number on the ratio. Percentages get quoted for years after the study behind them stopped being true, and they invite the wrong conclusion anyway. The useful observation is structural, not statistical: a software company writes the logic that differentiates its product while relying on a large and mostly unexamined population of components, services, tools, and maintainers to make that logic executable, deployable, and reachable.
Leverage of that magnitude is a genuine achievement. But leverage creates dependency, dependency creates trust relationships, and trust relationships are invisible right up until the moment something changes on the other end.
Which leads to the question this article is built around: how much of your software do you actually control?
Ownership, Control, Trust, and Visibility Are Four Different Things
Founders and engineering leaders tend to collapse these into one idea — "our codebase" — and the collapse is where most supply-chain surprises live. Pulling them apart is the single most useful conceptual move available.
Ownership is a legal and organizational fact. You own the repository. You hold the copyright on what your team wrote. You can sell it, license it, and put it in a data room.
Control is an operational fact. Control means you can change the thing, on your own schedule, without asking anyone. You control your application code. You almost certainly do not control the version of OpenSSL inside your base image, the resolution algorithm your package manager uses, the availability of the registry you fetch from, or whether an upstream maintainer decides next month to rewrite the library's error semantics.
Trust is what you extend when you cannot control something but need it to behave correctly anyway. Every import, every FROM, every uses: in a workflow file, every SDK initialization is a trust statement. Most of them are made once, in a pull request, by one engineer, on a Tuesday, and never revisited.
Visibility is whether you can currently see the thing at all. Visibility is the precondition for the other three being manageable. You cannot govern a component you cannot enumerate, and you cannot enumerate a component you don't know entered the build.
A company can own its application code completely and still not control the package registry, the upstream maintainer, the container image, the compiler, the CI action, the identity provider, a third-party SDK, a hosted model endpoint, a managed database, a payment processor, or a transitive package five levels below anything a human on the team has read.
The customer experiences none of this separation. To the customer there is one product, and it either works or it doesn't. When a dependency five levels deep changes how it parses a timestamp and an invoice comes out wrong, the customer does not file a bug against the upstream library. They file it against you.
So the quality and security question for modern software is not only what did our developers write? It is:
What had to be trusted for this build to become production software?
Everything that follows is an attempt to answer that question layer by layer — first by taking a product apart, then by looking at what holds the pieces together, and finally by proposing frameworks for deciding which trust relationships deserve active management.
The Teardown: Ten Layers of a Running Product
What follows is an architecture specimen rather than a company. Assume a mid-sized B2B SaaS product: a browser client, an API, background workers, a relational database, some object storage, deployed as containers into a cloud account through an automated pipeline. The specific technologies matter less than the layering, which looks broadly similar whether the stack is TypeScript, Java, Python, Go, .NET, Ruby, or some combination.
Layer 1 — Business code
The code the company intentionally created because the market rewards it: domain rules, pricing and entitlement logic, proprietary matching or scoring algorithms, the specific interaction design of the interface, internal services that exist only because this business exists. This is the layer where competitive differentiation actually lives, and in most products it is a minority of the total lines that execute in production.
It is also the layer with the highest visibility. Every line was reviewed by someone. Every change went through a pull request. There is an owner, a test suite, and an on-call rotation.
Layer 2 — Application frameworks
The frontend framework and its rendering model. The backend framework, its routing and middleware conventions, its serialization behavior. The ORM. The HTTP client. The authentication and session libraries. Frameworks are chosen deliberately and everyone knows they're there — but their internals are rarely read, and their upgrade paths often become multi-quarter projects because so much application behavior is expressed in framework idioms.
Layer 3 — Direct packages
Everything explicitly declared in the manifest: package.json, pyproject.toml, pom.xml, go.mod, Gemfile, .csproj, Cargo.toml. Someone on the team typed each of these names. There was, at minimum, a moment of intent. In a healthy repository this list is reviewable in an afternoon.
Layer 4 — Transitive packages
Everything the direct packages required, plus everything those required, recursively. Nobody typed these names. They arrived because someone else's engineering judgment decided they were necessary. In many ecosystems this layer is an order of magnitude larger than Layer 3, and it is where the gap between ownership and visibility becomes widest.
Layer 5 — Runtime
The Node.js runtime, the JVM, the CPython interpreter, the .NET runtime, the Go runtime compiled into the binary. Each carries its own standard library, TLS behavior, security release cadence, and end-of-support date. Runtime versions are frequently pinned once at project creation and then inherited silently for years by every new service.
Layer 6 — Build toolchain
Compilers and transpilers. Bundlers and minifiers. The package manager itself, which is not a downloader but a resolution engine making semantic decisions about which versions co-exist. Build plugins. Code generators that turn schemas into clients.
Anything that transforms source is part of the product's causal history even though it doesn't ship — or rather, usually doesn't. Bundlers inline dependency code into the artifact; generators emit source that gets committed. The line between "build tool" and "shipped component" is blurrier than most inventories assume.
Layer 7 — Container and operating system
The base image named in a single FROM line, and everything inside it: a userland, a package manager, system libraries, certificate stores, shells, sometimes utilities nobody knew were there. A container image is a filesystem, and filesystems accumulate.
Layer 8 — CI/CD
The runner and its host image. The workflow definitions. Third-party actions and plugins invoked by name and version. Deployment tooling. Test infrastructure. Artifact publishing steps. Secret injection. This is the layer with the least application code and, frequently, the most privilege.
Layer 9 — Infrastructure
Managed databases, caches, queues, object storage, DNS, load balancing, secrets management, identity policy — plus the code that describes all of it: Terraform modules, Helm charts, Kubernetes operators, deployment manifests. Infrastructure-as-code is source code with production-shaped privileges.
Layer 10 — External capabilities
Authentication providers, payment processors, email and SMS delivery, error tracking, analytics, observability agents, model inference endpoints, feature management, support widgets loaded into the customer's browser. Some run in your process, some in your customers' browsers, some in someone else's data center. All are part of what the customer experiences as the product.
The software ownership stack
Laid out vertically, with the question "who is actually in charge here?" applied at each level:
LAYER TYPICALLY AUTHORED BY YOU CAN PATCH IT?
┌──────────────────────────────────────────────────────────────────────────────┐
│ 10 External capabilities │ vendors │ no │
│ 9 Infrastructure + IaC │ cloud + module authors │ config only │
│ 8 CI/CD + actions │ platform + action authors │ pin / fork │
│ 7 Container + OS packages │ distro maintainers │ rebuild image │
│ 6 Build toolchain │ tool maintainers │ version only │
│ 5 Runtime │ runtime stewards │ version only │
│ 4 Transitive packages │ strangers, mostly │ fork / override │
│ 3 Direct packages │ chosen maintainers │ fork / override │
│ 2 Frameworks │ framework communities │ rarely, in prac.│
│ 1 Business code │ you │ yes │
└──────────────────────────────────────────────────────────────────────────────┘
▲ deletion pass in the opening removed everything above line 1
The table below expands this, and deliberately refuses to give binary answers. "Can you patch it?" is almost never yes or no — it is yes, at what cost, and how long can you sustain it?
| Layer | Example | Who created it? | Who maintains it? | Can you patch it? | Can you replace it? | How visible is it to the team? |
|---|---|---|---|---|---|---|
| 1. Business code | Entitlement engine | Your team | Your team | Yes | N/A — it is the product | Fully; owned, reviewed, tested |
| 2. Frameworks | Web framework, ORM | External community | Community + sponsors | You can fork, but forking a framework is a standing tax | Yes, at the cost of a rewrite of large parts of Layer 1 | High awareness, low internal knowledge of internals |
| 3. Direct packages | HTTP client, validation library | Chosen maintainers | Varies from a foundation to one person | Fork or apply an override; sustainable for small patches | Usually yes; effort scales with API surface used | Visible in the manifest; rarely re-reviewed after adoption |
| 4. Transitive packages | Parsers, polyfills, utilities | Unknown to you at adoption time | Unknown to you at adoption time | Only via overrides, resolutions, or vendoring | Only by changing or removing the parent | Low by default; requires deliberate tooling |
| 5. Runtime | Node.js, JVM, CPython, .NET | Runtime stewards | Vendor or foundation | Not realistically; you move versions | Yes, but it is a platform migration | Known but often treated as environmental, not as a dependency |
| 6. Build toolchain | Compiler, bundler, package manager | Tool maintainers | Tool maintainers | Version changes and configuration | Yes; usually invasive | Low — invisible while it works |
| 7. Container / OS | Base image, system libraries | Distro and image maintainers | Distro maintainers | Yes: rebuild with updated packages | Yes: change base image | Very low; one line in a Dockerfile hides thousands of files |
| 8. CI/CD | Runner, third-party actions | Platform + action authors | Platform + individual authors | Pin to a digest, or fork the action | Yes, usually cheaply | Low; workflow files are rarely reviewed like application code |
| 9. Infrastructure | Managed database, Terraform module | Cloud provider, module authors | Provider, module authors | No for the service; yes for the module | Expensive; data gravity is real | Moderate; visible in cost reports, less so in dependency reports |
| 10. External capabilities | Auth provider, payment SDK, model API | Vendor | Vendor | No | Contractually and technically expensive | High at the business level, low at the component level |
Two patterns fall out of this table immediately. First, visibility does not correlate with risk — Layers 4, 6, 7, and 8 combine low visibility with meaningful privilege. Second, replaceability is not a property of a component in isolation; it is a property of how deeply your Layer 1 has grown around it.
Four Kinds of Code, and One Kind of Non-Code
"Our code" is a phrase that hides at least five distinct relationships, each with different maintenance economics.
Authored code is written by the product team on purpose. You own it, you control it, you are responsible for its defects, and its quality is a direct function of your engineering practice.
Adopted code is external code the team deliberately selected: the framework, the direct dependencies, the SDK. Someone evaluated it. You control whether you use it and which version, but not what it does or where it goes next.
Inherited code arrived because adopted code required it. No one evaluated it. It executes with the same privileges as everything else in the process. It is where most of the population of a modern dependency graph lives.
Generated code is produced by machinery: protobuf and OpenAPI clients, ORM migrations, GraphQL typings, compiled output, scaffolding, and — increasingly — code proposed by AI assistants. It is not authored, because no human composed it. It is not adopted, because it did not come from a registry. Its governance question is what generated it, from what input, and who reviewed the result?
Hosted capability is functionality delivered through infrastructure or a network service rather than local source: the managed database's query planner, the identity provider's token issuance, the model behind the inference endpoint. There is no file to read. Change arrives without a version bump in any manifest you own.
| Category | How it enters | Who decided | Who fixes a defect | What changes without your action | Primary governance question |
|---|---|---|---|---|---|
| Authored | Written | Your team | You | Nothing | Is it correct, tested, and owned? |
| Adopted | Declared in a manifest | Your team, once | Upstream, then you pull the fix | New releases appear; you decide when | Do we still want this, and are we on a supported line? |
| Inherited | Pulled in by a parent | Someone else's team | Upstream of upstream | Resolution can change it under you | Do we know it's there, and what does it touch? |
| Generated | Emitted by a tool or model | Your team ran the generator | You, or the generator's maintainers | Regenerating produces different output | What produced it, and did a human verify it? |
| Hosted | Configured and called | Your team, contractually | The provider | Behavior, limits, deprecations, pricing | Can we observe it, and what happens if it degrades? |
A crucial clarification before going further: external code is not inferior code. A mature, widely reviewed cryptography library maintained by people who specialize in cryptography is almost always a better choice than an internal implementation written by a generalist under deadline. The same is true of TLS stacks, compression algorithms, date/time handling, and database engines. Rewriting these internally usually produces worse security, not better.
The question is never "did we write it?" The question is: do we understand what we depend on, and what happens when it changes?
The Dependencies Nobody Chose
Add one package to a project. Read what happens next.
YOUR APPLICATION
│
▼
PACKAGE A ← the only decision a human made
╱ ╲
▼ ▼
B C
│ ╱ ╲
▼ ▼ ▼
D E F
│
▼
G
The engineering team chose A. It read A's README, checked its release notes, maybe skimmed its issue tracker. It never consciously selected B, C, D, E, F, or G. Yet in most ecosystems all seven end up installed, and several of them end up inside the production artifact, executing in the same process, with the same filesystem access, the same network access, and the same environment variables — including the ones holding credentials.
This is the transitive dependency problem, and it has several distinct mechanics worth separating.
Nesting. Depth is not bounded by anything except upstream taste. A package four levels down may be a two-function utility whose author considered it finished in 2019 and moved on. Depth also decouples change from intent: G can ship a new release that reaches your build without any of A, B, or C being updated, depending on how versions are expressed.
Sharing. Multiple branches of the tree frequently require the same package. Ecosystems handle this differently — some hoist a single compatible version to be shared, some install multiple copies at different points in the tree, some refuse to proceed until a single version satisfies everyone. Each strategy trades disk and duplication against the probability of a version conflict surfacing at build time rather than at runtime. The practical consequence is the same everywhere: one popular low-level package can be present in a graph dozens of times over, reached through paths nobody has mapped.
Resolution. The package manager is not fetching a list. It is solving a constraint problem, and the solution depends on the constraints, the algorithm, the order of operations, and the state of the registry at the moment the resolution runs. Two developers running the same install command on the same manifest a week apart can legitimately end up with different trees. That is not a bug; it is the declared semantics of version ranges.
Lockfiles exist to convert a constraint problem into a fact. A lockfile records the resolution that was actually chosen — exact versions, and usually integrity hashes for the fetched contents. Committed and enforced, it makes the tree reproducible across machines and across time. Not committed, or bypassed by an install command that re-resolves, and the tree becomes a function of when the build ran.
The trust multiplier
A useful piece of shorthand — used here as an article-specific concept, not a formal metric — is the trust multiplier: one deliberate dependency decision creates an unknown number of indirect trust relationships. When a team adds a package, it is not extending trust to one maintainer. It is extending trust to that maintainer and to everyone that maintainer trusts, transitively, plus the registries those packages are published through, plus the accounts authorized to publish them.
There is no number attached to this. Inventing one would be worse than useless, because the multiplier varies enormously by ecosystem, by package, and by whether the dependency is needed at build time or run time. What matters is the direction of the effect: dependency decisions compound outward, and the compounding is invisible in the pull request that adds one line to a manifest.
Counting Packages Is Not Measuring Risk
"We have 1,200 dependencies" is a statement with almost no analytical content. It doesn't distinguish a build-time formatter from a cryptography primitive in the authentication path. Several properties matter far more than the total.
Direct versus transitive. Direct dependencies are governable by policy — review, ownership, justification. Transitive ones can only be governed indirectly, through what you allow at the top and how you constrain resolution.
Production versus development. A test runner that never ships still executes on developer machines and in CI, frequently with repository credentials in the environment. Excluding development dependencies from triage because they "don't ship" mistakes where code runs for what code can reach.
Build-time versus runtime. Install-time execution is a meaningfully different privilege category from runtime execution, which is why ecosystems have moved to constrain it — npm's v12 line disables install scripts by default and requires explicit approval for the ones a project genuinely needs.
Criticality, privilege, and exposure. If this stops working correctly, does a feature degrade or does the product stop? What can it reach — credentials, customer data, network egress, the deployment path? Does it process input from outside your trust boundary?
Maintenance activity and replaceability. Is there a path for a fix to reach you, and could you remove this in a week if you had to?
Consider two components. One is a large frontend utility used in a hundred files, contributing meaningfully to bundle size, with no access to secrets and no role in authorization. The other is a forty-line build plugin running inside CI with access to the environment holding your registry and cloud credentials. The first dominates every "largest dependencies" report. The second sits where a malicious release has a direct path to your deployment identity.
Risk is not proportional to package size. A few lines of code can occupy an extremely trusted position, and a large library can be entirely inert with respect to security while still carrying real maintenance weight. Size measures cost; position measures consequence.
| Position in the system | Illustrative component | Typical privilege | Untrusted input? | Failure mode that matters most |
|---|---|---|---|---|
| Authentication / session | Token verification middleware | Decides who is who | Yes — every request | Silent authorization bypass |
| Cryptography | Signing, hashing, TLS | Protects everything else | Indirect | Weakened guarantee that looks fine in tests |
| Deserialization / parsing | JSON, XML, YAML, image, archive | Runs before validation | Yes, by definition | Memory or logic flaws reachable pre-auth |
| Data formatting | Date, currency, locale | Correctness of records | Sometimes | Wrong numbers in invoices, reports, schedules |
| Build plugin / CI action | Code generator, deploy step | Secrets, artifacts, publish rights | Repository content | Compromise of the artifact itself |
| Infrastructure module | Terraform module, Helm chart | Cloud IAM, networking | Configuration | Over-permissive resources created quietly |
| Frontend utility | Formatting, animation helpers | Browser context only | User-visible content | Bundle size, rendering regressions |
The point is not to produce a score. It is to give a team a shared vocabulary for saying "this one is different" without arguing from intuition.
The Package Registry Is Part of Your Build System
Most teams draw their architecture with a box for source control, a box for CI, and a box for production. The registry rarely gets a box. It should, because dependency resolution is a network operation against infrastructure you do not run, and its output becomes part of your artifact.
Whatever the ecosystem — npm, PyPI, Maven Central, NuGet, RubyGems, crates.io, Go module proxies, container registries — a registry provides a similar set of functions, and each is a trust surface.
Naming and namespaces. The registry decides what a name means. left-pad and @acme/left-pad are different kinds of claim: one is a global name in a flat space, the other is scoped to an organization that had to prove control of the scope. Namespace design has direct security consequences, which is why the confusion attacks discussed later work as they do.
Version listing and resolution inputs. Your package manager asks the registry what versions exist and what each requires. If that answer changes — a new release appears, a version is unpublished, metadata is edited — your build changes without your repository changing.
Artifact retrieval and integrity. The registry serves bytes. Integrity hashes recorded in a lockfile are the difference between "we fetched a package with this name and version" and "we fetched exactly this artifact."
Publishing and authentication. Who may push a new version, with what credential? This is the highest-leverage control in the ecosystem, because publishing is what converts one compromised account into thousands of compromised builds.
That last point has driven most meaningful registry-level change in recent years. Trusted publishing — short-lived, workload-bound OIDC credentials issued by a CI provider instead of long-lived API tokens stored as secrets — was introduced by PyPI in 2023 and has since been adopted across RubyGems, crates.io, npm, and NuGet. GitHub describes removing long-lived credentials from publishing pipelines as the single highest-value change a maintainer can make, precisely because credential theft is the pivot point in most propagation chains. npm has continued along the same line: short-lived granular tokens replacing retired classic tokens, staged publishing so a credential alone is insufficient to make a version live, protective read-only holds on high-impact accounts after sensitive changes, and install scripts disabled by default.
The counterargument matters, though. Trusted publishing defends against stolen tokens; it does not defend against an attacker who has taken over the maintainer's account outright, because at that point the attacker inherits the legitimate publishing path. And controls that coexist with legacy alternatives are only as strong as the weakest path left open.
Availability deserves separate mention. If a registry is unreachable or serving a partial index, builds fail, and teams tend to discover this at the worst possible moment. Caching proxies and internal mirrors solve that and create a natural place to apply policy — allowlists, quarantine windows for new releases, scanning before a package reaches a developer's machine.
Which brings the section to its real question: if your repository is intact but dependency resolution is compromised, can you still trust the resulting artifact?
The honest answer is no. Source integrity and build integrity are separate properties, and much of what follows depends on that distinction.
"Version 4" and "Exactly This Artifact" Are Different Statements
Two lines in a manifest can look almost identical and mean completely different things.
"Use version 4" — expressed as a caret range, a tilde range, a wildcard, or a dynamic version — is a policy. It says: at resolution time, pick whatever the ecosystem currently considers a compatible release. It delegates a decision to the future.
"Use exactly this artifact" — an exact version plus an integrity hash recorded in a lockfile — is a fact. It says: install these specific bytes, and fail if they don't match.
Semantic versioning is the convention that makes ranges tolerable. Its promise is that a patch release fixes bugs, a minor release adds functionality without breaking existing usage, and a major release may break things. It is a promise made in good faith by maintainers who are human, working with an imperfect definition of "breaking." A behavior change that one maintainer considers a bug fix is a breaking change for the one downstream consumer who depended on the old behavior. Ranges work well most of the time and fail exactly when a build was expected to be reproducible.
Lockfiles convert ranges into resolved facts. Their value depends on three practices that are easy to get wrong: committing the lockfile, using the install command that respects it rather than the one that re-resolves, and having CI fail when the lockfile and manifest disagree. Many teams do the first, some do the second, few enforce the third.
Integrity hashes in the lockfile add a second guarantee: not just "version 1.4.2" but "the artifact whose contents hash to this value." That distinction matters if a registry serves different bytes for the same coordinates, whether through compromise, mirror inconsistency, or an ecosystem that permits mutable releases.
Reproducible builds extend the idea from inputs to outputs: given the same source and the same declared dependencies, does the build produce a bit-for-bit identical artifact? Full reproducibility is achievable in some ecosystems and genuinely difficult in others, because timestamps, file ordering, absolute paths, locale, and parallelism all leak into output. But the ambition is valuable even when it is only partially achieved, because it converts "trust the builder" into "compare two independent builds."
The trade-off nobody escapes
It would be convenient to conclude that exact pinning is simply correct. It isn't, and the counterargument is important.
Aggressive pinning of everything, everywhere, with no update process, produces a codebase that is stable in the short term and increasingly hazardous over time: known vulnerabilities accumulate with no path to a fix, the gap between your version and the current one widens until upgrading becomes a project rather than a task, and eventually the version you're on stops receiving security releases at all. Frozen is not the same as safe.
The opposite posture — accepting every new release automatically, everywhere, including transitively — reduces staleness but hands change control to strangers. Anything published upstream becomes your problem within hours, including behavior changes, new transitive dependencies with their own trust implications, and, in the worst case, a malicious release.
Neither extreme is a strategy. The real design goal is to hold two properties simultaneously:
Change control — nothing enters the artifact without being recorded, resolvable, and attributable.
Update velocity — the organization can move any dependency to a new version quickly when it needs to, with evidence that the product still works.
Most dependency dysfunction traces back to optimizing one of these at the expense of the other.
The Update Paradox
Two statements are both dangerous, and both are common:
"We don't update dependencies. If it works, don't touch it."
"We update everything automatically as soon as releases appear."
Old dependencies accumulate published vulnerabilities that anyone can look up, along with compatibility drift against platforms that keep moving, and eventually the loss of upstream support entirely. The cost is not linear. Each skipped major version makes the next upgrade harder, until a two-day task becomes a two-month migration that competes with roadmap work and loses.
New versions bring different risks: behavior changes not intended as breaking, regressions in edge cases the maintainer's tests don't cover, genuine breaking changes in majors, new transitive dependencies that were never evaluated, and — rarely but consequentially — a malicious release, since the pattern in most recent registry incidents is a legitimate package publishing a version containing code its maintainer did not write.
So the engineering question is not update or don't update. It is: how do we update with evidence?
The components of an answer are not exotic; the discipline is in connecting them.
Automated dependency pull requests turn updates into reviewable units of work rather than a periodic archaeology project. The design decision that matters is grouping: one pull request per package produces noise teams learn to ignore, while grouping by ecosystem, directory, or dependency family keeps the queue small enough to be read.
A cooldown on new releases. Because propagation attacks depend on speed, waiting a few days before pulling in a brand-new version gives detection signals time to surface. GitHub's Dependabot now applies a default three-day cooldown before opening version-update pull requests while continuing to open security updates immediately — the reasoning being that a delay on routine version churn costs almost nothing, and a delay on a fix for an actively exploited flaw costs a great deal.
CI verification that exercises what the dependency does. A green build proves the code compiles and existing tests pass. Whether that means anything depends on whether the tests touch the dependency's behavior, which is the subject of a later section.
Staged rollout. Pre-production, then a subset of production, then everywhere, with monitoring in between. Dependency changes deserve the same progressive delivery treatment as feature changes, because they are changes to production behavior.
Changelog review for anything privileged, and security prioritization kept separate from version currency — "we are three minors behind" and "we are running a version with a known exploited flaw" are different conditions with different response times.
A Small Package Can Have a Large Blast Radius
Four short cases, deliberately mundane, to make the point that consequence follows position rather than size.
A date parsing library. Trivial-looking. It sits underneath invoice generation, subscription renewal dates, report boundaries, scheduling, retention policies, and audit timestamps. A change to how it handles time zones, daylight saving transitions, or ambiguous formats does not throw an exception. It produces subtly wrong output that flows into billing and into records customers rely on. The failure is silent, it is financial, and it is discovered by the customer.
A serialization library. It determines the wire format of your API responses. A change in how it orders keys, encodes numbers, handles nulls, or escapes characters is invisible in your own test suite if the tests compare parsed objects rather than bytes. It is highly visible to the mobile client that has been consuming your API for two years, and to the partner whose integration parses your payloads with assumptions you never documented.
Authentication middleware. Perhaps a few hundred lines. It decides, for every request, who the caller is and what they may do. Its code footprint is negligible and its trust position is total. A defect here is not a degraded feature; it is a data breach with your name on it.
An image or document parser. It processes files uploaded by customers — which means it processes input controlled by whoever can create an account. Parsers of complex binary formats are historically among the most defect-prone code that exists, and this one runs before most of your validation logic does.
Dependency criticality is a function of two variables that have nothing to do with lines of code:
Where the component sits — in the request path, in the authorization decision, in the build, in the deployment.
What data and privilege it touches — customer records, credentials, money, untrusted input, publish rights.
A team that can answer those two questions for its top forty components has better supply-chain awareness than a team with a complete inventory of twelve hundred it has never sorted.
The Maintainer Question
Underneath every dependency graph is a social graph. Packages do not maintain themselves; people and organizations do, usually for free, often in evenings, sometimes for a decade.
Useful questions when evaluating or re-evaluating a component:
- Who maintains this project, and is that an individual, a company, or a foundation?
- Is maintenance active — not measured by commits, but by whether reports get responses and fixes get released?
- How are releases produced? By hand from a laptop, or from a pipeline with attested provenance?
- Who holds publishing rights, and how many of them are there?
- Is there a documented process for reporting a vulnerability privately?
- Is there any statement about supported versions and how long they receive fixes?
- What happens if the maintainer stops? Is there a successor, a foundation, an organization with an interest in continuity?
Now the cautions, because this is an area where reasonable-sounding heuristics are wrong.
A single-maintainer project is not automatically unsafe. Some of the most carefully engineered software in existence has one author. A single maintainer with a narrow scope, a stable API, a slow release cadence, and a finished mental model may be a lower-variance dependency than a large project with many contributors and a fast-moving surface. What single maintainership creates is concentration — of knowledge, of publishing rights, and of continuity risk. Concentration is a fact to be managed, not a verdict.
A popular project is not automatically safe. Download counts measure adoption, not review. Popularity is, if anything, an attractant: attackers target components with the widest reach, and the incidents of recent years have overwhelmingly involved packages with very large user bases. Popularity is a signal about how many people would be affected, not about how many people have read the code.
Contributor count is not review coverage. Many contributors can coexist with very few reviewers, and the security-relevant parts of a project may be maintained by exactly one person.
Corporate backing is not permanence. Companies reorganize, deprecate, and exit. Foundation stewardship provides different guarantees than a vendor's roadmap, and neither is unconditional.
Maintainer structure is one signal among several. Its main practical use is not to accept or reject a dependency, but to predict how a fix would reach you if one were needed urgently. That is the question worth asking at adoption time and worth re-asking later.
When Trust Changes Hands
Dependencies have lifecycles, and the decision that was correct three years ago may not survive contact with what happened since.
A package can change maintainers, sometimes to a person the original author has never met, sometimes because the original author was looking for a successor and someone volunteered. It can be archived, with the repository frozen and the registry entry left in place, still installable, no longer fixed. It can move repositories or organizations, breaking the link between what you reviewed and what you now install. It can change how releases are produced — from a signed pipeline to a laptop, or the reverse. It can change governance, from an individual to a company, or from a company to a foundation, with corresponding changes in what "supported" means. It can change license (more on that later). It can be superseded by a rewrite under a new name, leaving the original in maintenance mode indefinitely.
The most instructive real example remains the compromise of the xz compression utilities disclosed in early 2024, tracked as CVE-2024-3094. Malicious code was introduced into release tarballs of versions 5.6.0 and 5.6.1 by an individual who had spent roughly two years contributing to the project and had been granted maintainer trust. The backdoor targeted a path where the library was linked into the SSH daemon on some distributions, and it was found by an engineer investigating unexplained performance behavior before the affected versions reached most stable distribution channels.
The mechanism worth extracting is not the technical payload. It is that the trust relationship was legitimate at every step. There was no stolen credential. A project with an overworked maintainer accepted help, the helper became a maintainer over a long period, and the release process — which built artifacts differing from the public repository contents — became the delivery mechanism. Every individual step looked like open source working normally, because for two years it was.
The defensive conclusion is not "distrust contributors." It is that trust is not a permanent property of a dependency. Passing review once, in a specific year, under a specific set of maintainers, with a specific release process, does not confer permanent standing.
This suggests a practice worth naming: dependency re-validation. Periodically, for the subset of components that occupy privileged positions, re-ask the adoption questions. Not all 1,200 — the twenty or forty that would matter. Has ownership changed? Has the release process changed? Is the project still active? Is our version still on a supported line? Would we choose it today?
Re-validation is cheap when it is scheduled and expensive when it is triggered by an incident.
How Dependencies Get Compromised — and What Each Case Actually Violates
This section is deliberately architectural. The goal is to understand which trust relationship fails in each class of compromise, so that controls can be matched to mechanisms rather than sprayed across a checklist. No operational detail is given, and none is needed: defenders need the shape of the problem, not a procedure.
Compromised maintainer account. An attacker publishes as a legitimate maintainer, typically after phishing or credential reuse. The violated trust is identity: the registry, and therefore your package manager, believes the release came from the person who has always published it. Exposure is reduced by phishing-resistant multi-factor authentication on publishing accounts, trusted publishing that binds releases to a workflow rather than a portable secret, staged publishing requiring a second authorization, and protective holds after sensitive account changes — plus, on the consuming side, cooldown windows and enforced lockfiles.
Malicious release from a legitimate project. A published version contains code the community never reviewed — via a compromised account, a maintainer acting in bad faith, or a release process that builds from something other than the reviewed source. The violated trust is the equivalence of source and artifact. Controls: build provenance tying the artifact to a specific source revision and process; reproducible builds where achievable.
Compromised CI system. The attacker gains execution inside the build environment and modifies the artifact or steals publishing credentials. The violated trust is build integrity. Controls: ephemeral isolated build environments; strict separation between untrusted-input workflows and privileged ones; restrictions on what less-trusted workflows write to shared state such as caches; egress visibility from runners; and short-lived, workload-scoped credentials.
Tampered artifact in transit or at rest. Modification after the build — in a registry, a mirror, a bucket, a CDN. The violated trust is integrity of the delivered bytes. Controls: content-addressed references rather than mutable tags, integrity hashes verified on install, and signature verification enforced at deployment rather than merely produced at build.
Dependency confusion. A name meant to refer to an internal package resolves to a public one. The violated trust is namespace authority.
Typosquatting. A package whose name resembles a legitimate one is installed by mistake. The violated trust is human name recognition. Both are addressed below.
Compromised build tooling. Not the CI service but something it runs: a runner image, a widely used action, a compiler distribution, a toolchain installer. The violated trust is the toolchain itself — the hardest case, because the toolchain is what you would otherwise use to check everything else. Controls: digest-pinned tooling from authenticated channels, minimizing third-party components with build-time execution, and independent rebuild comparison where feasible.
Vulnerable upstream component. No attacker at publish time at all — a genuine defect becomes exploitable in your product. The violated trust is correctness, which nobody ever guaranteed. Controls: inventory that answers "where is this component" quickly, advisory monitoring, and a pipeline that can ship a patched version fast.
| Compromise class | Trust violated | Where it enters | Controls that reduce exposure |
|---|---|---|---|
| Maintainer account takeover | Publisher identity | Registry publish | Phishing-resistant MFA, trusted publishing, staged publishing, consumer-side cooldown |
| Malicious release | Source ≟ artifact | Registry publish | Build provenance, reproducible builds, release review |
| CI compromise | Build environment integrity | Build | Ephemeral isolated runners, trigger and cache boundaries, scoped short-lived credentials |
| Artifact tampering | Delivered bytes | Storage / transport | Digest pinning, integrity hashes, signature verification at deploy |
| Dependency confusion | Namespace authority | Resolution | Scoped internal names, single-source resolution, registry configuration policy |
| Typosquatting | Human name recognition | Developer action | Allowlists, install-time policy, proxy scanning, review of new direct dependencies |
| Toolchain compromise | The verifier itself | Build | Digest-pinned tooling, minimal build-time third parties, independent rebuilds |
| Upstream vulnerability | Correctness | Any | Inventory, advisory monitoring, fast patch delivery |
Typosquatting and dependency confusion, defensively
These two are frequently mentioned together and are mechanically different.
Typosquatting exploits the gap between what a developer meant to type and what they typed. A name one character from a popular one, or with a plausible variant spelling, accumulates installs from mistakes and from copy-paste out of tutorials and generated snippets. The defense is not developer vigilance — humans are reliably bad at this — but structural: a curated allowlist or internal proxy mediating what can be installed, automated policy flagging any first-time direct dependency, and review that treats a new manifest entry as a decision rather than a formatting change.
Dependency confusion exploits how resolution treats names that exist in more than one place. If a build can reach both an internal source and a public registry, and a name exists in both, the resolution outcome depends on configuration — and the failure mode is that a public package is silently preferred over the internal one it was meant to reference. The defenses are configuration-level and mostly one-time:
- Use namespaces or scopes for internal packages so that internal names cannot collide with unscoped public names.
- Where the ecosystem supports it, register or reserve the organization's namespace publicly so it cannot be claimed by someone else.
- Configure resolution so that each name has exactly one authoritative source, rather than multiple sources consulted in an order that can change.
- Route all dependency traffic through a single controlled proxy so that policy has one place to live.
- Fail builds on unexpected source-of-origin rather than warning.
For a founder, the memorable version is this: dependency confusion is a naming and configuration problem, not an exotic attack. It is one of the few supply-chain issues that can be substantially closed by a platform team in a week and then stay closed.
Your CI/CD System Is a Supply-Chain Dependency
Application dependencies get the attention because they have manifests. The pipeline usually doesn't, and the pipeline is where privilege concentrates.
Trace what a production release passes through: source control, a runner (someone's virtual machine, running someone's image), a checkout step, a dependency installation step, a series of third-party workflow actions or plugins invoked by name, a build step, a test step, an artifact publish step, and a deployment step. Each of those third-party components is code, fetched from somewhere, executing inside an environment that holds credentials.
What can a CI workflow reach? Frequently:
- The complete source of the repository, and sometimes of others.
- Package registry credentials, including publish rights.
- Container registry credentials.
- Signing keys, if signing happens in the pipeline.
- Cloud credentials, often broader than the deployment strictly requires.
- Deployment permissions to production.
- Environment secrets for third-party services.
- Shared caches used by other workflows.
Therefore a twenty-line third-party action that formats a comment on a pull request may occupy a more privileged position than any library in the application. It is Layer 8 code with Layer 1 consequences.
This suggests a second article-specific concept, offered as a way of thinking rather than as a standardized metric: privilege-weighted dependency risk. Instead of ranking components by size, popularity, or count of findings, rank them by what they can reach if they behave badly. A dependency's weight is a function of its position and its access, not its prominence.
Applying that lens to pipelines produces a fairly short list of high-value practices:
Pin third-party actions and plugins by immutable digest, not by tag. A tag is a mutable pointer; a commit SHA or content digest is exactly this code. It is the lockfile distinction, applied one layer up.
Separate untrusted-input workflows from privileged ones. The most common CI compromise pattern is a workflow running in a privileged context while executing content that came from a fork. Platform defaults have moved this way — GitHub changed actions/checkout defaults to prevent checkout of untrusted fork code in commonly exploited trigger contexts, and added policies letting organizations restrict which triggers are allowed and who may fire them.
Treat shared caches as a trust boundary. A cache entry written by a low-privilege workflow and read by a high-privilege one is an escalation path; restricting writes from less-trusted triggers closes it. The pattern generalizes across CI providers.
Scope credentials tightly and briefly. OIDC federation between the CI provider and your cloud or registry removes long-lived secrets from the environment entirely — which removes the thing most propagation chains are trying to steal. Deployment identities accumulate permissions the way applications accumulate dependencies; check the assumption periodically.
Watch egress. Build runners have little legitimate reason to contact arbitrary hosts. Logging outbound connections is cheap; restricting them is stronger.
Review workflow changes as seriously as application changes. A pull request modifying a pipeline definition is a pull request modifying who can do what to production.
The organizational point underneath all of this: pipeline configuration is production configuration. It is frequently owned by whoever set it up first and reviewed by nobody thereafter.
The Build System Is Part of the Product
Here is the founder-level version of everything above.
Your customers do not run the code in your repository. They run the artifact your build system produced. Between those two things sits an entire environment — dependencies resolved at a particular moment, a toolchain of a particular version, a runner with particular contents, a sequence of steps with particular privileges — and the artifact is the output of that environment, not a copy of the repository.
Therefore source integrity is necessary and insufficient. Perfect branch protection, mandatory review, signed commits, and a clean audit trail in version control tell you a great deal about what your engineers intended. They tell you comparatively little about what shipped.
The complementary property is build integrity: confidence that the artifact is the faithful result of the intended source, built with the intended dependencies, in an environment that nothing untrusted could modify.
That confidence has to come from evidence, and the evidence has to be produced at build time, because it cannot be reconstructed afterward.
Source → Build → Artifact → Production
A clean model, with the same three questions asked at every transition:
SOURCE
│ What is trusted? Who can modify it? What evidence remains?
▼
DEPENDENCIES
│
▼
BUILD ENVIRONMENT
│
▼
TOOLCHAIN
│
▼
ARTIFACT
│
▼
REGISTRY
│
▼
DEPLOYMENT
│
▼
PRODUCTION
| Transition | What is being trusted | Who can modify it | What evidence remains afterward |
|---|---|---|---|
| Source → Dependencies | That the manifest and lockfile describe what will actually be fetched | Anyone with write access; upstream maintainers; registry operators | Lockfile with versions and integrity hashes; resolution logs |
| Dependencies → Build environment | That the runner image and toolchain are what they claim to be | Platform provider; whoever pins the image | Runner and image identifiers, if recorded |
| Build environment → Toolchain | Compiler, bundler, plugins, and code generators | Tool maintainers; whoever chose the versions | Toolchain versions, if captured in provenance |
| Toolchain → Artifact | That the build steps did only what the definition says | Anyone who can change the pipeline; anything with execution in the run | Build logs; provenance attestation; artifact digest |
| Artifact → Registry | That what was pushed is what was built | Whoever holds publish credentials | Digest, signature, publish audit record |
| Registry → Deployment | That what is pulled is what was pushed | Registry operator; anyone with write access to tags | Digest match; admission decision record |
| Deployment → Production | That what runs is what was admitted | Cluster operators; anyone with runtime access | Running image digests; deployment history |
Reading down the third column is instructive. In many organizations the answer to "who can modify it" is "more people than the org chart suggests," and the answer to "what evidence remains" is "logs, for ninety days, if anyone thinks to look."
Provenance: Where Did This Artifact Come From?
Provenance is the recorded answer to that question, produced automatically by the system that did the work.
It is not "a certificate." A signature says that some key signed some bytes. Provenance is metadata describing how the artifact came to exist. A team with meaningful provenance can answer, for any artifact in production: which source repository and exact revision produced it; which build process ran; which top-level inputs, including dependencies, participated; which platform executed the build; and whether the artifact has changed since.
The most widely referenced framework is SLSA (Supply-chain Levels for Software Artifacts), developed through industry consensus and hosted under the Linux Foundation. Its current published version is v1.2, and its structure is worth stating accurately because it is often misquoted.
SLSA is organized into tracks, each addressing a different part of the supply chain, each with its own levels. Version 1.2 defines a Build track and a Source track.
The Build track describes increasing trustworthiness and completeness of an artifact's provenance:
- Build L0 — no requirements; the absence of SLSA.
- Build L1 — provenance exists, generated automatically by the build platform, describing what entity built the package, what process was used, and what the top-level inputs were. The specification is explicit that L1 provenance may be incomplete or unsigned and is trivial to forge; its value is preventing mistakes and enabling debugging, patching, rebuilding, and inventory.
- Build L2 — builds run on a hosted platform that generates and signs the provenance, and consumers validate its authenticity. Aimed at tampering after the build.
- Build L3 — builds run on a hardened platform with controls preventing runs from influencing one another and preventing user-defined build steps from reaching the provenance signing material. Aimed at tampering during the build.
The Source track, reintroduced in v1.2 after being deferred from v1.0, addresses trust in how a source revision was created — version control practices, history integrity, enforced controls, and review — with the repository owner defining the expected process and consumers verifying attestations against it.
Two details are frequently lost in summary. First, the point of the build track is verification: provenance is only useful if consumers hold expectations about what correct provenance looks like and actually compare against them. Provenance produced and never checked is metadata, not a control. Second, a SLSA level applies to an artifact, not to its dependency tree — the specification states that an artifact's level is independent of the levels of its dependencies, and that no single level covers an artifact together with everything beneath it.
That second point is an antidote to a specific kind of overclaiming. "We are SLSA Build L3" describes how your artifact was built. It says nothing about the two thousand components inside it.
Signing Does Not Mean Safe
This is where non-security founders are most often misled, usually by well-meaning summaries.
A signature establishes that a particular key signed particular bytes, and that the bytes have not changed since. Combined with a trustworthy binding between the key and an identity, it supports two claims:
Authenticity — this came from who it claims to have come from.
Integrity — this is unmodified since it was signed.
That is the entire guarantee. In particular, a signature does not establish:
Security — the signed software may contain vulnerabilities, including severe ones. Signing is orthogonal to defect density.
Quality — the signed software may be poorly engineered, badly tested, or unfit for your use.
Trustworthiness — if the signer's account was compromised, or the signer is acting in bad faith, or a compromised build environment produced the artifact before signing, then the signature faithfully attests to a malicious artifact. Every recent registry compromise involving a legitimate maintainer account produced releases that were, from the ecosystem's perspective, correctly authenticated.
The distinctions arranged in order of what they actually tell you:
| Property | Question it answers | What it does not tell you |
|---|---|---|
| Integrity | Did these bytes change? | Whether the bytes were ever good |
| Authenticity | Who produced this? | Whether that party was compromised, or competent |
| Provenance | Through what process, from what source, with what inputs? | Whether the source or inputs were sound |
| Security | Are there known exploitable weaknesses? | Whether unknown ones exist |
| Quality | Does it behave correctly under your conditions? | Anything about origin |
| Trustworthiness | Should we rely on this in this position? | Nothing on its own — it is a judgment built from all of the above |
Signing and provenance are genuinely valuable. They convert a large class of "we cannot tell what happened" problems into "we can tell, and we can prove it." They make incident response dramatically faster, because you can enumerate exactly which artifacts came from which source revision. They are the foundation for admission policies that refuse to run anything unattested.
But the sentence "our artifacts are signed" answers the question who made this and has it changed? It does not answer is this good? Treating the first as if it were the second is a category error that shows up in vendor questionnaires, board slides, and — occasionally — architecture decisions.
Containers Did Not Remove the Supply Chain
Containers solved a real problem: they shrank the "works on my machine" category by shipping the environment with the application. What they did not do — despite how the word "isolation" gets used — is reduce the number of components a team depends on. They relocated them and made them easier to ignore.
Open a typical application image and count what is inside: a base operating system userland with a package manager, shell, core utilities, and certificate stores; system libraries for TLS, compression, encoding, and DNS; a language runtime and its standard library; application dependencies resolved at image build time; the application code; and frequently leftovers — build tools from an earlier stage, debugging utilities added during an incident, package manager caches.
Everything on that list has its own maintainers, release cadence, and vulnerability history. None of it appears in your application's manifest. Which means this line:
FROM some-base-image:some-tag
is a dependency declaration with a larger transitive footprint than most of the manifest below it.
Several mechanics matter. Tags are mutable pointers; digests are content. A tag refers to whatever the publisher most recently pushed, which is often good — you get updates — and also means two builds a week apart can produce different images from identical Dockerfiles. Digest references give exactness at the cost of manual movement; the mature pattern is usually digest pinning updated by automation on a schedule.
Layers are cached and inherited. Anything installed in a lower layer persists even if a later layer deletes it from the visible filesystem, and cached layers can make a rebuild reuse months-old contents. "We rebuilt the image" and "the image contains current packages" are different statements.
Image registries are registries — the same authentication, publish-rights, availability, and mutability considerations apply, as does the value of verifying content rather than names. Container scanning is different from code scanning, inspecting installed OS packages and files present in the filesystem, which surfaces components the application never calls and is a major source of the noise discussed later. And provenance applies to images too: attestations can bind an image digest to the source and pipeline that produced it, with admission control enforcing it at the cluster boundary.
None of this argues against containers, which remain an excellent packaging and isolation mechanism. It argues against one specific inference: containerization is not supply-chain isolation. A container isolates a process at runtime. It does nothing to reduce what you trusted at build time — it packages that trust and ships it.
The Base Image Problem
Push on one specific consequence, because it exposes a structural gap in how most teams release software.
The application team wrote no operating-system code. Nobody at the company has ever contributed to the distribution's OpenSSL packaging, its glibc build, or its certificate bundle. Yet those components are in production, in your account, processing your customers' traffic.
So:
- Who monitors them? Application dependency tooling frequently doesn't. If your dependency alerts come from your manifest ecosystem, OS-level components sit outside that field of view entirely unless something explicitly scans images.
- Who updates them? In most organizations, the answer is "whoever rebuilds the image," which means the update path for a system library runs through the application team's release process.
- When does the image get rebuilt? Here is the trap: many pipelines rebuild images only when application code changes. A service that is feature-complete and stable — often the most important kind of service — may go months without a rebuild. During those months its base layer ages, and every published fix for every component inside it accumulates unapplied.
- What is actually running right now? Not what the Dockerfile says. What the digest of the currently deployed image contains.
This produces a genuinely counterintuitive outcome: the most stable services drift furthest. The service nobody has needed to change is the service whose base layer is oldest. Release frequency, which is usually discussed as a delivery-velocity metric, turns out to also be a security-currency metric.
The structural fix is to decouple rebuild from code change. Scheduled rebuilds of unchanged services, with automated verification and deployment, keep base layers current without requiring a business reason to ship. This is a platform-team responsibility more than an application-team one, and it works only if the verification is good enough that a rebuild-and-deploy of unchanged code is a routine, boring event rather than a risk.
The counterargument deserves acknowledgment: rebuilding an unchanged service is a change to production, and changes carry risk. A base image update can alter TLS defaults, locale behavior, DNS resolution, or timezone data. This is precisely why rebuild automation and dependency-aware verification belong in the same conversation, and why "we never rebuild because it's risky" and "we rebuild blindly on a cron" are both wrong answers.
Infrastructure as Code Has Dependencies Too
The supply chain does not stop at the application boundary. The systems that create your infrastructure are themselves assembled from third-party parts, and those parts run with the permissions required to create infrastructure.
The component types vary by stack: Terraform or OpenTofu modules pulled from public registries, Helm charts from chart repositories, Kubernetes operators and controllers installed into clusters, CloudFormation or ARM templates, Ansible roles and collections, Pulumi packages, shared CI workflow templates, policy bundles, and cluster add-ons installed once during setup and never revisited.
Two properties make this category distinctive.
Privilege. A module that provisions a network, an identity policy, a database, or a cluster role must be granted permissions commensurate with that task. A community Terraform module executed with administrative cloud credentials has, at minimum, the ability to create resources you did not ask for. A Kubernetes operator typically runs continuously inside the cluster with broad API access — it is not a build-time artifact but a permanent, privileged resident.
Opacity of effect. With an application library, you can usually reason about the effect of a call. With an infrastructure module, the effect is a set of cloud resources whose configuration you may never inspect directly, because the whole point of the module was not having to. A module that quietly attaches a broader policy than necessary, opens a security group wider than expected, or disables a logging default produces a durable misconfiguration rather than an immediate error.
The practices that help are the same ones from earlier layers, applied here:
- Pin infrastructure modules and charts to exact versions or digests, and update them deliberately.
- Prefer modules from sources with clear ownership and release practices, and re-validate that ownership periodically.
- Review the effect — the plan, the diff, the rendered manifests — not just the module version. Plan output is the closest thing this layer has to a code review.
- Apply policy as code at the boundary so that unexpected resource shapes fail regardless of which module produced them.
- Keep an inventory of cluster-resident third-party components. Operators and add-ons installed during a project two years ago are the infrastructure equivalent of a forgotten dependency, except they hold live API credentials.
- Scope the credentials used by automation to what the automation actually provisions.
The general principle: the more privileged the dependency, the more its provenance and update path matter. Infrastructure modules sit near the top of that ordering, and near the bottom of most inventories.
The AI Layer
AI has added several genuinely new categories of supply-chain relationship in a short period. They are worth separating, because they raise different governance questions and are often discussed as one thing.
Model as a service. The product calls a hosted inference endpoint. There is no artifact, no version in a lockfile, and often no way to pin behavior precisely. The provider may update the underlying model, adjust filters, deprecate an identifier, or shift output distribution in ways no dependency tool can see. It behaves like a hosted capability — closest in character to a managed database — except that outputs are non-deterministic and behavior can change without any version identifier changing. The governance questions are observability, evaluation, deprecation notice, data handling, and fallback.
Model as an artifact. The team downloads weights and runs inference itself. Now there is an artifact: a large binary, fetched from a hub, identified by name and revision, loaded by a library. Provenance questions apply exactly as they would to any artifact. Serialization format matters, because some historical model formats permit code execution on load and data-only formats exist specifically to remove that property. Licensing matters, because model licenses are frequently not standard open-source licenses and carry use restrictions no license scanner recognizes. And the weights are far too large to review — this is a dependency you can hash but cannot read.
Supporting libraries. Inference runtimes, tokenizers, embedding libraries, vector database clients, orchestration frameworks, agent tooling, provider SDKs. Ordinary dependencies deserving ordinary treatment — except that this part of the ecosystem is young, fast-moving, and characterized by broad transitive footprints and rapid major-version churn.
Data as a dependency. Embeddings, indexes, fine-tuning and evaluation sets, retrieval corpora. These determine behavior, they change, and they are almost never inventoried. Rebuild a retrieval index from a source that changed and product behavior changes with no code change and no dependency update.
Inventory frameworks are catching up. CISA's 2026 SBOM minimum elements apply to all software including AI systems and SaaS, while noting that AI systems may have components the minimum elements do not fully capture and pointing to complementary work on minimum elements for AI bills of materials produced with G7 partners. CycloneDX has carried machine-learning BOM capabilities for some time, and its 1.7 specification — ratified as ECMA-424, 2nd edition, in December 2025 — covers machine learning models alongside cryptographic assets and conventional components.
| AI relationship | What you actually depend on | Can you pin it? | Can you inspect it? | Primary risk to manage |
|---|---|---|---|---|
| Model as a service | A provider's endpoint and current model | Sometimes by identifier, never fully | No | Silent behavior change, deprecation, data handling |
| Model as an artifact | A weights file and its loader | Yes, by digest | Not meaningfully | Provenance, unsafe load formats, licensing |
| Inference / vector libraries | Ordinary packages | Yes | Yes | Normal dependency risk in a fast-moving area |
| Embeddings and indexes | Derived data | Rarely | Partially | Undetected behavior change with no code change |
| AI-generated source | Code now in your repository | It is yours once merged | Yes | Verification, not origin |
AI-Generated Code Arrives With Dependencies Attached
The last row deserves its own treatment, because it is where the AI conversation meets the dependency conversation most directly.
Coding assistants do not only produce logic. They produce choices: which library to use for a task, which package name to add to the manifest, which API to call, which pattern to implement. Those choices are dependency decisions, made at a rate and volume that outpaces the review habits most teams built when dependency additions were rare and deliberate.
Three specific consequences.
Package suggestions are dependency additions. A generated snippet that imports a library the team has never used is proposing a new trust relationship. It should go through whatever process a human proposing that library would go through. In practice, generated code often skips this because the import arrives inside a change whose purpose was something else.
Hallucinated package names are a real failure mode. A model can confidently reference a package that does not exist. The immediate consequence is a failed install. The second-order consequence is more interesting: consistently hallucinated names are predictable, and a name that many developers try to install is a name worth squatting. This is a variant of typosquatting where the mistake is generated rather than typed, and the defense is identical — mediated installs, allowlists, and treating any new direct dependency as a reviewable decision.
Patterns can be outdated or subtly wrong. Generated code reflects patterns that were common in its training data, which may include deprecated APIs, superseded cryptographic practices, or idioms that were fine in an older major version. This is not unique to machine generation — copying from an old blog post produces the same result — but the volume is higher.
The important principle is symmetric, and it cuts against alarmism as much as against complacency:
Origin does not remove verification requirements, and it does not add them either.
Human-written and machine-generated code both become production dependencies after acceptance. Both should pass the same review, the same tests, the same static analysis, the same dependency policy. AI-generated code is not inherently insecure; the risk is not in the generation but in the acceptance path. Where teams get into trouble is when the volume of generated change makes existing review practices statistically ineffective — not because the code is worse, but because there is more of it moving faster through the same gate.
The practical adjustment is usually to move enforcement from human attention to automation: policy that blocks unapproved package sources regardless of who or what added them, tests that must cover changed paths, and generated-code review focused on the decisions rather than the syntax.
License Risk Is Also Supply-Chain Risk
Vulnerabilities dominate the conversation, but the obligation that most often surprises a company during due diligence is licensing.
Every component you ship carries terms. Permissive licenses generally require preserving copyright notices and license text — trivial to satisfy and remarkably easy to overlook when nobody generates attribution files. Copyleft licenses impose conditions on distribution of derivative works, with specifics depending on the license, on whether your product is distributed at all, and on how components are combined. Network-copyleft licenses extend conditions to software offered over a network, which is directly relevant to SaaS. Source-available and business-source licenses are not open source in the usual sense and frequently restrict competing commercial use. Model weights and datasets often carry bespoke terms, including field-of-use restrictions no conventional scanner is built to interpret.
Three dynamics make this a supply-chain problem rather than a one-time legal review. Licenses change — several prominent projects have relicensed, in both directions, and your obligations follow the version you ship rather than the version you evaluated. Transitive components carry terms too, because obligations attach to what is distributed rather than to what was deliberately selected. And obligations vary with how you ship: the same component can carry different practical requirements in a hosted service, an on-premise installation, a mobile application, and a container image handed to a customer.
This article does not provide legal advice, and license interpretation is genuinely fact-specific — the same text can produce different conclusions depending on how components are combined and distributed. Where the question is material, qualified counsel should be involved, and the engineering organization's job is to give counsel something accurate to work with.
That is the engineering point, and it is narrow: you cannot review obligations for components you cannot identify. License analysis is downstream of inventory. Which is why the 2026 SBOM minimum elements added Component License as a new required data field, reasoning that license information helps organizations manage the risk of copyright infringement claims and that incorrect or fraudulent license data can affect an organization's ability to rely on a component or obtain support for it.
The practical program is modest: generate attribution from the actual dependency graph rather than from memory, define which license categories are acceptable for which uses, enforce that in CI so violations surface at the pull request rather than at the acquisition, and re-check when major versions change.
The Component You Forgot You Shipped
Inventories drift. Not because anyone is careless, but because software accretes and nothing removes.
A package is added for a two-week experiment. The experiment is abandoned. The package stays in the manifest, gets resolved on every install, and appears in every scan for the next four years.
A migration moves the product from one payment provider to another. The new SDK is added. The old one is not removed, because removing it requires touching code paths nobody wants to touch, and it is not causing problems.
A Dockerfile installs a debugging utility during an incident. The line survives review because it is one line among forty, and it is now in every image the service ships.
A test-only library ends up in the production dependency section, because the distinction between dependency groups is a convention that is easy to get wrong and produces no immediate feedback.
A feature is removed from the interface but its supporting library remains imported by a module that is still compiled into the bundle.
An internal tool is vendored into the repository, forked, and forgotten, and it is now the only copy of that code in existence.
Unused components are not harmless. Each one contributes:
- Maintenance load — it appears in every dependency update queue and every review.
- Vulnerability surface — its advisories land in your alerts, consuming triage capacity regardless of whether the code is reachable, and if it is reachable through some path nobody mapped, it is a genuine exposure.
- License obligations — if it ships, its terms apply, whether or not it is called.
- Build complexity — it participates in resolution, contributes to conflicts, and slows builds.
- Cognitive load — engineers assume dependencies exist for reasons, and preserve them accordingly.
Dependency pruning is unglamorous and unusually high-return. A few practices make it sustainable rather than heroic:
- Use tooling that identifies declared-but-unimported packages, and treat the report as a prompt for judgment rather than as a delete list — dynamic imports, plugin systems, and reflection defeat static analysis regularly.
- Separate production and development dependency groups correctly, and enforce that separation in CI.
- Use multi-stage container builds so that build-time tooling never reaches the final image.
- When a migration completes, treat removal of the old component as part of the migration's definition of done, not as follow-up work.
- Periodically compare the declared inventory against what the runtime actually loads. The gap is usually informative.
What an Engineering Organization Should Actually Know
Before reaching for a format or a tool, it is worth stating plainly what the organization needs to be able to answer. Not aspirationally — operationally, in the twenty minutes after someone asks.
At minimum, for the components that make up shipped software:
- Component — what it is, identified in a way that machines can match against advisory data.
- Version — exactly which one, per artifact, not per repository.
- Origin — where it came from, and who produces it.
- Relationship — direct or transitive, and through what path.
- Environment — production, build-time, development, test.
- Owner — which team is responsible for decisions about it.
- License — what terms attach to shipping it.
- Known risk — advisories affecting this version, and their assessed relevance.
- Update path — how a new version reaches production, and how long that takes.
Two of these are routinely missing even in organizations with good tooling. Owner is missing because dependency inventories are generated from manifests, and manifests do not contain organizational information. Update path is missing because it is a property of the delivery system rather than of the component, and nobody thinks to record it until an urgent patch reveals that nobody knows.
That list is also, not coincidentally, an informal specification of a software bill of materials — which is where the discussion goes next.
SBOM: The Ingredient List, Not the Safety Certificate
A software bill of materials is a machine-readable, nested inventory of the components that make up a piece of software and the relationships between them. The ingredient-list analogy is apt in one specific way and misleading in another: it correctly conveys that the document enumerates contents, and it incorrectly implies that reading it tells you whether the food is safe to eat.
The current authoritative baseline is the 2026 Minimum Elements for a Software Bill of Materials, published on 29 July 2026 by CISA together with NSA, FBI, and a long list of international partner agencies including Germany's BSI, France's ANSSI, Japan's METI, India's CERT-In, and the national cybersecurity centres of Canada, Australia, the Netherlands, New Zealand, Korea, and others. It updates and replaces the NTIA minimum elements published in 2021, incorporating feedback from a 2025 public comment period.
Several changes in that update matter for how teams should think about SBOMs in practice.
Coverage replaced depth, and there is no minimum depth. The 2021 guidance covered top-level dependencies only — a limitation the authors describe as reflecting the tooling of the time rather than the information actually needed. The 2026 guidance states that an SBOM should include all components that make up the target software including transitive dependencies. The rationale is operational: a recipient should be able to conclude that a newly reported vulnerability does not affect them if the component is absent from the SBOM, and that conclusion is only valid if the SBOM is complete.
Integrity and licensing joined the baseline. Component Hash Value, Component Hash Algorithm, and Component License are new elements — the first two so a recipient can validate that the component described is the component received.
The document itself became attestable. SBOM Author Signature is new, along with SBOM Data Format Name and Version, Tool Name and Version, SBOM Version, and SBOM Generation Context — the last capturing whether the SBOM was produced before build, at build, or after build, because component data legitimately differs between those phases.
Unknowns must be explicit, and withholding is distinguished from ignorance. Authors should state whether missing information is unknown or deliberately withheld, and organizations may treat an SBOM as incomplete if essential component data is withheld.
On formats, the guidance is deliberately non-exclusive: it identifies SPDX and CycloneDX as the two data formats currently widely used to generate and consume SBOMs, both products of open international processes and both machine-processable and human-readable. SPDX is maintained through the Linux Foundation and has been standardized through ISO; CycloneDX is maintained by the OWASP Foundation and Ecma International's TC54 and is published as ECMA-424, with its 1.7 specification ratified as the standard's second edition in December 2025. The guidance recommends supporting widely used, open, interoperable formats and avoiding deprecated versions — not picking a winner.
Regulation is now pulling in the same direction. The EU Cyber Resilience Act (Regulation (EU) 2024/2847) entered into force in December 2024 and phases in over roughly three years: obligations for conformity assessment bodies applied from 11 June 2026, reporting obligations for actively exploited vulnerabilities and severe incidents apply from 11 September 2026, and the bulk of the essential requirements — including the requirement to document components in a machine-readable SBOM covering at least top-level dependencies as part of technical documentation — apply from 11 December 2027. The sequencing is worth noticing: the reporting obligation arrives first, and a manufacturer cannot report which of its products are affected by an exploited vulnerability within a tight window unless it already knows what is inside them.
What an SBOM genuinely enables:
- Inventory — a durable, per-artifact record of composition.
- Incident response — when a widely used component is found to be exploitable, the difference between answering "are we affected, and where?" in minutes versus days.
- Vulnerability mapping — automated correlation between components and advisory data.
- Customer and procurement requirements — increasingly a contractual expectation, and in the EU a regulatory one.
- Compliance and audit workflows — evidence that composition is known and managed.
- Acquisition review — during diligence, knowing what a target company actually ships.
What an SBOM does not do:
It does not tell you whether the software is secure. It does not tell you whether a listed vulnerability is exploitable in your configuration. It does not tell you whether the components are well-maintained, appropriately licensed for your use, or correctly configured. It does not update itself. And it does not, by itself, cause anyone to do anything.
The False Comfort Problem
Here is a situation that occurs regularly: an organization generates SBOMs automatically for every build, stores them, can produce them on request — and has weak supply-chain security.
How? Because inventory without a consuming process is documentation, and documentation does not defend anything.
The questions that reveal whether an SBOM program is real:
- Who reads the output? Not who receives the alert — who is accountable for a decision resulting from it?
- Who owns remediation, per component, and how is that ownership recorded anywhere other than in a person's memory?
- How quickly is a newly disclosed issue in a critical component assessed — hours, days, or the next quarterly review?
- Are build and development dependencies included, or only what ships? The pipeline is part of the attack surface.
- Are container contents included, or only the application manifest? OS packages are usually the largest population by count.
- Are infrastructure modules and cluster-resident components included? Almost never, in practice.
- Are artifacts mapped to deployments? An SBOM for a build is useful. Knowing which build is running in which environment, right now, is what makes it actionable.
- Is the inventory current? The guidance is that each software version or update should have an associated SBOM, and that a new build integrating updated components should produce a new SBOM. An inventory that lags the deployment is an inventory that will mislead you during an incident.
- Can you query across it? "Which of our products contain component X at any version below Y" should be a query, not a project.
The principle worth keeping:
Visibility is a capability. Action is a process.
Neither replaces the other. An organization with excellent inventory and no ownership will produce beautiful reports about problems it does not fix. An organization with strong response habits and no inventory will respond quickly to the things it happens to notice, and remain unaware of the rest. The value is in the connection, and the connection is the part that requires organizational design rather than tool procurement.
SBOM, Scanner, and Advisory Data Are Three Different Things
These get conflated constantly, including in vendor material, and the conflation makes it hard to reason about what a given tool actually tells you.
| SBOM | Dependency scanner / SCA | Vulnerability database or advisory source | |
|---|---|---|---|
| What it is | A document describing composition | A tool that analyzes composition and matches it against risk data | A curated body of information about known flaws |
| Produced by | A generation tool, at a point in the lifecycle | A vendor or open-source project | CNAs, maintainers, national agencies, researchers, vendors |
| Answers | "What is in this artifact?" | "What does the current risk data say about what is in this artifact?" | "What is known about this flaw?" |
| Freshness | As of generation time | As of the last scan and the last data sync | Continuously changing |
| Fails when | It is incomplete, stale, or not tied to a deployed artifact | Component identification is wrong, or matching is imprecise | Data is missing, delayed, unenriched, or wrong |
| Does not tell you | Whether anything is wrong | Whether a finding is exploitable in your context | Whether you are affected at all |
Scanners deserve a specific note, because their output is the primary interface most teams have with this whole domain, and their failure modes are systematic rather than random.
Scanners can produce useful findings: a known-exploited flaw in a component that sits directly in your request path, with a fixed version available, is exactly the signal the tool exists to produce.
Scanners also produce noisy findings, for structural reasons:
- Identification ambiguity. Matching a component to advisory records depends on identifiers, and identifier ecosystems are imperfect. The same component can be named differently across sources; a version string can be interpreted several ways; a vendor backport can fix a flaw without changing the version number that the scanner reads.
- Presence versus use. A container image contains a shell, a compression library, and a certificate tool. The application never invokes them. The scanner reports them, because it can see the files.
- Development-only components. Findings in test frameworks and build tooling are reported alongside production components with no distinction of consequence.
- Duplicate paths. One component reachable through eight dependency paths can produce eight findings that are one problem.
- Advisory data quality. Which is now a bigger issue than it was, and is worth a section of its own.
The right response is not to disable scanning. It is to stop treating a finding count as a risk measure, and to build triage that incorporates position, privilege, exposure, and exploitability — which is the subject of the next section.
A Vulnerable Component Present Is Not a Vulnerability Exploitable
This is the most consequential technical nuance in the whole subject, and it is where security and engineering most often talk past each other.
Two claims that sound similar:
"A component with a published vulnerability is present in our artifact." This is an inventory fact. It is verifiable, usually automatable, and it is what a scanner reports.
"An attacker can exploit that vulnerability against our product as deployed." This is a claim about reachability, configuration, and threat, and it requires analysis that no inventory alone can perform.
The gap between them is enormous, and it runs in both directions. A component can be present, listed as vulnerable, and genuinely unexploitable — because the vulnerable function is never called, because the vulnerable code path requires a configuration you don't use, because the input required to trigger it never crosses your boundary, or because a control in front of it prevents the precondition. A component can also be present, not listed as vulnerable, and genuinely dangerous — because the flaw has not been disclosed, or was fixed silently without an advisory, or affects your usage in a way the advisory does not describe.
Nothing here argues for minimizing vulnerabilities. It argues for spending finite remediation capacity where it changes outcomes.
The vocabulary, stated accurately
CVE is an identifier. A CVE ID names a specific publicly disclosed vulnerability so that everyone can refer to the same thing. It is not a severity, not a score, and not a statement that you are affected.
CVSS, maintained by FIRST, communicates the characteristics and severity of a vulnerability. The current version is CVSS v4.0, released on 1 November 2023, organized into four metric groups: Base, Threat, Environmental, and Supplemental. Two properties are routinely misunderstood. The Base score describes intrinsic technical characteristics in isolation — it says nothing about whether anyone is attacking it or whether your deployment is reachable. The Threat and Environmental groups exist precisely to add that context, and are the metrics most organizations never apply, leaving them to decide from the one score never intended to be used alone.
EPSS, also from FIRST, estimates the probability that a vulnerability will be exploited in the wild within the next 30 days. That is a different quantity from severity: a technically severe flaw with no exploitation activity and a moderate flaw under active mass exploitation are different operational problems. The model is revised periodically and scores republished frequently, so EPSS should be consumed as a live feed rather than recorded once at triage.
CISA's Known Exploited Vulnerabilities (KEV) catalog lists vulnerabilities with reliable evidence of active exploitation. Inclusion requires three things: a CVE ID, clear remediation guidance, and reliable evidence of exploitation in the wild. KEV originated under Binding Operational Directive 22-01, revoked and superseded in June 2026 by BOD 26-04, "Prioritizing Security Updates Based on Risk," which carries the catalog criteria forward while integrating them with other patching-timeline decision points. The directive binds US federal civilian agencies; CISA's guidance to everyone else has consistently been to use KEV as an input to prioritization rather than as the sole criterion.
VEX — Vulnerability Exploitability eXchange — is the mechanism for asserting, in machine-readable form, whether a specific product is actually affected by a specific vulnerability. Under CISA's minimum requirements, a VEX statement conveys one of four statuses: affected, not affected, fixed, or under investigation. Where the status is "not affected," the statement must carry either a machine-readable justification or a free-text impact statement explaining why. The justification labels published by the VEX working group cover the situations that actually arise in practice: the component is not present; the vulnerable code is not present; the vulnerable code is present but not in the execute path; the vulnerable code cannot be controlled by an adversary; or inline mitigations already exist.
VEX is not a way to make findings disappear. It is a way to record an analysis once, in a form that tooling and customers can consume, so that the same triage is not repeated by every downstream party. Its value scales with how many people are asking you the same question — which, for any company selling to enterprises, is a growing number.
Advisory data is not a solved input
An assumption underlies most vulnerability programs: that when a flaw is disclosed, authoritative enriched data about it will exist promptly. That assumption has weakened, and teams building process on top of it should know why.
In April 2026, NIST announced a change to how the National Vulnerability Database operates, citing a 263% increase in CVE submissions between 2020 and 2025 and stating plainly that it has been unable to clear the enrichment backlog that developed starting in early 2024. Under the new model, NIST enriches CVEs meeting specific prioritization criteria — including those in the KEV catalog, those affecting software used within the federal government, and those affecting critical software categories as defined under Executive Order 14028 — while all other submissions are still listed but categorized as "Not Scheduled." Backlogged CVEs published before 1 March 2026 were moved into that category.
The practical consequences are concrete. Absence of a severity score is not absence of risk — "Not Scheduled" describes NIST's queue, not the flaw. Single-source vulnerability programs are now fragile, because vendor advisories, ecosystem advisory databases, maintainer release notes, and regional databases such as the EU's all carry information that may not appear in one place. Maintainer advisories deserve more weight than they used to: a security release is actionable regardless of whether an enriched record exists anywhere. And not everything security-relevant gets a CVE at all — silent fixes in release notes are common, which argues for tracking upstream releases of privileged components rather than waiting for advisories about them.
Prioritization inputs that actually discriminate
Given all of the above, a defensible triage considers:
- Exposure — is the affected surface reachable from outside, from authenticated users, or only from internal systems?
- Execution path — is the vulnerable function called at all? Reachability analysis, where tooling supports it, is the highest-value noise reduction available.
- Privilege — what does the component have access to if it misbehaves?
- Data sensitivity — what flows through it?
- Exploitation evidence — is it in KEV, is exploit code public, what does EPSS suggest?
- Compensating controls — what stands between an attacker and the precondition?
- Fix availability and cost — is there a patched version, and what does moving to it require?
And a caution against a tempting shortcut: do not invent a proprietary composite score. Organizations that build their own scoring formula usually end up with a number nobody can interpret, that cannot be compared with anyone else's, and that encodes assumptions made once and never revisited. Use the established signals, apply your own context explicitly, and document the reasoning — that is what VEX statuses exist to capture.
The Patch-Everything Problem
Security teams want exposure reduced quickly. Product teams want production to keep working. These goals are frequently framed as a conflict, and they are not — but they do impose different failure costs, and pretending otherwise produces bad process.
Consider "patch everything immediately, automatically" as an actual policy. It reduces the window of exposure for known flaws, which is real value. It also means that any upstream release — including a bad one, including a malicious one — reaches production quickly, that behavioral changes arrive without anyone reading a changelog, and that when something breaks, the change set is large and undifferentiated. Automated updates are not the same as safe updates.
Now consider the opposite: a quarterly patch window with manual review of every change. Exposure windows stretch to months. Urgent fixes queue behind routine ones. And the batch size grows until each window is a high-risk event, which creates pressure to skip windows, which grows the next batch.
What actually works is a system with two speeds and clear rules about which applies:
Fast information, controlled change.
- Prioritization determines speed, not process. A fix for an actively exploited flaw in an internet-facing component should move on a path measured in hours. A minor version bump of a formatting library should move on a path measured in weeks, batched with others. Both use the same pipeline; they use different urgency tiers.
- Automated testing is the enabling constraint. The maximum safe patch velocity of an organization is a function of how much confidence its test suite produces per unit of time. This is the single largest lever, and it is usually treated as a quality concern rather than a security one.
- Staged upgrades. Pre-production, then canary, then full. For dependency changes specifically, the canary stage is where behavioral differences surface, because they often depend on production data shapes that test fixtures do not reproduce.
- Deployment monitoring tied to the change. Error rates, latency percentiles, and business-level signals compared against the pre-change baseline, with a defined observation window before proceeding.
- Rollback that is genuinely fast. The willingness to patch quickly is a direct function of confidence in reverting quickly. Teams that cannot roll back in minutes will not patch in hours, no matter what the policy says.
- Grouping strategy. Batching related updates reduces overhead; batching unrelated ones makes failures hard to attribute. Group by ecosystem and by blast radius, not by convenience.
The goal is not to slow security down. It is to build a delivery system in which moving fast does not require moving blind.
Who Owns Dependency Health?
Ask five people in an engineering organization who owns dependency updates and you will often get five answers, all of them plausible, none of them authoritative.
Security says they provide the findings and the policy, but they cannot merge changes into product code. Developers say they will handle it when it is prioritized, which requires someone to prioritize it against feature work. Platform says they own base images and the pipeline, not what application teams declare. DevOps says they deploy what they are given. QA says they test what they are asked to test. In organizations with an open-source program office, that office may own license policy and community engagement but not remediation.
Everyone is describing their part correctly. The gap is between the parts.
"Everyone owns it" reliably means nobody does, because dependency work has three properties that make it fall through organizational cracks: it is never urgent until it is critical, it produces no visible feature value, and it crosses team boundaries in a way that makes assignment ambiguous.
Rather than prescribing an org chart — models legitimately vary with size, structure, and regulatory context — the useful move is to make ownership explicit along the lines that already exist:
- Application teams own the dependencies they declare. If a team added it, that team decides whether to keep it, when to update it, and whether it still earns its place.
- Platform engineering owns the shared substrate: base images, runner images, pipeline templates, the internal registry or proxy, and the rebuild cadence for services whose code has not changed.
- Security owns policy, tooling, risk guidance, threat context, and the triage of what must move quickly. It sets the standard and provides the information; it does not merge the pull request.
- Quality engineering owns the question of whether the product still behaves correctly after a dependency change — which is a different question from whether the build passed, and is discussed in detail next.
- DevOps/SRE owns what is actually deployed, the ability to observe change in production, and the ability to reverse it.
- Legal and procurement engage where licensing or vendor obligations become material.
Two mechanisms make explicit ownership durable. First, codeowners-style mapping applied to manifests and pipeline definitions, so that a dependency change automatically routes to the accountable team. Second, an agreed service level for classes of change — what response time applies to an actively exploited flaw versus a routine version bump — so that urgency is a shared standard rather than a negotiation held during every incident.
Testing What the Update Actually Changed
"QA should test dependency updates" sounds correct and is nearly useless, because it does not say what to test, and testing everything on every update is not viable at any meaningful dependency count.
The more precise question: what can a dependency update change? Behavior in edge cases. Output formatting — dates, numbers, currencies, sorting, precision, locale rendering. API semantics — nullability, error types, defaults, removals. Serialization — field ordering, encoding, escaping, how absent values are represented. Performance and concurrency characteristics. Browser and platform support matrices. Error handling, where what used to warn now throws. Security controls — TLS defaults, cipher selection, cookie attributes, certificate validation strictness. And the transitive graph itself, since new packages arrive with the update.
Few of those are caught by "the build passed." Compilation and unit tests verify that code still fits together. They frequently do not verify the properties above, especially when tests mock the dependency in question — a mocked HTTP client cannot tell you that the real one changed its redirect behavior.
This suggests a practice worth naming: dependency-aware regression. Verification scope should follow the dependency's behavioral reach rather than a fixed template. In practice that means maintaining, for components that matter, a rough map from component to affected product behavior, and letting that map drive test selection.
| Dependency category | What genuinely needs verification | What is often skipped and shouldn't be |
|---|---|---|
| Date/time, currency, locale | Formatting and arithmetic across time zones, DST boundaries, month ends, rounding | Financial outputs: invoices, prorations, statements |
| Serialization / API layer | Byte-level response comparison against recorded contracts | Backward compatibility for existing clients and integrations |
| HTTP / networking | Timeouts, retries, redirects, TLS negotiation, proxy behavior | Failure-mode behavior, not just the happy path |
| Authentication / authorization | Token validation, expiry, signature verification, session lifecycle | Explicit tests that access is denied where it should be |
| Parsers (files, images, documents) | Malformed and adversarial inputs, size limits, resource consumption | Anything beyond a small set of well-formed fixtures |
| ORM / data access | Query generation, transaction semantics, migrations, connection handling | Behavior under concurrency and at realistic data volume |
| Frontend framework / UI libraries | Rendering, accessibility semantics, events across supported browsers | Older supported browsers and assistive technology |
| Cryptography | Interoperability with existing stored data and with counterparties | That previously encrypted or signed data still verifies |
| Build tooling / bundlers | Artifact contents, size, and equivalence to previous output | Whether the produced artifact actually changed, and how |
| Infrastructure modules | Plan diff review, policy evaluation | The effect on permissions and network exposure |
Two techniques earn their cost for high-value dependencies. Contract tests against the real component: where a mock stands in, add a small suite that exercises the actual library and asserts the properties your code relies on, so that a change produces a targeted failure instead of a mysterious production incident. And golden-output comparison: for anything producing documents, exports, invoices, reports, or API payloads, record known-good outputs and diff them across dependency changes. That catches the entire class of silent formatting drift unit tests structurally cannot see.
A Security Patch Can Still Break the Product
The two preceding sections meet here, and the intersection is where organizations get hurt.
A dependency update can simultaneously fix a genuine vulnerability and change behavior the product depends on. These are not separable events — it is one release, containing both. Which means the common organizational arrangement, where security patching runs as a compliance workflow and functional verification runs as a delivery workflow, guarantees that at some point a required security update will ship without adequate verification, or an adequate verification process will delay a required security update.
The tension is real and should be named honestly:
Security urgency says the exposure window should be as short as possible, and that a known-exploited flaw in an internet-facing component is an emergency.
Change confidence says that shipping unverified changes to production is how outages happen, and that an outage caused by a rushed patch is a real cost, not a hypothetical one.
The resolution is not to pick a side. It is to invest in the thing that dissolves the trade-off: a delivery system capable of applying changes quickly with evidence.
Concretely, that means:
- Verification that is fast enough to run on the urgent path, not only on the weekly one. If the full suite takes six hours, define a risk-targeted subset that runs in fifteen minutes and covers what this dependency touches.
- Progressive delivery as the default for dependency changes, so the first exposure to production reality is small and observable.
- Monitoring that is specific enough to detect a behavioral regression rather than only a crash — business-level signals, not just error rates.
- Rollback capability that is exercised routinely, not theoretically available.
- A standing agreement that for the highest-urgency class, the product ships with the patch and with elevated monitoring, and the team accepts a higher probability of a small, quickly reversible regression over a longer exposure window.
The last point is a leadership decision, not an engineering one, and it should be made in advance rather than argued about during an incident.
Supply-Chain Testing Is Not One Kind of Test
It is a stack of distinct verification activities, each answering a different question, each owned by a different part of the organization. Collapsing them into "we scan our dependencies" loses most of the value.
INVENTORY Do we know what exists?
│
▼
INTEGRITY Did we get exactly what we expected?
│
▼
SECURITY ANALYSIS Are known issues present, and do they matter here?
│
▼
FUNCTIONAL Does the product still behave correctly?
│
▼
INTEGRATION Do connected systems still agree?
│
▼
PRODUCTION Is real behavior healthy after the change?
│
▼
(back to inventory — the artifact just changed)
Inventory — generation of complete, per-artifact composition data covering application dependencies, container contents, build-time components, and infrastructure modules. Owned by platform and security tooling; verified by whether it can answer a question under time pressure.
Integrity — lockfile enforcement, integrity hash verification, digest-pinned images and actions, provenance attestation generation and verification, signature checking at admission. Owned by platform engineering. The distinguishing question: is verification enforced, or merely available?
Security analysis — scanning, advisory correlation, reachability analysis where supported, triage against exposure and exploitation evidence, VEX assertions for what has been analyzed. Owned by security, executed continuously.
Functional verification — dependency-aware regression as described above. Owned by quality engineering, and the part most often missing from supply-chain programs, which tend to be designed by security teams for security outcomes.
Integration verification — contract tests against dependent services, partner integrations, mobile clients, and anything consuming your API. Dependency changes that alter serialization or error semantics break other people's software before they break yours.
Production observation — monitoring specific enough to attribute a behavioral change to a dependency update, retained long enough to compare against a baseline, and connected to the deployment record so that "what changed?" has an answer.
The loop matters more than the list. Each pass through it produces a new artifact, which resets inventory. A supply-chain program that runs these activities once, or runs them in disconnected quarterly cycles, is measuring a system it is no longer looking at.
The Software Trust Map
Here is a framework worth building once and maintaining deliberately — not for every component, but for the few dozen whose failure would be consequential.
For each, record: what it is; where it comes from and who produces it; who can cause a new version to exist; how it enters the build; what it can reach; what data flows through it; how a new version reaches production and how long that takes; how you would notice it misbehaving; and what removal would require.
| Component | Origin | Who can change it | How it enters the build | Privilege | Data touched | How it updates | How you'd detect a problem | Removal difficulty |
|---|---|---|---|---|---|---|---|---|
| Serialization library (direct package) | Public registry, community project | Maintainers with publish rights | Declared in manifest, locked | In-process, full application privileges | Every API request and response | Automated PR, batched, tested | Contract tests; API consumer reports | Moderate — used broadly but mechanically replaceable |
| Transitive parser five levels deep | Public registry, unknown maintainer | Anyone with publish rights on that package | Pulled in by a parent | In-process, full application privileges | Untrusted uploaded content | Only when a parent updates, or via override | Fuzz/regression on parsing paths; error-rate anomalies | Hard — requires changing or removing the parent |
| Third-party CI action | Public action registry | Action author; anyone with repo write access if tag-referenced | uses: in a workflow |
Build secrets, registry credentials, artifact write | Source code, credentials | Digest pin updated by automation | Egress anomaly from runners; unexpected artifact diff | Easy — usually a few lines |
| Container base image | Distribution / vendor registry | Image publisher | FROM line |
Whole runtime environment | Everything in the process | Scheduled rebuild + redeploy | Image scanning; runtime behavior after rebuild | Moderate — a base image swap is a compatibility project |
| Payment SDK | Vendor | Vendor | Declared dependency + vendor's servers | Payment data, credentials | Cardholder-adjacent data | Vendor releases; contractual notice | Transaction failure rates; vendor status | Hard — contractual, certification, and integration cost |
| Hosted model endpoint | AI provider | Provider | Runtime network call | Prompt content and returned output | Whatever you send it | Provider-side; identifiers may be deprecated | Output evaluation suites; latency and refusal rates | Moderate — abstraction helps, prompt tuning does not transfer cleanly |
| Terraform module | Public module registry | Module author | Referenced in IaC source | Cloud IAM and networking | Infrastructure configuration | Version bump + plan review | Plan diff; policy-as-code violations; drift detection | Easy to swap, hard to unwind resources already created |
| Identity provider | Vendor | Vendor | Runtime integration + SDK | Authentication for every user | Identity data | Vendor-side; SDK versions in manifest | Login success rates; token validation failures | Very hard — migration touches every user |
No scores. The value is in the columns, because the columns are what you act on: when something goes wrong with any of these, the response plan is largely legible from the row.
The most useful discovery for most teams is not any individual row. It is noticing which rows have "unknown" in the detection column — components whose misbehavior nothing currently in place would catch.
The Control Spectrum
A companion framework, for reasoning about architecture rather than incidents.
Every component sits somewhere on a spectrum of control, and the point of locating it is awareness, not judgment.
Fully controlled — written and operated internally. You can change it whenever you like, and you are responsible for everything about it, including its defects and its maintenance forever.
Strongly governed — external, but subject to a controlled process: pinned versions, verified integrity, reviewed updates, a known owner, tested changes. You do not write it, but nothing about it changes without passing through your system.
Partially controlled — external, observable, replaceable, but not modifiable. You can watch what it does, wrap it, test around it, and swap it out with effort, but you cannot change its behavior.
Low-control dependency — a capability where you have limited influence over implementation, roadmap, or change timing. You can choose whether to use it and can build abstractions around it, but its evolution is not yours.
| Level | Typical examples | What you control | What you don't | Failure mode to plan for |
|---|---|---|---|---|
| Fully controlled | Business logic, internal services and libraries | Everything | Nothing | Your own defects; key-person knowledge concentration |
| Strongly governed | Pinned direct packages, digest-pinned base images and actions, vendored forks | Version, timing, verification, rollback | The component's internal design and direction | Upstream stops maintaining, forcing a fork or migration |
| Partially controlled | Managed database engines, hosted CI platforms, most SDKs | Configuration, usage patterns, abstraction boundaries | Implementation, release timing, deprecation schedule | Deprecation or behavior change on the provider's schedule |
| Low control | Foundation model APIs, payment networks, identity providers, app store policies | Whether to use it; how deeply to couple to it | Almost everything else | Pricing, policy, or capability changes that alter your product |
The essential clarification: low control does not mean bad. Some of the best engineering decisions available are low-control ones. Using a managed relational database instead of operating your own removes an entire discipline from your hiring plan. Using an established payment provider removes a compliance program most companies should not attempt. Using a hosted identity provider removes a category of security defects that has embarrassed far more capable organizations than yours.
The purpose of locating a component on this spectrum is to know what you are buying and what you are giving up — so that the coupling can be designed intentionally, the abstraction boundary placed deliberately, and the exit path considered before it is needed rather than during a pricing renegotiation.
Build, Buy, Adopt, or Depend
"Build versus buy" is a two-option framing for a decision that usually has six or seven options, each transferring a different bundle of work and risk.
- Build — write and operate it internally. You take all the development work, all the maintenance, all the operational burden, and all the security responsibility, and you get complete roadmap control.
- Buy — license commercial software you run yourself. Development transfers; operations mostly stay; you inherit the vendor's release cadence and support terms.
- Adopt open source — take a community component. Development transfers, maintenance is shared but ultimately unowned, operations stay with you, and you retain the option to fork if the project's direction diverges from yours.
- Use a managed service — the provider runs it. Operations transfer almost entirely, security responsibility splits along a shared-responsibility boundary you should read carefully, and roadmap control goes to zero.
- Call an API — capability over the network, with no code in your process. Integration and failure handling stay with you; everything else leaves.
- Embed an SDK — vendor code executing inside your application, which means vendor code with your process's privileges. This is the option teams underestimate most, because it feels like buying but behaves like adopting.
- Use a generated component — code produced by a tool or model, which becomes authored code you own from the moment it merges.
Each option answers "who does the work?" differently — and, crucially, "who is responsible when it breaks at 3 a.m.?" differently too. There is no universally correct choice. What is consistently wrong is making the choice without noticing which one you made: teams that "bought" a capability and discovered later that they had embedded a vendor's code, with a vendor's dependency tree, into their production process.
What Actually Deserves to Be Built In-House
The supply-chain conversation has a failure mode: reading it as an argument for self-sufficiency. It isn't. Rebuilding mature capabilities internally usually produces worse security, not better, along with slower delivery and higher cost. An internally written cryptography implementation, TLS stack, authentication protocol, or parser for a complex format is very likely to be less safe than a widely deployed one that thousands of adversarial eyes have already attacked.
In-house ownership earns its cost in a short list of situations:
Highly differentiating business logic. If it is why customers choose you, own it. Outsourcing differentiation is how products become interchangeable.
Specialized behavior no external component models well. When the available options require so much adaptation that you are effectively maintaining a fork with extra steps, owning the abstraction may be cheaper than fighting one.
Cases where a vendor constraint directly limits the product. If a dependency's roadmap, rate limits, data residency, or pricing model is a hard ceiling on what you can sell, that dependency has become a strategic constraint rather than a technical one.
Where replacement risk is unacceptable and no alternative exists. A single-source dependency with no viable substitute, in a position where failure is existential, may justify building an escape hatch — often a thin internal abstraction plus a tested fallback rather than a full replacement.
Even then, nuance applies. "Build it in-house" often really means "build a thin internal layer over external components, so that swapping them is a contained project." That is usually a better answer than either extreme.
The Dependency Budget
A mental model, offered as an article-specific framing rather than a metric: treat dependencies as a budget rather than a count to be minimized.
Every dependency provides leverage — work you did not have to do, expertise you did not have to hire, correctness you did not have to discover. Every dependency also consumes budget in the form of ongoing obligations:
- update responsibility
- security monitoring and triage capacity
- compatibility risk across runtime and platform changes
- license awareness
- build complexity and time
- operational dependence during incidents
- the review attention required to notice when its situation changes
A company should not minimize dependency count at all costs — a team that reimplements mature functionality to keep its manifest short has converted a managed obligation into an unmanaged one. The right question is comparative: does this dependency deliver enough value to justify the responsibility it introduces?
For a well-maintained cryptography library: obviously yes, by an enormous margin. For a three-line utility that does something the standard library does: probably not, because the responsibility is nonzero and the value is near zero. For a framework: yes, but the budget consumed is large, and the decision deserves the seriousness of an architectural commitment rather than the ceremony of an install command.
The budget framing also explains something teams find puzzling: why adding a dependency feels free and removing one feels expensive. The cost is not paid at adoption. It is paid continuously, in small amounts, by people who were not in the room.
Four Questions Before Adding a Dependency
For any dependency that will occupy a meaningful position — not every utility, but anything in a request path, a security decision, a data transformation, or the build:
1. What work are we avoiding by adopting this? Name it specifically. "Weeks of implementation plus ongoing correctness burden for time-zone arithmetic" is a real answer. "It's convenient" is not. If the avoided work is small, the trade is probably bad.
2. What trust are we introducing? Who can cause new versions to exist? What will this code be able to reach — credentials, customer data, untrusted input, the build environment? How many indirect relationships arrive with it?
3. What will keep this component healthy over time? Is the project active? Is there a security reporting path? Who updates it here, and how will they know when they need to? A dependency with no update owner is a dependency that will age until it becomes an incident.
4. How painful would removal become? If this turns out to be wrong in eighteen months, what does exit look like? Dependencies that spread through the codebase — because their types leak into your interfaces, or their idioms shape your architecture — are far harder to remove than their install size suggests. This is the question most often skipped, and the one whose answer costs the most later.
Five Questions After a Dependency Has Been There Two Years
The same component, different perspective. Adoption is a decision; continuation should also be one.
1. Do we still need it? The feature that motivated it may be gone. The standard library may have grown the capability. Usage may have shrunk to two call sites.
2. Is it still maintained? Not "is the repository present" — are issues answered, are releases shipping, is there evidence of anyone home?
3. Has its ownership or governance changed? New maintainers, new organization, new release process, new publishing arrangement, new license.
4. Are we still on a supported and current path? Is our major version still receiving fixes? Is there an upgrade route that is still open, or has the gap grown to the point where migration is now a project?
5. Would we choose it again today? The most clarifying question available. Alternatives improve, the ecosystem consolidates, and a decision made against the options of three years ago should not be treated as permanent.
Applying these to the top few dozen components once or twice a year is not bureaucracy. It is the mechanism that prevents an inventory from becoming an archaeology site.
The Software Supply-Chain Review
The practical framework. Ten areas, each with questions worth asking, what weak visibility looks like, and what mature practice looks like. This is not a compliance checklist — the point is to find the places where nobody can answer, because those are the places where risk lives undetected.
1. Inventory
Can we produce a complete list of components for a specific deployed artifact? Does it include container contents and build-time dependencies? Is it generated automatically on every build? Can we query across all products for a single component?
Weak: the manifest is treated as the inventory; container and pipeline contents are not enumerated; answering "where do we use X" requires engineers to grep repositories. Mature: per-artifact SBOMs generated automatically, covering application dependencies, OS packages, and build tooling, stored and queryable, and linked to what is currently deployed.
2. Provenance
For any artifact in production, can we identify the source revision, the build process, and the environment that produced it? Is provenance verified at deployment, or only generated?
Weak: artifacts identified by tag; provenance either absent or produced and never checked. Mature: builds emit provenance automatically; deployment admission verifies it against expectations; artifacts are referenced by digest end to end.
3. Direct dependencies
Who approved each one? Is there an owner? Is adding a dependency a reviewable decision? Do we know which are in privileged positions?
Weak: manifest entries with no history, no owner, and no record of why. Mature: ownership mapped to teams, new direct dependencies reviewed as decisions, a maintained list of components in security-critical positions.
4. Transitive dependencies
Do we know the full graph? Can we detect when it changes? Can we override or exclude a specific transitive package if we must? Is resolution deterministic?
Weak: lockfiles absent or not enforced; graph changes discovered when something breaks. Mature: lockfiles committed and enforced in CI; graph diffs surfaced in dependency pull requests; override mechanisms understood before they are needed.
5. Build system
What third-party code executes during a build? Is it pinned immutably? What credentials are present in the build environment? Can untrusted input reach a privileged workflow?
Weak: actions and plugins referenced by mutable tags; long-lived secrets in the environment; no separation between untrusted-trigger and privileged workflows. Mature: digest-pinned tooling; short-lived federated credentials; strict trigger and cache boundaries; workflow changes reviewed as production changes.
6. Artifacts and containers
Are images referenced by digest? How often are base images rebuilt for unchanged services? Are image contents scanned? Is anything verified before it runs?
Weak: :latest or floating tags; images rebuilt only when application code changes; scanning results reviewed occasionally. Mature: digest-pinned base images updated by automation; scheduled rebuilds independent of code change; admission control requiring signature and provenance.
7. Privileged tooling
Which third-party components can reach secrets, publish artifacts, or modify infrastructure? Who reviews changes to them? What would a compromise of each one enable?
Weak: nobody has enumerated them; pipeline and IaC dependencies fall outside every inventory. Mature: a maintained list of privileged third-party components with owners, pinning, and review requirements proportional to what they can reach.
8. Vulnerability management
How do we learn about a new issue? How quickly is a critical one assessed? Who decides urgency? Do we distinguish present from exploitable? Do we track more than one advisory source?
Weak: a scanner producing a queue that grows; severity taken from a single score; no distinction between reachable and unreachable findings. Mature: multiple advisory sources; triage incorporating exposure, reachability, and exploitation evidence; documented urgency tiers with response targets; analysis recorded in a reusable form.
9. Licensing
Do we know the license of everything we ship, including transitively? Is attribution generated from the actual graph? Is there a policy, and is it enforced automatically? What happens when a license changes?
Weak: attribution assembled manually and out of date; license questions answered during due diligence rather than before it. Mature: license data captured in the SBOM, policy enforced in CI, attribution generated from the build, changes flagged on major upgrades.
10. Ownership and update process
Who owns dependency health for each area? What is the target time to ship a critical patch, and have we measured it? Can we roll back? Is verification good enough to allow moving quickly?
Weak: ownership implicit; patch timelines unmeasured; nobody knows how long a critical update would actually take. Mature: explicit ownership by layer; agreed response targets by urgency class; measured and rehearsed patch delivery; dependency-aware verification that makes speed safe.
The Founder Version: Seven Questions
If you read none of the above and want a way to assess this in a thirty-minute conversation with your engineering leadership, ask these. What you are listening for is not a perfect answer — it is whether the answer exists at all, and whether the person answering can point at a system rather than a person's memory.
1. Do we know what actually ships with our product? Not what we wrote. Everything inside the thing customers use, including the operating-system components in our containers and the tools that built it. If the answer requires a week of work, that is the finding.
2. Which external component has the most privilege inside our build or runtime? This question is more diagnostic than any inventory number. A team that can answer it has thought about position and privilege. A team that starts listing the biggest libraries has been thinking about size.
3. How do we learn when a critical dependency becomes vulnerable or abandoned? Is there a channel, does someone read it, and does reading it lead to action? "We'd hear about it" is not a mechanism.
4. How quickly can we update a dependency without losing confidence in the product? The honest answer is a measurement, not an aspiration. This number is the ceiling on your security responsiveness, and it is set by your testing and deployment systems, not by your security team.
5. Can we identify exactly which production versions contain a particular component? This is the question you will be asked — by a customer, a regulator, or your own security team — within hours of the next widely publicized flaw. Under the EU Cyber Resilience Act, manufacturers selling into the EU face reporting obligations for actively exploited vulnerabilities from September 2026, on timelines that make manual investigation infeasible.
6. Which dependencies would be extremely difficult to replace? Identity, payments, the primary database, the cloud provider, possibly a model provider. These are strategic dependencies, and they belong in board-level risk discussions rather than in a scanner's output.
7. Who owns this process? If the answer is "everyone," the answer is nobody. If the answer is a name, ask that person what their target response time is for a critical patch and whether they have measured actual performance against it.
What Good Does Not Look Like
Several signals get treated as evidence of maturity. Each is useful. None, alone, indicates control.
"We run dependency scanning." Scanning produces findings. Maturity is what happens to them: whether they are triaged by exploitability, whether ownership is assigned, whether the queue shrinks. A large permanent backlog of unreviewed findings is worse than no scanner, because it creates the impression of coverage while training everyone to ignore alerts.
"We generate SBOMs." For every build, automatically, in a current format — genuinely good. But an SBOM that is not linked to what is deployed, not queryable, and not consumed by anyone is an artifact produced to satisfy a request, not a capability.
"We use containers." Containers package dependencies; they do not reduce them. An unrebuilt image is a snapshot of the vulnerabilities that existed on the day it was built.
"We have security tooling." Tools produce information. Information without an owner and a decision path is cost without benefit.
"We update packages automatically." Automation without verification transfers change control to upstream maintainers. Automation with dependency-aware verification and staged rollout is a genuine capability. The difference is invisible in a tool inventory.
"We sign our artifacts." Signing establishes origin and integrity. It says nothing about whether the signed thing is safe. If signatures are produced but never verified at deployment, they are metadata.
"We're SLSA Build L3." A real achievement about how your artifacts are built — and explicitly not a statement about your dependencies, since a SLSA level applies to an artifact independently of the levels of what it contains.
Software supply chain maturity is not any of these individually. It is whether they are connected:
INVENTORY ──► PROVENANCE ──► RISK INFORMATION
▲ │
│ ▼
PRODUCTION ◄── CONTROLLED ◄── VERIFICATION
OBSERVATION UPDATE │
│ ▼
└────────────────────────── OWNERSHIP
Every arrow in that loop is a place where organizations break the chain. Inventory that never reaches risk information. Risk information with no owner. Ownership with no verification capacity, so updates stall. Updates that ship with no production observation, so nobody learns whether they worked. The individual capabilities are commodities; the connections are the engineering.
The Software Supply Chain Is an Engineering System, Not a Security Report
One structural observation to close the analytical portion.
Software supply-chain security is usually assigned to a security team, which then produces reports for other teams to act on. This arrangement fails predictably, because the boundaries of the problem do not match the boundaries of the org chart.
Development selects dependencies. Every meaningful supply-chain decision is made in a pull request by an engineer solving an immediate problem, usually without supply-chain context and usually correctly, given what they knew.
Platform engineering owns the machinery that turns source into artifacts — the pipelines, base images, registries, and rebuild cadence. Most build-integrity and provenance work lives here, and platform teams are typically measured on delivery throughput rather than on artifact assurance.
Security owns policy, threat context, and prioritization. It has the information and, usually, none of the merge rights.
Quality engineering owns whether the product still behaves correctly after a change — which is the constraint that determines how fast security fixes can actually ship. This is the connection most supply-chain programs miss entirely: patch velocity is a testing capability before it is a security capability.
DevOps and SRE own what is deployed and what is observed, which makes them the only function that can answer "what is actually running right now" and "did that change hurt anything."
Legal and procurement engage where licensing, contractual obligations, or regulatory duties become material — increasingly often, given the direction of both customer requirements and regulation.
A dependency update touches every one of these functions. This is why the software supply chain resists being owned by a single team: nobody owns it end to end, and it is nobody's primary metric. That is precisely why the work needs an explicit operating model rather than a quarterly report: named ownership per layer, agreed response targets, verification capacity treated as a security investment, and enough shared visibility that the same question gets the same answer regardless of who is asked.
The Product You Own Is Assembled From Things You Trust
Return to the deletion pass at the beginning.
Everything the company did not create itself was removed, and what remained was a specification without a machine — real, valuable, differentiating, and unable to run. That should now look less like an indictment and more like a description of how modern software works.
The productivity of a contemporary software company is built on decades of accumulated work by people who will never see the product: open-source maintainers who solved parsing and cryptography and networking so that nobody has to solve them again; cloud providers who turned data centers into API calls; framework authors who encoded architectural patterns into defaults; distribution maintainers who assemble and patch operating systems; standards communities who made interoperability possible; tool vendors who made compilation, testing, and deployment routine. Refusing that inheritance is not a software supply chain strategy. It is a way to build worse software more slowly, with more defects, using a smaller team's judgment in place of a large community's scrutiny.
The mistake was never depending on others. Software has always been assembled, and the assembly is the achievement.
The mistake is letting dependency become invisibility — treating the boundary of the repository as the boundary of responsibility, and discovering the rest of the system only when it changes without permission.
So the closing position is not "own more." It is this:
Every component in your product occupies a position, holds some privilege, touches some data, arrived through some path, and is controlled by someone. For the components that matter, those facts should be known before they are needed, written down somewhere other than an engineer's memory, and revisited on a schedule rather than on an incident.
You do not need to control everything you ship. You need to know what you are trusting, why that trust is reasonable, how it reaches production, how you would find out if it stopped being reasonable — and what you will do on the day it does.