elastic / elastic/detection-rules

[New Rule] Sensitive personal data (CPF/SSN/DNI) exposure in a Claude Code session

Open
#6,560 1 comment 0 reactions 0 assignees View on GitHub
community
Dominant language
Python
Stars
2.7k
Forks
696
Avg merge
4d 17h
Merged PRs (30d)
87

Description

**Description**

Detects labeled, format-valid national identity numbers (Brazilian CPF/CNPJ, US Social Security Number, Spanish DNI/NIE) appearing in a Claude Code agentic session: in the user's prompt text, or in the input/parameters of a tool call (for example, a Bash command or file write). This means real personal data was typed into, or passed through, an AI coding assistant's session and tool-execution pipeline. It's a data-handling/DLP-style signal, not necessarily evidence of an attack. The most common cause is a user pasting real customer/employee records into a prompt or into a file the assistant is asked to create or edit.

There are currently 4 Anthropic/Claude integration packages (`anthropic`, `anthropic_metrics`, `claude_code`, `claude_cowork`) but zero rules in this repo reference "claude" or "anthropic" anywhere (checked via `gh search code`/`gh issue list`). `claude_code`'s OTel telemetry can capture the literal prompt text and tool arguments (gated behind `OTEL_LOG_USER_PROMPTS`/`OTEL_LOG_TOOL_DETAILS`), which is exactly where personal data pasted into the assistant would show up: a real, currently-uncovered data-loss-prevention gap.

Matching requires both the correct digit/letter shape AND a nearby label (`CPF`, `CNPJ`, `SSN`, `DNI`, `NIE`) rather than the shape alone, because format-shaped strings that are not actually personal data (order numbers, invoice references, ticket IDs) are common and would otherwise cause false positives. This was confirmed during development: a shape-only first pass matched an unrelated SSN-shaped invoice reference string, and later a CNPJ-shaped tracking code and an NIE-shaped product SKU. Adding the label-proximity requirement eliminated all of them while still matching every genuine case.

