The slack sub-tests in TestDiscordOnly_CreateRejectsNonDiscord and TestBlocker3_CreateProviderRejectsNonDiscordWithSecurityEvents were omitting the required token field from their request payloads. CreateProvider enforces that Slack providers must have a non-empty token (the webhook URL) at creation time. Without it the service returns "slack webhook URL is required", which the handler does not classify as a 400 validation error, so it falls through to 500. Add a token field to each test struct, populate it for the slack case with a valid-format Slack webhook URL, and use WithSlackURLValidator to bypass the real format check in unit tests — matching the pattern used in all existing service-level Slack tests.
327 lines
12 KiB
Markdown
327 lines
12 KiB
Markdown
# QA Audit Report — Slack Sub-Test Fixes
|
||
|
||
**Date:** 2026-03-15
|
||
**Branch:** `feature/beta-release`
|
||
**Commit:** `6e4294dc` — `fix: validate Slack webhook URL at provider create/update time`
|
||
**Scope:** Two handler test files + service test + notification_service.go (Slack URL validation)
|
||
**Reviewer:** QA Security Agent
|
||
|
||
---
|
||
|
||
## Overall Verdict: PASS
|
||
|
||
All quality and security gates pass. The two test-only fixes are safe, coverage is maintained above threshold, and no security regressions were introduced.
|
||
|
||
---
|
||
|
||
## Changes Under Review
|
||
|
||
| File | Type | Description |
|
||
|---|---|---|
|
||
| `backend/internal/services/notification_service.go` | Production | Added `WithSlackURLValidator` option and Slack URL validation at create/update |
|
||
| `backend/internal/services/notification_service_test.go` | Test | Added Slack sub-tests using `WithSlackURLValidator` bypass |
|
||
| `backend/internal/api/handlers/notification_provider_discord_only_test.go` | Test | Added `token` field and `WithSlackURLValidator` to fix failing `slack` sub-test |
|
||
| `backend/internal/api/handlers/notification_provider_blocker3_test.go` | Test | Added `token` field and `WithSlackURLValidator` to fix failing `slack` sub-test |
|
||
|
||
---
|
||
|
||
## Gate Summary
|
||
|
||
| # | Gate | Result | Details |
|
||
|---|---|---|---|
|
||
| 1 | Backend test suite — pass/fail | **PASS** | 0 failures across all packages |
|
||
| 2 | Line coverage ≥87% | **PASS** | 88.2% line / 87.9% statement |
|
||
| 3 | Patch coverage ≥90% (overall) | **PASS** | 95.1% on changed lines |
|
||
| 4 | go vet | **PASS** | 0 issues |
|
||
| 5 | golangci-lint (fast) | **PASS** | 0 issues |
|
||
| 6 | Pre-commit hooks (lefthook) | **PASS** | All 6 hooks pass |
|
||
| 7 | Trivy filesystem scan (Critical/High) | **PASS** | 0 findings |
|
||
| 8 | GORM security scan (Critical/High) | **PASS** | 0 findings |
|
||
| 9 | Security review of `WithSlackURLValidator` | **PASS** | Production defaults are safe; informational note only |
|
||
|
||
---
|
||
|
||
## 1. Backend Test Suite
|
||
|
||
**Command:** `bash /projects/Charon/.github/skills/scripts/skill-runner.sh test-backend-coverage`
|
||
|
||
| Metric | Result | Threshold |
|
||
|---|---|---|
|
||
| Test failures | **0** | 0 |
|
||
| Statement coverage | **87.9%** | ≥87% |
|
||
| Line coverage | **88.2%** | ≥87% |
|
||
|
||
All packages returned `ok`. No `FAIL` entries. Selected package breakdown:
|
||
|
||
| Package | Coverage |
|
||
|---|---|
|
||
| `internal/api/handlers` | 86.3% |
|
||
| `internal/api/middleware` | 97.2% |
|
||
| `internal/api/routes` | 89.5% |
|
||
| `internal/caddy` | 96.8% |
|
||
| `internal/cerberus` | 93.8% |
|
||
| `internal/metrics` | 100.0% |
|
||
| `internal/models` | 97.3% |
|
||
| `internal/services` | included in global |
|
||
|
||
---
|
||
|
||
## 2. Patch Coverage (Local Patch Report)
|
||
|
||
**Command:** `bash /projects/Charon/scripts/local-patch-report.sh`
|
||
|
||
| Scope | Changed Lines | Covered | Patch Coverage | Threshold |
|
||
|---|---|---|---|---|
|
||
| Overall | 81 | 77 | **95.1%** | ≥90% |
|
||
| Backend | 75 | 71 | **94.7%** | ≥85% |
|
||
| Frontend | 6 | 6 | **100.0%** | ≥85% |
|
||
|
||
**4 uncovered changed lines** in `notification_service.go` at lines 477–478 and 481–482. These are error-handling branches for `json.Marshal` and `bytes.Buffer.Write` in Slack payload normalization — conditions requiring OOM-class failures and effectively unreachable in practice. Not a coverage concern.
|
||
|
||
---
|
||
|
||
## 3. Pre-Commit Hooks
|
||
|
||
**Commands:** `lefthook run pre-commit`; `go vet ./...`; `golangci-lint`
|
||
|
||
| Hook | Result |
|
||
|---|---|
|
||
| `end-of-file-fixer` | PASS |
|
||
| `trailing-whitespace` | PASS |
|
||
| `check-yaml` | PASS |
|
||
| `actionlint` | PASS |
|
||
| `dockerfile-check` | PASS |
|
||
| `shellcheck` | PASS |
|
||
| `go vet ./...` (manual) | PASS — 0 issues |
|
||
| `golangci-lint` v2.9.0 (manual) | PASS — 0 issues |
|
||
|
||
Go and frontend hooks were skipped by lefthook (no staged files). Both were run manually and returned clean results.
|
||
|
||
---
|
||
|
||
## 4. Trivy Filesystem Scan
|
||
|
||
**Command:** `bash /projects/Charon/.github/skills/scripts/skill-runner.sh security-scan-trivy`
|
||
|
||
Scanners: `vuln,secret`. Severity filter: `CRITICAL,HIGH,MEDIUM`.
|
||
|
||
| Target | Type | Vulnerabilities | Secrets |
|
||
|---|---|---|---|
|
||
| `backend/go.mod` | gomod | **0** | — |
|
||
| `frontend/package-lock.json` | npm | **0** | — |
|
||
| `package-lock.json` | npm | **0** | — |
|
||
| `playwright/.auth/user.json` | text | — | **0** |
|
||
|
||
**Result: 0 findings.**
|
||
|
||
---
|
||
|
||
## 5. GORM Security Scan
|
||
|
||
**Command:** `bash /projects/Charon/.github/skills/scripts/skill-runner.sh security-scan-gorm`
|
||
|
||
Scanned 41 Go files (2,253 lines).
|
||
|
||
| Severity | Count |
|
||
|---|---|
|
||
| CRITICAL | **0** |
|
||
| HIGH | **0** |
|
||
| MEDIUM | **0** |
|
||
| INFO | 2 (pre-existing, informational only) |
|
||
|
||
**Result: 0 actionable issues.**
|
||
|
||
---
|
||
|
||
## 6. Security Review: `WithSlackURLValidator`
|
||
|
||
The new `WithSlackURLValidator` functional option exposes a test-injectable hook in `NotificationService`.
|
||
|
||
| Concern | Assessment |
|
||
|---|---|
|
||
| SSRF via production bypass | **Not a risk.** `NewNotificationService` always defaults to `validateSlackWebhookURL`. The option must be explicitly passed at construction time. |
|
||
| Allowlist strength | Regex `^https://hooks\.slack\.com/services/T[A-Za-z0-9_-]+/B[A-Za-z0-9_-]+/[A-Za-z0-9_-]+$` — pins to HTTPS, exact domain, and enforced path structure. Robust against SSRF. |
|
||
| Test bypass scope | Only used in `*_test.go` files. Comment states: *"Intended for use in tests that need to bypass real URL validation without mutating shared state."* |
|
||
| Exported from production package | **LOW / Informational.** The option is exported from `services` with no `//go:build test` build-tag guard. It could theoretically be called from production code, but there is no evidence of misuse and the default is safe. Consider adding a build-tag guard in a future cleanup pass if stricter separation is desired. |
|
||
|
||
---
|
||
|
||
## 7. Scope Exclusions
|
||
|
||
| Check | Excluded | Justification |
|
||
|---|---|---|
|
||
| E2E Playwright tests | Yes | No UI or routing changes |
|
||
| CodeQL | Yes | No Go or JavaScript semantic changes |
|
||
| Docker image scan | Yes | No Dockerfile changes |
|
||
| Frontend unit coverage | N/A | Frontend patch coverage 100% |
|
||
|
||
|
||
---
|
||
|
||
## Change Summary
|
||
|
||
| File | Change | Line |
|
||
|------|--------|------|
|
||
| `scripts/cerberus_integration.sh` | Add `-e PORT=80` to `docker run ... mccutchen/go-httpbin` | L174 |
|
||
| `scripts/waf_integration.sh` | Add `-e PORT=80` to `docker run ... mccutchen/go-httpbin` | L167 |
|
||
| `scripts/rate_limit_integration.sh` | Add `-e PORT=80` to `docker run ... mccutchen/go-httpbin` | L187 |
|
||
| `scripts/coraza_integration.sh` | Add `-e PORT=80` to `docker run ... mccutchen/go-httpbin` | L158 |
|
||
| `scripts/crowdsec_startup_test.sh` | Replace `curl -sf` with `wget -qO -` in `docker exec` | L179 |
|
||
| `scripts/diagnose-test-env.sh` | Replace `curl -sf` with `wget -qO /dev/null` in `docker exec` | L104 |
|
||
|
||
---
|
||
|
||
## Gate Summary
|
||
|
||
| # | Gate | Result | Details |
|
||
|---|------|--------|---------|
|
||
| 1 | Syntax Validation (`bash -n`) | **PASS** | All 6 scripts parse cleanly |
|
||
| 2 | ShellCheck (error severity) | **PASS** | 0 errors; matches lefthook `--severity=error` |
|
||
| 3 | ShellCheck (all severities) | **PASS** | No findings on any modified line; all findings pre-existing |
|
||
| 4 | Pre-commit Hooks (lefthook) | **PASS** | All 6 hooks passed (shellcheck, actionlint, yaml, whitespace, eof, dockerfile) |
|
||
| 5 | Verification: go-httpbin PORT | **PASS** | 4/4 `docker run` lines have `-e PORT=80` |
|
||
| 6 | Verification: docker exec curl | **PASS** | 0 executed curl calls; 2 echo-only references (hints) |
|
||
| 7 | Security Review | **PASS** | No secrets, credentials, injection vectors, or Gotify tokens |
|
||
| 8 | Trivy Filesystem Scan | **PASS** | 0 secrets, 0 misconfigurations |
|
||
|
||
---
|
||
|
||
## 1. Syntax Validation (`bash -n`)
|
||
|
||
| Script | Result |
|
||
|--------|--------|
|
||
| `scripts/cerberus_integration.sh` | PASS |
|
||
| `scripts/waf_integration.sh` | PASS |
|
||
| `scripts/rate_limit_integration.sh` | PASS |
|
||
| `scripts/coraza_integration.sh` | PASS |
|
||
| `scripts/crowdsec_startup_test.sh` | PASS |
|
||
| `scripts/diagnose-test-env.sh` | PASS |
|
||
|
||
---
|
||
|
||
## 2. ShellCheck
|
||
|
||
### At error severity (`--severity=error`, matching lefthook pre-commit)
|
||
|
||
**Result: PASS** — Zero errors across all 6 scripts. Exit code 0.
|
||
|
||
### At default severity (full informational audit)
|
||
|
||
Exit code 1 (findings present, all `note` or `warning` severity).
|
||
|
||
| Script | Findings | Severity | On Modified Lines? |
|
||
|--------|----------|----------|--------------------|
|
||
| `cerberus_integration.sh` | 2× SC2086 (unquoted variable) | note | No (L204, L219) |
|
||
| `waf_integration.sh` | ~30× SC2317 (unreachable code in trap), 3× SC2086 | note | No |
|
||
| `rate_limit_integration.sh` | 9× SC2086 | note | No |
|
||
| `coraza_integration.sh` | 10× SC2086, 2× SC2034 (unused variable) | note/warning | No |
|
||
| `crowdsec_startup_test.sh` | ~10× SC2317, 1× SC2086 | note | No |
|
||
| `diagnose-test-env.sh` | 1× SC2034 (unused variable) | warning | No |
|
||
|
||
**No ShellCheck findings on any of the 6 modified lines.** All findings are pre-existing.
|
||
|
||
---
|
||
|
||
## 3. Pre-commit Hooks (lefthook)
|
||
|
||
Ran `lefthook run pre-commit`:
|
||
|
||
| Hook | Result | Duration |
|
||
|------|--------|----------|
|
||
| check-yaml | PASS | 1.93s |
|
||
| actionlint | PASS | 4.36s |
|
||
| end-of-file-fixer | PASS | 9.23s |
|
||
| trailing-whitespace | PASS | 9.49s |
|
||
| dockerfile-check | PASS | 10.41s |
|
||
| shellcheck | PASS | 11.24s |
|
||
|
||
Hooks for Go, TypeScript, and Semgrep correctly skipped (no matching files).
|
||
|
||
---
|
||
|
||
## 4. Verification Greps
|
||
|
||
### 4a. All `mccutchen/go-httpbin` `docker run` instances have `-e PORT=80`
|
||
|
||
```
|
||
scripts/cerberus_integration.sh:174: docker run ... -e PORT=80 mccutchen/go-httpbin
|
||
scripts/waf_integration.sh:167: docker run ... -e PORT=80 mccutchen/go-httpbin
|
||
scripts/rate_limit_integration.sh:187:docker run ... -e PORT=80 mccutchen/go-httpbin
|
||
scripts/coraza_integration.sh:158: docker run ... -e PORT=80 mccutchen/go-httpbin
|
||
```
|
||
|
||
Remaining `mccutchen/go-httpbin` matches are `docker pull` lines (no `-e PORT` needed).
|
||
|
||
**Result: PASS** — 4/4 confirmed.
|
||
|
||
### 4b. Zero executed `docker exec ... curl` calls
|
||
|
||
Only 2 matches found in `scripts/verify_crowdsec_app_config.sh` (L94–95) — both inside `echo` statements (user hint text, not executed). Confirmed by manual review.
|
||
|
||
**Result: PASS** — 0 executed `docker exec ... curl` calls.
|
||
|
||
---
|
||
|
||
## 5. Security Review
|
||
|
||
| Check | Result | Notes |
|
||
|-------|--------|-------|
|
||
| Secrets/credentials in diff | PASS | `git diff | grep -iE "password\|secret\|key\|token\|credential\|auth"` — no matches |
|
||
| Gotify tokens | PASS | `grep -rn "Gotify\|gotify\|token="` across all 6 scripts — no matches |
|
||
| Injection vectors | PASS | `-e PORT=80` is a static literal; no user-controlled input flows into new code |
|
||
| Command injection | PASS | `wget -qO` flags are hardcoded; no interpolated user input |
|
||
| SSRF | N/A | URLs are internal container addresses (127.0.0.1, localhost) in CI-only scripts |
|
||
| Sensitive data in logs | PASS | No new log/echo statements added |
|
||
| URL query parameters | PASS | No tokenized URLs (e.g., `?token=...`) in changed or adjacent code |
|
||
|
||
---
|
||
|
||
## 6. Trivy Filesystem Scan
|
||
|
||
Scanners: `secret,misconfig`. Severity filter: `CRITICAL,HIGH,MEDIUM`.
|
||
|
||
| Target | Type | Secrets | Misconfigurations |
|
||
|--------|------|---------|-------------------|
|
||
| `backend/go.mod` | gomod | — | — |
|
||
| `frontend/package-lock.json` | npm | — | — |
|
||
| `package-lock.json` | npm | — | — |
|
||
| `Dockerfile` | dockerfile | — | 0 |
|
||
| `playwright/.auth/user.json` | text | 0 | — |
|
||
|
||
**Result: 0 findings. Exit code 0.**
|
||
|
||
---
|
||
|
||
## 7. Scope Exclusions
|
||
|
||
| Check | Excluded? | Justification |
|
||
|-------|-----------|---------------|
|
||
| E2E Playwright tests | Yes | Scripts are CI-only; no UI changes |
|
||
| Backend unit coverage | Yes | No Go code changes |
|
||
| Frontend unit coverage | Yes | No TypeScript/React changes |
|
||
| Docker image scan | Yes | No Dockerfile or image changes |
|
||
| CodeQL | Yes | No Go or JavaScript changes |
|
||
| GORM security scan | Yes | No model/database changes |
|
||
| Local patch coverage report | Yes | No application code; scripts not coverage-tracked |
|
||
|
||
---
|
||
|
||
## 8. Pre-existing Issues (Not Introduced by This Change)
|
||
|
||
| Category | Count | Scripts Affected | Risk |
|
||
|----------|-------|-----------------|------|
|
||
| SC2086 (unquoted variables) | ~25 | All 6 | Low — CI-controlled variables |
|
||
| SC2317 (unreachable code) | ~40 | waf, crowdsec | None — trap cleanup functions (ShellCheck false positive) |
|
||
| SC2034 (unused variables) | 3 | coraza, diagnose | Low — may be planned for future use |
|
||
|
||
---
|
||
|
||
## Remaining Validation (CI)
|
||
|
||
The integration scripts cannot be executed locally without a built `charon:local` image and Docker network. Full end-to-end validation will occur when the PR triggers CI:
|
||
|
||
- `.github/workflows/cerberus-integration.yml`
|
||
- `.github/workflows/waf-integration.yml`
|
||
- `.github/workflows/rate-limit-integration.yml`
|
||
- `.github/workflows/crowdsec-integration.yml`
|