opentide

Elastic Security (`platform::elastic::1.0`)

`configurations.elastic` is an Elastic Security detection rule. Deploy creates it through the Kibana detection engine API (`/api/detection_engine/rules`, API version `2023-10-31`). One platform serves self-managed, Elastic Cloud Hosted, and Elastic Cloud Serverless Security: the deployment type changes only the tenant

Summary

configurations.elastic is an Elastic Security detection rule. Deploy creates it through the Kibana detection engine API (/api/detection_engine/rules, API version 2023-10-31). One platform serves self-managed, Elastic Cloud Hosted, and Elastic Cloud Serverless Security: the deployment type changes only the tenant URLs, the credentials, and which features the licence or project tier enables.

type selects one of seven rule types, every type the Security create-rule UI offers except a saved query. The detection language comes from the block, not from the platform: KQL or Lucene for query, threshold, new_terms, and threat_match; EQL for eql; ES|QL for esql; none for machine_learning. Query validation is supported and live only: Kibana rule preview plus the Elasticsearch checks, run against a tenant. There is no offline syntax check.

The Kibana rule_id is the rule's metadata.uuid, so one UUID addresses the rule in every tenant and space. Elastic never stores an external id: Kibana's internal id is not recorded in the block, in the rule file, or in deployment state.

Shared fields (enabled, name, schema, status, flags, tenants, contributors) are defined in platforms. On this block status is required, and it alone decides whether the deployed rule is enabled. The design, the research behind it, and a reference test against Kibana 8.19 and 9.5 are in RFC 0007.

Requirements

Static validation (opentide validate) and registry load:

  • type and status MUST be present. type MUST be one of query, eql, esql, threshold, new_terms, threat_match, or machine_learning.
  • query MUST be present for every type except machine_learning. When present it MUST be a non-blank string.
  • A block MUST satisfy every constraint. A violation MUST be reported with its constraint code.
  • Unknown keys MUST fail, at the block root and inside every object in the field tables. The open objects are filters[].meta, filters[].query, the entries of threat.filters, the entries of actions.notify, and the osquery object of an actions.respond entry.
  • Legacy external-id keys (rule_id, rule_id_bundle, rule_id::<tenant>) at the block root MUST fail with external_id_in_rule (External rule ids). Registry load drops those keys and passes every other key to the model, so registry load accepts exactly the blocks static validation accepts, apart from the legacy keys.
  • Field names (suppression.group_by, threshold.field, threshold.cardinality.field, new_terms.fields, highlighted_fields, required_fields[].name, threat.mapping[].entries[].field and value) name fields in the tenant's data. They MUST NOT be checked against a closed list. Deploy checks the aggregation, timestamp, and indicator fields against the tenant instead (Elasticsearch checks).
  • Durations MUST follow Durations.
  • An advisory SHOULD be reported as a warning. A reported advisory MUST carry its advisory code. An advisory never rejects a rule.

Deploy:

  • Every Kibana request MUST carry elastic-api-version: 2023-10-31 and Authorization: ApiKey <api_key>. Every Kibana request other than GET MUST also carry kbn-xsrf: true. Elasticsearch requests use the same API key.
  • The compiled body MUST NOT contain id or version. Deploy MUST NOT call _bulk_create, _bulk_update, or _bulk_delete, which Kibana 9.0 removed.
  • Deploy MUST run the Elasticsearch checks before every create or update of a rule. A failed check fails that rule: it is not created, updated, or replaced.
  • Deploy MUST NOT modify an Elastic prebuilt rule (Deploy).
  • Deploy MUST NOT write to the rule file and MUST NOT create a deployment-state shard.
  • Deploy MUST NOT delete a remote rule that has no MDR. Orphan pruning is not part of this revision.
  • Deploy MUST NOT call Kibana Stack alerting (/api/alerting/rule), Watcher, Cases, Fleet, Elastic Defend policy or artifact APIs, or the prebuilt-rule installer. It MUST NOT create or edit data views and MUST NOT write the space settings securitySolution:defaultIndex or securitySolution:defaultThreatIndex.
  • When a tenant sets suppression = false, deploy MUST omit alert_suppression and MUST warn once per affected rule.

Query validation:

  • opentide validate query --platform elastic --live MUST preview each rule and run the Elasticsearch checks (Query validation). Without --live the command refuses and names --live, as on every platform.

Definition

Block

