CAAMS Docs
Documentation

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.

5
Frameworks
0
External deps
3
Export formats
Users & assessments

Quick Start

1. Install dependencies

bash
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.

bash
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.

bash
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.

bash
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
bash start.sh
# → https://localhost:8443
Dev shortcut — to skip TLS during local testing:
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:

bash
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

FeatureDetails
Framework coverage mappingMap 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 lifecycleDraft → In Review → Approved → Archived, with signed-off stage transitions and full history on every action.
Recurring assessmentsSet a configurable recurrence interval (e.g. 90 days). CAAMS tracks the next review date and surfaces overdue renewals on the dashboard.
Evidence managementUpload files per-control with descriptions and expiry dates. Admins approve or reject with reasons. Download evidence packages as ZIP.
Findings trackerLog findings with severity (critical → informational), remediation owner, target date, and full status lifecycle (open → remediated → closed).
Risk acceptancesFormally accept residual risk with a rated justification, named approver, and expiry date. Surfaces in exports and the audit trail.
RFIsCreate Requests for Information with priority levels, assignees, and due dates. Assignees respond inline; admins close when resolved.
Control overridesManually override a control's computed status with a justification and optional expiry date.
Ownership trackingAssign owner, team, and evidence owner per control.
Control review workflowTrack per-control review status: not_reviewed → in_review → approved / rejected.
Statement of ApplicabilityMark controls as not applicable with exclusion reasons. Included in the SOA sheet of the XLSX export.
Executive dashboardOrg-wide compliance posture across all frameworks — scores, open findings, overdue controls, and the assessment renewal pipeline.
Framework crosswalkTag-based automatic overlap mapping between any two loaded frameworks.
Assessment cloneDuplicate any assessment including tools, ownership, and notes.
Tool recommendationsRanked list of tools not yet in scope that would close the most coverage gaps.
Auditor share linksScoped, time-limited share links for external auditors. No login required — access limited to exactly the controls you choose.
Immutable audit logEvery state-changing action recorded with user, timestamp, IP, and detail payload. Cannot be edited or deleted.
Role-based accessAdmin / Contributor / Viewer / Auditor roles enforced on every endpoint.
REST APIFull FastAPI backend with interactive Swagger UI at /docs. Long-lived API tokens for CI/CD pipelines.
Rate limitingLogin endpoint is rate-limited to 10 attempts per minute per IP.

Supported Frameworks

FrameworkVersionControls
CIS Controlsv818
NIST Cybersecurity Frameworkv2.06 functions / 22 categories
SOC 2 Trust Services Criteria20179
PCI DSSv4.012 requirements
HIPAA Security Rule45 CFR Part 16416 standards

Additional frameworks can be added by dropping a JSON file into app/data/. See Adding Frameworks.

Usage Guide

Creating an assessment

  1. Click Assessments → New Assessment
  2. Enter a name, pick a framework, add scope notes, and optionally set a recurrence schedule
  3. 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

ActionAllowed byTransition
Submit for ReviewContributorDraft → In Review
ApproveAdminIn Review → Approved
Return to DraftAdmin / ContributorIn Review → Draft
ArchiveAdminAny → 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:

StatusMeaning
CoveredAll required capability tags are satisfied by selected tools
PartialSome required tags are present but not all
Not CoveredNo required capability tags are satisfied
Not ApplicableExcluded from scope with a documented justification
Coverage Score Formula
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

bash
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.

RolePermissions
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

  1. User calls GET /auth/mfa/setup — returns a TOTP secret and an SVG QR code (no image dependencies)
  2. User scans the QR code in their authenticator app
  3. User calls POST /auth/mfa/confirm with a valid 6-digit code to activate MFA on their account

Login with MFA enabled

  1. POST /auth/login returns a short-lived MFA challenge token instead of a full JWT pair
  2. Client prompts the user for their 6-digit code
  3. POST /auth/mfa/verify-login exchanges 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

MethodPathDescription
GET/auth/mfa/setupGenerate TOTP secret and SVG QR code for enrollment
POST/auth/mfa/confirmVerify code and activate MFA on the calling user's account
POST/auth/mfa/disableDisable MFA (requires a valid current code)
POST/auth/mfa/verify-loginExchange MFA challenge token + code for a JWT pair
DELETE/auth/mfa/admin/{user_id}Admin: force-disable MFA for a user (device recovery)

Auditor Share Links

Share a read-only, scoped view of an assessment with an external auditor — no CAAMS account required. Share links can be time-limited and scoped to a subset of controls.

Creating a share link

bash
# Share the full assessment, expires in 30 days
curl -X POST https://localhost:8443/assessments/5/auditor-shares \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"expires_in_days": 30}'

# Scope to specific controls only
curl -X POST https://localhost:8443/assessments/5/auditor-shares \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"expires_in_days": 14, "control_ids": ["CC6", "CC7", "CC9"]}'

The response includes a token. Send the auditor a URL in the form:

