Recovery verdicts and evidence
Auditors and cyber insurers increasingly ask for proof that you can recover, not just that you have backups. This guide shows how to pull Elastio’s recovery verdict for the whole estate, list the assets that aren’t proven recoverable and why, and export the evidence behind each verdict.
What a verdict means
Section titled “What a verdict means”Each live asset is checked against an effective target: a recovery RPO in seconds (r_rpo_seconds) and any required copies (immutable, air_gapped, cross_region). The target comes from one of four sources, recorded in effective_target.source: program_class_floor, program_node, policy or default.
proven: there’s a recent enough clean recovery point that has the required copies.qualifying_pointnames it.unproven: no such point exists.unproven_reasonsexplains why, for exampleno_recovery_point,no_clean_point,r_rpo_exceeded,missing_immutable_copyorair_gapped_copy_not_observable.
A proven asset can also be at risk (at_risk: true), meaning it’s proven today but likely to lapse. The reasons are no_enabled_policy, plan_not_activated, near_expiry and policy_looser_than_target. It can also be proven with exceptions, where a commitment has been relaxed by an approved, time-limited governance override listed in exceptions.
Get the estate-wide verdict
Section titled “Get the estate-wide verdict”This is the headline figure: counts.proven_count out of estate_total live assets, from the last evaluation (evaluated_at). You get it whether or not you have committed a Resilience Program. Assets excluded from the program are still counted here, measured against their policy or default target. An asset that hasn’t been evaluated yet counts as unproven, with no reason.
curl "https://acme.app.elastio.com/gateway/shared-services/v1/recovery-verdict" \ -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/shared-services/v1/recovery-verdict", headers=HEADERS, timeout=60)resp.raise_for_status()v = resp.json()c = v["counts"]print(f"{c['proven_count']}/{v['estate_total']} assets proven recoverable (evaluated {v['evaluated_at']})")print("top unproven reasons:", sorted(c["unproven_by_reason"].items(), key=lambda kv: -kv[1])[:3])$Base = "https://acme.app.elastio.com"$Headers = @{ Authorization = "Bearer $env:ELASTIO_TOKEN" }$v = Invoke-RestMethod -Uri "$Base/gateway/shared-services/v1/recovery-verdict" -Headers $Headers"{0}/{1} proven (evaluated {2})" -f $v.counts.proven_count, $v.estate_total, $v.evaluated_at$v.counts.unproven_by_reasonExample response (trimmed):
{ "verdict_model": "recovery_verdict_v1", "evaluated_at": "2026-10-12T11:00:00Z", "program_committed": true, "estate_total": 555, "counts": { "proven_count": 489, "unproven_count": 66, "at_risk_count": 21, "proven_with_exceptions_count": 8, "unproven_by_reason": { "r_rpo_exceeded": 31, "no_clean_point": 4, "missing_immutable_copy": 22, "no_recovery_point": 12 }, "at_risk_by_reason": { "near_expiry": 14, "no_enabled_policy": 7 } }, "by_class": { "critical": { "asset_total": 60, "proven_count": 56, "unproven_count": 4, "at_risk_count": 2, "proven_with_exceptions_count": 1, "unproven_by_reason": { "r_rpo_exceeded": 4 }, "at_risk_by_reason": { "near_expiry": 2 } }, "important": { … }, "standard": { … }, "excluded": { … } }, "by_target_source": { "program_class_floor": 300, "program_node": 140, "policy": 80, "default": 35 }}An asset with several unproven reasons counts under each of them, so the reason counts can add up to more than unproven_count.
List the assets that aren’t proven
Section titled “List the assets that aren’t proven”The per-asset list gives you a work list, or the appendix for an audit. Filters are combined with AND:
| Parameter | Values |
|---|---|
scope |
estate (default, every live asset) or program (the program’s promised assets) |
verdict |
proven, unproven |
reason |
One unproven or at-risk reason code |
class |
critical, important, standard, excluded |
with_exceptions |
true for assets proven with exceptions |
page, page_size |
1-based page number. Page size defaults to 500, which is also the maximum. |
Rows are ordered by asset name. total is the count before paging, so with scope=program, the totals match the snapshot’s totals.proven_count and totals.unproven_count. An asset that hasn’t been evaluated yet has recovery: null.
curl "https://acme.app.elastio.com/gateway/shared-services/v1/recovery-verdict/assets?verdict=unproven&class=critical&page=1&page_size=500" \ -H "Authorization: Bearer $ELASTIO_TOKEN"import csv
params = {"verdict": "unproven", "class": "critical", "page": 1, "page_size": 500}rows = []while True: resp = requests.get(f"{BASE}/gateway/shared-services/v1/recovery-verdict/assets", headers=HEADERS, params=params, timeout=60) resp.raise_for_status() page = resp.json() rows.extend(page["assets"]) if params["page"] * page["page_size"] >= page["total"]: break params["page"] += 1
with open("unproven-critical.csv", "w", newline="") as f: w = csv.writer(f) w.writerow(["asset_id", "name", "kind", "account_id", "region", "reasons", "newest_clean_point_at"]) for a in rows: r = a["recovery"] or {} w.writerow([a["asset_id"], a["name"], a["kind"], a["account_id"], a["region"], ";".join(r.get("unproven_reasons", [])), r.get("newest_clean_point_at")])$page = 1$rows = @()do { $resp = Invoke-RestMethod -Uri "$Base/gateway/shared-services/v1/recovery-verdict/assets?verdict=unproven&class=critical&page=$page&page_size=500" -Headers $Headers $rows += $resp.assets $page++} while (($page - 1) * $resp.page_size -lt $resp.total)
$rows | Select-Object asset_id, name, kind, account_id, region, @{ n = "reasons"; e = { $_.recovery.unproven_reasons -join ";" } } | Export-Csv -NoTypeInformation unproven-critical.csvExample response (trimmed):
{ "assets": [ { "asset_id": "2b7e9c1d-3f4a-4b5c-8d6e-7f8091a2b3c4", "name": "prod-db-01", "kind": "…", "class": "critical", "account_id": "123456789012", "region": "us-east-1", "recovery": { "verdict": "unproven", "unproven_reasons": ["r_rpo_exceeded"], "at_risk": false, "at_risk_reasons": [], "proven_with_exceptions": false, "exceptions": [], "effective_target": { "r_rpo_seconds": 86400, "required_copies": ["immutable"], "source": "program_class_floor" }, "newest_clean_point_at": "2026-10-09T02:04:00Z", "qualifying_point": null, "compromise_started_at": null, "evaluated_at": "2026-10-12T11:00:00Z" } } ], "page": 1, "page_size": 500, "total": 4}Export one asset’s evidence
Section titled “Export one asset’s evidence”/gateway/shared-services/v1/assets/{asset_id}/evidenceGet the aggregated evidence envelope for one assetThe evidence envelope collects everything Elastio knows about one asset’s recoverability in a single document:
determination: the latest hunt result (clean,infectedornot_scanned) and when it ran.recovery_posture: the last clean recovery point, backup age against the RPO target, and up to 200 recovery points from the last 365 days.recovery_verdict: the same object asrecoveryin the per-asset list.coverage: the full matrix of analyses that were run, including clean ones.tested_backup: the latest backup, inventory, hunt, restore and verification results.threatsandviolations: open findings on the asset and its attached volumes or disks.
If one section can’t be read, the call still returns 200 and names that section in degraded. Check degraded before you treat a missing section as “no evidence”. A 404 means the tenant has no such live asset.
curl "https://acme.app.elastio.com/gateway/shared-services/v1/assets/2b7e9c1d-3f4a-4b5c-8d6e-7f8091a2b3c4/evidence" \ -H "Authorization: Bearer $ELASTIO_TOKEN" \ -o prod-db-01-evidence.jsonimport json
asset_id = "2b7e9c1d-3f4a-4b5c-8d6e-7f8091a2b3c4"resp = requests.get(f"{BASE}/gateway/shared-services/v1/assets/{asset_id}/evidence", headers=HEADERS, timeout=60)resp.raise_for_status()evidence = resp.json()
if evidence["degraded"]: print("WARNING: incomplete sections:", evidence["degraded"])print("hunt state:", evidence["determination"]["state"], "as of", evidence["determination"]["as_of"])verdict = evidence["recovery_verdict"]print("verdict:", verdict["verdict"] if verdict else "not evaluated")
with open(f"{asset_id}-evidence.json", "w") as f: json.dump(evidence, f, indent=2)$assetId = "2b7e9c1d-3f4a-4b5c-8d6e-7f8091a2b3c4"$evidence = Invoke-RestMethod -Uri "$Base/gateway/shared-services/v1/assets/$assetId/evidence" -Headers $Headersif ($evidence.degraded.Count -gt 0) { Write-Warning "Incomplete sections: $($evidence.degraded -join ', ')" }$evidence | ConvertTo-Json -Depth 20 | Set-Content "$assetId-evidence.json"Example response (trimmed):
{ "asset": { "id": "2b7e9c1d-3f4a-4b5c-8d6e-7f8091a2b3c4", "name": "prod-db-01", "kind": "…", "platform": "aws", "health": "…" }, "determination": { "state": "clean", "as_of": "2026-10-12T02:04:00Z", "elapsed_seconds": 34560 }, "recovery_posture": { "last_clean_rp": { "timestamp": "2026-10-12T02:04:00Z", "policy_names": ["prod-db-nightly"], "backup_vendor": null }, "objectives": { "backup_age_seconds": 34560, "rpo_target_seconds": 86400 }, "recovery_points": [ { "id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d", "when": "2026-10-12T02:04:00Z", "status": "…", "is_last_clean": true } ], "coverage_sources": ["…"], "efs_backup_association": null }, "recovery_verdict": { "verdict": "proven", "unproven_reasons": [], "at_risk": false, "at_risk_reasons": [], "proven_with_exceptions": false, "exceptions": [], "effective_target": { "r_rpo_seconds": 86400, "required_copies": ["immutable"], "source": "program_class_floor" }, "qualifying_point": { "recovery_point_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d", "created_at": "2026-10-12T02:04:00Z", "copies": ["immutable"] }, "evaluated_at": "2026-10-12T11:00:00Z" }, "coverage": [ { "method": "…", "status": "ran", "verdict": "clean", "at": "2026-10-12T02:04:00Z", "subject": { "id": "…", "kind": "…", "label": "…" }, "finding_id": null, "freshness": null, "score": null, "note": null } ], "tested_backup": { "backup": { "at": "…", "result": "…" }, "iscan": { "at": "…", "result": "…" }, "restore": null, "inventory": null, "verification": null }, "threats": [], "violations": { "by_severity": { "critical": 0, "high": 0, "medium": 0, "low": 0 }, "earliest_detected": null, "items": [] }, "degraded": []}Export the fleet-wide evidence ledger
Section titled “Export the fleet-wide evidence ledger”The ledger lists every analysis record across the fleet, one row per analysis, in the same shape as an asset’s coverage entries plus the asset. coverage and population summarise the whole scope, not just the current page: how many analyses ran, came back clean, raised findings, weren’t run, were inconclusive or errored, and how many assets were expected versus retrieved. Inconclusive and errored analyses are never counted in ran or expected. Page with page_size (default 500) and next_page_token.
curl "https://acme.app.elastio.com/gateway/shared-services/v1/evidence/ledger?page_size=500" \ -H "Authorization: Bearer $ELASTIO_TOKEN"params = {"page_size": 500}records, seen_tokens = [], set()while True: resp = requests.get(f"{BASE}/gateway/shared-services/v1/evidence/ledger", headers=HEADERS, params=params, timeout=120) resp.raise_for_status() page = resp.json() records.extend(page["records"]) token = page["pagination"].get("next_page_token") if not token or token in seen_tokens or len(records) >= page["pagination"]["total"]: break seen_tokens.add(token) params["next_page_token"] = token
cov, pop = page["coverage"], page["population"]print(f"{cov['ran']}/{cov['expected']} expected analyses ran; {cov['findings']} findings; {cov['inconclusive']} inconclusive")print(f"{pop['assets_retrieved']}/{pop['assets_expected']} assets retrieved; cap applied: {pop['cap_applied']}")$ledger = Invoke-RestMethod -Uri "$Base/gateway/shared-services/v1/evidence/ledger?page_size=500" -Headers $Headers$ledger.coverage$ledger.population$ledger.records | Select-Object @{ n = "asset"; e = { $_.asset.name } }, method, status, verdict, at | Export-Csv -NoTypeInformation ledger-page1.csvExample response (trimmed):
{ "scope": { "kind": "…", "label": "…", "params": {} }, "coverage": { "expected": 1660, "ran": 1602, "clean": 1571, "findings": 31, "not_run": 58, "inconclusive": 6, "errored": 2 }, "population": { "assets_expected": 555, "assets_attempted": 555, "assets_retrieved": 553, "assets_failed": 2, "cap": 0, "cap_applied": false }, "records": [ { "asset": { "id": "2b7e9c1d-3f4a-4b5c-8d6e-7f8091a2b3c4", "name": "prod-db-01", "kind": "…", "platform": "aws", "health": "…" }, "method": "…", "status": "ran", "verdict": "clean", "at": "2026-10-12T02:04:00Z", "subject": { "id": "…", "kind": "…", "label": "…" }, "finding_id": null, "freshness": null, "score": null, "note": null } ], "pagination": { "offset": 0, "page_size": 500, "total": 1660, "next_page_token": "…" }, "degraded": []}