Article · Scan coverage

Zero findings is not zero risk

A dependency scan that reports nothing has two possible meanings: nothing is wrong, or nothing was read. In the report they look identical — the same green tick, the same empty table, often the same reassuring 0 errors. This article is about how a scan ends up confidently empty, and the four checks that tell the two apart.

Updated 12 Aug 2026 · by the depproof team

A scanner can only count what it recognised

Every dependency scanner — every software composition analysis (SCA) tool — works in two stages. First it reads a file and builds a list of components. Then it checks that list against advisory data. Nearly all the attention goes to the second stage — which database, how fresh, how many false positives — because that is the stage vendors compete on. The first stage is where scans actually fail.

The reason is a counting problem. A scanner can report the things it tried to process and failed on: a malformed file, an unreachable coordinate, a version it could not pin. Those become errors, and errors get counted. But a dependency the scanner never recognised as a dependency is not a failure. It is not an error, not a warning, not an “unresolved” item. It is absent from every count in the report, including the counts that are supposed to describe the report’s own reliability.

This produces the inversion at the heart of the problem: the fewer dependencies a scanner recognised, the cleaner its error count looks. A tool that understood one line of your build and nothing else will tell you, accurately and in good faith, that it encountered no problems.

Four ways a dependency scan comes back empty

These are properties of the files, not of any particular tool. Any scanner handed the same input has the same ceiling.

  • 1. It read a build script, which is a program. A Gradle build script is executable code — versions can arrive from a platform, an imported BOM, a plugin, or a variable computed at build time. A tool reading it as text can only find declarations matching the shapes it knows. Consider a dependency declared with no version literal anywhere in the line, because a Spring Boot plugin supplies it: implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web'. There is nothing there for a text match to key on. It is not found, and — the important part — it is not reported as missing either. A project can declare a dozen starters this way and scan to a confident, empty result. The fix is to hand the scanner the build’s own output instead — see scanning Maven and Gradle dependencies for which input to use.
  • 2. It read a manifest instead of a lockfile. package.json, pyproject.toml and a hand-written requirements.txt record what you asked for, usually as ranges. They contain no transitive dependencies at all, because that resolution only happens when a package manager performs it. Since the large majority of vulnerabilities live in dependencies nobody chose deliberately, a manifest-only scan is missing the part of the tree where the findings are — and the same gap hides copyleft licenses several levels down that nothing in your manifest will reveal.
  • 3. It read the wrong file, correctly. The subtlest case, because it fails in the opposite direction. A Gradle version catalog (libs.versions.toml) resolves perfectly — and describes a menu of versions available to the build, with no statement of which module uses which. The result is a real, well-formed inventory of the wrong set, typically dominated by build and CI tooling you never ship. Nothing errors. The numbers are simply about something else.
  • 4. It read requirements, not selections. go.mod states the versions your module requires. Go’s minimal version selection can settle on a higher version because some other module in the graph asked for one, so what builds is not what is written. The names are right and the versions quietly are not — and no error is available to tell you, because from the file’s point of view nothing went wrong.

Four signatures of an incomplete dependency scan

You do not need to know how your scanner works to sanity-check its coverage. Open the last report and look for these.

  • Zero transitive dependencies, with a plausible direct count. The signature of a manifest scan. A real application tree is usually five to twenty times larger than its direct dependency list; if the report has forty components and you know you declared forty, it stopped at the surface.
  • Everything marked direct, all at depth 1. Some lockfile formats record an exact set of packages with no parent-child structure. The inventory is trustworthy, but the shape is gone — so the report can tell you that you are affected and cannot tell you which single upgrade clears the most findings.
  • A component count that doesn’t match reality in either direction. Far fewer components than you expect means something was not read. Far more — especially a list heavy with linters, test runners and build plugins — usually means the wrong file was read.
  • No production-versus-test distinction anywhere. If everything is unclassified, the scanner could not determine scope from what it was given, which is itself a strong hint about the quality of the input. It also means you cannot tell a critical finding you ship from one that only ever ran in CI.

Stop measuring errors; measure what was read

