Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 190 additions & 1 deletion .claude/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,196 @@ The format is based on the regulated environment requirements:

---

## [2026-05-02 12:00] - warp 0.4.2 migration (PR #52 follow-up)
## [2026-05-02 10:00] - Symbol-import auto-VEX (roadmap Phase 3, scope-revised)

**Author:** Erick Bourgeois

### Added
- `src/auto_vex_reachability.rs` (library module, ~190 LOC) +
`src/auto_vex_reachability_tests.rs` (20 unit tests, 100%
positive/negative/exception coverage per project rule):
- `parse_nm_output` — parses `nm -D --undefined-only` text into a
set of imported symbol names; strips `@GLIBC_x.y` version
suffixes; defensively skips lines whose type column isn't `U`.
- `load_affected_functions_from_path` — loads a JSON map of CVE id
→ [public-API function names]. Underscore-prefixed keys
(`_comment`, `_meta`) are sidecar metadata and skipped silently.
- `compute_reachability_vex` — pure function that, given a Grype
report, the imported-symbol set, the affected-functions mapping,
and the already-triaged CVE set, emits one `not_affected +
vulnerable_code_not_in_execute_path` statement per CVE whose
listed functions are *all absent* from the imports.
- Re-exports `Document`, `Statement`, `Vuln`, `Product`,
`GrypeReport`, `GrypeMatch`, `GrypeVuln`, `GrypeArtifact`,
`build_document`, `load_triaged_from_vex_dir` from
`auto_vex_presence` so callers can import the OpenVEX shape from
one place.
- `src/bin/auto_vex_reachability.rs` — thin clap-driven CLI wrapping
the library. Reads `--grype-json`, `--binary-symbols` (the path to
an `nm` output text file), `--affected-functions`, `--vex-dir`,
`--product-purl`, `--id`, optional `--author` and `--timestamp`.
- `Cargo.toml`: `[[bin]] auto-vex-reachability` entry.
- `.vex/.affected-functions.json` (new, dot-prefixed): curated CVE →
[function names] mapping covering 9 of the 15 hand-authored
statements in `.vex/`. Filename is dot-prefixed so default shell
globs (`vexctl merge .vex/*.json`) skip it. The 6 unmapped CVEs
(`CVE-2019-1010022/3/4/5`, `CVE-2026-4437`, `GHSA-cq8v-f236-94qc`)
are not function-bounded — preconditions / adversary-model claims
/ Rust-crate-only — and stay hand-authored. One mapped CVE
(`CVE-2026-4438`, gethostbyaddr) is *also* retained hand-authored
because its statement carries the stronger
`vulnerable_code_cannot_be_controlled_by_adversary` justification;
the mapping entry stays as a safety net so auto-reachability would
jump in with the weaker `vulnerable_code_not_in_execute_path` if
the hand-authored file is ever removed.
- `Makefile`: new `vex-auto-reachability` target that defaults
`RELEASE_BINARY=target/release/5spot` and
`AFFECTED_FUNCTIONS=.vex/.affected-functions.json`. macOS Mach-O
fallback to `nm -gU` since `-D --undefined-only` is ELF-only.
`.PHONY` extended.
- `.github/workflows/build.yaml`: new `auto-vex-reachability` job
gated on `github.event_name != 'pull_request'`. `needs: [build,
extract-version, grype-triage]`. Builds the bin (cached cargo),
downloads the `5spot-linux-amd64` artifact + the `grype-triage-*`
artifacts, runs `nm -D --undefined-only` on the binary, runs the
bin once per variant, merges per-variant outputs via `vexctl
merge`, uploads `vex-auto-reachability` (the merged VEX doc) and
`vex-auto-reachability-evidence` (the raw `nm` output, for the
Security team to re-derive against).

