Detection rules › Kusto

Kusto rules: 0s

1Password - Successful anomalous sign-in

#
Severity
low
Time window
14d
Source
github.com/Azure/Azure-Sentinel

This will alert when a new successful MFA confirmed sign-in has occurred from a location that was not seen within the last 14 days. Ref: https://1password.com/ Ref: https://github.com/securehats/

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

PlatformRecord / event type
1Passwordcredentials_ok
1Passwordmfa_ok

Rule Body

id: ceb20a5c-adce-4eba-9728-541361d47d87
name: 1Password - Successful anomalous sign-in
version: 1.0.0
kind: Scheduled
description: |-
  This will alert when a new successful MFA confirmed sign-in has occurred from a location that was not seen within the last 14 days.
  Ref: https://1password.com/
  Ref: https://github.com/securehats/
severity: Low
requiredDataConnectors:
  - connectorId: 1Password
    dataTypes:
      - OnePasswordEventLogs_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |-
  let rulefrequency = 1h;
  let lookback = 14d;
  let onePasswordSigninBaseline =
      OnePasswordEventLogs_CL
      | where TimeGenerated between (ago(lookback) .. now())
      | where log_source == "signinattempts"
      | where (category == "success" and action_type == "mfa_ok") or (category == "success" and action_type == "credentials_ok")
      | summarize count() by tostring(target_user.uuid), tostring(target_user.email), tostring(client.ip_address), tostring(location.country)
      | extend identifier = strcat(target_user_uuid, client_ip_address)
      | summarize make_list(identifier)
  ;
  OnePasswordEventLogs_CL
  | where TimeGenerated between (ago(rulefrequency) .. now())
  | where log_source == "signinattempts"
  | where (category == "success" and action_type == "mfa_ok") or (category == "success" and action_type == "credentials_ok")
  // limit the amount of incident triggers by enabling and adjusting the following in order to exclude country specific sign-ins
  // | where country !in~ ("CA", "US")
  | extend identifier = strcat(target_user.uuid, client.ip_address)
  | where identifier !in (onePasswordSigninBaseline)
  | extend
      TargetUsername = target_user.email
      , SrcIpAddr = client.ip_address
suppressionEnabled: false
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: 1h
    matchingMethod: AllEntities
suppressionDuration: 1h
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
  alertDynamicProperties: []
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: FullName
    columnName: TargetUsername
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: SrcIpAddr

Stages and Predicates

Parameters

let rulefrequency = 1h;
let lookback = 14d;

Let Binding: onePasswordSigninBaseline used in Stage 6

let onePasswordSigninBaseline = OnePasswordEventLogs_CL
    | where TimeGenerated between (ago(lookback) .. now())
    | where log_source == "signinattempts"
    | where (category == "success" and action_type == "mfa_ok") or (category == "success" and action_type == "credentials_ok")
    | summarize count() by tostring(target_user.uuid), tostring(target_user.email), tostring(client.ip_address), tostring(location.country)
    | extend identifier = strcat(target_user_uuid, client_ip_address)
    | summarize make_list(identifier);

Stage 1: source

OnePasswordEventLogs_CL

Stage 2: where

| where TimeGenerated between (ago(rulefrequency) .. now())

Stage 3: where

| where log_source == "signinattempts"

Stage 4: where

| where (category == "success" and action_type == "mfa_ok") or (category == "success" and action_type == "credentials_ok")

Stage 5: extend

| extend identifier = strcat(target_user.uuid, client.ip_address)

Stage 6: where

| where identifier !in (onePasswordSigninBaseline)

Stage 7: extend

| extend
    TargetUsername = target_user.email
    , SrcIpAddr = client.ip_address

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
identifiereqonePasswordSigninBaselineexcludes:identifier field:"identifier" value:"onePasswordSigninBaseline"

Output Fields

These fields are emitted when the rule matches.

FieldSource
identifierextend
SrcIpAddrextend
TargetUsernameextend

AD FS Remote Auth Sync Connection

#
Status
available
Severity
medium
Time window
1d
Group by
$left.Computer, $left.InstanceId, $right.Computer, $right.InstanceId
Source
github.com/Azure/Azure-Sentinel

This detection uses Security events from the "AD FS Auditing" provider to detect suspicious authentication events on an AD FS server. The results then get correlated with events from the Windows Filtering Platform (WFP) to detect suspicious incoming network traffic on port 80 on the AD FS server. This could be a sign of a threat actor trying to use replication services on the AD FS server to get its configuration settings and extract sensitive information such as AD FS certificates. In order to use this query you need to enable AD FS auditing on the AD FS Server. References: https://docs.microsoft.com/windows-server/identity/ad-fs/troubleshooting/ad-fs-tshoot-logging https://twitter.com/OTR_Community/status/1387038995016732672

MITRE ATT&CK Coverage

TacticTechniques
Collection

Telemetry Coverage

Rule Body

id: 2f4165a6-c4fb-4e94-861e-37f1b4d6c0e6
name: AD FS Remote Auth Sync Connection
description: |
  'This detection uses Security events from the "AD FS Auditing" provider to detect suspicious authentication events on an AD FS server. The results then get correlated with events from the Windows Filtering Platform (WFP) to detect suspicious incoming network traffic on port 80 on the AD FS server.
  This could be a sign of a threat actor trying to use replication services on the AD FS server to get its configuration settings and extract sensitive information such as AD FS certificates. In order to use this query you need to enable AD FS auditing on the AD FS Server.
  References:
  https://docs.microsoft.com/windows-server/identity/ad-fs/troubleshooting/ad-fs-tshoot-logging
  https://twitter.com/OTR_Community/status/1387038995016732672'
severity: Medium
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Collection
relevantTechniques:
  - T1005
tags:
  - SimuLand
query: |
  // Adjust this to use a longer timeframe to identify ADFS servers
  //let lookback = 0d;
  // Adjust this to adjust detection timeframe
  //let timeframe = 1d;
  // SamAccountName of AD FS Service Account. Filter on the use of a specific AD FS user account
  //let adfsuser = 'adfsadmin';
  // Identify ADFS Servers
  let ADFS_Servers = (
      SecurityEvent
      //| where TimeGenerated > ago(timeframe+lookback)
      | where EventSourceName == 'AD FS Auditing'
      | distinct Computer
  );
  SecurityEvent
      //| where TimeGenerated > ago(timeframe)
      | where Computer in~ (ADFS_Servers)
      // A token of type 'http://schemas.microsoft.com/ws/2006/05/servicemodel/tokens/SecureConversation'
      // for relying party '-' was successfully authenticated.
      | where EventID == 412
      | extend EventData = parse_xml(EventData).EventData.Data
      | extend InstanceId = tostring(EventData[0])
  | join kind=inner
  (
      SecurityEvent
      //| where TimeGenerated > ago(timeframe)
      | where Computer in~ (ADFS_Servers)
      // Events to identify caller identity from event 412
      | where EventID == 501
      | extend EventData = parse_xml(EventData).EventData.Data
      | where tostring(EventData[1]) contains 'identity/claims/name'
      | extend InstanceId = tostring(EventData[0])
      | extend ClaimsName = tostring(EventData[2])
      // Filter on the use of a specific AD FS user account
      //| where ClaimsName contains adfsuser
  )
  on $left.InstanceId == $right.InstanceId
  | join kind=inner
  (
      SecurityEvent
      | where EventID == 5156
      | where Computer in~ (ADFS_Servers)
      | extend EventData = parse_xml(EventData).EventData.Data
      | mv-expand bagexpansion=array EventData
      | evaluate bag_unpack(EventData)
      | extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
      | evaluate pivot(Key, any(Value), TimeGenerated, Computer, EventID)
      | extend DestPort = column_ifexists("DestPort", ""),
            Direction = column_ifexists("Direction", ""),
            Application = column_ifexists("Application", ""),
            DestAddress = column_ifexists("DestAddress", ""),
            SourceAddress = column_ifexists("SourceAddress", ""),
            SourcePort = column_ifexists("SourcePort", "")
      // Look for inbound connections from endpoints on port 80
      | where DestPort == 80 and Direction == '%%14592' and Application == 'System'
      | where DestAddress !in ('::1','0:0:0:0:0:0:0:1')
  )
  on $left.Computer == $right.Computer
  | project TimeGenerated, Computer, ClaimsName, SourceAddress, SourcePort
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | extend AccountName = tostring(split(ClaimsName, @'\')[1]), AccountNTDomain = tostring(split(ClaimsName, @'\')[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ClaimsName
      - identifier: Name
        columnName: AccountName
      - identifier: NTDomain
        columnName: AccountNTDomain
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceAddress
version: 1.0.4
kind: Scheduled

Stages and Predicates

Let Binding: ADFS_Servers used in Stages 2, 6, 7

let ADFS_Servers = (
    SecurityEvent
    | where EventSourceName == 'AD FS Auditing'
    | distinct Computer
);

Stage 1: source

SecurityEvent

Stage 2: where

| where Computer in~ (ADFS_Servers)

Stage 3: where

| where EventID == 412

Stage 4: extend

| extend EventData = parse_xml(EventData).EventData.Data

Stage 5: extend

| extend InstanceId = tostring(EventData[0])

Stage 6: join

| join kind=inner
(
    SecurityEvent
    | where Computer in~ (ADFS_Servers)
    | where EventID == 501
    | extend EventData = parse_xml(EventData).EventData.Data
    | where tostring(EventData[1]) contains 'identity/claims/name'
    | extend InstanceId = tostring(EventData[0])
    | extend ClaimsName = tostring(EventData[2])
)
on $left.InstanceId == $right.InstanceId

Stage 7: join

| join kind=inner
(
    SecurityEvent
    | where EventID == 5156
    | where Computer in~ (ADFS_Servers)
    | extend EventData = parse_xml(EventData).EventData.Data
    | mv-expand bagexpansion=array EventData
    | evaluate bag_unpack(EventData)
    | extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
    | evaluate pivot(Key, any(Value), TimeGenerated, Computer, EventID)
    | extend DestPort = column_ifexists("DestPort", ""),
          Direction = column_ifexists("Direction", ""),
          Application = column_ifexists("Application", ""),
          DestAddress = column_ifexists("DestAddress", ""),
          SourceAddress = column_ifexists("SourceAddress", ""),
          SourcePort = column_ifexists("SourcePort", "")
    | where DestPort == 80 and Direction == '%%14592' and Application == 'System'
    | where DestAddress !in ('::1','0:0:0:0:0:0:0:1')
)
on $left.Computer == $right.Computer

Stage 8: project

project ClaimsName, Computer, SourceAddress, SourcePort, TimeGenerated

Stage 9: extend (3 consecutive steps)

extend AccountNTDomain, AccountName, DomainIndex, HostName, HostNameDomain

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DestAddressin0:0:0:0:0:0:0:1, ::1excludes:DestAddress field:"DestAddress" value:"0:0:0:0:0:0:0:1" field:"DestAddress" value:"::1"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ClaimsNameproject
Computerproject
SourceAddressproject
SourcePortproject
TimeGeneratedproject
DomainIndexextend
HostNameextend
HostNameDomainextend
AccountNTDomainextend
AccountNameextend

Admin password not updated in 30 days

#

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
30m
Source
github.com/Azure/Azure-Sentinel

The policy detects an administrative account where the password of the account was not updated in the last 30 days.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 63d87fcb-d197-48d2-a642-de4813f0219a
name: Admin password not updated in 30 days
kind: Scheduled
description: The policy detects an administrative account where the password of the account was not updated in the last 30 days.
severity: Medium
status: Available
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |-
  Authomize_v2_CL
  | where ingestion_time() >= ago(30m)
  | extend EventID = id_s, Policy = policy_name_s, Severity = severity_s,Description = description_s,Recommendation = recommendation_s,URL = url_s,Tactics = tactics_s
  | where Policy has "Admin password wasn't updated during the last 30 days"
  | project  EventID, Policy, Severity, Description, Recommendation, URL, Category, Tactics
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: URL
requiredDataConnectors:
- connectorId: Authomize
  dataTypes: [ "Authomize_v2_CL" ]
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
  alertnameFormat: Alert from Authomize - Admin password wasn't updated during the last 30 days
  alertDescriptionFormat: |
    Admin password wasn't updated during the last 30 days. The policy detects admin account where password wasn't updated during the last 30 days.
  alertSeverity: Severity
  alertTactics: Tactics
  alertDynamicProperties:
  - alertProperty: AlertLink
    value: URL
customDetails:
  AuthomizeEventID: EventID
  EventName: Policy
  EventDescription: Description
  EventRecommendation: Recommendation
  ReferencedURL: URL
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.2

Stages and Predicates

Stage 1: source

Authomize_v2_CL

Stage 2: where

| where ingestion_time() >= ago(30m)

Stage 3: extend

| extend EventID = id_s, Policy = policy_name_s, Severity = severity_s,Description = description_s,Recommendation = recommendation_s,URL = url_s,Tactics = tactics_s

Stage 4: where

| where Policy has "Admin password wasn't updated during the last 30 days"

Stage 5: project

| project  EventID, Policy, Severity, Description, Recommendation, URL, Category, Tactics

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Policymatch
  • Admin password wasn't updated during the last 30 days transforms: term
field:"policy_name_s" kind:match value:"Admin password wasn't updated during the last 30 days"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Categoryproject
Descriptionproject
EventIDproject
Policyproject
Recommendationproject
Severityproject
Tacticsproject
URLproject

Anomaly in SMB Traffic(ASIM Network Session schema)

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

This detection detects abnormal SMB traffic, a file-sharing protocol. By calculating the average deviation of SMB connections over last 14 days, flagging sources exceeding 50 average deviations.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 8717e498-7b5d-4e23-9e7c-fa4913dbfd79
name: Anomaly in SMB Traffic(ASIM Network Session schema)
description: |
  'This detection detects abnormal SMB traffic, a file-sharing protocol. By calculating the average deviation of SMB connections over last 14 days, flagging sources exceeding 50 average deviations.'
severity: Medium
status: Available 
tags:
  - Schema: ASimNetworkSessions
    SchemaVersion: 0.2.4
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - LateralMovement
relevantTechniques:
  - T1021
  - T1021.002
query: |
  // Define the threshold for deviation
  let threshold = 50;
  // Define the time range for the baseline data
  let starttime = 14d;
  let endtime = 1d;
  // Define the SMB ports to monitor
  let SMBPorts = dynamic(["139", "445"]);
  // Get the baseline data for user network sessions and Filter for the defined time range
  let userBaseline = _Im_NetworkSession(starttime=ago(starttime), endtime=ago(endtime))
    | where ipv4_is_private(SrcIpAddr) and tostring(DstPortNumber) has_any (SMBPorts) and SrcIpAddr != DstIpAddr // Filter for private IP addresses and SMB ports
    | summarize Count = count() by SrcIpAddr, DstPortNumber // Group by source IP and destination port
    | summarize AvgCount = avg(Count) by SrcIpAddr, DstPortNumber; // Calculate the average count
  // Get the recent user activity data and Filter for recent activity
  let recentUserActivity = _Im_NetworkSession(starttime=ago(endtime))
    | where ipv4_is_private(SrcIpAddr) and tostring(DstPortNumber) has_any (SMBPorts) and SrcIpAddr != DstIpAddr // Filter for private IP addresses and SMB ports
    | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), RecentCount = count() by SrcIpAddr, DstPortNumber; // Group by source IP and destination port
  // Join the baseline and recent activity data
  let UserBehaviorAnalysis = userBaseline
    | join kind=inner (recentUserActivity) on SrcIpAddr, DstPortNumber
    | extend Deviation = abs(RecentCount - AvgCount) / AvgCount; // Calculate the deviation
  // Filter for deviations greater than the threshold
  UserBehaviorAnalysis
    | where Deviation > threshold
    | project SrcIpAddr, DstPortNumber, Deviation, Count = RecentCount; // Project the required columns
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
eventGroupingSettings:
  aggregationKind: AlertPerResult
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 50;
let starttime = 14d;
let endtime = 1d;
let SMBPorts = dynamic(["139", "445"]);

let userBaseline and let UserBehaviorAnalysis are inlined into the numbered stages below.

Let Binding: recentUserActivity used in Stages 2, 8

let recentUserActivity = _Im_NetworkSession(starttime=ago(endtime))
  | where ipv4_is_private(SrcIpAddr) and tostring(DstPortNumber) has_any (SMBPorts) and SrcIpAddr != DstIpAddr
  | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), RecentCount = count() by SrcIpAddr, DstPortNumber;

