Spec: F54 — CI Security Scanning & Vulnerability Gates
Status: shipped · Audited: 2026-07-14
1. Problem & CEO Job
Job: Trust the signal — but there is no signal when a CVE ships to production. Lumi’s merge path now blocks high-severity Python, JavaScript, CodeQL, and repository-wide Trivy findings, while Android has its own dependency audit and production deploy scans the exact image before push. The remaining platform gaps are explicit: CodeQL annotations/Security-tab upload are unavailable on the current private-repository plan, and iOS package-vulnerability proof is not yet attached.
This spec adds security scanning to the deployment-path CI runs: Python (pip-audit), JavaScript (npm audit), Gradle, and GitHub Actions CodeQL analysis. Critical/high CVEs block the merge. The cost of adding these is measured in minutes of CI configuration, not days of development.
2. User Stories & Acceptance Criteria
-
As a security-conscious developer, I want CI to fail when a Python dependency has a known CVE, so that vulnerable code never reaches production.
-
pip-auditruns in thebackendCI job for backend paths ondeploy/backend*pushes/PRs. - Critical CVEs → CI fails immediately.
- High CVEs → CI fails.
- Medium CVEs → CI passes with warning annotation.
- Low CVEs → no action.
-
pip-audituses thebackend/pyproject.tomldependency manifest. (backend/requirements.txtwas deleted in F56;pip-auditnow auditspyproject.toml.)
-
-
As a frontend developer, I want CI to fail when an npm dependency has a known vulnerability, so that XSS or supply-chain attacks through npm are caught at the PR stage.
-
npm auditruns in thefrontendCI job for frontend paths ondeploy/*pushes/PRs. -
--audit-level=high— fails on high and critical vulnerabilities. - Dependabot npm PRs that pass
npm auditare auto-merged (F42 auto-merge for patch-level). -
npm auditoutput is human-readable in CI logs.
-
-
As a mobile developer, I want dependency scanning for Gradle (Android) and Swift (iOS), so that mobile builds don’t ship vulnerable libraries.
- Android CI runs
./gradlew dependencyCheckAnalyzeorowasp-dependency-check-gradleplugin. - iOS CI runs Swift package vulnerability check (Xcode 16+ has built-in package vulnerability warnings — ensure they fail the build).
- Both CI jobs upload vulnerability reports as workflow artifacts.
- Android CI runs
-
As a security engineer, I want CodeQL analysis on every relevant
deploy/backend-prodpush and PR, so that code-level vulnerabilities (SQL injection patterns, XSS vectors, hardcoded secrets) are detected.- GitHub CodeQL workflow analyzes Python and JavaScript/TypeScript codebases.
- CodeQL runs only on relevant
deploy/backend-prodpushes and pull requests. - CodeQL analysis is pinned to GitHub-hosted
ubuntu-latestand does not use the shared trusted-event router. - CodeQL findings appear as PR annotations and Security tab alerts.
- CodeQL error-level or security-severity ≥7.0 findings block merge through a repository-owned SARIF gate; lower severities remain advisory.
-
As a developer, I want the
.githooks/pre-commithook to catch basic secrets before commit, so that I don’t push API keys or tokens accidentally.- Pre-commit hook runs
detect-secretsortrufflehogon staged files. - Blocked patterns:
[A-Za-z0-9_-]{20,}that look like API keys or tokens,.p8private key files,.pemcertificate files. - Allowlist: known placeholder values (demo passwords, dev keys where explicitly marked with
# nosecrets).
- Pre-commit hook runs
-
As a maintainer, I want trusted Linux jobs to use the repository’s runner most of the time without exposing it to pull-request code, so that CI cost falls while the persistent host remains a trusted boundary.
- One reusable router emits JSON labels consumed through
fromJSON. - Pull-request events always select
ubuntu-latest. - Trusted events select
[self-hosted, ubuntu-22.04]for SHA buckets 1–9 andubuntu-latestfor bucket 0. - Every routed Linux job explicitly depends on the router; CodeQL, fixed macOS/Windows jobs, and the persistent
deploy-workerjob remain pinned to their required hosts. - The workflow policy gate rejects routing drift, missing router dependencies, unsafe PR selection, or a changed 98/2 ratio.
- One reusable router emits JSON labels consumed through
3. Scope Boundaries
- In:
pip-auditfor Python (backend/pyproject.toml). (backend/requirements.txtwas deleted in F56.)npm auditfor JavaScript (frontend/package.json).- CodeQL for Python and JavaScript/TypeScript.
- Pre-commit hook for basic secret detection.
- Gradle dependency check (Android) — basic integration.
- Failing CI on critical/high CVEs.
- Deterministic trusted-event Linux runner routing with a hosted-only pull-request boundary.
- Out:
- Docker container scanning (Trivy/Grype for
backend/Dockerfileandfrontend/Dockerfile) — separate spec when container security becomes a priority. - Software Bill of Materials (SBOM) generation.
- Runtime vulnerability scanning (Snyk, Wiz, or similar).
- SAST beyond CodeQL (no SonarQube, no Semgrep).
- DAST (dynamic application security testing — requires a running instance).
- Penetration testing automation.
- Supply-chain attestation / SLSA framework compliance.
- Fixing discovered vulnerabilities — scanning finds them; fixing is covered by existing Dependabot + F42 auto-merge.
- Docker container scanning (Trivy/Grype for
4. Data Model
No model changes. CI configuration only.
4.1 CI Workflow Changes
jobs:
router:
uses: ./.github/workflows/runner-router.yml
backend:
needs: router
runs-on: ${{ fromJSON(needs.router.outputs.runner) }}
The reusable router runs on ubuntu-latest and does not check out repository code.
For pull_request it emits ["ubuntu-latest"]. For trusted events it hashes
github.sha into ten stable buckets: bucket 0 emits ["ubuntu-latest"]; buckets
1–9 emit ["self-hosted","ubuntu-22.04"].
CodeQL is intentionally outside this router and is pinned to ubuntu-latest for
all supported events, so security analysis never runs on the self-hosted runner.
jobs:
backend:
# ... existing steps ...
- name: Python vulnerability scan
run: |
pip install pip-audit
pip-audit --project backend/pyproject.toml \
--ignore-vuln PYSEC-2024-XXX # explicit allowlist for triaged CVEs
# Note: backend/requirements.txt was deleted in F56; pyproject.toml is the dependency manifest.
frontend:
# ... existing steps ...
- name: JavaScript vulnerability scan
run: |
cd frontend
npm audit --audit-level=high
android:
# ... existing steps ...
- name: Gradle dependency check
run: |
cd android
./gradlew dependencyCheckAnalyze --info
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: python, javascript-typescript
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
4.2 .githooks/pre-commit Changes
#!/bin/bash
# ... existing hooks ...
# Secret detection
if command -v detect-secrets &> /dev/null; then
detect-secrets-hook --baseline .secrets.baseline "$@"
fi
# Block .p8 and .pem files
staged_p8=$(git diff --cached --name-only --diff-filter=A | grep '\.p8$' || true)
if [ -n "$staged_p8" ]; then
echo "ERROR: Private key file detected: $staged_p8"
echo "Store secrets in AWS Secrets Manager, not in git."
exit 1
fi
4.3 .secrets.baseline (detect-secrets allowlist)
Generated by detect-secrets scan and updated when legitimate strings are committed. Contains hashes of allowlisted secrets. Checked into the repo.
5. API Contract
No API changes.
6. UI / UX
No application UI changes. Developer workflow change:
- Developer opens PR.
- Deployment-path CI runs: backend tests + pip-audit + frontend build + npm audit + CodeQL.
- If
pip-auditfinds a CVE-2024-XXXX in dependencyrequests==2.31.0:- CI job fails.
- PR comments: ”🚨 Security:
requests==2.31.0has CVE-2024-XXXX (Critical). Upgrade torequests>=2.32.0.” - Dependabot automatically opens a PR to upgrade
requestsif it hasn’t already.
- Developer upgrades the dependency or adds an explicit allowlist (
--ignore-vuln CVE-2024-XXXX) with a justification comment. - CI re-runs.
pip-auditpasses. PR unblocks.
6.1 Vulnerability Triage Flow
When a vulnerability is found but can’t be fixed immediately (no upgrade available, fix would break compatibility):
- Developer adds
--ignore-vuln CVE-XXXX-YYYYto the CI command. - Developer adds a comment:
# CVE-XXXX-YYYY: Low severity, no fix available. Review by 2026-08-01. - The allowlist expires after 60 days — CI starts failing again if not removed, forcing re-review.
- This is enforced via a CI check on the allowlist comment date.
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | pip-audit passes on clean backend/pyproject.toml | ci | CI workflow run |
| 2 | pip-audit fails when a known-vulnerable package is added | ci | Test PR with requests==2.31.0 pinned |
| 3 | npm audit passes on clean package.json | ci | CI workflow run |
| 4 | npm audit fails on high-severity vulnerability | ci | Test PR with vulnerable package |
| 5 | CodeQL passes on clean codebase | ci | CI workflow run |
| 6 | CodeQL catches hardcoded secret pattern | ci | Test PR with SECRET_KEY = "test-secret-12345" |
| 7 | Pre-commit blocks .p8 file staging | unit | Git hook test |
| 8 | Vulnerability allowlist expires after 60 days | ci | Timestamp check in CI |
8. Current Behavior
- CI runs linting (ruff E,F,W rules) + tests + coverage.
pip-auditruns in backend CI.pnpm audit --audit-level=highruns in frontend CI.- CodeQL workflow exists at
.github/workflows/security-codeql.yml; it runs only ondeploy/backend-prodpushes and pull requests, writes SARIF locally, and blocks on error-level or security-severity ≥7.0 findings without requiring private-repository code-scanning upload. - CodeQL analysis is pinned to GitHub-hosted
ubuntu-latestand is excluded from the shared 98/2 trusted-event router. - Pull-request-executable jobs use isolated GitHub-hosted runners; release/deploy-only jobs may retain controlled self-hosted runners.
- Trusted Linux workflow runs share one deterministic router: 90% self-hosted and 10% GitHub-hosted by SHA; pull requests are forced hosted. Fixed macOS/Windows jobs and the persistent Docker worker deploy remain pinned.
- Routed JavaScript jobs establish their declared Node version before pnpm setup; pnpm’s action owns its cache so hardened persistent runners do not fall back to an incompatible standalone bootstrap.
- Dependency audits and PR security scans are blocking, repository-wide Trivy reviews supported manifest changes without requiring GitHub Advanced Security, and production scans the exact image before push.
scripts/check_ci_security_policy.pyis the regression gate for runner, trigger, warn-only, CodeQL-local-SARIF, and image-scan ordering policy; its owning docs workflow and dependency review run ondeploy/backend*pull requests, and the checker tests that trigger.scripts/check_codeql_sarif.pyenforces the finding threshold.- Pre-commit secret detection is active in
.githooks/pre-commit. - Dependabot opens PRs but doesn’t fail CI.
- Remaining gap: allowlist-expiry enforcement is still not wired.
9. Error States
| # | Scenario | User-visible | Internal | Log | Retry |
|---|---|---|---|---|---|
| 1 | pip-audit cannot reach PyPI advisory database | CI job fails with: “pip-audit: could not fetch advisory database. Check network connectivity.” | Subprocess error captured, exit code 2 | ERROR “pip_audit_network_error” | CI re-run on push |
| 2 | npm audit cannot reach npm registry | CI job fails with: “npm audit: advisory database unavailable.” | npm exit code 1, network error | ERROR “npm_audit_network_error” | CI re-run on push |
| 3 | CodeQL analysis times out or emits no readable SARIF | CodeQL job fails; merge remains blocked | Analyze step fails or local SARIF gate rejects missing/malformed output | ERROR “codeql_analysis_unavailable” | Re-run after correcting the runner/tool failure |
| 4 | Secret detected in staged file via pre-commit hook | CLI: “ERROR: Secret pattern detected in: | Hook blocks commit, exit code 1 | INFO “precommit_secret_detected” | Developer removes secret or adds allowlist |
| 5 | Secret pushed via --no-verify detected in CI | CI fails: “Secret pattern detected in commit | CI regex check on commit diff | ERROR “ci_secret_detected_post_push” | Developer must amend commit or revoke leaked secret |
| 6 | NVD feed unavailable for Gradle OWASP dependency check | CI step emits: “NVD feed unavailable. Skipping dependency check for this run.” | OWASP plugin returns feed error | WARNING “gradle_dependency_nvd_unavailable” | Auto-retry on next CI run, max 3 consecutive skips before failing |
| 7 | pip-audit version pinned but OS-level Python version mismatch | CI fails: “pip-audit requires Python 3.9+. Runner has 3.8.” | Environment mismatch | ERROR “pip_audit_env_mismatch” | Fix CI Python version, re-run |
| 8 | pip-audit finds critical CVE with no fix available (no patched version) | CI fails with CVE details + “No patched version available for | pip-audit exit code 1 | WARNING “cve_no_fix_available” | Developer adds ignore with expiry comment, re-runs CI |
| 9 | Gradle dependency check plugin not configured (not yet installed) | CI step emits: “Dependency check plugin not configured. Skipping.” Passes. | Plugin presence check fails | INFO “gradle_dependency_plugin_missing” | n/a — informational only |
| 10 | .secrets.baseline file missing or unreadable | Pre-commit hook emits: “detect-secrets baseline not found. Run detect-secrets scan > .secrets.baseline first.” Blocks commit. | File not found, exit code 1 | ERROR “secrets_baseline_missing” | Developer generates baseline, re-commits |
| 11 | Router output is missing, malformed JSON, or references unavailable labels | Dependent Linux jobs do not start; the policy gate names the invalid router contract before merge | fromJSON or runner matching fails closed | ERROR “ci_runner_router_invalid” | Correct the reusable router and re-run CI |
10. Diagrams & Docs Touched
deploy/DIAGRAMS.mdor CI workflow diagrams- F39 security program docs
- Contributing or release docs that define merge gates
11. Implementation Audit
Edge Cases Catalog (formerly §9, 2026-06-14; 9 items): scanner outage explicit failure, suppression expiry (60-day), monorepo dedup via per-manifest scanning, CodeQL false positive 90-day annotation review, --no-verify bypass retroactive CI catch, pip-audit version pinning, production-manifest scan, pnpm audit --prod, Gradle plugin-absent guard. Full catalog in git history.
| # | Claim | Status | Evidence |
|---|---|---|---|
| 1 | pip-audit in backend CI job | ✅ match | .github/workflows/ci-be.yml backend job |
| 2 | pnpm audit --audit-level=high in frontend CI job | ✅ match | .github/workflows/ci-fe.yml frontend job |
| 3 | bandit SAST + safety dependency check | ✅ match | .github/workflows/security-scan.yml |
| 4 | CodeQL workflow (Python + JS/TS; deploy/backend-prod pushes/PRs) | ✅ match | .github/workflows/security-codeql.yml |
| 5 | Pre-commit secret detection (.p8/.pem + pattern scan) | ✅ match | .githooks/pre-commit |
| 6 | Gradle Android dependency check (with plugin-absent guard) | ✅ match | .github/workflows/android-ci.yml |
| 7 | Vulnerability allowlist expiry enforcement (60-day parse at CI runtime) | ⚠️ gap | Implementation Order item 6 — not confirmed built |
12. Open Questions / Decisions
- pip-audit vs safety: Both scan PyPI packages for CVEs.
pip-auditis free (PyPA-backed),safetyrequires a free account for the vulnerability DB. Decision:pip-audit— zero signup, zero API key, PyPA-backed, and supports--ignore-vulnfor triage. npm auditfalse positives: npm audit has a reputation for noisy dev-dependency warnings. Decision:--audit-level=highfilters out low/medium.--productionignores dev dependencies in CI (devDeps are not in the production bundle). If false positives persist,--audit-level=criticalis the fallback.- Gradle dependency scanning tool: OWASP Dependency-Check Gradle plugin vs Gradle’s built-in dependency verification. Decision: OWASP Dependency-Check — battle-tested, supports NVD data feed, generates HTML reports. More thorough than Gradle’s built-in signature verification.
- CodeQL language coverage: Python + JavaScript/TypeScript covers the current web/backend critical paths. Kotlin is not included in the shipped CodeQL workflow; Android vulnerability scanning relies on the separate Gradle dependency-check lane. Swift is not supported by CodeQL. iOS vulnerability scanning relies on Xcode’s built-in package vulnerability warnings (Xcode 16+ checks Swift Package dependencies against the NVD).
13. Bug / Change Log
2026-07-14 — Make supply-chain controls merge/deploy gates
Type: Security hardening
- Moved pull-request execution to hosted runners and added PR CodeQL, repository-native dependency review, and security scanning.
- Removed warn-only Python/JavaScript dependency audit behavior.
- Pinned Trivy and scan the final SHA-tagged container before pushing it.
- Added executable workflow-policy fixtures and an F54 Contract Graph declaration.
- Replaced unavailable private-repository CodeQL upload with a fail-closed local SARIF gate; GitHub annotations/Security-tab alerts remain an explicit platform gap.
2026-07-15 — Scope backend deployment CI gates
Type: CI policy
- Scoped CI Docs, Dependency Review, Security Scan, and Android Benchmark Gate to
deploy/backend*, matching the backend deployment promotion path. - Updated the executable workflow-policy checker, fixture tests, deploy guide, and CI/CD diagram to keep trigger policy machine-checked and documented.
2026-07-16 — Restrict CodeQL to production backend promotion
Type: CI policy
- Scoped CodeQL push and pull-request execution to the exact
deploy/backend-prodbranch. - Updated the executable trigger policy, fixtures, CI trigger map, deployment diagram, and current F39/F54 audit truth.
2026-07-16 — Pin CodeQL to hosted Linux
Type: CI infrastructure
- Pinned the CodeQL analysis matrix to GitHub-hosted
ubuntu-latestfor every supported event. - Kept the shared 98/2 runner router for other eligible Linux jobs while adding a machine-checked CodeQL exception and updating the deployment/security docs.
2026-07-15 — Route trusted Linux jobs 98/2
Type: CI infrastructure
- Added one reusable runner router and migrated Linux-capable workflow jobs to depend on its JSON output.
- Trusted events use stable SHA buckets: 90% target the registered
self-hosted+ubuntu-22.04runner and 10% useubuntu-latest. - Pull-request events remain hosted-only; fixed macOS/Windows jobs and the
persistent
deploy-workerhost are explicit exceptions. - Extended the executable F54 workflow policy so routing coverage, ratio, labels, and the PR safety override cannot silently drift.
2026-06-14 — Spec restructured to TEMPLATE.md canonical order (§1–§13)
Type: Documentation Change
Summary:
- §9 Edge Cases (9 items) absorbed into §11 Implementation Audit note.
- §12 Implementation Audit converted from 3-column table to canonical 4-column table (7 rows: 6 ✅, 1 ⚠️ gap for allowlist expiry enforcement).
- Sections renumbered: §10 ES → §9, §11 Diags → §10, §12 Audit → §11, §13 OQ → §12, §14 BL → §13.
Reason:
- Align with TEMPLATE.md canonical section order per SP fleet restructuring pass.
Impact:
- Spec authors and future implementation agents.
2026-07-02 — Fix stale paths: backend/requirements.txt → backend/pyproject.toml (batch 3)
Type: Documentation
Summary:
backend/requirements.txtwas deleted in F56 (F56 lockedbackend/pyproject.tomlas the single dependency manifest).- Updated all five occurrences across §2 AC, §3 Scope Boundaries, §4.1 CI YAML snippet, §7 Test Plan row 1, and §13 safety command to reference
backend/pyproject.tomlinstead. - No code changes; no CI workflow files changed.
2026-07-01 — Close CodeQL workflow drift and stale evidence paths
Type: CI hardening + documentation truth fix
Summary:
security-codeql.ymlnow runs on protected-branch pushes and pull requests in addition to the weekly sweep, matching the intended merge gate.- Updated F54 to point at the actual workflow filenames in the repo (
security-codeql.yml,security-scan.yml) and removed stale prose that still claimed CodeQL and pre-commit secret detection did not exist. - Kept the remaining open gap narrow and honest: allowlist-expiry enforcement is still not built.
2026-06-13 (T3-F R11) — security-scan CI job added
CI security scanning steps (Partial)
- Added
security-scanjob to CI; the active backend security scan now lives in.github/workflows/ci-be.yml. safety check --project backend/pyproject.toml || true— warn-only until baseline clean. (backend/requirements.txtwas deleted in F56; safety now targets pyproject.toml.)bandit -r backend/apps/ -ll— medium+ severity findings fail the job (this is the blocking gate).pip-audit(backend job) andpnpm audit --audit-level=high(frontend job) both already present withcontinue-on-error: true.- Remaining: CodeQL workflow, pre-commit hook, Gradle dependency check, allowlist expiry.
2026-07-02 — Spec drift fix: checked shipped ACs in §2
Type: Documentation
Summary:
- 8 of 19
[ ]ACs in §2 flipped to[x]— confirmed shipped against §11 audit rows 1–6. - Checked:
pip-auditin backend CI;npm/pnpm audit --audit-level=highin frontend CI; Android Gradle dependency check; CodeQL (Python + JS/TS; protected-branch pushes/PRs); pre-commit secret detection (.p8/.pem + pattern scan). - Left
[ ]: severity-threshold sub-behaviors (not enumerated in §11 rows), iOS Swift vulnerability check, artifact upload, CodeQL PR annotations/severity gate, npm audit human-readable logs, allowlist, Dependabot auto-merge, and the confirmed-open ⚠️ gap (allowlist expiry enforcement). - No code changes.
2026-06-13 — Spec hardening: numbered edge cases, detailed error matrix, status → agreed
Type: Documentation Change
Summary:
- Replaced §9 loose bullet points with 9 numbered edge cases (9.1–9.9) covering scanner outages, baseline expiry, monorepo dedup, CodeQL false positives, pre-commit bypass detection, version drift, manifest mismatch, devDependency noise, and Gradle plugin availability.
- Replaced §10 with 10-row detailed error matrix (user-visible/internal/log/retry).
- Updated spec status from draft to agreed.
Reason:
- Spec needed hardening per TEMPLATE.md. Edge cases were loose prose, error states lacked structured detail.
Impact:
- Spec authors and future implementation agents.
Tests:
- Spec-only change — no code tests affected.