Files
Charon/docs/reports/qa_report.md
GitHub Actions 5bafd92edf fix: supply slack webhook token in handler create sub-tests
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.
2026-03-15 15:17:23 +00:00

327 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 477478 and 481482. 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` (L9495) — 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`