Hunt rules
Hunt Rules tell Hunt what to look for, including YARA rules. Create and version them as code, keep a library of YARA rules and rulesets, attach rules to policies, and read the custom hunts that ran against an asset.
Before you begin
Section titled “Before you begin”- A personal access token with access to hunt rules. Reads are viewer-level, but every create, update, delete, attach, revert and toggle call is admin-only. See Authentication.
- To attach a rule, the ID of the policy that should run it.
Concepts
Section titled “Concepts”- A hunt rule has a
name(letters, digits,-and_only), aseverity(low,medium,high,critical), astatus(active,tuning,inactive; defaultactive) and asource(CUSTOM,ELASTIO,IMPORTED; defaultCUSTOM). - Rule code lives in
bodies, keyed by primitive:structuralholds SQL,contentholds YARA, andpatternholds a regex. List the primitives the rule uses inprimitives. - Structural SQL can call library YARA with
yara_match('yara/...'). Each library path used must be pinned inyara_refs, and refs that aren’t used are rejected. - Every content change creates a new immutable version. Turning a single signature on or off is tuning, not content, so it doesn’t create a version.
List hunt rules
Section titled “List hunt rules”Filter with comma-separated severity, status, source, primitive or tag values (a rule matches if it has any of them), with policy_id for rules attached to one policy, or with q for a case-insensitive name substring.
curl "https://acme.app.elastio.com/gateway/shared-services/v3/hunt-rules?severity=high,critical&status=active" \ -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/v3/hunt-rules", headers=HEADERS, params={"severity": "high,critical", "status": "active"}, timeout=30,)resp.raise_for_status()for rule in resp.json()["hunt_rules"]: print(rule["id"], rule["name"], rule["severity"], rule["version"], [p["name"] for p in rule["attached_policies"]])$Base = "https://acme.app.elastio.com"$Headers = @{ Authorization = "Bearer $env:ELASTIO_TOKEN" }$Rules = Invoke-RestMethod -Uri "$Base/gateway/shared-services/v3/hunt-rules?severity=high,critical&status=active" -Headers $Headers$Rules.hunt_rules | Select-Object id, name, severity, status, version{ "total_count": 1, "hunt_rules": [ { "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "name": "webshell_dropper", "description": "Flags PHP web shells dropped into upload directories", "author": "secops@acme.example", "severity": "high", "status": "active", "source": "CUSTOM", "primitives": ["content"], "bodies": { "content": "rule webshell_dropper { strings: $a = \"eval(base64_decode(\" condition: $a }" }, "prompt": "", "tags": ["webshell"], "mitre_mapping": ["T1505.003"], "attached_policies": [{ "id": "9c8b7a6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d", "name": "prod-daily" }], "yara_refs": [], "signature_count": 1, "last_match_at": null, "matches_30d": 0, "version": 2, "version_count": 2, "cloned_from_rule_id": null, "tenant_name": "acme", "created_at": "2026-09-30T14:02:11Z", "updated_at": "2026-10-08T10:45:00Z" } ]}GET /gateway/shared-services/v3/hunt-rules/{id} returns the same shape plus signatures, the parsed per-signature list with its enabled flags.
Create a hunt rule
Section titled “Create a hunt rule”name and severity are required. This example creates a YARA (content) rule:
curl -X POST "https://acme.app.elastio.com/gateway/shared-services/v3/hunt-rules" \ -H "Authorization: Bearer $ELASTIO_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "webshell_dropper", "description": "Flags PHP web shells dropped into upload directories", "severity": "high", "primitives": ["content"], "bodies": { "content": "rule webshell_dropper { strings: $a = \"eval(base64_decode(\" condition: $a }" }, "tags": ["webshell"], "mitre_mapping": ["T1505.003"] }'body = { "name": "webshell_dropper", "description": "Flags PHP web shells dropped into upload directories", "severity": "high", "primitives": ["content"], "bodies": {"content": 'rule webshell_dropper { strings: $a = "eval(base64_decode(" condition: $a }'}, "tags": ["webshell"], "mitre_mapping": ["T1505.003"],}resp = requests.post(f"{BASE}/gateway/shared-services/v3/hunt-rules", headers=HEADERS, json=body, timeout=30)resp.raise_for_status()rule = resp.json()print(rule["id"], rule["version"], rule["signature_count"])$Body = @{ name = "webshell_dropper" description = "Flags PHP web shells dropped into upload directories" severity = "high" primitives = @("content") bodies = @{ content = 'rule webshell_dropper { strings: $a = "eval(base64_decode(" condition: $a }' } tags = @("webshell") mitre_mapping = @("T1505.003")} | ConvertTo-Json -Depth 5$Rule = Invoke-RestMethod -Method Post -Uri "$Base/gateway/shared-services/v3/hunt-rules" ` -Headers $Headers -ContentType "application/json" -Body $Body$Rule.idThe response is 201 with the full rule, in the same shape as a list item, with version: 1.
To keep reusable YARA in a shared library, import it with POST /gateway/shared-services/v3/yara-rules (body is the raw YARA text; name, severity, status, tags and source are optional) or POST /gateway/shared-services/v3/yara-rulesets. Each import gets an immutable path such as yara/<source>/<slug>.yar, which you then reference from a rule’s yara_refs and structural SQL. An import with no rule X {} block is rejected with 400 NO_RULES_FOUND.
Update, version and revert
Section titled “Update, version and revert”PATCH keeps any field you leave out and replaces any field you send, including empty [] or {}. The merged rule is validated again. You can’t send name; to use a different name, create a new rule. Add a change_summary to label the new version.
curl -X PATCH "https://acme.app.elastio.com/gateway/shared-services/v3/hunt-rules/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" \ -H "Authorization: Bearer $ELASTIO_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "severity": "critical", "change_summary": "Raise severity after IR review" }'rule_id = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"resp = requests.patch( f"{BASE}/gateway/shared-services/v3/hunt-rules/{rule_id}", headers=HEADERS, json={"severity": "critical", "change_summary": "Raise severity after IR review"}, timeout=30,)resp.raise_for_status()print(resp.json()["severity"], resp.json()["version"])$RuleId = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"$Body = @{ severity = "critical"; change_summary = "Raise severity after IR review" } | ConvertTo-JsonInvoke-RestMethod -Method Patch -Uri "$Base/gateway/shared-services/v3/hunt-rules/$RuleId" ` -Headers $Headers -ContentType "application/json" -Body $BodyReview history with GET …/hunt-rules/{id}/versions, which lists snapshots newest first, each with version, change_summary, created_by and the full snapshot. GET …/hunt-rules/{id}/audit-log lists the audit entries. To roll back, POST …/hunt-rules/{id}/revert with { "to_version": 1 }. This creates a new version that copies the old content, so the history stays linear.
{ "total_count": 2, "versions": [ { "version": 2, "change_summary": "Raise severity after IR review", "created_by": "…", "created_at": "2026-10-08T10:45:00Z", "snapshot": { "…": "…" } }, { "version": 1, "change_summary": "", "created_by": "…", "created_at": "2026-09-30T14:02:11Z", "snapshot": { "…": "…" } } ]}To silence one noisy signature without changing the rule, POST …/hunt-rules/{id}/signatures/{signatureId}/toggle with { "enabled": false }. The flag survives later content changes.
Attach a rule to a policy
Section titled “Attach a rule to a policy”Attaching binds the rule to a policy. Only active rules can be attached; tuning or inactive rules are rejected with 400 HUNT_RULE_NOT_ACTIVE. policy_name is optional and only stored as a display snapshot.
curl -X POST "https://acme.app.elastio.com/gateway/shared-services/v3/hunt-rules/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/policies" \ -H "Authorization: Bearer $ELASTIO_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "policy_id": "9c8b7a6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d", "policy_name": "prod-daily" }'resp = requests.post( f"{BASE}/gateway/shared-services/v3/hunt-rules/{rule_id}/policies", headers=HEADERS, json={"policy_id": "9c8b7a6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d", "policy_name": "prod-daily"}, timeout=30,)resp.raise_for_status()print(resp.json()["attached_policies"])$Body = @{ policy_id = "9c8b7a6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d"; policy_name = "prod-daily" } | ConvertTo-Json(Invoke-RestMethod -Method Post -Uri "$Base/gateway/shared-services/v3/hunt-rules/$RuleId/policies" ` -Headers $Headers -ContentType "application/json" -Body $Body).attached_policiesThe response is 201 with the rule and its updated attached_policies. To detach, call DELETE …/hunt-rules/{id}/policies/{policyId}. To delete a rule, call DELETE …/hunt-rules/{id}, or POST /gateway/shared-services/v3/hunt-rules:batch-delete for several at once. Deletion is a soft delete that frees the name for reuse, and it’s blocked with 409 DELETE_BLOCKED while the rule is attached to a policy. A batch delete returns 200 when nothing was blocked, 207 for a mixed outcome, and 409 when every ID was blocked.
You can also run hunt rules on demand: the scan_type of POST /gateway/api/v1/scans/schedule accepts hunt_rules for AWS targets. See Run a hunt.
Read custom hunt results
Section titled “Read custom hunt results”Each custom hunt run against an asset is returned in hunt_scans. For EC2 instances and Azure VMs, the list includes hunts on attached volumes and disks. Filter by start_date and end_date (default: the last six months) and page with page_size (default 50, max 500) and page_token.
curl "https://acme.app.elastio.com/gateway/api/v1/assets/6f1c2a9e-3b4d-4e8f-9a7b-1c2d3e4f5a6b/custom-hunts?page_size=20" \ -H "Authorization: Bearer $ELASTIO_TOKEN"asset_id = "6f1c2a9e-3b4d-4e8f-9a7b-1c2d3e4f5a6b"resp = requests.get(f"{BASE}/gateway/api/v1/assets/{asset_id}/custom-hunts", headers=HEADERS, params={"page_size": 20}, timeout=30)resp.raise_for_status()for hs in resp.json()["hunt_scans"]: print(hs["id"], hs["job_id"], hs["created_at"])$AssetId = "6f1c2a9e-3b4d-4e8f-9a7b-1c2d3e4f5a6b"(Invoke-RestMethod -Uri "$Base/gateway/api/v1/assets/$AssetId/custom-hunts?page_size=20" -Headers $Headers).hunt_scans | Select-Object id, job_id, created_at{ "hunt_scans": [ { "id": "d4c3b2a1-f6e5-4b7a-9d8c-5e4f3a2b1c0d", "tenant_name": "acme", "cc_id": "…", "job_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d", "target": { "…": "…" }, "target_asset": { "id": "0e1f2a3b-4c5d-4e6f-8a7b-9c0d1e2f3a4b", "kind": "ebs_volume", "name": "vol-0123456789abcdef0" }, "report_location": { "bucket": "…", "key": "…" }, "created_at": "2026-10-11T21:40:02Z", "updated_at": "2026-10-11T21:58:12Z" } ], "pagination": { "next_page_token": "", "prev_page_token": "", "total_count": 1 }}GET /gateway/api/v1/custom-hunts/{hunt_scan_id} returns one custom hunt with its queries, each with the query text and raw_json results. For matches as findings, use GET /gateway/api/v2/scanjobs/{scanJobId}/threats/custom/findings (per job) or GET /gateway/api/v2/threats?threat_kind=custom (across the estate). See Review Hunt findings.
All endpoints
Section titled “All endpoints”Hunt Rules
30 operations in the reference →- GET
/gateway/shared-services/v3/hunt-rulesList hunt rules - POST
/gateway/shared-services/v3/hunt-rulesCreate a hunt rule - GET
/gateway/shared-services/v3/hunt-rules/{id}Get a hunt rule - PATCH
/gateway/shared-services/v3/hunt-rules/{id}Update a hunt rule - DELETE
/gateway/shared-services/v3/hunt-rules/{id}Soft-delete a hunt rule - GET
/gateway/shared-services/v3/hunt-rules/{id}/audit-logList a hunt rule's audit entries - POST
/gateway/shared-services/v3/hunt-rules/{id}/policiesAttach a hunt rule to a policy - DELETE
/gateway/shared-services/v3/hunt-rules/{id}/policies/{policyId}Detach a hunt rule from a policy - POST
/gateway/shared-services/v3/hunt-rules/{id}/revertRevert a hunt rule to an earlier version - POST
/gateway/shared-services/v3/hunt-rules/{id}/signatures/{signatureId}/toggleEnable / disable a single signature - GET
/gateway/shared-services/v3/hunt-rules/{id}/versionsList a hunt rule's version snapshots - POST
/gateway/shared-services/v3/hunt-rules:batch-deleteBatch soft-delete hunt rules - GET
/gateway/shared-services/v3/yara-rulesList YARA rules - POST
/gateway/shared-services/v3/yara-rulesImport a YARA rule (pasted body) - GET
/gateway/shared-services/v3/yara-rules/{id}Get a YARA rule - PATCH
/gateway/shared-services/v3/yara-rules/{id}Update a YARA rule - DELETE
/gateway/shared-services/v3/yara-rules/{id}Soft-delete a YARA rule - GET
/gateway/shared-services/v3/yara-rules/{id}/audit-logList a YARA rule's audit entries - POST
/gateway/shared-services/v3/yara-rules/{id}/revertRevert a YARA rule to an earlier version - POST
/gateway/shared-services/v3/yara-rules/{id}/signatures/{signatureId}/toggleEnable / disable a single YARA signature - GET
/gateway/shared-services/v3/yara-rules/{id}/versionsList a YARA rule's version snapshots - GET
/gateway/shared-services/v3/yara-rulesetsList YARA rulesets - POST
/gateway/shared-services/v3/yara-rulesetsImport a YARA ruleset - GET
/gateway/shared-services/v3/yara-rulesets/{id}Get a YARA ruleset - PATCH
/gateway/shared-services/v3/yara-rulesets/{id}Update a YARA ruleset - DELETE
/gateway/shared-services/v3/yara-rulesets/{id}Soft-delete a YARA ruleset - GET
/gateway/shared-services/v3/yara-rulesets/{id}/audit-logGet a YARA ruleset's audit log - POST
/gateway/shared-services/v3/yara-rulesets/{id}/revertRevert a YARA ruleset to a prior version - POST
/gateway/shared-services/v3/yara-rulesets/{id}/signatures/{signatureID}/toggleEnable / disable a single ruleset signature - GET
/gateway/shared-services/v3/yara-rulesets/{id}/versionsList a YARA ruleset's version snapshots