Lane A (A.7-A.13): collectors, and a host boundary that now holds - #11
Merged
Conversation
The poller, the bulk-archive bootstrap, the host collector, the repo SCA collector, the warm-start accelerator, and the two security critics. Both critics returned FAIL; two adversarial rounds followed. The host read-only boundary S7 says the host agent is READ-ONLY -- no package manager in a mutating mode, NOT BEHIND A FLAG -- and section 0.2 reserved the strongest available reviewer for it because a miss is unrecoverable on a server someone depends on. The first guard matched callees by SOURCE SPELLING, and the critic proved it by compiling two host-mutating exec sites into the package that no analyser saw: an import alias and a function value. That is the third time on this project a check has lost by matching NAMES instead of resolving IDENTITY. It now resolves by import path, treats a reference to a spawning function as a spawn site, and FAILS CLOSED on what it cannot read -- dot imports, blank imports, assembly bodies, go:linkname, and any .s/.c/.syso in the package. The import set became an ALLOWLIST, because the old denylist let a fourth file import anything nobody had listed. An adversary then compiled a mutating exec in EIGHT shapes -- alias, dot import, function value, in-package wrapper, method value, interface call -- and every one was caught, most by three or four independent guards. All 18 mutating command lines are refused. The systemd guard now covers every Exec* directive and every occurrence, not ExecStart alone, and strips the +/!/!! prefixes that run outside the sandbox. The filesystem guard lost the same way and was fixed the same way: it was a hand-enumerated denylist that permitted os.OpenRoot, the *os.Root methods, os.CopyFS and os.FindProcess().Kill(). A read-only collector that can kill processes is not read-only. It is now an allowlist of eight operations, none of which writes, with handle tracking so os.OpenRoot -> root.WriteFile is two findings rather than none. And the claim "no package this one may import can spawn a process" was documented and enforced nowhere -- and was also FALSE, since os exports StartProcess. Deleted rather than softened, per the rule this project arrived at after four revisions of an overstated compliance document, and replaced with a transitive import-graph check measured across linux/amd64, linux/arm64, windows/amd64 and darwin/arm64 with CGO on and off. rpm -qa is not read-only, and the doc now says so On a Berkeley-DB-backed rpmdb -- RHEL/CentOS 7 and 8, SLES, Amazon Linux 2 -- opening the database creates and updates /var/lib/rpm/__db.001..003 when the caller can write. No rpm flag suppresses it. So the sentence claiming this collector never mutates the host was DELETED, not qualified, and replaced with the precise statement: which distros, which backends, and the mitigation, which is not in this package at all -- it is the unit's DynamicUser=yes plus ProtectSystem=strict, and removing either re-opens it. The accelerator Three blockers, all closed: a full warm start wrote the un-redistributable Trivy DB into mirror/tier2 on Windows because the quarantine check compared paths byte-exactly; the cache directory sat inside the git work tree matched by no ignore rule; and exchangeToken let THE REGISTRY choose where the operator's credential was sent, by parsing realm out of a header the registry controls. That last one is credential exfiltration driven by untrusted input, in a tool built to run against untrusted things. Then a Windows directory JUNCTION -- mklink /J, which needs no elevation -- defeated the fixed path check, because os.Lstat reports a junction as ModeIrregular and filepath.EvalSymlinks walks straight through it. Fixed at both configure time and write time, since a directory can be replaced by a junction between the two. The reason that survived matters more than the bug The one test that would have caught it SKIPPED four times on this host and the package still printed ok. A.12 had independently found the same failure mode on a different guard. Twice is a pattern. So internal/SKIPPED-CONTROLS.md inventories all 19 t.Skip sites in the repository, measured by running the suite rather than by reading conditions: 11 legitimate, 9 hazards, 8 now fixed. Among them a catch-all skip on the S7 "verified fixed" integrity gate that swallowed ANY fixture error, and two skips that fired on a CHECKED-IN file failing to load. The ninth is closed here, by me, because it needed a CI change: after the fix, the "a real scanner does not report a vulnerable fixture clean" control was honest but ran on NO MACHINE. There is now a trivy-e2e job that installs a pinned Trivy, runs it, and then HIDES THE BINARY and fails if the test still passes -- because a job that proves the control exists must itself be proved. Evidence: gofmt, vet, build clean; go test -count=1 ./... green across 17 packages. -race cannot run on this Windows host; CI runs it on Linux.
The trivy-e2e job 404ed. I wrote TRIVY_VERSION=0.58.2 from memory and it is not a release; the asset NAME pattern was right and the version was invented. That is the same assert-from-memory failure this project keeps catching in its own code, committed by me in the same PR that adds a job whose whole purpose is proving a control is real. Now v0.73.0, read from the GitHub releases API, and pinned by DIGEST as well as version because a tag can be moved and a version pin alone does not survive that. sha256sum -c runs before the archive is unpacked, so a substituted artifact fails the job rather than installing. Stated in the comment rather than left implicit: the digest is GitHub's, computed over the stored bytes. That is a primary source but it is NOT an independent re-hash -- the same distinction M0.7 flagged about its own opengrep pins, and worth keeping honest in both places.
The trivy-e2e job installed Trivy correctly (digest verified) and then the test failed with FATAL "--skip-db-update cannot be specified on the first run". That is not a CI quirk, it is the control doing its job. The collector invokes Trivy with --skip-db-update --offline-scan, which is the RIGHT posture -- S0 says Anvil never browses the live web at inference time -- but the consequence is that Trivy refuses to run at all until a database already exists locally. No test against recorded Trivy output could have surfaced that, because recorded output presupposes a run that succeeded. In production the database is what A.11's warm-start accelerator supplies. Here it is seeded the ordinary way, so the E2E control exercises the real invocation rather than a softened one. Worth stating because it sharpens an existing open question: this is a second, independent reason the accelerator matters. Its critic already flagged that consuming the Trivy DB may launder share-alike data past the licence quarantine, which is an argument against shipping it. This is an argument that something must fill that role regardless -- an offline scanner with no database is not a scanner.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The poller, bulk-archive bootstrap, host collector, repo SCA collector, warm-start accelerator, and the two security critics. Both critics returned FAIL; two adversarial rounds followed.
The host read-only boundary
S7: the host agent is read-only — no package manager in a mutating mode, not behind a flag — and §0.2 reserved the strongest reviewer for it because a miss is unrecoverable on a server someone depends on.
The first guard matched callees by source spelling, and the critic proved it by compiling two host-mutating exec sites into the package that no analyser saw. That's the third time on this project a check has lost by matching names instead of resolving identity.
It now resolves by import path, treats a reference to a spawning function as a spawn site, and fails closed on what it cannot read — dot imports, blank imports, assembly bodies,
go:linkname, any.s/.c/.syso. The import set became an allowlist, because the old denylist let a fourth file import anything nobody listed.An adversary then compiled a mutating exec eight ways — alias, dot import, function value, in-package wrapper, method value, interface call — and every one was caught, most by 3–4 independent guards. All 18 mutating command lines refused. The systemd guard now covers every
Exec*directive and every occurrence, and strips the+/!/!!prefixes that run outside the sandbox the unit's whole argument rests on.The filesystem guard lost the same way and was fixed the same way: a hand-enumerated denylist permitted
os.OpenRoot, the*os.Rootmethods,os.CopyFSandos.FindProcess().Kill(). A read-only collector that can kill processes is not read-only. Now an allowlist of eight operations, none of which writes, with handle tracking soos.OpenRoot→root.WriteFileis two findings rather than none.And "no package this one may import can spawn a process" was documented, enforced nowhere, and false (
osexportsStartProcess). Deleted rather than softened — the rule this project reached after four revisions of an overstated compliance document — and replaced with a transitive import-graph check measured across four platforms with CGO on and off.rpm -qais not read-only, and the doc now says soOn a Berkeley-DB-backed rpmdb — RHEL/CentOS 7 and 8, SLES, Amazon Linux 2 — opening the database creates and updates
/var/lib/rpm/__db.001..003when the caller can write. No rpm flag suppresses it.So the sentence claiming this collector never mutates the host was deleted, not qualified, and replaced with the precise statement: which distros, which backends, and the mitigation — which is not in this package at all. It's the unit's
DynamicUser=yesplusProtectSystem=strict, and removing either re-opens it.The accelerator
Three blockers, all closed: the un-redistributable Trivy DB written into
mirror/tier2on Windows (byte-exact path comparison); the cache directory inside the git work tree matched by no ignore rule; andexchangeTokenletting the registry choose where the operator's credential was sent, by parsingrealmout of a header the registry controls — credential exfiltration driven by untrusted input, in a tool built to run against untrusted things.Then a Windows directory junction (
mklink /J, no elevation) defeated the fixed path check, becauseos.Lstatreports a junction asModeIrregularandfilepath.EvalSymlinkswalks straight through. Fixed at configure time and write time, since a directory can be replaced by a junction between the two.The reason that survived matters more than the bug
The one test that would have caught it skipped four times on this host and the package still printed
ok.A.12had independently found the same failure mode on a different guard. Twice is a pattern.internal/SKIPPED-CONTROLS.mdnow inventories all 19t.Skipsites, measured by running the suite rather than reading conditions: 11 legitimate, 9 hazards, 8 fixed. Among them a catch-all skip on the S7 "verified fixed" integrity gate that swallowed any fixture error, and two skips that fired when a checked-in file failed to load.The ninth is closed here by me, because it needed a CI change: after the fix, the "a real scanner does not report a vulnerable fixture clean" control was honest but ran on no machine. There's now a
trivy-e2ejob that installs a pinned Trivy, runs it, then hides the binary and fails if the test still passes — a job that proves a control must itself be proved.Testing
-racecan't run on this Windows host; CI runs it on Linux.For you, not fixed here
The accelerator's critic found a share-alike laundering path: the Trivy DB aggregates exactly the CC-BY-SA-4.0/ODbL upstreams
mirror/tier2quarantines, so a finding derived from that cache can reach publication without the quarantine ever being consulted — nothing crossed a tiered path. The directory rule works correctly and is answering a question that no longer covers the case. It may be a reason not to ship the accelerator at all.