Detection rules › Kusto

Risky user signin observed in non-Microsoft network device

Severity
medium
Time window
1d
Group by
ConditionalAccessStatus, IPAddress, IsRisky, ResourceDisplayName, ResultType, RiskDetail, RiskLevelAggregated, RiskLevelDuringSignIn, RiskState, SourceSystem, UserPrincipalName
Author
Arjun Trivedi
Source
github.com/Azure/Azure-Sentinel

'This content is utilized to identify instances of successful login by risky users, who have been observed engaging in potentially suspicious network activity on non-Microsoft network devices.'

MITRE ATT&CK coverage

TacticTechniques
Command & ControlT1071 Application Layer Protocol

Rules detecting the same action

Other rules on this platform that filter on the same API call or operation.

Rule body kusto

id: 042f2801-a375-4cfd-bd29-041fc7ed88a0
name: Risky user signin observed in non-Microsoft network device
description: |
  'This content is utilized to identify instances of successful login by risky users, who have been observed engaging in potentially suspicious network activity on non-Microsoft network devices.'
severity: Medium
requiredDataConnectors:
  - connectorId:  AzureActiveDirectory
    dataTypes:
        - SigninLogs
  - connectorId:  PaloAltoNetworks
    dataTypes:
    - CommonSecurityLog (PaloAlto)
  - connectorId:  Fortinet
    dataTypes:
    - CommonSecurityLog (Fortinet)
  - connectorId:  CheckPoint
    dataTypes:
    - CommonSecurityLog (CheckPoint)
  - connectorId:  Zscaler
    dataTypes:
    - CommonSecurityLog (Zscaler)
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
    SigninLogs
    //Find risky Signin
    | where RiskState == "atRisk" and ResultType == 0
    | extend Signin_Time = TimeGenerated
    | summarize
        AppDisplayName=make_set(AppDisplayName),
        ClientAppUsed=make_set(ClientAppUsed),
        UserAgent=make_set(UserAgent),
        CorrelationId=make_set(CorrelationId),
        Signin_Time= min(Signin_Time),
        RiskEventTypes=make_set(RiskEventTypes)
        by
        ConditionalAccessStatus,
        IPAddress,
        IsRisky,
        ResourceDisplayName,
        RiskDetail,
        ResultType,
        RiskLevelAggregated,
        RiskLevelDuringSignIn,
        RiskState,
        UserPrincipalName=tostring(tolower(UserPrincipalName)),
        SourceSystem
    | join kind=inner (
        CommonSecurityLog
        | where DeviceVendor has_any  ("Palo Alto Networks", "Fortinet", "Check Point", "Zscaler")
        | where DeviceProduct startswith "FortiGate" or DeviceProduct startswith  "PAN" or DeviceProduct startswith  "VPN" or DeviceProduct startswith "FireWall" or DeviceProduct startswith  "NSSWeblog" or DeviceProduct startswith "URL"
        | where DeviceAction != "Block"
        | where isnotempty(RequestURL)
        | where isnotempty(SourceUserName)
        | extend SourceUserName = tolower(SourceUserName)
        | summarize
            min(TimeGenerated),
            max(TimeGenerated),
            Activity=make_set(Activity)
            by DestinationHostName, DestinationIP, RequestURL, SourceUserName=tostring(tolower(SourceUserName)),DeviceVendor,DeviceProduct
        | extend 3p_observed_Time= min_TimeGenerated,Name = tostring(split(SourceUserName,"@")[0]),UPNSuffix =tostring(split(SourceUserName,"@")[1]))
        on $left.IPAddress == $right.DestinationIP and $left.UserPrincipalName == $right.SourceUserName
    | extend Timediff = datetime_diff('day', 3p_observed_Time, Signin_Time)
    | where Timediff <= 1 and Timediff >= 0
entityMappings:
    - entityType: Account
      fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
    - entityType: IP
      fieldMappings:
      - identifier: Address
        columnName: DestinationIP
    - entityType: DNS
      fieldMappings:
      - identifier: DomainName
        columnName: DestinationHostName
kind: Scheduled
version: 1.0.6
metadata:
    source:
        kind: Community
    author:
        name: Arjun Trivedi
    support:
        tier: Community
    categories:
        domains: [ "Security - Threat Protection" ]

Stages and Predicates

Stage 1: source

SigninLogs

Stage 2: where

| where RiskState == "atRisk" and ResultType == 0

Stage 3: extend

| extend Signin_Time = TimeGenerated

Stage 4: summarize

| summarize
    AppDisplayName=make_set(AppDisplayName),
    ClientAppUsed=make_set(ClientAppUsed),
    UserAgent=make_set(UserAgent),
    CorrelationId=make_set(CorrelationId),
    Signin_Time= min(Signin_Time),
    RiskEventTypes=make_set(RiskEventTypes)
    by
    ConditionalAccessStatus,
    IPAddress,
    IsRisky,
    ResourceDisplayName,
    RiskDetail,
    ResultType,
    RiskLevelAggregated,
    RiskLevelDuringSignIn,
    RiskState,
    UserPrincipalName=tostring(tolower(UserPrincipalName)),
    SourceSystem

Stage 5: join

| join kind=inner (
    CommonSecurityLog
    | where DeviceVendor has_any  ("Palo Alto Networks", "Fortinet", "Check Point", "Zscaler")
    | where DeviceProduct startswith "FortiGate" or DeviceProduct startswith  "PAN" or DeviceProduct startswith  "VPN" or DeviceProduct startswith "FireWall" or DeviceProduct startswith  "NSSWeblog" or DeviceProduct startswith "URL"
    | where DeviceAction != "Block"
    | where isnotempty(RequestURL)
    | where isnotempty(SourceUserName)
    | extend SourceUserName = tolower(SourceUserName)
    | summarize
        min(TimeGenerated),
        max(TimeGenerated),
        Activity=make_set(Activity)
        by DestinationHostName, DestinationIP, RequestURL, SourceUserName=tostring(tolower(SourceUserName)),DeviceVendor,DeviceProduct
    | extend 3p_observed_Time= min_TimeGenerated,Name = tostring(split(SourceUserName,"@")[0]),UPNSuffix =tostring(split(SourceUserName,"@")[1]))
    on $left.IPAddress == $right.DestinationIP and $left.UserPrincipalName == $right.SourceUserName

Stage 6: extend

| extend Timediff = datetime_diff('day', 3p_observed_Time, Signin_Time)

Stage 7: where

| where Timediff <= 1 and Timediff >= 0

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
DeviceActionne
  • Block transforms: cased
DeviceProductstarts_with
  • FireWall
  • FortiGate
  • NSSWeblog
  • PAN
  • URL
  • VPN
DeviceVendormatch
  • Check Point
  • Fortinet
  • Palo Alto Networks
  • Zscaler
RequestURLis_not_null
  • (no value, null check)
ResultTypeeq
  • 0 transforms: cased
RiskStateeq
  • atRisk transforms: cased
SourceUserNameis_not_null
  • (no value, null check)
Timediffge
  • 0 transforms: cased
Timediffle
  • 1 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
AppDisplayNamesummarize
ClientAppUsedsummarize
ConditionalAccessStatussummarize
CorrelationIdsummarize
IPAddresssummarize
IsRiskysummarize
ResourceDisplayNamesummarize
ResultTypesummarize
RiskDetailsummarize
RiskEventTypessummarize
RiskLevelAggregatedsummarize
RiskLevelDuringSignInsummarize
RiskStatesummarize
Signin_Timesummarize
SourceSystemsummarize
UserAgentsummarize
UserPrincipalNamesummarize
Timediffextend