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.reason records why each document was included (sample_exec, sample_actor, descendant_actor, target, network, xpc, or malware).
  • 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.

EntryRequiredNotes
nameNoDisplay 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.
queryYesThe 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 / languageNoA 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.
indexNoList 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_idNoA stable identifier of your choosing (printable ASCII). Falls back to the rule's Bintracer id when omitted.
[[rule.threat]]NoMITRE 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 .toml files up to 1 MiB. Each account can store up to 10,000 custom rules.
  • Unused upstream entries (the note triage guide, setup blocks, actions, and other metadata) are ignored, and unicode punctuation in display-only fields such as name and description is normalized rather than rejected.
  • Some constructs are rejected at load time with an explanation: eventN.field cross-event references and descendant of (Elasticsearch EQL cannot evaluate them), the Endpoint-only persistence event category (Bintracer emits persistence as file events — match on event.action instead), 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 patternContents
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 the process, file, network, library, api, authentication, and session forms) are accepted as aliases over the same indices, so upstream Elastic rules keep their index arrays verbatim.
  • event.dataset and data_stream.dataset name the physical index (bintracer.events.process|file|network): api, library, authentication, and session documents carry bintracer.events.process, and configuration and malware documents carry bintracer.events.file. Filter on event.category for 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.

FieldNotes
@timestampKernel/occurrence time of the event.
event.createdWhen the producing sensor observed and enqueued the event; can trail @timestamp.
event.categoryArray with one of process, file, network, api, library, configuration, authentication, session, malware.
event.typeECS type array — see the event catalog.
event.actionBintracer-specific action — see the event catalog.
event.outcomesuccess 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.moduleevent / endpoint.
session.idSandbox detonation session id.
host.id, host.name, host.hostname, host.ipSandbox 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_sequenceMonotonic per-run event sequence number (long).
bintracer.extension_idProducing sensor: ESMonitor, NetMonitor, DyldMonitor, DyldInsertMonitor, …
bintracer.relevance.reasonWhy 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.

FieldNotes
process.entity_idThe 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.executableBasic identity of the acting process.
process.args, process.args_count, process.command_linecommand_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_varsOnly on process/exec documents.
process.start, process.working_directoryStart 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.ancestryOrdered 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.typeevent.actionNotes
startexecFull 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).
startforkChild 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.
endendprocess.exit_code; bintracer.process.{raw_wait_status,exited,signaled,signal}.
changecs_invalidatedCode signature invalidated at runtime.
infopty_grantPseudoterminal granted (interactive/reverse-shell evidence); bintracer.pty.{dev,major,minor}.
startremote_threadThread created in another process; bintracer.interprocess.target_process.*. Matches the Elastic Endpoint remote-thread event shape.
accessprocess_opentask_for_pid access to another process; bintracer.interprocess.{target_process.*,get_task_type}.
changesetuid / setgid / seteuid / setegid / setreuid / setregidCredential changes; the affected ids land under user.*/group.*. Unchanged -1 arguments to setreuid/setregid serialize as 4294967295.

api (cross-process / IPC)

event.typeevent.actionNotes
accesstrace, get_task, get_task_read, get_task_inspect, get_task_namebintracer.interprocess.{target_process.*,get_task_type}. The task_for_pid flavor ships as process/process_open instead.
changeproc_suspend_resumebintracer.interprocess.{target_process.*,operation}.
infosignalbintracer.interprocess.{target_process.*,signal} (number).
startxpc_connectbintracer.xpc.{service_name,service_domain_type}.
startuipc_connectUnix-socket connect; file.* carries the socket path plus bintracer.uipc.{domain,socket_type,protocol}.
startuipc_bindUnix-socket bind; file.* carries the socket's directory plus bintracer.uipc.{filename,mode}.

library

