Skip to main content
Solved

Example to create a struct in additional fields in UDM

  • November 27, 2025
  • 5 replies
  • 126 views

HusHusHus
Forum|alt.badge.img+2

I am trying to learn extending the parser and I have learned that the UDM entity data model has a field called additional and this accepts a Struct. 

So my idea was to parse any changes to the IAM policies and basically copy the object and put it into the additional fields. Using the AI lab had not much success but it might help you to find an example for me how this is done as you can see in the example. Because basically it creates a new key for each field in the bindingDeltas object.

Here you can see the work that I have done with the AI lab at the same time.
https://gist.github.com/sakirma/0a33218d0dfd2a329dc4f53b8b39c5a1

Best answer by cmmartin_google

Additional is a Struct -  https://protobuf.dev/reference/protobuf/google.protobuf/#struct - but I now understand you’re trying to add a Struct within the existing Struct :)

Here is the how Additional looks (using the BigQuery schema which calls a STRUCT a RECORD):

---

additional (RECORD)

  • fields (RECORD)

    • key (STRING)

    • value (RECORD)

      • string_value (STRING)

      • bool_value (BOOLEAN)

      • number_value (FLOAT)

---

So if you had an input log like below:

{
"timestamp": "2023-10-27T10:00:00Z",
"process": "Updater.exe",
"field": {
"Name": "MaxConnections",
"OldValue": 100,
"NewValue": 500
},
"action": "WriteProperty"
}

Then you could use GROK code as follows:

filter {
# 1. Initialize variables to empty strings to prevent "field not found" errors
mutate {
replace => {
"extracted_field_name" => ""
"extracted_old_value_str" => ""
"extracted_new_value_str" => ""
"json_parse_failed_flag" => "" # Flag to indicate if JSON parsing failed, will be "true" on error
"temp_old_value_struct_field" => "" # Temporary variable for the OldValue struct field
"temp_new_value_struct_field" => "" # Temporary variable for the NewValue struct field
"temp_main_additional_field_item" => "" # Temporary variable for the main item to be added to additional.fields
}
}

# 2. Parse the incoming message, assuming it is in JSON format
json {
source => "message"
array_function => "split_columns" # Useful for handling potential top-level JSON arrays
on_error => "json_parse_failed_flag" # Set this flag to "true" if JSON parsing fails
}

# 3. Proceed only if JSON parsing was successful (i.e., the error flag is NOT "true")
if [json_parse_failed_flag] != "true" {

# 4. Extract the 'Name' from the 'field' object
if [field][Name] != "" {
mutate {
replace => { "extracted_field_name" => "%{field.Name}" }
}
}

# 5. Convert 'OldValue' from integer to string and store
if [field][OldValue] != "" {
mutate { convert => { "field.OldValue" => "string" } }
mutate { replace => { "extracted_old_value_str" => "%{field.OldValue}" } }
}

# 5. Convert 'NewValue' from integer to string and store
if [field][NewValue] != "" {
mutate { convert => { "field.NewValue" => "string" } }
mutate { replace => { "extracted_new_value_str" => "%{field.NewValue}" } }
}

# 6. Construct the nested structure for additional.fields if the field name was extracted
if [extracted_field_name] != "" {

# Create the inner struct field for 'NewValue' (comes first in desired output)
mutate {
replace => {
"temp_new_value_struct_field.key" => "NewValue"
"temp_new_value_struct_field.value.string_value" => "%{extracted_new_value_str}"
}
}

# Create the inner struct field for 'OldValue'
mutate {
replace => {
"temp_old_value_struct_field.key" => "OldValue"
"temp_old_value_struct_field.value.string_value" => "%{extracted_old_value_str}"
}
}

# Initialize the main temporary variable for the additional.fields item
# This will hold the structure: { "key": "MaxConnections", "value": { "struct_value": { "fields": [...] } } }
mutate {
replace => {
"temp_main_additional_field_item.key" => "%{extracted_field_name}"
}
}

# Merge the individual struct fields into the 'fields' array within 'struct_value'
# The order matters here for the desired UDM output: NewValue then OldValue.
mutate {
merge => {
"temp_main_additional_field_item.value.struct_value.fields" => "temp_new_value_struct_field"
}
}
mutate {
merge => {
"temp_main_additional_field_item.value.struct_value.fields" => "temp_old_value_struct_field"
}
}

# 7. Finally, merge the complete custom item into the UDM's 'additional.fields'
mutate {
merge => {
"event.idm.read_only_udm.additional.fields" => "temp_main_additional_field_item"
}
}
}
}

# 8. Output the constructed event to Chronicle UDM
mutate {
merge => {
"@output" => "event"
}
}
}