The deeper fix is to change which number you trust. “0 errors” and “0 unresolved” describe the scanner’s experience of the run, not its coverage of your project — and as we have seen, they read best precisely when coverage is worst. They are the wrong metric, not merely an imprecise one.

The useful question is not how many problems did the scan hit? but which file did it read, and what could that file possibly have contained? That reframing is powerful because it is answerable in advance and never lies:

  • A lockfile or resolver outputpackage-lock.json, pnpm-lock.yaml, yarn.lock, poetry.lock, uv.lock, a Gradle dependency dump, a resolved pom.xml — is the build’s own answer. What you scan is what you ship, which is also what makes it the only sound basis for an SBOM you hand to a customer.
  • A manifest or build scriptpackage.json, pyproject.toml, build.gradle, go.mod — is a statement of intent. It is useful, and it is categorically not an inventory of what you ship.

No setting on any scanner promotes the second into the first. The only way to raise the quality of a scan is to hand it a better file: commit the lockfile your package manager already produces, dump your dependency tree in CI, or generate a pinned requirements.txt with pip-compile. Those are small, one-time changes, and they matter more than the choice of scanner.

What to ask of any dependency scanner

Whatever tool you use — including ours — three questions separate a report you can hand an auditor from one you merely hope is right.

  • Which file did you read? Per repository, per module. If the answer isn’t in the output, the report cannot be independently trusted.
  • Was that file resolved or declared? The single most important property of a scan, and the one almost no tool states plainly.
  • What did you decide not to read? Skipped directories, unreadable files, modules with no recognisable input. Honest scope is a feature; silence about scope is not the same as full coverage.

A scanner that answers all three can be checked. One that reports only findings and an error count is asking for trust it hasn’t given you the means to verify — and “zero findings” from such a tool is not a result, it is an absence of information.

Frequently asked questions

Why did my dependency scan find zero vulnerabilities?

There are two possibilities and a findings count cannot distinguish them: your dependencies genuinely have no known vulnerabilities, or the scanner did not read the dependencies you think it did. The second is more common than teams expect, because a scanner reports on what it recognised — a dependency it never recognised is not counted as an error, it is simply absent. Before trusting a clean result, check the component count against the number of dependencies you know you have, and check whether transitive dependencies appear at all.

Why does my scan show no transitive dependencies?

Almost always because the scanner read a manifest rather than a lockfile. Files like package.json, pyproject.toml and a hand-written requirements.txt record the dependencies you asked for, as version ranges. They do not record the dependencies those pull in — that resolution only exists once a package manager performs it and writes a lockfile. A scan of a manifest alone therefore reports only your direct dependencies, and the large majority of known vulnerabilities are found in transitive ones.

Can a scanner read my build.gradle file accurately?

Only partially, and the shortfall is not measurable from inside the scan. A Gradle build script is a program, not a data file — dependency versions can come from a platform, a BOM, a plugin or a variable resolved at build time. A tool that reads the script as text finds the declarations that match its patterns; declarations expressed in a form it does not match are not reported as failures, because nothing knows they exist. The reliable inputs are Gradle lockfiles or the output of a dependency-tree dump, both of which are the build stating its own answer.

Is "0 errors" or "0 unresolved" a good measure of scan coverage?

No — it is close to the worst available measure, because it moves the wrong way. Those counters record items the scanner tried to process and failed on. A dependency that was never recognised in the first place is never attempted, so it never appears in the failure count. Coverage is therefore lowest exactly when the error count looks best. What you want instead is a statement of what the scanner read: which file it used, and whether that file contained a resolved dependency set or only a declaration of intent.

How do I verify that a dependency scan actually covered my project?

Four checks take about five minutes. First, compare the reported component count against a number you can estimate independently — your own direct dependency list, or the size of your installed tree. Second, confirm transitive dependencies are present at all; a report of only direct dependencies indicates a manifest was read. Third, look at whether the report distinguishes production from test dependencies. Fourth, check which file the scan actually used, and whether it was a lockfile or resolver output rather than a manifest or build script.

Keep reading

General information for engineering teams. Product and project names are used nominatively; depproof is not affiliated with them.