### Changed
- `.github/workflows/build.yaml`:
- `build-vex` job: `needs` extended with `auto-vex-reachability`.
Added a download step for the `vex-auto-reachability` artifact
and an explicit `test -f` guard. `vexctl merge` invocation now
includes `auto/vex.auto-reachability.json` alongside
`.vex/*.json` and `auto/vex.auto-presence.json`.
- Header comments on `build-vex` updated to describe both
automated phases.
- `src/auto_vex_presence.rs`: `load_triaged_from_vex_dir` now skips
dot-prefixed `*.json` files, so `.vex/.affected-functions.json`
isn't mis-parsed as an OpenVEX statement when `auto-vex-reachability`
loads `--vex-dir`. Added `load_triaged_skips_dotfiles` test in
`auto_vex_presence_tests.rs`.
- `src/lib.rs`: re-export `pub mod auto_vex_reachability`.
- `.vex/README.md`: rewritten "Automated statements" section to
describe both Phase 2 (presence) and Phase 3 (reachability)
artifacts. New sub-section documenting the
`.vex/.affected-functions.json` mapping format.
- `docs/src/security/vex.md`:
- CI flow steps renumbered to 7 — added step 3 for
auto-reachability between presence (step 2) and assemble (step 4).
- "What we automate" section: added the symbol-import-reachability
bullet and a new "Why symbol-imports, not LLVM-IR call graphs"
sub-section explaining the scope revision.
- Roadmap (`~/dev/roadmaps/5spot-automated-vex-generation.md`):
status line updated to "Phase 3 ✅ shipped (symbol-import variant)".
Phase 3 section rewritten to document the scope revision with
rationale. Rollout section updated.

### Removed
- `.vex/CVE-2010-4756.json` (glob/fnmatch — superseded by auto-reachability)
- `.vex/CVE-2018-20796.json` (regcomp/regexec)
- `.vex/CVE-2019-9192.json` (regcomp/regexec)
- `.vex/CVE-2026-27171.json` (crc32_combine[64])
- `.vex/CVE-2026-4046.json` (iconv/iconv_open/iconv_close)
- `.vex/CVE-2026-5358.json` (NIS / yp_*)
- `.vex/CVE-2026-5450.json` (scanf family)
- `.vex/CVE-2026-5928.json` (ungetwc)

These 8 statements all asserted `vulnerable_code_not_in_execute_path`
and are now produced by `auto-vex-reachability` from the curated
mapping + the `nm -D --undefined-only` symbol-import evidence
uploaded by the new CI job. The Security team verifies them by
re-running the same `nm` invocation against the release-attested
binary. `CVE-2026-4438.json` is intentionally retained because its
hand-authored statement carries the stronger
`vulnerable_code_cannot_be_controlled_by_adversary` justification —
see Why below.

### Why
Phase 3 of the automated VEX generation roadmap, with a scope
revision. The original plan targeted Rust LLVM-IR call-graph
reachability against RustSec advisories carrying `affected.functions`.
At implementation time `cargo audit` reports zero open Rust-level
vulnerabilities for this codebase — every CVE in `.vex/` is a
base-image glibc/zlib finding from Grype scanning the Docker image,
and RustSec doesn't track those. The Rust call-graph approach
addressed zero current findings.

