From a971d17a8c436253636ad1ee1f0802c2a82f8a69 Mon Sep 17 00:00:00 2001 From: "randomizedcoder dave.seddon.ca@gmail.com" Date: Wed, 11 Nov 2026 17:46:05 -0800 Subject: [PATCH 1/2] feat(nix): govulncheck check, live-database app and CycloneDX SBOMs - checks.govulncheck: govulncheck against a pinned offline snapshot of golang/vulndb (nix/lib/govulndb.nix), with no tags and with every destination and enrichment tag. A reachable finding not in the check's accepted list fails; GO-2026-6859 (net/http, fixed in go1.27.4) is accepted until the toolchain bump. - nix run .#govulncheck-fresh: the same scans against the live database, plus go mod verify. - sbom- packages: a CycloneDX SBOM per binary from the linked module list (cyclonedx-gomod bin). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 +++ docs/static-analysis.md | 1 + nix/checks/default.nix | 2 + nix/checks/govulncheck.nix | 83 ++++++++++++++++++++++++++++++++++++++ nix/default.nix | 24 +++++++++++ nix/govulncheck-fresh.nix | 42 +++++++++++++++++++ nix/lib/govulndb.nix | 57 ++++++++++++++++++++++++++ nix/lib/sbom.nix | 25 ++++++++++++ nix/versions.nix | 4 ++ 9 files changed, 244 insertions(+) create mode 100644 nix/checks/govulncheck.nix create mode 100644 nix/govulncheck-fresh.nix create mode 100644 nix/lib/govulndb.nix create mode 100644 nix/lib/sbom.nix diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d7302ef8..92a82bc3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### New + +- **Vulnerability check.** `checks.govulncheck` runs govulncheck against a pinned snapshot of the Go vulnerability database, with no tags and with every flavor tag, and fails on a reachable finding not in its accepted list. One is accepted: GO-2026-6859 in `net/http`, fixed in go1.27.4, until the toolchain moves. `nix run .#govulncheck-fresh` checks against the live database and runs `go mod verify`; `nix build .#sbom-` writes a CycloneDX SBOM for any binary package. + ## 0.2.1 (2026-11) Fixes for what the first fleet rollout of 0.2.0 found: the link monitor baseline, the uplink columns, and container attribution. No schema, flag-default or `XtcpConfig` change; 0.2.0 clients and consumers work unchanged. diff --git a/docs/static-analysis.md b/docs/static-analysis.md index 82c35c0f6..8055c4d31 100644 --- a/docs/static-analysis.md +++ b/docs/static-analysis.md @@ -93,6 +93,7 @@ otherwise until 2026-10-05. | `golangci-lint` (Tier 1) | `.golangci.yml` | yes | ~2 min | `errcheck`, `gosec`, `misspell`, `contextcheck`, `noctx`, `gocritic`, `gocyclo` (promoted 2026-10-06) | | `golangci-lint-comprehensive` (Tier 2) | `.golangci-comprehensive.yml` | yes | ~10 min | **`funlen`, `goconst`, `unconvert`, `exhaustive`, `prealloc`, `dupl`, `nakedret`** | | `go-sec` | gosec directly | yes | ~2 min | gosec standalone; sees **no `_test.go`**, since it does not pass `-tests` and gosec skips them by default | +| `govulncheck` | `nix/checks/govulncheck.nix`, database `nix/lib/govulndb.nix` | yes | minutes | known vulnerabilities the code reaches, against a pinned offline database, with and without every flavor tag; fails on one not in the file's `accepted` list. `nix run .#govulncheck-fresh` checks against the live database and runs `go mod verify`. `nix build .#sbom-` writes a CycloneDX SBOM | | `go-vet` | `enable-all`, minus `fieldalignment` and `shadow` | yes | fast | — | | `gofmt` / `nix-fmt` | — | yes | fast | formatting | | `deadnix` / `statix` | `statix.toml` | yes | fast | Nix dead code and antipatterns | diff --git a/nix/checks/default.nix b/nix/checks/default.nix index 11d6ed084..d98e14605 100644 --- a/nix/checks/default.nix +++ b/nix/checks/default.nix @@ -94,6 +94,8 @@ in inherit pkgs vendoredSource; }; go-sec = import ./go-sec.nix { inherit pkgs vendoredSource; }; + # Reachable known vulnerabilities, against a pinned offline database. + govulncheck = import ./govulncheck.nix { inherit pkgs vendoredSource; }; # The lint ratchet, and the only check here that is GREEN at baseline. The # three tier checks above report a tier's whole finding list, which has been diff --git a/nix/checks/govulncheck.nix b/nix/checks/govulncheck.nix new file mode 100644 index 000000000..5e9d9f3de --- /dev/null +++ b/nix/checks/govulncheck.nix @@ -0,0 +1,83 @@ +# nix/checks/govulncheck.nix +# +# Known vulnerabilities reachable from xtcp2's code, by govulncheck against an +# offline snapshot of the Go vulnerability database (nix/lib/govulndb.nix), +# so the check is pure and its result changes only when the code, the Go +# toolchain or the snapshot does. A finding the code reaches fails the build +# unless it is in `accepted` below; a finding in a required module the code +# does not call is reported but does not fail. +# +# Two passes: no build tags (the min flavor), and every destination and +# enrichment tag, so code behind a flavor tag is scanned too. +# +# The snapshot ages; `nix run .#govulncheck-fresh` checks the working tree +# against the live database, and moving the pin in nix/lib/govulndb.nix brings +# what it finds into this check. +{ + pkgs, + vendoredSource, +}: + +let + versions = import ../versions.nix { inherit pkgs; }; + db = import ../lib/govulndb.nix { inherit pkgs; }; + allTags = pkgs.lib.concatStringsSep "," ( + map (d: "dest_${d}") versions.allLibraryDestinations + ++ map (e: "enrich_${e}") versions.allEnrichmentFeatures + ); + + # Reachable findings accepted for now, each with why and what removes it. + # Keep this short: an entry is a known exposure, not a suppression. + accepted = { + "GO-2026-6859" = + "net/http, fixed in go1.27.4; the pinned nixpkgs has go1.27.3. Remove with the toolchain bump."; + }; + acceptedJSON = builtins.toJSON (builtins.attrNames accepted); +in +pkgs.runCommand "xtcp2-govulncheck" + { + nativeBuildInputs = [ + versions.go + versions.govulncheck + pkgs.jq + ]; + inherit vendoredSource; + # For the govulncheck-fresh app, so both scan the same tag sets. + passthru = { inherit allTags; }; + } + '' + cp -r $vendoredSource ./xtcp2 && chmod -R +w ./xtcp2 + cd ./xtcp2 + export HOME=$(mktemp -d) + export CGO_ENABLED=0 + export GOFLAGS=-mod=vendor + export GOTOOLCHAIN=local + mkdir -p $out + # One pass per tag set: the text report for people, the JSON stream for + # the gate. A finding is reachable when its trace starts at a function. + scan() { + govulncheck -db file://${db} "$@" ./... > "$out/$name.txt" 2>&1 || true + govulncheck -db file://${db} -format json "$@" ./... > "$out/$name.json" + jq -r 'select(.finding and .finding.trace[0].function) | .finding.osv' \ + "$out/$name.json" | sort -u > "$out/$name.reachable" + } + name=default scan + name=all-tags scan -tags ${allTags} + + echo '${acceptedJSON}' | jq -r '.[]' | sort -u > accepted + sort -u $out/*.reachable > reachable + comm -23 reachable accepted > $out/unaccepted + comm -13 reachable accepted > $out/stale + + cat $out/default.txt + if [ -s $out/stale ]; then + echo "accepted but no longer reachable, remove from nix/checks/govulncheck.nix:" + cat $out/stale + fi + if [ -s $out/unaccepted ]; then + echo "FAIL: reachable vulnerabilities not in the accepted list:" + cat $out/unaccepted + cat $out/all-tags.txt + exit 1 + fi + '' diff --git a/nix/default.nix b/nix/default.nix index eb82061c1..f3939d3ab 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -277,6 +277,14 @@ let # Its hermetic counterpart, checks.upstream-pins, keeps the manifest honest. checkUpstreamPins = import ./check-upstream-pins.nix { inherit pkgs; }; + # Live-database counterpart of checks.govulncheck, for the same reason. + govulncheckFresh = import ./govulncheck-fresh.nix { + inherit pkgs versions; + inherit (checks.govulncheck) allTags; + }; + + sbomFor = import ./lib/sbom.nix { inherit pkgs versions; }; + # The five golangci-lint tier helpers (lint-quick / lint / # lint-comprehensive / lint-fix / lint-new). They live in their own file # because nix/devshell.nix puts the very same derivations on the dev @@ -973,6 +981,17 @@ in # oci-ipfeed-collector ASN artifact builder # oci-xtcp2-tcp-stress TCP_MODE-dispatched stress image // (lib.filterAttrs (n: _v: lib.hasPrefix "oci-" n) containers) + # sbom-: a CycloneDX SBOM per binary (nix/lib/sbom.nix). + // lib.mapAttrs' (name: drv: lib.nameValuePair "sbom-${name}" (sbomFor drv)) ( + lib.filterAttrs (_n: lib.isDerivation) ( + removeAttrs binaries [ + "byVariant" + "joins" + "xtcp2ByFlavor" + "xtcp2OnlyByFlavor" + ] + ) + ) // containersWithIpmetaBootstrap // linkmonitorPackages # lint-quick / lint / lint-comprehensive / lint-fix / lint-new. `all` is @@ -1202,6 +1221,11 @@ in type = "app"; program = "${checkUpstreamPins}/bin/xtcp2-check-upstream-pins"; }; + # Known vulnerabilities against the live database, and `go mod verify`. + govulncheck-fresh = { + type = "app"; + program = "${govulncheckFresh}/bin/xtcp2-govulncheck-fresh"; + }; # Run the whole microVM integration suite sequentially. Lifecycle sweep # by default; `-- --soak [--duration 1h]` adds the duration runners. integration-all = { diff --git a/nix/govulncheck-fresh.nix b/nix/govulncheck-fresh.nix new file mode 100644 index 000000000..360763374 --- /dev/null +++ b/nix/govulncheck-fresh.nix @@ -0,0 +1,42 @@ +# nix/govulncheck-fresh.nix +# +# Networked half of the vulnerability check: govulncheck on the working tree +# against the live Go vulnerability database, plus `go mod verify`. The flake +# check (nix/checks/govulncheck.nix) is pure, so it only knows the pinned +# snapshot in nix/lib/govulndb.nix; this runner says what the snapshot does +# not know yet. A RUNNER, not a check, for the same reason as +# check-upstream-pins: the check sandbox has no network. +# +# nix run .#govulncheck-fresh # report, exit 1 on any finding +# +# It reports every reachable finding, accepted or not: moving the snapshot pin +# brings them into the check, where the accepted list applies. +# +{ + pkgs, + versions, + allTags, +}: + +pkgs.writeShellApplication { + name = "xtcp2-govulncheck-fresh"; + runtimeInputs = [ + versions.go + versions.govulncheck + ]; + text = '' + if [ ! -f go.mod ]; then + echo "run from the xtcp2 repository root" >&2 + exit 2 + fi + export GOTOOLCHAIN=local + rc=0 + echo "==> go mod verify" + go mod verify || rc=1 + echo "==> govulncheck (no tags)" + govulncheck ./... || rc=1 + echo "==> govulncheck (-tags ${allTags})" + govulncheck -tags ${allTags} ./... || rc=1 + exit "$rc" + ''; +} diff --git a/nix/lib/govulndb.nix b/nix/lib/govulndb.nix new file mode 100644 index 000000000..20062fa71 --- /dev/null +++ b/nix/lib/govulndb.nix @@ -0,0 +1,57 @@ +# nix/lib/govulndb.nix +# +# An offline Go vulnerability database for govulncheck's `-db file://`, built +# from a pinned commit of github.com/golang/vulndb. vuln.go.dev serves the same +# OSV entries, but from mutable URLs a fixed-output fetch cannot pin; the repo +# carries them at data/osv, one GO-YYYY-NNNN.json each, so a commit is a +# reproducible snapshot. +# +# govulncheck reads the v1 layout (https://go.dev/security/vuln/database): +# index/db.json {"modified": the snapshot's date} +# index/modules.json [{path, vulns: [{id, modified}]}], one per module +# index/vulns.json [{id, modified, aliases}] +# ID/.json the OSV entry +# modules.json leaves out each vuln's optional "fixed": the client uses it only +# to skip fetching entries already fixed in the scanned version, so without it +# every entry for a module in the build is read and matched in full. +# +# Refresh: move `rev` to golang/vulndb's current master and update `hash` +# (`nix build .#checks.x86_64-linux.govulncheck` prints the new one). `nix run +# .#govulncheck-fresh` checks against the live database in between. +{ pkgs }: + +let + # The commit's date: the repo's entries carry zero timestamps (vuln.go.dev + # stamps them when it publishes), so the index says when the snapshot is from. + snapshot = "2026-11-11T00:00:00Z"; + vulndb = pkgs.fetchFromGitHub { + owner = "golang"; + repo = "vulndb"; + # master on 2026-11-11. + rev = "e91d8491530fd8782760dd3916cd2708182b9f10"; + hash = "sha256-QjH81HGQZ8yR/kYtK6pXtReRzc+uloTDSB90qxhFSck="; + sparseCheckout = [ "data/osv" ]; + }; +in +pkgs.runCommand "go-vulndb" + { + nativeBuildInputs = [ pkgs.jq ]; + passthru = { inherit vulndb; }; + } + '' + mkdir -p $out/ID $out/index + cp ${vulndb}/data/osv/GO-*.json $out/ID/ + count=$(ls $out/ID | wc -l) + if [ "$count" -lt 1000 ]; then + echo "go-vulndb: only $count entries in data/osv; the layout changed?" >&2 + exit 1 + fi + jq -cn --arg m ${snapshot} '{modified: $m}' > $out/index/db.json + jq -cs 'map({id, modified, aliases: (.aliases // [])})' $out/ID/*.json > $out/index/vulns.json + jq -cs ' + [ .[] as $v | ($v.affected // [])[] | {path: .package.name, id: $v.id, modified: $v.modified} ] + | group_by(.path) + | map({path: .[0].path, vulns: (map({id, modified}) | unique_by(.id))}) + ' $out/ID/*.json > $out/index/modules.json + echo "go-vulndb: $count entries, $(jq length $out/index/modules.json) modules, modified $(jq -r .modified $out/index/db.json)" + '' diff --git a/nix/lib/sbom.nix b/nix/lib/sbom.nix new file mode 100644 index 000000000..b29735d57 --- /dev/null +++ b/nix/lib/sbom.nix @@ -0,0 +1,25 @@ +# nix/lib/sbom.nix +# +# A CycloneDX SBOM for a Go binary package: one .cdx.json per file in +# its bin/, read by `cyclonedx-gomod bin` from the module list the Go linker +# embeds, so it names exactly the module versions that were linked in. +# +# nix build .#sbom-xtcp2-s3parquet +# +{ + pkgs, + versions, +}: + +drv: +pkgs.runCommand "${drv.pname or drv.name}-sbom" + { + nativeBuildInputs = [ versions.cyclonedx-gomod ]; + } + '' + mkdir -p $out + for b in ${drv}/bin/*; do + cyclonedx-gomod bin -json -version ${drv.version or "devel"} \ + -output "$out/$(basename "$b").cdx.json" "$b" + done + '' diff --git a/nix/versions.nix b/nix/versions.nix index d8168d873..de3c3e81a 100644 --- a/nix/versions.nix +++ b/nix/versions.nix @@ -55,6 +55,10 @@ }); inherit (pkgs) nixfmt; + # Supply chain: nix/checks/govulncheck.nix (with the offline database in + # nix/lib/govulndb.nix), the govulncheck-fresh app, and the CycloneDX SBOMs. + inherit (pkgs) govulncheck cyclonedx-gomod; + # gRPC / proto inspection inherit (pkgs) grpcurl; From 68c33d843da7ab7de0bea555ff3f59e760dcc706 Mon Sep 17 00:00:00 2001 From: "randomizedcoder dave.seddon.ca@gmail.com" Date: Wed, 11 Nov 2026 20:33:11 -0800 Subject: [PATCH 2/2] style(nix): strict ShellCheck cleanups in the govulncheck-fresh runner [[ ]] for the go.mod test and a braced ${rc}; shellcheck -o all is clean. Co-Authored-By: Claude Opus 5.5 --- nix/govulncheck-fresh.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/govulncheck-fresh.nix b/nix/govulncheck-fresh.nix index 360763374..fa01890fd 100644 --- a/nix/govulncheck-fresh.nix +++ b/nix/govulncheck-fresh.nix @@ -25,7 +25,7 @@ pkgs.writeShellApplication { versions.govulncheck ]; text = '' - if [ ! -f go.mod ]; then + if [[ ! -f go.mod ]]; then echo "run from the xtcp2 repository root" >&2 exit 2 fi @@ -37,6 +37,6 @@ pkgs.writeShellApplication { govulncheck ./... || rc=1 echo "==> govulncheck (-tags ${allTags})" govulncheck -tags ${allTags} ./... || rc=1 - exit "$rc" + exit "''${rc}" ''; }