CSPM Service API
CSPM (Cloud Security Posture Management) is the FastAPI service that scans a cloud
account with a fixed set of checks and reports the results and the compliance
frameworks each check maps to. It scans AWS only. This page lists the routes defined in
open-security-cspm/app/main.py and how to reach them through the gateway.
In the examples, <host> is the name you reach the gateway by, and IDs, account
numbers and credentials are placeholders.
Table of Contents
- Base URL and routing
- Authentication and permissions
- Endpoint summary
- List providers
- List checks
- Start a scan
- Start several scans
- Read a scan’s status
- Read a scan’s report
- Read a scan’s compliance report
- Cancel a scan
- Team summary
- Team compliance summary
- Team compliance findings
- Configuration
- Health check
- Rate limits
- Errors
Base URL and routing
CSPM serves its API under /api/v1/. The gateway replaces /api/v1/cspm/ with
/api/v1/:
https://<host>/api/v1/cspm/<path> -> cspm /api/v1/<path>
So /api/v1/scans on the service is https://<host>/api/v1/cspm/scans through the
gateway. Set up a shell for the examples:
CA=open-security-gateway/ssl/wildbox.crt
BASE="https://<host>/api/v1/cspm"
Authentication and permissions
The gateway authenticates every request and passes the caller’s user, team and role to CSPM in trusted headers, with the secret that proves the request came from the gateway. Use either credential:
- JWT bearer token. Sign in with
POST https://<host>/auth/jwt/login(form-encodedusernameandpassword) and send theaccess_tokenasAuthorization: Bearer <token>. - API key. Create one with
POST /api/v1/identity/api-keysorPOST /api/v1/identity/teams/{team_id}/api-keys(see the Identity Service API) and send it asX-API-Key: <key>.
TOKEN=$(curl -s --cacert "$CA" -X POST "https://<host>/auth/jwt/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "username=analyst@example.com" \
--data-urlencode "password=<password>" | jq -r .access_token)
Scopes and roles:
- API key scopes are checked by the gateway. A key limited by scopes needs
read(orwrite) forGETrequests andwriteforPOSTandDELETE(ROUTE_SCOPESinopen-security-gateway/nginx/lua/auth_handler.lua);adminsatisfies both. A session token is not limited by scopes. CSPM does not check the scopes again: its dependency reads the user, team, role and gateway secret only (get_current_userinapp/main.py). - No route asks for a role. Any member of a team can start, read and cancel the team’s scans.
- Scans belong to the team that started them. Reading, reporting on or
cancelling another team’s scan answers
403(Access denied); a scan that does not exist answers404. The team summaries count the caller’s team only.
Endpoint summary
| Method | Gateway path | Service path | Description |
|---|---|---|---|
GET |
/api/v1/cspm/providers |
/api/v1/providers |
Providers a scan can name |
GET |
/api/v1/cspm/checks |
/api/v1/checks |
Check catalog (see the defect under List checks) |
POST |
/api/v1/cspm/scans |
/api/v1/scans |
Start a scan |
POST |
/api/v1/cspm/batch/scans |
/api/v1/batch/scans |
Start several scans |
GET |
/api/v1/cspm/scans/{scan_id} |
/api/v1/scans/{scan_id} |
Status of a scan |
GET |
/api/v1/cspm/scans/{scan_id}/report |
/api/v1/scans/{scan_id}/report |
Report of a completed scan |
GET |
/api/v1/cspm/scans/{scan_id}/compliance |
/api/v1/scans/{scan_id}/compliance |
Per-framework figures of one scan (see the defect under Read a scan’s compliance report) |
DELETE |
/api/v1/cspm/scans/{scan_id} |
/api/v1/scans/{scan_id} |
Cancel a scan |
GET |
/api/v1/cspm/dashboard/summary |
/api/v1/dashboard/summary |
Scan count and findings of the team |
GET |
/api/v1/cspm/compliance/summary |
/api/v1/compliance/summary |
Compliance of the team’s accounts |
GET |
/api/v1/cspm/compliance/findings |
/api/v1/compliance/findings |
Check verdicts of the team’s accounts |
There is no route that lists a team’s scans, none that reads a batch by its
batch_id, and none that stores cloud credentials: every scan request carries its
own. scan_id is a UUID in lowercase; anything else answers 422.
List providers
GET /api/v1/cspm/providers
curl -s --cacert "$CA" "$BASE/providers" \
-H "Authorization: Bearer $TOKEN"
{
"providers": [
{"provider": "aws", "name": "Amazon Web Services", "checks": 22}
]
}
A provider is listed when CSPM can open a session for it (SESSION_FACTORIES in
app/providers.py) and loaded at least one enabled check for it. Only AWS has
both. checks is the number of checks a scan runs when it names no check_ids.
The scan routes refuse every provider that is not in this list.
List checks
GET /api/v1/cspm/checks
Query parameters, all optional: provider (aws, gcp or azure), category
and severity (both compared without regard to case).
Defect: this route answers 500 whenever at least one check matches. The
catalog entries the check runner returns have no remediation field
(get_available_checks in app/checks/runner.py), and the response model requires
one (CheckMetadataSchema in app/schemas.py), so building the response fails and
the handler answers:
{
"error": {
"code": 500,
"message": "Failed to list checks",
"type": "HTTPException",
"request_id": "<request id>"
}
}
What it does answer today:
| Request | Answer |
|---|---|
| No filter, or filters that match a check | 500, as above |
Filters that match no check, such as provider=gcp |
200 with {"total_checks": 0, "checks": [], "providers": [], "categories": []} |
A provider that is not aws, gcp or azure |
500, as above |
The checks themselves are listed in the
CSPM README:
22 AWS checks, in open-security-cspm/app/checks/aws/. The number of checks a scan
runs is also in List providers.
Start a scan
POST /api/v1/cspm/scans
curl -s --cacert "$CA" -X POST "$BASE/scans" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"provider": "aws",
"credentials": {
"auth_method": "access_key",
"access_key_id": "<access key id>",
"secret_access_key": "<secret access key>"
},
"account_id": "123456789012",
"account_name": "Production",
"regions": ["us-east-1", "eu-west-1"]
}'
Request fields (ScanRequest in app/schemas.py):
| Field | Required | Description |
|---|---|---|
provider |
yes | aws. The schema also accepts gcp and azure, which the route then refuses with 400 |
credentials |
yes | The provider’s credentials; for AWS, the fields below |
account_id |
yes | The account’s identifier, as a string. The summaries group scans by provider and account_id |
account_name |
no | A name for the account, returned in the report |
regions |
no | Regions to scan. Without it: us-east-1, us-west-2 and eu-west-1 |
check_ids |
no | Check ids to run. Without it: every enabled check. An id that names no check is ignored |
metadata |
no | An object passed to the worker with the task. requested_by and team_id in it are overwritten with the caller’s |
AWS credentials (AWSCredentials):
| Field | Required | Description |
|---|---|---|
auth_method |
no | access_key (default) or assume_role; another value answers 422 |
access_key_id |
yes | Access key id |
secret_access_key |
yes | Secret access key |
region |
no | Region of the session, us-east-1 by default |
role_arn |
for assume_role |
ARN of the IAM role to assume |
external_id |
no | External id sent with assume_role |
Response (202 Accepted):
{
"scan_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"status": "started",
"provider": "aws",
"account_id": "123456789012",
"started_at": "2026-10-03T10:30:00.118204",
"estimated_duration_minutes": 15
}
- The API only queues the scan. It encrypts the credentials into Redis with
CSPM_CREDENTIAL_KEYfor five minutes, records the scan for the caller’s team and queues a Celery task; thecspm-workerservice runs it and deletes the credentials when it starts. The credentials are never returned by any route. estimated_duration_minutesis a fixed figure, not a measurement: 15 for AWS, plus 2 for each region beyond three, halved (5 at least) whencheck_idsis given (_estimate_scan_durationinapp/utils.py).- Credentials are checked when the worker opens the session, not here. An access
key id that is not 16 to 128 letters, digits or underscores, or
assume_rolewithout the ARN of an IAM role, is accepted with202and the scan then readsfailed. Well-formed keys that AWS rejects do not fail the scan: a check that meets an error records a result with statuserror, or no result at all, and the scan readscompleted. - Times are UTC without an offset, here and in every other response.
A provider other than aws answers 400, before anything is stored or queued:
{
"error": {
"code": 400,
"message": "Unsupported provider: gcp. Supported providers: aws.",
"type": "HTTPException",
"request_id": "<request id>"
}
}
Start several scans
POST /api/v1/cspm/batch/scans
curl -s --cacert "$CA" -X POST "$BASE/batch/scans" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"scans": [
{"provider": "aws", "account_id": "111111111111",
"credentials": {"access_key_id": "<id>", "secret_access_key": "<secret>"}},
{"provider": "aws", "account_id": "222222222222",
"credentials": {"access_key_id": "<id>", "secret_access_key": "<secret>"}}
]
}'
scans is a list of scan requests, each with the fields of
Start a scan. Response (200 OK):
{
"batch_id": "4d8836d6-b1b9-4256-9d58-9df16ab1f66d",
"total_scans": 2,
"scans": [
{
"scan_id": "f91e7425-0611-40f4-afca-a7a05ed32404",
"provider": "aws",
"account_id": "111111111111",
"task_id": "f91e7425-0611-40f4-afca-a7a05ed32404",
"status": "started"
},
{
"scan_id": "8a040509-fa95-48e6-9c87-129436237146",
"provider": "aws",
"account_id": "222222222222",
"task_id": "8a040509-fa95-48e6-9c87-129436237146",
"status": "started"
}
],
"started_at": "2026-10-03T10:30:00.226235"
}
- Each scan is started exactly as
POST /scansstarts one, and is read, reported on and cancelled by its ownscan_id.task_idis the same value. - A batch that names a provider other than
awsis refused whole with400; none of its scans is stored or queued. - An empty
scanslist is accepted and answers200withtotal_scans0. - The request also accepts
parallel_execution_limitandmetadata. Neither is used: every scan is queued at once, and the worker’s concurrency decides how many run together. batch_idis recorded with each scan and is not a key to read anything by.
Read a scan’s status
GET /api/v1/cspm/scans/{scan_id}
curl -s --cacert "$CA" "$BASE/scans/<scan-id>" \
-H "Authorization: Bearer $TOKEN"
{
"scan_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"status": "running",
"provider": "aws",
"account_id": "123456789012",
"started_at": "2026-10-03T10:30:00.117873",
"completed_at": null,
"progress": {
"current_status": "initializing",
"total_checks": null,
"completed_checks": null,
"current_region": null
}
}
| Field | Description |
|---|---|
status |
queued until a worker takes the scan, running while it runs, then completed, failed or cancelled. unknown when the task is in a state the route does not map |
completed_at |
Set only when status is completed |
progress |
Set only while status is running. current_status is running when the worker took the task and initializing once it opened the session; the worker reports no counts, so total_checks, completed_checks and current_region are always null |
A scan fails when its credentials expired before a worker took it (five minutes after
it was queued), when no session can be opened with them, or when it exceeds its time
limit (SCAN_TIMEOUT_SECONDS). The response does not say which: the reason is in the
worker’s log.
A scan is kept for CSPM_REPORT_RETENTION_DAYS days (90 by default) from the last
time it was written; after that its id answers 404.
Read a scan’s report
GET /api/v1/cspm/scans/{scan_id}/report
The report the worker stored when the scan completed. Abridged to one result:
{
"scan_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"provider": "aws",
"account_id": "123456789012",
"account_name": "Production",
"regions": ["us-east-1", "eu-west-1"],
"started_at": "2026-10-03T10:30:04.070242",
"completed_at": "2026-10-03T10:41:12.904416",
"status": "completed",
"total_checks": 4,
"passed_checks": 1,
"failed_checks": 2,
"error_checks": 1,
"skipped_checks": 0,
"not_implemented_checks": 0,
"critical_findings": 0,
"high_findings": 1,
"medium_findings": 0,
"low_findings": 0,
"info_findings": 0,
"compliance_score": 33.33333333333333,
"results": [
{
"check_id": "AWS_CLOUDTRAIL_001",
"resource_id": "<resource id>",
"resource_type": "<resource type>",
"resource_name": null,
"region": "us-east-1",
"status": "failed",
"message": "<what the check found>",
"details": {},
"remediation": "<how to fix it>",
"compliance_frameworks": ["CIS AWS Foundations", "PCI DSS"],
"timestamp": "2026-10-03T10:30:09.070362"
}
],
"summary": {
"duration_seconds": 668.834174,
"checks_by_status": {"passed": 1, "failed": 2, "error": 1, "skipped": 0, "not_implemented": 0},
"findings_by_severity": {"critical": 0, "high": 1, "medium": 0, "low": 0, "info": 0, "unknown": 1},
"compliance_frameworks": {
"CIS AWS Foundations": {"total": 4, "passed": 1, "failed": 2, "compliance_percentage": 25.0},
"PCI DSS": {"total": 4, "passed": 1, "failed": 2, "compliance_percentage": 25.0}
},
"recommendations": ["<how to fix it>"]
}
}
The figures above are illustrative. The fields come from ScanReportSchema in
app/schemas.py and ScanReport in app/checks/framework.py:
| Field | Description |
|---|---|
total_checks and the *_checks counts |
Count results, not checks: a check gives one result per resource it inspected, in each region |
critical_findings to info_findings |
Failed results, by the severity their check declares |
compliance_score |
passed_checks / (passed_checks + failed_checks) * 100; results that errored, were skipped or are not implemented do not count |
results[].status |
passed, failed, error, skipped or not_implemented |
summary.findings_by_severity.unknown |
Failed results whose check is not in the catalog. The count has no top-level field in this response |
summary.compliance_frameworks |
Per framework: total counts every result tagged with it, whatever its status |
summary.recommendations |
The five most frequent remediation texts among failed results |
| Scan is | Answer |
|---|---|
| Completed | 200 with the report |
| Queued, running, failed or cancelled before it completed | 400, Scan is not completed |
| Completed, and its stored report cannot be read | 500, Scan report not available |
Read a scan’s compliance report
GET /api/v1/cspm/scans/{scan_id}/compliance
Optional query parameter: framework, the exact name of one framework.
Defect: this route answers 500 for every completed scan. The handler gives
generated_at a date and time (get_compliance_report in app/main.py) where the
response model declares a string (ComplianceReportResponse in app/schemas.py),
so building the response fails:
{
"error": {
"code": 500,
"message": "Failed to generate compliance report",
"type": "HTTPException",
"request_id": "<request id>"
}
}
For a scan that has not completed it answers as
the report route does: 400, 403 or 404.
The same per-framework figures are in the report itself, under
summary.compliance_frameworks, and for the team’s accounts together in
Team compliance summary.
Cancel a scan
DELETE /api/v1/cspm/scans/{scan_id}
curl -s --cacert "$CA" -X DELETE "$BASE/scans/<scan-id>" \
-H "Authorization: Bearer $TOKEN"
Response (200 OK):
{"message": "Scan cancelled successfully"}
The route revokes the scan’s task, terminating it if a worker is running it, and
records the scan as cancelled. It does not look at the scan’s state first: a
scan that already completed or failed is recorded as cancelled too. Its stored
report stays readable and keeps counting in the team summaries.
Team summary
GET /api/v1/cspm/dashboard/summary
Query parameter: days, from 1 to 365, 30 by default.
{
"total_scans": 4,
"last_scan_at": "2026-10-03T10:30:00.848581",
"summary_period_days": 30,
"accounts_assessed": 1,
"compliance_score": 33.3,
"total_findings": 2,
"critical_findings": 0,
"high_findings": 1,
"medium_findings": 0,
"low_findings": 0,
"info_findings": 0,
"unknown_severity_findings": 1
}
| Field | Description |
|---|---|
total_scans, last_scan_at |
Every scan of the team that is still kept, whatever its status and whatever days says |
accounts_assessed |
Accounts with a stored report of a scan started in the period |
compliance_score |
Passed share of the passed and failed results, to one decimal; null when there are none |
total_findings and the severity counts |
Failed results, by the severity their check declares today |
unknown_severity_findings |
Failed results whose check is no longer in the catalog |
Every figure but the first two comes from the newest report of each of the team’s
accounts (an account is a provider and an account_id) among the scans started in
the last days days. With no such report the counts are 0 and compliance_score is
null: nothing was assessed, which is not 0%.
Team compliance summary
GET /api/v1/cspm/compliance/summary
Query parameters: days (1 to 365, 30 by default) and provider, which keeps the
scans of one provider.
{
"total_resources": 3,
"compliant_resources": 1,
"non_compliant_resources": 2,
"overall_score": 33.3,
"frameworks": [
{
"name": "CIS AWS Foundations",
"total_checks": 3,
"passed_checks": 1,
"failed_checks": 2,
"compliance_percentage": 33.3,
"last_assessment": "2026-10-03T10:41:12.904416"
}
],
"scans_considered": 1,
"summary_period_days": 30,
"provider_filter": null,
"last_updated": "2026-10-03T10:41:12.904416"
}
| Field | Description |
|---|---|
total_resources |
Distinct resources (account and resource_id) with at least one passed or failed result |
non_compliant_resources |
Resources with at least one failed result |
overall_score |
Passed share of the passed and failed results, to one decimal; null when there are none |
frameworks |
One entry per framework name the results carry, sorted by name. total_checks counts passed and failed results only, unlike summary.compliance_frameworks in a report |
scans_considered |
Reports the figures come from: the newest of each account in the period |
last_updated |
Completion time of the newest of those scans |
With no report in the period every count is 0, frameworks is empty and
overall_score and last_updated are null.
Team compliance findings
GET /api/v1/cspm/compliance/findings
One entry per passed or failed result in the reports the
compliance summary reads, newest scan first.
| Parameter | Default | Description |
|---|---|---|
days |
30 |
Period, 1 to 365 |
provider |
none | Keep the scans of one provider |
framework |
none | Keep the results tagged with this exact framework name |
severity |
none | critical, high, medium, low or info, in lowercase |
status |
none | passed or failed |
limit |
100 |
Page size, 1 to 1000 |
offset |
0 |
Results to skip |
curl -s --cacert "$CA" "$BASE/compliance/findings?status=failed&limit=1" \
-H "Authorization: Bearer $TOKEN"
{
"findings": [
{
"finding_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479:0",
"scan_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"check_id": "AWS_CLOUDTRAIL_001",
"title": "CloudTrail Enabled in All Regions",
"frameworks": ["CIS AWS Foundations", "PCI DSS"],
"resource_id": "<resource id>",
"resource_type": "<resource type>",
"region": "us-east-1",
"status": "failed",
"severity": "high",
"description": "<what the check found>",
"remediation": "<how to fix it>",
"last_checked": "2026-10-03T10:30:09.070362"
}
],
"total_count": 2,
"limit": 1,
"offset": 0,
"has_more": true
}
finding_idis the scan’s id and the position of the result in its report. It is not stable across scans, and no route reads a finding by it.titleandseveritycome from the check’s metadata as the service has it now. A result whose check is no longer in the catalog has itscheck_idastitleandseveritynull.- A filter value that matches nothing gives an empty page, not an error.
Configuration
Environment variables the CSPM API and its worker read (app/config.py,
app/credential_crypto.py and open-security-shared/gateway_auth.py). The worker
is the cspm-worker service, built from the same directory; it needs the same
values except GATEWAY_INTERNAL_SECRET.
| Variable | Default | Description |
|---|---|---|
SECRET_KEY |
none | Required, at least 32 characters: the service does not start without it. docker-compose.yml sets it from CSPM_SECRET_KEY. Also the credential key when CSPM_CREDENTIAL_KEY is not set |
CSPM_CREDENTIAL_KEY |
SECRET_KEY |
Encrypts a scan’s cloud credentials in Redis. Required by docker-compose.yml |
GATEWAY_INTERNAL_SECRET |
none | Checked on every request under /api/v1/. Without it they answer 503 |
REDIS_URL |
redis://localhost:6379/0 |
Scan records, team indexes and reports. Database 3 in docker-compose.yml |
CELERY_BROKER_URL, CELERY_RESULT_BACKEND |
redis://localhost:6379/0 |
The scan queue and the state of queued and running scans. Database 3 in docker-compose.yml |
SCAN_TIMEOUT_SECONDS |
3600 |
Time limit of one scan, 120 to 86400; the API and the worker do not start otherwise. docker-compose.yml sets it from CSPM_SCAN_TIMEOUT_SECONDS |
CSPM_REPORT_RETENTION_DAYS |
90 |
Days a scan’s record, index entry and report are kept, 1 to 3650; the API and the worker do not start otherwise |
MAX_CONCURRENT_SCANS |
5 |
Check executions that run at once within one scan |
CORS_ORIGINS |
["http://localhost:3000"] |
Origins the service’s own CORS middleware allows, as a JSON list |
LOG_LEVEL |
INFO |
Log level |
ENVIRONMENT |
none | /docs, /redoc and /openapi.json are served on the service port only when it is development. Required by docker-compose.yml |
HOST, PORT, WORKERS, DEBUG |
0.0.0.0, 8019, 4, false |
Read only when the module is run directly (python -m app.main); the image starts uvicorn on port 8019 |
How the worker runs scans, its time limit and concurrency, and the Redis memory the reports need are described in the CSPM README.
Health check
CSPM’s health checks are on the service’s own port (bound to 127.0.0.1:8019 in
docker-compose.yml), without authentication. They are not under /api/v1/, so the
gateway does not route them (/api/v1/cspm/health maps to /api/v1/health, which
does not exist).
GET /health/liveanswers{"status": "alive"}while the process runs.GET /healthasks Redis and the Celery workers:
{
"status": "healthy",
"timestamp": "2026-10-03T10:30:00.670356",
"version": "0.1.6",
"uptime_seconds": 3600.3,
"checks": {"redis": "healthy", "celery": "healthy", "api": "healthy"}
}
status is degraded, still with 200, when Redis answers and no worker does.
When Redis cannot be reached the route answers 500 in the
error format, not a health body.
Rate limits
CSPM has no rate limit of its own. The gateway’s limit per team applies to these
routes as to every other; a request over it gets 429 from the gateway. See the
gateway README.
Errors
CSPM errors use the shared Wildbox error format (open-security-shared/errors.py):
{
"error": {
"code": 404,
"message": "Scan not found",
"type": "HTTPException",
"request_id": "<request id>"
}
}
A 422 adds error.details, a list with one entry per invalid field (type, loc
and msg). A request that did not come through the gateway adds error.details
with the reason in error.details.code: GATEWAY_AUTH_REQUIRED,
GATEWAY_SECRET_REQUIRED or INVALID_GATEWAY_HEADERS.
| Status | Meaning |
|---|---|
200 |
The request succeeded. Batch scans and a cancellation answer 200 |
202 |
A single scan was queued |
400 |
A provider that cannot be scanned; a report asked of a scan that has not completed; malformed identity headers |
401 |
No valid credential (answered by the gateway) |
403 |
The scan belongs to another team; the API key’s scopes do not allow the request (answered by the gateway); or the request did not come through the gateway |
404 |
A scan that does not exist or is past its retention, or a path the service does not serve |
422 |
The body, a query parameter or the scan_id is not valid |
429 |
Gateway rate limit exceeded |
500 |
The service failed to handle the request. Also the answer of the two routes marked as defects above, and of every route when Redis cannot be reached |
503 |
GATEWAY_INTERNAL_SECRET is not set in the service |
POST /scans has a 503 (Task queue temporarily unavailable) for a connection
error, but the errors the Redis client and the Celery broker raise are not of the
types it catches, so a request made while Redis is unreachable answers 500.
Related Documentation
- Identity Service API - Sign-in and API keys
- Guardian Service API - Asset and vulnerability management
- Authentication guide - Tokens and API keys through the gateway
- Security Policy - Authentication requirements