# Oshun v1 — Audit pass 3 gap findings

Date: 2026-05-28 Owner: platform team Previous pass:
[`docs/audits/2026-05-28-v2-gap-audit.md`](./2026-05-28-v2-gap-audit.md)

Audit pass 2 closed tasks #90–#118. This third sweep used seven parallel
read-only explore agents to find what those didn't catch, across seven
non-overlapping surfaces. **93 net-new actionable findings.** This doc is the
authoritative source while tasks #120+ are filed.

## How the sweep was scoped

Each agent was briefed with the exact #90–#118 task list as "out of scope" and
instructed to find file:line evidence for severity-ranked findings only. Agents
were told to skip stylistic issues and cap at 20–25 findings each.

| Agent            | Findings | Top themes                                                                 |
| ---------------- | -------- | -------------------------------------------------------------------------- |
| security         | 10       | SSRF in gpu-dispatcher callback; OAuth open redirect; CORS regression      |
| reliability      | 15       | 23+ services missing SIGTERM; unbounded Maps; missing /health, /metrics    |
| data integrity   | 20       | FK relations without onDelete; soft-delete fields with no sweeper          |
| build / ci / ops | 8        | 67 workflows w/o `permissions:` block; 65 w/o concurrency cancel-in-prog   |
| test / contract  | 10       | `expect(true).toBe(true)` stubs; skipped svc-auth integration suite        |
| privacy / pii    | 15       | JWT body contains email+IP; 6 audit trails store full IPs un-minimized     |
| performance      | 15       | iris GraphQL N+1 cluster (resolvers + dataloaders); regex DoS in DLP rules |

## High-severity findings — fix candidates this pass

### Security

| #   | Title                                                      | Location                                                                |
| --- | ---------------------------------------------------------- | ----------------------------------------------------------------------- |
| S1  | SSRF via user-controlled callbackUrl in GPU dispatcher     | `apps/infra/gpu-dispatcher/src/callbacks.ts:596`                        |
| S2  | OAuth `redirect_uri` reflected without allowlist           | `apps/lilith/svc-auth/src/auth-missing-endpoints.ts:176`                |
| S3  | Wildcard CORS still in 8+ services missed by #71           | `apps/aja/svc-motion-ai/src/app.ts:114` + veritas/\*, aphrodite/devices |
| S4  | RevenueCat webhook signature optional on env var           | `apps/lilith/svc-payment-orchestrator/src/iap/iap-routes.ts:282`        |
| S5  | `jwt.decode` instead of `jwt.verify` in offline encryption | `apps/lilith/svc-offline/src/encryption.ts`                             |

### Reliability

| #   | Title                                                         | Location                                                                                                                                                      |
| --- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| R1  | 23+ Fastify services missing SIGTERM handler                  | `apps/lilith/svc-yoga-practice/src/server.ts:16`, `svc-voice-pipeline/src/server.ts:18`, `svc-webrtc/src/server.ts:8`, `svc-stt/src/server.ts:8`, + 19 others |
| R2  | 10 unbounded Maps in voice-pipeline (sessions, PTT, VAD, MOS) | `apps/lilith/svc-voice-pipeline/src/app.ts:68-78`                                                                                                             |
| R3  | 4 unbounded Maps in community-translation                     | `apps/lilith/svc-community-translation/src/app.ts:2013-2016`                                                                                                  |
| R4  | `fetch(url)` without timeout in model registry downloads      | `apps/lilith/svc-ai/src/distilled-model-registry.ts:577`                                                                                                      |
| R5  | Unbounded notifications array in VoiceCloningRequestManager   | `apps/lilith/svc-tts/src/voice-cloning-request-manager.ts:72`                                                                                                 |

### Privacy / PII

| #   | Title                                               | Location                                                                                                                  |
| --- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| P1  | ConsoleEmailSender logs tokens + emails in dev mode | `apps/lilith/svc-auth/src/email-sender.ts:232`                                                                            |
| P2  | JWT body contains cleartext email + IP              | `apps/lilith/svc-auth/src/jwt-types.ts:45`                                                                                |
| P3  | Full IPs stored unminimized in 2 audit trails       | `apps/lilith/svc-data-governance/src/app.ts:2293`, `apps/maat/api-gateway/src/middleware/request-response-logging.ts:433` |
| P4  | pg_dump backups gzipped but not encrypted           | `scripts/db-backup.sh:177`                                                                                                |
| P5  | user_hash analytics derives identity without salt   | `apps/lilith/svc-observability/src/analytics-routes.ts:52`                                                                |

### Performance

| #   | Title                                                             | Location                                                                      |
| --- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Pe1 | iris/api dataloaders loop instead of batch (4 of them)            | `apps/iris/api/src/graphql/data-loaders.ts:130,154,181,208`                   |
| Pe2 | iris GraphQL resolvers run N+1 over agents / KBs / conversations  | `apps/iris/api/src/graphql/resolvers.ts:312,496,1202`                         |
| Pe3 | Hardcoded `pageSize=1000` in three service-data-source list calls | `apps/iris/api/src/graphql/service-data-sources.ts:918,1059,1115`             |
| Pe4 | RegExp compiled from user-supplied DLP rule pattern (regex DoS)   | `apps/iris/developer-portal/src/services/data-loss-prevention-service.ts:487` |

