Detection rules › Kusto

Kusto rules: 2j

Apache - Requests to rare files

#
Status
available
Severity
medium
Time window
14d
Source
github.com/Azure/Azure-Sentinel

'Shows requests to rare files'

MITRE ATT&CK Coverage

Rule Body

id: 14d7e15e-3afb-11ec-8d3d-0242ac130003
name: Apache - Requests to rare files
description: |
  'Shows requests to rare files'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    datatypes:
      - ApacheHTTPServer_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  let lb_period_start = 14d;
  let lb_period_end = 1d;
  let known_files = ApacheHTTPServer
  | where TimeGenerated between (ago(lb_period_start) .. ago(lb_period_end))
  | extend File = extract(@"(.*\/)?(.*)", 2, tostring(UrlOriginal))
  | summarize makeset(File);
  ApacheHTTPServer
  | where TimeGenerated > ago(24h)
  | extend File = extract(@"(.*\/)?(.*)", 2, tostring(UrlOriginal))
  | where File in~ (known_files)
  | extend FileCustomEntity = File, UrlCustomEntity = UrlOriginal
entityMappings:
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: FileCustomEntity
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: UrlCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let lb_period_start = 14d;
let lb_period_end = 1d;

Let Binding: known_files used in Stage 4

let known_files = ApacheHTTPServer
| where TimeGenerated between (ago(lb_period_start) .. ago(lb_period_end))
| extend File = extract(@"(.*\/)?(.*)", 2, tostring(UrlOriginal))
| summarize makeset(File);

Stage 1: source

ApacheHTTPServer

Stage 2: where

| where TimeGenerated > ago(24h)

Stage 3: extend

| extend File = extract(@"(.*\/)?(.*)", 2, tostring(UrlOriginal))

Stage 4: where

| where File in~ (known_files)

Stage 5: extend

| extend FileCustomEntity = File, UrlCustomEntity = UrlOriginal

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Filein
  • known_files
field:"File" kind:in value:"known_files"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Fileextend
FileCustomEntityextend
UrlCustomEntityextend

API - Anomaly Detection

#
Status
available
Severity
low
Time window
5m
Source
github.com/Azure/Azure-Sentinel

'42Crunch API protection anomaly detection'

MITRE ATT&CK Coverage

Rule Body

id: 2c59e609-e0a0-4e8e-adc5-ab4224be8a36
name: API - Anomaly Detection
description: |
  '42Crunch API protection anomaly detection'
severity: Low
requiredDataConnectors:
  - connectorId: 42CrunchAPIProtection
    dataTypes:
      - FortyTwoCrunchAPIProtection
  - connectorId: FortyTwoCrunchAPIProtection
    dataTypes:
      - FortyTwoCrunchAPIProtection
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: SingleAlert
status: Available
tactics:
  - Reconnaissance
relevantTechniques:
  - T1593
  - T1589
query: |
  let infoRec = FortyTwoCrunchAPIProtection
  | where TimeGenerated >= ago(5m) 
  | project-away NonBlockingMode, SourcePort, DestinationPort, Query, ApiId, RequestHeader, ResponseHeader, Errors, EventType, Uuid
  | where UriPath has "/api/users/info" and Status == 200;
  let timestamp = toscalar(infoRec | top 1 by Timestamp desc | summarize by Timestamp);
  let activityRec = FortyTwoCrunchAPIProtection
    | where Timestamp < timestamp and TimeGenerated >= ago(5m)
    | project-away NonBlockingMode, SourcePort, DestinationPort, Query, ApiId, RequestHeader, ResponseHeader, Errors, EventType, Uuid
    | where UriPath has "/api/users/activity" and Status == 200;
  activityRec
customDetails:
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIp
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InstanceName
version: 3.0.1
kind: Scheduled

Stages and Predicates

let activityRec is inlined into the numbered stages below.

Let Binding: infoRec

let infoRec = FortyTwoCrunchAPIProtection
| where TimeGenerated >= ago(5m) 
| project-away NonBlockingMode, SourcePort, DestinationPort, Query, ApiId, RequestHeader, ResponseHeader, Errors, EventType, Uuid
| where UriPath has "/api/users/info" and Status == 200;

Let Binding: timestamp used in Stage 2

let timestamp = toscalar(infoRec | top 1 by Timestamp desc | summarize by Timestamp);

Stage 1: source

FortyTwoCrunchAPIProtection

Stage 2: where

| where Timestamp < timestamp and TimeGenerated >= ago(5m)

Stage 3: project-away

| project-away NonBlockingMode, SourcePort, DestinationPort, Query, ApiId, RequestHeader, ResponseHeader, Errors, EventType, Uuid

Stage 4: where

| where UriPath has "/api/users/activity" and Status == 200

Indicators

These rows show field, operator, and value matches.

AWSCloudTrail - Amazon ECR image scanning disabled

#
Status
available
Severity
medium
Time window
1d
Source
github.com/Azure/Azure-Sentinel

Identifies Amazon ECR image scanning being disabled. This change can reduce visibility into vulnerable container images and may indicate defense evasion or weakening of container security controls.

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Telemetry Coverage

Rule Body

id: 19602494-94af-43c8-90ba-eb0e14999612
name: AWSCloudTrail - Amazon ECR image scanning disabled
description: |
  Identifies Amazon ECR image scanning being disabled. This change can reduce visibility into vulnerable container images and may indicate defense evasion or weakening of container security controls.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562.001
query: |
  AWSCloudTrail
  | where EventName == "PutImageScanningConfiguration" and isempty(ErrorCode) and isempty(ErrorMessage)
  | extend scanOnPush = parse_json(tostring((parse_json(RequestParameters).imageScanningConfiguration))).scanOnPush
  | where scanOnPush == false
  | extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
  | extend UserName = tostring(split(UserIdentityArn, '/')[-1])
  | extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
  | extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
    AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
  | distinct TimeGenerated, EventName, SourceIpAddress, UserIdentityArn, UserIdentityUserName, RecipientAccountId, AccountName, AccountUPNSuffix
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
  EventName: EventName
  UserIdentityArn: UserIdentityArn
  UserIdentityUserName: UserIdentityUserName
  RecipientAccountId: RecipientAccountId
alertDetailsOverride:
  alertDisplayNameFormat: 'Amazon ECR image scanning disabled by {{AccountName}} from {{SourceIpAddress}}'
  alertDescriptionFormat: 'Amazon ECR image scanning was disabled by {{AccountName}} from {{SourceIpAddress}} in account {{RecipientAccountId}}'
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

AWSCloudTrail

Stage 2: where

| where EventName == "PutImageScanningConfiguration" and isempty(ErrorCode) and isempty(ErrorMessage)

Stage 3: extend

| extend scanOnPush = parse_json(tostring((parse_json(RequestParameters).imageScanningConfiguration))).scanOnPush

Stage 4: where

| where scanOnPush == false

Stage 5: extend (4 consecutive steps)

| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
| extend UserName = tostring(split(UserIdentityArn, '/')[-1])
| extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
| extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
  AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")

Stage 6: distinct

| distinct TimeGenerated, EventName, SourceIpAddress, UserIdentityArn, UserIdentityUserName, RecipientAccountId, AccountName, AccountUPNSuffix

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
scanOnPushextend
UserIdentityArnextend
UserNameextend
AccountNameextend
AccountUPNSuffixextend

BloodHound Attack Path Finding - Non-Tier Zero Principals With ADCS ESC10 Scenario B Privileges

#

This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

BloodHound Attack Path Finding - Non-Tier Zero Principals With ADCS ESC10 Scenario B Privileges

Rule Body

id: d9fb446e-58a9-4788-afee-66a2b5391347
name: BloodHound Attack Path Finding - Non-Tier Zero Principals With ADCS ESC10 Scenario
  B Privileges