FieldTypeRequiredDefaultDescription
typestringyes—Rule type (Rule types). Kibana cannot change it in place; see Deploy.
statusstringyes—Shared field, required on this block. A status from deployment.md. The rule's own status is not read.
querystringyes, except machine_learningnullThe event query, in the language the type selects. Multiline. Not sent for machine_learning.
languagestringoptionalnullkuery (KQL) or lucene, for the event query of query, threshold, new_terms, and threat_match. Compiled as kuery when omitted. MUST NOT be set for eql, esql, or machine_learning.
indexlist[string]optionalnullIndex patterns. A single pattern is a one-item list ([logs-*]). See Data source.
data_view_idstringoptionalnullOne existing Kibana data view. Replaces index.
filterslist[ElasticFilter]optionalnullRule query filters. Not Elastic Defend event filters.
schedulingElasticSchedulingoptionalnullRuns every, additional look-back, and max alerts per run.
severitystring | ElasticSeverityoptionalnullAn alert_severity value, or a default plus severity overrides (Severity and risk).
riskint | ElasticRiskoptionalnullRisk score 0–100, or a score plus risk score overrides.
suppressionElasticSuppressionoptionalnullAlert suppression.
highlighted_fieldslist[string]optionalnullCustom highlighted fields on the alert.
thresholdElasticThresholdiff type: thresholdnullGroup-by fields and count.
new_termsElasticNewTermsiff type: new_termsnullFields whose new values alert, and the history window.
eqlElasticEqloptional, type: eql onlynullEQL-only settings.
threatElasticThreatMatchiff type: threat_matchnullThe indicator side of an indicator match rule. Not the ATT&CK array (ATT&CK).
machine_learningElasticMachineLearningiff type: machine_learningnullAnomaly jobs and threshold.
integrationlist[string | ElasticIntegration]optionalnullIntegrations the rule expects. Recorded on the rule; Fleet is not touched.
required_fieldslist[ElasticRequiredField]optionalnullFields the rule needs, with their mapping types.
guideElasticGuideoptionalnullInvestigation guide and Setup guide.
false_positiveslist[string]optionalnullKnown false positives.
tagslist[string]optionalnullRule tags, after OpenTide and the tenant tags.
exceptionsElasticExceptionsoptionalnullException lists to attach.
building_blockbooleanoptionalfalseBuilding block rule: alerts are hidden from the default Alerts view.
overridesElasticOverridesoptionalnullRule name override and timestamp override.
actionsElasticActionsoptionalnullRule actions, response actions, and the Timeline template.

Not authored, because the Security create-rule screens do not ask for them: saved queries (saved_id), the prebuilt-rule license string, concurrent_searches, items_per_search, meta, output_index, namespace, and throttle. The last six are kept from the remote rule on update (Deploy).

Rule types

typeSecurity UI namequerylanguageType object
queryCustom queryKQL or Luceneoptional—
eqlEvent correlationEQLMUST NOT be seteql, optional
esqlES|QLES|QLMUST NOT be set— (FROM selects the data)
thresholdThresholdKQL or Luceneoptionalthreshold, required
new_termsNew termsKQL or Luceneoptionalnew_terms, required
threat_matchIndicator matchKQL or Lucene (the event query)optional (the event query)threat, required
machine_learningMachine learningnot usedMUST NOT be setmachine_learning, required

A new terms rule alerts when a value, or a combination of up to three values, appears that the rule has not seen in the history window. It does not count events; a threshold rule does.

Data source

On query, eql, threshold, new_terms, and the event side of threat_match, the compiled index is the block index when set, otherwise the tenant setup.index, otherwise absent. When it is absent, Kibana applies the space setting securitySolution:defaultIndex. data_view_id names one existing data view and replaces that chain. esql and machine_learning send neither index nor data_view_id: an ES|QL FROM clause and a job's datafeed select their data. threat.index is always required; the indicator UI pre-fills logs-ti_*, but the API does not.

Durations

A duration is a string in one of two equivalent forms:

; literals are case-sensitive
duration  = shorthand / iso8601
shorthand = [n "d"] [n "h"] [n "m"] [n "s"]                 ; at least one part
iso8601   = "P" [n "D"] ["T" [n "H"] [n "M"] [n "S"]]       ; at least one part
n         = 1*DIGIT
  • Shorthand is single-unit (90s, 5m, 1h, 14d) or compound (1h10m, 1d2h, 2m30s). Units are lower case, appear in the order d, h, m, s, and each appears at most once.
  • ISO 8601 is the same set of whole days, hours, minutes, and seconds (PT1H, P14D, P1DT2H), with upper-case designators. After T at least one time part MUST follow.
  • The value is the sum of its parts, so 1h10m and PT1H10M are the same duration. Weeks, fractions, signs, and spaces are invalid.
  • Every duration MUST be greater than zero, except scheduling.lookback, which MAY be zero (0m).

Kibana accepts only a single-unit value: it rejects ISO 8601 and weeks in interval, and alert_suppression.duration has no d unit. The compiler therefore sends every duration as one whole number in the largest unit that divides it exactly, choosing from d, h, m, s, and from h, m, s for alert_suppression.duration:

Writteninterval / fromalert_suppression.durationhistory_window_start
5m5m / now-5m{value: 5, unit: m}now-5m
90s90s / now-90s{value: 90, unit: s}now-90s
60m1h / now-1h{value: 1, unit: h}now-1h
1d1d / now-1d{value: 24, unit: h}now-1d
14d14d / now-14d{value: 336, unit: h}now-14d
PT1H1h / now-1h{value: 1, unit: h}now-1h
1h10m70m / now-70m{value: 70, unit: m}now-70m
2m30s150s / now-150s{value: 150, unit: s}now-150s
1d2h26h / now-26h{value: 26, unit: h}now-26h

The from column shows the duration alone. A rule's from is the sum of interval and lookback (Schedule).

Constraints

Static validation MUST reject a block that breaks any of these, and report the code:

CodeConstraint
type_blocktype is one of the seven types. threshold iff type: threshold. new_terms iff type: new_terms, and it has fields and history_window_start. eql only for type: eql. threat iff type: threat_match, and it has index, query, and mapping. machine_learning iff type: machine_learning, and it has job_id and anomaly_threshold. query is required except for machine_learning. actions is an object, not a list. The Kibana flat names and the retired pre-grouping keys are invalid at the block root: saved_id, interval, from, license, meta, alert_suppression, related_integrations, investigation_fields, max_signals, note, setup, endpoint_exceptions, exceptions_list, timeline_id, timeline_title, timestamp_override, timestamp_override_fallback_disabled, risk_score, risk_score_mapping, severity_mapping, timestamp_field, event_category_override, tiebreaker_field, new_terms_fields, history_window_start, threat_index, threat_query, threat_language, threat_mapping, threat_filters, threat_indicator_path, concurrent_searches, items_per_search, machine_learning_job_id, anomaly_threshold, rule_name_override, response_actions, timestamp, timeline, and passthrough.
languagelanguage MUST NOT be set for eql, esql, or machine_learning.
esql_sourceesql and machine_learning MUST NOT set index, data_view_id, or filters.
index_xor_data_viewindex and data_view_id are mutually exclusive.
index_listindex and threat.index, when set, are lists of one or more non-empty pattern strings. A string is invalid. index itself is optional.
suppression_shapesuppression is an object. For threshold, suppression.duration is required and group_by is forbidden. For every other type, group_by is required, with 1–3 distinct, non-empty field names.
new_terms_fieldsnew_terms.fields is a list of 1–3 entries.
threshold_fieldsthreshold.field has 0–5 entries, and threshold.value is an integer of at least 1.
risk_scorerisk, or risk.score when set, is an integer in 0–100.
durationEvery duration follows Durations and is greater than zero. scheduling.lookback may be zero.
filter_shapeEach entry of filters and threat.filters is exactly one complete shape: the phrase shorthand {field, value, negate?} or a raw {meta, query} filter. An entry with keys of both shapes, with neither, or with half of one is invalid.
exception_typeexceptions.lists[].type is detection or rule_default. The Elastic Endpoint list is attached only by exceptions.endpoint: true.
severityseverity, or severity.default, is a value of the alert_severity vocabulary (Informational, Low, Medium, High, Critical), compared case-insensitively. An unknown value is invalid. It is never compiled to a fallback.
response_actionEach actions.respond entry is {endpoint, comment?, field?} with endpoint one of isolate, kill-process, suspend-process, or runscript, or {osquery} with a non-empty osquery.query. No other key is allowed beside them. Kibana's own response-action shape (action_type_id, params) is invalid. runscript needs Kibana 9.4 or later.

A type object (threshold, new_terms, eql, threat, machine_learning) whose value is null counts as absent. A retired key in the type_block list is invalid even when its value is null. A block that omits status fails as a missing required field; implementations MAY report the constraint codes only for blocks that set status.

scheduling (ElasticScheduling)

FieldTypeRequiredDefaultDescription
intervaldurationoptional5mRuns every.
lookbackdurationoptional1mAdditional look-back time. 0m is valid.
max_alertsintoptionalnullMax alerts per run (max_signals). Omitted leaves the detection engine's default of 100.

Schedule

lookback is only the overlap added to interval. The compiler sends interval and sets from to now- plus the sum, so the defaults become interval: 5m and from: now-6m, and lookback: 0m sends from equal to interval. The detection engine has no separate field for the overlap; the UI shows from minus interval. Sentinel's scheduling.lookback has a different meaning (the whole query window): an Elastic lookback of 1m with an interval of 5m covers six minutes, not one. to is always now.

severity (ElasticSeverity)

FieldTypeRequiredDefaultDescription
defaultstringyes—An alert_severity value.
mappinglist[SeverityMapping]optionalnullSeverity override: alerts whose field equals value get severity.

mapping[]:

FieldTypeRequiredDefaultDescription
fieldstringyes—Source event field.
valuestringyes—Value to match.
operatorstringoptionalequalsequals is the only operator.
severitystringyes—Kibana severity: low, medium, high, or critical.

risk (ElasticRisk)

FieldTypeRequiredDefaultDescription
scoreintoptionalnull0–100. Omitted uses the severity table.
mappinglist[RiskScoreMapping]optionalnullRisk score override: the alert takes its score from field.

mapping[]:

FieldTypeRequiredDefaultDescription
fieldstringyes—Source event field holding a score.
operatorstringoptionalequalsequals is the only operator.
valuestringoptional""Kibana sends an empty string.

suppression (ElasticSuppression)

FieldTypeRequiredDefaultDescription
group_bylist[string]conditionalnull1–3 field names. Required for every type except threshold, where it is forbidden.
durationdurationconditionalnullSuppression window. Required for threshold. Omitted on other types suppresses for the rule execution only.
missing_fields_strategystringoptionalsuppresssuppress or doNotSuppress: whether alerts missing a group_by field are suppressed together. Not sent for threshold.

Alert suppression needs a Platinum licence or higher on self-managed and Hosted deployments, and a project tier that includes it on Serverless. On a Basic licence Kibana stores, previews, and runs alert_suppression without error but writes one alert per match. A least-privilege API key cannot read the licence, so the tenant declares it with setup.suppression (Tenant setup).

threshold (ElasticThreshold)

FieldTypeRequiredDefaultDescription
fieldlist[string]optional[]Group by, 0–5 fields. [] counts all matches.
valueintyes—Threshold, at least 1.
cardinalityElasticCardinalityoptionalnullCount: alert only when field has at least value distinct values.

cardinality:

FieldTypeRequiredDescription
fieldstringyesField whose distinct values are counted.
valueintyesMinimum number of distinct values.

new_terms (ElasticNewTerms)

FieldTypeRequiredDescription
fieldslist[string]yes1–3 fields whose value, or combination of values, must be new.
history_window_startdurationyesHistory window size. Sent as now-<duration>. Separate from the schedule.

eql (ElasticEql)

FieldTypeRequiredDefaultDescription
timestamp_fieldstringoptionalnullTimestamp field for EQL sequencing.
event_category_overridestringoptionalnullEvent category field.
tiebreaker_fieldstringoptionalnullTiebreaker for events with equal timestamps.

threat (ElasticThreatMatch)

The indicator side of an indicator match rule. The event query, language, index, and filters stay on the block. This object is not the detection engine's ATT&CK threat array, and the compiler MUST NOT copy it to that body key.

FieldTypeRequiredDefaultDescription
indexlist[string]yes—Indicator index patterns. Always a list.
querystringyes—Indicator query.
languagestringoptionalnullkuery or lucene, for the indicator query.
mappinglist[ThreatGroup]yes—Groups are OR. Entries in a group are AND.
filterslist[object]optionalnullIndicator filters, in the same two shapes as filters.
indicator_pathstringoptionalnullIndicator prefix override. Kibana's default is threat.indicator.

mapping[] has one key, entries (list, required). entries[]:

FieldTypeRequiredDefaultDescription
fieldstringyes—Event field.
valuestringyes—Indicator field.
typestringoptionalmappingmapping is the only type.
negatebooleanoptionalnulltrue is the UI's DOES NOT MATCH. Not sent when null.

machine_learning (ElasticMachineLearning)

FieldTypeRequiredDescription
job_idstring | list[string]yesExisting anomaly detection job id or ids. Sent unchanged, in either shape. The block does not create jobs.
anomaly_thresholdintyesAnomaly score cutoff on Kibana's 0–100 scale.

machine_learning.job_id and threat.index name objects in one tenant. The block carries them as written.

filters[] (ElasticFilter)

FieldTypeRequiredDefaultDescription
fieldstringwith valuenullPhrase shorthand: field to match.
valuestringwith fieldnullPhrase shorthand: value to match.
negatebooleanoptionalfalsePhrase shorthand: exclude matches.
metaobjectwith querynullRaw detection-engine filter meta.
queryobjectwith metanullRaw detection-engine filter query.

A shorthand entry compiles to a phrase filter:

{
  "meta": {"key": "<field>", "negate": false, "disabled": false, "type": "phrase", "params": {"query": "<value>"}},
  "query": {"match_phrase": {"<field>": "<value>"}}
}

A raw entry is copied unchanged.

integration[] (ElasticIntegration)

An entry is a package name (endpoint) or an object:

FieldTypeRequiredDefaultDescription
packagestringyes—Integration package, for example windows, endpoint, aws.
integrationstringoptionalnullPolicy template inside the package, for example cloudtrail.
versionstringoptional*Semver range, for example ^2.0.0.

required_fields[] (ElasticRequiredField)

FieldTypeRequiredDescription
namestringyesField name.
typestringyesElasticsearch mapping type: keyword, constant_keyword, wildcard, text, match_only_text, long, integer, short, byte, unsigned_long, double, float, half_float, scaled_float, date, date_nanos, boolean, ip, version, binary, geo_point, geo_shape, object, flattened, or nested.

Kibana adds ecs to each entry on create. It is true only when both the name and the type match ECS.

guide (ElasticGuide)

FieldTypeRequiredDefaultDescription
investigationstringoptionalnullInvestigation guide (Kibana note). Markdown.
setupstringoptionalnullSetup guide (Kibana setup). Not the tenant setup table.

exceptions (ElasticExceptions)

FieldTypeRequiredDefaultDescription
endpointbooleanoptionalfalseAttach the Elastic Endpoint exception list.
listslist[ExceptionList]optionalnullExisting exception lists.

lists[]:

FieldTypeRequiredDescription
idstringyesException list id.
list_idstringyesException list list_id.
namespace_typestringyessingle (one space) or agnostic (every space).
typestringyesdetection or rule_default.

overrides (ElasticOverrides)

FieldTypeRequiredDefaultDescription
namestringoptionalnullRule name override: the alert title comes from this field.
timestampstringoptionalnullTimestamp override: the rule's time range runs on this field instead of @timestamp.
disable_fallbackbooleanoptionalnull"Do not use @timestamp as a fallback timestamp field".

actions (ElasticActions)

FieldTypeRequiredDefaultDescription
notifylist[object]optionalnullRule actions: connector id, action_type_id, group, params, and frequency, in Kibana's shape. Copied unchanged.
respondlist[object]optionalnullResponse actions, one of the two shapes below.
timelineElasticTimelineoptionalnullTimeline template attached to the rule: id and title, both optional strings.

A respond entry compiles as follows:

WrittenCompiled response_actions[] entry
{endpoint: <command>, comment?}{action_type_id: .endpoint, params: {command: <command>, comment?}}. comment is sent when it is not empty.
{endpoint: <command>, field: <f>, comment?}as above, plus params.config: {field: <f>, overwrite: false}
{endpoint: kill-process|suspend-process, comment?}, without fieldas above, plus params.config: {field: process.entity_id, overwrite: true}
{osquery: {...}}{action_type_id: .osquery, params: {...}}, the osquery object copied unchanged

