ES|QL LOOKUP JOIN实现日志去重现代方案

哈希、存储、关联:基于ES|QL LOOKUP JOIN的现代日志去重解决方案

数据保真度的高成本:当更多数据并非更好时

在网络安全和可观测性领域,领导层面临一个持续困境:对完全可见性的非协商要求与有限预算的严峻现实之间的冲突。这种冲突在PowerShell日志记录中表现得最为明显。

对于任何拥有大量Windows环境现代企业而言,PowerShell是管理和自动化的引擎。然而,其强大功能和普遍性使其成为攻击者的主要目标,他们利用它进行无文件恶意软件执行、横向移动和凭据盗窃。

为了应对这种威胁,安全框架要求进行全面日志记录,特别是PowerShell脚本块日志记录(事件ID 4104)。这些日志对于威胁狩猎至关重要,因为它们捕获了攻击者运行的确切去混淆代码。然而,这种安全要求可能造成数据管理危机。启用完整脚本块日志记录会生成大量事件,可能产生TB级数据,导致显著的存储和索引成本。

潜力:大幅降低存储

让我们量化潜在收益。考虑一个现实企业场景:标准的9KB PowerShell健康检查脚本在10,000台服务器上每10分钟运行一次。这每天生成1,440,000个事件。该技术对存储消耗的影响是变革性的。

指标优化前优化后影响
脚本文本存储/天12.96GB9KB减少99.99%
记录的总事件数144万144万(精简)+ 1(完整)事件元数据无损失
分析能力完整脚本文本可用按需提供完整脚本文本保真度零损失
安全规则覆盖完整完整(通过查找索引扫描进行调查)零盲点创建

策略:哈希、存储一次、按需查找

该解决方案的核心原则是智能数据去重。在任何大型企业中,同一组用于健康检查、应用程序监控和管理任务的PowerShell脚本每天执行数百万次。传统方法在每次执行时捕获这些脚本的完整多KB文本,导致大量数据冗余。

考虑您的日常通勤。您不需要每次旅行都有新的详细地图;只需要一个说明:走了通常路线。我们的日志记录策略应用相同的逻辑。我们不每次都存储完整的"地图"(脚本文本),而是存储一次,然后简单地为每次执行记录一个轻量级引用——哈希。

该架构建立在三个支柱上:

事件克隆(Logstash):作为多功能服务器端数据处理管道,Logstash适用于复杂路由。我们将使用其克隆过滤器为每个PowerShell脚本块日志事件创建飞行中副本。

通过哈希去重(摄取管道):Elasticsearch摄取管道将处理原始事件和克隆事件。它生成脚本文本的唯一哈希,作为去重键。管道从原始事件中剥离庞大的脚本文本,将克隆转换为最小化的"查找"文档,仅包含脚本文本、相关元数据和安全检测规则及其哈希。"查找"文档存储在单分片查找索引中,重复项会自动覆盖。

按需丰富(ES|QL):ES|QL是支持原生联接的强大管道查询引擎。在查询时,分析师可以使用LOOKUP JOIN命令无缝地将精简事件元数据(如时间戳、主机和用户)与查找索引中的完整脚本文本重新结合,按需提供完整上下文。

实施展示

先决条件

开始前,确保环境满足以下要求:

  • Elastic Stack版本8.18/9.0及更新版本
  • 启用PowerShell日志记录
  • Elastic Windows集成
  • Logstash部署
  • 了解数据特征

步骤1:设置查找索引

首先为查找索引创建模板,包括具有最小映射的组件模板和将index.mode设置为lookup的索引模板。