description: BloodHound Attack Path Finding - Non-Tier Zero Principals With ADCS ESC10
  Scenario B Privileges
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BloodHoundEnterprise
    dataTypes:
      - BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: BHEAttackPathsData_CL | where Finding == "T0ADCSESC10b"
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: domain_name
customDetails:
  Description: ShortDescription
  Remediation: ShortRemediation
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

BHEAttackPathsData_CL

Stage 2: where

| where Finding == "T0ADCSESC10b"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Findingeq
  • T0ADCSESC10b
field:"Finding" kind:eq value:"T0ADCSESC10b"

Cisco SE - Malware execusion on host

#
Status
available
Severity
high
Time window
15m
Source
github.com/Azure/Azure-Sentinel

'Detects malware execution on host.'

MITRE ATT&CK Coverage

Rule Body

id: aea4468e-6322-48b6-bd83-f9d300cce855
name: Cisco SE - Malware execusion on host
description: |
  'Detects malware execution on host.'
severity: High
requiredDataConnectors:
  - connectorId: CiscoSecureEndpoint
    dataTypes:
      - CiscoSecureEndpoint
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Execution
relevantTechniques:
  - T1204.002
query: |
  CiscoSecureEndpoint
  | where EventMessage has 'Executed Malware'
  | extend HostCustomEntity = DstHostname, MalwareCustomEntity = ThreatName
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostCustomEntity
  - entityType: Malware
    fieldMappings:
      - identifier: Name
        columnName: MalwareCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CiscoSecureEndpoint

Stage 2: where

| where EventMessage has 'Executed Malware'

Stage 3: extend

| extend HostCustomEntity = DstHostname, MalwareCustomEntity = ThreatName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventMessagematch
  • Executed Malware transforms: term
field:"EventMessage" kind:match value:"Executed Malware"

Output Fields

These fields are emitted when the rule matches.

FieldSource
HostCustomEntityextend
MalwareCustomEntityextend

Detect requests for an uncommon resources on the web (ASIM Web Session)

#
Status
available
Severity
low
Time window
1d
Group by
Domain
Source
github.com/Azure/Azure-Sentinel

'This detection mechanism examines connections made to a domain where only a single file is requested, which is considered unusual since most contemporary web applications require additional resources. Such activity is often associated with malware beaconing or tracking URLs delivered via emails. The query includes a sample set of popular web script extensions (scriptExtensions), which should be customized to align with the specific requirements of your environment'

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: c99cf650-c53b-4c4c-9671-7d7500191a10
name: Detect requests for an uncommon resources on the web (ASIM Web Session)
description: |
  'This detection mechanism examines connections made to a domain where only a single file is requested, which is considered unusual since most contemporary web applications require additional resources. Such activity is often associated with malware beaconing or tracking URLs delivered via emails. 
  The query includes a sample set of popular web script extensions (scriptExtensions), which should be customized to align with the specific requirements of your environment'
severity: Low
status: Available 
tags:
  - Schema: WebSession
    SchemaVersion: 0.2.6
  - RuleReference: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Zscaler%20Internet%20Access/Analytic%20Rules/Zscaler-LowVolumeDomainRequests.yaml
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1102
  - T1071
query: |
  let lookback = 1d;
  let scriptExtensions = dynamic([".php", ".aspx", ".asp", ".cfml", ".py", ".sh", ".bash", ".pl"]);
  //The number of URI's seen to be suspicious, higher = less likely to be suspicious
  let uriThreshold = 1;
  let ReqestCountThreshold = 10; // set minimum requests count to confirm repetitive connections
  // Only look at connections that were allowed through the web proxy
  _Im_WebSession (starttime=ago(lookback), eventresult='Success')
  | project
      SrcBytes,
      DstBytes,
      Url,
      TimeGenerated,
      DstIpAddr,
      SrcIpAddr,
      HttpRequestMethod,
      HttpReferrer
  | where not(ipv4_is_private(DstIpAddr)) // Only take traffic going to internet
  | extend DestHostName = tostring(parse_url(Url)["Host"])
  // Only look at connections where some data was exchanged.
  | where SrcBytes > 0 and DstBytes > 0
  // Extract Domain
  | extend Domain = iif(countof(DestHostName, '.') >= 2, strcat(split(DestHostName, '.')[-2], '.', split(DestHostName, '.')[-1]), DestHostName)
  | extend GetData = iff(Url contains "?", 1, 0)
  | summarize
      EventStartTime = min(TimeGenerated),
      EventEndTime = max(TimeGenerated),
      make_set(Url, 100),
      make_set(DstIpAddr, 100),
      make_set(SrcIpAddr, 100),
      EventCount = count(),
      make_set(HttpRequestMethod, 10),
      max(GetData),
      max(HttpReferrer)
      by Domain
  // Determine the number of URIs that have been visited for the domain
  | extend destinationURICount = array_length(set_Url)
  | where destinationURICount <= uriThreshold and EventCount > ReqestCountThreshold // check for repetitive requests for single resource only.
  | where tostring(set_Url) has_any(scriptExtensions)
  //Remove matches with referer
  | where max_HttpReferrer == ""
  //Keep requests where data was transferred either in a GET with parameters or a POST
  | where set_HttpRequestMethod in~ ("POST") or max_GetData == 1
  //Defeat email click tracking, may increase FN's while decreasing FP's
  | where set_Url !has "click" and set_HttpRequestMethod !has "GET"
  | mv-expand set_Url, set_DstIpAddr, set_SrcIpAddr
  | extend
      RequestURL = tostring(set_Url),
      DestinationIP = tostring(set_DstIpAddr),
      SourceIP = tostring(set_SrcIpAddr)
  | project
      EventStartTime,
      EventEndTime,
      SourceIP,
      DestinationIP,
      EventCount,
      RequestURL,
      set_HttpRequestMethod
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: DestinationIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: RequestURL
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  EventStartTime: EventStartTime
  EventEndTime: EventEndTime
  EventCount: EventCount
alertDetailsOverride:
  alertDisplayNameFormat: "User with IP '{{SourceIP}}' has been observed making request for a rare resource"
  alertDescriptionFormat: "User requested (TotalEvents='{{EventCount}}') for URL '{{RequestURL}}' which contains a known script extension. The domain associated with this URL has not been accessed by any other user. This activity could be a potential beaconing activity to maintain control over compromised systems, receive instructions, or exfiltrate data"
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let lookback = 1d;
let scriptExtensions = dynamic([".php", ".aspx", ".asp", ".cfml", ".py", ".sh", ".bash", ".pl"]);
let uriThreshold = 1;
let ReqestCountThreshold = 10;

Stage 1: source

_Im_WebSession (starttime=ago(lookback), eventresult='Success')

Stage 2: project

| project
    SrcBytes,
    DstBytes,
    Url,
    TimeGenerated,
    DstIpAddr,
    SrcIpAddr,
    HttpRequestMethod,
    HttpReferrer

Stage 3: where

| where not(ipv4_is_private(DstIpAddr))

Stage 4: extend

| extend DestHostName = tostring(parse_url(Url)["Host"])

Stage 5: where

| where SrcBytes > 0 and DstBytes > 0

Stage 6: extend

| extend Domain = iif(countof(DestHostName, '.') >= 2, strcat(split(DestHostName, '.')[-2], '.', split(DestHostName, '.')[-1]), DestHostName)

Stage 7: extend

| extend GetData = iff(Url contains "?", 1, 0)
GetData =
ifUrl contains "?"1
else0

Stage 8: summarize

| summarize
    EventStartTime = min(TimeGenerated),
    EventEndTime = max(TimeGenerated),
    make_set(Url, 100),
    make_set(DstIpAddr, 100),
    make_set(SrcIpAddr, 100),
    EventCount = count(),
    make_set(HttpRequestMethod, 10),
    max(GetData),
    max(HttpReferrer)
    by Domain

Stage 9: extend

| extend destinationURICount = array_length(set_Url)