field is the custom process field that kill-process and suspend-process act on. Kibana rejects those two commands without params.config, so when field is omitted the compiler sends Kibana's default, the process.entity_id field with overwrite: true (opentide#558, opentide#562). isolate and runscript without field send no config.

Tenant setup

Tenants are workspace configuration in .opentide/configurations/platforms/elastic.toml (platforms), not part of the block. Three setup keys change the compiled body.

tenants.setup fieldTypeRequiredDefaultMeaning
urlstringyes—Kibana base URL, without /s/<space>.
api_keystringyes—Encoded API key (the encoded value from POST /_security/api_key). MUST be a $ENV reference.
elasticsearch_urlstringyes—Elasticsearch base URL of the same deployment, for the Elasticsearch checks.
spacestringnodefaultKibana space id. default uses the unprefixed path.
indexlist[string]nonullIndex patterns for blocks that set neither index nor data_view_id.
tagslist[string]no[]Tags added to every rule deployed to this tenant.
suppressionbooleannotrueWhether the licence or project tier applies alert suppression. false omits alert_suppression with a warning.
versionstringnonullLowest stack version among the deployments the tenant reaches: MAJOR.MINOR ("8.19", "9.5") or "serverless". Absent means unknown (Stack version).
sslbooleanyes—Shared tenant setup: TLS verification.

A tenant that sets proxy is refused; the proxy is the global opentide --proxy option. One API key serves Kibana and Elasticsearch. Rules run with the privileges of the key that last created or updated them, so each tenant SHOULD use a dedicated, non-personal key with Security rules management All in its space, read and view_index_metadata on every source index the rules query, and, for live validation, read on .preview.alerts-security.alerts-<space> and .internal.preview.alerts-security.alerts-<space>-*. Kibana renames the feature privileges between versions, so this spec does not pin their names.

The supported floor is Kibana 8.19 and 9.x. ES|QL rules need 8.14 or later. The detection engine and the query, eql, esql, threshold, and new_terms types work on the Basic licence; alert suppression does not (suppression).

Stack version

Query syntax differs across the supported range: ES|QL commands that shipped in 9.2 or later, such as INLINE STATS, are missing on 8.19, and so is FORK. setup.version lets a tenant declare the lowest stack version among the deployments it reaches, as a string MAJOR.MINOR ("8.19", "9.5") or "serverless" for Elastic Cloud Serverless. Any other value, a TOML number included (9.10 would read as 9.1), is a configuration error. Versions compare by major, then minor, as integers; serverless is later than every MAJOR.MINOR.

  • Static validation and editors MAY use the lowest version declared across a rule's target tenants (the block's tenants, or every tenant when it is empty or omitted) to report syntax that version lacks. A target tenant without version counts as the floor.
  • A version older than 8.19 is outside the supported range and is checked as 8.19.
  • Absent means unknown: report against the spec floor, Kibana 8.19.
  • Deploy MUST NOT read the version from the cluster on the author's behalf. A least-privilege key may not see it, as with suppression.
  • When the lowest version is 9.4 or later, or serverless, validation MAY skip esql_metadata_id (Advisories).

version does not change the compiled body.

Compiled body

Deploy compiles each rule into one detection-engine create or update body. Kibana's rule_id is metadata.uuid, verbatim. Every other key:

Detection-engine fieldSourceRule
typetypeverbatim
nameblock name, else rule name
descriptionrule descriptiontrailing whitespace stripped
enabledblock statusfalse iff that status's strategy is DISABLEMENT. Always sent, because an update without it keeps the remote value.
severity, risk_scoreseverity, riskSeverity and risk
severity_mappingseverity.mappingoperator defaults to equals
risk_score_mappingrisk.mappingoperator defaults to equals
intervalscheduling.intervalDurations; default 5m
fromscheduling.interval + scheduling.lookbacknow-<sum> (Schedule)
to—now
languagetype, languageeql for eql, esql for esql, omitted for machine_learning, else language or kuery
queryquerytrailing whitespace stripped; omitted for machine_learning
indexblock index, else tenant setup.indexData source. Omitted for esql and machine_learning, when data_view_id is set, or when both sources are empty.
data_view_iddata_view_idverbatim
filtersfiltersfilters; omitted for esql and machine_learning
thresholdthreshold{field, value}, plus cardinality as a one-item list when set
new_terms_fieldsnew_terms.fieldsverbatim
history_window_startnew_terms.history_window_startnow-<duration>
timestamp_field, event_category_override, tiebreaker_fieldeql.*verbatim, when set
threat_indexthreat.indexlist
threat_query, threat_language, threat_indicator_paththreat.query, threat.language, threat.indicator_pathverbatim, when set
threat_filtersthreat.filterscompiled like filters
threat_mappingthreat.mappingan entry without type is sent with type: mapping; a null negate is not sent
machine_learning_job_id, anomaly_thresholdmachine_learning.job_id, machine_learning.anomaly_thresholdverbatim; a string stays a string and a list stays a list
alert_suppressionsuppression{group_by, duration?, missing_fields_strategy}, or {duration} for threshold. duration is {value, unit}. Omitted when tenant setup.suppression is false.
investigation_fieldshighlighted_fields{field_names: [...]}
required_fieldsrequired_fieldsone {name, type} per entry, sorted by name
related_integrationsintegrationa string p becomes {package: p, version: "*"}; an object keeps its set keys and version defaults to "*"
false_positivesfalse_positivesverbatim
setupguide.setuptrailing whitespace stripped
noteguide.investigation, else response.procedure.analysistrailing whitespace stripped
max_signalsscheduling.max_alertsverbatim
rule_name_overrideoverrides.nameverbatim
timestamp_overrideoverrides.timestampverbatim
timestamp_override_fallback_disabledoverrides.disable_fallbacksent when not null
exceptions_listexceptions.lists, exceptions.endpointthe lists, then {id: endpoint_list, list_id: endpoint_list, namespace_type: agnostic, type: endpoint} when endpoint is true; omitted when empty
building_block_typebuilding_block: true"default"; false is not sent
actionsactions.notifyverbatim
response_actionsactions.respondactions
timeline_id, timeline_titleactions.timeline.id, actions.timeline.titleverbatim, when set
tags"opentide", tenant setup.tags, block tagsin that order, de-duplicated
authormetadata.author, metadata.contributorsde-duplicated; omitted when empty
referencesreferences.publicvalues in ascending key order
threatresolved techniquesATT&CK

