作者:来自 Elastic Stephen Brown
如何使用 Elasticsearch 和 NLP 检测和评估日志中的 PII。
简介:
分布式系统中高熵日志的普遍存在大大增加了 PII(Personally Identifiable Information - 个人身份信息)渗入我们日志的风险,这可能导致安全和合规性问题。这篇由两部分组成的博客深入探讨了使用 Elastic Stack 识别和管理此问题的关键任务。我们将探索使用 NLP(Natural Language Processing - 自然语言处理)和模式匹配来检测、评估并在可行的情况下从正在被提取到 Elasticsearch 的日志中删除 PII。
在本博客的第 1 部分中,我们将介绍以下内容:
- 回顾我们可用于管理日志中的 PII 的技术和工具
- 了解 NLP/NER 在 PII 检测中的作用
- 构建可组合的处理管道以检测和评估 PII
- 对日志进行采样并通过 NER 模型运行它们
- 评估 NER 模型的结果
在本博客的第 2 部分中,我们将介绍以下内容:
- 使用 NER 和 redact 处理器编辑 PII
- 应用字段级安全性来控制对未编辑数据的访问
- 增强仪表板和警报
- 生产注意事项和扩展
- 如何在传入或历史数据上运行这些流程
以下是我们将在 2 个博客中构建的总体流程:
本练习的所有代码均可在以下位置找到:https://github.com/bvader/elastic-pii。
工具和技术
我们将在本练习中使用四种通用功能。
- 命名实体识别检测 (NER)
- 模式匹配检测
- 日志采样
- 将采集管道作为可组合处理
命名实体识别 (Named Entity Recognition - NER) 检测
NER 是自然语言处理 (NLP) 的一个子任务,涉及识别非结构化文本中的命名实体并将其归类为预定义类别,例如:
- 人(Person):个人姓名,包括名人、政治家和历史人物。
- 组织(Organization):公司、机构和组织的名称。
- 位置(Location):地理位置,包括城市、国家和地标。
- 事件(Event):事件名称,包括会议、会议和节日。
对于我们的 PII 用例,我们将选择基本 BERT NER 模型 bert-base-NER,该模型可以从 Hugging Face 下载并作为训练模型加载到 Elasticsearch 中。
重要提示:NER/NLP 模型占用大量 CPU 资源,大规模运行成本高昂;因此,我们希望采用采样技术来了解日志中的风险,而无需通过 NER 模型发送完整的日志量。我们将在博客的第 2 部分讨论 NER 模型的性能和扩展性。
模式匹配检测
除了使用 NER,正则表达式模式匹配也是一种基于常见模式检测和编辑 PII 的强大工具。Elasticsearch redact 处理器就是为这种用例构建的。
日志采样
考虑到 NER 的性能影响以及我们可能会将大量日志导入 Elasticsearch 的事实,对传入的日志进行采样是有意义的。我们将构建一个简单的日志采样器来实现这一点。
摄取管道作为可组合处理
我们将创建多个管道,每个管道专注于特定功能,并创建一个主摄取管道来协调整个过程。
构建处理流程
日志采样 + 可组合采集管道
我们要做的第一件事是设置一个采样器来采样我们的日志。此采集管道仅采用 0(无日志)和 10000(所有日志)之间的采样率,允许低至 ~0.01% 的采样率,并使用 sample.sampled: true 标记采样日志。对日志的进一步处理将由 sample.sampled 的值驱动。sample.sample_rate 可以在此处设置或从编排管道 "passed in"。
该命令应从 Kibana -> Dev Tools 运行
以下三段代码可在此处找到。
日志采样器管道代码:
# logs-sampler pipeline - part 1
DELETE _ingest/pipeline/logs-sampler
PUT _ingest/pipeline/logs-sampler
{
"processors": [
{
"set": {
"description": "Set Sampling Rate 0 None 10000 all allows for 0.01% precision",
"if": "ctx.sample.sample_rate == null",
"field": "sample.sample_rate",
"value": 10000
}
},
{
"set": {
"description": "Determine if keeping unsampled docs",
"if": "ctx.sample.keep_unsampled == null",
"field": "sample.keep_unsampled",
"value": true
}
},
{
"set": {
"field": "sample.sampled",
"value": false
}
},
{
"script": {
"source": """ Random r = new Random();
ctx.sample.random = r.nextInt(params.max); """,
"params": {
"max": 10000
}
}
},
{
"set": {
"if": "ctx.sample.random <= ctx.sample.sample_rate",
"field": "sample.sampled",
"value": true
}
},
{
"drop": {
"description": "Drop unsampled document if applicable",
"if": "ctx.sample.keep_unsampled == false && ctx.sample.sampled == false"
}
}
]
}
现在,让我们测试日志采样器。我们将构建可组合管道的第一部分。我们将向 logs-generic-default 数据流发送日志。考虑到这一点,我们将创建 logs@custom 摄取管道,该管道将使用日志数据流框架自动调用以进行自定义。我们将添加一个额外的抽象级别,以便你可以将此 PII 处理应用于其他数据流。
接下来,我们将创建 process-pii 管道。这是核心处理管道,我们将在其中编排 PII 处理组件管道。在第一步中,我们将简单地应用采样逻辑。请注意,我们将采样率设置为 100,相当于日志的 10%。
process-pii 管道代码:
# Process PII pipeline - part 1
DELETE _ingest/pipeline/process-pii
PUT _ingest/pipeline/process-pii
{
"processors": [
{
"set": {
"description": "Set true if enabling sampling, otherwise false",
"field": "sample.enabled",
"value": true
}
},
{
"set": {
"description": "Set Sampling Rate 0 None 10000 all allows for 0.01% precision",
"field": "sample.sample_rate",
"value": 1000
}
},
{
"set": {
"description": "Set to false if you want to drop unsampled data, handy for reindexing hostorical data",
"field": "sample.keep_unsampled",
"value": true
}
},
{
"pipeline": {
"if": "ctx.sample.enabled == true",
"name": "logs-sampler",
"ignore_failure": true
}
}
]
}
最后,我们创建日志 logs@custom,它将根据正确的 data_stream.dataset 简单地调用我们的 process-pii 管道
logs@custom 管道代码
# logs@custom pipeline - part 1
DELETE _ingest/pipeline/logs@custom
PUT _ingest/pipeline/logs@custom
{
"processors": [
{
"set": {
"field": "pipelinetoplevel",
"value": "logs@custom"
}
},
{
"set": {
"field": "pipelinetoplevelinfo",
"value": "{{{data_stream.dataset}}}"
}
},
{
"pipeline": {
"description" : "Call the process_pii pipeline on the correct dataset",
"if": "ctx?.data_stream?.dataset == 'pii'",
"name": "process-pii"
}
}
]
}
现在,让我们测试一下,看看采样是如何进行的。
按照 数据加载附录 中的说明加载数据。让我们先使用示例数据,稍后我们将在本博客的末尾讨论如何使用传入或历史日志进行测试。
如果你使用 KQL 过滤器 data_stream.dataset : pii 和 Breakdown by sample.sampled 查看 Observability -> Logs -> Logs Explorer,你应该会看到细分约为 10%
此时,我们有一个可组合的采集管道,用于 "sampling" 日志。作为奖励,你还可以将此日志采样器用于任何其他用例。
NER 管道的加载、配置和执行
加载 NER 模型
你需要一个机器学习节点来运行 NER 模型。在本练习中,我们使用 AWS 上的 Elastic Cloud Hosted Deployment 和 CPU Optimized (ARM) 架构。NER 推理将在机器学习 AWS c5d 节点上运行。未来会有 GPU 选项,但今天我们将坚持使用 CPU 架构。
本练习将使用单个 c5d,配备 8 GB RAM 和 4.2 vCPU,最高可达 8.4 vCPU。
请参阅有关如何将 NLP 训练的模型导入 Elasticsearch 的官方文档,获取有关上传、配置和部署模型的完整说明。
获取模型的最快方法是使用 Eland Docker 方法。
以下命令将模型加载到 Elasticsearch 中,但不会启动它。我们将在下一步中执行此操作。
docker run -it --rm --network host docker.elastic.co/eland/eland \
eland_import_hub_model \
--url https://mydeployment.es.us-west-1.aws.found.io:443/ \
-u elastic -p password \
--hub-model-id dslim/bert-base-NER --task-type ner
部署并启动 NER 模型
一般来说,为了提高摄取性能,可以通过向部署添加更多分配来提高吞吐量。为了提高搜索速度,请增加每个分配的线程数。
为了扩展摄取,我们将专注于扩展已部署模型的分配。有关此主题的更多信息,请在此处获取。分配的数量必须小于每个节点可用的分配处理器(核,而不是 vCPU)。
部署并启动 NER 模型。我们将使用启动训练模型部署 API 执行此操作
我们将配置以下内容:
-
4 个分配以允许更多并行摄取
-
每个分配 1 个线程
-
0 个 Byes Cache,因为我们预计缓存命中率较低
-
8192 队列
Start the model with 4 Allocators x 1 Thread, no cache, and 8192 queue
POST _ml/trained_models/dslim__bert-base-ner/deployment/_start?cache_size=0b&number_of_allocations=4&threads_per_allocation=1&queue_capacity=8192
你应该会收到类似这样的回应:
{
"assignment": {
"task_parameters": {
"model_id": "dslim__bert-base-ner",
"deployment_id": "dslim__bert-base-ner",
"model_bytes": 430974836,
"threads_per_allocation": 1,
"number_of_allocations": 4,
"queue_capacity": 8192,
"cache_size": "0",
"priority": "normal",
"per_deployment_memory_bytes": 430914596,
"per_allocation_memory_bytes": 629366952
},
...
"assignment_state": "started",
"start_time": "2024-09-23T21:39:18.476066615Z",
"max_assigned_allocations": 4
}
}
NER 模型已部署并启动,可供使用。
以下摄取管道通过 inference处理器实现 NER 模型。
这里有大量代码,但现在只有两个感兴趣的项目。其余代码是条件逻辑,用于驱动一些额外的特定行为,我们将在将来仔细研究这些行为。
- 推理处理器通过我们之前加载的 ID 调用 NER 模型,并传递要分析的文本,在本例中是 message 字段,这是我们想要传递给 NER 模型以分析 PII 的 text_field
- script 处理器循环遍历消息字段并使用 NER 模型生成的数据将识别的 PII 替换为编辑的占位符。这看起来比实际更复杂,因为它只是循环遍历 ML 预测数组并用常量替换消息字符串中的预测,并将结果存储在新字段 redact.message 中。我们将在以下步骤中更仔细地研究这一点。
以下三段代码可在此处找到。
NER PII 管道:
# NER Pipeline
DELETE _ingest/pipeline/logs-ner-pii-processor
PUT _ingest/pipeline/logs-ner-pii-processor
{
"processors": [
{
"set": {
"description": "Set to true to actually redact, false will run processors but leave original",
"field": "redact.enable",
"value": true
}
},
{
"set": {
"description": "Set to true to keep ml results for debugging",
"field": "redact.ner.keep_result",
"value": true
}
},
{
"set": {
"description": "Set to PER, LOC, ORG to skip, or NONE to not drop any replacement",
"field": "redact.ner.skip_entity",
"value": "NONE"
}
},
{
"set": {
"description": "Set to PER, LOC, ORG to skip, or NONE to not drop any replacement",
"field": "redact.ner.minimum_score",
"value": 0.0
}
},
{
"set": {
"if" : "ctx.redact.message == null",
"field": "redact.message",
"copy_from": "message"
}
},
{
"set": {
"field": "redact.successful",
"value": true
}
},
{
"inference": {
"model_id": "dslim__bert-base-ner",
"field_map": {
"message": "text_field"
},
"on_failure": [
{
"set": {
"description": "Set 'error.message'",
"field": "failure",
"value": "REDACT_NER_FAILED"
}
},
{
"set": {
"field": "redact.successful",
"value": false
}
}
]
}
},
{
"script": {
"if": "ctx.failure_ner != 'REDACT_NER_FAILED'",
"lang": "painless",
"source": """String msg = ctx['message'];
for (item in ctx['ml']['inference']['entities']) {
if ((item['class_name'] != ctx.redact.ner.skip_entity) &&
(item['class_probability'] >= ctx.redact.ner.minimum_score)) {
msg = msg.replace(item['entity'], '<' +
'REDACTNER-'+ item['class_name'] + '>')
}
}
ctx.redact.message = msg""",
"on_failure": [
{
"set": {
"description": "Set 'error.message'",
"field": "failure",
"value": "REDACT_REPLACEMENT_SCRIPT_FAILED",
"override": false
}
},
{
"set": {
"field": "redact.successful",
"value": false
}
}
]
}
},
{
"remove": {
"if": "ctx.redact.ner.keep_result != true",
"field": [
"ml"
],
"ignore_missing": true,
"ignore_failure": true
}
}
],
"on_failure": [
{
"set": {
"field": "failure",
"value": "GENERAL_FAILURE",
"override": false
}
}
]
}
更新后的 PII 处理器管道,现在调用 NER 管道。
process-pii 管道代码:
# Updated Process PII pipeline that now call the NER pipeline
DELETE _ingest/pipeline/process-pii
PUT _ingest/pipeline/process-pii
{
"processors": [
{
"set": {
"description": "Set true if enabling sampling, otherwise false",
"field": "sample.enabled",
"value": true
}
},
{
"set": {
"description": "Set Sampling Rate 0 None 10000 all allows for 0.01% precision",
"field": "sample.sample_rate",
"value": 1000
}
},
{
"set": {
"description": "Set to false if you want to drop unsampled data, handy for reindexing hostorical data",
"field": "sample.keep_unsampled",
"value": true
}
},
{
"pipeline": {
"if": "ctx.sample.enabled == true",
"name": "logs-sampler",
"ignore_failure": true
}
},
{
"pipeline": {
"if": "ctx.sample.enabled == false || (ctx.sample.enabled == true && ctx.sample.sampled == true)",
"name": "logs-ner-pii-processor"
}
}
]
}
现在按照重新加载日志中的说明重新加载数据
结果
让我们看看 NER 处理后的结果。在带有 KQL 查询栏的日志资源管理器中,执行以下查询 data_stream.dataset : pii and ml.inference.entities.class_name : ("PER" and "LOC" and "ORG" )
日志资源管理器(Logs Explorer)应该看起来像这样,打开顶部消息查看详细信息。
NER 模型结果
让我们仔细看看这些字段的含义。
字段:ml.inference.entities.class_name
示例值:[PER、PER、LOC、ORG、ORG]
说明:NER 模型已识别的命名实体类的数组。
字段:ml.inference.entities.class_probability
示例值:[0.999、0.972、0.896、0.506、0.595]
说明:class_probability 是介于 0 和 1 之间的值,表示给定数据点属于某个类的可能性。数字越高,数据点属于命名类的概率就越高。这很重要,因为在下一篇博客中,我们可以决定要用来发出警报和编辑的阈值。你可以在此示例中看到它将 LOC 标识为 ORG,我们可以通过设置阈值来过滤掉/找到它们。
字段:ml.inference.entities.entity
示例值:[Paul Buck, Steven Glens, South Amyborough, ME, Costco]
描述:已识别的实体数组,与 class_name 和 class_probability 位置对齐。
字段:ml.inference.predicted_value
示例值:[2024-09-23T14:32:14.608207-07:00Z] log.level=INFO:订单 #4594 付款成功(用户:[Paul Buck](PER&Paul+Buck),david59@burgess.net)。电话:726-632-0527x520,地址:3713 [Steven Glens](PER&Steven+Glens), [South Amyborough](LOC&South+Amyborough), [ME](ORG&ME) 93580,订购自:[Costco](ORG&Costco)
描述:模型的预测值。
PII 评估仪表板
让我们快速浏览一下为评估 PII 数据而构建的仪表板。
要加载仪表板,请转到 Kibana -> Stack Management -> Saved Objects 并导入 pii-dashboard-part-1.ndjson 文件,该文件可在此处找到:
https://github.com/bvader/elastic-pii/elastic/pii-dashboard-part-1.ndjson
有关 Kibana 已保存对象的更多完整说明可在此处找到。
加载仪表板后,导航到它并选择正确的时间范围,你应该会看到如下所示的内容。它显示了诸如采样率、带有 NER 的日志百分比、NER 分数趋势等指标。我们将在本博客的第 2 部分中检查评估和操作。
总结和后续步骤
在博客的第一部分中,我们完成了以下工作。
- 回顾了我们可用于 PII 检测和评估的技术和工具
- 回顾了 NLP / NER 在 PII 检测和评估中的作用
- 构建了必要的可组合摄取管道以对日志进行采样并通过 NER 模型运行它们
- 回顾了 NER 结果并准备转到第二篇博客
在本博客的即将发布的第二部分中,我们将介绍以下内容:
- 使用 NER 和编辑处理器编辑 PII
- 应用字段级安全性来控制对未编辑数据的访问
- 增强仪表板和警报
- 生产注意事项和扩展
- 如何在传入或历史数据上运行这些流程
数据加载附录
代码
数据加载代码可在此处找到:
https://github.com/bvader/elastic-pii
$ git clone https://github.com/bvader/elastic-pii.git
如果不更改任何参数,这将在名为 pii.log 的文件中创建 10000 个随机日志,其中含有包含和不包含 PII 的日志。
编辑 load_logs.py 并设置以下内容:
# The Elastic User
ELASTIC_USER = "elastic"
# Password for the 'elastic' user generated by Elasticsearch
ELASTIC_PASSWORD = "askdjfhasldfkjhasdf"
# Found in the 'Manage Deployment' page
ELASTIC_CLOUD_ID = "deployment:sadfjhasfdlkjsdhf3VuZC5pbzo0NDMkYjA0NmQ0YjFiYzg5NDM3ZDgxM2YxM2RhZjQ3OGE3MzIkZGJmNTE0OGEwODEzNGEwN2E3M2YwYjcyZjljYTliZWQ="
然后运行以下命令。
$ python load_logs.py
重新加载日志
注意要重新加载日志,你只需重新运行上述命令即可。在此练习期间,你可以多次运行该命令,日志将被重新加载(实际上是再次加载)。新日志不会与之前的运行发生冲突,因为每次运行都会有一个唯一的 run.id,它会在加载过程结束时显示。
$ python load_logs.py