SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
6.2 KB · 112 lines markdown
Rendered Raw Blame History
1# CancerIndex — Security23Scope: the public API (`apps/api`), the worker (`workers/`), the web app and the database on the4MacLustr node (spec §160-163).56## Threat model in one paragraph78CancerIndex is a read-mostly public service over open scientific data. The valuable assets are data9integrity (no silent corruption of canonical or derived data), availability, upstream credentials10(NCBI, SEER), and the operator surface (admin endpoints, database). There is no end-user PII beyond11API-key owner e-mails.1213## Controls1415### Network and exposure1617- Only the web app (port 8250) is exposed through the ngrok tunnel for `www.cancerindex.io`. The API18  listens on `127.0.0.1:8251` and is reached through the web app's `/api/v1/*` proxy; the worker has19  no listener; PostgreSQL listens on localhost only.20- CORS allows browser reads from any origin (public data), exposes only rate-limit/request-id21  headers, methods `GET, POST, OPTIONS`.22- The API sets `trustProxy: true` because it sits behind the web proxy and ngrok; rate limiting keys23  on the forwarded client IP.2425### Read-only public API2627- All `/v1` public endpoints are `GET` and run only `SELECT` statements; the database role used in28  production should be granted read-only privileges for the API process where possible.29- Every query parameter is validated by zod (types, enums, `limit ≤ 200`); all SQL uses30  parameterised queries (`sql` template) — no string interpolation of user input.31- Numeric database ids are never accepted as public references (`classifyRef`), so enumeration of32  internal ids is not possible.3334### Rate limiting and API keys3536- Anonymous: 60 requests/minute per IP; keyed: `api_keys.rate_limit_per_minute`. `X-RateLimit-*`37  headers are always sent; 429 includes `retry-after`.38- API keys are random 192-bit values (`cix_` + base64url); only the sha256 hash and a 12-character39  prefix are stored. Keys are shown once at creation (`apps/api/scripts/create-key.ts`), can be40  deactivated (`active = false`) and record `last_used_at` (throttled write).41- An invalid key returns 401 rather than silently falling back to anonymous limits.4243### Admin surface4445- `/v1/admin/*` requires the `x-admin-token` header; comparison is constant-time. When46  `ADMIN_TOKEN` is unset or left at the `.env.example` placeholder, admin endpoints answer 503 and47  are effectively disabled.48- Admin mutations never run ingestion in the API process: they enqueue pg-boss jobs. Every49  mutation writes an `audit_log` row (`actor`, `action`, `before`, `after`, `reason`).50- The admin token must be rotated whenever an operator leaves or the value is exposed; it is51  injected by `mld` from the manifest placeholder `{{ADMIN_TOKEN}}`, never committed.5253### Secrets and configuration5455- `.env` is git-ignored; `.env.example` contains no real values. Production values live in the mld56  manifest `env_overrides` / process env on the node, or in `~/.claude/.env`-style operator files57  that never enter the repository.58- Upstream credentials (`NCBI_API_KEY`, `SEER_API_KEY`) are only read by connectors; a missing59  credential gates the connector (`awaiting_credentials`) instead of failing loudly with the value60  in logs.61- Logs are structured (pino) and must not include authorization headers or keys; Fastify's default62  request serialiser logs method, URL and remote address only.6364### Data integrity6566- Connectors are idempotent (payload hash), restartable (cursor), rate-limited and guarded against67  destructive updates (anomaly guard: never mass-mark `source_missing` on a shrunken response).68- Raw payloads are retained in the data lake so any canonical value can be re-derived and audited69  (`traceValue`).70- Schema changes go through Drizzle migrations reviewed by the integrator; no manual production71  mutation.72- Interrupted runs are safe: SIGTERM/SIGINT persist the cursor and mark the run `aborted`; the raw73  lake is flushed so referenced payloads stay readable; a killed run resumes from its last checkpoint74  (docs/connectors/README.md). A shrunken upstream response trips the anomaly guard (`ingest_runs.anomaly`,75  critical alert) instead of propagating deletions.76- Operational alerts (`system_alerts`) carry connector ids, messages and small JSON details only —77  never credentials or request headers. `GET /v1/admin/alerts` is behind the admin token.7879### Backups and recovery (§172)8081- Daily `pg_dump -Fc` (PM2 `cancerindex-backup`, 05:20 UTC) to `~/apps/cancerindex/backups`,82  verified with `pg_restore --list`, 14 daily + 8 weekly retained, log in `logs/backup.log`. Dumps83  contain no end-user PII beyond API-key owner e-mails and only sha256 hashes of API keys, but they84  do contain the full canonical/derived data: keep the backups directory readable by the service85  user only and mirror it (with `data/raw`) to the NAS over the LAN, never to a public bucket.86- `deploy/restore.sh` always restores into a new `cancerindex_restore_<ts>` database; promoting a87  restore is an explicit operator action (rename databases after stopping PM2). Test a restore after88  every schema migration.89- `pnpm cix doctor` is the post-incident checklist: environment, database, pending migrations,90  connector freshness and open alerts in one exit code.9192### Dependencies and runtime9394- Node ≥ 22, pnpm with a committed lockfile (`--frozen-lockfile` in deployment).95- Only maintained, widely used packages: Fastify 5 and official `@fastify/*` plugins, zod 4,96  drizzle-orm, postgres, pg-boss 12, pino. Run `pnpm audit` before releases.97- Processes run under PM2 with `max_memory_restart`; the worker stops gracefully on `SIGTERM`98  (finishes the active job or lets pg-boss retry after expiry).99100### Content and liability101102- The platform states on every page and in the OpenAPI description that it is not a physician, not103  a diagnostic tool and offers no treatment recommendations. Survival statistics are labelled as104  population statistics.105- Source licenses and attribution are surfaced in every API response (`sources`) and on `/sources`;106  connectors with unresolved licensing (`review`, `blocked`) never ingest.107108## Reporting109110Report vulnerabilities privately to the maintainer (see repository README). Please include the111`x-request-id` of any suspicious response and do not test against production data volumes.112