An absent optional source produces no key. Kibana's concurrent_searches, items_per_search, meta, output_index, namespace, throttle, saved_id, and license are never compiled.

Severity and risk

The alert severity is severity (a string), else severity.default, else response.alert_severity, else Informational, matched case-insensitively:

alert_severityseverityrisk_score
Informationallow1
Lowlow21
Mediummedium47
Highhigh73
Criticalcritical99

risk (an integer) or risk.score replaces the table score. The table follows Elastic's prebuilt-rule convention and sits inside Kibana's risk bands (0–21 low, 22–47 medium, 48–73 high, 74–100 critical). An explicit score SHOULD stay inside its severity's band (risk_band).

ATT&CK

The body key threat is the ATT&CK array. It is derived from the rule's techniques plus the techniques inherited through detection_model (objective, then its threats), de-duplicated. Sentinel uses only the inherited set; Elastic takes the union, so a rule's own techniques are never dropped.

  1. Keep Enterprise techniques only. Mobile and ICS entries in att&ck.vocab.toml carry a Mobile : or Industrial : name prefix. Deploy MUST skip them with a warning (technique_not_enterprise).
  2. Map each technique's tide.vocab.stages to tactics with the table below. A sub-technique uses its own stages. A legacy stage is first renamed through the legacy-stage table; any other stage outside the table emits no tactic.
  3. Emit one entry per tactic, in table order, with framework: "MITRE ATT&CK". Techniques are sorted by id. A sub-technique appears under its parent technique, named without the <parent name>: prefix. An empty subtechnique list is omitted.
  4. reference is the vocabulary link for a technique and https://attack.mitre.org/tactics/<id>/ for a tactic.
StageTactic id
ReconnaissanceTA0043
Resource DevelopmentTA0042
Initial AccessTA0001
ExecutionTA0002
PersistenceTA0003
Privilege EscalationTA0004
StealthTA0005
Defense ImpairmentTA0112
Credential AccessTA0006
DiscoveryTA0007
Lateral MovementTA0008
CollectionTA0009
Command and ControlTA0011
ExfiltrationTA0010
ImpactTA0040

ATT&CK v19 renamed Defense Evasion (TA0005) to Stealth and split Defense Impairment (TA0112) out of it. Keys that ATT&CK has since revoked stay in att&ck::1.x with removed = "2.0" and keep the stage they were last published with. A legacy stage maps to the stage that now holds its tactic id:

Legacy stageStage
Defense EvasionStealth

Deploy

The base path is <url>/api/detection_engine/rules in space default, and <url>/s/<space>/api/detection_engine/rules in any other space.

Block status strategyCallsResult
PREVIEW, RELEASEThe Elasticsearch checks, then GET ?rule_id=<uuid>. A 404 leads to POST with the compiled body. A 200 leads to PUT with the compiled body plus the preserved fields, or to DELETE then POST when the remote type differs.rule created or replaced, enabled: true
DISABLEMENTas aboverule kept, enabled: false
DELETIONDELETE ?rule_id=<uuid>200 and 404 are both success
INERTnonenot deployed

PUT replaces the whole rule, so a key the block omits returns to Kibana's default. These remote fields are the exception: when the compiled body does not set one, deploy copies it from the GET response.

Preserved fieldWhy
actionsnotification connectors on this rule
exceptions_listexception lists attached to this rule
response_actionsElastic Defend and Osquery response actions on this rule
timeline_id, timeline_titleTimeline template on this rule
concurrent_searches, items_per_search, metadetection-engine fields the create UI does not show
output_index, namespace, throttlelegacy detection-rule fields

Type changes. Kibana cannot change type with PUT. When the remote type differs, deploy MUST DELETE the rule and POST the compiled body plus the preserved fields. The rule_id stays the same; Kibana assigns a new internal id, so execution history restarts.

Prebuilt rules. A remote rule with rule_source.type: external (customised prebuilt rules included), or with immutable: true on versions without rule_source, is an Elastic prebuilt rule. Deploy MUST report an error for it and MUST NOT change it.

Drift. GET also returns server-owned fields (id, revision, created_at, updated_at, execution_summary, rule_source, immutable) and defaults such as max_signals and risk_score_mapping, and Kibana adds ecs to each required_fields entry. A deployer that compares remote and compiled rules MUST compare only the compiled keys and MUST ignore required_fields[].ecs.

