← Compendium

It was secure yesterday, wasn’t it?

Security is often treated as a point in time: “We check at release.” Yet most risks do not arise because new code is written, but because what we know about dependencies shipped long ago changes. Security is therefore an attribute of operations, not of the build, and the growing gap between the state of the system and the state of knowledge is exactly what security debt amounts to in practice.

Security thought of as a point in time

When a team runs a product of its own, a certain pattern appears sooner or later. Security is treated like a point in time, when you look once and then carry on working. Checks happen at release, when a new feature is built in or when someone deliberately changes something. In between, the system counts as secure, because nothing has moved.

This view seems logical, because to us risk feels like change. As long as nobody touches the code, nothing seems able to happen. This is exactly where intuition leads us astray, because security does not only depend on what we changed last.

It is not the code that moves, it is the knowledge

A large share of the risks does not arise because new code is written today, but because what we know about dependencies shipped long ago changes. The code on the server sits still, while knowledge about it keeps moving. What looked unremarkable yesterday is a documented finding today, and what nobody knows about today will at some point become visible as a CVE, an advisory or an entire class of exploits.

That shifts the real question. It is not whether we have changed anything since the last release, but whether our picture of the libraries we use still matches what has since become public knowledge about them. This gap grows on its own, even when nobody works on the code.

A journey through three lockfiles

To make this tangible, I took a small security journey through time for a Node.js project. I took the last `package-lock.json` from three consecutive years, that is the states at the end of 2023, the end of 2024 and the end of 2025, and then checked these snapshots in 2026 with `npm audit` against today’s state.

The trick is that nothing changes in the old lockfiles. `npm audit` compares the pinned dependency tree against the current advisory database, so every check looks at the same old system, only with today’s knowledge. So I am not measuring how the code has changed, but how our knowledge about the same code has changed. Side by side, the three snapshots give the following picture.

Lockfile state Vulnerabilities critical high End of 2023 73 4 22 End of 2024 30 1 6 End of 2025 3 0 2 Early 2026 0 0 0
The same three lockfiles, checked in 2026 with `npm audit` against today’s advisories. The code in each snapshot is unchanged; only the knowledge about it has grown.

What the numbers really say

The interesting part is not that the numbers get smaller towards the more recent snapshot, but what they reveal about our perception. The obvious conclusion would be that a system becomes less secure over time. The more precise reading is the opposite: the 73 vulnerabilities in the 2023 state were there from the start. It just took time until they were discovered, described reproducibly and made usable in a database. A CVE is therefore less often a new problem than a new insight into an old reality.

Why the gap grows on its own

If the code sits still and the gap grows anyway, it is worth asking where that growth actually comes from. Three forces work together here, and none of them has anything to do with someone working on the product. The first is attention, because a widely used package is read, fuzzed and taken apart by ever more eyes over time, so a long-unnoticed hole eventually surfaces. The second is the maturing of attacks, because an abstract vulnerability turns over time into a finished, reproducible exploit that is usable even without deep knowledge.

The third force is the depth of the dependency tree, and in practice it is the most uncomfortable one. A modern Node project rarely ships only the handful of packages listed in its `package.json`, but several hundred transitive dependencies that nobody deliberately chose. Exactly these deep, never directly touched packages are the ones that age most quietly, because nobody keeps an eye on them and their risk can build up over years without ever becoming visible.

From this follows an important distinction that turns a long list of findings into a ranking in the first place. Not every reported vulnerability weighs the same, because there is a difference between vulnerable code actually running on a reachable path in production and code sitting in a part of a package you never call. A critical hole in a function your product does not use is in practice less urgent than a medium one right in the hot path. Draw that line and you turn the raw number of findings into a list you can actually work through.

Security is an attribute of operations

This is exactly the catch with security work that only starts at release. It treats security as a property of the build, although it is at least as much a property of operations. Anyone who only checks at release is not measuring whether the system is secure, but whether it looked unremarkable with the knowledge of the time. In production, however, the same system runs day after day against today’s knowledge.

