A Finding Without an Owner
Triage · July 28, 2026 · 9 min read
Ask why the backlog is large and the answer usually blames the scanners: too noisy, too many false positives, too many overlapping tools reporting the same thing. Some of that is true. None of it explains the backlog.
The backlog exists because a finding is not an action. It is the beginning of a decision procedure with five steps, and only the first is automatable:
- Is this real, in this code, at this version?
- Is it reachable in this deployment?
- Who is able to change it?
- Is the change safe to make?
- If we are not making it, who is accepting that?
Scanners answer question one, imperfectly. Adding a sixth scanner produces more question ones. The queue grows at the rate of detection and drains at the rate of questions three, four and five, which are organisational rather than technical. That mismatch is the entire phenomenon.
Findings accumulate where ownership is thinnest
There is a pattern in every large backlog and it is not random. The code generating the most findings is disproportionately the code with the least clear owner: a base image maintained by nobody in particular, a shared internal library whose author moved teams, a service inherited during a reorganisation, a build tool that has worked for years and therefore has no one assigned to it. Actively developed code with a named owning team gets its findings resolved, because the routing question has an obvious answer and the team touches the code weekly anyway.
So the backlog is, quite precisely, a map of where ownership is unclear. This is why efforts to shrink it through better prioritisation disappoint. Re-sorting a queue does not create an owner for the top item. If the highest-priority finding is in a component nobody owns, prioritisation has produced a better label on the same stuck ticket.
One cause, many instances
A second structural inflation is arithmetic. A vulnerable package in a base layer used by forty services produces forty findings. A shared library pulled in by every internal application produces one finding per application. Two scanners that both look at containers produce two findings per instance.
The response to a large number is to work through it, so the same underlying issue gets investigated dozens of times by dozens of people, each reaching the same conclusion independently — while one fix in one place would close all of them.
Aggregating at the cause rather than the instance changes both the number and the routing. “Update this base image” is a piece of work with an owner and a single verification. “Forty services have a vulnerable library” is a spreadsheet. Grouping findings by the artefact that introduced them rather than the artefact that contains them is usually the highest-leverage change available, and unglamorous enough that it rarely gets funded.
Reachability is the honest filter
Question two deserves more weight than it gets. A flaw in a dependency is present in your build; it is only exposed if something in your application can reach the affected code path with input an attacker can influence.
Reachability analysis is imperfect — dynamic dispatch, reflection, configuration-driven behaviour and plugin loading all defeat naive call-graph walking, so a “not reachable” verdict is a probabilistic statement rather than a proof. But the asymmetry is favourable: when a tool can demonstrate that a vulnerable function is called from your code, it converts an argument about labels into an argument about a specific call site, and concrete disagreements resolve faster than abstract ones.
The failure to avoid is treating unreachable as equivalent to absent. The dependency is still shipped and still there for a future code change to start calling. Unreachable means not first, not not ever.
Routing beats prioritising
A finding delivered to a central security queue is delivered to nobody who can fix it. It must then be re-delivered by a human, and that step silently consumes most of the programme’s capacity: a small team doing lookup and forwarding, full time, for a queue that grows faster than they forward.
The alternative is to make delivery automatic and specific. A finding should arrive in the owning team’s own tracker, in their normal work stream, ideally with the change already drafted. A proposed dependency bump with tests already run is a fundamentally different object from a ticket saying a package is vulnerable, because it moves the work from “investigate and decide” to “review and merge” — and the second is something a team can do between other tasks.
This depends entirely on an ownership map that is accurate, which most organisations do not have and cannot buy. Building one is tedious, political and the actual prerequisite. Every improvement downstream of routing is capped by its accuracy.
Give people a legitimate way to say no
If there is no supported path for “we are not fixing this, here is why”, people will use unsupported ones: suppressions with no expiry, tickets closed as duplicates, scanners configured to skip a directory, findings marked won’t-fix with an empty justification. The information about why is lost, so the same finding returns next quarter and is re-investigated from scratch.
An acceptance path that works has four properties. It requires a stated reason. It names a person or role accepting it, not a team in the abstract. It expires, so that acceptance is a decision to revisit rather than a decision to forget. And it is easier than the illegitimate routes, because if suppressing a scanner rule is faster than filing an acceptance, that is what will happen.
The counter-intuitive part is that a good acceptance path shrinks the real backlog dramatically and makes the reported one look worse, because previously invisible decisions become visible records. That is a reporting problem worth having.
Gate on the delta
Build-breaking gates are the standard proposal and they work in exactly one configuration.
Gating on total findings in an existing codebase fails immediately: the build is red on day one for reasons unrelated to the change being made, so the gate is disabled within a week or routed around with a permanent exception, and the team learns that security gates are obstacles to be neutralised.
Gating on the delta works. The build fails if this change introduces a new finding above an agreed threshold, and passes otherwise. The developer is looking at the code, the cause is one thing they just did, and the fix is usually to pick a different version. It stops accumulation, which is the only thing a gate can do — the existing backlog is a separate project.
One qualification: a gate is only fair when a fix exists. Blocking a release over a vulnerability with no published fix teaches people to override gates, and every override normalises the next one. Those should route into the acceptance path automatically rather than leaving a developer with bypass as the only way forward.
Measure the decision, not the count
Finding counts are driven by disclosure schedules and scanner coverage, both outside your control. They rise when you add a tool and fall when you remove one, which makes them useless as a measure of the programme and actively harmful as a target.
The number that reflects the actual constraint is decision latency: from a finding appearing to it reaching a state of fixed, accepted with a reason, or demonstrated not applicable. It measures the part of the system that is genuinely broken, cannot be improved by buying another scanner, and gets worse immediately if you add detection without adding anywhere for the results to go.