<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Stephen Brown - Elastic Observability Labs]]></title>
    <description><![CDATA[Trusted security news & research from the team at Elastic.]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Stephen Brown - Elastic Observability Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltad972c1c27dbefc6/6a88d9782904ea5e8511d473/observability-labs-thumbnail.png</url>
      <link>https://www.elastic.co/observability-labs/author/stephen-brown</link>
    </image>
    <link>https://www.elastic.co/observability-labs/author/stephen-brown</link>
    <atom:link href="https://www.elastic.co/observability-labs/rss/author/stephen-brown.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Sat, 12 Sep 2026 01:05:17 GMT</lastBuildDate>
  <item>
    <title><![CDATA[From five dashboards to one prompt: how we built an APM health monitor with Elastic Agent Builder]]></title>
    <description><![CDATA[Five ES|QL tools score latency, errors, throughput and dependencies to find the root cause, so you don't dashboard-hop during an APM incident.]]></description>
    <content:encoded><![CDATA[<p>Using Elastic Agent Builder, we built an agent that answers one question: is your service healthy? </p>
<p>This agent fans out to five ES|QL queries over your existing traces-* data and answers Red, Yellow or Green with the root cause attached, in one response, without switching dashboards or correlating anything by hand. It runs on Elastic Agent Builder, tested on Elasticsearch and Kibana 9.3, and one deployment covers every service in your fleet with no per-service setup. </p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt45cee36cefac285f/6a7f02a0227b1cec34598160/image5.png" alt="" /></p>
<p>Here's how it's built</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt06ae6cd3234ce2f8/6a7f02a305b7b56bea18b4ab/image2.png" alt="" /></p>
<h2 id="ismyservicehealthyonequestiononeagentoneanswer">Is my service healthy? One question, one agent, one answer</h2>
<p>There is a question every engineer dreads during an incident: "Is the service healthy?"</p>
<p>It sounds simple. It is not. Answering it properly means switching between dashboards, firing off multiple queries, correlating latency spikes with error logs, and checking whether downstream dependencies are contributing to the problem, all under pressure and often at an inconvenient hour.</p>
<p>What if that entire investigation collapsed into a single conversation?</p>
<p>That is what we set out to build: an AI agent that behaves like a knowledgeable SRE sitting beside you. It knows which questions to ask, knows how to query your APM data, and returns a clear Red, Yellow, or Green health verdict with context and recommendations. We call it the APM Service Health Monitor, and it runs entirely on Elastic.</p>
<h2 id="whyapmhealthisaderivedsignalnotasinglemetric">Why APM health is a derived signal, not a single metric</h2>
<p>Here is the thing about APM data: you already have everything you need. Latency percentiles, error rates, throughput, and span-level dependency traces are all sitting in your <code>traces-*</code> indices, indexed and ready.</p>
<p>The missing piece is not data. It is the reasoning layer that connects those signals into a coherent picture.</p>
<p>A p95 latency of 890 ms means very little on its own. But a p95 latency of 890 ms that runs 38% above the 24-hour baseline, alongside a 6% error rate on your postgres-primary dependency? That is a story. That is a Red, and it tells an engineer exactly where to look.</p>
<p>The APM Service Health Monitor encodes that reasoning (threshold logic, trend comparison, and dependency blast-radius analysis) into an Elastic AI agent that runs it on demand, for any service, anytime.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt60839b56219ee418/6a7f02a6448e4e84155c0260/image4.png" alt="APM Service Health Monitor agent at the centre, fanning out to five ES|QL tools querying traces-*" />
<em>APM Service Health Monitor agent at the centre, fanning out to five ES|QL tools querying traces-*</em></p>
<h2 id="howdoestheapmservicehealthmonitorarchitecturework">How does the APM Service Health Monitor architecture work?</h2>
<p>The agent is registered in Elastic Agent Builder as a chat-type agent wired to five ES|QL-powered tools. Each tool does one thing precisely. The agent orchestrates, compares, and reasons across all five.</p>
<pre><code>POST kbn:/api/agent_builder/agents
{
  "id": "apm_service_health_agent",
  "type": "chat",
  "name": "APM Service Health Monitor",
  ...
}
</code></pre>
<p>The APM Service Health Monitor sits at the center, fanning out to five ES|QL tools that query <code>traces-*</code>.</p>
<p>Here is how the tool layer is composed.</p>
<h3 id="tool1apm_metrics_overview_toolthe24hourbaseline">Tool 1: apm_metrics_overview_tool, the 24-hour baseline</h3>
<p>Every health assessment starts with a baseline. This tool computes the aggregate picture for a service over the last 24 hours (average latency, p95 and p99 latency, error rate, and throughput) in a single ES|QL query:</p>
<pre><code>FROM traces-*
| WHERE service.name == ?service
| WHERE transaction.type == "request"
| WHERE @timestamp &gt;= NOW() - 24 hours
| EVAL is_error = CASE(event.outcome == "failure", 1, 0)
| STATS
    avg_latency_ms  = AVG(transaction.duration.us / 1000),
    p95_latency_ms  = PERCENTILE(transaction.duration.us / 1000, 95),
    p99_latency_ms  = PERCENTILE(transaction.duration.us / 1000, 99),
    error_rate      = 100.0 * SUM(is_error) / COUNT(*),
    throughput_rps  = COUNT(*) / (24*3600)
</code></pre>
<p>This snapshot becomes the anchor. Every trend tool that follows compares its latest readings back to these numbers, so the agent always has a reference point, not just a raw value.</p>
<h3 id="tool2apm_latency_trend_toolperformanceovertime">Tool 2: apm_latency_trend_tool, performance over time</h3>
<p>Averages hide inflection points. The latency trend tool buckets p95, p75, and average latency into 5-minute intervals across the last 24 hours, giving the agent a time-series view of performance:</p>
<pre><code>| STATS
    avg_latency_ms = AVG(transaction.duration.us / 1000),
    p95_latency_ms = PERCENTILE(transaction.duration.us / 1000, 95),
    p75_latency_ms = PERCENTILE(transaction.duration.us / 1000, 75)
  BY time_bucket = DATE_TRUNC(5 minutes, @timestamp)
| SORT time_bucket ASC
</code></pre>
<p>The agent takes the most recent 5-minute bucket's <code>p95_latency_ms</code> and compares it against the <code>p95_latency_ms</code> from <code>apm_metrics_overview_tool</code>, the 24-hour rolling average. That percentage difference is what gets scored Green, Yellow, or Red.</p>
<h3 id="tool3apm_error_trend_toolfailureshapedetection">Tool 3: apm_error_trend_tool, failure shape detection</h3>
<p>Error rate is directional. A gradual rise from 0.5% to 1.2% tells a different story than a sudden spike to 8%. The error trend tool captures this shape with 5-minute buckets:</p>
<pre><code>| EVAL is_error = CASE(event.outcome == "failure", 1, 0)
| STATS
    total_requests = COUNT(*),
    error_count    = SUM(is_error),
    error_rate     = 100.0 * SUM(is_error) / COUNT(*)
  BY time_bucket = DATE_TRUNC(5 minutes, @timestamp)
</code></pre>
<p>The agent reads the most recent bucket's <code>error_rate</code> from this trend and scores it directly against the fixed thresholds (below 1% Green, 1 to 5% Yellow, above 5% Red).</p>
<h3 id="tool4apm_throughput_trend_tooltrafficasahealthsignal">Tool 4: apm_throughput_trend_tool, traffic as a health signal</h3>
<p>Throughput is underrated as a health indicator. A 40% drop in requests per second is itself an incident. It can mean a deployment regression, a misconfigured load balancer, or a silent upstream failure.</p>
<pre><code>| STATS requests_count = COUNT(*)
  BY time_bucket = DATE_TRUNC(5 minutes, @timestamp)
| SORT time_bucket ASC
</code></pre>
<p>The agent normalizes each 5-minute bucket's request count to a per-second rate and compares it against <code>throughput_rps</code> from <code>apm_metrics_overview_tool.</code>. The same 10% and 30% drift thresholds apply. A service that is suspiciously quiet is flagged just as quickly as one that is on fire.</p>
<h3 id="tool5apm_dependency_health_tooltheblastradiusview">Tool 5: apm_dependency_health_tool, the blast-radius view</h3>
<p>The <code>apm_dependency_health_tool</code> is often the one that finds what the others cannot surface directly. A service can look internally healthy while a downstream database or external API silently accumulates failures. This tool maps every dependency via <code>span.destination.service.resource</code> and computes its error rate from span data:</p>
<pre><code>FROM traces-*
| WHERE processor.event == "span"
  AND span.destination.service.resource IS NOT NULL
| EVAL is_error = CASE(event.outcome == "failure", 1, 0)
| STATS
    total_calls  = COUNT(*),
    failed_calls = SUM(is_error),
    error_rate   = 100.0 * SUM(is_error) / COUNT(*)
  BY dependency_name = span.destination.service.resource
| SORT error_rate DESC
</code></pre>
<p>Unlike the other four tools, dependency health is scored independently. Each dependency's <code>error_rate</code> is compared directly against fixed thresholds (1% or below Green, above 1% Yellow, above 5% Red), with no baseline comparison needed.</p>
<h2 id="howthehealthscoringworks">How the health scoring works</h2>
<p>Once all five tools return their data, the agent computes a health state for each metric in a single reasoning step. There is no polling loop and no intermediate storage. Here is exactly what gets compared to what:</p>
<p>| Metric | Tool providing the value | Compared against | How scored |
| :---- | :---- | :---- | :---- |
| Latency (p95) | <code>apm_latency_trend_tool</code> | 24h p95 from <code>apm_metrics_overview_tool</code> | &lt; 10% Green · 10–30% Yellow · &gt; 30% Red|
| Error rate | <code>apm_error_trend_tool</code> | Fixed thresholds + 24h <code>error_rate</code> from overview | &lt; 1% Green · 1–5% Yellow · &gt; 5% Red |
| Throughput | <code>apm_throughput_trend_tool</code> | 24h <code>throughput_rps</code> from <code>apm_metrics_overview_tool</code> | &lt; 10% Green · 10–30% Yellow · &gt; 30% Red|
| Each dependency | <code>apm_dependency_health_tool</code> | Fixed thresholds only (no baseline) | All ≤1% Green · Any &gt;1% Yellow · Any &gt;5% Red |</p>
<p>The overall verdict follows a single rule: <strong>the worst individual metric wins</strong>.</p>
<pre><code>Any metric is Red                → Overall = 🔴 Red
Any metric is Yellow (no Red)   → Overall = 🟡 Yellow
All metrics are Green            → Overall = 🟢 Green
</code></pre>
<p>The Red/Yellow/Green scoring logic lives in the agent's natural-language instructions. It is readable, auditable, and adjustable without touching code or redeploying infrastructure.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte61fab3108287137/6a7f02a9eab5be1d6e20a270/image1.png" alt="Four metric columns - Latency, Error Rate, Throughput, Dependencies - converging into Red/Yellow/Green outcome states" />
<em>Four metric columns (Latency, Error Rate, Throughput, Dependencies) converging into Red/Yellow/Green outcome states</em></p>
<h2 id="deployingtheapmservicehealthmonitorinagentbuilder">Deploying the APM Service Health Monitor in Agent Builder</h2>
<p>Registration follows Elastic's standard Agent Builder API pattern, tested against Elasticsearch 9.3 and Kibana 9.3. Register the tools first, then wire the agent to all five tool IDs:</p>
<pre><code># Step 1: Register the five ES|QL tools
POST kbn:/api/agent_builder/tools   # apm_metrics_overview_tool
POST kbn:/api/agent_builder/tools   # apm_latency_trend_tool
POST kbn:/api/agent_builder/tools   # apm_error_trend_tool
POST kbn:/api/agent_builder/tools   # apm_throughput_trend_tool
POST kbn:/api/agent_builder/tools   # apm_dependency_health_tool

# Step 2: Register the agent with tools wired in
POST kbn:/api/agent_builder/agents  # apm_service_health_agent

# Step 3: Verify the agent is live
GET kbn:/api/agent_builder/agents/apm_service_health_agent
</code></pre>
<p>Each tool is parameterized with a service binding, so the same agent serves every service in your fleet with no per-service configuration. The queries target <code>traces-*</code> with cross-cluster wildcard support, so a single deployment covers multi-cluster environments out of the box.</p>
<h2 id="tryitasktheagentaboutyourservice">Try it: ask the agent about your service</h2>
<p>Once deployed, open the agent in Kibana and type this:</p>
<pre><code>What is the health of my checkout-service in the last 24 hours?
</code></pre>
<p>The agent fans out to all five tools, computes the metric states, applies the health logic in one pass, and responds with a structured report.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte61fab3108287137/6a7f02a9eab5be1d6e20a270/image1.png" alt="Kibana chat card: checkout-service · Last 24h · per-metric table with Red/Yellow/Green status pills · Overall: Red" />
<em>Kibana chat card: checkout-service · Last 24h · per-metric table with Red/Yellow/Green status pills · Overall: 🔴 Red</em></p>
<p>The agent identified the root cause without any manual correlation: postgres-primary is failing 6.1% of its calls, and that is cascading directly into the p95 latency spike. No dashboard pivoting, no manual ES|QL. One prompt, full situational awareness.</p>
<p>You can continue the conversation with follow-up questions in the same session:</p>
<ul>
<li>Which other services depend on postgres-primary?</li>
<li>How does this compare to yesterday's health?</li>
<li>Show me the error trend for the last 6 hours only.</li>
</ul>
<p>The agent invokes the appropriate tools for each follow-up, keeping the full context of the original health assessment in view.</p>
<h2 id="whyagentbuilderandesqlaretherightstackforthis">Why Agent Builder and ES|QL are the right stack for this</h2>
<p>A few deliberate choices made this design work cleanly.</p>
<h3 id="esqlasthequerylayer">ES|QL as the query layer</h3>
<p>ES|QL's pipe-based syntax makes each tool query readable, testable, and independently verifiable. The <code>DATE_TRUNC</code> bucketing for trends, the PERCENTILE aggregations for latency, and the span.destination.service.resource grouping for dependencies are precise, auditable queries. Run any of them directly in Kibana Dev Tools and you will see exactly what the agent sees.</p>
<h3 id="narrowstatelesstools">Narrow, stateless tools</h3>
<p>Each tool does one thing and returns structured data. The agent provides the orchestration and reasoning. Adding a new metric dimension means registering one new tool and updating the agent instructions. Nothing else changes.</p>
<h3 id="instructionsasarunbook">Instructions as a runbook</h3>
<p>The agent's health logic is expressed in natural language inside its configuration. Anyone on your team can read it, adjust it without a code deploy, and audit it in full from the Agent Builder UI in Kibana.</p>
<h2 id="whatsnextfortheapmservicehealthmonitor">What's next for the APM Service Health Monitor</h2>
<p>The APM Service Health Monitor is a foundation, not a ceiling. Natural extensions include:</p>
<ul>
<li><p><strong>Alert-triggered health checks:</strong> wire the agent to fire automatically when an anomaly detection rule triggers, attaching the health summary directly to the alert notification.</p></li>
<li><p><strong>Deployment correlation:</strong> integrate change event data so the agent can identify whether a Red status started after a specific deployment.</p></li>
<li><p><strong>SLO-aware thresholds:</strong> replace fixed percentage thresholds with per-service error budget consumption, so Red, Yellow, and Green reflect actual business impact against defined SLOs.</p></li>
<li><p><strong>Cross-service traversal:</strong> extend the dependency tool to recursively assess upstream and downstream services, building a full topology view from a single query.</p></li>
</ul>
<h2 id="requirementsanddeployment">Requirements and deployment</h2>
<p>Requirements:</p>
<ul>
<li>Elasticsearch 9.3 and Kibana 9.3 (tested version).</li>
<li><a href="https://www.elastic.co/docs/solutions/search/agent-builder/get-started">Agent Builder</a> enabled in Kibana.</li>
<li>APM data flowing into <code>traces-*</code> indices via any Elastic APM agent.</li>
<li><a href="https://www.elastic.co/subscriptions">Elastic Enterprise license</a> is required to utilize Agent Builder and <a href="https://www.elastic.co/search-labs/blog/esql-cross-cluster-search">ES|QL cross-cluster search</a>.</li>
</ul>
<p>The full agent and tool definitions are available in <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/observability-labs/elastic-agent-apm-health-monitor">this repository</a>. Once your traces are indexed, all five ES|QL tools work against your data with no modification, parameterized by service name at query time, covering your entire service fleet from a single deployment.</p>
<p>The next time someone asks "is the service healthy?", you will have a precise, data-backed answer before the question finishes echoing in the incident channel.</p>
<p>Questions, extensions, or feedback? Join the conversation in the <a href="https://discuss.elastic.co/">Elastic community forums</a>.</p>
<h2 id="fulldeploymentreference">Full deployment reference</h2>
<p>Everything you need to deploy the APM Service Health Monitor, in execution order. Run the five tool registrations first, then the agent.</p>
<p><strong>Tool 1: Dependency health</strong></p>
<pre><code>POST kbn:/api/agent_builder/tools
{
  "id": "apm_dependency_health_tool",
  "type": "esql",
  "description": "Evaluates the health of external dependencies (DBs, APIs, caches)...",
  "configuration": {
    "query": "FROM traces-*\n| WHERE service.name == ?service\n| WHERE processor.event == \"span\"\n  AND span.destination.service.resource IS NOT NULL\n| WHERE @timestamp &gt;= NOW() - 24 hours\n| EVAL is_error = CASE(event.outcome == \"failure\", 1, 0)\n| STATS\n    total_calls = COUNT(*),\n    failed_calls = SUM(is_error),\n    error_rate = 100.0 * SUM(is_error)/COUNT(*)\n  BY dependency_name = span.destination.service.resource\n| SORT error_rate DESC | LIMIT 100"
  }
}
</code></pre>
<p><strong>Tool 2: Error trend</strong></p>
<pre><code>POST kbn:/api/agent_builder/tools
{
  "id": "apm_error_trend_tool",
  "type": "esql",
  "description": "Tracks 5-minute bucketed error rates for the service over 24 hours...",
  "configuration": {
    "query": "FROM traces-*\n| WHERE service.name == ?service\n| WHERE transaction.type == \"request\"\n| WHERE @timestamp &gt;= NOW() - 24 hours\n| EVAL is_error = CASE(event.outcome == \"failure\", 1, 0)\n| STATS\n    total_requests = COUNT(*),\n    error_count = SUM(is_error),\n    error_rate = 100.0 * SUM(is_error)/COUNT(*)\n  BY time_bucket = DATE_TRUNC(5 minutes, @timestamp)\n| SORT time_bucket ASC | LIMIT 1000"
  }
}
</code></pre>
<p><strong>Tool 3: Latency trend</strong></p>
<pre><code>POST kbn:/api/agent_builder/tools
{
  "id": "apm_latency_trend_tool",
  "type": "esql",
  "description": "Provides 5-minute bucketed latency trends (avg, p95, p75) for last 24 hours...",
  "configuration": {
    "query": "FROM traces-*\n| WHERE service.name == ?service\n| WHERE transaction.type == \"request\"\n| WHERE @timestamp &gt;= NOW() - 24 hours\n| STATS\n    avg_latency_ms = AVG(transaction.duration.us / 1000),\n    p95_latency_ms = PERCENTILE(transaction.duration.us / 1000, 95),\n    p75_latency_ms = PERCENTILE(transaction.duration.us / 1000, 75)\n  BY time_bucket = DATE_TRUNC(5 minutes, @timestamp)\n| SORT time_bucket ASC | LIMIT 1000"
  }
}
</code></pre>
<p><strong>Tool 4: Metrics overview</strong></p>
<pre><code>POST kbn:/api/agent_builder/tools
{
  "id": "apm_metrics_overview_tool",
  "type": "esql",
  "description": "Aggregates key service metrics over last 24 hours: avg latency, p95/p99, error rate, throughput...",
  "configuration": {
    "query": "FROM traces-*\n| WHERE service.name == ?service\n| WHERE transaction.type == \"request\"\n| WHERE @timestamp &gt;= NOW() - 24 hours\n| EVAL is_error = CASE(event.outcome == \"failure\", 1, 0)\n| STATS\n    avg_latency_ms = AVG(transaction.duration.us / 1000),\n    p95_latency_ms = PERCENTILE(transaction.duration.us / 1000, 95),\n    p99_latency_ms = PERCENTILE(transaction.duration.us / 1000, 99),\n    error_rate = 100.0 * SUM(is_error) / COUNT(*),\n    throughput_rps = COUNT(*) / (24*3600)"
  }
}
</code></pre>
<p><strong>Tool 5: Throughput trend</strong></p>
<pre><code>POST kbn:/api/agent_builder/tools
{
  "id": "apm_throughput_trend_tool",
  "type": "esql",
  "description": "Provides service throughput trends in 5-minute intervals over 24 hours...",
  "configuration": {
    "query": "FROM traces-*\n| WHERE service.name == ?service\n| WHERE transaction.type == \"request\"\n| WHERE @timestamp &gt;= NOW() - 24 hours\n| STATS requests_count = COUNT(*)\n  BY time_bucket = DATE_TRUNC(5 minutes, @timestamp)\n| SORT time_bucket ASC | LIMIT 1000"
  }
}
</code></pre>
<p><strong>Agent: APM service health monitor</strong></p>
<pre><code>POST kbn:/api/agent_builder/agents
{
  "id": "apm_service_health_agent",
  "name": "APM Service Health Monitor",
  "description": "Provides Red/Yellow/Green health status and trends for services over 24 hours.",
  "labels": ["apm", "health", "service", "monitoring"],
  "avatar_color": "#4CAF50",
  "configuration": {
    "instructions": "You are the Service Health Agent...",
    "tools": [{
      "tool_ids": [
        "apm_dependency_health_tool",
        "apm_error_trend_tool",
        "apm_latency_trend_tool",
        "apm_metrics_overview_tool",
        "apm_throughput_trend_tool"
      ]
    }]
  }
}
</code></pre>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/apm-health-check-elastic-agent-builder</link>
    <guid isPermaLink="false">apm-health-check-elastic-agent-builder</guid>
    <category><![CDATA[APM]]></category>
    <dc:creator><![CDATA[Naga Putta,Stephen Brown]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt60839b56219ee418/6a7f02a6448e4e84155c0260/image4.png" length="0" type="image/png"/>
    <pubDate>Thu, 09 Jul 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Using NLP and Pattern Matching to Detect, Assess, and Redact PII in Logs - Part 2]]></title>
    <description><![CDATA[How to detect, assess, and redact PII in your logs using Elasticsearch, NLP and Pattern Matching]]></description>
    <content:encoded><![CDATA[<h2 id="introduction">Introduction:</h2>
<p>The prevalence of high-entropy logs in distributed systems has significantly raised the risk of PII (Personally Identifiable Information) seeping into our logs, which can result in security and compliance issues. This 2-part blog delves into the crucial task of identifying and managing this issue using the Elastic Stack. We will explore using NLP (Natural Language Processing) and Pattern matching to detect, assess, and, where feasible, redact PII from logs being ingested into Elasticsearch.</p>
<p>In <a href="https://www.elastic.co/observability-labs/blog/pii-ner-regex-assess-redact-part-1">Part 1 of this blog</a>, we covered the following:</p>
<ul>
<li>Review the techniques and tools we have available to manage PII in our logs</li>
<li>Understand the roles of NLP / NER in PII detection</li>
<li>Build a composable processing pipeline to detect and assess PII</li>
<li>Sample logs and run them through the NER Model</li>
<li>Assess the results of the NER Model </li>
</ul>
<p>In <strong>Part 2</strong> of this blog, we will cover the following:</p>
<ul>
<li>Apply the <code>redact</code> regex pattern processor and assess the results</li>
<li>Create Alerts using ESQL</li>
<li>Apply field-level security to control access to the un-redacted data</li>
<li>Production considerations and scaling</li>
<li>How to run these processes on incoming or historical data</li>
</ul>
<p>Reminder of the overall flow we will construct over the 2 blogs:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb2e3d5e77752e778/6a886127982926638858ace9/pii-overall-flow.png" alt="PII Overall Flow" /></p>
<p>All code for this exercise can be found at:
<a href="https://github.com/bvader/elastic-pii">https://github.com/bvader/elastic-pii</a>. </p>
<h3 id="part1prerequisites">Part 1 Prerequisites</h3>
<p>This blog picks up where <a href="https://www.elastic.co/observability-labs/blog/pii-ner-regex-assess-redact-part-1">Part 1 of this blog</a> left off. You must have the NER model, ingest pipelines, and dashboard from Part 1 installed and working.</p>
<ul>
<li>Loaded and configured NER Model </li>
<li>Installed all the composable ingest pipelines from Part 1 of the blog</li>
<li>Installed dashboard</li>
</ul>
<p>You can access the <a href="https://github.com/bvader/elastic-pii/blob/main/elastic/blog-part-1/logs-sampler-composable-pipelines-blog-1-complete.json">complete solution for Blog 1 here</a>. Don't forget to load the dashboard, found <a href="https://github.com/bvader/elastic-pii/blob/main/elastic/blog-part-1/pii-dashboard-part-1.ndjson">here</a>.</p>
<h3 id="applyingtheredactprocessor">Applying the Redact Processor</h3>
<p>Next, we will apply the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/redact-processor.html"><code>redact</code> processor</a>. The <code>redact</code> processor is a simple regex-based processor that takes a list of regex patterns and looks for them in a field and replaces them with literals when found. The <code>redact</code> processor is reasonably performant and can run at scale. At the end, we will discuss this in detail in the <a href="https://www.elastic.co/observability-labs/blog/pii-ner-regex-assess-redact-part-2#production-scaling">production scaling</a> section.</p>
<p>Elasticsearch comes packaged with a number of useful predefined <a href="https://github.com/elastic/elasticsearch/blob/8.15/libs/grok/src/main/resources/patterns/ecs-v1">patterns</a> that can be conveniently referenced by the <code>redact</code> processor. If one does not suit your needs, create a new pattern with a custom definition. The Redact processor replaces every occurrence of a match. If there are multiple matches, they will all be replaced with the pattern name.</p>
<p>In the code below, we leveraged some of the predefined patterns as well as constructing several custom patterns.</p>
<pre><code>        "patterns": [
          "%{EMAILADDRESS:EMAIL_REGEX}",      &lt;&lt; Predefined
          "%{IP:IP_ADDRESS_REGEX}",           &lt;&lt; Predefined
          "%{CREDIT_CARD:CREDIT_CARD_REGEX}", &lt;&lt; Custom
          "%{SSN:SSN_REGEX}",                 &lt;&lt; Custom
          "%{PHONE:PHONE_REGEX}"              &lt;&lt; Custom
        ]
</code></pre>
<p>We also replaced the PII with easily identifiable patterns we can use for assessment. </p>
<p>In addition, it is important to note that since the redact processor is a simple regex find and replace, it can be used against many "secrets" patterns, not just PII. There are many references for regex and secrets patterns, so you can reuse this capability to detect secrets in your logs.</p>
<p><a href="https://github.com/bvader/elastic-pii/blob/main/elastic/blog-part-2/pii-redact-composable-pipelines-blog-2-redact-processor-1.json">The code can be found here</a> for the following two sections of code. </p>
<p></p>
  redact processor pipeline code - click to open/close<p></p>
<pre><code># Add the PII redact processor pipeline
DELETE _ingest/pipeline/logs-pii-redact-processor
PUT _ingest/pipeline/logs-pii-redact-processor
{
  "processors": [
    {
      "set": {
        "field": "redact.proc.successful",
        "value": true
      }
    },
    {
      "set": {
        "field": "redact.proc.found",
        "value": false
      }
    },
    {
      "set": {
        "if": "ctx?.redact?.message == null",
        "field": "redact.message",
        "copy_from": "message"
      }
    },
    {
      "redact": {
        "field": "redact.message",
        "prefix": "&lt;REDACTPROC-",
        "suffix": "&gt;",
        "patterns": [
          "%{EMAILADDRESS:EMAIL_REGEX}",
          "%{IP:IP_ADDRESS_REGEX}",
          "%{CREDIT_CARD:CREDIT_CARD_REGEX}",
          "%{SSN:SSN_REGEX}",
          "%{PHONE:PHONE_REGEX}"
        ],
        "pattern_definitions": {
          "CREDIT_CARD": """\d{4}[ -]\d{4}[ -]\d{4}[ -]\d{4}""",
          "SSN": """\d{3}-\d{2}-\d{4}""",
          "PHONE": """(\+\d{1,2}\s?)?1?\-?\.?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}"""
        },
        "on_failure": [
          {
            "set": {
              "description": "Set 'error.message'",
              "field": "failure",
              "value": "REDACT_PROCESSOR_FAILED",
              "override": false
            }
          },
          {
            "set": {
              "field": "redact.proc.successful",
              "value": false
            }
          }
        ]
      }
    },
    {
      "set": {
        "if": "ctx?.redact?.message.contains('REDACTPROC')",
        "field": "redact.proc.found",
        "value": true
      }
    },
    {
      "set": {
        "if": "ctx?.redact?.pii?.found == null",
        "field": "redact.pii.found",
        "value": false
      }
    },
    {
      "set": {
        "if": "ctx?.redact?.proc?.found == true",
        "field": "redact.pii.found",
        "value": true
      }
    }
  ],
  "on_failure": [
    {
      "set": {
        "field": "failure",
        "value": "GENERAL_FAILURE",
        "override": false
      }
    }
  ]
}
</code></pre>
<p></p><p></p>
<p>And now, we will add the <code>logs-pii-redact-processor</code> pipeline to the overall <code>process-pii</code> pipeline 
</p>
  redact processor pipeline code - click to open/close<p></p>
<pre><code># Updated Process PII pipeline that now call the NER and Redact Processor 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 &amp;&amp; ctx.sample.sampled == true)",
        "name": "logs-ner-pii-processor"
      }
    },
    {
      "pipeline": {
        "if": "ctx.sample.enabled == false || (ctx.sample.enabled == true &amp;&amp;  ctx.sample.sampled == true)",
        "name": "logs-pii-redact-processor"
      }
    }
  ]
}
</code></pre>
<p></p><p></p>
<p>Reload the data as described in the <a href="https://www.elastic.co/observability-labs/blog/pii-ner-regex-assess-redact-part-2#reloading-the-logs">Reloading the logs</a>. If you have not generated the logs the first time, follow the instructions in the <a href="https://www.elastic.co/observability-labs/blog/pii-ner-regex-assess-redact-part-2#data-loading-appendix">Data Loading Appendix</a></p>
<p>Go to Discover and enter the following into the KQL bar
<code>sample.sampled : true and redact.message: REDACTPROC</code> and add the <code>redact.message</code> to the table and you should see something like this.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7f529ae6171d39bf/6a7f19a0de231544dffd8085/pii-discover-1-part-2.png" alt="PII Discover Blog 2 Part 1" /></p>
<p>And if you did not load the dashboard from Blog Part 1 at already, load it, it can be found <a href="https://github.com/bvader/elastic-pii/blob/main/elastic/blog-part-1/pii-dashboard-part-1.ndjson">here</a> using the Kibana -&gt; Stack Management -&gt; Saved Objects -&gt; Import. </p>
<p>It should look something like this now. Note that the REGEX portions of the dashboard are now active.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt19d420e064cc7b13/6a7f19a33cab1ce4a80e4c5d/pii-dashboard-1-part-2.png" alt="PII Dashboards Blog 2 Part 1" /></p>
<h2 id="checkpoint">Checkpoint</h2>
<p>At this point, we have the following capabilities:</p>
<ul>
<li>Ability to sample incoming logs and apply this PII redaction </li>
<li>Detect and Assess PII with the NER/NLP and Pattern Matching</li>
<li>Assess the amount, type and quality of the PII detections</li>
</ul>
<p>This is a great point to stop if you are just running all this once to see how it works, but we have a few more steps to make this useful in production systems.</p>
<ul>
<li>Clean up the working and unredacted data</li>
<li>Update the Dashboard to work with the cleaned-up data</li>
<li>Apply Role Based Access Control to protect the raw  unredacted data</li>
<li>Create Alerts</li>
<li>Production and Scaling Considerations</li>
<li>How to run these processes on incoming or historical data</li>
</ul>
<h2 id="applyingtoproductionsystems">Applying to Production Systems</h2>
<h3 id="cleanupworkingdataandupdatethedashboard">Cleanup working data and update the dashboard</h3>
<p>And now we will add the cleanup code to the overall <code>process-pii</code> pipeline.</p>
<p>In short, we set a flag <code>redact.enable: true</code> that directs the pipeline to move the unredacted <code>message</code> field to <code>raw.message</code> and the move the redacted message field <code>redact.message</code>to the <code>message</code> field. We will "protect" the <code>raw.message</code> in the following section. </p>
<p><strong>NOTE:</strong> Of course you can change this behavior if you want to completely delete the unredacted data. In this exercise we will keep it and protect it. </p>
<p>In addition we set <code>redact.cleanup: true</code> to clean up the NLP working data.</p>
<p>These fields allow a lot of control over what data you decide to keep and analyze. </p>
<p><a href="https://github.com/bvader/elastic-pii/blob/main/elastic/blog-part-2/pii-redact-composable-pipelines-blog-2-redact-processor-2.json">The code can be found here</a> for the following two sections of code. </p>
<p></p>
  redact processor pipeline code - click to open/close<p></p>
<pre><code># Updated Process PII pipeline that now call the NER and Redact Processor pipeline and cleans up 
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 &amp;&amp; ctx.sample.sampled == true)",
        "name": "logs-ner-pii-processor"
      }
    },
    {
      "pipeline": {
        "if": "ctx.sample.enabled == false || (ctx.sample.enabled == true &amp;&amp;  ctx.sample.sampled == true)",
        "name": "logs-pii-redact-processor"
      }
    },
    {
      "set": {
        "description": "Set to true to actually redact, false will run processors but leave original",
        "field": "redact.enable",
        "value": true
      }
    },
    {
      "rename": {
        "if": "ctx?.redact?.pii?.found == true &amp;&amp; ctx?.redact?.enable == true",
        "field": "message",
        "target_field": "raw.message"
      }
    },
    {
      "rename": {
        "if": "ctx?.redact?.pii?.found == true &amp;&amp; ctx?.redact?.enable == true",
        "field": "redact.message",
        "target_field": "message"
      }
    },
    {
      "set": {
        "description": "Set to true to actually to clean up working data",
        "field": "redact.cleanup",
        "value": true
      }
    },
    {
      "remove": {
        "if": "ctx?.redact?.cleanup == true",
        "field": [
          "ml"
        ],
        "ignore_failure": true
      }
    }
  ]
}
</code></pre>
<p></p><p></p>
<p>Reload the data as described here in the <a href="https://www.elastic.co/observability-labs/blog/pii-ner-regex-assess-redact-part-2#reloading-the-logs">Reloading the logs</a>. </p>
<p>Go to Discover and enter the following into the KQL bar
<code>sample.sampled : true and redact.pii.found: true</code> and add the following fields to the table</p>
<p><code>message</code>,<code>raw.message</code>,<code>redact.ner.found</code>,<code>redact.proc.found</code>,<code>redact.pii.found</code></p>
<p>You should see something like this
<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte9e78d6abc37b097/6a7f19a7ead8eca63abaac42/pii-discover-2-part-2.png" alt="PII Discover Part 2 Blog 2" /></p>
<p>We have everything we need to move forward with protecting the PII and Alerting on it. </p>
<p>Load up the new dashboard that works on the cleaned-up data </p>
<p>To load the dashboard, go to Kibana -&gt; Stack Management -&gt; Saved Objects and import the <code>pii-dashboard-part-2.ndjson</code> file that can be found <a href="https://github.com/bvader/elastic-pii/blob/main/elastic/blog-part-2/pii-dashboard-part-2.ndjson">here</a>. </p>
<p>The new dashboard should look like this. Note: It uses different fields under the covers since we have cleaned up the underlying data. </p>
<p>You should see something like this
<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltba60df9557e900b3/6a7f19aa96b5a6370087b86f/pii-dashboard-2-part-2.png" alt="PII Dashboard Part 2 Blog 2" /></p>
<h3 id="applyrolebasedaccesscontroltoprotecttherawunredacteddata">Apply Role Based Access Control to protect the raw unredacted data</h3>
<p>Elasticsearch supports role-based access control, including field and document level access control natively; it dramatically reduces the operational and maintenance complexity required to secure our application.</p>
<p>We will create a Role that does not allow access to the <code>raw.message</code> field and then create a user and assign that user the role. With that role, the user will only be able to see the redacted message, which is now in the <code>message</code> field, but will not be able to access the protected <code>raw.message</code> field.</p>
<p><strong>NOTE:</strong> Since we only sampled 10% of the data in this exercise the non-sampled <code>message</code> fields are not moved to the <code>raw.message</code>, so they are still viewable, but this shows the capability you can apply in a production system.</p>
<p><a href="https://github.com/bvader/elastic-pii/blob/main/elastic/blog-part-2/pii-redact-composable-pipelines-blog-2-rbac.json">The code can be found here</a> for the following section of code. </p>
<p></p>
  RBAC protect-pii role and user code - click to open/close<p></p>
