6.3 KiB
Executable File
QA Audit Report — PR-1
Date: 2026-03-17
Scope: Remove binding:"required" from Value field in UpdateSettingRequest
File: backend/internal/api/handlers/settings_handler.go
Purpose: Allow admins to set a setting to an empty string value (required to fix the fresh-install CrowdSec enabling bug where value was legitimately empty).
Summary
| # | Check | Result | Notes |
|---|---|---|---|
| 1 | Handler unit tests | ✅ PASS (after fix) | One stale test updated |
| 2 | Full backend test suite | ✅ PASS | No regressions |
| 3 | Lint (staticcheck) | ⚠️ SKIP | Pre-existing toolchain mismatch, not PR-1 |
| 4 | Pre-commit hooks | ⚠️ SKIP | .pre-commit-config.yaml not present |
| 5 | Security — admin auth check | ✅ PASS | Three independent protection layers confirmed |
| 6 | Trivy scan | ⚠️ NOTE | 1 pre-existing HIGH unrelated to PR-1 |
Check 1 — Handler Unit Tests
Command: go test ./internal/api/handlers/... -v
Initial result: FAIL
--- FAIL: TestSettingsHandler_Errors at settings_handler_test.go:805
Root cause: The sub-case "Missing Key/Value" sent a payload with key but no value and asserted HTTP 400. This assertion was valid against the old binding:"required" constraint on Value. After PR-1 removed that constraint, the handler correctly accepts an absent value (empty string) and returns HTTP 200, breaking the stale assertion.
Fix applied: backend/internal/api/handlers/settings_handler_test.go
- Updated the "value absent" sub-case to assert
HTTP 200— empty string is now a valid value. - Added a new sub-case: payload with
valuebut nokey→ assertsHTTP 400(key remainsbinding:"required").
Result after fix: ✅ PASS
Check 2 — Full Backend Test Suite
Command: go test ./...
All packages pass:
ok github.com/Wikid82/charon/backend/cmd/seed
ok github.com/Wikid82/charon/backend/internal/api
ok github.com/Wikid82/charon/backend/internal/api/handlers
ok github.com/Wikid82/charon/backend/internal/api/middleware
ok github.com/Wikid82/charon/backend/internal/api/routes
ok github.com/Wikid82/charon/backend/internal/api/tests
ok github.com/Wikid82/charon/backend/internal/caddy
ok github.com/Wikid82/charon/backend/internal/cerberus
ok github.com/Wikid82/charon/backend/internal/crowdsec
ok github.com/Wikid82/charon/backend/internal/crypto
ok github.com/Wikid82/charon/backend/internal/database
ok github.com/Wikid82/charon/backend/internal/models
ok github.com/Wikid82/charon/backend/internal/notifications
ok github.com/Wikid82/charon/backend/internal/services
ok github.com/Wikid82/charon/backend/internal/server
... (all packages green, 0 failures)
Result: ✅ PASS — no regressions.
Check 3 — Lint
Attempted: make lint-fast → target does not exist.
Fallback: staticcheck ./...
Output:
-: module requires at least go1.26.1, but Staticcheck was built with go1.25.5 (compile)
Pre-existing toolchain version mismatch between the installed staticcheck binary (go1.25.5) and the module minimum (go1.26.1). Not caused by this PR.
Result: ⚠️ SKIP — pre-existing environment issue, not a PR-1 blocker.
Check 4 — Pre-commit Hooks
Command: pre-commit run --all-files
Output: InvalidConfigError: .pre-commit-config.yaml is not a file
No .pre-commit-config.yaml exists at the repository root; lefthook.yml is the configured hook runner.
Result: ⚠️ SKIP — pre-commit not configured for this repo.
Check 5 — Security: Endpoint Authentication
The change removes only an input validation constraint on the Value field. No authorization logic is touched. Verified the full protection chain:
Route registration (backend/internal/api/routes/routes.go)
protected := api.Group("/")
protected.Use(authMiddleware) // Layer 1: JWT required → 401 otherwise
management := protected.Group("/")
management.Use(RequireManagementAccess()) // Layer 2: blocks passthrough role → 403
management.POST("/settings", settingsHandler.UpdateSetting)
management.PATCH("/settings", settingsHandler.UpdateSetting)
Handler guard (settings_handler.go line 124)
func (h *SettingsHandler) UpdateSetting(c *gin.Context) {
if !requireAdmin(c) { // Layer 3: admin role required → 403 otherwise
return
}
...
}
Layer summary
| Layer | Mechanism | Failure response |
|---|---|---|
| 1 | authMiddleware (JWT) |
401 Unauthorized |
| 2 | RequireManagementAccess() middleware |
403 Forbidden (passthrough role) |
| 3 | requireAdmin(c) in handler |
403 Forbidden (non-admin) |
An unauthenticated or non-admin request cannot reach the Value binding logic.
The PR-1 change affects only the binding validation of Value; the authorization path is unchanged.
Result: ✅ PASS — endpoint is properly admin-gated by three independent layers.
Check 6 — Trivy Security Scan
trivy CLI not installed in this environment. Analysis based on cached trivy-image-report.json
(generated 2026-02-25, Trivy v0.69.1, image charon:local).
| Severity | Count | Details |
|---|---|---|
| CRITICAL | 0 | None |
| HIGH | 1 | CVE-2026-25793 — github.com/slackhq/nebula v1.9.7 |
CVE-2026-25793 detail:
- Package:
github.com/slackhq/nebula v1.9.7 - Fixed in:
v1.10.3 - Title: Blocklist evasion via ECDSA Signature Malleability
- Relation to PR-1: None — pre-existing transitive dependency issue, tracked separately.
Result: ⚠️ NOTE — 1 pre-existing HIGH; no new vulnerabilities introduced by PR-1.
Overall Verdict
PR-1 is safe to merge with the accompanying test fix.
The change is behaviorally correct: an empty string is a valid setting value for certain admin operations (specifically the CrowdSec enrollment flow on fresh installs). Authorization is unaffected — three independent layers continue to restrict this endpoint to authenticated admins.
Required: Test fix
backend/internal/api/handlers/settings_handler_test.go — TestSettingsHandler_Errors updated
to reflect the new contract (empty value → 200; missing key → 400 still enforced).
Tracked separately
github.com/slackhq/nebulashould be bumped tov1.10.3to resolve CVE-2026-25793.