Errors. HTTP 400 or 409 on one rule, or a failed Elasticsearch check, fails that rule with the server's message, and the batch continues. HTTP 401 or 403 fails the tenant, and deploy moves to the next tenant. HTTP 429 and 5xx are retried up to three times with exponential backoff.

A deployer MAY list managed rules with GET _find?filter=alert.attributes.tags:"opentide" instead of one GET per rule.

Query validation

elastic is in the query-validation allowlist. The language comes from each rule's block: its type, and language for the types that take one (Rule types).

ModeBehaviour
without --liveRefuses and names --live. There is no offline syntax check.
--livePOST <base>/preview with the compiled body plus {"invocationCount": 1, "timeframeEnd": "<now>"}, and the Elasticsearch checks. timeframeEnd MUST be UTC with a Z suffix (2026-09-25T11:10:00.000Z); Kibana rejects a +00:00 offset.
Preview responseResult
HTTP 200, every logs[].errors emptyvalid. logs[].warnings are reported as warnings.
HTTP 200 with logs[].errorsinvalid, with the errors. A missing preview-index privilege is a tenant configuration error, not a query error.
HTTP 400invalid: a rule-level schema error
HTTP 401 or 403the tenant fails
isAborted: trueinconclusive, reported as a warning

Preview runs the rule once against live data and writes only to the preview alerts index. It catches KQL, Lucene, and EQL syntax errors, unknown EQL fields, ES|QL errors inside a command, unknown ES|QL columns, and rule-level schema errors. It does not check licence-gated behaviour, on 9.x it does not catch an unknown ES|QL command, and it cannot tell a misspelt aggregation field from one with no matches. The Elasticsearch checks cover those gaps. CI SHOULD point --live at a staging tenant, because preview costs a real query.

Elasticsearch checks

Kibana accepts some rules that then fail silently. Before every create or update, and during live validation, deploy MUST run these checks against the tenant's elasticsearch_url with its API key. HTTP 401, 403, 429, and 5xx follow the deploy error rules. A check that could not run is not a pass.

ES|QL pre-flight. Kibana does not parse ES|QL on create: on 8.19 a mistyped command makes every scheduled run fail, and on 9.5 Kibana drops the unknown command and the rule matches everything in its window. For every esql rule, deploy sends POST <elasticsearch_url>/_query with the query plus \n| LIMIT 0. HTTP 200 passes and returns the result columns. A parsing_exception fails the rule. On a verification_exception, deploy calls GET <elasticsearch_url>/_resolve/index/<FROM sources>?ignore_unavailable=true&allow_no_indices=true: when indices, aliases, and data_streams are all empty, the sources are not onboarded yet and the rule deploys with a warning; otherwise the rule fails.

Aggregation fields. suppression.group_by, threshold.field, threshold.cardinality.field, and new_terms.fields MUST each be present with aggregatable: true in GET <elasticsearch_url>/<sources>/_field_caps?fields=<fields>&ignore_unavailable=true&allow_no_indices=true. <sources> is the compiled index, or the title of the data view from GET /api/data_views/data_view/<data_view_id> in the tenant's space. Otherwise the rule fails. A data view lookup or _field_caps call that does not return 200 also fails the rule. An empty indices list in the response means the sources are not onboarded yet: the rule deploys with a warning. A rule with no explicit source uses the space default index, and the check is skipped with a warning. For esql, the group_by fields MUST instead be among the pre-flight columns.

Timestamp fields. When overrides.timestamp or eql.timestamp_field is set to a field other than @timestamp, that field MUST be mapped as date or date_nanos in the same <sources>; any other mapping fails the rule. Sources not onboarded yet, or a field not mapped yet, are a warning.

Indicator match fields. For threat_match, each threat.mapping entry's field MUST be searchable in the event <sources>, and its value searchable in threat.index. A field mapped as not searchable fails the rule. Sources not onboarded yet, or a field not mapped yet, are a warning.

Advisories

Validation SHOULD warn, without rejecting the rule, when a condition below holds. A warning MUST carry its code, so the CLI and an editor name the same finding the same way.

CodeCondition
lookback_zeroscheduling.lookback is zero: the next run starts as the window ends, and Elastic recommends at least one minute.
lookback_longscheduling.lookback exceeds 1h: lookback is overlap added to interval, not the whole window as in Sentinel.
threshold_lookbacka threshold rule's lookback exceeds its interval: threshold counts are not de-duplicated across overlapping runs.
esql_metadata_ida non-aggregating ES|QL query lacks METADATA _id: alerts are not de-duplicated.
eql_maxspan_windowan EQL sequence maxspan exceeds interval + lookback: the sequence cannot fit one run.
eql_maxspan_lookbackan EQL sequence maxspan exceeds lookback but not interval + lookback: a sequence that straddles two runs is missed.
risk_bandan explicit risk score is outside its severity's band.
technique_not_enterprisethe rule names a technique that is not Enterprise ATT&CK, which deploy skips (ATT&CK). Static validation reports it from the technique list, before deploy.

Each condition has one code, and a rule that meets two conditions gets both warnings. The two EQL conditions do not overlap: a maxspan above interval + lookback reports only eql_maxspan_window.