url
https://your-host:8443/assessments/5/auditor-view?token=<token>

What the auditor can see

  • Coverage results and control statuses for in-scope controls
  • Evidence metadata (file names, descriptions, approval status — not the files themselves)
  • Open findings
  • A comment thread for RFI-style questions (no login required to comment)
Share links do not grant access to export endpoints, admin settings, or any other assessment.

Auditor share endpoints

MethodPathDescription
GET/assessments/{id}/auditor-sharesList active share links for an assessment
POST/assessments/{id}/auditor-sharesCreate a new share link (optional expiry and control scope)
DELETE/assessments/{id}/auditor-shares/{sid}Revoke a share link immediately
GET/assessments/{id}/auditor-view?token=…Read-only view for external auditors (no auth header needed)
POST/assessments/{id}/comments/external?token=…Add a comment as an external auditor

API Tokens

For CI/CD pipelines and external integrations, create long-lived API tokens via Admin → API Tokens or via the API:

bash
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}'
The plaintext token is returned once at creation. Store it securely — it cannot be retrieved again.

API Reference

The full interactive Swagger UI is available at /docs on any running CAAMS instance.

Auth

MethodPathDescription
GET/auth/setup-neededReturns {"needed": true} if no users exist
POST/auth/setupCreate the first admin account (one-time only)
POST/auth/loginExchange credentials for a JWT (form-encoded, rate-limited)
GET/auth/meCurrent user profile
GET/auth/usersList all users (admin only)
POST/auth/usersCreate 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

MethodPathDescription
GET/frameworksList all frameworks (includes control_count)
GET/frameworks/{id}/controlsList controls for a framework
GET/toolsList all tools in the catalog
POST/toolsAdd a tool (admin only)
DELETE/tools/{id}Remove a tool (admin only)
POST/tools/uploadBulk-import tools from a JSON array (admin only)
GET/tools/template/downloadDownload the JSON import template

Assessments

MethodPathDescription
POST/assessmentsCreate an assessment (contributor)
GET/assessmentsList all assessments
GET/assessments/historyList with pre-computed metrics (scores, counts)
GET/assessments/{id}Get assessment metadata
DELETE/assessments/{id}Delete an assessment (admin only)
POST/assessments/{id}/cloneClone with tools, notes, and ownership (contributor)
POST/assessments/{id}/lifecycleSubmit / approve / return / archive
GET/assessments/{id}/signoffsLifecycle sign-off history
GET/assessments/{id}/resultsFull coverage results for all controls
GET/assessments/{id}/toolsTools currently in scope
PATCH/assessments/{id}/toolsUpdate tool selection
GET/assessments/{id}/recommendationsRanked tool recommendations to close coverage gaps

Controls

MethodPathDescription
PATCH/assessments/{id}/controls/{cid}/notesUpsert notes, evidence URL, override status, applicability (contributor)
PATCH/assessments/{id}/controls/{cid}/reviewSet review status (contributor)
PATCH/assessments/{id}/controls/{cid}/ownershipSet owner, team, and evidence owner (contributor)

Evidence

MethodPathDescription
GET/assessments/{id}/evidenceList evidence files for an assessment
POST/assessments/{id}/evidenceUpload a file (multipart/form-data)
PATCH/assessments/{id}/evidence/{fid}/approvalApprove or reject with reason (admin only)
GET/assessments/{id}/evidence/{fid}/downloadDownload the file
DELETE/assessments/{id}/evidence/{fid}Delete an evidence file

Findings & RFIs

MethodPathDescription
GET/assessments/{id}/findingsList findings
POST/assessments/{id}/findingsCreate a finding (contributor)
PATCH/assessments/{id}/findings/{fid}Update a finding (contributor)
DELETE/assessments/{id}/findings/{fid}Delete a finding (contributor)
GET/assessments/{id}/rfisList RFIs
POST/assessments/{id}/rfisCreate an RFI
PATCH/assessments/{id}/rfis/{rid}Update RFI status (close, reopen)
POST/assessments/{id}/rfis/{rid}/responsesSubmit a response to an RFI

Exports

MethodPathReturns
GET/assessments/{id}/exportXLSX workbook (6 sheets)
GET/assessments/{id}/export/soaStandalone SOA XLSX
GET/assessments/{id}/export/pdfPDF report
GET/assessments/{id}/export/evidence-packageZIP (PDF + evidence files + manifest CSV)

Dashboard, Audit Log & Misc

MethodPathDescription
GET/dashboardOrg-wide compliance dashboard data
GET/audit-logGlobal audit log (admin, paginated)
GET/audit-log/assessment/{id}Per-assessment audit log
GET/api-tokensList API tokens for current user
POST/api-tokensCreate a long-lived API token
DELETE/api-tokens/{id}Revoke an API token
GET/crosswalkTag-based crosswalk between two frameworks
GET/crosswalk/multi-frameworkCoverage of all frameworks from one assessment
GET/healthHealth check (no auth required)