Stage 10: where

| where destinationURICount <= uriThreshold and EventCount > ReqestCountThreshold

Stage 11: where

| where tostring(set_Url) has_any(scriptExtensions)

Stage 12: where

| where max_HttpReferrer == ""

Stage 13: where

| where set_HttpRequestMethod in~ ("POST") or max_GetData == 1

Stage 14: where

| where set_Url !has "click" and set_HttpRequestMethod !has "GET"

Stage 15: mv-expand

| mv-expand set_Url, set_DstIpAddr, set_SrcIpAddr

Stage 16: extend

| extend
    RequestURL = tostring(set_Url),
    DestinationIP = tostring(set_DstIpAddr),
    SourceIP = tostring(set_SrcIpAddr)

Stage 17: project

| project
    EventStartTime,
    EventEndTime,
    SourceIP,
    DestinationIP,
    EventCount,
    RequestURL,
    set_HttpRequestMethod

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DstBytesgt
  • 0 transforms: number
field:"DstBytes" kind:gt value:"0"
EventCountgt
  • 10 transforms: number
field:"EventCount" kind:gt value:"10"
SrcBytesgt
  • 0 transforms: number
field:"SrcBytes" kind:gt value:"0"
destinationURICountle
  • 1 transforms: number
field:"destinationURICount" kind:le value:"1"
max_GetDataeq
  • 1
field:"max_GetData" kind:eq value:"1"
set_HttpRequestMethodin
  • POST
field:"set_HttpRequestMethod" kind:in value:"POST"
set_Urlmatch
  • .asp transforms: tostring, term
  • .aspx transforms: tostring, term
  • .bash transforms: tostring, term
  • .cfml transforms: tostring, term
  • .php transforms: tostring, term
  • .pl transforms: tostring, term
  • .py transforms: tostring, term
  • .sh transforms: tostring, term
field:"set_Url" kind:match

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DstIpAddrcidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:DstIpAddr
set_HttpRequestMethodmatchGETexcludes:set_HttpRequestMethod field:"set_HttpRequestMethod" value:"GET"
set_Urlmatchclickexcludes:set_Url field:"set_Url" value:"click"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DestinationIPproject
EventCountproject
EventEndTimeproject
EventStartTimeproject
RequestURLproject
SourceIPproject
set_HttpRequestMethodproject

Google DNS - Possible data exfiltration

#
Severity
high
Time window
15m
Source
github.com/Azure/Azure-Sentinel

'Detects possible data exfiltration.'

MITRE ATT&CK Coverage

TacticTechniques
Exfiltration

Rule Body

id: 705bed63-668f-4508-9d2d-26faf4010700
name: Google DNS - Possible data exfiltration
description: |
  'Detects possible data exfiltration.'
severity: High
requiredDataConnectors:
  - connectorId: GCPDNSDataConnector
    dataTypes:
      - GCPCloudDNS
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1567
query: |
  GCPCloudDNS
  | where Query has 'webhook.site'
  | extend DNSCustomEntity = Query, IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: DNSCustomEntity
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

GCPCloudDNS

Stage 2: where

| where Query has 'webhook.site'

Stage 3: extend

| extend DNSCustomEntity = Query, IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Querymatch
  • webhook.site transforms: term
field:"Query" kind:match value:"webhook.site"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DNSCustomEntityextend
IPCustomEntityextend

Hunt Device Discovery Subnet Ranges

#
Group by
IPv4Dhcp, NetworkName
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

This KQL query helps you identify which subnet ranges are behind the Microsoft Defender for Endpoint Device Discovery 'Monitored Networks' page. By using this query you can investigate if all of your corporate networks are being monitored and change monitored states effectivly. More information can be found in the references.

References

Telemetry Coverage

Rule Body

// OPTIONAL - Device cap used to ignore network with less then X devices in them
let device_cap = 0;
DeviceNetworkInfo
| where Timestamp > ago(7d)
// Ignore empty networks
| where ConnectedNetworks  != ""
// Get networks data
| extend ConnectedNetworksExp = parse_json(ConnectedNetworks)
| mv-expand bagexpansion = array ConnectedNetworks=ConnectedNetworksExp
| extend NetworkName = tostring(ConnectedNetworks ["Name"]), NetworkDescription = tostring(ConnectedNetworks ["Description"]), NetworkCategory = tostring(ConnectedNetworks ["Category"])
// Get subnet data for IPv4 Addresses
| extend IPAddressesExp = parse_json(IPAddresses)
| mv-expand bagexpansion = array IPAddresses=IPAddressesExp
| extend IPAddress = tostring(IPAddresses ["IPAddress"]), SubnetPrefix = tolong(IPAddresses ["SubnetPrefix"])
| extend NetworkAddress = format_ipv4(IPAddress, SubnetPrefix)
| extend SubnetRange = strcat(NetworkAddress, "/", SubnetPrefix)
// Exclude IPv6 and APIPPA
| where SubnetPrefix <= 32
| where IPAddress !startswith "169.254"
// Ignore unidentified networks
| where not(NetworkName has_any ("Unidentified", "Identifying..."))
// Provide list
| distinct DeviceId, NetworkName, IPv4Dhcp, SubnetRange
| summarize Devices = count(), SubnetRanges = make_set(SubnetRange) by NetworkName, IPv4Dhcp
// Ignore network with very low device count
| where Devices >= device_cap
| sort by Devices desc

// OPTIONAL - Device cap used to ignore network with less then X devices in them
let device_cap = 0;
DeviceNetworkInfo
| where TimeGenerated > ago(7d)
// Ignore empty networks
| where ConnectedNetworks  != ""
// Get networks data
| extend ConnectedNetworksExp = parse_json(ConnectedNetworks)
| mv-expand bagexpansion = array ConnectedNetworks=ConnectedNetworksExp
| extend NetworkName = tostring(ConnectedNetworks ["Name"]), NetworkDescription = tostring(ConnectedNetworks ["Description"]), NetworkCategory = tostring(ConnectedNetworks ["Category"])
// Get subnet data for IPv4 Addresses
| extend IPAddressesExp = parse_json(IPAddresses)
| mv-expand bagexpansion = array IPAddresses=IPAddressesExp
| extend IPAddress = tostring(IPAddresses ["IPAddress"]), SubnetPrefix = tolong(IPAddresses ["SubnetPrefix"])
| extend NetworkAddress = format_ipv4(IPAddress, SubnetPrefix)
| extend SubnetRange = strcat(NetworkAddress, "/", SubnetPrefix)
// Exclude IPv6 and APIPPA
| where SubnetPrefix <= 32
| where IPAddress !startswith "169.254"
// Ignore unidentified networks
| where not(NetworkName has_any ("Unidentified", "Identifying..."))
// Provide list
| distinct DeviceId, NetworkName, IPv4Dhcp, SubnetRange
| summarize Devices = count(), SubnetRanges = make_set(SubnetRange) by NetworkName, IPv4Dhcp
// Ignore network with very low device count
| where Devices >= device_cap
| sort by Devices desc

Stages and Predicates

Parameters

let device_cap = 0;

Stage 1: source

DeviceNetworkInfo

Stage 2: where

| where Timestamp > ago(7d)

Stage 3: where

| where ConnectedNetworks  != ""

Stage 4: extend

| extend ConnectedNetworksExp = parse_json(ConnectedNetworks)

Stage 5: mv-expand

| mv-expand bagexpansion = array ConnectedNetworks=ConnectedNetworksExp

Stage 6: extend

| extend NetworkName = tostring(ConnectedNetworks ["Name"]), NetworkDescription = tostring(ConnectedNetworks ["Description"]), NetworkCategory = tostring(ConnectedNetworks ["Category"])

Stage 7: extend

| extend IPAddressesExp = parse_json(IPAddresses)

Stage 8: mv-expand

| mv-expand bagexpansion = array IPAddresses=IPAddressesExp