<pre><code># Create role with no access to the raw.message field
GET _security/role/protect-pii
DELETE _security/role/protect-pii
PUT _security/role/protect-pii
{
  "cluster": [],
  "indices": [
    {
      "names": [
        "logs-*"
      ],
      "privileges": [
        "read",
        "view_index_metadata"
      ],
      "field_security": {
        "grant": [
          "*"
        ],
        "except": [
          "raw.message"
        ]
      },
      "allow_restricted_indices": false
    }
  ],
  "applications": [
    {
      "application": "kibana-.kibana",
      "privileges": [
        "all"
      ],
      "resources": [
        "*"
      ]
    }
  ],
  "run_as": [],
  "metadata": {},
  "transient_metadata": {
    "enabled": true
  }
}

# Create user stephen with protect-pii role
GET _security/user/stephen
DELETE /_security/user/stephen
POST /_security/user/stephen
{
  "password" : "mypassword",
  "roles" : [ "protect-pii" ],
  "full_name" : "Stephen Brown"
}
</code></pre>
<p></p><p></p>
<p>Now log into a separate window with the new user <code>stephen</code> with the <code>protect-pii role</code>. Go to Discover and put <code>redact.pii.found : true</code> in the KQL bar and add the <code>message</code> field to the table. Also, notice that the <code>raw.message</code> is not available. </p>
<p>You should see something like this
<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta1d5ca4051cdf880/6a7f19ad3cab1c04430e4c63/pii-discover-3-part-2.png" alt="PII Dashboard Part 2 Blog 2" /></p>
<h3 id="createanalertwhenpiidetected">Create an Alert when PII Detected</h3>
<p>Now, with the processing of the pipelines, creating an alert when PII is detected is easy. To review <a href="https://www.elastic.co/guide/en/kibana/current/alerting-getting-started.html">Alerting in Kibana</a> in detail if needed  </p>
<p>NOTE: <a href="https://www.elastic.co/observability-labs/blog/pii-ner-regex-assess-redact-part-2#reloading-the-logs">Reload</a> the data if needed to have recent data. </p>
<p>First, we will create a simple <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html">ES|QL query</a> in Discover. </p>
<p><a href="https://github.com/bvader/elastic-pii/blob/main/elastic/blog-part-2/pii-redact-esql-alert-blog-2.txt">The code can be found here.</a></p>
<pre><code>FROM logs-pii-default
| WHERE redact.pii.found == true
| STATS pii_count = count(*)
| WHERE pii_count &gt; 0
</code></pre>
<p>When you run this you should see something like this.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt125ed63dd1353c17/6a7f19b005b7b5771a18bd33/pii-esql-1-part-2.png" alt="PII ESQL Part 1 Blog 2" /></p>
<p>Now click the Alerts menu and select <code>Create search threshold rule</code>, and will create an alert to alert us when PII is found. </p>
<p><strong>Select a time field: @timestamp
Set the time window: 5 minutes</strong></p>
<p>Assuming you loaded the data recently when you run <strong>Test</strong> it should do something like </p>
<p>pii_count : <code>343</code>
Alerts generated <code>query matched</code></p>
<p>Add an action when the alert is Active. </p>
<p><strong>For each alert: <code>On status changes</code>
Run when: <code>Query matched</code></strong></p>
<pre><code>Elasticsearch query rule {{rule.name}} is active:

