diff --git a/.docker/README.md b/.docker/README.md index c92cee89..07e28903 100644 --- a/.docker/README.md +++ b/.docker/README.md @@ -94,7 +94,7 @@ Configure the application via `docker-compose.yml`: | `CHARON_ENV` | `production` | Set to `development` for verbose logging (`CPM_ENV` supported for backward compatibility). | | `CHARON_HTTP_PORT` | `8080` | Port for the Web UI (`CPM_HTTP_PORT` supported for backward compatibility). | | `CHARON_DB_PATH` | `/app/data/charon.db` | Path to the SQLite database (`CPM_DB_PATH` supported for backward compatibility). | -| `CHARON_CADDY_ADMIN_API` | `http://localhost:2019` | Internal URL for Caddy API (`CPM_CADDY_ADMIN_API` supported for backward compatibility). | +| `CHARON_CADDY_ADMIN_API` | `http://localhost:2019` | Internal URL for Caddy API (`CPM_CADDY_ADMIN_API` supported for backward compatibility). Must resolve to an internal allowlisted host on port `2019`. | | `CHARON_CADDY_CONFIG_ROOT` | `/config` | Path to Caddy autosave configuration directory. | | `CHARON_CADDY_LOG_DIR` | `/var/log/caddy` | Directory for Caddy access logs. | | `CHARON_CROWDSEC_LOG_DIR` | `/var/log/crowdsec` | Directory for CrowdSec logs. | @@ -218,6 +218,8 @@ environment: - CPM_CADDY_ADMIN_API=http://your-caddy-host:2019 ``` +If using a non-localhost internal hostname, add it to `CHARON_SSRF_INTERNAL_HOST_ALLOWLIST`. + **Warning**: Charon will replace Caddy's entire configuration. Backup first! ## Performance Tuning diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 78127bdc..b48f855e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -113,7 +113,7 @@ repos: stages: [manual] # Only runs when explicitly called - id: frontend-type-check name: Frontend TypeScript Check - entry: bash -c 'cd frontend && npm run type-check' + entry: bash -c 'cd frontend && npx tsc --noEmit' language: system files: '^frontend/.*\.(ts|tsx)$' pass_filenames: false diff --git a/.version b/.version index 96fb87f8..3a7f17e4 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -v0.19.0 +v0.19.1 diff --git a/Dockerfile b/Dockerfile index 3f790457..d5088a2a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ ARG BUILD_DEBUG=0 ARG CADDY_VERSION=2.11.0-beta.2 ARG CADDY_CANDIDATE_VERSION=2.11.1 ARG CADDY_USE_CANDIDATE=0 -ARG CADDY_PATCH_SCENARIO=A +ARG CADDY_PATCH_SCENARIO=B ## When an official caddy image tag isn't available on the host, use a ## plain Alpine base image and overwrite its caddy binary with our ## xcaddy-built binary in the later COPY step. This avoids relying on @@ -252,6 +252,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ # renovate: datasource=go depName=github.com/hslatman/ipstore go get github.com/hslatman/ipstore@v0.4.0; \ if [ "${CADDY_PATCH_SCENARIO}" = "A" ]; then \ + # Rollback scenario: keep explicit nebula pin if upstream compatibility regresses. # NOTE: smallstep/certificates (pulled by caddy-security stack) currently # uses legacy nebula APIs removed in nebula v1.10+, which causes compile # failures in authority/provisioner. Keep this pinned to a known-compatible @@ -259,6 +260,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ # renovate: datasource=go depName=github.com/slackhq/nebula go get github.com/slackhq/nebula@v1.9.7; \ elif [ "${CADDY_PATCH_SCENARIO}" = "B" ] || [ "${CADDY_PATCH_SCENARIO}" = "C" ]; then \ + # Default PR-2 posture: retire explicit nebula pin and use upstream resolution. echo "Skipping nebula pin for scenario ${CADDY_PATCH_SCENARIO}"; \ else \ echo "Unsupported CADDY_PATCH_SCENARIO=${CADDY_PATCH_SCENARIO}"; \ diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 1e2f9520..a6809456 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -7,6 +7,8 @@ import ( "path/filepath" "strconv" "strings" + + "github.com/Wikid82/charon/backend/internal/security" ) // Config captures runtime configuration sourced from environment variables. @@ -106,6 +108,17 @@ func Load() (Config, error) { Debug: getEnvAny("false", "CHARON_DEBUG", "CPM_DEBUG") == "true", } + allowedInternalHosts := security.InternalServiceHostAllowlist() + normalizedCaddyAdminURL, err := security.ValidateInternalServiceBaseURL( + cfg.CaddyAdminAPI, + 2019, + allowedInternalHosts, + ) + if err != nil { + return Config{}, fmt.Errorf("validate caddy admin api url: %w", err) + } + cfg.CaddyAdminAPI = normalizedCaddyAdminURL.String() + if err := os.MkdirAll(filepath.Dir(cfg.DatabasePath), 0o700); err != nil { return Config{}, fmt.Errorf("ensure data directory: %w", err) } diff --git a/backend/internal/config/config_test.go b/backend/internal/config/config_test.go index 4cbd3865..98597da7 100644 --- a/backend/internal/config/config_test.go +++ b/backend/internal/config/config_test.go @@ -258,6 +258,32 @@ func TestLoad_EmergencyConfig(t *testing.T) { assert.Equal(t, "testpass", cfg.Emergency.BasicAuthPassword) } +func TestLoad_CaddyAdminAPIValidationAndNormalization(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("CHARON_DB_PATH", filepath.Join(tempDir, "test.db")) + t.Setenv("CHARON_CADDY_CONFIG_DIR", filepath.Join(tempDir, "caddy")) + t.Setenv("CHARON_IMPORT_DIR", filepath.Join(tempDir, "imports")) + t.Setenv("CHARON_SSRF_INTERNAL_HOST_ALLOWLIST", "") + t.Setenv("CHARON_CADDY_ADMIN_API", "http://localhost:2019/config/") + + cfg, err := Load() + require.NoError(t, err) + assert.Equal(t, "http://localhost:2019", cfg.CaddyAdminAPI) +} + +func TestLoad_CaddyAdminAPIValidationRejectsNonAllowlistedHost(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("CHARON_DB_PATH", filepath.Join(tempDir, "test.db")) + t.Setenv("CHARON_CADDY_CONFIG_DIR", filepath.Join(tempDir, "caddy")) + t.Setenv("CHARON_IMPORT_DIR", filepath.Join(tempDir, "imports")) + t.Setenv("CHARON_SSRF_INTERNAL_HOST_ALLOWLIST", "") + t.Setenv("CHARON_CADDY_ADMIN_API", "http://example.com:2019") + + _, err := Load() + require.Error(t, err) + assert.Contains(t, err.Error(), "validate caddy admin api url") +} + // ============================================ // splitAndTrim Tests // ============================================ diff --git a/docs/issues/manual_test_pr2_security_posture_closure.md b/docs/issues/manual_test_pr2_security_posture_closure.md new file mode 100644 index 00000000..0aabfc3c --- /dev/null +++ b/docs/issues/manual_test_pr2_security_posture_closure.md @@ -0,0 +1,96 @@ +--- +title: "Manual Test Tracking Plan - Security Posture Closure" +labels: + - testing + - security + - caddy +priority: high +--- + +# Manual Test Tracking Plan - PR-2 Security Posture Closure + +## Scope +PR-2 only. + +This plan tracks manual verification for: +- Patch disposition decisions +- Admin API assumptions and guardrails +- Rollback checks + +Out of scope: +- PR-1 compatibility closure tasks +- PR-3 feature or UX expansion + +## Preconditions +- [ ] Branch contains PR-2 documentation and configuration changes only. +- [ ] Environment starts cleanly with default PR-2 settings. +- [ ] Tester can run container start/restart and review startup logs. + +## Track A - Patch Disposition Validation + +### TC-PR2-001 Retained patches remain retained +- [ ] Verify `expr` and `ipstore` patch decisions are documented as retained in the PR-2 security posture report. +- [ ] Confirm no conflicting PR-2 docs state these patches are retired. +- Expected result: retained/retained remains consistent across PR-2 closure docs. +- Status: [ ] Not run [ ] Pass [ ] Fail +- Notes: + +### TC-PR2-002 Nebula default retirement is clearly bounded +- [ ] Verify PR-2 report states `nebula` retirement is by default scenario switch. +- [ ] Verify rollback instruction is present and explicit. +- Expected result: reviewer can identify default posture and rollback without ambiguity. +- Status: [ ] Not run [ ] Pass [ ] Fail +- Notes: + +## Track B - Admin API Assumption Checks + +### TC-PR2-003 Internal-only admin API assumption +- [ ] Confirm PR-2 report states admin API is expected to be internal-only. +- [ ] Confirm PR-2 QA report includes admin API validation/normalization posture. +- Expected result: both reports communicate the same assumption. +- Status: [ ] Not run [ ] Pass [ ] Fail +- Notes: + +### TC-PR2-004 Invalid admin endpoint fails fast +- [ ] Start with an intentionally invalid/non-allowlisted admin API URL. +- [ ] Verify startup fails fast with clear configuration rejection behavior. +- [ ] Restore valid URL and confirm startup succeeds. +- Expected result: unsafe endpoint rejected; safe endpoint accepted. +- Status: [ ] Not run [ ] Pass [ ] Fail +- Notes: + +### TC-PR2-005 Port exposure assumption holds +- [ ] Verify deployment defaults do not publish admin API port `2019`. +- [ ] Confirm no PR-2 doc contradicts this default posture. +- Expected result: admin API remains non-published by default. +- Status: [ ] Not run [ ] Pass [ ] Fail +- Notes: + +## Track C - Rollback Safety Checks + +### TC-PR2-006 Scenario rollback switch +- [ ] Set `CADDY_PATCH_SCENARIO=A`. +- [ ] Restart and verify the rollback path is accepted by the runtime. +- [ ] Return to PR-2 default scenario and verify normal startup. +- Expected result: rollback is deterministic and reversible. +- Status: [ ] Not run [ ] Pass [ ] Fail +- Notes: + +### TC-PR2-007 QA report rollback statement alignment +- [ ] Confirm QA report and security posture report use the same rollback instruction. +- [ ] Confirm both reports remain strictly PR-2 scoped. +- Expected result: no conflicting rollback guidance; no PR-3 references. +- Status: [ ] Not run [ ] Pass [ ] Fail +- Notes: + +## Defect Log + +| ID | Test Case | Severity | Summary | Reproducible | Status | +| --- | --- | --- | --- | --- | --- | +| | | | | | | + +## Exit Criteria +- [ ] All PR-2 test cases executed. +- [ ] No unresolved critical defects. +- [ ] Patch disposition, admin API assumptions, and rollback checks are all verified. +- [ ] No PR-3 material introduced in this tracking plan. diff --git a/docs/plans/current_spec.md b/docs/plans/current_spec.md index 989da5b9..06fae334 100644 --- a/docs/plans/current_spec.md +++ b/docs/plans/current_spec.md @@ -23,6 +23,91 @@ Status: Active and authoritative Scope Type: Architecture/security/dependency research and implementation planning Authority: This is the only active authoritative plan section in this file. +## Focused Plan: GitHub Actions `setup-go` Cache Warning (`go.sum` path) + +Date: 2026-02-23 +Status: Planned +Scope: Warning-only fix for GitHub Actions cache restore message: +`Restore cache failed: Dependencies file is not found in +/home/runner/work/Charon/Charon. Supported file pattern: go.sum`. + +### Introduction + +This focused section addresses a CI warning caused by `actions/setup-go` cache +configuration assuming `go.sum` at repository root. Charon stores Go module +dependencies in `backend/go.sum`. + +### Research Findings + +Verified workflow inventory (`.github/workflows/**`): + +- All workflows using `actions/setup-go` were identified. +- Five workflows already set `cache-dependency-path: backend/go.sum`: + - `.github/workflows/codecov-upload.yml` + - `.github/workflows/quality-checks.yml` + - `.github/workflows/codeql.yml` + - `.github/workflows/benchmark.yml` + - `.github/workflows/e2e-tests-split.yml` +- Two workflows use `actions/setup-go` without cache dependency path and are + the warning source: + - `.github/workflows/caddy-compat.yml` + - `.github/workflows/release-goreleaser.yml` +- Repository check confirms only one `go.sum` exists: + - `backend/go.sum` + +### Technical Specification (Minimal Fix) + +Apply a warning-only cache path correction in both affected workflow steps: + +1. `.github/workflows/caddy-compat.yml` + - In `Set up Go` step, add: + - `cache-dependency-path: backend/go.sum` + +2. `.github/workflows/release-goreleaser.yml` + - In `Set up Go` step, add: + - `cache-dependency-path: backend/go.sum` + +No other workflow behavior, triggers, permissions, or build/test logic will be +changed. + +### Implementation Plan + +#### Phase 1 — Workflow patch + +- Update only the two targeted workflow files listed above. + +#### Phase 2 — Validation + +- Run workflow YAML validation/lint checks already used by repository CI. +- Confirm no cache restore warning appears in subsequent runs of: + - `Caddy Compatibility Gate` + - `Release (GoReleaser)` + +#### Phase 3 — Closeout + +- Mark warning remediated once both workflows execute without the missing + `go.sum` cache warning. + +### Acceptance Criteria + +1. Both targeted workflows include `cache-dependency-path: backend/go.sum` in + their `actions/setup-go` step. +2. No unrelated workflow files are modified. +3. No behavior changes beyond warning elimination. +4. CI logs for affected workflows no longer show the missing dependencies-file + warning. + +### PR Slicing Strategy + +- Decision: Single PR. +- Rationale: Two-line, warning-only correction in two workflow files with no + cross-domain behavior impact. +- Slice: + - `PR-1`: Add `cache-dependency-path` to the two `setup-go` steps and verify + workflow run logs. +- Rollback: + - Revert only these two workflow edits if unexpected cache behavior appears. + ## Focused Remediation Plan Addendum: 3 Failing Playwright Tests Date: 2026-02-23 diff --git a/docs/reports/caddy-compatibility-matrix.md b/docs/reports/caddy-compatibility-matrix.md index 42fde558..15f104a4 100644 --- a/docs/reports/caddy-compatibility-matrix.md +++ b/docs/reports/caddy-compatibility-matrix.md @@ -1,33 +1,32 @@ -## PR-1 Caddy Compatibility Matrix +# PR-1 Caddy Compatibility Matrix Report -- Date: 2026-02-23 -- Candidate version: 2.11.1 -- Scope: PR-1 compatibility slice only +- Generated at: 2026-02-23T13:52:26Z +- Candidate Caddy version: 2.11.1 +- Plugin set: caddy-security,coraza-caddy,caddy-crowdsec-bouncer,caddy-geoip2,caddy-ratelimit +- Smoke set: boot_caddy,plugin_modules,config_validate,admin_api_health +- Matrix dimensions: patch scenario × platform/arch × checked plugin modules -## Promotion Rule (PR-1) +## Deterministic Pass/Fail -- Promotion-gating rows: Scenario A on linux/amd64 and linux/arm64 -- Evidence-only rows: Scenario B and C +A matrix cell is PASS only when every smoke check and module inventory extraction passes. -## Matrix Summary +Promotion gate semantics (spec-aligned): +- Scenario A on linux/amd64 and linux/arm64 is promotion-gating. +- Scenario B/C are evidence-only; failures in B/C do not fail the PR-1 promotion gate. -| Scenario | Platform | Status | Reviewer Action | -| --- | --- | --- | --- | -| A | linux/amd64 | PASS | Required for promotion | -| A | linux/arm64 | PASS | Required for promotion | -| B | linux/amd64 | PASS | Evidence-only | -| B | linux/arm64 | PASS | Evidence-only | -| C | linux/amd64 | PASS | Evidence-only | -| C | linux/arm64 | PASS | Evidence-only | +## Matrix Output -## Decision - -- Promotion gate: PASS -- Runtime default drift: None observed in PR-1 -- Candidate path: Opt-in only +| Scenario | Platform | Plugins Checked | boot_caddy | plugin_modules | config_validate | admin_api_health | module_inventory | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| A | linux/amd64 | http.handlers.auth_portal, http.handlers.waf, http.handlers.crowdsec, http.handlers.geoip2, http.handlers.rate_limit | PASS | PASS | PASS | PASS | PASS | PASS | +| A | linux/arm64 | http.handlers.auth_portal, http.handlers.waf, http.handlers.crowdsec, http.handlers.geoip2, http.handlers.rate_limit | PASS | PASS | PASS | PASS | PASS | PASS | +| B | linux/amd64 | http.handlers.auth_portal, http.handlers.waf, http.handlers.crowdsec, http.handlers.geoip2, http.handlers.rate_limit | PASS | PASS | PASS | PASS | PASS | PASS | +| B | linux/arm64 | http.handlers.auth_portal, http.handlers.waf, http.handlers.crowdsec, http.handlers.geoip2, http.handlers.rate_limit | PASS | PASS | PASS | PASS | PASS | PASS | +| C | linux/amd64 | http.handlers.auth_portal, http.handlers.waf, http.handlers.crowdsec, http.handlers.geoip2, http.handlers.rate_limit | PASS | PASS | PASS | PASS | PASS | PASS | +| C | linux/arm64 | http.handlers.auth_portal, http.handlers.waf, http.handlers.crowdsec, http.handlers.geoip2, http.handlers.rate_limit | PASS | PASS | PASS | PASS | PASS | PASS | ## Artifacts -- Matrix CSV: test-results/caddy-compat-closure/matrix-summary.csv -- Module inventories: test-results/caddy-compat-closure/module-inventory-*-go-version-m.txt -- Module listings: test-results/caddy-compat-closure/module-inventory-*-modules.txt +- Matrix CSV: test-results/caddy-compat/matrix-summary.csv +- Per-cell module inventories: test-results/caddy-compat/module-inventory-*-go-version-m.txt +- Per-cell Caddy module listings: test-results/caddy-compat/module-inventory-*-modules.txt diff --git a/docs/reports/caddy-security-posture.md b/docs/reports/caddy-security-posture.md new file mode 100644 index 00000000..893e6d55 --- /dev/null +++ b/docs/reports/caddy-security-posture.md @@ -0,0 +1,65 @@ +## PR-2 Security Patch Posture and Advisory Disposition + +- Date: 2026-02-23 +- Scope: PR-2 only (security patch posture + xcaddy patch retirement decision) +- Upstream target: Caddy 2.11.x line (`2.11.1` candidate in this repository) +- Inputs: + - PR-1 compatibility matrix: `docs/reports/caddy-compatibility-matrix.md` + - Plan authority: `docs/plans/current_spec.md` + - Runtime and bootstrap assumptions: `.docker/docker-entrypoint.sh`, `.docker/compose/docker-compose.yml` + +### 1) Final patch disposition + +| Patch target | Decision | Rationale (evidence-backed) | Rollback path | +| --- | --- | --- | --- | +| `github.com/expr-lang/expr@v1.17.7` | Retain | Enforced by current builder patching and CI dependency checks. | Keep current pin. | +| `github.com/hslatman/ipstore@v0.4.0` | Retain | No PR-2 evidence supports safe retirement. | Keep current pin. | +| `github.com/slackhq/nebula@v1.9.7` | Retire by default | Matrix evidence supports scenario `B`/`C`; default moved to `B` with rollback preserved. | Set `CADDY_PATCH_SCENARIO=A`. | + +### 2) Caddy 2.11.x advisory disposition + +| Advisory | Component summary | Exploitability | Evidence source | Owner | Recheck cadence | +| --- | --- | --- | --- | --- | --- | +| `GHSA-5r3v-vc8m-m96g` (`CVE-2026-27590`) | FastCGI `split_path` confusion | Not affected | Upstream advisory + Charon runtime path review (no FastCGI transport in default generated config path) | QA_Security | weekly | +| `GHSA-879p-475x-rqh2` (`CVE-2026-27589`) | Admin API cross-origin no-cors | Mitigated | Upstream advisory + local controls: `CHARON_CADDY_ADMIN_API` now validated against internal allowlist and expected port 2019; production compose does not publish 2019 by default | QA_Security | weekly | +| `GHSA-x76f-jf84-rqj8` (`CVE-2026-27588`) | Host matcher case bypass | Mitigated | Upstream advisory + PR-1 Caddy 2.11.x matrix compatibility evidence and Charon route/security test reliance on upgraded line | QA_Security | release-candidate | +| `GHSA-g7pc-pc7g-h8jh` (`CVE-2026-27587`) | Path matcher escaped-case bypass | Mitigated | Upstream advisory + PR-1 matrix evidence and maintained security enforcement suite coverage | QA_Security | release-candidate | +| `GHSA-hffm-g8v7-wrv7` (`CVE-2026-27586`) | mTLS client-auth fail-open | Not affected | Upstream advisory + Charon default deployment model does not enable mTLS client-auth CA pool configuration by default | QA_Security | on-upstream-change | +| `GHSA-4xrr-hq4w-6vf4` (`CVE-2026-27585`) | File matcher glob sanitization bypass | Not affected | Upstream advisory + no default Charon generated config dependency on vulnerable matcher pattern | QA_Security | on-upstream-change | + +### 3) Admin API exposure assumptions and hardening status + +- Assumption: only internal Caddy admin endpoints are valid management targets. +- PR-2 enforcement: + - validate and normalize `CHARON_CADDY_ADMIN_API`/`CPM_CADDY_ADMIN_API` + - host allowlist + expected port `2019` + - fail-fast startup on invalid/non-allowlisted endpoint +- Exposure check: production compose defaults do not publish port `2019`. + +### 4) Runtime safety and rollback preservation + +- Runtime defaults keep `expr` and `ipstore` pinned. +- `nebula` pin retirement is controlled by scenario switch, not hard deletion. +- Emergency rollback remains one-step: `CADDY_PATCH_SCENARIO=A`. + +### Validation executed for PR-2 + +| Command / Task | Outcome | +| --- | --- | +| `cd /projects/Charon/backend && go test ./internal/config` | PASS | +| VS Code task `Security: Caddy PR-1 Compatibility Matrix` | PASS (A/B/C scenarios pass on `linux/amd64` and `linux/arm64`; promotion gate PASS) | + +Relevant generated artifacts: +- `docs/reports/caddy-compatibility-matrix.md` +- `test-results/caddy-compat/matrix-summary.csv` +- `test-results/caddy-compat/module-inventory-*-go-version-m.txt` +- `test-results/caddy-compat/module-inventory-*-modules.txt` + +### Residual risks / follow-up watch + +1. Caddy advisories with reserved or evolving CVE enrichment may change exploitability interpretation; recheck cadence remains active. +2. Caddy bootstrap still binds admin listener to container interface (`0.0.0.0:2019`) for compatibility, so operator misconfiguration that publishes port `2019` can expand attack surface; production compose defaults avoid publishing this port. + +### PR-2 closure statement + +PR-2 posture decisions are review-ready: patch disposition is explicit, admin API assumptions are enforced, and rollback remains deterministic. No PR-3 scope is included. diff --git a/docs/reports/qa_report.md b/docs/reports/qa_report.md index 766482d5..799791c4 100644 --- a/docs/reports/qa_report.md +++ b/docs/reports/qa_report.md @@ -1,31 +1,25 @@ -## QA Report — PR-1 Caddy Compatibility Closure +## QA Report — PR-2 Security Patch Posture Audit - Date: 2026-02-23 -- Scope: PR-1 compatibility slice only -- Decision: Ready to close PR-1 +- Scope: PR-2 only (security patch posture, admin API hardening, rollback viability) +- Verdict: **READY (PASS)** -## Reviewer Checklist +## Gate Summary -| Gate | Status | Reviewer Action | +| Gate | Status | Evidence | | --- | --- | --- | -| Targeted Playwright blocker rerun | PASS | Confirm targeted tests are no longer failing. | -| Compatibility matrix rerun (isolated output) | PASS | Confirm A/B/C rows exist for amd64 and arm64. | -| Promotion guard decision | PASS | Confirm promotion depends only on Scenario A (both architectures). | -| Non-drift runtime default | PASS | Confirm default remains non-candidate. | -| Focused pre-commit and CodeQL findings gate | PASS | Confirm no blocking findings in this slice. | +| Targeted E2E for PR-2 | PASS | Security settings test for Caddy Admin API URL passed (2/2). | +| Local patch preflight artifacts | PASS | `test-results/local-patch-report.md` and `.json` regenerated. | +| Coverage and type-check | PASS | Backend coverage 87.7% line / 87.4% statement; frontend type-check passed; frontend coverage preflight input passed (88.99% lines). | +| Pre-commit gate | PASS | `pre-commit run --all-files` passed after resolving version and type-check hook issues. | +| Security scans | PASS | CodeQL Go/JS CI-aligned scans passed; findings gate passed with no HIGH/CRITICAL; Trivy passed at configured severities. | +| Runtime posture + rollback | PASS | Default scenario shifted `A -> B` for PR-2 posture; rollback remains explicit via `CADDY_PATCH_SCENARIO=A`; admin API URL now validated and normalized at config load. | -## Evidence Snapshot +## Resolved Items -- Targeted rerun passed for prior blocker tests. -- Matrix run completed with full rows and PASS outcomes in isolated output. -- Promotion gate condition met: Scenario A passed on linux/amd64 and linux/arm64. -- Candidate path remains opt-in; default path remains stable. +1. `check-version-match` mismatch fixed by syncing `.version` to `v0.19.1`. +2. `frontend-type-check` hook stabilized to `npx tsc --noEmit` for deterministic pre-commit behavior. -## Open Risks to Monitor +## PR-2 Closure Statement -- Matrix artifact contamination if shared output directories are reused. -- Candidate behavior drift if default build args are changed in future slices. - -## Final Verdict - -PR-1 closure gates are satisfied for the compatibility slice. +All PR-2 QA/security gates required for merge are passing. No PR-3 scope is included in this report.