Stage 9: extend (3 consecutive steps)

| extend IPAddress = tostring(IPAddresses ["IPAddress"]), SubnetPrefix = tolong(IPAddresses ["SubnetPrefix"])
| extend NetworkAddress = format_ipv4(IPAddress, SubnetPrefix)
| extend SubnetRange = strcat(NetworkAddress, "/", SubnetPrefix)

Stage 10: where

| where SubnetPrefix <= 32

Stage 11: where

| where IPAddress !startswith "169.254"

Stage 12: where

| where not(NetworkName has_any ("Unidentified", "Identifying..."))

Stage 13: distinct

| distinct DeviceId, NetworkName, IPv4Dhcp, SubnetRange

Stage 14: summarize

| summarize Devices = count(), SubnetRanges = make_set(SubnetRange) by NetworkName, IPv4Dhcp

Stage 15: where

| where Devices >= device_cap

Stage 16: sort

| sort by Devices desc

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Devicesge
  • 0 transforms: number
field:"Devices" kind:ge value:"0"
SubnetPrefixle
  • 32 transforms: number
field:"SubnetPrefix" kind:le value:"32"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
IPAddressstarts_with169.254excludes:IPAddress field:"IPAddress" value:"169.254"
NetworkNamematchUnidentified, Identifying...excludes:NetworkName field:"NetworkName" value:"Unidentified" field:"NetworkName" value:"Identifying..."

Output Fields

These fields are emitted when the rule matches.

FieldSource
Devicessummarize
IPv4Dhcpsummarize
NetworkNamesummarize
SubnetRangessummarize

Infoblox - IQ for TD Insight Detected - CDC Source

#
Status
available
Severity
medium
Time window
1d
Group by
InfobloxInsightID
Source
github.com/Azure/Azure-Sentinel

'Infoblox IQ for Threat Defense Insight detected in logs sourced via Infoblox CDC. Customize scheduling, responses and more. This rule depends on a parser based on a Kusto Function to work as expected called InfobloxCDC_SOCInsights.'

MITRE ATT&CK Coverage

Rule Body

id: d04f1963-df27-4127-b1ec-3d37148d65be
name: Infoblox - IQ for TD Insight Detected - CDC Source
description: |
  'Infoblox IQ for Threat Defense Insight detected in logs sourced via Infoblox CDC. Customize scheduling, responses and more. This rule depends on a parser based on a Kusto Function to work as expected called [**InfobloxCDC_SOCInsights**](https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Infoblox/Parsers/InfobloxCDC_SOCInsights.yaml).'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: InfobloxSOCInsightsDataConnector_AMA
    dataTypes: 
      - CommonSecurityLog (InfobloxCDC_SOCInsights)
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1498
  - T1565
query: |
  InfobloxCDC_SOCInsights
  | summarize arg_max(TimeGenerated, *) by InfobloxInsightID
entityMappings:
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: ThreatIndicator
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: AssetHostName
customDetails:
  InfobloxInsightID: InfobloxInsightID
  Status: Status
  Severity: Severity
  SubType: InsightSubType
  ThreatIndicator: ThreatIndicator
  IndicatorCount: IndicatorCount
  AssetHostName: AssetHostName
  AssetCount: AssetCount
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: 'Infoblox - IQ for TD Insight - {{Name}}'
  alertDescriptionFormat: 'Observed via CDC. {{Description}}'
  alertSeverityColumnName: IncidentSeverity
incidentConfiguration:
  createIncident: true
version: 1.1.0
kind: Scheduled

Stages and Predicates

Stage 1: source

InfobloxCDC_SOCInsights

Stage 2: summarize

| summarize arg_max(TimeGenerated, *) by InfobloxInsightID

Output Fields

These fields are emitted when the rule matches.

FieldSource
InfobloxInsightIDsummarize

Port Scan

#
Status
available
Severity
medium
Time window
30s
Group by
Fqdn, SourceIp
Source
github.com/Azure/Azure-Sentinel

'Identifies a source IP scanning multiple open ports on Azure Firewall. This can indicate malicious scanning of ports by an attacker, trying to reveal open ports in the organization that can be compromised for initial access. Configurable Parameters: - Port scan time - the time range to look for multiple ports scanned. Default is set to 30 seconds. - Minimum different ports threshold - alert only if more than this number of ports scanned. Default is set to 100.'

MITRE ATT&CK Coverage

Rule Body

id: b2c5907b-1040-4692-9802-9946031017e8
name: Port Scan
description: |
  'Identifies a source IP scanning multiple open ports on Azure Firewall. This can indicate malicious scanning of ports by an attacker, trying to reveal open ports in the organization that can be compromised for initial access.

  Configurable Parameters:

  - Port scan time - the time range to look for multiple ports scanned. Default is set to 30 seconds.
  - Minimum different ports threshold - alert only if more than this number of ports scanned. Default is set to 100.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AzureFirewall
    dataTypes:
      - AzureDiagnostics
      - AZFWApplicationRule
      - AZFWNetworkRule
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
  - Reconnaissance
relevantTechniques:
  - T1046
  - T1595.001
query: |
  let MinimumDifferentPortsThreshold = 100;
  let BinTime = 30s;
  // Exclude known benign scanner IPs in your environment to reduce noise
  let KnownScannerIPs = dynamic([]); 
  union isfuzzy=true(
  AZFWApplicationRule
  | where SourceIp !in (KnownScannerIPs)
  | summarize AlertTimedCountPortsInBinTime = dcount(DestinationPort) by SourceIp, bin(TimeGenerated, BinTime), Fqdn
  | where AlertTimedCountPortsInBinTime > MinimumDifferentPortsThreshold),
  (AZFWNetworkRule
  | where SourceIp !in (KnownScannerIPs)
  | extend Fqdn = DestinationIp
  | summarize AlertTimedCountPortsInBinTime = dcount(DestinationPort) by SourceIp, bin(TimeGenerated, BinTime), Fqdn
  | where AlertTimedCountPortsInBinTime > MinimumDifferentPortsThreshold),
  (AzureDiagnostics
  | where OperationName == "AzureFirewallApplicationRuleLog" or OperationName == "AzureFirewallNetworkRuleLog"
  | parse msg_s with * "from " SourceIp ":" SourcePort:int " to " Fqdn ":" DestinationPort:int *
  | where isnotempty(Fqdn) and isnotempty(SourceIp)
  | where SourceIp !in (KnownScannerIPs) 
  | summarize AlertTimedCountPortsInBinTime = dcount(DestinationPort) by SourceIp, bin(TimeGenerated, BinTime), Fqdn
  | where AlertTimedCountPortsInBinTime > MinimumDifferentPortsThreshold)
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIp
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Fqdn
alertDetailsOverride:
  alertDisplayNameFormat: 'Port Scan Detected from {{SourceIp}}'
  alertDescriptionFormat: 'Source IP {{SourceIp}} has scanned {{AlertTimedCountPortsInBinTime}} different ports within a short time frame, which may indicate reconnaissance activity.'
version: 1.1.3
kind: Scheduled

Stages and Predicates

Parameters

let MinimumDifferentPortsThreshold = 100;
let BinTime = 30s;
let KnownScannerIPs = dynamic([]);