Stage 1: source

let userBaseline

Stage 2: source

let recentUserActivity

Stage 3: source

let UserBehaviorAnalysis

Stage 4: source

_Im_NetworkSession(...)

Stage 5: where

where (DstPortNumber has 139 or DstPortNumber has 445) and (ipv4_is_in_range(SrcIpAddr, "10.0.0.0/8") or ipv4_is_in_range(SrcIpAddr, "172.16.0.0/12") or ipv4_is_in_range(SrcIpAddr, "192.168.0.0/16") or ipv4_is_in_range(SrcIpAddr, "169.254.0.0/16") or ipv4_is_in_range(SrcIpAddr, "127.0.0.0/8")) and SrcIpAddr != DstIpAddr

Stage 6: summarize

summarize Count by SrcIpAddr, DstPortNumber

Stage 7: summarize

summarize AvgCount by SrcIpAddr, DstPortNumber

Stage 8: join

join kind=inner (recentUserActivity) on SrcIpAddr, DstPortNumber

Stage 9: extend

extend Deviation

Stage 10: where

where Deviation > 50

Stage 11: project

project Count, Deviation, DstPortNumber, SrcIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Deviationgt
  • 50 transforms: number
field:"Deviation" kind:gt value:"50"
DstPortNumbermatch
  • 139 transforms: tostring, term
  • 445 transforms: tostring, term
field:"DstPortNumber" kind:match
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:"SrcIpAddr" kind:cidr_match

Output Fields

These fields are emitted when the rule matches.

FieldSource
Countproject
Deviationproject
DstPortNumberproject
SrcIpAddrproject

API - Rate limiting

#
Status
available
Severity
low
Time window
7d
Source
github.com/Azure/Azure-Sentinel

'42Crunch API protection against first-time access'

MITRE ATT&CK Coverage

Rule Body

id: c6258d51-7b82-4942-8293-94c1dcf91595
name: API - Rate limiting
description: |
  '42Crunch API protection against first-time access'
severity: Low
requiredDataConnectors:
  - connectorId: 42CrunchAPIProtection
    dataTypes:
      - FortyTwoCrunchAPIProtection
  - connectorId: FortyTwoCrunchAPIProtection
    dataTypes:
      - FortyTwoCrunchAPIProtection
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: SingleAlert
status: Available
tactics:
  - Discovery
  - InitialAccess
relevantTechniques:
  - T1087
  - T1190
query: |
  let firstRec = FortyTwoCrunchAPIProtection
  | where TimeGenerated >= ago(5m) 
  | project-away NonBlockingMode, SourcePort, DestinationPort, Query, ApiId, RequestHeader, ResponseHeader, Errors, EventType, Uuid
  | where UriPath has "/api/login" |
  top 1 by Timestamp desc;
  let ipAddress = toscalar(firstRec | summarize by SourceIp);
  let secondRec = FortyTwoCrunchAPIProtection
  | where TimeGenerated >= ago(7d) 
  | project-away NonBlockingMode, SourcePort, DestinationPort, Query, ApiId, RequestHeader, ResponseHeader, Errors, EventType, Uuid
  | where UriPath has "/api/login" and SourceIp == ipAddress;
  let recCount = iff((toscalar(secondRec | count) > 1), 0, 1);
  secondRec | take recCount
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 secondRec is inlined into the numbered stages below.

Let Binding: firstRec

let firstRec = FortyTwoCrunchAPIProtection
| where TimeGenerated >= ago(5m) 
| project-away NonBlockingMode, SourcePort, DestinationPort, Query, ApiId, RequestHeader, ResponseHeader, Errors, EventType, Uuid
| where UriPath has "/api/login" |
top 1 by Timestamp desc;

Let Binding: ipAddress used in Stage 4

let ipAddress = toscalar(firstRec | summarize by SourceIp);

Let Binding: recCount used in Stage 5

let recCount = iff((toscalar(secondRec | count) > 1), 0, 1);

Stages 1 to 4 define let secondRec as the rule's main pipeline source. Stage 5 uses it.

Stage 1: source

FortyTwoCrunchAPIProtection

Stage 2: where

| where TimeGenerated >= ago(7d)

Stage 3: project-away

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

Stage 4: where

| where UriPath has "/api/login" and SourceIp == ipAddress

Stage 5: take

secondRec
| take recCount

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
UriPathmatch
  • /api/login transforms: term
field:"UriPath" kind:match value:"/api/login"

BitSight - drop in company ratings

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

'Rule helps to detect when there is a drop of 10% or more in BitSight company ratings.'

MITRE ATT&CK Coverage

TacticTechniques
Reconnaissance
Command & Control

Rule Body

id: d8844f11-3a36-4b97-9062-1e6d57c00e37
name: BitSight - drop in company ratings
description: |
  'Rule helps to detect when there is a drop of 10% or more in BitSight company ratings.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: BitSight
    dataTypes:
      - BitSightGraphData
queryFrequency: 1d
queryPeriod: 24h
triggerOperator: GreaterThan
triggerThreshold: 0
tactics:
  - Reconnaissance
  - CommandAndControl
relevantTechniques:
  - T1591
  - T1090
query: |
    let timeframe = 24h;
    BitSightGraphData
    | where ingestion_time() > ago(timeframe)and toint(RatingDifferance) < 0
    | extend percentage = -(toreal(RatingDifferance)/toreal(Rating))*100
    | where percentage >= 10
    | project RatingDate, Rating, CompanyName, percentage
incidentConfiguration:
    createIncident: false
eventGroupingSettings:
    aggregationKind: AlertPerResult
alertDetailsOverride:
    alertDisplayNameFormat: 'BitSight : Alert for >10% drop in ratings of {{CompanyName}}.'
    alertDescriptionFormat: 'Alert is generated for {{CompanyName}}.\n\nRating Date: {{RatingDate}}\nPercentage Drop: {{percentage}}%'
customDetails:
  CompanyName: CompanyName
  CompanyRating: Rating
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let timeframe = 24h;

Stage 1: source

BitSightGraphData

Stage 2: where

| where ingestion_time() > ago(timeframe)and toint(RatingDifferance) < 0

Stage 3: extend

| extend percentage = -(toreal(RatingDifferance)/toreal(Rating))*100

Stage 4: where

| where percentage >= 10

Stage 5: project

| project RatingDate, Rating, CompanyName, percentage

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
CompanyNameproject
Ratingproject
RatingDateproject
percentageproject

BitSight - new breach found

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

'Rule helps to detect a new breach generated in BitSight.'

MITRE ATT&CK Coverage

Rule Body

id: a5526ba9-5997-47c6-bf2e-60a08b681e9b
name: BitSight - new breach found
description: |
  'Rule helps to detect a new breach generated in BitSight.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BitSight
    dataTypes:
      - BitSightBreaches
queryFrequency: 1d
queryPeriod: 24h
triggerOperator: GreaterThan
triggerThreshold: 0
tactics:
  - Impact
  - InitialAccess
relevantTechniques:
  - T1491
  - T1190
query: |
    let timeframe = 24h;
    BitSightBreaches
    | where ingestion_time() > ago(timeframe)
    | extend Severity = toreal(Severity)
    | extend Severity = case( Severity == 1, "Low",
                              Severity == 2, "Medium",
                              Severity == 3, "High",
                              "Informational")
    | project DateCreated, Companyname, Severity, PreviwURL, GUID
incidentConfiguration:
    createIncident: false
eventGroupingSettings:
    aggregationKind: AlertPerResult
alertDetailsOverride:
    alertDisplayNameFormat: 'BitSight: Alert for new breach in {{Companyname}}.'
    alertDescriptionFormat: 'Alert is generated on {{DateCreated}} at BitSight.\n\nGUID: {{GUID}}\nPreview URL: {{PreviwURL}}'
    alertSeverityColumnName: Severity
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: PreviwURL
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let timeframe = 24h;

Stage 1: source

BitSightBreaches

Stage 2: where

| where ingestion_time() > ago(timeframe)

Stage 3: extend

| extend Severity = toreal(Severity)

Stage 4: extend

| extend Severity = case( Severity == 1, "Low",
                          Severity == 2, "Medium",
                          Severity == 3, "High",
                          "Informational")
Severity =
ifSeverity == 1"Low"
elifSeverity == 2"Medium"
elifSeverity == 3"High"
else"Informational"

Stage 5: project

| project DateCreated, Companyname, Severity, PreviwURL, GUID

Output Fields

These fields are emitted when the rule matches.

FieldSource
Companynameproject
DateCreatedproject
GUIDproject
PreviwURLproject
Severityproject

BloodHound Attack Path Finding - Non-Tier Zero Principals With ADCS ESC4 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 ESC4 Privileges

Rule Body

id: dcd1c727-2084-44d8-871c-a74ac0d8f091
name: BloodHound Attack Path Finding - Non-Tier Zero Principals With ADCS ESC4 Privileges
description: BloodHound Attack Path Finding - Non-Tier Zero Principals With ADCS ESC4
  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 == "T0ADCSESC4"
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 == "T0ADCSESC4"

Indicators

These rows show field, operator, and value matches.

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

Cisco Duo - Admin user deleted

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

'Detects when admin user is deleted.'

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: 6424c623-31a5-4892-be33-452586fd4075
name: Cisco Duo - Admin user deleted
description: |
  'Detects when admin user is deleted.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CiscoDuoActivityConnectorDefinition
    dataTypes:
      - DuoActivity_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1531
query: |
  DuoActivity_CL
  | where ActionName =~ "admin_delete"
  | extend AccountCustomEntity = TargetName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

DuoActivity_CL

Stage 2: where

| where ActionName =~ "admin_delete"

Stage 3: extend

| extend AccountCustomEntity = TargetName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend

Cognni Incidents for Medium Sensitivity Business Information

#

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
5h
Source
github.com/Azure/Azure-Sentinel

'Display incidents in which medium sensitivity business information was placed at risk by user sharing.'

MITRE ATT&CK Coverage

TacticTechniques
Collection

Rule Body

id: 2c286288-3756-4824-b599-d3c499836c11
name: Cognni Incidents for Medium Sensitivity Business Information
description: |
  'Display incidents in which medium sensitivity business information was placed at risk by user sharing.'
severity: Medium
requiredDataConnectors:
  - connectorId: CognniSentinelDataConnector
    dataTypes:
      - CognniIncidents_CL
queryFrequency: 5h
queryPeriod: 5h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Collection
relevantTechniques:
  - T1530
query: |
  let mediumRisk = 2;
  let business = 'Business Information';
  CognniIncidents_CL 
  | where Severity == mediumRisk
  | where informationType_s == business
  | where TimeGenerated >= ago(5h)
  | extend AccountCustomEntity = userId_s
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let mediumRisk = 2;
let business = 'Business Information';

Stage 1: source