- PII Found: true
- PII Count: {{#context.hits}} {{_source.pii_count}}{{/context.hits}}
- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}
- Timestamp: {{context.date}}
- Link: {{context.link}}
</code></pre>
<p>Add an Action for when the Alert is Recovered. </p>
<p><strong>For each alert: <code>On status changes</code>
Run when: <code>Recovered</code></strong></p>
<pre><code>Elasticsearch query rule {{rule.name}} is Recovered:

- PII Found: false
- Conditions Not Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}
- Timestamp: {{context.date}}
- Link: {{context.link}}
</code></pre>
<p>When all setup it should look like this and <code>Save</code></p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt23fac59dfe9353b5/6a7f19b43cab1c618c0e4c67/pii-alert-1-part2.png" alt="Alert Setup" />\
<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltabee92bbafa00948/6a7f19b64c4bfb0351ccd8e4/pii-alert-2-part2.png" alt="Action Alert" />\
<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcade3df9ba1adcb8/6a7f19b9b43770d7974d70ee/pii-alert-3-part2.png" alt="Action Alert" /></p>
<p>You should get an Active alert that looks like this if you have recent data. I sent mine to Slack. </p>
<pre><code>Elasticsearch query rule pii-found-esql is active:
- PII Found: true
- PII Count:  374
- Conditions Met: Query matched documents over 5m
- Timestamp: 2024-10-15T02:44:52.795Z
- Link: https://mydeployment123.aws.found.io:9243/app/management/insightsAndAlerting/triggersActions/rule/7d6faecf-964e-46da-aaba-8a2f89f33989
</code></pre>
<p>And then if you wait you will get a Recovered alert that looks like this. </p>
<pre><code>Elasticsearch query rule pii-found-esql is Recovered:
- PII Found: false
- Conditions Not Met: Query did NOT match documents over 5m
- Timestamp: 2024-10-15T02:49:04.815Z
- Link: https://mydeployment123.kb.us-west-1.aws.found.io:9243/app/management/insightsAndAlerting/triggersActions/rule/7d6faecf-964e-46da-aaba-8a2f89f33989
</code></pre>
<h3 id="productionscaling">Production Scaling</h3>
<h4 id="nerscaling">NER Scaling</h4>
<p>As we mentioned <a href="https://www.elastic.co/observability-labs/blog/pii-ner-regex-assess-redact-part-1#named-entity-recognition-ner-detection">Part 1 of this blog</a> of this blog, NER / NLP Models are CPU-intensive and expensive to run at scale; thus, we employed a sampling technique to understand the risk in our logs without sending the full logs volume through the NER Model.</p>
<p>Please review <a href="https://www.elastic.co/observability-labs/blog/pii-ner-regex-assess-redact-part-1#loading-configuration-and-execution-of-the-ner-pipeline">the setup and configuration of the NER</a> model from Part 1 of the blog.</p>
<p>We chose the base BERT NER model <a href="https://huggingface.co/dslim/bert-base-NER">bert-base-NER</a> for our PII case.</p>
<p>To scale ingest, we will focus on scaling the allocations for the deployed model. More information on this topic is available <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-deploy-model.html">here</a>. The number of allocations must be less than the available allocated processors (cores, not vCPUs) per node.</p>
<p>The metrics below are related to the model and configuration from Part 1 of the blog.  </p>
<ul>
<li>4 Allocations to allow for more parallel ingestion</li>
<li>1 Thread per Allocation</li>
<li>0 Byes Cache, as we expect a low cache hit rate
<strong>Note</strong> If there are many repeated logs, cache can help, but with timestamps and other variations, cache will not help and can even slow down the process</li>
<li>8192 Queue </li>
</ul>
<pre><code>GET _ml/trained_models/dslim__bert-base-ner/_stats
.....
           "node": {
              "0m4tq7tMRC2H5p5eeZoQig": {
.....
                "attributes": {
                  "xpack.installed": "true",
                  "region": "us-west-1",
                  "ml.allocated_processors": "5", &lt;&lt; HERE 
.....
            },
            "inference_count": 5040,
            "average_inference_time_ms": 138.44285714285715, &lt;&lt; HERE 
            "average_inference_time_ms_excluding_cache_hits": 138.44285714285715,
            "inference_cache_hit_count": 0,
.....
            "threads_per_allocation": 1,
            "number_of_allocations": 4,  &lt;&lt;&lt; HERE
            "peak_throughput_per_minute": 1550,
            "throughput_last_minute": 1373,
            "average_inference_time_ms_last_minute": 137.55280407865988,
            "inference_cache_hit_count_last_minute": 0
          }
        ]
      }
    }
