Detection rules › Elastic

Potential Credential Discovery via Recursive Grep

Status
production
Severity
high
Time window
9m
Group by
Esql.time_bucket, agent.id, host.id, host.name, process.name, process.parent.name, user.name
Author
Elastic
Source
github.com/elastic/detection-rules

Identifies recursive grep activity on Linux or macOS where the command line suggests hunting for secrets, credentials, keys, tokens, or sensitive paths (for example .env, .git, .aws). Events are aggregated per host, user, parent process, and one-minute window, the rule surfaces activity only when at least three distinct grep command lines match in the same bucket, to reduce noise from one-off searches.

MITRE ATT&CK coverage

Rule body elastic

[metadata]
creation_date = "2026/03/25"
integration = ["endpoint"]
maturity = "production"
updated_date = "2026/03/25"

[rule]
author = ["Elastic"]
description = """
Identifies recursive grep activity on Linux or macOS where the command line suggests hunting for secrets, credentials,
keys, tokens, or sensitive paths (for example .env, .git, .aws). Events are aggregated per host, user, parent process,
and one-minute window, the rule surfaces activity only when at least three distinct grep command lines match in the same
bucket, to reduce noise from one-off searches.
"""
from = "now-9m"
interval = "5m"
language = "esql"
license = "Elastic License v2"
name = "Potential Credential Discovery via Recursive Grep"
note = """## Triage and analysis

### Investigating Potential Credential Discovery via Recursive Grep

Adversaries and insider threats sometimes use `grep -r` (or `--recursive`, `-R`) across directories to find passwords,
API keys, private keys, cloud tokens, or repository and environment files. This rule looks for `grep`/`egrep` process
starts with recursive flags and command-line patterns associated with credential and secret discovery, then requires
**three or more distinct command lines** in the same one-minute bucket per host, user, and parent process.

### Possible investigation steps

- Review **Esql.cmd_values** for the exact patterns searched (paths, regex, file globs).
- Inspect **Esql.pcmd_values** and **process.parent.name** to see the launch context (interactive shell, script, IDE, CI).
- Confirm whether the user and host normally run security scans, audits, or developer tooling that legitimately greps for secrets.
- If suspicious, search the same host for file access, archive exfiltration, or cloud API use in the surrounding timeframe.

### False positive analysis

- Security scanners, secret scanners (e.g. in CI), and compliance scripts may match. Tune by **parent process**, **user**,
  **working directory**, or organizational allowlists.
- Legitimate searches in documentation for the word "password" can match; the **unique_cmd >= 3** threshold reduces but
  does not eliminate this.

### Response and remediation

- If unauthorized: contain the host, reset or rotate any credentials that may have been exposed, and review VCS and
  cloud audit logs for follow-on abuse.
"""
references = [
    "https://attack.mitre.org/techniques/T1552/001/",
    "https://attack.mitre.org/techniques/T1083/",
]
risk_score = 73
rule_id = "b8e4c2a1-7f3d-4e9b-8c5a-1d0e6f2a4b8c"
severity = "high"
tags = [
    "Domain: Endpoint",
    "OS: Linux",
    "OS: macOS",
    "Use Case: Threat Detection",
    "Tactic: Credential Access",
    "Tactic: Discovery",
    "Resources: Investigation Guide",
    "Data Source: Elastic Defend",
]
timestamp_override = "event.ingested"
type = "esql"

query = '''
from logs-endpoint.events.process-* metadata _id, _version, _index
| where host.os.type in ("linux", "macos")
  and event.category == "process"
  and process.name in ("grep", "egrep")
  and (to_lower(process.command_line) like "* -r*" or to_lower(process.command_line) like "*--recursive*")
  and (
    process.command_line like "*password*"
    or process.command_line like "*passwd*"
    or process.command_line like "*pwd*"
    or process.command_line like "*secret*"
    or process.command_line like "*token*"
    or process.command_line like "*apikey*"
    or process.command_line like "*api_key*"
    or process.command_line like "*api.key*"
    or process.command_line like "*access_key*"
    or process.command_line like "*private_key*"
    or process.command_line like "*client_secret*"
    or process.command_line like "*credential*"
    or process.command_line like "*auth*"
    or process.command_line like "*bearer*"
    or process.command_line like "*BEGIN*PRIVATE*KEY*"
    or process.command_line like "*ssh-rsa*"
    or process.command_line like "*ghp_*"
    or process.command_line like "*github_pat*"
    or process.command_line like "*xoxb-*"
    or process.command_line like "*hooks.slack.com*"
    or process.command_line like "*discord.com/api/webhooks*"
    or process.command_line like "*/.aws/*"
    or process.command_line like "*/.git/*"
    or process.command_line like "*/.env*"
  )
  and (process.parent.command_line is null or not (to_lower(process.parent.command_line) like "*shell-snapshots*" and process.parent.name in ("bash", "sh", "zsh")))
| eval Esql.time_bucket = date_trunc(1 minute, @timestamp)
| stats Esql.unique_cmd = count_distinct(process.command_line),
        Esql.cmd_values = values(process.command_line),
        Esql.pcmd_values = values(process.parent.command_line)
  by process.name, host.id, host.name, agent.id, process.parent.name, user.name, Esql.time_bucket
| where Esql.unique_cmd >= 3
| keep host.id, host.name, agent.id, user.name, process.parent.name, Esql.*
'''