CognniIncidents_CL

Stage 2: where

| where Severity == mediumRisk

Stage 3: where

| where informationType_s == business

Stage 4: where

| where TimeGenerated >= ago(5h)

Stage 5: extend

| extend AccountCustomEntity = userId_s

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend

Cross-tenant Access Settings Organization Added

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

Organizations are added in the Cross-tenant Access Settings to control communication inbound or outbound for users and applications. This detection notifies when an Organization is added other than the list that is supposed to exist from the Microsoft Entra ID Cross-tenant Access Settings.

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 757e6a79-6d23-4ae6-9845-4dac170656b5
name: Cross-tenant Access Settings Organization Added
description: |
  'Organizations are added in the Cross-tenant Access Settings to control communication inbound or outbound for users and applications. This detection notifies when an Organization is added other than the list that is supposed to exist from the Microsoft Entra ID Cross-tenant Access Settings.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 2d
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - Persistence
  - Discovery
relevantTechniques:
  - T1078.004
  - T1136.003
  - T1087.004
query: |
  // Tenants IDs can be found by navigating to Azure Active Directory then from menu on the left, select External Identities, then from menu on the left, select Cross-tenant access settings and from the list shown of Tenants
  let ExpectedTenantIDs = dynamic(["List of expected tenant IDs","Tenant ID 2"]);
  AuditLogs
  | where OperationName has "Add a partner to cross-tenant access setting"
  | mv-apply TargetResource = TargetResources on
    (
        where TargetResource.type =~ "Policy"
        | extend Properties = TargetResource.modifiedProperties
    )
  | mv-apply Property = Properties on
    (
        where Property.displayName =~ "tenantId"
        | extend ExtTenantIDAdded = trim('"',tostring(Property.newValue))
    )
  | where ExtTenantIDAdded !in (ExpectedTenantIDs)
  | extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
  | extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
  | extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
  | extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
  | extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
  | extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: InitiatingAppName
      - identifier: AadUserId
        columnName: InitiatingAppServicePrincipalId
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName
      - identifier: Name
        columnName: InitiatingAccountName
      - identifier: UPNSuffix
        columnName: InitiatingAccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIpAddress
version: 1.1.1
kind: Scheduled

Stages and Predicates

Parameters

let ExpectedTenantIDs = dynamic(["List of expected tenant IDs","Tenant ID 2"]);

Stage 1: source

AuditLogs

Stage 2: where

| where OperationName has "Add a partner to cross-tenant access setting"

Stage 3: kusto:mv-apply

| mv-apply TargetResource = TargetResources on
  (
      where TargetResource.type =~ "Policy"
      | extend Properties = TargetResource.modifiedProperties
  )

Stage 4: kusto:mv-apply

| mv-apply Property = Properties on
  (
      where Property.displayName =~ "tenantId"
      | extend ExtTenantIDAdded = trim('"',tostring(Property.newValue))
  )

Stage 5: where

| where ExtTenantIDAdded !in (ExpectedTenantIDs)

Stage 6: extend (6 consecutive steps)

| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
OperationNamematch
  • Add a partner to cross-tenant access setting transforms: term
field:"OperationName" kind:match value:"Add a partner to cross-tenant access setting"
Property.displayNameeq
  • tenantId transforms: kusto_dynamic_path
field:"Property.displayName" kind:eq value:"tenantId"
TargetResource.typeeq
  • Policy transforms: kusto_dynamic_path
field:"TargetResource.type" kind:eq value:"Policy"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ExtTenantIDAddedinList of expected tenant IDs, Tenant ID 2excludes:ExtTenantIDAdded field:"ExtTenantIDAdded" value:"List of expected tenant IDs" field:"ExtTenantIDAdded" value:"Tenant ID 2"

Output Fields

These fields are emitted when the rule matches.

FieldSource
InitiatingAppNameextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
InitiatingIpAddressextend
InitiatingAccountNameextend
InitiatingAccountUPNSuffixextend

Cyble Vision Alerts Discord Keyword

#

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
low
Time window
30m
Source
github.com/Azure/Azure-Sentinel

'Triggers when monitored keywords or risky content appear in Discord channels. Useful for detecting data leakage, impersonation, abuse or reputational threats originating from social platforms.'

MITRE ATT&CK Coverage

Rule Body

id: 601a5859-0dc2-452d-8d1e-66dc651c16d5
name: Cyble Vision Alerts Discord Keyword
description: |
  'Triggers when monitored keywords or risky content appear in Discord channels. Useful for detecting data leakage, impersonation, abuse or reputational threats originating from social platforms.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CybleVisionAlerts
  dataTypes:
  - CybleVisionAlerts_CL
enabled: true
queryfrequency: 30m
queryPeriod: 30m
triggerOperator: GreaterThan
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics:
- Reconnaissance
- InitialAccess
relevantTechniques:
- T1598
- T1664
query: |
  Alerts_discord  
  | where Service == "discord" 
  | extend MappedSeverity = Severity
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
  alertDetailsOverride:
  alertDisplayNameFormat: Discord Alert Risky Content by {{DC_AuthorUsername}}
  alertDescriptionFormat: |
    A monitored keyword or harmful content was detected in Discord.
    **Server:** {{DC_ServerName}}
    **Channel:** {{DC_ChannelName}}
    **Message:** {{DC_Content}}
customDetails:
  MappedSeverity: Severity
  Status: Status
  AlertID: AlertID
  Service: Service
  DC_MessageID: DC_MessageID
  DC_Content: DC_Content
  DC_ChannelName: DC_ChannelName
  DC_ServerName: DC_ServerName
  DC_AuthorUsername: DC_AuthorUsername
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: AadUserId
    columnName: DC_AuthorID
  - identifier: Name
    columnName: DC_AuthorUsername
- entityType: CloudApplication
  fieldMappings:
  - identifier: Name
    columnName: DC_ServerName
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

Alerts_discord

Stage 2: where

| where Service == "discord"

Stage 3: extend

| extend MappedSeverity = Severity

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
MappedSeverityextend

CYFIRMA - Medium severity Command & Control Network Indicators with Monitor Recommendation Rule

#

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.

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

"This analytics rule detects network-based indicators identified by CYFIRMA threat intelligence as associated with Command & Control (C2) infrastructure. These may represent attacker-controlled endpoints used to maintain persistence, exfiltrate data, or receive commands from malware-infected hosts."

MITRE ATT&CK Coverage

Rule Body

id: 75931755-27cd-4e4b-a51c-efcca452e2ff
name: CYFIRMA - Medium severity Command & Control Network Indicators with Monitor Recommendation Rule
description: |
  "This analytics rule detects network-based indicators identified by CYFIRMA threat intelligence as associated with Command & Control (C2) infrastructure.
  These may represent attacker-controlled endpoints used to maintain persistence, exfiltrate data, or receive commands from malware-infected hosts."
version: 1.0.1
kind: Scheduled
severity: Medium
enabled: false
requiredDataConnectors:
  - connectorId: CyfirmaCyberIntelligenceDC
    dataTypes:
      - CyfirmaIndicators_CL
query: |
  // Network Indicators query with Monitor Recommended Action And C2 Roles
  let timeFrame= 5m;
  CyfirmaIndicators_CL 
  | where  (ConfidenceScore < 80 and ConfidenceScore >= 50)
      and TimeGenerated between (ago(timeFrame) .. now())
      and pattern !contains 'file:hashes' and RecommendedActions has 'Monitor' and (Roles contains 'c2' or Roles contains 'Command & Control')
  | extend IPv4 = extract(@"ipv4-addr:value\s*=\s*'([^']+)'", 1, pattern)
  | extend IPv6 = extract(@"ipv6-addr:value\s*=\s*'([^']+)'", 1, pattern)
  | extend URL = extract(@"url:value\s*=\s*'([^']+)'", 1, pattern)
  | extend Domain = extract(@"domain-name:value\s*=\s*'([^']+)'", 1, pattern)
  | extend parsed = parse_json(extensions)
  | extend extensionKeys = bag_keys(parsed)
  | mv-expand extensionKeys
  | extend extensionKeyStr = tostring(extensionKeys)
  | extend ext = parsed[extensionKeyStr]
  | extend props = ext.properties
  | extend 
      extension_id = extensionKeyStr,
      ASN_Owner = props.asn_owner,
      ASN = props.asn,
      ProviderName = 'CYFIRMA',
      ProductName = 'DeCYFIR/DeTCT'
  | project
      IPv4,
      IPv6,
      URL,
      Domain,
      ThreatActors,
      RecommendedActions,
      Sources,
      Roles,
      Country,
      IPAbuse,
      name,
      Description,
      ConfidenceScore,
      IndicatorID,
      created,
      modified,
      valid_from,
      Tags,
      ThreatType,
      TimeGenerated,
      SecurityVendors,
      ProductName,
      ProviderName
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: GreaterThan
triggerThreshold: 0
suppressionDuration: 5m
suppressionEnabled: true
tactics:
  - CommandAndControl
  - InitialAccess
  - Exfiltration
relevantTechniques:
  - T1071
  - T1090
  - T1573
  - T1105
  - T1568
  - T1566
  - T1041
  - T1071.001
  - T1573.001
  - T1568.002
  - T1566.002
alertDetailsOverride:
  alertDisplayNameFormat: "High-Confidence Command & Control Network Indicators with Monitor Recommendation Rule - {{name}} "
  alertDescriptionFormat: "{{Description}} - {{name}} "
  alertDynamicProperties:
    - alertProperty: ProductName
      value: ProductName
    - alertProperty: ProviderName
      value: ProviderName
customDetails:
  ThreatActors: ThreatActors
  Sources: Sources
  RecommendedActions: RecommendedActions
  Roles: Roles
  Country: Country
  Description: Description
  ConfidenceScore: ConfidenceScore
  SecurityVendors: SecurityVendors
  IndicatorID: IndicatorID
  Created: created
  Modified: modified
  ValidFrom: valid_from
  Tags: Tags
  ThreatType: ThreatType
  TimeGenerated: TimeGenerated
  IPAbuse: IPAbuse
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPv4
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPv6
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: Domain
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: URL
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult

Stages and Predicates

Parameters

let timeFrame = 5m;

Stage 1: source

CyfirmaIndicators_CL

Stage 2: where

| where  (ConfidenceScore < 80 and ConfidenceScore >= 50)
    and TimeGenerated between (ago(timeFrame) .. now())
    and pattern !contains 'file:hashes' and RecommendedActions has 'Monitor' and (Roles contains 'c2' or Roles contains 'Command & Control')

Stage 3: extend (6 consecutive steps)

| extend IPv4 = extract(@"ipv4-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend IPv6 = extract(@"ipv6-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend URL = extract(@"url:value\s*=\s*'([^']+)'", 1, pattern)
| extend Domain = extract(@"domain-name:value\s*=\s*'([^']+)'", 1, pattern)
| extend parsed = parse_json(extensions)
| extend extensionKeys = bag_keys(parsed)

Stage 4: mv-expand

| mv-expand extensionKeys

Stage 5: extend (4 consecutive steps)

| extend extensionKeyStr = tostring(extensionKeys)
| extend ext = parsed[extensionKeyStr]
| extend props = ext.properties
| extend 
    extension_id = extensionKeyStr,
    ASN_Owner = props.asn_owner,
    ASN = props.asn,
    ProviderName = 'CYFIRMA',
    ProductName = 'DeCYFIR/DeTCT'

Stage 6: project

| project
    IPv4,
    IPv6,
    URL,
    Domain,
    ThreatActors,
    RecommendedActions,
    Sources,
    Roles,
    Country,
    IPAbuse,
    name,
    Description,
    ConfidenceScore,
    IndicatorID,
    created,
    modified,
    valid_from,
    Tags,
    ThreatType,
    TimeGenerated,
    SecurityVendors,
    ProductName,
    ProviderName

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
patterncontainsfile:hashesexcludes:pattern field:"pattern" value:"file:hashes"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ConfidenceScoreproject
Countryproject
Descriptionproject
Domainproject
IPAbuseproject
IPv4project
IPv6project
IndicatorIDproject
ProductNameproject
ProviderNameproject
RecommendedActionsproject
Rolesproject
SecurityVendorsproject
Sourcesproject
Tagsproject
ThreatActorsproject
ThreatTypeproject
TimeGeneratedproject
URLproject
createdproject
modifiedproject
nameproject
valid_fromproject

CYFIRMA - Social and Public Exposure - Confidential Files Information Exposure Rule

#

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
high
Time window
5m
Source
github.com/Azure/Azure-Sentinel

"This rule detects high-severity alerts from CYFIRMA regarding exposure of confidential files or forms linked to internal or client-related information, publicly accessible on platforms. These exposures could lead to data leakage, compliance violations, or targeted attacks."

MITRE ATT&CK Coverage

Rule Body

id: 67e9c4aa-a2fa-4e4e-9272-1a8da41475c6
name: CYFIRMA - Social and Public Exposure - Confidential Files Information Exposure Rule
description: |
  "This rule detects high-severity alerts from CYFIRMA regarding exposure of confidential files or forms linked to internal or client-related information, publicly accessible on platforms. 
  These exposures could lead to data leakage, compliance violations, or targeted attacks."
version: 1.0.1

kind: Scheduled
severity: High
requiredDataConnectors:
  - connectorId: CyfirmaDigitalRiskAlertsConnector
    dataTypes:
      - CyfirmaSPEConfidentialFilesAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - Exfiltration
  - Collection
  - Reconnaissance
relevantTechniques:
  - T1189
  - T1213
  - T1593
  - T1567.002
query: |
  // High severity - Social and Public Exposure - Confidential Files Information Exposure
  let timeFrame = 5m;
  CyfirmaSPEConfidentialFilesAlerts_CL
  | where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())
  | extend
      Description=description,
      FirstSeen=first_seen,
      LastSeen=last_seen,
      RiskScore=risk_score,
      AlertUID=alert_uid,
      UID=uid,
      AssetType=asset_type,
      AssetValue=signature,
      Impact=impact,
      Recommendation=recommendation,
      ProviderName='CYFIRMA',
      ProductName='DeCYFIR/DeTCT',
      AlertTitle=Alert_title
  | project
      TimeGenerated,
      Description,
      RiskScore,
      FirstSeen,
      LastSeen,
      AlertUID,
      UID,
      AssetType,
      AssetValue,
      Impact,
      Recommendation,
      ProductName,
      ProviderName,
      AlertTitle
