Prove a Finding Is Not Exploitable
Marking a finding as Vulnerable Code Not In Execute Path is a claim about your binaries. This guide shows how to substantiate it: symbol-level analysis of the shipped packages, which for dynamically linked code answers the question "is this function ever called?" directly rather than by argument.
The examples use Debian packages, because system packages in container images are where this situation arises most often. The method applies to any dynamically linked ELF binary.
What you are deciding between
| Outcome | Meaning | Records as |
|---|---|---|
| False positive | The vulnerable function is never called. The code path is structurally unreachable. | Vulnerable Code Not In Execute Path |
| Accept risk | The function could be called, but the risk is acceptable — deprecated feature, DoS only, no patch planned. | Accept risk with a written rationale |
Prerequisites
curl— download.debpackagesarandtar— extract themnmandobjdump— inspect symbols (frombinutils)dpkg— inspect package metadata
A reproducible environment with Nix
All the tools are packaged as a Nix flake, which is the recommended way to run the analysis — especially on macOS or in CI.
Step 1 — Read the dependency path
DevGuard's Path to component graph on the vulnerability's detail page gives you exactly what you need:
Your application → debian/wget → debian/libgnutls30t64 → debian/libtasn1-6
- The last node is the vulnerable package.
- The second-to-last node pulls it in.
- Your task is to determine whether that intermediate package actually calls the vulnerable function.
Note the exact versions — you will analyze those specific builds, not the latest ones.
Step 2 — Research the CVE
Before touching a binary, know what you are looking for.
Search for the vulnerable function name and the Debian tracker entry:
CVE-XXXX-XXXXX <library-name> vulnerable function
CVE-XXXX-XXXXX debian security tracker
Answer three questions:
- What is the exact vulnerable function? e.g.
asn1_expand_octet_string - What triggers it? Does exploitation require a specific call pattern or input?
- How does Debian classify it? Check
https://security-tracker.debian.org/tracker/CVE-XXXX-XXXXX. A<no-dsa>marking means Debian considers it minor and plans no security update — supporting evidence for accept risk, though never proof of non-exploitability on its own.
Step 3 — Obtain the exact package build
You need the precise version present in your image. Debian's snapshot archive preserves every historical build and is the canonical source.
Verify the version on the system if your scanner's report is not enough:
Then browse to https://snapshot.debian.org/binary/<package-name>/, which lists every version with direct
download links. The URLs follow this pattern:
https://snapshot.debian.org/archive/debian/<timestamp>/pool/<section>/<letter>/<source-package>/<package>_<version>_amd64.deb
For libgnutls30t64 version 3.8.6-2:
https://snapshot.debian.org/archive/debian/20240831T204032Z/pool/main/g/gnutls28/libgnutls30t64_3.8.6-2_amd64.deb
Step 4 — Extract the packages
Run inside nix develop:
Repeat for each package in the chain you need to examine.
Step 5 — Analyze the symbols
nm -D lists the dynamic symbol table. A U (undefined) entry means the binary calls that function from
an external library. If a function does not appear, that binary never calls it through dynamic linking.
Is the vulnerable library linked at all?
No output means this binary is not linked against the library — a strong false-positive signal.
Is the specific vulnerable function called?
No output means this library never calls the vulnerable function.
Packages that ship many binaries
Some packages install a whole directory of executables. Check them all, and record which one is responsible:
Step 6 — Decide
| Situation | Decision |
|---|---|
| The vulnerable function appears in no binary's symbol table | False positive → Vulnerable Code Not In Execute Path |
| It is used only by a legacy binary that is never invoked in your workload | False positive → Vulnerable Code Not In Execute Path, naming the binary |
| It is called, but the trigger condition can never be met | False positive → Vulnerable Code Cannot Be Controlled By Adversary |
It is reachable and the risk is low (DoS only, <no-dsa>, mitigated elsewhere) | Accept risk |
See Choose a Mechanical Justification for the full decision table.
Step 7 — Write the justification
A defensible justification has three parts: what the vulnerability is, what your analysis showed, and any supporting evidence. Keep it under 4000 characters — DevGuard's limit.
False positive:
CVE-XXXX-XXXXX is assessed as a false positive.
The vulnerability is a <type> in the <function_name> function of <library>.
According to the advisory, exploitation requires that the target program
explicitly calls <function_name>.
Symbol analysis of the extracted Debian package <package>_<version>_amd64.deb
via `nm -D` shows that <intermediate_library> does not reference
<function_name>. Since <top_level_package> accesses <library> exclusively
through <intermediate_library>, the vulnerable code path is not reached.
Checked: dynamic symbol tables of all binaries shipped by <package>. Not
covered: runtime dlopen and statically linked copies, of which none are
present in this image.
[Optional: Debian's security team classifies this CVE as a minor issue
(<no-dsa>) for <suite> and plans no dedicated security update.]
Accept risk:
CVE-XXXX-XXXXX is assessed as accepted risk.
The vulnerability affects <library> <version>, present as a <direct|transitive>
dependency of <package>. The vulnerable function <function_name> is called by
<binary>, which implements <feature>.
The risk is acceptable because:
1. <e.g. the feature is deprecated and unused in this workload>
2. <e.g. the vulnerability class is DoS only, no RCE>
3. <e.g. Debian classifies it as <no-dsa> and plans no patch>
To be re-evaluated once a patched version ships.
Step 8 — Record it in DevGuard
Turn the conclusion into a decision that also covers future scans:
- The intermediate package does not call the function — click that edge in the Path to component graph. DevGuard pre-fills a VEX rule pinning the CVE and that dependency path, so every finding arriving the same way is dismissed too.
- The whole component version is unaffected — write a rule matching the purl directly. Note that
matchesPurlversion constraints do not work for Debian versions; compare the purl with==. See the CEL Expression Reference. - This one finding only — record it as a vulnerability event.
Paste your justification text into the rule or event. It is exported in the VEX and CSAF documents your consumers read, and it is what makes the claim auditable.
Worked examples
CVE-2025-59375 — libexpat via git
Path: Your application → debian/git → debian/libexpat1
Stack overflow in libexpat through recursive XML entity expansion (DoS), fixed in 2.7.2.
Decision: false positive. libexpat is reachable only through git-http-push, a deprecated binary
implementing WebDAV-based HTTP push, which this workload never invokes.
CVE-2025-13151 — libtasn1 via libgnutls via wget
Path: Your application → debian/wget → debian/libgnutls30t64 → debian/libtasn1-6
Stack-based buffer overflow in asn1_expand_octet_string; exploitation requires the program to call that
function explicitly.
Decision: false positive. libgnutls never calls asn1_expand_octet_string, and wget reaches libtasn1
exclusively through libgnutls, so the vulnerable function is unreachable.
Related Documentation
- Choose a Mechanical Justification — which justification your evidence supports
- Create & Manage VEX Rules — turning the result into an automated decision
- Reduce False Positives — why these findings appear, including backported patches
- CVE Decision Guide — remediation options to exhaust first
- Container Hardening Checklist — reducing the attack surface structurally