Which would give you an output as follows:

idm:
readOnlyUdm:
additional:
MaxConnections:
OldValue: "100"
NewValue: "500"

 

5 replies

HusHusHus
Forum|alt.badge.img+2
  • Author
  • Bronze 1
  • November 27, 2025

Just after having posted this. I got other problems to deal with as well. Which is that my “WIP” example just doesn’t work for every log. So I have to fix that too...


Forum|alt.badge.img+12

Here are some parser extensions which include a sample of creating additionals - 

https://docs.cloud.google.com/chronicle/docs/event-processing/parser-extension-examples#code_snippet_arbitrary_field_extraction_into_additional_object


HusHusHus
Forum|alt.badge.img+2
  • Author
  • Bronze 1
  • November 28, 2025

@cmmartin_google The CSV example unfortunately doesn’t show how you can add additional struct. The UDM documentation shows that it is possible to add structs. Do you know how I can fix this with the following code example?

mutate {
replace => {
"_additional_field_action" => ""
"_additional_policy_delta" => ""
}
}
 
mutate {
replace => {
"_additional_policy_delta.key" => "test"
"_additional_policy_delta.value.struct_value.fields" => "protoPayload.serviceData.policyDelta.bindingDeltas"
}
}
 
statedump {
label => "testing"
}
 
mutate {
merge => {
"event.idm.read_only_udm.additional.fields" => "_additional_policy_delta"
}
}

I have done something like this and then I get error like this


generic::unknown: pipeline.ParseLogEntry failed: LOG_PARSING_CBN_ERROR: "generic::invalid_argument: failed to convert raw output to events: failed to convert raw message 0: field \"idm\": index 0: recursive rawDataToProto failed: field \"read_only_udm\": index 0: recursive rawDataToProto failed: field \"additional\": index 0: recursive rawDataToProto failed: field \"fields\": index 0: recursive rawDataToProto failed: field \"value\": index 0: recursive rawDataToProto failed: field \"struct_value\": index 0: recursive rawDataToProto failed: field \"fields\": failed to make strategy: received non-slice or non-array raw output for repeated field"

HusHusHus
Forum|alt.badge.img+2
  • Author
  • Bronze 1
  • November 28, 2025
mutate {
replace => {
"_additional_field_action" => ""
"_additional_policy_delta" => ""
}
}
 
mutate {
replace => {
"_additional_policy_delta.key" => "test"
"_additional_policy_delta.value.struct_value" => "protoPayload.serviceData.policyDelta.bindingDeltas"
}
}
 
statedump {
}
 
mutate {
merge => {
"event.idm.read_only_udm.additional.fields" => "_additional_policy_delta"
}
}

with this example, it doesn’t work either. The statedump shows clearly that bindingDeltas doesn’t work 
"_additional_policy_delta": {    "key": "test",    "value": {      "struct_value": "protoPayload.serviceData.policyDelta.bindingDeltas"    }  },

Forum|alt.badge.img+12

Additional is a Struct -  https://protobuf.dev/reference/protobuf/google.protobuf/#struct - but I now understand you’re trying to add a Struct within the existing Struct :)

