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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>: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)
Expand Down
6 changes: 3 additions & 3 deletions SECURITY-POSTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,15 @@ A SHA pin is never "tidied" back down to a bare tag: upstream tags are force-rep

#### Vulnerability suppression

<!-- Last reviewed: 2026-09-18 -->
<!-- Last reviewed: 2026-10-14 -->

`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

Expand Down
61 changes: 49 additions & 12 deletions osv-scanner.toml
Original file line number Diff line number Diff line change
@@ -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.
"""
50 changes: 47 additions & 3 deletions scripts/filter-govulncheck-sarif.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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):
Expand All @@ -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", []):
Expand Down
Loading