Findings and violations
A finding is something Elastio wants you to act on: a Hunt finding on an asset, a policy violation, or a sensitive-data result. This guide covers how to count and list findings, and how to move them through their lifecycle from a script, including restoring a compromised asset from a clean recovery point.
The finding lifecycle
Section titled “The finding lifecycle”Every finding has a disposition of open, acknowledged, suppressed or resolved. The finding endpoints all take a findingId, which comes in one of these forms:
| Finding class | findingId format |
Where to get it |
|---|---|---|
| Hunt finding (Elastio hunts and Pursuit) | threat:{asset_id}:{hunt} |
finding.id on an incident |
| Hunt finding (custom hunts) | threat:{asset_id}:custom:{finding_id} |
finding.id on an incident |
| Violation | The violation’s UUID | id on a row from GET /gateway/api/v2/violations |
Not every transition is allowed for every class:
- Acknowledge marks that an analyst owns the finding. It’s optional, and no other transition requires it.
- Suppress silences a finding that isn’t actionable. It requires a reason and an expiry. A suppressed finding doesn’t re-alert on any channel.
- Resolve records that the problem is fixed. Hunt findings only. Violations are resolved by the evaluator, and resolving a violation ID returns
409 UNSUPPORTED_FINDING_CLASS. If a resolved finding is found again, it reopens automatically. - Recover restores the asset from a clean recovery point and resolves the finding when the restore job succeeds. Hunt findings only.
- Reopen moves a
suppressedorresolvedfinding back toopen. Calling it on anopenoracknowledgedfinding returns409.
Get headline counts
Section titled “Get headline counts”This returns how many Hunt findings, open violations and assets with sensitive data you can see, plus how many unique assets are affected in each bucket. It’s a cheap call for a dashboard tile or a health check.
curl "https://acme.app.elastio.com/gateway/api/v2/findings/count" \ -H "Authorization: Bearer $ELASTIO_TOKEN"import os, requests
BASE = "https://acme.app.elastio.com"HEADERS = {"Authorization": f"Bearer {os.environ['ELASTIO_TOKEN']}"}
resp = requests.get(f"{BASE}/gateway/api/v2/findings/count", headers=HEADERS, timeout=30)resp.raise_for_status()counts = resp.json()print(f"{counts['threats']} Hunt findings on {counts['affected_assets']['threats']} assets")$Base = "https://acme.app.elastio.com"$Headers = @{ Authorization = "Bearer $env:ELASTIO_TOKEN" }Invoke-RestMethod -Uri "$Base/gateway/api/v2/findings/count" -Headers $HeadersExample response:
{ "threats": 7, "violations": 54, "data_classification": 12, "affected_assets": { "threats": 4, "violations": 31, "data_classification": 12, "total": 40 }}List policy violations
Section titled “List policy violations”The violations list returns open violations by default. Pass status=all, or a single status (open, acknowledged, suppressed, resolved), to see others. violation_code, severity, category, policy_id and asset_id each take comma-separated values. Use policy_id=unpoliced to find assets that no policy covers. The list uses token pagination: page_size (default 10, max 100) and next_page_token.
curl -G "https://acme.app.elastio.com/gateway/api/v2/violations" \ -H "Authorization: Bearer $ELASTIO_TOKEN" \ --data-urlencode "severity=critical,high" \ --data-urlencode "page_size=100"params = {"severity": "critical,high", "page_size": 100}violations = []while True: resp = requests.get(f"{BASE}/gateway/api/v2/violations", headers=HEADERS, params=params, timeout=30) resp.raise_for_status() page = resp.json() violations.extend(page.get("data", [])) token = (page.get("pagination") or {}).get("next_page_token") if not token: break params["next_page_token"] = token
for v in violations: print(v["severity"], v["violation_code"], v["asset_name"], v["id"])$uri = "$Base/gateway/api/v2/violations?severity=critical,high&page_size=100"$all = @()do { $page = Invoke-RestMethod -Uri $uri -Headers $Headers $all += $page.data $token = $page.pagination.next_page_token $uri = "$Base/gateway/api/v2/violations?severity=critical,high&page_size=100&next_page_token=$([uri]::EscapeDataString("$token"))"} while ($token)$all | Format-Table severity, violation_code, asset_name, idExample response (trimmed):
{ "total": 18, "data": [ { "id": "0c3e5a7b-1d2f-4a6b-9c8d-7e6f5a4b3c2d", "asset_id": "2b7e9c1d-3f4a-4b5c-8d6e-7f8091a2b3c4", "asset_name": "prod-db-01", "asset_kind": "ec2", "platform": "aws", "platform_details": { "account_id": "123456789012", "region": "us-east-1" }, "policies": [], "violation_code": "…", "title": "…", "category": "…", "severity": "critical", "status": "open", "created_at": "2026-10-01T06:00:00Z", "last_evaluated_at": "2026-10-12T06:00:00Z", "suppressed_until": null, "resolved_at": null, … } ], "pagination": { "next_page_token": "…" }}Change a finding’s disposition
Section titled “Change a finding’s disposition”Suppress a finding
Section titled “Suppress a finding”reason and until are both required. until is an RFC 3339 UTC timestamp and must be in the future. By convention, reason takes the form <disposition>: <note>. A successful call returns 204 No Content.
curl -X POST "https://acme.app.elastio.com/gateway/api/v2/findings/0c3e5a7b-1d2f-4a6b-9c8d-7e6f5a4b3c2d/suppress" \ -H "Authorization: Bearer $ELASTIO_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "reason": "accepted risk: legacy test bucket, decommission scheduled", "until": "2026-12-31T00:00:00Z" }'finding_id = "0c3e5a7b-1d2f-4a6b-9c8d-7e6f5a4b3c2d"resp = requests.post( f"{BASE}/gateway/api/v2/findings/{finding_id}/suppress", headers=HEADERS, json={ "reason": "accepted risk: legacy test bucket, decommission scheduled", "until": "2026-12-31T00:00:00Z", }, timeout=30,)resp.raise_for_status() # 204 No Content on success$findingId = "0c3e5a7b-1d2f-4a6b-9c8d-7e6f5a4b3c2d"$body = @{ reason = "accepted risk: legacy test bucket, decommission scheduled" until = "2026-12-31T00:00:00Z"} | ConvertTo-JsonInvoke-RestMethod -Method Post -Uri "$Base/gateway/api/v2/findings/$findingId/suppress" ` -Headers $Headers -ContentType "application/json" -Body $bodyAcknowledge (/acknowledge) and resolve (/resolve) work the same way. Both take an optional note of up to 1024 characters. Reopen (/reopen) takes an optional reason. All three return 204 on success.
Recover an asset from a clean recovery point
Section titled “Recover an asset from a clean recovery point”/gateway/api/v2/findings/{findingId}/recoverRecover a finding by restoring from a clean recovery pointRecover schedules a restore of the finding’s asset and returns 202 Accepted with a job handle. recovery_point_id is required. The recovery point must protect the finding’s asset, and its health must be clean. When the job succeeds, the finding is resolved automatically and the restore is recorded in its history. This only works for Hunt findings.
curl -X POST "https://acme.app.elastio.com/gateway/api/v2/findings/threat:2b7e9c1d-3f4a-4b5c-8d6e-7f8091a2b3c4:ransomware/recover" \ -H "Authorization: Bearer $ELASTIO_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "recovery_point_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d", "note": "INC-000042" }'from urllib.parse import quote
threat_id = "threat:2b7e9c1d-3f4a-4b5c-8d6e-7f8091a2b3c4:ransomware"resp = requests.post( f"{BASE}/gateway/api/v2/findings/{quote(threat_id, safe='')}/recover", headers=HEADERS, json={"recovery_point_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d", "note": "INC-000042"}, timeout=30,)resp.raise_for_status()print("restore job", resp.json()["job_id"])$threatId = [uri]::EscapeDataString("threat:2b7e9c1d-3f4a-4b5c-8d6e-7f8091a2b3c4:ransomware")$body = @{ recovery_point_id = "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d"; note = "INC-000042" } | ConvertTo-Json$job = Invoke-RestMethod -Method Post -Uri "$Base/gateway/api/v2/findings/$threatId/recover" ` -Headers $Headers -ContentType "application/json" -Body $body$job.job_idExample response (202 Accepted):
{ "job_id": "7e6f5a4b-3c2d-4e1f-9a8b-0c1d2e3f4a5b", "finding_id": "threat:2b7e9c1d-3f4a-4b5c-8d6e-7f8091a2b3c4:ransomware", "recovery_point_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d", "state": "scheduled"}Follow the restore in the jobs catalog. See Track jobs.
Audit what happened to a finding
Section titled “Audit what happened to a finding”The status log is an append-only record of every transition, newest first. It includes automatic ones: suppression expiry, a finding being found again, and recover completion. It uses token pagination (page_size, next_page_token).
curl "https://acme.app.elastio.com/gateway/api/v2/findings/0c3e5a7b-1d2f-4a6b-9c8d-7e6f5a4b3c2d/status-log" \ -H "Authorization: Bearer $ELASTIO_TOKEN"resp = requests.get(f"{BASE}/gateway/api/v2/findings/{finding_id}/status-log", headers=HEADERS, timeout=30)resp.raise_for_status()for entry in resp.json().get("data", []): print(entry["at"], entry["actor"], entry["verb"], f"{entry['from']} -> {entry['to']}")$log = Invoke-RestMethod -Uri "$Base/gateway/api/v2/findings/$findingId/status-log" -Headers $Headers$log.data | Format-Table at, actor, verb, from, toExample response (trimmed):
{ "total": 2, "data": [ { "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "at": "2026-10-12T10:15:00Z", "actor": "analyst@example.com", "from": "open", "to": "suppressed", "verb": "suppress", "source": "manual", "reason": "accepted risk: legacy test bucket, decommission scheduled", "until": "2026-12-31T00:00:00Z", "note": null, "job_id": null, "recovery_point_id": null }, … ], "pagination": { "next_page_token": "…" }}To review dispositions across the tenant, list them with GET /gateway/api/v2/findings/suppressions (filter by class; add include_expired=true to include lapsed suppressions) and GET /gateway/api/v2/findings/resolutions (filter by class and by source=manual or source=recover).
Coverage issues
Section titled “Coverage issues”The v1 Issues endpoints (GET /gateway/api/v1/issues and GET /gateway/api/v1/issues-filters) list protection-coverage problems per asset. You can filter by issue_types, asset_type, platform, account_ids, policy_coverage (covered, not_covered) and tags, and pass type=asset to get one row per affected asset instead of one per issue. Call the filters endpoint to get the valid filter values for your tenant.
All endpoints
Section titled “All endpoints”Findings
9 operations in the reference →- GET
/gateway/api/v2/findings/countGet findings counts - POST
/gateway/api/v2/findings/{findingId}/acknowledgeAcknowledge a finding - POST
/gateway/api/v2/findings/{findingId}/suppressSuppress a finding - POST
/gateway/api/v2/findings/{findingId}/resolveResolve a finding - POST
/gateway/api/v2/findings/{findingId}/recoverRecover a finding by restoring from a clean recovery point - POST
/gateway/api/v2/findings/{findingId}/reopenReopen a finding - GET
/gateway/api/v2/findings/suppressionsList suppressed findings - GET
/gateway/api/v2/findings/resolutionsList resolved findings - GET
/gateway/api/v2/findings/{findingId}/status-logGet a finding's status log
Violations
6 operations in the reference →- GET
/gateway/api/v2/assets/{assetId}/violationsGet open violations for a specific asset - GET
/gateway/api/v2/violations/definitionsGet supported violation definitions - GET
/gateway/api/v2/violationsGet list of open asset violations - GET
/gateway/api/v2/violations/groupsGet v2 violation group summaries - GET
/gateway/api/v2/violations/filtersGet contextual filter options for v2 violations - GET
/gateway/api/v2/violations/{violationId}Get a violation by ID