| sidebar_position | 4 |
|---|---|
| title | Analytics Endpoints |
| description | REST API endpoints for ProjectAchilles analytics — defense scores, heatmaps, trends, and executions. |
Endpoints under /api/analytics/* query the Elasticsearch-backed view of test results. All require analytics:dashboards:read permission (or higher); the configuration endpoints require analytics:settings:read / analytics:settings:write.
:::tip Use the Programmatic Access guide for examples This page is the parameter reference. For curl examples, common recipes (CI gates, SIEM forwarders, drift detection), and a walkthrough of the three score variants, read the Programmatic Access guide. :::
Most endpoints accept the same filter vocabulary. Pass any combination:
| Param | Type | Description |
|---|---|---|
from |
string | Start of window. Accepts ISO-8601 (2026-04-23T00:00:00Z), calendar dates (2026-04-23), or Elasticsearch date math (now-30d). |
to |
string | End of window. Same formats as from. |
org |
string | Filter by organization UUID (Clerk org id). |
tests |
string | Comma-separated test UUIDs. |
techniques |
string | Comma-separated MITRE ATT&CK technique IDs (e.g. T1486,T1059). |
hostnames |
string | Comma-separated hostnames. |
categories |
string | Comma-separated test categories. |
severities |
string | Comma-separated severities: low, medium, high, critical. |
threatActors |
string | Comma-separated threat-actor names. |
tags |
string | Comma-separated tags. |
errorNames |
string | Comma-separated error names (resolved from exit codes). |
errorCodes |
string | Comma-separated exit codes. |
bundleNames |
string | Comma-separated bundle test names. |
result |
string | One of: all, protected, unprotected, inconclusive. |
scoringMode |
string | all-stages (default — bundle protected only if every stage protected) or any-stage (bundle protected if any stage protected). |
:::warning Default time window is 7 days
If you don't pass from/to, every endpoint applies a now-7d date filter. The dashboard UI defaults to Last 30 days — so the same query that fills the UI may return zero rows when called from the API without explicit dates. Always pass from (and optionally to) when scripting against the API.
:::
GET /api/analytics/defense-score
Returns the aggregate defense score and breakdown counts for the filtered window.
Response shape:
See the guide for when to use each score variant.
GET /api/analytics/defense-score/trend
Returns the score over time for trend visualization.
Additional parameters:
| Param | Type | Description |
|---|---|---|
interval |
string | Histogram bucket size (e.g. day, hour). |
windowDays |
number | Rolling window size, 1–90 days, applied per bucket. |
GET /api/analytics/defense-score/by-test
GET /api/analytics/defense-score/by-technique
GET /api/analytics/defense-score/by-org
GET /api/analytics/defense-score/by-severity
GET /api/analytics/defense-score/by-category
GET /api/analytics/defense-score/by-category-subcategory
GET /api/analytics/defense-score/by-hostname
All accept the shared filter parameters and return protected/unprotected counts grouped by the named dimension.
GET /api/analytics/executions
Flat list of recent test executions, enriched with catalog metadata. Returns at most limit rows.
Additional parameters:
| Param | Type | Description |
|---|---|---|
limit |
number | Max rows returned (default 50). |
Response: array of EnrichedTestExecution (8 fields per row: test_uuid, test_name, hostname, is_protected, org, timestamp, error_code, error_name).
GET /api/analytics/executions/paginated
Filtered, grouped, paginated execution results. Accepts every shared filter plus pagination controls.
Additional parameters:
| Param | Type | Description |
|---|---|---|
page |
number | Page number (1-indexed, default 1). |
pageSize |
number | Results per page (default 25). |
sortField |
string | Field to sort by. |
sortOrder |
string | asc or desc. |
grouped |
boolean | If true, groups results by bundle/standalone identity. |
Response shape depends on the grouped parameter:
{
"data": [
{ /* one EnrichedTestExecution per ES document — ~20 fields incl. test_uuid, test_name,
hostname, is_protected, timestamp, error_code, error_name, category, severity,
techniques, tactics, score, bundle_id, control_id, is_bundle_control,
defender_detected, … */ }
],
"pagination": { "page": 1, "pageSize": 25, "totalItems": 480, "totalPages": 20, "hasNext": true, "hasPrevious": false }
}One row per actual execution. Bundle tests appear as multiple rows (one per control, with is_bundle_control: true and a control_id). Best for SIEM forwarding, CSV exports, or any per-execution analysis.
{
"groups": [
{
"groupKey": "standalone::<test_uuid>::<hostname>::<event_time_ms>",
"type": "standalone" | "bundle",
"representative": { /* one EnrichedTestExecution */ },
"members": [ /* all matching docs in this group */ ],
"protectedCount": 3,
"unprotectedCount": 1,
"totalCount": 4,
"defenderDetected": true
}
],
"pagination": { "page": 1, "pageSize": 25, "totalGroups": 142, "totalDocuments": 480, "totalPages": 6, "hasNext": true, "hasPrevious": false }
}One row per "run" — bundle tests collapse from N controls into a single entry. Best for dashboards and rollups where you want to count "one bundle run" rather than N control checks.
GET /api/analytics/host-test-matrix — heatmap data (host × test)
GET /api/analytics/technique-distribution — protected/unprotected per MITRE technique
GET /api/analytics/test-coverage — protected/unprotected per test
GET /api/analytics/threat-actor-coverage — coverage broken down by threat actor
GET /api/analytics/error-rate — error count and rate over the window
GET /api/analytics/error-rate/trend — error rate over time
GET /api/analytics/results-by-error-type — breakdown by error code/name
All accept the shared filter parameters.
GET /api/analytics/unique-hostnames — distinct host count
GET /api/analytics/unique-tests — distinct test count
GET /api/analytics/canonical-test-count — count of canonical (deduped) tests
GET /api/analytics/organizations — known organizations
GET /api/analytics/available-tests — all tests with any execution
GET /api/analytics/executed-test-uuids — every test UUID that has been executed
GET /api/analytics/available-techniques — MITRE techniques with any data
GET /api/analytics/available-hostnames — hostnames seen in results
GET /api/analytics/available-categories
GET /api/analytics/available-severities
GET /api/analytics/available-threat-actors
GET /api/analytics/available-tags
GET /api/analytics/available-error-names
GET /api/analytics/available-error-codes
GET /api/analytics/available-bundle-names
These power the filter dropdowns in the UI. They accept the shared filter parameters but most ignore date filters by default so the dropdowns stay populated.
POST /api/analytics/executions/archive — archive by group keys
POST /api/analytics/executions/archive-by-date — archive everything before a date
Require analytics:executions:archive permission. See the Programmatic Access guide for the response envelope and idempotency notes.
GET /api/analytics/indices — list configured indices
POST /api/analytics/index/create — create a new index (requires analytics:index:create)
GET /api/analytics/settings — fetch current settings (masked)
POST /api/analytics/settings — save settings (requires analytics:settings:write)
POST /api/analytics/settings/test — test a candidate configuration
Stored encrypted at rest (AES-256-GCM) in ~/.projectachilles/analytics.json on filesystem deployments, or in Vercel Blob on serverless.
- Programmatic Access guide — curl examples, alerting recipes, SIEM-forwarder pattern, time-window gotcha
- Overview & Authentication — auth model
- Bundle Results — schema for multi-control bundle test responses
{ "score": 54.59, // EDR-protected OR Defender-detected, risk-accepted excluded "realScore": 53.21, // EDR-protected only, risk-accepted excluded "rawScore": 53.83, // EDR-protected OR Defender-detected, WITHOUT risk-acceptance exclusion "protectedCount": 1391, // strictly EDR-protected "detectedCount": 36, // Defender caught (EDR missed) "unprotectedCount": 1187, // neither "totalExecutions": 2614, // = protected + detected + unprotected "riskAcceptedCount": 37 // excluded from totalExecutions }