### Build / CI / ops

| #   | Title                                                         | Location                                     |
| --- | ------------------------------------------------------------- | -------------------------------------------- |
| B1  | 67 workflows missing `permissions:` block (write-all default) | `.github/workflows/*.yml` (mass)             |
| B2  | 65 PR workflows missing `concurrency: cancel-in-progress`     | `.github/workflows/*.yml` (mass)             |
| B3  | Pre-commit hook has typecheck step commented out              | `.husky/pre-commit:19`                       |
| B4  | 20+ Dockerfiles use unpinned base image tags                  | `docker/psyche/Dockerfile.tools:19` + others |

### Test / contract

| #   | Title                                                           | Location                                                                         |
| --- | --------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| T1  | `describe.skip` permanently disables svc-auth integration suite | `apps/lilith/svc-auth/src/__tests__/integration/auth-mock-typed-demo.test.ts:34` |
| T2  | `expect(true).toBe(true)` stub in DSR processing test           | `apps/lilith/svc-auth/src/__tests__/audit-logging.test.ts:418`                   |
| T3  | `expect(true).toBe(true)` stub in plagiarism detection test     | `apps/lilith/svc-moderation/__tests__/academic-integrity.test.ts`                |

### Data integrity — top 6 of 20

| #   | Title                                                                | Location                                            |
| --- | -------------------------------------------------------------------- | --------------------------------------------------- |
| D1  | bellona `ExportJob.build` relation lacks `onDelete:` policy          | `libs/bellona/database/prisma/schema.prisma:342`    |
| D2  | `veritas.PaymentTransaction.amount` stored as `String` not `Decimal` | `libs/veritas/database/prisma/schema.prisma:1851`   |
| D3  | sophia `Document.deletedAt` + `Citation.deletedAt` without sweeper   | `libs/sophia/database/prisma/schema.prisma:336,489` |
| D4  | sophia `Entity.canonicalId` + `mergedInto` allow cycles              | `libs/sophia/database/prisma/schema.prisma:551-553` |
| D5  | nisaba `VariantUnit.manuscriptId` FK without explicit onDelete       | `libs/nisaba/database/prisma/schema.prisma:388`     |
| D6  | yemaya `Tag.organizationId` allows multiple NULL global collisions   | `libs/yemaya/database/prisma/schema.prisma:553`     |

## What's being addressed THIS pass (in-session fixes)

Tracked as tasks #120–#... — see `TODOS.md` and `TaskList`. Highest-leverage
small-effort items going in immediately:

- B3 — uncomment typecheck step in `.husky/pre-commit`
- B1/B2 — sweep `.github/workflows/*.yml`, add `permissions: contents: read`
  - `concurrency: { group: ${{ github.workflow }}-${{ github.ref }}, cancel-in-progress: true }`
    to PR-triggered workflows
- P1 — strip token from ConsoleEmailSender dev log; keep redacted "magic link
  sent to <hash>" form
- R4 — wrap distilled-model-registry `downloadFromURL` fetch in
  `AbortSignal.timeout(60_000)`
- R5 — cap voice-cloning-request-manager notifications array at 10_000 with
  oldest-first eviction
- Pe4 — wrap DLP rule regex in `try { new RegExp(...) } catch` + reject patterns
  over 256 chars or with `(.+)+`-style nested quantifiers
- T1 — un-skip or `it.skip.each` the auth integration suite with an explicit
  env-var gate instead of permanent `describe.skip`
- T2 / T3 — replace truthy stub assertions with real domain assertions or mark
  `.todo` with tracking issue

## What's being filed (separate work)

The following land as #120+ but are bigger pieces:

- S1 SSRF in gpu-dispatcher (needs URL allowlist + tests)
- S2 OAuth `redirect_uri` allowlist (registration table)
- S3 CORS sweep across remaining Hono services
- S4 RevenueCat make signature required (not env-gated)
- S5 `jwt.verify` migration
- R1 graceful shutdown across 23+ services (mass refactor)
- R2 / R3 voice-pipeline + community-translation Map eviction (per-service)
- R6 `process.on('unhandledRejection')` across services
- P2 JWT body PII removal (compat migration)
- P3 IP minimization helper + sweep audit trails
- P4 pg_dump encryption (gpg + key management)
- P5 user_hash salt
- Pe1–Pe3 iris GraphQL N+1 cluster (substantial DataLoader refactor)
- All 20 data-integrity findings (schema + migration per fix)
- B4 Docker base image SHA pinning

## Methodology notes

Agents were instructed to use the same stub-indicator framing the v2 audit used:
"Search for evidence of X" rather than "verify X is real." The adversarial
framing surfaced regressions in #71 (CORS) and #98 (graceful shutdown only
landed in veritas/video, not other services).

False positives were minimal: the test/contract agent's findings about
`describe.skip` for STRIPE_SECRET_KEY-gated tests were correctly classified as
out-of-scope (intentional integration test pattern).