Exports

XLSX Workbook GET /assessments/{id}/export

SheetContents
SummaryAssessment name, framework, status, dates, and aggregate compliance metrics
Coverage ReportAll controls with status, override, owners, covered-by tools, missing tags, notes, evidence URL, and finding counts
Evidence ChecklistOne row per required evidence item per control, with owners and status
SOAStatement of Applicability — applicable flag, exclusion reason, override, and reviewer per control
FindingsAll findings with severity (color-coded), status, remediation owner, and dates
RecommendationsTools 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

VariableRequiredDefaultDescription
CAAMS_SECRET_KEYRequired64-char hex string to sign JWTs. Generate with python3 -c "import secrets; print(secrets.token_hex(32))". App refuses to start without it.
DATABASE_URLOptionalsqlite:///caams.dbSQLAlchemy connection string. Defaults to SQLite in the working directory. Set to postgresql://user:pass@host/db for Postgres (Docker Compose sets this automatically).
CAAMS_HOSTOptional0.0.0.0Bind address
CAAMS_PORTOptional8443Port (bare metal). Docker Compose defaults to 8000.
CAAMS_WORKERSOptional2Uvicorn worker processes. Increase on multi-core hosts.
CAAMS_ENABLE_DOCSOptionalfalseSet to true to expose the Swagger UI at /docs and /redoc. Not recommended in production.
CAAMS_CORS_ORIGINOptionalYour intranet hostname (e.g. https://caams.corp.local) to allow credentialed cross-origin requests.
CAAMS_USE_HSTSOptionalfalseSet to true to send a Strict-Transport-Security header. Enable only when terminating TLS at the app.
CAAMS_LOG_LEVELOptionalINFOLogging verbosity: DEBUG, INFO, WARNING, ERROR

Sessions & Uploads

VariableRequiredDefaultDescription
CAAMS_ACCESS_TOKEN_MINUTESOptional30JWT access token lifetime in minutes.
CAAMS_REFRESH_TOKEN_DAYSOptional7JWT refresh token lifetime in days.
CAAMS_MAX_UPLOAD_MBOptional50Maximum evidence file upload size in MB.
CAAMS_INVITE_TOKEN_HOURSOptional72How long an invite link remains valid before expiring.
CAAMS_APP_BASE_URLOptionalPublic 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.

VariableRequiredDefaultDescription
CAAMS_SMTP_HOSTOptionalSMTP server hostname. Leave unset to disable outbound email.
CAAMS_SMTP_PORTOptional587SMTP port.
CAAMS_SMTP_FROMOptionalFrom address for outbound email (e.g. caams@corp.local).
CAAMS_SMTP_USEROptionalSMTP username. Omit for anonymous relay.
CAAMS_SMTP_PASSWORDOptionalSMTP password.
CAAMS_SMTP_USE_TLSOptionaltrueSet 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.

VariableRequiredDefaultDescription
CAAMS_OIDC_ISSUEROptionalOIDC provider issuer URL (e.g. https://accounts.google.com). Must expose /.well-known/openid-configuration.
CAAMS_OIDC_CLIENT_IDOptionalOAuth2 client ID registered with your IdP.
CAAMS_OIDC_CLIENT_SECRETOptionalOAuth2 client secret.
CAAMS_OIDC_DEFAULT_ROLEOptionalviewerRole assigned to new users provisioned via SSO. One of viewer, contributor, admin.

MFA

VariableRequiredDefaultDescription
CAAMS_MFA_ISSUEROptionalCAAMSIssuer 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.

bash
# 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:

.env — optional additions
# 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

bash
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
caamscaams-enterprise:latestFastAPI app on port 8000
postgrespostgres:16-alpinePrimary database; data persisted in postgres_data volume
pg_backuppostgres:16-alpineRuns pg_dump on start then every 24 h; keeps the 7 most recent .sql.gz files in the caams_backups volume

Common operations

bash
# 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.

bash — recommended
git pull
bash setup.sh

Or manually if you prefer:

bash — manual
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.

bash
sudo bash install_service.sh

The installer:

  1. Verifies prerequisites (systemctl, python3)
  2. Copies the app to /opt/caams/
  3. Creates a virtualenv at /opt/caams/venv and installs all dependencies
  4. Requires TLS certificates at certs/cert.pem + certs/key.pem — prints generation instructions and aborts if missing
  5. Creates a dedicated caams system user and group
  6. Generates a random CAAMS_SECRET_KEY and writes it to /etc/caams.env (readable only by root and the service account)
  7. Seeds the database if caams.db does not exist
  8. Writes the systemd unit to /etc/systemd/system/caams.service, enables, and starts it
bash — post-install
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.

bash
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

access.log
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

app.log
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/:

app/data/my_framework.json
{
  "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:

bash
python seed.py    # safe to re-run — existing data is not affected
Tags must match capability tags in 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

app/data/tools_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

bash
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/
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