proposal-system/AUDIT-REPORT.md

249 lines
11 KiB
Markdown
Raw Normal View History

# Proposal System — Production Readiness Audit Report
**Date:** 2026-05-27
**Auditor:** Claude Code (6 parallel specialist agents)
**Scope:** Full monorepo — API, Web, Mobile, Lambdas, Infrastructure/CI/CD, QA/Testing
---
## Executive Summary
The Proposal System has a solid architectural foundation with clean separation of concerns, proper Cognito JWT auth at the API Gateway layer, encrypted RDS, and a working end-to-end flow. However, the audit uncovered **5 Critical**, **36 High**, **75+ Medium**, and **60+ Low** severity findings across all layers.
**The most urgent issues:**
1. **Internal API key middleware applies globally** — any endpoint can be accessed with the internal key, bypassing JWT auth entirely (API Critical)
2. **JWT validation skipped when Authority not configured** — tokens are parsed but signatures are never verified (API Critical)
3. **Lambda Function URL has AUTH_NONE** — publicly accessible, bypassing API Gateway authorization (Infra/Lambda Critical)
4. **JWT stored in localStorage** — vulnerable to XSS token theft (Web Critical)
5. **Zero test coverage across entire monorepo** — no test files, no test frameworks, CI passes with zero tests (QA Critical)
6. **DevMode has no environment guard** — if `Auth:DevMode=true` leaks to production, anyone can mint tokens for any role (API High)
---
## Findings by Domain
### 1. API Security (2 Critical, 8 High, 14 Medium, 13 Low)
#### Critical
| ID | Finding | File | Fix |
|----|---------|------|-----|
| API-C1 | Internal API key middleware applies to ALL routes, not scoped to internal paths. Any request with the key bypasses JWT and gets admin role. | `Middleware/InternalApiKeyMiddleware.cs:22-48` | Scope to internal-only paths via path check |
| API-C2 | Auth callback skips JWT signature validation when `Auth:Authority` is empty — tokens are parsed but never verified. `appsettings.json` defaults to `""`. | `Controllers/AuthController.cs:70-73` | Throw on empty authority in non-dev mode; remove unvalidated path |
#### High
| ID | Finding | File |
|----|---------|------|
| API-H1 | Invalid API key does not short-circuit — request continues through normal auth pipeline | `InternalApiKeyMiddleware.cs:42-47` |
| API-H2 | Auth callback `redirectUri` not validated server-side | `AuthController.cs:31-124` |
| API-H3 | `UpdateProposalRequest` exposes `Status` field — over-posting risk | `ProposalDtos.cs:22`, `ProposalService.cs:178-180` |
| API-H4 | No validator for `UpdateProposalRequest` — unlimited string lengths | `Validators/` (missing file) |
| API-H5 | `InvalidOperationException` messages leaked to clients | `GlobalExceptionHandler.cs:60-67` |
| API-H6 | No structured logging in any service or controller | All services |
| API-H7 | No Swagger/OpenAPI configuration at all | `Program.cs`, `.csproj` |
| API-H8 | DevMode controlled solely by config — no `IsDevelopment()` guard | `Program.cs:20` |
#### Medium
| ID | Finding |
|----|---------|
| API-M1 | Internal API key always grants `admins` role, never `sysadmins` |
| API-M2 | Silent auth failure when neither Cognito nor DevMode configured |
| API-M3 | Dispatchers can read any proposal's line items (no ownership check) |
| API-M4 | Dispatchers can access PDF endpoints for any proposal |
| API-M5 | Missing validators for VendorProposal, GeneratedPdf, SimilarReference DTOs |
| API-M6 | No file size validation on presigned upload URLs |
| API-M7 | No `.AsNoTracking()` on read-only queries |
| API-M8 | BulkUpdate uses delete-all/insert-all without explicit transaction |
| API-M9 | Dev PDF generation leaks stderr to client |
| API-M10 | Auth callback reveals config state in error responses |
| API-M11 | Silent exception swallowing on audit logging (`catch { }`) |
| API-M12 | Audit trail does not capture before/after values |
| API-M13 | User role change audit does not log previous role |
| API-M14 | Dev signing key hardcoded in committed config |
---
### 2. Web Frontend (1 Critical, 6 High, 13 Medium, 8 Low)
#### Critical
| ID | Finding | File |
|----|---------|------|
| WEB-C1 | JWT token stored in localStorage — XSS token theft risk | `authSlice.ts:51,58`, `client.ts:14` |
#### High
| ID | Finding | File |
|----|---------|------|
| WEB-H1 | No token refresh mechanism — expired tokens cause abrupt redirect | `authSlice.ts:21-30` |
| WEB-H2 | ProtectedRoute doesn't check loading state — flash-redirect on hydration | `ProtectedRoute.tsx:4-12` |
| WEB-H3 | Dispatcher can view any proposal via direct URL | `App.tsx:40` |
| WEB-H4 | "View Access Roles" button does nothing | `App.tsx:57-59` |
| WEB-H5 | saveMutation has no onError; partial failure leaves inconsistent state | `AdminWorkspace.tsx:114-138` |
| WEB-H6 | approveMutation chains save→approve with no error recovery | `AdminWorkspace.tsx:140-154` |
| WEB-H7 | No React error boundary — blank white screen on crash | `main.tsx` |
#### Medium
| ID | Finding |
|----|---------|
| WEB-M1 | Dev login shown when client ID absent — verify API gate |
| WEB-M2 | 401 interceptor clears token but not Redux state |
| WEB-M3 | Proposal form accepts 1-char scope (no minimum) |
| WEB-M4 | ServiceCategory `Other` not in shared contract |
| WEB-M5 | `CreateProposalRequest` type diverges from shared contract |
| WEB-M6 | No file size/type validation on vendor PDF upload |
| WEB-M7 | AdminWorkspace shows no error state for failed fetch |
| WEB-M8 | Dashboard stats show zeros on fetch error |
| WEB-M9 | Missing loading state for line items |
| WEB-M10 | Proposal state transitions not guarded on client |
| WEB-M11 | `returnToReview` API method wired but never called from UI |
| WEB-M12 | Table rows not keyboard accessible |
| WEB-M13 | ToastContainer rendered outside RouterProvider |
---
### 3. Mobile (0 Critical, 4 High, 12 Medium, 11 Low)
#### High
| ID | Finding | File |
|----|---------|------|
| MOB-H1 | Offline queue race condition — no mutex, duplicate proposals | `useOfflineDraft.ts:63-94` |
| MOB-H2 | Conditional screen registration — push/deep links may crash | `RootNavigator.tsx:33-78` |
| MOB-H3 | Offline queue sync errors silently swallowed | `App.tsx:80` |
| MOB-H4 | Bulk line item update has no optimistic concurrency | `LineItemEditScreen.tsx:55-101` |
#### Medium
| ID | Finding |
|----|---------|
| MOB-M1-M5 | Token refresh gaps, queue processing blocks on first failure, no queue UI, processes on every network event |
| MOB-M6-M8 | Shared contract mismatches (poNumber, id field) |
| MOB-M9-M12 | Navigation UX, loading states, unhandled promise rejections, atob encoding |
---
### 4. Lambda Pipeline (1 Critical, 5 High, 14 Medium, 8 Low)
#### Critical
| ID | Finding | File |
|----|---------|------|
| LAM-C1 | Function URL `authType: NONE` — publicly accessible | `compute-stack.ts:241-243` |
#### High
| ID | Finding | File |
|----|---------|------|
| LAM-H1 | pdf-generate: `register_pdf` failure doesn't raise — PDF in S3 but not in DB | `pdf-generate/app.py:96-98` |
| LAM-H2 | pdf-extract: exception swallowed, SQS considers it success, no retry | `pdf-extract/app.py:79-83` |
| LAM-H3 | pdf-extract: missing `s3Key` silently skips without batch failure | `pdf-extract/app.py:54-55` |
| LAM-H4 | suggestions: duplicate SQS delivery overwrites admin-edited line items | `suggestions/app.py:232-289` |
| LAM-H5 | `_retry_request` can return undefined `resp` | All 4 Lambda files |
#### Medium
| ID | Finding |
|----|---------|
| LAM-M1-M4 | Event validation, prompt injection risk, PDF size limits, Bedrock timeout |
| LAM-M5-M8 | Missing stack traces, numeric validation, tight Lambda timeout, S3 key sanitization |
| LAM-M9-M14 | Stale API key cache, empty env var defaults, KB sync flooding, CDK bundling gaps |
---
### 5. Infrastructure & CI/CD (0 Critical, 5 High, 9 Medium, 10 Low)
#### High
| ID | Finding | File |
|----|---------|------|
| INF-H1 | Function URL `authType: NONE` | `compute-stack.ts:241-243` |
| INF-H2 | SQS queues have no encryption at rest | `foundation-stack.ts:162-174` |
| INF-H3 | OpenSearch Serverless allows public network access | `compute-stack.ts:61-70` |
| INF-H4 | No MFA configured on Cognito user pool | `foundation-stack.ts:177-194` |
| INF-H5 | No access logging on HTTP API Gateway | `compute-stack.ts:246-263` |
#### Medium
| ID | Finding |
|----|---------|
| INF-M1-M2 | Bedrock wildcard model ARN, AOSS `aoss:*` permissions |
| INF-M3-M4 | No Cognito advanced security, Google OAuth not in CDK |
| INF-M5-M7 | No S3 enforceSSL, no custom domain on CF, no WAF |
| INF-M8-M9 | Workflows pinned to @main, --require-approval never locally |
---
### 6. QA & Testing (6 Critical, 16 High)
**ZERO application-level test coverage across the entire monorepo.**
#### Critical Gaps
| ID | What's Untested | Risk |
|----|-----------------|------|
| QA-C1 | No test project in .NET solution | CI `dotnet test` is a no-op |
| QA-C2 | Proposal state machine | Invalid transitions undetectable |
| QA-C3 | Authorization enforcement | Privilege escalation undetectable |
| QA-C4 | InternalApiKeyMiddleware | Auth bypass regression risk |
| QA-C5 | ProtectedRoute and RoleGuard | Client-side auth unverified |
| QA-C6 | Mobile offline draft and queue | Data loss risk |
#### High Gaps
Validators, ProposalNumberGenerator, AuthController, LineItemService state guards, all frontend components, API client interceptors, all Lambda handlers, SQS event parsing, PDF parsers, suggestions logic, CI pipeline executes zero tests.
---
## Prioritized Remediation Plan
### Phase 1 — Critical Security Fixes
1. Scope internal API key middleware to internal-only routes
2. Guard JWT validation — reject when Authority not configured in non-dev
3. Add DevMode environment guard (`IsDevelopment()` required)
4. Make invalid API key reject request immediately (401)
5. Add React error boundary to web app
6. Fix ProtectedRoute loading state
### Phase 2 — High Security & Reliability Fixes
7. Remove `Status` from `UpdateProposalRequest`
8. Add `UpdateProposalValidator`
9. Sanitize error messages in GlobalExceptionHandler
10. Fix Lambda error propagation (pdf-extract, pdf-generate)
11. Add suggestions idempotency check
12. Fix `_retry_request` undefined variable
13. Fix AdminWorkspace mutation error handling
14. Fix dead "View Access Roles" button
15. Add mobile offline queue mutex and error handling
### Phase 3 — Swagger/OpenAPI
16. Add Swashbuckle and configure
17. Add JWT security definition
18. Add `[ProducesResponseType]` to all controllers
19. Gate Swagger UI to dev/staging
### Phase 4 — Infrastructure Hardening (CDK)
20. SQS encryption, API Gateway logging, Cognito MFA, S3 enforceSSL
### Phase 5 — Test Infrastructure (future)
25. .NET test project, vitest for web, pytest for Lambdas, CI integration
---
## Positive Findings
- RDS: private subnets, not publicly accessible, encrypted, deletion protection, 7-day backups
- Cognito: self-signup disabled (admin-created accounts only)
- CORS: properly scoped to production origin
- Secrets: production connection string uses Secrets Manager
- S3: all buckets have `BlockPublicAccess.BLOCK_ALL`
- CloudFront: OAC, HTTPS redirect, security headers, TLS 1.2 minimum
- GitHub Actions: OIDC (no long-lived credentials), minimal permissions
- Monitoring: alarms for DLQ depth, RDS metrics, Lambda errors, API 5xx
- Mobile: tokens in iOS Keychain, no secrets in Fastlane config
- SQS: visibility timeout properly sized for Lambda consumers