Query Details

Azure Active Directory TI Map IP Entity To Signin Logs

Query

let dt_lookBack = 90d;
let ioc_lookBack = 30d;
let aadFunc = (tableName: string) {
    ThreatIntelligenceIndicator
    | where TimeGenerated >= ago(ioc_lookBack) and ExpirationDateTime > now()
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
    | where Active == true
    // Picking up only IOC's that contain the entities we want
    | where isnotempty(NetworkIP)
        or isnotempty(EmailSourceIpAddress)
        or isnotempty(NetworkDestinationIP)
        or isnotempty(NetworkSourceIP)
    // As there is potentially more than 1 indicator type for matching IP, taking NetworkIP first, then others if that is empty.
    // Taking the first non-empty value based on potential IOC match availability
    | extend TI_ipEntity = iff(isnotempty(NetworkIP), NetworkIP, NetworkDestinationIP)
    | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
    | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(EmailSourceIpAddress), EmailSourceIpAddress, TI_ipEntity)
    // using innerunique to keep perf fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
    | join kind=innerunique (
        table(tableName)
        | where TimeGenerated >= ago(dt_lookBack)
        | extend Status = todynamic(Status), LocationDetails = todynamic(LocationDetails)
        | extend
            StatusCode = tostring(Status.errorCode),
            StatusDetails = tostring(Status.additionalDetails),
            StatusReason = tostring(Status.failureReason)
            | where StatusCode == "0"
            //| where StatusCode  != "0"
        | extend
            State = tostring(LocationDetails.state),
            City = tostring(LocationDetails.city),
            Region = tostring(LocationDetails.countryOrRegion)
        // renaming time column so it is clear the log this came from
        | extend SigninLogs_TimeGenerated = TimeGenerated, Type = Type
        )
        on $left.TI_ipEntity == $right.IPAddress
    | where SigninLogs_TimeGenerated < ExpirationDateTime
    | summarize SigninLogs_TimeGenerated = arg_max(SigninLogs_TimeGenerated, *) by IndicatorId, IPAddress
    | project
        SigninLogs_TimeGenerated,
        Description,
        ActivityGroupNames,
        IndicatorId,
        ThreatType,
        Url,
        ExpirationDateTime,
        ConfidenceScore,
        TI_ipEntity,
        IPAddress,
        UserPrincipalName,
        AppDisplayName,
        StatusCode,
        StatusDetails,
        StatusReason,
        NetworkIP,
        NetworkDestinationIP,
        NetworkSourceIP,
        EmailSourceIpAddress,
        Type
    | extend
        timestamp = SigninLogs_TimeGenerated,
        Name = tostring(split(UserPrincipalName, '@', 0)[0]),
        UPNSuffix = tostring(split(UserPrincipalName, '@', 1)[0])
};
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
union isfuzzy=true aadSignin, aadNonInt

About this query

Explanation

The query is used to identify matches in SigninLogs from any IP Indicator of Compromise (IOC) from Threat Intelligence. It retrieves information about successful and unsuccessful logins from Azure Active Directory and joins it with Threat Intelligence data to identify potential malicious activity. The query also includes additional filtering options to modify the results.

Details

Alex Verboon profile picture

Alex Verboon

Released: June 4, 2023

Tables

SecurityIncidentSecurityAlertThreatIntelligenceIndicatorSigninLogsAADNonInteractiveUserSignInLogs

Keywords

DevicesIntuneUserAzure Active DirectoryTISigninLogs

Operators

whereletdynamichas_anysummarizearg_maxbymv-expandextendjoinonparse_jsoniffdcountkind=inneruniquetabletodynamictostringStateCityRegionSigninLogs_TimeGeneratedType<projectsplit'@'01unionisfuzzy=true

Actions

GitHub