Skip to content
Try the API

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.

  • 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.
  • A hunt rule has a name (letters, digits, - and _ only), a severity (low, medium, high, critical), a status (active, tuning, inactive; default active) and a source (CUSTOM, ELASTIO, IMPORTED; default CUSTOM).
  • Rule code lives in bodies, keyed by primitive: structural holds SQL, content holds YARA, and pattern holds a regex. List the primitives the rule uses in primitives.
  • Structural SQL can call library YARA with yara_match('yara/...'). Each library path used must be pinned in yara_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.

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.

GET
/gateway/shared-services/v3/hunt-rulesList hunt rules
Try it
Terminal window
curl "https://acme.app.elastio.com/gateway/shared-services/v3/hunt-rules?severity=high,critical&status=active" \
-H "Authorization: Bearer $ELASTIO_TOKEN"
{
"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.

name and severity are required. This example creates a YARA (content) rule:

POST
/gateway/shared-services/v3/hunt-rulesCreate a hunt rule
Try it
Terminal window
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"]
}'

The 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.

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.

PATCH
/gateway/shared-services/v3/hunt-rules/{id}Update a hunt rule
Try it
Terminal window
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" }'

Review 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.

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.

POST
/gateway/shared-services/v3/hunt-rules/{id}/policiesAttach a hunt rule to a policy
Try it
Terminal window
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" }'

The 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.

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.

GET
/gateway/api/v1/assets/{asset_id}/custom-huntsList custom hunt runs for an asset
Try it
Terminal window
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"
{
"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.

Hunt Rules

30 operations in the reference →

Custom Hunts

2 operations in the reference →