diff --git a/google_secops_parser/README.md b/google_secops_parser/README.md index 01edf77a..ba0525d0 100644 --- a/google_secops_parser/README.md +++ b/google_secops_parser/README.md @@ -71,11 +71,16 @@ DMARC types. 2. **JSON types** — Chronicle's `json{}` filter **preserves the original JSON type**, so parsedmarc's booleans and numbers are handled differently: - **Booleans** (`dmarc_aligned` / `spf_aligned` / `dkim_aligned` / - `normalized_timespan`) are converted to strings so `[dmarc_aligned] == - "false"` works, and stored as `string_value` (Google's content-hub parsers - never use `bool_value`). Note that `testing` is **not** a boolean — - parsedmarc emits the RFC 9990 `t=` flag as the string `"y"`/`"n"` — so it - is passed through as a string and guarded with `!= ""`. + `normalized_timespan`) are converted to strings because CBN conditionals + compare against the preserved type, so `[dmarc_aligned] == "false"` needs + a string. In `additional.fields` they are stored as typed **`bool_value`** + — built as a string, `convert`-ed back to `boolean`, then renamed — the + boolean pattern from Google's + [parser extension examples](https://docs.cloud.google.com/chronicle/docs/event-processing/parser-extension-examples); + UDM search matches them with `value.bool_value`. Note that `testing` is + **not** a boolean — parsedmarc emits the RFC 9990 `t=` flag as the string + `"y"`/`"n"` — so it is passed through as a string and guarded with + `!= ""`. - **Numbers** (`count` / `*_session_count` / `source_asn`) are stored as `number_value` — built as a string, `convert`-ed to `uinteger`, then renamed — so SecOps can range-query and sort them (parsedmarc's "store @@ -83,6 +88,9 @@ DMARC types. so a tenant where `%{}` rejects non-string fields degrades to a missing `additional.fields` entry instead of `_failed_parsing_`. + Both value types match what the `[gsecops]` API output emits, so UDM + searches and dashboards port between the two delivery paths unchanged. + Every `if`-tested field is initialized to `""` *before* `json` and guarded with `!= ""`: CBN raises `_failed_parsing_` on a conditional referencing an absent field, and treats an initialized-but-empty field as present. A @@ -205,7 +213,8 @@ Suggested order: depends on. 2. The two aggregates — confirm the `dmarc_aligned=false` one yields `security_result.category = AUTH_VIOLATION`, that `count` and `source_asn` - land as `number_value`, and that the boolean→string handling holds. + land as `number_value`, and that the alignment booleans land as + `bool_value` (queryable as `additional.fields["dmarc_aligned"].value.bool_value`). 3. The SMTP TLS rows — confirm the success row produces a `GENERIC_EVENT` with `target.hostname` and no `security_result`, and the failure rows produce `security_result.action = FAIL`. diff --git a/google_secops_parser/parsedmarc.conf b/google_secops_parser/parsedmarc.conf index 7d9de151..841ab594 100644 --- a/google_secops_parser/parsedmarc.conf +++ b/google_secops_parser/parsedmarc.conf @@ -58,13 +58,17 @@ filter { # 2. JSON TYPES ARE PRESERVED. The CBN json{} filter keeps the original JSON # type (a JSON boolean stays a boolean, a number stays a number). # Booleans (*_aligned / normalized_timespan) are converted to strings - # (step 1b) so `== "true"` / `== "false"` tests work, and stored as - # string_value (content-hub never uses bool_value). "testing" is NOT a - # boolean: parsedmarc emits the RFC 9990 t= flag as the string "y"/"n". - # Numbers (count / *_session_count / source_asn) are stored as - # number_value -- built as a string, convert-ed to uinteger, then - # renamed to number_value -- so SecOps can range-query and sort them. - # Matches the content-hub parsers. + # (step 1b) because CBN conditionals compare against the preserved type, + # so `== "true"` / `== "false"` tests need strings. In additional.fields + # they are stored as typed bool_value -- built as a string, convert-ed + # back to boolean, renamed to bool_value -- following the boolean + # example in Google's parser extension docs; UDM search matches them + # with value.bool_value. "testing" is NOT a boolean: parsedmarc emits + # the RFC 9990 t= flag as the string "y"/"n". Numbers (count / + # *_session_count / source_asn) are stored the same way as number_value + # so SecOps can range-query and sort them. Value types match the + # [gsecops] API output, so searches port between the two paths. + # https://docs.cloud.google.com/chronicle/docs/event-processing/parser-extension-examples # 2b. CONDITIONALS. Every field tested in an `if` is initialized to "" in # step 1a, and guards use `if [field] != ""` (not bare `if [field]`, # which is true for an initialized-but-empty field). Matches content-hub. @@ -446,10 +450,23 @@ filter { merge => { "event.idm.read_only_udm.additional.fields" => "f_disc" } } } + # Booleans are stored as typed bool_value via the documented pattern for + # boolean UDM fields (build string -> convert to boolean -> rename), the + # boolean analogue of the number_value chains below. UDM search matches + # them with additional.fields["..."].value.bool_value, and the type + # matches what parsedmarc's [gsecops] API output emits. + # https://docs.cloud.google.com/chronicle/docs/event-processing/parser-extension-examples if [normalized_timespan] == "true" or [normalized_timespan] == "false" { mutate { replace => { "f_norm.key" => "normalized_timespan" "f_norm.value.string_value" => "%{normalized_timespan}" } - merge => { "event.idm.read_only_udm.additional.fields" => "f_norm" } + } + mutate { + convert => { "f_norm.value.string_value" => "boolean" } + on_error => "norm_not_bool" + } + if ![norm_not_bool] { + mutate { rename => { "f_norm.value.string_value" => "f_norm.value.bool_value" } } + mutate { merge => { "event.idm.read_only_udm.additional.fields" => "f_norm" } } } } @@ -457,19 +474,40 @@ filter { if [dmarc_aligned] == "true" or [dmarc_aligned] == "false" { mutate { replace => { "f_dmarc.key" => "dmarc_aligned" "f_dmarc.value.string_value" => "%{dmarc_aligned}" } - merge => { "event.idm.read_only_udm.additional.fields" => "f_dmarc" } + } + mutate { + convert => { "f_dmarc.value.string_value" => "boolean" } + on_error => "dmarc_not_bool" + } + if ![dmarc_not_bool] { + mutate { rename => { "f_dmarc.value.string_value" => "f_dmarc.value.bool_value" } } + mutate { merge => { "event.idm.read_only_udm.additional.fields" => "f_dmarc" } } } } if [spf_aligned] == "true" or [spf_aligned] == "false" { mutate { replace => { "f_spfa.key" => "spf_aligned" "f_spfa.value.string_value" => "%{spf_aligned}" } - merge => { "event.idm.read_only_udm.additional.fields" => "f_spfa" } + } + mutate { + convert => { "f_spfa.value.string_value" => "boolean" } + on_error => "spfa_not_bool" + } + if ![spfa_not_bool] { + mutate { rename => { "f_spfa.value.string_value" => "f_spfa.value.bool_value" } } + mutate { merge => { "event.idm.read_only_udm.additional.fields" => "f_spfa" } } } } if [dkim_aligned] == "true" or [dkim_aligned] == "false" { mutate { replace => { "f_dkima.key" => "dkim_aligned" "f_dkima.value.string_value" => "%{dkim_aligned}" } - merge => { "event.idm.read_only_udm.additional.fields" => "f_dkima" } + } + mutate { + convert => { "f_dkima.value.string_value" => "boolean" } + on_error => "dkima_not_bool" + } + if ![dkima_not_bool] { + mutate { rename => { "f_dkima.value.string_value" => "f_dkima.value.bool_value" } } + mutate { merge => { "event.idm.read_only_udm.additional.fields" => "f_dkima" } } } } if [disposition] != "" {