Query Details

HUNT 26 Risky Anonymizer Signin Timeline

Query

// Hunt     : Hunt - Risky & Anonymizer Sign-in Timeline (30d)
// Tactics  : InitialAccess, DefenseEvasion
// MITRE    : T1078.004, T1090.003
// Purpose  : Broad companion to RULE-19. Timeline of successful sign-ins that AAD Identity
//            Protection flagged as risky or anonymizer-sourced (Tor/anonymous VPN), across both
//            users and workload identities. Use to baseline anonymizer usage, hunt for
//            low-and-slow access under the alert-rule window, and pivot on repeat IPs/ASNs.
//==========================================================================================

let Window = 30d;
let AnonRiskTypes = dynamic(["anonymizedIPAddress", "anonymousIPAddress"]);
let UserRisky = SigninLogs
    | where TimeGenerated > ago(Window)
    | where ResultType == 0
    | where isnotempty(IPAddress)
    | extend RiskTypes = tostring(column_ifexists("RiskEventTypes_V2", dynamic([])))
    | where RiskTypes has_any (AnonRiskTypes)
        or RiskLevelAggregated in~ ("medium", "high")
        or RiskState =~ "atRisk"
    | project TimeGenerated, IdentityKind = "User", Identity = UserPrincipalName,
        IPAddress, Location = tostring(Location), AppDisplayName,
        RiskLevel = tostring(RiskLevelAggregated), RiskTypes,
        ConditionalAccessStatus;
let SPRisky = AADServicePrincipalSignInLogs
    | where TimeGenerated > ago(Window)
    | where ResultType == 0
    | where isnotempty(IPAddress)
    | extend RiskTypes = tostring(column_ifexists("RiskEventTypes_V2", dynamic([])))
    | where RiskTypes has_any (AnonRiskTypes)
    | project TimeGenerated, IdentityKind = "ServicePrincipal", Identity = ServicePrincipalName,
        IPAddress, Location = tostring(LocationDetails.countryOrRegion), AppDisplayName = AppId,
        RiskLevel = "n/a", RiskTypes, ConditionalAccessStatus;
union UserRisky, SPRisky
| order by TimeGenerated desc

Explanation

This query is designed to identify and analyze potentially risky sign-ins over the past 30 days. It focuses on sign-ins flagged by Azure Active Directory (AAD) Identity Protection as risky or originating from anonymized sources like Tor or anonymous VPNs. Here's a simplified breakdown:

  1. Time Frame: The query looks at data from the last 30 days.

  2. Risk Types: It specifically targets sign-ins associated with anonymized IP addresses or those flagged as risky by AAD.

  3. User Sign-ins:

    • It filters successful user sign-ins (where the result was successful) with non-empty IP addresses.
    • It checks if the sign-in was flagged with certain risk types or had a medium/high risk level.
    • It extracts relevant details such as the time of sign-in, user identity, IP address, location, application name, risk level, and conditional access status.
  4. Service Principal Sign-ins:

    • It similarly filters successful sign-ins for service principals with non-empty IP addresses.
    • It checks for the same anonymized risk types.
    • It extracts details like the time of sign-in, service principal identity, IP address, location, application ID, and conditional access status.
  5. Combining Results: The query combines the results from both user and service principal sign-ins.

  6. Sorting: The combined results are sorted by the time of sign-in in descending order, showing the most recent sign-ins first.

The purpose of this query is to establish a baseline for anonymizer usage, identify potential low-and-slow access attempts that might evade alert rules, and investigate repeated IP addresses or Autonomous System Numbers (ASNs) associated with risky sign-ins.

Details

David Alonso profile picture

David Alonso

Released: July 27, 2026

Tables

SigninLogsAADServicePrincipalSignInLogs

Keywords

HuntRiskyAnonymizerSigninTimelineInitialAccessDefenseEvasionMitrePurposeDevicesIntuneUserIdentityProtectionVPNIPsASNsSigninLogsIPAddressUserPrincipalNameLocationAppDisplayNameRiskLevelConditionalAccessStatusAADServicePrincipalSignInLogsServicePrincipalNameLocationDetailsAppId

Operators

letdynamicSigninLogswhereagoResultTypeisnotemptyIPAddressextendtostringcolumn_ifexistshas_anyin~=~projectAADServicePrincipalSignInLogsunionorder bydesc

MITRE Techniques

Actions

GitHub