Query Details

15 SIGNIN Off Hours Privileged Sign In

Query

id: e5f6a7b8-c9d0-4e1f-2a3b-4c5d6e7f8a9b
name: "SigninLogs — Off-Hours Sign-In by Privileged Account"
version: 1.0.0
kind: Scheduled
description: |
  Detects privileged accounts signing in between 22:00 and 05:00 UTC. Off-hours admin access is a key hunting signal for insider threats, living-off-the-land attackers, and compromised admin credentials. Investigate alongside concurrent AuditLogs activity. MITRE ATT&CK: T1078 (Valid Accounts), T1098 (Account Manipulation).
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
queryFrequency: PT1H
queryPeriod: PT1H
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Persistence
relevantTechniques:
  - T1078
  - T1098
query: |

    // ---- Network Allowlist (exclude trusted IPs / CIDR / ranges) --------------
    let _allow = materialize(union isfuzzy=true (print R="" | take 0), (_GetWatchlist('NetworkAllowlist') | project R = tostring(IPOrRange)) | where isnotempty(R));
    let _allowCIDR  = toscalar(_allow | where not(R matches regex @'^\d+\.\d+\.\d+\.\d+-\d+\.\d+\.\d+\.\d+$') | extend R = iff(R has '/', R, strcat(R, '/32')) | summarize make_list(R));
    let _allowRange = toscalar(_allow | where R matches regex @'^\d+\.\d+\.\d+\.\d+-\d+\.\d+\.\d+\.\d+$' | summarize make_list(R));
    let _ExcludeAllowlistedIPs = (T:(IPAddress:string)) {
        T
        | extend IPAddress = tostring(IPAddress)
        | where array_length(_allowCIDR) == 0 or isnull(ipv4_is_in_any_range(IPAddress, _allowCIDR)) or not(ipv4_is_in_any_range(IPAddress, _allowCIDR))
        | mv-apply _r = _allowRange to typeof(string) on (
            extend _lo = tostring(split(_r,'-')[0]), _hi = tostring(split(_r,'-')[1])
            | extend _inRange = ipv4_compare(IPAddress, _lo) >= 0 and ipv4_compare(IPAddress, _hi) <= 0
            | summarize _anyInRange = max(toint(_inRange)))
        | where isnull(_anyInRange) or _anyInRange == 0
        | project-away _anyInRange
    };
    // ---------------------------------------------------------------------------
    let privilegedKeywords = dynamic([
        "admin", "adm-", "adm.", "-adm", "_adm",
        "da-", "ea-", "ga-", "pa-",
        "tier0", "tier-0", "t0-",
        "priv", "svc.adm", "svc-adm",
        "globaladmin", "secadmin", "cloudadmin"
    ]);
    let offHoursStart = 22;
    let offHoursEnd   = 5;
    SigninLogs
    | invoke _ExcludeAllowlistedIPs()
    | where TimeGenerated > ago(1h)
    | where ResultType == "0"
    | where UserPrincipalName has_any (privilegedKeywords)
    | extend HourOfDay = hourofday(TimeGenerated)
    | where HourOfDay >= offHoursStart or HourOfDay < offHoursEnd
    | project
        TimeGenerated, UserPrincipalName,
        IPAddress, Location,
        AppDisplayName, HourOfDay,
        RiskLevelDuringSignIn, ConditionalAccessStatus,
        IsInteractive
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
customDetails:
  HourOfDay: HourOfDay
  AppDisplayName: AppDisplayName
alertDetailsOverride:
  alertDisplayNameFormat: "Off-Hours Privileged Sign-In — {{UserPrincipalName}} at {{HourOfDay}}:00 UTC from {{Location}}"
  alertDescriptionFormat: "Privileged account {{UserPrincipalName}} signed in at {{HourOfDay}}:00 UTC outside business hours. Investigate for unauthorized admin activity."
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT1H
    matchingMethod: AllEntities
    groupByEntities:
  - Account



Explanation

This query is designed to detect when privileged accounts sign in during off-hours, specifically between 10:00 PM and 5:00 AM UTC. Such activity can be a sign of insider threats, attackers using legitimate credentials, or compromised admin accounts. The query focuses on accounts with names that include keywords like "admin," "priv," or "tier0," which are often associated with higher-level access.

Here's a simple breakdown of what the query does:

  1. Network Allowlist: It excludes sign-ins from trusted IP addresses or ranges, which are defined in a watchlist called 'NetworkAllowlist.'

  2. Privileged Keywords: It looks for sign-ins by accounts with names containing certain keywords that suggest they have privileged access.

  3. Off-Hours Detection: It checks if the sign-in occurred between 10:00 PM and 5:00 AM UTC.

  4. Data Filtering: It filters the sign-in logs to include only successful sign-ins (ResultType "0") from the last hour.

  5. Output: The query outputs details like the time of sign-in, user name, IP address, location, application name, and risk level.

  6. Alert Configuration: If any such sign-ins are detected, an alert is generated with details about the sign-in, and an incident is created for further investigation.

The query runs every hour and looks back over the past hour to ensure timely detection of suspicious activity.

Details

David Alonso profile picture

David Alonso

Released: April 20, 2026

Tables

SigninLogs

Keywords

SigninLogsAzureActiveDirectoryAccountIPAddressLocationAppDisplayNameRiskLevelDuringSignInConditionalAccessStatusIsInteractiveNetworkAllowlistUserPrincipalNameTimeGeneratedHourOfDayPrivilegedAccount

Operators

letmaterializeunionisfuzzyprinttakeprojecttostringwhereisnotemptytoscalarmatchesregexextendiffstrcatsummarizemake_listnotarray_lengthisnullipv4_is_in_any_rangemv-applytotypeofonsplitipv4_comparemaxtointproject-awaydynamicSigninLogsinvokeagohas_anyhourofdayorproject

Actions