feat(cortex-xdr): watermark incidents on modification_time, not creation
An XDR incident is not finished when it is created. Alerts keep joining it, an analyst changes its status, its severity is raised. A creation_time watermark fetches it once, the watermark moves past it, and nothing that happens afterwards ever reaches Riposte — which is precisely the content the full fetch exists to bring in. modified_after filters and sorts on modification_time instead, so an incident comes back on every change and dedup on incident_id turns the second visit into an enrichment of the incident already there. It is now what the ingest hint prefills; created_after stays for a one-shot backfill. Worth knowing about that enrichment: it merges context and can fill a detection anchor that was missing, but it does not restate the incident's severity or status. An incident XDR later raises to critical stays at the severity it was ingested with. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -127,10 +127,20 @@ def main():
|
||||
created_ms = to_ms(inputs.get("created_after"))
|
||||
if created_ms is not None:
|
||||
filters.append({"field": "creation_time", "operator": "gte", "value": created_ms})
|
||||
# An XDR incident keeps growing after it is created: alerts join it, an
|
||||
# analyst changes its status. Watermarking on creation_time fetches it once
|
||||
# and never looks again, so everything that happened afterwards is lost.
|
||||
# Watermarking on modification_time brings it back on every change, where
|
||||
# dedup on incident_id turns the second visit into an enrichment.
|
||||
modified_ms = to_ms(inputs.get("modified_after"))
|
||||
if modified_ms is not None:
|
||||
filters.append({"field": "modification_time", "operator": "gte", "value": modified_ms})
|
||||
incremental = created_ms is not None or modified_ms is not None
|
||||
# Oldest first on an incremental fetch, so that a window holding more
|
||||
# incidents than `limit` drops its most RECENT ones — the only ones the next
|
||||
# poll can still see. Newest first otherwise, for a hand-run command.
|
||||
keyword = "asc" if created_ms is not None else "desc"
|
||||
sort_field = "modification_time" if modified_ms is not None else "creation_time"
|
||||
keyword = "asc" if incremental else "desc"
|
||||
exclude = str(inputs.get("exclude_artifacts") or "").lower() in ("1", "true", "yes")
|
||||
|
||||
incidents, total = [], None
|
||||
@@ -138,7 +148,7 @@ def main():
|
||||
rd = {
|
||||
"search_from": len(incidents),
|
||||
"search_to": min(len(incidents) + PAGE, limit),
|
||||
"sort": {"field": "creation_time", "keyword": keyword},
|
||||
"sort": {"field": sort_field, "keyword": keyword},
|
||||
# Without this the nested alerts come back trimmed to a handful of
|
||||
# fields — the very thing this command exists to avoid.
|
||||
"full_alert_fields": True,
|
||||
|
||||
Reference in New Issue
Block a user