Detection rules › Kusto
Kusto rules: 01
Account Created and Deleted in Short Timeframe
#Search for user principal name (UPN) events. Look for accounts created and then deleted within 7 days. Attackers may create an account for their use, and then remove the account when no longer needed. Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#short-lived-account
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Add user |
| Azure | Delete user |
| Entra ID | SigninLogs sign-in event: Sign-in record (SigninLogs, any result) |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Authentication Method Changed for Privileged Account (Kusto)
- Account Created And Deleted Within A Close Time Frame (Sigma)
- Account created from non-approved sources (Kusto)
- Account created or deleted by non-approved user (Kusto)
- Addition of a Temporary Access Pass to a Privileged Account (Kusto)
- Authentication Method Changed for Privileged Account (Kusto)
- Authentication Methods Changed for Privileged Account (Kusto)
- Azure AD External Guest User Invited (Splunk)
Rule Body
id: bb616d82-108f-47d3-9dec-9652ea0d3bf6
name: Account Created and Deleted in Short Timeframe
description: |
'Search for user principal name (UPN) events. Look for accounts created and then deleted within 7 days. Attackers may create an account for their use, and then remove the account when no longer needed.
Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#short-lived-account'
severity: High
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
queryFrequency: 1h
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
relevantTechniques:
- T1078.004
tags:
- AADSecOpsGuide
query: |
let queryfrequency = 1h;
let queryperiod = 7d; // Increased queryperiod to 7 days to mitigate timing-based bypasses
AuditLogs
| where TimeGenerated > ago(queryfrequency)
| where OperationName =~ "Delete user"
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type == "User"
// Normalize TargetUserPrincipalName by removing optional 32-char hex prefixes and underscores
// Making extraction case-insensitive, and converting the result to lowercase for consistency
| extend TargetUserPrincipalName = tolower(extract(@"(?i)([a-f0-9]{32})?_?(.*)", 2, tostring(TargetResource.userPrincipalName)))
// Standardize UserId to ensure consistent, immutable identification
| extend UserId = tostring(TargetResource.id)
)
| extend DeletedByApp = tostring(InitiatedBy.app.displayName),
DeletedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId),
DeletedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName),
DeletedByAadUserId = tostring(InitiatedBy.user.id),
DeletedByIPAddress = tostring(InitiatedBy.user.ipAddress)
| project Deletion_TimeGenerated = TimeGenerated, TargetUserPrincipalName, UserId, DeletedByApp, DeletedByAppServicePrincipalId, DeletedByUserPrincipalName, DeletedByAadUserId, DeletedByIPAddress,
Deletion_AdditionalDetails = AdditionalDetails, Deletion_InitiatedBy = InitiatedBy, Deletion_TargetResources = TargetResources
| join kind=inner (
AuditLogs
| where TimeGenerated > ago(queryperiod)
| where OperationName =~ "Add user"
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type == "User"
// Normalize TargetUserPrincipalName by removing optional 32-char hex prefixes and underscores
// Making extraction case-insensitive, and converting the result to lowercase for consistency
| extend TargetUserPrincipalName = tolower(extract(@"(?i)([a-f0-9]{32})?_?(.*)", 2, tostring(TargetResource.userPrincipalName)))
// Standardize UserId to ensure consistent, immutable identification
| extend UserId = tostring(TargetResource.id)
)
| project-rename Creation_TimeGenerated = TimeGenerated
) on UserId
| extend TimeDelta = Deletion_TimeGenerated - Creation_TimeGenerated
| where TimeDelta between (time(0s) .. queryperiod)
| extend CreatedByApp = tostring(InitiatedBy.app.displayName),
CreatedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId),
CreatedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName),
CreatedByAadUserId = tostring(InitiatedBy.user.id),
CreatedByIPAddress = tostring(InitiatedBy.user.ipAddress)
| project Creation_TimeGenerated, Deletion_TimeGenerated, TimeDelta, UserId, TargetUserPrincipalName, DeletedByApp, DeletedByAppServicePrincipalId, DeletedByUserPrincipalName, DeletedByAadUserId, DeletedByIPAddress,
CreatedByApp, CreatedByAppServicePrincipalId, CreatedByUserPrincipalName, CreatedByAadUserId, CreatedByIPAddress, Creation_AdditionalDetails = AdditionalDetails, Creation_InitiatedBy = InitiatedBy, Creation_TargetResources = TargetResources, Deletion_AdditionalDetails, Deletion_InitiatedBy, Deletion_TargetResources
| extend TargetName = tostring(split(TargetUserPrincipalName,'@',0)[0]), TargetUPNSuffix = tostring(split(TargetUserPrincipalName,'@',1)[0])
| extend CreatedByName = tostring(split(CreatedByUserPrincipalName,'@',0)[0]), CreatedByUPNSuffix = tostring(split(CreatedByUserPrincipalName,'@',1)[0])
| extend DeletedByName = tostring(split(DeletedByUserPrincipalName,'@',0)[0]), DeletedByUPNSuffix = tostring(split(DeletedByUserPrincipalName,'@',1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: TargetUserPrincipalName
- identifier: Name
columnName: TargetName
- identifier: UPNSuffix
columnName: TargetUPNSuffix
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: CreatedByUserPrincipalName
- identifier: Name
columnName: CreatedByName
- identifier: UPNSuffix
columnName: CreatedByUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: CreatedByAadUserId
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: DeletedByUserPrincipalName
- identifier: Name
columnName: DeletedByName
- identifier: UPNSuffix
columnName: DeletedByUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: DeletedByAadUserId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: CreatedByIPAddress
- entityType: IP
fieldMappings:
- identifier: Address
columnName: DeletedByIPAddress
version: 1.1.2
kind: Scheduled
Stages and Predicates
Parameters
let queryfrequency = 1h;
let queryperiod = 7d;
Stage 1: source
AuditLogs
Stage 2: where
| where TimeGenerated > ago(queryfrequency)
Stage 3: where
| where OperationName =~ "Delete user"
Stage 4: kusto:mv-apply
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type == "User"
| extend TargetUserPrincipalName = tolower(extract(@"(?i)([a-f0-9]{32})?_?(.*)", 2, tostring(TargetResource.userPrincipalName)))
| extend UserId = tostring(TargetResource.id)
)
Stage 5: extend
| extend DeletedByApp = tostring(InitiatedBy.app.displayName),
DeletedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId),
DeletedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName),
DeletedByAadUserId = tostring(InitiatedBy.user.id),
DeletedByIPAddress = tostring(InitiatedBy.user.ipAddress)
Stage 6: project
| project Deletion_TimeGenerated = TimeGenerated, TargetUserPrincipalName, UserId, DeletedByApp, DeletedByAppServicePrincipalId, DeletedByUserPrincipalName, DeletedByAadUserId, DeletedByIPAddress,
Deletion_AdditionalDetails = AdditionalDetails, Deletion_InitiatedBy = InitiatedBy, Deletion_TargetResources = TargetResources
Stage 7: join
| join kind=inner (
AuditLogs
| where TimeGenerated > ago(queryperiod)
| where OperationName =~ "Add user"
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type == "User"
| extend TargetUserPrincipalName = tolower(extract(@"(?i)([a-f0-9]{32})?_?(.*)", 2, tostring(TargetResource.userPrincipalName)))
| extend UserId = tostring(TargetResource.id)
)
| project-rename Creation_TimeGenerated = TimeGenerated
) on UserId
Stage 8: extend
extend TimeDelta
Stage 9: where
where /* macro: (TimeDelta between (time(0s) .. 7d)) */
Stage 10: extend
extend CreatedByAadUserId, CreatedByApp, CreatedByAppServicePrincipalId, CreatedByIPAddress, CreatedByUserPrincipalName
Stage 11: project
project CreatedByAadUserId, CreatedByApp, CreatedByAppServicePrincipalId, CreatedByIPAddress, CreatedByUserPrincipalName, Creation_AdditionalDetails, Creation_InitiatedBy, Creation_TargetResources, Creation_TimeGenerated, DeletedByAadUserId, DeletedByApp, DeletedByAppServicePrincipalId, DeletedByIPAddress, DeletedByUserPrincipalName, Deletion_AdditionalDetails, Deletion_InitiatedBy, Deletion_TargetResources, Deletion_TimeGenerated, TargetUserPrincipalName, TimeDelta, UserId
Stage 12: extend (3 consecutive steps)
extend CreatedByName, CreatedByUPNSuffix, DeletedByName, DeletedByUPNSuffix, TargetName, TargetUPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | eq |
| field:"OperationName" kind:eq |
TargetResource.type | eq |
| field:"TargetResource.type" kind:eq value:"User" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CreatedByAadUserId | project |
CreatedByApp | project |
CreatedByAppServicePrincipalId | project |
CreatedByIPAddress | project |
CreatedByUserPrincipalName | project |
Creation_AdditionalDetails | project |
Creation_InitiatedBy | project |
Creation_TargetResources | project |
Creation_TimeGenerated | project |
DeletedByAadUserId | project |
DeletedByApp | project |
DeletedByAppServicePrincipalId | project |
DeletedByIPAddress | project |
DeletedByUserPrincipalName | project |
Deletion_AdditionalDetails | project |
Deletion_InitiatedBy | project |
Deletion_TargetResources | project |
Deletion_TimeGenerated | project |
TargetUserPrincipalName | project |
TimeDelta | project |
UserId | project |
TargetName | extend |
TargetUPNSuffix | extend |
CreatedByName | extend |
CreatedByUPNSuffix | extend |
DeletedByName | extend |
DeletedByUPNSuffix | extend |
Acronis - Multiple Inboxes with Malicious Content Detected
#Many inboxes containing malicious content could indicate a potential ongoing phishing attack.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 5090ad7b-4b47-4cab-9015-bffb43aecde8
name: Acronis - Multiple Inboxes with Malicious Content Detected
description: Many inboxes containing malicious content could indicate a potential ongoing phishing attack.
severity: Medium
query: |
CommonSecurityLog
| where DeviceVendor == "Acronis"
| where DeviceEventClassID in
("MaliciousURLDetectedInM365MailboxBackup",
"MalwareDetectedInM365MailboxBackup",
"MaliciousEmailDetectedPerceptionPointWarning")
| summarize MaliciousContent = count() by DeviceName
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 2
tactics:
- InitialAccess
relevantTechniques:
- T1566.002
- T1566.001
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: true
lookbackDuration: P1D
matchingMethod: AnyAlert
eventGroupingSettings:
aggregationKind: SingleAlert
customDetails:
DeviceName: DeviceName
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: DeviceName
version: 1.0.0
kind: Scheduled
requiredDataConnectors: []
Stages and Predicates
Stage 1: source
CommonSecurityLog
Stage 2: where
| where DeviceVendor == "Acronis"
Stage 3: where
| where DeviceEventClassID in
("MaliciousURLDetectedInM365MailboxBackup",
"MalwareDetectedInM365MailboxBackup",
"MaliciousEmailDetectedPerceptionPointWarning")
Stage 4: summarize
| summarize MaliciousContent = count() by DeviceName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DeviceEventClassID | in |
| field:"DeviceEventClassID" kind:in |
DeviceVendor | eq |
| field:"DeviceVendor" kind:eq value:"Acronis" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceName | summarize |
MaliciousContent | summarize |
AWS role with admin privileges
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
The policy detects the creation of new AWS roles with administrative privileges. The policy configuration allows limiting the policy to specific accounts.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 734c00a0-a95b-44dd-9b69-d926ed44256d
name: AWS role with admin privileges
kind: Scheduled
description: The policy detects the creation of new AWS roles with administrative privileges. The policy configuration allows limiting the policy to specific accounts.
severity: High
status: Available
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |-
Authomize_v2_CL
| where ingestion_time() >= ago(30m)
| extend EventID = id_s, Policy = policy_name_s, Severity = severity_s,Description = description_s,Recommendation = recommendation_s,URL = url_s,Tactics = tactics_s
| where Policy has "AWS role with admin privileges"
| project EventID, Policy, Severity, Description, Recommendation, URL, Category, Tactics
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: URL
requiredDataConnectors:
- connectorId: Authomize
dataTypes: [ "Authomize_v2_CL" ]
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 5h
matchingMethod: AnyAlert
groupByEntities: []
groupByAlertDetails: []
groupByCustomDetails: []
eventGroupingSettings:
aggregationKind: SingleAlert
alertDetailsOverride:
alertnameFormat: Alert from Authomize - AWS role with admin privileges
alertDescriptionFormat: AWS role with admin privileges. The policy detects the creation of new AWS roles with administrative privileges. The policy configuration allows limiting the policy to specific accounts.
alertSeverity: Severity
alertTactics: Tactics
alertDynamicProperties:
- alertProperty: AlertLink
value: URL
customDetails:
AuthomizeEventID: EventID
EventName: Policy
EventDescription: Description
EventRecommendation: Recommendation
ReferencedURL: URL
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.2
Stages and Predicates
Stage 1: source
Authomize_v2_CL
Stage 2: where
| where ingestion_time() >= ago(30m)
Stage 3: extend
| extend EventID = id_s, Policy = policy_name_s, Severity = severity_s,Description = description_s,Recommendation = recommendation_s,URL = url_s,Tactics = tactics_s
Stage 4: where
| where Policy has "AWS role with admin privileges"
Stage 5: project
| project EventID, Policy, Severity, Description, Recommendation, URL, Category, Tactics
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Policy | match |
| field:"policy_name_s" kind:match value:"AWS role with admin privileges" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Category | project |
Description | project |
EventID | project |
Policy | project |
Recommendation | project |
Severity | project |
Tactics | project |
URL | project |
AWSCloudTrail - Login to AWS Management Console without MFA
#Identifies successful AWS Management Console sign-ins where CloudTrail records a ConsoleLogin event without multi-factor authentication. The rule looks for logins where MFAUsed is not Yes and the console response is not Failure, which can indicate credential misuse or weak account protection.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence | |
| Privilege Escalation | |
| Stealth |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- AWS Console Login (Panther)
- AWS Console Login Failed During MFA Challenge (Splunk)
- AWS ConsoleLogin Failed Authentication (Sigma)
- AWS CreateLoginProfile (Splunk)
- AWS Credential Access Failed Login (Splunk)
- AWS High Number Of Failed Authentications For User (Splunk)
- AWS High Number Of Failed Authentications From Ip (Splunk)
- AWS Multiple Failed MFA Requests For User (Splunk)
Rule Body
id: d25b1998-a592-4bc5-8a3a-92b39eedb1bc
name: AWSCloudTrail - Login to AWS Management Console without MFA
description: |
Identifies successful AWS Management Console sign-ins where CloudTrail records a ConsoleLogin event without
multi-factor authentication. The rule looks for logins where MFAUsed is not Yes and the console response is not
Failure, which can indicate credential misuse or weak account protection.
severity: Low
status: Available
requiredDataConnectors:
- connectorId: AWS
dataTypes:
- AWSCloudTrail
- connectorId: AWSS3
dataTypes:
- AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
- PrivilegeEscalation
- Persistence
- InitialAccess
relevantTechniques:
- T1078
query: |
AWSCloudTrail
| where EventName =~ "ConsoleLogin"
| extend MFAUsed = tostring(parse_json(AdditionalEventData).MFAUsed), LoginResult = tostring(parse_json(ResponseElements).ConsoleLogin), indexId = indexof(tostring(UserIdentityPrincipalid),":")
| where MFAUsed !~ "Yes" and LoginResult !~ "Failure"
| where SessionIssuerUserName !contains "AWSReservedSSO"
| 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]), "")
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by EventName, EventTypeName, LoginResult, MFAUsed, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, UserIdentityPrincipalid, UserAgent,
UserIdentityUserName, SessionMfaAuthenticated, SourceIpAddress, AWSRegion, indexId
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- identifier: CloudAppAccountId
columnName: RecipientAccountId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIpAddress
customDetails:
EventName: EventName
LoginResult: LoginResult
MFAUsed: MFAUsed
AWSRegion: AWSRegion
UserAgent: UserAgent
alertDetailsOverride:
alertDisplayNameFormat: 'AWS Management Console sign-in without MFA: {{AccountName}} from {{SourceIpAddress}}'
alertDescriptionFormat: 'Successful ConsoleLogin without MFA for {{AccountName}} from {{SourceIpAddress}} in account {{RecipientAccountId}}.'
version: 1.0.6
kind: Scheduled
Stages and Predicates
Stage 1: source
AWSCloudTrail
Stage 2: where
| where EventName =~ "ConsoleLogin"
Stage 3: extend
| extend MFAUsed = tostring(parse_json(AdditionalEventData).MFAUsed), LoginResult = tostring(parse_json(ResponseElements).ConsoleLogin), indexId = indexof(tostring(UserIdentityPrincipalid),":")
Stage 4: where
| where MFAUsed !~ "Yes" and LoginResult !~ "Failure"
Stage 5: where
| where SessionIssuerUserName !contains "AWSReservedSSO"
Stage 6: 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 7: summarize
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by EventName, EventTypeName, LoginResult, MFAUsed, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, UserIdentityPrincipalid, UserAgent,
UserIdentityUserName, SessionMfaAuthenticated, SourceIpAddress, AWSRegion, indexId
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventName | eq |
| field:"aws::eventName" kind:eq value:"ConsoleLogin" |
LoginResult | ne |
| field:"LoginResult" kind:ne value:"Failure" |
MFAUsed | ne |
| field:"MFAUsed" kind:ne value:"Yes" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
SessionIssuerUserName | contains | AWSReservedSSO | excludes:SessionIssuerUserName field:"SessionIssuerUserName" value:"AWSReservedSSO" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AWSRegion | summarize |
AccountName | summarize |
AccountUPNSuffix | summarize |
EndTimeUtc | summarize |
EventName | summarize |
EventTypeName | summarize |
LoginResult | summarize |
MFAUsed | summarize |
RecipientAccountId | summarize |
SessionMfaAuthenticated | summarize |
SourceIpAddress | summarize |
StartTimeUtc | summarize |
UserAgent | summarize |
UserIdentityAccountId | summarize |
UserIdentityPrincipalid | summarize |
UserIdentityUserName | summarize |
indexId | summarize |
AWSCloudTrail - S3 bucket exposed via ACL
#Detects S3 bucket ACL changes that grant public or broadly authenticated access. Overly permissive ACL assignments can expose bucket contents to unauthorized users.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- AWS S3 Bucket Policy Modified (Panther)
Rule Body
id: 6b9b4ee6-f4c1-4b86-8c8c-beb0bb59ae44
name: AWSCloudTrail - S3 bucket exposed via ACL
description: |
Detects S3 bucket ACL changes that grant public or broadly authenticated access. Overly permissive ACL
assignments can expose bucket contents to unauthorized users.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AWS
dataTypes:
- AWSCloudTrail
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
relevantTechniques:
- T1537
query: |
AWSCloudTrail
| where EventName == "PutBucketAcl" 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]), "")
| extend Grant = parse_json(tostring((parse_json(RequestParameters).AccessControlPolicy))).AccessControlList.Grant
| mvexpand Grant
| extend cannedacl = parse_json(tostring((parse_json(RequestParameters))))
| extend URI = parse_json(Grant).Grantee.URI, type = parse_json(Grant).Grantee.["xsi:type"], xamzacl = parse_json(cannedacl).["x-amz-acl"]
| where (type == "Group" and (URI endswith "AllUsers" or URI endswith "AuthenticatedUsers"))
or xamzacl in ("authenticated-read","public-read","public-read-write")
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- identifier: CloudAppAccountId
columnName: RecipientAccountId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIpAddress
customDetails:
EventName: EventName
URI: URI
xamzacl: xamzacl
RecipientAccountId: RecipientAccountId
alertDetailsOverride:
alertDisplayNameFormat: 'AWS S3 bucket ACL exposure change by {{AccountName}}'
alertDescriptionFormat: 'Detected {{EventName}} from {{SourceIpAddress}} applying public or broad ACL on S3 bucket in account {{RecipientAccountId}}.'
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
AWSCloudTrail
Stage 2: where
| where EventName == "PutBucketAcl" and isempty(ErrorCode) and isempty(ErrorMessage)
Stage 3: extend (5 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]), "")
| extend Grant = parse_json(tostring((parse_json(RequestParameters).AccessControlPolicy))).AccessControlList.Grant
Stage 4: mv-expand
| mvexpand Grant
Stage 5: extend
| extend cannedacl = parse_json(tostring((parse_json(RequestParameters))))
Stage 6: extend
| extend URI = parse_json(Grant).Grantee.URI, type = parse_json(Grant).Grantee.["xsi:type"], xamzacl = parse_json(cannedacl).["x-amz-acl"]
Stage 7: where
| where (type == "Group" and (URI endswith "AllUsers" or URI endswith "AuthenticatedUsers"))
or xamzacl in ("authenticated-read","public-read","public-read-write")
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ErrorCode | is_null | field:"aws::errorCode" kind:is_null | |
ErrorMessage | is_null | field:"aws::errorMessage" kind:is_null | |
EventName | eq |
| field:"aws::eventName" kind:eq value:"PutBucketAcl" |
URI | ends_with |
| field:"URI" kind:ends_with |
type | eq |
| field:"type" kind:eq value:"Group" |
xamzacl | in |
| field:"xamzacl" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
UserIdentityArn | extend |
UserName | extend |
AccountName | extend |
AccountUPNSuffix | extend |
Grant | extend |
cannedacl | extend |
URI | extend |
type | extend |
xamzacl | extend |
CertUtil Used for File Download (Living off the Land)
#Detects certutil.exe being used to download files from remote URLs via the -urlcache or -verifyctl flags. CertUtil is a signed Windows binary (LOLBin) that attackers abuse to download payloads while bypassing application whitelisting and network controls. Tune AllowlistedDomains to match your PKI/CA infrastructure.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Stealth | |
| Command & Control |
Telemetry Coverage
Rule Body
id: 4a9d3c2e-7f1b-4e58-9a0c-2d5b8e3f1a7c
name: CertUtil Used for File Download (Living off the Land)
description: |
Detects certutil.exe being used to download files from remote URLs via the
-urlcache or -verifyctl flags. CertUtil is a signed Windows binary (LOLBin)
that attackers abuse to download payloads while bypassing application
whitelisting and network controls.
Tune AllowlistedDomains to match your PKI/CA infrastructure.
severity: High
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceProcessEvents
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
- DefenseEvasion
relevantTechniques:
- T1105
- T1218
- T1140
query: |
let Lookback = 1h;
let AllowlistedDomains = dynamic([
"windowsupdate.microsoft.com",
"download.microsoft.com",
"dl.delivery.mp.microsoft.com",
"ctldl.windowsupdate.com"
]);
let MDE_Results =
DeviceProcessEvents
| where Timestamp >= ago(Lookback)
| where FileName =~ "certutil.exe"
| where ProcessCommandLine has_any ("-urlcache", "-verifyctl", "-decode", "-decodehex")
| where ProcessCommandLine has_any ("http://", "https://", "ftp://")
| where not(ProcessCommandLine has_any (AllowlistedDomains))
| extend
AccountName = InitiatingProcessAccountName,
AccountDomain = InitiatingProcessAccountDomain,
HostName = DeviceName,
EventTime = Timestamp
| project EventTime, HostName, AccountName, AccountDomain,
ProcessCommandLine, InitiatingProcessFileName;
let SecEvent_Results =
SecurityEvent
| where TimeGenerated >= ago(Lookback)
| where EventID == 4688
| where NewProcessName endswith "\\certutil.exe"
| where CommandLine has_any ("-urlcache", "-verifyctl", "-decode", "-decodehex")
| where CommandLine has_any ("http://", "https://", "ftp://")
| where not(CommandLine has_any (AllowlistedDomains))
| extend
AccountName = SubjectUserName,
AccountDomain = SubjectDomainName,
HostName = Computer,
EventTime = TimeGenerated,
ProcessCommandLine = CommandLine,
InitiatingProcessFileName = ParentProcessName
| project EventTime, HostName, AccountName, AccountDomain,
ProcessCommandLine, InitiatingProcessFileName;
union MDE_Results, SecEvent_Results
| sort by EventTime desc
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: HostName
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: NTDomain
columnName: AccountDomain
- entityType: Process
fieldMappings:
- identifier: CommandLine
columnName: ProcessCommandLine
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let Lookback = 1h;
Let Binding: AllowlistedDomains
let AllowlistedDomains = dynamic([
"windowsupdate.microsoft.com",
"download.microsoft.com",
"dl.delivery.mp.microsoft.com",
"ctldl.windowsupdate.com"
]);
Union (2 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: MDE_Results, SecEvent_Results
Leg 1: MDE_Results
Leg 2: SecEvent_Results
Applied to the Combined Result
| sort by EventTime desc
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
CommandLine | match |
| field:"CommandLine" kind:match |
EventID | eq |
| field:"EventID" kind:eq value:"4688" |
FileName | eq |
| field:"FileName" kind:eq value:"certutil.exe" |
NewProcessName | ends_with |
| field:"NewProcessName" kind:ends_with value:"\certutil.exe" |
ProcessCommandLine | match |
| field:"ProcessCommandLine" kind:match |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
ProcessCommandLine | match | windowsupdate.microsoft.com, download.microsoft.com, dl.delivery.mp.microsoft.com, ctldl.windowsupdate.com | excludes:ProcessCommandLine |
CommandLine | match | windowsupdate.microsoft.com, download.microsoft.com, dl.delivery.mp.microsoft.com, ctldl.windowsupdate.com | excludes:CommandLine |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountDomain | project |
AccountName | project |
EventTime | project |
HostName | project |
InitiatingProcessFileName | project |
ProcessCommandLine | project |
Contrast ADR - DLP SQL Injection Correlation
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
'Detects successful SQL injection attacks identified by Contrast ADR and correlates them with WAF/DLP logs. This rule identifies critical database security breaches that have bypassed initial defenses and may result in data exfiltration or unauthorized database access.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance | |
| Initial Access | |
| Credential Access | |
| Discovery | |
| Lateral Movement | |
| Collection | |
| Command & Control | |
| Exfiltration |
Rule Body
id: 1aac7737-d52f-483d-b225-6a27c1b29a9e
name: Contrast ADR - DLP SQL Injection Correlation
description: |
'Detects successful SQL injection attacks identified by Contrast ADR and correlates them with WAF/DLP logs. This rule identifies critical database security breaches that have bypassed initial defenses and may result in data exfiltration or unauthorized database access.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: ContrastADRCCF
dataTypes:
- ContrastADRAttackEvents_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- CredentialAccess
- Collection
- Exfiltration
- CommandAndControl
- Reconnaissance
- CredentialAccess
- LateralMovement
- Discovery
relevantTechniques:
- T1190
- T1552
- T1005
- T1041
- T1008
- T1590
- T1571
- T1528
- T1021
- T1046
query: |
ContrastADRAttackEvents_CL
| where result =~ "EXPLOITED" and rule =~ "SQL-INJECTION"
| project-rename hostname = host_hostname
//please add your DLP logs table in place of ContrastWAFLogs_CL and hostname colomun in place of hostname below and uncomment the queries below
//| join kind= inner (ContrastWAFLogs_CL | where TimeGenerated >= ago(5m)) on hostname
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: sourceIp
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: hostname
alertDetailsOverride:
alertDisplayNameFormat: 'Confirmed active SQL Injection by Contrast ADR on {{request_headers_referer}} endpoint of {{application_name}} '
alertDescriptionFormat: 'Confirmed active SQL Injection by Contrast ADR on {{request_headers_referer}} endpoint of {{application_name}} '
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: PT30M
matchingMethod: Selected
groupByEntities:
- Host
- IP
eventGroupingSettings:
aggregationKind: AlertPerResult
kind: Scheduled
version: 1.0.1
Stages and Predicates
Stage 1: source
ContrastADRAttackEvents_CL
Stage 2: where
| where result =~ "EXPLOITED" and rule =~ "SQL-INJECTION"
Stage 3: project-rename
| project-rename hostname = host_hostname
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
result | eq |
| field:"result" kind:eq value:"EXPLOITED" |
rule | eq |
| field:"rule" kind:eq value:"SQL-INJECTION" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
hostname | project-rename |
Firewall errors stateful anomaly on database
#'This query batches of distinct SQL queries that failed with error codes that might indicate malicious attempts to gain illegitimate access to the data. When attacker attempts to scan or gain access to server protected by firewall, he will be blocked by firewall and fail with error code 40615. Thus, if we see a large number of logins with such error codes, this could indicate attempts to gain access.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rules Detecting the Same Action
These rules filter on the same operation.
- Affected rows stateful anomaly on database (Kusto)
- Credential errors stateful anomaly on database (Kusto)
- Drop attempts stateful anomaly on database (Kusto)
- Execution attempts stateful anomaly on database (Kusto)
- Firewall rule manipulation attempts stateful anomaly on database (Kusto)
- OLE object manipulation attempts stateful anomaly on database (Kusto)
- Outgoing connection attempts stateful anomaly on database (Kusto)
- Response rows stateful anomaly on database (Kusto)
Rule Body
id: 20f87813-3de0-4a9f-a8c0-6aaa3187be08
name: Firewall errors stateful anomaly on database
description: |
'This query batches of distinct SQL queries that failed with error codes that might indicate malicious attempts to gain illegitimate access to the data. When attacker attempts to scan or gain access to server protected by firewall, he will be blocked by firewall and fail with error code 40615. Thus, if we see a large number of logins with such error codes, this could indicate attempts to gain access.'
severity: Medium
requiredDataConnectors:
- connectorId: AzureSql
dataTypes:
- AzureDiagnostics
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
relevantTechniques:
- T1190
tags:
- SQL
query: |
let monitoredStatementsThreshold = 1; // Minimal number of monitored statements in the slice to trigger an anomaly.
let trainingSlicesThreshold = 5; // The maximal amount of slices with monitored statements in the training window before anomaly detection is throttled.
let timeSliceSize = 1h; // The size of the single timeSlice for individual aggregation.
let detectionWindow = 1h; // The size of the recent detection window for detecting anomalies.
let trainingWindow = detectionWindow + 14d; // The size of the training window before the detection window for learning the normal state.
let monitoredErrors = pack_array(40615); // List of sql error codes relevant for this detection.
let processedData = materialize (
AzureDiagnostics
| where TimeGenerated >= ago(trainingWindow)
| where Category == 'SQLSecurityAuditEvents' and action_id_s has_any ("RCM", "BCM") // Keep only SQL affected rows
| project TimeGenerated, PrincipalName = server_principal_name_s, ClientIp = client_ip_s, HostName = host_name_s, ResourceId,
ApplicationName = application_name_s, ActionName = action_name_s, Database = strcat(LogicalServerName_s, '/', database_name_s),
IsSuccess = succeeded_s, AffectedRows = affected_rows_d,
ResponseRows = response_rows_d, Statement = statement_s,
Error = case( additional_information_s has 'error_code', toint(extract("<error_code>([0-9.]+)", 1, additional_information_s))
, additional_information_s has 'failure_reason', toint(extract("<failure_reason>Err ([0-9.]+)", 1, additional_information_s))
, 0),
State = case( additional_information_s has 'error_state', toint(extract("<error_state>([0-9.]+)", 1, additional_information_s))
, additional_information_s has 'failure_reason', toint(extract("<failure_reason>Err ([0-9.]+), Level ([0-9.]+)", 2, additional_information_s))
, 0),
AdditionalInfo = additional_information_s, timeSlice = floor(TimeGenerated, timeSliceSize)
| summarize countEvents = count(), countStatements = dcount(Statement), countStatementsWithError = dcountif(Statement, Error in (monitoredErrors))
, anyMonitoredStatement = anyif(Statement, Error in (monitoredErrors)), anyInfo = anyif(AdditionalInfo, Error in (monitoredErrors))
by Database, ClientIp, ApplicationName, PrincipalName, timeSlice,HostName,ResourceId
| extend WindowType = case( timeSlice >= ago(detectionWindow), 'detection',
(ago(trainingWindow) <= timeSlice and timeSlice < ago(detectionWindow)), 'training', 'other')
| where WindowType in ('detection', 'training'));
let trainingSet =
processedData
| where WindowType == 'training'
| summarize countSlicesWithErrors = dcountif(timeSlice, countStatementsWithError >= monitoredStatementsThreshold)
by Database;
processedData
| where WindowType == 'detection'
| join kind = inner (trainingSet) on Database
| extend IsErrorAnomalyOnStatement = iff(((countStatementsWithError >= monitoredStatementsThreshold) and (countSlicesWithErrors <= trainingSlicesThreshold)), true, false)
, anomalyScore = round(countStatementsWithError/monitoredStatementsThreshold, 0)
| where IsErrorAnomalyOnStatement == 'true'
| project TimeGenerated = timeSlice, Database, ClientIp, ApplicationName, PrincipalName, HostName, ResourceId, countEvents, countStatements, countStatementsWithError, anyMonitoredStatement, anyInfo, anomalyScore
| extend Name = tostring(split(PrincipalName,'@',0)[0]), UPNSuffix = tostring(split(PrincipalName,'@',1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ClientIp
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: ApplicationName
- entityType: AzureResource
fieldMappings:
- identifier: ResourceId
columnName: ResourceId
alertDetailsOverride:
alertDisplayNameFormat: 'Firewall errors stateful anomaly on database {{Database}}'
alertDescriptionFormat: 'An anomaly was detected on database {{Database}} with {{countStatementsWithError}} statements with monitored errors in the last hour. Investigate the database activity for potential malicious attempts to gain illegitimate access to the data.'
version: 1.1.2
kind: Scheduled
Stages and Predicates
Parameters
let monitoredStatementsThreshold = 1;
let trainingSlicesThreshold = 5;
let timeSliceSize = 1h;
let detectionWindow = 1h;
let trainingWindow = detectionWindow + 14d;
let monitoredErrors = pack_array(40615);
let processedData is inlined into the numbered stages below.
Let Binding: trainingSet
let trainingSet = processedData
| where WindowType == 'training'
| summarize countSlicesWithErrors = dcountif(timeSlice, countStatementsWithError >= monitoredStatementsThreshold)
by Database;
Stages 1 to 7 define let processedData as the rule's main pipeline source. Stages 8 to 13 use it.
Stage 1: source
AzureDiagnostics
Stage 2: where
| where TimeGenerated >= ago(trainingWindow)
Stage 3: where
| where Category == 'SQLSecurityAuditEvents' and action_id_s has_any ("RCM", "BCM")
Stage 4: project
| project TimeGenerated, PrincipalName = server_principal_name_s, ClientIp = client_ip_s, HostName = host_name_s, ResourceId,
ApplicationName = application_name_s, ActionName = action_name_s, Database = strcat(LogicalServerName_s, '/', database_name_s),
IsSuccess = succeeded_s, AffectedRows = affected_rows_d,
ResponseRows = response_rows_d, Statement = statement_s,
Error = case( additional_information_s has 'error_code', toint(extract("<error_code>([0-9.]+)", 1, additional_information_s))
, additional_information_s has 'failure_reason', toint(extract("<failure_reason>Err ([0-9.]+)", 1, additional_information_s))
, 0),
State = case( additional_information_s has 'error_state', toint(extract("<error_state>([0-9.]+)", 1, additional_information_s))
, additional_information_s has 'failure_reason', toint(extract("<failure_reason>Err ([0-9.]+), Level ([0-9.]+)", 2, additional_information_s))
, 0),
AdditionalInfo = additional_information_s, timeSlice = floor(TimeGenerated, timeSliceSize)
Stage 5: summarize
| summarize countEvents = count(), countStatements = dcount(Statement), countStatementsWithError = dcountif(Statement, Error in (monitoredErrors))
, anyMonitoredStatement = anyif(Statement, Error in (monitoredErrors)), anyInfo = anyif(AdditionalInfo, Error in (monitoredErrors))
by Database, ClientIp, ApplicationName, PrincipalName, timeSlice,HostName,ResourceId
Stage 6: extend
| extend WindowType = case( timeSlice >= ago(detectionWindow), 'detection',
(ago(trainingWindow) <= timeSlice and timeSlice < ago(detectionWindow)), 'training', 'other')
WindowType =/* macro: (timeSlice >= ago(detectionWindow)) */'detection'/* macro: (ago(trainingWindow) <= timeSlice) */'training''other'Stage 7: where
| where WindowType in ('detection', 'training')
Stage 8: where
processedData
| where WindowType == 'detection'
Stage 9: join
| join kind = inner (trainingSet) on Database
Stage 10: extend
extend IsErrorAnomalyOnStatement, anomalyScore
IsErrorAnomalyOnStatement =countStatementsWithError >= monitoredStatementsThreshold and countSlicesWithErrors <= trainingSlicesThresholdtruefalseStage 11: where
where IsErrorAnomalyOnStatement == true
Stage 12: project
project ApplicationName, ClientIp, Database, HostName, PrincipalName, ResourceId, TimeGenerated, anomalyScore, anyInfo, anyMonitoredStatement, countEvents, countStatements, countStatementsWithError
Stage 13: extend
extend Name, UPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Category | eq |
| field:"Category" kind:eq value:"SQLSecurityAuditEvents" |
IsErrorAnomalyOnStatement | eq |
| field:"IsErrorAnomalyOnStatement" kind:eq value:"true" |
WindowType | eq |
| field:"WindowType" kind:eq |
WindowType | in |
| field:"WindowType" kind:in |
action_id_s | match |
| field:"action_id_s" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ApplicationName | project |
ClientIp | project |
Database | project |
HostName | project |
PrincipalName | project |
ResourceId | project |
TimeGenerated | project |
anomalyScore | project |
anyInfo | project |
anyMonitoredStatement | project |
countEvents | project |
countStatements | project |
countStatementsWithError | project |
Name | extend |
UPNSuffix | extend |
Hunt for RDP sessions to unmanaged and non TPM devices
#This query can help you find devices performing RDP sessions to unmanaged or non-TPM protected devices.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Lateral Movement |
References
Telemetry Coverage
Rule Body
let no_tpm_devices = (
ExposureGraphNodes
// Get device nodes with their inventory ID
| where NodeLabel == "device"
| mv-expand EntityIds
| where EntityIds.type == "DeviceInventoryId"
// Get interesting properties
| extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]),
TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]),
TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]),
TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]),
DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]),
DeviceId = tostring(EntityIds.id)
// Search for distinct devices
| distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated
// Get Unmanaged devices and device not supporting a TPM
| where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true")
| extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported),
TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated),
TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled)
);
let no_tpm_device_info = (
DeviceNetworkInfo
| where Timestamp > ago(7d)
// Get latest network info for each device ID
| summarize arg_max(Timestamp, *) by DeviceId
| mv-expand todynamic(IPAddresses)
| extend IPAddress = tostring(IPAddresses.IPAddress)
// Find no TPM devices and join with their network information
| join kind=inner no_tpm_devices on DeviceId
| project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported
);
DeviceNetworkEvents
// Search for RDP connections to non-tpm devices
| where Timestamp > ago(1h)
| where ActionType == "ConnectionSuccess"
| where RemotePort == 3389
// Exclude MDI RDP Connections (known for NNR)
| where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe"
| join kind=inner no_tpm_device_info on $left.RemoteIP == $right.IPAddress
| project-rename RemoteDeviceId = DeviceId1, RemoteDeviceName = DeviceName1, RemoteMacAddress = MacAddress, RemoteDeviceOnboardingStatus = OnboardingStatus, RemoteDeviceTpmActivated = TpmActivated, RemoteDeviceTpmEnabled = TpmEnabled, RemoteDeviceTpmSupported = TpmSupported
| project-away IPAddress
let no_tpm_devices = (
ExposureGraphNodes
// Get device nodes with their inventory ID
| where NodeLabel == "device"
| mv-expand EntityIds
| where EntityIds.type == "DeviceInventoryId"
// Get interesting properties
| extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]),
TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]),
TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]),
TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]),
DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]),
DeviceId = tostring(EntityIds.id)
// Search for distinct devices
| distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated
// Get Unmanaged devices and device not supporting a TPM
| where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true")
| extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported),
TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated),
TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled)
);
let no_tpm_device_info = (
DeviceNetworkInfo
| where TimeGenerated > ago(7d)
// Get latest network info for each device ID
| summarize arg_max(TimeGenerated, *) by DeviceId
| mv-expand todynamic(IPAddresses)
| extend IPAddress = tostring(IPAddresses.IPAddress)
// Find no TPM devices and join with their network information
| join kind=inner no_tpm_devices on DeviceId
| project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported
);
DeviceNetworkEvents
// Search for RDP connections to non-tpm devices
| where TimeGenerated > ago(1h)
| where ActionType == "ConnectionSuccess"
| where RemotePort == 3389
// Exclude MDI RDP Connections (known for NNR)
| where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe"
| join kind=inner no_tpm_device_info on $left.RemoteIP == $right.IPAddress
| project-rename RemoteDeviceId = DeviceId1, RemoteDeviceName = DeviceName1, RemoteMacAddress = MacAddress, RemoteDeviceOnboardingStatus = OnboardingStatus, RemoteDeviceTpmActivated = TpmActivated, RemoteDeviceTpmEnabled = TpmEnabled, RemoteDeviceTpmSupported = TpmSupported
| project-away IPAddress
Stages and Predicates
Let Binding: no_tpm_devices
let no_tpm_devices = (
ExposureGraphNodes
| where NodeLabel == "device"
| mv-expand EntityIds
| where EntityIds.type == "DeviceInventoryId"
| extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]),
TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]),
TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]),
TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]),
DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]),
DeviceId = tostring(EntityIds.id)
| distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated
| where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true")
| extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported),
TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated),
TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled)
);
Let Binding: no_tpm_device_info
let no_tpm_device_info = (
DeviceNetworkInfo
| where Timestamp > ago(7d)
| summarize arg_max(Timestamp, *) by DeviceId
| mv-expand todynamic(IPAddresses)
| extend IPAddress = tostring(IPAddresses.IPAddress)
| join kind=inner no_tpm_devices on DeviceId
| project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported
);
Stage 1: source
let no_tpm_devices
Stage 2: source
let no_tpm_device_info
Stage 3: source
DeviceNetworkEvents
Stage 4: where
where Timestamp > ago(3600s)
Stage 5: where
where ActionType == "ConnectionSuccess"
Stage 6: where
where RemotePort == 3389
Stage 7: where
where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe"
Stage 8: join
join kind=inner (no_tpm_device_info) on $left.RemoteIP, $right.IPAddress
Stage 9: project-rename
project-rename
Stage 10: project-away
project-away IPAddress
Stage 11: summarize aggregation inside the join branch
summarize by DeviceId
Indicators
These rows show field, operator, and value matches.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceId | summarize |
Palo Alto Prisma Cloud - Maximum risk score alert
#'Detects alerts with maximum risk score value.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 119a574d-f37a-403a-a67a-4d6f5083d9cf
name: Palo Alto Prisma Cloud - Maximum risk score alert
description: |
'Detects alerts with maximum risk score value.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: PaloAltoPrismaCloud
dataTypes:
- PaloAltoPrismaCloud
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1133
query: |
PaloAltoPrismaCloud
| where Reason =~ 'NEW_ALERT'
| where Status =~ 'open'
| where RiskDetailRiskScoreScore == RiskDetailRiskScoreMaxScore
| extend AccountCustomEntity = UserName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
PaloAltoPrismaCloud
Stage 2: where
| where Reason =~ 'NEW_ALERT'
Stage 3: where
| where Status =~ 'open'
Stage 4: where
| where RiskDetailRiskScoreScore == RiskDetailRiskScoreMaxScore
Stage 5: extend
| extend AccountCustomEntity = UserName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Reason | eq |
| field:"Reason" kind:eq value:"NEW_ALERT" |
Status | eq |
| field:"Status" kind:eq value:"open" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountCustomEntity | extend |
Prestige ransomware IOCs Oct 2022
#This query looks for file hashes and AV signatures associated with Prestige ransomware payload.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution |
Telemetry Coverage
Rule Body
id: bca9c877-2afc-4246-a26d-087ab1cdcd5f
name: Prestige ransomware IOCs Oct 2022
description: |
'This query looks for file hashes and AV signatures associated with Prestige ransomware payload.'
severity: High
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceEvents
- DeviceFileEvents
- DeviceImageLoadEvents
- connectorId: SecurityEvents
dataTypes:
- SecurityEvents
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Execution
relevantTechniques:
- T1203
tags:
- Prestige
- Ransomware
query: |
let sha256Hashes = dynamic(["5dd1ca0d471dee41eb3ea0b6ea117810f228354fc3b7b47400a812573d40d91d", "5fc44c7342b84f50f24758e39c8848b2f0991e8817ef5465844f5f2ff6085a57", "6cff0bbd62efe99f381e5cc0c4182b0fb7a9a34e4be9ce68ee6b0d0ea3eee39c"]);
let signames = dynamic(["Ransom:Win32/Prestige"]);
(union isfuzzy=true
(CommonSecurityLog
| where FileHash in (sha256Hashes)
| project TimeGenerated, Message, SourceUserID, FileHash, Type
| extend timestamp = TimeGenerated, Algorithm = "SHA256", AccountNTName = SourceUserID
),
(imFileEvent
| where TargetFileSHA256 has_any (sha256Hashes)
| extend AccountNT = ActorUsername, Computer = DvcHostname, IPAddress = SrcIpAddr, CommandLine = ActingProcessCommandLine, FileHash = TargetFileSHA256
| project Type, TimeGenerated, Computer, AccountNT, IPAddress, CommandLine, FileHash, Algorithm = "SHA256"
),
(Event
| where Source =~ "Microsoft-Windows-Sysmon"
| where EventID == 1
| extend EvData = parse_xml(EventData)
| extend EventDetail = EvData.DataItem.EventData.Data
| extend ProcessId = tolong(EventDetail.[3].["#text"]), Image = tostring(EventDetail.[4].["#text"]), CommandLine = tostring(EventDetail.[10].["#text"]), Hashes = tostring(EventDetail.[17].["#text"])
| extend Hashes = extract_all(@"(?P<key>\w+)=(?P<value>[a-zA-Z0-9]+)", dynamic(["key","value"]), Hashes)
| extend Hashes = column_ifexists("Hashes", dynamic(["", ""])), CommandLine = column_ifexists("CommandLine", "")
| mv-expand Hashes
| where Hashes[0] =~ "SHA256" and Hashes[1] has_any (sha256Hashes)
| project TimeGenerated, EventDetail, UserName, Computer, Type, Source, ProcessId, Hashes, CommandLine, Image
| extend Type = strcat(Type, ": ", Source)
| extend AccountNT = UserName, InitiatingProcessId = ProcessId
| extend Process = tostring(split(Image, '\\', -1)[-1]), Algorithm = "SHA256", FileHash = tostring(Hashes[1])
),
(DeviceEvents
| where InitiatingProcessSHA256 has_any (sha256Hashes) or SHA256 has_any (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessSHA256, Type
| extend AccountNT = InitiatingProcessAccountName, Computer = DeviceName
| extend Algorithm = "SHA256", FileHash = tostring(InitiatingProcessSHA256), CommandLine = InitiatingProcessCommandLine, Image = InitiatingProcessFolderPath
),
(DeviceFileEvents
| where SHA256 has_any (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessSHA256, Type
| extend AccountNT = InitiatingProcessAccountName, Computer = DeviceName
| extend Algorithm = "SHA256", FileHash = tostring(InitiatingProcessSHA256), CommandLine = InitiatingProcessCommandLine, Image = InitiatingProcessFolderPath
),
(DeviceImageLoadEvents
| where SHA256 has_any (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessSHA256, Type
| extend AccountNT = InitiatingProcessAccountName, Computer = DeviceName
| extend Algorithm = "SHA256", FileHash = tostring(InitiatingProcessSHA256), CommandLine = InitiatingProcessCommandLine, Image = InitiatingProcessFolderPath
),
(SecurityAlert
| where ProductName == "Microsoft Defender Advanced Threat Protection"
| extend ThreatName = tostring(parse_json(ExtendedProperties).ThreatName)
| where isnotempty(ThreatName)
| where ThreatName has_any (signames)
| extend Computer = tostring(parse_json(Entities)[0].HostName)
)
)
| extend AccountNTName = tostring(split(AccountNT, "\\")[0]), AccountNTDomain = tostring(split(AccountNT, "\\")[1])
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| project-away DomainIndex
entityMappings:
- entityType: FileHash
fieldMappings:
- identifier: Algorithm
columnName: Algorithm
- identifier: Value
columnName: FileHash
- entityType: Process
fieldMappings:
- identifier: ProcessId
columnName: InitiatingProcessId
- identifier: CommandLine
columnName: CommandLine
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountNT
- identifier: Name
columnName: AccountNTName
- identifier: UPNSuffix
columnName: AccountNTDomain
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: NTDomain
columnName: HostNameDomain
version: 1.0.4
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others" ]
Stages and Predicates
Parameters
let signames = dynamic(["Ransom:Win32/Prestige"]);
Let Binding: sha256Hashes
let sha256Hashes = dynamic(["5dd1ca0d471dee41eb3ea0b6ea117810f228354fc3b7b47400a812573d40d91d", "5fc44c7342b84f50f24758e39c8848b2f0991e8817ef5465844f5f2ff6085a57", "6cff0bbd62efe99f381e5cc0c4182b0fb7a9a34e4be9ce68ee6b0d0ea3eee39c"]);
Union isfuzzy=true (7 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: CommonSecurityLog, imFileEvent, Event, DeviceEvents, DeviceFileEvents, DeviceImageLoadEvents, SecurityAlert
Leg 1: CommonSecurityLog
CommonSecurityLog
| where FileHash in (sha256Hashes)
| project TimeGenerated, Message, SourceUserID, FileHash, Type
| extend timestamp = TimeGenerated, Algorithm = "SHA256", AccountNTName = SourceUserID
Leg 2: imFileEvent
imFileEvent
| where TargetFileSHA256 has_any (sha256Hashes)
| extend AccountNT = ActorUsername, Computer = DvcHostname, IPAddress = SrcIpAddr, CommandLine = ActingProcessCommandLine, FileHash = TargetFileSHA256
| project Type, TimeGenerated, Computer, AccountNT, IPAddress, CommandLine, FileHash, Algorithm = "SHA256"
Leg 3: Event
Event
| where Source =~ "Microsoft-Windows-Sysmon"
| where EventID == 1
| extend EvData = parse_xml(EventData)
| extend EventDetail = EvData.DataItem.EventData.Data
| extend ProcessId = tolong(EventDetail.[3].["#text"]), Image = tostring(EventDetail.[4].["#text"]), CommandLine = tostring(EventDetail.[10].["#text"]), Hashes = tostring(EventDetail.[17].["#text"])
| extend Hashes = extract_all(@"(?P<key>\w+)=(?P<value>[a-zA-Z0-9]+)", dynamic(["key","value"]), Hashes)
| extend Hashes = column_ifexists("Hashes", dynamic(["", ""])), CommandLine = column_ifexists("CommandLine", "")
| mv-expand Hashes
| where Hashes[0] =~ "SHA256" and Hashes[1] has_any (sha256Hashes)
| project TimeGenerated, EventDetail, UserName, Computer, Type, Source, ProcessId, Hashes, CommandLine, Image
| extend Type = strcat(Type, ": ", Source)
| extend AccountNT = UserName, InitiatingProcessId = ProcessId
| extend Process = tostring(split(Image, '\\', -1)[-1]), Algorithm = "SHA256", FileHash = tostring(Hashes[1])
Leg 4: DeviceEvents
DeviceEvents
| where InitiatingProcessSHA256 has_any (sha256Hashes) or SHA256 has_any (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessSHA256, Type
| extend AccountNT = InitiatingProcessAccountName, Computer = DeviceName
| extend Algorithm = "SHA256", FileHash = tostring(InitiatingProcessSHA256), CommandLine = InitiatingProcessCommandLine, Image = InitiatingProcessFolderPath
Leg 5: DeviceFileEvents
DeviceFileEvents
| where SHA256 has_any (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessSHA256, Type
| extend AccountNT = InitiatingProcessAccountName, Computer = DeviceName
| extend Algorithm = "SHA256", FileHash = tostring(InitiatingProcessSHA256), CommandLine = InitiatingProcessCommandLine, Image = InitiatingProcessFolderPath
Leg 6: DeviceImageLoadEvents
DeviceImageLoadEvents
| where SHA256 has_any (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessSHA256, Type
| extend AccountNT = InitiatingProcessAccountName, Computer = DeviceName
| extend Algorithm = "SHA256", FileHash = tostring(InitiatingProcessSHA256), CommandLine = InitiatingProcessCommandLine, Image = InitiatingProcessFolderPath
Leg 7: SecurityAlert
SecurityAlert
| where ProductName == "Microsoft Defender Advanced Threat Protection"
| extend ThreatName = tostring(parse_json(ExtendedProperties).ThreatName)
| where isnotempty(ThreatName)
| where ThreatName has_any (signames)
| extend Computer = tostring(parse_json(Entities)[0].HostName)
Applied to the Combined Result
| extend AccountNTName = tostring(split(AccountNT, "\\")[0]), AccountNTDomain = tostring(split(AccountNT, "\\")[1])
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| project-away DomainIndex
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventID | eq |
| field:"EventID" kind:eq value:"1" |
FileHash | in |
| field:"FileHash" kind:in |
Hashes[0] | eq |
| field:"Hashes[0]" kind:eq value:"SHA256" |
Hashes[1] | match |
| field:"Hashes[1]" kind:match |
InitiatingProcessSHA256 | match |
| field:"InitiatingProcessSHA256" kind:match |
ProductName | eq |
| field:"ProductName" kind:eq value:"Microsoft Defender Advanced Threat Protection" |
SHA256 | match |
| field:"sha256" kind:match |
TargetFileSHA256 | match |
| field:"sha256" kind:match |
ThreatName | is_not_null | field:"ThreatName" kind:is_not_null | |
ThreatName | match |
| field:"ThreatName" kind:match value:"signames" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActionType | project |
DeviceId | project |
DeviceName | project |
InitiatingProcessAccountDomain | project |
InitiatingProcessAccountName | project |
InitiatingProcessCommandLine | project |
InitiatingProcessFileName | project |
InitiatingProcessFolderPath | project |
InitiatingProcessId | project |
InitiatingProcessParentFileName | project |
InitiatingProcessSHA256 | project |
TimeGenerated | project |
Type | project |
AccountNT | extend |
Computer | extend |
Algorithm | extend |
CommandLine | extend |
FileHash | extend |
Image | extend |
ThreatName | extend |
AccountNTDomain | extend |
AccountNTName | extend |
HostName | extend |
HostNameDomain | extend |
PROD (TM006.1) - ACL - Modified at Tier Level OU
#ACL modification of any Tier Model OU.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 5136: A directory service object was modified. |
Rule Body
id: f3e2433b-6456-4009-a1cb-0d4eb3a36125
name: 'PROD (TM006.1) - ACL - Modified at Tier Level OU'
description: |
ACL modification of any Tier Model OU.
severity: High
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- DefenseEvasion
relevantTechniques:
- T1222.001
query: |
SecurityEvent
| where EventID == 5136
and (EventData matches regex @"(?i)OU=Domain Controllers"
or EventData matches regex @"(?i)OU=Tier 0 Member Servers"
or EventData matches regex @"(?i)OU=Tier 1 Member Servers"
or EventData matches regex @"(?i)OU=Tier 2 End-User Accounts"
or EventData matches regex @"(?i)OU=Tier 2 End-User Devices"
or EventData matches regex @"(?i)OU=Tier 2 End-User Groups"
or EventData matches regex @"(?i)OU=Tier Model Administration"
or EventData matches regex @"(?i)OU=Tier Model Computer Quarantine")
and Activity has "modified"
and EventData has '"ObjectClass">organizationalUnit'
and EventData has '<Data Name="AttributeLDAPDisplayName">nTSecurityDescriptor</Data>'
and EventData has '"OperationType">%%14674'
| extend ObjectName = extract("(?i)<Data Name=\"ObjectDN\">(((?i)OU=[^,]+,?)+),[dD][cC]=", 1, EventData)
| extend ObjectTier = case(
ObjectName has "Tier 0", "Tier 0",
ObjectName has "Tier 1", "Tier 1",
ObjectName has "Tier 2", "Tier 2",
ObjectName has "Tier Model Administration", "Tier 0",
ObjectName has "Tier Model Computer Quarantine", "Tier 0",
ObjectName has "Domain Controllers", "Tier 0",
"Unknown")
| extend AttributeLDAP = extract("<Data Name=\"AttributeLDAPDisplayName\">([^<]+)</Data>", 1, EventData)
| extend AttributeValue = extract("\\[([^\\]]+)\\]", 1, EventData)
| extend OperationType = iff(EventData has 'nTSecurityDescriptor', "ACL Modified", tostring(OperationType))
| extend ActivityType = extract("([a-zA-Z]+)\\.$", 1, Activity)
| extend ObjectClass = extract("<Data Name=\"ObjectClass\">([^<]+)</Data>", 1, EventData)
| extend Domain = extract("([^.]+\\.[^.]+)$", 1, Computer)
| project TimeGenerated
, ObjectName
, ObjectTier
, ObjectClass
, OperationType
, ActivityType
, AttributeLDAP
, 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
OperationType: OperationType
ActivityType: ActivityType
AttributeLDAP: AttributeLDAP
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: '(TM006.1) A {{ObjectTier}} OU ACL was modified at the following {{ObjectName}} OU'
alertDescriptionFormat: 'Modification to any Tier Model OU ACL.'
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
SecurityEvent
Stage 2: where
| where EventID == 5136
and (EventData matches regex @"(?i)OU=Domain Controllers"
or EventData matches regex @"(?i)OU=Tier 0 Member Servers"
or EventData matches regex @"(?i)OU=Tier 1 Member Servers"
or EventData matches regex @"(?i)OU=Tier 2 End-User Accounts"
or EventData matches regex @"(?i)OU=Tier 2 End-User Devices"
or EventData matches regex @"(?i)OU=Tier 2 End-User Groups"
or EventData matches regex @"(?i)OU=Tier Model Administration"
or EventData matches regex @"(?i)OU=Tier Model Computer Quarantine")
and Activity has "modified"
and EventData has '"ObjectClass">organizationalUnit'
and EventData has '<Data Name="AttributeLDAPDisplayName">nTSecurityDescriptor</Data>'
and EventData has '"OperationType">%%14674'
Stage 3: extend (8 consecutive steps)
| extend ObjectName = extract("(?i)<Data Name=\"ObjectDN\">(((?i)OU=[^,]+,?)+),[dD][cC]=", 1, EventData)
| extend ObjectTier = case(
ObjectName has "Tier 0", "Tier 0",
ObjectName has "Tier 1", "Tier 1",
ObjectName has "Tier 2", "Tier 2",
ObjectName has "Tier Model Administration", "Tier 0",
ObjectName has "Tier Model Computer Quarantine", "Tier 0",
ObjectName has "Domain Controllers", "Tier 0",
"Unknown")
| extend AttributeLDAP = extract("<Data Name=\"AttributeLDAPDisplayName\">([^<]+)</Data>", 1, EventData)
| extend AttributeValue = extract("\\[([^\\]]+)\\]", 1, EventData)
| extend OperationType = iff(EventData has 'nTSecurityDescriptor', "ACL Modified", tostring(OperationType))
| extend ActivityType = extract("([a-zA-Z]+)\\.$", 1, Activity)
| extend ObjectClass = extract("<Data Name=\"ObjectClass\">([^<]+)</Data>", 1, EventData)
| extend Domain = extract("([^.]+\\.[^.]+)$", 1, Computer)
Stage 4: project
| project TimeGenerated
, ObjectName
, ObjectTier
, ObjectClass
, OperationType
, ActivityType
, AttributeLDAP
, Account
, Domain
, Computer
, Channel
, EventID
, EventData
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Activity | match |
| field:"Activity" kind:match value:"modified" |
EventData | match |
| field:"EventData" kind:match |
EventData | regex_match |
| field:"EventData" kind:regex_match |
EventID | eq |
| field:"EventID" kind:eq value:"5136" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | project |
ActivityType | project |
AttributeLDAP | project |
Channel | project |
Computer | project |
Domain | project |
EventData | project |
EventID | project |
ObjectClass | project |
ObjectName | project |
ObjectTier | project |
OperationType | project |
TimeGenerated | project |
Successful AWS Console Login from IP Address Observed Conducting Password Spray
#This query aims to detect instances of successful AWS console login events followed by multiple failed app logons alerts generated by Microsoft Cloud App Security or password spray alerts generated by Defender Products. Specifically, it focuses on scenarios where the successful login takes place within a 60-minute timeframe of the high-severity alert. The login is considered relevant if it originates from an IP address associated with potential attackers.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | IdentityInfo action any: Identity information |
Rule Body
id: 188db479-d50a-4a9c-a041-644bae347d1f
name: Successful AWS Console Login from IP Address Observed Conducting Password Spray
description: |
'This query aims to detect instances of successful AWS console login events followed by multiple failed app logons alerts generated by Microsoft Cloud App Security or password spray alerts generated by Defender Products.
Specifically, it focuses on scenarios where the successful login takes place within a 60-minute timeframe of the high-severity alert.
The login is considered relevant if it originates from an IP address associated with potential attackers.'
severity: Medium
requiredDataConnectors:
- connectorId: AWS
dataTypes:
- AWSCloudTrail
- connectorId: MicrosoftDefenderAdvancedThreatProtection
dataTypes:
- SecurityAlert
- connectorId: AzureActiveDirectoryIdentityProtection
dataTypes:
- SecurityAlert (IPC)
- connectorId: BehaviorAnalytics
dataTypes:
- IdentityInfo
- connectorId: MicrosoftThreatProtection
dataTypes:
- SecurityAlert
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- CredentialAccess
relevantTechniques:
- T1110
- T1078
query: |
SecurityAlert
// Filtering alerts based on Microsoft product names and Relevent alert names
| where ProductName in ( "Microsoft Cloud App Security","Azure Active Directory Identity Protection")
|where AlertName in ("Multiple failed user log on attempts to an app","Password Spray")
// Parsing and extending the 'Entities' column as JSON objects
| extend Entities = parse_json(Entities)
// Exploring IP entities within the alert entities
| mv-apply Entity = Entities on
(
where Entity.Type == 'ip'
| extend EntityIp = tostring(Entity.Address)
)
// Exploring account entities within the alert entities
| mv-apply Entity = Entities on
(
where Entity.Type == 'account'
| extend AccountObjectId = tostring(Entity.AadUserId)
)
// Filtering out alerts with missing IP or account information
| where isnotempty(EntityIp) and isnotempty(AccountObjectId)
// Summarizing relevant fields for further analysis
| summarize
by
AlertName,
ProductName,
ProviderName,
AlertSeverity,
EntityIp,
Tactics,
Techniques,
AlertTime= bin(TimeGenerated, 1min),
AccountObjectId,
AlertTimeGenerated=TimeGenerated
// Joining with IdentityInfo to obtain additional account details
| join kind=inner (
IdentityInfo
| where TimeGenerated >= ago(1d)
| distinct AccountObjectId, AccountUPN=tolower(AccountUPN)
)
on AccountObjectId
|extend Name = tostring(split(AccountUPN,'@')[0]), UPNSuffix =tostring(split(AccountUPN,'@')[1])
// Joining with AWSCloudTrail data to correlate AWS console logins
| join kind=inner (
AWSCloudTrail
| where EventName == "ConsoleLogin"
| extend CTUPN= tolower(tostring(tolower(split(UserIdentityArn, "/", 2)[0])))
| extend ActionType= tostring(parse_json(ResponseElements).ConsoleLogin)
| where ActionType == "Success"
| extend AWSTime= bin(TimeGenerated, 1min)
| project
EventName,
EventSource,
EventTypeName,
RecipientAccountId,
ResponseElements,
SessionMfaAuthenticated,
SourceIpAddress,
TimeGenerated,
UserAgent,
UserIdentityArn,
UserIdentityType,
CTUPN,
AWSTime,
UserIdentityUserName
)
on $left.EntityIp == $right.SourceIpAddress
// Filtering login event after the Alert generation time
| where AlertTimeGenerated between ((AWSTime - 1h)..(AWSTime + 1h))
// Calculating the time difference between alert generation and AWS login
| extend timediff = datetime_diff('minute', AlertTimeGenerated, TimeGenerated)
// Filtering alerts with a time difference of up to 60 minutes
| where timediff <= 60
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountUPN
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIpAddress
customDetails:
AWSUser: UserIdentityArn
UserAgent: UserAgent
AWSUserUPN: CTUPN
kind: Scheduled
version: 1.0.1
Stages and Predicates
Stage 1: source
SecurityAlert
Stage 2: where
| where ProductName in ( "Microsoft Cloud App Security","Azure Active Directory Identity Protection")
Stage 3: where
| where AlertName in ("Multiple failed user log on attempts to an app","Password Spray")
Stage 4: extend
| extend Entities = parse_json(Entities)
Stage 5: kusto:mv-apply
| mv-apply Entity = Entities on
(
where Entity.Type == 'ip'
| extend EntityIp = tostring(Entity.Address)
)
Stage 6: kusto:mv-apply
| mv-apply Entity = Entities on
(
where Entity.Type == 'account'
| extend AccountObjectId = tostring(Entity.AadUserId)
)
Stage 7: where
| where isnotempty(EntityIp) and isnotempty(AccountObjectId)
Stage 8: summarize
| summarize
by
AlertName,
ProductName,
ProviderName,
AlertSeverity,
EntityIp,
Tactics,
Techniques,
AlertTime= bin(TimeGenerated, 1min),
AccountObjectId,
AlertTimeGenerated=TimeGenerated
Stage 9: join
| join kind=inner (
IdentityInfo
| where TimeGenerated >= ago(1d)
| distinct AccountObjectId, AccountUPN=tolower(AccountUPN)
)
on AccountObjectId
Stage 10: extend
extend Name, UPNSuffix
Stage 11: join
| join kind=inner (
AWSCloudTrail
| where EventName == "ConsoleLogin"
| extend CTUPN= tolower(tostring(tolower(split(UserIdentityArn, "/", 2)[0])))
| extend ActionType= tostring(parse_json(ResponseElements).ConsoleLogin)
| where ActionType == "Success"
| extend AWSTime= bin(TimeGenerated, 1min)
| project
EventName,
EventSource,
EventTypeName,
RecipientAccountId,
ResponseElements,
SessionMfaAuthenticated,
SourceIpAddress,
TimeGenerated,
UserAgent,
UserIdentityArn,
UserIdentityType,
CTUPN,
AWSTime,
UserIdentityUserName
)
on $left.EntityIp == $right.SourceIpAddress
Stage 12: where
where /* macro: (AlertTimeGenerated between ((AWSTime - 1h) .. (AWSTime + 1h))) */
Stage 13: extend
extend timediff
Stage 14: where
where timediff <= 60
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AccountObjectId | is_not_null | field:"AccountObjectId" kind:is_not_null | |
ActionType | eq |
| field:"ActionType" kind:eq value:"Success" |
AlertName | in |
| field:"AlertName" kind:in |
Entity.Type | eq |
| field:"Entity.Type" kind:eq |
EntityIp | is_not_null | field:"EntityIp" kind:is_not_null | |
EventName | eq |
| field:"EventName" kind:eq value:"ConsoleLogin" |
ProductName | in |
| field:"ProductName" kind:in |
timediff | le |
| field:"timediff" kind:le value:"60" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountObjectId | summarize |
AlertName | summarize |
AlertSeverity | summarize |
AlertTime | summarize |
AlertTimeGenerated | summarize |
EntityIp | summarize |
ProductName | summarize |
ProviderName | summarize |
Tactics | summarize |
Techniques | summarize |
Name | extend |
UPNSuffix | extend |
timediff | extend |
Theom Insights
#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 Microsoft Sentinel incidents for Theom insight alerts."
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance | |
| Stealth | |
| Credential Access | |
| Discovery | |
| Collection | |
| Command & Control | |
| Exfiltration | |
| Impact |
Rule Body
id: d200da84-0191-44ce-ad9e-b85e64c84c89
name: Theom Insights
description: |
"Creates Microsoft Sentinel incidents for Theom insight alerts."
severity: Low
status : Available
requiredDataConnectors:
- connectorId: Theom
dataTypes:
- TheomAlerts_CL
queryPeriod: 5m
queryFrequency: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Collection
- CommandAndControl
- CredentialAccess
- DefenseEvasion
- Discovery
- Exfiltration
- Impact
- Reconnaissance
relevantTechniques:
- T1592
- T1589
- T1070
- T1552
- T1619
- T1119
- T1560
- T1530
- T1213
- T1001
- T1041
- T1537
- T1485
- T1486
- T1565
query: |
TheomAlerts_CL
| where priority_s == "P5"
eventGroupingSettings:
aggregationKind: AlertPerResult
alertDetailsOverride:
alertDisplayNameFormat: "Theom Alert ID: {{id_s}} "
alertDescriptionFormat: "\nSummary: {{summary_s}} \nAdditional info: {{details_s}}\nResource ID: {{customProps_AssetName_s}}\n"
entityMappings:
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: customProps_AssetName_s
- entityType: URL
fieldMappings:
- identifier: Url
columnName: deepLink_s
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
TheomAlerts_CL
Stage 2: where
| where priority_s == "P5"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
priority_s | eq |
| field:"priority_s" kind:eq value:"P5" |
TI Map Domain entity to SecurityAlert
#'Identifies a match in SecurityAlert table from any Domain IOC from TI'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: df88b403-1cb9-49ea-a43d-b6613051cf7f
name: TI Map Domain entity to SecurityAlert
description: |
'Identifies a match in SecurityAlert table from any Domain IOC from TI'
severity: Medium
requiredDataConnectors:
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelIndicators
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelIndicators
- connectorId: MicrosoftCloudAppSecurity
dataTypes:
- SecurityAlert
- connectorId: AzureSecurityCenter
dataTypes:
- SecurityAlert
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
query: |
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let SecurityAlerts = SecurityAlert
| where TimeGenerated > ago(dt_lookBack)
| where AlertName != "TI map Domain entity to SecurityAlert"
| extend domain = todynamic(dynamic_to_json(extract_all(@"(((xn--)?[a-z0-9\-]+\.)+([a-z]+|(xn--[a-z0-9]+)))", dynamic([1]), tolower(Entities))))
| where isnotempty(domain)
| mv-expand domain
| extend domain = tostring(domain)
| extend EntitiesDynamicArray = parse_json(Entities)
| mv-apply EntitiesDynamicArray on
(summarize
HostName = take_anyif(tostring(EntitiesDynamicArray.HostName), EntitiesDynamicArray.Type == "host"),
IP_addr = take_anyif(tostring(EntitiesDynamicArray.Address), EntitiesDynamicArray.Type == "ip")
)
| extend Alert_TimeGenerated = TimeGenerated
| extend Alert_Description = Description;
let AlertDomains = SecurityAlerts
| distinct domain
| summarize make_list(domain);
let Domain_Indicators = materialize(ThreatIntelIndicators
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
| where IndicatorType == "domain-name"
| extend DomainName = tolower(ObservableValue)
| where TimeGenerated >= ago(ioc_lookBack)
| where DomainName in (AlertDomains)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
| extend Description = tostring(parse_json(Data).description)
| where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;");
Domain_Indicators
// Using innerunique to keep performance fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
| join kind=innerunique (SecurityAlerts) on $left.DomainName == $right.domain
| where Alert_TimeGenerated < ValidUntil
| summarize Alert_TimeGenerated = arg_max(Alert_TimeGenerated, *) by Id, AlertName
| extend ActivityGroupNames = extract("ActivityGroup:([^,]+)", 1, tostring(parse_json(Data).labels))
| extend Url = DomainName
| project Alert_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, ConfidenceScore, DomainName, AlertName, Alert_Description, ProviderName, AlertSeverity, ConfidenceLevel, HostName, IP_addr, Url, Entities, Type
| extend timestamp = Alert_TimeGenerated
| project-reorder *, ConfidenceScore, DomainName, AlertName, Alert_TimeGenerated, ProviderName, AlertSeverity, Entities, Type, timestamp
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IP_addr
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
version: 1.4.7
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let Domain_Indicators is inlined into the numbered stages below.
Let Binding: SecurityAlerts
let SecurityAlerts = SecurityAlert
| where TimeGenerated > ago(dt_lookBack)
| where AlertName != "TI map Domain entity to SecurityAlert"
| extend domain = todynamic(dynamic_to_json(extract_all(@"(((xn--)?[a-z0-9\-]+\.)+([a-z]+|(xn--[a-z0-9]+)))", dynamic([1]), tolower(Entities))))
| where isnotempty(domain)
| mv-expand domain
| extend domain = tostring(domain)
| extend EntitiesDynamicArray = parse_json(Entities)
| mv-apply EntitiesDynamicArray on
(summarize
HostName = take_anyif(tostring(EntitiesDynamicArray.HostName), EntitiesDynamicArray.Type == "host"),
IP_addr = take_anyif(tostring(EntitiesDynamicArray.Address), EntitiesDynamicArray.Type == "ip")
)
| extend Alert_TimeGenerated = TimeGenerated
| extend Alert_Description = Description;
Let Binding: AlertDomains
let AlertDomains = SecurityAlerts
| distinct domain
| summarize make_list(domain);
Stages 1 to 10 define let Domain_Indicators as the rule's main pipeline source. Stages 11 to 18 use it.
Stage 1: source
ThreatIntelIndicators
Stage 2: extend
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
Stage 3: where
| where IndicatorType == "domain-name"
Stage 4: extend
| extend DomainName = tolower(ObservableValue)
Stage 5: where
| where TimeGenerated >= ago(ioc_lookBack)
Stage 6: where
| where DomainName in (AlertDomains)
Stage 7: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
Stage 8: where
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
Stage 9: extend
| extend Description = tostring(parse_json(Data).description)
Stage 10: where
| where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"
Stage 11: join
Domain_Indicators
| join kind=innerunique (SecurityAlerts) on $left.DomainName == $right.domain
Stage 12: where
where Alert_TimeGenerated < ValidUntil
Stage 13: summarize
summarize Alert_TimeGenerated by Id, AlertName
Stage 14: extend
extend ActivityGroupNames
Stage 15: extend
extend Url
Stage 16: project
project ActivityGroupNames, AlertName, AlertSeverity, Alert_Description, Alert_TimeGenerated, ConfidenceLevel, ConfidenceScore, Description, DomainName, Entities, HostName, IP_addr, Id, ProviderName, Type, Url, ValidUntil
Stage 17: extend
extend timestamp
Stage 18: project-reorder
project-reorder
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AlertName | ne |
| field:"AlertName" kind:ne value:"TI map Domain entity to SecurityAlert" |
DomainName | in |
| field:"DomainName" kind:in value:"AlertDomains" |
IndicatorType | eq |
| field:"IndicatorType" kind:eq value:"domain-name" |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
ValidUntil | is_null | field:"ValidUntil" kind:is_null | |
domain | is_not_null | field:"domain" kind:is_not_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Description | contains | State: falsepos; | excludes:Description field:"Description" value:"State: falsepos;" |
Description | contains | State: inactive; | excludes:Description field:"Description" value:"State: inactive;" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
AlertName | project |
AlertSeverity | project |
Alert_Description | project |
Alert_TimeGenerated | project |
ConfidenceLevel | project |
ConfidenceScore | project |
Description | project |
DomainName | project |
Entities | project |
HostName | project |
IP_addr | project |
Id | project |
ProviderName | project |
Type | project |
Url | project |
ValidUntil | project |
timestamp | extend |
Unusual Volume of file deletion by users
#This query looks for users performing file deletion activities. Spikes in file deletion observed from risky sign-in sessions are flagged here. This applies to SharePoint and OneDrive users. Audit event and Cloud application identifier references. Reference - https://learn.microsoft.com/microsoft-365/compliance/audit-log-activities?view=o365-worldwide Reference - https://learn.microsoft.com/azure/sentinel/entities-reference#cloud-application-identifiers
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Suspicious Continuous OAuth Token Usage (Kusto)
- Anomalous sign-in location by user account and authenticating application (Kusto)
- Anomalous Single Factor Signin (Kusto)
- Authentications of Privileged Accounts Outside of Expected Controls (Kusto)
- Azure Portal sign in from another Azure Tenant (Kusto)
- Azure Service Principal Sign-In Followed by Arc Cluster Credential Access (Elastic)
- Azure SignIn via Legacy Authentication Protocol (Panther)
- Cisco - firewall block but success logon to Microsoft Entra ID (Kusto)
Rule Body
id: e5f8e196-3544-4a8b-96a9-17c1b6a49710
name: Unusual Volume of file deletion by users
description: |
This query looks for users performing file deletion activities. Spikes in file deletion observed from risky sign-in sessions are flagged here.
This applies to SharePoint and OneDrive users.
Audit event and Cloud application identifier references.
Reference - https://learn.microsoft.com/microsoft-365/compliance/audit-log-activities?view=o365-worldwide
Reference - https://learn.microsoft.com/azure/sentinel/entities-reference#cloud-application-identifiers
severity: High
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- CloudAppEvents
- AADSignInEventsBeta
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1485
query: |
let relevantOperations = pack_array("FileDeleted", "FileRecycled", "FileDeletedFirstStageRecycleBin", "FileDeletedSecondStageRecycleBin", "FileVersionsAllMinorsRecycled", "FileVersionRecycled", "FileVersionsAllRecycled");
let relevantAppIds = pack_array(int(20892), int(15600)); // App Ids for SharePoint and OneDrive
let timeWindow = 7d;
let timeNow = now();
//
let riskyUsers= // Look for users with risky sign-ins
SigninLogs
| where CreatedDateTime between ((timeNow - timeWindow) .. (timeNow))
| where isnotempty(UserId) and isnotempty(OriginalRequestId)
| where ResultType == '0'
| where RiskLevelDuringSignIn == 'high'
| project UserId, RiskLevelDuringSignIn, CreatedDateTime, SessionId=OriginalRequestId
;
let hasUsers = isnotempty(toscalar(riskyUsers));
//
let deleteEvents = // look for file deletion activity and scope it to risky users
CloudAppEvents
| where hasUsers
| where TimeGenerated between ((timeNow - timeWindow) .. (timeNow))
| where ApplicationId in (relevantAppIds)
| where isnotempty(AccountObjectId)
| where AccountObjectId in (riskyUsers)
| where ActionType in (relevantOperations)
| extend SessionId= tostring(RawEventData.AppAccessContext.AADSessionId)
| where isnotempty(SessionId)
| project UserId=AccountObjectId, AccountDisplayName, ApplicationId, SessionId, ActionType, TimeGenerated, ReportId
;
//
deleteEvents
| join kind=leftsemi riskyUsers on UserId, SessionId
| summarize Count=count() , (Timestamp, ReportId)=arg_min(TimeGenerated, ReportId) by UserId, AccountDisplayName, ApplicationId, ActionType, Time=bin(TimeGenerated, 5m)
// look for only those scoped users who have generated an increase in file deletion activity.
| summarize TotalCount= countif(Count > 50), (Timestamp, ReportId)=arg_min(Timestamp, ReportId) by UserId, AccountDisplayName, ApplicationId
| where TotalCount >= 3
| project UserId, AccountDisplayName, ApplicationId, TotalCount, ReportId, Timestamp
| extend NTDomain = tostring(split(AccountDisplayName,'\\',0)[0]), Name = tostring(split(AccountDisplayName,'\\',1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: UserId
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Name
- identifier: NTDomain
columnName: NTDomain
- entityType: CloudApplication
fieldMappings:
- identifier: AppId
columnName: ApplicationId
customDetails:
Count: TotalCount
version: 1.0.3
kind: Scheduled
Stages and Predicates
Parameters
let relevantAppIds = pack_array(int(20892), int(15600));
let timeWindow = 7d;
let timeNow = now();
let hasUsers = isnotempty(toscalar(riskyUsers));
let deleteEvents is inlined into the numbered stages below.
Let Binding: relevantOperations
let relevantOperations = pack_array("FileDeleted", "FileRecycled", "FileDeletedFirstStageRecycleBin", "FileDeletedSecondStageRecycleBin", "FileVersionsAllMinorsRecycled", "FileVersionRecycled", "FileVersionsAllRecycled");
Let Binding: riskyUsers
let riskyUsers = SigninLogs
| where CreatedDateTime between ((timeNow - timeWindow) .. (timeNow))
| where isnotempty(UserId) and isnotempty(OriginalRequestId)
| where ResultType == '0'
| where RiskLevelDuringSignIn == 'high'
| project UserId, RiskLevelDuringSignIn, CreatedDateTime, SessionId=OriginalRequestId;
Stage 1: source
let deleteEvents
Stage 2: source
CloudAppEvents
Stage 3: where
where hasUsers == true
Stage 4: where
where /* macro: (TimeGenerated between ((timeNow - 7d) .. timeNow)) */
Stage 5: where
where ApplicationId == "relevantAppIds"
Stage 6: where
where isnotempty(AccountObjectId)
Stage 7: where
where AccountObjectId == "riskyUsers"
Stage 8: where
where ActionType == "relevantOperations"
Stage 9: extend
extend SessionId
Stage 10: where
where isnotempty(SessionId)
Stage 11: project
project AccountDisplayName, ActionType, ApplicationId, ReportId, SessionId, TimeGenerated, UserId
Stage 12: join
join kind=leftsemi (riskyUsers) on UserId, SessionId
Stage 13: summarize
summarize Count, ReportId, Time, Timestamp by UserId, AccountDisplayName, ApplicationId, ActionType, Time
Stage 14: summarize
summarize ReportId, Timestamp, TotalCount by UserId, AccountDisplayName, ApplicationId
Stage 15: where
where TotalCount >= 3
Stage 16: project
project AccountDisplayName, ApplicationId, ReportId, Timestamp, TotalCount, UserId
Stage 17: extend
extend NTDomain, Name
Indicators
These rows show field, operator, and value matches.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountDisplayName | project |
ApplicationId | project |
ReportId | project |
Timestamp | project |
TotalCount | project |
UserId | project |
NTDomain | extend |
Name | extend |
Vaikora - Feed outage detection
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
Identifies when no Vaikora data has arrived in the Vaikora_AgentSignals_CL table for 12 or more hours, which may indicate a failed playbook, expired API key, or connectivity issue.
Rule Body
id: 5f7789fa-0a6b-4dff-a2da-dfa4b682f3af
name: Vaikora - Feed outage detection
description: |
Identifies when no Vaikora data has arrived in the Vaikora_AgentSignals_CL table for 12 or more hours, which may indicate a failed playbook, expired API key, or connectivity issue.
severity: Low
requiredDataConnectors:
- connectorId: VaikoraSecurityCenter
dataTypes:
- Vaikora_AgentSignals_CL
queryFrequency: 12h
queryPeriod: 12h
triggerOperator: lt
triggerThreshold: 1
status: Available
tactics: []
relevantTechniques: []
query: |
Vaikora_AgentSignals_CL
| where TimeGenerated >= ago(12h)
| summarize Count = count()
| where Count == 0
| extend
Alert = "No Vaikora data ingested in the last 12 hours",
Suggestion = "Check the VaikoraToAzureSecurityCenter Logic App run history and verify the Vaikora API key is valid."
| project Alert, Suggestion
alertDetailsOverride:
alertDisplayNameFormat: "Vaikora feed outage detected"
alertDescriptionFormat: "Vaikora_AgentSignals_CL has received no records in the last 12 hours."
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
Vaikora_AgentSignals_CL
Stage 2: where
| where TimeGenerated >= ago(12h)
Stage 3: summarize
| summarize Count = count()
Stage 4: where
| where Count == 0
Stage 5: extend
| extend
Alert = "No Vaikora data ingested in the last 12 hours",
Suggestion = "Check the VaikoraToAzureSecurityCenter Logic App run history and verify the Vaikora API key is valid."
Stage 6: project
| project Alert, Suggestion
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Count | eq |
| field:"Count" kind:eq value:"0" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Alert | project |
Suggestion | project |
Vaikora - Quarantine rate spike
#Identifies an abnormal spike in Vaikora for O365 quarantine volume for a tenant. Fires when the hourly count exceeds three times the rolling 7-day baseline, signaling a targeted campaign or model surge worth admin review.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 3b3eb1cd-578d-4198-9b7a-bd7253b0dc9f
name: Vaikora - Quarantine rate spike
description: |
Identifies an abnormal spike in Vaikora for O365 quarantine volume for a tenant. Fires when the hourly count exceeds three times the rolling 7-day baseline, signaling a targeted campaign or model surge worth admin review.
severity: Medium
requiredDataConnectors:
- connectorId: VaikoraO365
dataTypes:
- VaikoraO365_Quarantine_CL
queryFrequency: 1h
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
relevantTechniques:
- T1566
query: |
let baseline = VaikoraO365_Quarantine_CL
| where TimeGenerated between (ago(7d) .. ago(1h))
| summarize HourlyCount = count() by bin(TimeGenerated, 1h), TenantId_s
| summarize AvgHourlyCount = avg(HourlyCount) by TenantId_s;
VaikoraO365_Quarantine_CL
| where TimeGenerated >= ago(1h)
| summarize RecentCount = count() by TenantId_s
| join kind=inner baseline on TenantId_s
| where RecentCount > (AvgHourlyCount * 3.0)
| where RecentCount >= 10
| project TimeGenerated = now(), TenantId = TenantId_s, RecentCount, AvgHourlyCount, RatioOverBaseline = (RecentCount / AvgHourlyCount)
entityMappings:
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: TenantId
alertDetailsOverride:
alertDisplayNameFormat: "Vaikora-O365 quarantine spike on tenant {{TenantId}}"
alertDescriptionFormat: "Quarantine count for the last hour is {{RecentCount}}, which is more than three times the 7-day baseline of {{AvgHourlyCount}}."
version: 1.0.0
kind: Scheduled
Stages and Predicates
Let Binding: baseline
let baseline = VaikoraO365_Quarantine_CL
| where TimeGenerated between (ago(7d) .. ago(1h))
| summarize HourlyCount = count() by bin(TimeGenerated, 1h), TenantId_s
| summarize AvgHourlyCount = avg(HourlyCount) by TenantId_s;
Stage 1: source
VaikoraO365_Quarantine_CL
Stage 2: where
| where TimeGenerated >= ago(1h)
Stage 3: summarize
| summarize RecentCount = count() by TenantId_s
Stage 4: join
| join kind=inner baseline on TenantId_s
Stage 5: where
where RecentCount > (AvgHourlyCount * 3.0)
Stage 6: where
where RecentCount >= 10
Stage 7: project
project AvgHourlyCount, RatioOverBaseline, RecentCount, TenantId, TimeGenerated
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
RecentCount | ge |
| field:"RecentCount" kind:ge value:"10" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AvgHourlyCount | project |
RatioOverBaseline | project |
RecentCount | project |
TenantId | project |
TimeGenerated | project |
Vectra Host's Behaviors
#'This analytic rule is looking for new attacker behaviors observed by the Vectra Platform. This rule is focused on host's detections.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access | |
| Discovery | |
| Lateral Movement | |
| Collection | |
| Command & Control | |
| Exfiltration | |
| Impact |
Rule Body
id: 33e3b6da-2660-4cd7-9032-11be76db88d2
name: Vectra Host's Behaviors
description: |
'This analytic rule is looking for new attacker behaviors observed by the Vectra Platform. This rule is focused on host's detections.'
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics:
- CredentialAccess
- Discovery
- LateralMovement
- Collection
- CommandAndControl
- Exfiltration
- Impact
relevantTechniques:
- T1003
- T1087
- T1021
- T1119
- T1071
- T1041
- T1499
query: |
CommonSecurityLog
| where DeviceVendor == "Vectra Networks"
| where DeviceProduct == "X Series"
| where DeviceEventClassID != "campaigns"
and DeviceEventClassID != "hsc"
and DeviceEventClassID != "audit"
and DeviceEventClassID != "health"
and DeviceEventClassID != "asc"
| extend Category = coalesce(
column_ifexists("DeviceEventCategory", ""),
extract("cat=(.+?)(;|$)", 1, AdditionalExtensions),
""
)
| project-rename threat_score = FlexNumber1
| project-rename certainty_score = FlexNumber2
| project-rename vectra_URL = DeviceCustomString4
| project-rename detection_name = DeviceEventClassID
| project-rename triaged = DeviceCustomString5
| where triaged != "True" and AdditionalExtensions !has "account"
| extend source_entity = case(isnotempty(SourceHostName), SourceHostName,
"UNKNWON")
| extend level = case(threat_score == 0 and certainty_score == 0, "Info",
threat_score < 50 and certainty_score < 50, "Low",
threat_score < 50 and certainty_score >= 50, "Medium",
threat_score >= 50 and certainty_score <= 50, "High",
threat_score >= 50 and certainty_score >= 50, "Critical",
"UNKNOWN")
| extend Severity = case(level == "Info", "Informational",level == "Critical", "High", level)
| summarize arg_max(threat_score, *) by source_entity, Activity
| sort by TimeGenerated
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: SourceHostName
alertDetailsOverride:
alertDisplayNameFormat: Vectra AI - {{Activity}} Detected
alertDescriptionFormat: |
Entity is a host. Category is {{Category}}. Threat score is {{threat_score}} and certainty score is {{certainty_score}}.
alertSeverityColumnName: Severity
alertDynamicProperties:
- alertProperty: AlertLink
value: vectra_URL
- alertProperty: ProductName
value: DeviceProduct
- alertProperty: ProviderName
value: DeviceVendor
- alertProperty: ConfidenceScore
value: certainty_score
customDetails:
AttackType: Activity
AttackCategory: Category
version: 1.0.5
kind: Scheduled
Stages and Predicates
Stage 1: source
CommonSecurityLog
Stage 2: where
| where DeviceVendor == "Vectra Networks"
Stage 3: where
| where DeviceProduct == "X Series"
Stage 4: where
| where DeviceEventClassID != "campaigns"
and DeviceEventClassID != "hsc"
and DeviceEventClassID != "audit"
and DeviceEventClassID != "health"
and DeviceEventClassID != "asc"
Stage 5: extend
| extend Category = coalesce(
column_ifexists("DeviceEventCategory", ""),
extract("cat=(.+?)(;|$)", 1, AdditionalExtensions),
""
)
Stage 6: project-rename
| project-rename threat_score = FlexNumber1
Stage 7: project-rename
| project-rename certainty_score = FlexNumber2
Stage 8: project-rename
| project-rename vectra_URL = DeviceCustomString4
Stage 9: project-rename
| project-rename detection_name = DeviceEventClassID
Stage 10: project-rename
| project-rename triaged = DeviceCustomString5
Stage 11: where
| where triaged != "True" and AdditionalExtensions !has "account"
Stage 12: extend (3 consecutive steps)
| extend source_entity = case(isnotempty(SourceHostName), SourceHostName,
"UNKNWON")
| extend level = case(threat_score == 0 and certainty_score == 0, "Info",
threat_score < 50 and certainty_score < 50, "Low",
threat_score < 50 and certainty_score >= 50, "Medium",
threat_score >= 50 and certainty_score <= 50, "High",
threat_score >= 50 and certainty_score >= 50, "Critical",
"UNKNOWN")
| extend Severity = case(level == "Info", "Informational",level == "Critical", "High", level)
source_entity =isnotempty(SourceHostName)SourceHostName"UNKNWON"Stage 13: summarize
| summarize arg_max(threat_score, *) by source_entity, Activity
Stage 14: sort
| sort by TimeGenerated
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DeviceEventClassID | ne |
| field:"DeviceEventClassID" kind:ne |
DeviceProduct | eq |
| field:"DeviceProduct" kind:eq value:"X Series" |
DeviceVendor | eq |
| field:"DeviceVendor" kind:eq value:"Vectra Networks" |
triaged | ne |
| field:"DeviceCustomString5" kind:ne value:"True" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
AdditionalExtensions | match | account | excludes:AdditionalExtensions field:"AdditionalExtensions" value:"account" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Activity | summarize |
source_entity | summarize |
VMware ESXi - Multiple new VMs started
#'Detects when multiple new VMs were started.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: bdea247f-7d17-498c-ac0e-c7e764cbdbbe
name: VMware ESXi - Multiple new VMs started
description: |
'Detects when multiple new VMs were started.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SyslogAma
datatypes:
- Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |
let threshold = 5;
let a_vm =
VMwareESXi
| where SyslogMessage has ('VmPoweredOnEvent')
| extend DstHostname = extract(@'\[\d+\]\s+\[(.*?)\s+on', 1, SyslogMessage)
| summarize vm_l = make_set(DstHostname,200)
| extend k=1;
VMwareESXi
| where SyslogMessage has ('VmPoweredOnEvent')
| extend DstHostname = extract(@'\[\d+\]\s+\[(.*?)\s+on', 1, SyslogMessage)
| extend SrcUsername = extract(@'\[info\]\s+\[(.*?)\]', 1, SyslogMessage)
| extend k = 1
| join (a_vm) on k
| where vm_l !has DstHostname
| summarize n_vm = make_set(DstHostname,200) by SrcUsername, bin(TimeGenerated, 10m)
| where array_length(n_vm) >= threshold
| extend hostname = iff(array_length(n_vm) > 0, n_vm[0], "")
| extend NTDomain = tostring(split(hostname, '\\', 1)[0]), HostName = tostring(split(hostname, '.', 0)[0])
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- identifier: NTDomain
columnName: NTDomain
version: 1.0.3
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 5;
Let Binding: a_vm
let a_vm = VMwareESXi
| where SyslogMessage has ('VmPoweredOnEvent')
| extend DstHostname = extract(@'\[\d+\]\s+\[(.*?)\s+on', 1, SyslogMessage)
| summarize vm_l = make_set(DstHostname,200)
| extend k=1;
Stage 1: source
VMwareESXi
Stage 2: where
| where SyslogMessage has ('VmPoweredOnEvent')
Stage 3: extend (3 consecutive steps)
| extend DstHostname = extract(@'\[\d+\]\s+\[(.*?)\s+on', 1, SyslogMessage)
| extend SrcUsername = extract(@'\[info\]\s+\[(.*?)\]', 1, SyslogMessage)
| extend k = 1
Stage 4: join
| join (a_vm) on k
Stage 5: where
where vm_l =~ DstHostname
Stage 6: summarize
summarize n_vm by SrcUsername
Stage 7: where
where n_vm >= 5
Stage 8: extend
extend hostname
hostname =n_vm > 0n_vm[0]""Stage 9: extend
extend HostName, NTDomain
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
SyslogMessage | match |
| field:"SyslogMessage" kind:match value:"VmPoweredOnEvent" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SrcUsername | summarize |
n_vm | summarize |
hostname | extend |
HostName | extend |
NTDomain | extend |