@@ -9,7 +9,196 @@ The format is based on the regulated environment requirements:
99
1010---
1111
12- ## [ 2026-05-02 12:00] - warp 0.4.2 migration (PR #52 follow-up)
12+ ## [ 2026-05-02 10:00] - Symbol-import auto-VEX (roadmap Phase 3, scope-revised)
13+
14+ ** Author:** Erick Bourgeois
15+
16+ ### Added
17+ - ` src/auto_vex_reachability.rs ` (library module, ~ 190 LOC) +
18+ ` src/auto_vex_reachability_tests.rs ` (20 unit tests, 100%
19+ positive/negative/exception coverage per project rule):
20+ - ` parse_nm_output ` — parses ` nm -D --undefined-only ` text into a
21+ set of imported symbol names; strips ` @GLIBC_x.y ` version
22+ suffixes; defensively skips lines whose type column isn't ` U ` .
23+ - ` load_affected_functions_from_path ` — loads a JSON map of CVE id
24+ → [ public-API function names] . Underscore-prefixed keys
25+ (` _comment ` , ` _meta ` ) are sidecar metadata and skipped silently.
26+ - ` compute_reachability_vex ` — pure function that, given a Grype
27+ report, the imported-symbol set, the affected-functions mapping,
28+ and the already-triaged CVE set, emits one `not_affected +
29+ vulnerable_code_not_in_execute_path` statement per CVE whose
30+ listed functions are * all absent* from the imports.
31+ - Re-exports ` Document ` , ` Statement ` , ` Vuln ` , ` Product ` ,
32+ ` GrypeReport ` , ` GrypeMatch ` , ` GrypeVuln ` , ` GrypeArtifact ` ,
33+ ` build_document ` , ` load_triaged_from_vex_dir ` from
34+ ` auto_vex_presence ` so callers can import the OpenVEX shape from
35+ one place.
36+ - ` src/bin/auto_vex_reachability.rs ` — thin clap-driven CLI wrapping
37+ the library. Reads ` --grype-json ` , ` --binary-symbols ` (the path to
38+ an ` nm ` output text file), ` --affected-functions ` , ` --vex-dir ` ,
39+ ` --product-purl ` , ` --id ` , optional ` --author ` and ` --timestamp ` .
40+ - ` Cargo.toml ` : ` [[bin]] auto-vex-reachability ` entry.
41+ - ` .vex/.affected-functions.json ` (new, dot-prefixed): curated CVE →
42+ [ function names] mapping covering 9 of the 15 hand-authored
43+ statements in ` .vex/ ` . Filename is dot-prefixed so default shell
44+ globs (` vexctl merge .vex/*.json ` ) skip it. The 6 unmapped CVEs
45+ (` CVE-2019-1010022/3/4/5 ` , ` CVE-2026-4437 ` , ` GHSA-cq8v-f236-94qc ` )
46+ are not function-bounded — preconditions / adversary-model claims
47+ / Rust-crate-only — and stay hand-authored. One mapped CVE
48+ (` CVE-2026-4438 ` , gethostbyaddr) is * also* retained hand-authored
49+ because its statement carries the stronger
50+ ` vulnerable_code_cannot_be_controlled_by_adversary ` justification;
51+ the mapping entry stays as a safety net so auto-reachability would
52+ jump in with the weaker ` vulnerable_code_not_in_execute_path ` if
53+ the hand-authored file is ever removed.
54+ - ` Makefile ` : new ` vex-auto-reachability ` target that defaults
55+ ` RELEASE_BINARY=target/release/5spot ` and
56+ ` AFFECTED_FUNCTIONS=.vex/.affected-functions.json ` . macOS Mach-O
57+ fallback to ` nm -gU ` since ` -D --undefined-only ` is ELF-only.
58+ ` .PHONY ` extended.
59+ - ` .github/workflows/build.yaml ` : new ` auto-vex-reachability ` job
60+ gated on ` github.event_name != 'pull_request' ` . `needs: [ build,
61+ extract-version, grype-triage] `. Builds the bin (cached cargo),
62+ downloads the ` 5spot-linux-amd64 ` artifact + the ` grype-triage-* `
63+ artifacts, runs ` nm -D --undefined-only ` on the binary, runs the
64+ bin once per variant, merges per-variant outputs via `vexctl
65+ merge` , uploads ` vex-auto-reachability` (the merged VEX doc) and
66+ ` vex-auto-reachability-evidence ` (the raw ` nm ` output, for the
67+ Security team to re-derive against).
68+
69+ ### Changed
70+ - ` .github/workflows/build.yaml ` :
71+ - ` build-vex ` job: ` needs ` extended with ` auto-vex-reachability ` .
72+ Added a download step for the ` vex-auto-reachability ` artifact
73+ and an explicit ` test -f ` guard. ` vexctl merge ` invocation now
74+ includes ` auto/vex.auto-reachability.json ` alongside
75+ ` .vex/*.json ` and ` auto/vex.auto-presence.json ` .
76+ - Header comments on ` build-vex ` updated to describe both
77+ automated phases.
78+ - ` src/auto_vex_presence.rs ` : ` load_triaged_from_vex_dir ` now skips
79+ dot-prefixed ` *.json ` files, so ` .vex/.affected-functions.json `
80+ isn't mis-parsed as an OpenVEX statement when ` auto-vex-reachability `
81+ loads ` --vex-dir ` . Added ` load_triaged_skips_dotfiles ` test in
82+ ` auto_vex_presence_tests.rs ` .
83+ - ` src/lib.rs ` : re-export ` pub mod auto_vex_reachability ` .
84+ - ` .vex/README.md ` : rewritten "Automated statements" section to
85+ describe both Phase 2 (presence) and Phase 3 (reachability)
86+ artifacts. New sub-section documenting the
87+ ` .vex/.affected-functions.json ` mapping format.
88+ - ` docs/src/security/vex.md ` :
89+ - CI flow steps renumbered to 7 — added step 3 for
90+ auto-reachability between presence (step 2) and assemble (step 4).
91+ - "What we automate" section: added the symbol-import-reachability
92+ bullet and a new "Why symbol-imports, not LLVM-IR call graphs"
93+ sub-section explaining the scope revision.
94+ - Roadmap (` ~/dev/roadmaps/5spot-automated-vex-generation.md ` ):
95+ status line updated to "Phase 3 ✅ shipped (symbol-import variant)".
96+ Phase 3 section rewritten to document the scope revision with
97+ rationale. Rollout section updated.
98+
99+ ### Removed
100+ - ` .vex/CVE-2010-4756.json ` (glob/fnmatch — superseded by auto-reachability)
101+ - ` .vex/CVE-2018-20796.json ` (regcomp/regexec)
102+ - ` .vex/CVE-2019-9192.json ` (regcomp/regexec)
103+ - ` .vex/CVE-2026-27171.json ` (crc32_combine[ 64] )
104+ - ` .vex/CVE-2026-4046.json ` (iconv/iconv_open/iconv_close)
105+ - ` .vex/CVE-2026-5358.json ` (NIS / yp_ * )
106+ - ` .vex/CVE-2026-5450.json ` (scanf family)
107+ - ` .vex/CVE-2026-5928.json ` (ungetwc)
108+
109+ These 8 statements all asserted ` vulnerable_code_not_in_execute_path `
110+ and are now produced by ` auto-vex-reachability ` from the curated
111+ mapping + the ` nm -D --undefined-only ` symbol-import evidence
112+ uploaded by the new CI job. The Security team verifies them by
113+ re-running the same ` nm ` invocation against the release-attested
114+ binary. ` CVE-2026-4438.json ` is intentionally retained because its
115+ hand-authored statement carries the stronger
116+ ` vulnerable_code_cannot_be_controlled_by_adversary ` justification —
117+ see Why below.
118+
119+ ### Why
120+ Phase 3 of the automated VEX generation roadmap, with a scope
121+ revision. The original plan targeted Rust LLVM-IR call-graph
122+ reachability against RustSec advisories carrying ` affected.functions ` .
123+ At implementation time ` cargo audit ` reports zero open Rust-level
124+ vulnerabilities for this codebase — every CVE in ` .vex/ ` is a
125+ base-image glibc/zlib finding from Grype scanning the Docker image,
126+ and RustSec doesn't track those. The Rust call-graph approach
127+ addressed zero current findings.
128+
129+ The mechanical equivalent for our actual data is symbol-import
130+ absence: if the Rust binary's dynamic-symbol-import table doesn't
131+ contain any of the documented public-API entry points for an
132+ affected glibc function, the affected code path cannot be reached
133+ through documented APIs. This is exactly what the existing 14
134+ hand-authored impact statements claim in prose ("5-Spot performs no
135+ character-set conversions; the glibc iconv() path exercised by this
136+ CVE is unreachable from the controller binary"). Auto-reachability
137+ turns that prose into a verifiable check that the Security team
138+ re-derives during counter-signing.
139+
140+ The shipped variant addresses 9 of the 15 hand-authored statements
141+ through the curated mapping. Of those 9, ** 8 hand-authored
142+ statements were deleted** in this same change (their
143+ ` vulnerable_code_not_in_execute_path ` claim is now produced by
144+ auto-reachability and re-derivable from the uploaded ` nm ` evidence).
145+ The 9th mapped CVE (` CVE-2026-4438 ` ) was retained because its
146+ hand-authored claim is the stronger
147+ ` vulnerable_code_cannot_be_controlled_by_adversary ` — the auto-VEX
148+ would replace it with the weaker function-absence claim. The
149+ remaining 6 hand-authored statements are not function-bounded
150+ (adversary-model preconditions, ASLR bypasses,
151+ missing-shell-at-runtime, Rust-crate combo) and stay hand-authored.
152+ The original LLVM-IR call-graph approach is deferred to when (a)
153+ ` cargo audit ` starts returning Rust-side findings with
154+ function-level data, or (b) we adopt a Rust analyzer that exposes
155+ call-graph data without LTO loss.
156+
157+ ### Verification
158+ - ` cargo fmt --all -- --check ` : clean.
159+ - ` cargo clippy --all-targets --all-features -- -D warnings ` : clean.
160+ - ` cargo test --lib ` : 418 tests pass (398 pre-existing + 20 new for
161+ ` auto_vex_reachability ` ).
162+ - Smoke tests:
163+ - 10 Grype matches (9 mapped + 1 unmapped) + an empty ` .vex/ ` dir +
164+ nm output without any of the affected glibc symbols → 9
165+ statements emitted in deterministic CVE-id order. Unmapped CVE
166+ correctly skipped.
167+ - Same input but with ` glob ` added to nm output → 8 statements
168+ emitted (CVE-2010 -4756 correctly excluded because ` glob ` is
169+ importable).
170+ - Same input against the real ` .vex/ ` directory (all 9 mapped CVEs
171+ are hand-authored there) → 0 statements emitted because
172+ ` load_triaged_from_vex_dir ` correctly skips re-deriving
173+ already-triaged CVEs (and correctly skips
174+ ` .affected-functions.json ` thanks to the new dotfile filter).
175+ - ` vexctl merge ` of the auto-reachability output + ` .vex/*.json ` +
176+ vex.auto-presence.json produces a well-formed merged document
177+ parseable by ` vexctl ` with no errors.
178+
179+ ### Trust model (unchanged from Phase 2)
180+ The merged VEX (hand-authored + auto-presence + auto-reachability)
181+ is Cosign-attested by CI keyless against both image digests. The
182+ Security team independently re-runs ` nm -D --undefined-only ` against
183+ the release-attested binary and checks each auto-reachability
184+ statement's claim, then counter-signs with their own OIDC identity
185+ if they agree. Downstream consumers gate on both attestations via
186+ ` cosign verify-attestation ` with twin
187+ ` --certificate-identity-regexp ` invocations.
188+
189+ ### Impact
190+ - [ ] Breaking change
191+ - [ ] Requires cluster rollout
192+ - [ ] Config change only
193+ - [x] Documentation only
194+
195+ * (Not really "documentation only" — net +700 LOC of Rust + tests + a
196+ new CI job — but no runtime, CRD, or API-surface change. Closest
197+ pre-existing box.)*
198+
199+ ---
200+
201+ ## [ 2026-04-30 14:00] - Phases 5 + 6 of security audit: RBAC tightening + defence-in-depth docs
13202
14203** Author:** Erick Bourgeois
15204
0 commit comments