Run a hunt
Start a hunt of a live asset or backup on demand, or hunt backwards through an asset’s recovery points to find the last clean one. Then follow the hunt until it finishes and pull what it found.
Before you begin
Section titled “Before you begin”- A personal access token with access to hunts and Hunt findings. Starting a hunt needs write access. See Authentication.
- The cloud identifiers of the target, such as the EC2 instance ID, AWS account and region. Find assets returns them.
- For AWS targets, a Cloud Connector deployed in that account and region. See Connect cloud accounts.
Start an on-demand hunt
Section titled “Start an on-demand hunt”POST /gateway/api/v1/scans/schedule (Schedule hunts and investigations) takes a scans array. Each entry picks a target with a type discriminator and sets what to look for in scan_type. Supported type values:
| Platform | type values |
|---|---|
| AWS | ec2, aws_ebs, aws_ebs_snapshot, aws_efs, aws_s3, aws_backup_ec2_rp, aws_backup_ebs_rp, aws_ec2_ami |
| Elastio recovery points | erp_scan |
| Azure | azure_vm, azure_managed_disk, azure_blob_container, azure_file_share, azure_machine_image |
| IBM | ibm_cos_bucket |
| GCP | gcp_compute_vm, gcp_compute_disk, gcp_cloud_storage_bucket, gcp_compute_machine_image |
For most targets, scan_type accepts malware, ransomware, entropy, data_classification, hunt with hunt_rules, and pursuit with an optional pursuit_playbook. At least one option must be true, hunt_rules must be non-empty, or a Pursuit playbook must be set. erp_scan is the exception: it takes malware, ransomware and fs_check, all three required. Custom hunt rules aren’t available for on-demand Azure, IBM or GCP hunts.
curl -X POST "https://acme.app.elastio.com/gateway/api/v1/scans/schedule" \ -H "Authorization: Bearer $ELASTIO_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "scans": [ { "type": "ec2", "aws_instance_id": "i-0abc123def4567890", "aws_account_id": "123456789012", "aws_region": "us-east-1", "scan_type": { "malware": true, "ransomware": true } } ] }'import os, requests
BASE = "https://acme.app.elastio.com"HEADERS = {"Authorization": f"Bearer {os.environ['ELASTIO_TOKEN']}"}
body = { "scans": [ { "type": "ec2", "aws_instance_id": "i-0abc123def4567890", "aws_account_id": "123456789012", "aws_region": "us-east-1", "scan_type": {"malware": True, "ransomware": True}, } ]}resp = requests.post(f"{BASE}/gateway/api/v1/scans/schedule", headers=HEADERS, json=body, timeout=30)resp.raise_for_status()for r in resp.json()["results"]: print(r["resource"], r["result"], r.get("job_id") or r.get("error_message") or r.get("queued"))$Base = "https://acme.app.elastio.com"$Headers = @{ Authorization = "Bearer $env:ELASTIO_TOKEN" }$Body = @{ scans = @( @{ type = "ec2" aws_instance_id = "i-0abc123def4567890" aws_account_id = "123456789012" aws_region = "us-east-1" scan_type = @{ malware = $true; ransomware = $true } } )} | ConvertTo-Json -Depth 5$Result = Invoke-RestMethod -Method Post -Uri "$Base/gateway/api/v1/scans/schedule" ` -Headers $Headers -ContentType "application/json" -Body $Body$Result.results{ "results": [ { "result": "job_id", "job_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d", "resource": "aws_ec2", "aws_ec2": { "aws_instance_id": "i-0abc123def4567890", "aws_region": "us-east-1", "aws_account_id": "123456789012" } } ]}Each result’s result field tells you which other field is set:
job_id: the hunt’s job ID. Use it to poll the hunt and read what it found.error_message: this hunt couldn’t be scheduled. Other entries in the same request can still succeed.queued: Azure, IBM and GCP hunts are queued on a standalone cluster. You getcluster_nameandposition, but no job ID to poll.
Hunt back through recovery points
Section titled “Hunt back through recovery points”Start a hunt across recovery points to work backwards in time through an AWS asset’s backups. Without oldest_timestamp it keeps going until it finds a clean one. With oldest_timestamp it only hunts backups newer than that time. Set exactly one property of asset_kind:
ec2_backup: AWS Backup recovery points of an EC2 instanceec2_drs: AWS DRS recovery snapshots of an EC2 instanceebs: EBS snapshots of a volumeebs_backup: AWS Backup recovery points of an EBS volume
scan_type is one of none, malware, ransomware, encryption-detection, ransomware-malware, ransomware-encryption-detection, malware-encryption-detection or full. max_parallel_scan_jobs can be 1–50. aws_backup_vault defaults to Default, and asset_account_id defaults to the connector’s account.
curl -X POST "https://acme.app.elastio.com/gateway/scans/v1/threat-hunt" \ -H "Authorization: Bearer $ELASTIO_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "tenant_name": "acme", "cloud_connector": { "aws_account_id": "123456789012", "aws_region": "us-east-1" }, "asset_kind": { "ec2_backup": { "instance_id": "i-0abc123def4567890" } }, "scan_type": "ransomware-malware", "max_parallel_scan_jobs": 5 }'body = { "tenant_name": "acme", "cloud_connector": {"aws_account_id": "123456789012", "aws_region": "us-east-1"}, "asset_kind": {"ec2_backup": {"instance_id": "i-0abc123def4567890"}}, "scan_type": "ransomware-malware", "max_parallel_scan_jobs": 5,}resp = requests.post(f"{BASE}/gateway/scans/v1/threat-hunt", headers=HEADERS, json=body, timeout=30)resp.raise_for_status()hunt = resp.json()print(hunt["success"], hunt.get("job_id"), hunt["message"])$Body = @{ tenant_name = "acme" cloud_connector = @{ aws_account_id = "123456789012"; aws_region = "us-east-1" } asset_kind = @{ ec2_backup = @{ instance_id = "i-0abc123def4567890" } } scan_type = "ransomware-malware" max_parallel_scan_jobs = 5} | ConvertTo-Json -Depth 5Invoke-RestMethod -Method Post -Uri "$Base/gateway/scans/v1/threat-hunt" ` -Headers $Headers -ContentType "application/json" -Body $Body{ "success": true, "message": "…", "job_id": "…", "aws_backup_vault": "Default", "timestamp": "2026-10-12T09:15:03Z"}tenant_name is your tenant identifier (in these examples, the acme subdomain). The hunt runs asynchronously. If submission fails, success is false and error explains why.
Check a hunt’s status
Section titled “Check a hunt’s status”Read the hunt result through the asset it targeted. The response lists every entry in scans, each with its own kind, status and type, plus the job’s overall health and the sources that triggered it.
curl "https://acme.app.elastio.com/gateway/api/v1/assets/6f1c2a9e-3b4d-4e8f-9a7b-1c2d3e4f5a6b/scanjobs/5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d" \ -H "Authorization: Bearer $ELASTIO_TOKEN"asset_id = "6f1c2a9e-3b4d-4e8f-9a7b-1c2d3e4f5a6b"job_id = "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d"resp = requests.get(f"{BASE}/gateway/api/v1/assets/{asset_id}/scanjobs/{job_id}", headers=HEADERS, timeout=30)resp.raise_for_status()job = resp.json()print(job["health"], [(s["kind"], s["status"]) for s in job["scans"]])$AssetId = "6f1c2a9e-3b4d-4e8f-9a7b-1c2d3e4f5a6b"$JobId = "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d"$Job = Invoke-RestMethod -Uri "$Base/gateway/api/v1/assets/$AssetId/scanjobs/$JobId" -Headers $Headers$Job.scans | Select-Object kind, status, type, updated_at{ "job_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d", "scans": [ { "id": "8e7d6c5b-4a3f-4e2d-9c1b-0a9f8e7d6c5b", "target": { "id": "6f1c2a9e-3b4d-4e8f-9a7b-1c2d3e4f5a6b", "kind_tag": "asset" }, "job_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d", "kind": "ransomware_scan", "status": "…", "type": "direct", "created_at": "2026-10-12T09:15:04Z", "updated_at": "2026-10-12T09:31:47Z" } ], "health": "…", "type": "direct", "timestamp": "2026-10-12T09:31:47Z", "triggered_by_sources": [], "triggered_by": null}Polling guidance
Section titled “Polling guidance”- Hunts take minutes, not seconds. Poll every 30–60 seconds and back off for long-running jobs. Don’t poll in a tight loop.
- Give up after a timeout that suits the asset’s size, and log the job ID so you can come back to it.
- The spec doesn’t list the possible
statusvalues for an entry inscans. Check the values your tenant returns before you hard-code a terminal state. GET /gateway/api/v1/assets/{assetId}/scanjobslists recent hunt results for an asset. Filter bykind,type,backupType,triggerSourceordays, and page withpageSizeandpageToken. This is useful when you don’t have the job ID, for example for hunts a policy started.- Azure, IBM and GCP hunts that come back as
queuedhave no job ID to poll. Follow them through the asset’s hunt history instead.
List what a hunt found
Section titled “List what a hunt found”When the hunt has finished, page through its findings. Filter by asset_id, threat_kind, threat_status (comma-separated) or suppressed. Pagination uses page_size (default 10) with next_page_token and prev_page_token.
curl "https://acme.app.elastio.com/gateway/api/v1/scanjobs/5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d/threats?page_size=50&suppressed=false" \ -H "Authorization: Bearer $ELASTIO_TOKEN"params = {"page_size": 50, "suppressed": "false"}threats = []while True: resp = requests.get(f"{BASE}/gateway/api/v1/scanjobs/{job_id}/threats", headers=HEADERS, params=params, timeout=30) resp.raise_for_status() body = resp.json() threats.extend(body.get("data", [])) token = (body.get("pagination") or {}).get("next_page_token") if not token: break params["next_page_token"] = token
for t in threats: print(t["kind"], t["name"], t["location"], t["detected_at"])$Threats = @()$Token = $nulldo { $Uri = "$Base/gateway/api/v1/scanjobs/$JobId/threats?page_size=50&suppressed=false" if ($Token) { $Uri += "&next_page_token=$([uri]::EscapeDataString($Token))" } $Page = Invoke-RestMethod -Uri $Uri -Headers $Headers $Threats += $Page.data $Token = $Page.pagination.next_page_token} while ($Token)
$Threats | Select-Object kind, name, location, detected_at{ "status": 200, "total": 1, "pagination": { "next_page_token": "", "prev_page_token": "" }, "data": [ { "id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b", "cc_id": "…", "backup_ids": ["4b3a2c1d-0e9f-4a8b-9c7d-6e5f4a3b2c1d"], "name": "…", "location": "/var/www/uploads/invoice.pdf.exe", "kind": "…", "status": "…", "detected_at": "2026-10-12T09:31:40Z", "ignored_at": null, "resolved_at": null, "detected_by_scan_ids": ["8e7d6c5b-4a3f-4e2d-9c1b-0a9f8e7d6c5b"], "suppressed_until": null, "suppression_reason": null, "file_name": "invoice.pdf.exe", "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" } ], "message": "OK"}Related reads for the same hunt:
GET /gateway/api/v1/scanjobs/{scanJobId}/backups: the backups the hunt covered, with their check statuses and protected assets.GET /gateway/api/v2/scanjobs/{scanJobId}/threats/pursuit/findings: Pursuit findings from an investigation that ran in this job. See Investigate with Pursuit.GET /gateway/api/v2/scanjobs/{scanJobId}/threats/custom/findings: custom hunt matches from this hunt, one row per target and rule. See Hunt rules.
To suppress a finding you’ve triaged, see Review Hunt findings.
All endpoints
Section titled “All endpoints”Hunts
2 operations in the reference →Hunt Results
4 operations in the reference →- GET
/gateway/api/v1/scanjobs/{scanJobId}/backupsList the backups a hunt covered - GET
/gateway/api/v1/scanjobs/{scanJobId}/threatsList what a hunt found - GET
/gateway/api/v2/scanjobs/{scanJobId}/threats/pursuit/findingsList Pursuit findings from an investigation - GET
/gateway/api/v2/scanjobs/{scanJobId}/threats/custom/findingsList custom hunt findings from a hunt