When two software composition analysis (SCA) tools disagree, compare the same repository revision, resolved package, advisory and scan scope before comparing their verdicts. One tool may have a different dependency inventory, use a different package identity, interpret the affected version range differently, or lack newer advisory data. The disagreement can also be about whether a vulnerable component is present versus whether its vulnerable behavior affects the application. Reproduce each tool's input and rule, consult the authoritative advisory, and record a bounded decision: affected, not affected for a stated reason, or still under investigation. An empty result from one tool never cancels a positive result on its own.
The OWASP dependency graph and SBOM guidance separates a component inventory from the relationships needed to understand where a dependency enters. That distinction is useful when reconciling output. The security engineer's job is to find which factual proposition conflicts, not to take an average of two risk scores.
Illustrative disagreement: one parser, two verdicts
Illustrative case; all package, tool and advisory names below are invented. Harbor Reports pins commit r52. Tool A reports DEMO-ADV-17 against scanlib@2.4.1, a transitive npm package under the production PDF service. Tool B reports no issue. The first dashboard says “high severity,” the second says “clean.” Neither message tells the reviewer what Tool B actually examined.
The reviewer exports both tools' metadata and checks the lockfile. npm's lockfile documentation explains that it records the exact generated dependency tree. Here, the lockfile contains scanlib@2.4.1 twice: one occurrence belongs to the PDF service, and one to a development tool. Tool B scanned only the root manifest and did not ingest the workspace lockfile. Its silence says nothing about the PDF service's resolved copy. The reviewer has resolved an inventory difference, not yet established that Harbor Reports is exploitable.
| Item to normalize | Tool A | Tool B | Decisive check |
|---|---|---|---|
| Revision and workspace | r52, PDF workspace |
r52, repository root |
Compare the scan configuration with the repository layout. |
| Package identity | npm scanlib |
No matching package row | Inspect exact lockfile node, package manager and parent path. |
| Resolved version | 2.4.1 |
Unknown | Use the version resolved in the relevant artifact, not a broad manifest range. |
| Advisory | DEMO-ADV-17 |
No advisory shown | Read the authoritative advisory's affected and fixed ranges. |
| Deployment | Called “production” by Tool A | Not assessed | Ask the release owner which workspace and artifact actually ship. |
| Application effect | Not assessed | Not assessed | Trace whether the vulnerable behavior is invoked with relevant input. |
This comparison note is the artifact to attach to the investigation. It prevents the team's ticket from becoming “Tool A versus Tool B.”
Work from input to decision
- Freeze the comparison. Record each scan's time, data-feed version if available, repository commit, scope, manifest or SBOM input, package manager and tool configuration. The owner is the analyst receiving the conflict. The output is a side-by-side record; if these inputs differ, rerun or re-export the authorized assessment at a common revision before treating verdicts as comparable.
- Establish package identity. Match ecosystem, package name, namespace, distribution, version and dependency path. A similarly named package from another ecosystem is not the same component. For a transitive dependency, follow the parent chain to the service or artifact. GitHub explains that its dependency graph recognizes supported manifests, lockfiles and submitted dependency data; another tool may have different inputs. Inventory gaps deserve their own corrective work even when the advisory turns out inapplicable.
- Read the underlying advisory. Prefer the publishing project or advisory authority for affected versions, fixed versions, prerequisites and any amendments. The OSV schema represents affected packages and version ranges explicitly; NVD applicability statements use CPE matching criteria and may express platform conditions. Check which model each tool used. A CPE product match, an npm package range and a source-commit range are related facts, not interchangeable strings.
- Check how each tool reached its verdict. Was Tool A matching a broad CPE name against a package with a different vendor? Did Tool B miss the workspace? Did one tool use a stale database snapshot? Did either deduplicate two copies of the package? A plausible explanation still needs evidence from configuration, output and the exact inventory row. Write “unresolved” if the tool cannot expose enough information.
- Separate presence from application effect. Once the exact vulnerable version is established, assess whether the relevant code is in the deployed build and whether an actor can reach the triggering behavior through this application. A source-supported call path is evidence; an executed exploit would be a different, separately authorized activity. A package in a lockfile alone cannot answer deployment or attack-path questions. CISA's VEX status guidance gives reasons that can support a product-specific “not affected” assertion, such as vulnerable code not present or not in the execution path; it also states that such assertions are made by their author and may be evaluated by the consumer.
- Record the disposition and check it. If the service resolves an affected version and a plausible attacker-to-effect path remains, record the supported facts and prioritize a patch or mitigation. Use independent review when it would resolve a disputed path or a consequential claim; it is not a universal gate before opening repair work. If a reviewed, necessary control defeats the precise path, document it with revision and scope. Keep unresolved deployment, call-path or advisory-mapping questions visible even when a low-risk update proceeds. Assign an owner and due date to each missing fact.
Resolve the inventory conflict, then notice the identity conflict
The first reconciliation is completed when Tool B ingests the same workspace lockfile: both tools now list npm scanlib 2.4.1. The analyst records that Tool B's earlier silence came from scope, preserving the original result rather than pretending the tools always agreed. Advisory applicability and deployment still need their own evidence.
A second synthetic conflict demonstrates why rerunning both tools is not a universal cure. An image inventory contains an OS package identified by distribution, release, architecture and distribution version 2.4.1-8distro2. Tool A maps its upstream-looking 2.4.1 string to an affected upstream range. Tool B reports it fixed based on a distribution advisory naming the backported package revision.
The reviewer must compare the exact installed distribution package against that distribution's signed package metadata and security advisory. The fictional distro2 suffix itself proves nothing. This example assumes those primary records explicitly name 2.4.1-8distro2 as containing the relevant fix; no real distribution or advisory is being represented.
| Conflict | Evidence supplied | Reconciled disposition |
|---|---|---|
| npm workspace omitted | Scan configuration and lockfile node show Tool B missed the PDF workspace | Inventory gap resolved; retain advisory investigation |
| Upstream range versus distribution package | Exact distribution/release/package revision matches the supplied backport advisory and artifact inventory | Reject the broad upstream-version match for this specific OS package |
| Bundled npm copy with similar name | Separate PURL and path; not covered by the OS package advisory | Separate candidate remains open |
That last row prevents an overcorrection. A backport statement for an OS package does not patch a separately bundled copy merely because their names match. Keep the original tool result, normalized identity, governing advisory, exact artifact and reviewer rationale in one record.
The final note is: “Tool B originally omitted the npm workspace; its clean result was not applicable. The separate OS-package alert is rejected for the named distribution build using its backport evidence. The bundled npm candidate still needs its own affected-range and deployed-path review.” The team has two resolved disagreements and one clearly bounded investigation, rather than a vote on which scanner to trust.
If the distribution record cannot be obtained or does not name the installed build, leave that second verdict unresolved. Do not manufacture certainty from a package suffix, a generic vendor statement or the newest tool database.
Close the disagreement without losing its cause
A reconciliation should produce both an alert decision and an explanation of why the outputs diverged. Those are separate deliverables. In Harbor's example, correcting Tool B's workspace selection repairs an inventory gap. Rejecting the OS-package match corrects a package-to-advisory interpretation. Neither action establishes the bundled npm package's application effect. If all three outcomes are stored as “duplicate closed,” the next analyst loses the distinction and may repeat the same mistake.
Use a compact correction record with the original result identifiers, normalized component identity, artifact, governing advisory, decision and remaining question. Include the input that would invalidate the decision. The OS-package rejection should reopen when the deployed distribution build changes outside the reviewed advisory's scope. The workspace correction should be checked when workspace layout or scan configuration changes. These are targeted triggers, not a requirement to reopen every resolved discrepancy after every commit.
When the tool exposes no database version, preserve what it does expose: scan timestamp, exported advisory text, reported affected range, configuration and result identifier. Record the absent version as a reproducibility limitation. Do not infer freshness from a dashboard's current appearance. A fresh interface may be displaying a cached result, while an older export may contain the evidence needed to explain yesterday's decision.
There is also an important unresolved branch. Imagine both tools now inventory the same npm package, but one reports an advisory range that the publisher has amended and the other cannot explain its match. The team can document the publisher's current statement and choose a proportionate dependency update while waiting for a vendor response. It should not describe the vendor's matching defect as confirmed unless the relevant inputs and interpretation support that conclusion. Record the practical disposition independently from the diagnosis of the tool.
For a recurring conflict, collect a small set of reconciled examples before changing organization-wide suppression rules. If every observed discrepancy comes from one unsupported workspace type, fix ingestion or disclose that limitation. If the problem comes from a broad package-name match, a narrow identity correction may be appropriate. A blanket CVE suppression could hide a legitimate copy elsewhere in the application.
The analyst's handoff to engineering should therefore fit into two sentences: “The production npm candidate remains open; update or investigate that package path. The separate OS-package alert was rejected for this exact distribution artifact, and Tool B's missing workspace has been corrected.” Engineering gets an actionable item while the security team retains the evidence needed to improve its tooling.
Failure modes that leave a false sense of agreement
Matching only a CVE identifier can hide package identity differences. Comparing severity labels hides differences in scoring versions and environment. Scanning a source checkout can miss generated or vendored material in a shipped artifact; scanning an image can miss the code path needed to interpret an advisory. A vulnerable range may differ by platform or configuration. A “not affected” assertion without the product version and reason cannot be applied to every downstream service. None of these limitations means the alert is automatically true. They identify the next evidence needed.
For the next tool disagreement, fill the six-row comparison table before changing alert status. If the missing fact is whether the package ships, continue with how to check whether a vulnerable package is deployed. If both tools agree on presence but application effect remains unclear, read how to explain CVE reachability without promising safety.