YARA-L 2.0 已知问题
本文档适用于希望调试规则逻辑并优化 YARA-L 2.0 执行的检测工程师。其中介绍了如何处理非标准引擎行为,例如字段取消嵌套、聚合中的笛卡尔积扩展以及最终一致性丰富。通过遵循这些方法,您可以防止逻辑错误导致结果值虚高或漏检。
YARA-L 2.0 使用的执行模型会在评估期间将重复字段展开为单独的事件行。由于此转换发生在引擎级别,因此引用多个重复字段或对无符号统一数据模型 (UDM) 类型执行算术运算需要语法解决方法,以避免编译器错误、运行时错误或不正确的结果集。本文档概述了这些技术限制以及解决这些限制的逻辑模式。
准备工作
在测试或修改 YARA-L 2.0 规则之前,请确保您的账号具有以下技术权限:
所需 IAM 角色
roles/chronicle.viewer(安全运维 Viewer):查看现有规则和检测元数据。roles/chronicle.editor(安全运维 Editor):修改规则逻辑并保存更改。
所需权限
chronicle.rules.runTest:需要对历史数据执行运行测试功能。chronicle.detections.get:在检测信息中心内检查非嵌套事件的输出。
主要术语
- UDM(统一数据模型):一种标准化架构,用于在整个平台上对所有提取的安全遥测数据进行结构化处理。
- 取消嵌套:在引擎级别将包含重复字段(数组)的单个 UDM 事件展开为多行。每一行都代表数组中的一个唯一元素,这可能会导致在规则评估期间行数成倍增加。
- T₀(初始运行):针对传入遥测数据首次执行规则。这发生在“流式传输”阶段,通常在后台丰富流程(例如 GeoIP 或 ASN 修正)完成之前。
通过重复字段展开进行结果汇总
如果规则引用了包含多个元素的事件变量中的重复字段,则每个元素都会拆分为单独的事件行。
例如,事件 $e 的重复字段 target.ip 中的两个 IP 地址会被拆分为两个 $e 实例,每个实例具有不同的 target.ip 值。
rule outbound_ip_per_app {
meta:
events:
$e.principal.application = $app
match:
$app over 10m
outcome:
$outbound_ip_count = count($e.target.ip) // yields 2.
condition:
$e
}
事件记录:unnest 之前和之后
本部分中的表格展示了如何将包含 IP 地址数组的单个事件转换为两条不同的记录。
取消嵌套之前
下表显示了在对重复字段进行非嵌套处理之前的事件记录:
| metadata.id | principal.application | target.ip |
|---|---|---|
aaaaaaaaa |
Google SecOps |
[192.0.2.20,192.0.2.28] |
取消嵌套后
下表显示了对重复字段进行非嵌套处理后的事件记录:
| metadata.id | principal.application | target.ip |
|---|---|---|
aaaaaaaaa |
Google SecOps |
192.0.2.20 |
aaaaaaaaa |
Google SecOps |
192.0.2.28 |
嵌套重复字段(笛卡尔积)
当规则引用嵌套在另一个重复字段中的重复字段(例如 security_results.action)时,系统会同时在两个级别(父级和子级)执行取消嵌套操作。这会产生所有元素的笛卡尔积。
在以下示例中,一个事件 $e 在 security_results 上有两个重复值,在 security_results.actions 上有两个重复值,这些值会被取消嵌套到四个实例中。
rule security_action_per_app {
meta:
events:
$e.principal.application = $app
match:
$app over 10m
outcome:
$security_action_count = count($e.security_results.actions) // yields 4.
condition:
$e
}
嵌套 unnesting 之前的事件记录
原始记录将操作存储在嵌套数组结构中。
| metadata.id | principal.application | security_results |
|---|---|---|
aaaaaaaaa |
Google SecOps |
[ { actions: [ ALLOW, FAIL ] },{ actions: [ CHALLENGE, BLOCK ] } ] |
嵌套取消嵌套后的事件记录
展开后,每个唯一操作都会变成单独的一行,这可能会导致非去重聚合中的计数不符合预期。
| metadata.id | principal.application | security_results.actions |
|---|---|---|
aaaaaaaaa |
Google SecOps |
允许 |
aaaaaaaaa |
Google SecOps |
未通过 |
aaaaaaaaa |
Google SecOps |
挑战 |
aaaaaaaaa |
Google SecOps |
屏蔽 |
对不相关字段的影响
如果规则引用一个或多个重复字段,且其父字段也是重复字段,则规则评估中的这种取消嵌套行为可能会产生意外的结果聚合。sum()、array() 和 count() 等非非重复聚合无法考虑由取消嵌套行为产生的同一事件中其他字段的重复值。
在以下示例中,事件 $e 只有一个主机名 (google.com),但结果 (hostnames) 会对同一事件 $e 的四个未嵌套实例进行汇总,每个实例都有一个重复的 principal.hostname 值。由于 security_results.actions 上的重复值被展开,此结果会产生四个主机名(而不是一个)。
rule security_action_per_app {
meta:
events:
$e.principal.application = $app
match:
$app over 10m
outcome:
$hostnames = array($e.principal.hostname) // yields 4.
$security_action_count = count($e.security_results.action) // yields 4.
condition:
$e
}
包含不相关字段的事件记录(未展开)
主机名是单个值,但它与重复的安全结果并列。
| metadata.id | principal.application | principal.hostname | security_results |
|---|---|---|---|
aaaaaaaaa |
Google SecOps |
google.com |
[ { action: [ ALLOW, FAIL ] },{ action: [ CHALLENGE, BLOCK ] } ] |
包含不相关字段的事件记录(在取消嵌套后)
主机名现在在四行中重复出现,导致 array() 函数将其提取了四次。
| metadata.id | principal.application | principal.hostname | security_results.action |
|---|---|---|---|
aaaaaaaaa |
Google SecOps |
google.com |
允许 |
aaaaaaaaa |
Google SecOps |
google.com |
未通过 |
aaaaaaaaa |
Google SecOps |
google.com |
挑战 |
aaaaaaaaa |
Google SecOps |
google.com |
屏蔽 |
针对 unnesting 行为的解决方法
为确保在发生取消嵌套时结果值准确无误,请使用所选聚合的非重复版本。以下函数会忽略通过取消嵌套创建的重复行:
max()min()array_distinct()count_distinct()
包含多个事件变量的结果聚合
如果规则包含多个事件变量,则检测中包含的每个事件组合在聚合中都有一个单独的项。例如,如果针对列出的事件运行以下示例规则:
events:
$e1.field = $e2.field
$e2.somefield = $ph
match:
$ph over 1h
outcome:
$some_outcome = sum(if($e1.otherfield = "value", 1, 0))
condition:
$e1 and $e2
event1:
// UDM event 1
field="a"
somefield="d"
event2:
// UDM event 2
field="b"
somefield="d"
event3:
// UDM event 3
field="c"
somefield="d"
系统会计算每种事件组合的总和,让您可以在结果值计算中使用这两个事件变量。计算中使用了以下元素:
1: $e1 = event1, $e2 = event2
2: $e1 = event1, $e2 = event3
3: $e1 = event2, $e2 = event1
4: $e1 = event2, $e2 = event3
5: $e1 = event3, $e2 = event1
5: $e1 = event3, $e2 = event2
这会导致潜在的最大总和为 6,即使 $e2 只能对应 3 个不同的事件。
这会影响总和、计数和数组。对于计数和数组,使用 count_distinct 或 array_distinct 可以解决此问题,但对于求和,没有解决方法。
表达式开头的括号
不支持以括号开头的表达式,这会在规则编辑器中触发解析错误。
语法无效
parsing: error with token: ")"
invalid operator in events predicate
以下示例会生成此类错误:
($event.metadata.ingested_timestamp.seconds -
$event.metadata.event_timestamp.seconds) / 3600 > 1
有效的语法变体
以下语法变体可返回相同的结果,但语法有效:
$event.metadata.ingested_timestamp.seconds / 3600 -
$event.metadata.event_timestamp.seconds / 3600 > 1
1 / 3600 * ($event.metadata.ingested_timestamp.seconds -
$event.metadata.event_timestamp.seconds) > 1
1 < ($event.metadata.ingested_timestamp.seconds -
$event.metadata.event_timestamp.seconds) / 3600
结果中的索引数组需要汇总
不允许在 outcome 部分中直接为重复字段编制数组索引。它需要一个临时占位变量。
outcome:
$principal_user_dept = $suspicious.principal.user.department[0]
临时解决方法
在 events 部分中将特定数组索引捕获到占位符变量中,然后在结果中引用该占位符。
events:
$principal_user_dept = $suspicious.principal.user.department[0]
outcome:
$principal_user_department = $principal_user_dept
OR 条件(不存在)
如果您在两个单独的事件变量之间应用 OR 条件,并且规则在不存在时匹配,则规则会成功编译,但可能会产生假正例检测。
例如,以下规则语法可以匹配具有 $event_a.field = "something" 的事件,即使它不应该匹配:
events:
not ($event_a.field = "something" **or** $event_b.field = "something")
condition:
$event_a and #event_b >= 0
临时解决方法
将不存在性检查拆分为针对每个变量的单独代码块,以保持逻辑完整性。
events:
not ($event_a.field = "something")
not ($event_b.field = "something")
condition:
$event_a and #event_b >= 0
使用无符号事件字段进行算术运算
如果您在算术(+、-、*)或模 (%) 运算中将整数字面量与无符号整数 UDM 字段一起使用,则会收到错误。标准整数字面量默认为有符号 64 位整数 (INT64),这与定义为无符号 64 位整数 (UINT64) 的 UDM 字段(例如 network.received_bytes、network.sent_bytes 和 principal.file.size)不兼容。
此严格类型限制适用于 events 和 outcome 部分,包括 sum() 或 max() 聚合和除数表达式。YARA-L 语法检查器会将有符号和无符号整数都视为通用 int 类型。因此,混合使用 UINT64 UDM 字段和 INT64 整数文字的规则可能会通过保存时语法验证,但在规则执行期间或点击运行测试时失败。当规则失败时,它会生成一个通用的“规则错误”(cannot complete [arithmetic/mod] operation between unsigned and signed integer)。
例如,以下 events 比较在保存时会通过语法验证,但在执行期间会失败,因为 $e.network.received_bytes (UINT64) 乘以有符号整数字面量 2 (INT64):
events:
$total_bytes = $e.network.total_bytes
$total_bytes = $e.network.received_bytes * 2
解决方法
您可以使用以下任一方法来解决此类型不匹配问题:
- 显式整数转换(
cast.as_int()或cast.as_uint()):使用cast.as_int()将无符号 UDM 字段转换为有符号整数,或使用cast.as_uint()将整数字面量转换为无符号整数。当您需要整数结果或使用仅接受整数操作数的模 (%) 运算符时,请使用此方法。 - 浮点强制转换:使用浮点字面量(例如
2.0或100.0)强制整数常量表现为float。在同一行中没有其他除法运算符的简单表达式中,您也可以使用除法运算,例如(2/1)。在已包含除法运算符 (/) 的表达式中,请使用浮点字面量或cast.as_int(),而不是(N/1)除法强制转换。同一行中的多个/字符可能会被解释为正则表达式字面量,从而导致编译期间出现解析错误或破坏规则编辑器中的语法突出显示。
events 部分中的权宜解决方法
使用 cast.as_int()、浮点字面量或除法强制转换来修复 events 部分中的错误:
使用
cast.as_int():events: $total_bytes = $e.network.total_bytes $total_bytes = cast.as_int($e.network.received_bytes) * 2使用浮点字面量:
events: $total_bytes = $e.network.total_bytes $total_bytes = $e.network.received_bytes * 2.0使用除法强制转换:
events: $total_bytes = $e.network.total_bytes $total_bytes = $e.network.received_bytes * (2/1)
outcome 部分中的权宜解决方法
以下 outcome 表达式在保存时不会出现语法错误,但在运行时会失败,因为 sum($e.network.sent_bytes) 和 sum($e.network.received_bytes) 返回的 UINT64 值会与有符号 INT64 字面量 100 和 1 组合:
outcome:
// Saves, but fails at runtime:
$upload_ratio_pct = sum($e.network.sent_bytes) * 100 / (sum($e.network.received_bytes) + 1)
使用浮点字面量或 cast.as_int() 重写表达式,以便其保存和执行时不会出错:
使用浮点数字面量:
outcome: $upload_ratio_pct = sum($e.network.sent_bytes) * 100.0 / (sum($e.network.received_bytes) + 1.0)使用
cast.as_int():outcome: $upload_ratio_pct = sum(cast.as_int($e.network.sent_bytes)) * 100 / (sum(cast.as_int($e.network.received_bytes)) + 1)
GeoIP 丰富和最终一致性
在初始丰富阶段(流式传输和延迟敏感),系统会优先考虑速度,而不是即时准确性,这可能会导致数据缺失和潜在的假正例。系统会继续在后台丰富数据,但在运行规则时,数据可能不可用。这是正常的最终一致性过程的一部分。
为防止因丰富延迟而导致误报,请在评估字段值之前明确检查该字段是否不为空。
例如,假设有以下规则事件:
$e.principal.ip_geo_artifact.network.asn = "16509" AND
$e.principal.ip_geo_artifact.location.country_or_region = "United Kingdom"
该规则依赖于以下事实:事件必须同时具有 $e.principal.ip_geo_artifact.network.asn = "16509" 和 $e.principal.ip_geo_artifact.location.country_or_region = "United Kingdom",这两个字段都是丰富字段。如果未及时完成丰富,规则将产生假正例。
为避免出现这种情况,最好对该规则进行如下检查:
$e.principal.ip_geo_artifact.network.asn != "" AND
$e.principal.ip_geo_artifact.network.asn = "16509" AND
$e.principal.ip_geo_artifact.location.country_or_region != "" AND
$e.principal.ip_geo_artifact.location.country_or_region = "United Kingdom"
此规则可排除由具有 ASN 16509 但位于英国境外的 IP 触发事件的可能性。这可以提高规则的整体精确度。
了解如何排查丰富化延迟时间问题。
问题排查
本部分概述了预期性能,并针对实际检测行为与测试结果不一致的常见问题提供了自助修复方法。
未来日期的活动
多事件规则旨在按相对于提取时间的时间顺序处理事件。如果您指定并激活多事件规则,该规则不会为具有未来时间戳的事件创建检测结果,例如,当 event.timestamp 的日期和时间设置在 ingest.timestamp 之后时。
丰富化延迟时间
Google SecOps 会优先考虑提取速度,以便尽快显示初始提醒。不过,后台丰富化流程(例如解析 GeoIP、ASN 或 UDM 元数据)遵循最终一致性模型。
初始运行 (T₀)
实时引擎可能会在后台丰富化完成之前评估规则。根据您的逻辑是否依赖于富化字段进行检测或排除,这可能会导致以下临时差异:
假负例(检测延迟):这是一种常见结果。如果某条规则依赖于某个丰富字段(例如
target.user.department == "Finance")来触发,而该字段为null,则该规则在初始运行期间不会匹配。假正例(排除项缺失):如果您的规则使用丰富字段来滤除已知正常活动(例如
NOT target.ip_geo_country == "US"),则该规则可能会触发假正例,因为“排除”数据尚未应用。
调整运行
这些后台运行会在延迟一段时间(例如 4 小时或 30 小时)后重新评估数据。这会“修正”检测状态,如下所示:
延迟检测:在 T₀ 时刻被判定为“假负例”的事件,在富化完成后现在会生成检测结果。
更正:任何 T₀ 假正例都会保留在系统中,但完全丰富的数据会在 UDM 查看器中显示,以便进行手动分类。
运行测试差异
Run Test 工具会处理已对账的历史数据。由于在您运行手动测试时,数据已完全丰富,因此您可以立即看到“校准”结果。这意味着,您将看不到在首次实际运行期间发生的 T₀ 假负例或基于排除项的假正例。
错误修复
请使用下表解决实时提醒与测试结果之间的差异。
| 问题 | 说明 | 可采取的修复措施 |
|---|---|---|
| 排除失败 | 尽管存在排除项(例如 != "ASN_123"),但规则仍会触发,因为该字段在初始运行期间为 null。 |
向事件部分添加非 null 检查,以确保在评估之前对数据进行丰富,例如:$e.principal.ip_geo_artifact.network.asn != ""
|
| 实际比赛与测试比赛 | 实时规则会触发提醒,但对相同数据运行测试时会显示“"No Results”。 |
添加 $e.field != "",用于检查所有丰富字段(GeoIP、ASN、File Path),以同步实时行为和历史行为。 |
| 缺少元数据 | 检测结果会显示在信息中心内,其中 GeoIP 或 File Path 字段为空。 |
对于 T0 运行,这是正常现象。如需解决此问题,请在规则时间表中添加 field != "" 检查或配置结算延迟,以便留出更多时间进行提取。
|
验证和测试
如需验证规则是否正确处理了延迟丰富,请执行以下操作:
找出假正例:找到您认为是假正例的检测结果。在检测类型列中,查看是否有
<span class="material-icons">lightbulb</span>图标。没有此图标的提醒来自主要运行,其中最常见的是丰富化延迟。更新规则逻辑:为逻辑中使用的所有丰富数据点添加
field != ""检查。
示例(文件路径):
$e.target.process.parent_process.file.full_path != ""测试和验证:
- 使用运行测试功能,确保您的逻辑仍与预期的历史数据一致。
- 验证在填充富化字段后,该规则现在是否仅在校准运行期间触发(或正确排除)。
如需了解详情,请参阅了解规则运行时间安排和为规则配置自定义时间表。
需要更多帮助?获得社区成员和 Google SecOps 专业人士的解答。