</code></pre>
<p>There are 3 key pieces of information above:</p>
<ul>
<li><p><code>"ml.allocated_processors": "5"</code>
The number of physical cores / processors available </p></li>
<li><p><code>"number_of_allocations": 4</code>
The number of allocations which is maximum 1 per physical core. <strong>Note</strong>: we could have used 5 allocations, but we only allocated 4 for this exercise</p></li>
<li><p><code>"average_inference_time_ms": 138.44285714285715</code>
The averages inference time per document. </p></li>
</ul>
<p>The math is pretty straightforward for throughput for Inferences per Min (IPM) per allocation (1 allocation per physical core), since an inference uses a single core and a single thread.</p>
<p>Then the Inferences per Min per Allocation is simply: </p>
<p><code>IPM per allocation = 60,000 ms (in a minute) / 138ms per inference = 435</code></p>
<p>When then lines up with the Total Inferences per Minute</p>
<p><code>Total IPM = 435 IPM / allocation * 4 Allocations = ~1740</code></p>
<p>Suppose we want to do 10,000 IPMs, how many allocations (cores) would I need? </p>
<p><code>Allocations = 10,000 IPM / 435 IPM per allocation = 23 Allocation (cores rounded up)</code></p>
<p>Or perhaps logs are coming in at 5000 EPS and you want to do 1% Sampling. </p>
<p><code>IPM = 5000 EPS * 60sec * 0.01 sampling = 3000 IPM sampled</code></p>
<p>Then </p>
<p><code>Number of Allocators = 3000 IPM / 435 IPM per allocation = 7 allocations (cores rounded up)</code></p>
<p><strong>Want Faster!</strong> Turns out there is a more lightweight NER Model <a href="https://huggingface.co/dslim/distilbert-NER">
distilbert-NER</a> model that is faster, but the tradeoff is a little less accuracy. </p>
<p>Running the logs through this model results in an inference time nearly twice as fast!</p>
<p><code>"average_inference_time_ms": 66.0263959390863</code></p>
<p>Here is some quick math:
<code>$IPM per allocation = 60,000 ms (in a minute) / 61ms per inference = 983</code></p>
<p>Suppose we want to do 25,000 IPMs, how many allocations (cores) would I need?</p>
<p><code>Allocations = 25,000 IPM / 983 IPM per allocation = 26 Allocation (cores rounded up)</code></p>
<p><strong>Now you can apply this math to determine the correct sampling and NER scaling to support your logging use case.</strong></p>
<h4 id="redactprocessorscaling">Redact Processor Scaling</h4>
<p>In short, the <code>redact</code> processor should scale to production loads as long as you are using appropriately sized and configured nodes and have well-constructed regex patterns. </p>
<h3 id="assessingincominglogs">Assessing incoming logs</h3>
<p>If you want to test on incoming logs data in a data stream. All you need to do is change the conditional in the <code>logs@custom</code> pipeline to apply the <code>process-pii</code> to the dataset you want to. You can use any conditional that fits your condition.</p>
<p>Note: Just make sure that you have accounted for the proper scaling for the NER and Redact processors they were described above in <a href="https://www.elastic.co/observability-labs/blog/pii-ner-regex-assess-redact-part-2#production-scaling">Production Scaling</a></p>
<pre><code>    {
      "pipeline": {
        "description" : "Call the process_pii pipeline on the correct dataset",
        "if": "ctx?.data_stream?.dataset == 'pii'", &lt;&lt;&lt; HERE
        "name": "process-pii"
      }
    }