An ES|QL query is non-aggregating as Kibana decides it: no pipeline step is STATS. INLINE STATS keeps every row, so it does not make a query aggregating. Kibana 9.4 and later add METADATA _id to a non-aggregating query themselves; esql_metadata_id matters for Kibana 8.19 through 9.3.

Relationships

Defaults & overrides

FieldDefault
languagekuery at compile, for the types that take a language
scheduling.interval5m
scheduling.lookback1m, so from is now-6m with the default interval
scheduling.max_alertsnot sent; the detection engine's 100
severityresponse.alert_severity, then Informational
riskthe severity table score
suppression.missing_fields_strategysuppress
threshold.field[]
threat.mapping[].entries[].typemapping
integration[].version*
severity.mapping[].operator, risk.mapping[].operatorequals
risk.mapping[].value""
exceptions.endpointfalse
building_blockfalse
indextenant setup.index, then the space's securitySolution:defaultIndex
kill-process and suspend-process config{field: process.entity_id, overwrite: true} when field is omitted
Tenant spacedefault
Tenant suppressiontrue
Tenant versionunknown: syntax is reported against Kibana 8.19

Examples

The smallest useful block, a custom query:

configurations:
  elastic:
    enabled: true
    schema: platform::elastic::1.0
    status: STAGING
    type: query
    language: kuery
    query: 'event.category : "process"'
    index: [logs-*]
    scheduling:
      interval: 5m
      lookback: 1m

With rule name: Rule name, metadata.uuid: 00000000-0000-4000-8000-000000000000, metadata.author: Detection Engineering, description: What this rule detects., response.alert_severity: Medium, and no techniques, it compiles for a tenant with no index or tags to:

{
  "rule_id": "00000000-0000-4000-8000-000000000000",
  "type": "query",
  "name": "Rule name",
  "description": "What this rule detects.",
  "enabled": true,
  "severity": "medium",
  "risk_score": 47,
  "interval": "5m",
  "from": "now-6m",
  "to": "now",
  "language": "kuery",
  "query": "event.category : \"process\"",
  "index": ["logs-*"],
  "tags": ["opentide"],
  "author": ["Detection Engineering"]
}

The keys that change with the rule type:

# eql: no language
type: eql
query: |
  sequence by host.id with maxspan=1m
    [process where event.type == "start" and process.name == "x.exe"]
index: [logs-endpoint.events.process-*]
eql:
  timestamp_field: "@timestamp"

# esql: no index, data_view_id, filters, or language
type: esql
query: |
  FROM logs-*
  | STATS count = COUNT(*) BY host.name
  | WHERE count > 10

# threshold: suppression is a duration, with no group_by
type: threshold
query: 'event.category : "authentication" and event.outcome : "failure"'
threshold:
  field: [source.ip]
  value: 20
  cardinality: {field: user.name, value: 5}
suppression:
  duration: 1h

# new_terms
type: new_terms
query: 'event.category : "iam"'
new_terms:
  fields: [user.name]
  history_window_start: 14d

# threat_match: the event query stays flat; the indicator side is threat
type: threat_match
query: 'event.category : "file"'
threat:
  index: [logs-ti_*]
  query: '@timestamp >= "now-30d/d"'
  mapping:
    - entries:
        - field: file.hash.sha256
          value: threat.indicator.file.hash.sha256

# machine_learning: no query, index, or language
type: machine_learning
machine_learning:
  job_id: rare_process_by_host
  anomaly_threshold: 50

This block breaks three constraints and reports esql_source, language, and suppression_shape:

elastic:
  enabled: true
  schema: platform::elastic::1.0
  status: STAGING
  type: esql
  language: kuery
  query: FROM logs-* | LIMIT 10
  index: [logs-*]
  suppression: {duration: 1h}

Fixtures:

RFC 0007 has a complete rule for every rule type, and the compiled body of its Example A.

History

VersionDateNotes
1.12026-10-23Optional tenant setup.version: the lowest stack version a tenant reaches, MAJOR.MINOR or serverless (Stack version). Static validation and editors MAY report syntax the lowest declared version across a rule's target tenants lacks; absent means the 8.19 floor. Deploy MUST NOT read the version from the cluster (specifications#59). Schema id unchanged.
1.12026-10-23Advisories carry stable codes, one per condition: lookback_zero, lookback_long, threshold_lookback, esql_metadata_id, eql_maxspan_window, eql_maxspan_lookback, risk_band, and technique_not_enterprise. The EQL maxspan advisory is split in two. A non-aggregating ES|QL query is one with no STATS step; INLINE STATS does not count. Kibana 9.4 and later add METADATA _id themselves (specifications#57). Schema id unchanged.
1.02026-10-19The tactic table follows ATT&CK v19 as att&ck::1.2 (ATT&CK 20.0) publishes it: TA0005 is Stealth, and Defense Impairment is TA0112. Removed keys still staged Defense Evasion map to Stealth through the legacy-stage table. Without this, techniques staged only in those tactics emitted no threat entry (opentide#568).
1.02026-10-19Initial field contract from RFC 0007 and the opentide implementation (specifications#17). Amends the RFC in two places. Durations accept compound shorthand (1h10m, 1d2h, 2m30s) as well as single-unit shorthand and ISO 8601 (opentide#564). kill-process and suspend-process without field send Kibana's default process field, process.entity_id with overwrite: true (opentide#562).

On this page