Detection rules › Kusto
Kusto rules: 2e
AWSCloudTrail - Creating keys with encrypt policy without MFA
#Identifies AWS KMS key creation or policy changes that allow kms:Encrypt or kms:* to any principal. This configuration can expose encryption capability outside approved boundaries and should be investigated as overly permissive or malicious.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
Rule Body
id: 454133a7-5427-4a7c-bdc4-0adfa84dda16
name: AWSCloudTrail - Creating keys with encrypt policy without MFA
description: |
Identifies AWS KMS key creation or policy changes that allow `kms:Encrypt` or `kms:*` to any principal. This configuration can expose encryption capability outside approved boundaries and should be investigated as overly permissive or malicious.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AWS
dataTypes:
- AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1485
query: |
let check_actions = AWSCloudTrail
| where EventName in ("CreateKey", "PutKeyPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
| extend state=parse_json(parse_json(replace_string(tostring(parse_json(RequestParameters)['policy']),'\\"','"')).['Statement'])
| mv-expand state
| extend Action= tostring(parse_json(state.['Action'][0])), Effect=tostring(parse_json(state.['Effect'])),
Principal=tostring(parse_json(state.['Principal']))
| where Action in ("kms:Encrypt", "kms:*") and (Effect == 'Allow') and (Principal has "*")
| distinct AwsEventId;
AWSCloudTrail
| where EventName in ("CreateKey", "PutKeyPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
| 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]), "")
| join kind=inner (check_actions) on AwsEventId
| extend timestamp = TimeGenerated
| project-away AwsEventId1
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- identifier: CloudAppAccountId
columnName: RecipientAccountId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIpAddress
customDetails:
AwsEventId: AwsEventId
EventName: EventName
UserIdentityArn: UserIdentityArn
alertDetailsOverride:
alertDisplayNameFormat: 'AWS KMS key creation or policy update by {{AccountName}} without MFA'
alertDescriptionFormat: 'KMS key creation or policy update by {{AccountName}} without MFA resulted in permissive encrypt access. AwsEventId: {{AwsEventId}}'
version: 1.0.3
kind: Scheduled
Stages and Predicates
Let Binding: check_actions
let check_actions = AWSCloudTrail
| where EventName in ("CreateKey", "PutKeyPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
| extend state=parse_json(parse_json(replace_string(tostring(parse_json(RequestParameters)['policy']),'\\"','"')).['Statement'])
| mv-expand state
| extend Action= tostring(parse_json(state.['Action'][0])), Effect=tostring(parse_json(state.['Effect'])),
Principal=tostring(parse_json(state.['Principal']))
| where Action in ("kms:Encrypt", "kms:*") and (Effect == 'Allow') and (Principal has "*")
| distinct AwsEventId;
Stage 1: source
AWSCloudTrail
Stage 2: where
| where EventName in ("CreateKey", "PutKeyPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
Stage 3: 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 4: join
| join kind=inner (check_actions) on AwsEventId
Stage 5: extend
extend timestamp
Stage 6: project-away
project-away AwsEventId1
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | in |
| field:"Action" kind:in |
Effect | eq |
| field:"Effect" kind:eq value:"Allow" |
ErrorCode | is_null | field:"ErrorCode" kind:is_null | |
ErrorMessage | is_null | field:"ErrorMessage" kind:is_null | |
EventName | in |
| field:"aws::eventName" kind:in |
Principal | match |
| field:"Principal" kind:match value:"*" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
UserIdentityArn | extend |
UserName | extend |
AccountName | extend |
AccountUPNSuffix | extend |
timestamp | extend |
Azure DevOps Pipeline modified by a new user
#'There are several potential pipeline steps that could be modified by an attacker to inject malicious code into the build cycle. A likely attacker path is the modification to an existing pipeline that they have access to. This detection looks for users modifying a pipeline when they have not previously been observed modifying or creating that pipeline before. This query also joins events with data to Microsoft Entra ID Protection in order to show if the user conducting the action has any associated Microsoft Entra ID Protection alerts. You can also choose to filter this detection to only alert when the user also has Microsoft Entra ID Protection alerts associated with them.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Defense Impairment |
Rule Body
id: 155e9134-d5ad-4a6f-88f3-99c220040b66
name: Azure DevOps Pipeline modified by a new user
description: |
'There are several potential pipeline steps that could be modified by an attacker to inject malicious code into the build cycle. A likely attacker path is the modification to an existing pipeline that they have access to.
This detection looks for users modifying a pipeline when they have not previously been observed modifying or creating that pipeline before. This query also joins events with data to Microsoft Entra ID Protection in order to show if the user conducting the action has any associated Microsoft Entra ID Protection alerts. You can also choose to filter this detection to only alert when the user also has Microsoft Entra ID Protection alerts associated with them.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Execution
- DefenseEvasion
relevantTechniques:
- T1578
- T1569
query: |
// Set the lookback to determine if user has created pipelines before
let timeback = 14d;
// Set the period for detections
let timeframe = 1d;
// Get a list of previous Release Pipeline creators to exclude
let releaseusers = ADOAuditLogs
| where TimeGenerated > ago(timeback) and TimeGenerated < ago(timeframe)
| where OperationName in ("Release.ReleasePipelineCreated", "Release.ReleasePipelineModified")
// We want to look for users performing actions in specific projects so we create this userscope object to match on
| extend UserScope = strcat(ActorUserId, "-", ProjectName)
| summarize by UserScope;
// Get Release Pipeline creations by new users
ADOAuditLogs
| where TimeGenerated > ago(timeframe)
| where OperationName =~ "Release.ReleasePipelineModified"
| extend UserScope = strcat(ActorUserId, "-", ProjectName)
| where UserScope !in (releaseusers)
| extend ActorUPN = tolower(ActorUPN)
| project-away Id, ActivityId, ActorCUID, ScopeId, ProjectId, TenantId, SourceSystem, UserScope
// See if any of these users have Azure AD alerts associated with them in the same timeframe
| join kind = leftouter (
SecurityAlert
| where TimeGenerated > ago(timeframe)
| where ProviderName == "IPC"
| extend AadUserId = tostring(parse_json(Entities)[0].AadUserId)
| summarize Alerts=count() by AadUserId) on $left.ActorUserId == $right.AadUserId
| extend Alerts = iif(isnotempty(Alerts), Alerts, 0)
// Uncomment the line below to only show results where the user as AADIdP alerts
//| where Alerts > 0
| extend timestamp = TimeGenerated
| extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ActorUPN
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpAddress
version: 1.0.8
kind: Scheduled
Stages and Predicates
Parameters
let timeback = 14d;
let timeframe = 1d;
Let Binding: releaseusers
let releaseusers = ADOAuditLogs
| where TimeGenerated > ago(timeback) and TimeGenerated < ago(timeframe)
| where OperationName in ("Release.ReleasePipelineCreated", "Release.ReleasePipelineModified")
| extend UserScope = strcat(ActorUserId, "-", ProjectName)
| summarize by UserScope;
Stage 1: source
ADOAuditLogs
Stage 2: where
| where TimeGenerated > ago(timeframe)
Stage 3: where
| where OperationName =~ "Release.ReleasePipelineModified"
Stage 4: extend
| extend UserScope = strcat(ActorUserId, "-", ProjectName)
Stage 5: where
| where UserScope !in (releaseusers)
Stage 6: extend
| extend ActorUPN = tolower(ActorUPN)
Stage 7: project-away
| project-away Id, ActivityId, ActorCUID, ScopeId, ProjectId, TenantId, SourceSystem, UserScope
Stage 8: join
| join kind = leftouter (
SecurityAlert
| where TimeGenerated > ago(timeframe)
| where ProviderName == "IPC"
| extend AadUserId = tostring(parse_json(Entities)[0].AadUserId)
| summarize Alerts=count() by AadUserId) on $left.ActorUserId == $right.AadUserId
Stage 9: extend (3 consecutive steps)
extend AccountName, AccountUPNSuffix, Alerts, timestamp
Stage 10: summarize aggregation inside the join branch
summarize by AadUserId
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | eq |
| field:"OperationName" kind:eq value:"Release.ReleasePipelineModified" |
ProviderName | eq |
| field:"ProviderName" kind:eq value:"IPC" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
UserScope | eq | releaseusers | excludes:UserScope field:"UserScope" value:"releaseusers" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AadUserId | summarize |
CDM_ContinuousDiagnostics&Mitigation_PostureChanged
#'This alert is designed to monitor Azure policies aligned with the Continuous Diagnostics & Mitigation (CDM) Program. The alert triggers when policy compliance falls below 70% within a 1 week timeframe.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Discovery |
Rule Body
id: fd950af9-d9db-4879-a60a-7267cc041beb
name: CDM_ContinuousDiagnostics&Mitigation_PostureChanged
description: |
'This alert is designed to monitor Azure policies aligned with the Continuous Diagnostics & Mitigation (CDM) Program. The alert triggers when policy compliance falls below 70% within a 1 week timeframe.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 7d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Discovery
relevantTechniques:
- T1082
query: |
SecurityRecommendation
| where RecommendationDisplayName <> ""
| extend MaturityLevel=iff(RecommendationDisplayName has_any("log", "defender", "asset", "arc"), "Hardware Asset Management (HWAM)",
iff(RecommendationDisplayName has_any("allow", "software", "application"), "Software Asset Management (SWAM)",
iff(RecommendationDisplayName has_any("account", "user", "identity", "trust"), "User Trust (TRUST)",
iff(RecommendationDisplayName has_any("access", "auth", "key", "cert", "token"), "Credentials & Authenticators (CRED)",
iff(RecommendationDisplayName has_any("priv", "admin", "root"), "Privileges (PRIV)",
iff(RecommendationDisplayName has_any("network", "internet", "traffic", "firewall", "intrusion", "bound", "tls", "gateway", "subnet", "web", "url", "proxy", "just", "port", "JIT", "http"), "Boundary Protection (BOUND)",
iff(RecommendationDisplayName has_any("event", "agent", "incident", "back", "privacy", "audit", "collect"), "Manage Events (MNGEVT)",
iff(RecommendationDisplayName has_any("edr", "malware", "endpoint protection", "detect", "respon"), "Endpoint Detection & Response (EDR)",
iff(RecommendationDisplayName has_any("aks", "contain", "kube", "supply"), "Design & Build in Security (DBS)",
iff(RecommendationDisplayName has_any("data", "storage", "sql", "cmk", "key"), "Data Protection Management (DPM)", "Other"))))))))))
| where MaturityLevel in ('Hardware Asset Management (HWAM)', 'Software Asset Management (SWAM)', 'User Trust (TRUST)', 'Credentials & Authenticators (CRED)', 'Privileges (PRIV)', 'Boundary Protection (BOUND)', 'Manage Events (MNGEVT)', 'Endpoint Detection & Response (EDR)', 'Design & Build in Security (DBS)', 'Data Protection Management (DPM)')
| summarize arg_max(TimeGenerated, *) by AssessedResourceId, RecommendationDisplayName
| summarize
Failed = countif(RecommendationState == "Unhealthy"),
Passed = countif(RecommendationState == "Healthy"),
NotApplicable = countif(RecommendationState == "NotApplicable" or RecommendationState == "Removed"),
Applicable = countif(RecommendationState == "Unhealthy" or RecommendationState == "Healthy"),
Total = countif(RecommendationState == "Unhealthy" or RecommendationState == "Healthy" or RecommendationState == "NotApplicable" or RecommendationState == "Removed")
by MaturityLevel
| extend PassedControls = (Passed / todouble(Applicable)) * 100
| project MaturityLevel, Total, PassedControls, Passed, Failed, Applicable, NotApplicable
| sort by Total, Passed desc
| extend RemediationLink = strcat('https://portal.azure.com/#blade/Microsoft_Azure_Security/SecurityMenuBlade/22')
| project CapabilityArea=MaturityLevel, Total, PassedControls, Passed, Failed, RemediationLink, LastObserved=now()
| where CapabilityArea <> ''
| where PassedControls < 70
//Adjust Either FailedRatePercentage or PassedRatePercentage Thresholds within organizational requirements
| sort by PassedControls desc
| extend URLCustomEntity = RemediationLink
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: URLCustomEntity
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
SecurityRecommendation
Stage 2: where
| where RecommendationDisplayName <> ""
Stage 3: extend
| extend MaturityLevel=iff(RecommendationDisplayName has_any("log", "defender", "asset", "arc"), "Hardware Asset Management (HWAM)",
iff(RecommendationDisplayName has_any("allow", "software", "application"), "Software Asset Management (SWAM)",
iff(RecommendationDisplayName has_any("account", "user", "identity", "trust"), "User Trust (TRUST)",
iff(RecommendationDisplayName has_any("access", "auth", "key", "cert", "token"), "Credentials & Authenticators (CRED)",
iff(RecommendationDisplayName has_any("priv", "admin", "root"), "Privileges (PRIV)",
iff(RecommendationDisplayName has_any("network", "internet", "traffic", "firewall", "intrusion", "bound", "tls", "gateway", "subnet", "web", "url", "proxy", "just", "port", "JIT", "http"), "Boundary Protection (BOUND)",
iff(RecommendationDisplayName has_any("event", "agent", "incident", "back", "privacy", "audit", "collect"), "Manage Events (MNGEVT)",
iff(RecommendationDisplayName has_any("edr", "malware", "endpoint protection", "detect", "respon"), "Endpoint Detection & Response (EDR)",
iff(RecommendationDisplayName has_any("aks", "contain", "kube", "supply"), "Design & Build in Security (DBS)",
iff(RecommendationDisplayName has_any("data", "storage", "sql", "cmk", "key"), "Data Protection Management (DPM)", "Other"))))))))))
MaturityLevel =(RecommendationDisplayName has "log" or RecommendationDisplayName has "defender" or RecommendationDisplayName has "asset" or RecommendationDisplayName has "arc")"Hardware Asset Management (HWAM)"iff((RecommendationDisplayName has_any ("allow", "software", "application")), "Software Asset Management (SWAM)", iff((RecommendationDisplayName has_any ("account", "user", "identity", "trust")), "User Trust (TRUST)", iff((RecommendationDisplayName has_any ("access", "auth", "key", "cert", "token")), "Credentials & Authenticators (CRED)", iff((RecommendationDisplayName has_any ("priv", "admin", "root")), "Privileges (PRIV)", iff((RecommendationDisplayName has_any ("network", "internet", "traffic", "firewall", "intrusion", "bound", "tls", "gateway", "subnet", "web", "url", "proxy", "just", "port", "JIT", "http")), "Boundary Protection (BOUND)", iff((RecommendationDisplayName has_any ("event", "agent", "incident", "back", "privacy", "audit", "collect")), "Manage Events (MNGEVT)", iff((RecommendationDisplayName has_any ("edr", "malware", "endpoint protection", "detect", "respon")), "Endpoint Detection & Response (EDR)", iff((RecommendationDisplayName has_any ("aks", "contain", "kube", "supply")), "Design & Build in Security (DBS)", iff((RecommendationDisplayName has_any ("data", "storage", "sql", "cmk", "key")), "Data Protection Management (DPM)", "Other")))))))))Stage 4: where
| where MaturityLevel in ('Hardware Asset Management (HWAM)', 'Software Asset Management (SWAM)', 'User Trust (TRUST)', 'Credentials & Authenticators (CRED)', 'Privileges (PRIV)', 'Boundary Protection (BOUND)', 'Manage Events (MNGEVT)', 'Endpoint Detection & Response (EDR)', 'Design & Build in Security (DBS)', 'Data Protection Management (DPM)')
Stage 5: summarize
| summarize arg_max(TimeGenerated, *) by AssessedResourceId, RecommendationDisplayName
Stage 6: summarize
| summarize
Failed = countif(RecommendationState == "Unhealthy"),
Passed = countif(RecommendationState == "Healthy"),
NotApplicable = countif(RecommendationState == "NotApplicable" or RecommendationState == "Removed"),
Applicable = countif(RecommendationState == "Unhealthy" or RecommendationState == "Healthy"),
Total = countif(RecommendationState == "Unhealthy" or RecommendationState == "Healthy" or RecommendationState == "NotApplicable" or RecommendationState == "Removed")
by MaturityLevel
Stage 7: extend
| extend PassedControls = (Passed / todouble(Applicable)) * 100
Stage 8: project
| project MaturityLevel, Total, PassedControls, Passed, Failed, Applicable, NotApplicable
Stage 9: sort
| sort by Total, Passed desc
Stage 10: extend
| extend RemediationLink = strcat('https://portal.azure.com/#blade/Microsoft_Azure_Security/SecurityMenuBlade/22')
Stage 11: project
| project CapabilityArea=MaturityLevel, Total, PassedControls, Passed, Failed, RemediationLink, LastObserved=now()
Stage 12: where
| where CapabilityArea <> ''
Stage 13: where
| where PassedControls < 70
Stage 14: sort
| sort by PassedControls desc
Stage 15: extend
| extend URLCustomEntity = RemediationLink
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
MaturityLevel | in |
| field:"MaturityLevel" kind:in |
PassedControls | lt |
| field:"PassedControls" kind:lt value:"70" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CapabilityArea | project |
Failed | project |
LastObserved | project |
Passed | project |
PassedControls | project |
RemediationLink | project |
Total | project |
URLCustomEntity | extend |
Cross-tenant Access Settings Organization Outbound Collaboration Settings Changed
#Organizations are added in the Cross-tenant Access Settings to control communication inbound or outbound for users and applications. This detection notifies when Organization Outbound Collaboration Settings are changed for "Users & Groups" and for "Applications".
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence | |
| Discovery |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Rules Detecting the Same Action
These rules filter on the same operation.
- Cross-tenant Access Settings Organization Added (Kusto)
- Cross-tenant Access Settings Organization Deleted (Kusto)
- Cross-tenant Access Settings Organization Inbound Collaboration Settings Changed (Kusto)
- Cross-tenant Access Settings Organization Inbound Direct Settings Changed (Kusto)
- Cross-tenant Access Settings Organization Outbound Direct Settings Changed (Kusto)
Rule Body
id: 229f71ba-d83b-42a5-b83b-11a641049ed1
name: Cross-tenant Access Settings Organization Outbound Collaboration Settings Changed
description: |
'Organizations are added in the Cross-tenant Access Settings to control communication inbound or outbound for users and applications. This detection notifies when Organization Outbound Collaboration Settings are changed for "Users & Groups" and for "Applications".'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 2d
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
- Persistence
- Discovery
relevantTechniques:
- T1078.004
- T1136.003
- T1087.004
query: |
// In User & Groups and in Applications, the following "AccessType" values in columns PremodifiedOutboundSettings and ModifiedOutboundSettings are interpreted accordingly
// When Access Type in premodified outbound settings value was 1 that means that the initial access was allowed. When Access Type in premodified outbound settings value was 2 that means that the initial access was blocked.
// When Access Type in modified outbound settings value is 1 that means that now access is allowed. When Access Type in modified outbound settings value is 2 that means that now access is blocked.
AuditLogs
| where OperationName has "Update a partner cross-tenant access setting"
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "Policy"
| extend Properties = TargetResource.modifiedProperties
)
| mv-apply Property = Properties on
(
where Property.displayName =~ "b2bCollaborationOutbound"
| extend PremodifiedOutboundSettings = trim('"',tostring(Property.oldValue)),
ModifiedOutboundSettings = trim(@'"',tostring(Property.newValue))
)
| where PremodifiedOutboundSettings != ModifiedOutboundSettings
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: InitiatingAppName
- identifier: AadUserId
columnName: InitiatingAppServicePrincipalId
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatingUserPrincipalName
- identifier: Name
columnName: InitiatingAccountName
- identifier: UPNSuffix
columnName: InitiatingAccountUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAadUserId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: InitiatingIpAddress
version: 1.1.1
kind: Scheduled
Stages and Predicates
Stage 1: source
AuditLogs
Stage 2: where
| where OperationName has "Update a partner cross-tenant access setting"
Stage 3: kusto:mv-apply
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "Policy"
| extend Properties = TargetResource.modifiedProperties
)
Stage 4: kusto:mv-apply
| mv-apply Property = Properties on
(
where Property.displayName =~ "b2bCollaborationOutbound"
| extend PremodifiedOutboundSettings = trim('"',tostring(Property.oldValue)),
ModifiedOutboundSettings = trim(@'"',tostring(Property.newValue))
)
Stage 5: where
| where PremodifiedOutboundSettings != ModifiedOutboundSettings
Stage 6: extend (6 consecutive steps)
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | match |
| field:"OperationName" kind:match value:"Update a partner cross-tenant access setting" |
Property.displayName | eq |
| field:"Property.displayName" kind:eq value:"b2bCollaborationOutbound" |
TargetResource.type | eq |
| field:"TargetResource.type" kind:eq value:"Policy" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
InitiatingAppName | extend |
InitiatingAppServicePrincipalId | extend |
InitiatingUserPrincipalName | extend |
InitiatingAadUserId | extend |
InitiatingIpAddress | extend |
InitiatingAccountName | extend |
InitiatingAccountUPNSuffix | extend |
Cynerio - IoT - Default password
#User signed in using default credentials
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: 84e0ea1f-766d-4775-836a-c0c9cca05085
name: Cynerio - IoT - Default password
description: User signed in using default credentials
severity: High
tags:
- Schema: ASimAuthentication
SchemaVersion: 0.1.3
requiredDataConnectors:
- connectorId: CynerioSecurityEvents
dataTypes:
- CynerioEvent_CL
queryFrequency: 5h
queryPeriod: 5h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1552
query: |
CynerioEvent_CL
| where TimeGenerated > ago(24h)
| where module_s == 'IDS'
| where title_s == 'Default Password'
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: asset_ip_s
eventGroupingSettings:
aggregationKind: SingleAlert
customDetails:
Service: service_s
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
CynerioEvent_CL
Stage 2: where
| where TimeGenerated > ago(24h)
Stage 3: where
| where module_s == 'IDS'
Stage 4: where
| where title_s == 'Default Password'
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
module_s | eq |
| field:"module_s" kind:eq value:"IDS" |
title_s | eq |
| field:"title_s" kind:eq value:"Default Password" |
Dataverse - TI map URL to DataverseActivity
#Identifies a match in DataverseActivity from any URL IOC from Microsoft Sentinel Threat Intelligence.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Initial Access | |
| Initial Access | |
| Execution | |
| Persistence |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft 365 | any: Dataverse and model-driven apps audit (catch-all) |
Rule Body
id: d88a0e22-3b6a-40c2-af28-c064b44d03b7
kind: Scheduled
name: Dataverse - TI map URL to DataverseActivity
description: Identifies a match in DataverseActivity from any URL IOC from Microsoft
Sentinel Threat Intelligence.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Dataverse
dataTypes:
- DataverseActivity
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Execution
- Persistence
relevantTechniques:
- T1566
- T1456
- T1474
- T0819
- T0865
- T0862
- T0863
- T1204
- T1574
- T0873
query: |
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
ThreatIntelligenceIndicator
| where TimeGenerated >= ago(ioc_lookBack) and ExpirationDateTime > now()
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
| where Active == true
| where isnotempty(Url)
| join kind=innerunique (
DataverseActivity
| where TimeGenerated >= ago(dt_lookBack)
| where Message in ("Create", "Update")
| where isnotempty(Fields) and Fields has "http"
| extend
ExtractedUrls = extract_all("(http[s]?://(?:[a-zA-Z\\.-]|[0-9])+)", tostring(Fields)),
DataverseActivity_TimeGenerated = TimeGenerated
| mv-expand Url = ExtractedUrls
| project
DataverseActivity_TimeGenerated,
tostring(Url),
UserId,
ClientIp,
InstanceUrl,
EntityName
)
on Url
| where DataverseActivity_TimeGenerated < ExpirationDateTime
| summarize DataverseActivity_TimeGenerated = arg_max(DataverseActivity_TimeGenerated, *) by IndicatorId, Url
| extend
CloudAppId = int(32780),
AccountName = tostring(split(UserId, '@')[0]),
UPNSuffix = tostring(split(UserId, '@')[1])
| project
DataverseActivity_TimeGenerated,
Description,
ActivityGroupNames,
IndicatorId,
ThreatType,
ExpirationDateTime,
ConfidenceScore,
UserId,
ClientIp,
InstanceUrl,
CloudAppId,
AccountName,
UPNSuffix,
Url
eventGroupingSettings:
aggregationKind: AlertPerResult
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ClientIp
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
- entityType: CloudApplication
fieldMappings:
- identifier: AppId
columnName: CloudAppId
- identifier: InstanceName
columnName: InstanceUrl
alertDetailsOverride:
alertDisplayNameFormat: Dataverse - TI match on URL in {{InstanceUrl}}
alertDescriptionFormat: Malicous IP {{Url}} was found in {{InstanceUrl}}. Associated
user is {{UserId}}
version: 3.2.0
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
Stage 1: source
ThreatIntelligenceIndicator
Stage 2: where
| where TimeGenerated >= ago(ioc_lookBack) and ExpirationDateTime > now()
Stage 3: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
Stage 4: where
| where Active == true
Stage 5: where
| where isnotempty(Url)
Stage 6: join
| join kind=innerunique (
DataverseActivity
| where TimeGenerated >= ago(dt_lookBack)
| where Message in ("Create", "Update")
| where isnotempty(Fields) and Fields has "http"
| extend
ExtractedUrls = extract_all("(http[s]?://(?:[a-zA-Z\\.-]|[0-9])+)", tostring(Fields)),
DataverseActivity_TimeGenerated = TimeGenerated
| mv-expand Url = ExtractedUrls
| project
DataverseActivity_TimeGenerated,
tostring(Url),
UserId,
ClientIp,
InstanceUrl,
EntityName
)
on Url
Stage 7: where
where DataverseActivity_TimeGenerated < ExpirationDateTime
Stage 8: summarize
summarize DataverseActivity_TimeGenerated by IndicatorId, Url
Stage 9: extend
extend AccountName, CloudAppId, UPNSuffix
Stage 10: project
project AccountName, ActivityGroupNames, ClientIp, CloudAppId, ConfidenceScore, DataverseActivity_TimeGenerated, Description, ExpirationDateTime, IndicatorId, InstanceUrl, ThreatType, UPNSuffix, Url, UserId
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Active | eq |
| field:"Active" kind:eq value:"true" |
Fields | is_not_null | field:"Fields" kind:is_not_null | |
Fields | match |
| field:"Fields" kind:match value:"http" |
Message | in |
| field:"Message" kind:in |
Url | is_not_null | field:"Url" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
ActivityGroupNames | project |
ClientIp | project |
CloudAppId | project |
ConfidenceScore | project |
DataverseActivity_TimeGenerated | project |
Description | project |
ExpirationDateTime | project |
IndicatorId | project |
InstanceUrl | project |
ThreatType | project |
UPNSuffix | project |
Url | project |
UserId | project |
Detect URLs containing known malicious keywords or commands (ASIM Web Session)
#'The utilization of system commands or functions in the request URL may suggest that an attacker is trying to gain unauthorized access to the environment by exploiting a vulnerable service.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Command & Control |
Rule Body
id: 32c08696-2e37-4730-86f8-97d9c8b184c9
name: Detect URLs containing known malicious keywords or commands (ASIM Web Session)
description: |
'The utilization of system commands or functions in the request URL may suggest that an attacker is trying to gain unauthorized access to the environment by exploiting a vulnerable service.'
severity: High
status: Available
tags:
- Schema: WebSession
SchemaVersion: 0.2.6
requiredDataConnectors: []
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- CommandAndControl
relevantTechniques:
- T1190
- T1133
- T1071
query: |
let lookback = 5m;
let RiskyCommandsInUrl = materialize(externaldata(Commands: string)
[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/CommandsInURL.csv"]
with(format="csv", ignoreFirstRecord=True));
let CustomRiskyCommandsInUrl = (_ASIM_GetWatchlistRaw("Web_RiskyCommandsInUrl") // Create new Watchlist and add your custom indicators(Optional)
| extend
Commands = tostring(WatchlistItem["Commands"])
| project Commands
| where isnotempty(Commands));
let CombinedRiskyCommandsInUrl = union RiskyCommandsInUrl, CustomRiskyCommandsInUrl;
let knownRiskyCommandsInUrl=toscalar(CombinedRiskyCommandsInUrl
| where isnotempty(Commands)
| summarize make_set(Commands, 1000));
// You can add more keywords to the query as necessary, depending on the specific indicators you want to detect.
_Im_WebSession (starttime=ago(lookback), eventresult='Success', url_has_any=knownRiskyCommandsInUrl)
| where isnotempty(Url)
| project Url, SrcIpAddr, SrcUsername, SrcHostname, DstIpAddr, TimeGenerated
| extend Decoded_url = url_decode(Url)
| where Decoded_url has_any (knownRiskyCommandsInUrl)
| summarize
EventCount=count(),
EventStartTime=min(TimeGenerated),
EventEndTime=max(TimeGenerated)
by SrcIpAddr, SrcUsername, SrcHostname, Url, Decoded_url, DstIpAddr
| extend
Name = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 0)[0]), SrcUsername),
UPNSuffix = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 1)[0]), "")
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
- entityType: IP
fieldMappings:
- identifier: Address
columnName: DstIpAddr
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: SrcUsername
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
eventGroupingSettings:
aggregationKind: AlertPerResult
customDetails:
EventCount: EventCount
Decoded_url: Decoded_url
EventStartTime: EventStartTime
EventEndTime: EventEndTime
alertDetailsOverride:
alertDisplayNameFormat: "User '{{SrcUsername}}' with IP '{{SrcIpAddr}}' has been identified as making request for URL '{{Url}}' that includes a recognizable malicious command"
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let lookback = 5m;
let CombinedRiskyCommandsInUrl = union RiskyCommandsInUrl, CustomRiskyCommandsInUrl;
Let Binding: RiskyCommandsInUrl
let RiskyCommandsInUrl = materialize(externaldata(Commands: string)
[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/CommandsInURL.csv"]
with(format="csv", ignoreFirstRecord=True));
Let Binding: CustomRiskyCommandsInUrl
let CustomRiskyCommandsInUrl = (_ASIM_GetWatchlistRaw("Web_RiskyCommandsInUrl")
| extend
Commands = tostring(WatchlistItem["Commands"])
| project Commands
| where isnotempty(Commands));
Let Binding: knownRiskyCommandsInUrl
let knownRiskyCommandsInUrl = toscalar(CombinedRiskyCommandsInUrl
| where isnotempty(Commands)
| summarize make_set(Commands, 1000));
Stage 1: source
_Im_WebSession (starttime=ago(lookback), eventresult='Success', url_has_any=knownRiskyCommandsInUrl)
Stage 2: where
| where isnotempty(Url)
Stage 3: project
| project Url, SrcIpAddr, SrcUsername, SrcHostname, DstIpAddr, TimeGenerated
Stage 4: extend
| extend Decoded_url = url_decode(Url)
Stage 5: where
| where Decoded_url has_any (knownRiskyCommandsInUrl)
Stage 6: summarize
| summarize
EventCount=count(),
EventStartTime=min(TimeGenerated),
EventEndTime=max(TimeGenerated)
by SrcIpAddr, SrcUsername, SrcHostname, Url, Decoded_url, DstIpAddr
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 |
|---|---|---|---|
Decoded_url | match |
| field:"Decoded_url" kind:match value:"knownRiskyCommandsInUrl" |
Url | is_not_null | field:"Url" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Decoded_url | summarize |
DstIpAddr | summarize |
EventCount | summarize |
EventEndTime | summarize |
EventStartTime | summarize |
SrcHostname | summarize |
SrcIpAddr | summarize |
SrcUsername | summarize |
Url | summarize |
Name | extend |
UPNSuffix | extend |
GitHub - Repository was created
#Detect activities when a repository was created. This query runs every day and its severity is Medium.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| GitHub | Audit log action repo.create: Repository created |
Rules Detecting the Same Action
These rules filter on the same operation.
- Github Public Repository Created (Panther)
- GitHub Repo Created (Elastic)
- GitHub Repository Created (Panther)
- NX Supply Chain - S1ngularity Repository Detection (Panther)
Rule Body
id: 0b85a077-8ba5-4cb5-90f7-1e882afe10c2
name: GitHub - Repository was created
description: |
'Detect activities when a repository was created. This query runs every day and its severity is Medium.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |
GitHubAuditData
| where Action == "repo.create"
| extend AccountName = tostring(split(Actor, "@")[0]), AccountUPNSuffix = tostring(split(Actor, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Actor
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
GitHubAuditData
Stage 2: where
| where Action == "repo.create"
Stage 3: extend
| extend AccountName = tostring(split(Actor, "@")[0]), AccountUPNSuffix = tostring(split(Actor, "@")[1])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | eq |
| field:"Action" kind:eq value:"repo.create" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | extend |
AccountUPNSuffix | extend |
Mimecast Data Leak Prevention - Hold
#Detects threat for data leak when action is hold
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Rule Body
id: 8e52bcf1-4f50-4c39-8678-d9efad64e379
name: Mimecast Data Leak Prevention - Hold
description: Detects threat for data leak when action is hold
severity: Informational
requiredDataConnectors:
- connectorId: MimecastSEGAPI
dataTypes:
- MimecastDLP
enabled: true
query: |
MimecastDLP
| where Action == "hold"
| extend SenderAddress = ['Sender Address'] ,RecipientAddress = ['Recipient Address']
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 5h
suppressionEnabled: false
tactics:
- Exfiltration
relevantTechniques:
- T1030
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: P7D
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: MailMessage
fieldMappings:
- identifier: Sender
columnName: SenderAddress
- identifier: Recipient
columnName: RecipientAddress
- identifier: DeliveryAction
columnName: Action
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
MimecastDLP
Stage 2: where
| where Action == "hold"
Stage 3: extend
| extend SenderAddress = ['Sender Address'] ,RecipientAddress = ['Recipient Address']
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | eq |
| field:"Action" kind:eq value:"hold" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
RecipientAddress | extend |
SenderAddress | extend |
PaloAlto - Inbound connection to high risk ports
#'Detects inbound connection to high risk ports.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: b2dd2dac-51c9-11ec-bf63-0242ac130002
name: PaloAlto - Inbound connection to high risk ports
description: |
'Detects inbound connection to high risk ports.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
let HighRiskPorts = dynamic(['21', '22', '23', '25', '53', '443', '110', '135', '137', '138', '139', '1433', '1434']);
PaloAltoCDLEvent
| where EventResourceId =~ 'TRAFFIC'
| where ipv4_is_private(SrcIpAddr) == false
| where DstPortNumber in (HighRiskPorts)
| extend IPCustomEntity = SrcIpAddr
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.3
kind: Scheduled
Stages and Predicates
Parameters
let HighRiskPorts = dynamic(['21', '22', '23', '25', '53', '443', '110', '135', '137', '138', '139', '1433', '1434']);
Stage 1: source
PaloAltoCDLEvent
Stage 2: where
| where EventResourceId =~ 'TRAFFIC'
Stage 3: where
| where ipv4_is_private(SrcIpAddr) == false
Stage 4: where
| where DstPortNumber in (HighRiskPorts)
Stage 5: extend
| extend IPCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DstPortNumber | in |
| field:"DstPortNumber" kind:in |
EventResourceId | eq |
| field:"EventResourceId" kind:eq value:"TRAFFIC" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
SrcIpAddr | 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:SrcIpAddr |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
IPCustomEntity | extend |
Pathlock TDnR - SAP Cloud Account Administration Events
#Detects account administration events in SAP Cloud environments, forwarded by Pathlock Threat Detection and Response. Suspicious cloud account activities may indicate unauthorized provisioning, privilege escalation, or account takeover in SAP cloud tenants.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence |
Rule Body
id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c21
name: Pathlock TDnR - SAP Cloud Account Administration Events
kind: Scheduled
description: >-
Detects account administration events in SAP Cloud environments, forwarded by Pathlock Threat
Detection and Response. Suspicious cloud account activities may indicate unauthorized provisioning,
privilege escalation, or account takeover in SAP cloud tenants.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Pathlock_TDnR
dataTypes:
- Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Persistence
relevantTechniques:
- T1078
- T1136
query: |
Pathlock_TDnR_CL
| where DataSource == "CLOUD_ACCOUNT_LOGS"
| 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 == "CLOUD_ACCOUNT_LOGS"
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:"CLOUD_ACCOUNT_LOGS" |
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 |
Privileged Role Assigned Outside PIM
#Identifies a privileged role being assigned to a user outside of PIM Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#things-to-monitor-1
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Add member to role |
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: 269435e3-1db8-4423-9dfc-9bf59997da1c
name: Privileged Role Assigned Outside PIM
description: |
'Identifies a privileged role being assigned to a user outside of PIM
Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#things-to-monitor-1'
severity: Low
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1078.004
tags:
- AADSecOpsGuide
query: |
AuditLogs
| where Category =~ "RoleManagement"
| where OperationName has "Add member to role outside of PIM"
or (LoggedByService =~ "Core Directory" and OperationName =~ "Add member to role" and Identity != "MS-PIM" and Identity != "MS-PIM-Fairfax")
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "User"
| extend TargetUserPrincipalName = tostring(TargetResource.userPrincipalName)
)
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend TargetName = tostring(split(TargetUserPrincipalName,'@',0)[0]), TargetUPNSuffix = tostring(split(TargetUserPrincipalName,'@',1)[0])
| extend InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatingUserPrincipalName
- identifier: Name
columnName: InitiatedByName
- identifier: UPNSuffix
columnName: InitiatedByUPNSuffix
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: TargetUserPrincipalName
- identifier: Name
columnName: TargetName
- identifier: UPNSuffix
columnName: TargetUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAadUserId
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAppServicePrincipalId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: InitiatingIpAddress
version: 1.0.6
kind: Scheduled
Stages and Predicates
Stage 1: source
AuditLogs
Stage 2: where
| where Category =~ "RoleManagement"
Stage 3: where
| where OperationName has "Add member to role outside of PIM"
or (LoggedByService =~ "Core Directory" and OperationName =~ "Add member to role" and Identity != "MS-PIM" and Identity != "MS-PIM-Fairfax")
Stage 4: kusto:mv-apply
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "User"
| extend TargetUserPrincipalName = tostring(TargetResource.userPrincipalName)
)
Stage 5: extend (7 consecutive steps)
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend TargetName = tostring(split(TargetUserPrincipalName,'@',0)[0]), TargetUPNSuffix = tostring(split(TargetUserPrincipalName,'@',1)[0])
| extend InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Category | eq |
| field:"Category" kind:eq value:"RoleManagement" |
Identity | ne |
| field:"Identity" kind:ne |
LoggedByService | eq |
| field:"azure_ad::logged_by_service" kind:eq value:"Core Directory" |
OperationName | eq |
| field:"OperationName" kind:eq value:"Add member to role" |
OperationName | match |
| field:"OperationName" kind:match value:"Add member to role outside of PIM" |
TargetResource.type | eq |
| field:"TargetResource.type" kind:eq value:"User" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
InitiatingAppName | extend |
InitiatingAppServicePrincipalId | extend |
InitiatingUserPrincipalName | extend |
InitiatingAadUserId | extend |
InitiatingIpAddress | extend |
TargetName | extend |
TargetUPNSuffix | extend |
InitiatedByName | extend |
InitiatedByUPNSuffix | extend |
PROD (TM010.1) - BITLOCKER - Stored Bitlocker Recovery Key to Tier Level Computer Object
#A Bitlocker recovery key was stored within a Tier Model computer object.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 5137: A directory service object was created. |
Rule Body
id: eea9c640-afe0-4ce4-9a41-c409d4c2c924
name: 'PROD (TM010.1) - BITLOCKER - Stored Bitlocker Recovery Key to Tier Level Computer Object'
description: |
A Bitlocker recovery key was stored within a Tier Model computer object.
severity: Informational
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
queryFrequency: 5h
queryPeriod: 5h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- CredentialAccess
relevantTechniques:
- T1552.004
query: |
SecurityEvent
| where EventID == 5137
and (EventData matches regex @"(?i)OU=Domain Controllers"
or EventData matches regex @"(?i)OU=Tier 0 PAW"
or EventData matches regex @"(?i)OU=Tier 1 PAW"
or EventData matches regex @"(?i)OU=Tier 2 PAW"
or EventData matches regex @"(?i)OU=Tier 0 Member Servers"
or EventData matches regex @"(?i)OU=Tier 1 Member Servers")
| extend ObjectClass = extract("<Data Name=\"ObjectClass\">([^<]+)</Data>", 1, EventData)
| where ObjectClass has "msFVE-RecoveryInformation"
| extend ObjectBitName_e = extract("<Data Name=\"ObjectDN\">([^<]+)</Data>", 1, EventData)
| extend ObjectFullName = extract("(?i)(CN=[^,]+,(?i)OU=[^,]+)", 1, ObjectBitName_e)
| extend ObjectName = extract("(?i)CN=([^,]+)", 1, ObjectFullName)
| extend ObjectTier = case(
ObjectFullName has "Tier 0", "Tier 0",
ObjectFullName has "Tier 1", "Tier 1",
ObjectFullName has "Tier Model Administration", "Tier 0",
ObjectFullName has "Tier Model Computer Quarantine", "Tier 0",
ObjectFullName has "Domain Controllers", "Tier 0",
"Unknown")
| extend ActivityType = extract("([a-zA-Z]+)\\.$", 1, Activity)
| extend Domain = extract("([^.]+\\.[^.]+)$", 1, Computer)
| project TimeGenerated
, ObjectName
, ObjectTier
, ObjectClass
, ActivityType
, Account
, Domain
, Computer
, Channel
, EventID
, EventData
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ObjectName
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Computer
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Account
customDetails:
ObjectName: ObjectName
ObjectTier: ObjectTier
ObjectClass: ObjectClass
ActivityType: ActivityType
Account: Account
Domain: Domain
Computer: Computer
Channel: Channel
EventID: EventID
EventData: EventData
eventGroupingSettings:
aggregationKind: AlertPerResult
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
alertDetailsOverride:
alertDisplayNameFormat: '(TM010.1) A {{ObjectTier}} Bitlocker Recovery Key was stored in AD'
alertDescriptionFormat: 'A Tier Model object Bitlocker Key was stored in AD. This Informational alert is generated as part of reporting.'
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
SecurityEvent
Stage 2: where
| where EventID == 5137
and (EventData matches regex @"(?i)OU=Domain Controllers"
or EventData matches regex @"(?i)OU=Tier 0 PAW"
or EventData matches regex @"(?i)OU=Tier 1 PAW"
or EventData matches regex @"(?i)OU=Tier 2 PAW"
or EventData matches regex @"(?i)OU=Tier 0 Member Servers"
or EventData matches regex @"(?i)OU=Tier 1 Member Servers")
Stage 3: extend
| extend ObjectClass = extract("<Data Name=\"ObjectClass\">([^<]+)</Data>", 1, EventData)
Stage 4: where
| where ObjectClass has "msFVE-RecoveryInformation"
Stage 5: extend (6 consecutive steps)
| extend ObjectBitName_e = extract("<Data Name=\"ObjectDN\">([^<]+)</Data>", 1, EventData)
| extend ObjectFullName = extract("(?i)(CN=[^,]+,(?i)OU=[^,]+)", 1, ObjectBitName_e)
| extend ObjectName = extract("(?i)CN=([^,]+)", 1, ObjectFullName)
| extend ObjectTier = case(
ObjectFullName has "Tier 0", "Tier 0",
ObjectFullName has "Tier 1", "Tier 1",
ObjectFullName has "Tier Model Administration", "Tier 0",
ObjectFullName has "Tier Model Computer Quarantine", "Tier 0",
ObjectFullName has "Domain Controllers", "Tier 0",
"Unknown")
| extend ActivityType = extract("([a-zA-Z]+)\\.$", 1, Activity)
| extend Domain = extract("([^.]+\\.[^.]+)$", 1, Computer)
Stage 6: project
| project TimeGenerated
, ObjectName
, ObjectTier
, ObjectClass
, ActivityType
, Account
, Domain
, Computer
, Channel
, EventID
, EventData
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventData | regex_match |
| field:"EventData" kind:regex_match |
EventID | eq |
| field:"EventID" kind:eq value:"5137" |
ObjectClass | match |
| field:"ObjectClass" kind:match value:"msFVE-RecoveryInformation" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | project |
ActivityType | project |
Channel | project |
Computer | project |
Domain | project |
EventData | project |
EventID | project |
ObjectClass | project |
ObjectName | project |
ObjectTier | project |
TimeGenerated | project |
SlackAudit - Unknown User Agent
#'Detects Slack workspace activity from unknown user agents by comparing recent UserAgentOriginal values against a 14d baseline of known user agents.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: 3b11f06e-4afd-4ae6-8477-c61136619ac8
name: SlackAudit - Unknown User Agent
description: |
'Detects Slack workspace activity from unknown user agents by comparing recent UserAgentOriginal values against
a 14d baseline of known user agents.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: SlackAuditAPI
dataTypes:
- SlackAudit_CL
queryFrequency: 24h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
- T1071.001
query: |
let lbperiod = 14d;
let known_UAs = SlackAudit
| where TimeGenerated > ago(lbperiod)
| where isnotempty(UserAgentOriginal)
| summarize makeset(UserAgentOriginal);
SlackAudit
| where UserAgentOriginal !in (known_UAs)
| extend AccountCustomEntity = SrcUserName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountCustomEntity
alertDetailsOverride:
alertDisplayNameFormat: Slack audit unknown user agent for {{AccountCustomEntity}}
alertDescriptionFormat: Slack activity from {{AccountCustomEntity}} used unknown user agent {{UserAgentOriginal}}
customDetails:
Account: AccountCustomEntity
UserAgent: UserAgentOriginal
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let lbperiod = 14d;
Let Binding: known_UAs
let known_UAs = SlackAudit
| where TimeGenerated > ago(lbperiod)
| where isnotempty(UserAgentOriginal)
| summarize makeset(UserAgentOriginal);
Stage 1: source
SlackAudit
Stage 2: where
| where UserAgentOriginal !in (known_UAs)
Stage 3: extend
| extend AccountCustomEntity = SrcUserName
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
UserAgentOriginal | eq | known_UAs | excludes:UserAgentOriginal field:"UserAgentOriginal" value:"known_UAs" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountCustomEntity | extend |
Suspicious Network Beacons - Microsoft Defender(MDE/M365D)
#Below query detects suspicious beaconing activity by analyzing DeviceNetworkEvents data.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control | No specific technique |
Telemetry Coverage
Rule Body
// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
// Link to original post:
// Part-1: https://mergene.medium.com/enterprise-scale-threat-hunting-network-beacon-detection-with-unsupervised-machine-learning-and-277c4c30304f
// Part-2: https://mergene.medium.com/enterprise-scale-threat-hunting-network-beacon-detection-with-unsupervised-ml-and-kql-part-2-bff46cfc1e7e
//
// Read the blog to understand how this query works and how to analyze the results.
// This query may not be able to detect beacons that have large sleep values like 6h-1d. Refactoring and additional analysis are required.
// WARNING!: Since MDE doesn't log every single network connection, there is a chance of FALSE NEGATIVES.
//
// Query parameters:
let starttime = 1d;
let endtime = 1s;
// Set the minimum beacon sleep. Increase it to get less results. the format is (hour,minute,second.milisecond).
// Be careful when changing the value. run " print ['timespan'] = make_timespan(0, x, y) " to verify you have the correct value set.
let TimeDeltaThresholdMin = make_timespan(0,0,0.001);
let TotalEventsThresholdMin = 15;
let TotalEventsThresholdMax=toint(((totimespan(starttime) - totimespan(endtime))/TimeDeltaThresholdMin));
let JitterThreshold = 50; // jitter in percentage. Set to filter out false positives: small threshold means tighter filtering/fewer results.
// Outlier thresholds. 1.5 means the value is a normal outlier, 3 means the value is far far out.
let OutlierThresholdMax = 2; //increase or decrease this value to get more or less results
// Time delta data set can have some outliers. Define how many outliers are acceptable for a beacon. Values between 1 to 3 should be fine.
let OutlierCountMax = 3; // increasing the value provides more results.
// Define how many devices can have the same beacon.
let CompromisedDeviceCountMax = 10; // increasing the value provides more results.
// Get all beacon candidates just by jitter filtering.
let BeaconCandidates = materialize (
DeviceNetworkEvents
| where Timestamp between (ago(starttime)..ago(endtime))
| where RemoteIPType !in ("Reserved", "Private", "LinkLocal", "Loopback")
| where isnotempty(RemoteIP) and RemoteIP !in ("0.0.0.0")
| where not (ipv4_is_private(RemoteIP))
| where ActionType in ("ConnectionSuccess", "CsonnectionRequest", "CsonnectionFailed") // Fix the typos if you want to inlcude connreq. and connfail.
| summarize hint.strategy=shuffle make_set(Timestamp) by DeviceId, DeviceName,InitiatingProcessAccountName, InitiatingProcessAccountDomain, InitiatingProcessFileName, RemoteIP, RemotePort
| where array_length(set_Timestamp) > TotalEventsThresholdMin and array_length(set_Timestamp) < TotalEventsThresholdMax
| project DeviceId, DeviceName,InitiatingProcessAccountName, InitiatingProcessAccountDomain, InitiatingProcessFileName, RemoteIP, RemotePort, Timestamp=array_sort_asc(set_Timestamp)
| mv-apply Timestamp to typeof(datetime) on
(
extend nextTimestamp = next(Timestamp, 1), nextInitiatingProcessAccountName = next(InitiatingProcessAccountName, 1), nextDeviceId = next(DeviceId, 1), nextDeviceName = next(DeviceName, 1), nextRemoteIP = next(RemoteIP, 1), nextRemotePort = next(RemotePort, 1), nextInitiatingProcessFileName = next(InitiatingProcessFileName, 1)
| extend TimeDeltaInSeconds = datetime_diff('second',nextTimestamp,Timestamp)
| where nextInitiatingProcessAccountName == InitiatingProcessAccountName and nextDeviceId == DeviceId and nextDeviceName == DeviceName and nextInitiatingProcessFileName == InitiatingProcessFileName and nextRemoteIP == RemoteIP and nextRemotePort == RemotePort
| project Timestamp, TimeDeltaInSeconds, DeviceId, DeviceName, InitiatingProcessAccountName, InitiatingProcessFileName, RemoteIP, RemotePort
// compute statistical values including standard deviation
| summarize count(), min(Timestamp), max(Timestamp), Duration=datetime_diff("second", max(Timestamp), min(Timestamp)),
percentiles(TimeDeltaInSeconds, 5, 25, 50, 75, 95),
TimeDeltaList=make_list(TimeDeltaInSeconds) by DeviceId, DeviceName, InitiatingProcessAccountName, InitiatingProcessFileName, RemoteIP, RemotePort
| extend (TimeDeltaInSeconds_min,TimeDeltaInSeconds_min_index,TimeDeltaInSeconds_max,TimeDeltaInSeconds_max_index,TimeDeltaInSeconds_avg,TimeDeltaInSeconds_stdev,TimeDeltaInSeconds_variance)=series_stats(TimeDeltaList)
| extend Jitter=(TimeDeltaInSeconds_stdev/TimeDeltaInSeconds_avg)*100,
BeaconSleepMin=TimeDeltaInSeconds_avg - TimeDeltaInSeconds_stdev,
BeaconSleepMax=TimeDeltaInSeconds_avg + TimeDeltaInSeconds_stdev
// Filter out impossible beacons based on jitter threshold defined.
| where Jitter < JitterThreshold
)
// Try to enricht IP with the hostname
| join kind=leftouter
(
DeviceNetworkEvents
| where Timestamp > ago(starttime+1d)
// Extract domain from the RemoteUrl
| extend Host=tostring(parse_url(iif(RemoteUrl !startswith "http", strcat(@'http://',RemoteUrl),RemoteUrl)).Host)
| extend domain = reverse(replace(@'([A-z0-9-]+\.[A-z0-9-]+\.[A-z0-9-]+)\..*',@'\1',reverse(Host)))
| project domain, RemoteIP
| summarize Domains = make_set(domain) by RemoteIP
) on RemoteIP
| project-reorder DeviceId, DeviceName, InitiatingProcessAccountName, InitiatingProcessAccountDomain
)
;
// Get potentially suspicious beacons based on CompromisedDeviceCountMax
let PotentialBeacons = materialize (
BeaconCandidates
| summarize dcount(DeviceId) by InitiatingProcessFileName, RemoteIP, RemotePort
// Filter out beacon destinations if many devices are connecting to it (like windows update)
| where dcount_DeviceId <= CompromisedDeviceCountMax
| join kind=inner BeaconCandidates on InitiatingProcessFileName, RemoteIP, RemotePort
| project-away *1
)
;
// Get candidates that can't be beacons based on outlier analysis on the time delta
let ImpossibleBeaconsByTimeDelta = materialize
(
PotentialBeacons
| extend outliers = series_outliers(TimeDeltaList)
| mv-expand TimeDeltaList, outliers to typeof(double)
| where outliers > OutlierThresholdMax or outliers < (-1 * OutlierCountMax) // outlier can be negative or positive.
| summarize count(), make_set(outliers) by DeviceId, DeviceName,InitiatingProcessAccountName, InitiatingProcessAccountDomain, InitiatingProcessFileName, RemoteIP, RemotePort
| where count_ > OutlierCountMax
)
;
// Remove ImpossibleBeaconsByTimeDelta from potentially suspicious beacons.
PotentialBeacons
| join kind=leftantisemi ImpossibleBeaconsByTimeDelta on DeviceId, DeviceName,InitiatingProcessAccountName, InitiatingProcessAccountDomain, InitiatingProcessFileName, RemoteIP, RemotePort
| extend Timestamp = min_Timestamp // just to make it easy to jump to the device timeline etc.
// if the logs have extra information, they can be used for filtering the nonmalicious destinations
| order by Jitter asc, TimeDeltaInSeconds_avg asc
Stages and Predicates
Parameters
let starttime = 1d;
let endtime = 1s;
let TimeDeltaThresholdMin = make_timespan(0,0,0.001);
let TotalEventsThresholdMin = 15;
let TotalEventsThresholdMax = toint(((totimespan(starttime) - totimespan(endtime))/TimeDeltaThresholdMin));
let JitterThreshold = 50;
let OutlierThresholdMax = 2;
let OutlierCountMax = 3;
let CompromisedDeviceCountMax = 10;
let BeaconCandidates and let PotentialBeacons are inlined into the numbered stages below.
Let Binding: ImpossibleBeaconsByTimeDelta
let ImpossibleBeaconsByTimeDelta = materialize
(
PotentialBeacons
| extend outliers = series_outliers(TimeDeltaList)
| mv-expand TimeDeltaList, outliers to typeof(double)
| where outliers > OutlierThresholdMax or outliers < (-1 * OutlierCountMax)
| summarize count(), make_set(outliers) by DeviceId, DeviceName,InitiatingProcessAccountName, InitiatingProcessAccountDomain, InitiatingProcessFileName, RemoteIP, RemotePort
| where count_ > OutlierCountMax
);
Stage 1: source
let BeaconCandidates
Stage 2: source
let PotentialBeacons
Stage 3: source
DeviceNetworkEvents
Stage 4: where
where Timestamp between (ago(86400s) .. ago(1s))
Stage 5: where
where not (RemoteIPType in ("LinkLocal", "Loopback", "Private", "Reserved"))
Stage 6: where
where not (RemoteIP == "0.0.0.0") and isnotempty(RemoteIP)
Stage 7: where
where not ((ipv4_is_in_range(RemoteIP, "10.0.0.0/8") or ipv4_is_in_range(RemoteIP, "172.16.0.0/12") or ipv4_is_in_range(RemoteIP, "192.168.0.0/16") or ipv4_is_in_range(RemoteIP, "169.254.0.0/16") or ipv4_is_in_range(RemoteIP, "127.0.0.0/8")))
Stage 8: where
where ActionType in ("ConnectionSuccess", "CsonnectionFailed", "CsonnectionRequest")
Stage 9: summarize
summarize by DeviceId, DeviceName, InitiatingProcessAccountName, InitiatingProcessAccountDomain, InitiatingProcessFileName, RemoteIP, RemotePort
Stage 10: where
where <unknown:array_length(, set_Timestamp)> < TotalEventsThresholdMax and set_Timestamp > 15
Stage 11: project
project DeviceId, DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessFileName, RemoteIP, RemotePort, Timestamp
Stage 12: kusto:mv-apply
kusto:mv-apply Jitter < JitterThreshold and nextDeviceId =~ DeviceId and nextDeviceName =~ DeviceName and nextInitiatingProcessAccountName =~ InitiatingProcessAccountName and nextInitiatingProcessFileName =~ InitiatingProcessFileName and nextRemoteIP =~ RemoteIP and nextRemotePort =~ RemotePort
Stage 13: join
join kind=leftouter (DeviceNetworkEvents) on RemoteIP
Stage 14: project-reorder
project-reorder
Stage 15: summarize
summarize by InitiatingProcessFileName, RemoteIP, RemotePort
Stage 16: where
where dcount_DeviceId <= 10
Stage 17: join
join kind=inner (BeaconCandidates) on InitiatingProcessFileName, RemoteIP, RemotePort
Stage 18: project-away
project-away *1
Stage 19: join (negated)
join kind=leftantisemi (ImpossibleBeaconsByTimeDelta) on DeviceId, DeviceName, InitiatingProcessAccountName, InitiatingProcessAccountDomain, InitiatingProcessFileName, RemoteIP, RemotePort
Stage 20: extend
extend Timestamp
Stage 21: sort
sort by Jitter, TimeDeltaInSeconds_avg
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionType | in |
| field:"ActionType" kind:in |
RemoteIP | cidr_match |
| field:"RemoteIP" kind:cidr_match |
RemoteIP | in |
| field:"RemoteIP" kind:in value:"0.0.0.0" |
RemoteIP | is_not_null | field:"RemoteIP" kind:is_not_null | |
RemoteIPType | in |
| field:"RemoteIPType" kind:in |
dcount_DeviceId | le |
| field:"dcount_DeviceId" kind:le value:"10" |
Exclusions
The rule actively suppresses these predicates.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
InitiatingProcessFileName | summarize |
RemoteIP | summarize |
RemotePort | summarize |
Timestamp | extend |
The download of potentially risky files from the Discord Content Delivery Network (CDN) (ASIM Web Session)
#'This detection mechanism identifies instances where requests are made to Discord CDN addresses for file extensions that are considered risky. It triggers when a callout is made to a Discord server that has only been encountered once in your environment. The uniqueness of Discord servers is determined based on the server ID present in the request URL (DiscordServerId in the query). Discord CDN has been utilized in numerous campaigns to download additional payloads, highlighting the importance of monitoring such activities. The query includes a sample set of popular web script extensions (scriptExtensions), which should be customized to align with the specific requirements of your environment'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: b7fe8f27-7010-404b-aec5-6e5245cea580
name: The download of potentially risky files from the Discord Content Delivery Network (CDN) (ASIM Web Session)
description: |
'This detection mechanism identifies instances where requests are made to Discord CDN addresses for file extensions that are considered risky.
It triggers when a callout is made to a Discord server that has only been encountered once in your environment. The uniqueness of Discord servers is determined based on the server ID present in the request URL (DiscordServerId in the query).
Discord CDN has been utilized in numerous campaigns to download additional payloads, highlighting the importance of monitoring such activities.
The query includes a sample set of popular web script extensions (scriptExtensions), which should be customized to align with the specific requirements of your environment'
severity: Medium
status: Available
tags:
- Schema: WebSession
SchemaVersion: 0.2.6
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071.001
query: |
let lookback = 1d;
let connectionThreshold = 1;
let RiskyFileExtensions = materialize(externaldata(Extensions: string)
[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/RiskyFileExtensionsInUrl.csv"]
with(format="csv", ignoreFirstRecord=True));
let CustomRiskyFileExtensions = (_ASIM_GetWatchlistRaw("Web_RiskyFileExtensions") // Create new Watchlist and add your custom indicators(Optional)
| extend
Extensions = tostring(WatchlistItem["Extensions"])
| project Extensions
| where isnotempty(Extensions));
let CombinedRiskyFileExtensions = union RiskyFileExtensions, CustomRiskyFileExtensions;
let knownRiskyFileExtensions=toscalar(CombinedRiskyFileExtensions
| where isnotempty(Extensions)
| summarize make_set(Extensions, 1000));
let discord=dynamic(["cdn.discordapp.com", "media.discordapp.com"]);
let WebData = _Im_WebSession(starttime=ago(lookback), url_has_any=discord, eventresult='Success')
| where isnotempty(Url)
| project Url, SrcUsername, SrcIpAddr, TimeGenerated, SrcHostname
| where Url has "attachments"
| extend DiscordServerId = extract(@"\/attachments\/([0-9]+)\/", 1, Url);
WebData
| summarize
dcount(Url),
min(TimeGenerated),
max(TimeGenerated)
by DiscordServerId
| join kind=inner (WebData) on DiscordServerId
| where dcount_Url <= connectionThreshold and Url has_any (knownRiskyFileExtensions)
| summarize EventCount = count()
by
EventStartTime=min_TimeGenerated,
EventEndTime=max_TimeGenerated,
SrcUsername,
SrcHostname,
SrcIpAddr,
Url
| extend
Name = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 0)[0]), SrcUsername),
UPNSuffix = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 1)[0]), "")
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: SrcHostname
eventGroupingSettings:
aggregationKind: AlertPerResult
customDetails:
EventStartTime: EventStartTime
EventEndTime: EventEndTime
alertDetailsOverride:
alertDisplayNameFormat: "User '{{SrcUsername}}' with the IP address '{{SrcIpAddr}}' has been detected downloading potentially risky files from the Discord CDN"
alertDescriptionFormat: " Client requested for URL '{{Url}}' that contains a files hosted on a recognized Discord Content Delivery Network (CDN) which are considered to be potentially risky. It is essential to investigate further to determine the nature of the files being requested and the intent of the users involved"
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let lookback = 1d;
let connectionThreshold = 1;
let CombinedRiskyFileExtensions = union RiskyFileExtensions, CustomRiskyFileExtensions;
let discord = dynamic(["cdn.discordapp.com", "media.discordapp.com"]);
let WebData is inlined into the numbered stages below.
Let Binding: RiskyFileExtensions
let RiskyFileExtensions = materialize(externaldata(Extensions: string)
[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/RiskyFileExtensionsInUrl.csv"]
with(format="csv", ignoreFirstRecord=True));
Let Binding: CustomRiskyFileExtensions
let CustomRiskyFileExtensions = (_ASIM_GetWatchlistRaw("Web_RiskyFileExtensions")
| extend
Extensions = tostring(WatchlistItem["Extensions"])
| project Extensions
| where isnotempty(Extensions));
Let Binding: knownRiskyFileExtensions
let knownRiskyFileExtensions = toscalar(CombinedRiskyFileExtensions
| where isnotempty(Extensions)
| summarize make_set(Extensions, 1000));
Stages 1 to 5 define let WebData as the rule's main pipeline source. Stages 6 to 10 use it.
Stage 1: source
_Im_WebSession(starttime=ago(lookback), url_has_any=discord, eventresult='Success')
Stage 2: where
| where isnotempty(Url)
Stage 3: project
| project Url, SrcUsername, SrcIpAddr, TimeGenerated, SrcHostname
Stage 4: where
| where Url has "attachments"
Stage 5: extend
| extend DiscordServerId = extract(@"\/attachments\/([0-9]+)\/", 1, Url)
Stage 6: summarize
WebData
| summarize
dcount(Url),
min(TimeGenerated),
max(TimeGenerated)
by DiscordServerId
Stage 7: join
| join kind=inner (WebData) on DiscordServerId
Stage 8: where
where Url has "knownRiskyFileExtensions" and dcount_Url <= 1
Stage 9: summarize
summarize EventCount, EventEndTime, EventStartTime by EventStartTime, EventEndTime, SrcUsername, SrcHostname, SrcIpAddr, Url
Stage 10: extend
extend Name, UPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Url | is_not_null | field:"Url" kind:is_not_null | |
Url | match |
| field:"Url" kind:match |
dcount_Url | le |
| field:"dcount_Url" kind:le value:"1" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
EventCount | summarize |
EventEndTime | summarize |
EventStartTime | summarize |
SrcHostname | summarize |
SrcIpAddr | summarize |
SrcUsername | summarize |
Url | summarize |
Name | extend |
UPNSuffix | extend |
TI map Domain entity to Dns Events (ASIM DNS Schema)
#Identifies a match in DNS events from any Domain IOC from TI 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: 999e9f5d-db4a-4b07-a206-29c4e667b7e8
name: TI map Domain entity to Dns Events (ASIM DNS Schema)
description: |
Identifies a match in DNS events from any Domain IOC from TI
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: ThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelligenceIndicator
- 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: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: CiscoUmbrellaDataConnector
dataTypes:
- Cisco_Umbrella_dns_CL
- connectorId: Corelight
dataTypes:
- Corelight_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
tags:
- Id: 85aca4d1-5d15-4001-abd9-acb86ca1786a
version: 1.0.0
- Schema: ASIMDns
SchemaVersion: 0.1.1
query: |
let HAS_ANY_MAX = 10000;
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let DomainTIs= ThreatIntelligenceIndicator
// Picking up only IOC's that contain the entities we want
| where isnotempty(DomainName)
| where TimeGenerated >= ago(ioc_lookBack)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
| where Active == true and ExpirationDateTime > now();
let Domains = DomainTIs | where isnotempty(DomainName) |summarize NDomains=dcount(DomainName), DomainsList=make_set(DomainName)
| project DomainList = iff(NDomains > HAS_ANY_MAX, dynamic([]), DomainsList) ;
DomainTIs
| join (
_Im_Dns(starttime=ago(dt_lookBack), domain_has_any=toscalar(Domains))
| extend DNS_TimeGenerated = TimeGenerated
) on $left.DomainName==$right.DnsQuery
| where DNS_TimeGenerated < ExpirationDateTime
| project LatestIndicatorTime, Description, ActivityGroupNames, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, Url, DNS_TimeGenerated, Dvc, SrcIpAddr, Domain, DnsQuery, DnsQueryType
| extend HostName = tostring(split(Dvc, ".")[0]), DomainIndex = toint(indexof(Dvc, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Dvc, DomainIndex + 1), Dvc)
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Dvc
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: HostNameDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: Domain
customDetails:
LatestIndicatorTime: LatestIndicatorTime
Description: Description
ActivityGroupNames: ActivityGroupNames
IndicatorId: IndicatorId
ThreatType: ThreatType
ExpirationDateTime: ExpirationDateTime
ConfidenceScore: ConfidenceScore
DNSRequestTime: DNS_TimeGenerated
SourceIPAddress: SrcIpAddr
DnsQuery: DnsQuery
QueryType: DnsQueryType
version: 1.1.8
kind: Scheduled
Stages and Predicates
Parameters
let HAS_ANY_MAX = 10000;
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let DomainTIs is inlined into the numbered stages below.
Let Binding: Domains
let Domains = DomainTIs | where isnotempty(DomainName) |summarize NDomains=dcount(DomainName), DomainsList=make_set(DomainName)
| project DomainList = iff(NDomains > HAS_ANY_MAX, dynamic([]), DomainsList);
Stages 1 to 5 define let DomainTIs as the rule's main pipeline source. Stages 6 to 10 use it.
Stage 1: source
ThreatIntelligenceIndicator
Stage 2: where
| where isnotempty(DomainName)
Stage 3: where
| where TimeGenerated >= ago(ioc_lookBack)
Stage 4: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
Stage 5: where
| where Active == true and ExpirationDateTime > now()
Stage 6: join
DomainTIs
| join (
_Im_Dns(starttime=ago(dt_lookBack), domain_has_any=toscalar(Domains))
| extend DNS_TimeGenerated = TimeGenerated
) on $left.DomainName==$right.DnsQuery
Stage 7: where
where DNS_TimeGenerated < ExpirationDateTime
Stage 8: project
project ActivityGroupNames, ConfidenceScore, DNS_TimeGenerated, Description, DnsQuery, DnsQueryType, Domain, Dvc, ExpirationDateTime, IndicatorId, LatestIndicatorTime, SrcIpAddr, ThreatType, Url
Stage 9: extend
extend DomainIndex, HostName
Stage 10: extend
extend HostNameDomain
HostNameDomain =DomainIndex != -1substring(Dvc, (DomainIndex + 1))DvcIndicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Active | eq |
| field:"Active" kind:eq value:"true" |
DomainName | is_not_null | field:"DomainName" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
ConfidenceScore | project |
DNS_TimeGenerated | project |
Description | project |
DnsQuery | project |
DnsQueryType | project |
Domain | project |
Dvc | project |
ExpirationDateTime | project |
IndicatorId | project |
LatestIndicatorTime | project |
SrcIpAddr | project |
ThreatType | project |
Url | project |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |
TIE Active Directory attacks pathways
#'Searches for triggered Indicators of Exposures related to Active Directory attacks pathways.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: de549a62-f595-4810-88bd-621338186588
name: TIE Active Directory attacks pathways
description: |
'Searches for triggered Indicators of Exposures related to Active Directory attacks pathways.'
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
];
let codeNameList = datatable(Codename:string)["C-PRIV-ACCOUNTS-SPN", "C-SDPROP-CONSISTENCY", "C-DANG-PRIMGROUPID", "C-GPO-HARDENING", "C-DC-ACCESS-CONSISTENCY", "C-DANGEROUS-TRUST-RELATIONSHIP", "C-UNCONST-DELEG", "C-ABNORMAL-ENTRIES-IN-SCHEMA"];
afad_parser
| where MessageType == 0 and Codename in~ (codeNameList)
| lookup kind=leftouter SeverityTable on Severity
| order by Level
| extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
version: 1.0.1
kind: Scheduled
Stages and Predicates
Let Binding: SeverityTable
let SeverityTable = datatable(Severity:string,Level:int) [
"low", 1,
"medium", 2,
"high", 3,
"critical", 4
];
Let Binding: codeNameList
let codeNameList = datatable(Codename:string)["C-PRIV-ACCOUNTS-SPN", "C-SDPROP-CONSISTENCY", "C-DANG-PRIMGROUPID", "C-GPO-HARDENING", "C-DC-ACCESS-CONSISTENCY", "C-DANGEROUS-TRUST-RELATIONSHIP", "C-UNCONST-DELEG", "C-ABNORMAL-ENTRIES-IN-SCHEMA"];
Stage 1: source
afad_parser
Stage 2: where
| where MessageType == 0 and Codename in~ (codeNameList)
Stage 3: kusto:lookup
lookup kind=leftouter (SeverityTable) on Severity
Stage 4: sort
sort by Level
Stage 5: extend
extend DnsDomain, HostName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Codename | in |
| field:"Codename" kind:in value:"codeNameList" |
MessageType | eq |
| field:"MessageType" kind:eq value:"0" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DnsDomain | extend |
HostName | extend |
XbowCriticalHighFindings
#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.
Creates an incident for each Critical or High severity finding reported by XBOW that is currently in an open state. These findings represent the most severe security issues and require immediate attention. Each alert is deduplicated per finding so re-ingestion of the same finding does not produce duplicate incidents.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: f8e7d6c5-4b3a-4912-8f0e-2d1c3b4a5678
name: XbowCriticalHighFindings
description: |
Creates an incident for each Critical or High severity finding reported by XBOW that
is currently in an open state. These findings represent the most severe security issues
and require immediate attention. Each alert is deduplicated per finding so re-ingestion
of the same finding does not produce duplicate incidents.
severity: High
status: Available
requiredDataConnectors:
- connectorId: XbowSecurityConnector
dataTypes:
- XbowFindings_CL
- XbowAssets_CL
queryFrequency: 30m
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Execution
- PrivilegeEscalation
- DefenseEvasion
- Impact
relevantTechniques:
- T1190
query: |
XbowFindings_CL
| where TimeGenerated > ago(1h)
| where tolower(Severity) in ('critical', 'high')
| where isempty(State) or tolower(State) == 'open'
| summarize arg_max(TimeGenerated, *) by FindingId
| join kind=leftouter (
XbowAssets_CL
| summarize arg_max(TimeGenerated, *) by AssetId
| project AssetId, StartUrl
) on AssetId
| project
TimeGenerated,
FindingId,
FindingName,
Severity,
State,
Summary,
Impact,
Mitigations,
Recipe,
AssetId,
AssetName,
OrganizationId,
CreatedAt,
StartUrl
eventGroupingSettings:
aggregationKind: AlertPerResult
alertDetailsOverride:
alertDisplayNameFormat: 'XBOW {{Severity}}: {{FindingName}}'
alertDescriptionFormat: '{{Severity}} severity finding on {{AssetName}}. {{Summary}}'
customDetails:
FindingID: FindingId
FindingName: FindingName
Severity: Severity
State: State
AssetID: AssetId
AssetName: AssetName
OrganizationID: OrganizationId
CreatedAt: CreatedAt
Mitigations: Mitigations
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 24h
matchingMethod: Selected
groupByCustomDetails:
- FindingID
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: StartUrl
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
XbowFindings_CL
Stage 2: where
| where TimeGenerated > ago(1h)
Stage 3: where
| where tolower(Severity) in ('critical', 'high')
Stage 4: where
| where isempty(State) or tolower(State) == 'open'
Stage 5: summarize
| summarize arg_max(TimeGenerated, *) by FindingId
Stage 6: join
| join kind=leftouter (
XbowAssets_CL
| summarize arg_max(TimeGenerated, *) by AssetId
| project AssetId, StartUrl
) on AssetId
Stage 7: project
project AssetId, AssetName, CreatedAt, FindingId, FindingName, Impact, Mitigations, OrganizationId, Recipe, Severity, StartUrl, State, Summary, TimeGenerated
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Severity | in |
| field:"Severity" kind:in |
State | eq |
| field:"State" kind:eq value:"open" |
State | is_null | field:"State" kind:is_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AssetId | project |
AssetName | project |
CreatedAt | project |
FindingId | project |
FindingName | project |
Impact | project |
Mitigations | project |
OrganizationId | project |
Recipe | project |
Severity | project |
StartUrl | project |
State | project |
Summary | project |
TimeGenerated | project |