如何在同一 Sentinel workbook 中保存查询结果并供后续查询复用?
可以复用多列结果,具体方式取决于后续如何使用:
- 多个组件展示同一份结果,或关联已经加载的结果:使用 Workbooks 的
Merge数据源,其中Duplicate table可以引用已有查询结果。 - 后续还需要执行 KQL,与
UrlClickEvents等后端表关联:可以将较小的初始结果序列化为 JSON,存入查询驱动的文本参数,再在后续查询中还原。
Workbooks 不支持通过 let 在不同查询组件之间共享后端临时表。这里的“只执行一次”是指避免每个下游组件重复提交初始查询;刷新、调整时间范围等操作仍可能触发重新执行。
为什么 let 无法跨查询复用
在 Sentinel workbook 中,每个日志查询组件都会提交自己的查询。组件 A 定义的 let Base = ... 不在组件 B 的查询作用域内。
let 只是给表达式命名,并不保证缓存计算结果。materialize() 可以在一次查询执行期间保存表格表达式的结果,但其他查询组件无法访问这份缓存。参见 Microsoft 的 let 说明和 materialize() 说明。
方案一:通过 Merge 复用已有多列结果
如果初始查询已经返回所需数据,可以直接使用 Merge,无需将结果放入下拉参数。
- 在 workbook 中添加初始日志查询。
- 在该组件的
Advanced settings中设置名称,例如IncidentBase。 - 添加另一个查询组件,将
Data source设置为Merge。 - 选择
Add Merge。 - 将
Merge Type设置为Duplicate table,将Table设置为IncidentBase。 - 运行
Run Merge,再配置列和可视化方式。
其他组件也可以引用 IncidentBase。Microsoft 的查询结果复用示例采用的就是这种方式。
如果另一个组件已经查出了邮件数据,也可以通过 Merge 按共同字段关联两份结果。Merge 支持多种 join、Union 和 Duplicate table,可处理多列数据,不限于一两列。参见支持的合并类型。
Duplicate table 只能引用 workbook 内的结果,不会在 Log Analytics 中创建名为 IncidentBase 的表。因此,不能在另一个日志查询中直接写:
IncidentBase
| join kind=inner UrlClickEvents on NetworkMessageId
Merge 关联的是已经返回的数据。如果先从后端读取大量点击记录,再在 workbook 中过滤,就可能遇到数据量过大或结果截断的问题。如果需要先按消息 ID 缩小后端查询范围,更适合使用下面的参数方案。
方案二:将小型多列结果打包成 JSON 参数
参数接收的一个值可以包含多个业务字段。将多行、多列数据打包为 JSON 数组后,参数查询只需返回一行一列。
Workbooks 的 Text 参数支持通过查询获取默认值,也可以用作供其他组件使用的内部变量。参见文本参数文档。
1. 准备初始结果并打包
先在现有初始查询中完成 SecurityIncident 筛选和实体提取,输出后续需要的列。
以下示例使用演示数据。实际使用时,将 datatable(...) 替换为你的初始查询,并保留对应输出列:
datatable(
IncidentName:string,
NetworkMessageId:string,
Url:string
)
[
"incident-demo-1", "message-demo-1", "https://example.com/a",
"incident-demo-2", "message-demo-2", "https://example.org/b"
]
| summarize Rows = make_list(
bag_pack(
"IncidentName", IncidentName,
"NetworkMessageId", NetworkMessageId,
"Url", Url
)
)
| project SeedRows = tostring(Rows)
打包后,原本的三列数据变成一个包含对象数组的字符串。后续需要 Recipient、Subject 等字段时,也可以将它们显式加入打包对象。
不能直接假设 SecurityIncident 中有 NetworkMessageId 或邮件实体列。该表包含 AlertIds 等字段,具体邮件信息需要根据实际告警和实体结构获取。参见 SecurityIncident 表结构。
2. 创建查询驱动的文本参数
在 workbook 顶部添加参数:
Parameter name:SeedRowsParameter type:TextGet data from:Query
选择目标工作区,将上面的打包查询设为参数取值查询,并确认结果为一行一列。
初始查询只放在这个参数查询中执行。顶部的事件列表也应从 SeedRows 还原数据,避免另设一个组件重复执行初始查询。
3. 在后续查询中还原多列结果
let Base =
print Rows = parse_json(
base64_decode_tostring('{SeedRows:base64}')
)
| mv-expand Row = Rows
| project
IncidentName = tostring(Row.IncidentName),
NetworkMessageId = tostring(Row.NetworkMessageId),
Url = tostring(Row.Url);
Base
{SeedRows:base64} 是 Workbooks 支持的参数格式。先编码再解码,可以避免将含有引号的 JSON 原文直接嵌入 KQL 字符串时产生转义问题。Base64 不提供加密。参见参数格式说明。
各下游查询都可以使用这段还原代码,处理参数中的数据,无需重新运行原来的 SecurityIncident 查询。
4. 查询相同消息的全部 URL 点击事件
保留上面的 let Base = ...;,将最后一行 Base 替换为:
UrlClickEvents
| where Timestamp >= ago(7d)
| where isnotempty(NetworkMessageId)
| join kind=leftsemi (
Base
| where isnotempty(NetworkMessageId)
| distinct NetworkMessageId
) on NetworkMessageId
| project Timestamp, NetworkMessageId, AccountUpn, Url, ActionType
| order by Timestamp desc
这里使用 leftsemi,只保留消息 ID 匹配的点击记录,避免初始结果中的重复消息 ID 放大关联结果。
运行此示例需要查询环境能够访问 UrlClickEvents。该表是否可用取决于相关服务和数据接入情况,其中的 NetworkMessageId 字段用于标识包含被点击链接的邮件。参见 UrlClickEvents 表说明。
5. 展示去重后的域名
如果需要提取初始点击 URL 中的域名,在还原 Base 后使用:
Base
| extend Domain = tolower(tostring(parse_url(Url).Host))
| where isnotempty(Domain)
| distinct Domain
| order by Domain asc
如果需要提取相同消息的全部点击事件中的域名,则在上一步关联结果之后进行同样的提取和去重。
parse_url() 的 Host 返回主机名,可能包含子域名,不会自动计算可注册根域名。输入也必须符合绝对 URL 的格式。参见 parse_url() 文档。
使用时需要注意什么
JSON 参数方案适合经过筛选的小型结果集。每个后续 KQL 查询都会携带并解析这份 JSON。如果关联只需要 NetworkMessageId,优先传递去重后的 ID 集合,以减少传输量。
make_list() 有元素数量上限,查询结果和请求大小也各有限制,因此这种方式无法用作任意规模的表格缓存。参见 make_list() 限制。
查询驱动的文本参数通过查询获取默认值。时间范围或筛选条件变化后,需要确认参数是否按预期重新取值,避免下游继续使用旧数据。
对于 mailMessage 列表,应在初始查询中保留所需的实体字段,或使用传递出的关联键查询实际实体来源;不要将实体类型名称直接当作日志表名。
如果只分析用户选中的事件,也可以从网格导出参数。将 Field to export 留空可导出整行 JSON,但导出的只是选中项,不能等同于自动保存全部初始结果。参见网格参数导出说明。
备注:内容仅供参考。