The mechanical equivalent for our actual data is symbol-import
absence: if the Rust binary's dynamic-symbol-import table doesn't
contain any of the documented public-API entry points for an
affected glibc function, the affected code path cannot be reached
through documented APIs. This is exactly what the existing 14
hand-authored impact statements claim in prose ("5-Spot performs no
character-set conversions; the glibc iconv() path exercised by this
CVE is unreachable from the controller binary"). Auto-reachability
turns that prose into a verifiable check that the Security team
re-derives during counter-signing.

The shipped variant addresses 9 of the 15 hand-authored statements
through the curated mapping. Of those 9, **8 hand-authored
statements were deleted** in this same change (their
`vulnerable_code_not_in_execute_path` claim is now produced by
auto-reachability and re-derivable from the uploaded `nm` evidence).
The 9th mapped CVE (`CVE-2026-4438`) was retained because its
hand-authored claim is the stronger
`vulnerable_code_cannot_be_controlled_by_adversary` — the auto-VEX
would replace it with the weaker function-absence claim. The
remaining 6 hand-authored statements are not function-bounded
(adversary-model preconditions, ASLR bypasses,
missing-shell-at-runtime, Rust-crate combo) and stay hand-authored.
The original LLVM-IR call-graph approach is deferred to when (a)
`cargo audit` starts returning Rust-side findings with
function-level data, or (b) we adopt a Rust analyzer that exposes
call-graph data without LTO loss.

### Verification
- `cargo fmt --all -- --check`: clean.
- `cargo clippy --all-targets --all-features -- -D warnings`: clean.
- `cargo test --lib`: 418 tests pass (398 pre-existing + 20 new for
`auto_vex_reachability`).
- Smoke tests:
- 10 Grype matches (9 mapped + 1 unmapped) + an empty `.vex/` dir +
nm output without any of the affected glibc symbols → 9
statements emitted in deterministic CVE-id order. Unmapped CVE
correctly skipped.
- Same input but with `glob` added to nm output → 8 statements
emitted (CVE-2010-4756 correctly excluded because `glob` is
importable).
- Same input against the real `.vex/` directory (all 9 mapped CVEs
are hand-authored there) → 0 statements emitted because
`load_triaged_from_vex_dir` correctly skips re-deriving
already-triaged CVEs (and correctly skips
`.affected-functions.json` thanks to the new dotfile filter).
- `vexctl merge` of the auto-reachability output + `.vex/*.json` +
vex.auto-presence.json produces a well-formed merged document
parseable by `vexctl` with no errors.

### Trust model (unchanged from Phase 2)
The merged VEX (hand-authored + auto-presence + auto-reachability)
is Cosign-attested by CI keyless against both image digests. The
Security team independently re-runs `nm -D --undefined-only` against
the release-attested binary and checks each auto-reachability
statement's claim, then counter-signs with their own OIDC identity
if they agree. Downstream consumers gate on both attestations via
`cosign verify-attestation` with twin
`--certificate-identity-regexp` invocations.

### Impact
- [ ] Breaking change
- [ ] Requires cluster rollout
- [ ] Config change only
- [x] Documentation only

*(Not really "documentation only" — net +700 LOC of Rust + tests + a
new CI job — but no runtime, CRD, or API-surface change. Closest
pre-existing box.)*

---

## [2026-04-30 14:00] - Phases 5 + 6 of security audit: RBAC tightening + defence-in-depth docs

**Author:** Erick Bourgeois

Expand Down
155 changes: 146 additions & 9 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,135 @@ jobs:
path: vex.auto-presence.json
if-no-files-found: error

# ─────────────────────────────────────────────────────────────────────────────
# Auto-VEX (symbol-import reachability) — push + release. Roadmap Phase 3.
#
# For each Grype triage finding whose CVE id maps to a list of
# affected library function names in `.vex/.affected-functions.json`,
# check whether the release binary's dynamic symbol-import table
# contains any of those names. If none → emit a `not_affected +
# vulnerable_code_not_in_execute_path` statement. The result is
# uploaded as the `vex-auto-reachability` artifact and merged
# unconditionally into the signed VEX document by `build-vex`.
#
# Why "symbol-import reachability" instead of full LLVM-IR call graph:
# cargo audit reports zero open Rust-level vulnerabilities for this
# codebase; every CVE we triage is a base-image glibc/zlib finding
# surfaced by Grype. Those CVEs are not tracked in RustSec, so the
# original roadmap's RustSec-affected-functions approach addresses
# zero current findings. Symbol-import absence is the mechanical
# equivalent for our actual data: if our Rust binary doesn't link
# against `glob`/`scanf`/`iconv`, the affected glibc code paths
# cannot be reached through documented entry points.
# ─────────────────────────────────────────────────────────────────────────────
auto-vex-reachability:
name: 🤖 Auto-VEX (reachability)
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
needs: [build, extract-version, grype-triage]
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable (moving ref — re-pin quarterly)

- name: Cache cargo dependencies
uses: firestoned/github-actions/rust/cache-cargo@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6

- name: Build auto-vex-reachability
run: cargo build --release --bin auto-vex-reachability

- name: Download release binary (5spot-linux-amd64)
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: 5spot-linux-amd64
path: ./bin

- name: Download triage scan artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: grype-triage-*
path: ./triage
merge-multiple: true

# Capture the binary's dynamic symbol-import table. This is the
# ELF view; on Linux runners we can use nm -D --undefined-only
# directly. The output becomes the evidence the Security team
# re-derives downstream during counter-signing.
- name: Capture binary symbol imports
run: |
set -euo pipefail
chmod +x bin/5spot
nm -D --undefined-only bin/5spot > symbols.txt
echo "── imported symbol count ──"
wc -l symbols.txt
echo "── first 20 symbols ──"
head -20 symbols.txt

# Run once per variant (binary symbols are identical across
# variants but Grype findings differ), merge per-variant outputs
# at the end.
- name: Run auto-vex-reachability
run: |
set -euo pipefail
vex_id="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/auto-vex-reachability"
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
per_variant=()
for variant in Chainguard Distroless; do
triage="triage/grype-triage-${variant}.json"
if [ ! -f "$triage" ]; then
echo "missing $triage; skipping variant $variant"
continue
fi
out="vex.auto-reachability-${variant}.json"
./target/release/auto-vex-reachability \
--grype-json "$triage" \
--binary-symbols symbols.txt \
--affected-functions .vex/.affected-functions.json \
--vex-dir .vex \
--product-purl "pkg:oci/5-spot" \
--id "${vex_id}/${variant}" \
--author "auto-vex-reachability" \
--timestamp "$ts" \
--output "$out"
per_variant+=("$out")
done
if [ ${#per_variant[@]} -eq 0 ]; then
echo "ERROR: no variant produced an auto-reachability document"
exit 1
fi

- name: Merge per-variant auto-reachability documents
run: |
make vexctl-install
vexctl merge \
--id "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/auto-vex-reachability" \
--author "auto-vex-reachability" \
vex.auto-reachability-*.json > vex.auto-reachability.json
echo "── vex.auto-reachability.json ─────────────────────────────────"
cat vex.auto-reachability.json

- name: Upload auto-reachability artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: vex-auto-reachability
path: vex.auto-reachability.json
if-no-files-found: error

# The symbols.txt evidence is uploaded separately so the Security
# team can re-run the same reachability analysis against the
# release-attested binary digest and confirm the auto-emitted
# statements.
- name: Upload symbol-imports evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: vex-auto-reachability-evidence
path: symbols.txt
if-no-files-found: error

# ─────────────────────────────────────────────────────────────────────────────
# VEX document — push to main + release.
#
Expand All @@ -666,19 +795,20 @@ jobs:
# the assembly here.
#
# Trust model: this job emits and Cosign-attests the VEX document. The
# auto-presence artifact from the Phase 2 job is merged unconditionally
# alongside the hand-authored `.vex/*.json` statements. Verification of
# the resulting claims is performed downstream by the Security team,
# which independently re-evaluates the VEX against the attached evidence
# (SBOMs, Cosign attestations, SLSA provenance) and counter-signs if
# they agree. There is no feature-flag gate on our side — automation is
# auto-presence (Phase 2) and auto-reachability (Phase 3) artifacts are
# merged unconditionally alongside the hand-authored `.vex/*.json`
# statements. Verification of the resulting claims is performed
# downstream by the Security team, which independently re-evaluates the
# VEX against the attached evidence (SBOMs, symbol-imports,
# Cosign attestations, SLSA provenance) and counter-signs if they
# agree. There is no feature-flag gate on our side — automation is
# aggressive, verification is external.
# ─────────────────────────────────────────────────────────────────────────────
build-vex:
name: 🧾 Assemble OpenVEX
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
needs: [docker, extract-version, auto-vex-presence]
needs: [docker, extract-version, auto-vex-presence, auto-vex-reachability]
permissions:
contents: read
id-token: write
Expand All @@ -697,9 +827,15 @@ jobs:
name: vex-auto-presence
path: ./auto

- name: Download auto-reachability artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: vex-auto-reachability
path: ./auto

# Canonical @id varies by event so downstream consumers can link back
# to the exact commit or release tag that produced the document.
# The auto-presence document is merged alongside hand-authored
# Both auto-* documents are merged alongside hand-authored
# statements on every build.
- name: Assemble OpenVEX document
id: assemble
Expand All @@ -717,10 +853,11 @@ jobs:
;;
esac
test -f auto/vex.auto-presence.json || { echo "ERROR: auto-presence artifact missing"; exit 1; }
test -f auto/vex.auto-reachability.json || { echo "ERROR: auto-reachability artifact missing"; exit 1; }
vexctl merge \
--id "$vex_id" \
--author "${{ github.event.release.author.login || github.actor }}" \
.vex/*.json auto/vex.auto-presence.json > vex.openvex.json
.vex/*.json auto/vex.auto-presence.json auto/vex.auto-reachability.json > vex.openvex.json
echo "── vex.openvex.json ─────────────────────────────────────────"
cat vex.openvex.json

Expand Down
Loading
Loading