Union isfuzzy=true (3 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: AZFWApplicationRule, AZFWNetworkRule, AzureDiagnostics

Leg 1: AZFWApplicationRule

AZFWApplicationRule
| where SourceIp !in (KnownScannerIPs)
| summarize AlertTimedCountPortsInBinTime = dcount(DestinationPort) by SourceIp, bin(TimeGenerated, BinTime), Fqdn
| where AlertTimedCountPortsInBinTime > MinimumDifferentPortsThreshold

Leg 2: AZFWNetworkRule

AZFWNetworkRule
| where SourceIp !in (KnownScannerIPs)
| extend Fqdn = DestinationIp
| summarize AlertTimedCountPortsInBinTime = dcount(DestinationPort) by SourceIp, bin(TimeGenerated, BinTime), Fqdn
| where AlertTimedCountPortsInBinTime > MinimumDifferentPortsThreshold

Leg 3: AzureDiagnostics

AzureDiagnostics
| where OperationName == "AzureFirewallApplicationRuleLog" or OperationName == "AzureFirewallNetworkRuleLog"
| parse msg_s with * "from " SourceIp ":" SourcePort:int " to " Fqdn ":" DestinationPort:int *
| where isnotempty(Fqdn) and isnotempty(SourceIp)
| where SourceIp !in (KnownScannerIPs) 
| summarize AlertTimedCountPortsInBinTime = dcount(DestinationPort) by SourceIp, bin(TimeGenerated, BinTime), Fqdn
| where AlertTimedCountPortsInBinTime > MinimumDifferentPortsThreshold

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AlertTimedCountPortsInBinTimegt
  • 100 transforms: number
field:"AlertTimedCountPortsInBinTime" kind:gt value:"100"
Fqdnis_not_null
  • (no value, null check)
field:"Fqdn" kind:is_not_null
OperationNameeq
  • AzureFirewallApplicationRuleLog
  • AzureFirewallNetworkRuleLog
field:"OperationName" kind:eq
SourceIpis_not_null
  • (no value, null check)
field:"src_ip" kind:is_not_null

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
SourceIpeq[]excludes:SourceIp field:"SourceIp" value:"[]"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertTimedCountPortsInBinTimesummarize
Fqdnsummarize
SourceIpsummarize

SailPointIdentityNowUserWithFailedEvent

#
Status
available
Severity
high
Time window
14d
Source
github.com/Azure/Azure-Sentinel

'Detects any failed event for a particular user.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 2a215222-bfc5-4858-a530-6d4088ebfa15
name: SailPointIdentityNowUserWithFailedEvent
description: |
  'Detects any failed event for a particular user.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: SailPointIdentityNow
    dataTypes:
      - SailPointIDN_Events
  - connectorId: SailPointIdentityNowConnector
    dataTypes:
      - SailPointIDN_Events
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1133
query: |
  declare query_parameters(lbperiod:timespan = 14d, type:string = "ACCESS_ITEM", actorName:string = "test.tester", targetName:string = "test.tester");
    SailPointIDN_Events
    | where TimeGenerated > ago(lbperiod)
    | where EventType == type
    | where Status == "FAILED"
    | where ActorName == actorName
    | where TargetName == targetName
    | sort by Created
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: TechnicalName
version: 1.1.0
kind: Scheduled

Stages and Predicates

Stage 1: source

declare query_parameters(lbperiod:timespan = 14d, type:string = "ACCESS_ITEM", actorName:string = "test.tester", targetName:string = "test.tester");
  SailPointIDN_Events

Stage 2: where

| where TimeGenerated > ago(lbperiod)

Stage 3: where

| where EventType == type

Stage 4: where

| where Status == "FAILED"

Stage 5: where

| where ActorName == actorName

Stage 6: where

| where TargetName == targetName

Stage 7: sort

| sort by Created

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Statuseq
  • FAILED
field:"Status" kind:eq value:"FAILED"

Sonrai Ticket Risk Accepted

#

This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

Status
available
Severity
medium
Time window
5m
Source
github.com/Azure/Azure-Sentinel

'Checks if Sonrai tickets have had their risk accepted. It uses the action type to check if a ticket has had it's risk accepted'

MITRE ATT&CK Coverage

Rule Body

id: 080191e8-271d-4ae6-85ce-c7bcd4b06b40
name: Sonrai Ticket Risk Accepted
description: |
  'Checks if Sonrai tickets have had their risk accepted. 
  It uses the action type to check if a ticket has had it's risk accepted'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SonraiDataConnector
    dataTypes:
      - Sonrai_Tickets_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
  - CommandAndControl
  - CredentialAccess
  - DefenseEvasion
  - Discovery
  - Execution
  - Exfiltration
  - Impact
  - InitialAccess
  - LateralMovement
  - Persistence
  - PrivilegeEscalation
relevantTechniques:
  - T1566
  - T1059
  - T1547
  - T1548
  - T1562
  - T1003
  - T1087
  - T1021
  - T1119
  - T1071
  - T1041
  - T1499
query: |
  Sonrai_Tickets_CL
  | where action_d == 7
entityMappings: 
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: digest_criticalResourceName_s
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  ticketName: digest_title_s
  ticketStatus: digest_status_s
  ticketSeverity: digest_severityCategory_s
  resourceLabel: digest_resourceLabel_s
  resourceType: digest_resourceType_s
  criticalResource: digest_criticalResourceName_s
  ticketOrg: digest_org_s
alertDetailsOverride:
  alertSeverityColumnName: digest_severityCategory_s
  alertDisplayNameFormat: Risk Accepted - {{digest_ticketSrn_s}} - {{digest_ticketKeyName_s}}
  alertDescriptionFormat: digest_ticketKeyDescription_s
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

Sonrai_Tickets_CL

Stage 2: where

| where action_d == 7

Indicators

These rows show field, operator, and value matches.

Suspicious application consent similar to PwnAuth

#
Status
available
Severity
medium
Time window
14d
Group by
AppClientId, CorrelationId
Source
github.com/Azure/Azure-Sentinel

This will alert when a user consents to provide a previously-unknown Azure application with the same OAuth permissions used by the FireEye PwnAuth toolkit (https://github.com/fireeye/PwnAuth). The default permissions/scope for the PwnAuth toolkit are user.read, offline_access, mail.readwrite, mail.send, and files.read.all. Consent to applications with these permissions should be rare, especially as the knownApplications list is expanded. Public contributions to expand this filter are welcome! For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 39198934-62a0-4781-8416-a81265c03fd6
name: Suspicious application consent similar to PwnAuth
description: |
  'This will alert when a user consents to provide a previously-unknown Azure application with the same OAuth permissions used by the FireEye PwnAuth toolkit (https://github.com/fireeye/PwnAuth).
  The default permissions/scope for the PwnAuth toolkit are user.read, offline_access, mail.readwrite, mail.send, and files.read.all.
  Consent to applications with these permissions should be rare, especially as the knownApplications list is expanded. Public contributions to expand this filter are welcome!
  For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
  - DefenseEvasion
relevantTechniques:
  - T1528
  - T1550
query: |
  let detectionTime = 1d;
  let joinLookback = 14d;
  AuditLogs
  | where TimeGenerated > ago(detectionTime)
  | where LoggedByService =~ "Core Directory"
  | where Category =~ "ApplicationManagement"
  | where OperationName =~ "Consent to application"
  | where TargetResources has "offline"
  | mv-apply TargetResource = TargetResources on 
    (
        where TargetResource.type =~ "ServicePrincipal"
        | extend AppDisplayName = tostring(TargetResource.displayName),
                 AppClientId = tostring(TargetResource.id),
                 props = TargetResource.modifiedProperties
    )
  | where AppClientId !in ((externaldata(knownAppClientId:string, knownAppDisplayName:string)[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/Microsoft.OAuth.KnownApplications.csv"] with (format="csv")))
  | mv-apply ConsentFull = props on 
    (
        where ConsentFull.displayName =~ "ConsentAction.Permissions"
    )
  | parse ConsentFull with * "ConsentType: " GrantConsentType ", Scope: " GrantScope1 "]" *
  | where ConsentFull has_all ("user.read", "offline_access", "mail.readwrite", "mail.send", "files.read.all")
  | where GrantConsentType != "AllPrincipals" // NOTE: we are ignoring if OAuth application was granted to all users via an admin - but admin due diligence should be audited occasionally
  | extend GrantInitiatedByAppName = tostring(InitiatedBy.app.displayName)
  | extend GrantInitiatedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
  | extend GrantInitiatedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
  | extend GrantInitiatedByAadUserId = tostring(InitiatedBy.user.id)
  | extend GrantIpAddress = iff(isnotempty(InitiatedBy.user.ipAddress), tostring(InitiatedBy.user.ipAddress), tostring(InitiatedBy.app.ipAddress))
  | extend GrantInitiatedBy = iff(isnotempty(GrantInitiatedByUserPrincipalName), GrantInitiatedByUserPrincipalName, GrantInitiatedByAppName)
  | mv-apply AdditionalDetail = AdditionalDetails on 
    (
        where AdditionalDetail.key =~ "User-Agent"
        | extend GrantUserAgent = AdditionalDetail.value
    )
  | project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, OperationName, ConsentFull, CorrelationId
  | join kind = leftouter (AuditLogs
  | where TimeGenerated > ago(joinLookback)
  | where LoggedByService =~ "Core Directory"
  | where Category =~ "ApplicationManagement"
  | where OperationName =~ "Add service principal"
    | mv-apply TargetResource = TargetResources on 
        (
            where TargetResource.type =~ "ServicePrincipal"
            | extend props = TargetResource.modifiedProperties,
                    AppClientId = tostring(TargetResource.id)
        )
    | mv-apply Property = props on 
        (
            where Property.displayName =~ "AppAddress" and Property.newValue has "AddressType"
            | extend AppReplyURLs = trim('"',tostring(Property.newValue))
        )
  | distinct AppClientId, tostring(AppReplyURLs)
  )
  on AppClientId
  | join kind = innerunique (AuditLogs
  | where TimeGenerated > ago(joinLookback)
  | where LoggedByService =~ "Core Directory"
  | where Category =~ "ApplicationManagement"
  | where OperationName =~ "Add OAuth2PermissionGrant" or OperationName =~ "Add delegated permission grant"
        | mv-apply TargetResource = TargetResources on 
            (
                where TargetResource.type =~ "ServicePrincipal" and array_length(TargetResource.modifiedProperties) > 0 and isnotnull(TargetResource.displayName)
                | extend GrantAuthentication = tostring(TargetResource.displayName)
            )
  | extend GrantOperation = OperationName
  | project GrantAuthentication, GrantOperation, CorrelationId
  ) on CorrelationId
  | project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, AppReplyURLs, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, GrantAuthentication, OperationName, GrantOperation, CorrelationId, ConsentFull
  | extend timestamp = TimeGenerated, Name = tostring(split(GrantInitiatedByUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(GrantInitiatedByUserPrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: GrantInitiatedByUserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: GrantInitiatedByAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: GrantInitiatedByAppServicePrincipalId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: GrantIpAddress
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let detectionTime = 1d;
let joinLookback = 14d;

Stage 1: source

AuditLogs

Stage 2: where

| where TimeGenerated > ago(detectionTime)

Stage 3: where

| where LoggedByService =~ "Core Directory"

Stage 4: where

| where Category =~ "ApplicationManagement"

Stage 5: where

| where OperationName =~ "Consent to application"

Stage 6: where

| where TargetResources has "offline"

Stage 7: kusto:mv-apply

| mv-apply TargetResource = TargetResources on 
  (
      where TargetResource.type =~ "ServicePrincipal"
      | extend AppDisplayName = tostring(TargetResource.displayName),
               AppClientId = tostring(TargetResource.id),
               props = TargetResource.modifiedProperties
  )

Stage 8: where

| where AppClientId !in ((externaldata(knownAppClientId:string, knownAppDisplayName:string)[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/Microsoft.OAuth.KnownApplications.csv"] with (format="csv")))

Stage 9: kusto:mv-apply

| mv-apply ConsentFull = props on 
  (
      where ConsentFull.displayName =~ "ConsentAction.Permissions"
  )

Stage 10: parse

| parse ConsentFull with * "ConsentType: " GrantConsentType ", Scope: " GrantScope1 "]" *

Stage 11: where

| where ConsentFull has_all ("user.read", "offline_access", "mail.readwrite", "mail.send", "files.read.all")

Stage 12: where

| where GrantConsentType != "AllPrincipals"

Stage 13: extend (6 consecutive steps)

| extend GrantInitiatedByAppName = tostring(InitiatedBy.app.displayName)
| extend GrantInitiatedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend GrantInitiatedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend GrantInitiatedByAadUserId = tostring(InitiatedBy.user.id)
| extend GrantIpAddress = iff(isnotempty(InitiatedBy.user.ipAddress), tostring(InitiatedBy.user.ipAddress), tostring(InitiatedBy.app.ipAddress))
| extend GrantInitiatedBy = iff(isnotempty(GrantInitiatedByUserPrincipalName), GrantInitiatedByUserPrincipalName, GrantInitiatedByAppName)

Stage 14: kusto:mv-apply

| mv-apply AdditionalDetail = AdditionalDetails on 
  (
      where AdditionalDetail.key =~ "User-Agent"
      | extend GrantUserAgent = AdditionalDetail.value
  )

Stage 15: project

| project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, OperationName, ConsentFull, CorrelationId

Stage 16: join

| join kind = leftouter (AuditLogs
| where TimeGenerated > ago(joinLookback)
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName =~ "Add service principal"
  | mv-apply TargetResource = TargetResources on 
      (
          where TargetResource.type =~ "ServicePrincipal"
          | extend props = TargetResource.modifiedProperties,
                  AppClientId = tostring(TargetResource.id)
      )
  | mv-apply Property = props on 
      (
          where Property.displayName =~ "AppAddress" and Property.newValue has "AddressType"
          | extend AppReplyURLs = trim('"',tostring(Property.newValue))
      )
| distinct AppClientId, tostring(AppReplyURLs)
)
on AppClientId

Stage 17: join

| join kind = innerunique (AuditLogs
| where TimeGenerated > ago(joinLookback)
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName =~ "Add OAuth2PermissionGrant" or OperationName =~ "Add delegated permission grant"
      | mv-apply TargetResource = TargetResources on 
          (
              where TargetResource.type =~ "ServicePrincipal" and array_length(TargetResource.modifiedProperties) > 0 and isnotnull(TargetResource.displayName)
              | extend GrantAuthentication = tostring(TargetResource.displayName)
          )
| extend GrantOperation = OperationName
| project GrantAuthentication, GrantOperation, CorrelationId
) on CorrelationId

Stage 18: project

project AppClientId, AppDisplayName, AppReplyURLs, ConsentFull, CorrelationId, GrantAuthentication, GrantConsentType, GrantInitiatedBy, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantInitiatedByUserPrincipalName, GrantIpAddress, GrantOperation, GrantScope1, GrantUserAgent, OperationName, TimeGenerated

Stage 19: extend

extend Name, UPNSuffix, timestamp

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AdditionalDetail.keyeq
  • User-Agent transforms: kusto_dynamic_path
field:"AdditionalDetail.key" kind:eq value:"User-Agent"
Categoryeq
  • ApplicationManagement corpus 13 (kusto 13)
field:"Category" kind:eq value:"ApplicationManagement"
ConsentFullmatch
  • files.read.all transforms: term
  • mail.readwrite transforms: term
  • mail.send transforms: term
  • offline_access transforms: term
  • user.read transforms: term
field:"ConsentFull" kind:match
ConsentFull.displayNameeq
  • ConsentAction.Permissions transforms: kusto_dynamic_path
field:"ConsentFull.displayName" kind:eq value:"ConsentAction.Permissions"
GrantConsentTypene
  • AllPrincipals
field:"GrantConsentType" kind:ne value:"AllPrincipals"
LoggedByServiceeq
  • Core Directory corpus 11 (kusto 10, sigma 1)
  • Core Directory
field:"azure_ad::logged_by_service" kind:eq
OperationNameeq
  • Add OAuth2PermissionGrant
  • Add delegated permission grant
  • Add service principal
  • Consent to application corpus 5 (kusto 5)
field:"OperationName" kind:eq
Property.displayNameeq
  • AppAddress transforms: kusto_dynamic_path
field:"Property.displayName" kind:eq value:"AppAddress"
Property.newValuematch
  • AddressType transforms: term, kusto_dynamic_path
field:"Property.newValue" kind:match value:"AddressType"
TargetResource.displayNameis_not_null
  • (no value, null check)
field:"TargetResource.displayName" kind:is_not_null
TargetResource.typeeq
  • ServicePrincipal transforms: kusto_dynamic_path
field:"TargetResource.type" kind:eq value:"ServicePrincipal"
TargetResourcesmatch
  • offline transforms: term corpus 2 (kusto 2)
field:"azure_ad::target_resources" kind:match value:"offline"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AppClientIdproject
AppDisplayNameproject
AppReplyURLsproject
ConsentFullproject
CorrelationIdproject
GrantAuthenticationproject
GrantConsentTypeproject
GrantInitiatedByproject
GrantInitiatedByAadUserIdproject
GrantInitiatedByAppNameproject
GrantInitiatedByAppServicePrincipalIdproject
GrantInitiatedByUserPrincipalNameproject
GrantIpAddressproject
GrantOperationproject
GrantScope1project
GrantUserAgentproject
OperationNameproject
TimeGeneratedproject
Nameextend
UPNSuffixextend
timestampextend

Tenable.ad privileged accounts issues

#
Severity
low
Time window
2h
Group by
Severity
Source
github.com/Azure/Azure-Sentinel

'Searches for triggered Indicators of Exposures related to privileged accounts issues.'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: 353d6474-d795-4086-a179-ba1db4d8bbcb
name: Tenable.ad privileged accounts issues
description: |
    'Searches for triggered Indicators of Exposures related to privileged accounts issues.'
severity: Low
requiredDataConnectors:
  - connectorId: Tenable.ad
    dataTypes:
      - Tenable_ad_CL
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  // For the query to work properly, make sure you have imported the afad_parser.kql parser into the workspace
  // Retrieve the parser here: https://raw.githubusercontent.com/tenable/Azure-Sentinel/Tenable.ad-connector/Solutions/TenableAD/Parsers/afad_parser.kql
  // Then, create the Kusto Function with alias afad_parser
  let SeverityTable=datatable(Severity:string,Level:int) [
  "low", 1,
  "medium", 2,
  "high", 3,
  "critical", 4
  ];
  let codeNameList = datatable(Codename:string)["C-PRIV-ACCOUNTS-SPN", "C-NATIVE-ADM-GROUP-MEMBERS", "C-KRBTGT-PASSWORD", "C-PROTECTED-USERS-GROUP-UNUSED", "C-ADMINCOUNT-ACCOUNT-PROPS", "C-ADM-ACC-USAGE", "C-LAPS-UNSECURE-CONFIG", "C-DISABLED-ACCOUNTS-PRIV-GROUPS"];
  afad_parser
  | where MessageType == 0 and Codename in~ (codeNameList)
  | lookup kind=leftouter SeverityTable on Severity
  | order by Level
  | extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 1.0.1
kind: Scheduled

Stages and Predicates

Let Binding: SeverityTable used in Stage 3

let SeverityTable = datatable(Severity:string,Level:int) [
"low", 1,
"medium", 2,
"high", 3,
"critical", 4
];

Let Binding: codeNameList used in Stage 2

let codeNameList = datatable(Codename:string)["C-PRIV-ACCOUNTS-SPN", "C-NATIVE-ADM-GROUP-MEMBERS", "C-KRBTGT-PASSWORD", "C-PROTECTED-USERS-GROUP-UNUSED", "C-ADMINCOUNT-ACCOUNT-PROPS", "C-ADM-ACC-USAGE", "C-LAPS-UNSECURE-CONFIG", "C-DISABLED-ACCOUNTS-PRIV-GROUPS"];

Stage 1: source

afad_parser

Stage 2: where

| where MessageType == 0 and Codename in~ (codeNameList)

Stage 3: kusto:lookup

lookup kind=leftouter (SeverityTable) on Severity

Stage 4: sort

sort by Level

Stage 5: extend

extend DnsDomain, HostName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DnsDomainextend
HostNameextend

TI map File Hash to Security Event

#
Severity
medium
Time window
14d
Group by
$left.FileHashValue, $right.FileHash, FileHash, IndicatorId
Source
github.com/Azure/Azure-Sentinel

Identifies a match in Security Event data from any File Hash IOC from TI

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Telemetry Coverage

Rule Body

id: a7427ed7-04b4-4e3b-b323-08b981b9b4bf
name: TI map File Hash to Security Event
description: |
  'Identifies a match in Security Event data from any File Hash IOC from TI'
severity: Medium
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvents
  - connectorId: WindowsForwardedEvents
    dataTypes:
      - WindowsEvent
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  ThreatIntelligenceIndicator
  | where isnotempty(FileHashValue)
  | where TimeGenerated >= ago(ioc_lookBack)
  | extend FileHashValue = toupper(FileHashValue)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
  | where Active == true and ExpirationDateTime > now()
  // 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 ( union isfuzzy=true
    (SecurityEvent | where TimeGenerated >= ago(dt_lookBack)
        | where EventID in ("8003","8002","8005")
        | where isnotempty(FileHash)
        | extend SecurityEvent_TimeGenerated = TimeGenerated, Event = EventID, FileHash = toupper(FileHash)
    ),
    (WindowsEvent | where TimeGenerated >= ago(dt_lookBack)
        | where EventID in ("8003","8002","8005")
        | where isnotempty(EventData.FileHash)
        | extend SecurityEvent_TimeGenerated = TimeGenerated, Event = EventID, FileHash = toupper(EventData.FileHash)
    )
  )
  on $left.FileHashValue == $right.FileHash
  | where SecurityEvent_TimeGenerated < ExpirationDateTime
  | summarize SecurityEvent_TimeGenerated = arg_max(SecurityEvent_TimeGenerated, *) by IndicatorId, FileHash
  | project SecurityEvent_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore,
  Process, FileHash, Computer, Account, Event, FileHashValue, FileHashType
  | extend NTDomain = tostring(split(Account, '\\', 0)[0]), Name = tostring(split(Account, '\\', 1)[0])
  | extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')) 
  | extend timestamp = SecurityEvent_TimeGenerated
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
      - identifier: Name
        columnName: Name
      - identifier: NTDomain
        columnName: NTDomain
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
  - entityType: FileHash
    fieldMappings:
      - identifier: Value
        columnName: FileHashValue
      - identifier: Algorithm
        columnName: FileHashType
version: 1.4.6
kind: Scheduled

Stages and Predicates

Parameters

let dt_lookBack = 1h;
let ioc_lookBack = 14d;

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

| where isnotempty(FileHashValue)

Stage 3: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 4: extend

| extend FileHashValue = toupper(FileHashValue)

Stage 5: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId

Stage 6: where

| where Active == true and ExpirationDateTime > now()

Stage 7: join

join kind=innerunique (...) on $left.FileHashValue, $right.FileHash

Stage 8: where

where SecurityEvent_TimeGenerated < ExpirationDateTime

Stage 9: summarize

summarize SecurityEvent_TimeGenerated by IndicatorId, FileHash

Stage 10: project

project Account, ActivityGroupNames, Computer, ConfidenceScore, Description, Event, ExpirationDateTime, FileHash, FileHashType, FileHashValue, IndicatorId, Process, SecurityEvent_TimeGenerated, ThreatType, Url

Stage 11: extend (3 consecutive steps)

extend DnsDomain, HostName, NTDomain, Name, timestamp

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Activeeq
  • true corpus 60 (kusto 60)
field:"Active" kind:eq value:"true"
EventData.FileHashis_not_null
  • (no value, null check)
field:"EventData.FileHash" kind:is_not_null
EventIDin
  • 8002
  • 8003
  • 8005
field:"EventID" kind:in
FileHashis_not_null
  • (no value, null check)
field:"FileHash" kind:is_not_null
FileHashValueis_not_null
  • (no value, null check)
field:"FileHashValue" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountproject
ActivityGroupNamesproject
Computerproject
ConfidenceScoreproject
Descriptionproject
Eventproject
ExpirationDateTimeproject
FileHashproject
FileHashTypeproject
FileHashValueproject
IndicatorIdproject
Processproject
SecurityEvent_TimeGeneratedproject
ThreatTypeproject
Urlproject
NTDomainextend
Nameextend
DnsDomainextend
HostNameextend
timestampextend

Ubiquiti - Possible connection to cryptominning pool

#
Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

'Detects connections which may indicate that device is infected with cryptominer.'

MITRE ATT&CK Coverage

Rule Body

id: 7feb3c32-2a11-4eb8-a2d7-e3792b31cb80
name: Ubiquiti - Possible connection to cryptominning pool
description: |
  'Detects connections which may indicate that device is infected with cryptominer.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    dataTypes:
      - Ubiquiti_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
  - T1095
  - T1571
query: |
  let susp_dst_ports = dynamic(['14433', '14444', '3333', '3334', '3335', '3336', '4444', '45560', '45700', '5555', '5556', '6666', '7777', '8788', '8888', '8899', '9999', '10034']);
  UbiquitiAuditEvent
  | where ipv4_is_private(SrcIpAddr)
  | where ipv4_is_private(DstIpAddr) == 'False'
  | where DstPortNumber in (susp_dst_ports)  or (DnsQuery contains 'pool' and DnsQuery contains 'xmr')
  | extend IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Let Binding: susp_dst_ports used in Stage 4

let susp_dst_ports = dynamic(['14433', '14444', '3333', '3334', '3335', '3336', '4444', '45560', '45700', '5555', '5556', '6666', '7777', '8788', '8888', '8899', '9999', '10034']);

Stage 1: source

UbiquitiAuditEvent

Stage 2: where

| where ipv4_is_private(SrcIpAddr)

Stage 3: where

| where ipv4_is_private(DstIpAddr) == 'False'

Stage 4: where

| where DstPortNumber in (susp_dst_ports)  or (DnsQuery contains 'pool' and DnsQuery contains 'xmr')

Stage 5: extend

| extend IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DnsQuerycontains
  • pool
  • xmr
field:"DnsQuery" kind:contains
DstPortNumberin
  • 10034
  • 14433
  • 14444
  • 3333
  • 3334
  • 3335
  • 3336
  • 4444
  • 45560
  • 45700
  • 5555
  • 5556
  • 6666
  • 7777
  • 8788
  • 8888
  • 8899
  • 9999
field:"DstPortNumber" kind:in
SrcIpAddrcidr_match
  • 10.0.0.0/8
  • 127.0.0.0/8
  • 169.254.0.0/16
  • 172.16.0.0/12
  • 192.168.0.0/16
field:"src_ip" kind:cidr_match

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DstIpAddrcidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:DstIpAddr

Output Fields

These fields are emitted when the rule matches.

FieldSource
IPCustomEntityextend

UniFi Site Manager: External WAN IP changed

#
Status
available
Severity
high
Time window
1h
Group by
SiteId, SiteName
Source
github.com/Azure/Azure-Sentinel

Identifies when a site reports more than one distinct WAN external IP within an hour, which may indicate ISP DHCP refresh, WAN reconfiguration, or routing hijack.

MITRE ATT&CK Coverage

TacticTechniques
Reconnaissance

Rule Body

id: ef1a293a-9e2b-b087-7816-2610814ed2d4
name: "UniFi Site Manager: External WAN IP changed"
description: |
  Identifies when a site reports more than one distinct WAN external IP within an hour, which may indicate ISP DHCP refresh, WAN reconfiguration, or routing hijack.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: UniFiSiteManagerConnectorDefinition
    dataTypes:
      - Unifi_SiteManager_Sites_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Reconnaissance
relevantTechniques:
  - T1590
subTechniques:
  - T1590.005
query: |
  Unifi_SiteManager_Sites_CL
        | where TimeGenerated > ago(1h)
        | extend SiteName = tostring(Meta.name),
                 currentIp = tostring(SiteStatistics.wans.WAN.externalIp)
        | where isnotempty(currentIp)
        | summarize DistinctIps = make_set(currentIp), arg_max(TimeGenerated, currentIp) by SiteId, SiteName
        | where array_length(DistinctIps) > 1
        | extend Activity = strcat('WAN IP changed; observed: ', tostring(DistinctIps))
        | project TimeGenerated, SiteId, SiteName, Activity, CurrentIp = currentIp, ObservedIps = DistinctIps
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CurrentIp
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SiteName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

Stage 1: source

Unifi_SiteManager_Sites_CL

Stage 2: where

| where TimeGenerated > ago(1h)

Stage 3: extend

| extend SiteName = tostring(Meta.name),
               currentIp = tostring(SiteStatistics.wans.WAN.externalIp)

Stage 4: where

| where isnotempty(currentIp)

Stage 5: summarize

| summarize DistinctIps = make_set(currentIp), arg_max(TimeGenerated, currentIp) by SiteId, SiteName

Stage 6: where

| where array_length(DistinctIps) > 1

Stage 7: extend

| extend Activity = strcat('WAN IP changed; observed: ', tostring(DistinctIps))

Stage 8: project

| project TimeGenerated, SiteId, SiteName, Activity, CurrentIp = currentIp, ObservedIps = DistinctIps

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
currentIpis_not_null
  • (no value, null check)
field:"currentIp" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
Activityproject
CurrentIpproject
ObservedIpsproject
SiteIdproject
SiteNameproject
TimeGeneratedproject

Utimaco ESKM - Multiple KMIP authentication failures from same IP

#
Status
available
Severity
medium
Time window
15m
Group by
IP
Source
github.com/Azure/Azure-Sentinel

This query searches for 5 or more KMIP "Authentication Failure" events originating from the same client IP within a 15 minute window on Utimaco ESKM. This may indicate brute-force or credential-stuffing activity against the KMIP server.

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: 9b1a3b3e-7e16-4a3b-8a8f-7f1f2b1c0a01
name: Utimaco ESKM - Multiple KMIP authentication failures from same IP
description: |
  This query searches for 5 or more KMIP "Authentication Failure" events originating from the same client IP within a 15 minute window on Utimaco ESKM. This may indicate brute-force or credential-stuffing activity against the KMIP server.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: UtimacoESKMConnector
    dataTypes:
      - UtimacoESKMKmipServerLogs_CL
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  UtimacoESKMKmipServerLogs_CL
  | where TimeGenerated > ago(15m)
  | where Event == "Authentication Failure"
  | where isnotempty(IP)
  | summarize FailureCount = count(), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), Users = make_set(User, 50) by IP
  | where FailureCount >= 5
  | extend IPCustomEntity = IP
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

UtimacoESKMKmipServerLogs_CL

Stage 2: where

| where TimeGenerated > ago(15m)

Stage 3: where

| where Event == "Authentication Failure"

Stage 4: where

| where isnotempty(IP)

Stage 5: summarize

| summarize FailureCount = count(), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), Users = make_set(User, 50) by IP

Stage 6: where

| where FailureCount >= 5

Stage 7: extend

| extend IPCustomEntity = IP

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Eventeq
  • Authentication Failure
field:"Event" kind:eq value:"Authentication Failure"
FailureCountge
  • 5 transforms: number
field:"FailureCount" kind:ge value:"5"
IPis_not_null
  • (no value, null check)
field:"IP" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
EndTimesummarize
FailureCountsummarize
IPsummarize
StartTimesummarize
Userssummarize
IPCustomEntityextend

Back to all Kusto rules