</code></pre>
<p>So if for example your logs are coming into <code>logs-mycustomapp-default</code> you would just change the conditional to</p>
<pre><code>        "if": "ctx?.data_stream?.dataset == 'mycustomapp'",
</code></pre>
<h3 id="assessinghistoricaldata">Assessing historical data</h3>
<p>If you have a historical (already ingested) data stream or index you can run the assessment over them using the <code>_reindex</code> API&gt; </p>
<p>Note: Just make sure that you have accounted for the proper scaling for the NER and Redact processors they were described above in <a href="https://www.elastic.co/observability-labs/blog/pii-ner-regex-assess-redact-part-2#production-scaling">Production Scaling</a></p>
<p>There are a couple of extra steps: 
<a href="https://github.com/bvader/elastic-pii/blob/main/elastic/blog-part-2/pii-redact-historical-data-blog-2.json">The code can be found here.</a></p>
<p>1) First we can set the parameters to ONLY keep the sampled data as there is no reason to make a copy of all the unsampled data. In the <code>process-pii</code> pipeline, there is a setting <code>sample.keep_unsampled</code>, which we can set to <code>false</code>, which will then only keep the sampled data </p>
<pre><code>    {
      "set": {
        "description": "Set to false if you want to drop unsampled data, handy for reindexing hostorical data",
        "field": "sample.keep_unsampled",
        "value": false &lt;&lt;&lt; SET TO false
      }
    },
