Panther high experimental python

Okta SWA Off-Hours Credential Access - Behavioral

Detects Okta SWA credential access occurring outside normal business hours using behavioral z-score analysis on temporal patterns. Compromised admin accounts often access SWA credentials at unusual times - late at night, during weekends, or from a different geographic location than normal. This detection builds a 90-day baseline for each admin's temporal credential access patterns, then identifies anomalous shifts toward off-hours, late-night, and weekend activity in the last 7 days. **Detection Logic:** - Z-score: Off-hours ratio spike (> 3σ above normal off-hours proportion) - Z-score: Late-night ratio spike (2 AM - 6 AM accesses) (> 2σ) - Z-score: Weekend ratio spike (> 2σ) - Cold-start: First-time off-hours credential access (>= 3 events, no prior baseline) - Cold-start: First-time late-night access (>= 2 events, no prior baseline) - Cold-start: First-time weekend access (>= 2 events, no prior baseline) - Compound: Geographic shift + off-hours activity (high-confidence indicator) **Why This Matters:** Attackers using stolen admin credentials typically operate at off-hours to avoid detection and minimize interference with active users. A sudden shift in the time distribution of SWA credential accesses is a strong indicator of account compromise. **Complementary Detection:** Use alongside `Okta.SWA.BulkAccess.Behavioral` which detects the same attack vector based on volume rather than temporal patterns.

View Source

Detection Logic

def rule(event):
    # Query already filtered for is_anomalous = TRUE.
    # Guard against malformed rows missing the primary key field.
    return bool(event.get("admin_email"))


def title(event):
    admin = event.get("admin_email", "Unknown")
    is_geo_shift = event.get("is_geographic_shift") or False
    is_cold_start = event.get("is_cold_start") or False
    is_inactive_hour = event.get("is_inactive_hour_anomaly") or False
    baseline_country = event.get("baseline_primary_country", "Unknown")
    recent_country = event.get("recent_primary_country", "Unknown")
    if is_geo_shift and is_inactive_hour:
        return (
            f"Okta SWA: Off-Hours Credential Access with Geographic Shift for {admin}"
            f" ({baseline_country} -> {recent_country})"
        )
    if is_cold_start:
        return f"Okta SWA: Credential Access with No Baseline (Cold Start) for {admin}"
    if is_geo_shift:
        return (
            f"Okta SWA: Geographic Shift in Credential Access for {admin}"
            f" ({baseline_country} -> {recent_country})"
        )
    return f"Okta SWA: Off-Hours Credential Access Anomaly for {admin}"


def severity(event):
    is_geo_shift = event.get("is_geographic_shift") or False
    is_inactive_hour = event.get("is_inactive_hour_anomaly") or False
    is_cold_start = event.get("is_cold_start") or False
    z_score = event.get("z_score_inactive_slot_ratio") or 0
    score = event.get("anomaly_severity_score") or 0
    if is_geo_shift and is_inactive_hour:
        return "CRITICAL"
    if is_geo_shift or is_cold_start or z_score > 6 or score > 20:
        return "HIGH"
    if is_inactive_hour:
        return "MEDIUM"
    return "LOW"


def dedup_key(event):
    admin = event.get("admin_email", "unknown")
    first_event = str(event.get("recent_first_event", "unknown"))[:10]
    return f"okta_swa_offhours_{admin}_{first_event}"


def alert_context(event):
    return {
        "admin_email": event.get("admin_email"),
        "recent_total_credential_access": event.get("recent_total_credential_access"),
        "recent_inactive_slot_events": event.get("recent_inactive_slot_events"),
        "recent_inactive_slot_ratio": event.get("recent_inactive_slot_ratio"),
        "recent_avg_inactive_slot_ratio_per_hour": event.get(
            "recent_avg_inactive_slot_ratio_per_hour"
        ),
        "z_score_inactive_slot_ratio": event.get("z_score_inactive_slot_ratio"),
        "baseline_active_slot_count": event.get("baseline_active_slot_count"),
        "is_inactive_hour_anomaly": event.get("is_inactive_hour_anomaly"),
        "is_cold_start": event.get("is_cold_start"),
        "is_geographic_shift": event.get("is_geographic_shift"),
        "baseline_primary_country": event.get("baseline_primary_country"),
        "recent_primary_country": event.get("recent_primary_country"),
        "anomaly_severity_score": event.get("anomaly_severity_score"),
        "recent_first_event": event.get("recent_first_event"),
        "recent_last_event": event.get("recent_last_event"),
    }

Field Validations

Loading…

Comments (0)

Loading comments...