Files
Charon/docs/reports/qa_report_pushover_notifications.md
akanealw eec8c28fb3
Some checks are pending
Go Benchmark / Performance Regression Check (push) Waiting to run
Cerberus Integration / Cerberus Security Stack Integration (push) Waiting to run
Upload Coverage to Codecov / Backend Codecov Upload (push) Waiting to run
Upload Coverage to Codecov / Frontend Codecov Upload (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (go) (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (javascript-typescript) (push) Waiting to run
CrowdSec Integration / CrowdSec Bouncer Integration (push) Waiting to run
Docker Build, Publish & Test / build-and-push (push) Waiting to run
Docker Build, Publish & Test / Security Scan PR Image (push) Blocked by required conditions
Quality Checks / Auth Route Protection Contract (push) Waiting to run
Quality Checks / Codecov Trigger/Comment Parity Guard (push) Waiting to run
Quality Checks / Backend (Go) (push) Waiting to run
Quality Checks / Frontend (React) (push) Waiting to run
Rate Limit integration / Rate Limiting Integration (push) Waiting to run
Security Scan (PR) / Trivy Binary Scan (push) Waiting to run
Supply Chain Verification (PR) / Verify Supply Chain (push) Waiting to run
WAF integration / Coraza WAF Integration (push) Waiting to run
changed perms
2026-04-22 18:19:14 +00:00

15 KiB
Executable File
Raw Blame History

QA & Security Audit Report — Pushover Notification Provider

Date: 2026-03-16 Scope: Pushover notification provider full-stack implementation Auditor: QA/Security Review Verdict: PASS with one test fix applied (see FE-001 below)


1. Scope of Changes Reviewed

Area Files
Backend feature flags backend/internal/notifications/feature_flags.go
Backend router backend/internal/notifications/router.go
Backend notification service backend/internal/services/notification_service.go
Backend enhanced security service backend/internal/services/enhanced_security_notification_service.go
Backend handler (CRUD + Test guards) backend/internal/api/handlers/notification_provider_handler.go
Backend unit tests (~10 new test cases) backend/internal/services/notification_service_test.go
Frontend form fields frontend/src/pages/Notifications.tsx
Frontend supported types frontend/src/api/notifications.ts
Frontend i18n frontend/src/locales/en/translation.json
Frontend unit tests frontend/src/pages/__tests__/Notifications.test.tsx
Model backend/internal/models/notification_provider.go

2. Required Checks — Results

2.1 Backend Compilation

cd /projects/Charon/backend && go build ./...

Result: PASS — Zero compilation errors across all packages.


2.2 Backend Unit Tests

cd /projects/Charon/backend && go test ./...

Result: PASS — All 33 packages pass with no failures.

Package Status
internal/api/handlers ok (66.1s)
internal/services ok (75.4s)
internal/notifications ok (cached)
All other packages ok

Pushover-specific tests (10 cases, all PASS):

Test Result
TestPushoverDispatch_Success PASS
TestPushoverDispatch_MissingToken PASS
TestPushoverDispatch_MissingUserKey PASS
TestPushoverDispatch_MessageFieldRequired PASS
TestPushoverDispatch_EmergencyPriorityRejected PASS
TestPushoverDispatch_PayloadInjection PASS
TestPushoverDispatch_FeatureFlagDisabled PASS
TestPushoverDispatch_SSRFValidation PASS
TestIsDispatchEnabled_PushoverDefaultTrue PASS
TestIsDispatchEnabled_PushoverDisabledByFlag PASS

2.3 Backend Linting

cd /projects/Charon && make lint-fast

Result: PASS0 issues. (staticcheck, govet, errcheck, ineffassign, unused)


2.4 Frontend TypeScript Check

cd /projects/Charon/frontend && npx tsc --noEmit

Result: PASS — No TypeScript errors.


2.5 Frontend Unit Tests

cd /projects/Charon/frontend && npx vitest run

Result: PASS (after fix applied — see FE-001)

Test File Tests Status
SecurityNotificationSettingsModal.test.tsx 4 PASS
Notifications.test.tsx 34 PASS
notifications.test.ts (API layer) 4 PASS

Pushover-specific frontend tests confirmed in Notifications.test.tsx:

  • renders pushover form with API Token field and User Key placeholder — PASS
  • Provider type select includes 'pushover' in options array — PASS

2.6 Pre-commit / Lefthook Hooks

Result: ⚠️ N/A — The project uses Lefthook (lefthook.yml), not pre-commit native. No .pre-commit-config.yaml is present. Running the pre-commit binary directly raises InvalidConfigError: .pre-commit-config.yaml is not a file. Code hygiene was verified manually in changed files; no whitespace or formatting issues were found.


2.7 Trivy Filesystem Security Scan

cd /projects/Charon && .github/skills/scripts/skill-runner.sh security-scan-trivy

Result: PASS — No vulnerabilities or secrets detected.

Report Summary
┌────────────────────────────────┬───────┬─────────────────┬─────────┐
│             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    │
└────────────────────────────────┴───────┴─────────────────┴─────────┘
[SUCCESS] Trivy scan completed - no issues found

2.8 Regression — Services Package

cd /projects/Charon/backend && go test ./internal/services/... -v 2>&1 | grep -E "^(--- PASS|--- FAIL|FAIL|ok)"

Result: PASS — All existing service tests continue to pass; no regressions introduced.


2.9 Regression — Handlers Package

cd /projects/Charon/backend && go test ./internal/api/handlers/... -v 2>&1 | grep -E "^(--- PASS|--- FAIL|FAIL|ok)"

Result: PASS — All existing handler tests continue to pass; no regressions introduced.


3. Security Code Review

3.1 Token JSON Serialization (json:"-")

Model field (backend/internal/models/notification_provider.go):

Token string `json:"-"` // Auth token for providers — never exposed in API

Finding: SECURE

The Token field on models.NotificationProvider carries json:"-", preventing it from being marshalled into any JSON response. Handler-level defense-in-depth also explicitly clears the token before responding in List, Create, and Update:

provider.HasToken = provider.Token != ""
provider.Token = ""
c.JSON(http.StatusOK, provider)

Two independent layers prevent token leakage.


3.2 SSRF Hostname Pin (api.pushover.net)

Production dispatch path (notification_service.go):

pushoverBase := s.pushoverAPIBaseURL  // always "https://api.pushover.net" in production
dispatchURL = pushoverBase + "/1/messages.json"

parsedURL, parseErr := neturl.Parse(dispatchURL)
expectedHost := "api.pushover.net"
// test-seam bypass: only applies when pushoverAPIBaseURL has been overridden in tests
if parsedURL != nil && parsedURL.Hostname() != "" && pushoverBase != "https://api.pushover.net" {
    expectedHost = parsedURL.Hostname()
}
if parseErr != nil || parsedURL.Hostname() != expectedHost {
    return fmt.Errorf("pushover dispatch URL validation failed: invalid hostname")
}

Finding: SECURE

In production, pushoverAPIBaseURL is always "https://api.pushover.net" (set in NewNotificationService). The bypass condition pushoverBase != "https://api.pushover.net" is only true in unit tests where the field is overridden via direct struct access (svc.pushoverAPIBaseURL = server.URL). This field is:

  • A private Go struct field — cannot be set via any API endpoint
  • Absent from notificationProviderUpsertRequest and notificationProviderTestRequest
  • Identical in design to the existing Telegram SSRF pin (reviewed previously)

No user-supplied input can influence the dispatch hostname.


3.3 Template Injection — token/user Field Override

Dispatch logic (notification_service.go):

// Template payload is rendered, then server-side values OVERWRITE any user-supplied keys:
jsonPayload["token"] = decryptedToken   // from DB
jsonPayload["user"] = p.URL             // from DB

Finding: SECURE

Server-side values always overwrite any token or user keys that may have been injected via the provider's Config template. This is explicitly exercised by TestPushoverDispatch_PayloadInjection, which confirms that a template containing "token": "fake-token", "user": "fake-user" is replaced with the real decrypted DB values before the outbound HTTP request is made.


3.4 Emergency Priority=2 Rejection

Validation (notification_service.go):

if priority, ok := jsonPayload["priority"]; ok {
    if p, isFloat := priority.(float64); isFloat && p == 2 {
        return fmt.Errorf("pushover emergency priority (2) requires retry and expire parameters; not yet supported")
    }
}

Finding: CORRECT

Emergency priority (2) is blocked with a clear, actionable error. JSON numbers are decoded as float64 by json.Unmarshal, so the p == 2 comparison is type-safe. Non-emergency priorities (-2, -1, 0, 1) pass through. The comparison float64(2) == 2 evaluates correctly in Go.

Covered by TestPushoverDispatch_EmergencyPriorityRejected.


3.5 Test() Write-Only Guard — Pushover and Telegram

Handler (notification_provider_handler.go):

if providerType == "pushover" && strings.TrimSpace(req.Token) != "" {
    respondSanitizedProviderError(c, http.StatusBadRequest, "TOKEN_WRITE_ONLY", "validation",
        "Pushover API token is accepted only on provider create/update")
    return
}

if providerType == "telegram" && strings.TrimSpace(req.Token) != "" {
    respondSanitizedProviderError(c, http.StatusBadRequest, "TOKEN_WRITE_ONLY", "validation",
        "Telegram bot token is accepted only on provider create/update")
    return
}

Finding: CORRECT

Passing a token in the Test request body is rejected with HTTP 400 TOKEN_WRITE_ONLY for both Pushover and Telegram. The test dispatch always reads credentials from the database (provider ID is required), preventing token exfiltration or injection via the test endpoint. The same guard exists for Gotify and Slack, maintaining symmetry across all token-based providers.


3.6 pushoverAPIBaseURL Accessibility via API

Finding: SECURE

pushoverAPIBaseURL is a private struct field with no API exposure:

  1. Not exported from the NotificationService struct
  2. Not present in any request struct unmarshalled from user input
  3. Only modified in test code via svc.pushoverAPIBaseURL = server.URL
  4. Never read from user input, headers, query parameters, or provider Config

Production dispatches invariably target https://api.pushover.net/1/messages.json.


4. Findings

FE-001 — Stale Test Assertion After Adding Pushover Provider Type

Severity: 🟡 MEDIUM (test failure, blocks CI) File: frontend/src/components/__tests__/SecurityNotificationSettingsModal.test.tsx:89 Status: FIXED

Description: After Pushover was added to SUPPORTED_NOTIFICATION_PROVIDER_TYPES in notifications.ts, the assertion checking the provider type dropdown options was not updated. The test expected 6 types but the implementation exposes 7, causing the test to fail and block CI.

Before:

expect(Array.from(typeSelect.options).map((option) => option.value))
    .toEqual(['discord', 'gotify', 'webhook', 'email', 'telegram', 'slack']);

After (applied):

expect(Array.from(typeSelect.options).map((option) => option.value))
    .toEqual(['discord', 'gotify', 'webhook', 'email', 'telegram', 'slack', 'pushover']);

All 4 tests in SecurityNotificationSettingsModal.test.tsx pass after the fix.


BE-001 — No Handler-Level Unit Tests for Pushover TOKEN_WRITE_ONLY Guard

Severity: 🟢 LOW (coverage gap, not a functional defect) File: backend/internal/api/handlers/notification_provider_handler_test.go Status: ⚠️ INFORMATIONAL

Description: The Test() handler's Pushover TOKEN_WRITE_ONLY guard is correctly implemented and is structurally identical to the existing Gotify, Slack, and Telegram guards. The guard is verified at the code-review level but no dedicated handler integration test exercises it. This gap applies to all four token-based providers, not Pushover in isolation.

Recommendation: Add handler integration tests for TOKEN_WRITE_ONLY guards across Gotify, Telegram, Slack, and Pushover in a follow-up issue to achieve symmetrical handler coverage.


E2E-001 — No Playwright E2E Spec for Pushover Provider

Severity: 🟢 LOW (coverage gap) Status: ⚠️ INFORMATIONAL

Description: The implementation scope stated "New E2E spec" but no Playwright .spec.ts file for Pushover was found in the repository. The playwright/ directory contains only the auth fixture. Frontend unit tests (Notifications.test.tsx) provide partial coverage of the form rendering path, but there is no browser-level test exercising the full add/edit/test flow for Pushover.

Recommendation: Create a Playwright spec covering: add Pushover provider, verify "User Key" and "API Token (Application)" field labels, test provider response handling. Target the next release cycle.


SEC-001 — SSRF Test Bypass Pattern (Design Note)

Severity: INFORMATIONAL (no action required)

Description: The pushoverAPIBaseURL field allows the SSRF pin to be bypassed in test environments. This is intentional, mirrors the existing Telegram test-seam pattern, and is not exploitable via any API vector. Documented for audit trail completeness.


5. Summary

Check Result
Backend compilation (go build ./...) PASS
Backend unit tests (go test ./...) PASS
Backend linting (make lint-fast) PASS
Frontend TypeScript (tsc --noEmit) PASS
Frontend unit tests (vitest run) PASS (after FE-001 fix)
Pre-commit hooks ⚠️ N/A (project uses Lefthook)
Trivy filesystem scan PASS — 0 vulns, 0 secrets
Regression — services package PASS
Regression — handlers package PASS
Token field json:"-" guard SECURE
SSRF hostname pin (api.pushover.net) SECURE
Template injection guard SECURE
Emergency priority=2 rejection CORRECT
Test() write-only guard (Pushover + Telegram) CORRECT
pushoverAPIBaseURL API inaccessibility SECURE

Critical/High security findings: 0 Total findings: 4 (1 fixed, 3 informational coverage gaps)

The Pushover notification provider implementation is secure and functionally correct. The one blocking defect (FE-001) was identified and resolved during this audit. The three remaining findings are non-blocking coverage gaps with no security impact and no CVE surface.