Detection rules › Kusto
Kusto rules: 2a
AWS role with shadow admin 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.
The policy detect the creation of new AWS roles with shadow admin privileges. The policy configuration allows limiting the policy to specific accounts.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 2526079b-3355-4756-a2d1-21e9cd957261
name: AWS role with shadow admin privileges
kind: Scheduled
description: The policy detect the creation of new AWS roles with shadow admin privileges. The policy configuration allows limiting the policy to specific accounts.
severity: High
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 "AWS role with shadow admin privileges"
| 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 - AWS role with shadow admin privileges
alertDescriptionFormat: |
IaaS shadow admin detected. The policy detect the creation of new AWS roles with shadow admin privileges. The policy configuration allows limiting the policy to specific accounts.
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 "AWS role with shadow admin privileges"
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:"AWS role with shadow admin privileges" |
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 |
Cayosoft Guardian - Core Identity and Infrastructure Threats
#Creates Microsoft Sentinel incidents for identity and infrastructure security threats reported by Cayosoft Guardian across on-premises Active Directory and Microsoft Entra ID. This includes threats related to accounts, service accounts, computers, security groups, and trusted domains. Incident severity is determined dynamically based on the severity of the detected threat. This rule complements the Cloud Application Security Threats rule, which covers cloud application and service principal threat alerts not included in this rule. Alerts with the same Cayosoft ThreatId are grouped into a single incident within a 1-day lookback window. Each alert includes a link to the Cayosoft Threat Directory for guidance on remediation.
Rule Body
id: 4720d7a5-6845-4ce4-aa45-79334e1a1176
name: Cayosoft Guardian - Core Identity and Infrastructure Threats
description: |
Creates Microsoft Sentinel incidents for identity and infrastructure security threats reported by Cayosoft Guardian across on-premises Active Directory and Microsoft Entra ID. This includes threats related to accounts, service accounts, computers, security groups, and trusted domains. Incident severity is determined dynamically based on the severity of the detected threat.
This rule complements the Cloud Application Security Threats rule, which covers cloud application and service principal threat alerts not included in this rule.
Alerts with the same Cayosoft ThreatId are grouped into a single incident within a 1-day lookback window. Each alert includes a link to the Cayosoft Threat Directory for guidance on remediation.
contentId: CayosoftGuardianMainAlerts
status: Available
requiredDataConnectors:
- connectorId: CayosoftGuardianConnector
dataTypes:
- CayosoftThreatAlerts_CL
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 1h
suppressionEnabled: false
severity: Medium
tactics: []
relevantTechniques: []
query: |
// ATTENTION: ruleFrequency must match the "queryFrequency" value in the rule's scheduling settings!
// ATTENTION: lookbackPeriod must match the "queryPeriod" value in the rule's scheduling settings!
// If you change one, change both, in every location, or you will reintroduce duplicate alerts.
// Pattern reference: https://learn.microsoft.com/en-us/azure/sentinel/ingestion-delay
let ruleFrequency = 1h;
let lookbackPeriod = 1d;
let window_end = bin(now(), ruleFrequency);
let window_start = window_end - ruleFrequency;
CayosoftThreatAlerts_CL
| where TimeGenerated >= ago(lookbackPeriod)
| where ingestion_time() > window_start and ingestion_time() <= window_end
| extend ParsedCategories = todynamic(Categories)
| extend TargetObjectTypeNormalized = tostring(TargetObjectType)
| extend TargetLocationNormalized = tostring(TargetLocation)
| mv-apply Cat = ParsedCategories on (
summarize Values = make_list(tostring(Cat.value)) by Key = iff(tostring(Cat.name) startswith 'MITRE ATT', 'MITRE ATT&CK', tostring(Cat.name))
| summarize CleanCategories = make_bag(pack(Key, strcat_array(Values, ', ')))
)
| extend Tactics = iff(isnotempty(CleanCategories['Attack tactics']), split(replace_string(tostring(CleanCategories['Attack tactics']), ' ', ''), ','), dynamic([]))
| extend Techniques = iff(isnotempty(CleanCategories['MITRE ATT&CK']), extract_all(@"(T\d{4})", tostring(CleanCategories['MITRE ATT&CK'])), dynamic([]))
| extend ExtractedDomain = tostring(split(TargetLocationNormalized, '/')[0])
| extend IsEntra = TargetObjectTypeNormalized startswith 'microsoft.graph'
| extend IsAD = TargetObjectTypeNormalized startswith 'microsoft.ad' or TargetObjectTypeNormalized has 'ServiceAccount'
| extend IsGMSA = TargetObjectTypeNormalized contains 'ManagedServiceAccount'
| extend ParsedUser = case(
TargetObjectTypeNormalized has 'user' or TargetObjectTypeNormalized has 'ServiceAccount' or TargetObjectTypeNormalized has 'inetOrgPerson', TargetObjectName,
extract("member\\s+'([^']+)'", 1, Subject)
)
| extend TargetAccountAadUserId = iff(IsEntra and TargetObjectTypeNormalized has 'user', TargetObjectId, dynamic(null))
| extend TargetAccountName = case(
IsEntra and TargetObjectTypeNormalized has 'user', iff(ParsedUser contains '@', tostring(split(ParsedUser, '@')[0]), ParsedUser),
IsAD and (TargetObjectTypeNormalized has 'user' or TargetObjectTypeNormalized has 'foreignSecurityPrincipal' or IsGMSA), TargetObjectName,
IsAD and TargetObjectTypeNormalized has 'directoryObject', TargetObjectName,
isnotempty(ParsedUser), iff(ParsedUser contains '@', tostring(split(ParsedUser, '@')[0]), ParsedUser),
dynamic(null)
)
| extend TargetAccountObjectGuid = iff(IsAD and (TargetObjectTypeNormalized has 'user' or TargetObjectTypeNormalized has 'foreignSecurityPrincipal' or IsGMSA), TargetObjectId, dynamic(null))
| extend TargetHostName = iff(TargetObjectTypeNormalized has 'computer' or TargetObjectTypeNormalized has 'device', TargetObjectName, dynamic(null))
| extend TargetHostDomain = iff(IsAD and TargetObjectTypeNormalized has 'computer', ExtractedDomain, dynamic(null))
| extend TargetHostAzureID = iff(IsEntra and TargetObjectTypeNormalized has 'device', TargetObjectId, dynamic(null))
| extend RawPath = iff(TargetLocationNormalized contains '/', substring(TargetLocationNormalized, indexof(TargetLocationNormalized, '/') + 1), '')
// Convert domain
| extend DomainDN = strcat('DC=', replace_string(ExtractedDomain, '.', ',DC='))
// Smart path processing
| extend PathDN = case(
isempty(RawPath), '',
RawPath has_any ('Builtin', 'Users', 'Computers', 'ForeignSecurityPrincipals', 'Managed Service Accounts'), strcat('CN=', replace_string(RawPath, '/', ',CN=')),
strcat('OU=', replace_string(RawPath, '/', ',OU='))
)
// Assemble the final DistinguishedName and Name for the group
| extend TargetGroupName = iff(TargetObjectTypeNormalized has 'group', TargetObjectName, dynamic(null))
| extend TargetGroupDistinguishedName = iff(
TargetObjectTypeNormalized has 'group',
strcat('CN=', TargetObjectName, iff(isnotempty(PathDN), strcat(',', PathDN), ''), ',', DomainDN),
dynamic(null)
)
| extend TargetGroupObjectGuid = iff(TargetObjectTypeNormalized has 'group', TargetObjectId, dynamic(null))
| extend TargetDNSName = case(
IsAD and TargetObjectTypeNormalized has 'trustedDomain', TargetObjectName,
isnotempty(TargetLocationNormalized), ExtractedDomain,
dynamic(null)
)
| extend ThreatDocUrl = tolower( strcat('https://www.cayosoft.com/threat-directory/', ThreatId, '/'))
| extend AlertSeverity = case(
Severity == 'high', 'High',
Severity == 'medium', 'Medium',
Severity == 'low', 'Low',
Severity == 'informational', 'Informational',
'Informational'
)
| project-away Categories, ParsedCategories
| project-rename Categories = CleanCategories
| where not(IsEntra and (TargetObjectType has 'application' or TargetObjectType has 'servicePrincipal'))
eventGroupingSettings:
aggregationKind: AlertPerResult
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: P1D
matchingMethod: Selected
groupByCustomDetails:
- ThreatId
alertDetailsOverride:
alertDisplayNameFormat: "{{Subject}}"
alertDescriptionFormat: "Details: {{ThreatDocUrl}}"
alertTacticsColumnName: Tactics
alertSeverityColumnName: AlertSeverity
alertDynamicProperties:
- alertProperty: AlertLink
value: ThreatDocUrl
- alertProperty: Techniques
value: Techniques
customDetails:
ThreatId: ThreatId
Categories: Categories
Evidence: Evidence
Where: TargetLocation
TargetName: TargetObjectName
TargetType: TargetObjectType
CreatedDateTime: CreatedDateTime
RemediationComplex: RemediationComplexity
ThreatDocUrl: ThreatDocUrl
entityMappings:
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: TargetAccountAadUserId
- identifier: Name
columnName: TargetAccountName
- identifier: ObjectGuid
columnName: TargetAccountObjectGuid
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: TargetHostName
- identifier: NTDomain
columnName: TargetHostDomain
- identifier: AzureID
columnName: TargetHostAzureID
- entityType: SecurityGroup
fieldMappings:
- identifier: DistinguishedName
columnName: TargetGroupDistinguishedName
- identifier: ObjectGuid
columnName: TargetGroupObjectGuid
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: TargetDNSName
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let ruleFrequency = 1h;
let lookbackPeriod = 1d;
let window_end = bin(now(), ruleFrequency);
let window_start = window_end - ruleFrequency;
Stage 1: source
CayosoftThreatAlerts_CL
Stage 2: where
| where TimeGenerated >= ago(lookbackPeriod)
Stage 3: where
| where ingestion_time() > window_start and ingestion_time() <= window_end
Stage 4: extend (3 consecutive steps)
| extend ParsedCategories = todynamic(Categories)
| extend TargetObjectTypeNormalized = tostring(TargetObjectType)
| extend TargetLocationNormalized = tostring(TargetLocation)
Stage 5: kusto:mv-apply
| mv-apply Cat = ParsedCategories on (
summarize Values = make_list(tostring(Cat.value)) by Key = iff(tostring(Cat.name) startswith 'MITRE ATT', 'MITRE ATT&CK', tostring(Cat.name))
| summarize CleanCategories = make_bag(pack(Key, strcat_array(Values, ', ')))
)
Stage 6: extend (22 consecutive steps)
| extend Tactics = iff(isnotempty(CleanCategories['Attack tactics']), split(replace_string(tostring(CleanCategories['Attack tactics']), ' ', ''), ','), dynamic([]))
| extend Techniques = iff(isnotempty(CleanCategories['MITRE ATT&CK']), extract_all(@"(T\d{4})", tostring(CleanCategories['MITRE ATT&CK'])), dynamic([]))
| extend ExtractedDomain = tostring(split(TargetLocationNormalized, '/')[0])
| extend IsEntra = TargetObjectTypeNormalized startswith 'microsoft.graph'
| extend IsAD = TargetObjectTypeNormalized startswith 'microsoft.ad' or TargetObjectTypeNormalized has 'ServiceAccount'
| extend IsGMSA = TargetObjectTypeNormalized contains 'ManagedServiceAccount'
| extend ParsedUser = case(
TargetObjectTypeNormalized has 'user' or TargetObjectTypeNormalized has 'ServiceAccount' or TargetObjectTypeNormalized has 'inetOrgPerson', TargetObjectName,
extract("member\\s+'([^']+)'", 1, Subject)
)
| extend TargetAccountAadUserId = iff(IsEntra and TargetObjectTypeNormalized has 'user', TargetObjectId, dynamic(null))
| extend TargetAccountName = case(
IsEntra and TargetObjectTypeNormalized has 'user', iff(ParsedUser contains '@', tostring(split(ParsedUser, '@')[0]), ParsedUser),
IsAD and (TargetObjectTypeNormalized has 'user' or TargetObjectTypeNormalized has 'foreignSecurityPrincipal' or IsGMSA), TargetObjectName,
IsAD and TargetObjectTypeNormalized has 'directoryObject', TargetObjectName,
isnotempty(ParsedUser), iff(ParsedUser contains '@', tostring(split(ParsedUser, '@')[0]), ParsedUser),
dynamic(null)
)
| extend TargetAccountObjectGuid = iff(IsAD and (TargetObjectTypeNormalized has 'user' or TargetObjectTypeNormalized has 'foreignSecurityPrincipal' or IsGMSA), TargetObjectId, dynamic(null))
| extend TargetHostName = iff(TargetObjectTypeNormalized has 'computer' or TargetObjectTypeNormalized has 'device', TargetObjectName, dynamic(null))
| extend TargetHostDomain = iff(IsAD and TargetObjectTypeNormalized has 'computer', ExtractedDomain, dynamic(null))
| extend TargetHostAzureID = iff(IsEntra and TargetObjectTypeNormalized has 'device', TargetObjectId, dynamic(null))
| extend RawPath = iff(TargetLocationNormalized contains '/', substring(TargetLocationNormalized, indexof(TargetLocationNormalized, '/') + 1), '')
| extend DomainDN = strcat('DC=', replace_string(ExtractedDomain, '.', ',DC='))
| extend PathDN = case(
isempty(RawPath), '',
RawPath has_any ('Builtin', 'Users', 'Computers', 'ForeignSecurityPrincipals', 'Managed Service Accounts'), strcat('CN=', replace_string(RawPath, '/', ',CN=')),
strcat('OU=', replace_string(RawPath, '/', ',OU='))
)
| extend TargetGroupName = iff(TargetObjectTypeNormalized has 'group', TargetObjectName, dynamic(null))
| extend TargetGroupDistinguishedName = iff(
TargetObjectTypeNormalized has 'group',
strcat('CN=', TargetObjectName, iff(isnotempty(PathDN), strcat(',', PathDN), ''), ',', DomainDN),
dynamic(null)
)
| extend TargetGroupObjectGuid = iff(TargetObjectTypeNormalized has 'group', TargetObjectId, dynamic(null))
| extend TargetDNSName = case(
IsAD and TargetObjectTypeNormalized has 'trustedDomain', TargetObjectName,
isnotempty(TargetLocationNormalized), ExtractedDomain,
dynamic(null)
)
| extend ThreatDocUrl = tolower( strcat('https://www.cayosoft.com/threat-directory/', ThreatId, '/'))
| extend AlertSeverity = case(
Severity == 'high', 'High',
Severity == 'medium', 'Medium',
Severity == 'low', 'Low',
Severity == 'informational', 'Informational',
'Informational'
)
Tactics =isnotempty(CleanCategories["Attack tactics"])split(replace_string(tostring(CleanCategories["Attack tactics"]), ' ', ''), ',')dynamic([])Stage 7: project-away
| project-away Categories, ParsedCategories
Stage 8: project-rename
| project-rename Categories = CleanCategories
Stage 9: where
| where not(IsEntra and (TargetObjectType has 'application' or TargetObjectType has 'servicePrincipal'))
Stage 10: summarize aggregation inside the mv-apply branch
summarize by Key
Stage 11: summarize aggregation inside the mv-apply branch
summarize
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
TargetObjectType | match | application | excludes:TargetObjectType field:"TargetObjectType" value:"application" |
TargetObjectType | match | servicePrincipal | excludes:TargetObjectType field:"TargetObjectType" value:"servicePrincipal" |
IsEntra | eq | true | excludes:IsEntra field:"IsEntra" value:"true" |
Contrast ADR - Exploited Attack Event
#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.
'Detects successful exploitation of security vulnerabilities across all environments as identified by Contrast ADR. This rule captures confirmed exploited attacks that bypassed application security controls and require security team investigation.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Stealth | |
| Lateral Movement | |
| Command & Control |
Rule Body
id: ae4f67a6-0713-4a26-ae61-284e67b408c1
name: Contrast ADR - Exploited Attack Event
description: |
'Detects successful exploitation of security vulnerabilities across all environments as identified by Contrast ADR. This rule captures confirmed exploited attacks that bypassed application security controls and require security team investigation.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: ContrastADRCCF
dataTypes:
- ContrastADRAttackEvents_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Execution
- DefenseEvasion
- LateralMovement
- CommandAndControl
relevantTechniques:
- T1190
- T1059
- T1055
- T1210
- T1008
query: |
ContrastADRAttackEvents_CL
| where result =~ "exploited"
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: sourceIp
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: host_hostname
alertDetailsOverride:
alertDisplayNameFormat: '{{result}} {{rule}} from {{sourceIp}} '
alertDescriptionFormat: '{{result}} on {{request_headers_referer}} endpoint of {{application_name}} '
customDetails:
AttackResult: result
AttackRule: rule
ApplicationName: application_name
AttackedEndpoint: request_headers_referer
Environment: environment
TargetHost: host_hostname
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: PT1H
matchingMethod: Selected
groupByEntities:
- IP
- Host
eventGroupingSettings:
aggregationKind: AlertPerResult
kind: Scheduled
version: 1.0.1
Stages and Predicates
Stage 1: source
ContrastADRAttackEvents_CL
Stage 2: where
| where result =~ "exploited"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
result | eq |
| field:"result" kind:eq value:"exploited" |
Cyble Vision Alerts Domain Watchlist
#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 domain DNS records change (A, NS, MX, TXT, SOA). DNS record changes may indicate misconfiguration, domain takeover attempts, or infrastructure shifts requiring review.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Resource Development |
Rule Body
id: 0a54dc90-9a9d-4300-af21-feb5136e81da
name: Cyble Vision Alerts Domain Watchlist
description: |
'Triggers when monitored domain DNS records change (A, NS, MX, TXT, SOA). DNS record changes may indicate misconfiguration, domain takeover attempts, or infrastructure shifts requiring review.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CybleVisionAlerts
dataTypes:
- CybleVisionAlerts_CL
enabled: true
queryfrequency: 30m
queryPeriod: 30m
triggerOperator: GreaterThan
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics:
- ResourceDevelopment
relevantTechniques:
- T1584
query: |
Alerts_domain_watchlist
| where Service == "domain_watchlist"
| extend MappedSeverity = Severity
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
alertDetailsOverride:
alertDisplayNameFormat: DNS Record Change Detected for {{DW_New_Domain}}
alertDescriptionFormat: |
Domain Watchlist Alert: DNS records for {{DW_New_Domain}} have changed. Updated fields {{DW_UpdatedFields}}. Review differences between old and new DNS records to determine if this is a legitimate update or a potential hijack attempt.
customDetails:
MappedSeverity: Severity
Status: Status
AlertID: AlertID
Service: Service
DW_UpdatedFields: DW_UpdatedFields
DW_Old_Domain: DW_Old_Domain
DW_New_Domain: DW_New_Domain
entityMappings:
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: DW_New_Domain
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
Alerts_domain_watchlist
Stage 2: where
| where Service == "domain_watchlist"
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:"domain_watchlist" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
MappedSeverity | extend |
Dataverse - Login from IP not in the allow list
#Identifies logons from IPv4 addresses not matching IPv4 subnets maintained on an allow list. This analytics rule uses the NetworkAddresses watchlist template.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft 365 | any: Dataverse and model-driven apps audit (catch-all) |
Rule Body
id: 81c693fe-f6c4-4352-bc10-3526f6e22637
kind: Scheduled
name: Dataverse - Login from IP not in the allow list
description: Identifies logons from IPv4 addresses not matching IPv4 subnets maintained
on an allow list. This analytics rule uses the NetworkAddresses watchlist template.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Dataverse
dataTypes:
- DataverseActivity
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
- T1190
- T1133
query: |
// Use static IP address or CIDR list specified in the
// NetworkAddresses watchlist template with tag "AllowDataverse"
let allowed_networks = MSBizAppsNetworkAddresses()
| where Tags has "AllowDataverse"
| summarize by IPSubnet;
let query_frequency = 1h;
let watchlist_entries_count = toscalar (allowed_networks
| summarize count());
let dataverse_signin_activity = materialize(
DataverseActivity
| where watchlist_entries_count > 0
| where TimeGenerated >= ago (query_frequency)
| where Message == "UserSignIn" and isnotempty(ClientIp)
| summarize FirstEvent = arg_min(TimeGenerated, *) by UserId, ClientIp, InstanceUrl
);
let authorized_ip_addresses = dataverse_signin_activity
| evaluate ipv4_lookup(allowed_networks, ClientIp, IPSubnet);
dataverse_signin_activity
| join kind=leftanti(authorized_ip_addresses) on ClientIp
| extend
CloudAppId = int(32780),
AccountName = tostring(split(UserId, '@')[0]),
UPNSuffix = tostring(split(UserId, '@')[1])
| project
FirstEvent,
UserId,
ClientIp,
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 - Login from IP not on the allow list in {{InstanceUrl}} '
alertDescriptionFormat: Sign-in activity detected in {{InstanceUrl}} from an IP
{{ClientIp}} not on the allow list.
version: 3.2.0
Stages and Predicates
Parameters
let query_frequency = 1h;
let dataverse_signin_activity is inlined into the numbered stages below.
Let Binding: allowed_networks
let allowed_networks = MSBizAppsNetworkAddresses()
| where Tags has "AllowDataverse"
| summarize by IPSubnet;
Let Binding: watchlist_entries_count
let watchlist_entries_count = toscalar (allowed_networks
| summarize count());
Let Binding: authorized_ip_addresses
let authorized_ip_addresses = dataverse_signin_activity
| evaluate ipv4_lookup(allowed_networks, ClientIp, IPSubnet);
Stages 1 to 5 define let dataverse_signin_activity as the rule's main pipeline source. Stages 6 to 8 use it.
Stage 1: source
DataverseActivity
Stage 2: where
| where watchlist_entries_count > 0
Stage 3: where
| where TimeGenerated >= ago (query_frequency)
Stage 4: where
| where Message == "UserSignIn" and isnotempty(ClientIp)
Stage 5: summarize
| summarize FirstEvent = arg_min(TimeGenerated, *) by UserId, ClientIp, InstanceUrl
Stage 6: join (negated)
join kind=leftanti (authorized_ip_addresses) on ClientIp
Stage 7: extend
extend AccountName, CloudAppId, UPNSuffix
Stage 8: project
project AccountName, ClientIp, CloudAppId, FirstEvent, InstanceUrl, UPNSuffix, UserId
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ClientIp | is_not_null | field:"ClientIp" kind:is_not_null | |
Message | eq |
| field:"Message" kind:eq value:"UserSignIn" |
watchlist_entries_count | gt |
| field:"watchlist_entries_count" kind:gt value:"0" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
ClientIp | is_not_null | excludes:ClientIp | |
Message | eq | UserSignIn | excludes:Message field:"Message" value:"UserSignIn" |
watchlist_entries_count | gt | 0 | excludes:watchlist_entries_count field:"watchlist_entries_count" value:"0" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
ClientIp | project |
CloudAppId | project |
FirstEvent | project |
InstanceUrl | project |
UPNSuffix | project |
UserId | project |
Detect external user sending suspicious link to multiple users
#An external sender suddenly sending the same link to multiple internal users, can indicate that external user being compromised and used for BEC Attacks. In these kind of attacks compromised accounts are used to send phishing links or attachments to users in business relationships.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
References
Telemetry Coverage
Rule Body
// External user sending same link to multiple users via Teams
let threshold = 5;
MessageEvents
| where TimeGenerated > ago(1d)
// Focus on chat messsages
| where ThreadType == "chat"
// Only return external users sending messages
| join kind=leftanti (
IdentityInfo
| where TimeGenerated > ago(14d)
| distinct AccountObjectId
) on $left.SenderObjectId == $right.AccountObjectId
// Only flag messages with Teams Links
| join kind=inner MessageUrlInfo on TeamsMessageId
// Exclude teams file thumbnails
| where Url !~ "http://dummy.jpg/"
// Make a set of the chats a user sends a specific URL to
| summarize ChatSet = make_set(ThreadId) by SenderEmailAddress, Url
// Count the amount of chats
| extend ChatCount = array_length(ChatSet)
| where ChatCount > threshold
Stages and Predicates
Parameters
let threshold = 5;
Stage 1: source
MessageEvents
Stage 2: where
| where TimeGenerated > ago(1d)
Stage 3: where
| where ThreadType == "chat"
Stage 4: join (negated)
join kind=leftanti (IdentityInfo) on $left.SenderObjectId, $right.AccountObjectId
Stage 5: join
| join kind=inner MessageUrlInfo on TeamsMessageId
Stage 6: where
where Url !~ "http://dummy.jpg/"
Stage 7: summarize
summarize ChatSet by SenderEmailAddress, Url
Stage 8: extend
extend ChatCount
Stage 9: where
where ChatCount > 5
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ChatCount | gt |
| field:"ChatCount" kind:gt value:"5" |
ThreadType | eq |
| field:"ThreadType" kind:eq value:"chat" |
Url | ne |
| field:"Url" kind:ne value:"http://dummy.jpg/" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ChatSet | summarize |
SenderEmailAddress | summarize |
Url | summarize |
ChatCount | extend |
Detect potential presence of a malicious file with a double extension (ASIM Web Session)
#'Double extension vulnerability is a significant concern in file uploads, as it can lead to various issues if an attacker successfully uploads a virus-infected file.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Stealth | |
| Command & Control |
Rule Body
id: 6a71687f-00cf-44d3-93fc-8cbacc7b5615
name: Detect potential presence of a malicious file with a double extension (ASIM Web Session)
description: |
'Double extension vulnerability is a significant concern in file uploads, as it can lead to various issues if an attacker successfully uploads a virus-infected file.'
severity: Medium
status: Available
tags:
- Schema: WebSession
SchemaVersion: 0.2.6
requiredDataConnectors: []
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
- Persistence
- CommandAndControl
relevantTechniques:
- T1036
- T1505
- T1071
query: |
let common_file_ext_list = dynamic([".txt", ".xlsx", ".doc", ".docx", ".csv", ".pdf", ".png", ".jpg", ".jpeg"]); // Add list of common files as per your environment
_Im_WebSession (starttime=ago(1h), eventresult='Success')
| where HttpRequestMethod in~ ("POST", "PUT")
| project
Url,
SrcIpAddr,
SrcUsername,
SrcHostname,
DstIpAddr,
DstPortNumber,
DstHostname,
TimeGenerated
| extend requestedFileName=tostring(split(tostring(parse_url(Url)["Path"]), '/')[-1])
| extend FileWithdualextension = extract(@'([\w-]+\.\w+\.\w+)$', 1, requestedFileName, typeof(string))
| extend SecondExt = tostring(split(FileWithdualextension, '.')[-1])
| where strcat('.', SecondExt) in~ (common_file_ext_list) // Second extension is mostly from the common files
| summarize
EventCount=count(),
EventStartTime=min(TimeGenerated),
EventEndTime=max(TimeGenerated)
by
SrcIpAddr,
Url,
FileWithdualextension,
SrcUsername,
SrcHostname,
DstIpAddr,
DstPortNumber,
DstHostname
| extend Name = iif(SrcUsername contains "@", tostring(split(SrcUsername,'@',0)[0]),SrcUsername), UPNSuffix = iif(SrcUsername contains "@",tostring(split(SrcUsername,'@',1)[0]),"")
entityMappings:
- entityType: File
fieldMappings:
- identifier: Name
columnName: FileWithdualextension
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: SrcHostname
eventGroupingSettings:
aggregationKind: AlertPerResult
customDetails:
EventCount: EventCount
EventStartTime: EventStartTime
EventEndTime: EventEndTime
DstHostname: DstHostname
alertDetailsOverride:
alertDisplayNameFormat: "User '{{SrcUsername}}' with IP address '{{SrcIpAddr}}' has been observed with posting potentially risky dual extension file"
alertDescriptionFormat: "User posted file '{{FileWithdualextension}}' which potentially contain dual extensions. This type of activity could be malicious and performed to bypass file upload filters or security measures implemented by the application. Destination server name this request was targetted to - '{{DstHostname}}'"
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let common_file_ext_list = dynamic([".txt", ".xlsx", ".doc", ".docx", ".csv", ".pdf", ".png", ".jpg", ".jpeg"]);
Stage 1: source
_Im_WebSession (starttime=ago(1h), eventresult='Success')
Stage 2: where
| where HttpRequestMethod in~ ("POST", "PUT")
Stage 3: project
| project
Url,
SrcIpAddr,
SrcUsername,
SrcHostname,
DstIpAddr,
DstPortNumber,
DstHostname,
TimeGenerated
Stage 4: extend (3 consecutive steps)
| extend requestedFileName=tostring(split(tostring(parse_url(Url)["Path"]), '/')[-1])
| extend FileWithdualextension = extract(@'([\w-]+\.\w+\.\w+)$', 1, requestedFileName, typeof(string))
| extend SecondExt = tostring(split(FileWithdualextension, '.')[-1])
Stage 5: where
| where strcat('.', SecondExt) in~ (common_file_ext_list)
Stage 6: summarize
| summarize
EventCount=count(),
EventStartTime=min(TimeGenerated),
EventEndTime=max(TimeGenerated)
by
SrcIpAddr,
Url,
FileWithdualextension,
SrcUsername,
SrcHostname,
DstIpAddr,
DstPortNumber,
DstHostname
Stage 7: extend
| extend Name = iif(SrcUsername contains "@", tostring(split(SrcUsername,'@',0)[0]),SrcUsername), UPNSuffix = iif(SrcUsername contains "@",tostring(split(SrcUsername,'@',1)[0]),"")
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
HttpRequestMethod | in |
| field:"HttpRequestMethod" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DstHostname | summarize |
DstIpAddr | summarize |
DstPortNumber | summarize |
EventCount | summarize |
EventEndTime | summarize |
EventStartTime | summarize |
FileWithdualextension | summarize |
SrcHostname | summarize |
SrcIpAddr | summarize |
SrcUsername | summarize |
Url | summarize |
Name | extend |
UPNSuffix | extend |
Excessive number of failed connections from a single source (ASIM Network Session schema)
#This rule identifies a single source that generates an excessive amount of failed connections. Modify the threshold to change the sensitivity of the rule: the higher the threshold, the less sensitive is the rule and less incidents will be generated. This analytic rule uses ASIM and supports any built-in or custom source that supports the ASIM NetworkSession schema
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry Coverage
Rule Body
id: 4902eddb-34f7-44a8-ac94-8486366e9494
name: Excessive number of failed connections from a single source (ASIM Network Session schema)
description: |
'This rule identifies a single source that generates an excessive amount of failed connections. Modify the threshold to change the sensitivity of the rule: the higher the threshold, the less sensitive is the rule and less incidents will be generated.
This analytic rule uses [ASIM](https://aka.ms/AboutASIM) and supports any built-in or custom source that supports the ASIM NetworkSession schema'
severity: Medium
status: Available
tags:
- ParentAlert: https://github.com/Azure/Azure-Sentinel/tree/master/Solutions/Sophos%20XG%20Firewall/Analytic%20Rules/ExcessiveAmountofDeniedConnectionsfromASingleSource.yaml
version: 1.0.0
- Schema: ASimNetworkSessions
SchemaVersion: 0.2.4
requiredDataConnectors:
- connectorId: AWSS3
dataTypes:
- AWSVPCFlow
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceNetworkEvents
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsForwardedEvents
dataTypes:
- WindowsEvent
- connectorId: Zscaler
dataTypes:
- CommonSecurityLog
- connectorId: MicrosoftSysmonForLinux
dataTypes:
- Syslog
- connectorId: PaloAltoNetworks
dataTypes:
- CommonSecurityLog
- connectorId: AzureMonitor(VMInsights)
dataTypes:
- VMConnection
- connectorId: AzureFirewall
dataTypes:
- AzureDiagnostics
- connectorId: AzureNSG
dataTypes:
- AzureDiagnostics
- connectorId: CiscoASA
dataTypes:
- CommonSecurityLog
- connectorId: CiscoAsaAma
dataTypes:
- CommonSecurityLog
- connectorId: Corelight
dataTypes:
- Corelight_CL
- connectorId: AIVectraStream
dataTypes:
- VectraStream
- connectorId: CheckPoint
dataTypes:
- CommonSecurityLog
- connectorId: Fortinet
dataTypes:
- CommonSecurityLog
- connectorId: CiscoMeraki
dataTypes:
- Syslog
- CiscoMerakiNativePoller
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1499
query: |
let threshold = 5000;
_Im_NetworkSession(eventresult='Failure')
| summarize Count=count() by SrcIpAddr, bin(TimeGenerated,5m)
| where Count > threshold
| extend timestamp = TimeGenerated, threshold
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
customDetails:
NumberOfDenies: Count
alertDetailsOverride:
alertDisplayNameFormat: Excessive number of failed connections from {{SrcIpAddr}}
alertDescriptionFormat: 'The client at address {{SrcIpAddr}} generated more than {{threshold}} failures over a 5 minutes time window, which may indicate malicious activity.'
version: 1.2.7
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 5000;
Stage 1: source
_Im_NetworkSession(eventresult='Failure')
Stage 2: summarize
| summarize Count=count() by SrcIpAddr, bin(TimeGenerated,5m)
Stage 3: where
| where Count > threshold
Stage 4: extend
| extend timestamp = TimeGenerated, threshold
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Count | gt |
| field:"Count" kind:gt value:"5000" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Count | summarize |
SrcIpAddr | summarize |
threshold | extend |
timestamp | extend |
Google DNS - Multiple errors for source
#'Detects multiple errors for the same source IP address.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: 7e81a935-5e91-45a5-92fd-3b58c180513b
name: Google DNS - Multiple errors for source
description: |
'Detects multiple errors for the same source IP address.'
severity: Medium
requiredDataConnectors:
- connectorId: GCPDNSDataConnector
dataTypes:
- GCPCloudDNS
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1095
query: |
let threshold = 10;
let err = dynamic(['NXDOMAIN', 'SERVFAIL', 'REFUSED']);
GCPCloudDNS
| where EventResultDetails in~ (err)
| summarize count() by SrcIpAddr, bin(TimeGenerated, 10m)
| where count_ > threshold
| extend IPCustomEntity = SrcIpAddr
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 10;
let err = dynamic(['NXDOMAIN', 'SERVFAIL', 'REFUSED']);
Stage 1: source
GCPCloudDNS
Stage 2: where
| where EventResultDetails in~ (err)
Stage 3: summarize
| summarize count() by SrcIpAddr, bin(TimeGenerated, 10m)
Stage 4: where
| where count_ > threshold
Stage 5: extend
| extend IPCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventResultDetails | in |
| field:"EventResultDetails" kind:in |
count_ | gt |
| field:"count_" kind:gt value:"10" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SrcIpAddr | summarize |
IPCustomEntity | extend |
NordPass - Manual invitation, suspension, or deletion
#This will alert you when the user is manually invited, suspended, or deleted. !This rule should be enabled only by organizations that have User and Group Provisioning enabled.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Rule Body
id: 693c5217-e840-427f-9661-3fa0ef266040
name: NordPass - Manual invitation, suspension, or deletion
kind: Scheduled
version: 1.0.0
displayName: Manual invitation, suspension, or deletion
description: |
This will alert you when the user is manually invited, suspended, or deleted.
!This rule should be enabled only by organizations that have User and Group Provisioning enabled.
severity: Medium
query: |
NordPassEventLogs_CL
| extend metadataParsed = parse_json(metadata)
| where (event_type == "invites" and action == "user_invited") or (event_type == "access" and action == "user_status_updated" and (metadataParsed.status == "suspended" or metadataParsed.status == "deleted"))
| extend TargetEmail = user_email
requiredDataConnectors:
- connectorId: NordPass
dataTypes:
- NordPassEventLogs_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
relevantTechniques:
- T1098
incidentConfiguration:
createIncident: false
entityMappings:
- entityType: Mailbox
fieldMappings:
- identifier: MailboxPrimaryAddress
columnName: TargetEmail
Stages and Predicates
Stage 1: source
NordPassEventLogs_CL
Stage 2: extend
| extend metadataParsed = parse_json(metadata)
Stage 3: where
| where (event_type == "invites" and action == "user_invited") or (event_type == "access" and action == "user_status_updated" and (metadataParsed.status == "suspended" or metadataParsed.status == "deleted"))
Stage 4: extend
| extend TargetEmail = user_email
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq |
event_type | eq |
| field:"event_type" kind:eq |
metadataParsed.status | eq |
| field:"metadataParsed.status" kind:eq |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
metadataParsed | extend |
TargetEmail | extend |
OracleDBAudit - SQL injection patterns
#'Detects common known SQL injection patterns used in automated scripts.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: ab352f0d-7c55-4ab2-a22e-b1c2d995e193
name: OracleDBAudit - SQL injection patterns
description: |
'Detects common known SQL injection patterns used in automated scripts.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SyslogAma
datatypes:
- Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
query: |
OracleDatabaseAuditEvent
| where isnotempty(DstUserName)
| where Action has_any ("admin' --" ,"admin' #", "admin'/*", "0=1", "1=0", "1=1", "1=2", "' or 1=1--", "' or 1=1#", "' or 1=1/*", "') or '1'='1--", "') or ('1'='1--")
| project SrcIpAddr, DstUserName, Action
| extend AccountCustomEntity = DstUserName
| extend IPCustomEntity = SrcIpAddr
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountCustomEntity
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
OracleDatabaseAuditEvent
Stage 2: where
| where isnotempty(DstUserName)
Stage 3: where
| where Action has_any ("admin' --" ,"admin' #", "admin'/*", "0=1", "1=0", "1=1", "1=2", "' or 1=1--", "' or 1=1#", "' or 1=1/*", "') or '1'='1--", "') or ('1'='1--")
Stage 4: project
| project SrcIpAddr, DstUserName, Action
Stage 5: extend
| extend AccountCustomEntity = DstUserName
Stage 6: extend
| extend IPCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | match |
| field:"Action" kind:match |
DstUserName | is_not_null | field:"DstUserName" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Action | project |
DstUserName | project |
SrcIpAddr | project |
AccountCustomEntity | extend |
IPCustomEntity | extend |
Pathlock TDnR - SAP HTTP Webserver Events
#Detects security-relevant events from the SAP HTTP webserver logfile, forwarded by Pathlock Threat Detection and Response. Anomalous HTTP activity may indicate web-based attacks, unauthorized access to SAP web services, or command-and-control communication.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Command & Control |
Rule Body
id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c31
name: Pathlock TDnR - SAP HTTP Webserver Events
kind: Scheduled
description: >-
Detects security-relevant events from the SAP HTTP webserver logfile, forwarded by Pathlock Threat
Detection and Response. Anomalous HTTP activity may indicate web-based attacks, unauthorized
access to SAP web services, or command-and-control communication.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Pathlock_TDnR
dataTypes:
- Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- CommandAndControl
relevantTechniques:
- T1190
- T1071
query: |
Pathlock_TDnR_CL
| where DataSource == "HTTP_LOG"
| 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 == "HTTP_LOG"
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:"HTTP_LOG" |
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 |
PowerShell without powershell.exe
#This query detects the use of PowerShell through "system.management.automation.dll" which is invoked by a process with a low global prevalence (i.e., fairly unique binary).
Known False Positives
- Many legitimate programs will use the "system.management.automation.dll". This might lead to false-positives that require filtering.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution |
Telemetry Coverage
Rule Body
let timeframe = 2*1h;
let default_global_prevalence = 0;
let suspiciousProcs = materialize(
DeviceImageLoadEvents
| where FileName =~ "system.management.automation.dll" or FileName =~ "system.management.automation.ni.dll" and not(isempty(InitiatingProcessSHA1))
| summarize count() by InitiatingProcessSHA1
// FileProfile is case-sensitive and works on lower-case hashes.
| extend InitiatingProcessSHA1=tolower(InitiatingProcessSHA1)
| invoke FileProfile(InitiatingProcessSHA1, 1000)
| where not(ProfileAvailability =~ "Error")
| where coalesce(IsRootSignerMicrosoft, false) == false or coalesce(IsCertificateValid, false) == false
| where (IsCertificateValid and coalesce(GlobalPrevalence,default_global_prevalence) < 200) or (not(coalesce(IsCertificateValid, false)) and coalesce(GlobalPrevalence,default_global_prevalence) < 500));
DeviceImageLoadEvents
| where ingestion_time() >= ago(timeframe)
// FileProfile is case-sensitive and works on lower-case hashes.
| extend InitiatingProcessSHA1=tolower(InitiatingProcessSHA1)
| where InitiatingProcessSHA1 in~ ((suspiciousProcs | project InitiatingProcessSHA1)) and FileName startswith "System.Management.Automation"
| join kind=inner suspiciousProcs on InitiatingProcessSHA1
// Begin environment-specific filter.
// End environment-specific filter.
Stages and Predicates
Parameters
let timeframe = 2*1h;
let default_global_prevalence = 0;
Let Binding: suspiciousProcs
let suspiciousProcs = materialize(
DeviceImageLoadEvents
| where FileName =~ "system.management.automation.dll" or FileName =~ "system.management.automation.ni.dll" and not(isempty(InitiatingProcessSHA1))
| summarize count() by InitiatingProcessSHA1
| extend InitiatingProcessSHA1=tolower(InitiatingProcessSHA1)
| invoke FileProfile(InitiatingProcessSHA1, 1000)
| where not(ProfileAvailability =~ "Error")
| where coalesce(IsRootSignerMicrosoft, false) == false or coalesce(IsCertificateValid, false) == false
| where (IsCertificateValid and coalesce(GlobalPrevalence,default_global_prevalence) < 200) or (not(coalesce(IsCertificateValid, false)) and coalesce(GlobalPrevalence,default_global_prevalence) < 500));
Stage 1: source
let suspiciousProcs
Stage 2: source
DeviceImageLoadEvents
Stage 3: where
where ...
Stage 4: extend
extend InitiatingProcessSHA1
Stage 5: where
where FileName startswith "System.Management.Automation"
Stage 6: join
join kind=inner (suspiciousProcs) on InitiatingProcessSHA1
Stage 7: summarize aggregation inside the join branch
summarize by InitiatingProcessSHA1
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
FileName | eq |
| field:"FileName" kind:eq |
FileName | starts_with |
| field:"FileName" kind:starts_with value:"System.Management.Automation" |
GlobalPrevalence | lt |
| field:"GlobalPrevalence" kind:lt |
IsCertificateValid | eq |
| field:"IsCertificateValid" kind:eq |
IsRootSignerMicrosoft | eq |
| field:"IsRootSignerMicrosoft" kind:eq value:"false" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
ProfileAvailability | eq | Error | excludes:ProfileAvailability field:"ProfileAvailability" value:"Error" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
InitiatingProcessSHA1 | summarize |
SAP LogServ - HANA DB - Audit Trail Policy Changes
#Identifies changes for HANA DB audit trail policies. Source Action: Create / update existing audit policy in security definitions. Data Sources: SAP LogServ - HANA DB (Syslog)
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | No specific technique |
| Stealth | No specific technique |
| Lateral Movement | No specific technique |
Rule Body
id: e8394afb-82a7-4718-8d31-cc57ad352fa8
kind: Scheduled
name: SAP LogServ - HANA DB - Audit Trail Policy Changes
description: |
Identifies changes for HANA DB audit trail policies.
Source Action: Create / update existing audit policy in security definitions.
*Data Sources: SAP LogServ - HANA DB (Syslog)*
severity: High
status: Available
requiredDataConnectors:
- connectorId: SAPLogServ
dataTypes:
- Syslog
queryFrequency: 10m
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- LateralMovement
- DefenseEvasion
relevantTechniques: []
query: |
let AuditTimeAgo = 60m;
Syslog
| where TimeGenerated >= ago(AuditTimeAgo)
| where SourceSystem == "HANADB" and CollectorHostName == "SAPLogServ"
| extend raw_split = split(SyslogMessage, ";")
| extend
event_timestamp__col_0 = tostring(raw_split[0]),
service_name__col_1 = tostring(raw_split[1]),
hostname__col_2 = tostring(raw_split[2]),
sid__col_3 = tostring(raw_split[3]),
instance_number__col_4 = tostring(raw_split[4]),
port_number__col_5 = tostring(raw_split[5]),
database_name__col_6 = tostring(raw_split[6]),
client_ip_address__col_7 = tostring(raw_split[7]),
client_name__col_8 = tostring(raw_split[8]),
client_process_id__col_9 = tostring(raw_split[9]),
client_port_number__col_10 = tostring(raw_split[10]),
policy_name__col_11 = tostring(raw_split[11]),
audit_level__col_12 = tostring(raw_split[12]),
audit_action__col_13 = tostring(raw_split[13]),
session_user__col_14 = tostring(raw_split[14]),
target_schema__col_15 = tostring(raw_split[15]),
target_object__col_16 = tostring(raw_split[16]),
privilege_name__col_17 = tostring(raw_split[17]),
grantable__col_18 = tostring(raw_split[18]),
role_name__col_19 = tostring(raw_split[19]),
target_principal__col_20 = tostring(raw_split[20]),
action_status__col_21 = tostring(raw_split[21]),
component__col_22 = tostring(raw_split[22]),
section__col_23 = tostring(raw_split[23]),
parameter__col_24 = tostring(raw_split[24]),
old_value__col_25 = tostring(raw_split[25]),
new_value__col_26 = tostring(raw_split[26]),
comment__col_27 = tostring(raw_split[27]),
executed_statement__col_28 = tostring(raw_split[28]),
session_id__col_29 = tostring(raw_split[29]),
application_user_name__col_30 = tostring(raw_split[30]),
role_schema_name__col_31 = tostring(raw_split[31]),
grantee_schema_name__col_32 = tostring(raw_split[32]),
origin_database_name__col_33 = tostring(raw_split[33]),
origin_user_name__col_34 = tostring(raw_split[34]),
xs_application_user_name__col_35 = tostring(raw_split[35]),
application_name__col_36 = tostring(raw_split[36]),
statement_user_name__col_37 = tostring(raw_split[37]),
create_time__col_38 = tostring(raw_split[38]),
xsa_message_ip__col_39 = tostring(raw_split[39]),
xsa_tenant__col_40 = tostring(raw_split[40]),
xsa_uuid__col_41 = tostring(raw_split[41]),
xsa_channel__col_42 = tostring(raw_split[42]),
xsa_attachment_id__col_43 = tostring(raw_split[43]),
xsa_attachment_name__col_44 = tostring(raw_split[44]),
xsa_organization_id__col_45 = tostring(raw_split[45]),
xsa_space_id__col_46 = tostring(raw_split[46]),
xsa_instance_id__col_47 = tostring(raw_split[47]),
xsa_binding_id__col_48 = tostring(raw_split[48]),
xsa_object__col_49 = tostring(raw_split[49]),
xsa_data_subject__col_50 = tostring(raw_split[50])
| where audit_action__col_13 contains 'AUDIT POLICY'
| extend AlertRuleUniqueName = 'hanadb-audittrailpolicychanges-logserv'
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: CloudApplication
fieldMappings:
- identifier: AppId
columnName: sid__col_3
- identifier: InstanceName
columnName: database_name__col_6
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: hostname__col_2
- entityType: IP
fieldMappings:
- identifier: Address
columnName: client_ip_address__col_7
alertDetailsOverride:
alertDisplayNameFormat: 'SAP LogServ - HANA DB - Audit Trail Policy Change'
alertDescriptionFormat: |
{{comment__col_27}}
customDetails:
SAP_User: session_user__col_14
version: 1.0.1
Stages and Predicates
Parameters
let AuditTimeAgo = 60m;
Stage 1: source
Syslog
Stage 2: where
| where TimeGenerated >= ago(AuditTimeAgo)
Stage 3: where
| where SourceSystem == "HANADB" and CollectorHostName == "SAPLogServ"
Stage 4: extend
| extend raw_split = split(SyslogMessage, ";")
Stage 5: extend
| extend
event_timestamp__col_0 = tostring(raw_split[0]),
service_name__col_1 = tostring(raw_split[1]),
hostname__col_2 = tostring(raw_split[2]),
sid__col_3 = tostring(raw_split[3]),
instance_number__col_4 = tostring(raw_split[4]),
port_number__col_5 = tostring(raw_split[5]),
database_name__col_6 = tostring(raw_split[6]),
client_ip_address__col_7 = tostring(raw_split[7]),
client_name__col_8 = tostring(raw_split[8]),
client_process_id__col_9 = tostring(raw_split[9]),
client_port_number__col_10 = tostring(raw_split[10]),
policy_name__col_11 = tostring(raw_split[11]),
audit_level__col_12 = tostring(raw_split[12]),
audit_action__col_13 = tostring(raw_split[13]),
session_user__col_14 = tostring(raw_split[14]),
target_schema__col_15 = tostring(raw_split[15]),
target_object__col_16 = tostring(raw_split[16]),
privilege_name__col_17 = tostring(raw_split[17]),
grantable__col_18 = tostring(raw_split[18]),
role_name__col_19 = tostring(raw_split[19]),
target_principal__col_20 = tostring(raw_split[20]),
action_status__col_21 = tostring(raw_split[21]),
component__col_22 = tostring(raw_split[22]),
section__col_23 = tostring(raw_split[23]),
parameter__col_24 = tostring(raw_split[24]),
old_value__col_25 = tostring(raw_split[25]),
new_value__col_26 = tostring(raw_split[26]),
comment__col_27 = tostring(raw_split[27]),
executed_statement__col_28 = tostring(raw_split[28]),
session_id__col_29 = tostring(raw_split[29]),
application_user_name__col_30 = tostring(raw_split[30]),
role_schema_name__col_31 = tostring(raw_split[31]),
grantee_schema_name__col_32 = tostring(raw_split[32]),
origin_database_name__col_33 = tostring(raw_split[33]),
origin_user_name__col_34 = tostring(raw_split[34]),
xs_application_user_name__col_35 = tostring(raw_split[35]),
application_name__col_36 = tostring(raw_split[36]),
statement_user_name__col_37 = tostring(raw_split[37]),
create_time__col_38 = tostring(raw_split[38]),
xsa_message_ip__col_39 = tostring(raw_split[39]),
xsa_tenant__col_40 = tostring(raw_split[40]),
xsa_uuid__col_41 = tostring(raw_split[41]),
xsa_channel__col_42 = tostring(raw_split[42]),
xsa_attachment_id__col_43 = tostring(raw_split[43]),
xsa_attachment_name__col_44 = tostring(raw_split[44]),
xsa_organization_id__col_45 = tostring(raw_split[45]),
xsa_space_id__col_46 = tostring(raw_split[46]),
xsa_instance_id__col_47 = tostring(raw_split[47]),
xsa_binding_id__col_48 = tostring(raw_split[48]),
xsa_object__col_49 = tostring(raw_split[49]),
xsa_data_subject__col_50 = tostring(raw_split[50])
Stage 6: where
| where audit_action__col_13 contains 'AUDIT POLICY'
Stage 7: extend
| extend AlertRuleUniqueName = 'hanadb-audittrailpolicychanges-logserv'
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
CollectorHostName | eq |
| field:"CollectorHostName" kind:eq value:"SAPLogServ" |
SourceSystem | eq |
| field:"SourceSystem" kind:eq value:"HANADB" |
audit_action__col_13 | contains |
| field:"audit_action__col_13" kind:contains value:"AUDIT POLICY" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
raw_split | extend |
action_status__col_21 | extend |
application_name__col_36 | extend |
application_user_name__col_30 | extend |
audit_action__col_13 | extend |
audit_level__col_12 | extend |
client_ip_address__col_7 | extend |
client_name__col_8 | extend |
client_port_number__col_10 | extend |
client_process_id__col_9 | extend |
comment__col_27 | extend |
component__col_22 | extend |
create_time__col_38 | extend |
database_name__col_6 | extend |
event_timestamp__col_0 | extend |
executed_statement__col_28 | extend |
grantable__col_18 | extend |
grantee_schema_name__col_32 | extend |
hostname__col_2 | extend |
instance_number__col_4 | extend |
new_value__col_26 | extend |
old_value__col_25 | extend |
origin_database_name__col_33 | extend |
origin_user_name__col_34 | extend |
parameter__col_24 | extend |
policy_name__col_11 | extend |
port_number__col_5 | extend |
privilege_name__col_17 | extend |
role_name__col_19 | extend |
role_schema_name__col_31 | extend |
section__col_23 | extend |
service_name__col_1 | extend |
session_id__col_29 | extend |
session_user__col_14 | extend |
sid__col_3 | extend |
statement_user_name__col_37 | extend |
target_object__col_16 | extend |
target_principal__col_20 | extend |
target_schema__col_15 | extend |
xs_application_user_name__col_35 | extend |
xsa_attachment_id__col_43 | extend |
xsa_attachment_name__col_44 | extend |
xsa_binding_id__col_48 | extend |
xsa_channel__col_42 | extend |
xsa_data_subject__col_50 | extend |
xsa_instance_id__col_47 | extend |
xsa_message_ip__col_39 | extend |
xsa_object__col_49 | extend |
xsa_organization_id__col_45 | extend |
xsa_space_id__col_46 | extend |
xsa_tenant__col_40 | extend |
xsa_uuid__col_41 | extend |
AlertRuleUniqueName | extend |
Successful logon from IP and failure from a different IP
#Identifies when a user account successfully logs onto an Azure App from one IP and within 10 mins failed to logon to the same App via a different IP (may indicate a malicious attempt at password guessing with known account). UEBA added for context to gather all asoociated information assocaited with IP addressed initiating Faile Logon and affected user. Please note, Failed logons from known IP ranges can be benign depending on the conditional access policies. In case of noisy behavior, consider tuning the source IP ranges after careful consideration
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access |
Telemetry Coverage
Rule Body
id: 02ef8d7e-fc3a-4d86-a457-650fa571d8d2
name: Successful logon from IP and failure from a different IP
description: |
'Identifies when a user account successfully logs onto an Azure App from one IP and within 10 mins failed to logon to the same App via a different IP (may indicate a malicious attempt at password guessing with known account).
UEBA added for context to gather all asoociated information assocaited with IP addressed initiating Faile Logon and affected user.
Please note, Failed logons from known IP ranges can be benign depending on the conditional access policies. In case of noisy behavior, consider tuning the source IP ranges after careful consideration'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
- connectorId: AzureActiveDirectory
dataTypes:
- AADNonInteractiveUserSignInLogs
- connectorId: BehaviorAnalytics
dataTypes:
- BehaviorAnalytics
- connectorId: BehaviorAnalytics
dataTypes:
- IdentityInfo
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- CredentialAccess
- InitialAccess
relevantTechniques:
- T1110
- T1078
query: |
let riskScoreCutoff = 3; //Adjust this score threshold based on volume of results. Activities identified as the most abnormal receive the highest scores (on a scale of 0-10)
let logonDiff = 10m;
let aadFunc = (tableName:string)
{
table(tableName)
| where ResultType == "0"
| where AppDisplayName !in ("Office 365 Exchange Online", "Skype for Business Online") // To remove false-positives, add more Apps to this array
// ---------- Fix for SuccessBlock to also consider IPv6
| extend SuccessIPv6Block = strcat(split(IPAddress, ":")[0], ":", split(IPAddress, ":")[1], ":", split(IPAddress, ":")[2], ":", split(IPAddress, ":")[3])
| extend SuccessIPv4Block = strcat(split(IPAddress, ".")[0], ".", split(IPAddress, ".")[1])
// ------------------
| project SuccessLogonTime = TimeGenerated, UserPrincipalName, SuccessIPAddress = IPAddress, SuccessLocation = Location, AppDisplayName, SuccessIPBlock = iff(IPAddress contains ":", strcat(split(IPAddress, ":")[0], ":", split(IPAddress, ":")[1]), strcat(split(IPAddress, ".")[0], ".", split(IPAddress, ".")[1])), Type
| join kind= inner (
table(tableName)
| where ResultType !in ("0", "50140")
| where ResultDescription !~ "Other"
| where AppDisplayName !in ("Office 365 Exchange Online", "Skype for Business Online")
| project FailedLogonTime = TimeGenerated, UserPrincipalName, FailedIPAddress = IPAddress, FailedLocation = Location, AppDisplayName, ResultType, ResultDescription, Type
) on UserPrincipalName, AppDisplayName
| where SuccessLogonTime < FailedLogonTime and FailedLogonTime - SuccessLogonTime <= logonDiff and FailedIPAddress !startswith SuccessIPBlock
| summarize FailedLogonTime = max(FailedLogonTime), SuccessLogonTime = max(SuccessLogonTime) by UserPrincipalName, SuccessIPAddress, SuccessLocation, AppDisplayName, FailedIPAddress, FailedLocation, ResultType, ResultDescription, Type
| extend timestamp = SuccessLogonTime
| extend UserPrincipalName = tolower(UserPrincipalName)};
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
union isfuzzy=true aadSignin, aadNonInt
| extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])
// UEBA context below - make sure you have these 2 datatypes, otherwise the query will not work. If so, comment all that is below.
| join kind=leftouter (
IdentityInfo
| summarize LatestReportTime = arg_max(TimeGenerated, *) by AccountUPN
| project AccountUPN, Tags, JobTitle, GroupMembership, AssignedRoles, UserType, IsAccountEnabled
| summarize
Tags = make_set(Tags, 1000),
GroupMembership = make_set(GroupMembership, 1000),
AssignedRoles = make_set(AssignedRoles, 1000),
UserType = make_set(UserType, 1000),
UserAccountControl = make_set(UserType, 1000)
by AccountUPN
| extend UserPrincipalName=tolower(AccountUPN)
) on UserPrincipalName
//Below it will be joined with BehaviorAnalytics table to the Failed IP Addresses
| join kind=leftouter (
BehaviorAnalytics
| where ActivityType in ("FailedLogOn", "LogOn")
| where isnotempty(SourceIPAddress)
| project UsersInsights, DevicesInsights, ActivityInsights, InvestigationPriority, SourceIPAddress, UserName
| project-rename FailedIPAddress = SourceIPAddress, Name = UserName
| summarize
MaxInvestigationScore = max(InvestigationPriority) // Only retrieve maximum Investigation Property score for both FailedIP and User
by FailedIPAddress, Name)
on FailedIPAddress, Name // Joining on both IP and User so as to only return context associated with same user
| extend UEBARiskScore = MaxInvestigationScore
| project-away *1 // removing duplicate columns post outer join from output
| where UEBARiskScore > riskScoreCutoff
| sort by UEBARiskScore desc
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserPrincipalName
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SuccessIPAddress
- entityType: IP
fieldMappings:
- identifier: Address
columnName: FailedIPAddress
version: 2.1.11
kind: Scheduled
Stages and Predicates
Parameters
let riskScoreCutoff = 3;
let logonDiff = 10m;
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
Let Binding: aadFunc
let aadFunc = (tableName:string)
{
table(tableName)
| where ResultType == "0"
| where AppDisplayName !in ("Office 365 Exchange Online", "Skype for Business Online")
| extend SuccessIPv6Block = strcat(split(IPAddress, ":")[0], ":", split(IPAddress, ":")[1], ":", split(IPAddress, ":")[2], ":", split(IPAddress, ":")[3])
| extend SuccessIPv4Block = strcat(split(IPAddress, ".")[0], ".", split(IPAddress, ".")[1])
| project SuccessLogonTime = TimeGenerated, UserPrincipalName, SuccessIPAddress = IPAddress, SuccessLocation = Location, AppDisplayName, SuccessIPBlock = iff(IPAddress contains ":", strcat(split(IPAddress, ":")[0], ":", split(IPAddress, ":")[1]), strcat(split(IPAddress, ".")[0], ".", split(IPAddress, ".")[1])), Type
| join kind= inner (
table(tableName)
| where ResultType !in ("0", "50140")
| where ResultDescription !~ "Other"
| where AppDisplayName !in ("Office 365 Exchange Online", "Skype for Business Online")
| project FailedLogonTime = TimeGenerated, UserPrincipalName, FailedIPAddress = IPAddress, FailedLocation = Location, AppDisplayName, ResultType, ResultDescription, Type
) on UserPrincipalName, AppDisplayName
| where SuccessLogonTime < FailedLogonTime and FailedLogonTime - SuccessLogonTime <= logonDiff and FailedIPAddress !startswith SuccessIPBlock
| summarize FailedLogonTime = max(FailedLogonTime), SuccessLogonTime = max(SuccessLogonTime) by UserPrincipalName, SuccessIPAddress, SuccessLocation, AppDisplayName, FailedIPAddress, FailedLocation, ResultType, ResultDescription, Type
| extend timestamp = SuccessLogonTime
| extend UserPrincipalName = tolower(UserPrincipalName)};
Union isfuzzy=true (2 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: aadSignin, aadNonInt
Leg 1: aadSignin
Leg 2: aadNonInt
Applied to the Combined Result
| extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0]) | join kind=leftouter (
IdentityInfo
| summarize LatestReportTime = arg_max(TimeGenerated, *) by AccountUPN
| project AccountUPN, Tags, JobTitle, GroupMembership, AssignedRoles, UserType, IsAccountEnabled
| summarize
Tags = make_set(Tags, 1000),
GroupMembership = make_set(GroupMembership, 1000),
AssignedRoles = make_set(AssignedRoles, 1000),
UserType = make_set(UserType, 1000),
UserAccountControl = make_set(UserType, 1000)
by AccountUPN
| extend UserPrincipalName=tolower(AccountUPN)
) on UserPrincipalName | join kind=leftouter (
BehaviorAnalytics
| where ActivityType in ("FailedLogOn", "LogOn")
| where isnotempty(SourceIPAddress)
| project UsersInsights, DevicesInsights, ActivityInsights, InvestigationPriority, SourceIPAddress, UserName
| project-rename FailedIPAddress = SourceIPAddress, Name = UserName
| summarize
MaxInvestigationScore = max(InvestigationPriority)
by FailedIPAddress, Name)
on FailedIPAddress, Name | extend UEBARiskScore = MaxInvestigationScore | project-away *1 | where UEBARiskScore > riskScoreCutoff | sort by UEBARiskScore desc
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActivityType | in |
| field:"ActivityType" kind:in |
SourceIPAddress | is_not_null | field:"SourceIPAddress" kind:is_not_null | |
UEBARiskScore | gt |
| field:"UEBARiskScore" kind:gt value:"3" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
FailedIPAddress | summarize |
Name | summarize |
Suspicious application consent for offline access
#This will alert when a user consents to provide a previously-unknown Azure application with offline access via OAuth. Offline access will provide the Azure App with access to the listed resources without requiring two-factor authentication. Consent to applications with offline access and read capabilities should be rare, especially as the knownApplications list is expanded. Public contributions to expand this filter are welcome! For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry Coverage
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: 3533f74c-9207-4047-96e2-0eb9383be587
name: Suspicious application consent for offline access
description: |
'This will alert when a user consents to provide a previously-unknown Azure application with offline access via OAuth.
Offline access will provide the Azure App with access to the listed resources without requiring two-factor authentication.
Consent to applications with offline access and read capabilities should be rare, especially as the knownApplications list is expanded. Public contributions to expand this filter are welcome!
For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.'
severity: Low
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- CredentialAccess
relevantTechniques:
- T1528
query: |
let detectionTime = 1d;
let joinLookback = 14d;
AuditLogs
| where TimeGenerated > ago(detectionTime)
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName =~ "Consent to application"
| where TargetResources has "offline"
| mv-apply TargetResource=TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend ModifiedProperties = TargetResource.modifiedProperties,
AppDisplayName = tostring(TargetResource.displayName),
AppClientId = tolower(tostring(TargetResource.id))
)
| where AppClientId !in ((externaldata(knownAppClientId:string, knownAppDisplayName:string)[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/Microsoft.OAuth.KnownApplications.csv"] with (format="csv")))
| mv-apply Properties=ModifiedProperties on
(
where Properties.displayName =~ "ConsentAction.Permissions"
| extend ConsentFull = tostring(Properties.newValue)
| extend ConsentFull = trim(@'"',tostring(ConsentFull))
)
| parse ConsentFull with * "ConsentType: " GrantConsentType ", Scope: " GrantScope1 "]" *
| where ConsentFull has "offline_access" and ConsentFull has_any ("Files.Read", "Mail.Read", "Notes.Read", "ChannelMessage.Read", "Chat.Read", "TeamsActivity.Read", "Group.Read", "EWS.AccessAsUser.All", "EAS.AccessAsUser.All")
| where GrantConsentType != "AllPrincipals" // NOTE: we are ignoring if OAuth application was granted to all users via an admin - but admin due diligence should be audited occasionally
| extend GrantInitiatedByAppName = tostring(InitiatedBy.app.displayName)
| extend GrantInitiatedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend GrantInitiatedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend GrantInitiatedByAadUserId = tostring(InitiatedBy.user.id)
| extend GrantIpAddress = iff(isnotempty(InitiatedBy.user.ipAddress), tostring(InitiatedBy.user.ipAddress), tostring(InitiatedBy.app.ipAddress))
| extend GrantInitiatedBy = iff(isnotempty(GrantInitiatedByUserPrincipalName), GrantInitiatedByUserPrincipalName, GrantInitiatedByAppName)
| extend GrantUserAgent = tostring(iff(AdditionalDetails[0].key =~ "User-Agent", AdditionalDetails[0].value, ""))
| project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, OperationName, ConsentFull, CorrelationId
| join kind = leftouter (AuditLogs
| where TimeGenerated > ago(joinLookback)
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName =~ "Add service principal"
| mv-apply TargetResource=TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend ModifiedProperties = TargetResource.modifiedProperties,
AppClientId = tolower(TargetResource.id)
)
| mv-apply ModifiedProperties=TargetResource.modifiedProperties on
(
where ModifiedProperties.displayName =~ "AppAddress" and ModifiedProperties.newValue has "AddressType"
| extend AppReplyURLs = ModifiedProperties.newValue
)
| distinct AppClientId, tostring(AppReplyURLs)
)
on AppClientId
| join kind = innerunique (AuditLogs
| where TimeGenerated > ago(joinLookback)
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName =~ "Add OAuth2PermissionGrant" or OperationName =~ "Add delegated permission grant"
| mv-apply TargetResource=TargetResources on
(
where TargetResource.type =~ "ServicePrincipal" and array_length(TargetResource.modifiedProperties) > 0 and isnotnull(TargetResource.displayName)
| extend GrantAuthentication = tostring(TargetResource.displayName)
)
| extend GrantOperation = OperationName
| project GrantAuthentication, GrantOperation, CorrelationId
) on CorrelationId
| project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, AppReplyURLs, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, GrantAuthentication, OperationName, GrantOperation, CorrelationId, ConsentFull
| extend Name = tostring(split(GrantInitiatedByUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(GrantInitiatedByUserPrincipalName,'@',1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: GrantInitiatedByUserPrincipalName
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: GrantInitiatedByAadUserId
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: GrantInitiatedByAppServicePrincipalId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: GrantIpAddress
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let detectionTime = 1d;
let joinLookback = 14d;
Stage 1: source
AuditLogs
Stage 2: where
| where TimeGenerated > ago(detectionTime)
Stage 3: where
| where LoggedByService =~ "Core Directory"
Stage 4: where
| where Category =~ "ApplicationManagement"
Stage 5: where
| where OperationName =~ "Consent to application"
Stage 6: where
| where TargetResources has "offline"
Stage 7: kusto:mv-apply
| mv-apply TargetResource=TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend ModifiedProperties = TargetResource.modifiedProperties,
AppDisplayName = tostring(TargetResource.displayName),
AppClientId = tolower(tostring(TargetResource.id))
)
Stage 8: where
| where AppClientId !in ((externaldata(knownAppClientId:string, knownAppDisplayName:string)[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/Microsoft.OAuth.KnownApplications.csv"] with (format="csv")))
Stage 9: kusto:mv-apply
| mv-apply Properties=ModifiedProperties on
(
where Properties.displayName =~ "ConsentAction.Permissions"
| extend ConsentFull = tostring(Properties.newValue)
| extend ConsentFull = trim(@'"',tostring(ConsentFull))
)
Stage 10: parse
| parse ConsentFull with * "ConsentType: " GrantConsentType ", Scope: " GrantScope1 "]" *
Stage 11: where
| where ConsentFull has "offline_access" and ConsentFull has_any ("Files.Read", "Mail.Read", "Notes.Read", "ChannelMessage.Read", "Chat.Read", "TeamsActivity.Read", "Group.Read", "EWS.AccessAsUser.All", "EAS.AccessAsUser.All")
Stage 12: where
| where GrantConsentType != "AllPrincipals"
Stage 13: extend (7 consecutive steps)
| extend GrantInitiatedByAppName = tostring(InitiatedBy.app.displayName)
| extend GrantInitiatedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend GrantInitiatedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend GrantInitiatedByAadUserId = tostring(InitiatedBy.user.id)
| extend GrantIpAddress = iff(isnotempty(InitiatedBy.user.ipAddress), tostring(InitiatedBy.user.ipAddress), tostring(InitiatedBy.app.ipAddress))
| extend GrantInitiatedBy = iff(isnotempty(GrantInitiatedByUserPrincipalName), GrantInitiatedByUserPrincipalName, GrantInitiatedByAppName)
| extend GrantUserAgent = tostring(iff(AdditionalDetails[0].key =~ "User-Agent", AdditionalDetails[0].value, ""))
Stage 14: project
| project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, OperationName, ConsentFull, CorrelationId
Stage 15: join
| join kind = leftouter (AuditLogs
| where TimeGenerated > ago(joinLookback)
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName =~ "Add service principal"
| mv-apply TargetResource=TargetResources on
(
where TargetResource.type =~ "ServicePrincipal"
| extend ModifiedProperties = TargetResource.modifiedProperties,
AppClientId = tolower(TargetResource.id)
)
| mv-apply ModifiedProperties=TargetResource.modifiedProperties on
(
where ModifiedProperties.displayName =~ "AppAddress" and ModifiedProperties.newValue has "AddressType"
| extend AppReplyURLs = ModifiedProperties.newValue
)
| distinct AppClientId, tostring(AppReplyURLs)
)
on AppClientId
Stage 16: join
| join kind = innerunique (AuditLogs
| where TimeGenerated > ago(joinLookback)
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName =~ "Add OAuth2PermissionGrant" or OperationName =~ "Add delegated permission grant"
| mv-apply TargetResource=TargetResources on
(
where TargetResource.type =~ "ServicePrincipal" and array_length(TargetResource.modifiedProperties) > 0 and isnotnull(TargetResource.displayName)
| extend GrantAuthentication = tostring(TargetResource.displayName)
)
| extend GrantOperation = OperationName
| project GrantAuthentication, GrantOperation, CorrelationId
) on CorrelationId
Stage 17: project
project AppClientId, AppDisplayName, AppReplyURLs, ConsentFull, CorrelationId, GrantAuthentication, GrantConsentType, GrantInitiatedBy, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantInitiatedByUserPrincipalName, GrantIpAddress, GrantOperation, GrantScope1, GrantUserAgent, OperationName, TimeGenerated
Stage 18: extend
extend Name, UPNSuffix
Indicators
These rows show field, operator, and value matches.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AppClientId | project |
AppDisplayName | project |
AppReplyURLs | project |
ConsentFull | project |
CorrelationId | project |
GrantAuthentication | project |
GrantConsentType | project |
GrantInitiatedBy | project |
GrantInitiatedByAadUserId | project |
GrantInitiatedByAppName | project |
GrantInitiatedByAppServicePrincipalId | project |
GrantInitiatedByUserPrincipalName | project |
GrantIpAddress | project |
GrantOperation | project |
GrantScope1 | project |
GrantUserAgent | project |
OperationName | project |
TimeGenerated | project |
Name | extend |
UPNSuffix | extend |
TIE Indicators of Attack
#'Searches for triggered Indicators of Attack.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: 6c75f0d2-2973-4188-bb05-ec7bc8696120
name: TIE Indicators of Attack
description: |
'Searches for triggered Indicators of Attack.'
severity: Low
requiredDataConnectors:
- connectorId: TenableIE
dataTypes:
- Tenable_IE_CL
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1110
query: |
// For the query to work properly, make sure you have imported the afad_parser.yaml parser into the workspace
// Retrieve the parser here: https://aka.ms/sentinel-TenableApp-afad-parser
// Then, create the Kusto Function with alias afad_parser
let SeverityTable=datatable(Severity:string,Level:int) [
"low", 1,
"medium", 2,
"high", 3,
"critical", 4
];
afad_parser
| where MessageType == 2
| lookup kind=leftouter SeverityTable on Severity
| order by Level
| extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
version: 1.0.1
kind: Scheduled
Stages and Predicates
Let Binding: SeverityTable
let SeverityTable = datatable(Severity:string,Level:int) [
"low", 1,
"medium", 2,
"high", 3,
"critical", 4
];
Stage 1: source
afad_parser
Stage 2: where
| where MessageType == 2
Stage 3: kusto:lookup
lookup kind=leftouter (SeverityTable) on Severity
Stage 4: sort
sort by Level
Stage 5: extend
extend DnsDomain, HostName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
MessageType | eq |
| field:"MessageType" kind:eq value:"2" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DnsDomain | extend |
HostName | extend |
Time series anomaly detection for total volume of traffic
#'Identifies anamalous spikes in network traffic logs as compared to baseline or normal historical patterns. The query leverages a KQL built-in anomaly detection algorithm to find large deviations from baseline patterns. Sudden increases in network traffic volume may be an indication of data exfiltration attempts and should be investigated. The higher the score, the further it is from the baseline value. The output is aggregated to provide summary view of unique source IP to destination IP address and port traffic observed in the flagged anomaly hour. The source IP addresses which were sending less than percentotalthreshold of the total traffic have been exluded whose value can be adjusted as needed . You may have to run queries for individual source IP addresses from SourceIPlist to determine if anything looks suspicious'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Rule Body
id: 06a9b845-6a95-4432-a78b-83919b28c375
name: Time series anomaly detection for total volume of traffic
description: |
'Identifies anamalous spikes in network traffic logs as compared to baseline or normal historical patterns.
The query leverages a KQL built-in anomaly detection algorithm to find large deviations from baseline patterns.
Sudden increases in network traffic volume may be an indication of data exfiltration attempts and should be investigated.
The higher the score, the further it is from the baseline value.
The output is aggregated to provide summary view of unique source IP to destination IP address and port traffic observed in the flagged anomaly hour.
The source IP addresses which were sending less than percentotalthreshold of the total traffic have been exluded whose value can be adjusted as needed .
You may have to run queries for individual source IP addresses from SourceIPlist to determine if anything looks suspicious'
severity: Medium
requiredDataConnectors:
- connectorId: Barracuda
dataTypes:
- CommonSecurityLog
- connectorId: CEF
dataTypes:
- CommonSecurityLog
- connectorId: CheckPoint
dataTypes:
- CommonSecurityLog
- connectorId: CiscoASA
dataTypes:
- CommonSecurityLog
- connectorId: F5
dataTypes:
- CommonSecurityLog
- connectorId: Fortinet
dataTypes:
- CommonSecurityLog
- connectorId: PaloAltoNetworks
dataTypes:
- CommonSecurityLog
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 3
tactics:
- Exfiltration
relevantTechniques:
- T1030
query: |
let starttime = 14d;
let endtime = 1d;
let timeframe = 1h;
let scorethreshold = 5;
let percentotalthreshold = 50;
let TimeSeriesData = CommonSecurityLog
| where isnotempty(DestinationIP) and isnotempty(SourceIP)
| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
| project TimeGenerated,SourceIP, DestinationIP, DeviceVendor
| make-series Total=count() on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by DeviceVendor;
// Filtering specific records associated with spikes as outliers
let TimeSeriesAlerts=materialize(TimeSeriesData
| extend (anomalies, score, baseline) = series_decompose_anomalies(Total, scorethreshold, -1, 'linefit')
| mv-expand Total to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double),score to typeof(double), baseline to typeof(long)
| where anomalies > 0 | extend score = round(score,2), AnomalyHour = TimeGenerated
| project DeviceVendor,AnomalyHour, TimeGenerated, Total, baseline, anomalies, score);
let AnomalyHours = materialize(TimeSeriesAlerts | where TimeGenerated > ago(2d) | project TimeGenerated);
// Join anomalies with Base Data to popalate associated records for investigation - Results sorted by score in descending order
TimeSeriesAlerts
| where TimeGenerated > ago(2d)
| join (
CommonSecurityLog
| where isnotempty(DestinationIP) and isnotempty(SourceIP)
| where TimeGenerated > ago(2d)
| extend DateHour = bin(TimeGenerated, 1h) // create a new column and round to hour
| where DateHour in ((AnomalyHours)) //filter the dataset to only selected anomaly hours
| summarize HourlyCount = count(), TimeGeneratedMax = arg_max(TimeGenerated, *), DestinationIPlist = make_set(DestinationIP, 100), DestinationPortlist = make_set(DestinationPort, 100) by DeviceVendor, SourceIP, TimeGeneratedHour= bin(TimeGenerated, 1h)
| extend AnomalyHour = TimeGeneratedHour
) on AnomalyHour, DeviceVendor
| extend PercentTotal = round((HourlyCount / Total) * 100, 3)
| where PercentTotal > percentotalthreshold
| project DeviceVendor , AnomalyHour, TimeGeneratedMax, SourceIP, DestinationIPlist, DestinationPortlist, HourlyCount, PercentTotal, Total, baseline, score, anomalies
| summarize HourlyCount=sum(HourlyCount), StartTimeUtc=min(TimeGeneratedMax), EndTimeUtc=max(TimeGeneratedMax), SourceIPlist = make_set(SourceIP, 100), SourceIPMax= arg_max(SourceIP, *), DestinationIPlist = make_set(DestinationIPlist, 100), DestinationPortlist = make_set(DestinationPortlist, 100) by DeviceVendor , AnomalyHour, Total, baseline, score, anomalies
| project DeviceVendor , AnomalyHour, EndTimeUtc, SourceIPMax ,SourceIPlist, DestinationIPlist, DestinationPortlist, HourlyCount, Total, baseline, score, anomalies
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIPMax
version: 1.0.4
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others" ]
Stages and Predicates
Parameters
let starttime = 14d;
let endtime = 1d;
let timeframe = 1h;
let scorethreshold = 5;
let percentotalthreshold = 50;
let TimeSeriesData and let TimeSeriesAlerts are inlined into the numbered stages below.
Let Binding: AnomalyHours
let AnomalyHours = materialize(TimeSeriesAlerts | where TimeGenerated > ago(2d) | project TimeGenerated);
Stages 1 to 10 define let TimeSeriesAlerts as the rule's main pipeline source. Stages 11 to 17 use it.
Stage 1: source
CommonSecurityLog
Stage 2: where
| where isnotempty(DestinationIP) and isnotempty(SourceIP)
Stage 3: where
| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
Stage 4: project
| project TimeGenerated,SourceIP, DestinationIP, DeviceVendor
The stages below score time-series anomalies (make-series, series_decompose_anomalies).
Stage 5: make-series
| make-series Total=count() on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by DeviceVendor
Stage 6: extend
| extend (anomalies, score, baseline) = series_decompose_anomalies(Total, scorethreshold, -1, 'linefit')
Stage 7: mv-expand
| mv-expand Total to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double),score to typeof(double), baseline to typeof(long)
Stage 8: where
| where anomalies > 0
Stage 9: extend
| extend score = round(score,2), AnomalyHour = TimeGenerated
Stage 10: project
| project DeviceVendor,AnomalyHour, TimeGenerated, Total, baseline, anomalies, score
Stage 11: where
TimeSeriesAlerts
| where TimeGenerated > ago(2d)
Stage 12: join
| join (
CommonSecurityLog
| where isnotempty(DestinationIP) and isnotempty(SourceIP)
| where TimeGenerated > ago(2d)
| extend DateHour = bin(TimeGenerated, 1h)
| where DateHour in ((AnomalyHours))
| summarize HourlyCount = count(), TimeGeneratedMax = arg_max(TimeGenerated, *), DestinationIPlist = make_set(DestinationIP, 100), DestinationPortlist = make_set(DestinationPort, 100) by DeviceVendor, SourceIP, TimeGeneratedHour= bin(TimeGenerated, 1h)
| extend AnomalyHour = TimeGeneratedHour
) on AnomalyHour, DeviceVendor
Stage 13: extend
extend PercentTotal
Stage 14: where
where PercentTotal > 50
Stage 15: project
project AnomalyHour, DestinationIPlist, DestinationPortlist, DeviceVendor, HourlyCount, PercentTotal, SourceIP, TimeGeneratedMax, Total, anomalies, baseline, score
Stage 16: summarize
summarize DestinationIPlist, DestinationPortlist, EndTimeUtc, HourlyCount, SourceIPMax, SourceIPlist, StartTimeUtc by DeviceVendor, AnomalyHour, Total, baseline, score, anomalies
Stage 17: project
project AnomalyHour, DestinationIPlist, DestinationPortlist, DeviceVendor, EndTimeUtc, HourlyCount, SourceIPMax, SourceIPlist, Total, anomalies, baseline, score
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DestinationIP | is_not_null | field:"DestinationIP" kind:is_not_null | |
PercentTotal | gt |
| field:"PercentTotal" kind:gt value:"50" |
SourceIP | is_not_null | field:"SourceIP" kind:is_not_null | |
anomalies | gt |
| field:"anomalies" kind:gt value:"0" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AnomalyHour | project |
DestinationIPlist | project |
DestinationPortlist | project |
DeviceVendor | project |
EndTimeUtc | project |
HourlyCount | project |
SourceIPMax | project |
SourceIPlist | project |
Total | project |
anomalies | project |
baseline | project |
score | project |
Zoom E2E Encryption Disabled
#'This alerts when end to end encryption is disabled for Zoom meetings.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access | |
| Discovery |
Rule Body
id: e4779bdc-397a-4b71-be28-59e6a1e1d16b
name: Zoom E2E Encryption Disabled
description: |
'This alerts when end to end encryption is disabled for Zoom meetings.'
severity: Medium
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
- Discovery
relevantTechniques:
- T1040
query: |
ZoomLogs
| where Event =~ "account.settings_updated"
| extend NewE2ESetting = columnifexists("payload_object_settings_in_meeting_e2e_encryption_b", "")
| extend OldE2ESetting = columnifexists("payload_old_object_settings_in_meeting_e2e_encryption_b", "")
| where OldE2ESetting =~ 'false' and NewE2ESetting =~ 'true'
| extend AccountName = tostring(split(User, "@")[0]), AccountUPNSuffix = tostring(split(User, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: User
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
version: 1.0.3
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others" ]
Stages and Predicates
Stage 1: source
ZoomLogs
Stage 2: where
| where Event =~ "account.settings_updated"
Stage 3: extend
| extend NewE2ESetting = columnifexists("payload_object_settings_in_meeting_e2e_encryption_b", "")
Stage 4: extend
| extend OldE2ESetting = columnifexists("payload_old_object_settings_in_meeting_e2e_encryption_b", "")
Stage 5: where
| where OldE2ESetting =~ 'false' and NewE2ESetting =~ 'true'
Stage 6: extend
| extend AccountName = tostring(split(User, "@")[0]), AccountUPNSuffix = tostring(split(User, "@")[1])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Event | eq |
| field:"Event" kind:eq value:"account.settings_updated" |
NewE2ESetting | eq |
| field:"NewE2ESetting" kind:eq value:"true" |
OldE2ESetting | eq |
| field:"OldE2ESetting" kind:eq value:"false" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
NewE2ESetting | extend |
OldE2ESetting | extend |
AccountName | extend |
AccountUPNSuffix | extend |