[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1552"
name = "Unsecured Credentials"
reference = "https://attack.mitre.org/techniques/T1552/"
[[rule.threat.technique.subtechnique]]
id = "T1552.001"
name = "Credentials In Files"
reference = "https://attack.mitre.org/techniques/T1552/001/"

[rule.threat.tactic]
id = "TA0006"
name = "Credential Access"
reference = "https://attack.mitre.org/tactics/TA0006/"

[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1083"
name = "File and Directory Discovery"
reference = "https://attack.mitre.org/techniques/T1083/"

[rule.threat.tactic]
id = "TA0007"
name = "Discovery"
reference = "https://attack.mitre.org/tactics/TA0007/"

Stages and Predicates

Stage 1: from

from logs-endpoint.events.process-* metadata _id, _version, _index

Stage 2: where

| where host.os.type in ("linux", "macos")
  and event.category == "process"
  and process.name in ("grep", "egrep")
  and (to_lower(process.command_line) like "* -r*" or to_lower(process.command_line) like "*--recursive*")
  and (
    process.command_line like "*password*"
    or process.command_line like "*passwd*"
    or process.command_line like "*pwd*"
    or process.command_line like "*secret*"
    or process.command_line like "*token*"
    or process.command_line like "*apikey*"
    or process.command_line like "*api_key*"
    or process.command_line like "*api.key*"
    or process.command_line like "*access_key*"
    or process.command_line like "*private_key*"
    or process.command_line like "*client_secret*"
    or process.command_line like "*credential*"
    or process.command_line like "*auth*"
    or process.command_line like "*bearer*"
    or process.command_line like "*BEGIN*PRIVATE*KEY*"
    or process.command_line like "*ssh-rsa*"
    or process.command_line like "*ghp_*"
    or process.command_line like "*github_pat*"
    or process.command_line like "*xoxb-*"
    or process.command_line like "*hooks.slack.com*"
    or process.command_line like "*discord.com/api/webhooks*"
    or process.command_line like "*/.aws/*"
    or process.command_line like "*/.git/*"
    or process.command_line like "*/.env*"
  )
  and (process.parent.command_line is null or not (to_lower(process.parent.command_line) like "*shell-snapshots*" and process.parent.name in ("bash", "sh", "zsh")))

Stage 3: eval

| eval Esql.time_bucket = date_trunc(1 minute, @timestamp)

Stage 4: stats

| stats Esql.unique_cmd = count_distinct(process.command_line),
        Esql.cmd_values = values(process.command_line),
        Esql.pcmd_values = values(process.parent.command_line)
  by process.name, host.id, host.name, agent.id, process.parent.name, user.name, Esql.time_bucket

Stage 5: where

| where Esql.unique_cmd >= 3

Stage 6: keep

| keep host.id, host.name, agent.id, user.name, process.parent.name, Esql.*

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
Esql.unique_cmdge
  • 3
event.categoryeq
  • process
process.command_linewildcard
  • */.aws/*
  • */.env*
  • */.git/*
  • *BEGIN*PRIVATE*KEY*
  • *access_key*
  • *api.key*
  • *api_key*
  • *apikey*
  • *auth*
  • *bearer*
  • *client_secret*
  • *credential*
  • *discord.com/api/webhooks*
  • *ghp_*
  • *github_pat*
  • *hooks.slack.com*
  • *passwd*
  • *password*
  • *private_key*
  • *pwd*
  • *secret*
  • *ssh-rsa*
  • *token*
  • *xoxb-*
process.namein
  • egrep
  • grep
process.parent.command_lineis_null
  • (no value, null check)

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
host.idKEEP host.id
host.nameKEEP host.name
agent.idKEEP agent.id
user.nameKEEP user.name
process.parent.nameKEEP process.parent.name
Esql.*KEEP Esql.*