A scanner tells you a dependency is "Reachable". Another says a finding is "Not exploitable". Both are single bits, and both are usually wrong in the same direction: they convert an incomplete analysis into a confident verdict, because the data model has nowhere else to put it.
Exposure needs four states, not two.
The four
Yes — exploitation was demonstrated. A payload ran, a request returned attacker-controlled output, a call path from an entrypoint to the vulnerable sink was constructed. Something happened that you can replay.
No — a required condition is definitively absent. Not "we found no evidence", but "the thing this CVE requires is provably not here": the vulnerable function is not imported, the affected plugin is not registered, the config option the advisory names is set the other way.
Not demonstrated — the conditions were checked and could not be settled. The dependency is present at a vulnerable version, nothing rules exploitation out, and nothing establishes it either. This is the state most real findings live in, and it is the one two-state models delete.
Unknown — nothing checked this at all. No analyser exists for the condition, or the analysis did not run.
The difference between the last two is the whole point. Not demonstrated means we looked. Unknown means we did not. A system that renders them the same way will eventually be asked which one applied to the finding that turned into an incident.
Why the middle state is the useful one
Consider a CVE that requires four configuration conditions to hold simultaneously. A scan finds three of them present and cannot determine the fourth.
Under a boolean model this is either "exploitable" — alarming, and unsupported, since one condition is unverified — or "not exploitable", which is a dismissal based on not knowing. Neither is true, and both are actionable in the wrong direction.
Under a four-state model it is not demonstrated, with the specific open question attached: the locale count in your Next.js config could not be determined. That is a ten-second task for whoever owns the repository, and it resolves the finding definitively in one direction or the other.
The middle state is not fence-sitting. It is the state that produces the smallest, most specific piece of work.
The combination rule, and why it is asymmetric
Given a set of preconditions, the overall verdict follows three rules:
- any precondition answered no → the whole assessment is no
- all preconditions answered yes → yes
- anything else → not demonstrated
Rule 1 is what makes the rest dangerous, and it is why the evidentiary standard for no has to be much higher than for yes.
A single no dismisses the entire finding. If any evaluator is allowed to answer no from the absence of evidence — "we searched the source and found no proxy configuration" — then every silence becomes a dismissal, and the model has quietly reinvented the false-negative machine it was built to replace.
So the rule for evaluators is: yes from positive evidence, no only from evidence that actively rules the condition out, everything else stays open.
In practice that means some conditions can never answer no at all, and that is correct rather than a gap:
NO_PROXYis an environment variable read at runtime. Not finding it in source is not evidence it is unset.- Authentication can be enforced by a reverse proxy that does not appear in the repository. No middleware file does not mean no authentication.
- Whether an attacker can reach a network endpoint is not a property of the code at all.
Each of those stays not_demonstrated forever until something outside the repository answers it. A model that lets them resolve to no produces cleaner dashboards and worse security.
Conditions with no evaluator must stay unknown
The subtler failure is a precondition that names an analyser nobody wrote.
If the catalogue says a CVE requires attacker-controlled input reaching a specific sink, and no taint analysis runs, that condition must come back unknown with an explicit note — no automated check available; requires manual review — and not default to satisfied, unsatisfied, or silently absent.
The reason is that defaults propagate. A condition that defaults to satisfied contributes to a yes. A condition that defaults to unsatisfied contributes to a no. Either way, a gap in the tooling has been laundered into a verdict about a customer's security posture.
Worth enforcing in the test suite rather than in review: assert that every precondition naming an evaluator has an implementation, so the catalogue cannot grow a claim the code does not back.
What this changes about the output
The user-facing consequence is that a finding stops being a severity badge and becomes a short argument:
Not demonstrated. Three of four conditions hold: the App Router is in use, Turbopack is the active bundler, and authentication runs through middleware. The locale count could not be determined — this project routes locales in code rather than declaring them in
next.config, so the count is not readable from configuration.
Nobody has to trust the verdict, because the verdict shows its work. And the one open question is small enough that the person who owns the repository can close it before their next standup — which is the actual measure of whether a vulnerability tool is working.