PUT _component_template/logs-windows.powershell_operational_lookup@package
{
  "template": {
    "settings": {
      "index": {
        "lifecycle": {
          "name": "logs-powershell_script_block"
        },
        "default_pipeline": "logs-windows.powershell_operational-3.0.0",
        "mapping": {
          "total_fields": {
            "limit": "1000"
          }
        }
      }
    },
    "mappings": {
      "properties": {
        "@timestamp": {
          "ignore_malformed": false,
          "type": "date"
        },
        "powershell": {
          "type": "object",
          "properties": {
            "file": {
              "type": "object",
              "properties": {
                "script_block_text": {
                  "analyzer": "powershell_script_analyzer",
                  "type": "text"
                },
                "script_block_hash": {
                  "ignore_above": 1024,
                  "type": "keyword"
                }
              }
            }
          }
        }
      }
    }
  }
}

步骤2:Logstash中的事件处理

在Logstash管道中拦截PowerShell脚本块日志(事件ID 4104),并使用克隆过滤器创建副本。

filter {
  if ([event][module] == "powershell" or [event][provider] == "Microsoft-Windows-PowerShell") and [event][code] == "4104" and [winlog][event_data][MessageTotal] == "1" {
    clone {
      clones => ["powershell_scriptblock_reduction"]
      ecs_compatibility => v8
    }
    if !("powershell_scriptblock_reduction" in [tags]) {
      mutate {
        update => { "[data_stream][namespace]" => "default_reduced" }
        add_field => {
          "[fields][duplicated]" => true
        }
      }
    }
  }
}

步骤3:路由到不同目的地

使用Logstash输出配置将事件路由到不同目的地。

output {
  if "powershell_scriptblock_reduction" in [tags]{
    elasticsearch {
      cloud_id => "..."
      index => "logs-windows.powershell_operational_lookup-default"
      action => "index"
      api_key => "${ES_API}"
      ssl_enabled => true
    }
  }
  else {
    elasticsearch {
      cloud_id => "..."
      data_stream => "true"
      api_key => "${ES_API}"
      ssl_enabled => true
    }
  }
}

步骤4:使用摄取管道处理

基于Elastic集成的摄取处理流程,两个事件都由自定义摄取管道处理,该管道使用条件逻辑根据在Logstash中添加的标志应用不同的处理器。

{
  "processors": [
    {
      "remove": {
        "ignore_missing": true,
        "field": "message",
        "if": "ctx.powershell?.file?.script_block_hash!= null && ctx.fields?.duplicated!= null && (ctx.fields?.duplicated == true || ctx.fields?.duplicated == 'true')"
      }
    },
    {
      "script": {
        "if": "ctx.tags!= null && ctx.tags.contains('powershell_scriptblock_reduction') && ctx.powershell?.file?.script_block_hash!= null",
        "source": "ctx._id = ctx.powershell.file.script_block_hash"
      }
    }
  ]
}

步骤5:使用ES|QL重建上下文

实施后,主数据流包含精简事件,查找索引保存唯一的非分段PowerShell脚本集。最后一步是在分析期间将这些上下文结合在一起。

FROM logs-windows.powershell_operational-default_reduced* 
| WHERE powershell.file.script_block_hash IS NOT NULL
| RENAME @timestamp AS original_timestamp, user.id AS original_user.id
| LOOKUP JOIN logs-windows.powershell_operational_lookup-default-2025.06.26-000004 ON powershell.file.script_block_hash
| RENAME original_timestamp AS @timestamp, original_user.id AS user.id

更智能日志记录的一瞥

这种"哈希、存储、关联"模式展示了一种打破安全可见性和数据成本之间僵局的强大方法。它应被视为任何具有庞大或重复数据的高容量日志源(如API有效负载或应用程序堆栈跟踪)的可重用蓝图。

对于希望将数据优化提升到新水平的团队,该技术可以与其他功能分层使用。在使用此方法移除高容量、重复内容后,可以在主数据流上启用logsdb索引模式(在9.0及更高版本的集群上默认启用)。
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)或者 我的个人博客 https://blog.qife122.com/
公众号二维码
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

