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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
# detector reports it against the FIX on a correct tree. What was not
# legitimate is that this workflow ran only the -race lane, so
# TestScopeBytesAreReadOnceUnderAConcurrentWriter -- the behavioural
# control whose measured numbers are in internal/SKIPPED-CONTROLS.md
# control whose measured numbers are in docs/controls.md
# under G4-1 (0 of 200 constructed Scopes mismatched on the shipped tree,
# 89 of 200 with the copy deleted, 102 of 200 with the copy replaced by
# `rawAlias := raw[:]`) -- was compiled in ZERO CI lanes. Recorded as N3;
Expand Down Expand Up @@ -109,7 +109,7 @@ jobs:
#
# This step exists so the ledger is READ on the machine where it says the
# most: ubuntu-latest has dpkg, so host.Collect runs for real here and the
# host-inventory link is PROVEN, where on the Windows dev host it is not.
# host-inventory link is PROVEN, where on a host with no package manager it is not.
- name: Lane A chain ledger (printed, not just passed)
run: go test -count=1 -v -run TestLaneAChain ./test/conformance/lanea/

Expand Down Expand Up @@ -318,7 +318,7 @@ jobs:
# there" are indistinguishable to a caller, and for a security scanner that is
# the worst possible failure -- an empty result reads as a clean repository.
#
# internal/SKIPPED-CONTROLS.md item N1 recorded this as OPEN: the test was
# docs/controls.md item N1 recorded this as OPEN: the test was
# correctly made fatal-when-opted-in rather than skip-when-opted-in, which
# made it honest, but no machine anywhere set the opt-in, so the control was
# proven on NOTHING. This job is what closes it. If it is ever removed, that
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ research/
plan/
tools/

# The owner's logo and branding designs -- kept local, never distributed.
assets/branding/

# Python
__pycache__/
*.py[cod]
Expand Down
2 changes: 2 additions & 0 deletions cmd/anvil-dast/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Command anvil-dast is the Anvil dynamic-analysis (DAST) binary.
package main
5 changes: 2 additions & 3 deletions cmd/anvil-dast/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
// Command anvil-dast is the Anvil dynamic-analysis (DAST) binary.
//
// It ships as a SEPARATE distribution artifact from the core anvil binary,
// anvil-dast ships as a SEPARATE distribution artifact from the core anvil binary,
// requires separate installation, and refuses to probe anything without an
// explicit attestation. See the two-artifact split for why this is a
// separate artifact rather than a configuration flag: a boolean inside a
Expand All @@ -12,6 +10,7 @@
//
// Bootstrap placeholder. The general entrypoint arrives with the live dynamic
// tier (plan node live); every request it makes routes through internal/dast/authz.

package main