</code></pre>
<p>2) Second, we will create a pipeline that will reroute the data to the correct data stream to run through all the PII assessment/detection pipelines. It also sets the correct <code>dataset</code> and <code>namespace</code></p>
<pre><code>DELETE _ingest/pipeline/sendtopii
PUT _ingest/pipeline/sendtopii
{
  "processors": [
    {
      "set": {
        "field": "data_stream.dataset",
        "value": "pii"
      }
    },
    {
      "set": {
        "field": "data_stream.namespace",
        "value": "default"
      }
    },
    {
      "reroute" : 
      {
        "dataset" : "{{data_stream.dataset}}",
        "namespace": "{{data_stream.namespace}}"
      }
    }
  ]
}
</code></pre>
<p>3) Finally, we can run a <code>_reindex</code> to select the data we want to test/assess. It is recommended to review the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html">_reindex</a> documents before trying this. First, select the source data stream you want to assess, in this example, it is the <code>logs-generic-default</code> logs data stream. Note: I also added a <code>range</code> filter to select a specific time range. There is a bit of a "trick" that we need to use since we are re-routing the data to the data stream <code>logs-pii-default</code>. To do this, we just set <code>"index": "logs-tmp-default"</code> in the <code>_reindex</code> as the correct data stream will be set in the pipeline. We must do that because <code>reroute</code> is a <code>noop</code> if it is called from/to the same datastream. </p>
<pre><code>POST _reindex?wait_for_completion=false
{
  "source": {
    "index": "logs-generic-default",
    "query": {
      "bool": {
        "filter": [
          {
            "range": {
              "@timestamp": {
                "gte": "now-1h/h",
                "lt": "now"
              }
            }
          }
        ]
      }
    }
  },
  "dest": {
    "op_type": "create",
    "index": "logs-tmp-default",
    "pipeline": "sendtopii"
  }
}
</code></pre>
<h2 id="summary">Summary</h2>
<p>At this point, you have the tools and processes need to assess, detect, analyze, alert and protect PII in your logs.  </p>
<p><a href="https://github.com/bvader/elastic-pii/tree/main/elastic/blog-complete-end-solution">The end state solution can be found here:</a>. </p>
<p>In <a href="https://www.elastic.co/observability-labs/blog/pii-ner-regex-assess-redact-part-1">Part 1 of this blog</a>, we accomplished the following.</p>
<ul>
<li>Reviewed the techniques and tools we have available for PII detection and assessment</li>
<li>Reviewed NLP / NER role in PII detection and assessment</li>
<li>Built the necessary composable ingest pipelines to sample logs and run them through the NER Model</li>
<li>Reviewed the NER results and are ready to move to the second blog</li>
</ul>
<p>In <strong>Part 2</strong> of this blog, we covered the following:</p>
<ul>
<li>Redact PII using NER and redact processor</li>
<li>Apply field-level security to control access to the un-redacted data</li>
<li>Enhance the dashboards and alerts</li>
<li>Production considerations and scaling</li>
<li>How to run these processes on incoming or historical data</li>
</ul>
<p><strong><em>So get to work and reduce risk in your logs!</em></strong></p>
<h2 id="dataloadingappendix">Data Loading Appendix</h2>
<h4 id="code">Code</h4>
<p>The data loading code can be found here: </p>
<p><a href="https://github.com/bvader/elastic-pii">https://github.com/bvader/elastic-pii</a></p>
<pre><code>$ git clone https://github.com/bvader/elastic-pii.git
</code></pre>
<h4 id="creatingandloadingthesampledataset">Creating and Loading the Sample Data Set</h4>
<pre><code>$ cd elastic-pii
$ cd python
$ python -m venv .env
$ source .env/bin/activate
$ pip install elasticsearch
$ pip install Faker
</code></pre>
<p>Run the log generator </p>
<pre><code>$ python generate_random_logs.py
</code></pre>
<p>If you do not changes any parameters, this will create 10000 random logs in a file named pii.log with a mix of logs that containe and do not contain PII. </p>
<p>Edit <code>load_logs.py</code> and set the following </p>
<pre><code># 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="
</code></pre>
<p>Then run the following command. </p>
<pre><code>$ python load_logs.py
</code></pre>
<h4 id="reloadingthelogs">Reloading the logs</h4>
<p><strong>Note</strong> To reload the logs, you can simply re-run the above command. You can run the command multiple time during this exercise and the logs will be reloaded (actually loaded again). The new logs will not collide with previous runs as there will be a unique <code>run.id</code> for each run which is displayed at the end of the loading process.</p>
<pre><code>$ python load_logs.py
</code></pre>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/pii-ner-regex-assess-redact-part-2</link>
    <guid isPermaLink="false">pii-ner-regex-assess-redact-part-2</guid>
    <category><![CDATA[Logs Analytics]]></category>
    <dc:creator><![CDATA[Stephen Brown]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0e5726114c49ca9d/6a7f19bde02fac86155d6999/pii-ner-regex-assess-redact-part-2.png" length="0" type="image/png"/>
    <pubDate>Tue, 22 Oct 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Using NLP and Pattern Matching to Detect, Assess, and Redact PII in Logs - Part 1]]></title>
    <description><![CDATA[How to detect and assess PII in your logs using Elasticsearch and NLP]]></description>
    <content:encoded><![CDATA[<h2 id="introduction">Introduction:</h2>
<p>The prevalence of high-entropy logs in distributed systems has significantly raised the risk of PII (Personally Identifiable Information) seeping into our logs, which can result in security and compliance issues. This 2-part blog delves into the crucial task of identifying and managing this issue using the Elastic Stack. We will explore using NLP (Natural Language Processing) and Pattern matching to detect, assess, and, where feasible, redact PII from logs that are being ingested into Elasticsearch.</p>
<p>In <strong>Part 1</strong> of this blog, we will cover the following:</p>
<ul>
<li>Review the techniques and tools we have available to manage PII in our logs</li>
<li>Understand the roles of NLP / NER in PII detection</li>
<li>Build a composable processing pipeline to detect and assess PII</li>
<li>Sample logs and run them through the NER Model</li>
<li>Assess the results of the NER Model </li>
</ul>
<p>In <a href="https://www.elastic.co/observability-labs/blog/pii-ner-regex-assess-redact-part-2">Part 2 of this blog</a> of this blog, we will cover the following:</p>
<ul>
<li>Redact PII using NER and the redact processor</li>
<li>Apply field-level security to control access to the un-redacted data</li>
<li>Enhance the dashboards and alerts</li>
<li>Production considerations and scaling</li>
<li>How to run these processes on incoming or historical data</li>
</ul>
<p>Here is the overall flow we will construct over the 2 blogs:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt13e652a6333de461/6a7f19893ce8e2179bcf5795/pii-overall-flow.png" alt="PII Overall Flow" /></p>
<p>All code for this exercise can be found at:
<a href="https://github.com/bvader/elastic-pii">https://github.com/bvader/elastic-pii</a>. </p>
<h2 id="toolsandtechniques">Tools and Techniques</h2>
<p>There are four general capabilities that we will use for this exercise. </p>
<ul>
<li>Named Entity Recognition Detection (NER)</li>
<li>Pattern Matching Detection</li>
<li>Log Sampling</li>
<li>Ingest Pipelines as Composable Processing </li>
</ul>
<h4 id="namedentityrecognitionnerdetection">Named Entity Recognition (NER) Detection</h4>
<p>NER is a sub-task of Natural Language Processing (NLP) that involves identifying and categorizing named entities in unstructured text into predefined categories such as:</p>
<ul>
<li>Person: Names of individuals, including celebrities, politicians, and historical figures.</li>
<li>Organization: Names of companies, institutions, and organizations.</li>
<li>Location: Geographic locations, including cities, countries, and landmarks.</li>
<li>Event: Names of events, including conferences, meetings, and festivals.</li>
</ul>
<p>For our use PII case, we will choose the base BERT NER model <a href="https://huggingface.co/dslim/bert-base-NER">bert-base-NER</a> that can be downloaded from <a href="https://huggingface.co">Hugging Face</a> and loaded into Elasticsearch as a trained model.</p>
<p><strong>Important Note:</strong>  NER / NLP Models are CPU-intensive and expensive to run at scale; thus, we will want to employ a sampling technique to understand the risk in our logs without sending the full logs volume through the NER Model. We will discuss the performance and scaling of the NER model in part 2 of the blog. </p>
<h4 id="patternmatchingdetection">Pattern Matching Detection</h4>
<p>In addition to using an NER, regex pattern matching is a powerful tool for detecting and redacting PII based on common patterns. The Elasticsearch <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/redact-processor.html">redact</a> processor is built for this use case.</p>
<h4 id="logsampling">Log Sampling</h4>
<p>Considering the performance implications of NER and the fact that we may be ingesting a large volume of logs into Elasticsearch, it makes sense to sample our incoming logs. We will build a simple log sampler to accomplish this. </p>
<h4 id="ingestpipelinesascomposableprocessing">Ingest Pipelines as Composable Processing</h4>
<p>We will create several pipelines, each focusing on a specific capability and a main ingest pipeline to orchestrate the overall process. </p>
<h2 id="buildingtheprocessingflow">Building the Processing Flow</h2>
<h4 id="logssamplingcomposableingestpipelines">Logs Sampling + Composable Ingest Pipelines</h4>
<p>The first thing we will do is set up a sampler to sample our logs. This ingest pipeline simply takes a sampling rate between 0 (no log) and 10000 (all logs), which allows as low as ~0.01% sampling rate and marks the sampled logs with <code>sample.sampled: true</code>. Further processing on the logs will be driven by the value of <code>sample.sampled</code>. The <code>sample.sample_rate</code> can be set here or "passed in" from the orchestration pipeline.</p>
<p>The command should be run from the Kibana -&gt; Dev Tools</p>
<p><a href="https://github.com/bvader/elastic-pii/blob/main/elastic/blog-part-1/logs-sampler-composable-pipelines-part-1.json">The code can be found here</a> for the following three sections of code. </p>
<p></p>
  logs-sampler pipeline code - click to open/close<p></p>
<pre><code># 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 &lt;= ctx.sample.sample_rate",
        "field": "sample.sampled",
        "value": true
      }
    },
    {
      "drop": {
         "description": "Drop unsampled document if applicable",
        "if": "ctx.sample.keep_unsampled == false &amp;&amp; ctx.sample.sampled == false"
      }
    }
  ]
}
</code></pre>
<p></p><p></p>
<p>Now, let's test the logs sampler. We will build the first part of the composable pipeline. We will be sending logs to the logs-generic-default data stream. With that in mind, we will create the <code>logs@custom</code> ingest pipeline that will be automatically called using the logs <a href="https://www.elastic.co/guide/en/fleet/current/data-streams.html#data-streams-pipelines">data stream framework</a> for customization. We will add one additional level of abstraction so that you can apply this PII processing to other data streams.</p>
<p>Next, we will create the <code>process-pii</code> pipeline. This is the core processing pipeline where we will orchestrate PII processing component pipelines. In this first step, we will simply apply the sampling logic. Note that we are setting the sampling rate to 100, which is equivalent to 10% of the logs.</p>
<p></p>
  process-pii pipeline code - click to open/close<p></p>