customDetails:
  AssetType: AssetType
  AssetValue: AssetValue
  RiskScore: RiskScore
  Impact: Impact
  Description: Description
  Recommendation: Recommendation
  FirstSeen: FirstSeen
  LastSeen: LastSeen
  AlertUID: AlertUID
  UID: UID
  TimeGenerated: TimeGenerated
alertDetailsOverride:
  alertDisplayNameFormat: "Cyfirma - High Severity Alert: Confidential Files Information Exposure on Public Platforms - {{AlertTitle}}  "
  alertDescriptionFormat: "{{Description}} "
  alertDynamicProperties:
    - alertProperty: ProductName
      value: ProductName
    - alertProperty: ProviderName
      value: ProviderName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult

Stages and Predicates

Parameters

let timeFrame = 5m;

Stage 1: source

CyfirmaSPEConfidentialFilesAlerts_CL

Stage 2: where

| where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())

Stage 3: extend

| extend
    Description=description,
    FirstSeen=first_seen,
    LastSeen=last_seen,
    RiskScore=risk_score,
    AlertUID=alert_uid,
    UID=uid,
    AssetType=asset_type,
    AssetValue=signature,
    Impact=impact,
    Recommendation=recommendation,
    ProviderName='CYFIRMA',
    ProductName='DeCYFIR/DeTCT',
    AlertTitle=Alert_title

Stage 4: project

| project
    TimeGenerated,
    Description,
    RiskScore,
    FirstSeen,
    LastSeen,
    AlertUID,
    UID,
    AssetType,
    AssetValue,
    Impact,
    Recommendation,
    ProductName,
    ProviderName,
    AlertTitle

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertTitleproject
AlertUIDproject
AssetTypeproject
AssetValueproject
Descriptionproject
FirstSeenproject
Impactproject
LastSeenproject
ProductNameproject
ProviderNameproject
Recommendationproject
RiskScoreproject
TimeGeneratedproject
UIDproject

Dataverse - Mass record updates

#
Status
available
Severity
medium
Time window
14d
Group by
ClientIp, EntityName, EventCount, InstanceUrl, Message, UserId
Source
github.com/Azure/Azure-Sentinel

This query detects mass record update changes in Dataverse and Dynamics 365, exceeding a pre-defined threshold.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: df577f0f-1d8a-4420-9057-a07f0edb15c8
kind: Scheduled
name: Dataverse - Mass record updates
description: This query detects mass record update changes in Dataverse and Dynamics
  365, exceeding a pre-defined threshold.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Dataverse
    dataTypes:
      - DataverseActivity
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1641
  - T1485
  - T1565
query: |
  // Set threshold for number of updated records
  let detection_threshold = 10000;
  let query_frequency = 1h;
  DataverseActivity
  | where TimeGenerated >= ago(query_frequency)
  | where Message == "Update"
  | summarize EventCount = count() by InstanceUrl, UserId, ClientIp, Message
  | where EventCount > detection_threshold
  | join kind=inner(
      DataverseActivity
      | where TimeGenerated >= ago(query_frequency))
      on InstanceUrl, UserId, ClientIp, Message
  | mv-expand Fields
  | summarize
      UpdatedFields = make_set(Fields.Name, 100),
      FirstEvent = min(TimeGenerated)
      by UserId, ClientIp, InstanceUrl, EventCount, EntityName
  | extend Details = bag_pack("Entity", EntityName, "Count", EventCount, "FieldsUpdated", UpdatedFields)
  | summarize
      TotalEvents = sum(EventCount),
      FirstEvent = min(FirstEvent),
      Details = make_list(Details, 100)
      by UserId, ClientIp, InstanceUrl
  | extend
      CloudAppId = int(32780),
      AccountName = tostring(split(UserId, '@')[0]),
      UPNSuffix = tostring(split(UserId, '@')[1])
  | project
      FirstEvent,
      UserId,
      ClientIp,
      TotalEvents,
      Details,
      InstanceUrl,
      CloudAppId,
      AccountName,
      UPNSuffix
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
      - identifier: InstanceName
        columnName: InstanceUrl
alertDetailsOverride:
  alertDisplayNameFormat: 'Dataverse - Mass record changes detected in {{{InstanceUrl}} '
  alertDescriptionFormat: A total of {{TotalEvents}} records were updated by {{UserId}}
    , breaching the mass update threshold in {{InstanceUrl}} .
customDetails:
  Details: Details
version: 3.2.0

Stages and Predicates

Parameters

let detection_threshold = 10000;
let query_frequency = 1h;

Stage 1: source

DataverseActivity

Stage 2: where

| where TimeGenerated >= ago(query_frequency)

Stage 3: where

| where Message == "Update"

Stage 4: summarize

| summarize EventCount = count() by InstanceUrl, UserId, ClientIp, Message

Stage 5: where

| where EventCount > detection_threshold

Stage 6: join

| join kind=inner(
    DataverseActivity
    | where TimeGenerated >= ago(query_frequency))
    on InstanceUrl, UserId, ClientIp, Message

Stage 7: mv-expand

mv-expand Fields

Stage 8: summarize

summarize FirstEvent, UpdatedFields by UserId, ClientIp, InstanceUrl, EventCount, EntityName

Stage 9: extend

extend Details

Stage 10: summarize

summarize Details, FirstEvent, TotalEvents by UserId, ClientIp, InstanceUrl

Stage 11: extend

extend AccountName, CloudAppId, UPNSuffix

Stage 12: project

project AccountName, ClientIp, CloudAppId, Details, FirstEvent, InstanceUrl, TotalEvents, UPNSuffix, UserId

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventCountgt
  • 10000 transforms: number
field:"EventCount" kind:gt value:"10000"
Messageeq
  • Update corpus 5 (kusto 5)
field:"Message" kind:eq value:"Update"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
ClientIpproject
CloudAppIdproject
Detailsproject
FirstEventproject
InstanceUrlproject
TotalEventsproject
UPNSuffixproject
UserIdproject

Dataverse - New non-interactive identity granted access

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

Identifies API level access grants, either via the delegated permissions of a Microsoft Entra application or direct assignment within Dataverse as an application user.

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 682e230c-e5da-4085-8666-701d1f1be7de
kind: Scheduled
name: Dataverse - New non-interactive identity granted access
description: Identifies API level access grants, either via the delegated permissions
  of a Microsoft Entra application or direct assignment within Dataverse as an application
  user.
severity: Informational
status: Available
requiredDataConnectors:
  - connectorId: Dataverse
    dataTypes:
      - DataverseActivity
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - LateralMovement
  - PrivilegeEscalation
relevantTechniques:
  - T1098
  - T0859
  - T1078
query: |
  let dataverse_app_id = "00000007-0000-0000-c000-000000000000";
  let query_frequency = 1h;
  let azure_ad_changes = AuditLogs
      | where TimeGenerated >= ago(query_frequency)
      | where OperationName =~ 'Update application'
      | where TargetResources has dataverse_app_id
      | extend TargetAppName = tostring(TargetResources[0].displayName)
      | extend TargetAppId = tostring(TargetResources[0].id)
      | extend UserId = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
      | extend ClientIp = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)
      | extend NewData = tostring(parse_json(tostring(parse_json(TargetResources)[0].modifiedProperties))[0].newValue)
      | where NewData has dataverse_app_id;
  let dataverse_changes = DataverseActivity
      | where TimeGenerated >= ago(query_frequency)
      | where (Message == "Create" and EntityName == "systemuser" and parse_json(Fields)[0].Name == "applicationid")
      | extend TargetAppId = tostring(Fields[0].Value);
  union azure_ad_changes, dataverse_changes
  | extend
      CloudAppId = int(32780),
      AccountName = tostring(split(UserId, '@')[0]),
      UPNSuffix = tostring(split(UserId, '@')[1])
  | project
      TimeGenerated,
      UserId,
      ClientIp,
      TargetAppName,
      TargetAppId,
      InstanceUrl,
      CloudAppId,
      AccountName,
      UPNSuffix
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
      - identifier: InstanceName
        columnName: InstanceUrl
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: TargetAppId
alertDetailsOverride:
  alertDisplayNameFormat: Dataverse - new non-interactive access granted
  alertDescriptionFormat: '{{UserId}} granted access to an Azure AD app {{{TargetAppName}}.
    Check to validate this access was authorized.'
version: 3.2.0

Stages and Predicates

Parameters

let dataverse_app_id = "00000007-0000-0000-c000-000000000000";
let query_frequency = 1h;

