Detection rules › Panther

Query.Okta.SWABulkAccessBehavioral

Tags
Okta, SWA, Credential Access, Anomaly Detection, Statistical Analysis
Source
github.com/panther-labs/panther-analysis

Detects Okta SWA bulk credential extraction, abuse, and access from new network sources using behavioral z-score analysis. Reads pre-computed 90-day baselines from the okta_baseline_90d lookup table, then compares recent (last 7 days) admin SWA access and credential extraction patterns against those baselines. DETECTION LOGIC: - Z-score: SWA authentication volume spike (> 3σ above baseline) - Z-score: Unique SWA app diversity spike (accessing many different apps in an hour) (> 3σ) - Z-score: Credential extraction volume spike (> 3σ) - Z-score: Victim diversity spike (targeting many different users) (> 2σ) - Cold-start: First-time bulk SWA access (>= 10 events, no baseline) - Cold-start: First-time credential extraction (>= 5 events, no baseline) - New source: SWA access from IP address not seen in 90-day baseline (requires baseline or >= 3 recent events) - New source: SWA access from user agent not seen in 90-day baseline - Critical compound: New IP + any credential extraction events PREREQUISITE: okta_baseline_90d lookup table must be populated.

MITRE ATT&CK coverage

TacticTechniques
Credential AccessNo specific technique

Rule body yaml

AnalysisType: scheduled_query
QueryName: "Query.Okta.SWABulkAccessBehavioral"
Enabled: false
Description: |
  Detects Okta SWA bulk credential extraction, abuse, and access from new network sources using
  behavioral z-score analysis. Reads pre-computed 90-day baselines from the okta_baseline_90d
  lookup table, then compares recent (last 7 days) admin SWA access and credential extraction
  patterns against those baselines.

  DETECTION LOGIC:
  - Z-score: SWA authentication volume spike (> 3σ above baseline)
  - Z-score: Unique SWA app diversity spike (accessing many different apps in an hour) (> 3σ)
  - Z-score: Credential extraction volume spike (> 3σ)
  - Z-score: Victim diversity spike (targeting many different users) (> 2σ)
  - Cold-start: First-time bulk SWA access (>= 10 events, no baseline)
  - Cold-start: First-time credential extraction (>= 5 events, no baseline)
  - New source: SWA access from IP address not seen in 90-day baseline (requires baseline or >= 3 recent events)
  - New source: SWA access from user agent not seen in 90-day baseline
  - Critical compound: New IP + any credential extraction events

  PREREQUISITE: okta_baseline_90d lookup table must be populated.