<pre><code># 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
      }
    }
  ]
}
</code></pre>
<p></p><p></p>
<p>Finally, we create the logs <code>logs@custom</code>, which will simply call our <code>process-pii</code> pipeline based on the correct <code>data_stream.dataset</code></p>
<p></p>
  logs@custom pipeline code - click to open/close<p></p>
<pre><code># 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"
      }
    }
  ]
}
</code></pre>
<p></p><p></p>
<p>Now, let's test to see the sampling at work.</p>
<p>Load the data as described here <a href="https://www.elastic.co/observability-labs/blog/pii-ner-regex-assess-redact-part-1#data-loading-appendix">Data Loading Appendix</a>. Let's use the sample data first, and we will talk about how to test with your incoming or historical logs later at the end of this blog. </p>
<p>If you look at Observability -&gt; Logs -&gt; Logs Explorer with KQL filter <code>data_stream.dataset : pii</code> and Breakdown by sample.sampled, you should see the breakdown to be approximately 10%</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0aab62015183d03c/6a7f198d5967e513345dd6a9/pii-discover-1-part-1.png" alt="PII Discover 1" /></p>
<p>At this point we have a composable ingest pipeline that is "sampling" logs. As a bonus, you can use this logs sampler for any other use cases you have as well. </p>
<h4 id="loadingconfigurationandexecutionofthenerpipeline">Loading, Configuration, and Execution of the NER Pipeline</h4>
<h5 id="loadingthenermodel">Loading the NER Model</h5>
<p>You will need a Machine Learning node to run the NER model on. In this exercise, we are using <a href="https://www.elastic.co/guide/en/cloud/current/ec-getting-started.html">Elastic Cloud Hosted Deployment </a>on AWS with the <a href="https://www.elastic.co/guide/en/cloud/current/ec_selecting_the_right_configuration_for_you.html">CPU Optimized (ARM)</a> architecture. The NER inference will run on a Machine Learning AWS c5d node. There will be GPU options in the future, but today, we will stick with CPU architecture.  </p>
<p>This exercise will use a single c5d with 8 GB RAM with 4.2 vCPU up to 8.4 vCPU</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte8ec884fa3092c65/6a7f1990c2cc0903e12499a2/pii-ml-node-part-1.png" alt="ML Node" /></p>
<p>Please refer to the official documentation on <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-import-model.html">how to import an NLP-trained model into Elasticsearch</a> for complete instructions on uploading, configuring, and deploying the model.</p>
<p>The quickest way to get the model is using the Eland Docker method. </p>
<p>The following command will load the model into Elasticsearch but will not start it. We will do that in the next step.  </p>
<pre><code>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
</code></pre>
<h5 id="deployandstartthenermodel">Deploy and Start the NER Model</h5>
<p>In general, to improve ingest performance, increase throughput by adding more allocations to the deployment. For improved search speed, increase the number of threads per allocation.</p>
<p>To scale ingest, we will focus on scaling the allocations for the deployed model. More information on this topic is available <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-deploy-model.html">here</a>. The number of allocations must be less than the available allocated processors (cores, not vCPUs) per node.</p>
<p>To deploy and start the NER Model. We will do this using the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.15/start-trained-model-deployment.html">Start trained model deployment API</a></p>
<p>We will configure the following:</p>
<ul>
<li>4 Allocations to allow for more parallel ingestion</li>
<li>1 Thread per Allocation</li>
<li>0 Byes Cache, as we expect a low cache hit rate </li>
<li>8192 Queue</li>
</ul>
<pre><code># 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&amp;number_of_allocations=4&amp;threads_per_allocation=1&amp;queue_capacity=8192
</code></pre>
<p>You should get a response that looks something like this.</p>
<pre><code>{
  "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
  }
}
</code></pre>
<p>The NER model has been deployed and started and is ready to be used.</p>
<p>The following ingest pipeline implements the NER model via the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/inference-processor.html">inference</a> processor. </p>
<p>There is a significant amount of code here, but only two items of interest now exist. The rest of the code is conditional logic to drive some additional specific behavior that we will look closer at in the future. </p>
<ol>
<li><p>The inference processor calls the NER model by ID, which we loaded previously, and passes the text to be analyzed, which, in this case, is the message field, which is the text_field we want to pass to the NER model to analyze for PII.</p></li>
<li><p>The script processor loops through the message field and uses the data generated by the NER model to replace the identified PII with redacted placeholders. This looks more complex than it really is, as it simply loops through the array of ML predictions and replaces them in the message string with constants, and stores the results in a new field <code>redact.message</code>. We will look at this a little closer in the following steps. </p></li>
</ol>
<p><a href="https://github.com/bvader/elastic-pii/blob/main/elastic/blog-part-1/logs-sampler-composable-pipelines-part-2.json">The code can be found here</a> for the following three sections of code. </p>
<p>The NER PII Pipeline</p>
<p></p>
  logs-ner-pii-processor pipeline code - click to open/close<p></p>
