Custom rules
EQL rule reference
The full specification for writing custom EQL detections against the process, file, and network telemetry Bintracer records while a sample detonates in the sandbox.
How rules are evaluated
After a detonation, the sandbox's raw telemetry is transformed into Elastic Common Schema (ECS) documents and indexed into per-scan event indices. Your EQL rules are then evaluated with Elasticsearch EQL search alongside Bintracer's built-in rules.
- Every rule search is automatically scoped to the scan under review. Rules never need to filter by scan, account, sample hash, or time range.
- Only sample-related telemetry is indexed: events attributable to the sample's process tree, its file and network targets, or files it created.
bintracer.relevance.reasonrecords why each document was included (sample_exec,sample_actor,descendant_actor,target,network,xpc, ormalware). - Matches appear on the scan report with the rule's name and MITRE ATT&CK techniques, visible only to your account.
- A rule the scan pipeline cannot load is skipped; it never fails your scan. The EQL query itself is syntax-checked by Elasticsearch at scan time, so a schema-valid rule can still be skipped there if the query is malformed. Every skipped rule is listed with its reason in the scan report's Rule diagnostics section, visible only to your account.
- A rule reports at most one hit per scan: evaluation records match/no-match, so a rule that matches many events still appears once. A scan lists at most 250 matched rules across built-in and custom rules; when the cap is reached the report flags the detection list as truncated and the remaining rules are not evaluated.
Rule file format
An EQL rule is a TOML file in the Elastic detection-rule shape: a [rule] table with the entries below.
| Entry | Required | Notes |
|---|---|---|
name | No | Display name shown with matches on the scan report. If omitted, it is derived from the first line of description, then the rule's file name, then its id. |
query | Yes | The EQL query, up to 64 KiB. Query syntax must be printable ASCII; non-ASCII characters are allowed inside string literals and comments so rules can match unicode artifacts. Control characters and lone surrogates are rejected anywhere. |
type / language | No | A declared rule must set type = "eql"; language defaults to type and, if present, must also be "eql". Only a bare behavior-format rule (no type, language, or index entries) may omit type — that shape is treated as EQL automatically. |
index | No | List of up to 32 index patterns from the supported set below. When omitted, all three event indices are searched; a bare behavior-format rule instead derives its indices from the event categories the query constrains. Unsupported patterns from other data sources (for example logs-jamf_protect*) are dropped with a warning as long as at least one supported pattern remains. |
rule_id | No | A stable identifier of your choosing (printable ASCII). Falls back to the rule's Bintracer id when omitted. |
[[rule.threat]] | No | MITRE ATT&CK mapping in the Elastic detection-rule shape (framework, tactic, technique, subtechnique). Technique ids must match T#### or T####.###; up to 20 techniques are kept per rule. |
- Upload EQL rules as plaintext
.tomlfiles up to 1 MiB. Each account can store up to 10,000 custom rules. - Unused upstream entries (the
notetriage guide, setup blocks, actions, and other metadata) are ignored, and unicode punctuation in display-only fields such asnameanddescriptionis normalized rather than rejected. - Some constructs are rejected at load time with an explanation:
eventN.fieldcross-event references anddescendant of(Elasticsearch EQL cannot evaluate them), the Endpoint-onlypersistenceevent category (Bintracer emits persistence asfileevents — match onevent.actioninstead), and non-EQL rule types (new_terms,esql,threshold,query).
Indices
Rules may target three event index patterns. Every event document lands in exactly one of them, keyed by its ECS event category.
| Index pattern | Contents |
|---|---|
logs-bintracer.events.process-* | process, api, library, authentication, and session category events |
logs-bintracer.events.file-* | file, configuration, and malware category events, including persistence and captured-artifact records |
logs-bintracer.events.network-* | network category events: flows, HTTP, TLS, and DNS |
- The Elastic Endpoint spellings
logs-endpoint.events.*(including theprocess,file,network,library,api,authentication, andsessionforms) are accepted as aliases over the same indices, so upstream Elastic rules keep theirindexarrays verbatim. event.datasetanddata_stream.datasetname the physical index (bintracer.events.process|file|network):api,library,authentication, andsessiondocuments carrybintracer.events.process, andconfigurationandmalwaredocuments carrybintracer.events.file. Filter onevent.categoryfor semantics.- The per-scan run summary lives in a separate index that rules cannot query.
Fields on every event
Every event document carries this envelope in addition to its per-event fields.
| Field | Notes |
|---|---|
@timestamp | Kernel/occurrence time of the event. |
event.created | When the producing sensor observed and enqueued the event; can trail @timestamp. |
event.category | Array with one of process, file, network, api, library, configuration, authentication, session, malware. |
event.type | ECS type array — see the event catalog. |
event.action | Bintracer-specific action — see the event catalog. |
event.outcome | success unless the event represents a failure (DNS errors, artifact-capture skips, directory- services mutations with a non-zero native error, failed XProtect remediations, failed logins); absent on dlopen_attempt. |
event.kind / event.module | event / endpoint. |
session.id | Sandbox detonation session id. |
host.id, host.name, host.hostname, host.ip | Sandbox VM identity. |
host.os.* | family/type/platform are macos, name is macOS; version, full, and build reflect the guest macOS build when measured. |
bintracer.host_sequence | Monotonic per-run event sequence number (long). |
bintracer.extension_id | Producing sensor: ESMonitor, NetMonitor, DyldMonitor, DyldInsertMonitor, … |
bintracer.relevance.reason | Why the document was included in the sample-scoped index (see above). |
Process context
The acting process (for exec documents the exec target, for network documents the flow's source process) is rendered under process.* on every event document.
| Field | Notes |
|---|---|
process.entity_id | The EQL sequence join key: a deterministic id that names one process for its whole lifetime. An exec target inherits the pre-exec image's id, so sequences joining by process.entity_id match across exec — the same semantics as Elastic Endpoint's entity id. |
process.pid, process.name, process.executable | Basic identity of the acting process. |
process.args, process.args_count, process.command_line | command_line supports fast like~/regex~ matching. args_count is the length of the shipped array; argv/env are capped at 256 entries, and exec documents flag clipping via bintracer.process.args_truncated/env_truncated with the full kernel counts in bintracer.process.{args_count_total,env_count_total}. |
process.env_vars | Only on process/exec documents. |
process.start, process.working_directory | Start time and working directory when known. |
process.code_signature.* | exists, trusted, status, signing_id, team_id, subject_name. status is the stable Security-framework OSStatus name (for example errSecCSUnsigned) with a decimal fallback for unrecognized codes; when no full verdict was captured, trusted falls back to kernel flags (valid and not ad-hoc) and status is absent. |
process.hash.* | Best-effort md5/sha1/sha256 of the executable. |
process.parent.* | Same shape as process.* (no env_vars). |
process.Ext.effective_parent.* | Responsible process (falls back to the parent): entity_id, pid, name, executable, code_signature.*. |
process.Ext.ancestry | Ordered list of ancestor entity ids. |
user.id, user.name, user.effective.* | Acting user when uid/euid evidence exists. |
Event catalog: process index
Events in logs-bintracer.events.process-*, grouped by event.category.
process
| event.type | event.action | Notes |
|---|---|---|
start | exec | Full argv, process.env_vars, process.working_directory. Interpreter execs carry the script path as bintracer.process.script; the image architecture ships as bintracer.process.image_arch (x86_64 on an ARM64 guest means Rosetta-translated execution). |
start | fork | Child is process.*, forking actor is process.parent.*. The child inherits the parent's argv into process.args but carries no process.command_line, so rules can pair fork/exec legs on that. |
end | end | process.exit_code; bintracer.process.{raw_wait_status,exited,signaled,signal}. |
change | cs_invalidated | Code signature invalidated at runtime. |
info | pty_grant | Pseudoterminal granted (interactive/reverse-shell evidence); bintracer.pty.{dev,major,minor}. |
start | remote_thread | Thread created in another process; bintracer.interprocess.target_process.*. Matches the Elastic Endpoint remote-thread event shape. |
access | process_open | task_for_pid access to another process; bintracer.interprocess.{target_process.*,get_task_type}. |
change | setuid / setgid / seteuid / setegid / setreuid / setregid | Credential changes; the affected ids land under user.*/group.*. Unchanged -1 arguments to setreuid/setregid serialize as 4294967295. |
api (cross-process / IPC)
| event.type | event.action | Notes |
|---|---|---|
access | trace, get_task, get_task_read, get_task_inspect, get_task_name | bintracer.interprocess.{target_process.*,get_task_type}. The task_for_pid flavor ships as process/process_open instead. |
change | proc_suspend_resume | bintracer.interprocess.{target_process.*,operation}. |
info | signal | bintracer.interprocess.{target_process.*,signal} (number). |
start | xpc_connect | bintracer.xpc.{service_name,service_domain_type}. |
start | uipc_connect | Unix-socket connect; file.* carries the socket path plus bintracer.uipc.{domain,socket_type,protocol}. |
start | uipc_bind | Unix-socket bind; file.* carries the socket's directory plus bintracer.uipc.{filename,mode}. |
library
| event.type | event.action | Notes |
|---|---|---|
start | load | dll.{path,name,extension,directory}, dll.hash.*, dll.code_signature.*; bintracer.library.in_shared_cache is false when the image loaded from outside the dyld shared cache. |
info | dyld_image_existing | Images already loaded at attach time (same fields as load). |
end | unload | Same fields as load. |
info | dlopen_attempt | Entry evidence only: bintracer.library.dlopen.* (requested path, mode, caller forensics) plus the calling thread as process.thread.id; no dll.* and no event.outcome. |
authentication
| event.type | event.action | Notes |
|---|---|---|
start | sudo / su | Initiator under user.{name,id}, target under user.effective.*; bintracer.authentication.* carries the command/shell/args and failure message. Outcome reflects the native success flag. |
start / end | login_login / login_logout | user.{name,id}; login failures carry bintracer.authentication.failure_message. |
start / end | openssh_login / openssh_logout | user.{name,id}, source.address (+ source.ip when it parses), bintracer.authentication.result_type. |
session
| event.type | event.action | Notes |
|---|---|---|
start / end / info | lw_session_login / lw_session_logout / lw_session_lock / lw_session_unlock | LoginWindow session activity; user.name, bintracer.session.graphical_session_id. |
start / end | screensharing_attach / screensharing_detach | user.name, source.address/source.ip, and bintracer.session.{viewer_appleid,authentication_type,authentication_username,session_username,graphical_session_id} (existing_session on attach only); outcome from the native success flag. |
Event catalog: file index
Events in logs-bintracer.events.file-*. File documents carry file.{path,name,extension,directory} plus, when known, file.size, file.uid/file.gid, file.mode (octal string), file.attributes (decoded flags such as hidden), file.mtime/file.created (compare for timestomp evidence), file.Ext.header_bytes (first bytes, lowercase hex), and file.hash.*.
file
| event.type | event.action | Notes |
|---|---|---|
creation | creation / clone | File create, link, copy, or clone; file.Ext.source.path names the source for links/copies/clones. |
change | modification | File write. |
change | rename | file.Ext.original.path is the pre-rename path. |
deletion | deletion | File unlink. |
access | open | bintracer.file.open_access decodes the open to read/write; bintracer.file.open_flags keeps the numeric kernel flag mask. |
change | extended_attributes_set / extended_attributes_delete | bintracer.file.extended_attribute_name. For com.apple.quarantine, com.apple.metadata:kMDItemWhereFroms, and com.apple.provenance, set events also carry the written value (read back from disk, capped at 4 KiB): bintracer.file.extended_attribute_value (text values), …value_base64/…value_size/…value_truncated/…value_error. Quarantine strings decode to bintracer.file.quarantine.{flags,timestamp,agent_name,event_uuid}; WhereFroms plists decode to bintracer.file.where_froms (download URL, then referrer). |
change | setmode / setowner / setflags | file.mode, file.uid/file.gid, or file.attributes report the requested new value. |
change | utimes | file.mtime/file.accessed report the requested (possibly timestomped) times. |
change | truncate / exchangedata | For exchangedata, file.Ext.source.path is the counterpart file. |
info | mount / unmount | file.* is the mount point; bintracer.mount.* carries mount point, source, filesystem type, flags, owner, and disposition (unmount events carry no disposition). |
change | remount | Same fields as mount. |
creation | launch_daemon | Background Task Management persistence registration; bintracer.persistence.{name,path,args,run_at_load,keep_alive}. |
deletion | btm_launch_item_remove | Persistence unregistration (cleanup / anti-forensics); bintracer.persistence.* is best-effort. |
info | artifact_capture | Bintracer rescued a copy of a sample-written file; file.hash.*, bintracer.artifact_source_event. Typed info so creation/modification rules do not fire on the rescued copy. |
info | artifact_capture_skipped / artifact_budget_exceeded | Capture skipped or capture budget hit; outcome failure with bintracer.artifact_capture_reason and, for budget events, bintracer.artifact_capture_budget.*. |
configuration
| event.type | event.action | Notes |
|---|---|---|
change | gatekeeper_override | file.*, file.hash.*, bintracer.gatekeeper.disposition. |
change | tcc_modify | TCC privacy-permission change; bintracer.tcc.{right,service,update_type} plus the requesting client under bintracer.tcc.client.{identity,identity_type,reason} (for example identity_type: "bundle_id", reason: "user_consent"). |
creation / change | profile_add | Configuration-profile install; creation for a first install, change when the native is_update flag is set (shipped as bintracer.profile.is_update). bintracer.profile.* carries identifier, uuid, install source, organization, display name, and scope. |
deletion | profile_remove | Configuration-profile removal; same bintracer.profile.* fields (no is_update). |
creation | od_create_user | Local directory-services account creation; the created account under user.name plus bintracer.od.*. A non-zero native error maps to outcome failure with error.code. The mutation never took effect. |
change | od_modify_password / od_enable_user / od_disable_user | Local directory-services account mutations; the affected account under user.name plus bintracer.od.*. Same native-error mapping to outcome failure with error.code. |
change | od_group_add | Local group-membership addition; group.name plus the added member under user.{name,id}. Same native-error mapping to outcome failure with error.code. |
malware
| event.type | event.action | Notes |
|---|---|---|
info | xp_malware_detected / xp_malware_remediated | XProtect verdicts: file.* is the detected/remediated path and bintracer.xprotect.* carries the signature version, malware identifier, and incident identifier that links a detection to its remediations; remediation events add bintracer.xprotect.{action_type,result_description,remediated_process.*}. process.* is the reporting Apple process, not the sample. A failed remediation maps to outcome failure. |
Event catalog: network index
Events in logs-bintracer.events.network-*. All network documents carry the source-process process.* block, and related.ip collects every IP on the document.
| event.type | event.action | Notes |
|---|---|---|
start | connection_attempted | Flow open: destination.{ip,port,domain}, source.{ip,port}, network.{direction,transport,type,iana_number}, url.* when the flow exposes a URL. |
protocol | http_request / http_response | network.protocol: "http", http.request.{method,body.bytes,body.content}, http.response.status_code, url.*, user_agent.original (action falls back to http if neither side parsed). |
protocol | tls_client_hello | Outbound TLS: network.protocol: "tls", tls.client.server_name (parsed SNI), destination.domain from the SNI. The server name also backfills destination.domain on the flow's other documents when the flow metadata carried no hostname. |
end | flow_summary | Flow close: destination.{address,bytes}, source.bytes, plus the flow fields above. |
start | dns_flow | DNS flow open: network.protocol: "dns", destination.*, bintracer.dns.*. |
protocol | lookup_requested / lookup_result | DNS query/response packets: dns.id, dns.question.{name,type}, plus destination.*, network.*, and bintracer.dns.*. Responses add dns.answers[], dns.resolved_ip (folded into related.ip), and dns.response_code (NOERROR, NXDOMAIN, SERVFAIL, …). |
error | dns_error | Resolver error: outcome failure, error.message, bintracer.dns.operation; carries no IP evidence. |
dns.answersitems carryname,type,type_code,class_code,ttl, anddata, pluspreference/exchangefor MX,stringsfor TXT, andrdata_base64for TXT and undecoded types.bintracer.dns.*context includesflow_id,upstream_resolver,source_app_signing_identifier,packet_length,tcp_framed, andparse_error.- DNS documents carry
event.category: network, so writenetwork where …(or an unqualifiedany where …) — adns where …category clause never matches.
Field types and search behavior
Fields are mapped for the query patterns rule authors need most.
- String fields are
keywordby default — exact match,in, and case-insensitivelike~patterns all work. - High-cardinality text is
wildcard-mapped with a full-text subfield for fastlike~/regex~and no length cap:process.command_line(also the parent's),url.full,url.path,http.request.body.content,user_agent.original,bintracer.library.dlopen.requested_path(also.requested_path_base64),bintracer.file.extended_attribute_value, andbintracer.file.where_froms. destination.ip,source.ip,dns.resolved_ip,host.ip, andrelated.ipareip-typed, so CIDR queries such asdestination.ip == "10.0.0.0/8"work.- Booleans include the code-signature
exists/trustedflags,bintracer.persistence.{run_at_load,keep_alive}, and the truncation flags underbintracer.process.*andbintracer.file.*. - Dates:
@timestamp,event.created,event.ingested,process.start,file.accessed,file.created,file.mtime,bintracer.file.quarantine.timestamp.
Example: sequence rule
Sequences join events with sequence by, typically on process.entity_id or the parent's. This rule flags a process that launches curl and then executes a file from a download location within 30 seconds, joined on the shared parent.
[rule]
name = "Download and Execute via curl"
type = "eql"
language = "eql"
index = ["logs-bintracer.events.process-*"]
query = '''
sequence by process.parent.entity_id with maxspan=30s
[process where event.action == "exec" and process.name == "curl"]
[process where event.action == "exec" and
process.executable like~ ("/tmp/*", "/private/tmp/*", "/Users/*/Downloads/*")]
'''