Phase 4: the DAST tier (D.1-D.28) - #15
Open
tom-snyder wants to merge 21 commits into
Open
Conversation
Twenty-one gates across four phases, plus the target manifest and the build-time
invariants that make gates 2 and 3 enforceable rather than aspirational. This is
the code every network request in Anvil passes through, and it is written on the
assumption that it will be attacked.
Three rulings were issued before dispatch, because plan/50-dast.md would
otherwise have produced a red tree and two silently wrong fields.
Its dast_status enum (:1149) is the PRE-RULING five-value set. Section 6 froze
ten values, and internal/record/contract_test.go:108-111 rejects "clean",
"findings", "failed_to_boot" and "partial" BY NAME, attributing each to area D.
Its target_provenance (:1150) is populated with anvil/target.provisioning values
-- two different fields, which is the original defect section 6 caught. And its
"Conflicts With Spine" section says it implements the config-gated DAST model
"because that is what spine S9 specifies"; S9 was AMENDED, and CI has enforced
the two-artifact split since Phase 3.
Three more rulings settled questions workers escalated rather than acted on.
Gate 7 was removed from the admission chain: it is a run-initiation gate, and
widening gateFunc to carry trigger provenance would have broken S7's "pure
function of (target, scope, attestation, clock)" -- the property that makes the
decision auditable. Gates 13-17 stay out of that chain but now have their own
chain TYPE, because an unregistered gate and a forgotten one are
indistinguishable by inspection. And gate 13 re-checks scope membership:
revalidationChain was {8,9,10}, which canonicalize, pin and screen reserved
ranges but never ask "is this host in the allow list", so a redirect to an
in-scope-LOOKING host would have sailed through.
What the critics caught
Sixteen critics have now run on this project and all sixteen correctly returned
FAIL. This round was no exception, and the two criticals were compiled and run
rather than reasoned about.
Gate 5's thirty-day ceiling was not a ceiling. NewCap was exported and
unvalidated, so NewCap(10*365*24*time.Hour) minted a ten-year attestation still
live nine years later -- while types.go asserted "raising is not expressible".
Cap is the same primitive gate 14's six caps are built from. It is unexported
now, and NewAttestation compares every lifetime against a package const BEFORE
consulting any supplied ceiling: an independent verifier forged a working Cap
through unsafe struct punning and the attestation was still refused, because a
const comparison takes no argument.
Scope was not sealed. NewScope shallow-copied ScopeEntry, sharing the Ports
backing arrays, and writing one integer turned an explicitly DENIED host into a
permitted one. The test that claimed to cover this mutated only the Host string
-- the one field where the copy happened to be real. A test that passes for the
wrong reason. Both directions are deep-copied now, in AND out, and NewScope no
longer accepts caller-built entries at all: it takes the raw bytes and derives
the hash from them, so the assertion has nowhere left to be made.
Four unfalsifiable comments were DELETED rather than softened, per the standard
this project holds itself to.
What is verified, and what is not
D.7 lost its report to a session cap but not its code. Rather than trust a green
suite, it was audited from scratch: 47 single-guard mutations, 41 red with
named on-point tests, file restored byte-identical (sha256 unchanged). That is
how gates 18-21 are known to work rather than assumed to.
D.9's guards resolve by IMPORT PATH, because a sibling guard in
internal/collector/host was defeated by an import ALIAS earlier in this build.
Its test builds a throwaway module and proves plain, alias, dot, blank and
transitive-through-an-innocently-named-intermediary imports are each caught.
The egress check is an ALLOWLIST of ~50 provably-inert import paths, so the next
networking API someone reaches for fails by default rather than by memory.
Gate 1 is recorded as VACUOUS IN BOTH DIRECTIONS and not papered over: `go list
-deps` finds zero internal/dast packages for anvil AND for anvil-dast, so the
split is currently satisfied by emptiness and would keep passing if this kernel
were deleted. D.9 prints a PROVEN/UNPROVEN ledger saying so. No consumer was
fabricated to make the number look better.
Open findings from this round -- gate 21 writing zero audit rows on the denials
it issues, attestation expiry not re-checked per request, a parse-then-hash
TOCTOU in the scope loader, an embargo reachable by back-dating vendor contact,
and a manifest that accepts 169.254.169.254 as an authorized service -- are
being fixed next and are named here so they are not lost if this commit is read
alone.
Evidence: gofmt, vet, build clean; go test -count=1 ./... green across 28
packages; no t.Skip added.
Nineteen critics have now run on this project and all nineteen correctly returned FAIL. Four of them ran against this kernel. What follows is what they found, because the findings are more instructive than the code. Two criticals, compiled and run rather than reasoned about Gate 5's thirty-day ceiling was not a ceiling: NewCap was exported and unvalidated, so one call minted a ten-year attestation still live nine years later -- while the source asserted "raising is not expressible". Cap is the same primitive gate 14's six caps are built from. It is unexported now, and NewAttestation compares every lifetime against a package const BEFORE consulting any supplied ceiling. A verifier forged a working Cap through unsafe struct punning and the attestation was still refused, because a const comparison takes no argument. Scope was not sealed: NewScope shallow-copied ScopeEntry, sharing the Ports backing arrays, and one integer write turned an explicitly DENIED host into a permitted one. The test claiming to cover this mutated only the Host string -- the one field where the copy happened to be real. Fixing that closed the named attack and RELOCATED it. The same function read the caller's buffer twice, so a concurrent write between parse and hash produced a sealed Scope whose entries came from one document and whose hash came from another: 2012 of 4000 scopes mismatched under measurement. Gate 5's scope binding is what makes an attestation specific to a scope rather than to a session, and that defeated it. Three rulings, each settling a root cause rather than a symptom A RUN HAS EXACTLY ONE CLOCK. An embargo could be driven to zero by dating the vendor contact in January, recording it against a January clock, and publishing at the real August present -- 45 days of embargo, zero days of vendor notice, every gate green. The previous round had bounded one caller-supplied instant against a second caller-supplied instant, which catches the lie told with one clock and misses the lie told with two. A function that TAKES a clock can be lied to; one that READS THE RUN'S clock cannot. RunClock is sealed by the same unexported idiom already carrying Scope and Attestation. AN UNVERIFIED CALLER ASSERTION MAY DELETE ONLY THE PART OF A CONTROL IT IS ABOUT. Gate 20's self-assertion bypass was fixed by hoisting the scope and attestation checks above the ownership branch -- and reappeared at gate 18, which had the identical shape. "This repo is mine" bears on whether a patch is unsolicited. It does not bear on whether an embargo elapsed or what time it is. PIN THE RELATION, NOT ONLY THE VALUES. MaxVendorContactBackdate and MinAcceleratedEmbargo were both 24h. Each was individually defensible; because they were EQUAL, a contact back-dated exactly to the permitted bound cancelled the acceleration floor and published in the same run that recorded first contact. The backdate bound moved to 1h -- that constant, not the floor, because the floor is a claim about the world (the shortest window in which a vendor told about active exploitation can act) and the bound is only operational slack for clock skew. The test now asserts the INEQUALITY, and the 23h difference, so the prose cannot drift from the code. A gate that can only see what the caller hands it cannot enforce a state machine A recorded decision to WITHHOLD was overturned by writing a second row, because gate 18 read only the PersistedDisclosure passed to it and DisclosureStore had no read method at all. It has one now, and "no row" and "I cannot tell you" are different facts: gate 18 is fail-closed on a read error, an absent row, a nil store and a wrong medium. A read that fails does not mean nothing was withheld. Three lessons about guards, all learned by watching one fail A DENYLIST LOSES: the first pin for the TOCTOU fix enumerated two spellings and lost to `rawAlias := raw[:]`, leaving the entire suite green. Its replacement was a shape allowlist that caught 18 of 21 attack shapes -- and matched the parameter BY POSITION, so a leading ctx parameter disarmed it silently. It resolves by identity now and fails loudly when it cannot find exactly one candidate. An allowlist matched by position is a denylist wearing better clothes. A CHECK THAT CANNOT SEE THE DAMAGE IS NOT A CHECK: a round reported the tree UTF-8 clean while four U+FFFD characters sat in production doc comments. The claim was true and irrelevant -- EF BF BD is itself valid UTF-8. The replacement guard scans for the codepoint. A CONTROL THAT RUNS NOWHERE IS NOT A CONTROL: the concurrency harness carries //go:build !race for a real reason (the detector fires on the fix itself), and the only CI step compiling that package was the -race one, so it was compiled in zero lanes. CI now runs both. Evidence: gofmt, vet clean; go test -race -count=1 ./... green; no t.Skip added. Two findings remain open from the final pass and are named in the next commit rather than left in a report: an audit-ordering inversion in AuditedPersistDisclosure, and a tree guard whose walk is narrower than the set of tracked files.
…ment Ephemeral provisioning, the netns/nftables ruleset, the canary assertion and the reset lifecycle. 8,355 lines. Docker is not installed here, Linux network namespaces do not exist here, and that shapes this commit more than the code does. The instruction that mattered was: the Windows path must REFUSE, not pass. A test going green by doing nothing would be the silent-clean failure this codebase keeps catching, and it would be worse here than anywhere else, because "the sandbox is contained" is the claim that licenses Anvil to run probes at all. A control that reports contained-when-unverified is the one that gets someone breached. The critic confirmed: no t.Skip, no Windows path that reports contained, no unconditional success in AssertContainment. The design spike the plan deferred (D.11) came out on the canary side. A host-side probe proves something about the netns AS CONFIGURED; a canary inside proves what a process in the sandbox can actually REACH. Only the second is the claim the gate makes. Three things the critic found, all real, none fixed yet -- named here so they are not lost in a report: Target.Containers() is a shallow copy whose doc comment asserts the opposite, and this is the Scope critical again in a new package: the test passes only because it mutates the single string field. Rewriting Mounts, SecurityOpt and CapDrop THROUGH the copy made the sealed Target's audit trail read docker.sock / seccomp=unconfined / CapDrop=NONE. The build/boot split -- which D.10's own packet calls the point -- is destroyed by ordering. health.timeout_seconds is the total build+start+health budget and the deadline branch is checked before the runner's own status, so a runner explicitly reporting BuildFailed after a slow image build is recorded as boot_failed. Two different failures with two different fixes, collapsed in the ordinary case. DeniedByRuleset fails OPEN for any zoned IPv6 address, including fd00:ec2::254%eth0, because netip.Prefix.Contains drops zones. The relation test exists; its generator cannot produce the input that breaks the relation. And the two halves do not compose: Provision seals booted_clean -- the literal that authorizes probing -- without any netns, and nothing in the tree calls SetupNetns, AssertContainment or Provision. The "runs on every scan" property is instantiated zero times today because there is no scan. Also in this commit, round 5 against the kernel: AuditedPersistDisclosure was applying its durable write BEFORE the audit write, so an audit-sink failure -- a transient disk-full is enough, no attacker required -- left the state transition applied with no audit row, which consumed the withheld control round 4 had just built. It is DECIDE -> AUDIT -> APPLY now, audit-first rather than compensating, because DisclosureStore has no delete and a compensating row is itself a write that can fail. The U+FFFD guard now walks git ls-files (236 of 236 tracked) instead of four hand-listed directories (151, none outside them) -- it had the same shape as the finding it was written to close. The race-detector fact is now written into the repo: go test -race works from PowerShell and fails from Git Bash on this host with ThreadSanitizer error code 87, uniformly across every package. That is an address-space issue, not a race. Two rounds were spent on this disagreement -- a worker claimed it cannot build, a critic called that false, and both were right about their own shell. Evidence: gofmt, vet clean; go test -count=1 ./... green; -race clean from PowerShell, 26 packages, 0 data races; no t.Skip added.
…uous
The nuclei and ZAP drivers, the containment fix round, and the template pinning
job. The pinning job closes a gap this phase has carried since D.1.
Gate 1's positive half was satisfied by emptiness
`go list -deps` found zero internal/dast packages for anvil AND for anvil-dast,
so the artifact split passed and would have kept passing if the entire kernel
were deleted. cmd/anvil-dast was a 32-line placeholder. D.17 is the first packet
whose write scope reaches it, and wiring pin-templates in makes the dependency
REAL rather than decorative:
go list -deps ./cmd/anvil-dast | grep internal/dast
internal/dast/authz
internal/dast/engines
go list -deps ./cmd/anvil | grep internal/dast
(empty)
The dependency is honest rather than manufactured to satisfy a check:
LoadTemplates is what detects a `code:` template, so the pinning job genuinely
needs the engines package. TestSplit_DASTBinaryActuallyLinksTheDASTTier now
fails if the kernel is deleted, instead of both halves staying green.
Every pin read from a primary source, none from memory
nuclei-templates at 83234ce456da3e90dda86dfbc5e605e64a846df3 (v10.4.7), MIT,
licence BODY archived with its sha256 -- read via git cat-file, not API
metadata, per S8. The worker also checked something worth recording: ls-remote
returns ZERO peeled ^{} refs, so every tag is lightweight and a tag SHA is a
commit SHA. That is the kind of assumption that silently breaks a pin, and it
was verified rather than assumed. A version invented from memory 404'd once on
this project already.
The shallow-copy defect, found for the second time
Container.clone() now deep-copies every reference field. The INBOUND snapshot
was shallow too, which was worse than the reported defect: a Docker seam
retaining the slices it returned could rewrite the sealed audit trail AFTER
containment was asserted. There is now a reflection guard that fails when a
reference-typed field is added to Container or Mount and left uncopied, because
"remember to update the clone" is not a control.
Three containment findings worth their own note
The build/boot split was destroyed by ordering in the ORDINARY case: one
timeout covered build+pull+create+start+health and the deadline was consulted
before the runner's own reported status, so a slow image build on a cold cache
recorded a working target as boot_failed. health.timeout_seconds is a POLLING
budget by D.1's own validation -- charging docker build to it leaves the health
wait nothing. Build now has its own budget; the runner's status is read first;
the clock decides only when the runner named no phase, and then says the phase
is UNKNOWN rather than guessing.
DeniedByRuleset failed OPEN for every zoned IPv6 address, because
netip.Prefix.Contains drops zones. The relation test existed and could not
construct the input that broke the relation -- proven by leaving the bug in and
deleting the new generator arm: 393,226 addresses swept, all green. The
generator was the defect.
The nftables chain hooked `output` only, which never sees forwarded traffic --
and provision.go's own NetworkMode assertion requires exactly the bridge
arrangement that forwards. So the ruleset constrained the canary and nothing
else. It hooks forward as well now, with the identical rule list, parsed back by
a reader separate from the writer.
Also: the 4-in-6 spelling ::ffff:169.254.169.254/128 walked past a metadata
refusal whose doc comment and six-row table both claimed to stop it --
canonicalize before matching, the lesson gate 8 already encodes, reusing the
kernel's own Unmap rather than a second implementation that can disagree.
CI now has a windows-latest lane, because the sole guard on "containment can
never report success where nothing was checked" ran in zero lanes: all five jobs
were ubuntu-latest, where that branch cannot fire.
Evidence: gofmt, vet clean; go test -count=1 ./... green; TestSplit passes with
a real dependency on both sides; no t.Skip added.
… allow Twenty-four thousand lines across five inventory packets, plus the end-to-end proof that became possible when gate 11 stopped being an unimplemented gate. The kernel had never admitted anything Every proof in this repository until now was a proof that the kernel REFUSES. Twenty-one gates, five fix rounds, twenty adversarial critics -- all of it demonstrating rejection. That was not thoroughness, it was a gap: Gate11RobotsDeny sat in admissionChain with nothing registered, and a missing gate is a refusal, so Adjudicate could not return an allow and no Authorization could be minted outside package authz at all. D.14 saw that and planted a tripwire rather than working around it. Ruling 6 made gate 11 a scope narrowing -- which is what its own row in the gate table describes, "removes paths from scope; permissive adds nothing" is a transformation, not a predicate -- and the tripwire fired on exactly the condition it was written for. It then named its own replacement: five tests, in order, then delete yourself. All five are written. The one that matters is the positive control: a gate stack only ever shown to refuse could be refusing for the wrong reason. It asserts ONE AUDIT ROW PER GATE against GovernorGateOrder() by position and identity -- and that assertion earned itself immediately, because a mutation writing only the last gate's row produced [gate14] against a want of six, which a len(rows)>0 check would have passed. Cross-target token reuse had never been exercised either, because no token existed to reuse. The sweep found that the pre-existing TestTargetSpecRefusesEveryUnauthorizedRoute stayed GREEN while a real Authorization was accepted for the wrong target: it only ever tried a ZERO Authorization, so it could not see the damage. Gate 3 was red for three packets, and that is the guard working encoding/xml was not on the egress allowlist, so it failed by default. D.19 named it, D.20 confirmed it by measurement, D.22 confirmed it a third time -- each by moving its own files out, re-running the gate, and comparing the finding count. None of them reached outside its write scope to fix it. That is the allowlist doing its job: a new import cannot enter the dynamic tier without someone deciding it is inert. The decision is recorded where it was made. Go's XML decoder resolves no external entities and fetches no DTDs -- it has no XXE, which is the only reason D.19 may point it at a spec file harvested from an untrusted repository. The one route to a socket is Decoder.CharsetReader, which is caller-supplied; verified that nothing under internal/ sets it. Where the plan was overridden, and why D.19 makes Tier 1 routes CANDIDATES. plan/50-dast.md:632-635 says every Tier 1 route is `status: confirmed`. That is the direction which moves an endpoint into endpoint_coverage's NUMERATOR, on the strength of a file the repository wrote. Confirmation is D.22's job and requires an observation Anvil made. D.20 did NOT vendor go-apispec. The maintenance spike the plan asked for was run against the network and measured: main last moved 2026-06-24, sixty days stale, while the repo's pushed_at reads 2026-08-21 because Dependabot branches move and main does not. Only the LICENCE body, NOTICE and a re-runnable PIN.md are in third_party/. The type-checked path is a seam that returns a loud ErrNoTypeCheckedExtractor rather than an empty route list. D.21 names what it is blind to in SupportMatrix().BlindTo, and the tests require those lists to be non-empty. A static extractor that silently finds nothing for a language it cannot really read does not merely miss endpoints -- it shrinks the DENOMINATOR, so coverage goes UP. The metric improves because the tool got worse, and nobody reading the number can see it. D.22 keys the union on (method, canonical path), so forty GraphQL root fields on one POST /graphql collapse to one row. That DEFLATES coverage, which is the direction nobody investigates, so it is reported rather than assumed: OperationCount() carries the second number and every multi-operation address leaves a MergeNote. Open, and named here so it is not lost in a report: internal/record carries no spec-harvest field. Ruling 7 told D.19 to reconcile its input shape against the record schema; it did, and found nothing to consume. D.19 is therefore PARTIAL. Evidence: gofmt, vet clean; go test -count=1 ./... green across 30 packages; -race clean from PowerShell; no t.Skip added.
… slots
Twenty-six of thirty-one packets. Two record-contract gaps closed, four DAST
packets landed, and two critics that found what the packets could not see about
themselves.
The record can now tell a specless repo from an unwired handoff
D.19 was PARTIAL because ruling 7 told it to reconcile against internal/record
and there was nothing there to consume -- no SpecFile, no artifacts list, no
run-level slot. That was the correct outcome of a reconciliation, not a failure
of it.
anvil/specHarvest closes it, and the test that proves it is the one worth
reading: TestASpeclessRepositoryAndAnUnwiredHandoffAreDifferentRecords. With the
field removed, both cases serialise to byte-identical records --
{"anvil/half":"sast","anvil/status":"running","anvil/sealedAt":null} -- and both
land in the denominator of anvil/dastCoverage while only one describes the
target.
OmittedFileCount is a POINTER for a reason that generalises: a plain int
defaults to 0, and 0 ASSERTS that the file list is complete. Forgetting to set
it would have been a silent claim. As a pointer, forgetting fails closed. The
same reasoning makes a nil SpecHarvest mean "the record makes no statement"
rather than harvest_skipped -- unknown and "this repo ships no specs" are
different facts.
The wire schema was checked by RUNNING it, not by reading it: fixtures
marshalled and validated against schemas/anvil-record-v1.schema.json with
jsonschema as draft 2020-12, twelve negative controls all refused. That found a
real Go/schema disagreement -- a skipped harvest marshals files as null, which
the Go validator accepted and the first schema draft refused.
What the critics found, and both are the same lesson in new places
CANONICALIZE BEFORE MATCHING, third occurrence. Gate 8 encodes it; containment
learned it when ::ffff:169.254.169.254/128 walked past a metadata refusal; and
now the crawler fetches /%2e%2e/admin while correctly refusing /admin. Every
browser resolves those to the same path, so gate 11's robots narrowing is being
matched against a spelling the spider will never request. Measured end to end
with a real kernel, on both the link route and the redirect route.
A GUARD POINTED AT THE WRONG TYPE. D.27's raw-body type closure is real work --
Finding genuinely has nowhere to put a body, and there is a test proving it
against a violating fixture. But the body leaves through the ERROR return:
ConfirmFinding wraps the Reprober's error with %w, unbounded, and an ordinary
Reprober that quotes an unparseable response puts 4301 verbatim body bytes onto
Ledger.Refusals()[0].Err. The walker that would have caught this is in the same
file and never points at Refusal.
And the one that matters most, because it is the failure this whole project is
built to avoid: a vulnerable target that rate-limits during the confirmation
pass produces rejected / did_not_reproduce_on_any_attempt / confidence 0.000 --
INDISTINGUISHABLE FROM A PHANTOM. AssertNotSilentlyClean ignores RejectedCount,
so that ledger derives completed_clean. A real vulnerability, reported clean,
because the target defended itself.
Credential redaction is byte-exact over three spellings, so an HTML-entity or
JSON-escaped credential is stored verbatim and every assertion reports clean --
demonstrated with D.24's OWN fixture password, and AuthArtifactStorageState is
JSON by definition, so the escaped case is the ordinary one.
All are open and being fixed next; named here so they are not lost in a report.
D.27's report was lost to a structured-output cap, the third packet to lose one.
Its code landed and D.28 reviewed it regardless, which is why the findings above
exist at all.
Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages; no
t.Skip added.
…arrowing
Gate 11 narrows the scope by removing paths a robots.txt disallows. The crawler
was matching that narrowing against the spelling it was HANDED, not the spelling
it would REQUEST -- so a target-supplied link or 302 Location of "/%2e%2e/admin"
was fetched while the plain "/admin" was correctly refused. Every browser
resolves those to the same path. Measured end to end by D.25 with a real
Governor, a real GateAudit and a real robots-narrowed scope, on both the link
route and the redirect route.
kernelAcceptsPath blocked only the literal ".." -- the one spelling a browser
would have rejected anyway.
THIS IS THE THIRD TIME THIS CODEBASE HAS LOST TO A NON-CANONICAL SPELLING. Gate
8 encodes the rule. Containment lost to ::ffff:169.254.169.254/128 walking past
a metadata refusal whose doc comment and six-row table both claimed to stop it.
Now the crawler. Three packages, three encodings, one lesson: canonicalize
BEFORE matching, and reuse the kernel's canonicalization rather than writing
another one that can disagree.
dotSegmentsSpelledPlainly rewrites every encoded spelling of "." and ".." before
the scope check, and the fix goes past what the critic demonstrated: encoded
separators ("..%2f", "%2e%2e%2fadmin", "x%5C..%5Cadmin") and doubly-encoded
segments ("%252e%252e"), the latter with the reasoning written down -- a browser
decodes once, so "%252e%252e" IS the ordinary segment "%2e%2e", and an origin
that decodes twice reads it as "..".
Four tests, and note what the last two are for: it is not enough that the
non-canonical spellings are dropped. TestEachNonCanonicalSpellingIsClassified
RatherThanMerelyDropped asserts each one is accounted for rather than silently
discarded, and TestCanonicalizationPreservesCoverageAndDoesNotDoubleCount
asserts the rewrite does not turn one endpoint into two in the union -- which
would inflate the denominator of endpoint_coverage and make coverage look worse
for a fix that made the crawler safer.
internal/dast/record is unchanged in this commit. A concurrent worker was
mid-way through replacing EvidenceRef.spanTruncatedFrom with spanOverBroadBytes
-- correctly, because truncating an over-broad match still inlines a raw body
prefix, and refusing to produce a span at all is the right answer -- when the
session cap stopped it. The change was coherent but half-applied and its
red-verification never ran, so it was reverted rather than hand-completed. An
unverified security fix that compiles is worse than an absent one, because it
looks done.
Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages.
a live vulnerability as clean Two criticals, four highs, and a lesson that outlasts all of them. The failure this project exists to avoid A genuinely vulnerable target whose rate limiter tripped during the confirmation pass produced outcome=rejected, reason=did_not_reproduce_on_any_attempt, confidence 0.000 -- byte-identical to what a phantom produces. Observation.Status was captured, stamped onto the Finding, and read by nothing; decide() never saw it. AssertNotSilentlyClean ignored RejectedCount, so the ledger derived completed_clean. A real vulnerability, reported clean, because the target defended itself. A defended attempt is now indecisive rather than disproven, and AssertNotSilentlyClean sees rejections. The raw body was leaving through the error return Finding's type closure is real -- it genuinely has nowhere to put a body, proven against a violating fixture. But ConfirmFinding wrapped the Reprober's error with %w, unbounded, and Refusal.Err is an exported error interface: a Reprober quoting an unparseable response put 4301 verbatim body bytes onto Ledger.Refusals()[0].Err. The Reprober lives on the far side of the gate-3 boundary, so this could never have been fixed by asking implementers to behave. The walker that would have caught it was in the same test file, pointed at three types that cannot carry the damage. Every exit is now verified closed: both returns, all fmt verbs, Unwrap, Refusal.Detail, Ledger.String(), and a reflective walk of every reachable string. The credential control stopped reading bytes A byte-exact sweep over three spellings stored an HTML-entity or JSON-escaped credential verbatim while every assertion reported clean -- demonstrated with the file's OWN fixture password, and AuthArtifactStorageState is JSON by definition, so the escaped case is the ordinary one. The primary control is now PROVENANCE, not content: an artifact belonging to a step that types a secret is refused for every kind, without looking at the bytes, so no spelling defeats it. The sweep is demoted to a backstop and says so -- AssertNoCredentialWasFound's nil now means "nothing was FOUND", not "there is nothing there". The fixture password was rewritten to BREAK the sweep instead of suiting it, which is why the encodings were found at all. An obligation no implementation can keep Headless-browser sub-requests could not reach the kernel: CrawlRequest expresses one method+path and CrawlPage has no field to report a fetch or XHR, so they hit no gate, spent no gate-14 token, wrote no gate-21 row. The worker declined to add a reporting field, and the reasoning is right: admitting a request already issued is not admission, it is a post-hoc audit row, and whether that is acceptable is a kernel ruling this file may not make. A rendering spider is refused instead, before FetchPage, across six discipline spellings. WHAT THE VERIFIER FOUND, AND IT IS ONE PATTERN Each fix replaced a losing enumeration with a NEW enumeration one layer down -- four defence statuses, an eight-byte entity-name bound, a seventeen-entry 720-byte benign corpus -- and each loses to the neighbouring input. 403 alone reproduces the completed_clean outcome. Zero-padded numeric character references walk past the entity decoder. A pattern requiring 721 bytes beats the benign corpus and confirms an ordinary product page at confidence 1.000. "A denylist loses" was already a standing order. It was obeyed at the top layer and reintroduced underneath. Those three are open and being closed next. Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages; all 111 Test identifiers referenced from non-test files resolve; no t.Skip.
… means
Ruling 8 said: when a guard must decide "is this X", enumerate what X IS, and
when the input domain is unbounded, generate rather than validate against a
fixed corpus. Three fixes applied it. A verifier then found the enumeration had
recurred a FOURTH time -- inside the fix written to end it.
What the inversion bought, and it is real
Defence detection was a four-entry status allowlist {429,502,503,504}, and 403
alone reproduced the defect that reported a live vulnerability as clean. The
question was wrong: "which statuses mean the target defended itself" is
unbounded and vendor-specific.
The question is now "did the target answer AS THE APPLICATION", with ten
statuses that establish it and exactly one code path returning true. Everything
else -- every status nobody has shipped, transport failure, Issued==false,
status 0 -- falls to indecisive by CONTROL FLOW rather than by list membership.
A -1..600 sweep through ConfirmAll produces zero rejections outside the
allowlist, and a mutation adding 403 to it fails with the reason written out.
The cost is stated rather than hidden: a 403-answering misconfiguration
candidate is now unconfirmed rather than rejected, and a 200 block page remains
indistinguishable from an application response with no DefenceSignature wired.
And one piece of work that ends a family instead of outrunning it: over-broad
LENGTH patterns are closed by ARITHMETIC. Go's regexp caps a repeat's expansion
at 1000 and checks the product, a repeat costs at least one pattern byte, and
MaxPatternBytes is 1024 -- so no compilable signature can demand more than
1,024,000 bytes, which is below authz.CodedMaxBodyBytes. Both bounds are
asserted. That is what a closed class looks like: no ceiling to raise, no
neighbouring input.
The lesson, which sharpens the ruling rather than repeating it
A GENERATOR WITH A FIXED SEED AND A HAND-WRITTEN ALPHABET IS A CORPUS.
The benign corpus was not generated out of the enumeration; the enumeration
moved into the generator. Five hand-written vocabulary slices and a fixed seed
produce 109 fixed strings, so the attacker's budget is unchanged in kind and
merely larger in size: one over-broad pattern anchored outside 13 tag names, 33
words, 14 JSON keys and 45 punctuation tokens. `(?s)<h1[\s\S]{0,400}` -- the
fix's own motivating example, one tag over -- passes, and an ordinary 404 page
carrying the word "expired" confirms a SQL-injection finding at confidence
1.000.
"Generate, do not enumerate" is satisfied by the sampling MECHANISM only when
the space being sampled is not itself a written-down list. The two places the
ruling held are the two where nothing needs keeping current: an allowlist with
no deny side, and a bound proved by arithmetic.
Also closed this round: IndecisiveRejectionCount was DELETED rather than kept as
an unreachable claim (a 52-case sweep reached it zero times), and replaced by an
assertion on a path that can fire. CarriedSession is exported but cannot widen
anything -- the decision requires an unexported seal bound to the labelling
session, and a struct literal from outside the package counts as not-carried and
is named unattested.
Open and being closed next: the generator's alphabet, a greedy one-way read of
semicolon-less character references, a meta-guard covering 21 of 26 exported
types, and 404/500 sitting in the application allowlist where a CDN answers them.
Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages.
The over-broadness decision no longer runs the pattern against bodies. It parses
it and walks the tree, and the class is closed by construction rather than by a
corpus with a size.
Three rules, and the first two contain no number at all:
R1 FOOTING -- every match path must require at least one byte of literal
content. The threshold is zero, so there is no ceiling to raise.
R2 NO OPEN POSITION -- every consumable rune must be spelled by the pattern or
drawn from a class confined to printable ASCII. THE CHARSET IS NOT A CHOICE: it
is extractSpan's own, asserted equal over all 256 byte values by running the
extractor. The rule is "a signature may not match through bytes its own evidence
extractor drops". That kills [\s\S], ., [^\n], \s and therefore the whole
bounded-prefix family whatever tag it names.
R3 QUOTATION -- positions that can cross a token boundary may not outnumber the
pattern's spelled bytes. One relation, 1:1 against the pattern's own footing.
shapeOf's default arm returns open, so an unknown regexp/syntax operator is a
refused signature -- an allowlist with no deny side, the same shape as the
status inversion.
Measured: 0 of 20 bounded-prefix HTML anchors accepted, where 17 passed before.
TestTheControlDecidesOnTheStructureAndNotOnASampleOfBodies runs NewSignature
with the corpus EMPTIED and requires the verdicts not to move -- which is the
claim, stated as a test.
The attack rerun found its own defect one class to the left
(?s)<h1[0-9A-Za-z]{0,400} was ACCEPTED and inlined 403 verbatim body bytes,
because a class of alphanumerics is not content-bearing and R3 deliberately lets
it repeat -- that carve-out is what allows AKIA[0-9A-Z]{16} to compile at all.
So the inlining was closed a second time, independently: extractSpan now inlines
a match only when L - spelled <= spelled, which re-applies the inequality to the
ACTUAL match and knows nothing about classes. It therefore holds for patterns R3
never examines. 403 bytes became 0.
The corpus is retained and DEMOTED IN WRITING to a backstop, with its vocabulary
named as a budget -- a backstop's budget, not a control's. Describing a backstop
as the control is how the previous four rounds happened.
Gate 3 caught the new import, which is the allowlist working
regexp/syntax is not on inertImports, so the build went red the moment ruling 9
landed. It is added here with the review it exists to force done in place: it is
the parser behind regexp -- Parse, the Regexp tree, the Op constants -- and it
compiles nothing, executes nothing and opens nothing.
Ruling 10 did not close, and the reason generalises
The union over ambiguous readings was taken at the point of MATCHING, against a
form that the decoder had already produced by picking the greedy reading. So the
union exists at encoding depth 0 and nowhere else: 0 of 450 semicolon-less
re-spellings missed flat, 135 of 450 missed under one url.QueryEscape -- exactly
the genuinely ambiguous ones -- with the first miss being the same fixture string
the previous round quoted as closed.
A union taken over the output of a decoder that already chose is not a union.
Ambiguity has to branch where it ARISES and the branches have to survive to the
comparison. Same root cause, second face: three decoders compose within one pass
and only the composition is retained.
Worth recording: asked directly whether its fix still had a budget, the
inventory worker answered "YES, AND I AM NAMING IT RATHER THAN CLAIMING
OTHERWISE" about a work-bytes ceiling, and wrote where it bites. That is the
answer the question was added to elicit.
Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages,
verified AFTER adding regexp/syntax to the egress allowlist.
The credential sweep no longer runs four fixed pipelines composing
decodeEntities(decodeBackslash(decodePercent(x))) and keeping only the
composition. One step is ONE decoder, every reading a step produces stays in the
candidate set, and the next step maps over all of them. Composition of passes is
composition of sets.
That closes the fifth recurrence at its root. Previously the union existed at
encoding depth 0 and nowhere else -- 0 of 450 semicolon-less re-spellings missed
flat, 135 of 450 missed under one url.QueryEscape. It also closes the second
face nobody had connected to it: three decoders composing within one pass
destroyed a secret whose own bytes contain an escape before the later decoder
saw it, losing six of ten realistic secret shapes.
Measured by an independent verifier, not by the author: 363 exhaustive orderings
at depths 1-5, 192 interleavings at depths 1-8, and all ten escape-shaped
secrets under three layers of each of the three encoders. ZERO MISSES. No
decoder grows a form -- 0 bytes of growth over 60,000 adversarial strings.
The half the matcher structurally cannot do
A non-greedy reading whose bytes must be DECODED AGAIN is invisible to any
union taken at match time: "ຝ" read at index 1 is '%' then "41", and one
further percent step makes it 'A'. The matcher matches, it does not decode. Only
a decoder emitting that reading as its own candidate reaches it. That is the
concrete reason ruling 11 is about WHERE the branch happens.
The list that matters more than the findings
An independent verifier was asked to state which decisions now close by
construction or arithmetic with no budget, and the list came back longer than
its findings:
the decoder composition (a real BFS over the DAG);
referenceReadingsAt's reading count, bounded at eight BY ARITHMETIC not a cap;
unionRunes normalising to at most 48 pairs whatever the pattern does;
shapeOf's default arm sending an unknown operator to open, which R2 refuses;
IsApplicationResponseStatus false for every unnamed status including 0;
extractSpan property 1b, which contains the inlining even of the pattern R3
still lets through -- a 383-byte match inlines 0 bytes;
go list -deps ./cmd/anvil reaching no internal/dast package.
Five disclosed residuals were re-measured independently and are accurate,
including a depth curve published as 60+/50/24/11 and measured at 70/50/24/11.
Two disclosures were NOT accurate and are open: the '+' branch has the same
same-depth cross-product residual as the reading index and nobody named it, and
codedSweepWorkBytes is disclosed as a bound on bytes SCANNED while the candidate
set is also RETAINED -- 183 MB live for one 4 MiB artifact, an axis the doc does
not mention.
Also open: R3's capture-group evasion is rebuilt one layer down. The union is
taken over CLASSES, and patternShape.consumes excludes spelled runes by design,
so spelling the punctuation as captured single-rune LITERALS restores it whole.
And closureViolations dedups by reflect.Type, so the SECOND []byte anywhere in a
closure is never reported.
Both are the same two shapes that produced the last two rounds: a capture group
blocking a merge, and a dedup key hiding a second instance.
Worth recording against the standing order it answers: asked whether its fix
still had a budget, the worker answered "YES, TWO, AND I AM NAMING BOTH RATHER
THAN CLAIMING OTHERWISE", published a measured curve instead of a sentence, and
wrote a test that PINS A WEAKNESS OPEN -- asserting a miss, with a failure
message instructing the next author to delete the test and the disclosure
together if the residual ever closes.
Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages;
-race clean from PowerShell.
Two guards were asking their question of a convenient representation instead of the thing itself. Both are re-keyed, and one of them is now closed for good. SHAPE B, CLOSED. closureViolations marked seen[reflect.Type] on entry, so the SECOND []byte anywhere in a closure was never reported -- adding Raw []byte AFTER Body left the package green. The subject is a ROUTE, not a type, and the dedup key merged two of them. The dedup is gone entirely, and dropping it is safe by GO'S OWN TYPE RULES rather than by luck: a struct may not contain itself directly or through arrays, so the part of the closure the walk descends into is finite and acyclic by construction, and every kind that could close a cycle -- pointer, slice, map, interface, func, chan -- is REPORTED and returned from rather than followed. That argument is asserted rather than claimed: a self-referential fixture proves termination by the assertion being reached at all. Verified by mutating the PRODUCTION types rather than fixtures -- zzRaw []byte inserted as the first, second and third field of Finding, EvidenceRef and Observation in turn: red 9 of 9, including the third-slot case that is the exact reported defect. Also caught: two []byte on different types in one closure, one behind a pointer, one behind a slice of structs, two routes through structurally identical anonymous structs, a named byte-slice type, and a fixed-size byte array. SHAPE A, CLOSED FOR EVERY SPELLING THAT GOES THROUGH AN ALTERNATION. A literal at an alternation position is an alphabet of one, and it is in the union now. The measured evasion -- 29 captured single-rune literals matching 252 of 321 bytes at confidence 1.000 -- is refused, as is the all-literal form with no class anywhere, which the worker constructed itself as an extension of the attack rather than waiting to be told. All 17 positive controls re-verified with their shape numbers read out. Two rationales in the test file were REWRITTEN rather than left standing, because the change made them false: they said a spelled rune is not in the union, and now it is. The dotted-quad pattern still compiles because digits and a dot carry no letter, which is a different reason than the one previously written down. The price is disclosed, not hidden: a repeat of an alternation whose union crosses letters into punctuation is now refused even when every branch is spelled. And the benchmark was RE-MEASURED rather than left stale -- 972ns to 2292ns/op, with the regression named. Still open, and it is the same move one level out: the promotion asks re.Op == OpAlternate, but an alternation is not the only spelling of an undecided position. x?y? denotes (?:|x|y|xy). Three spellings of one language: (?:[a,]?)* refused, (?:(?:a|,)?)* refused, (?:a?,?)* ACCEPTED -- 225 bytes at full alphabet, matching 369 of 369 bytes of an ordinary page at confidence 1.000. The verifier's no-budget list now runs to seventeen entries against two findings, fourteen of them verified in that round rather than carried forward. That ratio, not the FAIL, is the state of this component. Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages.
shapeOf no longer asks which operator a node is. patternShape carries `decided`
(every consumable position in the denoted language is pinned to one spelled
rune) and `positions`, both computed bottom-up from an ALLOWLIST of shapes that
produce a decided position. The zero value is false, so classes, alternations,
any-char and the default arm are undecided by inheritance rather than by
someone remembering to list them.
Verified by an independent verifier, not by the author: 40 mechanically
generated spellings of one language, every one refused; 40 spellings each of
four safe languages, every one accepted; 200 patterns, zero splits. The
invariant `decided` rests on was brute-forced over 1,439 generated patterns,
429 of them decided, zero counterexamples. All 20 positive controls compile
with the exact spelled counts claimed.
A second defect surfaced while fixing the first and was closed with it: the
promotion has to be folded into the repeat UNIT before the unit's quotation is
read, because a repeat is the only place quotation is taken over a union.
Without that, Z(?:a,?)* was accepted -- the unit is decided on its own, and the
promotion landing on the star node was read by nobody, because the star's parent
is a concatenation.
classShape's one-rune arm was DELETED rather than qualified. A parser probe
showed regexp/syntax rewrites every one-rune class into a literal, so the arm
was unreachable and its stated justification was never demonstrable. The test
prose claiming otherwise was corrected.
The eighth recurrence, and why it changes the approach
quotationOverUnion is called at OpAlternate and in all three repeat arms, and
never on an OpConcat. So a concatenation of individually-narrow but UNBOUNDED
repeats reports quoted=0 while `declared` is already unbounded and `consumes` is
already content-bearing: THE VALUE R3 REFUSES IS COMPUTED, SITS IN THE STRUCT,
AND THE ARM RETURNS WITHOUT READING IT.
One language, two verdicts: `X [a-z]*` accepted at quoted=0, `X(?: [a-z]*){1}`
refused. At 125 concatenated runs it matched 12,500,141 bytes of an ordinary
page. The residual disclosed as bounded at 255 bytes has no byte ceiling at all,
because MaxPatternBytes bounds the NUMBER of concatenated elements and not the
bytes each one consumes.
And the test the previous ruling commissioned cannot fail on it: every spelling
its generator produces is wrapped in an outer repeat, so the property is closed
over one shape rather than over the grammar.
The verifier also established that the naive repair is unavailable -- applying
quotationOverUnion to a concatenation's own shape refuses legitimate patterns.
Eight rounds have tried to decide statically what a pattern can match. That is
the wrong question, and the next commit changes it rather than answering it
again. The no-budget list stands at nineteen entries, seventeen verified in that
round.
Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages;
-race clean.
An independent verifier was asked whether the confirmation guarantee still
depends on any claim about the regex grammar, and answered: it does not.
At confirmation time the gate reads two integers -- the length of the match that
actually happened and the pattern's literal footing -- and compares them once.
No parse tree, no alphabet, no corpus, no sample. The verifier could not
construct a confirmation from a match that swallows an ordinary page by any
route: not the 125-run concatenation (12,500,016 bytes, unconfirmed), not the
minimal witness, not at the exact boundary (2s confirms, 2s+1 does not), not by
scaling spelled to its measured ceiling of 500,000, not through a mixed
multi-attempt run, and not through a second code path -- there is exactly one
assignment of outcome in the package and exactly one Reason that maps to
confirmed.
Nine rounds tried to decide statically what a pattern can match. The invariant
was always about a MATCH -- R3's own message says the span is the response
rather than evidence about it -- and extractSpan had been computing exactly that
comparison all along, wired to whether to inline a span rather than to whether
the finding was confirmed. The right check existed at the wrong decision.
The static layer was NOT tuned to make this work: the diff through shapeWalk,
refuseOverBroadPattern, classShape, unionRunes and unboundedIfDeclaring is
comment-only, and both disclosed acceptance cases still accept. R1/R2/R3 remain
as a cheap early refusal whose doc now says plainly that a signature which
passed NewSignature HAS NOT BEEN PROVEN NARROW.
The outcome choice was argued rather than picked: unconfirmed, not rejected,
because AssertNotSilentlyClean deliberately does not count rejections -- a
ledger of nothing but rejections is an earned clean -- so filing "the signature
cannot see" as "the target is fine" would be the silent clean arriving through a
new door. Demonstrated verbatim: the 12.5 MB attack yields unconfirmed and
AssertNotSilentlyClean fires.
AND IT OVERSHOT, IN THE DIRECTION THE BRIEF NAMED
spelled counts LITERAL footing, but a credential oracle's evidence is by
construction a character class -- so the arithmetic treats the thing the oracle
exists to find as the response quoting itself.
AKIA[0-9A-Z]{16} has four spelled bytes against a twenty-byte match. A
REPRODUCED AWS KEY EXPOSURE NOW LANDS UNCONFIRMED and never reaches dast_status
findings. The dotted-quad leak and the GitHub, Stripe and PHPSESSID oracles fail
identically: three of the fifteen patterns this package itself calls every
signature the suite and the plan actually use.
That is a real vulnerability reported as a signature defect. It is high rather
than critical only because AssertNotSilentlyClean still fires, so it is not a
silent clean. It is open and fixed next.
Also open: the demotion is partial. NewSignature's own doc comment -- the one
surface go doc renders to an external reader -- still calls the incomplete
static analysis THE CONTROL and argues it from "samples nothing", while the
withdrawal sits four lines below in the function body where go doc does not
show it.
Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages.
The over-broadness sequence is behaviourally complete. An independent verifier
tested both halves and both hold.
NOTHING OVER-BROAD CONFIRMS. The 125-run concatenation at 12,500,141 bytes
against 141 spelled, the 302-byte minimal witness, mixed multi-attempt runs in
all four orderings, the second code path, the 5001-byte fixed-width unit, the
exact span boundary, and the scaled-footing attack -- every one still fails to
confirm. The new floor was attacked directly and held: the boundary is exactly
at MaxUnspelledBytes end to end on a fixture the package does not own (255 and
256 confirm, 257 does not), and a 4,200-case sweep produced no inlined span
carrying more than 256 unspelled bytes.
EVERY REAL ORACLE CONFIRMS AGAIN. All 18 the package names, including the one
the previous round broke: AKIA[0-9A-Z]{16}, spelled=4, matchLen=20, CONFIRMED,
span "AKIA1234567890ABCDEF". Plus 16 of 22 further oracles the verifier
constructed -- private key header, JSESSIONID, ASP.NET and Django fingerprints,
SQLSTATE, /etc/passwd, XXE, CORS wildcard.
The floor is derived, not chosen, and the derivation is checkable in one
sentence: an inlined span exists only when its match fits in MaxSpanBytes, and
the quotation relation caps unspelled bytes at the spelled bytes, so
2q <= L <= MaxSpanBytes gives q <= MaxSpanBytes/2 = 256 -- which is exactly the
ceiling the R-rules header has published since it was written.
Worth recording how the alternative was rejected: a floor at MaxSpanBytes itself
was IMPLEMENTED AND RUN, and turned the minimal witness green -- 302 bytes
against 2 spelled, confirmed. MaxSpanBytes bounds the whole span, Anvil's
literals plus the response; the invariant is about the response half alone. And
MaxSpanBytes/8 would also have passed every acceptance case: the worker rejected
it in writing because /8 has no provenance, and kept the larger floor precisely
because it is derived rather than fitted.
WHAT STILL FAILS, AND IT IS DOCUMENTATION AND ONE PIN
The floor is unpinned. Replacing MaxSpanBytes/2 with the bare literal 299 leaves
the whole package green, window [255, 300], because the boundary test computes
its edge FROM MaxUnspelledBytes. That is "a pin computing its expectation from
the constant it pins" -- a standing sweep item -- reproduced inside the test
written to close it.
Two sentences state withdrawn arithmetic as current, one of them fifteen lines
below the paragraph that withdrew its twin, and both are measurably false at the
new boundary. That is the measurably-false-justification class, reopened in the
same comment block a previous round was ordered to correct.
And the refused-oracle residual is a FAMILY rather than the single case
disclosed: every credential oracle whose class is base64url -- containing an
underscore or a hyphen -- is refused upstream at R3.
Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages.
The confirmation gate's behaviour is settled. This round was claims and pins, and the production diff proves it: stripped of comments it is exactly one line pair -- a declaration byte-identical with a changed trailing comment. The test-file diff deletes one line, and it is the self-referential edge assignment that caused the finding. THE PIN THAT WAS NOT A PIN. MaxUnspelledBytes = MaxSpanBytes/2 is a correct derivation that enforced nothing: substituting the bare literal 299 left the whole package green, anywhere in [255, 300]. The cause sat inside the test written to close the previous round -- it took its edge FROM the constant it was pinning, which is the standing-sweep item reproduced inside the pin. Two pins now, neither computed from the other, and their independence is proven in both directions: the literal pin catches a bare substitution and is blind to a changed span bound; the relation pin catches a changed span bound and is blind to an equal literal. Six mutations -- 299, 255, 257, 64, MaxSpanBytes with the derivation kept, MaxSpanBytes with a literal floor left behind -- each turned the right test red with a message naming the integers. TWO FALSE SENTENCES DELETED RATHER THAN QUALIFIED. Both asserted the pre-floor arithmetic, and both were measured false at the witnesses that disprove them: q=256 against L=257 where the span is still inlined, and s=1 against L=201 where the finding confirms at confidence 1.000 with a 201-byte span. What survives is the half that holds unconditionally -- q <= 256 for every inlined span, carried by matchQuotesMoreThanItSpells and not by R3, which refuses early and does not make the inequality true. THE REFUSED-ORACLE FAMILY IS NAMED AND MEASURED. Every credential oracle whose class admits a letter and a non-alphanumeric is refused by R3 upstream of the floor -- base64url's underscore and hyphen, standard base64's plus and slash, a JWT's dot. Six measured members with their quoted/spelled pairs: JWT pairs, Google OAuth2, OpenAI-style keys, GitLab PATs, Slack bot tokens, raw bearer headers. The remedy is measured too, and so is its cost: dropping the punctuation from the class makes the oracle compile and MISSES 48% of real tokens, because the repeat's minimum must be met by alphanumerics alone. Two members that fail a DIFFERENT way are named and excluded rather than folded in. The disclosure is a test, and widening R3 tenfold turns it red, so it is not vacuous. Still open, and it is one sentence in the worst possible place: ExtractedSpan's GODOC -- the description a consumer reads without opening the file -- still carries the pre-floor arithmetic. The mandated sweep corrected the field comment twelve lines above it and the accessor six lines below it, and stepped over the exported method between them. Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages. Every attack re-measured by an independent verifier and not one verdict moved.
… spot Three rounds were told to sweep this file for false claims and each corrected the sentence it was pointed at and left another behind. A human sweep of prose is a denylist of sentences somebody remembered to read, so this round built a mechanical check instead: an arithmetic claim must CITE the identifier that enforces it and the test that measures it, and an uncited claim fails the build. BEHAVIOUR PROVED UNMOVED TWICE. The comment-strip on the production diff is empty, and an AST comparison against 1cf871e -- parse without comments, reprint, diff -- returns identical, which also rules out a moved string literal or constant that a strip cannot see. Every attack re-measured to the same verdict and all 18 oracles confirmed at 1.000 with their hard-coded pairs. THE EXPORTED GODOC IS NOW TRUE, AND SAYS MORE THAN IT DID. ExtractedSpan's description told a consumer the span is empty in TWO situations and that SpanOverBroadBytes tells them apart. There are FOUR, measured -- and the fourth was found by the worker rather than named in the brief: an all-indecisive run yields span "", dropped 0, over 0, byte-identical on all three fields to "matched nothing". So the field does not discriminate, and the godoc now opens by saying so rather than by claiming it does. The refuted witness is worth keeping in view: a CONFIRMED finding, oracle fired 3/3, with an empty span and SpanOverBroadBytes 0 -- exactly the pair the old sentence told a consumer meant the signature matched nothing. THE FAMILY DISCLOSURE GOT STRICTLY BETTER. What was six base64url examples is now a swept predicate: R3 refuses a repeat drawing from a class that admits a letter AND a printable non-alphanumeric, when the ceiling exceeds the literal footing. Adding each of the 33 printable non-alphanumeric runes in turn to an ACCEPTED class flips it to refusal, 33 of 33, none by another rule. Six members outside base64url were added. A verifier tried to falsify the widened predicate with a 960-member sweep and got zero falsifications. THE GUARD HAS A BLIND SPOT AND ITS DOC DENIES IT enforcerShape claims that requiring a case boundary "is what separates a NAME from an English word without keeping a denylist of English words". It does not. 436 declared identifiers satisfy it and about three dozen are ordinary English nouns this file's prose uses constantly -- Finding appears in 62 comment paragraphs, Signature in 27. A verifier appended a comment to the real file asserting q <= 4096 and that an inlined span may run to twice the published ceiling -- both false, the real numbers being 256 and 512 -- carrying no citation the author intended, only the nouns Evidence and Finding. THE GUARD PASSED IT. That is the exact shape the brief named as the thing to catch: a guard that silently covers less than it appears to. It is the defect this file has been fighting, arriving inside the control built to end it. Everything else about the guard is sound: it goes red on all three shapes it claims to catch, its other disclosed holes are real and honestly stated, and all twenty existing claims pass with a genuine technical enforcer named in each. Evidence: gofmt, vet clean; go test -count=1 ./... green across 31 packages.
The arithmetic-claim guard now requires a template rather than a name. enforcerShape and citeToken are deleted. A claim paragraph carries ENFORCED BY <identifier>, MEASURED BY <TestName>, and three arms run per citation: the FORM (prose naming the same two identifiers in English no longer satisfies anything), the NAMES (both resolve against declared identifiers), and the APTNESS (the cited test must mention the cited enforcer). The predecessor's attack is dead. The false 4096/1024 paragraph citing only the nouns Evidence and Finding is now caught, naming the missing form. Typos, wrong case, invented test names, undeclared enforcers and camel-case unrelated citations are all caught with the right defect named -- and the `defect` column that makes those arms separable was added by the worker on its own initiative, after it found that disabling BOTH resolution arms left the whole table green because the aptness arm was silently answering for all three. The ceiling is disclosed and demonstrated rather than asserted: the same false paragraph with a WELL-FORMED citation passes, and that is written down as the measured limit. BEHAVIOUR PROVABLY UNMOVED, third round running. The comment-stripped AST is identical to 6889aaf with zero diff lines, and a declaration-by-declaration hash comparison shows 153 production declarations unchanged and exactly 8 test declarations touched, all of them the guard. Two findings, and both are the same lesson recurring inside its own fix THE STREAK REACHED FIVE. The sentence this round was sent to correct still stands, verbatim, in the SIBLING FILE -- 1,331 lines from the copy that was fixed at length, in a paragraph about the same deleted counter, citing the same 52-run sweep. Four consecutive rounds have each left exactly one behind; this is the fifth and what it left behind is the same one. The guard's own header describes this failure mode and the round reproduced it at file scale. RULING 16 RECURRED INSIDE RULING 16. The aptness arm claims a citation cannot name a real rule and an unrelated real test. `mentions` is a whole-word TEXT SEARCH over the cited test's declaration, so an enforcer spelled like an English word is mentioned by any prose using the word. 281 of 770 declared identifiers are reachable this way, demonstrated as a matched pair: cited ENFORCED BY matchQuotesMoreThanItSpells it is caught; cited ENFORCED BY decide, MEASURED BY a test in internal/match about advisory ordering, it passes. Only the spelling of the name differs. Names are words. The form fixed the citation layer and the aptness layer still resolves a name against prose. Worth recording: the round also caught itself shipping unreproducible figures. The 436/62/27 numbers this ruling quoted do not reproduce against the tree, and rather than pin new prose figures the worker wrote a subtest that RECOMPUTES the population every run and asserts a floor. A number in a report that does not reproduce is the same defect as a false comment. Of 69 claim sites read against the code -- 20 cited arithmetic claims, 24 rule references, 25 "the only" claims -- 66 are true, and all 20 arithmetic claims hold. Evidence: gofmt, vet clean; 31/31 packages green; record green under -race.
The aptness arm asked whether a cited test "mentions" the cited enforcer by TEXT SEARCH, so an enforcer spelled like an English word was mentioned by any prose using the word -- 281 of 770 declared identifiers reachable that way. It now reads the cited test's REFERENCE SET: every ast.Ident under the FuncDecl, less its own name. Comments contribute no ast.Ident, so prose cannot satisfy a citation, and the self-certifying citation is dead by construction. A verifier attacked it with eleven purpose-built tests and could not break it. It refuses all four prose-and-string forms the withdrawn search accepts -- the enforcer named only in a string literal, only in an in-body comment, only in the test's own doc comment, only in a subtest name. Of the seven it accepts, two are correct and five fall inside holes the doc already discloses in terms. The verifier then wrote its OWN stricter reference set and re-ran all 31 shipped citations under it: zero verdicts changed. THE ONE-HOP ALTERNATIVE WAS BUILT AND MEASURED RATHER THAN ARGUED, and the measurement went the other way: over 1,435,112 pairs, text admits 35,450, direct 15,866, one hop 41,108. ONE HOP IS THE LOOSEST OF THE THREE -- a callee carries its whole reference set into every test naming it -- and it turns the aptness check's own reason-to-exist case green. That result was not the expected one, which is why it is asserted from a live count rather than reasoned about. The price was paid in the open: one citation now names the helper the test actually drives, with the outer function named in the prose beside it. THE STREAK IS NOT BROKEN, AND THE REASON IS THE LESSON The round built a withdrawn-phrasing registry and it works for its class: a synthetic sentence planted in both files at two different wrappings was caught in both, and half-correcting it left the surviving twin red. But the class that has recurred six times is a false ATTRIBUTION, not a wording, and a registry keyed on wording is a denylist. The verifier swept the general case instead -- every camelCase token in the two files' comments, resolved against every ast.Ident in the module. Eighteen resolve nowhere; seventeen are explicitly narrated as dead, historical or synthetic; the eighteenth is live prose at confirm_gate.go:2828 saying an EvidenceRef "came from newEvidenceRef", a constructor that has never existed -- 115 lines below the paragraph that withdraws exactly that attribution. False twice, in fact: the constructor does not exist, and the method it describes does not check constructor provenance at all. The registry behaved exactly as designed and the defect walked past it, because a paraphrase is invisible to a denylist. That is the same sentence this project has now written about a status list, an entity bound, a benign corpus, a generator alphabet, a class union, a type-keyed dedup, an operator tag, a concatenation arm, and a case-boundary heuristic. BEHAVIOUR PROVEN UNMOVED, fourth round running: confirm_gate.go at 197 declarations on both sides of an AST comparison against 27139d8, 197 identical, zero changed, zero added, zero removed. No new imports, no go/types, no t.Skip. Evidence: gofmt, vet clean; 31/31 packages green; race clean from PowerShell.
confirm_gate.go's package doc told every reader -- and go doc, which renders it first -- that regexp/syntax is NOT on gate 3's inert allowlist, that TestGate3NoSocketIsConstructedOutsideTheKernel therefore FAILS on this file "by design", that the one-line widening "IS NOT MADE HERE", and that it stood reported to the orchestrator for review. All four were false. regexp/syntax is at internal/dast/authz/egress_chokepoint_test.go:186; the gate is green (110 files scanned, 10 egress sites, all inside the kernel or on the allowlist); and the indented gate03-refused block it quoted no longer reproduces without deleting the allowlist line first. HOW IT BECAME FALSE IS WORTH MORE THAN THE CORRECTION Every sentence was TRUE WHEN WRITTEN. The packet reported the widening instead of making it, which is exactly right -- a packet that widens the allowlist in the same diff that needs it has reviewed itself. The orchestrator then made the edit, in the SAME COMMIT (e20f64c). Nobody was wrong and the paragraph was false on arrival, because a sentence about a PENDING DECISION rots the instant the decision lands. It then stood through nine commits and six fix rounds, in the doc go doc renders first, about the control that decides which packages may hold egress capability -- the one place in this repository where a reader believing a stale doc has a security consequence. This correction is the orchestrator's, not a worker's: the error was mine. It also named encoding/base64 in internal/dast/inventory/auth_helper.go as the same state. That request IS still open -- verified, the allowlist has no entry -- so the file was presenting one closed review and one open one identically. The corrected text names that as the specific error to avoid next time. The justification is kept verbatim, because it is the review record: regexp/syntax is the parser and compiler behind regexp, which is already on the allowlist; it has no dialer, listener, transport or I/O; gate 3 attributes by import line rather than transitively, which is why the line was needed at all. THE WINDOW GUARD CAUGHT THIS EDIT, WHICH IS THE POINT OF IT Rewriting the paragraph moved the comment-node and physical-line counts the window disclosure pins, and the suite went red naming both old figures and both new ones. Re-measured in the same diff: 6,198 to 6,211 nodes, 6,197 to 6,210 lines. A guard that fails when the thing it describes moves is the only kind that keeps a disclosure true. The production change is comments only -- verified by filtering the diff to non-comment lines, which is empty. Evidence: gofmt, vet clean; 31/31 packages green; gate 3 green.
The last verifier's headline: "NOT YET -- but it is documentary only, and the single defect is one wrong word." BEHAVIOUR: ZERO DEFECTS, SIX ROUNDS RUNNING, AND NOW MECHANICALLY PROVEN RATHER THAN ARGUED. confirm_gate.go is AST-identical to 06c69a4 with comments stripped -- verified by parsing both revisions with comments discarded and diffing the printed declarations, not by filtering comment lines out of a text diff. The test file gained one hoisted literal, one hoisted local, five equality assertions and four re-measured constants. No import, no exported declaration, no altered expectation, no t.Skip. RULING 19 LANDED ACROSS THE FILE. Eleven sites carrying 39 figures each got one of the two treatments or were deleted. Two are worth naming: The "17 of 20 bounded-prefix HTML anchors" figure, stated three times and once in the present tense, described a family of twenty THAT WAS NEVER WRITTEN DOWN ANYWHERE. Unreproducible by a reader, unpinnable by the package, and incapable of ever going red -- no vocabulary edit and no rename could move it. It is replaced by a hoisted anchorTags var and three pinned equalities over the corpus arm run alone, measured at 3 caught / 11 missed / 14 members. The benchmark block took treatment (a) with its reasoning stated: a timing is the one measurement here that CANNOT be pinned as an equality, because pinning it would pin the hardware. It now names the tree it was taken on, the Go version, CPU and OS, and the command to reproduce it. A duplicate of one figure sixty lines away was deleted on the ground that a timing written twice is a timing that goes stale once. THE LAST THREE, FIXED HERE RATHER THAN DISPATCHED zzCarrier was called "a two-field struct". It has one -- zzCarrier{ Payload []byte }; the two-field struct in that worked example is the OUTER one reaching it twice, which the entry's next clause already says correctly. The previous round was pointed at this exact string, corrected the clause after the comma, and left the clause before it. The plan citation read "D.27 (lines 870-911)". D.27's fenced block runs 869-907; 911 opens D.28's. Verified against the fence lines. And the corpus header count said its figure was written "four times over". It is written three times -- "nine vocabularies" twice and "NINE OF THEM" once. A count somebody took by eye, in the sentence complaining that the number it describes was a count somebody took by eye. Eight rounds each left exactly one behind. These three were verified directly against the tree rather than delegated, because a workflow for three words is not proportionate and the verification is a grep. The production diff is comments only. Evidence: gofmt, vet clean; 31/31 packages green; gate 3 green; race clean.
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.
Stacked on #14. Retargets to
mainautomatically when that merges.26 of 31 Phase 4 packets. D.29 inherits its base model from the SAST adjudicator selection, so it, its critic D.30, and the D.31 integration test are held on
M0.18.What is here
Verification
36 adversarial critics ran against this work and all 36 returned FAIL correctly. The last six found zero behavioural defects in the confirmation gate, proven by AST comparison rather than asserted.
Findings worth naming, all now closed:
AssertNotSilentlyCleanderivedcompleted_cleanfrom it..anvil/target.yamlpointing the health check at169.254.169.254. Gate 10 covers probe time; the health check is a different path.Rulings
Nineteen orchestrator rulings are recorded in the commit messages. Most are one lesson in new clothes — a denylist loses — which recurred nine times: a status list, an entity bound, a benign corpus, a generator's alphabet, a depth-0 union, a class union excluding literals, an operator tag, a case-boundary heuristic, a wording registry.
What ended it was ruling 14: enforce the invariant where it is exactly decidable. Nine rounds tried to decide statically what a regex could match. The invariant was always about a match — two integers, one comparison — and
extractSpanhad been computing it all along, wired to whether to inline a span rather than whether to confirm a finding.Gate 1 is no longer vacuous
go list -depspreviously found zerointernal/dastpackages in either binary, so the S9-AMENDED artifact split passed by emptiness and would have kept passing if the kernel were deleted.cmd/anvil-dastnow genuinely linksinternal/dast/{authz,engines}andcmd/anvilstays empty of them.Known limits
Docker, Linux network namespaces, nftables, nuclei, ZAP and Java are all absent from the dev host, so containment and the engine drivers are proven against recorded shapes with the tool-absent path refusing loudly. Every gap is recorded in
internal/SKIPPED-CONTROLS.mdwith what would settle it. Not.Skipwas added anywhere.Note on history
One commit range was rewritten before pushing: a synthetic GitLab-PAT-shaped test fixture (
glpat-plus 20 keyboard-pattern characters, inside the base64url oracle-family test) tripped GitHub push protection. It was never a real credential, but it matched the format exactly. Replaced with a zero-entropyglpat-NOTAREALTOKEN0000000that still matches the pattern under test, and purged from history rather than allow-listed — so the repository never contains a credential-shaped string.