fix(rapid7-insightidr): rule_keys_of_interest is an open vocabulary (v1.5.0)
Both mappers read that field through a hardcoded list of keys — hostname, process.cmd_line, parent_process.*, user — taken from the one alert this work was built against, a process_start_event detection. Every entry is a JSONata predicate on an exact key, so a rule emitting anything else matches none of them. A real alert from a second tenant showed what that costs. It is a custom ingress_auth rule, and its keys are: account locuz_hma@totogroup.net source_ip 183.82.103.101 geoip_country_code IN result SUCCESS Not one appears in the hardcoded list. The alert was fetched, inlined and mapped, and produced an incident as empty as the ones this integration set out to fix — with the account and the source IP sitting in the payload, unread. Two changes: - The whole list is flattened verbatim into `detection`, and rule_matching_keys into `detection_match`. Keys nobody anticipated now arrive intact, and they reach the AI analysis, which reads the mapped context and never `raw`. - account, source_ip, destination_ip, geoip_country_code and result gain explicit OCSF entries. The explicit mappings are not redundant with the flattening: a value under `detection.` is only ever seen by the IOC regex, so an account there would be indexed as an email, or not at all, rather than as a user. Typing an entity correctly still requires naming its field. evidences[] carries the flattened keys too, so the same holds for the second and third alert of an investigation, not only the first. Verified against both rule shapes end to end. The process rule yields the two hosts it did before; the authentication rule now yields the source IP and the account, where it previously yielded nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,29 @@ ocsf:
|
||||
# because rules are not consistent about which they emit.
|
||||
- { source_path: "alerts[0].rule_keys_of_interest[key='user'].values[0]", ocsf_field: "actor.user.name" }
|
||||
- { source_path: "alerts[0].rule_keys_of_interest[key='username'].values[0]", ocsf_field: "actor.user.name" }
|
||||
# ── Every key the rule found interesting ──────────────────────────
|
||||
# rule_keys_of_interest is an OPEN vocabulary: each detection rule names its own
|
||||
# keys. A process_start_event rule emits hostname / process.cmd_line /
|
||||
# parent_process.*; an ingress_auth rule emits account / source_ip /
|
||||
# geoip_country_code / result and none of the above. The explicit entries below
|
||||
# can therefore only ever cover the rules someone has already seen — on any
|
||||
# other rule they all resolve to nothing and the incident is empty again, which
|
||||
# is the exact failure this integration started from.
|
||||
#
|
||||
# So the whole list is also flattened, verbatim, into one object. Keys nobody
|
||||
# anticipated arrive intact and are visible to the analyst and to the AI
|
||||
# analysis, which reads the mapped context but never `raw`. The explicit
|
||||
# mappings stay because they are what types an entity correctly — a value under
|
||||
# `detection.` is only ever matched by the IOC regex, so an account there would
|
||||
# never be indexed as a user.
|
||||
- { source_path: "alerts[0].rule_keys_of_interest{key: values[0]}", ocsf_field: "detection" }
|
||||
- { source_path: "alerts[0].rule_matching_keys{key: values[0]}", ocsf_field: "detection_match" }
|
||||
# ── Keys seen on non-process rules ────────────────────────────────
|
||||
- { source_path: "alerts[0].rule_keys_of_interest[key='account'].values[0]", ocsf_field: "actor.user.account.name" }
|
||||
- { source_path: "alerts[0].rule_keys_of_interest[key='source_ip'].values[0]", ocsf_field: "src_endpoint.ip" }
|
||||
- { source_path: "alerts[0].rule_keys_of_interest[key='destination_ip'].values[0]", ocsf_field: "dst_endpoint.ip" }
|
||||
- { source_path: "alerts[0].rule_keys_of_interest[key='geoip_country_code'].values[0]", ocsf_field: "src_endpoint.location.country" }
|
||||
- { source_path: "alerts[0].rule_keys_of_interest[key='result'].values[0]", ocsf_field: "status_detail" }
|
||||
# ── Every alert, not just the first ───────────────────────────────
|
||||
# An investigation aggregates alerts, and the fields above only ever read alerts[0].
|
||||
# This entry lands the whole set as OCSF-shaped objects, so the host and the process of
|
||||
@@ -119,6 +142,7 @@ ocsf:
|
||||
"name": rule_keys_of_interest[key='process.name'].values[0],
|
||||
"cmd_line": rule_keys_of_interest[key='process.cmd_line'].values[0]
|
||||
},
|
||||
"attacks": { "technique": { "uid": rule.mitre_tcodes[$substring($, 0, 2) != 'TA'][0] } }
|
||||
"attacks": { "technique": { "uid": rule.mitre_tcodes[$substring($, 0, 2) != 'TA'][0] } },
|
||||
"detection": rule_keys_of_interest{key: values[0]}
|
||||
}
|
||||
ocsf_field: "evidences"
|
||||
|
||||
@@ -124,3 +124,26 @@ ocsf:
|
||||
- { source_path: "investigation.priority", ocsf_field: "finding_info.related_events.priority" }
|
||||
- { source_path: "investigation.disposition", ocsf_field: "finding_info.related_events.disposition" }
|
||||
- { source_path: "investigation.rrn", ocsf_field: "finding_info.related_events.uid" }
|
||||
# ── Every key the rule found interesting ──────────────────────────
|
||||
# rule_keys_of_interest is an OPEN vocabulary: each detection rule names its own
|
||||
# keys. A process_start_event rule emits hostname / process.cmd_line /
|
||||
# parent_process.*; an ingress_auth rule emits account / source_ip /
|
||||
# geoip_country_code / result and none of the above. The explicit entries below
|
||||
# can therefore only ever cover the rules someone has already seen — on any
|
||||
# other rule they all resolve to nothing and the incident is empty again, which
|
||||
# is the exact failure this integration started from.
|
||||
#
|
||||
# So the whole list is also flattened, verbatim, into one object. Keys nobody
|
||||
# anticipated arrive intact and are visible to the analyst and to the AI
|
||||
# analysis, which reads the mapped context but never `raw`. The explicit
|
||||
# mappings stay because they are what types an entity correctly — a value under
|
||||
# `detection.` is only ever matched by the IOC regex, so an account there would
|
||||
# never be indexed as a user.
|
||||
- { source_path: "rule_keys_of_interest{key: values[0]}", ocsf_field: "detection" }
|
||||
- { source_path: "rule_matching_keys{key: values[0]}", ocsf_field: "detection_match" }
|
||||
# ── Keys seen on non-process rules ────────────────────────────────
|
||||
- { source_path: "rule_keys_of_interest[key='account'].values[0]", ocsf_field: "actor.user.account.name" }
|
||||
- { source_path: "rule_keys_of_interest[key='source_ip'].values[0]", ocsf_field: "src_endpoint.ip" }
|
||||
- { source_path: "rule_keys_of_interest[key='destination_ip'].values[0]", ocsf_field: "dst_endpoint.ip" }
|
||||
- { source_path: "rule_keys_of_interest[key='geoip_country_code'].values[0]", ocsf_field: "src_endpoint.location.country" }
|
||||
- { source_path: "rule_keys_of_interest[key='result'].values[0]", ocsf_field: "status_detail" }
|
||||
|
||||
Reference in New Issue
Block a user