Detection rules › Kusto
Kusto rules: 17
AWSCloudTrail - Privilege escalation via EC2 policy
#Detects inline IAM policy updates that grant broad EC2 permissions or EC2 actions paired with IAM pass-role capability. This policy pattern can enable launch and control of compute resources as a privilege escalation path.
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 Lambda policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD S3 policy (Kusto)
Rule Body
id: a2b2a984-c820-4d93-830e-139bffd81fa3
name: AWSCloudTrail - Privilege escalation via EC2 policy
description: |
Detects inline IAM policy updates that grant broad EC2 permissions or EC2 actions paired with IAM pass-role
capability. This policy pattern can enable launch and control of compute resources as a privilege escalation path.
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 "iam:*" or Action contains "iam:PassRole") and Action contains "ec2:*") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "ec2:RunInstances") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "ec2:Run*")) or (Action contains "ec2:*") or (Action contains "ec2:ListInstances" and Action contains "ec2:StartInstance" and Action contains "ec2:ModifyInstanceAttribute") or (Action contains "ec2:List*" and Action contains "ec2:Start*" and Action contains "ec2:Modify*")) 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, UserIdentityArn, UserIdentityUserName, RecipientAccountId, AccountName, AccountUPNSuffix
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 EC2 privilege escalation policy update by {{AccountName}}'
alertDescriptionFormat: 'Detected {{EventName}} Event, updating inline EC2 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 "iam:*" or Action contains "iam:PassRole") and Action contains "ec2:*") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "ec2:RunInstances") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "ec2:Run*")) or (Action contains "ec2:*") or (Action contains "ec2:ListInstances" and Action contains "ec2:StartInstance" and Action contains "ec2:ModifyInstanceAttribute") or (Action contains "ec2:List*" and Action contains "ec2:Start*" and Action contains "ec2:Modify*")) 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, UserIdentityArn, UserIdentityUserName, RecipientAccountId, AccountName, AccountUPNSuffix
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 |
BloodHound Attack Path Finding - ReadLapsPassword Privileges on Tier Zero Objects
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
BloodHound Attack Path Finding - ReadLapsPassword Privileges on Tier Zero Objects
Rule Body
id: 1a7721b8-028f-47d2-a1aa-dbb307aefb27
name: BloodHound Attack Path Finding - ReadLapsPassword Privileges on Tier Zero Objects
description: BloodHound Attack Path Finding - ReadLapsPassword Privileges on Tier
Zero Objects
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: BloodHoundEnterprise
dataTypes:
- BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: BHEAttackPathsData_CL | where Finding == "T0ReadLAPS"
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: domain_name
customDetails:
Description: ShortDescription
Remediation: ShortRemediation
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
BHEAttackPathsData_CL
Stage 2: where
| where Finding == "T0ReadLAPS"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"T0ReadLAPS" |
Cisco SDWAN - Monitor Critical IPs
#'This analytic rule will monitor critical IPs in Syslog and Netflow Data.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: a62a207e-62be-4a74-acab-4466d5b3854f
name: Cisco SDWAN - Monitor Critical IPs
description: |
'This analytic rule will monitor critical IPs in Syslog and Netflow Data.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: CiscoSDWAN
dataTypes:
- CiscoSyslogUTD
- connectorId: CiscoSDWAN
dataTypes:
- CiscoSDWANNetflow
queryFrequency: 3h
queryPeriod: 3h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
query: |
CiscoSyslogUTD
| union (CiscoSDWANNetflow)
| where isnotempty(SourceIP) or isnotempty(NetflowFwSrcAddrIpv4)
| extend SourceIP = coalesce(SourceIP, NetflowFwSrcAddrIpv4)
| where ipv4_is_in_any_range(SourceIP, "Enter comma-separated IPs")
| summarize count() by SourceIP
incidentConfiguration:
createIncident: true
eventGroupingSettings:
aggregationKind: AlertPerResult
customDetails:
"critical_ip": "SourceIP"
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIP
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
CiscoSyslogUTD
Stage 2: union
union of 1 branches
Stage 3: source
CiscoSDWANNetflow
Stage 4: where
where (isnotempty(NetflowFwSrcAddrIpv4) or isnotempty(SourceIP))
Stage 5: extend
extend SourceIP
Stage 6: where
where /* macro: ipv4_is_in_any_range(SourceIP, "Enter comma-separated IPs") */
Stage 7: summarize
summarize by SourceIP
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
NetflowFwSrcAddrIpv4 | is_not_null | field:"NetflowFwSrcAddrIpv4" kind:is_not_null | |
SourceIP | is_not_null | field:"SourceIP" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SourceIP | summarize |
Cisco SEG - Malicious attachment not blocked
#'Detects mails with malicious attachments which were not blocked.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 236e872c-31d1-4b45-ac2a-fda3af465c97
name: Cisco SEG - Malicious attachment not blocked
description: |
'Detects mails with malicious attachments which were not blocked.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1566
query: |
CiscoSEGEvent
| where NetworkDirection =~ 'Incoming'
| where SimplifiedDeviceAction =~ 'DELIVERED'
| where tostring(AdditionalFields) has 'ESAAMPVerdict'
| extend amp_verdict = extract(@'ESAAMPVerdict":"(NOT_EVALUATED|CLEAN|FA_PENDING|UNKNOWN|SKIPPED|UNSCANNABLE|LOW_RISK|MALICIOUS)"', 1, tostring(AdditionalFields))
| where amp_verdict =~ 'MALICIOUS'
| extend AccountCustomEntity = DstUserName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.3
kind: Scheduled
Stages and Predicates
Stage 1: source
CiscoSEGEvent
Stage 2: where
| where NetworkDirection =~ 'Incoming'
Stage 3: where
| where SimplifiedDeviceAction =~ 'DELIVERED'
Stage 4: where
| where tostring(AdditionalFields) has 'ESAAMPVerdict'
Stage 5: extend
| extend amp_verdict = extract(@'ESAAMPVerdict":"(NOT_EVALUATED|CLEAN|FA_PENDING|UNKNOWN|SKIPPED|UNSCANNABLE|LOW_RISK|MALICIOUS)"', 1, tostring(AdditionalFields))
Stage 6: where
| where amp_verdict =~ 'MALICIOUS'
Stage 7: extend
| extend AccountCustomEntity = DstUserName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AdditionalFields | match |
| field:"AdditionalFields" kind:match value:"ESAAMPVerdict" |
NetworkDirection | eq |
| field:"NetworkDirection" kind:eq value:"Incoming" |
SimplifiedDeviceAction | eq |
| field:"SimplifiedDeviceAction" kind:eq value:"DELIVERED" |
amp_verdict | eq |
| field:"amp_verdict" kind:eq value:"MALICIOUS" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
amp_verdict | extend |
AccountCustomEntity | extend |
Cloudflare - Bad client IP
#'Detects requests from IP with bad reputation index.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: a7ce6135-9d55-4f14-b058-adc2e920a4fa
name: Cloudflare - Bad client IP
description: |
'Detects requests from IP with bad reputation index.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CloudflareDataConnector
dataTypes:
- Cloudflare
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
let ip_reputation = dynamic(['unknown', 'badHost', 'greylist', 'securityScanner', 'scan', 'tor']);
Cloudflare
| where ClientIPClass in~ (ip_reputation)
| extend CompleteUrl = strcat(HttpRequestHeaderHost,ClientRequestPath)
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
- entityType: URL
fieldMappings:
- identifier: Url
columnName: CompleteUrl
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let ip_reputation = dynamic(['unknown', 'badHost', 'greylist', 'securityScanner', 'scan', 'tor']);
Stage 1: source
Cloudflare
Stage 2: where
| where ClientIPClass in~ (ip_reputation)
Stage 3: extend
| extend CompleteUrl = strcat(HttpRequestHeaderHost,ClientRequestPath)
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ClientIPClass | in |
| field:"ClientIPClass" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CompleteUrl | extend |
Copilot - Plugin Tampering (Enable and Disable Within 5 Minutes)
#'Detects when a user enables and disables Copilot plugins within a 5-minute window. This behavior often indicates probing for security controls or living-off-Copilot techniques. This rule identifies discovery and defense evasion activities where users rapidly toggle plugin states, potentially testing security boundaries.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment | |
| Discovery |
Rule Body
id: d4e5f6a7-b8c9-40d1-e2f3-a4b5c6d7e8f9
name: Copilot - Plugin Tampering (Enable and Disable Within 5 Minutes)
description: |
'Detects when a user enables and disables Copilot plugins within a 5-minute window. This behavior often indicates probing for security controls or living-off-Copilot techniques.
This rule identifies discovery and defense evasion activities where users rapidly toggle plugin states, potentially testing security boundaries.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: MicrosoftCopilot
dataTypes:
- CopilotActivity
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Discovery
- DefenseEvasion
relevantTechniques:
- T1087
- T1562
query: |
CopilotActivity
| where RecordType in ("EnableCopilotPlugin","DisableCopilotPlugin")
| summarize
Actions = make_set(RecordType),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by ActorName, SrcIpAddr
| where Actions has "EnableCopilotPlugin" and Actions has "DisableCopilotPlugin"
| where LastSeen - FirstSeen < 5m
| extend AccountCustomEntity = ActorName, IPCustomEntity = SrcIpAddr
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountCustomEntity
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
CopilotActivity
Stage 2: where
| where RecordType in ("EnableCopilotPlugin","DisableCopilotPlugin")
Stage 3: summarize
| summarize
Actions = make_set(RecordType),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by ActorName, SrcIpAddr
Stage 4: where
| where Actions has "EnableCopilotPlugin" and Actions has "DisableCopilotPlugin"
Stage 5: where LastSeen - FirstSeen < 5m
| where LastSeen - FirstSeen < 5m
Stage 6: extend
| extend AccountCustomEntity = ActorName, IPCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Actions | match |
| field:"Actions" kind:match |
RecordType | in |
| field:"RecordType" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Actions | summarize |
ActorName | summarize |
FirstSeen | summarize |
LastSeen | summarize |
SrcIpAddr | summarize |
AccountCustomEntity | extend |
IPCustomEntity | extend |
Cyble Vision Alerts Website Defacement Content
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
'Triggers when monitored websites show new or suspicious content referencing known defacement patterns. Supports investigation into potential web compromise incidents.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: 91a00e4f-3edb-49e9-ba6f-cec87a5bd2f8
name: Cyble Vision Alerts Website Defacement Content
description: |
'Triggers when monitored websites show new or suspicious content referencing known defacement patterns. Supports investigation into potential web compromise incidents.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CybleVisionAlerts
dataTypes:
- CybleVisionAlerts_CL
enabled: true
queryfrequency: 30m
queryPeriod: 30m
triggerOperator: GreaterThan
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics:
- Impact
relevantTechniques:
- T1491
query: |
Alerts_defacement_content
| where Service == "defacement_content"
| extend MappedSeverity = Severity
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
alertDetailsOverride:
alertDisplayNameFormat: Defacement Activity Detected {{DC_DomainName}}
alertDescriptionFormat: |
New or modified content was detected on website {{DC_DomainName}}. URL {{DC_URL}}. This may indicate unauthorized website updates or defacement activity.
customDetails:
MappedSeverity: Severity
Status: Status
AlertID: AlertID
Service: Service
DC_URL: DC_URL
DC_DomainName: DC_DomainName
DC_Keywords: DC_Keywords
DC_RecordID: DC_RecordID
entityMappings:
- entityType: Url
fieldMappings:
- identifier: Url
columnName: DC_URL
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: DC_DomainName
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
Alerts_defacement_content
Stage 2: where
| where Service == "defacement_content"
Stage 3: extend
| extend MappedSeverity = Severity
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Service | eq |
| field:"Service" kind:eq value:"defacement_content" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
MappedSeverity | extend |
Dataverse - Login by a sensitive privileged user
#Identifies Dataverse and Dynamics 365 logons by sensitive users.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Privilege Escalation | |
| Credential Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft 365 | any: Dataverse and model-driven apps audit (catch-all) |
Rule Body
id: f327816b-9328-4b17-9290-a02adc2f4928
kind: Scheduled
name: Dataverse - Login by a sensitive privileged user
description: Identifies Dataverse and Dynamics 365 logons by sensitive users.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Dataverse
dataTypes:
- DataverseActivity
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- CredentialAccess
- PrivilegeEscalation
relevantTechniques:
- T1133
- T1190
- T1078
- T1212
query: |
# Sensitive users are marked in the VIP Users watchlist using the Tags field.
# Enter the tags values to monitor
let monitored_tags = dynamic(["DataverseSensitive"]);
let query_frequency = 1h;
let sensitive_users = MSBizAppsVIPUsers()
| where Tags in (monitored_tags);
sensitive_users
| join kind=inner (DataverseActivity
| where TimeGenerated >= ago(query_frequency)
| where Message == "UserSignIn")
on $left.UserPrincipalName == $right.UserId
| summarize FirstSeen = arg_max(TimeGenerated, *) by UserId
| extend
CloudAppId = int(32780),
AccountName = tostring(split(UserId, '@')[0]),
UPNSuffix = tostring(split(UserId, '@')[1])
| project
FirstSeen,
UserId,
ClientIp,
UserAgent,
InstanceUrl,
CloudAppId,
AccountName,
UPNSuffix
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: CloudApplication
fieldMappings:
- identifier: AppId
columnName: CloudAppId
- identifier: InstanceName
columnName: InstanceUrl
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ClientIp
alertDetailsOverride:
alertDisplayNameFormat: 'Dataverse - Sensitive user logged in in at {{InstanceUrl}} '
alertDescriptionFormat: A user marked as sensitive for Dataverse in the VIPUsers
watchlist signed in at {{InstanceUrl}}.
version: 3.2.0
Stages and Predicates
Parameters
let monitored_tags = dynamic(["DataverseSensitive"]);
let query_frequency = 1h;
let sensitive_users is inlined into the numbered stages below.
Stages 1 to 2 define let sensitive_users as the rule's main pipeline source. Stages 3 to 6 use it.
Stage 1: source
MSBizAppsVIPUsers()
Stage 2: where
| where Tags in (monitored_tags)
Stage 3: join
sensitive_users
| join kind=inner (DataverseActivity
| where TimeGenerated >= ago(query_frequency)
| where Message == "UserSignIn")
on $left.UserPrincipalName == $right.UserId
Stage 4: summarize
summarize FirstSeen by UserId
Stage 5: extend
extend AccountName, CloudAppId, UPNSuffix
Stage 6: project
project AccountName, ClientIp, CloudAppId, FirstSeen, InstanceUrl, UPNSuffix, UserAgent, UserId
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Message | eq |
| field:"Message" kind:eq value:"UserSignIn" |
Tags | in |
| field:"Tags" kind:in value:"monitored_tags" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
ClientIp | project |
CloudAppId | project |
FirstSeen | project |
InstanceUrl | project |
UPNSuffix | project |
UserAgent | project |
UserId | project |
Dataverse - New user agent type that was not used with Office 365
#Identifies users accessing Dynamics with a User Agent that has not been seen in any Office 365 workloads in the last 14 days.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft 365 | any: Dataverse and model-driven apps audit (catch-all) |
Rule Body
id: 094b3c0a-1f63-42f7-9535-c8c7b7198328
kind: Scheduled
name: Dataverse - New user agent type that was not used with Office 365
description: Identifies users accessing Dynamics with a User Agent that has not been
seen in any Office 365 workloads in the last 14 days.
severity: Low
status: Available
requiredDataConnectors:
- connectorId: Dataverse
dataTypes:
- DataverseActivity
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
let query_lookback = 14d;
let query_frequency = 1h;
let known_useragents = dynamic([
// Enter known user agents to exclude.
// example:
// "Agent1", "Agent2", "Agent3"
]);
DataverseActivity
| where TimeGenerated > ago(query_frequency)
| where not (UserId has_any ("@onmicrosoft.com", "@microsoft.com", "Unknown"))
| where isnotempty(UserAgent)
| where UserAgent !in~ (known_useragents)
| where UserAgent !hasprefix "azure-logic-apps" and UserAgent !hasprefix "PowerApps"
| join kind = leftanti (
OfficeActivity
| where TimeGenerated between(ago(query_lookback) .. ago(query_frequency))
| where isnotempty(UserAgent)
| summarize by UserAgent)
on UserAgent
// Exclude user agents with a render agent to reduce noise.
| join kind = leftanti(
DataverseActivity
| where TimeGenerated > ago(query_frequency)
| where UserAgent has_any ("Gecko", "WebKit", "Presto", "Trident", "EdgeHTML", "Blink"))
on UserAgent
| summarize
FirstSeen = min(TimeGenerated),
LatestIP = arg_max(ClientIp, TimeGenerated)
by UserAgent, UserId, InstanceUrl
| extend
AccountName = tostring(split(UserId, '@')[0]),
UPNSuffix = tostring(split(UserId, '@')[1]),
CloudAppId = int(32780)
| project
FirstSeen,
UserId,
UserAgent,
LatestIP,
InstanceUrl,
CloudAppId,
AccountName,
UPNSuffix
eventGroupingSettings:
aggregationKind: AlertPerResult
entityMappings:
- entityType: CloudApplication
fieldMappings:
- identifier: AppId
columnName: CloudAppId
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: LatestIP
version: 3.2.0
Stages and Predicates
Parameters
let query_lookback = 14d;
let query_frequency = 1h;
Let Binding: known_useragents
let known_useragents = dynamic([
]);
Stage 1: source
DataverseActivity
Stage 2: where
| where TimeGenerated > ago(query_frequency)
Stage 3: where
| where not (UserId has_any ("@onmicrosoft.com", "@microsoft.com", "Unknown"))
Stage 4: where
| where isnotempty(UserAgent)
Stage 5: where
| where UserAgent !in~ (known_useragents)
Stage 6: where
| where UserAgent !hasprefix "azure-logic-apps" and UserAgent !hasprefix "PowerApps"
Stage 7: join (negated)
join kind=leftanti (OfficeActivity) on UserAgent
Stage 8: join (negated)
join kind=leftanti (DataverseActivity) on UserAgent
Stage 9: summarize
summarize FirstSeen, LatestIP by UserAgent, UserId, InstanceUrl
Stage 10: extend
extend AccountName, CloudAppId, UPNSuffix
Stage 11: project
project AccountName, CloudAppId, FirstSeen, InstanceUrl, LatestIP, UPNSuffix, UserAgent, UserId
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
UserAgent | is_not_null | field:"UserAgent" kind:is_not_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
UserId | match | @onmicrosoft.com, @microsoft.com, Unknown | excludes:UserId field:"UserId" value:"@onmicrosoft.com" field:"UserId" value:"@microsoft.com" field:"UserId" value:"Unknown" |
UserAgent | eq | [] | excludes:UserAgent field:"UserAgent" value:"[]" |
UserAgent | starts_with | PowerApps | excludes:UserAgent field:"UserAgent" value:"PowerApps" |
UserAgent | starts_with | azure-logic-apps | excludes:UserAgent field:"UserAgent" value:"azure-logic-apps" |
UserAgent | is_not_null | excludes:UserAgent | |
UserAgent | match | Gecko, WebKit, Presto, Trident, EdgeHTML, Blink | excludes:UserAgent |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
CloudAppId | project |
FirstSeen | project |
InstanceUrl | project |
LatestIP | project |
UPNSuffix | project |
UserAgent | project |
UserId | project |
Digital Guardian - Bulk exfiltration to external domain
#'Detects bulk exfiltration to external domain.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Rule Body
id: 5f75a873-b524-4ba5-a3b8-2c20db517148
name: Digital Guardian - Bulk exfiltration to external domain
description: |
'Detects bulk exfiltration to external domain.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SyslogAma
datatypes:
- Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
relevantTechniques:
- T1048
query: |
let threshold = 10;
let corp_domain = dynamic(['example.com']);
DigitalGuardianDLPEvent
| where NetworkApplicationProtocol =~ 'SMTP'
| where isnotempty(inspected_document)
| extend s_domain = extract(@'@(.*)', 1, SrcUserName)
| extend d_domain = extract(@'@(.*)', 1, DstUserName)
| where s_domain in~ (corp_domain)
| where d_domain !in (corp_domain)
| summarize f = dcount(inspected_document) by SrcUserName, DstUserName, bin(TimeGenerated, 30m)
| where f >= threshold
| extend AccountCustomEntity = SrcUserName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 10;
let corp_domain = dynamic(['example.com']);
Stage 1: source
DigitalGuardianDLPEvent
Stage 2: where
| where NetworkApplicationProtocol =~ 'SMTP'
Stage 3: where
| where isnotempty(inspected_document)
Stage 4: extend
| extend s_domain = extract(@'@(.*)', 1, SrcUserName)
Stage 5: extend
| extend d_domain = extract(@'@(.*)', 1, DstUserName)
Stage 6: where
| where s_domain in~ (corp_domain)
Stage 7: where
| where d_domain !in (corp_domain)
Stage 8: summarize
| summarize f = dcount(inspected_document) by SrcUserName, DstUserName, bin(TimeGenerated, 30m)
Stage 9: where
| where f >= threshold
Stage 10: extend
| extend AccountCustomEntity = SrcUserName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
NetworkApplicationProtocol | eq |
| field:"NetworkApplicationProtocol" kind:eq value:"SMTP" |
f | ge |
| field:"f" kind:ge value:"10" |
inspected_document | is_not_null | field:"inspected_document" kind:is_not_null | |
s_domain | in |
| field:"s_domain" kind:in value:"corp_domain" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
d_domain | eq | corp_domain | excludes:d_domain field:"d_domain" value:"corp_domain" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DstUserName | summarize |
SrcUserName | summarize |
f | summarize |
AccountCustomEntity | extend |
GCP Audit Logs - Data Access Logging Exemption Added for Principal
#Detects when a principal (user or service account) is exempted from GCP data access audit logging. This is a critical security event as it reduces visibility into privileged operations and may indicate an attempt to hide malicious activity. Adversaries may exempt their accounts from audit logging to evade detection while performing reconnaissance, privilege escalation, or data exfiltration. This rule monitors SetIamPolicy operations that add audit log exemptions for ADMIN_READ, DATA_READ, or DATA_WRITE log types.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation | |
| Stealth | |
| Defense Impairment |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| GCP | SetIamPolicy |
Rules Detecting the Same Action
These rules filter on the same operation.
Rule Body
id: b7da45ce-fcc8-43c7-a37c-c08454579d26
name: GCP Audit Logs - Data Access Logging Exemption Added for Principal
description: |
'Detects when a principal (user or service account) is exempted from GCP data access audit logging.
This is a critical security event as it reduces visibility into privileged operations and may indicate an attempt to hide malicious activity.
Adversaries may exempt their accounts from audit logging to evade detection while performing reconnaissance, privilege escalation, or data exfiltration.
This rule monitors SetIamPolicy operations that add audit log exemptions for ADMIN_READ, DATA_READ, or DATA_WRITE log types.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: GCPAuditLogsDefinition
dataTypes:
- GCPAuditLogs
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
- PrivilegeEscalation
relevantTechniques:
- T1562.008
- T1078.004
tags:
- Cloud Security
- Audit Logging
- Defense Evasion
query: |
GCPAuditLogs
| where ServiceName == "cloudresourcemanager.googleapis.com"
| where MethodName == "SetIamPolicy"
| where GCPResourceType == "project" and Severity == "NOTICE"
| where isnotempty(ServiceData)
| extend ServiceDataJson = parse_json(ServiceData)
| extend PolicyDelta = ServiceDataJson.policyDelta.auditConfigDeltas
| where isnotempty(PolicyDelta)
| mv-expand ConfigDelta = PolicyDelta
| where ConfigDelta.action == "ADD"
| extend LogType = tostring(ConfigDelta.logType)
| where LogType in ("ADMIN_READ", "DATA_READ", "DATA_WRITE")
| extend
ExemptedMember = tostring(ConfigDelta.exemptedMember),
ServiceAffected = tostring(ConfigDelta.service),
RequestMetadataJson = parse_json(RequestMetadata),
AuthInfoJson = parse_json(AuthenticationInfo)
| where isnotempty(ExemptedMember)
| extend
CallerIpAddress = tostring(RequestMetadataJson.callerIp),
UserAgent = tostring(RequestMetadataJson.callerSuppliedUserAgent),
AuthEmail = tostring(AuthInfoJson.principalEmail),
ExemptedAccountName = tostring(split(ExemptedMember, ":")[1])
| summarize
ExemptedLogTypes = make_set(LogType, 10),
ExemptedServices = make_set(ServiceAffected, 50),
FirstExemption = min(TimeGenerated),
LastExemption = max(TimeGenerated)
by PrincipalEmail, ProjectId, GCPResourceName, ExemptedMember,
CallerIpAddress, UserAgent, LogName, ExemptedAccountName, MethodName, ServiceName, AuthEmail
| extend
AccountName = tostring(split(PrincipalEmail, "@")[0]),
AccountUPNSuffix = tostring(split(PrincipalEmail, "@")[1])
| project TimeGenerated = LastExemption,
PrincipalEmail,
ProjectId,
ResourceName = GCPResourceName,
ExemptedMember,
ExemptedAccountName,
ExemptedLogTypes,
ExemptedServices,
FirstExemption,
LastExemption,
CallerIpAddress,
UserAgent,
AuthEmail,
MethodName,
ServiceName,
LogName,
AccountName,
AccountUPNSuffix
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: PrincipalEmail
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: CallerIpAddress
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: ProjectId
- identifier: InstanceName
columnName: ResourceName
customDetails:
ProjectId: ProjectId
ExemptedMember: ExemptedMember
ExemptedAccountName: ExemptedAccountName
ExemptedLogTypes: ExemptedLogTypes
ExemptedServices: ExemptedServices
UserAgent: UserAgent
alertDetailsOverride:
alertDisplayNameFormat: "GCP Data Access Logging Exemption Added for {{ExemptedAccountName}} by {{PrincipalEmail}} in Service {{ExemptedServices}}"
alertDescriptionFormat: |-
Principal {{ExemptedAccountName}} added as exception from Data Access logging in project {{ProjectId}} for Service {{ExemptedServices}}.
This action reduces audit visibility and may indicate an attempt to evade detection. Verify this change was authorized and investigate any suspicious activity performed by the exempted principal.
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
GCPAuditLogs
Stage 2: where
| where ServiceName == "cloudresourcemanager.googleapis.com"
Stage 3: where
| where MethodName == "SetIamPolicy"
Stage 4: where
| where GCPResourceType == "project" and Severity == "NOTICE"
Stage 5: where
| where isnotempty(ServiceData)
Stage 6: extend
| extend ServiceDataJson = parse_json(ServiceData)
Stage 7: extend
| extend PolicyDelta = ServiceDataJson.policyDelta.auditConfigDeltas
Stage 8: where
| where isnotempty(PolicyDelta)
Stage 9: mv-expand
| mv-expand ConfigDelta = PolicyDelta
Stage 10: where
| where ConfigDelta.action == "ADD"
Stage 11: extend
| extend LogType = tostring(ConfigDelta.logType)
Stage 12: where
| where LogType in ("ADMIN_READ", "DATA_READ", "DATA_WRITE")
Stage 13: extend
| extend
ExemptedMember = tostring(ConfigDelta.exemptedMember),
ServiceAffected = tostring(ConfigDelta.service),
RequestMetadataJson = parse_json(RequestMetadata),
AuthInfoJson = parse_json(AuthenticationInfo)
Stage 14: where
| where isnotempty(ExemptedMember)
Stage 15: extend
| extend
CallerIpAddress = tostring(RequestMetadataJson.callerIp),
UserAgent = tostring(RequestMetadataJson.callerSuppliedUserAgent),
AuthEmail = tostring(AuthInfoJson.principalEmail),
ExemptedAccountName = tostring(split(ExemptedMember, ":")[1])
Stage 16: summarize
| summarize
ExemptedLogTypes = make_set(LogType, 10),
ExemptedServices = make_set(ServiceAffected, 50),
FirstExemption = min(TimeGenerated),
LastExemption = max(TimeGenerated)
by PrincipalEmail, ProjectId, GCPResourceName, ExemptedMember,
CallerIpAddress, UserAgent, LogName, ExemptedAccountName, MethodName, ServiceName, AuthEmail
Stage 17: extend
| extend
AccountName = tostring(split(PrincipalEmail, "@")[0]),
AccountUPNSuffix = tostring(split(PrincipalEmail, "@")[1])
Stage 18: project
| project TimeGenerated = LastExemption,
PrincipalEmail,
ProjectId,
ResourceName = GCPResourceName,
ExemptedMember,
ExemptedAccountName,
ExemptedLogTypes,
ExemptedServices,
FirstExemption,
LastExemption,
CallerIpAddress,
UserAgent,
AuthEmail,
MethodName,
ServiceName,
LogName,
AccountName,
AccountUPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ConfigDelta.action | eq |
| field:"ConfigDelta.action" kind:eq value:"ADD" |
ExemptedMember | is_not_null | field:"ExemptedMember" kind:is_not_null | |
GCPResourceType | eq |
| field:"gcp::resource_type" kind:eq value:"project" |
LogType | in |
| field:"LogType" kind:in |
MethodName | eq |
| field:"gcp::method_name" kind:eq value:"SetIamPolicy" |
PolicyDelta | is_not_null | field:"PolicyDelta" kind:is_not_null | |
ServiceData | is_not_null | field:"ServiceData" kind:is_not_null | |
ServiceName | eq |
| field:"ServiceName" kind:eq value:"cloudresourcemanager.googleapis.com" |
Severity | eq |
| field:"Severity" kind:eq value:"NOTICE" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
AccountUPNSuffix | project |
AuthEmail | project |
CallerIpAddress | project |
ExemptedAccountName | project |
ExemptedLogTypes | project |
ExemptedMember | project |
ExemptedServices | project |
FirstExemption | project |
LastExemption | project |
LogName | project |
MethodName | project |
PrincipalEmail | project |
ProjectId | project |
ResourceName | project |
ServiceName | project |
TimeGenerated | project |
UserAgent | project |
Generate alerts based on ExtraHop detections recommended for triage
#'This analytics rule will generate alerts in Microsoft Sentinel for detections from ExtraHop that are recommended for triage.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Rule Body
id: "cd65aebc-7e85-4cbb-9f91-ff0376c5d37d"
name: "Generate alerts based on ExtraHop detections recommended for triage"
description: |
'This analytics rule will generate alerts in Microsoft Sentinel for detections from ExtraHop that are recommended for triage.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: ExtraHop
dataTypes:
- ExtraHopDetections
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: GreaterThan
triggerThreshold: 0
tactics:
- Persistence
relevantTechniques:
- T1546
query: |
ExtraHopDetections
| where IsRecommended == true
incidentConfiguration:
createIncident: false
eventGroupingSettings:
aggregationKind: AlertPerResult
alertDetailsOverride:
alertDisplayNameFormat: "Alert from {{EventVendor}} for Detection {{Title}}"
alertDescriptionFormat: "Alert from {{EventVendor}} for Detection {{Title}} for Id {{Id}}"
alertTacticsColumnName: "TacticNames"
alertSeverityColumnName: "Severity"
alertDynamicProperties:
- alertProperty: AlertLink
value: Url
- alertProperty: Techniques
value: TechniqueIds
- alertProperty: ProductName
value: EventVendor
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: SourceHostname
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: DestinationHostname
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIpAddress
- entityType: IP
fieldMappings:
- identifier: Address
columnName: DestinationIpAddress
- entityType: Account
fieldMappings:
- identifier: Name
columnName: SourceUsername
- entityType: Account
fieldMappings:
- identifier: Name
columnName: DestinationUsername
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
ExtraHopDetections
Stage 2: where
| where IsRecommended == true
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
IsRecommended | eq |
| field:"IsRecommended" kind:eq value:"true" |
GitHub - pull request was merged
#Detect activities when a pull request was merged. 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 pull_request.merge: Pull request merged |
Rule Body
id: 0b85a077-8ba5-4cb5-90f7-1e882afe10c6
name: GitHub - pull request was merged
description: |
'Detect activities when a pull request was merged. 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 == "pull_request.merge"
| project Actor, Action
| 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 == "pull_request.merge"
Stage 3: project
| project Actor, Action
Stage 4: 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:"pull_request.merge" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Action | project |
Actor | project |
AccountName | extend |
AccountUPNSuffix | extend |
Global VM Exclusions Added
#Detects when global VM exclusion are added in Veeam Backup & Replication.
Rule Body
id: c9faf1ee-57dd-4f54-8cd1-fcf35a1aa424
name: Global VM Exclusions Added
description: Detects when global VM exclusion are added in Veeam Backup & Replication.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 40400\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 == 40400
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:"40400" |
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 |
Idira - Multiple Failed Actions Followed by Success (15m)
#Detects 3+ failed actions against an account followed by a success in a short window, indicating brute-force or credential guessing.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access | No specific technique |
Rule Body
id: bdf3cf98-d64f-4c55-97e4-43483d6d3237
kind: Scheduled
name: Idira - Multiple Failed Actions Followed by Success (15m)
description: >
Detects 3+ failed actions against an account followed by a success in a short window,
indicating brute-force or credential guessing.
severity: Medium
tactics:
- CredentialAccess
queryFrequency: 5M
queryPeriod: 15M
triggerOperator: GreaterThan
triggerThreshold: 0
query: |
let window = 15m;
let fails =
CyberArk_AuditEvents_CL
| where TimeGenerated > ago(window)
| where actionType has_any ("Fail","Failed","Denied","Error")
or message has_any ("fail","failed","denied","unauthorized")
| summarize Fails=count(), firstFail=min(TimeGenerated), lastFail=max(TimeGenerated) by username, targetAccount;
let success =
CyberArk_AuditEvents_CL
| where TimeGenerated > ago(window)
| where actionType has_any ("Success","Succeeded","Allow","Allowed")
or message has_any ("success","allowed")
| summarize Successes=count(), firstSuccess=min(TimeGenerated) by username, targetAccount;
fails
| where Fails >= 3
| join kind=inner success on username, targetAccount
| join kind=inner (
CyberArk_AuditEvents_CL
| extend cd = parse_json(customData)
| extend cd_client_ip = tostring(cd.client_ip_address),
cd_source_ip = tostring(cd.source_ip_address),
cd_device_os = tostring(cd.device_os),
cd_browser = tostring(cd.browser_name),
cd_geo_city = tostring(cd.geoip_city_name),
cd_geo_country = tostring(cd.geoip_country_name),
cd_target = coalesce(tostring(cd.target), tostring(cd.target_resource), tostring(cd.new_target))
| project username, targetAccount, any_cd_client_ip = cd_client_ip, any_cd_source_ip = cd_source_ip,
any_cd_device_os = cd_device_os, any_cd_browser = cd_browser,
any_cd_geo_city = cd_geo_city, any_cd_geo_country = cd_geo_country, any_cd_target = cd_target
) on username, targetAccount
| project username, targetAccount, Fails, Successes, firstFail, lastFail, firstSuccess,
any_cd_source_ip, any_cd_client_ip, any_cd_device_os, any_cd_browser,
any_cd_geo_city, any_cd_geo_country, any_cd_target
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: username
- entityType: Account
fieldMappings:
- identifier: Name
columnName: targetAccount
suppressionEnabled: false
version: 1.0.0
Stages and Predicates
Parameters
let window = 15m;
let fails is inlined into the numbered stages below.
Let Binding: success
let success = CyberArk_AuditEvents_CL
| where TimeGenerated > ago(window)
| where actionType has_any ("Success","Succeeded","Allow","Allowed")
or message has_any ("success","allowed")
| summarize Successes=count(), firstSuccess=min(TimeGenerated) by username, targetAccount;
Stages 1 to 4 define let fails as the rule's main pipeline source. Stages 5 to 8 use it.
Stage 1: source
CyberArk_AuditEvents_CL
Stage 2: where
| where TimeGenerated > ago(window)
Stage 3: where
| where actionType has_any ("Fail","Failed","Denied","Error")
or message has_any ("fail","failed","denied","unauthorized")
Stage 4: summarize
| summarize Fails=count(), firstFail=min(TimeGenerated), lastFail=max(TimeGenerated) by username, targetAccount
Stage 5: where
fails
| where Fails >= 3
Stage 6: join
| join kind=inner success on username, targetAccount
Stage 7: join
| join kind=inner (
CyberArk_AuditEvents_CL
| extend cd = parse_json(customData)
| extend cd_client_ip = tostring(cd.client_ip_address),
cd_source_ip = tostring(cd.source_ip_address),
cd_device_os = tostring(cd.device_os),
cd_browser = tostring(cd.browser_name),
cd_geo_city = tostring(cd.geoip_city_name),
cd_geo_country = tostring(cd.geoip_country_name),
cd_target = coalesce(tostring(cd.target), tostring(cd.target_resource), tostring(cd.new_target))
| project username, targetAccount, any_cd_client_ip = cd_client_ip, any_cd_source_ip = cd_source_ip,
any_cd_device_os = cd_device_os, any_cd_browser = cd_browser,
any_cd_geo_city = cd_geo_city, any_cd_geo_country = cd_geo_country, any_cd_target = cd_target
) on username, targetAccount
Stage 8: project
project Fails, Successes, any_cd_browser, any_cd_client_ip, any_cd_device_os, any_cd_geo_city, any_cd_geo_country, any_cd_source_ip, any_cd_target, firstFail, firstSuccess, lastFail, targetAccount, username
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Fails | ge |
| field:"Fails" kind:ge value:"3" |
actionType | match |
| field:"actionType" kind:match |
message | match |
| field:"message" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Fails | project |
Successes | project |
any_cd_browser | project |
any_cd_client_ip | project |
any_cd_device_os | project |
any_cd_geo_city | project |
any_cd_geo_country | project |
any_cd_source_ip | project |
any_cd_target | project |
firstFail | project |
firstSuccess | project |
lastFail | project |
targetAccount | project |
username | project |
McAfee ePO - Unable to clean or delete infected file
#Detects when McAfee failed to clean or delete infected file.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Stealth | |
| Defense Impairment |
Telemetry Coverage
Rule Body
id: 9860e89f-72c8-425e-bac9-4a170798d3ea
name: McAfee ePO - Unable to clean or delete infected file
description: |
'Detects when McAfee failed to clean or delete infected file.'
severity: High
requiredDataConnectors:
- connectorId: SyslogAma
datatypes:
- Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- DefenseEvasion
relevantTechniques:
- T1562
- T1070
query: |
McAfeeEPOEvent
| where EventId in ('1026', '1028', '1298', '1310', '1055', '2002', '2004', '2009')
| extend EventMessage = case(EventId == '1026', 'Unable to clean infected file',
EventId == '1028', 'Unable to delete infected file',
EventId == '1298', 'File infected. Delete failed, quarantine failed',
EventId == '1310', 'Multiple extension heuristic detection - delete failed, quarantine failed',
EventId == '1055', 'Unable to delete infected file',
EventId == '2002', 'Unable to clean infected file',
EventId == '2004', 'Unable to delete infected file',
'Unable to move infected file to quarantine')
| project DvcIpAddr, EventId, EventMessage, DstFileName
| extend IPCustomEntity = DvcIpAddr
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
McAfeeEPOEvent
Stage 2: where
| where EventId in ('1026', '1028', '1298', '1310', '1055', '2002', '2004', '2009')
Stage 3: extend
| extend EventMessage = case(EventId == '1026', 'Unable to clean infected file',
EventId == '1028', 'Unable to delete infected file',
EventId == '1298', 'File infected. Delete failed, quarantine failed',
EventId == '1310', 'Multiple extension heuristic detection - delete failed, quarantine failed',
EventId == '1055', 'Unable to delete infected file',
EventId == '2002', 'Unable to clean infected file',
EventId == '2004', 'Unable to delete infected file',
'Unable to move infected file to quarantine')
EventMessage =EventId == 1026'Unable to clean infected file'EventId == 1028'Unable to delete infected file'EventId == 1298'File infected. Delete failed, quarantine failed'EventId == 1310'Multiple extension heuristic detection - delete failed, quarantine failed'EventId == 1055'Unable to delete infected file'EventId == 2002'Unable to clean infected file'EventId == 2004'Unable to delete infected file''Unable to move infected file to quarantine'Stage 4: project
| project DvcIpAddr, EventId, EventMessage, DstFileName
Stage 5: extend
| extend IPCustomEntity = DvcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventId | in |
| field:"EventId" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DstFileName | project |
DvcIpAddr | project |
EventId | project |
EventMessage | project |
IPCustomEntity | extend |
Pathlock TDnR - RiskTrack Audit Results
#Detects Pathlock RiskTrack audit results forwarded to Microsoft Sentinel. RiskTrack findings represent completed risk assessments identifying segregation of duties conflicts, critical access violations, and compliance gaps that require remediation or SOC awareness.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Discovery |
Rule Body
id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c52
name: Pathlock TDnR - RiskTrack Audit Results
kind: Scheduled
description: >-
Detects Pathlock RiskTrack audit results forwarded to Microsoft Sentinel. RiskTrack findings
represent completed risk assessments identifying segregation of duties conflicts, critical
access violations, and compliance gaps that require remediation or SOC awareness.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Pathlock_TDnR
dataTypes:
- Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Discovery
relevantTechniques:
- T1082
query: |
Pathlock_TDnR_CL
| where DataSource == "SAST_RT"
| 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 == "SAST_RT"
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:"SAST_RT" |
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 |
Pathlock TDnR - User-Profile Assignment Changes
#Detects changes to user-to-profile assignments in SAP (SU01 profile tab), forwarded by Pathlock Threat Detection and Response. Unauthorized profile assignments can grant users broad authorizations outside of the role-based access control framework, bypassing segregation-of-duties controls.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
Rule Body
id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c75
name: Pathlock TDnR - User-Profile Assignment Changes
kind: Scheduled
description: >-
Detects changes to user-to-profile assignments in SAP (SU01 profile tab), forwarded by Pathlock
Threat Detection and Response. Unauthorized profile assignments can grant users broad authorizations
outside of the role-based access control framework, bypassing segregation-of-duties controls.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Pathlock_TDnR
dataTypes:
- Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
- Persistence
relevantTechniques:
- T1548
- T1098
query: |
Pathlock_TDnR_CL
| where DataSource == "USER_PROFILES"
| 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 == "USER_PROFILES"
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:"USER_PROFILES" |
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 |
Pure Failed Login
#Detect failed login attacks and delete user
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: ed32b115-5001-43a7-a2bb-f53026db4d97
name: Pure Failed Login
version: 1.0.0
kind: NRT
description: Detect failed login attacks and delete user
severity: High
tactics:
- CredentialAccess
relevantTechniques:
- T1212
query: |-
Syslog
| where SyslogMessage has "purity.alert" and SyslogMessage has "invalid username or password"
| extend Message = SyslogMessage
| extend ParsedLog = extract_all(@"((?P<process>.*?)\[(?P<processid>.*?)\][\s\S]*?Array name:\s*'(?P<arrayname>\S+)'[\s\S]*?Controller:\s*'?(?P<controller>[^']+)'?[\s\S]*Interface:\s*'(?P<interface>\S+)'.*?User:\s'(?P<login>.*?)'\sLocation: '(?P<location>[^']+)'\sSublocation: '(?P<sublocation>[^']+)\s*(?P<part2log>[\s\S]*))", dynamic(['process', 'processid', 'arrayname', 'controller', 'interface','login', 'location', 'sublocation', 'part2log']), Message)
| mv-expand ParsedLog
| extend ResidueLog = tostring(ParsedLog[8])
| extend Rlog = extract_all(@"[\s\S]*Action:\s'(?P<action>[^']+)'[\s\S]*Method:\s'(?P<method>[^']+)'[\s\S]*Result:\s(?P<result>[^']+)[\s\S]*Description:\s'(?P<description>[^']*)'", dynamic(['action', 'method', 'result', 'description']), ResidueLog)
| mv-expand Rlog
| extend PureLogType = ParsedLog[0], PureProcessID = ParsedLog[1], PureArrayName = ParsedLog[2], PureController = ParsedLog[3], PureInterface = ParsedLog[4], PureLogin = ParsedLog [5], PureLocation = ParsedLog [6], PureSublocation = ParsedLog [7], PureAction = Rlog [0], PureMethod = Rlog [1], PureResult = Rlog [2], PureDescription = Rlog [3]
| project-away ResidueLog, Rlog, ParsedLog
| summarize count() by tostring(PureLogin), tostring(PureArrayName), HostIP
| where count_ >= 10
suppressionDuration: 5h
suppressionEnabled: false
eventGroupingSettings:
aggregationKind: SingleAlert
alertDetailsOverride:
alertDynamicProperties: []
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: HostIP
- entityType: Account
fieldMappings:
- identifier: Name
columnName: PureLogin
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: PureArrayName
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
groupByEntities: []
groupByAlertDetails: []
groupByCustomDetails: []
Stages and Predicates
Stage 1: source
Syslog
Stage 2: where
| where SyslogMessage has "purity.alert" and SyslogMessage has "invalid username or password"
Stage 3: extend
| extend Message = SyslogMessage
Stage 4: extend
| extend ParsedLog = extract_all(@"((?P<process>.*?)\[(?P<processid>.*?)\][\s\S]*?Array name:\s*'(?P<arrayname>\S+)'[\s\S]*?Controller:\s*'?(?P<controller>[^']+)'?[\s\S]*Interface:\s*'(?P<interface>\S+)'.*?User:\s'(?P<login>.*?)'\sLocation: '(?P<location>[^']+)'\sSublocation: '(?P<sublocation>[^']+)\s*(?P<part2log>[\s\S]*))", dynamic(['process', 'processid', 'arrayname', 'controller', 'interface','login', 'location', 'sublocation', 'part2log']), Message)
Stage 5: mv-expand
| mv-expand ParsedLog
Stage 6: extend
| extend ResidueLog = tostring(ParsedLog[8])
Stage 7: extend
| extend Rlog = extract_all(@"[\s\S]*Action:\s'(?P<action>[^']+)'[\s\S]*Method:\s'(?P<method>[^']+)'[\s\S]*Result:\s(?P<result>[^']+)[\s\S]*Description:\s'(?P<description>[^']*)'", dynamic(['action', 'method', 'result', 'description']), ResidueLog)
Stage 8: mv-expand
| mv-expand Rlog
Stage 9: extend
| extend PureLogType = ParsedLog[0], PureProcessID = ParsedLog[1], PureArrayName = ParsedLog[2], PureController = ParsedLog[3], PureInterface = ParsedLog[4], PureLogin = ParsedLog [5], PureLocation = ParsedLog [6], PureSublocation = ParsedLog [7], PureAction = Rlog [0], PureMethod = Rlog [1], PureResult = Rlog [2], PureDescription = Rlog [3]
Stage 10: project-away
| project-away ResidueLog, Rlog, ParsedLog
Stage 11: summarize
| summarize count() by tostring(PureLogin), tostring(PureArrayName), HostIP
Stage 12: where
| where count_ >= 10
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
SyslogMessage | match |
| field:"SyslogMessage" kind:match |
count_ | ge |
| field:"count_" kind:ge value:"10" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
HostIP | summarize |
Service Principal Authentication Attempt from New Country
#Detects when there is a Service Principal login attempt from a country that has not seen a successful login in the previous 14 days. Threat actors may attempt to authenticate with credentials from compromised accounts - monitoring attempts from anomalous locations may help identify these attempts. Authentication attempts should be investigated to ensure the activity was legitimate and if there is other similar activity. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-failed-unusual-sign-ins
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Sign-in record (AADServicePrincipalSignInLogs, any result) |
Rule Body
id: 1baaaf00-655f-4de9-8ff8-312e902cda71
name: Service Principal Authentication Attempt from New Country
description: |
'Detects when there is a Service Principal login attempt from a country that has not seen a successful login in the previous 14 days.
Threat actors may attempt to authenticate with credentials from compromised accounts - monitoring attempts from anomalous locations may help identify these attempts.
Authentication attempts should be investigated to ensure the activity was legitimate and if there is other similar activity.
Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-failed-unusual-sign-ins'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AADServicePrincipalSignInLogs
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078.004
tags:
- AADSecOpsGuide
query: |
let known_locations = (
AADServicePrincipalSignInLogs
| where TimeGenerated between(ago(14d)..ago(1d))
| where ResultType == 0
| summarize by Location);
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(1d)
| where ResultType != 50126
| where Location !in (known_locations)
| extend City = tostring(parse_json(LocationDetails).city)
| extend State = tostring(parse_json(LocationDetails).state)
| extend Place = strcat(City, " - ", State)
| extend Result = strcat(tostring(ResultType), " - ", ResultDescription)
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), make_set(Result), make_set(IPAddress), make_set(Place) by ServicePrincipalName, Location
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: ServicePrincipalName
version: 1.0.1
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Pete Bryan
support:
tier: Community
categories:
domains: [ "Security - Others", "Identity" ]
Stages and Predicates
Let Binding: known_locations
let known_locations = (
AADServicePrincipalSignInLogs
| where TimeGenerated between(ago(14d)..ago(1d))
| where ResultType == 0
| summarize by Location);
Stage 1: source
AADServicePrincipalSignInLogs
Stage 2: where
| where TimeGenerated > ago(1d)
Stage 3: where
| where ResultType != 50126
Stage 4: where
| where Location !in (known_locations)
Stage 5: extend (4 consecutive steps)
| extend City = tostring(parse_json(LocationDetails).city)
| extend State = tostring(parse_json(LocationDetails).state)
| extend Place = strcat(City, " - ", State)
| extend Result = strcat(tostring(ResultType), " - ", ResultDescription)
Stage 6: summarize
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), make_set(Result), make_set(IPAddress), make_set(Place) by ServicePrincipalName, Location
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ResultType | ne |
| field:"ResultType" kind:ne value:"50126" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Location | eq | known_locations | excludes:Location field:"Location" value:"known_locations" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
FirstSeen | summarize |
LastSeen | summarize |
Location | summarize |
ServicePrincipalName | summarize |
SFTP File transfer folder count above threshold
#'Identifies SFTP File Transfers with distinct folder count above certain threshold in a 15min time period. It requires SFTP VERBOSE loglevel to be enabled. Please note that entity mapping for arrays is not supported, so when there is a single value in an array, we will pull that value from the array as a single string to populate the entity to support entity mapping features within Sentinel. Additionally, if the array is multivalued, we will input a string to indicate this with a unique hash so that matching will not occur.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Rule Body
id: 7355434e-09d5-4401-b56d-e03e9379dfb1
name: SFTP File transfer folder count above threshold
description: |
'Identifies SFTP File Transfers with distinct folder count above certain threshold in a 15min time period.
It requires SFTP VERBOSE loglevel to be enabled.
Please note that entity mapping for arrays is not supported, so when there is a single value in an array, we will pull that value from the array as a single string to populate the entity to support entity mapping features within Sentinel. Additionally, if the array is multivalued, we will input a string to indicate this with a unique hash so that matching will not occur.'
severity: Medium
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
relevantTechniques:
- T1020
query: |
let threshold = 10;
Syslog
| where ProcessName has "sftp"
and SyslogMessage has "close "
and SyslogMessage has " bytes read "
| parse SyslogMessage with "close \"" filepath "\" bytes read " readbytes: int " written " writtenbytes: int
| parse kind=regex filepath with dirpath:string "/" filename:string
| join kind=leftouter (
Syslog
| where ProcessName has "sftp" and SyslogMessage has "session opened for"
| parse SyslogMessage with "session opened for local user " username: string " from [" src_ip "]"
| project username, src_ip, ProcessID
)
on ProcessID
| project-away ProcessID1
| summarize count_distinct_dirpath=dcount(dirpath), dirlist=make_set(dirpath) by Computer, username, src_ip, bin(TimeGenerated, 15m)
| where count_distinct_dirpath >= threshold
| extend DirSample = iff(array_length(dirlist) == 1, tostring(dirlist[0]), strcat("SeeDirListField","_", tostring(hash(tostring(dirlist)))))
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: username
- entityType: IP
fieldMappings:
- identifier: Address
columnName: src_ip
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Computer
- entityType: File
fieldMappings:
- identifier: Name
columnName: DirSample
customDetails:
TransferCount: count_distinct_dirpath
FilesList: dirlist
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 5h
matchingMethod: Selected
groupByEntities:
- Account
- Host
groupByAlertDetails: []
groupByCustomDetails: []
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 10;
Stage 1: source
Syslog
Stage 2: where
| where ProcessName has "sftp"
and SyslogMessage has "close "
and SyslogMessage has " bytes read "
Stage 3: parse
| parse SyslogMessage with "close \"" filepath "\" bytes read " readbytes: int " written " writtenbytes: int
Stage 4: parse
| parse kind=regex filepath with dirpath:string "/" filename:string
Stage 5: join
| join kind=leftouter (
Syslog
| where ProcessName has "sftp" and SyslogMessage has "session opened for"
| parse SyslogMessage with "session opened for local user " username: string " from [" src_ip "]"
| project username, src_ip, ProcessID
)
on ProcessID
Stage 6: project-away
project-away ProcessID1
Stage 7: summarize
summarize count_distinct_dirpath, dirlist by Computer, username, src_ip
Stage 8: where
where count_distinct_dirpath >= 10
Stage 9: extend
extend DirSample
DirSample =dirlist == 1tostring(dirlist[0])strcat("SeeDirListField", "_", tostring(hash(tostring(dirlist))))Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ProcessName | match |
| field:"process_name" kind:match value:"sftp" |
SyslogMessage | match |
| field:"SyslogMessage" kind:match |
count_distinct_dirpath | ge |
| field:"count_distinct_dirpath" kind:ge value:"10" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Computer | summarize |
count_distinct_dirpath | summarize |
dirlist | summarize |
src_ip | summarize |
username | summarize |
DirSample | extend |
Suspicious access of BEC related documents in AWS S3 buckets
#This query looks for users with suspicious spikes in the number of files accessed that relate to topics commonly accessed as part of Business Email Compromise (BEC) attacks. The query looks for access to files in AWS S3 storage that relate to topics such as invoices or payments, and then looks for users accessing these files in significantly higher numbers than in the previous 14 days. Incidents raised by this analytic should be investigated to see if the user accessing these files should be accessing them, and if the volume they accessed them at was related to a legitimate business need. This query contains thresholds to reduce the chance of false positives, these can be adjusted to suit individual environments. In addition false positives could be generated by legitimate, scheduled actions that occur less often than every 14 days, additional exclusions can be added for these actions on username or IP address entities.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Collection |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| AWS | CloudTrail event GetObject: Retrieves an object from a specified S3 bucket. |
Rules Detecting the Same Action
These rules filter on the same operation.
- AWS Exfiltration via Anomalous GetObject API Activity (Splunk)
- AWSCloudTrail - S3 bucket suspicious ransomware activity (Kusto)
- AWSCloudTrail - S3 Object Exfiltration from Anonymous User (Kusto)
- AWSCloudTrail - Successful brute force attack on S3 Bucket (Kusto)
- S3 Access Via VPC Endpoint From External IP (Panther)
Rule Body
id: f3e2d35f-1202-4215-995c-4654ef07d1d8
name: Suspicious access of BEC related documents in AWS S3 buckets
description: |
'This query looks for users with suspicious spikes in the number of files accessed that relate to topics commonly accessed as part of Business Email Compromise (BEC) attacks.
The query looks for access to files in AWS S3 storage that relate to topics such as invoices or payments, and then looks for users accessing these files in significantly higher numbers than in the previous 14 days. Incidents raised by this analytic should be investigated to see if the user accessing these files should be accessing them, and if the volume they accessed them at was related to a legitimate business need.
This query contains thresholds to reduce the chance of false positives, these can be adjusted to suit individual environments. In addition false positives could be generated by legitimate, scheduled actions that occur less often than every 14 days, additional exclusions can be added for these actions on username or IP address entities.'
severity: Medium
requiredDataConnectors:
- connectorId: AWS
dataTypes:
- AWSCloudTrail
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Collection
relevantTechniques:
- T1530
eventGroupingSettings:
aggregationKind: SingleAlert
query: |
let BEC_Keywords = dynamic([ 'invoice','payment','paycheck','transfer','bank statement','bank details','closing','funds','bank account','account details','remittance','purchase','deposit',"PO#","Zahlung","Rechnung","Paiement", "virement bancaire","Bankuberweisung",'hacked','phishing']);
// Adjust this threshold based on your environment
let sensitivity = 2.5;
let Events = materialize(AWSCloudTrail
| where TimeGenerated between (ago(14d)..ago(0d))
| where UserIdentityAccountId != "anonymous"
| where EventSource startswith "s3."
| where EventName =~ "GetObject"
| extend FilePath = tostring(parse_json(RequestParameters).key)
| where FilePath has_any(BEC_Keywords)
);
Events
| summarize dcount(FilePath) by UserIdentityPrincipalid, bin(startofday(TimeGenerated), 1d)
| summarize CountOfDocs = make_list(dcount_FilePath, 10000), TimeStamp = make_list(TimeGenerated, 10000) by UserIdentityPrincipalid
| extend (Anomalies, Score, Baseline) = series_decompose_anomalies(CountOfDocs, sensitivity, -1, 'linefit')
| mv-expand CountOfDocs to typeof(double), TimeStamp to typeof(datetime), Anomalies to typeof(double),Score to typeof(double), Baseline to typeof(long)
| where Anomalies > 0
| project TimeStamp, CountOfDocs, Baseline, Score, Anomalies, UserIdentityPrincipalid
| join kind=inner(Events | extend TimeStamp = startofday(TimeGenerated)) on TimeStamp, UserIdentityPrincipalid
| extend Name = iif(UserIdentityUserName contains "@", split(UserIdentityUserName, "@")[0], UserIdentityUserName)
| extend UPNSuffix = iif(UserIdentityUserName contains "@", split(UserIdentityUserName, "@")[1], "")
| project-reorder TimeGenerated, UserIdentityType, UserIdentityPrincipalid, UserIdentityUserName, FilePath, EventName, UserAgent, SourceIpAddress, CountOfDocs, Baseline, Score
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserIdentityUserName
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIpAddress
- entityType: File
fieldMappings:
- identifier: Name
columnName: FilePath
customDetails:
UserType: UserIdentityType
Event: EventName
UserAgent: UserAgent
alertDetailsOverride:
alertDisplayNameFormat: Suspicious access of {{CountOfDocs}} BEC related documents in AWS S3 buckets by {{UserIdentityUserName}}
alertDescriptionFormat: |
This query looks for users (in this case {{UserIdentityUserName}}) with suspicious spikes in the number of files accessed (in this case {{CountOfDocs}})that relate to topics commonly accessed as part of Business Email Compromise (BEC) attacks. The query looks for access to files in AWS S3 storage that relate to topics such as invoices or payments, and then looks for users accessing these files in significantly higher numbers than in the previous 14 days. Incidents raised by this analytic should be investigated to see if the user accessing these files should be accessing them, and if the volume they accessed them at was related to a legitimate business need.
This query contains thresholds to reduce the chance of false positives, these can be adjusted to suit individual environments. In addition false positives could be generated by legitimate, scheduled actions that occur less often than every 14 days, additional exclusions can be added for these actions on username or IP address entities.
version: 1.0.4
kind: Scheduled
Stages and Predicates
Parameters
let sensitivity = 2.5;
let Events is inlined into the numbered stages below.
Let Binding: BEC_Keywords
let BEC_Keywords = dynamic([ 'invoice','payment','paycheck','transfer','bank statement','bank details','closing','funds','bank account','account details','remittance','purchase','deposit',"PO#","Zahlung","Rechnung","Paiement", "virement bancaire","Bankuberweisung",'hacked','phishing']);
Stages 1 to 7 define let Events as the rule's main pipeline source. Stages 8 to 17 use it.
Stage 1: source
AWSCloudTrail
Stage 2: where
| where TimeGenerated between (ago(14d)..ago(0d))
Stage 3: where
| where UserIdentityAccountId != "anonymous"
Stage 4: where
| where EventSource startswith "s3."
Stage 5: where
| where EventName =~ "GetObject"
Stage 6: extend
| extend FilePath = tostring(parse_json(RequestParameters).key)
Stage 7: where
| where FilePath has_any(BEC_Keywords)
Stage 8: summarize
Events
| summarize dcount(FilePath) by UserIdentityPrincipalid, bin(startofday(TimeGenerated), 1d)
Stage 9: summarize
| summarize CountOfDocs = make_list(dcount_FilePath, 10000), TimeStamp = make_list(TimeGenerated, 10000) by UserIdentityPrincipalid
The stages below score time-series anomalies (make-series, series_decompose_anomalies).
Stage 10: extend
| extend (Anomalies, Score, Baseline) = series_decompose_anomalies(CountOfDocs, sensitivity, -1, 'linefit')
Stage 11: mv-expand
| mv-expand CountOfDocs to typeof(double), TimeStamp to typeof(datetime), Anomalies to typeof(double),Score to typeof(double), Baseline to typeof(long)
Stage 12: where
| where Anomalies > 0
Stage 13: project
| project TimeStamp, CountOfDocs, Baseline, Score, Anomalies, UserIdentityPrincipalid
Stage 14: join
| join kind=inner(Events | extend TimeStamp = startofday(TimeGenerated)) on TimeStamp, UserIdentityPrincipalid
Stage 15: extend
extend Name
Stage 16: extend
extend UPNSuffix
Stage 17: project-reorder
project-reorder
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Anomalies | gt |
| field:"Anomalies" kind:gt value:"0" |
EventName | eq |
| field:"aws::eventName" kind:eq |
EventSource | starts_with |
| field:"aws::eventSource" kind:starts_with |
FilePath | match |
| field:"FilePath" kind:match |
UserIdentityAccountId | ne |
| field:"UserIdentityAccountId" kind:ne value:"anonymous" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Anomalies | project |
Baseline | project |
CountOfDocs | project |
Score | project |
TimeStamp | project |
UserIdentityPrincipalid | project |
Name | extend |
UPNSuffix | extend |
Tailscale: Device key expiring within 7 days
#Identifies tailnet devices whose machine key expires within the next 7 days and where key expiry is not disabled. Surface proactively so renewal can be scheduled rather than forced during an outage.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: b1a2c3d4-1234-5678-90ab-cdef12345001
name: "Tailscale: Device key expiring within 7 days"
description: |
Identifies tailnet devices whose machine key expires within the next 7 days and where key expiry is not disabled. Surface proactively so renewal can be scheduled rather than forced during an outage.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: TailscaleCCF
dataTypes:
- Tailscale_Devices_CL
queryFrequency: 6h
queryPeriod: 6h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |
Tailscale_Devices_CL
| where TimeGenerated > ago(6h)
| summarize arg_max(TimeGenerated, *) by DeviceId
| where KeyExpiryDisabled == false
| where isnotnull(Expires)
| where Expires between (now() .. now() + 7d)
| extend DaysToExpiry = datetime_diff('day', Expires, now())
| project TimeGenerated, DeviceName, Hostname, User, Os, DaysToExpiry, Expires, LastSeen
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Hostname
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: User
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: P1D
matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0
Stages and Predicates
Stage 1: source
Tailscale_Devices_CL
Stage 2: where
| where TimeGenerated > ago(6h)
Stage 3: summarize
| summarize arg_max(TimeGenerated, *) by DeviceId
Stage 4: where
| where KeyExpiryDisabled == false
Stage 5: where
| where isnotnull(Expires)
Stage 6: where
| where Expires between (now() .. now() + 7d)
Stage 7: extend
| extend DaysToExpiry = datetime_diff('day', Expires, now())
Stage 8: project
| project TimeGenerated, DeviceName, Hostname, User, Os, DaysToExpiry, Expires, LastSeen
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Expires | is_not_null | field:"Expires" kind:is_not_null | |
KeyExpiryDisabled | eq |
| field:"KeyExpiryDisabled" kind:eq value:"false" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DaysToExpiry | project |
DeviceName | project |
Expires | project |
Hostname | project |
LastSeen | project |
Os | project |
TimeGenerated | project |
User | project |
Trend Micro CAS - Multiple infected users
#'Detects when same malware was detected for multiple user account.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 65c2a6fe-ff7b-46b0-9278-61265f77f3bc
name: Trend Micro CAS - Multiple infected users
description: |
'Detects when same malware was detected for multiple user account.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: TrendMicroCAS
dataTypes:
- TrendMicroCAS
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1566
query: |
TrendMicroCAS
| where EventType has_all ('virtual', 'analyzer')
| where isnotempty(VirusName)
| summarize count() by DstUserName, VirusName, bin(TimeGenerated, 15m)
| where count_ >= 2
| extend AccountCustomEntity = DstUserName, MalwareCustomEntity = VirusName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
- entityType: Malware
fieldMappings:
- identifier: Name
columnName: MalwareCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
TrendMicroCAS
Stage 2: where
| where EventType has_all ('virtual', 'analyzer')
Stage 3: where
| where isnotempty(VirusName)
Stage 4: summarize
| summarize count() by DstUserName, VirusName, bin(TimeGenerated, 15m)
Stage 5: where
| where count_ >= 2
Stage 6: extend
| extend AccountCustomEntity = DstUserName, MalwareCustomEntity = VirusName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventType | match |
| field:"EventType" kind:match |
VirusName | is_not_null | field:"VirusName" kind:is_not_null | |
count_ | ge |
| field:"count_" kind:ge value:"2" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DstUserName | summarize |
VirusName | summarize |
AccountCustomEntity | extend |
MalwareCustomEntity | extend |
User or Group Added
#Detects when a user or user group is added to Veeam Backup & Replication.
Rule Body
id: 25111c10-44b5-4275-996d-d62f15a7d072
name: User or Group Added
description: Detects when a user or user group is added to Veeam Backup & Replication.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 31200\n| project\n Date\
\ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n\
\ EventId = instanceId,\n UserName = 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 == 31200
Stage 3: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
UserName = 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:"31200" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Severity | project |
UserName | project |