From 6f8d0b4ba681eca29bcb20a1d895b7711eb06493 Mon Sep 17 00:00:00 2001 From: Isaac Schepp Date: Wed, 14 Oct 2026 09:05:41 -0500 Subject: [PATCH 1/2] fix: Correct and bound the GO-2026-5932 osv-scanner suppression The suppression reason claimed golang.org/x/crypto is reached only through go-git's use of hkdf; the build graph also uses ssh, ssh/agent, ssh/knownhosts, blowfish, cast5, argon2, sha3 and more. List them with their importers, and say none is openpgp. Give the entry an ignoreUntil expiry so renewal is forced, strengthen the re-verification command to cover test imports and other GOOS build constraints, and record that the ID-only match cannot be scoped to a Go package, with govulncheck as the backstop. The govulncheck SARIF filter now honours ignoreUntil (matching osv-scanner's same-day expiry) and refuses config keys it does not implement, including an entry without id, so the two scanners cannot silently diverge. The toml header names the filter as a consumer and states that it covers ./go.mod only, since osv-scanner applies a config to its own directory. SECURITY-POSTURE.md no longer duplicates the per-entry rationale; it states the expiry rule and the ID-scoping limit and points at the toml. Assisted-by: Claude Signed-off-by: Isaac Schepp --- SECURITY-POSTURE.md | 6 +-- osv-scanner.toml | 61 +++++++++++++++++++++++------ scripts/filter-govulncheck-sarif.py | 50 +++++++++++++++++++++-- 3 files changed, 99 insertions(+), 18 deletions(-) diff --git a/SECURITY-POSTURE.md b/SECURITY-POSTURE.md index 557531713..bd03e3a47 100644 --- a/SECURITY-POSTURE.md +++ b/SECURITY-POSTURE.md @@ -82,15 +82,15 @@ A SHA pin is never "tidied" back down to a bare tag: upstream tags are force-rep #### Vulnerability suppression - + `govulncheck` decides applicability by reachability: it reports a dependency advisory only when the vulnerable symbol is actually called. The OpenSSF Scorecard *Vulnerabilities* check does not — it runs [osv-scanner](https://github.com/google/osv-scanner) over the manifests and reports every advisory that matches a resolved module version, whether or not the affected package is in the build graph. The two tools therefore disagree on advisories that are scoped to a package GLX does not import, and that disagreement is resolved in [`osv-scanner.toml`](https://github.com/genealogix/glx/blob/main/osv-scanner.toml) at the repository root, the file osv-scanner and Scorecard both read. `govulncheck`'s SARIF output still lists such an advisory as a module-level finding (level `note`: "your code depends on a vulnerable module, but doesn't appear to call any of the vulnerable symbols"), and Code Scanning raises an alert for it. Because `govulncheck` has no ignore mechanism, the [Security workflow](https://github.com/genealogix/glx/blob/main/.github/workflows/security.yml) passes its SARIF through [`scripts/filter-govulncheck-sarif.py`](https://github.com/genealogix/glx/blob/main/scripts/filter-govulncheck-sarif.py) before upload, which drops a finding only when its ID is in `osv-scanner.toml` **and** it is module-level. One list governs both scanners, and a suppressed advisory that becomes imported (`warning`) or called (`error`) is reported again, since that is exactly the case in which its recorded reasoning has stopped holding. -The rule for that file: an advisory is suppressed only when it cannot be cleared by upgrading **and** the affected package is provably outside the build graph, and every entry states the reason and the command that re-verifies it. An advisory that applies is fixed by bumping the dependency, never by an entry here — which is the same principle as [Action pinning](#action-pinning) above, where the control’s accepted consequence and its known limits are written down rather than papered over. +The rule for that file: an advisory is suppressed only when it cannot be cleared by upgrading **and** the affected package is provably outside the build graph, and every entry states the reason, the command that re-verifies it, and an `ignoreUntil` expiry by which that command must be re-run. An expired entry stops suppressing in osv-scanner and in the SARIF filter alike. An advisory that applies is fixed by bumping the dependency, never by an entry here — which is the same principle as [Action pinning](#action-pinning) above, where the control’s accepted consequence and its known limits are written down rather than papered over. -One entry exists today. `GO-2026-5932` marks `golang.org/x/crypto/openpgp` unmaintained; it has no fixed version, so no upgrade clears it. `golang.org/x/crypto` is reached only through `github.com/go-git/go-git/v5`, which uses `golang.org/x/crypto/hkdf` and gets its OpenPGP support from `github.com/ProtonMail/go-crypto/openpgp` — the maintained replacement. `go list -deps ./...` returns no `golang.org/x/crypto/openpgp` package, and `govulncheck` agrees, reporting it as present in a required module but uncalled. +Entries are keyed by advisory ID, because osv-scanner cannot scope one to a Go package; a revised advisory that starts covering an imported package is therefore hidden from osv-scanner until the entry expires, and `govulncheck` (reading the advisory's current package list) is the backstop that reports it in the meantime. The file covers the root `go.mod` only — osv-scanner applies a config to manifests in its own directory — so `ci-tools/go.mod` is scanned with no suppressions. The entries themselves, with the reasoning and re-verification command for each, live only in [`osv-scanner.toml`](https://github.com/genealogix/glx/blob/main/osv-scanner.toml); this section deliberately does not repeat them. ### Level 3 — Mature diff --git a/osv-scanner.toml b/osv-scanner.toml index 20153a7c0..afc9e463f 100644 --- a/osv-scanner.toml +++ b/osv-scanner.toml @@ -1,29 +1,66 @@ # osv-scanner configuration for the GLX root Go module. # # Entries here suppress an OSV advisory for this repository only. Every entry -# must state why the advisory does not apply, and must be removed as soon as the -# reasoning stops holding. This file is not a general allowlist: advisories that -# do apply are fixed by upgrading the dependency, not ignored here. +# must state why the advisory does not apply, must carry an `ignoreUntil` expiry +# so the reasoning is re-verified on a schedule, and must be removed as soon as +# the reasoning stops holding. This file is not a general allowlist: advisories +# that do apply are fixed by upgrading the dependency, not ignored here. # -# Consumed by `osv-scanner` and, transitively, by the OpenSSF Scorecard -# `Vulnerabilities` check (.github/workflows/scorecard.yml), which runs -# osv-scanner over the repository. +# Consumed by: +# - `osv-scanner`, and transitively the OpenSSF Scorecard `Vulnerabilities` +# check (.github/workflows/scorecard.yml), which runs osv-scanner over the +# repository; +# - scripts/filter-govulncheck-sarif.py, which applies the same list to the +# govulncheck SARIF upload in .github/workflows/security.yml. +# The filter understands only `[[IgnoredVulns]]` entries with `id`, +# `ignoreUntil` and `reason`, and refuses to run on anything else, so the two +# scanners cannot silently disagree about what is suppressed. +# +# Scope: osv-scanner applies an osv-scanner.toml only to manifests in its own +# directory, so this file covers ./go.mod and nothing else. ci-tools/go.mod is +# scanned without any suppressions; it does not depend on golang.org/x/crypto. +# A suppression it ever needs goes in ci-tools/osv-scanner.toml, not here. +# +# The reasoning for each entry lives here and only here; SECURITY-POSTURE.md +# ("Vulnerability suppression") states the policy and points at this file. [[IgnoredVulns]] id = "GO-2026-5932" +ignoreUntil = 2027-04-14 reason = """ Advisory scope is golang.org/x/crypto/openpgp (and its subpackages), which GLX does not import. The advisory has no fixed version -- it marks those packages as unmaintained rather than patching a defect -- so upgrading cannot clear it. -golang.org/x/crypto is an indirect dependency reached only through -github.com/go-git/go-git/v5, which uses golang.org/x/crypto/hkdf; go-git's -OpenPGP support is provided by github.com/ProtonMail/go-crypto/openpgp, the -maintained replacement. Verified with: +golang.org/x/crypto is an indirect dependency, reached entirely through +github.com/go-git/go-git/v5 and its dependencies. As of 2026-10-14 the build +graph (including tests) uses these x/crypto packages: + + ssh, ssh/agent, ssh/knownhosts, ssh/internal/bcrypt_pbkdf, blowfish + go-git's SSH transport, github.com/skeema/knownhosts, + github.com/xanzy/ssh-agent + cast5, argon2, sha3, hkdf + github.com/ProtonMail/go-crypto/openpgp, the maintained OpenPGP + replacement go-git uses for signing + cryptobyte, curve25519, chacha20, blake2b, internal/* + github.com/cloudflare/circl and the packages above - go list -deps ./... | grep golang.org/x/crypto/openpgp # no matches +None of them is openpgp. Verified with: -Re-verify with that command before renewing this entry; drop the entry if + for os in linux darwin windows; do GOOS=$os go list -deps -test ./...; done \ + | grep golang.org/x/crypto/openpgp # no matches + +`-test` includes test-only imports; the GOOS loop covers files behind OS build +constraints. Re-run it before renewing `ignoreUntil`; drop the entry if golang.org/x/crypto/openpgp ever enters the build graph, and fix the import instead. + +Known limit: osv-scanner matches this entry by advisory ID only and cannot +scope it to a Go package. If GO-2026-5932 is ever revised to cover another +x/crypto package -- cast5 and blowfish above are deprecated too -- osv-scanner +would hide that as well until `ignoreUntil` passes. govulncheck is the backstop: +it reads the advisory's current package list, and the SARIF filter keeps any +finding that reaches an imported package (level `warning`) or a called symbol +(`error`). Before renewing, also re-read the advisory's affected packages at +https://pkg.go.dev/vuln/GO-2026-5932. """ diff --git a/scripts/filter-govulncheck-sarif.py b/scripts/filter-govulncheck-sarif.py index 161bab4be..ddfabfc7b 100644 --- a/scripts/filter-govulncheck-sarif.py +++ b/scripts/filter-govulncheck-sarif.py @@ -13,6 +13,13 @@ imported) or "error" (symbol called) is kept, because that means the reasoning recorded in osv-scanner.toml no longer holds. +An entry whose `ignoreUntil` date has passed suppresses nothing, matching +osv-scanner, so an expired entry resurfaces in both scanners at once. The +filter understands only `[[IgnoredVulns]]` entries with `id`, `ignoreUntil` +and `reason`; any other key (for example `[[PackageOverrides]]`) is an error +rather than being skipped, because osv-scanner would honour it and the two +scanners would silently disagree. + It also drops repeated entries from each result's "stacks" array. govulncheck can emit the same trace twice (for example, when a vulnerable symbol is reached through both the standard library's bundled http2 and golang.org/x/net), and @@ -23,6 +30,7 @@ python3 scripts/filter-govulncheck-sarif.py osv-scanner.toml < in.sarif > out.sarif """ +import datetime import json import sys import tomllib @@ -31,10 +39,43 @@ DROPPABLE_LEVEL = "note" -def suppressed_ids(config_path): +# The osv-scanner.toml keys this filter implements; see the module docstring. +SUPPORTED_TOP_LEVEL_KEYS = {"IgnoredVulns"} +SUPPORTED_ENTRY_KEYS = {"id", "ignoreUntil", "reason"} + + +def expiry_date(value): + """Return ignoreUntil as a date. TOML may give a date or a datetime. + + osv-scanner reads a bare date as midnight, so the entry stops applying on + that day; callers compare with <= to match. + """ + if isinstance(value, datetime.datetime): + return value.date() + if isinstance(value, datetime.date): + return value + raise ValueError(f"ignoreUntil must be a TOML date, got {value!r}") + + +def suppressed_ids(config_path, today): with open(config_path, "rb") as f: config = tomllib.load(f) - return {entry["id"] for entry in config.get("IgnoredVulns", [])} + unsupported = set(config) - SUPPORTED_TOP_LEVEL_KEYS + if unsupported: + raise ValueError(f"{config_path}: unsupported key(s) {sorted(unsupported)}; teach this filter about them first") + + ids = set() + for index, entry in enumerate(config.get("IgnoredVulns", [])): + if "id" not in entry: + raise ValueError(f"{config_path}: IgnoredVulns entry {index} has no id") + unsupported = set(entry) - SUPPORTED_ENTRY_KEYS + if unsupported: + raise ValueError(f"{config_path}: {entry['id']} has unsupported key(s) {sorted(unsupported)}") + if "ignoreUntil" in entry and expiry_date(entry["ignoreUntil"]) <= today: + print(f"not suppressing {entry['id']}: ignoreUntil {entry['ignoreUntil']} has passed", file=sys.stderr) + continue + ids.add(entry["id"]) + return ids def dedupe_stacks(result): @@ -56,7 +97,10 @@ def dedupe_stacks(result): def main(): if len(sys.argv) != 2: sys.exit(__doc__) - ignored = suppressed_ids(sys.argv[1]) + try: + ignored = suppressed_ids(sys.argv[1], datetime.date.today()) + except ValueError as err: + sys.exit(f"filter-govulncheck-sarif: {err}") sarif = json.load(sys.stdin) for run in sarif.get("runs", []): From 2d989a8cf0726c048c591097f81ed46f1a177fe1 Mon Sep 17 00:00:00 2001 From: Isaac Schepp Date: Wed, 14 Oct 2026 09:08:21 -0500 Subject: [PATCH 2/2] docs: Add changelog entry for osv-scanner suppression fixes Assisted-by: Claude Signed-off-by: Isaac Schepp --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7722f1835..28e772876 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### Fixed +- **The `GO-2026-5932` suppression in `osv-scanner.toml` now expires and states the dependency graph correctly** — Its reason said `golang.org/x/crypto` is reached only through go-git's use of `hkdf`; the build graph also uses `ssh`, `ssh/agent`, `ssh/knownhosts`, `blowfish`, `cast5`, `argon2`, `sha3` and more (none of them `openpgp`), now listed with their importers. The entry gains an `ignoreUntil` expiry, its re-verification command now covers test imports and linux/darwin/windows build constraints (`go list -deps -test`), and it records that osv-scanner matches by advisory ID only, with govulncheck as the backstop. `scripts/filter-govulncheck-sarif.py` honours `ignoreUntil` the same way osv-scanner does and refuses config it does not implement (such as `[[PackageOverrides]]` or an entry without `id`) instead of silently diverging. The toml header names the filter as a consumer and notes that it covers the root `go.mod` only, so `ci-tools/go.mod` is scanned unsuppressed; `SECURITY-POSTURE.md` points at the toml instead of duplicating its reasoning. No GLX code changed. (PR [#1393](https://github.com/genealogix/glx/pull/1393)) - **`glx proof --question parentage` counts an assertion on the child's own participant** — A `parent_child` relationship asserted with `--participant :child`, the natural way to record "this baptism shows her as the child", was listed under Evidence Collected and then ignored: the conclusion read `INSUFFICIENT EVIDENCE — Parents not yet identified` even though the relationship names both parents. Such an assertion now supports the conclusion like one on a parent participant or an existential one; it still never supplies the answer itself, and step relationships, step roles and disproven assertions are excluded as before. (#1367) - **A baptism or christening is birth evidence for `glx analyze` and `glx proof`** — Before civil registration the baptism entry is the birth record, and many registers give no birth date at all. `glx analyze --check gaps` no longer reports HIGH "no birth date or place" for someone with a dated or placed baptism or christening, and the coverage checklist behind `glx proof` and `glx coverage` counts the person's own evidenced baptism or christening as the "Birth record" when no birth event is evidenced (a baptism the person attended in another role, such as godparent, does not count). (#1365) - **Event evidence respects privacy and asserted roles** — Publishing with `--living` drops assertions about fully redacted events so their citation text and media cannot reappear on a person's page. Participant assertions use the role asserted by the source, even when it differs from the event's conclusion; witness-only evidence no longer counts as the person's own record. (PR #1382)