You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add one field to the plugin contract's event model: SecurityEvent.outcome — "success" | "failure" | "unknown" | None — stating whether the event's attempt succeeded,
from the producing source's perspective. Anchored verbatim to ECS event.outcome; exported as
OCSF status_id. Populate it in the syslog-family normalizers, and give the contract's category field the honest semantics it has always lacked.
Why
FireWatch's CRITICAL detection ("brute force, then a successful login — probable compromise")
needs to know which events are failures and which are successes. Today that fact exists
only inside each plugin's private category strings — which is why the core rule is coupled to
one plugin's labels, and why a fully contract-conformant source (linux_auth, PR #73) shipped
with zero ability to reach the CRITICAL rule. The contract mandates category but defines no
vocabulary for it; outcome carries the actual fact, with two published standards behind it.
This makes "adding a source requires zero core edits" true in the sense that counts: the next
auth-capable source joins the CRITICAL path by setting values it already knows.
Context
ADR-0071 (draft, in review — link activates when the ADR batch is committed to docs/adr/):
D1 (the field), D2 (export mapping), D4 (contract v1.5 + category semantics).
Standards (fetched 2026-07-16; re-quote in the PR):
ECS event.outcome (https://github.com/elastic/ecs/blob/main/schemas/event.yml): "…simply
denotes whether the event represents a success or a failure from the perspective of the
entity that produced the event." unknown is for a genuine attempt with unknowable result;
"not all events will have an associated outcome… In such cases event.outcome should not
be populated" — that is the None case, and the distinction is contractual.
The store SHALL gain the matching nullable column via the established idempotent additive
migration pattern; FilterSpec MAY gain outcome filtering (cheap here, else follow-up).
The syslog normalizer SHALL set: "SSH Brute Force" → failure, "SSH Login" → success,
"Sudo Failure" → failure; the syslog_cef fallback path SHALL mirror it.
WHEN the OCSF export serializes an event whose class carries status_id, it SHALL map success→1, failure→2, unknown→0, and SHALL omit the attribute when outcome=None.
PLUGIN_CONTRACT.md SHALL gain (v1.5): outcome in the normalize() MUST-set-where-known
list with the ECS definitions quoted verbatim, and a category semantics subsection —
category is a stable, human-readable, per-source label; core detection logic MUST NOT key
on it (with the two named interim exceptions and their retirement pointer).
Must-NOT: no normalizer SHALL fabricate an outcome (Suricata / Azure WAF / AWS NFW /
ClamAV set nothing in this issue; events without a knowable result stay None, never
defaulted to failure).
Must-NOT:tests/golden/fixtures/expected_scores.json SHALL NOT move; outcome
assertions in normalize goldens are additive new pins, not a re-bless.
Collection modes: mode-agnostic — model + normalize() + export only.
Summary
Add one field to the plugin contract's event model:
SecurityEvent.outcome—"success" | "failure" | "unknown" | None— stating whether the event's attempt succeeded,from the producing source's perspective. Anchored verbatim to ECS
event.outcome; exported asOCSF
status_id. Populate it in the syslog-family normalizers, and give the contract'scategoryfield the honest semantics it has always lacked.Why
FireWatch's CRITICAL detection ("brute force, then a successful login — probable compromise")
needs to know which events are failures and which are successes. Today that fact exists
only inside each plugin's private
categorystrings — which is why the core rule is coupled toone plugin's labels, and why a fully contract-conformant source (linux_auth, PR #73) shipped
with zero ability to reach the CRITICAL rule. The contract mandates
categorybut defines novocabulary for it;
outcomecarries the actual fact, with two published standards behind it.This makes "adding a source requires zero core edits" true in the sense that counts: the next
auth-capable source joins the CRITICAL path by setting values it already knows.
Context
docs/adr/):D1 (the field), D2 (export mapping), D4 (contract v1.5 + category semantics).
normalize()responsibilities (the section that gainsoutcomeand the category-semanticssubsection). Contract version v1.5, sequenced after Docs/Contract: severity semantics are contract surface — PLUGIN_CONTRACT v1.4 + author guidance (ADR-0069 D5) #70's v1.4 changelog entry.
event.outcome(https://github.com/elastic/ecs/blob/main/schemas/event.yml): "…simplydenotes whether the event represents a success or a failure from the perspective of the
entity that produced the event."
unknownis for a genuine attempt with unknowable result;"not all events will have an associated outcome… In such cases
event.outcomeshould notbe populated" — that is the
Nonecase, and the distinction is contractual.status_id(https://schema.ocsf.io/api/1.8.0/classes/authentication):1Success ·2Failure ·0Unknown.migration.
that re-keys the correlation rules on this field (ADR-0071 D3).
Acceptance criteria
SecurityEventSHALL gainoutcome: Literal["success","failure","unknown"] | None = None(additive, defaulted — existing plugins remain conformant unchanged).
migration pattern;
FilterSpecMAY gainoutcomefiltering (cheap here, else follow-up).failure, "SSH Login" →success,"Sudo Failure" →
failure; the syslog_cef fallback path SHALL mirror it.this issue's diff.
status_id, it SHALL mapsuccess→1,failure→2,unknown→0, and SHALL omit the attribute whenoutcome=None.outcomein thenormalize()MUST-set-where-knownlist with the ECS definitions quoted verbatim, and a category semantics subsection —
category is a stable, human-readable, per-source label; core detection logic MUST NOT key
on it (with the two named interim exceptions and their retirement pointer).
ClamAV set nothing in this issue; events without a knowable result stay
None, neverdefaulted to
failure).tests/golden/fixtures/expected_scores.jsonSHALL NOT move; outcomeassertions in normalize goldens are additive new pins, not a re-bless.
normalize()+ export only.Out of scope
_brute_force_then_login/_ids_then_brute_forceon this field (the companioncore issue, ADR-0071 D3 — depends on this one and Sources: OCSF class correction — auth events are Authentication (3002/3), not Network Activity (4001/4) #76).
windows_event, incl. Defender detections) #24, Azure WAF derivedoutcomes) — same vocabulary, later issues, no new decision.