<pre><code># 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
      }
    },
    {
      "set": {
        "if": "ctx?.redact?.message == null",
        "field": "redact.message",
        "copy_from": "message"
      }
    },
    {
      "set": {
        "field": "redact.ner.successful",
        "value": true
      }
    },
    {
      "set": {
        "field": "redact.ner.found",
        "value": false
      }
    },
    {
      "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.ner.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) &amp;&amp; 
                (item['class_probability'] &gt;= ctx.redact.ner.minimum_score)) {  
                    msg = msg.replace(item['entity'], '&lt;' + 
                    'REDACTNER-'+ item['class_name'] + '_NER&gt;')
              }
          }
          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
            }
          }
        ]
      }
    },

    {
      "set": {
        "if": "ctx?.ml?.inference?.entities.size() &gt; 0", 
        "field": "redact.ner.found",
        "value": true,
        "ignore_failure": true
      }
    },
    {
      "set": {
        "if": "ctx?.redact?.pii?.found == null",
        "field": "redact.pii.found",
        "value": false
      }
    },
    {
      "set": {
        "if": "ctx?.redact?.ner?.found == true",
        "field": "redact.pii.found",
        "value": true
      }
    },
    {
      "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
      }
    }
  ]
}
</code></pre>
<p></p><p></p>
<p>The updated PII Processor Pipeline, which now calls the NER Pipeline</p>
<p></p>
  process-pii pipeline code - click to open/close<p></p>
<pre><code># 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 &amp;&amp; ctx.sample.sampled == true)",
        "name": "logs-ner-pii-processor"
      }
    }
  ]
}
</code></pre>
<p></p><p></p>
<p>Now reload the data as described here in <a href="https://www.elastic.co/observability-labs/blog/pii-ner-regex-assess-redact-part-1#reloading-the-logs">Reloading the logs</a></p>
<h3 id="results">Results</h3>
<p>Let's take a look at the results with the NER processing in place. In the Logs Explorer with KQL query bar, execute the following query
<code>data_stream.dataset : pii and ml.inference.entities.class_name : ("PER" and "LOC" and "ORG" )</code> </p>
<p>Logs Explorer should look something like this, open the top message to see the details.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt15d140ffc3b719b0/6a7f1993bdcff051cac43293/pii-discover-2-part-1.png" alt="PII Discover 2" /></p>
<h4 id="nermodelresults">NER Model Results</h4>
<p>Lets take a closer look at what these fields mean.</p>
<p><strong>Field:</strong> <code>ml.inference.entities.class_name</code>\
<strong>Sample Value:</strong> <code>[PER, PER, LOC, ORG, ORG]</code>\
<strong>Description:</strong> An array of the named entity classes that the NER model has identified.</p>
<p><strong>Field:</strong> <code>ml.inference.entities.class_probability</code>\
<strong>Sample Value:</strong> <code>[0.999, 0.972, 0.896, 0.506, 0.595]</code>\
<strong>Description:</strong> The class_probability is a value between 0 and 1, which indicates how likely it is that a given data point belongs to a certain class. The higher the number, the higher the probability that the data point belongs to the named class. <strong>This is important as in the next blog we can decide a threshold that we will want to use to alert and redact on.</strong>'
You can see in this example it identified a <code>LOC</code> as an <code>ORG</code>, we can filter this out / find them by setting a threshold. </p>
<p><strong>Field:</strong> <code>ml.inference.entities.entity</code>\
<strong>Sample Value:</strong> <code>[Paul Buck, Steven Glens, South Amyborough, ME, Costco]</code>\
<strong>Description:</strong> The array of entities identified that align positionally with the <code>class_name</code> and <code>class_probability</code>.</p>
<p><strong>Field:</strong> <code>ml.inference.predicted_value</code>\
<strong>Sample Value:</strong> <code>[2024-09-23T14:32:14.608207-07:00Z] log.level=INFO: Payment successful for order #4594 (user: [Paul Buck](PER&amp;Paul+Buck), david59@burgess.net). Phone: 726-632-0527x520, Address: 3713 [Steven Glens](PER&amp;Steven+Glens), [South Amyborough](LOC&amp;South+Amyborough), [ME](ORG&amp;ME) 93580, Ordered from: [Costco](ORG&amp;Costco)</code>\
<strong>Description:</strong> The predicted value of the model.</p>
<h4 id="piiassessmentdashboard">PII Assessment Dashboard</h4>
<p>Lets take a quick look at a dashboard built to assess PII the data. </p>
<p>To load the dashboard, go to Kibana -&gt; Stack Management -&gt; Saved Objects and import the <code>pii-dashboard-part-1.ndjson</code> file that can be found here: </p>
<p>https://github.com/bvader/elastic-pii/blob/main/elastic/blog-part-1/pii-dashboard-part-1.ndjson</p>
<p>More complete instructions on Kibana Saved Objects can be found <a href="https://www.elastic.co/guide/en/kibana/current/managing-saved-objects.html">here</a>.</p>
<p>After loading the dashboard, navigate to it and select the right time range and you should see something like below. It shows metrics such as sample rate, percent of logs with NER, NER Score Trends etc. We will examine the assessment and actions in part 2 of this blog. </p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt70c587367d5fadd9/6a7f199705b7b560a118bd2f/pii-dashboard-1-part-1.png" alt="PII Dashboard 1" /></p>
<h2 id="summaryandnextsteps">Summary and Next Steps</h2>
<p>In this first part of the blog, we have accomplished the following.</p>
<ul>
<li>Reviewed the techniques and tools we have available for PII detection and assement</li>
<li>Reviewed NLP / NER role in PII detection and assessment</li>
<li>Built the necessary composable ingest pipelines to sample logs and run them through the NER Model</li>
<li>Reviewed the NER results and are ready to move to the second blog</li>
</ul>
<p>In the upcoming <a href="https://www.elastic.co/observability-labs/blog/pii-ner-regex-assess-redact-part-2">Part 2 of this blog</a> of this blog, we will cover the following:</p>
<ul>
<li>Redact PII using NER and redact processor</li>
<li>Apply field-level security to control access to the un-redacted data</li>
<li>Enhance the dashboards and alerts</li>
<li>Production considerations and scaling</li>
<li>How to run these processes on incoming or historical data</li>
</ul>
<h2 id="dataloadingappendix">Data Loading Appendix</h2>
<h4 id="code">Code</h4>
<p>The data loading code can be found here: </p>
<p><a href="https://github.com/bvader/elastic-pii">https://github.com/bvader/elastic-pii</a></p>
<pre><code>$ git clone https://github.com/bvader/elastic-pii.git
</code></pre>
<h4 id="creatingandloadingthesampledataset">Creating and Loading the Sample Data Set</h4>
<pre><code>$ cd elastic-pii
$ cd python
$ python -m venv .env
$ source .env/bin/activate
$ pip install elasticsearch
$ pip install Faker
</code></pre>
<p>Run the log generator </p>
<pre><code>$ python generate_random_logs.py
</code></pre>
<p>If you do not changes any parameters, this will create 10000 random logs in a file named pii.log with a mix of logs that containe and do not contain PII. </p>
<p>Edit <code>load_logs.py</code> and set the following </p>
<pre><code># 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="
</code></pre>
<p>Then run the following command. </p>
<pre><code>$ python load_logs.py
</code></pre>
<h4 id="reloadingthelogs">Reloading the logs</h4>
<p><strong>Note</strong> To reload the logs, you can simply re-run the above command. You can run the command multiple time during this exercise and the logs will be reloaded (actually loaded again). The new logs will not collide with previous runs as there will be a unique <code>run.id</code> for each run which is displayed at the end of the loading process.</p>
<pre><code>$ python load_logs.py
</code></pre>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/pii-ner-regex-assess-redact-part-1</link>
    <guid isPermaLink="false">pii-ner-regex-assess-redact-part-1</guid>
    <category><![CDATA[Logs Analytics]]></category>
    <dc:creator><![CDATA[Stephen Brown]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltce9ee72734da81d6/6a7f199bb6b734381ce491b0/pii-ner-regex-assess-redact-part-1.png" length="0" type="image/png"/>
    <pubDate>Wed, 25 Sep 2024 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>