import (
Expand Down
1 change: 1 addition & 0 deletions cmd/anvil-dast/pin-templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
// `code:` template is refused at load time. This job calls it on BOTH trees
// and diffs the results, which means the definition of "dangerous" lives in
// exactly one place and this file cannot drift from it.

package main

import (
Expand Down
3 changes: 3 additions & 0 deletions cmd/anvil-host-collector/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Command anvil-host-collector runs Anvil's read-only host package collector
// once, writes the resulting inventory to stdout as JSON, and exits.
package main
8 changes: 3 additions & 5 deletions cmd/anvil-host-collector/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// Command anvil-host-collector runs Anvil's read-only host package collector
// once, writes the resulting inventory to stdout as JSON, and exits.
//
// # Why this main package exists
//
// deploy/systemd/anvil-host-collector.service has always declared
Expand Down Expand Up @@ -55,7 +52,7 @@
//
// RUN IT UNDER THE SHIPPED UNIT, OR REPRODUCE WHAT THE UNIT DOES. Running this
// binary as root outside the unit re-opens the rpmdb write side effect
// documented in internal/collector/host's package comment: on a
// documented in the comment at the top of internal/collector/host/collect.go: on a
// Berkeley-DB-backed rpmdb, `rpm -qa` creates /var/lib/rpm/__db.001..003 when
// the caller can write that directory. The binary cannot prevent that — no rpm
// flag suppresses it, and branching on the effective uid is forbidden — so the
Expand All @@ -72,6 +69,7 @@
// is that a run never reports a silent "clean", and "we could not look" has
// to be distinguishable from "there is nothing here" by a caller reading
// the exit status alone.

package main

import (
Expand All @@ -85,7 +83,7 @@ import (
"github.com/Susquehanna-Syntax/Anvil/internal/collector/host"
)

// Exit statuses, documented in the package comment above.
// Exit statuses, documented in the comment at the top of this file.
const (
exitCollected = 0
exitFailed = 1
Expand Down
2 changes: 1 addition & 1 deletion cmd/anvil-host-collector/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func TestTheBinaryRefusesEveryArgument(t *testing.T) {
// On a Linux host with a package manager this is the host collector's stop condition and
// the read-only-boundary review's "confirmation the binary runs successfully as
// a non-root user in the test fixture" — the thing that could not be given
// before, because there was no binary. Everywhere else (a Windows development
// before, because there was no binary. Everywhere else (a non-Linux
// host, a Linux container with no package manager) the collector reports no
// package manager, which is exit status 3 WITH an inventory: the coverage
// report is the point, and this asserts it is emitted rather than swallowed.
Expand Down
2 changes: 2 additions & 0 deletions cmd/anvil/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Command anvil is the Anvil core binary.
package main
3 changes: 1 addition & 2 deletions cmd/anvil/main.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// Command anvil is the Anvil core binary.
//
// Anvil finds vulnerabilities in Linux servers and code repositories and
// proposes fixes. This binary is the core artifact: Lane A (deterministic
// SBOM/host package matching), Lane B (first-party source detection), the
Expand All @@ -15,6 +13,7 @@
// Bootstrap placeholder. The real entrypoint — the anvil scan and
// anvil daemon --loop subcommands wired to internal/scanctl and
// internal/queue — is plan node cli (plan/anvil.html).

package main

import (
Expand Down
56 changes: 36 additions & 20 deletions internal/SKIPPED-CONTROLS.md → docs/controls.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,22 @@ be listed here.
## Method

The **"skips here"** column is **measured, not reasoned**: `go test -count=1 -v
./...` was run on the Windows dev host and the `--- SKIP` lines were read out of
the output. The **"skips in CI"** column *is* reasoned, from the condition plus
./...` was run on the development machine and the `--- SKIP` lines were read out
of the output. Development moved from a Windows 11 machine to a Debian machine in
September 2026; the column was re-measured on Debian on 2026-10-03, and where a
row differs between the two the entry says which. The **"skips in CI"** column *is* reasoned, from the condition plus
`.github/workflows/ci.yml` (`runs-on: ubuntu-latest`, `go test -race -count=1
./...`, no Trivy install, no `ANVIL_TRIVY_E2E`, no acquired licence bodies —
CI is always a fresh clone). Where the reasoning does not settle it, the column
says so rather than guessing.

- Host measured: `go1.26.5 windows/amd64`, Windows 11, non-elevated,
- Host measured on 2026-10-03: `go1.26.5 linux/amd64`, Debian 13, kernel
6.12.107, uid 1000 (not root). `dpkg-query` is present and lists 3,467
packages; `rpm`, `apk`, `nft`, `docker`, `podman`, `runsc`, `nuclei`, `zap.sh`,
`java`, `trivy` and `opengrep` are absent. `gcc` is present, so `-race` runs
locally. An unprivileged user and network namespace can be created
(`unshare -rn true` succeeds).
- Host first measured: `go1.26.5 windows/amd64`, Windows 11, non-elevated,
Developer Mode **off**.
- Suite state after the changes below: `gofmt` clean, `go vet` clean,
`go build ./...` clean, `go test -count=1 ./...` **green** (17 packages).
Expand Down Expand Up @@ -667,12 +675,13 @@ and all of it is tested:
passes, so the first half is not passing because the function refuses
everything. **The stop condition's FIRST clause — "Default-deny ruleset
installs correctly on a real target fixture" — is NOT met and cannot be met
on this host**: nothing here installs a ruleset anywhere, and item (1) below
on the development machine** (Debian, measured 2026-10-03: no `nft` binary, uid
1000): nothing here installs a ruleset anywhere, and item (1) below
is what would settle it. This entry previously claimed the whole stop
condition was met, which is how a gap ships.
- `SystemCommander` **refuses** on any non-Linux GOOS and
`TestSystemCommanderRefusesOffLinux` asserts that refusal on this host. There
is no no-op Commander, so there is no path by which Windows returns "contained".
`TestSystemCommanderRefusesOffLinux` asserts that refusal wherever GOOS is not
linux. There is no no-op Commander, so there is no path by which Windows returns "contained".
**That guard now runs in a CI lane**: `containment-non-linux` in
`.github/workflows/ci.yml` runs the package on `windows-latest`, asserts the
named test actually executed and passed, then DELETES the
Expand All @@ -684,9 +693,12 @@ and all of it is tested:
### What it does not prove, and exactly what would settle it

Everything above is a statement about Anvil's decision logic. None of it is a
statement about a kernel. Windows has no network namespaces and no nftables;
WSL2 is present on the dev host and is **not** the target runtime, so it does
not settle this either.
statement about a kernel. The Debian development machine (measured 2026-10-03)
has network namespaces and lets an unprivileged process create a user and
network namespace, but `nft` is not installed and the suite runs as uid 1000, so
nothing installs a ruleset and nothing here settles this either. Installing
nftables is the owner's decision; with it, item (1) below may be runnable inside
an unprivileged user namespace on this machine, which has not been tried.

What would settle it, in order of decreasing cost:

Expand Down Expand Up @@ -822,7 +834,8 @@ against.

**Docker is not installed on the host this packet was written on.** Measured,
not assumed: `Get-Command docker` and `Get-Command runsc` both return nothing
on `go1.26.5 windows/amd64`, Windows 11.
on `go1.26.5 windows/amd64`, Windows 11. Re-measured 2026-10-03 on the Debian
development machine: `docker`, `podman` and `runsc` are still absent.

**Nothing in the tree implements the `Docker` interface.** A repository-wide
grep for `ComposeUp` and `EngineInfo(` finds the interface, its call sites in
Expand Down Expand Up @@ -1046,7 +1059,8 @@ container engine**, and this entry is the standing record of that.

### (a) nuclei is not installed, and no adapter exists

**Measured** on the development host: `Get-Command nuclei` finds nothing, and
**Measured** on the Windows development machine, and again on 2026-10-03 on the
Debian one (`command -v nuclei` finds nothing): no engine binary, and
`go list -m all` contains no `projectdiscovery` module. `SystemEngine()`
therefore returns `*EngineUnavailableError` on **every** host — it never
returns a no-op engine — and `ScanResult.AssertNotSilentlyEmpty` refuses to let
Expand Down Expand Up @@ -1256,7 +1270,8 @@ the same commit.

**What would settle it, and what stops it here.** The plan's own suggestion is
one `docker stats` run during a representative scheduled scan.
**MEASURED 2026-08-22, PowerShell, on the development host:**
**MEASURED 2026-08-22, PowerShell, on the Windows development machine** (re-measured
2026-10-03 on the Debian one: `docker`, `zap.sh` and `java` are all absent there):

```
Get-Command docker -> NOT FOUND
Expand Down Expand Up @@ -1999,7 +2014,7 @@ consequence.
|---|---|
| **File** | `internal/collector/host/collect_test.go:3967` |
| **Trigger** | `runtime.GOOS != "linux"` |
| **Skipped here?** | **YES — measured.** `no native package manager on windows` |
| **Skipped here?** | **No — measured 2026-10-03 on Debian**: `dpkg-query` is present and the test enumerates the host. It skipped on the Windows machine (`no native package manager on windows`) |
| **Skips in CI?** | No. `ubuntu-latest` is Linux |
| **Property unverified** | Real `dpkg-query`/`rpm`/`apk` enumeration against a live host |
| **Security control?** | No — a collector coverage claim |
Expand All @@ -2011,7 +2026,7 @@ consequence.
|---|---|
| **File** | `internal/collector/host/collect_test.go:3976` |
| **Trigger** | Linux, but none of `dpkg-query`/`rpm`/`apk` resolves |
| **Skipped here?** | No — unreachable on Windows (L1 returns first) |
| **Skipped here?** | No — measured 2026-10-03: `dpkg-query` resolves on the Debian machine |
| **Skips in CI?** | No. `ubuntu-latest` ships `dpkg-query` |
| **Property unverified** | As L1 |
| **Security control?** | No |
Expand All @@ -2023,7 +2038,7 @@ consequence.
|---|---|
| **File** | `internal/collector/host/collect_test.go:3462` |
| **Trigger** | `runtime.GOOS != "windows" && os.Geteuid() == 0` |
| **Skipped here?** | No — measured. `os.Geteuid()` is `-1` on Windows, so the condition is short-circuited |
| **Skipped here?** | No — measured 2026-10-03: the suite runs as uid 1000, so the condition is false (on Windows `os.Geteuid()` is `-1` and the GOOS check short-circuits it) |
| **Skips in CI?** | No. GitHub's `ubuntu-latest` runs as the non-root `runner` user. **Would** skip in a root container |
| **Property unverified** | Successful enumeration under a non-root UID |
| **Security control?** | Partly — the root-free-by-design claim |
Expand Down Expand Up @@ -2154,11 +2169,11 @@ missing artefact and the command that produces it.
| Hazards closed by a test-only change | 9 (7 sites removed, 2 narrowed) |
| Hazards needing a non-test change | 4 (N1 closed by CI, N2 closed elsewhere, N3 closed, N4 closed) |
| Legitimate skips, left in place | 11 |
| Skips that fire on the Windows dev host | 4 tests / 15 subtest lines |
| Skips that fire on the development machine (Debian, 2026-10-03) | 4 tests / 14 SKIP lines (L4, L5, L7 ×11, L11) |
| `t.SkipNow` sites | 0 |

Skips still firing on this host, all classified LEGITIMATE above:
`TestCollectAgainstTheRealHost` (L1), `TestRealTrivyScansAFixtureRepo` (L4),
Skips still firing on the development machine (Debian, measured 2026-10-03), all
classified LEGITIMATE above: `TestRealTrivyScansAFixtureRepo` (L4),
`TestBothConsumersAgree` (L5), `TestPinnedLicenceBodiesMatchTheirPins` ×11
(L7), `TestXVM3RelatedLocationsAreCapped/loc0_rel3000` (L11).

Expand Down Expand Up @@ -2213,8 +2228,9 @@ What remains open in U4 and U5 is the **tooling**: no Nuclei engine and no ZAP
on any host or CI lane here, and ZAP's JVM footprint still unmeasured.

U1 (`internal/dast/containment`, network containment) is the
highest-stakes of them: the package is green on Windows and has never run
against a Linux kernel. See its entry for the privileged Linux CI lane that
highest-stakes of them: the package is green on Windows and on the Debian
development machine and has never installed a ruleset into a kernel (no `nft`
binary there, measured 2026-10-03). See its entry for the privileged Linux CI lane that
would close it, and U1a/U1b for the two things that lane would still not
settle on its own.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

**This was a SAME-FAMILY critic.** The packet routes this review to an OpenCode
`openai/gpt-5.5-fast` route; that route is withdrawn (plan/design/routing.md OWNER
DECISION, 2026-08-07). This review was performed by a Claude subagent — the same
DECISION, 2026-08-07). This review was performed by an automated review agent from the same
model family that produced the accelerator. Correlated blind spots are therefore *not*
excluded, and this verdict must never be recorded as "cross-family critic: PASS".
Compensation applied: every claim was checked against the file, the suite was
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ the package's own guards:
returns nothing (`TestNoSourceFileReachesForAClockOrARandomSource`, re-run and independently
reproduced).
- `grep -rn "t.Skip" internal/match/` — **none**. No new entry is owed to
`internal/SKIPPED-CONTROLS.md`.
`docs/controls.md`.
- Both of the package's own guards (G1 import allowlist, G5 dependency graph) carry working RED
controls, and G5's negative control genuinely observes `modernc.org/…` under
`internal/ingest/cache`. These are real guards, not decorative ones.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ the spine's corrected-requirements, record, safety and Go control-plane sections

## 0. WHICH GUARANTEE THIS DOCUMENT ACTUALLY PROVIDES — READ THIS FIRST

**This was a SAME-FAMILY critic.** The critic and both implementers (the record contract, the fingerprint) are Anthropic models.
**This was a SAME-FAMILY critic.** The critic and both implementers (the record contract, the fingerprint) are models from the same family.
`plan/design/routing.md` originally required a **different model family** here precisely so that a shared
blind spot could not survive review; the owner withdrew external routes on 2026-08-07 because running
them means copying `plan/` — deliberately private — to a third-party provider. See the OWNER DECISION
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
**Verdict: FAIL.** One blocker, two majors, three minors.

**This was a SAME-FAMILY critic.** The packet routes this review to OpenCode `openai/gpt-5.5`; that route is
withdrawn per the OWNER DECISION block atop `plan/design/routing.md`. This review was performed by a Claude
subagent — the same family that wrote record emission — and therefore carries none of the independence the
withdrawn per the OWNER DECISION block atop `plan/design/routing.md`. This review was performed by an automated
review agent from the same model family that wrote record emission, and therefore carries none of the independence the
cross-family guard was written to buy. Do **not** record this as "cross-family critic: PASS". To
compensate, every claim below is backed by a probe I wrote and ran against the committed code, not by
reading record emission's own tests. Nine probes were written; six passed, three failed. The probes were deleted
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ block dated 2026-08-07 at the top of `routing.md` withdrew all OpenCode/OpenRout
because running one copies private repository contents to a third-party provider; this review's
packet still says `OpenCode route (openai/gpt-5.5)` and that route is dead.

The replacement is an Anthropic subagent given an explicit instruction to **refute rather than
The replacement is a same-family review agent given an explicit instruction to **refute rather than
assess**, with named failure modes to hunt. That narrows the shared-blind-spot gap; it does not
close it. **Do not read this file as "cross-family critic: PASS."** It is a same-family critic,
and a later reader deciding whether the cross-family gate has been met should treat it as
Expand Down
File renamed without changes.
25 changes: 0 additions & 25 deletions internal/buildpin/pin.go

This file was deleted.

Loading
Loading