### Flink Lookup Join 的使用方法及常见问题解决方案 Flink 中的 Lookup Join 是一种流与外部存储介质(如 Redis、MySQL、HBase 等)进行实时关联的操作。它允许流式数据通过实时查找的方式访问外部维表,从而丰富流中的数据内容。以下是 Lookup Join 的使用方法及常见问题的解决方案。 #### 1. 使用方法 Lookup Join实现通常基于 Flink SQL 或 DataStream API。以下为具体实现方式: - **Flink SQL 实现**: 在 Flink SQL 中,可以通过定义一个外部表并将其作为维表参与 Join 操作来实现 Lookup Join[^3]。例如,假设需要将 Kafka 流中的数据与 MySQL 中的维表进行关联,可以按照以下步骤操作: ```sql -- 定义 Kafka 表 CREATE TABLE kafka_table ( id BIGINT, name STRING, ts TIMESTAMP(3), WATERMARK FOR ts AS ts - INTERVAL '5' SECOND ) WITH ( 'connector' = 'kafka', 'topic' = 'user_topic', 'properties.bootstrap.servers' = 'localhost:9092', 'format' = 'json' ); -- 定义 MySQL 维表 CREATE TABLE mysql_table ( id BIGINT, address STRING, PRIMARY KEY (id) NOT ENFORCED ) WITH ( 'connector' = 'jdbc', 'url' = 'jdbc:mysql://localhost:3306/db', 'table-name' = 'dim_address', 'username' = 'root', 'password' = '123456' ); -- 执行 Lookup Join SELECT k.id, k.name, m.address FROM kafka_table AS k JOIN mysql_table FOR SYSTEM_TIME AS OF k.ts AS m ON k.id = m.id; ``` - **DataStream API 实现**: 如果使用 DataStream API,则可以通过异步 I/O 或缓存机制实现 Lookup Join。以下是一个简单的示例代码: ```java import org.apache.flink.api.common.functions.RichMapFunction; import org.apache.flink.streaming.api.datastream.DataStream; import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; public class LookupJoinExample { public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); // 模拟 Kafka 数据流 DataStream<String> kafkaStream = env.fromElements("1", "2", "3"); // 使用 RichMapFunction 实现 Lookup Join DataStream<String> result = kafkaStream.map(new RichMapFunction<String, String>() { @Override public String map(String id) throws Exception { // 模拟从 MySQL 查询地址 String address = queryAddressFromMySQL(id); return id + ": " + address; } private String queryAddressFromMySQL(String id) { // 实际查询逻辑 return "Address-" + id; } }); result.print(); env.execute("Lookup Join Example"); } } ``` #### 2. 常见问题及解决方案 - **数据库连接数不足**: 当大量 Task 并行访问数据库时,可能导致连接数耗尽。为解决此问题,可以采取以下措施: - 使用连接池控制最大并发连接数[^1]。 - 为查询量大的场景准备只读分库或分片。 - 使用专门的缓存层(如 Redis 或 HBase)代替直连数据库[^1]。 - **性能优化**: 在高吞吐场景下,Lookup Join 的性能可能会成为瓶颈。以下是一些优化策略: - **按 key 分桶 + Local Cache**:通过按 key 分桶的方式,让大多数数据的维表关联访问本地缓存,减少外部存储的访问次数[^4]。 - **异步访问外存**:利用 DataStream API 的异步算子,通过线程池同时多次请求维表外部存储,提升吞吐量。 - **批量访问外存**:使用外部存储的批量处理能力(如 Redis 的 Pipeline),攒一批数据后一次性发送请求,降低网络开销[^4]。 - **数据一致性问题**: Lookup Join 中的维表数据可能发生变化,导致流中数据无法关联到最新的维度信息。为确保数据一致性,可以选择支持增量更新的维表,并结合 Flink 的 Checkpoint 机制定期刷新缓存[^3]。 #### 3. 注意项 - 只有支持 lookup source connector 类型的表才可以用于 Lookup Join,例如 JDBC connector[^3]。 - Lookup Join 的性能高度依赖于外部存储的响应速度和网络延迟。因此,在设计系统时应充分考虑这些因素[^4]。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值