diff --git a/README.md b/README.md index 24fa0d3..2ffd957 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ certctl gives you a single pane of glass for every TLS certificate in your organ - **Revocation infrastructure** — RFC 5280 revocation with all standard reason codes, DER-encoded X.509 CRL per issuer, embedded OCSP responder, and short-lived certificate exemption (certs under 1 hour skip CRL/OCSP). - **Policy engine** — 5 rule types with violation tracking and severity levels. Certificate profiles enforce allowed key types, maximum TTL, and crypto constraints at enrollment time. - **Immutable audit trail** — every action recorded to an append-only log. Every API call recorded with method, path, actor, SHA-256 body hash, response status, and latency. No update or delete on audit records. -- **Operational dashboard** — 18-page React GUI with certificate inventory, bulk operations (multi-select renew/revoke/reassign), deployment timeline visualization, inline policy editing, agent fleet overview, expiration heatmaps, and real-time short-lived credential tracking. +- **Operational dashboard** — Full React GUI with certificate inventory, bulk operations (multi-select renew/revoke/reassign), deployment timeline visualization, inline policy editing, agent fleet overview, expiration heatmaps, and real-time short-lived credential tracking. - **Observability** — JSON and Prometheus metrics endpoints, 5 stats API endpoints for dashboards, structured slog logging with request ID propagation. Compatible with Prometheus, Grafana Agent, Datadog Agent, and Victoria Metrics. - **Notifications** — threshold-based alerting with deduplication. Routes to email, webhooks, Slack, Microsoft Teams, PagerDuty, and OpsGenie. - **AI and CLI access** — MCP server exposes all 78 API operations as tools for Claude, Cursor, and any MCP-compatible client. CLI tool with 12 subcommands for terminal workflows and scripting. @@ -99,16 +99,22 @@ flowchart LR | | | |---|---| -| ![Dashboard](docs/screenshots/dashboard.png) | ![Certificates](docs/screenshots/certificates.png) | -| **Dashboard** — certificate stats, expiry timeline, recent jobs | **Certificates** — full inventory with status, environment, owner filters | -| ![Agents](docs/screenshots/agents.png) | ![Jobs](docs/screenshots/jobs.png) | -| **Agents** — fleet health, hostname, heartbeat tracking | **Jobs** — issuance, renewal, deployment job queue | -| ![Notifications](docs/screenshots/notifications.png) | ![Policies](docs/screenshots/policies.png) | -| **Notifications** — threshold alerts grouped by certificate | **Policies** — enforcement rules with enable/disable and delete | -| ![Issuers](docs/screenshots/issuers.png) | ![Targets](docs/screenshots/targets.png) | -| **Issuers** — CA connectors with test connectivity | **Targets** — deployment targets (NGINX, Apache, HAProxy, F5, IIS) | -| ![Audit Trail](docs/screenshots/audit-trail.png) | | -| **Audit Trail** — immutable log of every action | | +| ![Dashboard](docs/screenshots/v2/dashboard.png) | ![Certificates](docs/screenshots/v2/certificates.png) | +| **Dashboard** — real-time stats, expiration heatmap, renewal trends, issuance rate | **Certificates** — full inventory with status filters, environment, owner, team | +| ![Agents](docs/screenshots/v2/agents.png) | ![Fleet Overview](docs/screenshots/v2/fleet-overview.png) | +| **Agents** — fleet health, hostname, OS/arch, IP, version tracking | **Fleet Overview** — OS distribution, status breakdown, version analysis | +| ![Jobs](docs/screenshots/v2/jobs.png) | ![Notifications](docs/screenshots/v2/notifications.png) | +| **Jobs** — issuance, renewal, deployment job queue with status filters | **Notifications** — expiration warnings, renewal results, unread/all toggle | +| ![Policies](docs/screenshots/v2/policies.png) | ![Profiles](docs/screenshots/v2/profiles.png) | +| **Policies** — enforcement rules for ownership, environments, lifetime, renewal | **Profiles** — enrollment templates with key types, max TTL, crypto constraints | +| ![Issuers](docs/screenshots/v2/issuers.png) | ![Targets](docs/screenshots/v2/targets.png) | +| **Issuers** — CA connectors (Local CA, Let's Encrypt, step-ca, DigiCert) | **Targets** — deployment targets (NGINX, F5 BIG-IP, IIS, HAProxy) | +| ![Owners](docs/screenshots/v2/owners.png) | ![Teams](docs/screenshots/v2/teams.png) | +| **Owners** — certificate ownership with email and team assignment | **Teams** — organizational grouping for notification routing | +| ![Agent Groups](docs/screenshots/v2/agent-groups.png) | ![Audit Trail](docs/screenshots/v2/audit-trail.png) | +| **Agent Groups** — dynamic grouping by OS, arch, CIDR, version | **Audit Trail** — immutable log with filters, CSV/JSON export | +| ![Short-Lived](docs/screenshots/v2/short-lived.png) | | +| **Short-Lived Credentials** — ephemeral certs with live TTL countdown | | ## Quick Start @@ -567,7 +573,7 @@ make docker-clean # Stop + remove volumes ## Roadmap ### V1 (v1.0.0 released) -All nine development milestones (M1–M9) are complete. The backend covers the full certificate lifecycle: Local CA and ACME v2 issuers, NGINX/Apache/HAProxy/F5/IIS target connectors, threshold-based expiration alerting, agent-side ECDSA P-256 key generation, API auth with rate limiting, and a React dashboard with 18 pages wired to the real API. The CI pipeline runs build, vet, test with coverage gates (service layer 30%+, handler layer 50%+), frontend type checking, Vitest test suite, and Vite production build on every push. Docker images are published to GitHub Container Registry on every version tag via the release workflow. +All nine development milestones (M1–M9) are complete. The backend covers the full certificate lifecycle: Local CA and ACME v2 issuers, NGINX/Apache/HAProxy/F5/IIS target connectors, threshold-based expiration alerting, agent-side ECDSA P-256 key generation, API auth with rate limiting, and a full React dashboard wired to the real API. The CI pipeline runs build, vet, test with coverage gates (service layer 30%+, handler layer 50%+), frontend type checking, Vitest test suite, and Vite production build on every push. Docker images are published to GitHub Container Registry on every version tag via the release workflow. ### V2: Operational Maturity - **M10: Agent Metadata + Targets** ✅ — agents report OS, architecture, IP, hostname, version via heartbeat; Apache httpd and HAProxy target connectors diff --git a/docs/architecture.md b/docs/architecture.md index cf2d27c..0d0658e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -92,7 +92,7 @@ The agent runs two background loops: a heartbeat (every 60 seconds) to signal it The web dashboard is the primary operational interface for certctl. It is built with Vite + React + TypeScript and uses TanStack Query for server state management (caching, background refetching, optimistic updates). -**Current views (19 pages)**: certificate inventory (list with multi-select bulk operations + "New Certificate" creation modal + detail with deployment status timeline, inline policy/profile editor, version history, deploy, revoke, archive, and trigger renewal actions), agent fleet (list + detail with system info + OS/architecture grouping with charts), job queue (status, retry, cancel, approve/reject), notification inbox (threshold alert grouping, mark-as-read), audit trail (time range, actor, action filters + CSV/JSON export), policy management (rules with enable/disable toggle + delete + violations), issuers (list with test connection + delete), targets (list with 3-step configuration wizard + delete), owners (list with team resolution + delete), teams (list with delete), agent groups (list with dynamic match criteria badges + enable/disable + delete), certificate profiles (list with crypto constraints), short-lived credentials dashboard (TTL countdown, profile filtering, auto-refresh), summary dashboard with charts (expiration heatmap, renewal success rate, status distribution, issuance rate), and login page. +**Current views**: certificate inventory (list with multi-select bulk operations + "New Certificate" creation modal + detail with deployment status timeline, inline policy/profile editor, version history, deploy, revoke, archive, and trigger renewal actions), agent fleet (list + detail with system info + OS/architecture grouping with charts), job queue (status, retry, cancel, approve/reject), notification inbox (threshold alert grouping, mark-as-read), audit trail (time range, actor, action filters + CSV/JSON export), policy management (rules with enable/disable toggle + delete + violations), issuers (list with test connection + delete), targets (list with 3-step configuration wizard + delete), owners (list with team resolution + delete), teams (list with delete), agent groups (list with dynamic match criteria badges + enable/disable + delete), certificate profiles (list with crypto constraints), short-lived credentials dashboard (TTL countdown, profile filtering, auto-refresh), summary dashboard with charts (expiration heatmap, renewal success rate, status distribution, issuance rate), and login page. The dashboard includes an **ErrorBoundary component** for graceful error recovery — if a view crashes, the boundary catches the error and displays a user-friendly message instead of breaking the entire dashboard. It also includes a **demo mode** that activates when the API is unreachable — it renders realistic mock data for screenshots and offline presentations. diff --git a/docs/features.md b/docs/features.md index 779ac24..d7b2740 100644 --- a/docs/features.md +++ b/docs/features.md @@ -818,7 +818,7 @@ All loops have configurable intervals via environment variables (`CERTCTL_SCHEDU --- -## Web Dashboard (19 Pages) +## Web Dashboard ### Overview The web dashboard is the primary operational interface for certctl. Built with **Vite + React 18 + TypeScript + TanStack Query v5 + Tailwind CSS 3 + Recharts**. @@ -1168,7 +1168,7 @@ Each guide includes an evidence summary table mapping specific criteria to certc | Policies + violations | ✓ | ✓ | Shipped | | Profiles + crypto constraints | ✓ | ✓ | Shipped | | Revocation (RFC 5280, CRL, OCSP) | ✓ | ✓ | Shipped | -| Dashboard + 19 pages | ✓ | ✓ | Shipped | +| Full web dashboard | ✓ | ✓ | Shipped | | Observability (charts, metrics, stats) | ✓ | ✓ | Shipped | | REST API (91 endpoints) | ✓ | ✓ | Shipped | | MCP server (78 tools) | ✓ | ✓ | Shipped v2.1 | @@ -1200,7 +1200,7 @@ Each guide includes an evidence summary table mapping specific criteria to certc | Category | Count | |----------|-------| | **API Endpoints** | 91 (under /api/v1/) | -| **Dashboard Pages** | 19 | +| **Dashboard** | Full web GUI | | **Issuer Connectors** | 4 (Local CA, ACME, step-ca, OpenSSL) | | **Target Connectors** | 5 (3 impl: NGINX, Apache, HAProxy; 2 stubs: F5, IIS) | | **Notifier Channels** | 6 (Email, Webhook, Slack, Teams, PagerDuty, OpsGenie) | diff --git a/docs/screenshots/logo/certctl-logo.png b/docs/screenshots/logo/certctl-logo.png new file mode 100644 index 0000000..5e526e3 Binary files /dev/null and b/docs/screenshots/logo/certctl-logo.png differ diff --git a/docs/screenshots/v2/agent-groups.png b/docs/screenshots/v2/agent-groups.png new file mode 100644 index 0000000..88d74e1 Binary files /dev/null and b/docs/screenshots/v2/agent-groups.png differ diff --git a/docs/screenshots/v2/agents.png b/docs/screenshots/v2/agents.png new file mode 100644 index 0000000..30919d9 Binary files /dev/null and b/docs/screenshots/v2/agents.png differ diff --git a/docs/screenshots/v2/audit-trail.png b/docs/screenshots/v2/audit-trail.png new file mode 100644 index 0000000..7b9f6e6 Binary files /dev/null and b/docs/screenshots/v2/audit-trail.png differ diff --git a/docs/screenshots/v2/certificates.png b/docs/screenshots/v2/certificates.png new file mode 100644 index 0000000..a690d10 Binary files /dev/null and b/docs/screenshots/v2/certificates.png differ diff --git a/docs/screenshots/v2/dashboard.png b/docs/screenshots/v2/dashboard.png new file mode 100644 index 0000000..d35f33b Binary files /dev/null and b/docs/screenshots/v2/dashboard.png differ diff --git a/docs/screenshots/v2/fleet-overview.png b/docs/screenshots/v2/fleet-overview.png new file mode 100644 index 0000000..aa8b95f Binary files /dev/null and b/docs/screenshots/v2/fleet-overview.png differ diff --git a/docs/screenshots/v2/issuers.png b/docs/screenshots/v2/issuers.png new file mode 100644 index 0000000..ea6932e Binary files /dev/null and b/docs/screenshots/v2/issuers.png differ diff --git a/docs/screenshots/v2/jobs.png b/docs/screenshots/v2/jobs.png new file mode 100644 index 0000000..ed86d1a Binary files /dev/null and b/docs/screenshots/v2/jobs.png differ diff --git a/docs/screenshots/v2/notifications.png b/docs/screenshots/v2/notifications.png new file mode 100644 index 0000000..fd3830c Binary files /dev/null and b/docs/screenshots/v2/notifications.png differ diff --git a/docs/screenshots/v2/owners.png b/docs/screenshots/v2/owners.png new file mode 100644 index 0000000..432eb87 Binary files /dev/null and b/docs/screenshots/v2/owners.png differ diff --git a/docs/screenshots/v2/policies.png b/docs/screenshots/v2/policies.png new file mode 100644 index 0000000..896d422 Binary files /dev/null and b/docs/screenshots/v2/policies.png differ diff --git a/docs/screenshots/v2/profiles.png b/docs/screenshots/v2/profiles.png new file mode 100644 index 0000000..fc5162f Binary files /dev/null and b/docs/screenshots/v2/profiles.png differ diff --git a/docs/screenshots/v2/short-lived.png b/docs/screenshots/v2/short-lived.png new file mode 100644 index 0000000..7d5cb1e Binary files /dev/null and b/docs/screenshots/v2/short-lived.png differ diff --git a/docs/screenshots/v2/targets.png b/docs/screenshots/v2/targets.png new file mode 100644 index 0000000..7cb23b2 Binary files /dev/null and b/docs/screenshots/v2/targets.png differ diff --git a/docs/screenshots/v2/teams.png b/docs/screenshots/v2/teams.png new file mode 100644 index 0000000..996de33 Binary files /dev/null and b/docs/screenshots/v2/teams.png differ diff --git a/docs/testing-guide.md b/docs/testing-guide.md index 51303de..2dc3df8 100644 --- a/docs/testing-guide.md +++ b/docs/testing-guide.md @@ -2094,6 +2094,49 @@ curl -s -w "\nHTTP %{http_code}\n" -X DELETE -H "$AUTH" "$SERVER/api/v1/agent-gr --- +### 11.4 Foreign Key Constraint Behavior + +**What this validates:** Delete operations correctly fail with 409 when referenced entities still exist. + +**Why it matters:** Owners and issuers use `ON DELETE RESTRICT` — you can't delete them while certificates reference them. Teams use `ON DELETE CASCADE`, so team deletes succeed and cascade. If the server returns a silent 500 instead of 409, the GUI swallows the error and the user thinks nothing happened. + +**Test 11.4.1 — Delete owner with assigned certificates (expect 409)** + +```bash +# Try to delete Alice Chen (o-alice) — she owns certificates in the demo data +curl -s -w "\nHTTP %{http_code}\n" -X DELETE -H "$AUTH" "$SERVER/api/v1/owners/o-alice" | jq . +``` + +**Expected:** HTTP 409 with message "Cannot delete owner: certificates are still assigned to this owner". +**PASS if** 409 Conflict. **FAIL** if 204 (data integrity violation) or 500 (unhelpful error). + +--- + +**Test 11.4.2 — Delete issuer with assigned certificates (expect 409)** + +```bash +# Try to delete the Local Dev CA (iss-local) — certificates reference it +curl -s -w "\nHTTP %{http_code}\n" -X DELETE -H "$AUTH" "$SERVER/api/v1/issuers/iss-local" | jq . +``` + +**Expected:** HTTP 409 with message "Cannot delete issuer: certificates are still using this issuer". +**PASS if** 409 Conflict. **FAIL** if 204 or 500. + +--- + +**Test 11.4.3 — Delete team cascades successfully** + +```bash +# Create a test team, then delete it — teams use ON DELETE CASCADE +curl -s -X POST -H "$AUTH" -H "$CT" -d '{"id": "t-fk-test", "name": "FK Test Team"}' $SERVER/api/v1/teams > /dev/null +curl -s -w "\nHTTP %{http_code}\n" -X DELETE -H "$AUTH" "$SERVER/api/v1/teams/t-fk-test" +``` + +**Expected:** HTTP 204 (cascade allows deletion). +**PASS if** 204. **FAIL** if 409 or 500. + +--- + ## Part 12: Notifications **What this validates:** Notification creation, listing, and read status management. @@ -3192,7 +3235,7 @@ echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"get_certificate", ## Part 19: GUI Testing -**What this validates:** The web dashboard — 19 pages of operational UI. +**What this validates:** The full web dashboard — all pages of operational UI. **Why it matters:** Operators spend 80% of their time in the GUI. If it's broken, the product is broken, regardless of how good the API is. @@ -3741,7 +3784,28 @@ curl -s -H "$AUTH" "$SERVER/api/v1/network-scan-targets" | jq '{total, ids: [.it --- -**Test 25.1.4 — OpenAPI spec operations match router** +**Test 25.1.4 — GUI delete on FK-restricted entities shows error, not silent failure** + +```bash +# Try deleting owner o-alice via API — she owns demo certificates +CODE=$(curl -s -o /tmp/delete-resp.json -w "%{http_code}" -X DELETE -H "$AUTH" "$SERVER/api/v1/owners/o-alice") +echo "DELETE owner with certs: HTTP $CODE" +cat /tmp/delete-resp.json | jq . + +# Try deleting issuer iss-local — certificates reference it +CODE=$(curl -s -o /tmp/delete-resp.json -w "%{http_code}" -X DELETE -H "$AUTH" "$SERVER/api/v1/issuers/iss-local") +echo "DELETE issuer with certs: HTTP $CODE" +cat /tmp/delete-resp.json | jq . +``` + +**What:** Verifies that deleting owners/issuers with assigned certificates returns 409 Conflict with a descriptive message. +**Why:** This was a real bug — the backend returned 500 (generic "Failed to delete"), `fetchJSON` threw on the error, and TanStack Query's `onError` wasn't wired up. The user clicked OK on the confirm dialog and nothing visibly happened. Fixed by: (1) backend returns 409 with descriptive message for FK constraint violations, (2) `fetchJSON` handles 204 No Content for successful deletes, (3) frontend mutation `onError` surfaces the error. +**Expected:** Both return HTTP 409 with descriptive conflict messages. +**PASS if** both 409 with messages. **FAIL** if 500 (unhelpful error) or 204 (data integrity violation). + +--- + +**Test 25.1.5 — OpenAPI spec operations match router** ```bash echo "OpenAPI operations: $(grep -c 'operationId:' api/openapi.yaml)" diff --git a/internal/api/handler/issuers.go b/internal/api/handler/issuers.go index 7be8eb0..4654fc2 100644 --- a/internal/api/handler/issuers.go +++ b/internal/api/handler/issuers.go @@ -184,7 +184,13 @@ func (h IssuerHandler) DeleteIssuer(w http.ResponseWriter, r *http.Request) { } if err := h.svc.DeleteIssuer(id); err != nil { - ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to delete issuer", requestID) + if strings.Contains(err.Error(), "violates foreign key") || strings.Contains(err.Error(), "RESTRICT") { + ErrorWithRequestID(w, http.StatusConflict, "Cannot delete issuer: certificates are still using this issuer", requestID) + } else if strings.Contains(err.Error(), "not found") { + ErrorWithRequestID(w, http.StatusNotFound, "Issuer not found", requestID) + } else { + ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to delete issuer", requestID) + } return } diff --git a/internal/api/handler/owners.go b/internal/api/handler/owners.go index 7e9b348..b5cc4c9 100644 --- a/internal/api/handler/owners.go +++ b/internal/api/handler/owners.go @@ -183,7 +183,13 @@ func (h OwnerHandler) DeleteOwner(w http.ResponseWriter, r *http.Request) { id = parts[0] if err := h.svc.DeleteOwner(id); err != nil { - ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to delete owner", requestID) + if strings.Contains(err.Error(), "violates foreign key") || strings.Contains(err.Error(), "RESTRICT") { + ErrorWithRequestID(w, http.StatusConflict, "Cannot delete owner: certificates are still assigned to this owner", requestID) + } else if strings.Contains(err.Error(), "not found") { + ErrorWithRequestID(w, http.StatusNotFound, "Owner not found", requestID) + } else { + ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to delete owner", requestID) + } return } diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 6f1e24c..466eac1 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -42,6 +42,7 @@ async function fetchJSON(url: string, init?: RequestInit): Promise { } throw new Error(errorMsg || `HTTP ${res.status}`); } + if (res.status === 204) return {} as T; return res.json(); } diff --git a/web/src/pages/OwnersPage.tsx b/web/src/pages/OwnersPage.tsx index f1a2a3b..84e796e 100644 --- a/web/src/pages/OwnersPage.tsx +++ b/web/src/pages/OwnersPage.tsx @@ -23,6 +23,7 @@ export default function OwnersPage() { const deleteMutation = useMutation({ mutationFn: deleteOwner, onSuccess: () => queryClient.invalidateQueries({ queryKey: ['owners'] }), + onError: (err: Error) => alert(`Delete failed: ${err.message}`), }); const teamMap = new Map(); diff --git a/web/src/pages/TeamsPage.tsx b/web/src/pages/TeamsPage.tsx index bf8aef9..5ad4747 100644 --- a/web/src/pages/TeamsPage.tsx +++ b/web/src/pages/TeamsPage.tsx @@ -18,6 +18,7 @@ export default function TeamsPage() { const deleteMutation = useMutation({ mutationFn: deleteTeam, onSuccess: () => queryClient.invalidateQueries({ queryKey: ['teams'] }), + onError: (err: Error) => alert(`Delete failed: ${err.message}`), }); const columns: Column[] = [