Detection rules › Kusto

StealthTalk - After hours work

Status
available
Severity
low
Time window
2d
Group by
UserId
Source
github.com/Azure/Azure-Sentinel

Identifies systematic off-hours activity for a single StealthTalk user - repeated authentications outside the user's configured working hours OR on weekends, observed across at least two distinct calendar days within a 48-hour window. The pattern is a common indicator of credential misuse, insider threat, or compromise of the account by an attacker operating in a different timezone. An "off-hours event" is one where IsWeekend=true OR DeviationMinutes >= 180 (i.e. >= 3 hours after the configured working-hours end). Three or more such events on at least two distinct days are required for an incident to fire - a single late-evening login is not enough.

MITRE ATT&CK coverage

TacticTechniques
Initial AccessT1078 Valid Accounts
PersistenceT1078 Valid Accounts
StealthT1078 Valid Accounts

Rule body kusto

id: e3a8b2f1-5c7d-4d89-9b6e-0f1a2c3d4e5f
name: StealthTalk - After hours work
description: |
  Identifies systematic off-hours activity for a single StealthTalk user - repeated authentications
  outside the user's configured working hours OR on weekends, observed across at least two
  distinct calendar days within a 48-hour window. The pattern is a common indicator of credential
  misuse, insider threat, or compromise of the account by an attacker operating in a different
  timezone.

  An "off-hours event" is one where IsWeekend=true OR DeviationMinutes >= 180 (i.e. >= 3 hours
  after the configured working-hours end). Three or more such events on at least two distinct
  days are required for an incident to fire - a single late-evening login is not enough.
severity: Low
requiredDataConnectors:
  - connectorId: StealthTalkAnomalousAuth
    dataTypes:
      - StealthTalkAnomalousAuth_CL
queryFrequency: 1h
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - DefenseEvasion
  - Persistence
relevantTechniques:
  - T1078
query: |
  let LookbackPeriod    = 48h;
  let MinAttempts       = 3;
  let MinDistinctDays   = 2;
  let OffHoursThreshold = 180;
  StealthTalkAnomalousAuth_CL
  | where TimeGenerated >= ago(LookbackPeriod)
  | where EventType == "OffHoursLogin"
  | where IsWeekend == true or DeviationMinutes >= OffHoursThreshold
  | summarize
      AttemptCount   = count(),
      DistinctDays   = dcount(startofday(TimeGenerated)),
      FirstSeen      = min(TimeGenerated),
      LastSeen       = max(TimeGenerated),
      DeviceIds      = make_set(DeviceId),
      MaxDeviation   = max(DeviationMinutes),
      WeekendsCount  = countif(IsWeekend == true),
      AppVersions    = make_set(AppVersion)
    by UserId
  | where AttemptCount >= MinAttempts and DistinctDays >= MinDistinctDays
  | extend
      AlertName    = "AfterHoursWork",
      AlertDetails = strcat(
          "User ", UserId,
          " performed ", AttemptCount, " off-hours logins",
          " across ", DistinctDays, " distinct days.",
          " Max deviation from working hours: ", MaxDeviation, " min.",
          " Weekend logins: ", WeekendsCount, "."
      )
  | project
      TimeGenerated = LastSeen,
      UserId, AttemptCount, DistinctDays, MaxDeviation,
      WeekendsCount, DeviceIds, AppVersions, FirstSeen, LastSeen,
      AlertName, AlertDetails
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: UserId
customDetails:
  AttemptCount: AttemptCount
  DistinctDays: DistinctDays
  MaxDeviation: MaxDeviation
  WeekendsCount: WeekendsCount
  FirstSeen: FirstSeen
  LastSeen: LastSeen
alertDetailsOverride:
  alertDisplayNameFormat: 'StealthTalk: After-Hours Work - {{UserId}} ({{AttemptCount}} events / {{DistinctDays}} days)'
  alertDescriptionFormat: '{{AlertDetails}}'
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: Selected
    groupByEntities:
      - Account
suppressionEnabled: false
suppressionDuration: 5h
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let LookbackPeriod = 48h;
let MinAttempts = 3;
let MinDistinctDays = 2;
let OffHoursThreshold = 180;

Stage 1: source

StealthTalkAnomalousAuth_CL

Stage 2: where

| where TimeGenerated >= ago(LookbackPeriod)

Stage 3: where

| where EventType == "OffHoursLogin"

Stage 4: where

| where IsWeekend == true or DeviationMinutes >= OffHoursThreshold

Stage 5: summarize

| summarize
    AttemptCount   = count(),
    DistinctDays   = dcount(startofday(TimeGenerated)),
    FirstSeen      = min(TimeGenerated),
    LastSeen       = max(TimeGenerated),
    DeviceIds      = make_set(DeviceId),
    MaxDeviation   = max(DeviationMinutes),
    WeekendsCount  = countif(IsWeekend == true),
    AppVersions    = make_set(AppVersion)
  by UserId
Threshold
ge 3

Stage 6: where

| where AttemptCount >= MinAttempts and DistinctDays >= MinDistinctDays

Stage 7: extend

| extend
    AlertName    = "AfterHoursWork",
    AlertDetails = strcat(
        "User ", UserId,
        " performed ", AttemptCount, " off-hours logins",
        " across ", DistinctDays, " distinct days.",
        " Max deviation from working hours: ", MaxDeviation, " min.",
        " Weekend logins: ", WeekendsCount, "."
    )

Stage 8: project

| project
    TimeGenerated = LastSeen,
    UserId, AttemptCount, DistinctDays, MaxDeviation,
    WeekendsCount, DeviceIds, AppVersions, FirstSeen, LastSeen,
    AlertName, AlertDetails

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
AttemptCountge
  • 3 transforms: cased
DeviationMinutesge
  • 180 transforms: cased
DistinctDaysge
  • 2 transforms: cased
EventTypeeq
  • OffHoursLogin transforms: cased
IsWeekendeq
  • true transforms: cased

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
AlertDetailsproject
AlertNameproject
AppVersionsproject
AttemptCountproject
DeviceIdsproject
DistinctDaysproject
FirstSeenproject
LastSeenproject
MaxDeviationproject
TimeGeneratedproject
UserIdproject
WeekendsCountproject