event.typeevent.actionNotes
startloaddll.{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.
infodyld_image_existingImages already loaded at attach time (same fields as load).
endunloadSame fields as load.
infodlopen_attemptEntry 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.typeevent.actionNotes
startsudo / suInitiator 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 / endlogin_login / login_logoutuser.{name,id}; login failures carry bintracer.authentication.failure_message.
start / endopenssh_login / openssh_logoutuser.{name,id}, source.address (+ source.ip when it parses), bintracer.authentication.result_type.

session

event.typeevent.actionNotes
start / end / infolw_session_login / lw_session_logout / lw_session_lock / lw_session_unlockLoginWindow session activity; user.name, bintracer.session.graphical_session_id.
start / endscreensharing_attach / screensharing_detachuser.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.typeevent.actionNotes
creationcreation / cloneFile create, link, copy, or clone; file.Ext.source.path names the source for links/copies/clones.
changemodificationFile write.
changerenamefile.Ext.original.path is the pre-rename path.
deletiondeletionFile unlink.
accessopenbintracer.file.open_access decodes the open to read/write; bintracer.file.open_flags keeps the numeric kernel flag mask.
changeextended_attributes_set / extended_attributes_deletebintracer.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).
changesetmode / setowner / setflagsfile.mode, file.uid/file.gid, or file.attributes report the requested new value.
changeutimesfile.mtime/file.accessed report the requested (possibly timestomped) times.
changetruncate / exchangedataFor exchangedata, file.Ext.source.path is the counterpart file.
infomount / unmountfile.* is the mount point; bintracer.mount.* carries mount point, source, filesystem type, flags, owner, and disposition (unmount events carry no disposition).
changeremountSame fields as mount.
creationlaunch_daemonBackground Task Management persistence registration; bintracer.persistence.{name,path,args,run_at_load,keep_alive}.
deletionbtm_launch_item_removePersistence unregistration (cleanup / anti-forensics); bintracer.persistence.* is best-effort.
infoartifact_captureBintracer 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.
infoartifact_capture_skipped / artifact_budget_exceededCapture skipped or capture budget hit; outcome failure with bintracer.artifact_capture_reason and, for budget events, bintracer.artifact_capture_budget.*.

configuration

event.typeevent.actionNotes
changegatekeeper_overridefile.*, file.hash.*, bintracer.gatekeeper.disposition.
changetcc_modifyTCC 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 / changeprofile_addConfiguration-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.
deletionprofile_removeConfiguration-profile removal; same bintracer.profile.* fields (no is_update).
creationod_create_userLocal 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.
changeod_modify_password / od_enable_user / od_disable_userLocal directory-services account mutations; the affected account under user.name plus bintracer.od.*. Same native-error mapping to outcome failure with error.code.
changeod_group_addLocal 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.typeevent.actionNotes
infoxp_malware_detected / xp_malware_remediatedXProtect 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.typeevent.actionNotes
startconnection_attemptedFlow open: destination.{ip,port,domain}, source.{ip,port}, network.{direction,transport,type,iana_number}, url.* when the flow exposes a URL.
protocolhttp_request / http_responsenetwork.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).
protocoltls_client_helloOutbound 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.
endflow_summaryFlow close: destination.{address,bytes}, source.bytes, plus the flow fields above.
startdns_flowDNS flow open: network.protocol: "dns", destination.*, bintracer.dns.*.
protocollookup_requested / lookup_resultDNS 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, …).
errordns_errorResolver error: outcome failure, error.message, bintracer.dns.operation; carries no IP evidence.
  • dns.answers items carry name, type, type_code, class_code, ttl, and data, plus preference/exchange for MX, strings for TXT, and rdata_base64 for TXT and undecoded types.
  • bintracer.dns.* context includes flow_id, upstream_resolver, source_app_signing_identifier, packet_length, tcp_framed, and parse_error.
  • DNS documents carry event.category: network, so write network where … (or an unqualified any where …) — a dns where … category clause never matches.

Field types and search behavior

Fields are mapped for the query patterns rule authors need most.

  • String fields are keyword by default — exact match, in, and case-insensitive like~ patterns all work.
  • High-cardinality text is wildcard-mapped with a full-text subfield for fast like~/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, and bintracer.file.where_froms.
  • destination.ip, source.ip, dns.resolved_ip, host.ip, and related.ip are ip-typed, so CIDR queries such as destination.ip == "10.0.0.0/8" work.
  • Booleans include the code-signature exists/trusted flags, bintracer.persistence.{run_at_load,keep_alive}, and the truncation flags under bintracer.process.* and bintracer.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/*")]
'''