Query: |
  -- OKTA SWA BULK CREDENTIAL EXTRACTION BEHAVIORAL DETECTION
  -- Reads 90-day baseline from lookup table; scans only last 7 days of raw logs
  -- Flags: bulk SWA authentication spikes + credential extraction anomalies

  WITH swa_access_recent_hourly AS (
      SELECT
          actor:alternateId::string AS admin_email,
          DATE_TRUNC('hour', published) AS event_hour,
          COUNT(*) AS hourly_swa_events,
          COUNT(DISTINCT target[0]:displayName::string) AS hourly_app_diversity,
          COUNT(DISTINCT client:geographicalContext:country::string) AS hourly_country_diversity,
          COUNT(DISTINCT client:ipAddress::string) AS hourly_ip_diversity
      FROM panther_logs.public.okta_systemlog
      WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '7 days'
          AND eventType = 'user.authentication.sso'
          AND actor:alternateId::string LIKE '%@%'
          -- NOTE: swa_mode and signOnMode are internal Okta debug fields.
          -- Availability varies by tenant. Validate against actual log samples
          -- and adjust conditions if SWA events are not being detected.
          AND (
              debugContext:debugData:swa_mode::string = 'true'
              OR debugContext:debugData:signOnMode::string LIKE '%AUTO%LOGIN%'
              OR debugContext:debugData:signOnMode::string LIKE '%BROWSER%PLUGIN%'
          )
      GROUP BY admin_email, event_hour
  ),

  swa_access_recent_stats AS (
      SELECT
          admin_email,
          SUM(hourly_swa_events) AS recent_total_swa_events,
          MAX(hourly_swa_events) AS recent_max_swa_events_per_hour,
          MAX(hourly_app_diversity) AS recent_max_app_diversity_per_hour,
          MAX(hourly_country_diversity) AS recent_max_country_diversity_per_hour,
          MAX(hourly_ip_diversity) AS recent_max_ip_diversity_per_hour,
          AVG(hourly_swa_events)::FLOAT AS recent_avg_swa_events_per_hour,
          MIN(event_hour) AS recent_swa_first_event,
          MAX(event_hour) AS recent_swa_last_event
      FROM swa_access_recent_hourly
      GROUP BY admin_email
  ),

  recent_sources AS (
      SELECT
          actor:alternateId::string AS admin_email,
          ARRAY_COMPACT(ARRAY_AGG(DISTINCT client:ipAddress::string)) AS recent_ips,
          ARRAY_COMPACT(ARRAY_AGG(DISTINCT client:userAgent:rawUserAgent::string)) AS recent_user_agents
      FROM panther_logs.public.okta_systemlog
      WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '7 days'
          AND actor:alternateId::string LIKE '%@%'
          AND (
              (eventType = 'user.authentication.sso'
                  AND (
                      debugContext:debugData:swa_mode::string = 'true'
                      OR debugContext:debugData:signOnMode::string LIKE '%AUTO%LOGIN%'
                      OR debugContext:debugData:signOnMode::string LIKE '%BROWSER%PLUGIN%'
                  ))
              OR eventType = 'application.user_membership.change_username'
          )
      GROUP BY admin_email
  ),

  extraction_by_ip AS (
      SELECT
          actor:alternateId::string AS admin_email,
          client:ipAddress::string AS source_ip,
          COUNT(*) AS extractions_from_ip,
          COUNT(DISTINCT target[0]:alternateId::string) AS victims_from_ip
      FROM panther_logs.public.okta_systemlog
      WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '7 days'
          AND eventType = 'application.user_membership.change_username'
          AND actor:alternateId::string LIKE '%@%'
      GROUP BY admin_email, source_ip
  ),

  credential_extraction_recent_hourly AS (
      SELECT
          actor:alternateId::string AS admin_email,
          DATE_TRUNC('hour', published) AS event_hour,
          COUNT(*) AS hourly_credential_extractions,
          COUNT(DISTINCT target[0]:alternateId::string) AS hourly_victim_diversity,
          COUNT(DISTINCT client:geographicalContext:country::string) AS hourly_country_diversity
      FROM panther_logs.public.okta_systemlog
      WHERE p_event_time >= CURRENT_TIMESTAMP - INTERVAL '7 days'
          AND eventType = 'application.user_membership.change_username'
          AND actor:alternateId::string LIKE '%@%'
      GROUP BY admin_email, event_hour
  ),

  credential_extraction_recent_stats AS (
      SELECT
          admin_email,
          SUM(hourly_credential_extractions) AS recent_total_extractions,
          MAX(hourly_credential_extractions) AS recent_max_extractions_per_hour,
          MAX(hourly_victim_diversity) AS recent_max_victim_diversity_per_hour,
          MAX(hourly_country_diversity) AS recent_max_extraction_country_diversity_per_hour,
          AVG(hourly_credential_extractions)::FLOAT AS recent_avg_extractions_per_hour,
          MIN(event_hour) AS recent_extraction_first_event,
          MAX(event_hour) AS recent_extraction_last_event
      FROM credential_extraction_recent_hourly
      GROUP BY admin_email
  ),

  -- Pre-compute new-IP extraction stats to avoid repeated correlated subqueries
  new_ip_extraction_stats AS (
      SELECT
          eip.admin_email,
          SUM(eip.extractions_from_ip) AS new_ip_extraction_count,
          SUM(eip.victims_from_ip) AS new_ip_victim_count
      FROM extraction_by_ip eip
      LEFT JOIN panther_lookups.public.okta_baseline_90d b ON eip.admin_email = b.user_email
      WHERE b.swa_known_ips IS NULL
          OR NOT ARRAY_CONTAINS(eip.source_ip::variant, b.swa_known_ips)
      GROUP BY eip.admin_email
  ),

  admin_anomalies AS (
      SELECT
          COALESCE(sr.admin_email, cr.admin_email) AS admin_email,

          -- SWA ACCESS: BASELINE from lookup table
          COALESCE(b.baseline_total_swa_events, 0) AS baseline_total_swa_events,
          COALESCE(b.baseline_active_days_swa, 0) AS baseline_active_days_swa,
          ROUND(COALESCE(b.mean_swa_events_per_hour, 0), 2) AS baseline_mean_swa_events_per_hour,
          ROUND(COALESCE(b.stddev_swa_events_per_hour, 0), 2) AS baseline_stddev_swa_events_per_hour,
          ROUND(COALESCE(b.mean_app_diversity_per_hour, 0), 2) AS baseline_mean_app_diversity_per_hour,
          ROUND(COALESCE(b.stddev_app_diversity_per_hour, 0), 2) AS baseline_stddev_app_diversity_per_hour,
          ROUND(COALESCE(b.mean_country_diversity_per_hour, 0), 2) AS baseline_mean_country_diversity_per_hour,

          -- SWA ACCESS: RECENT ACTIVITY
          COALESCE(sr.recent_total_swa_events, 0) AS recent_total_swa_events,
          COALESCE(sr.recent_max_swa_events_per_hour, 0) AS recent_max_swa_events_per_hour,
          COALESCE(sr.recent_max_app_diversity_per_hour, 0) AS recent_max_app_diversity_per_hour,
          COALESCE(sr.recent_max_country_diversity_per_hour, 0) AS recent_max_country_diversity_per_hour,
          COALESCE(sr.recent_max_ip_diversity_per_hour, 0) AS recent_max_ip_diversity_per_hour,
          ROUND(COALESCE(sr.recent_avg_swa_events_per_hour, 0), 2) AS recent_avg_swa_events_per_hour,
          sr.recent_swa_first_event,
          sr.recent_swa_last_event,

          -- CREDENTIAL EXTRACTION: BASELINE from lookup table
          COALESCE(b.baseline_total_extractions, 0) AS baseline_total_extractions,
          COALESCE(b.baseline_active_days_extraction, 0) AS baseline_active_days_extraction,
          ROUND(COALESCE(b.mean_extractions_per_hour, 0), 2) AS baseline_mean_extractions_per_hour,
          ROUND(COALESCE(b.stddev_extractions_per_hour, 0), 2) AS baseline_stddev_extractions_per_hour,
          ROUND(COALESCE(b.mean_victim_diversity_per_hour, 0), 2) AS baseline_mean_victim_diversity_per_hour,
          ROUND(COALESCE(b.stddev_victim_diversity_per_hour, 0), 2) AS baseline_stddev_victim_diversity_per_hour,

          -- CREDENTIAL EXTRACTION: RECENT ACTIVITY
          COALESCE(cr.recent_total_extractions, 0) AS recent_total_extractions,
          COALESCE(cr.recent_max_extractions_per_hour, 0) AS recent_max_extractions_per_hour,
          COALESCE(cr.recent_max_victim_diversity_per_hour, 0) AS recent_max_victim_diversity_per_hour,
          COALESCE(cr.recent_max_extraction_country_diversity_per_hour, 0) AS recent_max_extraction_country_diversity_per_hour,
          ROUND(COALESCE(cr.recent_avg_extractions_per_hour, 0), 2) AS recent_avg_extractions_per_hour,
          cr.recent_extraction_first_event,
          cr.recent_extraction_last_event,

          -- Z-SCORES: SWA ACCESS
          CASE
              WHEN b.baseline_total_swa_events >= 3
              THEN ROUND(
                  (COALESCE(sr.recent_max_swa_events_per_hour, 0) - b.mean_swa_events_per_hour) /
                  NULLIF(b.stddev_swa_events_per_hour, 0),
                  2)
              ELSE NULL
          END AS z_score_swa_volume,

          CASE
              WHEN b.baseline_total_swa_events >= 3
              THEN ROUND(
                  (COALESCE(sr.recent_max_app_diversity_per_hour, 0) - b.mean_app_diversity_per_hour) /
                  NULLIF(b.stddev_app_diversity_per_hour, 0),
                  2)
              ELSE NULL
          END AS z_score_app_diversity,

          CASE
              WHEN b.baseline_total_swa_events >= 3
              THEN ROUND(
                  (COALESCE(sr.recent_max_country_diversity_per_hour, 0) - b.mean_country_diversity_per_hour) /
                  NULLIF(b.stddev_country_diversity_per_hour, 0),
                  2)
              ELSE NULL
          END AS z_score_swa_country_diversity,

          -- Z-SCORES: CREDENTIAL EXTRACTION
          CASE
              WHEN b.baseline_total_extractions >= 3
              THEN ROUND(
                  (COALESCE(cr.recent_max_extractions_per_hour, 0) - b.mean_extractions_per_hour) /
                  NULLIF(b.stddev_extractions_per_hour, 0),
                  2)
              ELSE NULL
          END AS z_score_extraction_volume,

          CASE
              WHEN b.baseline_total_extractions >= 3
              THEN ROUND(
                  (COALESCE(cr.recent_max_victim_diversity_per_hour, 0) - b.mean_victim_diversity_per_hour) /
                  NULLIF(b.stddev_victim_diversity_per_hour, 0),
                  2)
              ELSE NULL
          END AS z_score_victim_diversity,

          -- NEW SOURCE FLAGS
          CASE
              WHEN rs.admin_email IS NULL THEN FALSE
              WHEN b.swa_known_ips IS NULL THEN TRUE
              WHEN ARRAY_SIZE(ARRAY_EXCEPT(COALESCE(rs.recent_ips, ARRAY_CONSTRUCT()), b.swa_known_ips)) > 0 THEN TRUE
              ELSE FALSE
          END AS has_new_ip,

          CASE
              WHEN rs.admin_email IS NULL THEN FALSE
              WHEN b.swa_known_user_agents IS NULL THEN TRUE
              WHEN ARRAY_SIZE(ARRAY_EXCEPT(COALESCE(rs.recent_user_agents, ARRAY_CONSTRUCT()), b.swa_known_user_agents)) > 0 THEN TRUE
              ELSE FALSE
          END AS has_new_user_agent,

          CASE
              WHEN rs.admin_email IS NULL THEN 0
              WHEN b.swa_known_ips IS NULL THEN ARRAY_SIZE(COALESCE(rs.recent_ips, ARRAY_CONSTRUCT()))
              ELSE ARRAY_SIZE(ARRAY_EXCEPT(COALESCE(rs.recent_ips, ARRAY_CONSTRUCT()), b.swa_known_ips))
          END AS new_ip_count,

          COALESCE(niex.new_ip_extraction_count, 0) AS new_ip_extraction_count,
          COALESCE(niex.new_ip_victim_count, 0) AS new_ip_victim_count,

          -- ANOMALY FLAGS: Z-SCORE BASED
          CASE
              WHEN b.baseline_total_swa_events >= 3
                  AND (COALESCE(sr.recent_max_swa_events_per_hour, 0) - b.mean_swa_events_per_hour) /
                      NULLIF(b.stddev_swa_events_per_hour, 0) > 3
              THEN TRUE ELSE FALSE
          END AS is_swa_volume_anomaly,

          CASE
              WHEN b.baseline_total_swa_events >= 3
                  AND (COALESCE(sr.recent_max_app_diversity_per_hour, 0) - b.mean_app_diversity_per_hour) /
                      NULLIF(b.stddev_app_diversity_per_hour, 0) > 3
              THEN TRUE ELSE FALSE
          END AS is_app_diversity_anomaly,

          CASE
              WHEN b.baseline_total_extractions >= 3
                  AND (COALESCE(cr.recent_max_extractions_per_hour, 0) - b.mean_extractions_per_hour) /
                      NULLIF(b.stddev_extractions_per_hour, 0) > 3
              THEN TRUE ELSE FALSE
          END AS is_extraction_volume_anomaly,

          CASE
              WHEN b.baseline_total_extractions >= 3
                  AND (COALESCE(cr.recent_max_victim_diversity_per_hour, 0) - b.mean_victim_diversity_per_hour) /
                      NULLIF(b.stddev_victim_diversity_per_hour, 0) > 2
              THEN TRUE ELSE FALSE
          END AS is_victim_diversity_anomaly,

          -- COLD START FLAGS
          CASE
              WHEN (b.baseline_total_swa_events IS NULL OR b.baseline_total_swa_events < 3)
                  AND sr.recent_total_swa_events >= 10
              THEN TRUE ELSE FALSE
          END AS is_first_time_bulk_swa_access,

          CASE
              WHEN (b.baseline_total_extractions IS NULL OR b.baseline_total_extractions < 3)
                  AND cr.recent_total_extractions >= 5
              THEN TRUE ELSE FALSE
          END AS is_first_time_credential_extraction,

          -- OVERALL ANOMALY FLAG
          CASE
              WHEN (
                  (b.baseline_total_swa_events >= 3
                      AND (COALESCE(sr.recent_max_swa_events_per_hour, 0) - b.mean_swa_events_per_hour) /
                          NULLIF(b.stddev_swa_events_per_hour, 0) > 3)
                  OR
                  (b.baseline_total_swa_events >= 3
                      AND (COALESCE(sr.recent_max_app_diversity_per_hour, 0) - b.mean_app_diversity_per_hour) /
                          NULLIF(b.stddev_app_diversity_per_hour, 0) > 3)
                  OR
                  (b.baseline_total_extractions >= 3
                      AND (COALESCE(cr.recent_max_extractions_per_hour, 0) - b.mean_extractions_per_hour) /
                          NULLIF(b.stddev_extractions_per_hour, 0) > 3)
                  OR
                  (b.baseline_total_extractions >= 3
                      AND (COALESCE(cr.recent_max_victim_diversity_per_hour, 0) - b.mean_victim_diversity_per_hour) /
                          NULLIF(b.stddev_victim_diversity_per_hour, 0) > 2)
                  OR
                  ((b.baseline_total_swa_events IS NULL OR b.baseline_total_swa_events < 3)
                      AND sr.recent_total_swa_events >= 10)
                  OR
                  ((b.baseline_total_extractions IS NULL OR b.baseline_total_extractions < 3)
                      AND cr.recent_total_extractions >= 5)
                  OR
                  -- New IP: requires baseline to exist (known IPs list present) + new IP detected,
                  -- OR no baseline yet but minimum activity threshold to suppress new-employee noise
                  (
                      rs.admin_email IS NOT NULL
                      AND (
                          (b.swa_known_ips IS NOT NULL
                           AND ARRAY_SIZE(ARRAY_EXCEPT(COALESCE(rs.recent_ips, ARRAY_CONSTRUCT()), b.swa_known_ips)) > 0)
                          OR
                          (b.swa_known_ips IS NULL
                           AND (COALESCE(sr.recent_total_swa_events, 0) >= 3
                                OR COALESCE(cr.recent_total_extractions, 0) >= 1))
                      )
                      AND (COALESCE(sr.recent_total_swa_events, 0) > 0
                           OR COALESCE(cr.recent_total_extractions, 0) > 0)
                  )
                  OR
                  -- New user agent with any activity (requires existing baseline to avoid over-firing)
                  (
                      rs.admin_email IS NOT NULL
                      AND b.swa_known_user_agents IS NOT NULL
                      AND ARRAY_SIZE(ARRAY_EXCEPT(COALESCE(rs.recent_user_agents, ARRAY_CONSTRUCT()), b.swa_known_user_agents)) > 0
                      AND (COALESCE(sr.recent_total_swa_events, 0) > 0
                           OR COALESCE(cr.recent_total_extractions, 0) > 0)
                  )
              ) THEN TRUE
              ELSE FALSE
          END AS is_anomalous,

          -- ANOMALY SEVERITY SCORE
          CASE
              WHEN b.baseline_total_swa_events >= 3 OR b.baseline_total_extractions >= 3
              THEN
                  ROUND(
                      GREATEST(
                          COALESCE((COALESCE(sr.recent_max_swa_events_per_hour, 0) - b.mean_swa_events_per_hour) /
                              NULLIF(b.stddev_swa_events_per_hour, 0), 0),
                          0
                      ) +
                      GREATEST(
                          COALESCE((COALESCE(sr.recent_max_app_diversity_per_hour, 0) - b.mean_app_diversity_per_hour) /
                              NULLIF(b.stddev_app_diversity_per_hour, 0), 0),
                          0
                      ) +
                      GREATEST(
                          COALESCE((COALESCE(cr.recent_max_extractions_per_hour, 0) - b.mean_extractions_per_hour) /
                              NULLIF(b.stddev_extractions_per_hour, 0), 0) * 3,
                          0
                      ) +
                      GREATEST(
                          COALESCE((COALESCE(cr.recent_max_victim_diversity_per_hour, 0) - b.mean_victim_diversity_per_hour) /
                              NULLIF(b.stddev_victim_diversity_per_hour, 0), 0) * 2,
                          0
                      ) +
                      -- New IP + extraction: weight 5x extraction count from new IPs
                      GREATEST(COALESCE(niex.new_ip_extraction_count, 0) * 5.0, 0),
                      2
                  )
              ELSE
                  ROUND(
                      (COALESCE(sr.recent_total_swa_events, 0) * 0.5) +
                      (COALESCE(cr.recent_total_extractions, 0) * 10) +
                      (COALESCE(cr.recent_max_victim_diversity_per_hour, 0) * 5) +
                      COALESCE(niex.new_ip_extraction_count, 0) * 5.0,
                      2
                  )
          END AS anomaly_severity_score

      FROM swa_access_recent_stats sr
      FULL OUTER JOIN credential_extraction_recent_stats cr ON sr.admin_email = cr.admin_email
      LEFT JOIN panther_lookups.public.okta_baseline_90d b
          ON COALESCE(sr.admin_email, cr.admin_email) = b.user_email
      LEFT JOIN recent_sources rs ON COALESCE(sr.admin_email, cr.admin_email) = rs.admin_email
      LEFT JOIN new_ip_extraction_stats niex ON COALESCE(sr.admin_email, cr.admin_email) = niex.admin_email
  )

  SELECT
      admin_email,
      baseline_total_swa_events,
      baseline_active_days_swa,
      baseline_mean_swa_events_per_hour,
      baseline_stddev_swa_events_per_hour,
      baseline_mean_app_diversity_per_hour,
      baseline_stddev_app_diversity_per_hour,
      baseline_mean_country_diversity_per_hour,
      recent_total_swa_events,
      recent_max_swa_events_per_hour,
      recent_max_app_diversity_per_hour,
      recent_max_country_diversity_per_hour,
      recent_max_ip_diversity_per_hour,
      recent_avg_swa_events_per_hour,
      recent_swa_first_event,
      recent_swa_last_event,
      baseline_total_extractions,
      baseline_active_days_extraction,
      baseline_mean_extractions_per_hour,
      baseline_stddev_extractions_per_hour,
      baseline_mean_victim_diversity_per_hour,
      baseline_stddev_victim_diversity_per_hour,
      recent_total_extractions,
      recent_max_extractions_per_hour,
      recent_max_victim_diversity_per_hour,
      recent_max_extraction_country_diversity_per_hour,
      recent_avg_extractions_per_hour,
      recent_extraction_first_event,
      recent_extraction_last_event,
      z_score_swa_volume,
      z_score_app_diversity,
      z_score_swa_country_diversity,
      z_score_extraction_volume,
      z_score_victim_diversity,
      is_swa_volume_anomaly,
      is_app_diversity_anomaly,
      is_extraction_volume_anomaly,
      is_victim_diversity_anomaly,
      is_first_time_bulk_swa_access,
      is_first_time_credential_extraction,
      has_new_ip,
      has_new_user_agent,
      new_ip_count,
      new_ip_extraction_count,
      new_ip_victim_count,
      is_anomalous,
      anomaly_severity_score

  FROM admin_anomalies
  WHERE is_anomalous = TRUE
  ORDER BY anomaly_severity_score DESC
  LIMIT 100
