Detection rules › Kusto
Kusto rules: 0s
1Password - Successful anomalous sign-in
#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
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| 1Password | credentials_ok |
| 1Password | mfa_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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
action_type | eq |
| field:"action_type" kind:eq |
category | eq |
| field:"category" kind:eq value:"success" |
log_source | eq |
| field:"log_source" kind:eq value:"signinattempts" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
identifier | eq | onePasswordSigninBaseline | excludes:identifier field:"identifier" value:"onePasswordSigninBaseline" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
identifier | extend |
SrcIpAddr | extend |
TargetUsername | extend |
AD FS Remote Auth Sync Connection
#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
| Tactic | Techniques |
|---|---|
| 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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Application | eq |
| field:"Application" kind:eq value:"System" |
Computer | in |
| field:"Computer" kind:in value:"ADFS_Servers" |
DestPort | eq |
| field:"DestPort" kind:eq value:"80" |
Direction | eq |
| field:"Direction" kind:eq value:"%%14592" |
EventData[1] | contains |
| field:"EventData[1]" kind:contains value:"identity/claims/name" |
EventID | eq |
| field:"EventID" kind:eq |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
DestAddress | in | 0:0:0:0:0:0:0:1, ::1 | excludes: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.
| Field | Source |
|---|---|
ClaimsName | project |
Computer | project |
SourceAddress | project |
SourcePort | project |
TimeGenerated | project |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |
AccountNTDomain | extend |
AccountName | extend |
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.
The policy detects an administrative account where the password of the account was not updated in the last 30 days.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
Policy | match |
| 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.
| Field | Source |
|---|---|
Category | project |
Description | project |
EventID | project |
Policy | project |
Recommendation | project |
Severity | project |
Tactics | project |
URL | project |
Anomaly in SMB Traffic(ASIM Network Session schema)
#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
| Tactic | Techniques |
|---|---|
| Lateral Movement |
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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Deviation | gt |
| field:"Deviation" kind:gt value:"50" |
DstPortNumber | match |
| field:"DstPortNumber" kind:match |
SrcIpAddr | cidr_match |
| field:"SrcIpAddr" kind:cidr_match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Count | project |
Deviation | project |
DstPortNumber | project |
SrcIpAddr | project |
API - Rate limiting
#'42Crunch API protection against first-time access'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Discovery |
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
let ipAddress = toscalar(firstRec | summarize by SourceIp);
Let Binding: recCount
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.
| Field | Kind | Values | Search |
|---|---|---|---|
UriPath | match |
| field:"UriPath" kind:match value:"/api/login" |
BitSight - drop in company ratings
#'Rule helps to detect when there is a drop of 10% or more in BitSight company ratings.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
RatingDifferance | lt |
| field:"RatingDifferance" kind:lt value:"0" |
percentage | ge |
| field:"percentage" kind:ge value:"10" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CompanyName | project |
Rating | project |
RatingDate | project |
percentage | project |
BitSight - new breach found
#'Rule helps to detect a new breach generated in BitSight.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Impact |
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 =Severity == 1"Low"Severity == 2"Medium"Severity == 3"High""Informational"Stage 5: project
| project DateCreated, Companyname, Severity, PreviwURL, GUID
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Companyname | project |
DateCreated | project |
GUID | project |
PreviwURL | project |
Severity | project |
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.
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"T0ADCSESC4" |
Cisco Duo - Admin user deleted
#'Detects when admin user is deleted.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionName | eq |
| field:"ActionName" kind:eq value:"admin_delete" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountCustomEntity | extend |
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.
'Display incidents in which medium sensitivity business information was placed at risk by user sharing.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
Severity | eq |
| field:"Severity" kind:eq value:"2" |
informationType_s | eq |
| field:"informationType_s" kind:eq value:"Business Information" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountCustomEntity | extend |
Cross-tenant Access Settings Organization Added
#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
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence | |
| Discovery |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Rules Detecting the Same Action
These rules filter on the same operation.
- Cross-tenant Access Settings Organization Deleted (Kusto)
- Cross-tenant Access Settings Organization Inbound Collaboration Settings Changed (Kusto)
- Cross-tenant Access Settings Organization Inbound Direct Settings Changed (Kusto)
- Cross-tenant Access Settings Organization Outbound Collaboration Settings Changed (Kusto)
- Cross-tenant Access Settings Organization Outbound Direct Settings Changed (Kusto)
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.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | match |
| field:"OperationName" kind:match value:"Add a partner to cross-tenant access setting" |
Property.displayName | eq |
| field:"Property.displayName" kind:eq value:"tenantId" |
TargetResource.type | eq |
| field:"TargetResource.type" kind:eq value:"Policy" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
ExtTenantIDAdded | in | List of expected tenant IDs, Tenant ID 2 | excludes: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.
| Field | Source |
|---|---|
InitiatingAppName | extend |
InitiatingAppServicePrincipalId | extend |
InitiatingUserPrincipalName | extend |
InitiatingAadUserId | extend |
InitiatingIpAddress | extend |
InitiatingAccountName | extend |
InitiatingAccountUPNSuffix | extend |
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.
'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
| Tactic | Techniques |
|---|---|
| Reconnaissance | |
| Initial Access |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Service | eq |
| field:"Service" kind:eq value:"discord" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
MappedSeverity | extend |
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.
"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
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Command & Control | |
| Exfiltration |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
ConfidenceScore | ge |
| field:"ConfidenceScore" kind:ge value:"50" |
ConfidenceScore | lt |
| field:"ConfidenceScore" kind:lt value:"80" |
RecommendedActions | match |
| field:"RecommendedActions" kind:match value:"Monitor" |
Roles | contains |
| field:"Roles" kind:contains |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
pattern | contains | file:hashes | excludes:pattern field:"pattern" value:"file:hashes" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ConfidenceScore | project |
Country | project |
Description | project |
Domain | project |
IPAbuse | project |
IPv4 | project |
IPv6 | project |
IndicatorID | project |
ProductName | project |
ProviderName | project |
RecommendedActions | project |
Roles | project |
SecurityVendors | project |
Sources | project |
Tags | project |
ThreatActors | project |
ThreatType | project |
TimeGenerated | project |
URL | project |
created | project |
modified | project |
name | project |
valid_from | project |
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.
"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
| Tactic | Techniques |
|---|---|
| Reconnaissance | |
| Initial Access | |
| Collection | |
| Exfiltration |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
severity | eq |
| field:"severity" kind:eq value:"Critical" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertTitle | project |
AlertUID | project |
AssetType | project |
AssetValue | project |
Description | project |
FirstSeen | project |
Impact | project |
LastSeen | project |
ProductName | project |
ProviderName | project |
Recommendation | project |
RiskScore | project |
TimeGenerated | project |
UID | project |
Dataverse - Mass record updates
#This query detects mass record update changes in Dataverse and Dynamics 365, exceeding a pre-defined threshold.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact | |
| Impact |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft 365 | any: Dataverse and model-driven apps audit (catch-all) |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
EventCount | gt |
| field:"EventCount" kind:gt value:"10000" |
Message | eq |
| field:"Message" kind:eq value:"Update" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
ClientIp | project |
CloudAppId | project |
Details | project |
FirstEvent | project |
InstanceUrl | project |
TotalEvents | project |
UPNSuffix | project |
UserId | project |
Dataverse - New non-interactive identity granted access
#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
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation | |
| Persistence | |
| Lateral Movement |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Entra-AuditLogs | Update application |
| M365-CRM | any: Dataverse and model-driven apps audit (catch-all) |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Application Granted Administrative Permission to Assign Microsoft Entra ID Roles (Kusto)
- Added Credentials to Existing Application (Sigma)
- Added Owner To Application (Sigma)
- Admin promotion after Role Management Application Permission Grant (Kusto)
- App Granted Microsoft Permissions (Sigma)
- App Granted Privileged Delegated Or App Permissions (Sigma)
- Application AppID Uri Configuration Changes (Sigma)
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.
| Field | Kind | Values | Search |
|---|---|---|---|
EntityName | eq |
| field:"EntityName" kind:eq value:"systemuser" |
Fields[0].Name | eq |
| field:"Fields[0].Name" kind:eq value:"applicationid" |
Message | eq |
| field:"Message" kind:eq value:"Create" |
NewData | match |
| field:"NewData" kind:match value:"00000007-0000-0000-c000-000000000000" |
OperationName | eq |
| field:"OperationName" kind:eq value:"Update application" |
TargetResources | match |
| field:"azure_ad::target_resources" kind:match value:"00000007-0000-0000-c000-000000000000" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
ClientIp | project |
CloudAppId | project |
InstanceUrl | project |
TargetAppId | project |
TargetAppName | project |
TimeGenerated | project |
UPNSuffix | project |
UserId | project |
Deleted a Tenant
#'Deleted a Tenant from SOC Prime platform'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
EventName | eq |
| field:"aws::eventName" kind:eq value:"Deleted a Tenant" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
EventName | project |
SourceIp | project |
TimeGenerated | project |
Type | project |
Uri | project |
UserEmail | project |
UserName | project |
Digital Guardian - Incident with not blocked action
#'Detects when incident has not block action.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
IncidentStatus | is_not_null | field:"IncidentStatus" kind:is_not_null | |
inc_act | match |
| field:"inc_act" kind:match value:"New" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
inc_act | contains | Block | excludes:inc_act field:"inc_act" value:"Block" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
inc_act | extend |
AccountCustomEntity | extend |
F&O - Bank account change following network alias reassignment
#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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
LogType | eq |
| field:"LogType" kind:eq value:"Update" |
TableName | eq |
| field:"TableName" kind:eq |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountId | project |
AccountName | project |
AliasChangedBy | project |
AliasUpdated | project |
CurrentAccountNum | project |
CurrentAlias | project |
FinOpsAppId | project |
OldAccountNum | project |
PreviousAlias | project |
UPNSuffix | project |
Username | project |
Guardian- Sentiment Policy Violation Detection
#'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.
| Field | Kind | Values | Search |
|---|---|---|---|
PolicyViolatedControlFeature | eq |
| field:"PolicyViolatedControlFeature" kind:eq value:"Sentiment" |
Severity | eq |
| field:"Severity" kind:eq value:"Low" |
NRT GravityZone Incident Alerts
#The query identifies incident-level events received from the GravityZone Data Connector
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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 =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"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.
| Field | Source |
|---|---|
DvcAction | project |
DvcFQDN | project |
DvcHostname | project |
DvcId | project |
DvcIpAddr | project |
EventProduct | project |
EventSeverity | project |
EventStartTime | project |
EventUid | project |
EventVendor | project |
IncidentType | project |
Tactics | project |
Pathlock TDnR - RFC Connection Changes
#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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
DataSource | eq |
| field:"DataSource" kind:eq value:"RFC_DESTINATIONS" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AffectedUser | project |
Area | project |
Bname | project |
CentralTs | project |
DataSource | project |
DestIp | project |
Eventid | project |
Hostname | project |
Instance | project |
LogLine | project |
MessageV1 | project |
MessageV2 | project |
MessageV3 | project |
MessageV4 | project |
MsgId | project |
MsgNo | project |
MsgType | project |
Report | project |
SrcIp | project |
Subid | project |
Sysid | project |
Tcode | project |
TimeGenerated | project |
RDP Nesting
#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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
ClientIP | is_not_null | field:"ClientIP" kind:is_not_null | |
ConnectedNetworks | is_not_null | field:"ConnectedNetworks" kind:is_not_null | |
DeviceName | in |
| field:"DeviceName" kind:in |
EventData | match |
| field:"EventData" kind:match value:"10" |
EventID | eq |
| field:"EventID" kind:eq value:"4624" |
LogonType | eq |
| field:"LogonType" kind:eq value:"10" |
NetworkAdapterStatus | eq |
| field:"NetworkAdapterStatus" kind:eq value:"Up" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | summarize |
AccountType | summarize |
Activity | summarize |
FirstComputer | summarize |
FirstComputerIP | summarize |
FirstHopFirstSeen | summarize |
FirstHopLastSeen | summarize |
FirstRemoteIPAddress | summarize |
LogonTypeName | summarize |
ProcessName | summarize |
SecondComputer | summarize |
SecondComputerIP | summarize |
SecondHop | summarize |
SecondRemoteIPAddress | summarize |
AccountNTDomain | extend |
AccountName | extend |
HostName1 | extend |
HostNameDomain1 | extend |
HostName2 | extend |
HostNameDomain2 | extend |
RITA Beacon Analyzer for Windows Firewall Events
#Below queries analyze Windows Firewall logs and applies RITA beacon analyzer algorithm for C2 beaconing detection.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control | No specific technique |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 5156: The Windows Filtering Platform has permitted a connection. |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
CommunicationDirection | eq |
| field:"CommunicationDirection" kind:eq value:"SEND" |
DestinationIP | ne |
| field:"DestinationIP" kind:ne value:"127.0.0.1" |
EventData | match |
| field:"EventData" kind:match |
EventID | eq |
| field:"EventID" kind:eq value:"5156" |
FirewallAction | eq |
| field:"FirewallAction" kind:eq value:"ALLOW" |
Protocol | eq |
| field:"Protocol" kind:eq value:"TCP" |
dcount_Computer | le |
| field:"dcount_Computer" kind:le value:"5" |
duration_minutes | ge |
| field:"duration_minutes" kind:ge value:"180" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
EventData | match | <Data Name="DestAddress">127.0.0.1</Data> | excludes:EventData |
EventData | match | <Data Name="DestAddress">169.254. | excludes:EventData |
DestinationIP | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 | excludes:DestinationIP |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DestinationIP | summarize |
DestinationPort | summarize |
Protocol | summarize |
SAP BTP - Mass user deletion in Cloud Identity Service
#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.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"delete" |
impactedUser | is_not_null | field:"impactedUser" kind:is_not_null | |
objectType | eq |
| field:"objectType" kind:eq value:"authorization" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CloudApp | project |
DeleteCount | project |
DeletedUsers | project |
End | project |
SpaceId | project |
Start | project |
Tenant | project |
UserName | project |
AccountName | extend |
UPNSuffix | extend |
Sensitive Data Discovered in the Last 24 Hours - Customized
#'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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
Classification | contains |
| field:"Classification" kind:contains value:"Social Security Number" |
SUNBURST and SUPERNOVA backdoor hashes
#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
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Persistence |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceFileEvents action any: File activity |
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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
MD5 | in |
| field:"md5" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
HashAlgorithm | extend |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |
Tailscale Premium: New posture integration added
#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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | eq |
| field:"Action" kind:eq value:"CREATE" |
Target.type | eq |
| field:"Target.type" kind:eq value:"POSTURE_INTEGRATION" |
Target.type | starts_with |
| field:"Target.type" kind:starts_with value:"POSTURE" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActorLogin | project |
New | project |
Origin | project |
Provider | project |
Target | project |
TimeGenerated | project |