Built and validated against real telemetry, not synthetic ES documents: installed the actual `claude_code` Fleet package from the registry (real ingest pipeline/index template), then ran real headless Claude Code CLI sessions (`claude -p ...`) with OTel telemetry enabled against a local Elastic Stack, generating genuine `logs-claude_code.events.otel-*` documents. Test data used synthetic-but-format-valid identifiers only (real CPF/CNPJ/DNI/NIE checksum algorithms on arbitrary digits; the SSN pinned to the SSA's never-issued 900-999 area-number range), never real personal data. Verified the exact final query against 15 real documents across all 5 identifier types and 3 event types (`user_prompt`, `tool_decision`, `tool_result`): 15/15 correct matches, 0 false positives across 4 benign sessions and 3 deliberate false-positive edge cases (SSN-, CNPJ-, and NIE-shaped but unlabeled strings).

**Edit (same day):** extended from the initial CPF/SSN/DNI version to also cover CNPJ (Brazilian company registration number) and NIE (Spanish foreigner ID). These are natural same-country extensions rather than new locales, to keep scope tight. Same validation process applied: real sessions, real false-positive edge cases, full regression against the combined dataset.

**Target Ruleset**: other

**Target Rule Type**: ES|QL

**Tested ECS Version**: 9.4.0

**Query**
```sql
from logs-claude_code.events.otel-* metadata _id, _version, _index

| eval Esql.is_cpf = (claude_code.events.tool_input rlike ".*[Cc][Pp][Ff].{0,5}[0-9]{3}\\.[0-9]{3}\\.[0-9]{3}-[0-9]{2}.*")
or (claude_code.events.tool_parameters rlike ".*[Cc][Pp][Ff].{0,5}[0-9]{3}\\.[0-9]{3}\\.[0-9]{3}-[0-9]{2}.*")
or (claude_code.events.prompt_text rlike ".*[Cc][Pp][Ff].{0,5}[0-9]{3}\\.[0-9]{3}\\.[0-9]{3}-[0-9]{2}.*")
| eval Esql.is_cnpj = (claude_code.events.tool_input rlike ".*[Cc][Nn][Pp][Jj].{0,5}[0-9]{2}\\.[0-9]{3}\\.[0-9]{3}/[0-9]{4}-[0-9]{2}.*")
or (claude_code.events.tool_parameters rlike ".*[Cc][Nn][Pp][Jj].{0,5}[0-9]{2}\\.[0-9]{3}\\.[0-9]{3}/[0-9]{4}-[0-9]{2}.*")
or (claude_code.events.prompt_text rlike ".*[Cc][Nn][Pp][Jj].{0,5}[0-9]{2}\\.[0-9]{3}\\.[0-9]{3}/[0-9]{4}-[0-9]{2}.*")
| eval Esql.is_ssn = (claude_code.events.tool_input rlike ".*[Ss][Ss][Nn].{0,5}[0-9]{3}-[0-9]{2}-[0-9]{4}.*")
or (claude_code.events.tool_parameters rlike ".*[Ss][Ss][Nn].{0,5}[0-9]{3}-[0-9]{2}-[0-9]{4}.*")
or (claude_code.events.prompt_text rlike ".*[Ss][Ss][Nn].{0,5}[0-9]{3}-[0-9]{2}-[0-9]{4}.*")
| eval Esql.is_dni = (claude_code.events.tool_input rlike ".*[Dd][Nn][Ii].{0,5}[0-9]{8}[A-Za-z].*")
or (claude_code.events.tool_parameters rlike ".*[Dd][Nn][Ii].{0,5}[0-9]{8}[A-Za-z].*")
or (claude_code.events.prompt_text rlike ".*[Dd][Nn][Ii].{0,5}[0-9]{8}[A-Za-z].*")
| eval Esql.is_nie = (claude_code.events.tool_input rlike ".*[Nn][Ii][Ee].{0,5}[XxYyZz][0-9]{7}[A-Za-z].*")
or (claude_code.events.tool_parameters rlike ".*[Nn][Ii][Ee].{0,5}[XxYyZz][0-9]{7}[A-Za-z].*")
or (claude_code.events.prompt_text rlike ".*[Nn][Ii][Ee].{0,5}[XxYyZz][0-9]{7}[A-Za-z].*")

| where Esql.is_cpf or Esql.is_cnpj or Esql.is_ssn or Esql.is_dni or Esql.is_nie

| eval Esql.pii_type = case(Esql.is_cpf, "CPF (pt-BR)", Esql.is_cnpj, "CNPJ (pt-BR)", Esql.is_ssn, "SSN (en-US)", Esql.is_dni, "DNI (es)", "NIE (es)")

| keep _id, _version, _index, @timestamp, Esql.pii_type, user.email, user.id, claude_code.events.session.id,
claude_code.events.prompt_text, claude_code.events.tool_input, claude_code.events.tool_parameters,
event.action, process.command_line, file.path
```

**New fields required in ECS/data sources for this rule?**

None. This rule uses only fields already registered and populated by the real `claude_code` integration today (`claude_code.events.tool_input`, `claude_code.events.tool_parameters`, `claude_code.events.prompt_text`, `claude_code.events.session.id`, `event.action`, `user.email`, `user.id`, `process.command_line`, `file.path`), confirmed by reading the integration's actual `fields.yml` and ingest pipeline source, and by shipping real OTel telemetry through the real installed pipeline.

**Related issues or PRs**

None found. This appears to be the first rule proposal touching the `claude_code`/`anthropic` integration family in this repo.

**References**
- https://code.claude.com/docs/en/monitoring-usage
- https://owasp.org/www-project-top-10-for-large-language-model-applications/
- https://atlas.mitre.org/techniques/AML.T0024

**Redacted Example Data**

Real event from the local test stack (synthetic test data, a fake, checksum-valid Brazilian CPF, not a real person's data):
```json
{"@timestamp": "2026-08-02T15:51:08Z", "event.action": "tool_result", "user.email": "redacted@example.com", "user.id": "[redacted-user-hash]", "claude_code.events.session.id": "aae50ec1-6745-4dbb-9081-5912365acc3e", "claude_code.events.tool_input": "{\"command\":\"echo \\\"Nome: Maria Silva, CPF: 123.456.789-09, Cidade: Sao Paulo\\\" >> records.txt\",\"description\":\"Append customer record to records.txt\"}", "process.command_line": "echo \"Nome: Maria Silva, CPF: 123.456.789-09, Cidade: Sao Paulo\" >> records.txt"}
```

Contributor guide

Open the contributing guide

Research direction

Start by reviewing the supplied ES|QL query and the claude_code integration's fields.yml and ingest pipeline source. Compare the proposal with existing ES|QL rules in the repository and validate it against the described telemetry cases. Done means the rule detects labeled CPF, CNPJ, SSN, DNI, and NIE values in prompts or tool data without matching the listed unlabeled false positives.

Written by the indexing model from the issue text.

Assessment

Tech stack
elasticsearch
Domain
security
Issue type
Feature
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.