Schedule:
  RateMinutes: 1440  # Run once per day
  TimeoutMinutes: 10
Tags:
  - Okta
  - SWA
  - Credential Access
  - Anomaly Detection
  - Statistical Analysis

Detection logic

Stage 1: source

admin_anomalies

Stage 2: filter

is_anomalous eq "TRUE"

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
is_anomalouseq
  • TRUE

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.

Field
admin_email
baseline_total_swa_events
baseline_active_days_swa
baseline_mean_swa_events_per_hour
baseline_stddev_swa_events_per_hour
baseline_mean_app_diversity_per_hour
baseline_stddev_app_diversity_per_hour
baseline_mean_country_diversity_per_hour
recent_total_swa_events
recent_max_swa_events_per_hour
recent_max_app_diversity_per_hour
recent_max_country_diversity_per_hour
recent_max_ip_diversity_per_hour
recent_avg_swa_events_per_hour
recent_swa_first_event
recent_swa_last_event
baseline_total_extractions
baseline_active_days_extraction
baseline_mean_extractions_per_hour
baseline_stddev_extractions_per_hour
baseline_mean_victim_diversity_per_hour
baseline_stddev_victim_diversity_per_hour
recent_total_extractions
recent_max_extractions_per_hour
recent_max_victim_diversity_per_hour
recent_max_extraction_country_diversity_per_hour
recent_avg_extractions_per_hour
recent_extraction_first_event
recent_extraction_last_event
z_score_swa_volume
z_score_app_diversity
z_score_swa_country_diversity
z_score_extraction_volume
z_score_victim_diversity
is_swa_volume_anomaly
is_app_diversity_anomaly
is_extraction_volume_anomaly
is_victim_diversity_anomaly
is_first_time_bulk_swa_access
is_first_time_credential_extraction
has_new_ip
has_new_user_agent
new_ip_count
new_ip_extraction_count
new_ip_victim_count
is_anomalous
anomaly_severity_score