YARA-L 2.0 language syntax

This section describes the major elements of the YARA-L syntax. See also Overview of the YARA-L 2.0 language.

Rule structure

For YARA-L 2.0, you must specify variable declarations, definitions, and usages in the following order:

  1. meta
  2. events
  3. match (optional)
  4. outcome (optional)
  5. condition
  6. options (optional)

The following illustrates the generic structure of a rule:

rule <rule Name>
{
  meta:
    // Stores arbitrary key-value pairs of rule details, such as who wrote
    // it, what it detects on, version control, etc.

  events:
    // Conditions to filter events and the relationship between events.

  match:
    // Values to return when matches are found.

  outcome:
    // Additional information extracted from each detection.

  condition:
    // Condition to check events and the variables used to find matches.

  options:
    // Options to turn on or off while executing this rule.
}

Comments

Designate comments with two slash characters (// comment) or multi-line comments set off using slash asterisk characters (/* comment */), as you would in C.

Constants

Integer, string, boolean, and regex constants are supported.

String and regex constants

You can use either of the following quotation characters to enclose strings in YARA-L 2.0. However, quoted text is interpreted differently depending on which one you use.

  1. Double quotes (") — Use for normal strings. Must include escape characters.
    For example: "hello\tworld" —\t is interpreted as a tab

  2. Back quotes (`) — Use to interpret all characters literally.
    For example: `hello\tworld` —\t is not interpreted as a tab

For regular expressions, you have two options.

If you want to use regular expressions directly without the re.regex() function, use /regex/ for the regular expression constants.

You can also use string constants as regex constants when you use the re.regex() function. Note that for double quote string constants, you must escape backslash characters with backslash characters, which can look awkward.

For example, the following regular expressions are equivalent:

  • re.regex($e.network.email.from, `.*altostrat\.com`)
  • re.regex($e.network.email.from, ".*altostrat\\.com")
  • $e.network.email.from = /.*altostrat\\.com/

Google recommends using back quote characters for strings in regular expressions for ease of readability.

Operators

You can use the following operators in YARA-L:

Operator Description
= equal/declaration
!= not equal
< less than
<= less than or equal
> greater than
>= greater than or equal

Variables

In YARA-L 2.0, all variables are represented as $<variable name>.

You can define the following types of variables:

  • Event variables — Represent groups of events in normalized form (UDM) or entity events. Specify conditions for event variables in the events section. You identify event variables using a name, event source, and event fields. Allowed sources are udm (for normalized events) and graph (for entity events). If the source is omitted, udm is set as the default source. Event fields are represented as a chain of .<field name> (for example, $e.field1.field2). Event field chains always start from the top-level source (UDM or Entity).

  • Match variables — Declare in the match section. Match variables become grouping fields for the query, as one row is returned for each unique set of match variables (and for each time window). When the rule finds a match, the match variable values are returned. Specify what each match variable represents in the events section.

  • Placeholder variables — Declare and define in the events section. Placeholder variables are similar to match variables. However, you can use placeholder variables in the condition section to specify match conditions.

Use match variables and placeholder variables to declare relationships between event fields through transitive join conditions (see Events Section Syntax for more detail).

Functions

This section describes the YARA-L 2.0 functions that Chronicle supports in Detection Engine.

These functions can be used in the following areas in a rule:

String functions

Chronicle supports the following string manipulation functions:

  • strings.concat(a, b)
  • strings.to_lower(stringText)
  • strings.to_upper(stringText)
  • strings.base64_decode(encodedString)

The following sections describe how to use each.

Concatenate strings or integers

Returns the concatenation of two strings, two integers, or a combination of the two.

strings.concat(a, b)

This function takes two arguments, that can be either strings or integers, and returns the two values concatenated as a string. Integers are cast to a string before concatenation. The arguments can be literals or event fields. If both arguments are fields, the two attributes must be from the same event.

The following example includes a string variable and string literal as arguments.

"google-test" = strings.concat($e.principal.hostname, "-test")

The following example includes a string variable and integer variable as arguments. Both principal.hostname and principal.port are from the same event, $e, and are concatenated to return a string.

"google80" = strings.concat($e.principal.hostname, $e.principal.port)

The following example attempts to concatenate principal.port from event $e1, with principal.hostname from event $e2. It will return a compiler error because the arguments are different event variables.

// returns a compiler error
"test" = strings.concat($e1.principal.port, $e2.principal.hostname)

Convert string to uppercase or lowercase

These functions return string text after changing all characters to either uppercase or lowercase.

  • strings.to_lower(stringText)
  • strings.to_upper(stringText)
"test@google.com" = strings.to_lower($e.network.email.from)
"TEST@GOOGLE.COM" = strings.to_upper($e.network.email.to)

Base64 decode a string

Returns a string containing the base64 decoded version of the encoded string.

strings.base64_decode(encodedString)

This function takes one base64 encoded string as an argument. If encodedString is not a valid base64 encoded string, the function returns encodedString as-is.

This example returns True if principal.domain.name is "dGVzdA==", which is base64 encoding for the string "test".

"test" = strings.base64_decode($e.principal.domain.name)

RegExp functions

Chronicle supports the following regular expression functions:

  • re.regex(stringText, regex)
  • re.capture(stringText, regex)
  • re.replace(stringText, replaceRegex, replacementText)

RegExp match

You can define regular expression matching in YARA-L 2.0 using either of the following syntax:

  • Using YARA syntax — Related to events. The following is a generic representation of this syntax: $e.field = /regex/
  • Using YARA-L syntax — As a function taking in the following parameters:
    • Field the regular expression is applied to.
    • Regular expression specified as a string. You can use the nocase modifier after strings to indicate that the search should ignore capitalization. The following is a generic representation of this syntax: re.regex($e.field, `regex`)

Be aware of the following while defining regular expressions in YARA-L 2.0:

  • In either case, the predicate is true if the string contains a substring that matches the regular expression provided. It is unnecessary to add .* to the beginning or at the end of the regular expression.
  • To match the exact string or only a prefix or suffix, include the ^ (starting) and $ (ending) anchor characters in the regular expression. For example, /^full$/ matches "full" exactly, while /full/ could match "fullest", "lawfull", and "joyfully".
  • If the UDM field includes newline characters, the regexp only matches the first line of the UDM field. To enforce full UDM field matching, add a (?s) to the regular expression. For example, replace /.*allUDM.*/ with /(?s).allUDM.*/.

RegExp capture

Captures (extracts) data from a string using the regular expression pattern provided in the argument.

re.capture(stringText, regex)

This function takes two arguments:

  • stringText: the original string to search.
  • regex: the regular expression indicating the pattern to search for.

The regular expression can contain 0 or 1 capture groups in parentheses. If the regular expression contains 0 capture groups, the function returns the first entire matching substring. If the regular expression contains 1 capture group, it returns the first matching substring for the capture group. Defining two or more capture groups returns a compiler error.

In this example, if $e.principal.hostname contains "aaa1bbaa2" the following would be True, because the function returns the first instance. This example has no capture groups.

"aaa1" = re.capture($e.principal.hostname, "a+[1-9]")

This example captures everything after the @ symbol in an email. If the $e.network.email.from field is test@google.com, the example returns google.com. This example contains one capture group.

"google.com" = re.capture($e.network.email.from , "@(.*)")

RegExp replacement

Performs a regular expression replacement.

re.replace(stringText, replaceRegex, replacementText)

This function takes three arguments:

  • stringText: the original string.
  • replaceRegex: the regular expression indicating the pattern to search for.
  • replacementText: The text to insert into each match.

Returns a new string derived from the original stringText, where all substrings that match the pattern in replaceRegex are replaced with the value in replacementText. You can use backslash-escaped digits (\1 to \9) within replacementText to insert text matching the corresponding parenthesized group in the replaceRegex pattern. Use \0 to refer to the entire matching text.

The function replaces non-overlapping matches and will prioritize replacing the first occurrence found. For example, re.replace("banana", "ana", "111") returns the string "b111na".

This example captures everything after the @ symbol in an email, replaces com with org, and then returns the result. Notice the use of nested functions.

"email@google.org" = re.replace($e.network.email.from, "com", "org")

This example uses backslash-escaped digits in the replacementText argument to reference matches to the replaceRegex pattern.

"test1.com.google" = re.replace(
                       $e.principal.hostname, // holds "test1.test2.google.com"
                       "test2.([a-z]*).([a-z]*).*",
                       "\\2.\\1"  // \\1 holds "google", \\2 holds "com"
                     )

Date functions

Chronicle supports the following date-related functions:

  • timestamp.get_minute(unix_seconds [, time_zone])
  • timestamp.get_hour(unix_seconds [, time_zone])
  • timestamp.get_day_of_week(unix_seconds [, time_zone])
  • timestamp.get_week(unix_seconds [, time_zone])
  • timestamp.current_seconds()

Chronicle supports negative integers as the unix_seconds argument. Negative integers represent times before the Unix epoch. If you provide an invalid integer, for example a value that results in an overflow, the function will return -1. This is an uncommon scenario.

Because YARA-L 2 doesn't support negative integer literals, make sure to check for this condition using the less than or greater than operator. For example:

0 > timestamp.get_hour(123)

Time extraction

Returns an integer in the range [0, 59].

timestamp.get_minute(unix_seconds [, time_zone])

The following function returns an integer in the range [0, 23], representing the hour of day.

timestamp.get_hour(unix_seconds [, time_zone])

The following function returns an integer in the range [1, 7] representing the day of week starting with Sunday. For example, 1 = Sunday; 2 = Monday, etc.

timestamp.get_day_of_week(unix_seconds [, time_zone])

The following function returns an integer in the range [0, 53] representing the week of the year. Weeks begin with Sunday. Dates before the first Sunday of the year are in week 0.

timestamp.get_week(unix_seconds [, time_zone])

These time extraction functions have the same arguments.

  • unix_seconds is an integer representing the number of seconds past Unix epoch, such as $e.metadata.event_timestamp.seconds, or a placeholder containing that value.
  • time_zone is optional and is a string representing a time_zone. If omitted, the default is "GMT". You can specify time zones using string literals. The options are:
    • The TZ database name, for example "America/Los_Angeles". For more information, see the "TZ Database Name" column from this page
    • The time zone offset from UTC, in the format(+|-)H[H][:M[M]], for example: "-08:00".

In this example, the time_zone argument is omitted, so it defaults to "GMT".

$ts = $e.metadata.collected_timestamp.seconds

timestamp.get_hour($ts) = 15

This example uses a string literal to define the time_zone.

$ts = $e.metadata.collected_timestamp.seconds

2 = timestamp.get_day_of_week($ts, "America/Los_Angeles")

Here are examples of other valid time_zone specifiers, which you can pass as the second argument to time extraction functions:

  • "America/Los_Angeles", or "-08:00". ("PST" is not supported)
  • "America/New_York", or "-05:00". ("EST" is not supported)
  • "Europe/London"
  • "UTC"
  • "GMT"

Current timestamp

Returns an integer representing the current time in Unix seconds. This is approximately equal to the detection timestamp and is based on when the rule is run.

timestamp.current_seconds()

The following example returns True if the certificate has been expired for more than 24h. It calculates the time difference by subtracting the current Unix seconds, and then comparing using a greater than operator.

86400 < timestamp.current_seconds() - $e.network.tls.certificate.not_after

Math functions

Absolute value

Returns the absolute value of an integer expression.

math.abs(intExpression)

This example returns True if events were more than 5 minutes apart, regardless of which event came first. Notice how the example uses nested functions.

5 < timestamp.get_minute(
      math.abs($e1.metadata.event_timestamp.seconds
               - $e2.metadata.event_timestamp.seconds
      )
    )

Net functions

Returns true when the given IP address is within the specified subnetwork.

net.ip_in_range_cidr(ipAddress, subnetworkRange)

You can use YARA-L to search for UDM events across all of the IP addresses within a subnetwork using the net.ip_in_range_cidr() statement. Both IPv4 and IPv6 are supported.

To search across a range of IP addresses, specify an IP UDM field and a Classless Inter-Domain Routing (CIDR) range. YARA-L can handle both singular and repeating IP address fields.

IPv4 example:

net.ip_in_range_cidr($e.principal.ip, "192.0.2.0/24")

IPv6 example:

net.ip_in_range_cidr($e.network.dhcp.yiaddr, "2001:db8::/32")

For an example rule using the net.ip_in_range_cidr()statement, see the example rule. Single Event within Range of IP Addresses

Meta section syntax

Meta section is composed of multiple lines, where each line defines a key-value pair. A key part must be an unquoted string, and a value part must be a quoted string:

<key> = "<value>"

The following is an example of a valid meta section line: none meta: author = "Chronicle" severity = "HIGH"

Events section syntax

In the events section, list the predicates to specify the following:

  • What each match or placeholder variable represents
  • Simple binary expressions as conditions
  • Function expressions as conditions
  • Reference list expressions as conditions
  • Logical operators

Variable declarations

For variable declarations, use the following syntax:

  • <EVENT_FIELD> = <VAR>
  • <VAR> = <EVENT_FIELD>

Both are equivalent, as shown in the following examples:

  • $e.source.hostname = $hostname
  • $userid = $e.principal.user.userid

This declaration indicates that this variable represents the specified field for the event variable. When the event field is a repeated field, the match variable can represent any value in the array. It is also possible to assign multiple event fields to a single match or placeholder variable. This is a transitive join condition.

For example, the following:

  • $e1.source.ip = $ip
  • $e2.target.ip = $ip

Are equivalent to:

  • $e1.source.ip = $ip
  • $e1.source.ip = $e2.target.ip

When a variable is used, the variable must be declared through variable declaration. If a variable is used without any declaration, it is regarded as a compilation error.

Simple binary expressions as conditions

For a simple binary expression to use as condition, use the following syntax:

  • <EXPR> <OP> <EXPR>

Expression can be either event field, variable, constant, or function expression.

For example:

  • $e.source.hostname = "host1234"
  • $e.source.port < 1024
  • 1024 < $e.source.port
  • $e1.source.hostname != $e2.target.hostname
  • $e1.metadata.timestamp > $e2.metadata.timestamp
  • $port >= 25
  • $host = $e2.target.hostname
  • "google-test" = strings.concat($e.principal.hostname, "-test")
  • "email@google.org" = re.replace($e.network.email.from, "com", "org")

If both sides are constants, it is regarded as a compilation error.

Function expressions as conditions

Some function expressions return boolean value, which can be used as an individual predicate in the events section. Such functions are:

  • re.regex()
  • net.ip_in_range_cidr()

For example:

  • re.regex($e.principal.hostname, `.*\.google\.com`)
  • net.ip_in_range_cidr($e.principal.ip, "192.0.2.0/24")

Reference list expressions as conditions

Use the in operator to check for the existence of UDM values within a reference list based on equality. The in operator can be combined with not to exclude the values of a reference list. Reference list name must be prepended with % character.

Currently, only string values are supported:

  • $e.principal.hostname in %hostname_list
  • $e.about.ip in %phishing_site_list
  • $hostname in %whitelisted_hosts

Logical operators

You can use the logical and and logical or operators in the events section as shown in the following examples:

  • $e.metadata.event_type = "NETWORK_DNS" or $e.metadata.event_type = "NETWORK_DHCP"
  • ($e.metadata.event_type = "NETWORK_DNS" and $e.principal.ip = "192.0.2.12") or ($e.metadata.event_type = "NETWORK_DHCP" and $e.principal.mac = "AB:CD:01:10:EF:22")
  • not $e.metadata.event_type = "NETWORK_DNS"

By default, the precedence order from highest to lowest is not, and, or.

For example, "a or b and c" is evaluated as "a or (b and c)". You can use parentheses to alter the precedence if needed.

In the events section, all predicates are regarded as ANDed together by default.

Modifiers

nocase

When you have a comparison expression between string values or a regex expression, you can append nocase at the end of the expression to ignore capitalization.

  • $e.principal.hostname != "http-server" nocase
  • $e1.principal.hostname = $e2.target.hostname nocase
  • $e.principal.hostname = /dns-server-[0-9]+/ nocase
  • re.regex($e.target.hostname, `client-[0-9]+`) nocase

This cannot be used when a type of field is an enumerated value. Below examples are invalid and will generate compilation errors:

  • $e.metadata.event_type = "NETWORK_DNS" nocase
  • $e.network.ip_protocol = "TCP" nocase

Repeated fields

any, all

In UDM and Entity, some fields are labeled as repeated, which indicates they are lists of values or other types of messages. In YARA-L, each element in the repeated field is treated individually. That means, if the repeated field is used in the rule, we evaluate the rule for each element in the field. This can lead to an unexpected behavior. For example, if a rule has both $e.principal.ip = "1.2.3.4" and $e.principal.ip = "5.6.7.8" in the events section, the rule never generates any matches, even if both "1.2.3.4" and "5.6.7.8" are in principal.ip.

To evaluate the repeated field as a whole, you can use any and all operators. When any is used, the predicate is evaluated as true if any value in the repeated field satisfies the condition. When all is used, the predicate is evaluated as true if all values in the repeated field satisfy the condition.

  • any $e.target.ip = "127.0.0.1"
  • all $e.target.ip != "127.0.0.1"
  • re.regex(any $e.about.hostname, `server-[0-9]+`)
  • net.ip_in_range_cidr(all $e.principal.ip, "10.0.0.0/8")

The any and all operators can only be used with repeated fields. In addition, they cannot be used when assigning a repeated field to a placeholder variable or joining with a field of another event.

For example, any $e.principal.ip = $ip and any $e1.principal.ip = $e2.principal.ip are not valid syntax. To match or join a repeated field, use $e.principal.ip = $ip. There will be one match variable value or join for each element of the repeated field.

When writing a condition with any or all, be aware that negating the condition with not might not have the same meaning as using the negated operator.

For example:

  • not all $e.principal.ip = "192.168.12.16" checks if not all IP addresses match "192.168.12.16", meaning the rule is checking whether any IP address does not match "192.168.12.16".
  • all $e.principal.ip != "192.168.12.16" checks if all IP addresses do not match "192.168.12.16", meaning the rule is checking that no IP addresses match to "192.168.12.16".

Match section syntax

In the match section, list the match variables for group events before checking for match conditions. Those fields are returned with each match.

  • Specify what each match variable represents in the events section.
  • Specify the time range to use to correlate events after the over keyword. Events outside the time range are ignored.
  • Use the following syntax to specify the time range: <number><s/m/h/d> Where s/m/h/d means seconds, minutes, hours, and days respectively.
  • Minimum time you can specify is 1 minute.
  • Maximum time you can specify is 48 hours.

The following is an example of a valid match:

$var1, $var2 over 5m

This statement returns $var1 and $var2 (defined in the events section) when the rule finds a match. The time specified is 5 minutes. Events that are more than 5 minute apart are not correlated and therefore ignored by the rule.

Here is another example of a valid match:

$user over 1h

This statement returns $user when the rule finds a match. The time window specified is 1 hour. Events that are more than an hour apart are not correlated. The rule does not consider them to be a detection.

Here is another example of a valid match:

$source_ip, $target_ip, $hostname over 2m

This statement returns $source_ip, $target_ip, and $hostname when the rule finds a match. The time window specified is 2 minutes. Events that are more than 2 minutes apart are not correlated. The rule does not consider them to be a detection.

The following examples illustrate invalid match sections:

  • var1, var2 over 5m // invalid variable name
  • $user 1h // missing keyword

Sliding window

By default, YARA-L 2.0 rules are evaluated using hop windows. A time range of enterprise event data is divided into a set of overlapping hop windows, each with the duration specified in the match section. Events are then correlated within each hop window. With hop windows, it is impossible to search for events that happen in a specific order (for example, e1 happens up to 2 minutes after e2). An occurrence of event e1 and an occurrence of event e2 are correlated as long as they are within the hop window duration of each other.

Rules can also be evaluated using sliding windows. With sliding windows, sliding windows with the duration specified in the match section are generated when beginning or ending with a specified pivot event variable. Events are then correlated within each sliding window. This makes it possible to search for events that happen in a specific order (for example, e1 happens within 2 minutes of e2). An occurrence of event e1 and an occurrence of event e2 are correlated if event e1 occurs within the sliding window duration after event e2.

Specify sliding windows in the match section of a rule as follows:

<match-var-1>, <match-var-2>, ... over <duration> before|after <pivot-event-var>

The pivot event variable is the event variable that sliding windows are based on. If you use the before keyword, sliding windows are generated, ending with each occurrence of the pivot event. If the after keyword is used, sliding windows are generated beginning with each occurrence of the pivot event.

The following are examples of valid sliding window usages:

  • $var1, $var2 over 5m after $e1
  • $user over 1h before $e2

Outcome section syntax

In the outcome section, you must define a single variable name, called $risk_score. The value of $risk_score must be an integer that helps prioritize which detections are the most important to review.

You use conditional logic and aggregate functions to compute the value of $risk_score.

Notice that the example expression above includes the following syntax pattern to specify conditionals:

if(BOOL_CLAUSE, THEN_CLAUSE)
if(BOOL_CLAUSE, THEN_CLAUSE, ELSE_CLAUSE)

BOOL_CLAUSE must evaluate to a boolean value. A BOOL_CLAUSE expression takes a similar form as expressions in the events section. For example, it can contain:

  • UDM field names with comparison operator, such as $context.graph.entity.user.title = "Vendor"
  • variable name defined in the events section, such as $severity = "HIGH"
  • functions that return a boolean, such as re.regex($e.network.email.from, .*altostrat\.com)

The THEN_CLAUSE and ELSE_CLAUSE must be int literals.

If an ELSE_CLAUSE is not included, it evaluates to the default value. For example: if($e.field = "a", 5) is equivalent to if($e.field = "a", 5, 0)

You can read a conditional expression as "if BOOL_CLAUSE is true, then return THEN_CLAUSE, else return ELSE_CLAUSE".

The expression to compute the $risk_score value must be wrapped by an aggregate function. Chronicle supports the max, min, and sum functions.

The aggregate function is important when a rule includes a condition section that specifies multiple events must exist. For example, if your condition section is:

condition:
   #u > 2

and the risk_score computation has the line:

+ if($u.principal.hostname = "my-hostname").

Because there are multiple u events, an aggregate function operates on the various possible events.

Things to know when using the outcome section:

  • Variable name in the outcome section must be $risk_score.
  • $risk_score variable must store an integer.
  • Expression to compute $risk_score is mathematical statement which supports the addition and subtraction operators.
  • Expression can include the following:
    • Int literals
    • Conditional statements
  • You can only use the outcome section in multi-event rules, which are rules that have a match section.
  • The rule can contain an outcome section with a multi-event rule that evaluates UDM event data only. You do not need to include entity context data.
  • outcome section cannot include events that have not been defined in the events section. You can only correlate events that have already been correlated in the events section. Similarly, you cannot define a new placeholder variable that wasn't already defined in the events section.

You can find an example using the outcome section here. Also, please see here for details on detection deduping with the outcome section.

Condition section syntax

Count

The # character is a special character in the condition section. If it is used before any event or placeholder variable name, it represents the number of distinct events or values that satisfy all the events section conditions.

Syntax

In the condition section, specify the match condition over events and variables defined in the events section. List match predicates here, joined with the keyword and or or.

The following conditions are bounding conditions. They force the associated event variable to exist, meaning that at least one occurrence of the event must appear in any detection.

  • $var // equivalent to #var > 0
  • #var > n // where n >= 0
  • #var >= m // where m > 0

The following conditions are non-bounding conditions. They allow the associated event variable to not exist, meaning that it is possible that no occurrence of the event appears in a detection. This enables the making of non-existence rules, which search for the absence of a variable instead of the presence of a variable.

  • !$var // equivalent to #var = 0
  • #var >= 0
  • #var < n // where n > 0
  • #var <= m // where m >= 0

In the following example, the special character # on a variable (either the event variable or the placeholder variable) represents the count of distinct events or values of that variable:

$e and #port > 50 or #event1 > 2 or #event2 > 1 or #event3 > 0

The following non-existence example is also valid and evaluates to true if there are more than two distinct events from $event1, and zero distinct events from $event2:

#event1 > 2 and !$event2

The following are examples of invalid predicates:

  • $e, #port > 50 // incorrect keyword usage
  • $e or #port < 50 // or keyword not supported with non-bounding conditions

Options section syntax

In the options section, you can specify the options for the rule. Syntax for the options section is similar to that of the meta section. But a key must be one of predefined option names, and the value is not restricted to string type.

Currently, the only available option is allow_zero_values.

  • allow_zero_value — If set to true, matches generated by the rule can have zero values as match variable values. Zero values are given to event fields when they are left unpopulated. This option is set to false by default.

Following is the valid options section line:

  • allow_zero_values = true

Type checking

Chronicle performs type checking against your YARA-L syntax as you create rules within the interface. The type checking errors displayed help you to revise the rule in such a way as to ensure that it will work as expected.

The following are examples of invalid predicates:

// $e.target.port is of type integer which cannot be compared to a string.
$e.target.port = "80"

// "LOGIN" is not a valid event_type enum value.
$e.metadata.event_type = "LOGIN"