CAAMS Enterprise
A self-hosted, multi-user GRC platform. Select a security framework, map your tool stack, and run an auditor-ready compliance assessment — with full lifecycle management, evidence collection, findings tracking, RFIs, audit log, and multi-format exports.
Quick Start
1. Install dependencies
pip install -r requirements.txt
2. Seed the database
Loads all framework definitions and the tool catalog. Safe to re-run — skips anything already present.
python seed.py
3. Set the secret key
CAAMS requires a secret key to sign JWT tokens. The app will refuse to start without it.
export CAAMS_SECRET_KEY="$(python3 -c 'import secrets; print(secrets.token_hex(32))')"
Add that line to your shell profile or a .env file for persistent dev setups.
4. Generate TLS certificates
The default configuration serves over HTTPS. Self-signed certificates are required for local development.
mkdir -p certs openssl req -x509 -newkey rsa:4096 \ -keyout certs/key.pem -out certs/cert.pem \ -sha256 -days 3650 -nodes \ -subj "/CN=localhost" \ -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
5. Start the server
bash start.sh
# → https://localhost:8443
uvicorn app.main:app --reload --port 8000
First-Run Setup
On first visit, the UI shows a setup screen to create the initial admin account. You can also do this via the API:
curl -X POST https://localhost:8443/auth/setup \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "YourPassword"}'
This endpoint is only available when no users exist. After setup, it is permanently disabled.
Features
| Feature | Details |
|---|---|
| Framework coverage mapping | Map your tool stack against CIS v8, NIST CSF v2, SOC 2, PCI DSS v4, and HIPAA. Coverage is computed automatically from capability tags — no manual mapping. |
| Assessment lifecycle | Draft → In Review → Approved → Archived, with signed-off stage transitions and full history on every action. |
| Recurring assessments | Set a configurable recurrence interval (e.g. 90 days). CAAMS tracks the next review date and surfaces overdue renewals on the dashboard. |
| Evidence management | Upload files per-control with descriptions and expiry dates. Admins approve or reject with reasons. Download evidence packages as ZIP. |
| Findings tracker | Log findings with severity (critical → informational), remediation owner, target date, and full status lifecycle (open → remediated → closed). |
| Risk acceptances | Formally accept residual risk with a rated justification, named approver, and expiry date. Surfaces in exports and the audit trail. |
| RFIs | Create Requests for Information with priority levels, assignees, and due dates. Assignees respond inline; admins close when resolved. |
| Control overrides | Manually override a control's computed status with a justification and optional expiry date. |
| Ownership tracking | Assign owner, team, and evidence owner per control. |
| Control review workflow | Track per-control review status: not_reviewed → in_review → approved / rejected. |
| Statement of Applicability | Mark controls as not applicable with exclusion reasons. Included in the SOA sheet of the XLSX export. |
| Executive dashboard | Org-wide compliance posture across all frameworks — scores, open findings, overdue controls, and the assessment renewal pipeline. |
| Framework crosswalk | Tag-based automatic overlap mapping between any two loaded frameworks. |
| Assessment clone | Duplicate any assessment including tools, ownership, and notes. |
| Tool recommendations | Ranked list of tools not yet in scope that would close the most coverage gaps. |
| Auditor share links | Scoped, time-limited share links for external auditors. No login required — access limited to exactly the controls you choose. |
| Immutable audit log | Every state-changing action recorded with user, timestamp, IP, and detail payload. Cannot be edited or deleted. |
| Role-based access | Admin / Contributor / Viewer / Auditor roles enforced on every endpoint. |
| REST API | Full FastAPI backend with interactive Swagger UI at /docs. Long-lived API tokens for CI/CD pipelines. |
| Rate limiting | Login endpoint is rate-limited to 10 attempts per minute per IP. |
Supported Frameworks
| Framework | Version | Controls |
|---|---|---|
| CIS Controls | v8 | 18 |
| NIST Cybersecurity Framework | v2.0 | 6 functions / 22 categories |
| SOC 2 Trust Services Criteria | 2017 | 9 |
| PCI DSS | v4.0 | 12 requirements |
| HIPAA Security Rule | 45 CFR Part 164 | 16 standards |
Additional frameworks can be added by dropping a JSON file into app/data/. See Adding Frameworks.
Usage Guide
Creating an assessment
- Click Assessments → New Assessment
- Enter a name, pick a framework, add scope notes, and optionally set a recurrence schedule
- Click Submit — the assessment opens in Draft status
From the assessment detail view, switch to the Controls tab and click Edit on any control to set notes, evidence links, ownership, and override status.
Evidence
Upload files on the Evidence tab. Files are associated with a specific control, given a description and expiry date, and can be approved or rejected by admins. Full evidence packages (PDF report + all files + manifest CSV) are downloadable as a ZIP.
Findings
Log issues on the Findings tab. Each finding has severity (critical → informational), status, and a remediation owner + target date. Closing a finding automatically stamps the close date.
Requests for Information (RFIs)
Create RFIs with priority levels and due dates. Assignees respond inline; admins close RFIs when resolved. Useful for coordinating evidence requests with external auditors.
Dashboard
The Dashboard shows org-wide posture across all active assessments:
- Overall compliance score and per-framework bar chart
- Open findings by severity (doughnut chart)
- Overdue controls count
- Assessments due for renewal in the next 30 days
- Assessment pipeline (draft / in_review / approved count)
Assessment Lifecycle
| Action | Allowed by | Transition |
|---|---|---|
| Submit for Review | Contributor | Draft → In Review |
| Approve | Admin | In Review → Approved |
| Return to Draft | Admin / Contributor | In Review → Draft |
| Archive | Admin | Any → Archived |
Each transition creates a signed-off record with comments, visible on the Audit Log tab.
Coverage Scoring
CAAMS uses partial-credit scoring rather than a simple pass/fail:
| Status | Meaning |
|---|---|
| Covered | All required capability tags are satisfied by selected tools |
| Partial | Some required tags are present but not all |
| Not Covered | No required capability tags are satisfied |
| Not Applicable | Excluded from scope with a documented justification |
score = (covered + 0.5 × partial) / applicable_total × 100
Authentication
CAAMS uses JWT-based authentication (HS256, pure-Python — no C dependencies). All API endpoints except /health and /auth/setup require a valid bearer token.
Logging in
curl -X POST https://localhost:8443/auth/login \ -d "username=admin&password=YourPassword" # returns {"access_token": "...", "token_type": "bearer", "role": "admin"} # Pass the token in subsequent requests: curl https://localhost:8443/assessments \ -H "Authorization: Bearer <token>"
Tokens expire after 8 hours. Login is rate-limited to 10 attempts per minute per IP.
Roles
Roles are assigned at account creation and enforced on every endpoint.
| Role | Permissions |
|---|---|
| admin | Full access — create/delete assessments, manage users, approve lifecycle transitions, approve evidence, manage API tokens |
| contributor | Create and edit assessments, update notes, ownership, evidence, findings, and RFIs |
| viewer | Read-only access to all assessments, results, evidence, and findings |
| auditor | External access via scoped share link — no account needed. Read-only, limited to exactly the controls shared, with comment thread access |
MFA / TOTP
CAAMS supports time-based one-time passwords (TOTP) compatible with Google Authenticator, Authy, and any RFC 6238 authenticator app. MFA is per-user and optional — users enroll themselves; admins can force-disable if a device is lost.
Enrollment
- User calls
GET /auth/mfa/setup— returns a TOTP secret and an SVG QR code (no image dependencies) - User scans the QR code in their authenticator app
- User calls
POST /auth/mfa/confirmwith a valid 6-digit code to activate MFA on their account
Login with MFA enabled
POST /auth/loginreturns a short-lived MFA challenge token instead of a full JWT pair- Client prompts the user for their 6-digit code
POST /auth/mfa/verify-loginexchanges the challenge token + code for a normal JWT pair
Admin recovery
If a user loses their authenticator device, an admin can call DELETE /auth/mfa/admin/{user_id} to disable MFA on their account so they can re-enroll.
MFA endpoints
| Method | Path | Description |
|---|---|---|
| GET | /auth/mfa/setup | Generate TOTP secret and SVG QR code for enrollment |
| POST | /auth/mfa/confirm | Verify code and activate MFA on the calling user's account |
| POST | /auth/mfa/disable | Disable MFA (requires a valid current code) |
| POST | /auth/mfa/verify-login | Exchange MFA challenge token + code for a JWT pair |
| DELETE | /auth/mfa/admin/{user_id} | Admin: force-disable MFA for a user (device recovery) |
API Tokens
For CI/CD pipelines and external integrations, create long-lived API tokens via Admin → API Tokens or via the API:
curl -X POST https://localhost:8443/api-tokens \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{"name": "ci-pipeline", "expires_in_days": 365}'
API Reference
The full interactive Swagger UI is available at /docs on any running CAAMS instance.
Auth
| Method | Path | Description |
|---|---|---|
| GET | /auth/setup-needed | Returns {"needed": true} if no users exist |
| POST | /auth/setup | Create the first admin account (one-time only) |
| POST | /auth/login | Exchange credentials for a JWT (form-encoded, rate-limited) |
| GET | /auth/me | Current user profile |
| GET | /auth/users | List all users (admin only) |
| POST | /auth/users | Create a new user (admin only) |
| PATCH | /auth/users/{id} | Update role, password, or active flag (admin only) |
| DELETE | /auth/users/{id} | Delete a user (admin only) |
Frameworks & Tools
| Method | Path | Description |
|---|---|---|
| GET | /frameworks | List all frameworks (includes control_count) |
| GET | /frameworks/{id}/controls | List controls for a framework |
| GET | /tools | List all tools in the catalog |
| POST | /tools | Add a tool (admin only) |
| DELETE | /tools/{id} | Remove a tool (admin only) |
| POST | /tools/upload | Bulk-import tools from a JSON array (admin only) |
| GET | /tools/template/download | Download the JSON import template |
Assessments
| Method | Path | Description |
|---|---|---|
| POST | /assessments | Create an assessment (contributor) |
| GET | /assessments | List all assessments |
| GET | /assessments/history | List with pre-computed metrics (scores, counts) |
| GET | /assessments/{id} | Get assessment metadata |
| DELETE | /assessments/{id} | Delete an assessment (admin only) |
| POST | /assessments/{id}/clone | Clone with tools, notes, and ownership (contributor) |
| POST | /assessments/{id}/lifecycle | Submit / approve / return / archive |
| GET | /assessments/{id}/signoffs | Lifecycle sign-off history |
| GET | /assessments/{id}/results | Full coverage results for all controls |
| GET | /assessments/{id}/tools | Tools currently in scope |
| PATCH | /assessments/{id}/tools | Update tool selection |
| GET | /assessments/{id}/recommendations | Ranked tool recommendations to close coverage gaps |
Controls
| Method | Path | Description |
|---|---|---|
| PATCH | /assessments/{id}/controls/{cid}/notes | Upsert notes, evidence URL, override status, applicability (contributor) |
| PATCH | /assessments/{id}/controls/{cid}/review | Set review status (contributor) |
| PATCH | /assessments/{id}/controls/{cid}/ownership | Set owner, team, and evidence owner (contributor) |
Evidence
| Method | Path | Description |
|---|---|---|
| GET | /assessments/{id}/evidence | List evidence files for an assessment |
| POST | /assessments/{id}/evidence | Upload a file (multipart/form-data) |
| PATCH | /assessments/{id}/evidence/{fid}/approval | Approve or reject with reason (admin only) |
| GET | /assessments/{id}/evidence/{fid}/download | Download the file |
| DELETE | /assessments/{id}/evidence/{fid} | Delete an evidence file |
Findings & RFIs
| Method | Path | Description |
|---|---|---|
| GET | /assessments/{id}/findings | List findings |
| POST | /assessments/{id}/findings | Create a finding (contributor) |
| PATCH | /assessments/{id}/findings/{fid} | Update a finding (contributor) |
| DELETE | /assessments/{id}/findings/{fid} | Delete a finding (contributor) |
| GET | /assessments/{id}/rfis | List RFIs |
| POST | /assessments/{id}/rfis | Create an RFI |
| PATCH | /assessments/{id}/rfis/{rid} | Update RFI status (close, reopen) |
| POST | /assessments/{id}/rfis/{rid}/responses | Submit a response to an RFI |
Exports
| Method | Path | Returns |
|---|---|---|
| GET | /assessments/{id}/export | XLSX workbook (6 sheets) |
| GET | /assessments/{id}/export/soa | Standalone SOA XLSX |
| GET | /assessments/{id}/export/pdf | PDF report |
| GET | /assessments/{id}/export/evidence-package | ZIP (PDF + evidence files + manifest CSV) |
Dashboard, Audit Log & Misc
| Method | Path | Description |
|---|---|---|
| GET | /dashboard | Org-wide compliance dashboard data |
| GET | /audit-log | Global audit log (admin, paginated) |
| GET | /audit-log/assessment/{id} | Per-assessment audit log |
| GET | /api-tokens | List API tokens for current user |
| POST | /api-tokens | Create a long-lived API token |
| DELETE | /api-tokens/{id} | Revoke an API token |
| GET | /crosswalk | Tag-based crosswalk between two frameworks |
| GET | /crosswalk/multi-framework | Coverage of all frameworks from one assessment |
| GET | /health | Health check (no auth required) |
Exports
XLSX Workbook GET /assessments/{id}/export
| Sheet | Contents |
|---|---|
| Summary | Assessment name, framework, status, dates, and aggregate compliance metrics |
| Coverage Report | All controls with status, override, owners, covered-by tools, missing tags, notes, evidence URL, and finding counts |
| Evidence Checklist | One row per required evidence item per control, with owners and status |
| SOA | Statement of Applicability — applicable flag, exclusion reason, override, and reviewer per control |
| Findings | All findings with severity (color-coded), status, remediation owner, and dates |
| Recommendations | Tools not in scope ranked by number of additional controls they would cover |
PDF Report GET /assessments/{id}/export/pdf
- Branded cover page with assessment name, framework, and date
- Executive summary with aggregate metrics
- Tools-in-scope table
- Color-coded per-control coverage table
- Findings table with severity
Evidence ZIP Package GET /assessments/{id}/export/evidence-package
- Complete PDF report included at the root
- All evidence files grouped by control ID in subdirectories
- Manifest CSV mapping each file to its control, description, uploader, and approval status
Environment Variables
In a bare-metal install, variables are loaded from /etc/caams.env by the systemd unit. For Docker Compose, they go in .env at the repo root.
Core
| Variable | Required | Default | Description |
|---|---|---|---|
CAAMS_SECRET_KEY | Required | — | 64-char hex string to sign JWTs. Generate with python3 -c "import secrets; print(secrets.token_hex(32))". App refuses to start without it. |
DATABASE_URL | Optional | sqlite:///caams.db | SQLAlchemy connection string. Defaults to SQLite in the working directory. Set to postgresql://user:pass@host/db for Postgres (Docker Compose sets this automatically). |
CAAMS_HOST | Optional | 0.0.0.0 | Bind address |
CAAMS_PORT | Optional | 8443 | Port (bare metal). Docker Compose defaults to 8000. |
CAAMS_WORKERS | Optional | 2 | Uvicorn worker processes. Increase on multi-core hosts. |
CAAMS_ENABLE_DOCS | Optional | false | Set to true to expose the Swagger UI at /docs and /redoc. Not recommended in production. |
CAAMS_CORS_ORIGIN | Optional | — | Your intranet hostname (e.g. https://caams.corp.local) to allow credentialed cross-origin requests. |
CAAMS_USE_HSTS | Optional | false | Set to true to send a Strict-Transport-Security header. Enable only when terminating TLS at the app. |
CAAMS_LOG_LEVEL | Optional | INFO | Logging verbosity: DEBUG, INFO, WARNING, ERROR |
Sessions & Uploads
| Variable | Required | Default | Description |
|---|---|---|---|
CAAMS_ACCESS_TOKEN_MINUTES | Optional | 30 | JWT access token lifetime in minutes. |
CAAMS_REFRESH_TOKEN_DAYS | Optional | 7 | JWT refresh token lifetime in days. |
CAAMS_MAX_UPLOAD_MB | Optional | 50 | Maximum evidence file upload size in MB. |
CAAMS_INVITE_TOKEN_HOURS | Optional | 72 | How long an invite link remains valid before expiring. |
CAAMS_APP_BASE_URL | Optional | — | Public base URL of your CAAMS instance (e.g. https://caams.corp.local). Used to build invite links in emails. |
SMTP (Email)
Leave CAAMS_SMTP_HOST unset to disable email entirely. When disabled, invite tokens are returned in the API response instead of being emailed.
| Variable | Required | Default | Description |
|---|---|---|---|
CAAMS_SMTP_HOST | Optional | — | SMTP server hostname. Leave unset to disable outbound email. |
CAAMS_SMTP_PORT | Optional | 587 | SMTP port. |
CAAMS_SMTP_FROM | Optional | — | From address for outbound email (e.g. caams@corp.local). |
CAAMS_SMTP_USER | Optional | — | SMTP username. Omit for anonymous relay. |
CAAMS_SMTP_PASSWORD | Optional | — | SMTP password. |
CAAMS_SMTP_USE_TLS | Optional | true | Set to true (default) for STARTTLS. Set to false for plain SMTP on internal relays. |
SSO / OIDC
Leave CAAMS_OIDC_ISSUER unset to disable SSO. When enabled, CAAMS auto-provisions users on first login using the OIDC sub claim.
| Variable | Required | Default | Description |
|---|---|---|---|
CAAMS_OIDC_ISSUER | Optional | — | OIDC provider issuer URL (e.g. https://accounts.google.com). Must expose /.well-known/openid-configuration. |
CAAMS_OIDC_CLIENT_ID | Optional | — | OAuth2 client ID registered with your IdP. |
CAAMS_OIDC_CLIENT_SECRET | Optional | — | OAuth2 client secret. |
CAAMS_OIDC_DEFAULT_ROLE | Optional | viewer | Role assigned to new users provisioned via SSO. One of viewer, contributor, admin. |
MFA
| Variable | Required | Default | Description |
|---|---|---|---|
CAAMS_MFA_ISSUER | Optional | CAAMS | Issuer name shown in authenticator apps (Google Authenticator, Authy, etc.). |
Docker Compose
The included docker-compose.yml starts three containers — the CAAMS app, a PostgreSQL 16 database, and a daily backup service — with a single command. This is the recommended path for teams that already run Docker or want Postgres instead of SQLite.
Prerequisites
- Docker Engine 24+ and Docker Compose v2 (
docker compose) - Python 3 on the host only if you want to generate secrets with the one-liner below (optional — any random 32-byte hex string works)
1. Create .env
Two variables are required. The compose file will refuse to start without them.
# Generate both secrets in one step:
echo "CAAMS_SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_hex(32))')" >> .env
echo "DB_PASSWORD=$(python3 -c 'import secrets; print(secrets.token_hex(16))')" >> .env
All other variables are optional — see the full list in Environment variables. To add them, append lines to .env:
# SSO / OIDC CAAMS_OIDC_ISSUER=https://your-idp.example.com CAAMS_OIDC_CLIENT_ID=caams CAAMS_OIDC_CLIENT_SECRET=changeme # Outbound email for invite links CAAMS_SMTP_HOST=smtp.example.com CAAMS_SMTP_FROM=caams@example.com CAAMS_SMTP_USER=caams@example.com CAAMS_SMTP_PASSWORD=changeme CAAMS_APP_BASE_URL=https://caams.example.com
2. Start all services
docker compose up -d ✓ Container caams-enterprise-postgres-1 Started ✓ Container caams-enterprise-caams-1 Started ✓ Container caams-enterprise-pg_backup-1 Started
CAAMS is now reachable at http://<host>:8000. Place a TLS-terminating reverse proxy (nginx, Caddy, Traefik) in front of it for HTTPS.
What's running
| Container | Image | Role |
|---|---|---|
| caams | caams-enterprise:latest | FastAPI app on port 8000 |
| postgres | postgres:16-alpine | Primary database; data persisted in postgres_data volume |
| pg_backup | postgres:16-alpine | Runs pg_dump on start then every 24 h; keeps the 7 most recent .sql.gz files in the caams_backups volume |
Common operations
# Tail live logs docker compose logs -f caams # Check health docker compose ps # Restart app after a config change docker compose restart caams # Access Postgres directly docker compose exec postgres psql -U caams caams # List database backups docker compose exec pg_backup ls /backups
Upgrading
The simplest path is setup.sh — it's idempotent and handles everything: image rebuild, migrations, and re-seeding. Your .env and all volumes are preserved.
git pull bash setup.sh
Or manually if you prefer:
git pull
docker compose build caams
docker compose up -d
docker compose exec caams alembic upgrade head
docker compose exec caams python seed.py # safe to re-run; skips existing data
systemd (Bare Metal)
install_service.sh automates a full production install on any systemd-based Linux host (tested on Ubuntu 22.04+). Uses SQLite — no external database required.
sudo bash install_service.sh
The installer:
- Verifies prerequisites (
systemctl,python3) - Copies the app to
/opt/caams/ - Creates a virtualenv at
/opt/caams/venvand installs all dependencies - Requires TLS certificates at
certs/cert.pem+certs/key.pem— prints generation instructions and aborts if missing - Creates a dedicated
caamssystem user and group - Generates a random
CAAMS_SECRET_KEYand writes it to/etc/caams.env(readable only by root and the service account) - Seeds the database if
caams.dbdoes not exist - Writes the systemd unit to
/etc/systemd/system/caams.service, enables, and starts it
sudo systemctl status caams sudo journalctl -u caams -f # live log stream sudo tail -f /opt/caams/logs/app.log # app events only # Swap in a CA-signed certificate: sudo cp your-cert.pem /opt/caams/certs/cert.pem sudo cp your-key.pem /opt/caams/certs/key.pem sudo systemctl restart caams
Upgrading
install_service.sh is idempotent — re-running it after a git pull syncs the app files, updates dependencies, and restarts the service. Existing secrets and the database are left untouched.
git pull sudo bash install_service.sh
Logging
Two rotating log files are written to logs/ (10 MB per file, 5 backups). All entries also appear in stdout / journalctl -u caams.
logs/access.log — every HTTP request
2026-02-21 12:34:56 | 10.0.0.1 | POST /auth/login | 200 | 13ms 2026-02-21 12:34:57 | 10.0.0.1 | GET /assessments/5/results | 200 | 87ms
logs/app.log — application events
2026-02-21 12:34:55 | INFO | STARTUP | CAAMS v1.0.0 | database ready 2026-02-21 12:34:56 | WARNING | LOGIN failed | username=badguy | ip=10.0.0.3 2026-02-21 12:34:57 | INFO | LOGIN success | user=admin | role=admin | ip=10.0.0.1 2026-02-21 12:35:10 | INFO | ASSESSMENT created | id=5 | name=Q1 Audit | by=admin 2026-02-21 12:36:00 | INFO | LIFECYCLE | assessment=5 | action=approve | by=admin
Adding Frameworks
Create a JSON file in app/data/:
{
"name": "My Framework",
"version": "v1.0",
"description": "Optional description.",
"controls": [
{
"control_id": "MF-1",
"title": "Control Title",
"description": "What this control requires.",
"required_tags": ["tag-a", "tag-b"],
"optional_tags": ["tag-c"],
"evidence": [
"Evidence item description 1",
"Evidence item description 2"
]
}
]
}
Then add the filename to FRAMEWORK_FILES in seed.py and re-run:
python seed.py # safe to re-run — existing data is not affected
app/data/tools_catalog.json. To list all available tags:python3 -c "import json; data=json.load(open('app/data/tools_catalog.json')); print('\n'.join(sorted({t for tool in data for t in tool['capabilities']})))"Adding Tools
Three ways to add tools:
1. Edit the catalog JSON
{
"name": "My Tool",
"category": "EDR",
"description": "Endpoint detection and response.",
"capabilities": ["endpoint-protection", "malware-detection", "EDR"]
}
Re-run python seed.py to load it.
2. UI — Tools → Add Tool
Available to admins in the web UI under the Tools section.
3. API bulk import
curl -X POST https://localhost:8443/tools/upload \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '[{"name":"My Tool","category":"EDR","capabilities":["EDR"]}]'
Project Structure
caams/ ├── app/ │ ├── data/ # Framework JSON files and tool catalog │ ├── engine/ │ │ └── mapper.py # Coverage computation engine │ ├── importers/ │ │ └── cis_xlsx.py # CIS Controls XLSX importer │ ├── routers/ │ │ ├── api_tokens.py # Long-lived API token management │ │ ├── assessments.py # Assessment CRUD, lifecycle, notes, clone │ │ ├── audit_log.py # Immutable audit log endpoints │ │ ├── auditor_shares.py # Scoped external auditor share links │ │ ├── auth.py # Login, setup, user management │ │ ├── crosswalk.py # Framework crosswalk │ │ ├── dashboard.py # Org-wide executive dashboard │ │ ├── evidence.py # Evidence upload, approval, download │ │ ├── export.py # XLSX export │ │ ├── findings.py # Findings and risk acceptance tracker │ │ ├── frameworks.py # Framework and control endpoints │ │ ├── pdf_export.py # PDF report + evidence ZIP │ │ ├── rfi.py # Request for Information endpoints │ │ └── tools.py # Tool catalog endpoints │ ├── auth.py # JWT, password hashing, role dependencies │ ├── database.py # SQLAlchemy engine + session factory │ ├── jwt_utils.py # Pure-Python HS256 JWT (no C deps) │ ├── limiter.py # Shared rate limiter │ ├── logging_config.py # Rotating file handler │ ├── main.py # FastAPI app, middleware, CORS, lifespan │ ├── models.py # SQLAlchemy ORM models │ └── schemas.py # Pydantic v2 request/response schemas ├── static/ │ ├── index.html # SPA shell and all view templates │ ├── app.js # Alpine.js — all state and API calls │ └── app.css # Inputs, buttons, cards, badges ├── caams.service # systemd unit file ├── install_service.sh # Production installer ├── seed.py # Database seeder ├── start.sh # Dev start script (HTTPS) └── requirements.txt