Detection rules › Elastic

Multi-Cloud CLI Token and Credential Access Commands

Status
production
Severity
high
Time window
6m
Group by
host.id, host.name, user.name
Author
Elastic
Source
github.com/elastic/detection-rules

Correlates process telemetry for shells and major cloud/Kubernetes CLIs when command lines match token or credential material access patterns (GCP, Azure, AWS, GitHub, kubectl, DigitalOcean, OCI). Flags hosts where multiple cloud targets appear within a five-minute window.

MITRE ATT&CK coverage

Rule body elastic

[metadata]
creation_date = "2026/04/29"
integration = ["endpoint", "windows", "system"]
maturity = "production"
updated_date = "2026/04/29"

[rule]
author = ["Elastic"]
description = """
Correlates process telemetry for shells and major cloud/Kubernetes CLIs when command lines match token or credential
material access patterns (GCP, Azure, AWS, GitHub, kubectl, DigitalOcean, OCI). Flags hosts where multiple cloud
targets appear within a five-minute window.
"""
false_positives = [
    """
    Automation, CI runners, and platform engineering scripts may legitimately print tokens or dump kubeconfig across
    providers in one session. Baseline approved identities and runner images before tuning thresholds.
    """,
]
from = "now-6m"
language = "esql"
license = "Elastic License v2"
name = "Multi-Cloud CLI Token and Credential Access Commands"
note = """## Triage and analysis

### Investigating Multi-Cloud CLI Token and Credential Access Commands

Each result row summarizes activity for one host, user, and five-minute time bucket. Review `Esql.process_command_line_values` for the
exact invocations and confirm whether the session was interactive, automated, or tied to a known pipeline.

### Possible investigation steps

- Map `Esql.cloud_targets` and `Esql.unique_clouds` to the underlying `process.command_line` values and parent
  executables.
- Correlate with authentication, Kubernetes audit, and cloud API logs for misuse of printed tokens.
- Identify whether the parent chain indicates a remote shell, RMM, or scheduled task.

### Response and remediation

- If unauthorized, isolate the host, invalidate any printed material at the identity provider, and hunt for lateral
  movement using the same time window as the alert.

**GCP (gcloud / application-default credentials)**

- Sign the user or build identity out of local gcloud sessions on the affected machine (example host session):

  `gcloud auth revoke --all`

- Remove leaked Application Default Credentials on that host (often used by client libraries):

  `gcloud auth application-default revoke`

- If a user OAuth refresh token or service account key was exposed, revoke or rotate it in Google Cloud Console (IAM
  and admin: delete compromised keys; for end users, revoke OAuth tokens under Security or Workspace admin tools as
  applicable).

**Azure (`az` / `azd`)**

- Clear cached CLI sessions on the host so new tokens are not silently reusable from disk:

  `az logout`

  `az account clear`

- If `az account get-access-token`, `Get-AzAccessToken`, or `azd auth token` output was captured, treat the bearer as
  compromised: rotate the underlying secret (for example app registration client secret or federated credential),
  revoke sessions in Microsoft Entra ID where supported, and enforce re-authentication with Conditional Access.

**GitHub (`gh` / PATs)**

- Remove the GitHub CLI session from the affected profile:

  `gh auth logout`

- If a personal access token or fine-grained token was printed, revoke it under GitHub user or organization settings
  (Developer settings → Personal access tokens), and rotate any secrets or deploy keys that were readable with that
  token.

For all providers, prefer provider-console revocation and rotation when a token string left the trust boundary; local
`logout`/`revoke` alone does not invalidate tokens that were already copied off-host.
"""
references = [
    "https://attack.mitre.org/techniques/T1528/",
    "https://attack.mitre.org/techniques/T1552/",
]
risk_score = 73
rule_id = "2b9a3b7a-0891-4a89-abbe-dca753c403cd"
severity = "high"
tags = [
    "Domain: Endpoint",
    "Domain: Cloud",
    "OS: Windows",
    "OS: Linux",
    "OS: macOS",
    "Use Case: Threat Detection",
    "Tactic: Credential Access",
    "Data Source: Elastic Defend",
    "Data Source: Windows Security Event Logs",
    "Data Source: Sysmon",
    "Resources: Investigation Guide"
]
timestamp_override = "event.ingested"
type = "esql"
query = '''
FROM logs-endpoint.events.process-*, logs-system.security-*, logs-windows.sysmon_operational-* METADATA _id, _index, _version
| WHERE event.category == "process" AND KQL(""" event.type : "start" and not event.action : "fork" """)
  AND process.command_line IS NOT NULL 
  AND (
    TO_LOWER(process.name) IN (
      "cmd.exe", "powershell.exe", "pwsh.exe", 
      "sh", "bash", "zsh", "dash", "fish", "ksh",
      "gcloud", "gcloud.cmd", "az", "az.cmd", "azd", "azd.exe",
      "gh", "gh.exe", "aws", "aws.exe",
      "kubectl", "kubectl.exe",
      "doctl", "doctl.exe",
      "oci", "oci.exe"
    ) OR
    TO_LOWER(process.parent.name) IN (
      "cmd.exe", "powershell.exe", "pwsh.exe",
      "sh", "bash", "zsh", "dash", "fish", "ksh", "bun", "bun.exe", 
      "node", "node.exe", "java", "java.exe"
    )
  )
  AND process.command_line RLIKE """.*(config-helper\s.*--format|auth\s+print-access-token|auth\s+print-identity-token|auth\s+application-default\s+print|get-access-token\s.*--output|Get-AzAccessToken|azd\s+auth\s+token|az\s+account\s+get-access-token|gh\s+auth\s+(token|status)|aws\s+sts\s+(get-session-token|get-caller-identity|assume-role)|aws\s+configure\s+(export-credentials|list)|kubectl\s+config\s+view\s.*--raw|kubectl\s+get\s+secret|doctl\s+auth\s+(list|init)|oci\s+session\s+authenticate|oci\s+iam\s.*token).*"""
| EVAL cloud_target = CASE(
    process.command_line RLIKE ".*(gcloud|config-helper|print-access-token|print-identity-token).*", "GCP",
    process.command_line RLIKE ".*(azd auth|az account|Get-AzAccessToken).*", "AZURE",
    process.command_line RLIKE ".*(aws sts|aws configure).*", "AWS",
    process.command_line RLIKE ".*(gh auth).*", "GITHUB",
    process.command_line RLIKE ".*(kubectl config|kubectl get secret).*", "KUBERNETES",
    process.command_line RLIKE ".*(doctl).*", "DIGITALOCEAN",
    process.command_line RLIKE ".*(oci session|oci iam).*", "ORACLE"
  )
| WHERE cloud_target IS NOT NULL // drop unclassified events before aggregation
| STATS
    Esql.cloud_targets = VALUES(cloud_target),
    Esql.unique_clouds = COUNT_DISTINCT(cloud_target),
    Esql.process_command_line_values = VALUES(process.command_line),
    Esql.process_parent_executable_values = VALUES(process.parent.executable),
    Esql.first_seen = MIN(@timestamp),
    Esql.last_seen = MAX(@timestamp),
    Esql.event_count = COUNT(*)
  BY host.name, host.id, user.name
| WHERE Esql.unique_clouds >= 2
| KEEP Esql.*, user.name, host.name, host.id
'''