Union (2 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: azure_ad_changes, dataverse_changes

Leg 1: azure_ad_changes

Leg 2: dataverse_changes

Applied to the Combined Result

| extend
    CloudAppId = int(32780),
    AccountName = tostring(split(UserId, '@')[0]),
    UPNSuffix = tostring(split(UserId, '@')[1]) | project
    TimeGenerated,
    UserId,
    ClientIp,
    TargetAppName,
    TargetAppId,
    InstanceUrl,
    CloudAppId,
    AccountName,
    UPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EntityNameeq
  • systemuser
field:"EntityName" kind:eq value:"systemuser"
Fields[0].Nameeq
  • applicationid transforms: kusto_accessor_expr:{"expression":"parse_json(Fields)[0].Name","field":"Fields[0].Name"}, kusto_dynamic_path
field:"Fields[0].Name" kind:eq value:"applicationid"
Messageeq
  • Create
field:"Message" kind:eq value:"Create"
NewDatamatch
  • 00000007-0000-0000-c000-000000000000 transforms: term
field:"NewData" kind:match value:"00000007-0000-0000-c000-000000000000"
OperationNameeq
  • Update application corpus 3 (kusto 3)
field:"OperationName" kind:eq value:"Update application"
TargetResourcesmatch
  • 00000007-0000-0000-c000-000000000000 transforms: term
field:"azure_ad::target_resources" kind:match value:"00000007-0000-0000-c000-000000000000"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
ClientIpproject
CloudAppIdproject
InstanceUrlproject
TargetAppIdproject
TargetAppNameproject
TimeGeneratedproject
UPNSuffixproject
UserIdproject

Deleted a Tenant

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

'Deleted a Tenant from SOC Prime platform'

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Rule Body

id: 7d4a2b91-8e5c-4f2a-9d6b-3c1f0e4a5d8b
name: Deleted a Tenant
description: |
  'Deleted a Tenant from SOC Prime platform'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SOCPrimeAuditLogsDataConnector
    dataTypes:
      - SOCPrimeAuditLogs_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562.001
query: |
  SOCPrimeAuditLogs_CL
  | where EventName == "Deleted a Tenant"
  | project TimeGenerated, EventName, UserEmail, UserName, SourceIp, Uri, Type
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: UserName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIp
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

SOCPrimeAuditLogs_CL

Stage 2: where

| where EventName == "Deleted a Tenant"

Stage 3: project

| project TimeGenerated, EventName, UserEmail, UserName, SourceIp, Uri, Type

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
EventNameproject
SourceIpproject
TimeGeneratedproject
Typeproject
Uriproject
UserEmailproject
UserNameproject

Digital Guardian - Incident with not blocked action

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

'Detects when incident has not block action.'

MITRE ATT&CK Coverage

Rule Body

id: 07bca129-e7d6-4421-b489-32abade0b6a7
name: Digital Guardian - Incident with not blocked action
description: |
  'Detects when incident has not block action.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1048
query: |
  DigitalGuardianDLPEvent
  | where isnotempty(IncidentStatus)
  | extend inc_act = split(IncidentStatus, ',')
  | where inc_act has 'New'
  | where inc_act !contains 'Block'
  | extend AccountCustomEntity = SrcUserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

DigitalGuardianDLPEvent

Stage 2: where

| where isnotempty(IncidentStatus)

Stage 3: extend

| extend inc_act = split(IncidentStatus, ',')

Stage 4: where

| where inc_act has 'New'

Stage 5: where

| where inc_act !contains 'Block'

Stage 6: extend

| extend AccountCustomEntity = SrcUserName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
IncidentStatusis_not_null
  • (no value, null check)
field:"IncidentStatus" kind:is_not_null
inc_actmatch
  • New transforms: term
field:"inc_act" kind:match value:"New"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
inc_actcontainsBlockexcludes:inc_act field:"inc_act" value:"Block"

Output Fields

These fields are emitted when the rule matches.

FieldSource
inc_actextend
AccountCustomEntityextend

F&O - Bank account change following network alias reassignment

#
Status
available
Severity
low
Time window
1d
Group by
$left.UserId, $right.Username
Source
github.com/Azure/Azure-Sentinel

Identifies changes to user accounts where the network alias was modified to a new value. Shortly afterwards, the updated alias is used to update a bank account number.

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation
Credential Access
Persistence
Lateral Movement

Rule Body

id: dccbdb5b-2ce7-4931-bfbe-f1ad6523ee64
kind: Scheduled
name: F&O - Bank account change following network alias reassignment
description: Identifies changes to user accounts where the network alias was modified
  to a new value. Shortly afterwards, the updated alias is used to update a bank account
  number.
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: Dynamics365Finance
    dataTypes:
      - FinanceOperationsActivity_CL
queryFrequency: 15m
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
  - LateralMovement
  - PrivilegeEscalation
relevantTechniques:
  - T1556
  - T0859
  - T1078
query: |
  let query_frequency = 15m;
  FinanceOperationsActivity_CL
  | where LogType == "Update" and TableName == "UserInfo"
  | extend UserId = tostring(parse_json(tostring(FormattedData.["03::id"])).NewData)
  | extend NetworkAlias = parse_json(tostring(FormattedData.networkAlias))
  | extend
      CurrentAlias = tostring(NetworkAlias.NewData),
      PreviousAlias = tostring(NetworkAlias.OldData)
  | where CurrentAlias != PreviousAlias
  | extend
      AliasUpdated = LogCreatedDateTime,
      AliasChangedBy = Username
  | join kind=inner(FinanceOperationsActivity_CL
      | where TimeGenerated >= ago (query_frequency)
      | where LogType == "Update" and TableName == "BankAccountTable"
      | extend AccountId = tostring(parse_json(tostring(FormattedData.AccountID)).NewData)
      | extend AccountNum = parse_json(tostring(FormattedData.AccountNum))
      | extend
          CurrentAccountNum = tostring(AccountNum.NewData),
          OldAccountNum = tostring(AccountNum.OldData)
      | where CurrentAccountNum != OldAccountNum
      | extend BankUpdated = LogCreatedDateTime)
      on $left.UserId == $right.Username
  | where BankUpdated > AliasUpdated
  | extend
      FinOpsAppId = 32780,
      AccountName = tostring(split(CurrentAlias, "@")[0]),
      UPNSuffix = tostring(split(CurrentAlias, "@")[1])
  | project
      AliasUpdated,
      AliasChangedBy,
      Username,
      AccountId,
      CurrentAccountNum,
      OldAccountNum,
      CurrentAlias,
      PreviousAlias,
      FinOpsAppId,
      AccountName,
      UPNSuffix
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AliasChangedBy
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Username
alertDetailsOverride:
  alertDisplayNameFormat: F&O - Suspicious bank account changes
  alertDescriptionFormat: A user account alias was reassigned for {{Username}} by
    {{AliasChangedBy}} and shortly afterwards, bank account {{AccountId}} was modified.
version: 3.2.0

Stages and Predicates

Parameters

let query_frequency = 15m;

Stage 1: source

FinanceOperationsActivity_CL

Stage 2: where

| where LogType == "Update" and TableName == "UserInfo"

Stage 3: extend (3 consecutive steps)

| extend UserId = tostring(parse_json(tostring(FormattedData.["03::id"])).NewData)
| extend NetworkAlias = parse_json(tostring(FormattedData.networkAlias))
| extend
    CurrentAlias = tostring(NetworkAlias.NewData),
    PreviousAlias = tostring(NetworkAlias.OldData)

Stage 4: where

| where CurrentAlias != PreviousAlias

Stage 5: extend

| extend
    AliasUpdated = LogCreatedDateTime,
    AliasChangedBy = Username

Stage 6: join

| join kind=inner(FinanceOperationsActivity_CL
    | where TimeGenerated >= ago (query_frequency)
    | where LogType == "Update" and TableName == "BankAccountTable"
    | extend AccountId = tostring(parse_json(tostring(FormattedData.AccountID)).NewData)
    | extend AccountNum = parse_json(tostring(FormattedData.AccountNum))
    | extend
        CurrentAccountNum = tostring(AccountNum.NewData),
        OldAccountNum = tostring(AccountNum.OldData)
    | where CurrentAccountNum != OldAccountNum
    | extend BankUpdated = LogCreatedDateTime)
    on $left.UserId == $right.Username

Stage 7: where

where BankUpdated > AliasUpdated

Stage 8: extend

extend AccountName, FinOpsAppId, UPNSuffix

Stage 9: project

project AccountId, AccountName, AliasChangedBy, AliasUpdated, CurrentAccountNum, CurrentAlias, FinOpsAppId, OldAccountNum, PreviousAlias, UPNSuffix, Username

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
LogTypeeq
  • Update
field:"LogType" kind:eq value:"Update"
TableNameeq
  • BankAccountTable
  • UserInfo
field:"TableName" kind:eq

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountIdproject
AccountNameproject
AliasChangedByproject
AliasUpdatedproject
CurrentAccountNumproject
CurrentAliasproject
FinOpsAppIdproject
OldAccountNumproject
PreviousAliasproject
UPNSuffixproject
Usernameproject

Guardian- Sentiment Policy Violation Detection

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

'This alert creates an incident when Sentiment Policy Violation detected from the Guardian.'

Rule Body

id: 24538989-9dea-4cc7-aa78-0969ca116051
name: Guardian- Sentiment Policy Violation Detection
description: |
  'This alert creates an incident when Sentiment Policy Violation detected from the Guardian.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: BoschAIShield
    dataTypes:
      - Guardian
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: |
  Guardian
  | where PolicyViolatedControlFeature =~ 'Sentiment'
  | where Severity =~ 'Low'
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: NTDomain
        columnName: NTDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
  alertDisplayNameFormat: Guardian- Sentiment Policy Violation detection
  alertDescriptionFormat: |
    This query detects Sentiment Policy Violation detected from the Guardian generated at {{TimeGenerated}}.\n\nPlease check the source for more information and investigate further.
  alertTacticsColumnName: null
  alertSeverityColumnName: Severity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

Guardian

Stage 2: where

| where PolicyViolatedControlFeature =~ 'Sentiment'

Stage 3: where

| where Severity =~ 'Low'

Indicators

These rows show field, operator, and value matches.

NRT GravityZone Incident Alerts

#
Status
available
Severity
medium
Source
github.com/Azure/Azure-Sentinel

The query identifies incident-level events received from the GravityZone Data Connector

MITRE ATT&CK Coverage

TacticTechniques
Lateral Movement

Rule Body

id: 73c803aa-1188-45dd-8379-62a3319d3d9f
name: NRT GravityZone Incident Alerts
description: The query identifies incident-level events received from the GravityZone Data Connector
version: 1.0.0
kind: NRT
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: GravityZoneDataConnector
    dataTypes:
      - ASimAlertEventBitdefenderGravityZone
tactics:
  - LateralMovement
relevantTechniques:
  - T1210
query: |
  ASimAlertEventBitdefenderGravityZone(pack=true)
    | extend IncidentType = case(
        AdditionalFields.Module == "new-incident", "EDR Incident",
        AdditionalFields.Module == "new-extended-incident", "XDR Incident",
        AdditionalFields.Module == "ransomware-mitigation", "Ransomware Mitigation",
        AdditionalFields.Module == "network-sandboxing", "Sandbox Analyzer Detection",
        AdditionalFields.Module == "exchange-malware", "Exchange Malware Detection",
        "Incident"   // fallback value if null or unmatched
      ),
      Tactics = AdditionalFields.AttackTypes
    | project EventUid, EventSeverity, EventStartTime, IncidentType, Tactics, EventVendor, EventProduct, DvcId, DvcIpAddr, DvcHostname, DvcAction, DvcFQDN
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DvcHostname
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: DvcIpAddr
alertDetailsOverride:
  alertSeverityColumnName: EventSeverity
  alertDisplayNameFormat: 'GravityZone: {{IncidentType}}'
  alertDescriptionFormat: |
    Alert generated on {{EventStartTime}} in Bitdefender GravityZone.\n\nGravityZone Incident ID / Alert GUID: {{EventUid}}\n\nPlease check the source for more information and investigate further.
  alertTacticsColumnName: Tactics
  alertDynamicProperties:
    - alertProperty: ProductName
      value: EventProduct
    - alertProperty: ProviderName
      value: EventVendor

Stages and Predicates

Stage 1: source

ASimAlertEventBitdefenderGravityZone(pack=true)

Stage 2: extend

| extend IncidentType = case(
      AdditionalFields.Module == "new-incident", "EDR Incident",
      AdditionalFields.Module == "new-extended-incident", "XDR Incident",
      AdditionalFields.Module == "ransomware-mitigation", "Ransomware Mitigation",
      AdditionalFields.Module == "network-sandboxing", "Sandbox Analyzer Detection",
      AdditionalFields.Module == "exchange-malware", "Exchange Malware Detection",
      "Incident"
    ),
    Tactics = AdditionalFields.AttackTypes
IncidentType =
ifAdditionalFields.Module == "new-incident""EDR Incident"
elifAdditionalFields.Module == "new-extended-incident""XDR Incident"
elifAdditionalFields.Module == "ransomware-mitigation""Ransomware Mitigation"
elifAdditionalFields.Module == "network-sandboxing""Sandbox Analyzer Detection"
elifAdditionalFields.Module == "exchange-malware""Exchange Malware Detection"
else"Incident"

Stage 3: project

| project EventUid, EventSeverity, EventStartTime, IncidentType, Tactics, EventVendor, EventProduct, DvcId, DvcIpAddr, DvcHostname, DvcAction, DvcFQDN

Output Fields

These fields are emitted when the rule matches.

FieldSource
DvcActionproject
DvcFQDNproject
DvcHostnameproject
DvcIdproject
DvcIpAddrproject
EventProductproject
EventSeverityproject
EventStartTimeproject
EventUidproject
EventVendorproject
IncidentTypeproject
Tacticsproject

Pathlock TDnR - RFC Connection Changes

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

Detects changes to RFC (Remote Function Call) connection definitions in SAP (transaction SM59), forwarded by Pathlock Threat Detection and Response. Unauthorized RFC changes may introduce backdoor connections, redirect communications to malicious systems, or enable lateral movement across SAP landscapes.

MITRE ATT&CK Coverage

TacticTechniques
Persistence
Lateral Movement

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c45
name: Pathlock TDnR - RFC Connection Changes
kind: Scheduled
description: >-
  Detects changes to RFC (Remote Function Call) connection definitions in SAP (transaction SM59),
  forwarded by Pathlock Threat Detection and Response. Unauthorized RFC changes may introduce
  backdoor connections, redirect communications to malicious systems, or enable lateral movement
  across SAP landscapes.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - LateralMovement
  - Persistence
relevantTechniques:
  - T1021
  - T1098
query: |
  Pathlock_TDnR_CL
  | where DataSource == "RFC_DESTINATIONS"
  | project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
            Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
            MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Bname
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIp
eventGroupingSettings:
  aggregationKind: SingleAlert
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Stage 1: source

Pathlock_TDnR_CL

Stage 2: where

| where DataSource == "RFC_DESTINATIONS"

Stage 3: project

| project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
          Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
          MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AffectedUserproject
Areaproject
Bnameproject
CentralTsproject
DataSourceproject
DestIpproject
Eventidproject
Hostnameproject
Instanceproject
LogLineproject
MessageV1project
MessageV2project
MessageV3project
MessageV4project
MsgIdproject
MsgNoproject
MsgTypeproject
Reportproject
SrcIpproject
Subidproject
Sysidproject
Tcodeproject
TimeGeneratedproject

RDP Nesting

#
Severity
medium
Time window
8d
Group by
$left.FirstComputer, $left.SecondComputer, $right.Computer, Account, AccountType, Activity, FirstComputer, FirstComputerIP, FirstRemoteIPAddress, LogonTypeName, ProcessName, SecondComputer, SecondComputerIP, SecondHop, SecondRemoteIPAddress
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

Query detects potential lateral movement within a network by identifying when an RDP connection (EventID 4624, LogonType 10) is made to an initial system, followed by a subsequent RDP connection from that system to another, using the same account within a 60-minute window. To reduce false positives, it excludes scenarios where the same account has made 5 or more connections to the same set of computers in the previous 7 days. This approach focuses on highlighting unusual RDP behaviour that suggests lateral movement, which is often associated with attacker tactics during a network breach.

MITRE ATT&CK Coverage

TacticTechniques
Lateral Movement

Telemetry Coverage

Rule Body

id: 69a45b05-71f5-45ca-8944-2e038747fb39
name: RDP Nesting
description: |
  'Query detects potential lateral movement within a network by identifying when an RDP connection (EventID 4624, LogonType 10) is made to an initial system, followed by a subsequent RDP connection from that system to another, using the same account within a 60-minute window.
   To reduce false positives, it excludes scenarios where the same account has made 5 or more connections to the same set of computers in the previous 7 days. This approach focuses on highlighting unusual RDP behaviour that suggests lateral movement, which is often associated with attacker tactics during a network breach.'
severity: Medium
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsForwardedEvents
    dataTypes:
      - WindowsEvent
queryFrequency: 1d
queryPeriod: 8d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - LateralMovement
relevantTechniques:
  - T1021
query: |
  let endtime = 1d;
  // Function to resolve hostname to IP address using DNS logs or a lookup table (example syntax)
  let rdpConnections =
  (union isfuzzy=true
  (
  SecurityEvent
  | where TimeGenerated >= ago(endtime)
  | where EventID == 4624 and LogonType == 10
  // Labeling the first RDP connection time, computer and ip
  | extend
  FirstHop = bin(TimeGenerated, 1m),
  FirstComputer = toupper(Computer),
  FirstRemoteIPAddress = IpAddress,
  Account = tolower(Account)
  ),
  (
  WindowsEvent
  | where TimeGenerated >= ago(endtime)
  | where EventID == 4624 and EventData has ("10")
  | extend LogonType = tostring(EventData.LogonType)
  | where LogonType == 10 // Labeling the first RDP connection time, computer and ip
  | extend Account = strcat(tostring(EventData.TargetDomainName), "", tostring(EventData.TargetUserName))
  | extend IpAddress = tostring(EventData.IpAddress)
  | extend
  FirstHop = bin(TimeGenerated, 1m),
  FirstComputer = toupper(Computer),
  FirstRemoteIPAddress = IpAddress,
  Account = tolower(Account)
  ))
  | join kind=inner (
  (union isfuzzy=true
  (
  SecurityEvent
  | where TimeGenerated >= ago(endtime)
  | where EventID == 4624 and LogonType == 10
  // Labeling the second RDP connection time, computer and ip
  | extend
  SecondHop = bin(TimeGenerated, 1m),
  SecondComputer = toupper(Computer),
  SecondRemoteIPAddress = IpAddress,
  Account = tolower(Account)
  ),
  (
  WindowsEvent
  | where TimeGenerated >= ago(endtime)
  | where EventID == 4624 and EventData has ("10")
  | extend LogonType = toint(EventData.LogonType)
  | where LogonType == 10 // Labeling the second RDP connection time, computer and ip
  | extend Account = strcat(tostring(EventData.TargetDomainName), "", tostring(EventData.TargetUserName))
  | extend IpAddress = tostring(EventData.IpAddress)
  | extend
  SecondHop = bin(TimeGenerated, 1m),
  SecondComputer = toupper(Computer),
  SecondRemoteIPAddress = IpAddress,
  Account = tolower(Account)
  ))
  )
  on Account
  | distinct
  Account,
  FirstHop,
  FirstComputer,
  FirstRemoteIPAddress,
  SecondHop,
  SecondComputer,
  SecondRemoteIPAddress,
  AccountType,
  Activity,
  LogonTypeName,
  ProcessName;
  // Resolve hostnames to IP addresses device network Ip's
  let listOfFirstComputer = rdpConnections | distinct FirstComputer;
  let listOfSecondComputer = rdpConnections | distinct SecondComputer;
  let resolvedIPs =
  DeviceNetworkInfo
  | where TimeGenerated >= ago(endtime)
  | where isnotempty(ConnectedNetworks) and NetworkAdapterStatus == "Up"
  | extend ClientIP = tostring(parse_json(IPAddresses[0]).IPAddress)
  | where isnotempty(ClientIP)
  | where DeviceName in~ (listOfFirstComputer) or DeviceName in~ (listOfSecondComputer)
  | summarize arg_max(TimeGenerated, ClientIP) by Computer= DeviceName
  | project Computer=toupper(Computer), ResolvedIP = ClientIP;
  // Join resolved IPs with the RDP connections
  rdpConnections
  | join kind=inner (resolvedIPs) on $left.FirstComputer == $right.Computer
  | join kind=inner (resolvedIPs) on $left.SecondComputer == $right.Computer
  // | where ResolvedIP != ResolvedIP1
  | distinct
  Account,
  FirstHop,
  FirstComputer,
  FirstComputerIP = ResolvedIP,
  FirstRemoteIPAddress,
  SecondHop,
  SecondComputer,
  SecondComputerIP = ResolvedIP1,
  SecondRemoteIPAddress,
  AccountType,
  Activity,
  LogonTypeName,
  ProcessName
  // Ensure the first connection is before the second connection
  // Identify only RDP to another computer from within the first RDP connection by only choosing matches where the Computer names do not match
  // Ensure the IPAddresses do not match by excluding connections from the same computers with first hop RDP connections to multiple computers
  | where FirstComputer != SecondComputer
  and FirstRemoteIPAddress != SecondRemoteIPAddress
  and SecondHop > FirstHop
  // Ensure the second hop occurs within 30 minutes of the first hop
  | where SecondHop <= FirstHop + 30m
  | where SecondRemoteIPAddress == FirstComputerIP
  | summarize FirstHopFirstSeen = min(FirstHop), FirstHopLastSeen = max(FirstHop)
  by
  Account,
  FirstComputer,
  FirstComputerIP,
  FirstRemoteIPAddress,
  SecondHop,
  SecondComputer,
  SecondComputerIP,
  SecondRemoteIPAddress,
  AccountType,
  Activity,
  LogonTypeName,
  ProcessName
  | extend
  AccountName = tostring(split(Account, @"")[1]),
  AccountNTDomain = tostring(split(Account, @"")[0])
  | extend
  HostName1 = tostring(split(FirstComputer, ".")[0]),
  DomainIndex = toint(indexof(FirstComputer, '.'))
  | extend HostNameDomain1 = iff(DomainIndex != -1, substring(FirstComputer, DomainIndex + 1), FirstComputer)
  | extend
  HostName2 = tostring(split(SecondComputer, ".")[0]),
  DomainIndex = toint(indexof(SecondComputer, '.'))
  | extend HostNameDomain2 = iff(DomainIndex != -1, substring(SecondComputer, DomainIndex + 1), SecondComputer)
  | project-away DomainIndex
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
      - identifier: Name
        columnName: AccountName
      - identifier: NTDomain
        columnName: AccountNTDomain
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: FirstComputer
      - identifier: HostName
        columnName: HostName1
      - identifier: NTDomain
        columnName: HostNameDomain1
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: SecondComputer
      - identifier: HostName
        columnName: HostName2
      - identifier: NTDomain
        columnName: HostNameDomain2
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: FirstIPAddress
version: 1.2.8
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Threat Protection" ]

Stages and Predicates

Parameters

let endtime = 1d;

Let Binding: listOfFirstComputer

let listOfFirstComputer = rdpConnections | distinct FirstComputer;

Let Binding: listOfSecondComputer

let listOfSecondComputer = rdpConnections | distinct SecondComputer;

Let Binding: resolvedIPs

let resolvedIPs = DeviceNetworkInfo
| where TimeGenerated >= ago(endtime)
| where isnotempty(ConnectedNetworks) and NetworkAdapterStatus == "Up"
| extend ClientIP = tostring(parse_json(IPAddresses[0]).IPAddress)
| where isnotempty(ClientIP)
| where DeviceName in~ (listOfFirstComputer) or DeviceName in~ (listOfSecondComputer)
| summarize arg_max(TimeGenerated, ClientIP) by Computer= DeviceName
| project Computer=toupper(Computer), ResolvedIP = ClientIP;

Union isfuzzy=true (2 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: SecurityEvent, WindowsEvent

Leg 1: SecurityEvent

SecurityEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and LogonType == 10
| extend
FirstHop = bin(TimeGenerated, 1m),
FirstComputer = toupper(Computer),
FirstRemoteIPAddress = IpAddress,
Account = tolower(Account)

Leg 2: WindowsEvent

WindowsEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and EventData has ("10")
| extend LogonType = tostring(EventData.LogonType)
| where LogonType == 10
| extend Account = strcat(tostring(EventData.TargetDomainName), "", tostring(EventData.TargetUserName))
| extend IpAddress = tostring(EventData.IpAddress)
| extend
FirstHop = bin(TimeGenerated, 1m),
FirstComputer = toupper(Computer),
FirstRemoteIPAddress = IpAddress,
Account = tolower(Account)

Applied to the Combined Result

| join kind=inner (
(union isfuzzy=true
(
SecurityEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and LogonType == 10
| extend
SecondHop = bin(TimeGenerated, 1m),
SecondComputer = toupper(Computer),
SecondRemoteIPAddress = IpAddress,
Account = tolower(Account)
),
(
WindowsEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and EventData has ("10")
| extend LogonType = toint(EventData.LogonType)
| where LogonType == 10
| extend Account = strcat(tostring(EventData.TargetDomainName), "", tostring(EventData.TargetUserName))
| extend IpAddress = tostring(EventData.IpAddress)
| extend
SecondHop = bin(TimeGenerated, 1m),
SecondComputer = toupper(Computer),
SecondRemoteIPAddress = IpAddress,
Account = tolower(Account)
))
)
on Account
| distinct
Account,
FirstHop,
FirstComputer,
FirstRemoteIPAddress,
SecondHop,
SecondComputer,
SecondRemoteIPAddress,
AccountType,
Activity,
LogonTypeName,
ProcessName | join kind=inner (resolvedIPs) on $left.FirstComputer == $right.Computer | join kind=inner (resolvedIPs) on $left.SecondComputer == $right.Computer | distinct
Account,
FirstHop,
FirstComputer,
FirstComputerIP = ResolvedIP,
FirstRemoteIPAddress,
SecondHop,
SecondComputer,
SecondComputerIP = ResolvedIP1,
SecondRemoteIPAddress,
AccountType,
Activity,
LogonTypeName,
ProcessName | where FirstComputer != SecondComputer
and FirstRemoteIPAddress != SecondRemoteIPAddress
and SecondHop > FirstHop | where SecondHop <= FirstHop + 30m | where SecondRemoteIPAddress == FirstComputerIP | summarize FirstHopFirstSeen = min(FirstHop), FirstHopLastSeen = max(FirstHop)
by
Account,
FirstComputer,
FirstComputerIP,
FirstRemoteIPAddress,
SecondHop,
SecondComputer,
SecondComputerIP,
SecondRemoteIPAddress,
AccountType,
Activity,
LogonTypeName,
ProcessName | extend
AccountName = tostring(split(Account, @"")[1]),
AccountNTDomain = tostring(split(Account, @"")[0]) | extend
HostName1 = tostring(split(FirstComputer, ".")[0]),
DomainIndex = toint(indexof(FirstComputer, '.')) | extend HostNameDomain1 = iff(DomainIndex != -1, substring(FirstComputer, DomainIndex + 1), FirstComputer) | extend
HostName2 = tostring(split(SecondComputer, ".")[0]),
DomainIndex = toint(indexof(SecondComputer, '.')) | extend HostNameDomain2 = iff(DomainIndex != -1, substring(SecondComputer, DomainIndex + 1), SecondComputer) | project-away DomainIndex

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountsummarize
AccountTypesummarize
Activitysummarize
FirstComputersummarize
FirstComputerIPsummarize
FirstHopFirstSeensummarize
FirstHopLastSeensummarize
FirstRemoteIPAddresssummarize
LogonTypeNamesummarize
ProcessNamesummarize
SecondComputersummarize
SecondComputerIPsummarize
SecondHopsummarize
SecondRemoteIPAddresssummarize
AccountNTDomainextend
AccountNameextend
HostName1extend
HostNameDomain1extend
HostName2extend
HostNameDomain2extend

RITA Beacon Analyzer for Windows Firewall Events

#
Group by
Application, Computer, DestinationIP, DestinationPort, Protocol
Author
Cyb3rMonk
Source
github.com/Cyb3r-Monk/Threat-Hunting-and-Detection

Below queries analyze Windows Firewall logs and applies RITA beacon analyzer algorithm for C2 beaconing detection.

MITRE ATT&CK Coverage

TacticTechniques
Command & ControlNo specific technique

Telemetry Coverage

Rule Body

// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
// Link to original post:
// https://posts.bluraven.io/implementing-rita-using-kql-8ccb0ee8eeae
//
// Read the blog to understand how this query works and how to analyze the results.
// This query may not be able to detect beacons that have large sleep values like 6h-1d.  
//
// Query parameters:
let starttime = 1d;
let endtime = 1m;
let TotalEventsThresholdMin = 24; // A beacon should have at least 24 connections (1h sleep)
let DurationThreshold_minutes = 180; // only show beacons that had 180 minutes of duration
let ScoreThreshold = 0.85; // show beacons that have a score greater than 0.85 (max score is 1)
let MaxJitterInseconds = 30.0; // covers beacons that have max 30 seconds jitter(consider increasing this for better coverage)
// Define how many devices can have the same beacon. 
let CompromisedDeviceCountMax = 5; // number of devices that can be compromised at the same time and have the same beacon. Increasing the value generates more results.
let AllBeacons = materialize (
    SecurityEvent
    | where TimeGenerated between (ago(starttime)..ago(endtime))
    | where EventID == 5156
    | where EventData has_cs '<Data Name="Protocol">6</Data>' // TCP connections only
    | where EventData has_cs '<Data Name="Direction">%%14593</Data>' // Outbound direction
    | where EventData !has_cs '<Data Name="DestAddress">127.0.0.1</Data>'
    | where EventData !has_cs '<Data Name="DestAddress">169.254.'
    | parse EventData with * 'DestAddress">' DestinationIP '<' *
    | where not(ipv4_is_private(DestinationIP)) // analyze only outbound connections to the internet
    | parse EventData with * '"ProcessID">' ProcessId '<' * 'Application">' Application '<' * 'Direction">' Direction '<' * 'SourceAddress">' SourceIP '<' * 'SourcePort">' SourcePort '<' 
        * 'DestPort">' DestinationPort '<' * 'Protocol">' Protocol '<' * 'LayerName">' LayerName '<' * 'RemoteUserID">' RemoteUserID '<' * 'RemoteMachineID">' RemoteMachineID '<' *
    // Prepare data set for analysis.
    | summarize hint.strategy=shuffle start=min(TimeGenerated), end=max(TimeGenerated), make_list(TimeGenerated) 
        by Computer, Application, Protocol, DestinationIP, DestinationPort
    | where array_length(list_TimeGenerated) >= TotalEventsThresholdMin
    | extend
        duration_minutes=datetime_diff("minute", end, start),
        duration_seconds=datetime_diff("second", end, start)
    | where duration_minutes >= DurationThreshold_minutes
    // Keep data set as small as possibble, remove unnecessary columns.
    | project
        duration_minutes,
        duration_seconds,
        ConnRate = toreal(array_length(list_TimeGenerated)) / toreal(duration_minutes),
        Computer,
        Application,
        DestinationIP,
        DestinationPort,
        Protocol,
        TimeGenerated = array_sort_asc(list_TimeGenerated),
        ConnectionCount = array_length(list_TimeGenerated)
    // Start analysis: 
    | mv-apply TimeGenerated to typeof(datetime) on 
        (     
        extend
            nextTimeGenerated = next(TimeGenerated, 1),
            nextComputer = next(Computer, 1),
            nextApplication = next(Application, 1),
            nextProtocol = next(Protocol, 1),
            nextDestinationIP = next(DestinationIP, 1),
            nextDestinationPort = next(DestinationPort, 1)
        | extend TimeDeltaInSeconds = datetime_diff('second', nextTimeGenerated, TimeGenerated)
        | where Computer == nextComputer
            and nextProtocol == Protocol
            and nextDestinationIP == DestinationIP
            and nextDestinationPort == DestinationPort
            and nextApplication == Application
        | project
            TimeGenerated,
            TimeDeltaInSeconds,
            Computer,
            Application,
            Protocol,
            DestinationIP,
            DestinationPort,
            duration_minutes,
            duration_seconds
        // Calculate percentiles.
        | summarize hint.strategy=shuffle count(), min(TimeGenerated), max(TimeGenerated), 
            percentiles(TimeDeltaInSeconds, 10, 25, 50, 75, 90),
            TimeDeltaList=make_list(TimeDeltaInSeconds)
            by
            Computer,
            Application,
            Protocol,
            DestinationIP,
            DestinationPort,
            duration_minutes,
            duration_seconds
        | extend
            tsLow = (percentile_TimeDeltaInSeconds_10),
            tsMid = (percentile_TimeDeltaInSeconds_50),
            tsHigh = (percentile_TimeDeltaInSeconds_90)
        // calculate Bowley variables
        | extend tsBowleyNum = tsLow + tsHigh - 2 * tsMid, tsBowleyDen = tsHigh - tsLow
        // calculate Bowley's skewness
        | extend tsSkew = iif(tsBowleyDen != 0 and tsMid != tsLow and tsMid != tsHigh, toreal(tsBowleyNum) / toreal(tsBowleyDen), 0.0)
        // calculate skewness scores
        | extend tsSkewScore  = 1.0 - toreal(abs(tsSkew))
        // start of MADM calculation 
        | mv-expand TimeDeltaList
        | extend temp_tsdelta = abs(TimeDeltaList - tsMid)
        // calculate MADM of timedelta and sentbytes
        | summarize hint.strategy=shuffle tsMadm = percentiles(temp_tsdelta, 50), TimeDeltaList = make_list(TimeDeltaList)
            by
            tsLow,
            tsMid,
            tsHigh,
            Computer,
            Application,
            Protocol,
            DestinationIP,
            DestinationPort,
            count_,
            min_TimeGenerated,
            max_TimeGenerated,
            tsBowleyNum,
            tsBowleyDen,
            tsSkewScore,
            duration_minutes,
            duration_seconds
        // calculate MADM, smallness(sentbytes) and connection count score
        | extend tsMadmScore  = iif((1.0 - toreal(tsMadm) / MaxJitterInseconds) < 0, 0.0, 1.0 - toreal(tsMadm) / MaxJitterInseconds)
        | extend tsConnCountScore = iif(toreal(array_length(TimeDeltaList)) / (toreal(duration_seconds) / 90.0) > 1.0, 1.0, toreal(array_length(TimeDeltaList)) / (toreal(duration_seconds) / 90.0))
        // calculate sum of the scores(timedelta and sentbytes)
        | extend tsSum = tsSkewScore + tsMadmScore + tsConnCountScore
        // calculate timedelta, sentbytes and the overall score.
        | extend tsScore = ceiling((tsSum / 3.0) * 1000) / 1000
        // filter results based on the score threshold
        | where tsScore >= ScoreThreshold
        // calculate jitter. not used for filtering for now. 
        | extend tsJitter=iif(tsMid > 0, toreal(tsMadm) / toreal(tsMid) * 100, 0.0)
        )
    )
;
let PotentialBeacons = materialize 
    (
    AllBeacons
    | summarize hint.strategy=shuffle dcount(Computer) by Protocol, DestinationIP, DestinationPort
    | where dcount_Computer <= CompromisedDeviceCountMax
    | join kind=inner AllBeacons on Protocol, DestinationIP, DestinationPort
    | project-away *1
    )
;
PotentialBeacons
| project-away TimeDeltaList
| sort by tsScore desc, ConnectionCount desc
| project-reorder tsScore, ConnectionCount, dcount_Computer, Protocol, Computer, Application

// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
// Link to original post:
// https://posts.bluraven.io/implementing-rita-using-kql-8ccb0ee8eeae
//
// Read the blog to understand how this query works and how to analyze the results.
// This query may not be able to detect beacons that have large sleep values like 6h-1d.  
//
// Query parameters:
let starttime = 1d;
let endtime = 1m;
let TotalEventsThresholdMin = 24; // A beacon should have at least 24 connections (1h sleep)
let DurationThreshold_minutes = 180; // only show beacons that had 180 minutes of duration
let ScoreThreshold = 0.85; // show beacons that have a score greater than 0.85 (max score is 1)
let MaxJitterInseconds = 30.0; // covers beacons that have max 30 seconds jitter(consider increasing this for better coverage)
// Define how many devices can have the same beacon. 
let CompromisedDeviceCountMax = 5; // number of devices that can be compromised at the same time and have the same beacon. Increasing the value generates more results.
let AllBeacons = materialize (
    WindowsFirewall
    | where TimeGenerated between (ago(starttime)..ago(endtime))
    | where CommunicationDirection == "SEND"
    | where FirewallAction == "ALLOW"
    | where Protocol == "TCP"
    | where DestinationIP <> '127.0.0.1'
    | where not(ipv4_is_private(DestinationIP))
    // Prepare data set for analysis.
    | summarize hint.strategy=shuffle start=min(TimeGenerated), end=max(TimeGenerated), make_list(TimeGenerated) 
        by Computer, DestinationIP, Protocol, DestinationPort
    | where array_length(list_TimeGenerated) >= TotalEventsThresholdMin
    | extend
        duration_minutes=datetime_diff("minute", end, start),
        duration_seconds=datetime_diff("second", end, start)
    | where duration_minutes >= DurationThreshold_minutes
    // Keep data set as small as possibble, remove unnecessary columns.
    | project
        duration_minutes,
        duration_seconds,
        ConnRate = toreal(array_length(list_TimeGenerated)) / toreal(duration_minutes),
        Computer,
        DestinationIP,
        DestinationPort,
        Protocol,
        TimeGenerated = array_sort_asc(list_TimeGenerated),
        ConnectionCount = array_length(list_TimeGenerated)
    // Start analysis: 
    | mv-apply TimeGenerated to typeof(datetime) on 
        (     
        extend
            nextTimeGenerated = next(TimeGenerated, 1),
            nextComputer = next(Computer, 1),
            nextProtocol = next(Protocol, 1),
            nextDestinationIP = next(DestinationIP, 1),
            nextDestinationPort = next(DestinationPort, 1)
        | extend TimeDeltaInSeconds = datetime_diff('second', nextTimeGenerated, TimeGenerated)
        | where Computer == nextComputer
            and nextProtocol == Protocol
            and nextDestinationIP == DestinationIP
            and nextDestinationPort == DestinationPort
        | project
            TimeGenerated,
            TimeDeltaInSeconds,
            Computer,
            Protocol,
            DestinationIP,
            DestinationPort,
            duration_minutes,
            duration_seconds
        // Calculate percentiles.
        | summarize hint.strategy=shuffle count(), min(TimeGenerated), max(TimeGenerated), 
            percentiles(TimeDeltaInSeconds, 10, 25, 50, 75, 90),
            TimeDeltaList=make_list(TimeDeltaInSeconds)
            by
            Computer,
            Protocol,
            DestinationIP,
            DestinationPort,
            duration_minutes,
            duration_seconds
        | extend
            tsLow = (percentile_TimeDeltaInSeconds_10),
            tsMid = (percentile_TimeDeltaInSeconds_50),
            tsHigh = (percentile_TimeDeltaInSeconds_90)
        // calculate Bowley variables
        | extend tsBowleyNum = tsLow + tsHigh - 2 * tsMid, tsBowleyDen = tsHigh - tsLow
        // calculate Bowley's skewness
        | extend tsSkew = iif(tsBowleyDen != 0 and tsMid != tsLow and tsMid != tsHigh, toreal(tsBowleyNum) / toreal(tsBowleyDen), 0.0)
        // calculate skewness scores
        | extend tsSkewScore  = 1.0 - toreal(abs(tsSkew))
        // start of MADM calculation 
        | mv-expand TimeDeltaList
        | extend temp_tsdelta = abs(TimeDeltaList - tsMid)
        // calculate MADM of timedelta and sentbytes
        | summarize hint.strategy=shuffle tsMadm = percentiles(temp_tsdelta, 50), TimeDeltaList = make_list(TimeDeltaList)
            by
            tsLow,
            tsMid,
            tsHigh,
            Computer,
            Protocol,
            DestinationIP,
            DestinationPort,
            count_,
            min_TimeGenerated,
            max_TimeGenerated,
            tsBowleyNum,
            tsBowleyDen,
            tsSkewScore,
            duration_minutes,
            duration_seconds
        // calculate MADM, smallness(sentbytes) and connection count score
        | extend tsMadmScore  = iif((1.0 - toreal(tsMadm) / MaxJitterInseconds) < 0, 0.0, 1.0 - toreal(tsMadm) / MaxJitterInseconds)
        | extend tsConnCountScore = iif(toreal(array_length(TimeDeltaList)) / (toreal(duration_seconds) / 90.0) > 1.0, 1.0, toreal(array_length(TimeDeltaList)) / (toreal(duration_seconds) / 90.0))
        // calculate sum of the scores(timedelta and sentbytes)
        | extend tsSum = tsSkewScore + tsMadmScore + tsConnCountScore
        // calculate timedelta, sentbytes and the overall score.
        | extend tsScore = ceiling((tsSum / 3.0) * 1000) / 1000
        // filter results based on the score threshold
        | where tsScore >= ScoreThreshold
        // calculate jitter. not used for filtering for now. 
        | extend tsJitter=iif(tsMid > 0, toreal(tsMadm) / toreal(tsMid) * 100, 0.0)
        )
    )
;
let PotentialBeacons = materialize 
    (
    AllBeacons
    | summarize hint.strategy=shuffle dcount(Computer) by Protocol, DestinationIP, DestinationPort
    | where dcount_Computer <= CompromisedDeviceCountMax
    | join kind=inner AllBeacons on Protocol, DestinationIP, DestinationPort
    | project-away *1
    )
;
PotentialBeacons
| project-away TimeDeltaList
| sort by tsScore desc, ConnectionCount desc
| project-reorder tsScore, ConnectionCount, dcount_Computer, Protocol, Computer

Stages and Predicates

Parameters

let starttime = 1d;
let endtime = 1m;
let TotalEventsThresholdMin = 24;
let DurationThreshold_minutes = 180;
let ScoreThreshold = 0.85;
let MaxJitterInseconds = 30.0;
let CompromisedDeviceCountMax = 5;

let AllBeacons and let PotentialBeacons are inlined into the numbered stages below.

Stage 1: source

let AllBeacons

Stage 2: source

let PotentialBeacons

Stage 3: source

SecurityEvent

Stage 4: where

where TimeGenerated between (ago(86400s) .. ago(60s))

Stage 5: where

where EventID == 5156

Stage 6: where

where EventData has_cs "<Data Name=\"Protocol\">6</Data>"

Stage 7: where

where EventData has_cs "<Data Name=\"Direction\">%%14593</Data>"

Stage 8: where

where not (EventData has_cs "<Data Name=\"DestAddress\">127.0.0.1</Data>")

Stage 9: where

where not (EventData has_cs "<Data Name=\"DestAddress\">169.254.")

Stage 10: parse

parse

Stage 11: where

where not ((ipv4_is_in_range(DestinationIP, "10.0.0.0/8") or ipv4_is_in_range(DestinationIP, "172.16.0.0/12") or ipv4_is_in_range(DestinationIP, "192.168.0.0/16") or ipv4_is_in_range(DestinationIP, "169.254.0.0/16") or ipv4_is_in_range(DestinationIP, "127.0.0.0/8")))

Stage 12: parse

parse

Stage 13: summarize

summarize end, start by Computer, Application, Protocol, DestinationIP, DestinationPort

Stage 14: where

where list_TimeGenerated >= 24

Stage 15: extend

extend duration_minutes, duration_seconds

Stage 16: where

where duration_minutes >= 180

Stage 17: project

project Application, Computer, ConnRate, ConnectionCount, DestinationIP, DestinationPort, Protocol, TimeGenerated, duration_minutes, duration_seconds

Stage 18: kusto:mv-apply

kusto:mv-apply Computer =~ nextComputer and nextApplication =~ Application and nextDestinationIP =~ DestinationIP and nextDestinationPort =~ DestinationPort and nextProtocol =~ Protocol and tsScore >= ScoreThreshold

Stage 19: summarize

summarize by Protocol, DestinationIP, DestinationPort

Stage 20: where

where dcount_Computer <= 5

Stage 21: join

join kind=inner (AllBeacons) on Protocol, DestinationIP, DestinationPort

Stage 22: project-away

project-away *1

Stage 23: project-away

project-away TimeDeltaList

Stage 24: sort

sort by ConnectionCount, tsScore

Stage 25: project-reorder

project-reorder

Stage 26: source

WindowsFirewall

Stage 27: where

where TimeGenerated between (ago(86400s) .. ago(60s))

Stage 28: where

where CommunicationDirection == "SEND"

Stage 29: where

where FirewallAction == "ALLOW"

Stage 30: where

where Protocol == "TCP"

Stage 31: where

where DestinationIP != "127.0.0.1"

Stage 32: where

where not ((ipv4_is_in_range(DestinationIP, "10.0.0.0/8") or ipv4_is_in_range(DestinationIP, "172.16.0.0/12") or ipv4_is_in_range(DestinationIP, "192.168.0.0/16") or ipv4_is_in_range(DestinationIP, "169.254.0.0/16") or ipv4_is_in_range(DestinationIP, "127.0.0.0/8")))

Stage 33: summarize

summarize end, start by Computer, DestinationIP, Protocol, DestinationPort

Stage 34: where

where list_TimeGenerated >= 24

Stage 35: extend

extend duration_minutes, duration_seconds

Stage 36: where

where duration_minutes >= 180

Stage 37: project

project Computer, ConnRate, ConnectionCount, DestinationIP, DestinationPort, Protocol, TimeGenerated, duration_minutes, duration_seconds

Stage 38: kusto:mv-apply

kusto:mv-apply Computer =~ nextComputer and nextDestinationIP =~ DestinationIP and nextDestinationPort =~ DestinationPort and nextProtocol =~ Protocol and tsScore >= ScoreThreshold

Stage 39: summarize

summarize by Protocol, DestinationIP, DestinationPort

Stage 40: where

where dcount_Computer <= 5

Stage 41: join

join kind=inner (AllBeacons) on Protocol, DestinationIP, DestinationPort

Stage 42: project-away

project-away *1

Stage 43: project-away

project-away TimeDeltaList

Stage 44: sort

sort by ConnectionCount, tsScore

Stage 45: project-reorder

project-reorder

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
EventDatamatch<Data Name="DestAddress">127.0.0.1</Data>excludes:EventData
EventDatamatch<Data Name="DestAddress">169.254.excludes:EventData
DestinationIPcidr_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:DestinationIP

Output Fields

These fields are emitted when the rule matches.

FieldSource
DestinationIPsummarize
DestinationPortsummarize
Protocolsummarize

SAP BTP - Mass user deletion in Cloud Identity Service

#
Status
available
Severity
medium
Time window
1h
Group by
SpaceId, Tenant, UserName
Source
github.com/Azure/Azure-Sentinel

Identifies mass user deletion activity in SAP Cloud Identity Service where the amount of deleted users exceeds a predefined threshold.

MITRE ATT&CK Coverage

Rule Body

id: a3b8e7c4-5f2d-4a1e-9c6b-8d7f3e2a1b0c
kind: Scheduled
name: SAP BTP - Mass user deletion in Cloud Identity Service
description: Identifies mass user deletion activity in SAP Cloud Identity Service
  where the amount of deleted users exceeds a predefined threshold.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SAPBTPAuditEvents
    dataTypes:
      - SAPBTPAuditLog_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1531
  - T1485
  - T1489
  - T0813
  - T0826
  - T0827
query: |
  let bulk_delete_threshold = 10;
  SAPBTPAuditLog_CL
  | extend data_s = tostring(Message.data)
  | extend action = extract(@"action=""([^""]+)""", 1, data_s),
           objectType = extract(@"objectType=""([^""]+)""", 1, data_s),
           impactedUser = extract(@"serviceProviderName=""([^""]+)""", 1, data_s)
  | where action == "delete"
  | where objectType == "authorization"
  | where isnotempty(impactedUser)
  | summarize
      Start = min(UpdatedOn),
      End = max(UpdatedOn),
      DeleteCount = count(),
      DeletedUsers = make_set(impactedUser, 100)
      by UserName, Tenant, SpaceId
  | where array_length(DeletedUsers) > bulk_delete_threshold
  | project Start, End, UserName, DeletedUsers, DeleteCount, Tenant, SpaceId, CloudApp = "SAP BTP"
  | extend AccountName = split(UserName, "@")[0], UPNSuffix = split(UserName, "@")[1]
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: CloudApp
version: 3.0.1

Stages and Predicates

Parameters

let bulk_delete_threshold = 10;

Stage 1: source

SAPBTPAuditLog_CL

Stage 2: extend

| extend data_s = tostring(Message.data)

Stage 3: extend

| extend action = extract(@"action=""([^""]+)""", 1, data_s),
         objectType = extract(@"objectType=""([^""]+)""", 1, data_s),
         impactedUser = extract(@"serviceProviderName=""([^""]+)""", 1, data_s)

Stage 4: where

| where action == "delete"

Stage 5: where

| where objectType == "authorization"

Stage 6: where

| where isnotempty(impactedUser)

Stage 7: summarize

| summarize
    Start = min(UpdatedOn),
    End = max(UpdatedOn),
    DeleteCount = count(),
    DeletedUsers = make_set(impactedUser, 100)
    by UserName, Tenant, SpaceId

Stage 8: where

| where array_length(DeletedUsers) > bulk_delete_threshold

Stage 9: project

| project Start, End, UserName, DeletedUsers, DeleteCount, Tenant, SpaceId, CloudApp = "SAP BTP"

Stage 10: extend

| extend AccountName = split(UserName, "@")[0], UPNSuffix = split(UserName, "@")[1]

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
CloudAppproject
DeleteCountproject
DeletedUsersproject
Endproject
SpaceIdproject
Startproject
Tenantproject
UserNameproject
AccountNameextend
UPNSuffixextend

Sensitive Data Discovered in the Last 24 Hours - Customized

#
Severity
informational
Time window
1d
Source
github.com/Azure/Azure-Sentinel

'Customized query used to identify specific classifications and parameters that have been discovered on assets in the last 24 hours by Microsoft Purview. By default, the query identifies Social Security Numbers detected, but the specific classification monitored along with other data fields can be adjusted. A list of supported Microsoft Purview classifications can be found here: https://docs.microsoft.com/azure/purview/supported-classifications'

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Rule Body

id: 79f296d9-e6e4-45dc-9ca7-1770955435fa
name: Sensitive Data Discovered in the Last 24 Hours - Customized 
description: |
  'Customized query used to identify specific classifications and parameters that have been discovered on assets in the last 24 hours by Microsoft Purview. By default, the query identifies Social Security Numbers detected, but the specific classification monitored along with other data fields can be adjusted. A list of supported Microsoft Purview classifications can be found here: https://docs.microsoft.com/azure/purview/supported-classifications'
severity: Informational
requiredDataConnectors:
  - connectorId: MicrosoftAzurePurview
    dataTypes:
      - PurviewDataSensitivityLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T1087
query: |
  PurviewDataSensitivityLogs
  | where Classification contains "Social Security Number"
  //| where SourceRegion == "westeurope"
  //| where SourceType contains "Amazon"
  | where TimeGenerated > ago(24h)
entityMappings:
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: SourcePath
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: AssetName
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: PurviewAccountName
customDetails:
  AssetName: AssetName
  Classification: Classification
  AssetPath: AssetPath
  SourceRegion: SourceRegion
  PurviewAccount: PurviewAccountName
  LastScanTime: AssetLastScanTime
alertDetailsOverride:
  alertDisplayNameFormat: 'Sensitive Data Discovered in the Last 24 Hours by Microsoft Purview'
  alertDescriptionFormat: 'Within the last 24 hours, Microsoft Purview scanned assets that contained classifications. The classifications discovered include {{Classification}}.'
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

PurviewDataSensitivityLogs

Stage 2: where

| where Classification contains "Social Security Number"

Stage 3: where

| where TimeGenerated > ago(24h)

Indicators

These rows show field, operator, and value matches.

SUNBURST and SUPERNOVA backdoor hashes

#

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: a3c144f9-8051-47d4-ac29-ffb0c312c910
name: SUNBURST and SUPERNOVA backdoor hashes
description: |
  Identifies SolarWinds SUNBURST and SUPERNOVA backdoor file hash IOCs in DeviceFileEvents
  References:
  - https://www.fireeye.com/blog/threat-research/2020/12/evasive-attacker-leverages-solarwinds-supply-chain-compromises-with-sunburst-backdoor.html
  - https://gist.github.com/olafhartong/71ffdd4cab4b6acd5cbcd1a0691ff82f
severity: High
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceFileEvents
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
  - Persistence
  - InitialAccess
relevantTechniques:
  - T1195
  - T1059
  - T1546
query:  |
  let SunburstMD5=dynamic(["b91ce2fa41029f6955bff20079468448","02af7cec58b9a5da1c542b5a32151ba1","2c4a910a1299cdae2a4e55988a2f102e","846e27a652a5e1bfbd0ddd38a16dc865","4f2eb62fa529c0283b28d05ddd311fae"]);
  let SupernovaMD5="56ceb6d0011d87b6e4d7023d7ef85676";
  DeviceFileEvents
  | where MD5 in(SunburstMD5) or MD5 in(SupernovaMD5)
  | extend HashAlgorithm = "MD5"
  | extend HostName = tostring(split(DeviceName, ".")[0]), DomainIndex = toint(indexof(DeviceName, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(DeviceName, DomainIndex + 1), DeviceName)
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName 
        columnName: DeviceName
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
  - entityType: Account
    fieldMappings:
      - identifier: FullName 
        columnName: InitiatingProcessAccountUpn
      - identifier: Name
        columnName: InitiatingProcessAccountName
      - identifier: UPNSuffix
        columnName: InitiatingProcessAccountDomain
  - entityType: FileHash
    fieldMappings:
      - identifier: Algorithm
        columnName: HashAlgorithm
      - identifier: Value
        columnName: MD5
version: 1.0.7
kind: Scheduled

Stages and Predicates

Parameters

let SupernovaMD5 = "56ceb6d0011d87b6e4d7023d7ef85676";

Let Binding: SunburstMD5 used in Stage 2

let SunburstMD5 = dynamic(["b91ce2fa41029f6955bff20079468448","02af7cec58b9a5da1c542b5a32151ba1","2c4a910a1299cdae2a4e55988a2f102e","846e27a652a5e1bfbd0ddd38a16dc865","4f2eb62fa529c0283b28d05ddd311fae"]);

Stage 1: source

DeviceFileEvents

Stage 2: where

| where MD5 in(SunburstMD5) or MD5 in(SupernovaMD5)

Stage 3: extend (3 consecutive steps)

| extend HashAlgorithm = "MD5"
| extend HostName = tostring(split(DeviceName, ".")[0]), DomainIndex = toint(indexof(DeviceName, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(DeviceName, DomainIndex + 1), DeviceName)

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
MD5in
  • 02af7cec58b9a5da1c542b5a32151ba1 corpus 2 (kusto 2)
  • 2c4a910a1299cdae2a4e55988a2f102e corpus 2 (kusto 2)
  • 4f2eb62fa529c0283b28d05ddd311fae corpus 2 (kusto 2)
  • 56ceb6d0011d87b6e4d7023d7ef85676 corpus 2 (kusto 2)
  • 846e27a652a5e1bfbd0ddd38a16dc865 corpus 2 (kusto 2)
  • b91ce2fa41029f6955bff20079468448 corpus 2 (kusto 2)
field:"md5" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
HashAlgorithmextend
DomainIndexextend
HostNameextend
HostNameDomainextend

Tailscale Premium: New posture integration added

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

Identifies when a new device-posture integration is added to the tailnet (Jamf, Kandji, Intune, Kolide, Defender for Endpoint, CrowdStrike, SentinelOne). Verify the addition was sanctioned.

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Rule Body

id: b2c3d4e5-6789-0123-45ab-cdef12345031
name: "Tailscale Premium: New posture integration added"
description: Identifies when a new device-posture integration is added to the tailnet (Jamf, Kandji, Intune, Kolide, Defender for Endpoint, CrowdStrike, SentinelOne). Verify the addition was sanctioned.
description-detailed: |
  Identifies when a new device-posture integration is added to the tailnet (Jamf, Kandji, Intune, Kolide, Defender for Endpoint, CrowdStrike, SentinelOne, etc.). Unexpected additions may indicate an attacker establishing a control plane or bypassing compliance gates. Requires Tailscale Premium or Enterprise.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: TailscalePremiumCCF
    dataTypes:
      - Tailscale_Audit_CL
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1098
query: |
  Tailscale_Audit_CL
  | where Action == "CREATE"
  | where tostring(Target.type) == "POSTURE_INTEGRATION"
    or tostring(Target.type) startswith "POSTURE"
  | extend ActorLogin = tostring(Actor.loginName)
  | extend Provider = tostring(Target.name)
  | project TimeGenerated, ActorLogin, Provider, Target, New, Origin
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ActorLogin
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

Stage 1: source

Tailscale_Audit_CL

Stage 2: where

| where Action == "CREATE"

Stage 3: where

| where tostring(Target.type) == "POSTURE_INTEGRATION"
  or tostring(Target.type) startswith "POSTURE"

Stage 4: extend

| extend ActorLogin = tostring(Actor.loginName)

Stage 5: extend

| extend Provider = tostring(Target.name)

Stage 6: project

| project TimeGenerated, ActorLogin, Provider, Target, New, Origin

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actioneq
  • CREATE
field:"Action" kind:eq value:"CREATE"
Target.typeeq
  • POSTURE_INTEGRATION transforms: tostring, kusto_dynamic_path
field:"Target.type" kind:eq value:"POSTURE_INTEGRATION"
Target.typestarts_with
  • POSTURE transforms: tostring, kusto_dynamic_path
field:"Target.type" kind:starts_with value:"POSTURE"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActorLoginproject
Newproject
Originproject
Providerproject
Targetproject
TimeGeneratedproject

Back to all Kusto rules