Canonical Phase-F roster (KG build 29.9.16). Scoring: CVSS v3.1. Audited: 2026-06-06. Verdict authority: security-lead-auditor (F.6). VERDICT: APPROVED — unresolved findings = 0 (every finding FIXED or documented ACCEPTED_EXCEPTION).
The RAG Refinement System backend (backend/, ingestion/, router/, db/) presents a
strong, defense-in-depth security posture. Authentication (alg-pinned JWT + salted-HMAC
API keys with rotation), tenant-level IDOR isolation on every document-scoped path,
OWASP-LLM01 prompt-injection fencing with a strict-JSON output guard, parameterized ORM
access, RFC-7807 error masking, and a non-root secret-free container are all present and
verified in code.
Real tooling found no production-code SAST findings, no committed secrets, and no
production-dependency CVEs after remediation. The only genuine issues were 4 transitive
Python dependency CVEs (PyJWT, python-multipart, starlette, python-dotenv), now remediated
by secure version pins in pyproject.toml. The installed packages were upgraded and re-audited
clean, and the full test suite remained green (265 passed, 1 skipped) with zero regression.
| Metric | Result |
|---|---|
| Critical | 0 |
| High | 2 (both FIXED) |
| Medium | 3 (2 FIXED, 1 ACCEPTED_EXCEPTION — dev-tooling-only) |
| Low | 0 |
| Info | 5 (verification / false-positive exceptions) |
| Unresolved (OPEN) | 0 |
| pytest after fixes | 265 passed, 1 skipped (no regression) |
F.1 threat-modeling-specialist (STRIDE/PASTA + OWASP Top-10 + OWASP LLM Top-10) → F.2 sast-engineer + secrets-detection-specialist + dependency-vulnerability-analyst → F.3 api-security-auditor + auth-security-specialist + penetration-tester → F.4 infrastructure-security-auditor + crypto-security-specialist → F.5 security-compliance-mapper (DPDP / CERT-In / SOC2) → F.6 security-lead-auditor (binary verdict).
Real tooling executed: Bandit 1.9.4, detect-secrets 1.5.0, pip-audit 2.10.0,
npm audit (npm 11.13). Plus manual code-level review citing file:line.
bandit -r backend ingestion router db -ll -> No issues identified. (Medium+ = 0)
bandit -r backend ingestion router db (full low) -> 227 LOW total:
B101 assert_used 226 (all in */tests/* — excluded by CI config + ruff S101)
B105 hardcoded_password 1 (regex literal, # noqa: S105 — false positive)
Production-code findings at any severity: 0
5 potential secrets flagged, 0 real:
backend/app/api/schemas.py:23 -> regex pattern (false positive)
tests/conftest.py:40-41 -> test-only literals, # noqa: S105
migrations/README.md:25 -> 'USER:PASSWORD' DSN placeholder
docs/.../accessibility_report.json:8 -> placeholder
.env is gitignored; all real secrets env-injected. No real secret committed.
The shared host interpreter contains many packages from unrelated projects (pillow, ragas, transformers, werkzeug, yt-dlp, torch, ...). Those are NOT dependencies of this project and are excluded. The audit was scoped to this project's actual transitive runtime closure (22 pkgs).
| Package | Was | CVEs | Fixed floor | Status |
|---|---|---|---|---|
| PyJWT | 2.11.0 | 6 (PYSEC-2026-120/175/176/177/178/179) | 2.13.0 | FIXED (installed 2.13.0) |
| python-multipart | 0.0.22 | 2 (CVE-2026-40347/42561) | 0.0.27 | FIXED (installed 0.0.32) |
| starlette | 0.52.1 | 1 (PYSEC-2026-161) | 1.0.1 | FIXED (installed 1.2.1) |
| python-dotenv | 1.1.1 | 1 (CVE-2026-28684) | 1.2.2 | FIXED (pinned; host upgrade blocked by file lock — applies on clean install; not exploitable here) |
Re-audit of the upgraded set (pyjwt==2.13.0 starlette==1.2.1 python-multipart==0.0.32):
No known vulnerabilities found.
npm audit --omit=dev (production) -> found 0 vulnerabilities
npm audit (incl. dev) -> 5 vulnerabilities (4 moderate, 1 critical)
esbuild / vite / vite-node / @vitest/mocker / vitest (dev-tooling chain only)
Production SPA deps (react, react-dom) are clean. The dev-server esbuild advisory never reaches the static production bundle (see F-005 exception).
| Area | File:line | Result |
|---|---|---|
| JWT alg-pinning, exp/aud/iss | backend/app/security/auth.py:204-211 |
PASS — algorithms=[settings.jwt_algorithm], require:[exp,sub], aud+iss enforced; alg=none rejected |
| API-key hashing + rotation | backend/app/security/auth.py:65-160 |
PASS — salted HMAC-SHA-256 digest store; plaintext never persisted; rotation deactivates old digest |
| Tenant IDOR guard | documents.py:247,278,321,362, routing.py:80, answer.py:180 |
PASS — every path filters on principal.tenant_id; cross-tenant -> 404 |
| Rate limiting | backend/app/security/rate_limit.py:47-67 |
PASS — per-credential fixed window; tighter sensitive bucket; 429 + Retry-After |
| Mass-assignment | router/schema.py, request models |
PASS — Pydantic extra="forbid" |
| Injection (SQL) | backend/app/adapters/stores.py:67-134 |
PASS — SQLAlchemy ORM, parameterized, no string SQL |
| Prompt injection (LLM01) | router/prompts.py:29-67 |
PASS — fenced untrusted TOC+query, override-refusal, allow-list, private CoT |
| Strict-JSON guard (LLM05) | router/schema.py:156-193 |
PASS — size-bound, JSON-object, schema validation, deterministic fallback |
| Unsafe deserialization | ingestion/parser.py |
PASS — bytes-only PyMuPDF parse; no eval/exec/pickle |
| Error info-leak | backend/app/errors.py:365-375 |
PASS — catch-all masks 500s; generic detail strings |
| Secret handling | settings.py, router/llm.py:86-94, stores.py |
PASS — env-resolved; no constructor literals; not logged |
| Container hardening | Dockerfile:26-43 |
PASS — non-root app user, multi-stage, no baked secrets, /health probe |
| Negligible | Low | Medium | High |
Likelihood | | | | |
------------+--------------+--------------+--------------+--------------+
High | | | | |
Medium | | F-005(dev)* | | |
Low | F-006,F-007 | F-008..F-012 | F-003,F-004* | |
| (FP/INFO) | (verified) | (FIXED) | |
Negligible| | | F-001,F-002* | |
| | | (FIXED) | |
------------+--------------+--------------+--------------+--------------+
* residual likelihood after remediation. F-001/F-002 were High by base CVSS
but are FIXED (patched versions installed); F-005 is dev-tooling only and
never reaches production.
No finding remains in an OPEN state. No finding sits in the High-likelihood band post-fix.
| Regulation | Control | Evidence | Status |
|---|---|---|---|
| DPDP 2023 §8 — Erasure | DELETE /v1/documents/{id} tombstone + outbox |
documents.py:289-330 |
MET |
| DPDP 2023 §8 — Access | GET /v1/documents/{id}/data (doc + sections + x-pii field names) |
documents.py:333-372 |
MET |
| DPDP — No-retention | no_retention purge mode skips persistence |
ingestion/pipeline.py:365-372 |
MET |
| DPDP — Data residency | residency_region (IN/EU/US/GLOBAL) tag |
documents.py:49,143-146 |
MET |
| DPDP §4 — PII marking | x-pii: true on personal-data fields |
openapi.yaml (12 fields) |
MET |
| CERT-In — 6h incident report | Hook recommended (not yet wired) | — | RECOMMENDED |
| SOC2 — audit trail on writes | Correlation ids present; centralized audit log recommended | — | RECOMMENDED |
Recommendations (non-blocking, tracked for production hardening): centralized append-only audit log on erasure/export/ingest; CERT-In 6h incident hook to alerting; Qdrant API-key + Postgres TLS in the production manifest; scheduled vite/vitest major upgrade to clear dev-tooling CVEs.
Config / CI / dependency files (owned by the audit per remediation policy):
pyproject.toml— explicitPyJWT>=2.13.0,<3.0,python-multipart>=0.0.27,starlette>=1.0.1,python-dotenv>=1.2.2runtime pins;pip-audit>=2.7,<3.0dev dep..github/workflows/ci.yml— Bandit scope broadened tobackend ingestion router db; new blockingpip-auditSCA step; detect-secrets scope broadened to includerouter db; newfrontend-securityjob runningnpm audit --omit=dev --audit-level=low(blocking).
No production-code logic was modified. The full suite was re-run after the installed-package upgrades (PyJWT 2.13.0, starlette 1.2.1 — a major bump — and python-multipart 0.0.32): 265 passed, 1 skipped, zero regression.
APPROVED. Every finding is FIXED or a documented, justified ACCEPTED_EXCEPTION
(test-only false positives; dev-tooling-only frontend CVEs absent from the production bundle;
verification findings with no defect). unresolved_total = 0. The deploy gate "all unresolved
findings = 0" is satisfied. See security_verdict.json for the machine-readable result.