[[rule.threat]]
framework = "MITRE ATT&CK"

[[rule.threat.technique]]
id = "T1528"
name = "Steal Application Access Token"
reference = "https://attack.mitre.org/techniques/T1528/"

[[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/"

Stages and Predicates

Stage 1: from

FROM logs-endpoint.events.process-*, logs-system.security-*, logs-windows.sysmon_operational-* METADATA _id, _index, _version

Stage 2: where

| WHERE event.category == "process" AND KQL(""" event.type : "start" and not event.action : "fork" """)
  AND process.command_line IS NOT NULL 
  AND (
    TO_LOWER(process.name) IN (
      "cmd.exe", "powershell.exe", "pwsh.exe", 
      "sh", "bash", "zsh", "dash", "fish", "ksh",
      "gcloud", "gcloud.cmd", "az", "az.cmd", "azd", "azd.exe",
      "gh", "gh.exe", "aws", "aws.exe",
      "kubectl", "kubectl.exe",
      "doctl", "doctl.exe",
      "oci", "oci.exe"
    ) OR
    TO_LOWER(process.parent.name) IN (
      "cmd.exe", "powershell.exe", "pwsh.exe",
      "sh", "bash", "zsh", "dash", "fish", "ksh", "bun", "bun.exe", 
      "node", "node.exe", "java", "java.exe"
    )
  )
  AND process.command_line RLIKE """.*(config-helper\s.*--format|auth\s+print-access-token|auth\s+print-identity-token|auth\s+application-default\s+print|get-access-token\s.*--output|Get-AzAccessToken|azd\s+auth\s+token|az\s+account\s+get-access-token|gh\s+auth\s+(token|status)|aws\s+sts\s+(get-session-token|get-caller-identity|assume-role)|aws\s+configure\s+(export-credentials|list)|kubectl\s+config\s+view\s.*--raw|kubectl\s+get\s+secret|doctl\s+auth\s+(list|init)|oci\s+session\s+authenticate|oci\s+iam\s.*token).*"""

Stage 3: eval

| EVAL cloud_target = CASE(
    process.command_line RLIKE ".*(gcloud|config-helper|print-access-token|print-identity-token).*", "GCP",
    process.command_line RLIKE ".*(azd auth|az account|Get-AzAccessToken).*", "AZURE",
    process.command_line RLIKE ".*(aws sts|aws configure).*", "AWS",
    process.command_line RLIKE ".*(gh auth).*", "GITHUB",
    process.command_line RLIKE ".*(kubectl config|kubectl get secret).*", "KUBERNETES",
    process.command_line RLIKE ".*(doctl).*", "DIGITALOCEAN",
    process.command_line RLIKE ".*(oci session|oci iam).*", "ORACLE"
  )
cloud_target =
ifprocess.command_line RLIKE ".*(gcloud|config-helper|print-access-token|print-identity-token).*""GCP"
elifprocess.command_line RLIKE ".*(azd auth|az account|Get-AzAccessToken).*""AZURE"
elifprocess.command_line RLIKE ".*(aws sts|aws configure).*""AWS"
elifprocess.command_line RLIKE ".*(gh auth).*""GITHUB"
elifprocess.command_line RLIKE ".*(kubectl config|kubectl get secret).*""KUBERNETES"
elifprocess.command_line RLIKE ".*(doctl).*""DIGITALOCEAN"
else"ORACLE"

Stage 4: where

| WHERE cloud_target IS NOT NULL

Stage 5: stats

| STATS
    Esql.cloud_targets = VALUES(cloud_target),
    Esql.unique_clouds = COUNT_DISTINCT(cloud_target),
    Esql.process_command_line_values = VALUES(process.command_line),
    Esql.process_parent_executable_values = VALUES(process.parent.executable),
    Esql.first_seen = MIN(@timestamp),
    Esql.last_seen = MAX(@timestamp),
    Esql.event_count = COUNT(*)
  BY host.name, host.id, user.name

Stage 6: where

| WHERE Esql.unique_clouds >= 2

Stage 7: keep

| KEEP Esql.*, user.name, host.name, host.id

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_cloudsge
  • 2
cloud_targetis_not_null
  • (no value, null check)
event.categoryeq
  • process corpus 128 (elastic 128)
process.command_lineis_not_null
  • (no value, null check)
process.command_lineregex_match
  • .*(config-helper\s.*--format|auth\s+print-access-token|auth\s+print-identity-token|auth\s+application-default\s+print|get-access-token\s.*--output|Get-AzAccessToken|azd\s+auth\s+token|az\s+account\s+get-access-token|gh\s+auth\s+(token|status)|aws\s+sts\s+(get-session-token|get-caller-identity|assume-role)|aws\s+configure\s+(export-credentials|list)|kubectl\s+config\s+view\s.*--raw|kubectl\s+get\s+secret|doctl\s+auth\s+(list|init)|oci\s+session\s+authenticate|oci\s+iam\s.*token).*

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
Esql.*KEEP Esql.*
user.nameKEEP user.name
host.nameKEEP host.name
host.idKEEP host.id