Detection rules › Kusto
Kusto rules: 34
AWSCloudTrail - Privilege escalation via CRUD Lambda policy
#Detects inline IAM policy updates that grant broad AWS Lambda lifecycle permissions and related destructive capabilities. This behavior can establish execution control paths and may indicate privilege escalation through unauthorized policy expansion.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- AWS IAM Access Key Compromise Detection (Panther)
- AWS Lateral Movement from Kubernetes SA via AssumeRoleWithWebIdentity (Elastic)
- AWSCloudTrail - Privilege escalation via CloudFormation policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD DynamoDB policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD IAM policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD KMS policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD S3 policy (Kusto)
- AWSCloudTrail - Privilege escalation via DataPipeline policy (Kusto)
Rule Body
id: d0953d50-3dc1-4fa3-80fa-4d3e973a0959
name: AWSCloudTrail - Privilege escalation via CRUD Lambda policy
description: |
Detects inline IAM policy updates that grant broad AWS Lambda lifecycle permissions and related destructive
capabilities. This behavior can establish execution control paths and may indicate privilege escalation through
unauthorized policy expansion.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AWS
dataTypes:
- AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1098.003
query: |
AWSCloudTrail
| where EventName in ("PutUserPolicy","PutRolePolicy","PutGroupPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
| extend PolicyName = tostring(parse_json(RequestParameters).policyName)
| extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement
| mvexpand Statement
| extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition)
| extend Action = tostring(Action)
| where Effect =~ "Allow" and (Action contains "lambda:Create" and Action contains "lambda:Get" and Action contains "lambda:Update" and Action contains "kms:Delete") and Resource == "*" and Condition == ""
| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
| extend UserName = tostring(split(UserIdentityArn, '/')[-1])
| extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
| extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
| distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, UserIdentityUserName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- identifier: CloudAppAccountId
columnName: RecipientAccountId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIpAddress
customDetails:
PolicyName: PolicyName
EventName: EventName
UserIdentityArn: UserIdentityArn
RecipientAccountId: RecipientAccountId
alertDetailsOverride:
alertDisplayNameFormat: 'AWS Lambda privilege escalation policy update by {{AccountName}}'
alertDescriptionFormat: 'Detected {{EventName}} Event, updating inline Lambda escalation policy {{PolicyName}} in account {{RecipientAccountId}}.'
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
AWSCloudTrail
Stage 2: where
| where EventName in ("PutUserPolicy","PutRolePolicy","PutGroupPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
Stage 3: extend
| extend PolicyName = tostring(parse_json(RequestParameters).policyName)
Stage 4: extend
| extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement
Stage 5: mv-expand
| mvexpand Statement
Stage 6: extend
| extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition)
Stage 7: extend
| extend Action = tostring(Action)
Stage 8: where
| where Effect =~ "Allow" and (Action contains "lambda:Create" and Action contains "lambda:Get" and Action contains "lambda:Update" and Action contains "kms:Delete") and Resource == "*" and Condition == ""
Stage 9: extend (4 consecutive steps)
| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
| extend UserName = tostring(split(UserIdentityArn, '/')[-1])
| extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
| extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
Stage 10: distinct
| distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, UserIdentityUserName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | contains |
| field:"Action" kind:contains |
Effect | eq |
| field:"Effect" kind:eq value:"Allow" |
ErrorCode | is_null | field:"aws::errorCode" kind:is_null | |
ErrorMessage | is_null | field:"aws::errorMessage" kind:is_null | |
EventName | in |
| field:"aws::eventName" kind:in |
Resource | eq |
| field:"Resource" kind:eq value:"*" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
PolicyName | extend |
Statement | extend |
Action | extend |
Condition | extend |
Effect | extend |
Resource | extend |
UserIdentityArn | extend |
UserName | extend |
AccountName | extend |
AccountUPNSuffix | extend |
Cloudflare - Multiple error requests from single source
#'Detects multiple failure requests from single source in short timeframe.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: ef877d68-755f-4cf1-ac1d-f336e395667c
name: Cloudflare - Multiple error requests from single source
description: |
'Detects multiple failure requests from single source in short timeframe.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CloudflareDataConnector
dataTypes:
- Cloudflare
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
let threshold = 100;
Cloudflare
| where HttpRequestMethod =~ 'GET'
| summarize err_cnt = count() by SrcIpAddr, bin(TimeGenerated, 5m)
| where err_cnt > threshold
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 100;
Stage 1: source
Cloudflare
Stage 2: where
| where HttpRequestMethod =~ 'GET'
Stage 3: summarize
| summarize err_cnt = count() by SrcIpAddr, bin(TimeGenerated, 5m)
Stage 4: where
| where err_cnt > threshold
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
HttpRequestMethod | eq |
| field:"HttpRequestMethod" kind:eq value:"GET" |
err_cnt | gt |
| field:"err_cnt" kind:gt value:"100" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SrcIpAddr | summarize |
err_cnt | summarize |
Conditional Access - A Conditional Access user/group/role exclusion has changed
#A Conditional Access user/group/role exclusion has changed in Azure AD.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Stealth |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Update conditional access policy |
Rules Detecting the Same Action
These rules filter on the same operation.
- Azure MFA Disabled (Panther)
- Azure Policy Violation Detected (Panther)
- CA Policy Removed by Non Approved Actor (Sigma)
- CA Policy Updated by Non Approved Actor (Sigma)
- Certificate-Based Authentication Enabled (Sigma)
- Changes to Device Registration Policy (Sigma)
- Conditional Access - A Conditional Access app exclusion has changed (Kusto)
- Conditional Access - A Conditional Access Device platforms condition has changed (the Device platforms condition can be spoofed) (Kusto)
Rule Body
id: 2ce7f00d-3b3c-41b9-ae9a-b79c19d2394e
name: Conditional Access - A Conditional Access user/group/role exclusion has changed
version: 1.0.1
kind: Scheduled
description: A Conditional Access user/group/role exclusion has changed in Azure AD.
severity: High
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- DefenseEvasion
- CredentialAccess
relevantTechniques:
- T1098
- T1078
query: |+
// A Conditional Access user/group/role exclusion has changed.
AuditLogs
| where OperationName in ("Update conditional access policy")
| extend excludeUsersOld = extractjson("$.conditions.users.excludeUsers", tostring(TargetResources[0].modifiedProperties[0].oldValue))
| extend excludeGroupsOld = extractjson("$.conditions.users.excludeGroups", tostring(TargetResources[0].modifiedProperties[0].oldValue))
| extend excludeRolesOld = extractjson("$.conditions.users.excludeRoles", tostring(TargetResources[0].modifiedProperties[0].oldValue))
| extend excludeUsersNew = extractjson("$.conditions.users.excludeUsers", tostring(TargetResources[0].modifiedProperties[0].newValue))
| extend excludeGroupsNew = extractjson("$.conditions.users.excludeGroups", tostring(TargetResources[0].modifiedProperties[0].newValue))
| extend excludeRolesNew = extractjson("$.conditions.users.excludeRoles", tostring(TargetResources[0].modifiedProperties[0].newValue))
| where excludeUsersOld != excludeUsersNew or excludeGroupsOld != excludeGroupsNew or excludeRolesOld != excludeRolesNew
| extend modifiedBy = tostring(InitiatedBy.user.userPrincipalName)
| extend accountName = tostring(split(modifiedBy, "@")[0])
| extend upnSuffix = tostring(split(modifiedBy, "@")[1])
| project TimeGenerated, OperationName, policy = TargetResources[0].displayName, modifiedBy, accountName, upnSuffix, result = Result,
excludeUsersOld, excludeUsersNew, excludeGroupsOld, excludeGroupsNew, excludeRolesOld, excludeRolesNew
| order by TimeGenerated desc
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: accountName
- identifier: UPNSuffix
columnName: upnSuffix
suppressionEnabled: false
suppressionDuration: 5h
eventGroupingSettings:
aggregationKind: AlertPerResult
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT1H
matchingMethod: AllEntities
groupByEntities: []
groupByAlertDetails: []
groupByCustomDetails: []
Stages and Predicates
Stage 1: source
AuditLogs
Stage 2: where
| where OperationName in ("Update conditional access policy")
Stage 3: extend (6 consecutive steps)
| extend excludeUsersOld = extractjson("$.conditions.users.excludeUsers", tostring(TargetResources[0].modifiedProperties[0].oldValue))
| extend excludeGroupsOld = extractjson("$.conditions.users.excludeGroups", tostring(TargetResources[0].modifiedProperties[0].oldValue))
| extend excludeRolesOld = extractjson("$.conditions.users.excludeRoles", tostring(TargetResources[0].modifiedProperties[0].oldValue))
| extend excludeUsersNew = extractjson("$.conditions.users.excludeUsers", tostring(TargetResources[0].modifiedProperties[0].newValue))
| extend excludeGroupsNew = extractjson("$.conditions.users.excludeGroups", tostring(TargetResources[0].modifiedProperties[0].newValue))
| extend excludeRolesNew = extractjson("$.conditions.users.excludeRoles", tostring(TargetResources[0].modifiedProperties[0].newValue))
Stage 4: where
| where excludeUsersOld != excludeUsersNew or excludeGroupsOld != excludeGroupsNew or excludeRolesOld != excludeRolesNew
Stage 5: extend (3 consecutive steps)
| extend modifiedBy = tostring(InitiatedBy.user.userPrincipalName)
| extend accountName = tostring(split(modifiedBy, "@")[0])
| extend upnSuffix = tostring(split(modifiedBy, "@")[1])
Stage 6: project
| project TimeGenerated, OperationName, policy = TargetResources[0].displayName, modifiedBy, accountName, upnSuffix, result = Result,
excludeUsersOld, excludeUsersNew, excludeGroupsOld, excludeGroupsNew, excludeRolesOld, excludeRolesNew
Stage 7: sort
| order by TimeGenerated desc
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | in |
| field:"OperationName" kind:in value:"Update conditional access policy" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
OperationName | project |
TimeGenerated | project |
accountName | project |
excludeGroupsNew | project |
excludeGroupsOld | project |
excludeRolesNew | project |
excludeRolesOld | project |
excludeUsersNew | project |
excludeUsersOld | project |
modifiedBy | project |
policy | project |
result | project |
upnSuffix | project |
CTE - Device License Capacity High (95%)
#Device license capacity has reached 95%. Emitted once when usage newly crosses this threshold by the daily tenant health-check job (api/src/functions/tenantHealthChecks.js); resets and can re-fire if usage later drops back below 95% and crosses again.
Rule Body
id: 5e644219-6067-4849-85df-7458aa575aea
name: CTE - Device License Capacity High (95%)
description: |
Device license capacity has reached 95%. Emitted once when usage newly crosses this threshold by the daily tenant health-check job (api/src/functions/tenantHealthChecks.js); resets and can re-fire if usage later drops back below 95% and crosses again.
severity: Medium
requiredDataConnectors:
- connectorId: cybereinforce_cte
dataTypes:
- CybereinforceCTE_CL
queryFrequency: 24h
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
tactics:
[]
relevantTechniques: []
query: |
CybereinforceCTE_CL
| where EventType == "license.capacity_high"
| extend d = parse_json(Details)
| extend AppName = "Cybereinforce", ActiveDevices = toint(d.activeDevices), LicensedDevices = toint(d.licensedDevices), UsedPct = todouble(d.usedPct)
| project TimeGenerated, TenantId, AppName, ActiveDevices, LicensedDevices, UsedPct, Severity, Category, Source
entityMappings:
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: AppName
- identifier: InstanceName
columnName: TenantId
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
CybereinforceCTE_CL
Stage 2: where
| where EventType == "license.capacity_high"
Stage 3: extend
| extend d = parse_json(Details)
Stage 4: extend
| extend AppName = "Cybereinforce", ActiveDevices = toint(d.activeDevices), LicensedDevices = toint(d.licensedDevices), UsedPct = todouble(d.usedPct)
Stage 5: project
| project TimeGenerated, TenantId, AppName, ActiveDevices, LicensedDevices, UsedPct, Severity, Category, Source
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventType | eq |
| field:"EventType" kind:eq value:"license.capacity_high" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActiveDevices | project |
AppName | project |
Category | project |
LicensedDevices | project |
Severity | project |
Source | project |
TenantId | project |
TimeGenerated | project |
UsedPct | project |
CTERA Mass Deletions Detection Analytic
#This analytic rule detects and alerts when large amount of deletion operations generated by the CTERA Edge Filer
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: 5365f294-0c67-432a-bacf-b1282a3b6c46
name: CTERA Mass Deletions Detection Analytic
description: 'This analytic rule detects and alerts when large amount of deletion operations generated by the CTERA Edge Filer'
kind: Scheduled
severity: High
status: Available
requiredDataConnectors:
- connectorId: CTERA
dataTypes:
- Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: GreaterThan
triggerThreshold: 5000
tactics:
- Impact
relevantTechniques:
- T1485
query: |
Syslog
| where ProcessName == 'gw-audit'
| extend
TenantName = extract("\"vportal\":\"([^\"]*)\"", 1, SyslogMessage),
UserName = extract("user=([^|]*)", 1, SyslogMessage),
Permission = extract("op=([^|]*)", 1, SyslogMessage),
EdgeFiler = extract("\"client\":\"([^\"]*)\"", 1, SyslogMessage),
RootPath = extract("rootPath=([^|]*)", 1, SyslogMessage),
Share = extract("share=([^|]*)", 1, SyslogMessage),
LocalPath = extract("path=([^|]*)", 1, SyslogMessage),
Timestamp = todatetime(extract("\"@timestamp\":\"([^\"]*)\"", 1, SyslogMessage))
| where Permission == 'delete'
| summarize Count = count() by UserName, bin(Timestamp, 5m)
| where Count > 5000
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: SingleAlert
alertDetailsOverride:
alertnameFormat: 'CTERA Batch Access Denied Detection'
alertDescriptionFormat: >
Detected {{Count}} denied access attempts by user {{UserName}} on Edge Filer
{{EdgeFiler}} within 5 minutes. Please investigate unauthorized access attempts or misconfigurations.
customDetails:
TenantName: TenantName
UserName: UserName
EdgeFiler: EdgeFiler
RootPath: RootPath
Share: Share
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: UserName
- entityType: File
fieldMappings:
- identifier: Name
columnName: EdgeFiler
suppressionDuration: PT5H
suppressionEnabled: false
version: 1.0.0
Stages and Predicates
Stage 1: source
Syslog
Stage 2: where
| where ProcessName == 'gw-audit'
Stage 3: extend
| extend
TenantName = extract("\"vportal\":\"([^\"]*)\"", 1, SyslogMessage),
UserName = extract("user=([^|]*)", 1, SyslogMessage),
Permission = extract("op=([^|]*)", 1, SyslogMessage),
EdgeFiler = extract("\"client\":\"([^\"]*)\"", 1, SyslogMessage),
RootPath = extract("rootPath=([^|]*)", 1, SyslogMessage),
Share = extract("share=([^|]*)", 1, SyslogMessage),
LocalPath = extract("path=([^|]*)", 1, SyslogMessage),
Timestamp = todatetime(extract("\"@timestamp\":\"([^\"]*)\"", 1, SyslogMessage))
Stage 4: where
| where Permission == 'delete'
Stage 5: summarize
| summarize Count = count() by UserName, bin(Timestamp, 5m)
Stage 6: where
| where Count > 5000
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Count | gt |
| field:"Count" kind:gt value:"5000" |
Permission | eq |
| field:"Permission" kind:eq value:"delete" |
ProcessName | eq |
| field:"process_name" kind:eq value:"gw-audit" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Count | summarize |
UserName | summarize |
Detaching Backups Started
#Detects when a backup file is detached from a backup job.
Rule Body
id: e0d937ca-4582-4c3b-b95b-82fff33ed610
name: Detaching Backups Started
description: Detects when a backup file is detached from a backup job.
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 41200\n| project\n Date\
\ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n\
\ EventId = instanceId,\n [\"User Name\"] = user,\n MessageDetails = Description,\n\
\ Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
Date: Date
VbrHostName: DataSource
EventId: EventId
MessageDetails: MessageDetails
Severity: Severity
Stages and Predicates
Stage 1: source
Veeam_GetSecurityEvents
Stage 2: where
| where instanceId == 41200
Stage 3: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
["User Name"] = user,
MessageDetails = Description,
Severity = SeverityDescription
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"41200" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Severity | project |
User Name | project |
Detect PIM Alert Disabling activity
#Privileged Identity Management (PIM) generates alerts when there is suspicious or unsafe activity in Microsoft Entra ID (Azure AD) organization. This query will help detect attackers attempts to disable in product PIM alerts which are associated with Azure MFA requirements and could indicate activation of privileged access
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Mass Privileged Role Change Activity Detected (Kusto)
- [Entra ID] Privilege Elevation Request Denied (Kusto)
- [Entra ID] Privileged Role Assigned to a New User (Kusto)
- [Entra ID] Privileged Role Assigned to User (Kusto)
- Account Elevated to New Role (Kusto)
- Admin promotion after Role Management Application Permission Grant (Kusto)
- App Assigned To Azure RBAC/Microsoft Entra Role (Sigma)
Rule Body
id: 1f3b4dfd-21ff-4ed3-8e27-afc219e05c50
name: Detect PIM Alert Disabling activity
description: |
'Privileged Identity Management (PIM) generates alerts when there is suspicious or unsafe activity in Microsoft Entra ID (Azure AD) organization.
This query will help detect attackers attempts to disable in product PIM alerts which are associated with Azure MFA requirements and could indicate activation of privileged access'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- PrivilegeEscalation
relevantTechniques:
- T1098
- T1078
query: |
AuditLogs
| where LoggedByService =~ "PIM"
| where Category =~ "RoleManagement"
| where ActivityDisplayName has "Disable PIM Alert"
| extend IpAddress = case(
isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)) and tostring(parse_json(tostring(InitiatedBy.user)).ipAddress) != 'null', tostring(parse_json(tostring(InitiatedBy.user)).ipAddress),
isnotempty(tostring(parse_json(tostring(InitiatedBy.app)).ipAddress)) and tostring(parse_json(tostring(InitiatedBy.app)).ipAddress) != 'null', tostring(parse_json(tostring(InitiatedBy.app)).ipAddress),
'Not Available')
| extend InitiatedBy = iff(isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)),
tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName), tostring(parse_json(tostring(InitiatedBy.app)).displayName)), UserRoles = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)
| project InitiatedBy, ActivityDateTime, ActivityDisplayName, IpAddress, AADOperationType, AADTenantId, ResourceId, CorrelationId, Identity
| extend AccountName = tostring(split(InitiatedBy, "@")[0]), AccountUPNSuffix = tostring(split(InitiatedBy, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatedBy
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpAddress
- entityType: AzureResource
fieldMappings:
- identifier: ResourceId
columnName: ResourceId
version: 1.0.4
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others", "Identity" ]
Stages and Predicates
Stage 1: source
AuditLogs
Stage 2: where
| where LoggedByService =~ "PIM"
Stage 3: where
| where Category =~ "RoleManagement"
Stage 4: where
| where ActivityDisplayName has "Disable PIM Alert"
Stage 5: extend
| extend IpAddress = case(
isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)) and tostring(parse_json(tostring(InitiatedBy.user)).ipAddress) != 'null', tostring(parse_json(tostring(InitiatedBy.user)).ipAddress),
isnotempty(tostring(parse_json(tostring(InitiatedBy.app)).ipAddress)) and tostring(parse_json(tostring(InitiatedBy.app)).ipAddress) != 'null', tostring(parse_json(tostring(InitiatedBy.app)).ipAddress),
'Not Available')
IpAddress =tostring(parse_json(tostring(InitiatedBy.user)).ipAddress) != "null"tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)tostring(parse_json(tostring(InitiatedBy.app)).ipAddress) != "null"tostring(parse_json(tostring(InitiatedBy.app)).ipAddress)'Not Available'Stage 6: extend
| extend InitiatedBy = iff(isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)),
tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName), tostring(parse_json(tostring(InitiatedBy.app)).displayName)), UserRoles = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)
InitiatedBy =/* macro: isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)) */tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)tostring(parse_json(tostring(InitiatedBy.app)).displayName)Stage 7: project
| project InitiatedBy, ActivityDateTime, ActivityDisplayName, IpAddress, AADOperationType, AADTenantId, ResourceId, CorrelationId, Identity
Stage 8: extend
| extend AccountName = tostring(split(InitiatedBy, "@")[0]), AccountUPNSuffix = tostring(split(InitiatedBy, "@")[1])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActivityDisplayName | match |
| field:"azure_ad::activity_display_name" kind:match value:"Disable PIM Alert" |
Category | eq |
| field:"Category" kind:eq value:"RoleManagement" |
LoggedByService | eq |
| field:"azure_ad::logged_by_service" kind:eq value:"PIM" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AADOperationType | project |
AADTenantId | project |
ActivityDateTime | project |
ActivityDisplayName | project |
CorrelationId | project |
Identity | project |
InitiatedBy | project |
IpAddress | project |
ResourceId | project |
AccountName | extend |
AccountUPNSuffix | extend |
GSA - Detect Connections Outside Operational Hours
#This query identifies connections that occur outside of the defined operational hours. It helps in monitoring and flagging any unusual activity that may occur during non-business hours, indicating potential security concerns or policy violations.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 4c9f0a9e-44d7-4c9b-b7f0-f6a6e0d8f8fa
name: GSA - Detect Connections Outside Operational Hours
description: |
This query identifies connections that occur outside of the defined operational hours. It helps in monitoring and flagging any unusual activity that may occur during non-business hours, indicating potential security concerns or policy violations.
severity: High
status: Available
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- NetworkAccessTrafficLogs
queryFrequency: 1h
queryPeriod: 24h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
- T1133
query: |
let starttime = todatetime('{{StartTimeISO}}');
let endtime = todatetime('{{EndTimeISO}}');
let operational_start_hour = 8; // Start of operational hours (8 AM)
let operational_end_hour = 18; // End of operational hours (6 PM)
NetworkAccessTraffic
| where TimeGenerated between (starttime .. endtime)
| extend HourOfDay = datetime_part('hour', TimeGenerated)
| where HourOfDay < operational_start_hour or HourOfDay >= operational_end_hour
| project TimeGenerated, UserPrincipalName, SourceIp, DestinationIp, DestinationPort, Action, DeviceId, DeviceOperatingSystem, ConnectionId
| extend IPCustomEntity = SourceIp, AccountCustomEntity = UserPrincipalName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let starttime = todatetime('{{StartTimeISO}}');
let endtime = todatetime('{{EndTimeISO}}');
let operational_start_hour = 8;
let operational_end_hour = 18;
Stage 1: source
NetworkAccessTraffic
Stage 2: where
| where TimeGenerated between (starttime .. endtime)
Stage 3: extend
| extend HourOfDay = datetime_part('hour', TimeGenerated)
Stage 4: where
| where HourOfDay < operational_start_hour or HourOfDay >= operational_end_hour
Stage 5: project
| project TimeGenerated, UserPrincipalName, SourceIp, DestinationIp, DestinationPort, Action, DeviceId, DeviceOperatingSystem, ConnectionId
Stage 6: extend
| extend IPCustomEntity = SourceIp, AccountCustomEntity = UserPrincipalName
Indicators
These rows show field, operator, and value matches.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Action | project |
ConnectionId | project |
DestinationIp | project |
DestinationPort | project |
DeviceId | project |
DeviceOperatingSystem | project |
SourceIp | project |
TimeGenerated | project |
UserPrincipalName | project |
AccountCustomEntity | extend |
IPCustomEntity | extend |
Imperva - Malicious Client
#'Detects connections from known malicious clients.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 2ff35ed4-b26a-4cad-93a6-f67adb00e919
name: Imperva - Malicious Client
description: |
'Detects connections from known malicious clients.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: ImpervaWAFCloudAPI
dataTypes:
- ImpervaWAFCloud
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
ImpervaWAFCloud
| where ClientApp in~ ('VulnerabilityScanner', 'DDoSBot', 'ClickBot','CommentSpamBot','HackingTool', 'SpamBot', 'Worm')
| where DvcAction !startswith 'REQ_BLOCKED' or DvcAction !startswith 'REQ_BAD_'
| extend IPCustomEntity = SrcIpAddr, UrlCustomEntity = QueryString
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
- entityType: URL
fieldMappings:
- identifier: Url
columnName: UrlCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
ImpervaWAFCloud
Stage 2: where
| where ClientApp in~ ('VulnerabilityScanner', 'DDoSBot', 'ClickBot','CommentSpamBot','HackingTool', 'SpamBot', 'Worm')
Stage 3: where
| where DvcAction !startswith 'REQ_BLOCKED' or DvcAction !startswith 'REQ_BAD_'
Stage 4: extend
| extend IPCustomEntity = SrcIpAddr, UrlCustomEntity = QueryString
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ClientApp | in |
| field:"ClientApp" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
IPCustomEntity | extend |
UrlCustomEntity | extend |
Jira - Permission scheme updated
#'Detects when permission scheme was updated.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: 72592618-fa57-45e1-9f01-ca8706a5e3f5
name: Jira - Permission scheme updated
description: |
'Detects when permission scheme was updated.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: JiraAuditAPI
dataTypes:
- JiraAudit
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1531
query: |
JiraAudit
| where EventMessage =~ 'Permission scheme updated'
| project EventCreationTime, ObjectItemName, UserName, SrcIpAddr, ChangedValues
| extend AccountCustomEntity = UserName, IPCustomEntity = SrcIpAddr
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
JiraAudit
Stage 2: where
| where EventMessage =~ 'Permission scheme updated'
Stage 3: project
| project EventCreationTime, ObjectItemName, UserName, SrcIpAddr, ChangedValues
Stage 4: extend
| extend AccountCustomEntity = UserName, IPCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventMessage | eq |
| field:"EventMessage" kind:eq value:"Permission scheme updated" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ChangedValues | project |
EventCreationTime | project |
ObjectItemName | project |
SrcIpAddr | project |
UserName | project |
AccountCustomEntity | extend |
IPCustomEntity | extend |
Potential DGA detected (ASIM DNS Schema)
#Identifies clients with a high NXDomain count which could be indicative of a DGA (cycling through possible C2 domains where most C2s are not live). Alert is generated when a new IP address is seen (based on not being seen associated with NXDomain records in prior 10-day baseline period). This analytic rule uses ASIM and supports any built-in or custom source that supports the ASIM DNS schema
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Sysmon | Event ID 22: DNSEvent (DNS query) |
Rule Body
id: 983a6922-894d-413c-9f04-d7add0ecc307
name: Potential DGA detected (ASIM DNS Schema)
description: |
'Identifies clients with a high NXDomain count which could be indicative of a DGA (cycling through possible C2 domains where most C2s are not live). Alert is generated when a new IP address is seen (based on not being seen associated with
NXDomain records in prior 10-day baseline period).
This analytic rule uses [ASIM](https://aka.ms/AboutASIM) and supports any built-in or custom source that supports the ASIM DNS schema'
severity: Medium
requiredDataConnectors:
- connectorId: DNS
dataTypes:
- DnsEvents
- connectorId: AzureFirewall
dataTypes:
- AzureDiagnostics
- connectorId: Zscaler
dataTypes:
- CommonSecurityLog
- connectorId: InfobloxNIOS
dataTypes:
- Syslog
- connectorId: GCPDNSDataConnector
dataTypes:
- GCP_DNS_CL
- connectorId: NXLogDnsLogs
dataTypes:
- NXLog_DNS_Server_CL
- connectorId: CiscoUmbrellaDataConnector
dataTypes:
- Cisco_Umbrella_dns_CL
- connectorId: Corelight
dataTypes:
- Corelight_CL
queryFrequency: 1d
queryPeriod: 10d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1568
- T1008
tags:
- ParentAlert: https://github.com/Azure/Azure-Sentinel/blob/master/Detections/DnsEvents/DNS_HighNXDomainCount_detection.yaml
version: 1.0.0
- Schema: ASIMDns
SchemaVersion: 0.1.1
query: |
let referencestarttime = 10d;
let referenceendtime = 1d;
let threshold = 100;
let nxDomainDnsEvents = (stime:datetime, etime:datetime)
{_Im_Dns(responsecodename='NXDOMAIN', starttime=stime, endtime=etime)
| where DnsQueryTypeName in ("A", "AAAA")
| where ipv4_is_match("127.0.0.1", SrcIpAddr) == False
| where DnsQuery !contains "/" and DnsQuery contains "."};
nxDomainDnsEvents (stime=ago(referenceendtime) ,etime=now())
| extend sld = tostring(split(DnsQuery, ".")[-2])
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), dcount(sld) by SrcIpAddr
| where dcount_sld > threshold
// Filter out previously seen IPs
| join kind=leftanti (nxDomainDnsEvents (stime=ago(referencestarttime), etime=ago(referenceendtime))
| extend sld = tostring(split(DnsQuery, ".")[-2])
| summarize dcount(sld) by SrcIpAddr
| where dcount_sld > threshold ) on SrcIpAddr
// Pull out sample NXDomain responses for those remaining potentially infected IPs
| join kind = inner (nxDomainDnsEvents (stime=ago(referencestarttime), etime=now()) | summarize by DnsQuery, SrcIpAddr) on SrcIpAddr
| summarize StartTime = min(StartTime), EndTime = max(EndTime), sampleNXDomainList=make_list(DnsQuery, 100) by SrcIpAddr, dcount_sld
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
version: 1.3.4
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Yaron
support:
tier: Community
categories:
domains: [ "Security - Network" ]
Stages and Predicates
Parameters
let referencestarttime = 10d;
let referenceendtime = 1d;
let threshold = 100;
let nxDomainDnsEvents is inlined into the numbered stages below.
Stages 1 to 4 define let nxDomainDnsEvents as the rule's main pipeline source. Stages 5 to 10 use it.
Stage 1: source
_Im_Dns(responsecodename='NXDOMAIN', starttime=stime, endtime=etime)
Stage 2: where
| where DnsQueryTypeName in ("A", "AAAA")
Stage 3: where
| where ipv4_is_match("127.0.0.1", SrcIpAddr) == False
Stage 4: where
| where DnsQuery !contains "/" and DnsQuery contains "."
Stage 5: extend
nxDomainDnsEvents
| extend sld = tostring(split(DnsQuery, ".")[-2])
Stage 6: summarize
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), dcount(sld) by SrcIpAddr
Stage 7: where
| where dcount_sld > threshold
Stage 8: join (negated)
join kind=leftanti (nxDomainDnsEvents) on SrcIpAddr
Stage 9: join
| join kind = inner (nxDomainDnsEvents (stime=ago(referencestarttime), etime=now()) | summarize by DnsQuery, SrcIpAddr) on SrcIpAddr
Stage 10: summarize
summarize EndTime, StartTime, sampleNXDomainList by SrcIpAddr, dcount_sld
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DnsQuery | contains |
| field:"QueryName" kind:contains value:"." |
DnsQueryTypeName | in |
| field:"DnsQueryTypeName" kind:in |
dcount_sld | gt |
| field:"dcount_sld" kind:gt value:"100" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
SrcIpAddr | cidr_match | 127.0.0.1 | excludes:SrcIpAddr field:"SrcIpAddr" value:"127.0.0.1" |
DnsQuery | contains | / | excludes:DnsQuery field:"DnsQuery" value:"/" |
dcount_sld | gt | 100 | excludes:dcount_sld field:"dcount_sld" value:"100" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
EndTime | summarize |
SrcIpAddr | summarize |
StartTime | summarize |
dcount_sld | summarize |
sampleNXDomainList | summarize |
SAP BTP - Failed access attempts across multiple BAS subaccounts
#Identifies failed Business Application Studio access attempts over a predefined number of subaccounts.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance | |
| Discovery |
Rule Body
id: 74b243a6-3046-48aa-8b03-e43b3c529cc1
kind: Scheduled
name: SAP BTP - Failed access attempts across multiple BAS subaccounts
description: Identifies failed Business Application Studio access attempts over a
predefined number of subaccounts.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SAPBTPAuditEvents
dataTypes:
- SAPBTPAuditLog_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Reconnaissance
- Discovery
relevantTechniques:
- T1595
- T1526
query: |
let subaccount_detection_threshold = 3;
SAPBTPAuditLog_CL
| where Category == "audit.security-events" and Message has "Unauthorized access attempt"
| summarize Start=min(UpdatedOn), End=max(UpdatedOn), Tenants = make_set(Tenant, 100) by UserName
| where array_length(Tenants) > subaccount_detection_threshold
| project Start, End, UserName, Tenants, CloudApp = "SAP BTP"
| extend AccountName = split(UserName, "@")[0], UPNSuffix = split(UserName, "@")[1]
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: CloudApp
alertDetailsOverride:
alertDisplayNameFormat: SAP BTP - Unauthorized access attempt to multiple tenants
alertDescriptionFormat: '{{UserName}} attempted, and failed, to log into multiple
Business Application Studio dev spaces. Tenants accessed: {{Tenants}}'
version: 3.0.6
Stages and Predicates
Parameters
let subaccount_detection_threshold = 3;
Stage 1: source
SAPBTPAuditLog_CL
Stage 2: where
| where Category == "audit.security-events" and Message has "Unauthorized access attempt"
Stage 3: summarize
| summarize Start=min(UpdatedOn), End=max(UpdatedOn), Tenants = make_set(Tenant, 100) by UserName
Stage 4: where
| where array_length(Tenants) > subaccount_detection_threshold
Stage 5: project
| project Start, End, UserName, Tenants, CloudApp = "SAP BTP"
Stage 6: extend
| extend AccountName = split(UserName, "@")[0], UPNSuffix = split(UserName, "@")[1]
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Category | eq |
| field:"Category" kind:eq value:"audit.security-events" |
Message | match |
| field:"Message" kind:match value:"Unauthorized access attempt" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CloudApp | project |
End | project |
Start | project |
Tenants | project |
UserName | project |
AccountName | extend |
UPNSuffix | extend |
SAP ETD - Execution of Sensitive Function Module
#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.
Identifies execution of a sensitive ABAP Function Module using the watchlists provided by the Microsoft Sentinel Solution for SAP Source Action: Execute a sensitive function module directly using SE37. Data Sources: SAP Enterprise Thread Detection Solution - Alerts
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Discovery | No specific technique |
Rule Body
id: c6111e06-11e2-45eb-86ef-28313a06db35
kind: Scheduled
name: SAP ETD - Execution of Sensitive Function Module
description: |
Identifies execution of a sensitive ABAP Function Module using the watchlists provided by the Microsoft Sentinel Solution for SAP
Source Action: Execute a sensitive function module directly using SE37.
*Data Sources: SAP Enterprise Thread Detection Solution - Alerts*
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SAPETDAlerts
dataTypes:
- SAPETDAlerts_CL
queryFrequency: 5m
queryPeriod: 60m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Discovery
relevantTechniques: []
query: |
let AuditTimeAgo = 60m;
let minThreshold= 1;
let minScore= 50;
let SenseModules = _GetWatchlist('SAP - Sensitive Function Modules')
| project trimmedFunctionModule = trim(@"\s+", FunctionModule);
SAPETDAlerts_CL
| where TimeGenerated > ago(AuditTimeAgo)
| where Threshold >= minThreshold and Score >= minScore
| where PatternName == "Critical Function module call in Test framework calls"
| mv-expand NormalizedTriggeringEvents
| extend sapOriginalEvent = tostring(NormalizedTriggeringEvents.OriginalEvent)
| extend Id_ = NormalizedTriggeringEvents.Id
| extend extracted_user_ip = tostring(NormalizedTriggeringEvents.NetworkIPAddressInitiator)
| extend extracted_sap_user = tostring(NormalizedTriggeringEvents.UserAccountActing)
| extend extracted_sid = tostring(NormalizedTriggeringEvents.SystemIdActor)
| extend extracted_instance_name = tostring(NormalizedTriggeringEvents.NetworkHostnameActor)
| extend extracted_instance_host = tostring(NormalizedTriggeringEvents.ServiceInstanceName)
| extend extracted_function_module = trim(@"\s+", tostring(NormalizedTriggeringEvents.ServiceFunctionName))
| extend extracted_system_role = tostring(NormalizedTriggeringEvents.SystemRoleReporter)
| where extracted_function_module in (SenseModules)
| extend AlertName = strcat("SAP ETD - Sensitive Function Module ", extracted_function_module," was executed by user ", extracted_sap_user,
" in a ", tolower(extracted_system_role), " system"), Dummy = " "
| mv-expand Users
| extend
UserAccountName = tostring(Users.UserAccountName),
UserEmail = tostring(Users.EmailAddresses[0])
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: UserAccountName
- entityType: Mailbox
fieldMappings:
- identifier: MailboxPrimaryAddress
columnName: UserEmail
- entityType: CloudApplication
fieldMappings:
- identifier: AppId
columnName: extracted_sid
- identifier: InstanceName
columnName: extracted_instance_name
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: extracted_instance_host
- entityType: IP
fieldMappings:
- identifier: Address
columnName: extracted_user_ip
alertDetailsOverride:
alertDisplayNameFormat: 'SAP ETD - {{PatternName}} '
alertDescriptionFormat: |
{{PatternDescription}}
Source: SAP ETD
customDetails:
SAP_User: extracted_sap_user
SAP_UserEmail: UserEmail
ETD_AlertNumber: AlertId
version: 1.0.1
Stages and Predicates
Parameters
let AuditTimeAgo = 60m;
let minThreshold = 1;
let minScore = 50;
Let Binding: SenseModules
let SenseModules = _GetWatchlist('SAP - Sensitive Function Modules')
| project trimmedFunctionModule = trim(@"\s+", FunctionModule);
Stage 1: source
SAPETDAlerts_CL
Stage 2: where
| where TimeGenerated > ago(AuditTimeAgo)
Stage 3: where
| where Threshold >= minThreshold and Score >= minScore
Stage 4: where
| where PatternName == "Critical Function module call in Test framework calls"
Stage 5: mv-expand
| mv-expand NormalizedTriggeringEvents
Stage 6: extend (9 consecutive steps)
| extend sapOriginalEvent = tostring(NormalizedTriggeringEvents.OriginalEvent)
| extend Id_ = NormalizedTriggeringEvents.Id
| extend extracted_user_ip = tostring(NormalizedTriggeringEvents.NetworkIPAddressInitiator)
| extend extracted_sap_user = tostring(NormalizedTriggeringEvents.UserAccountActing)
| extend extracted_sid = tostring(NormalizedTriggeringEvents.SystemIdActor)
| extend extracted_instance_name = tostring(NormalizedTriggeringEvents.NetworkHostnameActor)
| extend extracted_instance_host = tostring(NormalizedTriggeringEvents.ServiceInstanceName)
| extend extracted_function_module = trim(@"\s+", tostring(NormalizedTriggeringEvents.ServiceFunctionName))
| extend extracted_system_role = tostring(NormalizedTriggeringEvents.SystemRoleReporter)
Stage 7: where
| where extracted_function_module in (SenseModules)
Stage 8: extend
| extend AlertName = strcat("SAP ETD - Sensitive Function Module ", extracted_function_module," was executed by user ", extracted_sap_user,
" in a ", tolower(extracted_system_role), " system"), Dummy = " "
Stage 9: mv-expand
| mv-expand Users
Stage 10: extend
| extend
UserAccountName = tostring(Users.UserAccountName),
UserEmail = tostring(Users.EmailAddresses[0])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
PatternName | eq |
| field:"PatternName" kind:eq value:"Critical Function module call in Test framework calls" |
Score | ge |
| field:"Score" kind:ge value:"50" |
Threshold | ge |
| field:"Threshold" kind:ge value:"1" |
extracted_function_module | in |
| field:"extracted_function_module" kind:in value:"SenseModules" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
sapOriginalEvent | extend |
Id_ | extend |
extracted_user_ip | extend |
extracted_sap_user | extend |
extracted_sid | extend |
extracted_instance_name | extend |
extracted_instance_host | extend |
extracted_function_module | extend |
extracted_system_role | extend |
AlertName | extend |
Dummy | extend |
UserAccountName | extend |
UserEmail | extend |
Snowflake - Possible data destraction
#'Detects possible data destruction.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: c2f93727-e4b0-4cb9-8f80-f52ebbd96ece
name: Snowflake - Possible data destraction
description: |
'Detects possible data destruction.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Snowflake
dataTypes:
- Snowflake
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1485
query: |
let threshold = 10;
Snowflake
| where QueryType =~ 'DROP'
| where QueryExecutionStatus =~ 'SUCCESS'
| summarize count() by TargetUsername, bin(TimeGenerated, 5m)
| where count_ > threshold
| extend AccountCustomEntity = TargetUsername
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 10;
Stage 1: source
Snowflake
Stage 2: where
| where QueryType =~ 'DROP'
Stage 3: where
| where QueryExecutionStatus =~ 'SUCCESS'
Stage 4: summarize
| summarize count() by TargetUsername, bin(TimeGenerated, 5m)
Stage 5: where
| where count_ > threshold
Stage 6: extend
| extend AccountCustomEntity = TargetUsername
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
QueryExecutionStatus | eq |
| field:"QueryExecutionStatus" kind:eq value:"SUCCESS" |
QueryType | eq |
| field:"QueryType" kind:eq value:"DROP" |
count_ | gt |
| field:"count_" kind:gt value:"10" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
TargetUsername | summarize |
AccountCustomEntity | extend |
T1558.003 - Kerberoasting
#Detects kerberoasting by using time-series analysis functions. Highly accurate in big environments. Step by step explanation is in the query to make it easy to understand.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
References
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 4769: A Kerberos service ticket was requested. |
Rule Body
// create a whitelist that consists of your AD domain names. ex: ("contoso.com","contoso.local","contoso.dmz")
let _whitelist_ServiceName = dynamic (["contoso.com","contoso.local","contoso.dmz"]);
let _fromDate = ago(8d);
let _thruDate = now();
let _min_dcount_threshold = 6;
let _period =2h; // this value should also be used as the rule frequency.
// If an account requested more than <_min_dcount_threshold> service tickets during the last <_period> hour,
// we need to check if it is suspicious or not by looking at historical data for the same user.
// STEP 1: Create a list of accounts to be checked based on their requests during the last <_period> hour.
// This approach also increases the rule performance as only the suspicious accounts are checked historically.
let _accounts_to_be_checked =
SecurityEvent
| where TimeGenerated > ago(_period)
| where EventID == 4769
| parse EventData with * 'Status">' Status "<" *
| where Status == '0x0'
| parse EventData with * 'ServiceName">' ServiceName "<" *
| where ServiceName !contains "$" and ServiceName !contains "krbtgt"
| where ServiceName !in~ (_whitelist_ServiceName )
| parse EventData with * 'TargetUserName">' TargetUserName "<" *
| where TargetUserName !contains ServiceName
| parse EventData with * 'TicketEncryptionType">' TicketEncryptionType "<" *
| where TicketEncryptionType in~ ('0x17','0x18') //these are the weak encryption types.
| parse EventData with * 'TicketOptions">' TicketOptions "<" *
| parse EventData with * 'IpAddress">::ffff:' ClientIPAddress "<" *
| summarize dcount(ServiceName) by TargetUserName
| where dcount_ServiceName > _min_dcount_threshold
| summarize make_list(TargetUserName);
// STEP 2: Create a time-series statistics per day for each account in the list, for the last 8 days.
let baseData = materialize(
SecurityEvent
| where TimeGenerated > _fromDate
| where EventID == 4769
| parse EventData with * 'Status">' Status "<" *
| where Status == '0x0'
| parse EventData with * 'ServiceName">' ServiceName "<" *
| where ServiceName !contains "$" and ServiceName !contains "krbtgt"
| where ServiceName !in~ (_whitelist_ServiceName )
| parse EventData with * 'TargetUserName">' TargetUserName "<" *
| where TargetUserName in~ (_accounts_to_be_checked) // only the accounts in the list will be analyzed.
| parse EventData with * 'TicketEncryptionType">' TicketEncryptionType "<" *
| where TicketEncryptionType in~ ('0x17','0x18')
| parse EventData with * 'TicketOptions">' TicketOptions "<" *
| parse EventData with * 'IpAddress">::ffff:' ClientIPAddress "<" *
//creating the time-series stats
| make-series dcount(ServiceName) default=0 on TimeGenerated in range(_fromDate, _thruDate, 1d) by TargetUserName, ClientIPAddress
| extend avg = todouble(series_stats_dynamic(dcount_ServiceName).avg )
);
// STEP 3: Find outliers(anomalies) in the time-series data and make a list of suspicious accounts.
let AnomTargetUserNames = (
baseData
| extend outliers = series_outliers(dcount_ServiceName)
| mvexpand dcount_ServiceName, TimeGenerated, outliers to typeof(double)
// dcount must be at least greater than the threshold. otherwise small numbers can also be an outlier.
// ex: 1,1,1,1,5 -> 5 is an outlier but probably not suspicious in big environment
| where dcount_ServiceName > _min_dcount_threshold
// time-series function rounds the time information and displays the time info as the startofday like '2020-08-23T00:00:00.0000000Z'.
// check if the anomlay happened on the current day.
| where TimeGenerated >= startofday(now())
// we are looking for positive outliers, meaning increase in the volume.
// this threshold can be adjusted based on the number of false positives or false negatives
| where outliers > 1.5
// we are looking for big outliers. Top 25 outliers should be more than enough.
| top 25 by outliers desc
| summarize make_list(TargetUserName)
);
// STEP 4: Anomaly is found but it's not clear when it happened exactly and there is not enough information for incident response.
// Get last <_period> of data and dcount of service names, set of service names that have been requested for each account in the anomalous account list.
// Join with the baseData as it has both avg values and other details of the anomaly.
// Avg value will also be used for preventing the rule from triggering the same alert again.
// We already found the anomalous users but we need to check if the anomaly happened in the last <_period> to prevent duplicate incidents.
SecurityEvent
| where TimeGenerated >= ago(_period)
| where EventID == 4769
| parse EventData with * 'Status">' Status "<" *
| where Status == '0x0'
| parse EventData with * 'ServiceName">' ServiceName "<" *
| where ServiceName !contains "$" and ServiceName !contains "krbtgt"
| where ServiceName !in~ (_whitelist_ServiceName )
| parse EventData with * 'TargetUserName">' TargetUserName "<" *
| where TargetUserName in (AnomTargetUserNames) // checking if the user is in the list of anomolous users
| parse EventData with * 'TicketEncryptionType">' TicketEncryptionType "<" *
| where TicketEncryptionType in~ ('0x17','0x18')
| parse EventData with * 'TicketOptions">' TicketOptions "<" *
| parse EventData with * 'IpAddress">::ffff:' ClientIPAddress "<" *
// Calculate the dcount of service tickets, create the set of service tickets requested
| summarize start_Time=min(TimeGenerated), end_Time=max(TimeGenerated), ServiceNameCount_LastPeriod = dcount(ServiceName), ServiceNameSet_LastPeriod = makeset(ServiceName) by TargetUserName
// Now join this with the baseData to display the details of the anomaly.
| join kind=leftouter baseData on TargetUserName
| where ServiceNameCount_LastPeriod > avg + 2 // this is a simple check to see if the anomaly happened in the last period.
// Display the columns we need
| project start_Time, end_Time, ClientIPAddress, TargetUserName, ServiceNameCount_Avg = avg, ServiceNameCount_LastPeriod, dcount_ServiceName, ServiceNameSet_LastPeriod
Stages and Predicates
Parameters
let _whitelist_ServiceName = dynamic (["contoso.com","contoso.local","contoso.dmz"]);
let _fromDate = ago(8d);
let _thruDate = now();
let _min_dcount_threshold = 6;
let _period = 2h;
Let Binding: _accounts_to_be_checked
let _accounts_to_be_checked = SecurityEvent
| where TimeGenerated > ago(_period)
| where EventID == 4769
| parse EventData with * 'Status">' Status "<" *
| where Status == '0x0'
| parse EventData with * 'ServiceName">' ServiceName "<" *
| where ServiceName !contains "$" and ServiceName !contains "krbtgt"
| where ServiceName !in~ (_whitelist_ServiceName )
| parse EventData with * 'TargetUserName">' TargetUserName "<" *
| where TargetUserName !contains ServiceName
| parse EventData with * 'TicketEncryptionType">' TicketEncryptionType "<" *
| where TicketEncryptionType in~ ('0x17','0x18')
| parse EventData with * 'TicketOptions">' TicketOptions "<" *
| parse EventData with * 'IpAddress">::ffff:' ClientIPAddress "<" *
| summarize dcount(ServiceName) by TargetUserName
| where dcount_ServiceName > _min_dcount_threshold
| summarize make_list(TargetUserName);
Let Binding: baseData
let baseData = materialize(
SecurityEvent
| where TimeGenerated > _fromDate
| where EventID == 4769
| parse EventData with * 'Status">' Status "<" *
| where Status == '0x0'
| parse EventData with * 'ServiceName">' ServiceName "<" *
| where ServiceName !contains "$" and ServiceName !contains "krbtgt"
| where ServiceName !in~ (_whitelist_ServiceName )
| parse EventData with * 'TargetUserName">' TargetUserName "<" *
| where TargetUserName in~ (_accounts_to_be_checked)
| parse EventData with * 'TicketEncryptionType">' TicketEncryptionType "<" *
| where TicketEncryptionType in~ ('0x17','0x18')
| parse EventData with * 'TicketOptions">' TicketOptions "<" *
| parse EventData with * 'IpAddress">::ffff:' ClientIPAddress "<" *
| make-series dcount(ServiceName) default=0 on TimeGenerated in range(_fromDate, _thruDate, 1d) by TargetUserName, ClientIPAddress
| extend avg = todouble(series_stats_dynamic(dcount_ServiceName).avg )
);
Let Binding: AnomTargetUserNames
let AnomTargetUserNames = (
baseData
| extend outliers = series_outliers(dcount_ServiceName)
| mvexpand dcount_ServiceName, TimeGenerated, outliers to typeof(double)
| where dcount_ServiceName > _min_dcount_threshold
| where TimeGenerated >= startofday(now())
| where outliers > 1.5
| top 25 by outliers desc
| summarize make_list(TargetUserName)
);
Stage 1: source
let _accounts_to_be_checked
Stage 2: source
let baseData
Stage 3: source
let AnomTargetUserNames
Stage 4: source
SecurityEvent
Stage 5: where
| where TimeGenerated >= ago(_period)
Stage 6: where
| where EventID == 4769
Stage 7: parse
| parse EventData with * 'Status">' Status "<" *
Stage 8: where
| where Status == '0x0'
Stage 9: parse
| parse EventData with * 'ServiceName">' ServiceName "<" *
Stage 10: where
| where ServiceName !contains "$" and ServiceName !contains "krbtgt"
Stage 11: where
| where ServiceName !in~ (_whitelist_ServiceName )
Stage 12: parse
| parse EventData with * 'TargetUserName">' TargetUserName "<" *
Stage 13: where
| where TargetUserName in (AnomTargetUserNames)
Stage 14: parse
| parse EventData with * 'TicketEncryptionType">' TicketEncryptionType "<" *
Stage 15: where
| where TicketEncryptionType in~ ('0x17','0x18')
Stage 16: parse
| parse EventData with * 'TicketOptions">' TicketOptions "<" *
Stage 17: parse
| parse EventData with * 'IpAddress">::ffff:' ClientIPAddress "<" *
Stage 18: summarize
| summarize start_Time=min(TimeGenerated), end_Time=max(TimeGenerated), ServiceNameCount_LastPeriod = dcount(ServiceName), ServiceNameSet_LastPeriod = makeset(ServiceName) by TargetUserName
Stage 19: join
| join kind=leftouter baseData on TargetUserName
Stage 20: where
where ServiceNameCount_LastPeriod > (avg + 2)
Stage 21: project
project ClientIPAddress, ServiceNameCount_Avg, ServiceNameCount_LastPeriod, ServiceNameSet_LastPeriod, TargetUserName, dcount_ServiceName, end_Time, start_Time
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventID | eq |
| field:"EventID" kind:eq value:"4769" |
Status | eq |
| field:"Status" kind:eq value:"0x0" |
TargetUserName | in |
| field:"TargetUserName" kind:in |
TicketEncryptionType | in |
| field:"TicketEncryptionType" kind:in |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
ServiceName | contains | $ | excludes:ServiceName field:"ServiceName" value:"$" |
ServiceName | contains | krbtgt | excludes:ServiceName field:"ServiceName" value:"krbtgt" |
ServiceName | in | contoso.com, contoso.dmz, contoso.local | excludes:ServiceName field:"ServiceName" value:"contoso.com" field:"ServiceName" value:"contoso.dmz" field:"ServiceName" value:"contoso.local" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ClientIPAddress | project |
ServiceNameCount_Avg | project |
ServiceNameCount_LastPeriod | project |
ServiceNameSet_LastPeriod | project |
TargetUserName | project |
dcount_ServiceName | project |
end_Time | project |
start_Time | project |
TI map File Hash to DeviceFileEvents Event
#Identifies a match in DeviceFileEvents Event data from any FileHash IOC from TI
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceFileEvents action any: File activity |
Rule Body
id: bc0eca2e-db50-44e6-8fa3-b85f91ff5ee7
name: TI map File Hash to DeviceFileEvents Event
description: |
'Identifies a match in DeviceFileEvents Event data from any FileHash IOC from TI'
severity: Medium
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceFileEvents
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
query: |
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let DeviceFileEvents_ = (union
(DeviceFileEvents | where TimeGenerated > ago(dt_lookBack) | where isnotempty(SHA1) | extend FileHashValue = SHA1),
(DeviceFileEvents | where TimeGenerated > ago(dt_lookBack) | where isnotempty(SHA256) | extend FileHashValue = SHA256));
let Hashes = DeviceFileEvents_ | distinct FileHashValue;
ThreatIntelligenceIndicator
| where isnotempty(FileHashValue)
| where TimeGenerated > ago(ioc_lookBack)
| where FileHashValue in (Hashes)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
| where Active == true and ExpirationDateTime > now()
| where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"
| join kind=innerunique (DeviceFileEvents_) on $left.FileHashValue == $right.FileHashValue
| where TimeGenerated < ExpirationDateTime
| summarize TimeGenerated = arg_max(TimeGenerated, *) by IndicatorId, DeviceId
| project TimeGenerated, TrafficLightProtocolLevel, Description, ActivityGroupNames, IndicatorId, ThreatType, FileHashValue, FileHashType, ExpirationDateTime, ConfidenceScore, ActionType, DeviceId, DeviceName, FolderPath, RequestAccountDomain, RequestAccountName, RequestAccountSid, MachineGroup
| extend timestamp = TimeGenerated
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: RequestAccountName
- identifier: Sid
columnName: RequestAccountSid
- identifier: NTDomain
columnName: RequestAccountDomain
- entityType: FileHash
fieldMappings:
- identifier: Value
columnName: FileHashValue
- identifier: Algorithm
columnName: FileHashType
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: DeviceName
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
Let Binding: DeviceFileEvents_
let DeviceFileEvents_ = (union
(DeviceFileEvents | where TimeGenerated > ago(dt_lookBack) | where isnotempty(SHA1) | extend FileHashValue = SHA1),
(DeviceFileEvents | where TimeGenerated > ago(dt_lookBack) | where isnotempty(SHA256) | extend FileHashValue = SHA256));
Let Binding: Hashes
let Hashes = DeviceFileEvents_ | distinct FileHashValue;
Stage 1: source
let DeviceFileEvents_
Stage 2: source
let Hashes
Stage 3: source
ThreatIntelligenceIndicator
Stage 4: where
where isnotempty(FileHashValue)
Stage 5: where
where TimeGenerated > ago(1209600s)
Stage 6: where
where FileHashValue == "Hashes"
Stage 7: summarize
summarize LatestIndicatorTime by IndicatorId
Stage 8: where
where Active == true
Stage 9: where
where not (Description contains_cs "State: falsepos;") and not (Description contains_cs "State: inactive;")
Stage 10: join
join kind=innerunique (DeviceFileEvents_) on $left.FileHashValue, $right.FileHashValue
Stage 11: where
where TimeGenerated < ExpirationDateTime
Stage 12: summarize
summarize TimeGenerated by IndicatorId, DeviceId
Stage 13: project
project ActionType, ActivityGroupNames, ConfidenceScore, Description, DeviceId, DeviceName, ExpirationDateTime, FileHashType, FileHashValue, FolderPath, IndicatorId, MachineGroup, RequestAccountDomain, RequestAccountName, RequestAccountSid, ThreatType, TimeGenerated, TrafficLightProtocolLevel
Stage 14: extend
extend timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Active | eq |
| field:"Active" kind:eq value:"true" |
FileHashValue | in |
| field:"FileHashValue" kind:in value:"Hashes" |
FileHashValue | is_not_null | field:"FileHashValue" kind:is_not_null | |
SHA1 | is_not_null | field:"SHA1" kind:is_not_null | |
SHA256 | is_not_null | field:"SHA256" kind:is_not_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Description | contains | State: falsepos; | excludes:Description field:"Description" value:"State: falsepos;" |
Description | contains | State: inactive; | excludes:Description field:"Description" value:"State: inactive;" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActionType | project |
ActivityGroupNames | project |
ConfidenceScore | project |
Description | project |
DeviceId | project |
DeviceName | project |
ExpirationDateTime | project |
FileHashType | project |
FileHashValue | project |
FolderPath | project |
IndicatorId | project |
MachineGroup | project |
RequestAccountDomain | project |
RequestAccountName | project |
RequestAccountSid | project |
ThreatType | project |
TimeGenerated | project |
TrafficLightProtocolLevel | project |
timestamp | extend |
TI Map IP Entity to W3CIISLog
#This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in W3CIISLog.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: 888c4736-e604-48eb-b2c7-3462356d9510
name: TI Map IP Entity to W3CIISLog
description: |
This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in W3CIISLog.
severity: Medium
requiredDataConnectors:
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelIndicators
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelIndicators
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelIndicators
- connectorId: AzureMonitor(IIS)
dataTypes:
- W3CIISLog
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
query: |
let dt_lookBack = 1h; // Look back 1 hour for W3CIISLog events
let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
// Fetch threat intelligence indicators related to IP addresses
let IP_Indicators = ThreatIntelIndicators
//extract key part of kv pair
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
| where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")
| extend NetworkSourceIP = toupper(ObservableValue)
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
| where TimeGenerated >= ago(ioc_lookBack)
| extend TI_ipEntity = iff(isnotempty(NetworkSourceIP), NetworkSourceIP, NetworkSourceIP)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
| where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
| where IsActive and (ValidUntil > now() or isempty(ValidUntil));
// Perform a join between IP indicators and W3CIISLog events
IP_Indicators
| project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity
// Use innerunique to keep performance fast and result set low, as we only need one match to indicate potential malicious activity that needs investigation
| join kind=innerunique (
W3CIISLog
| where TimeGenerated >= ago(dt_lookBack)
| where isnotempty(cIP)
| where ipv4_is_private(cIP) == false and cIP !startswith "fe80" and cIP !startswith "::" and cIP !startswith "127."
| extend W3CIISLog_TimeGenerated = TimeGenerated
)
on $left.TI_ipEntity == $right.cIP
// Filter out W3CIISLog events that occurred after the expiration of the corresponding indicator
| where W3CIISLog_TimeGenerated < ValidUntil
// Group the results by IndicatorId and keep the W3CIISLog event with the latest timestamp
| summarize W3CIISLog_TimeGenerated = arg_max(W3CIISLog_TimeGenerated, *) by Id, cIP
// Select the desired output fields
| extend Description = tostring(parse_json(Data).description)
| extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
| project timestamp = W3CIISLog_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence,
TI_ipEntity, Computer, sSiteName, cIP, sIP, sPort, csMethod, csUserName, scStatus, scSubStatus, scWin32Status,
NetworkSourceIP, Type, Url
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: csUserName
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Computer
- entityType: IP
fieldMappings:
- identifier: Address
columnName: cIP
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
version: 1.4.6
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let IP_Indicators is inlined into the numbered stages below.
Stages 1 to 10 define let IP_Indicators as the rule's main pipeline source. Stages 11 to 17 use it.
Stage 1: source
ThreatIntelIndicators
Stage 2: extend
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
Stage 3: where
| where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")
Stage 4: extend
| extend NetworkSourceIP = toupper(ObservableValue)
Stage 5: extend
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
Stage 6: where
| where TimeGenerated >= ago(ioc_lookBack)
Stage 7: extend (3 consecutive steps)
| extend TI_ipEntity = iff(isnotempty(NetworkSourceIP), NetworkSourceIP, NetworkSourceIP)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
TI_ipEntity =isnotempty(NetworkSourceIP)NetworkSourceIPNetworkSourceIPStage 8: where
| where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
Stage 9: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
Stage 10: where
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
Stage 11: project-reorder
IP_Indicators
| project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity
Stage 12: join
| join kind=innerunique (
W3CIISLog
| where TimeGenerated >= ago(dt_lookBack)
| where isnotempty(cIP)
| where ipv4_is_private(cIP) == false and cIP !startswith "fe80" and cIP !startswith "::" and cIP !startswith "127."
| extend W3CIISLog_TimeGenerated = TimeGenerated
)
on $left.TI_ipEntity == $right.cIP
Stage 13: where
where W3CIISLog_TimeGenerated < ValidUntil
Stage 14: summarize
summarize W3CIISLog_TimeGenerated by Id, cIP
Stage 15: extend
extend Description
Stage 16: extend
extend ActivityGroupNames
Stage 17: project
project ActivityGroupNames, Computer, Confidence, Description, Id, NetworkSourceIP, TI_ipEntity, Type, Url, ValidUntil, cIP, csMethod, csUserName, sIP, sPort, sSiteName, scStatus, scSubStatus, scWin32Status, timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
IndicatorType | in |
| field:"IndicatorType" kind:in |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
ValidUntil | is_null | field:"ValidUntil" kind:is_null | |
cIP | is_not_null | field:"cIP" kind:is_not_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
TI_ipEntity | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 | excludes:TI_ipEntity |
TI_ipEntity | starts_with | 127. | excludes:TI_ipEntity field:"TI_ipEntity" value:"127." |
TI_ipEntity | starts_with | :: | excludes:TI_ipEntity field:"TI_ipEntity" value:"::" |
TI_ipEntity | starts_with | fe80 | excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80" |
cIP | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 | excludes:cIP |
cIP | starts_with | 127. | excludes:cIP field:"cIP" value:"127." |
cIP | starts_with | :: | excludes:cIP field:"cIP" value:"::" |
cIP | starts_with | fe80 | excludes:cIP field:"cIP" value:"fe80" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
Computer | project |
Confidence | project |
Description | project |
Id | project |
NetworkSourceIP | project |
TI_ipEntity | project |
Type | project |
Url | project |
ValidUntil | project |
cIP | project |
csMethod | project |
csUserName | project |
sIP | project |
sPort | project |
sSiteName | project |
scStatus | project |
scSubStatus | project |
scWin32Status | project |
timestamp | project |
Trend Micro CAS - Unexpected file via mail
#'Detects when unexpected file received via mail.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 201fd2d1-9131-4b29-bace-ce5d19f3e4ee
name: Trend Micro CAS - Unexpected file via mail
description: |
'Detects when unexpected file received via mail.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: TrendMicroCAS
dataTypes:
- TrendMicroCAS
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1566
query: |
let f_types = dynamic(['ps1', 'bat', 'scr', 'sh', 'exe', 'js', 'lnk']);
TrendMicroCAS
| where EventCategoryType in~ ('exchange', 'gmail', 'exchangeserver')
| where isnotempty(SrcFileName)
| extend file_type = extract(@'\.(\w+)$', 1, SrcFileName)
| where file_type in~ (f_types)
| extend AccountCustomEntity = DstUserName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let f_types = dynamic(['ps1', 'bat', 'scr', 'sh', 'exe', 'js', 'lnk']);
Stage 1: source
TrendMicroCAS
Stage 2: where
| where EventCategoryType in~ ('exchange', 'gmail', 'exchangeserver')
Stage 3: where
| where isnotempty(SrcFileName)
Stage 4: extend
| extend file_type = extract(@'\.(\w+)$', 1, SrcFileName)
Stage 5: where
| where file_type in~ (f_types)
Stage 6: extend
| extend AccountCustomEntity = DstUserName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventCategoryType | in |
| field:"EventCategoryType" kind:in |
SrcFileName | is_not_null | field:"SrcFileName" kind:is_not_null | |
file_type | in |
| field:"file_type" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
file_type | extend |
AccountCustomEntity | extend |