Risk Backlog
Owner: Backend Lead Last Edited: March 26, 2026 Last Reviewed: March 26, 2026
How to Use This
Each item has a severity, category, and effort estimate. Work through these in severity order. When an item is resolved, mark it resolved with a date rather than deleting it — the history is useful.
Severity:
- High — meaningful security or operational risk; address in the next sprint
- Medium — real risk but limited blast radius or requires specific conditions
- Low — quality/hygiene issue or SOC 2 evidence gap with no direct attack surface
Security Risks
HIGH
RISK-S01: Branch protection not enforced (GitHub plan limitation)
Category: Change management
Detail: Branch protection rules (required reviews, required CI checks, no force push) cannot be enforced on private org repos without a paid GitHub org plan (Team/Enterprise). The protect script exists at .github/scripts/enforce-branch-protection.sh but cannot be applied until the org plan is upgraded.
Impact: PRs can be merged without review or passing CI; force pushes to main/qa are possible.
Fix: Upgrade the GitHub org (Magi-Reseach) to Team/Enterprise plan, then run the protect script. See docs/ops/go-backend-runbook.md Section 13.
Status: Open
RISK-S02: No frontend org-isolation regression tests
Category: Test coverage / data isolation
Detail: The Go backend has cross-org denial regression tests (org_isolation_test.go) for comps, TIMs, and reports. However, the frontend has no equivalent tests verifying that API responses don't leak data from wrong orgs. This is primarily a backend concern, but the frontend also constructs API requests with org context from Clerk — bugs there could result in wrong-org requests.
Impact: If a frontend bug sends the wrong org context, there is no automated test catching it.
Fix: Add frontend integration tests that mock the API and verify only the correct org's data is rendered. Alternatively, expand backend handler tests to cover tenant/owner/membership cross-org denial.
Status: Open
RISK-S03: ~70% of handlers and ~75% of services lack tests
Category: Test coverage
Detail: As of February 2026 (BACKEND_RISK_AND_TEST_REVIEW.md), the following handlers had no tests: export, admin, report, apikey, membership, building, comp, tim, geography, lookup, vacancy, labor blocks, broker assignment, building org override, leasing company, broker, audit log, building logistics, key point, tenant, owner, building park. Many have since had tests added, but coverage is still partial.
Impact: Regressions — including security regressions — in untested handlers go undetected until manual QA or production.
Fix: Work through the handler list and add at minimum: scope contract tests, happy-path tests, and 404/403 for cross-org and missing resources. Prioritize handlers that touch sensitive org-scoped data.
Status: Resolved 2026-03-26 — all previously untested handlers now covered: audit_log, broker, leasing_company, building_org_override, broker_assignment, labor_block_group. Each has scope contract, happy-path, 404/403, and org-isolation tests.
RISK-S04: All PostgreSQL repositories lack direct tests
Category: Test coverage / data layer Detail: 31 Postgres repository files have no direct tests. The mock repositories used in handler/service tests simulate behavior but do not test the actual SQL. Impact: SQL bugs (wrong WHERE clause, missing org filter, incorrect JOIN) are not caught until integration or production. Fix: Add integration tests that run against a real (test) database. This requires a test DB setup — either a local Docker Postgres or a GitHub Actions Postgres service container. Status: Open
MEDIUM
RISK-S05: API key prefix may not be uniquely identifying
Category: Operational security
Detail: API key generation uses key_prefix := plainKey[:8] (see go-backend/internal/service/apikey_service.go). Because keys are prefixed with prism_live_ (9 chars), the stored prefix is always prism_li, which does not help humans identify which key is which.
Impact: If a key is accidentally logged or exposed in a partial form, the prefix provides no identification value. Operators cannot tell keys apart from the stored prefix alone.
Fix: Derive the stored prefix from the random segment rather than the full key (e.g. plainKey[9:17] to skip the prism_live_ prefix).
Status: Resolved 2026-03-26 — changed to plainKey[11:19] to skip the full prism_live_ prefix (11 chars); stored prefix now reflects the random segment.
RISK-S06: Import staging files not cleaned up on job completion
Category: Data hygiene / disk usage
Detail: CSV files uploaded for import jobs are staged at IMPORT_UPLOAD_DIR. There is no evidence in the codebase of a cleanup step that removes processed files after an import job completes or fails.
Impact: Over time, the import directory accumulates CSV files on disk. On a long-running server, this could exhaust disk space. The files also contain customer data at rest in plaintext on the server filesystem.
Fix: Add a cleanup step in the import job completion path to delete the staged file after processing. Log the deletion.
Status: Resolved (pre-existing) — Process() in import_job_service.go already calls os.Remove(filePath) after job completion. Confirmed 2026-03-26 during audit review.
RISK-S07: Audit log coverage gaps for PATCH/partial update operations
Category: Auditability
Detail: The audit middleware covers POST, PUT, and DELETE on sensitive route prefixes. If any handlers use PATCH for partial updates, those are not currently captured.
Impact: Partial updates to sensitive records may not appear in the audit log.
Fix: Audit the handler list for any PATCH routes on sensitive resources. Either add PATCH to the audited methods, or confirm no sensitive PATCH routes exist.
Status: Resolved 2026-03-26 — added PATCH to isSensitiveOperation in audit.go; mapped to action partial_update. One sensitive PATCH route confirmed: PATCH /api/v1/tims/{id}/status.
RISK-S08: No log retention policy enforced
Category: SOC 2 / compliance Detail: Application logs and audit logs are produced but there is no documented or enforced retention period. SOC 2 typically expects at least 1 year of log retention. Impact: Logs needed for incident investigation or audit evidence may be purged before they are needed. Fix: Define retention periods (application logs: 90 days minimum; audit logs: 1 year minimum). Configure the managed logging service or DB to enforce these. Status: Open
RISK-S09: No vendor inventory or DPA tracking
Category: SOC 2 / vendor risk
Detail: The SOC 2 readiness assessment identified the following likely in-scope vendors with no documented review: Clerk, Supabase/RDS, Redis provider, Cloudflare, Slack, GitHub, monitoring vendors. No vendor inventory, DPAs, or security review records exist in the repo.
Impact: SOC 2 Type I requires demonstrating vendor risk management. Customer contracts may also require a subprocessor list.
Fix: Create docs/security/vendor-inventory.md listing each vendor, their data access, contract/DPA status, and last review date.
Status: Open
RISK-S10: No formal risk register or control ownership model
Category: SOC 2 / governance Detail: SOC 2 requires a named control owner for each control domain and a formal risk assessment. Neither exists in a structured form. Fix: Create a lightweight control library with named owners. Assign owners for: security, ops, app engineering, access administration, compliance evidence. Conduct an initial risk assessment and record it. Status: Open
LOW
RISK-S11: No current data import runbook
Category: Documentation / operations
Detail: Legacy import docs exist under docs/legacy/ but they describe a prior Python/Supabase workflow. The current Go-based import job system has no non-legacy runbook.
Fix: Write docs/data-import/README.md describing the current import path (upload CSV via /api/v1/import-jobs, queue for processing, monitor status).
Status: Open
RISK-S12: Marketing app has no deploy/security runbook
Category: Documentation / operations
Detail: docs/marketing/README.md covers quick start and env vars, but there is no staging/prod deploy runbook, no Turnstile key rotation procedure, and no security hardening guidance for the marketing app.
Fix: Write docs/marketing/deploy-runbook.md covering prod env vars, Turnstile keys, analytics IDs, caching strategy, and security headers.
Status: Open
RISK-S13: No monorepo env-var matrix
Category: Documentation / operations
Detail: No single document lists all environment variables across frontend/, go-backend/, and marketing/ with their purpose, required/optional status, and whether they are safe to expose.
Fix: Write docs/config/env-vars.md as a single reference table.
Status: Open
RISK-S14: Duplicate schema files risk future divergence
Category: Documentation / schema hygiene
Detail: database/PRISM vNext.sql and database/PRISM_vNext_FINAL.sql were identical as of February 14, 2026 (docs/audit/documentation-audit-2026-02-14.md). Having two files increases the risk that they diverge silently in the future.
Fix: Delete the duplicate file or add a CI check that asserts both files are identical. Document which is canonical.
Status: Open
RISK-S15: No export refresh scheduling runbook
Category: Documentation / operations
Detail: docs/backend/export-layer.md covers the export schema objects and refresh functions, but there is no runbook for scheduling export refreshes (pg_cron vs external scheduler) per environment.
Fix: Add a section to the export layer doc or production runbook describing how and when export refreshes are triggered in each environment.
Status: Open
Operational Evidence Gaps (SOC 2 Type I prerequisite)
These are not code or doc gaps — they are evidence that must be generated by operating the system. Listed here so nothing gets missed.
| Gap | Target | Owner |
|---|---|---|
| Monthly access review records | docs/audit/access-reviews/ | Admin |
| Restore drill records (quarterly) | docs/audit/restore-drills/ | Ops |
| Vulnerability log entries | docs/security/vulnerability-management.md | Backend Lead |
| Incident tabletop exercise | docs/audit/incidents/tabletop-YYYY-MM-DD.md | Backend Lead |
| MFA enforcement screenshots (GitHub, Clerk, Cloudflare) | docs/audit/screenshots/ | Admin |
| Branch protection config export | docs/audit/screenshots/ | Admin |
| Backup job records export | docs/audit/screenshots/ | Ops |
| Secret rotation log | docs/security/secrets-management.md Section 5 | Ops |
Resolved Items (for reference)
| ID | Description | Resolved date |
|---|---|---|
| SEC-001 | Import job status mass assignment / workflow bypass | 2026-02-17 |
| SEC-002 | BI credentials returned in API response | 2026-02-17 |
| SEC-003 | Dashboard SQL string concatenation (injection risk) | 2026-02-17 |
| SEC-004 | JWKS URL SSRF risk | 2026-02-17 |
| SEC-005 | Public /ready endpoint | 2026-02-17 |
| SEC-006 | Swagger and metrics unprotected in non-production | 2026-02-17 |
| SEC-007 | Default ALLOWED_ORIGINS in production | Deferred to go-live config |
| SEC-008 | Search ILIKE wildcard abuse | 2026-02-17 |
| — | Frontend CI (no automated typecheck/lint/test on PRs) | 2026-03-26 |
| — | No CI security automation (Dependabot, CodeQL, secret scanning, dep review) | 2026-03-26 |
| — | Audit log missing sensitive routes (memberships, imports, exports, operations, admin) | 2026-03-26 |
| — | No cross-org denial regression tests | 2026-03-26 |
| RISK-S05 | API key prefix always prism_li (non-identifying) | 2026-03-26 |
| RISK-S06 | Import staging files not cleaned up (pre-existing fix confirmed) | 2026-03-26 |
| RISK-S07 | PATCH routes not captured by audit middleware | 2026-03-26 |
| — | actor_user_id filter in audit log handler had no length cap (unbounded string passed to query) | 2026-03-26 |
| — | radius_miles in labor block group handler had no upper bound (could trigger oversized spatial queries); lat/lng had no range validation | 2026-03-26 |
| RISK-S03 | Handler test coverage gaps (6 handlers untested) | 2026-03-26 |