This also explains why security work sometimes feels like a bottomless pit. The reason is not that new bugs keep appearing, but that new truths about the existing ones keep coming to light. Once you accept this mechanism, you stop being surprised by the next finding and set up operations for it.

Security debt you can measure

The growing gap between what is in your own dependencies and what has since become known about them is what security debt means in practice. As long as the term just floats around as a metaphor, it changes nothing. It only becomes useful once you treat it like a metric you read regularly, instead of letting it startle you once a year.

That takes no big platform, only a fixed rhythm and a handful of questions you ask the same way at every check. How many known holes are currently in the production dependency tree, and how many of them sit on a reachable path? How old is the oldest directly used mandatory building block that nobody has updated yet? And how much time typically passes between the day a hole becomes public and the day we closed it? Those three numbers together describe the gap better than any single one, and their trend over the months shows whether the debt is growing or shrinking.

A thought experiment

You do not need a concrete incident to see how the mechanism works. It is enough to think it through once. Picture a small product team that launched cleanly: a fresh dependency tree, `npm audit` with no findings, and since then no reason to touch the lockfile. By the model of the journey through time you can predict what happens within a year, although nobody changes a line, because the gap to the state of knowledge grows quietly.

Suppose a hole becomes public in a transitive dependency deep in the tree. At this point two courses can be set side by side. Without an operational check in place, the team would probably learn of it more or less by chance, and nobody could say off the top of their head whether the vulnerable package is even in their own tree and on which path. Getting that answer in the rush would cost more than the actual fix.

With a running check the starting position would be a different one, because the question of what is in your own tree is already answered before it turns urgent. In both imagined courses it is the same product, the same hole and the same actual fix. The only difference is whether the gap was measured continuously, and even this single thought experiment shows that exactly this is what decides whether a known hole becomes a calm routine or an emergency.

What this means in practice

In practice this means seeing dependencies as an ongoing operational task rather than a project you complete once. That includes checking against today’s knowledge on a fixed rhythm, prioritising findings by reachable path rather than by raw count, and clarifying in advance who decides and how quickly a fix can be rolled out. The last row of the journey is no proof that you are ever done, only that continuous care keeps the snapshot close to the current state of knowledge.

This operational view rarely stands alone; it meshes with its neighbours. That an attack often begins when a package is installed locally rather than only in production is what we describe in Scanning after the commit is not enough, and how the same continuously maintained operational knowledge turns into an advantage during an incident is shown in An operations handbook that writes along.

If you want to take an honest look at how big this gap is for your own product right now, a structured view from outside helps. That is exactly what our free, anonymous security self-assessment at secure-vibe-coding.de is for: it shows you where you stand and which steps are worth taking next. If you would rather work through the results together and turn them into an ongoing part of operations, we are glad to talk it over.

Frequently asked questions

Does a system become less secure over time?

It is not the code that changes, but the knowledge about it. Many vulnerabilities were there all along and only become visible later as a CVE or advisory. Security is therefore an attribute of operations, not only of the build.

Is it enough to check at release?

No. Anyone who only checks at release measures against the knowledge of the time. In production, however, the system runs against today’s. Continuous care keeps the gap between the state of the system and the state of knowledge small.

Why does the gap grow although the code does not change?

Because three forces act independently of the code: a widely used package is reviewed by more people over time, vulnerabilities turn into finished exploits, and deep transitive dependencies age unnoticed because nobody keeps a direct eye on them.

Are all reported vulnerabilities equally urgent?

No. What matters is whether vulnerable code runs on an actually reachable path in production. A critical hole in a function you never call is less urgent than a medium one right in the hot path. That distinction turns a long list of findings into a ranking you can work through.

What does security debt mean in concrete terms?

The growing gap between what is in your dependencies and what has since become known about them. The term becomes useful once you treat it like a metric and read it regularly, rather than using it only as a metaphor.

How can I measure this gap myself?

Set up a fixed recurring run against today’s state, for example a weekly `npm audit --omit=dev` plus a scanner like `osv-scanner`, and track three numbers over time: open holes on a reachable path, the age of the oldest unmaintained mandatory building block, and the time from disclosure to fix.

Related topics