Detection rules › Kusto

GreyNoise TI Map IP Entity to SigninLogs

Severity
medium
Time window
14d
Group by
IPAddress, IndicatorId, TI_ipEntity
Source
github.com/Azure/Azure-Sentinel

This query maps any GreyNoise IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in SigninLogs.

MITRE ATT&CK coverage

TacticTechniques
Command & Control

Telemetry coverage

Rule body

id: f6c76cc9-218c-5b76-9b82-8607f09ea1b4
name: GreyNoise TI Map IP Entity to SigninLogs
version: 1.0.1
kind: Scheduled
description: |
  'This query maps any GreyNoise IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in SigninLogs.'
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: GreyNoise2SentinelAPI
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let aadFunc = (tableName:string){
  ThreatIntelligenceIndicator
  | where TimeGenerated >= ago(ioc_lookBack)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
  | where Active == true and ExpirationDateTime > now()
  | where SourceSystem == 'GreyNoise'
  // 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)
      | 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
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url

Stages and Predicates

Stage 0: let

let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let aadFunc = (tableName:string){
ThreatIntelligenceIndicator
| where TimeGenerated >= ago(ioc_lookBack)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
| where Active == true and ExpirationDateTime > now()
| where SourceSystem == 'GreyNoise'
| where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP)
| 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)
| 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)
    | extend State = tostring(LocationDetails.state), City = tostring(LocationDetails.city), Region = tostring(LocationDetails.countryOrRegion)
    | 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");

Stage 1: union

union of 2 branches

Stage 2: source

ThreatIntelligenceIndicator

Stage 3: where

where TimeGenerated >= ago(1209600s)

Stage 4: summarize

summarize LatestIndicatorTime by IndicatorId

Stage 5: where

where Active =~ true

Stage 6: where

where SourceSystem =~ "GreyNoise"

Stage 7: where

where (isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkIP) or isnotempty(NetworkSourceIP))

Stage 8: extend (3 consecutive steps)

extend TI_ipEntity
TI_ipEntity =
ifisnotempty(NetworkIP)NetworkIP
elseNetworkDestinationIP

Stage 9: join

join kind=innerunique (...) on TI_ipEntity, IPAddress

Stage 10: where

where SigninLogs_TimeGenerated < ExpirationDateTime

Stage 11: summarize

summarize SigninLogs_TimeGenerated by IndicatorId, IPAddress

Stage 12: project

project ActivityGroupNames, AppDisplayName, ConfidenceScore, Description, EmailSourceIpAddress, ExpirationDateTime, IPAddress, IndicatorId, NetworkDestinationIP, NetworkIP, NetworkSourceIP, SigninLogs_TimeGenerated, StatusCode, StatusDetails, StatusReason, TI_ipEntity, ThreatType, Type, Url, UserPrincipalName

Stage 13: extend

extend Name, UPNSuffix, timestamp

Stage 14: source

ThreatIntelligenceIndicator

Stage 15: where

where TimeGenerated >= ago(1209600s)

Stage 16: summarize

summarize LatestIndicatorTime by IndicatorId

Stage 17: where

where Active =~ true

Stage 18: where

where SourceSystem =~ "GreyNoise"

Stage 19: where

where (isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkIP) or isnotempty(NetworkSourceIP))

Stage 20: extend (3 consecutive steps)

extend TI_ipEntity
TI_ipEntity =
ifisnotempty(NetworkIP)NetworkIP
elseNetworkDestinationIP

Stage 21: join

join kind=innerunique (...) on TI_ipEntity, IPAddress

Stage 22: where

where SigninLogs_TimeGenerated < ExpirationDateTime

Stage 23: summarize

summarize SigninLogs_TimeGenerated by IndicatorId, IPAddress

Stage 24: project

project ActivityGroupNames, AppDisplayName, ConfidenceScore, Description, EmailSourceIpAddress, ExpirationDateTime, IPAddress, IndicatorId, NetworkDestinationIP, NetworkIP, NetworkSourceIP, SigninLogs_TimeGenerated, StatusCode, StatusDetails, StatusReason, TI_ipEntity, ThreatType, Type, Url, UserPrincipalName

Stage 25: extend

extend Name, UPNSuffix, timestamp

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
AppDisplayNameproject
ConfidenceScoreproject
Descriptionproject
EmailSourceIpAddressproject
ExpirationDateTimeproject
IPAddressproject
IndicatorIdproject
NetworkDestinationIPproject
NetworkIPproject
NetworkSourceIPproject
SigninLogs_TimeGeneratedproject
StatusCodeproject
StatusDetailsproject
StatusReasonproject
TI_ipEntityproject
ThreatTypeproject
Typeproject
Urlproject
UserPrincipalNameproject
Nameextend
UPNSuffixextend
timestampextend