Here is the how Additional looks (using the BigQuery schema which calls a STRUCT a RECORD):

---

additional (RECORD)

  • fields (RECORD)

    • key (STRING)

    • value (RECORD)

      • string_value (STRING)

      • bool_value (BOOLEAN)

      • number_value (FLOAT)

---

So if you had an input log like below:

{
"timestamp": "2023-10-27T10:00:00Z",
"process": "Updater.exe",
"field": {
"Name": "MaxConnections",
"OldValue": 100,
"NewValue": 500
},
"action": "WriteProperty"
}

Then you could use GROK code as follows:

filter {
# 1. Initialize variables to empty strings to prevent "field not found" errors
mutate {
replace => {
"extracted_field_name" => ""
"extracted_old_value_str" => ""
"extracted_new_value_str" => ""
"json_parse_failed_flag" => "" # Flag to indicate if JSON parsing failed, will be "true" on error
"temp_old_value_struct_field" => "" # Temporary variable for the OldValue struct field
"temp_new_value_struct_field" => "" # Temporary variable for the NewValue struct field
"temp_main_additional_field_item" => "" # Temporary variable for the main item to be added to additional.fields
}
}

# 2. Parse the incoming message, assuming it is in JSON format
json {
source => "message"
array_function => "split_columns" # Useful for handling potential top-level JSON arrays
on_error => "json_parse_failed_flag" # Set this flag to "true" if JSON parsing fails
}

# 3. Proceed only if JSON parsing was successful (i.e., the error flag is NOT "true")
if [json_parse_failed_flag] != "true" {

# 4. Extract the 'Name' from the 'field' object
if [field][Name] != "" {
mutate {
replace => { "extracted_field_name" => "%{field.Name}" }
}
}

# 5. Convert 'OldValue' from integer to string and store
if [field][OldValue] != "" {
mutate { convert => { "field.OldValue" => "string" } }
mutate { replace => { "extracted_old_value_str" => "%{field.OldValue}" } }
}

# 5. Convert 'NewValue' from integer to string and store
if [field][NewValue] != "" {
mutate { convert => { "field.NewValue" => "string" } }
mutate { replace => { "extracted_new_value_str" => "%{field.NewValue}" } }
}

# 6. Construct the nested structure for additional.fields if the field name was extracted
if [extracted_field_name] != "" {

# Create the inner struct field for 'NewValue' (comes first in desired output)
mutate {
replace => {
"temp_new_value_struct_field.key" => "NewValue"
"temp_new_value_struct_field.value.string_value" => "%{extracted_new_value_str}"
}
}

# Create the inner struct field for 'OldValue'
mutate {
replace => {
"temp_old_value_struct_field.key" => "OldValue"
"temp_old_value_struct_field.value.string_value" => "%{extracted_old_value_str}"
}
}

# Initialize the main temporary variable for the additional.fields item
# This will hold the structure: { "key": "MaxConnections", "value": { "struct_value": { "fields": [...] } } }
mutate {
replace => {
"temp_main_additional_field_item.key" => "%{extracted_field_name}"
}
}

# Merge the individual struct fields into the 'fields' array within 'struct_value'
# The order matters here for the desired UDM output: NewValue then OldValue.
mutate {
merge => {
"temp_main_additional_field_item.value.struct_value.fields" => "temp_new_value_struct_field"
}
}
mutate {
merge => {
"temp_main_additional_field_item.value.struct_value.fields" => "temp_old_value_struct_field"
}
}

# 7. Finally, merge the complete custom item into the UDM's 'additional.fields'
mutate {
merge => {
"event.idm.read_only_udm.additional.fields" => "temp_main_additional_field_item"
}
}
}
}

# 8. Output the constructed event to Chronicle UDM
mutate {
merge => {
"@output" => "event"
}
}
}

Which would give you an output as follows:

idm:
readOnlyUdm:
additional:
MaxConnections:
OldValue: "100"
NewValue: "500"