From 3247fbcf9295246474ae4585895db05934c946d4 Mon Sep 17 00:00:00 2001 From: shankar0123 Date: Tue, 28 Apr 2026 16:09:38 +0000 Subject: [PATCH] Release-notes hygiene: drop duplicated install block + retire hand-edited CHANGELOG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Triggered by Reddit feedback (sysadmin user complained that every release page shows the same install instructions instead of what actually changed). Two changes: 1) .github/workflows/release.yml: removed ~80 lines of hardcoded install/docker/helm boilerplate from the release body. Replaced with a single link to README.md#quick-start (the source of truth for install instructions). Kept the per-release supply-chain verification block (Cosign / SLSA / SBOM steps with the version baked into the commands) — that IS per-release-meaningful and the kind of content a security-conscious operator actually wants. generate_release_notes: true unchanged → GitHub auto-generates the 'What's Changed' section from commits between this tag and the previous one. 2) CHANGELOG.md: replaced 1393-line hand-edited document with a one-paragraph stub pointing at GitHub Releases as the source of truth. The old CHANGELOG had drifted (everything since v2.2.0 piled into [unreleased]; tags v2.0.55-v2.0.61 had no entries). A stale CHANGELOG is worse than no CHANGELOG — signals abandoned maintenance to operators doing security diligence. Auto-generated notes from commit messages work here because the project's commit message convention is already descriptive (see git log v2.0.50..HEAD for established pattern). Pre-v2.2.0 history preserved at the v2.2.0 git tag. Net result: every future release page shows - 'What's Changed' (auto from commits, per-release-unique) - 'Verifying this release' (Cosign/SLSA verification, per-release-version) - One-line link to README install …instead of the same 80-line install block on every release. Verification: - python3 yaml.safe_load(.github/workflows/release.yml): OK - No internal references to CHANGELOG.md elsewhere in repo (grep README.md docs/ → empty) - Release-pipeline change is YAML-only; no Go code touched Bundle: chore/release-notes-hygiene --- .github/workflows/release.yml | 88 +- CHANGELOG.md | 1465 +-------------------------------- 2 files changed, 40 insertions(+), 1513 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7615299..12eb9b6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -334,75 +334,21 @@ jobs: run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" - name: Create release with notes + # generate_release_notes: true asks GitHub to auto-generate the + # "What's Changed" section from PRs+commits between this tag and the + # previous one. The hardcoded body below appends a per-release + # supply-chain verification block (Cosign / SLSA / SBOM steps with the + # current version baked into the commands) plus a single link to the + # README's Quick Start section for install/upgrade instructions. + # We deliberately do NOT duplicate install instructions here — the + # README is the source of truth for those, and inlining them in every + # release page produces the kind of "every release looks identical" + # noise that gives operators no signal about what actually changed. uses: softprops/action-gh-release@v2 with: generate_release_notes: true body: | - ## Installation - - ### Quick Install (Linux/macOS) - - ```bash - curl -sSL https://raw.githubusercontent.com/shankar0123/certctl/master/install-agent.sh | bash - ``` - - ### Manual Binary Download - - Download the appropriate binary for your OS and architecture: - - - **Linux x86_64**: `certctl-agent-linux-amd64` - - **Linux ARM64**: `certctl-agent-linux-arm64` - - **macOS x86_64**: `certctl-agent-darwin-amd64` - - **macOS ARM64 (Apple Silicon)**: `certctl-agent-darwin-arm64` - - Then make it executable and start the service: - - ```bash - chmod +x certctl-agent-linux-amd64 - sudo mv certctl-agent-linux-amd64 /usr/local/bin/certctl-agent - ``` - - ## Docker Images - - Pull pre-built Docker images for server and agent: - - ```bash - docker pull ghcr.io/shankar0123/certctl-server:${{ steps.version.outputs.VERSION }} - docker pull ghcr.io/shankar0123/certctl-agent:${{ steps.version.outputs.VERSION }} - ``` - - Or use the latest tag: - - ```bash - docker pull ghcr.io/shankar0123/certctl-server:latest - docker pull ghcr.io/shankar0123/certctl-agent:latest - ``` - - ## Docker Compose Quick Start - - ```bash - git clone https://github.com/shankar0123/certctl.git - cd certctl - cp deploy/.env.example deploy/.env - docker compose -f deploy/docker-compose.yml up -d - ``` - - ## Server Binaries - - Pre-compiled server binaries are also available for direct installation: - - - **Linux x86_64**: `certctl-server-linux-amd64` - - **Linux ARM64**: `certctl-server-linux-arm64` - - **macOS x86_64**: `certctl-server-darwin-amd64` - - **macOS ARM64 (Apple Silicon)**: `certctl-server-darwin-arm64` - - ## CLI & MCP Server Binaries - - The `certctl-cli` (REST API wrapper) and `certctl-mcp-server` (Model Context - Protocol bridge) binaries ship for all four platforms as well: - - - `certctl-cli-{linux,darwin}-{amd64,arm64}` - - `certctl-mcp-server-{linux,darwin}-{amd64,arm64}` + > **Install / upgrade:** see the [Quick Start section in the README](https://github.com/shankar0123/certctl/blob/master/README.md#quick-start) for Docker Compose, agent install, Helm, and binary download instructions. ## Verifying this release @@ -463,15 +409,3 @@ jobs: --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ "$IMAGE" ``` - - ## Helm Chart - - Deploy certctl to Kubernetes using Helm: - - ```bash - helm repo add certctl https://github.com/shankar0123/certctl/tree/master/deploy/helm - helm repo update - helm install certctl certctl/certctl - ``` - - See `deploy/helm/certctl/` for values customization. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d3f2b9..a6e4a86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,1438 +1,31 @@ # Changelog -All notable changes to certctl are documented in this file. Dates use ISO 8601. Versions follow [Semantic Versioning](https://semver.org/). - -## [unreleased] — 2026-04-27 - -### Bundle R-CI-extended raise — CI threshold raises post-extensions - -> Final CI threshold raise commit on top of all the *-extended bundles. Each raise verified ≥3pp margin below current measured coverage to absorb the global-run per-file-average dip vs package-scoped runs. - -Floors lifted in `.github/workflows/ci.yml`: - -- `internal/connector/issuer/acme/`: **50 → 80** (post-Bundle-J-extended; HEAD 85.4%) -- `internal/service/`: **55 → 70** (post-Bundle-N.C-extended; HEAD 73.4%) -- `internal/api/handler/`: **60 → 75** (post-Bundle-N.C-extended; HEAD 79.8%) - -Held at prior floors (already met; further raises deferred): - -- `internal/crypto/`: 88 (HEAD 88.2%; 92 deferred — needs rand.Reader / aes.NewCipher seams) -- `internal/connector/issuer/local/`: 86 (HEAD 86.7%; 92 deferred — needs crypto/x509 signing-error seams) -- `internal/pkcs7/`: 100% informational (global-run measurement artifact) -- `internal/connector/issuer/stepca/`: 80 (HEAD 90.4%; can absorb a future raise) -- `internal/mcp/`: 85 (HEAD 93.1%; can absorb a future raise) - -YAML lint clean. - -### Bundle N.C-extended (Coverage Audit Extension): service + handler round-out — M-002 + M-003 partial-closed - -> Three new round-out test files (~26 tests) lifting service 70.5% → 73.4% and handler 79.4% → 79.8%. Both miss the prescribed 80% gate (by 6.6pp / 0.2pp respectively); marked partial-closed. - -`certificate_round_out_test.go` exercises CertificateService handler-interface delegators (Get/Create/Update/Archive/GetVersions/SetJobRepo/SetKeygenMode). `agent_round_out_test.go` exercises AgentService delegators (GetAgent/RegisterAgent/GetWork/CSRSubmit/CertificatePickup/GetAgentByAPIKey/SetProfileRepo/UpdateJobStatus). `round_out_test.go` exercises IssuerHandler constructor + HealthCheckHandler dispatch arms. Remaining gap: service needs CSR-submit happy-path + large-population list filters; handler needs SCEP `parseSignedDataForCSR` + DeleteHealthCheck/AcknowledgeHealthCheck. - -### Bundle N.A/B-extended (Coverage Audit Extension): 6 issuer connectors lifted via failure-mode tests — M-001 closed - -> Per-CA failure-mode `_failure_test.go` files added across vault, digicert, sectigo, globalsign, ejbca, entrust. Pattern: httptest.Server returning canned 401 / 403 / 404 / 5xx / malformed-JSON / missing-PEM / invalid-base64 + GetOrderStatus dispatch-arm tests for status variants (pending / processing / rejected / unknown). - -Coverage deltas: - -| Connector | Pre | Post | Δ | -|---|---|---|---| -| vault | 84.1% | **87.3%** | +3.2 | -| sectigo | 79.4% | **85.5%** | +6.1 | -| globalsign | 78.2% | **87.1%** | +8.9 | -| digicert | 81.0% | **84.9%** | +3.9 | -| ejbca | 76.5% | **84.3%** | +7.8 | -| entrust | 70.8% | **81.2%** | +10.4 | - -Already at or above 85%: stepca 90.4% (Bundle L.B), awsacmpca 83.5%, googlecas 83.4%. M-001 marked CLOSED (target-met-on-average). Entrust 81.2% + awsacmpca/googlecas 83% need interface seams for SDK-internal retry paths — tracked but not blocking. - -### Bundle J-extended (Coverage Audit Extension): ACME 55.6% → 85.4% via Pebble-style mock — C-001 fully closed - -> Closes the deferred ≥85% target on `internal/connector/issuer/acme` that Bundle J originally partial-closed at 55.6%. The remaining gap was `IssueCertificate` + `solveAuthorizations*` + `authorizeOrderWithProfile`'s JWS-POST branch — all uncoverable without a Pebble-style ACME mock. This extension ships that mock. - -#### What shipped - -`internal/connector/issuer/acme/pebble_mock_test.go` (~900 LoC). Hermetic in-process ACME server with: - -- **RFC 8555 state machine** — newAccount (handles `onlyReturnExisting=true` for `GetReg(ctx, "")` lookups, returning HTTP 200 vs 201 for the right path) + newOrder (with profile field passthrough) + authz (per-identifier, configurable `pending`/`valid` start state) + challenge (POST flips status + propagates to parent authz + recomputes order readiness) + finalize (parses JWS payload, decodes CSR, signs against fixture CA) + cert (returns PEM chain) + order-poll + account-self. -- **JWS envelope parsing** — base64url-decode protected header + payload, no signature verification (the stdlib client signs correctly; the test value is exercising connector code, not fuzzing stdlib JWS). -- **Nonce ring** — tracks issued/consumed; replays return `urn:ietf:params:acme:error:badNonce` with a fresh nonce. -- **In-process CA fixture** — self-signed ECDSA P-256 root used to sign issued certs; chain returned at /cert/. -- **Mock DNSSolver** — implements `Present` / `CleanUp` / `PresentPersist` for DNS-01 + DNS-PERSIST-01 tests. - -#### Tests (13 new) - -- `IssueCertificate_HappyPath` — single-domain, no profile -- `IssueCertificate_MultiSAN` — 3 SANs in one cert -- `IssueCertificate_WithProfile` — exercises `authorizeOrderWithProfile` (profile=`tlsserver`) -- `RenewCertificate_DelegatesToIssue` — confirms RenewCertificate flow -- `GetOrderStatus_HappyPath` — confirms order URI returned by issuance is queryable -- `NewAccountFailure_ReturnsError` — connector reports clean error when newAccount returns 400 -- `FinalizeProcessingStuck_RecoversToValid` — connector's WaitOrder fallback path on Pebble-style processing-state -- `FinalizeReturnsInvalid_FailsClean` — order-invalid surfaces as error -- `ContextCancel_DuringIssuance` — pre-cancelled ctx propagates -- `BadCSR_RejectedByMock` — malformed CSR fails before mock POST -- `IssueCertificate_HTTP01ChallengeFlow` — exercises `solveAuthorizationsHTTP01` + `startChallengeServer`; `HTTPPort: 0` for free-port binding -- `IssueCertificate_DNS01ChallengeFlow` + `DNS01_PresentFails_PropagatesError` + `DNS01_NoSolver_FailsClean` -- `IssueCertificate_DNSPersist01ChallengeFlow` + `DNSPersist01_FallbackToDNS01_WhenChallengeNotOffered` + `DNSPersist01_NoSolver_FailsClean` - -#### Per-function coverage deltas (vs. pre-Bundle-J baseline) - -| Function | Pre-J | Post-J | Post-J-extended | -|---|---|---|---| -| `IssueCertificate` | 0.0% | 6.4% | **100.0%** | -| `solveAuthorizations` | 0.0% | 0.0% | **100.0%** | -| `solveAuthorizationsHTTP01` | 0.0% | 0.0% | **88.4%** | -| `solveAuthorizationsDNS01` | 0.0% | 0.0% | **91.4%** | -| `solveAuthorizationsDNSPersist01` | 0.0% | 0.0% | **87.0%** | -| `authorizeOrderWithProfile` | 0.0% | 21.3% | **92.5%** | -| `GetOrderStatus` | 0.0% | 37.5% | **100.0%** | -| `startChallengeServer` | 0.0% | 30.4% | **100.0%** | - -Package overall: **41.8% → 55.6% → 85.4%** (+43.6pp; +0.4 above 85% gate). - -#### Verification - -- `go test -count=1 -timeout=20s ./internal/connector/issuer/acme/...`: PASS in 1.4s -- `go test -short -count=1 -cover ./internal/connector/issuer/acme/...`: 85.4% -- `go vet ./internal/connector/issuer/acme/...`: clean - -#### Audit deliverables - -- `findings.yaml` C-001: `partial_closed` → **`closed`** with full closure note enumerating all 13 tests + per-function deltas -- `gap-backlog.md` C-001: full strikethrough with closure note -- `coverage-audit-2026-04-27/extension-progress.md`: J-extended marked DONE - -Closes: C-001 (ACME Existential coverage) -Bundle: J-extended (Coverage Audit Extension) - -### Bundle S — Extension pipeline (partial: 4 of 7 + R-CI raise pending) - -> Four extensions shipped this session against the post-Bundle-R audit state. Three still pending due to scope (J-extended Pebble mock, N.A/B-extended 8 connectors, N.C-extended service+handler round-out). R-CI-extended raise deferred until prior extensions complete. Acquisition-readiness 4.3 → ~4.4 (modest lift; full +0.4-0.5 contingent on remaining extensions). - -#### Bundle I-001-extended (M-Q closure follow-on): test-naming guard promoted to hard-fail with relaxed convention - -`.github/workflows/ci.yml` Test-naming convention guard flipped from `continue-on-error: true` to hard-fail. Convention RELAXED: the original audit's `Test__` triple-token form was overzealous — single-Function pin tests like `TestNewAgent` follow Go's standard convention. The new guard catches genuine bugs (`func TestX[a-z]...` which Go's test runner silently skips). 0 hits at HEAD; safe to flip. The audit's prescription is preserved in `docs/qa-test-guide.md` as RECOMMENDED for parameterized scenarios but not gated repo-wide. - -#### Bundle M.SSH-extended (H-002 closure): SSH 71.6% → 90.2% - -`internal/connector/target/ssh/ssh_server_fixture_test.go` (~628 LoC, 14 tests) ships an embedded `golang.org/x/crypto/ssh` ServerConn + `pkg/sftp.NewServer` fixture bound to `net.Listen("tcp", "127.0.0.1:0")`. Same hand-rolled in-process protocol-server pattern as M.Email's SMTP fixture. ed25519 host keys; password + key auth; optional toggles for `rejectAuth` / `dropOnHandshake` / `failExec` / `failSFTP` failure modes. Coverage delta per-function: Connect 0%→~95%; Execute 25%→~95%; WriteFile 15.4%→~95%; StatFile 33.3%→~95%; Close 42.9%→~95%. Package overall: 71.6% → 90.2% (+18.6pp; +5.2 above 85% gate). H-002 status flips `partial_closed` → `closed`. - -#### Bundle 0.7-extended (cmd/agent overall round-out): 57.7% → 73.1% - -`cmd/agent/dispatch_test.go` (~640 LoC, 18 tests) lifts cmd/agent overall line coverage 57.7% → 73.1% (+15.4pp). Same httptest-backed pattern as the existing `agent_test.go`. Per-function deltas: executeCSRJob 14.1%→64.1%; executeDeploymentJob 46.7%→66.7%; Run 0%→62.2%; markRetired / getEnvDefault / getEnvBoolDefault all 0%→100%; verifyAndReportDeployment partial. Test groups: executeCSRJob happy path + empty-CN + CSR-rejection-400; executeDeploymentJob fetch-fail + key-missing + unknown-target; markRetired sync.Once safety; getEnv* every truthy/falsy spelling; Run context-cancel + 410-Gone retire signal; verifyAndReportDeployment probe-fail + nil-target. Remaining gap to 75% is `main()` (os.Exit) — tracked as `cmd/agent-main-extended`. - -#### Bundle P.2-extended (M-008 closure): RFC test-vector subsections - -Pure doc work. Three subsections added to `docs/testing-guide.md`: - -- **Part 21.99** — RFC 7030 EST test vectors: /cacerts response framing (§4.1.3), /simpleenroll request framing (§4.2.1), /serverkeygen multipart response (§4.4.2) -- **Part 23.99** — RFC 5280 SAN/EKU vectors: IPv4/IPv6/IDN-Punycode/otherName SAN encoding (§4.2.1.6); EKU OIDs + criticality (§4.2.1.12 + CA/B Forum BR §7.1.2.7) -- **Part 24.99** — RFC 6960 OCSP / RFC 5280 §5 CRL vectors: OCSP status (§4.2.2.3 tryLater), ResponderID byKey/byName (§4.2.2.2), nonce echo (§4.4.1); CRL TBSCertList (§5.1.2), reason codes (§5.3.1, reserved 7 + out-of-range), IDP extension (§5.2.5), no-delta-CRL (§5.2.4) - -Each vector cites RFC section + provides ASN.1 byte snippet where relevant + names the certctl pin location (file + test name). +225 lines; 56 Parts unchanged. M-008 fully closed. - -#### Pending extensions - -These are tracked in `coverage-audit-2026-04-27/extension-progress.md` for a continuation session: - -- **J-extended** — Pebble-style ACME mock (4-6 hr; ACME 55.6% → ≥85%) -- **N.A/B-extended** — per-CA failure-mode mocks for 8 issuers (6-8 hr; ~2500 LoC) -- **N.C-extended** — service+handler round-out (3-4 hr; service 70.5% → ≥80%, handler 79.4% → ≥80%) -- **R-CI-extended raise** — final +7pp threshold jumps (deferred until J + N.C land) - -### Bundle R (Coverage Audit Final Closure + CI raise checkpoint #3): audit closed 33/33; acquisition-readiness 4.3/5 - -> Closes the 2026-04-27 coverage audit. CI threshold raise #3 applied (defensible against post-Q measurements). Coverage matrix Post-Closure Summary appended. Acquisition-readiness final score: **4.3 / 5** — passing tech DD clean. The +0.2-0.7 gap to "exemplary, no DD asks" requires three operator-only workstation measurements that the agent sandbox can't run. - -#### R.1 — Re-run measurements (where feasible in sandbox) - -Sandbox-runnable subset of Phase 0 commands re-executed against post-Bundle-Q HEAD: - -- Existential cluster per-package coverage: **crypto 88.2%**, **pkcs7 100%**, **local 86.7%**, **acme 55.6%**, **stepca ~90% (Bundle L.B)**. -- gopter property-based tests pass (post-Q): crypto round-trip + wrong-passphrase rejection (50 + 30 generative iters); pkcs7 ASN.1 length round-trip (500 iters). -- YAML lint clean on `.github/workflows/ci.yml`. - -Operator-only measurements **not run** (require workstation + Docker + ≥10GB free disk): -- `go test -race -count=10 -timeout=45m ./...` -- `go-mutesting --debug ./internal/{crypto,pkcs7,connector/issuer/local,connector/issuer/acme}/...` (avito-tech fork; upstream zimmski blocked on arm64 due to syscall.Dup2) -- `go test -tags integration ./internal/repository/postgres/...` (testcontainers + PostgreSQL 16) -- `npx vitest run --coverage` (frontend per-page coverage) - -Each is documented in `coverage-matrix.md::Post-Closure Summary` with the exact command + rationale. - -#### R.2 — coverage-matrix.md Post-Closure Summary appended - -New section appended to `coverage-audit-2026-04-27/coverage-matrix.md` enumerating per-cluster coverage at post-Bundle-Q HEAD: 20 rows covering Existential / High / Medium / Low / Frontend / Mutation / Race / Repo-integration. Each row shows pre-audit → post-Q values + acquisition target + met/partial/operator-only status. - -#### R.3 — findings.yaml confirmation pass - -All 33 audit findings now have `closed` (or partial-closed with documented rationale + tracked-extension) status. Numeric tally: -- C-001..C-008: closed (8) -- H-001..H-009: closed or partial (9, with H-002 SSH-Connect tracked as M.SSH-extended, H-005/H-006/H-009 closed via Phase 0 measurements) -- M-001..M-012: closed or partial (12, with M-001 / M-002 / M-003 tracked as N.A/N.B/N.C-extended for follow-on bundles, M-008 tracked as P.2-extended) -- L-001..L-004: closed via Bundle Q (4) - -#### R.4 — acquisition-readiness.md final score - -`acquisition-readiness.md` gets a closure-status header + final score. **4.3 / 5** — passing tech DD clean. The path to 5.0 requires the four operator-only measurements (race / mutation / repo-integration / frontend coverage); each documented with exact command in the closure header. - -#### R.5 — CI threshold raise checkpoint #3 - -`.github/workflows/ci.yml` Existential-cluster floors lifted (defensible against post-Q HEAD measurements): - -- `internal/crypto/`: 85 → **88** (HEAD 88.2%; prescribed 92 deferred — needs interface seams for `rand.Reader` / `aes.NewCipher` failure branches; tracked R-CI-extended) -- `internal/connector/issuer/local/`: 85 → **86** (HEAD 86.7%; prescribed 92 deferred — same) -- `internal/pkcs7/`: 100% — informational gate retained (global-run measurement artifact; package-scoped 100% locked in via Bundle 7 fuzz targets) - -The prescribed +7pp jumps from the Bundle R prompt are not applied because the actual post-Q measurements don't support them. Tracked as **R-CI-extended**: needs ~200-400 LoC of `crypto/rand` interface plumbing + `aes` factory injection to make platform-failure branches testable. Out of session budget. - -#### R.6 — Workspace doc updates (no tag from agent) - -- `cowork/CLAUDE.md::Active Focus` updated: 2026-04-27 audit status flipped to CLOSED with operator-measurement gates noted; v2.1.0 gate language untouched (the audit closure ships independently). -- `coverage-audit-closure-plan.md` ticks Bundle R `[x]` with per-item breakdown. -- **No `git tag` from the agent.** The operator pushes the tag (typically v2.0.60 or v2.1.0) once they've run the four workstation measurements and confirmed green. - -#### R.7 — Audit folder archive marker - -- `coverage-report.md` gets a STATUS: CLOSED header at the top with all-bundles enumeration. -- `acquisition-readiness.md` gets a closure-status header with final score + path-to-5.0 documentation. -- Future audits start a new dated folder; `coverage-audit-2026-04-27/` is preserved as historical record. - -#### Verification - -- `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))"` clean. -- All Existential cluster coverage measurements run in-sandbox confirm the new floors are met with margin. -- `git diff --stat` against pre-Bundle-R: 6 files changed. - -### Bundle Q (Coverage Audit Closure — Property-Based Pilot + Hygiene): L-001 + L-002 + L-003 + L-004 + I-001 closed - -> Five small closures: cmd/cli round-out (7.1% → 63.5%), awssm round-out (78.2% → 96.0%), gopter property-based pilot, multi-agent architecture diagram update, and informational test-naming CI guard. All Low-tier and Info-tier audit findings now closed. - -#### Q.1 — cmd/cli dispatch coverage (L-001 closed) - -`cmd/cli/dispatch_test.go` adds ~30 dispatch tests covering every arm in `handleCerts`, `handleAgents`, `handleJobs`, `handleImport`, `handleStatus`. Strategy: `httptest.NewTLSServer` mocks the API; `cli.NewClient(server.URL, "test-key", "json", "", true)` constructs an insecure-skip-verify client to skip cert chain validation. Each test pins both the "missing-args usage print" path (returns nil) and the "happy path delegation" path (asserts request method + URL substring). Result: cmd/cli line coverage jumps **7.1% → 63.5%** — well above the ≥30% gate. - -#### Q.2 — awssm round-out (L-002 closed) - -`internal/connector/discovery/awssm/awssm_edge_test.go` rounds out the previously-uncovered paths: `New()` (real-client construction, nil cfg, nil logger), `extractKeyInfo` (ECDSA / Ed25519 / unknown — was RSA-only), `processSecret` filter arms (NamePrefix mismatch, TagFilter mismatch, empty-value short-circuit, GetSecretValue error propagation), `realSMClient` stub-contract pin (ListSecrets / GetSecretValue / NewRealSMClient — pins the documented "stub returns empty + nil" contract so a future SDK wire-up doesn't silently break callers), and `buildDiscoveredCertEntry` EmailAddresses → SAN extraction. Result: awssm coverage jumps **78.2% → 96.0%** — well above the ≥85% gate. - -#### Q.3 — Property-based testing pilot (L-003 closed) - -`gopter@v0.2.11` added to `go.mod`. Two property-based test files shipped: - -- `internal/crypto/encryption_property_test.go` — two properties: round-trip (`DecryptIfKeySet(EncryptIfKeySet(x, k), k) == x` for any plaintext + non-empty passphrase) and wrong-passphrase rejection (`DecryptIfKeySet(blob, wrongKey)` never returns nil error AND non-empty plaintext that bytes-equals the original). 50 + 30 successful test budgets — full PBKDF2 600k rounds × 50 iters ≈ 15s on -race CI. Skipped under `-short` to keep developer-loop fast. -- `internal/pkcs7/length_property_test.go` — three properties on `ASN1EncodeLength`: round-trip (`decodeLength(encode(x)) == x` for x ∈ [0, 2³¹−1]; decoder defined inline since production code only needs the encoder); short-form structural invariant (length < 128 produces 1 byte equal to length); long-form structural invariant (length ≥ 128 produces output[0] with high bit set + N = first byte & 0x7f indicating remainder length). 500 successful tests in <10ms. - -Strategy is "pilot" — one working property test per pattern. Full adoption (FSM transitions, more parsers, etc.) is post-Q backlog; gopter is non-blocking in CI for now. - -#### Q.4 — Architecture diagram multi-agent update (L-004 closed) - -`docs/qa-test-guide.md::Architecture` ASCII diagram updated to show "certctl-agent (×N)" + a callout explaining seed_demo.sql provisions 12 agent rows (1 active container, 2 retired, 9 reserved/sentinel) for Parts 04, 05, 55 + FSM coverage. Strengthening #7 from `qa-doc-strengthening.md` applied. Operators running parallel-agent topologies guided to set `AGENT_COUNT=N` and re-derive seed counts via `make qa-stats`. - -#### Q.5 — Test-naming CI guard (I-001 closed) - -`.github/workflows/ci.yml` Test-naming convention guard added after the QA-doc seed-count drift guard. Greps for `func Test(` patterns missing the `_` suffix; prints first 20 non-conformant tests as `::warning::` annotations. **Informational** (`continue-on-error: true`) — does not fail the build. Promotion to hard-fail tracked as I-001-extended once the team adopts the convention repo-wide. Excludes `TestMain` (Go's special init hook) and `TestProperty_*` (gopter naming convention from Q.3). - -#### Verification - -- `python3 -c "import yaml; yaml.safe_load(...)"` clean on ci.yml. -- `go vet ./cmd/cli/... ./internal/connector/discovery/awssm/... ./internal/crypto/... ./internal/pkcs7/...` clean. -- `go test -short -count=1` clean across all four packages. -- `go test -count=1 -timeout=60s ./internal/crypto/... ./internal/pkcs7/...` (no `-short`) PASSes both property-test packages — crypto in 15.4s (50 + 30 × 600k PBKDF2 rounds), pkcs7 in 5ms. - -Audit deliverables: gap-backlog.md strikethroughs L-001 / L-002 / L-003 / L-004 / I-001 with per-finding closure note. closure-plan.md flips Bundle Q `[x]` with per-item breakdown. - -### Bundle P (Coverage Audit Closure — QA Doc Strengthening): M-007 + M-009 + M-010 + M-011 + M-012 closed; M-008 deferred - -> Six structural strengthenings applied to the QA documentation surface, raising acquisition-readiness QA-doc score 4.0 → 4.7. M-008 (per-RFC test-vector subsections under Parts 21 + 24) deferred as "Bundle P.2-extended" — out of session budget. - -#### P.1 — `make qa-stats` single-source-of-truth (M-012 closed) - -New `qa-stats` PHONY target in `Makefile` emits 14 metrics that every count claim in `docs/qa-test-guide.md` and `docs/testing-guide.md` is derived from: backend test files / Test functions / `t.Run` subtests, frontend test files, fuzz targets, `t.Skip` sites, `qa_test.go` Part_ subtests, `testing-guide.md` Parts, and unique seed IDs (mc-* / ag-* / iss-* / tgt-* / nst-*). Iterated the seed-count regex (initial `sed`/`awk` produced wrong totals for greedy ranges) to a `grep -oE '-[a-z0-9_-]+' | sort -u | wc -l` form that produces deterministic unique-ID counts. Output emits at HEAD: 221 backend test files, 2454 Test functions, 778 t.Run subtests, 38 frontend test files, 11 fuzz targets, 60 t.Skip sites, 53 Part_ subtests, 56 testing-guide.md Parts, 32 mc-* / 14 ag-* / 18 iss-* / 8 tgt-* / 4 nst-* seed IDs. - -#### P.2 — CI drift guards (M-011 closed) - -Two new CI steps added to `.github/workflows/ci.yml` after the coverage upload: - -- **QA-doc Part-count drift guard:** extracts the "49 of N Parts" claim from `qa-test-guide.md`, compares to `^## Part N:` header count in `testing-guide.md`. Fails CI if mismatch. -- **QA-doc seed-count drift guard:** extracts "### Certificates (N total" + "### Issuers (N total" from `qa-test-guide.md`, compares to `mc-*` and `iss-*` unique-ID counts in `seed_demo.sql` with ≤5pp slack on issuers (issuer rows ≠ unique iss-* IDs because seed_demo.sql also uses iss-* prefix elsewhere). - -Both guards validated locally — pass at HEAD (56==56 Parts, 32==32 certs, 18 issuer IDs within 5pp slack of 13 issuer rows). YAML lint clean. - -#### P.3 — Test Suite Health dashboard at top of `qa-test-guide.md` (Strengthening #7) - -Single-page snapshot at the top of the file: file/function/subtest counts, fuzz/skip counts, frontend test count, last-coverage-audit date + status, last-mutation-run date + status, race-detector status, repository-integration test status. Pulls from `make qa-stats` output to keep counts at HEAD. Designed for first-look auditor / acquirer / new-engineer scanning. - -#### P.4 — Coverage by Risk Class table (M-007 closed) - -After the Coverage Map section in `qa-test-guide.md`: 6-row table (Existential / High / Medium / Low / Frontend / Compliance) × Parts × automation status. Cross-references each risk class to the corresponding `coverage-matrix.md` row. Replaces the prior implicit "everything is everything" framing with explicit per-risk-class coverage targets and the gates each must meet. - -#### P.5 — Release Day Sign-Off Matrix (M-010 closed) - -12-row release-readiness checklist in `qa-test-guide.md`: backend race-clean, fuzz seed-corpus regression, frontend Vitest green, CI drift guards green, mutation-test (sample) ≥ kill-rate floor, etc. Each row cites the verification command and the gate value. Sign-off is "all 12 green" — produces a per-release artifact attached to the tag. - -#### P.6 — Mutation Testing Targets (Strengthening #5) - -New section in `qa-test-guide.md` cataloging 8 packages × kill-rate target × tool, with operator runbook. Cites the avito-tech `go-mutesting` fork (the upstream `zimmski/go-mutesting` is sandbox-blocked on arm64 due to a `syscall.Dup2` reference). Targets aligned to risk class: Existential ≥85%, High ≥75%, others tracked-not-gated. - -#### P.7 — Per-Connector Failure-Mode Matrix (M-009 closed, condensed) - -New `Part 9.0 Per-Connector Failure-Mode Matrix` in `docs/testing-guide.md`: 12 issuers × 8 failure modes (auth-fail / 403 / 429+Retry-After / 5xx / malformed / DNS-failure / partial-response / timeout) = 96 cells with ✓/△/MISSING + Bundle citations (J/L/M/N). Notable gaps highlighted explicitly: 429+Retry-After missing for cloud-managed connectors, DNS-failure missing across the board, partial-response missing for non-ACME / non-StepCA connectors. Each gap is a candidate for a follow-on bundle. - -#### Deferred — M-008 (per-RFC test-vector subsections, Parts 21 + 24) - -Out of session budget. The two parts in question are: -- **Part 21** (Subject Alternative Name & EKU): would need RFC 5280 §4.2.1.6 / §4.2.1.12 test vectors — IPv4/IPv6 SAN encoding, OtherName, BMPString edge cases. -- **Part 24** (OCSP/CRL): would need RFC 6960 vector subsections — `tryLater` response, signed-by-delegated-responder vs by-CA, CRL with `idp` extension. - -Tracked as "Bundle P.2-extended". Each subsection is ~30-50 lines of structured test-vector callouts; total ≈100-150 LoC of doc work. Not gating acquisition-readiness — the acceptance gates (race-clean / coverage / mutation-kill) still hold without them; they sharpen the conformance story for an auditor. - -#### Verification - -- `make qa-stats` runs to completion, emits 14 lines, all integers parse cleanly. -- `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))"` clean. -- Both CI drift guards executed locally — both PASS at HEAD. -- `git diff --stat` against pre-Bundle-P: 4 files changed, +195 / -1. - -Audit deliverables: `gap-backlog.md` strikethroughs M-007 / M-010 / M-011 / M-012, partial-strike on M-009 (matrix shipped, deeper per-connector failure-mode test files are follow-on Bundle work tracked under M-009-extended), deferred-marker on M-008 (Bundle P.2-extended). Closure-log entry covers all 6 shipped strengthenings + the M-008 deferral. `closure-plan.md` ticks Bundle P `[x]` with per-item breakdown. - -### Bundle O (Coverage Audit Closure — Test Hygiene + FSM Coverage): M-004 + M-005 + M-006 closed - -> Three deliverables shipped: t.Skip rationale audit (~M-004~ closed; 0 orphans), fuzz target additions (~M-005~ closed; 9 → 11 targets), and FSM transition coverage tables (~M-006~ closed; all 5 FSMs catalogued). - -#### O.1 — t.Skip rationale audit (M-004 closed) - -Inventoried all `t.Skip` sites in the repo: **65 total** (audit-time estimate was 41; count grew via Bundle 0.7's keymem tests adding ~10 OS/root-permission skips and Bundle M.Cloud's tests adding a handful). Every site carries a valid rationale — none are orphan. - -Skip categories at HEAD: -- **OS-specific** (~30 sites): `permission semantics differ on windows`, `powershell.exe not available (non-Windows)`, `chmod-error branch is only reliably triggerable on linux via /sys` -- **Root-only constraint** (~5 sites): `running as root; cannot revoke parent dir write permission` -- **External dependency** (~15 sites): `Requires Docker socket`, `integration test requires PostgreSQL`, `Requires browser — manual test`, `Requires live Vault server`, `Requires DigiCert sandbox`, `Requires CA cert+key setup`, `Requires ACME CA with ARI support` -- **Manual-test markers** (4 sites — Bundle I additions): `Part 23 (S/MIME & EKU)`, `Part 24 (OCSP/CRL)`, `Part 55 (Agent Soft-Retirement)`, `Part 56 (Notification Retry/Dead-Letter)` -- **`-short` mode** (~6 sites): `skipping integration test in short mode` -- **State-dependent** (~5 sites): `agent not yet online`, `no certificate in Active state for renewal test`, `no discovered certificates yet (agent scan may not have run)` - -All class (a) per Bundle O's classification (still-valid rationale). No edits required. Bundle O documents the audit; future regressions are caught by the existing `M-009` CI guard pattern (any new `t.Skip` site without a comment fails CI). - -#### O.2 — Fuzz target audit (M-005 closed) - -Pre-Bundle: 9 fuzz targets. Bundle O adds 2 more, lifting to **11 total**. - -- `internal/config/config_fuzz_test.go::FuzzParseNamedAPIKeys` — pins the `CERTCTL_API_KEYS_NAMED` env-var parser added in Bundle G / L-004 (dual-key rotation primitive). Hand-rolled colon/comma split — exactly the kind of code path that benefits from fuzz coverage. 16 seed inputs covering happy-path (`alice:KEY1:admin`), dual-key rotation (`alice:OLD:admin,alice:NEW:admin`), degenerate (`""`, `":"`, `"name:"`, `:key`), whitespace-padded, wrong-case admin flag, 4-segment input (rejected), adversarial chars in name (`al/ice`, `al ice`, `alice@host`), long inputs. -- `internal/validation/command_fuzz_test.go::FuzzSanitizeForShell` — pins the POSIX shell-quote helper. Asserts no panic + output begins+ends with single-quote. 17 seed inputs covering plain, whitespace, embedded quotes / backticks / dollars, newlines, NULs, shell-metachar injections, unicode, 100×`'` stress, 10000×`a` length stress. - -Verification: `go vet ./internal/config/... ./internal/validation/...` clean; `go test -short -count=1 ./internal/config/... ./internal/validation/...` PASS; total fuzz-target count: `grep -rE 'func Fuzz[A-Z]' --include='*_test.go' internal/ | wc -l` == **11**. - -#### O.3 — FSM transition coverage tables (M-006 closed) - -New file `coverage-audit-2026-04-27/tables/fsm-coverage.md` — comprehensive enumeration of all 5 FSMs in certctl with per-transition test coverage. Sourced from `internal/domain/*.go::*Status*` const blocks and writers in `internal/service/*.go`. - -| FSM | States | Legal cov | Illegal cov | Risk class | Acquisition gate met? | -|---|---|---|---|---|---| -| **Job** | Pending → AwaitingCSR → AwaitingApproval → Running → Completed/Failed/Cancelled (+ retry) | 12/13 (92%) | 7/7 (100%) | Existential | ✓ | -| **Certificate** | Pending → Active → Expiring → RenewalInProgress → Active/Failed; Active → Revoked; (any) → Archived | 13/14 (93%) | 6/6 (100%) | Existential | ✓ | -| **Agent** | Online ↔ Offline; (either) → Degraded; (any) → Retired | 6/8 (75%) | 1/1 (100%) | High | △ Degraded gap | -| **Notification** | pending → sent/failed; failed → pending/dead; sent → read | 6/7 (86%) | 3/3 (100%) | Medium | ✓ | -| **Health-check** | unknown → healthy/degraded/down/cert_mismatch (recompute-on-tick) | 7/7 (100%) | n/a | Medium | ✓ | - -**4 of 5 FSMs meet** the Bundle O exit gate (≥80% legal + 100% illegal on Existential). Agent's Degraded transitions are the lone small gap; tracked as `M-006-extended`. The doc enables a future CI drift guard: when `internal/domain/*.go` adds a new `*Status*` constant, this table must grow with a corresponding row. - -Audit deliverables: `findings.yaml` doesn't have separate -0xxx entries for M-004/M-005/M-006 (they're table rows in `gap-backlog.md`); strikethroughs applied + Bundle O closure-log entry covering all three sub-deliverables; `closure-plan.md` ticks Bundle O `[x]`. - -### Bundle N (Coverage Audit Closure — Mid-tier Round-Out): partial — M-001 partial, M-002/M-003 deferred - -> Stubs-coverage tests shipped across 8 issuer connectors. Modest 1-3pp coverage lifts; full M-001 closure (all 9 connectors at ≥85%) requires per-CA failure-mode mock work that exceeds this session's budget. Service/handler round-out (M-002, M-003) and CI threshold raise #2 deferred until follow-on work lifts the underlying coverage. - -#### Stubs coverage (8 connectors) - -Each connector gets a `_stubs_test.go` (~50 LoC) pinning the not-supported `issuer.Connector` interface methods (`GenerateCRL`, `SignOCSPResponse`, `GetCACertPEM`, `GetRenewalInfo`). Most CAs delegate CRL/OCSP/CA-cert distribution to their managed services, so these methods are documented stubs that return errors. Pinning them ensures the stubs aren't silently replaced with no-ops in a future refactor. - -| Connector | Pre | Post | Δ | -|---|---|---|---| -| `digicert` | 79.3% | 81.0% | +1.7pp | -| `ejbca` | 75.8% | 76.5% | +0.7pp | -| `entrust` | 70.8% | 70.8% | (stubs already covered) | -| `sectigo` | 78.0% | 79.4% | +1.4pp | -| `vault` | 81.0% | **84.1%** | +3.1pp | -| `openssl` | 76.9% | 78.0% | +1.1pp | -| `googlecas` | 81.0% | **83.4%** | +2.4pp | -| `globalsign` | 75.9% | 78.2% | +2.3pp | - -`awsacmpca` not included — its 0%-coverage hotspots are stubClient methods (used when the AWS SDK isn't initialized), structurally different from the other 8 connectors' interface stubs. Already at 83.5%, near target. - -**Why the gates aren't yet met:** the stub functions are tiny (1-2 lines each, mostly `return nil, fmt.Errorf("not supported")`). Lifting each connector to ≥85% requires per-connector failure-mode test files mirroring **Bundle J's ACME pattern** (`httptest.Server` + canned 401 / 403 / 429+Retry-After / 5xx / malformed responses against the actual `IssueCertificate` / `RevokeCertificate` / `GetOrderStatus` paths). That's ~200-300 LoC × 9 connectors = ~2000-2700 LoC of bespoke per-CA mock work. Tracked as follow-on "Bundle N.A-extended / N.B-extended." - -**Deferred:** - -- **N.C (M-002 + M-003):** `internal/service` (70.5%) and `internal/api/handler` (79.4%) round-out **not yet started**. Tracked as "Bundle N.C-extended." -- **N.CI (CI threshold raise #2):** the prescribed raises (service 55→80, handler 60→80, issuer/* glob → 80) require the underlying coverage to actually be at those levels first. Service + handler are still below their proposed floors; issuer connectors average ~78% (range 70.8–84.1) below the proposed 80% floor. Raising prematurely would fail CI immediately. Tracked as "Bundle N.CI-extended" — gates raise once the follow-on bundles lift the underlying packages. - -Verification: `go vet ./internal/connector/issuer/{digicert,ejbca,entrust,sectigo,vault,openssl,googlecas,globalsign}/...` clean; `gofmt -l` clean; `go test -short -count=1` PASS for all 8 connectors. - -Audit deliverables: `gap-backlog.md::M-001` row marked partial-strikethrough with the per-connector coverage table; closure-log entry covers all four sub-batches' status; `closure-plan.md` Bundle N marked `[~]` with per-sub-batch breakdown. M-002 and M-003 row tooltip updated to reflect deferred status. - -### Bundle M.Cloud (Coverage Audit Closure — AzureKV + GCP-SM): H-004 closed - -> Closes the deferred 4th sub-batch from Bundle M. **Bundle M is now FULLY CLOSED across all 4 sub-batches.** - -| | Pre | Post | -|---|---|---| -| `internal/connector/discovery/azurekv` | 41.2% | **85.6%** (+44.4pp; +15.6 above 70% target) | -| `internal/connector/discovery/gcpsm` | 43.1% | **83.4%** (+40.3pp; +13.4 above 70% target) | - -**Engineering technique:** both Azure KV and GCP Secret Manager use hardcoded API URLs (`login.microsoftonline.com` for Azure AD, `oauth2.googleapis.com` + `secretmanager.googleapis.com` for GCP). To test these end-to-end without modifying production code, each test file ships a `rewritingTransport` — a custom `http.RoundTripper` that intercepts every outbound request and rewrites Host to point at an `httptest.Server`, while preserving Path + Query. For GCP specifically, the service-account JSON file written to `t.TempDir()` carries `token_uri` pointing at the test server (clean override path that needs no transport rewrite for the auth call itself). - -**`azurekv_failure_test.go`** (~280 LoC, 13 tests): -- `getAccessToken`: happy + cached-reuse (5-min buffer pinned via call-count assertion) + 401 + malformed JSON + empty-token + network-error -- `ListCertificates`: happy + token-failure + 5xx + malformed JSON + **multi-page pagination** (asserts both pages fetched via `nextLink`) -- `GetCertificate`: happy (round-trip with synthesized DER cert in CER field) + 404 + malformed JSON -- `New` constructor - -**`gcpsm_failure_test.go`** (~430 LoC, 19 tests): -- `loadServiceAccountKey`: happy + file-not-found + malformed JSON + bad-PEM + empty-private-key (returns saKey but nil rsaKey path) -- `getAccessToken`: happy (full JWT-bearer assertion flow) + cached-reuse + 401 + malformed JSON + empty-token + load-credentials-failure -- `ListSecrets`: happy + token-failure + 5xx + malformed JSON -- `AccessSecretVersion`: happy (base64 round-trip of payload) + 404 + bad-base64-payload -- `Name` / `Type` identity check - -Verification: `go vet` clean, `gofmt -l` clean, `staticcheck -checks all` clean (excluding pre-existing ST1005 hits in `azurekv.go` lines 148–162 — capitalized error strings predating Bundle M), `go test -short -count=1` PASS, `go test -race -count=1` PASS, 0 races. - -Audit deliverables: `findings.yaml::CRTCTL-COVAUDIT-2026-04-27-0011` flips status `open` → `closed` with full closure_note + per-connector coverage table. `gap-backlog.md` strikethroughs H-004 + adds Bundle M.Cloud closure-log entry. `coverage-matrix.md` adds two new rows for AzureKV and GCP-SM. `closure-plan.md` flips Bundle M `[~]` → `[x]` (all 4 sub-batches now closed). - -### Bundle M (Coverage Audit Closure — Connector Failure-Mode Round): 3 of 4 sub-batches - -> Closes H-001 (F5 ≥85%) and H-003 (Email ≥70%); partial-closes H-002 (SSH); defers H-004 (cloud-discovery) as scope-management. - -#### M.F5 — F5 BIG-IP iControl REST realclient (H-001 closed) - -`internal/connector/target/f5/f5_realclient_test.go` (~430 LoC, 23 tests). The existing `f5_test.go` tests the Connector via the F5Client interface using a hand-rolled mock; the realF5Client HTTP methods (~11 of them) sat at 0% coverage because the existing tests bypass HTTP entirely. Bundle M.F5 builds a `realF5Client` pointing at an `httptest.Server` returning canned iControl REST responses and exercises every method end-to-end. - -| | Pre | Post | -|---|---|---| -| `internal/connector/target/f5` overall | 44.6% | **90.1%** (+45.5pp; +5.1 above 85% target) | -| `Authenticate` | 0.0% | **100.0%** (happy + 5xx + network + malformed-JSON + empty-token) | -| `doRequest` | 0.0% | **95.2%** (incl. **401-retry** path verified end-to-end) | -| `UploadFile` | 0.0% | **100.0%** (Content-Range header asserted) | -| `InstallCert` / `InstallKey` | 0.0% | **100.0%** | -| `CreateTransaction` / `CommitTransaction` | 0.0% | **100.0%** | -| `UpdateSSLProfile` | 0.0% | **93.8%** (incl. X-F5-REST-Overriding-Collection header on transID) | -| `GetSSLProfile` / `DeleteCert` / `DeleteKey` | 0.0% | **88.9%–91.7%** | - -Plus a context-cancel test (UploadFile with 50ms timeout against a 2s server) that pins graceful cancellation. - -#### M.SSH — SSH/SFTP target connector (H-002 partial-closed) - -`internal/connector/target/ssh/ssh_realclient_test.go` (~150 LoC, 13 tests). Coverage 55.2% → **71.6%** (+16.4pp; below 85% target). - -Functions covered: `New` / `NewWithClient` / `applyDefaults` 100%; `buildAuthMethods` 100% (password / key-inline / key-from-path / file-not-found / no-key-configured / parse-failure / unsupported-method); `WriteFile` / `Execute` / `StatFile` not-connected guards 100%; `Close` idempotency 100%. - -**Why partial-closed:** `realSSHClient.Connect()` (~50 LoC including `net.DialTimeout` + `ssh.NewClientConn` + `sftp.NewClient`) cannot be exercised without a live SSH server. An embedded `golang.org/x/crypto/ssh` server fixture would be ~1000 LoC of test infrastructure (handshake, keyboard-interactive auth, channel multiplexing). Out of scope for Bundle M; tracked as a follow-on "Bundle M.SSH-extended". - -#### M.Email — Email notifier (H-003 closed) - -`internal/connector/notifier/email/email_failure_test.go` (~340 LoC, 15 tests). Coverage 39.7% → **70.5%** (+30.8pp; +0.5 above 70% target). - -Engineering technique: a hand-rolled minimal SMTP server (`net.Listen("tcp", "127.0.0.1:0")` + a goroutine that handles EHLO/AUTH/MAIL/RCPT/DATA/QUIT and writes canned 2xx/3xx/5xx responses based on a per-test `failOn` map). Real SMTP servers (Postfix, Exim, etc.) are 50K+-LoC products; this fake responds to the subset `net/smtp.Client.Mail/Rcpt/Data/Quit` actually exercises. - -Tests added: - -- **Header-injection guards (CWE-113):** `sendEmail` and `sendHTMLEmail` reject CR/LF/NUL in From/To/Subject before any SMTP I/O. Six tests pin all three field × two functions. -- **Connection refused** for both `sendEmail` and `sendHTMLEmail` (closed listener). -- **Happy paths:** `SendAlert` / `SendEvent` full SMTP transactions. -- **Server-side failures:** `SendEvent_RcptRejected` (RCPT 550 mailbox unavailable), `SendAlert_DataWriteFailure` (DATA 554 transaction failed). -- **Authentication:** `SendEmail_WithAuth` exercises the AUTH PLAIN path; `SendEmail_AuthFailure` pins the AUTH 535 wrap. - -#### M.Cloud — AzureKV + GCP-SM discovery (H-004 deferred) - -AzureKV at 41.2%, GCP-SM at 43.1%. Same approach as M.F5 (httptest.Server mocking the cloud REST API + OAuth2 token endpoint) is straightforward but the two cloud connectors together would add another ~600 LoC of tests + ~200 LoC of mock infrastructure — exceeds Bundle M's session budget. Tracked as a follow-on "Bundle M.Cloud-extended" against the same H-004 row in `findings.yaml`. - -Verification across all three sub-batches: `go vet` clean, `gofmt -l` clean, `staticcheck -checks all` clean (excluding pre-existing ST1000 hits in master), `go test -short -count=1` PASS, `go test -race -count=1` PASS, 0 races. - -Audit deliverable updates: `findings.yaml` flips `-0008` (F5) and `-0010` (Email) status `open` → `closed` with full closure_notes; `-0009` (SSH) → `partial_closed`; `-0011` (Cloud) retained as deferred. `gap-backlog.md` strikethroughs H-001 + H-003, partial-strike on H-002, deferred-marker on H-004 + Bundle M closure-log entry covering all four sub-batches. `coverage-matrix.md` adds three new rows for F5 / SSH / Email at the post-Bundle-M coverage. `closure-plan.md` ticks Bundle M `[~]` with per-sub-batch status breakdown. - -### Bundle L (Coverage Audit Closure — cmd/server + StepCA + Repo + CI raise #1) - -> Three sub-bundles + CI threshold raise. **L.B closes C-005** (StepCA 52.1% → 90.4%); **L.A defers C-003** (cmd/server needs production-code refactor before tests can move it); **L.C is operator-required** (testcontainers blocked in sandbox); **L.CI raises CI thresholds** for ACME, StepCA, and MCP based on Bundles J/L.B/K. - -#### L.B — StepCA failure-mode + JWE coverage (C-005 closed) - -`internal/connector/issuer/stepca/jwe_failure_test.go` (~580 LoC). The novel piece: a **test-side RFC 3394 AES Key Wrap implementation** that constructs a valid step-ca-shaped PBES2-HS256+A128KW + A128GCM provisioner-key JWE in-test. This unlocks hermetic round-trip testing of the four previously-0%-covered JWE/AES helpers. - -Coverage delta: - -| | Pre-Bundle-L.B | Post-Bundle-L.B | -|---|---|---| -| `internal/connector/issuer/stepca` overall | 52.1% | **90.4%** (+38.3pp; +5.4 above 85% target) | -| `decryptProvisionerKey` | 0.0% | **89.7%** | -| `aesKeyUnwrap` | 0.0% | **100.0%** | -| `jwkToECDSA` | 0.0% | **100.0%** | -| `loadProvisionerKey` | 0.0% | **76.9%** | - -Tests added (24 functions): - -- **JWE round-trip:** `TestDecryptProvisionerKey_RoundTrip` constructs a valid JWE for a known EC key + password, decrypts, and asserts every byte of the recovered private scalar D + public X/Y matches the original. Hits all four 0%-coverage functions in one test. -- **decryptProvisionerKey negative paths (10 cases):** malformed JSON, bad protected b64, malformed header JSON, unsupported alg ("RSA-OAEP"), unsupported enc ("A256CBC"), bad p2s b64, bad encrypted_key b64, bad IV b64, bad ciphertext b64, bad tag b64. -- **Wrong-password path:** confirms AES key unwrap integrity-check failure surfaces with `AES key unwrap failed` wrap. -- **aesKeyUnwrap negative paths (4 cases):** too short (<24 bytes), not multiple of 8, bad KEK size (17 bytes — invalid for AES), bad integrity check IV (all-zero ciphertext). -- **jwkToECDSA negative paths (3 cases):** unsupported curve ("secp192r1"), bad x/y/d base64. -- **jwkToECDSA all-supported curves:** P-256, P-384, P-521 round-trip. -- **loadProvisionerKey:** round-trip via `t.TempDir()` JWE fixture file + file-not-found path. -- **IssueCertificate failure modes (4 cases):** network-error (closed server), 5xx, 401 Unauthorized, 403 Forbidden. -- **RevokeCertificate failure modes (3 cases):** network-error, 5xx, 403. - -Verification: `go vet` clean; `go test -short -count=1` PASS at 90.4% coverage; `go test -race -count=1` PASS, 0 races. - -#### L.A — cmd/server startup coverage (C-003 deferred) - -cmd/server's 16.1% baseline is dominated by `main()`'s 1041-LoC startup body which is 0%-covered. The other named functions in cmd/server (`preflightSCEPChallengePassword`, `preflightEnrollmentIssuer`, `buildFinalHandler`, plus all of `tls.go`) are already at 85–100% coverage. A "test-only" bundle cannot move the headline meaningfully — it requires extracting `main()` into a testable `Run(*Config)` helper with injected dependencies, which is a production-code refactor. - -`findings.yaml::CRTCTL-COVAUDIT-2026-04-27-0003::status` flips from `open` to `deferred` with the rationale + tracked as a follow-on "Bundle L.A-extended" that combines a refactor commit with the test commit. - -#### L.C — Repository round-out (C-004 operator-required) - -Repository tests use testcontainers-go against PostgreSQL 16 Alpine; the sandbox cannot run Docker. Operator-runnable command: - -``` -go test -tags integration ./internal/repository/postgres/... -``` - -If any per-file coverage <75%, add CRUD + FK-violation + unique-constraint tests per the existing finding sketch. - -#### L.CI — CI threshold raise #1 - -`.github/workflows/ci.yml` adds three new package-coverage floors based on Bundles J / L.B / K: - -| Package | Floor | Rationale | -|---|---|---| -| `internal/connector/issuer/acme` | ≥50% | Bundle J partial-closure floor; bumps to 85 when Pebble-mock lands | -| `internal/connector/issuer/stepca` | ≥80% | Bundle L.B closure floor with 10pp margin from 90.4% | -| `internal/mcp` | ≥85% | Bundle K closure floor with 8pp margin from 93.1% | - -Each gate fails CI with a "do not lower the gate, add tests" message, matching the L-010 (`internal/connector/issuer/local`) pattern. cmd/server raise is deferred until Bundle L.A-extended lands. - -YAML validated via `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))"`. - -Audit deliverable updates: `findings.yaml` flips C-005 closed + C-003 deferred (+ retains C-004 as operator-pending); `gap-backlog.md` adds full Bundle L closure-log entry covering all four sub-bundles + updates the C-003/C-004/C-005 rows; `coverage-matrix.md` adds the post-Bundle-L.B StepCA row at 90.4%; `closure-plan.md` ticks Bundle L `[~]` with per-sub-bundle status breakdown. - -### Bundle K (Coverage Audit Closure — MCP Per-Tool Coverage): C-002 closed - -> Lifts `internal/mcp` line coverage from **28.0% → 93.1%** (+65.1pp; +8.1pp above the 85% acquisition target). Closes finding C-002 — the highest-leverage High-tier coverage gap in the audit. - -`internal/mcp/tools_per_tool_test.go` (~580 LoC) ships an in-process MCP harness using `gomcp.NewInMemoryTransports()`. Strategy: wire a server with `RegisterTools(server, client)` against a mock certctl API, then dispatch every one of the **87 registered tools** via `clientSession.CallTool(...)`. This is the first test in the package that actually exercises the closure bodies inside the `register*Tools` functions — existing tests (`tools_test.go`, `injection_regression_test.go`, `fence_guardrail_test.go`, `retire_agent_test.go`) tested the wrapper + underlying HTTP client in isolation, leaving the closure routing untested. - -Tests added (4 top-level + 174 sub-tests): - -- **`TestMCP_AllTools_HappyPath`** — dispatches all 87 tools against the mock API in "ok" mode; asserts each response carries the `--- UNTRUSTED MCP_RESPONSE START [nonce:...]` / `...END...` fence pair end-to-end (not just in isolation). 2 binary-blob tools (`certctl_get_der_crl`, `certctl_ocsp_check`) are exempted via the `noFenceTools` map — they intentionally bypass `textResult` and return a human-readable summary, matching the existing `fence_guardrail_test.go` allowlist. -- **`TestMCP_AllTools_ErrorPath`** — same 87 tools against a mock API in "5xx" mode; asserts the error path produces a fenced `MCP_ERROR` in either the err.Error() return value or in the IsError content payload. -- **`TestMCP_FenceInjectionResistance`** — 50 dispatches of `certctl_list_certificates`; asserts every per-call nonce is unique. The security property: an attacker who pre-computes a fence-break payload would succeed at most once before the nonce changes. -- **`TestMCP_FenceWithPlantedEndMarker`** — plants a literal `--- UNTRUSTED MCP_RESPONSE END [nonce:attacker-chosen]` inside the response body; asserts the real fence's nonce does NOT collide with `attacker-chosen` (RNG sanity), and the planted attacker-nonce is preserved verbatim inside the real fence (operator visibility per Bundle-3 strategy). -- **`TestMCP_RegisterTools_DispatchableToolCount`** — tool-inventory cross-check: 87 tools registered, 87 covered. If a new tool is added to `tools.go` without a corresponding `toolCase` entry, this test fails with the missing tool name. Forces every future tool into the coverage matrix. - -Per-`register*Tools`-function coverage delta: - -| Function | Pre-Bundle-K | Post-Bundle-K | -|---|---|---| -| `registerCertificateTools` | 11.2% | **84.1%** | -| `registerCRLOCSPTools` | 20.0% | **100.0%** | -| `registerIssuerTools` | 20.0% | **100.0%** | -| `registerTargetTools` | 20.0% | **100.0%** | -| `registerAgentTools` | 13.5% | **86.5%** | -| `registerJobTools` | 15.2% | **90.9%** | -| `registerPolicyTools` | 19.4% | **100.0%** | -| `registerProfileTools` | 20.0% | **100.0%** | -| `registerTeamTools` | 20.0% | **100.0%** | -| `registerOwnerTools` | 20.0% | **100.0%** | -| `registerAgentGroupTools` | 20.0% | **100.0%** | -| `registerAuditTools` | 20.0% | **100.0%** | -| `registerNotificationTools` | 17.4% | **95.7%** | -| `registerStatsTools` | 14.7% | **91.2%** | -| `registerDigestTools` | 20.0% | **100.0%** | -| `registerMetricsTools` | 20.0% | **100.0%** | -| `registerHealthTools` | 19.4% | **100.0%** | - -Verification: `go vet ./internal/mcp/...` clean; `gofmt -l` clean; `staticcheck -checks all` clean (excluding 1 pre-existing S1009 in `client.go:136` and 4 pre-existing ST1000 hits — both predate Bundle K and are out of scope per the bundle's "test-only" rule); `go test -short -cover ./internal/mcp/...` 93.1% coverage; `go test -race -count=1` PASS, 0 races. - -Audit deliverable updates: `findings.yaml::CRTCTL-COVAUDIT-2026-04-27-0002::status` open → closed with closure_note + per-function coverage table; `gap-backlog.md` strikethroughs C-002 + adds Bundle K closure-log entry; `coverage-matrix.md` adds the post-Bundle-K MCP row at 93.1%; `closure-plan.md` ticks Bundle K. - -### Bundle J (Coverage Audit Closure — ACME Existential Coverage): C-001 *partial-closed* - -> Lifts `internal/connector/issuer/acme` line coverage from **41.8% → 55.6%** (+13.8pp) by pinning every failure mode the audit's gap-backlog explicitly listed under C-001. Hermetic — every test uses `httptest.Server` (no Let's Encrypt staging, no ZeroSSL sandbox, no Pebble). Closes the failure-mode dimension of C-001; the residual ≥85%-target gap is documented as a follow-on Pebble-style mock bundle. - -`internal/connector/issuer/acme/acme_failure_test.go` (~700 LoC, 23 new test functions). Notable: - -- **EAB auto-fetch failure modes:** network-error (closed server), malformed-JSON, 5xx, 401, `success=false` with upstream message preserved. Plus an `ensureClient` integration test confirming the auto-fetch failure propagates with a `auto-fetch ZeroSSL EAB credentials` wrap. -- **ARI failure modes:** directory-unreachable (fallback URL exercised), ARI 5xx, ARI 404 (returns `nil, nil` short-circuit per RFC 9773 — CA doesn't support ARI), ARI malformed JSON, ARI empty `suggestedWindow` (RFC 9773 §4.1 invariant violation), directory-malformed-JSON falls back to `constructARIURLFallback`, invalid-cert-PEM (cert-ID computation failure), and a happy-path with non-zero suggestedWindow + explanationURL. -- **Profile-order failure modes:** directory discovery failure on the JWS-POST branch (profile-set path); empty-profile fast-path delegates to `client.AuthorizeOrder`. -- **fetchNonce:** no-URL, missing Replay-Nonce header, network-error, happy-path. -- **Always-error V1 paths:** `RevokeCertificate` (DER-not-supplied), `GenerateCRL`, `SignOCSPResponse`, `GetCACertPEM`. -- **ensureClient propagation:** `IssueCertificate`, `RenewCertificate`, `GetOrderStatus` all surface `ACME client init` wrap when ensureClient fails (e.g. EAB decode error). -- **Challenge handler** (HTTP-01): known-token serves the keyAuth, unknown-token returns 404; exercised via `httptest.Server` (port-binding-free). -- **`presentPersistRecord`:** no-solver short-circuit + DNSSolver fallback (when the solver is not a `*ScriptDNSSolver`). -- **Defense-in-depth:** error-message path scanned for HMAC key bytes — pins that wrapped errors don't leak the decoded HMAC scalar. - -Engineering technique: a `preWiredConnector` test fixture pre-sets `c.client` and `c.accountKey` so calls into `ensureClient` short-circuit (the `if c.client != nil { return nil }` early return). This lets tests exercise post-init code paths (ARI, profile, revoke, getOrderStatus) without standing up a full ACME registration mock. - -Per-function coverage delta: - -| Function | Pre-Bundle-J | Post-Bundle-J | -|---|---|---| -| `GetRenewalInfo` | 11.4% | **91.4%** | -| `getARIEndpoint` | 0.0% | **82.4%** | -| `computeARICertID` | 50.0% | **100.0%** | -| `RenewCertificate` | 0.0% | **100.0%** | -| `RevokeCertificate` | 0.0% | **80.0%** | -| `presentPersistRecord` | 0.0% | **80.0%** | -| `fetchZeroSSLEAB` | 80.8% | **88.5%** | -| `fetchNonce` | 78.6% | **92.9%** | -| `ensureClient` | 79.3% | **86.2%** | -| `GetOrderStatus` | 0.0% | 37.5% | -| `IssueCertificate` | 0.0% | 6.4% (entry-error only; full flow requires Pebble-mock) | -| `solveAuthorizations*` | 0.0% | 0.0% (Pebble-mock required) | -| `authorizeOrderWithProfile` | 19.1% | 21.3% (only Discover-fail branch reached) | - -Verification: `go vet ./internal/connector/issuer/acme/...` clean; `gofmt -l` clean; `staticcheck` clean; `go test -short -timeout=60s ./internal/connector/issuer/acme/...` PASS, no flakes. - -**Why partial:** the residual ~30pp gap to the ≥85% target lives entirely in `IssueCertificate` (~115 LoC) + `solveAuthorizations[HTTP01|DNS01|DNSPersist01]` (~280 LoC) + `authorizeOrderWithProfile`'s JWS-POST branch — all of which require an in-process ACME server that handles JWS-signed POST validation, the nonce dance, full newAccount registration, newOrder, authorization polling, finalize, and cert delivery. That's ~300-500 LoC of mock infrastructure plus ~500 LoC of test cases — the prompt scoped Bundle J at 4 engineer-days but a Pebble-from-scratch is realistically 6-8 days when the JWS validation is built up properly. C-001's `findings.yaml::status` flips from `open` → `partial_closed`; the remaining work is tracked as a follow-on "Bundle J-extended." - -Audit deliverable updates: `findings.yaml::CRTCTL-COVAUDIT-2026-04-27-0001::status` open → partial_closed with closure_note + per-function coverage table; `gap-backlog.md` adds Bundle J closure-log entry + updates the C-001 row to `Partial-closed`; `coverage-matrix.md` ACME row 41.8% → 55.6%; `closure-plan.md` Bundle J checkbox marked `[~]` (partial) with achieved-vs-remaining breakdown. - -### Bundle I (Coverage Audit Closure — QA Doc Cleanup): H-007 + H-008 closed - -> Applied Patches 1–7 from `coverage-audit-2026-04-27/tables/qa-doc-patches.md` to bring `docs/qa-test-guide.md` and `deploy/test/qa_test.go` back in sync with the code at HEAD. Acquisition-readiness QA-doc score lifts 2.5 → 4.0. - -`docs/qa-test-guide.md` updates: - -- **Patch 1 — Headline.** "covers all 54 Parts" → "49 of 56 Parts" + 4-not-yet-automated callout (Parts 23, 24, 55, 56). -- **Patch 2 — Totals line.** Replaced the static "~164 automated subtests" prose with a verified-2026-04-27 breakdown + recompute commands so the line stops drifting on every release. -- **Patch 3 — Coverage Map.** Added rows for Parts 23 (S/MIME & EKU), 24 (OCSP/CRL), 55 (Agent Soft-Retirement), 56 (Notification Retry & Dead-Letter) — each annotated "0 (NOT AUTOMATED)" with a `docs/testing-guide.md::Part N` pointer. -- **Patch 4 — What This Test Does NOT Cover.** New "Not Yet Automated (Parts 23, 24, 55, 56)" subsection enumerating the gaps and their manual-test rationale. -- **Patch 5 — Seed Data Reference.** Re-anchored against authoritative HEAD `migrations/seed_demo.sql` counts: **32 certs (already correct), 12 agents (was 9 — 8 named ag-* + server-scanner sentinel + 3 cloud-discovery sentinels), 13 issuers (was 9), 8 targets (already correct), 4 network scan targets (already correct).** Replaced narrow ID enumerations with `sed | grep` recompute commands so future seed additions don't silently drift the doc. Added a maintenance-note pointer to the proposed CI guard (Strengthening #6). Bundle I's Phase 0 recon discovered the original patch's anticipated counts (66 certs, 18 agents) were themselves drifted — the patch's recompute commands used overbroad regex that matched mc-* IDs across non-managed-certificates tables; corrected on the fly. -- **Patch 6 — Version History.** Added v1.2 entry citing Parts 55–56 documentation and Parts 23–24 not-yet-automated surfacing. -- Bonus fix: the integration_test comparison row "32 certs, 8 agents" → "32 certs, 12 agents, 13 issuers, 8 targets, realistic history". - -`deploy/test/qa_test.go` updates (Patch 7): - -- 4 new `t.Run("PartN_*", …)` blocks for Parts 23, 24, 55, 56. Each calls `t.Skip` with a `docs/testing-guide.md::Part N` pointer + automation-candidates list. The Skip-with-rationale form keeps Part numbering consistent in test output, makes the manual-test pointer machine-readable, and surfaces the gap to maintainers. Replacing each Skip with a real test body is gap-backlog work; this commit only closes the doc-vs-test drift. - -Verification gates met: -- `grep -cE '^## Part [0-9]+:' docs/testing-guide.md` == 56 ✓ -- `grep -cE 't\.Run\("Part[0-9]+_' deploy/test/qa_test.go` == 53 ✓ (49 live + 4 new Skip stubs) -- `go vet -tags qa ./deploy/test/...` clean -- `go test -tags qa -run='__nope__' ./deploy/test/...` PASS (compile) -- The full `go test -tags qa -run='TestQA/Part(23|24|55|56)' -v` SKIP-grep gate requires the live demo stack and is operator-runnable; the test bodies trivially `t.Skip` when reached. - -Audit deliverable updates: `findings.yaml` flips H-007 (`-0014`) and H-008 (`-0015`) status `open` → `closed` with closure_note + corrected counts; `gap-backlog.md` strikethroughs both rows + adds Bundle I closure-log entry; `tables/qa-doc-drift.md` gains a "PATCHES APPLIED 2026-04-27" header marker (preserved as audit-time snapshot, not retro-edited); `acquisition-readiness.md` "QA documentation rigor" criterion: 2.5 → 4.0; `coverage-audit-closure-plan.md` checklist ticks Bundle I. - -### Bundle 0.7 (Coverage Audit Closure): cmd/agent key-handling regression coverage — C-008 closed - -> Phase 0 of the 2026-04-27 coverage audit's closure plan triggered a halt-condition: `cmd/agent/keymem.go`'s two security-critical functions were at 0.0% / 11.1% line coverage despite being defense-in-depth for agent private-key memory hygiene (Bundle 9 / Audit L-002 + L-003 — agent edition). Bundle 0.7 was inserted before Bundle J as mandatory; this entry closes finding **C-008** (`CRTCTL-COVAUDIT-2026-04-27-0034`). - -`cmd/agent/keymem_test.go` (~510 LoC, 17 top-level test functions) ships: - -- **`marshalAgentKeyAndZeroize` regression coverage** — happy path, nil-key guard (asserts `onDER` is NOT invoked), upstream error propagation via `errors.Is`, and the **DER-buffer-zeroized-after-return invariant** verified observably: capture the slice header inside `onDER` (sharing the backing array, NOT a deep copy), then assert every byte reads `0x00` after the function returns. Pinned for both the happy path AND the `onDER`-error path. A future refactor that drops the `defer clear(der)` line would break the test even if the simpler assertions still pass. Also adds a "contract violator" defense test: a buggy caller that retains the slice past `onDER` reads zeros, not the private scalar. -- **`ensureAgentKeyDirSecure` regression coverage** — 13-row table-driven matrix covering empty/dot/root refuse with documented error wrap, create-with-0700, create-nested-0700, accept-existing-0700 (no-op short-circuit), tighten 0750/0755/0777 to 0700, accept-existing-0500/0400 (owner-only-no-write `mode&0o077 == 0` branch, no chmod), `filepath.Clean` normalization (trailing slash + dot prefix). Plus PathIsAFile (documents current behavior — function chmod's a file path silently, not a correctness bug per current call sites but a hardening candidate filed against any future refactor), Idempotent, Concurrent (`-race` clean across 8 goroutines), Stat/Mkdir/Chmod error-propagation paths (root-required ones `t.Skip` cleanly on non-root CI rather than being absent), and Format-includes-cleaned-path debuggability assertion. -- **End-to-end smoke** (`TestKeymem_AgentMainFlowSmoke`) replaying `cmd/agent/main.go`'s composition: `ensureAgentKeyDirSecure` → `marshalAgentKeyAndZeroize`. - -Coverage delta: - -| | Pre-Bundle-0.7 | Post-Bundle-0.7 | Gate | Met? | -|---|---|---|---|---| -| `cmd/agent/keymem.go::marshalAgentKeyAndZeroize` | 0.0% | **85.7%** | ≥85% | ✓ | -| `cmd/agent/keymem.go::ensureAgentKeyDirSecure` | 11.1% | **94.4%** | ≥85% | ✓ | -| `cmd/agent` overall | 54.3% | **57.7%** (+3.4pp) | (≥75% stretch) | △ partial | - -Verification: `go test -race -count=3 ./cmd/agent/...` clean (0 races); `gofmt -l` clean; `go vet ./cmd/agent/...` clean; `staticcheck ./cmd/agent/...` clean. The cmd/agent overall ≥75% stretch target is unachievable from a keymem-only test file (the package's bulk — `Run`, `main`, `executeCSRJob`, `executeDeploymentJob`, `verifyAndReportDeployment` — is unrelated to key-handling and dominates the denominator); the remaining lift is tracked as a follow-on cmd/agent flow-test bundle. - -Audit deliverable updates: `coverage-audit-2026-04-27/findings.yaml` flips C-008 `open` → `closed` with closure note + post-Bundle coverage numbers; `gap-backlog.md` adds a closure log entry and partial-closure note on H-006; `coverage-matrix.md` updates the cmd/agent row from "NOT MEASURED" to 57.7%; `coverage-report.md::Phase 0 Results` appends a Bundle 0.7 closure block with the coverage delta table and pinned-invariant list; `coverage-audit-closure-plan.md` checklist ticks Bundle 0.7. **Bundle J (ACME failure-mode coverage) unblocked.** - -### Bundle H (M-029 Drain — AUDIT FULLY CLOSED): 1 audit finding closed across 3 passes - -> Closes the last remaining open finding from the 2026-04-25 audit. **Score: 54/55 → 55/55 (100%); deferred 7/7 (100%); AUDIT CLOSED.** The M-029 frontend per-page migration backlog was framed by Bundle 8 as incremental ("closes per-PR as each page ships"); Bundle H shipped all three passes end-to-end across 9 merged commits to master rather than spread per-PR. - -#### Pass 1: useMutation → useTrackedMutation (56 sites, 6 batches) - -All 56 bare `useMutation` call sites in `web/src/` migrated to the Bundle 8 wrapper, which enforces the M-009 invalidation contract per-site via a discriminated-union type (`invalidates: QueryKey[] | 'noop'`). The wrapper invalidates BEFORE invoking the caller's onSuccess, so user code drops the redundant `qc.invalidateQueries` calls and lets the wrapper's contract become the source of truth. - -| Batch | Pages migrated | Sites | Commit | -|---|---|---|---| -| 1 | AgentsPage, CertificatesPage, DigestPage, IssuerDetailPage | 4 | `08ffbad` | -| 2 | DashboardPage, DiscoveryPage, NotificationsPage, TargetDetailPage, TargetsPage | 10 | `73c6883` | -| 3 | HealthMonitorPage, AgentGroupsPage, JobsPage | 9 | `64c6cd0` | -| 4 | OwnersPage, PoliciesPage, ProfilesPage, RenewalPoliciesPage, TeamsPage | 15 | `d5541fe` | -| 5 | IssuersPage, NetworkScanPage | 8 | `1c960ff` | -| 6 | CertificateDetailPage, OnboardingWizard | 10 | `1baefd4` | - -Total Pass 1: **56 → 0 bare `useMutation` sites**; 0 → 61 `useTrackedMutation` sites. (Pass 1's count grew net positive because some 5-mutation pages collapsed two `qc.invalidateQueries` calls into one `invalidates` array literal.) - -After Pass 1 completed, `0266f2b` tightened the `.github/workflows/ci.yml` M-009 guard from a soft-budget gate (`useMutation ≤ invalidations + 5`) to a hard-zero invariant: any bare `useMutation` call in `web/src/` outside `web/src/hooks/useTrackedMutation.ts` (the wrapper itself) fails CI immediately. Strictly stronger than the prior +5 budget; failure mode also improves — operators get the exact `file:line` of the offending bare call instead of a count delta. - -#### Pass 2: useState pagination → useListParams (1 site, 1 commit) - -Bundle 8's recon estimate of ~14 list pages turned out to be wrong: **only `CertificatesPage` had real UI-driven pagination state** (`setPage`/`setPerPage` with 7 filter `useState` hooks). Most other pages either fetch filter-dropdown sidecars with hardcoded `per_page` (not pagination) or were already using `useSearchParams` directly. - -`99f52a6` collapses CertificatesPage's 9 useState hooks (statusFilter, envFilter, issuerFilter, ownerFilter, profileFilter, teamFilter, expiresBefore, sortBy, page, perPage) into a single `useListParams({ pageSize: 50 })` call. Effect: - -- All 8 filter onChange handlers now call `setFilter('', value)`. -- `setFilter` automatically resets page to 1 on every filter / sort change, so the manual `setPage(1)` calls at three sites (team / expires_before / sort) are no longer needed — the F-1 contract is now hook-enforced. -- Pagination handler simplified: `onPerPageChange: setPageSize` (the hook drops the page param from the URL when pageSize changes). -- All filter / sort / pagination state is now URL-resident (`?filter[status]=Active&page=2&page_size=50`) — deep-link + browser-back correct. - -The existing CertificatesPage.test.tsx F-1 contract tests (5 cases: getCertificates params for team_id, expires_before, sort, plus page-reset on filter and per_page change) all continue to pass against the new shape. - -#### Pass 3: Per-page render + XSS-hardening test files for the 14 T-1-deferred pages (3 batches) - -Each new test: - -- Renders the page with mock data containing `` payloads in every text-rendering field. -- Asserts `document.querySelectorAll('script[data-xss=""]')` is empty post-render. -- Asserts `window.__xss_pwned__` stays undefined (no global side-effect from the script body). -- Asserts `document.body.textContent` contains the literal `