Detection rules › Kusto
Kusto rules: 2l
Abnormal Security - High-risk email attack detected
#'Identifies email attacks detected by Abnormal Security whose attack type maps to a high-risk category (credential phishing, Business Email Compromise, invoice/payment fraud, malware, extortion, sensitive-data phishing, internal account-takeover attacks, or scams). Lower-risk categories such as Spam, Graymail, and Reconnaissance are intentionally excluded. Use this to triage targeted email threats that reached a mailbox.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 8effd19a-abab-433a-9184-ae67ac51e6d0
name: Abnormal Security - High-risk email attack detected
description: |
'Identifies email attacks detected by Abnormal Security whose attack type maps to a
high-risk category (credential phishing, Business Email Compromise, invoice/payment
fraud, malware, extortion, sensitive-data phishing, internal account-takeover attacks,
or scams). Lower-risk categories such as Spam, Graymail, and Reconnaissance are
intentionally excluded. Use this to triage targeted email threats that reached a mailbox.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: AbnormalSecurityPush
dataTypes:
- ABNORMAL_SECURITY_THREAT_LOG_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1566
query: |
ABNORMAL_SECURITY_THREAT_LOG_CL
| where abx_body_abx_body_attack_type_s in~ (
"Phishing: Credential",
"Social Engineering (BEC)",
"Invoice/Payment Fraud (BEC)",
"Malware",
"Extortion",
"Phishing: Sensitive Data",
"Internal-to-Internal Attacks (Email Account Takeover)",
"Scam")
| extend
RecipientEmail = abx_body_abx_body_recipient_address_s,
SenderEmail = abx_body_abx_body_from_address_s,
SenderName = abx_body_abx_body_from_name_s,
SenderIP = abx_body_abx_body_sender_ip_address_s,
Subject = abx_body_abx_body_subject_s,
MessageId = abx_body_abx_body_internet_message_id_s,
AttackType = abx_body_abx_body_attack_type_s,
AttackScore = abx_body_abx_body_attack_score_d,
RemediationStatus = abx_body_abx_body_remediation_status_s,
PortalUrl = abx_body_abx_body_abx_portal_url_s
| project TimeGenerated, RecipientEmail, SenderEmail, SenderName, SenderIP, Subject, MessageId, AttackType, AttackScore, RemediationStatus, PortalUrl
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: RecipientEmail
- entityType: MailMessage
fieldMappings:
- identifier: Recipient
columnName: RecipientEmail
- identifier: Sender
columnName: SenderEmail
- identifier: NetworkMessageId
columnName: MessageId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SenderIP
customDetails:
AttackType: AttackType
AttackScore: AttackScore
RemediationStatus: RemediationStatus
AbnormalPortalUrl: PortalUrl
alertDetailsOverride:
alertDisplayNameFormat: "Abnormal Security: {{AttackType}} email delivered to {{RecipientEmail}}"
alertDescriptionFormat: "Abnormal Security detected a {{AttackType}} attack from {{SenderEmail}} to {{RecipientEmail}}. See attack score and remediation status in the custom details."
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: AlertPerResult
suppressionDuration: PT5H
suppressionEnabled: false
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
ABNORMAL_SECURITY_THREAT_LOG_CL
Stage 2: where
| where abx_body_abx_body_attack_type_s in~ (
"Phishing: Credential",
"Social Engineering (BEC)",
"Invoice/Payment Fraud (BEC)",
"Malware",
"Extortion",
"Phishing: Sensitive Data",
"Internal-to-Internal Attacks (Email Account Takeover)",
"Scam")
Stage 3: extend
| extend
RecipientEmail = abx_body_abx_body_recipient_address_s,
SenderEmail = abx_body_abx_body_from_address_s,
SenderName = abx_body_abx_body_from_name_s,
SenderIP = abx_body_abx_body_sender_ip_address_s,
Subject = abx_body_abx_body_subject_s,
MessageId = abx_body_abx_body_internet_message_id_s,
AttackType = abx_body_abx_body_attack_type_s,
AttackScore = abx_body_abx_body_attack_score_d,
RemediationStatus = abx_body_abx_body_remediation_status_s,
PortalUrl = abx_body_abx_body_abx_portal_url_s
Stage 4: project
| project TimeGenerated, RecipientEmail, SenderEmail, SenderName, SenderIP, Subject, MessageId, AttackType, AttackScore, RemediationStatus, PortalUrl
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
abx_body_abx_body_attack_type_s | in |
| field:"abx_body_abx_body_attack_type_s" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AttackScore | project |
AttackType | project |
MessageId | project |
PortalUrl | project |
RecipientEmail | project |
RemediationStatus | project |
SenderEmail | project |
SenderIP | project |
SenderName | project |
Subject | project |
TimeGenerated | project |
AD FS Remote HTTP Network Connection
#This detection uses Sysmon events (NetworkConnect events) to detect incoming network traffic on port 80 on AD FS servers. This could be a sign of a threat actor trying to use replication services on the AD FS server to get its configuration settings and extract sensitive information such as AD FS certificates. In order to use this query you need to enable Sysmon telemetry on the AD FS Server. Reference: https://twitter.com/OTR_Community/status/1387038995016732672
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Collection |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Sysmon | Event ID 3: Network connection |
| Sysmon | Event ID 18: PipeEvent (Pipe Connected) |
Rule Body
id: d57c33a9-76b9-40e0-9dfa-ff0404546410
name: AD FS Remote HTTP Network Connection
description: |
'This detection uses Sysmon events (NetworkConnect events) to detect incoming network traffic on port 80 on AD FS servers. This could be a sign of a threat actor trying to use replication services on the AD FS server to get its configuration settings and extract sensitive information such as AD FS certificates.
In order to use this query you need to enable Sysmon telemetry on the AD FS Server.
Reference: https://twitter.com/OTR_Community/status/1387038995016732672
'
severity: Medium
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- Collection
relevantTechniques:
- T1005
tags:
- SimuLand
query: |
// Adjust this to use a longer timeframe to identify ADFS servers
//let lookback = 0d;
// Adjust this to adjust detection timeframe
//let timeframe = 1d;
// Filter out other servers in the AD FS farm
let ADFSServersList = dynamic(["ADFS02.domain.com","ADFS03.domain.com"]);
// Start by identifying ADFS servers to reduce FP chance
let ADFS_Servers = (
Event
//| where TimeGenerated > ago(timeframe+lookback)
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 18
| where Computer !in (ADFSServersList)
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, MG, ManagementGroupName, _ResourceId)
| extend Image = column_ifexists("Image", "")
| extend process = split(Image, '\\', -1)[-1]
| where process =~ "Microsoft.IdentityServer.ServiceHost.exe"
| summarize by Computer
);
// Look for ADFS servers receiving connections over port 80
Event
//| where TimeGenerated > ago(timeframe)
| where Source == "Microsoft-Windows-Sysmon"
| where Computer in~ (ADFS_Servers)
| extend RenderedDescription = tostring(split(RenderedDescription, ":")[0])
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, RenderedDescription, MG, ManagementGroupName, _ResourceId)
| extend RuleName = column_ifexists("RuleName", ""), TechniqueId = column_ifexists("TechniqueId", ""), TechniqueName = column_ifexists("TechniqueName", "")
| parse RuleName with * 'technique_id=' TechniqueId ',' * 'technique_name=' TechniqueName
| where EventID == 3
// Look for endpoints connecting to the AD FS server over port 80
| extend DestinationPort = column_ifexists("DestinationPort", ""), Image = column_ifexists("Image", ""), Initiated = column_ifexists("Initiated", ""), SourceIp = column_ifexists("DestinationIp", ""), DestinationIp = column_ifexists("DestinationIp", "")
| where DestinationPort == 80
| extend process = split(Image, '\\', -1)[-1]
// Look for the System process receiving connections
| where process == 'System' and Initiated == 'false'
| where DestinationIp !in ('::1','0:0:0:0:0:0:0:1')
| extend Operation = RenderedDescription
| project-reorder TimeGenerated, Operation, Image, Computer, UserName
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(UserName, @'\')[1]), AccountNTDomain = tostring(split(UserName, @'\')[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserName
- identifier: Name
columnName: AccountName
- identifier: NTDomain
columnName: AccountNTDomain
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: HostNameDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIp
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let ADFSServersList = dynamic(["ADFS02.domain.com","ADFS03.domain.com"]);
Let Binding: ADFS_Servers
let ADFS_Servers = (
Event
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 18
| where Computer !in (ADFSServersList)
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, MG, ManagementGroupName, _ResourceId)
| extend Image = column_ifexists("Image", "")
| extend process = split(Image, '\\', -1)[-1]
| where process =~ "Microsoft.IdentityServer.ServiceHost.exe"
| summarize by Computer
);
Stage 1: source
let ADFS_Servers
Stage 2: source
Event
Stage 3: where
| where Source == "Microsoft-Windows-Sysmon"
Stage 4: where
| where Computer in~ (ADFS_Servers)
Stage 5: extend
| extend RenderedDescription = tostring(split(RenderedDescription, ":")[0])
Stage 6: extend
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
Stage 7: mv-expand
| mv-expand bagexpansion=array EventData
Stage 8: evaluate
| evaluate bag_unpack(EventData)
Stage 9: extend
| extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
Stage 10: evaluate
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, RenderedDescription, MG, ManagementGroupName, _ResourceId)
Stage 11: extend
| extend RuleName = column_ifexists("RuleName", ""), TechniqueId = column_ifexists("TechniqueId", ""), TechniqueName = column_ifexists("TechniqueName", "")
Stage 12: parse
| parse RuleName with * 'technique_id=' TechniqueId ',' * 'technique_name=' TechniqueName
Stage 13: where
| where EventID == 3
Stage 14: extend
| extend DestinationPort = column_ifexists("DestinationPort", ""), Image = column_ifexists("Image", ""), Initiated = column_ifexists("Initiated", ""), SourceIp = column_ifexists("DestinationIp", ""), DestinationIp = column_ifexists("DestinationIp", "")
Stage 15: where
| where DestinationPort == 80
Stage 16: extend
| extend process = split(Image, '\\', -1)[-1]
Stage 17: where
| where process == 'System' and Initiated == 'false'
Stage 18: where
| where DestinationIp !in ('::1','0:0:0:0:0:0:0:1')
Stage 19: extend
| extend Operation = RenderedDescription
Stage 20: project-reorder
| project-reorder TimeGenerated, Operation, Image, Computer, UserName
Stage 21: extend (3 consecutive steps)
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(UserName, @'\')[1]), AccountNTDomain = tostring(split(UserName, @'\')[0])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Computer | in |
| field:"Computer" kind:in value:"ADFS_Servers" |
DestinationPort | eq |
| field:"DestinationPort" kind:eq value:"80" |
EventID | eq |
| field:"EventID" kind:eq value:"3" |
Initiated | eq |
| field:"Initiated" kind:eq value:"false" |
Source | eq |
| field:"Source" kind:eq value:"Microsoft-Windows-Sysmon" |
process | eq |
| field:"process" kind:eq value:"System" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
DestinationIp | in | 0:0:0:0:0:0:0:1, ::1 | excludes:DestinationIp field:"DestinationIp" value:"0:0:0:0:0:0:0:1" field:"DestinationIp" value:"::1" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
RenderedDescription | extend |
EventData | extend |
Key | extend |
Value | extend |
RuleName | extend |
TechniqueId | extend |
TechniqueName | extend |
DestinationIp | extend |
DestinationPort | extend |
Image | extend |
Initiated | extend |
SourceIp | extend |
process | extend |
Operation | extend |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |
AccountNTDomain | extend |
AccountName | extend |
AWSCloudTrail - Privilege escalation with admin managed policy
#Detects successful attachment of admin-related managed IAM policies to users, roles, or groups, excluding the dedicated AdministratorAccess and FullAccess patterns handled by other detections. This behavior may indicate unauthorized privilege escalation and should be validated against approved administrative changes.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- AWS IAM CompromisedKeyQuarantine Policy Attached to User (Elastic)
- AWS Lateral Movement from Kubernetes SA via AssumeRoleWithWebIdentity (Elastic)
- AWSCloudTrail - CloudFormation policy created then used for privilege escalation (Kusto)
- AWSCloudTrail - Created CRUD S3 policy and then privilege escalation (Kusto)
- AWSCloudTrail - Creation of CRUD DynamoDB policy and then privilege escalation (Kusto)
- AWSCloudTrail - Creation of CRUD KMS policy and then privilege escalation (Kusto)
- AWSCloudTrail - Creation of CRUD Lambda policy and then privilege escalation (Kusto)
- AWSCloudTrail - Creation of DataPipeline policy and then privilege escalation (Kusto)
Rule Body
id: 49ce5322-60d7-4b02-ad79-99f650aa5790
name: AWSCloudTrail - Privilege escalation with admin managed policy
description: |
Detects successful attachment of admin-related managed IAM policies to users, roles, or groups, excluding
the dedicated AdministratorAccess and FullAccess patterns handled by other detections. This behavior may indicate
unauthorized privilege escalation and should be validated against approved administrative changes.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AWS
dataTypes:
- AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
- Persistence
relevantTechniques:
- T1098.003
query: |
AWSCloudTrail
| where EventName in ("AttachUserPolicy","AttachRolePolicy","AttachGroupPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
| where tostring(parse_json(RequestParameters).policyArn) contains "Admin" and tostring(parse_json(RequestParameters).policyArn) !contains "FullAccess" and tostring(parse_json(RequestParameters).policyArn) !startswith "arn:aws:iam::aws:policy/AdministratorAccess"
| 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]), "")
| project TimeGenerated, EventName, EventTypeName, UserIdentityAccountId, UserIdentityPrincipalid, UserAgent, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityUserName, SessionMfaAuthenticated, SourceIpAddress, AWSRegion, EventSource, AdditionalEventData, RequestParameters, ResponseElements, UserIdentityArn
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
EventSource: EventSource
AWSRegion: AWSRegion
UserIdentityArn: UserIdentityArn
alertDetailsOverride:
alertDisplayNameFormat: 'AWS admin managed policy attachment by {{AccountName}}'
alertDescriptionFormat: 'Detected {{EventName}} from {{SourceIpAddress}} attaching admin-related managed policy in account {{RecipientAccountId}}.'
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
AWSCloudTrail
Stage 2: where
| where EventName in ("AttachUserPolicy","AttachRolePolicy","AttachGroupPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
Stage 3: where
| where tostring(parse_json(RequestParameters).policyArn) contains "Admin" and tostring(parse_json(RequestParameters).policyArn) !contains "FullAccess" and tostring(parse_json(RequestParameters).policyArn) !startswith "arn:aws:iam::aws:policy/AdministratorAccess"
Stage 4: 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 5: project
| project TimeGenerated, EventName, EventTypeName, UserIdentityAccountId, UserIdentityPrincipalid, UserAgent, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityUserName, SessionMfaAuthenticated, SourceIpAddress, AWSRegion, EventSource, AdditionalEventData, RequestParameters, ResponseElements, UserIdentityArn
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 | in |
| field:"aws::eventName" kind:in |
RequestParameters.policyArn | contains |
| field:"RequestParameters.policyArn" kind:contains value:"Admin" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
RequestParameters.policyArn | contains | FullAccess | excludes:RequestParameters.policyArn field:"RequestParameters.policyArn" value:"FullAccess" |
RequestParameters.policyArn | starts_with | arn:aws:iam::aws:policy/AdministratorAccess | excludes:RequestParameters.policyArn field:"RequestParameters.policyArn" value:"arn:aws:iam::aws:policy/AdministratorAccess" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AWSRegion | project |
AccountName | project |
AccountUPNSuffix | project |
AdditionalEventData | project |
EventName | project |
EventSource | project |
EventTypeName | project |
RecipientAccountId | project |
RequestParameters | project |
ResponseElements | project |
SessionMfaAuthenticated | project |
SourceIpAddress | project |
TimeGenerated | project |
UserAgent | project |
UserIdentityAccountId | project |
UserIdentityArn | project |
UserIdentityPrincipalid | project |
UserIdentityUserName | project |
Azure DevOps Variable Secret Not Secured
#'Credentials used in the build process may be stored as Azure DevOps variables. To secure these variables they should be stored in KeyVault or marked as Secrets. This detection looks for new variables added with names that suggest they are credentials but where they are not set as Secrets or stored in KeyVault.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: 4ca74dc0-8352-4ac5-893c-73571cc78331
name: Azure DevOps Variable Secret Not Secured
description: |
'Credentials used in the build process may be stored as Azure DevOps variables. To secure these variables they should be stored in KeyVault or marked as Secrets.
This detection looks for new variables added with names that suggest they are credentials but where they are not set as Secrets or stored in KeyVault.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1552
query: |
let keywords = dynamic(["secret", "secrets", "password", "PAT", "passwd", "pswd", "pwd", "cred", "creds", "credentials", "credential", "key"]);
ADOAuditLogs
| where OperationName =~ "Library.VariableGroupModified"
| extend Type = tostring(Data.Type)
| extend VariableGroupId = tostring(Data.VariableGroupId)
| extend VariableGroupName = tostring(Data.VariableGroupName)
| mv-expand Data.Variables
| where VariableGroupName has_any (keywords) or Data_Variables has_any (keywords)
| where Type != "AzureKeyVault"
| where Data_Variables !has "IsSecret"
| extend timestamp = TimeGenerated
| extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ActorUPN
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpAddress
version: 1.0.4
kind: Scheduled
Stages and Predicates
Parameters
let keywords = dynamic(["secret", "secrets", "password", "PAT", "passwd", "pswd", "pwd", "cred", "creds", "credentials", "credential", "key"]);
Stage 1: source
ADOAuditLogs
Stage 2: where
| where OperationName =~ "Library.VariableGroupModified"
Stage 3: extend (3 consecutive steps)
| extend Type = tostring(Data.Type)
| extend VariableGroupId = tostring(Data.VariableGroupId)
| extend VariableGroupName = tostring(Data.VariableGroupName)
Stage 4: mv-expand
| mv-expand Data.Variables
Stage 5: where
| where VariableGroupName has_any (keywords) or Data_Variables has_any (keywords)
Stage 6: where
| where Type != "AzureKeyVault"
Stage 7: where
| where Data_Variables !has "IsSecret"
Stage 8: extend
| extend timestamp = TimeGenerated
Stage 9: extend
| extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Data_Variables | match |
| field:"Data_Variables" kind:match |
OperationName | eq |
| field:"OperationName" kind:eq value:"Library.VariableGroupModified" |
Type | ne |
| field:"Type" kind:ne value:"AzureKeyVault" |
VariableGroupName | match |
| field:"VariableGroupName" kind:match |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Data_Variables | match | IsSecret | excludes:Data_Variables field:"Data_Variables" value:"IsSecret" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Type | extend |
VariableGroupId | extend |
VariableGroupName | extend |
timestamp | extend |
AccountName | extend |
AccountUPNSuffix | extend |
BloodHound Attack Path Finding - Write Account Restrictions Privileges on Tier Zero Objects
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
BloodHound Attack Path Finding - Write Account Restrictions Privileges on Tier Zero Objects
Rule Body
id: f1a667b2-91a8-4a9b-8d31-9a54bb5f0cd5
name: BloodHound Attack Path Finding - Write Account Restrictions Privileges on Tier
Zero Objects
description: BloodHound Attack Path Finding - Write Account Restrictions Privileges
on Tier Zero Objects
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: BloodHoundEnterprise
dataTypes:
- BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: BHEAttackPathsData_CL | where Finding == "T0WriteAccountRestrictions"
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: domain_name
customDetails:
Description: ShortDescription
Remediation: ShortRemediation
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
BHEAttackPathsData_CL
Stage 2: where
| where Finding == "T0WriteAccountRestrictions"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"T0WriteAccountRestrictions" |
Box - User logged in as admin
#'Detects when user logged in as admin.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Rule Body
id: b2197d7f-4731-483c-89de-d48606b872da
name: Box - User logged in as admin
description: |
'Detects when user logged in as admin.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: BoxDataConnector
dataTypes:
- BoxEvents_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1078
query: |
let lbperiod_start = 14d;
let lbperiod_end = 1d;
let admins = BoxEvents
| where EventEndTime between (ago(lbperiod_start) .. ago(lbperiod_end))
| where EventType =~ 'ADMIN_LOGIN'
| summarize makeset(SourceLogin);
BoxEvents
| where EventType =~ 'ADMIN_LOGIN'
| where SourceLogin !in (admins)
| extend AccountCustomEntity = SourceLogin
| extend IPCustomEntity = SrcIpAddr
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountCustomEntity
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let lbperiod_start = 14d;
let lbperiod_end = 1d;
Let Binding: admins
let admins = BoxEvents
| where EventEndTime between (ago(lbperiod_start) .. ago(lbperiod_end))
| where EventType =~ 'ADMIN_LOGIN'
| summarize makeset(SourceLogin);
Stage 1: source
BoxEvents
Stage 2: where
| where EventType =~ 'ADMIN_LOGIN'
Stage 3: where
| where SourceLogin !in (admins)
Stage 4: extend
| extend AccountCustomEntity = SourceLogin
Stage 5: extend
| extend IPCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventType | eq |
| field:"EventType" kind:eq value:"ADMIN_LOGIN" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
SourceLogin | eq | admins | excludes:SourceLogin field:"SourceLogin" value:"admins" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountCustomEntity | extend |
IPCustomEntity | extend |
Box - User role changed to owner
#'Detects when user collaboration role is changed to owner.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Rule Body
id: 174c31c9-22ec-42e5-8226-814391c08200
name: Box - User role changed to owner
description: |
'Detects when user collaboration role is changed to owner.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: BoxDataConnector
dataTypes:
- BoxEvents_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1078
query: |
let lbperiod = 14d;
let lbtime = 1h;
BoxEvents
| where EventEndTime between (ago(lbperiod) .. ago(lbtime))
| where EventType =~ 'COLLABORATION_INVITE'
| where AdditionalDetailsRole !~ 'Owner'
| summarize min(EventEndTime) by AccessibleByName, FileDirectory, AdditionalDetailsRole
| project AccessibleByName, FileDirectory, InitialRole = AdditionalDetailsRole
|join (BoxEvents
| where EventType =~ 'COLLABORATION_ROLE_CHANGE'
| summarize max(EventEndTime) by AccessibleByName, FileDirectory, AdditionalDetailsRole
| project AccessibleByName, FileDirectory, NewRole = AdditionalDetailsRole
) on FileDirectory, AccessibleByName
| where NewRole =~ 'Owner'
| project AccessibleByName, FileDirectory
| extend AccountCustomEntity = AccessibleByName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let lbperiod = 14d;
let lbtime = 1h;
Stage 1: source
BoxEvents
Stage 2: where
| where EventEndTime between (ago(lbperiod) .. ago(lbtime))
Stage 3: where
| where EventType =~ 'COLLABORATION_INVITE'
Stage 4: where
| where AdditionalDetailsRole !~ 'Owner'
Stage 5: summarize
| summarize min(EventEndTime) by AccessibleByName, FileDirectory, AdditionalDetailsRole
Stage 6: project
| project AccessibleByName, FileDirectory, InitialRole = AdditionalDetailsRole
Stage 7: join
| join (BoxEvents
| where EventType =~ 'COLLABORATION_ROLE_CHANGE'
| summarize max(EventEndTime) by AccessibleByName, FileDirectory, AdditionalDetailsRole
| project AccessibleByName, FileDirectory, NewRole = AdditionalDetailsRole
) on FileDirectory, AccessibleByName
Stage 8: where
where NewRole =~ "Owner"
Stage 9: project
project AccessibleByName, FileDirectory
Stage 10: extend
extend AccountCustomEntity
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AdditionalDetailsRole | ne |
| field:"AdditionalDetailsRole" kind:ne value:"Owner" |
EventType | eq |
| field:"EventType" kind:eq |
NewRole | eq |
| field:"AdditionalDetailsRole" kind:eq value:"Owner" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccessibleByName | project |
FileDirectory | project |
AccountCustomEntity | extend |
Detect credential add to Connect Sync Application
#This detection specifically flags credentials being added to the Connect Sync Application in Entra ID, a technique known to have persistance from On-premise AD DS to Entra ID. It tries to look at both certificate, client secret, and federated credentials being added, and tries to remove legitimate renewal processes. Since the legitimate renewal process first adds a new certificate only te remove the old one short after, we by default allow for a maximum of 1 minute between the certificate create and delete events.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence | |
| Privilege Escalation | |
| Stealth | |
| Defense Impairment | |
| Credential Access |
References
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Application Granted Administrative Permission to Assign Microsoft Entra ID Roles (Kusto)
- Added Credentials to Existing Application (Sigma)
- Added Owner To Application (Sigma)
- Admin promotion after Role Management Application Permission Grant (Kusto)
- App Granted Microsoft Permissions (Sigma)
- App Granted Privileged Delegated Or App Permissions (Sigma)
- Application AppID Uri Configuration Changes (Sigma)
Rule Body
// Flag adding credential that does not look like a renewal
let base = materialize (
AuditLogs
// Search events happening on the Sync Account
| extend AppName = tostring(TargetResources[0].displayName)
| where AppName startswith "ConnectSyncProvisioning_"
// Only get cretificate or secret changes
| where OperationName has_any ("Add service principal", "Certificates and secrets management", "Update application")
// Expand the target resources and modified properties, and only use events ralted to KeyDescription
| mv-expand TargetResources
| extend ModifiedProperties = TargetResources.modifiedProperties
| mv-expand ModifiedProperties
| where ModifiedProperties.displayName == "KeyDescription"
// Save the old and new values of the secrets on the application
| extend OldValue = tostring(ModifiedProperties.oldValue),
NewValue = tostring(ModifiedProperties.newValue)
// Save the old and new credential names in an array
| extend OldCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), OldValue),
NewCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), NewValue)
);
let newCredsAdd = (
base
// Flag when there are more new credentials than old ones
| where array_length(OldCredentialNames) < array_length(NewCredentialNames)
| project-rename NewCredAddTimeGenerated = TimeGenerated
);
let credRemove = (
base
// Get events where credentials are being removed
| where array_length(OldCredentialNames) > array_length(NewCredentialNames)
| project-rename CredRemoveTimeGenerated = TimeGenerated
);
// Only flag when credentials are added without another being removed for the same application within a small time window
// This excludes a normal renewal process
newCredsAdd
| join kind=leftouter credRemove on AppName
| where CredRemoveTimeGenerated - NewCredAddTimeGenerated > 1m
| project NewCredAddTimeGenerated, CredRemoveTimeGenerated, OperationName, AdditionalDetails, InitiatedBy, AppName, ModifiedProperties, OldCredentialNames, NewCredentialNames
Stages and Predicates
let base and let newCredsAdd are inlined into the numbered stages below.
Let Binding: credRemove
let credRemove = (
base
| where array_length(OldCredentialNames) > array_length(NewCredentialNames)
| project-rename CredRemoveTimeGenerated = TimeGenerated
);
Stages 1 to 12 define let newCredsAdd as the rule's main pipeline source. Stages 13 to 15 use it.
Stage 1: source
AuditLogs
Stage 2: extend
| extend AppName = tostring(TargetResources[0].displayName)
Stage 3: where
| where AppName startswith "ConnectSyncProvisioning_"
Stage 4: where
| where OperationName has_any ("Add service principal", "Certificates and secrets management", "Update application")
Stage 5: mv-expand
| mv-expand TargetResources
Stage 6: extend
| extend ModifiedProperties = TargetResources.modifiedProperties
Stage 7: mv-expand
| mv-expand ModifiedProperties
Stage 8: where
| where ModifiedProperties.displayName == "KeyDescription"
Stage 9: extend
| extend OldValue = tostring(ModifiedProperties.oldValue),
NewValue = tostring(ModifiedProperties.newValue)
Stage 10: extend
| extend OldCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), OldValue),
NewCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), NewValue)
Stage 11: where
| where array_length(OldCredentialNames) < array_length(NewCredentialNames)
Stage 12: project-rename
| project-rename NewCredAddTimeGenerated = TimeGenerated
Stage 13: join
newCredsAdd
| join kind=leftouter credRemove on AppName
Stage 14: where CredRemoveTimeGenerated - NewCredAddTimeGenerated > 1m
where CredRemoveTimeGenerated - NewCredAddTimeGenerated > 1m
Stage 15: project
project AdditionalDetails, AppName, CredRemoveTimeGenerated, InitiatedBy, ModifiedProperties, NewCredAddTimeGenerated, NewCredentialNames, OldCredentialNames, OperationName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AppName | starts_with |
| field:"AppName" kind:starts_with value:"ConnectSyncProvisioning_" |
ModifiedProperties.displayName | eq |
| field:"ModifiedProperties.displayName" kind:eq value:"KeyDescription" |
OperationName | match |
| field:"OperationName" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AdditionalDetails | project |
AppName | project |
CredRemoveTimeGenerated | project |
InitiatedBy | project |
ModifiedProperties | project |
NewCredAddTimeGenerated | project |
NewCredentialNames | project |
OldCredentialNames | project |
OperationName | project |
Device Registration from Malicious IP
#This rule identifies successful Okta device registration events that originate from IP addresses also observed by Okta ThreatInsight as malicious or suspicious.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- Okta Security Threat Detected (Sigma)
- Okta ThreatInsight Login Failure With High Unknown Users (YARA-L)
- Okta ThreatInsight Security Threat Detected (Panther)
- Okta ThreatInsight Suspected Bruteforce Attack (YARA-L)
- Okta ThreatInsight Suspected Password Spray Attack (YARA-L)
- Okta ThreatInsight Targeted Bruteforce Attack (YARA-L)
- Okta ThreatInsight Threat Detected (Splunk)
- Okta ThreatInsight Threat Suspected Promotion (Elastic)
Rule Body
id: e36c6bd6-f86a-4282-93a5-b4a1b48dd849
name: Device Registration from Malicious IP
description: |
This rule identifies successful Okta device registration events that originate from IP addresses also observed by Okta ThreatInsight as malicious or suspicious.
severity: High
status: Available
requiredDataConnectors:
- connectorId: OktaSSO
dataTypes:
- Okta_CL
- connectorId: OktaSSOv2
dataTypes:
- OktaSSO
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
relevantTechniques:
- T1098.005
query: |
let Events = dynamic(["device.enrollment.create"]);
let ThreatInsightOperations = dynamic(["security.threat.detected", "security.attack.start", "security.attack.end" ]);
// Adjust list of allowed IPs to reduce false positives
let AllowedIPs = dynamic([]);
let DeviceRegistrations = OktaSSO
| where eventType_s in (Events)
| where outcome_result_s == "SUCCESS"
| where isnotempty(client_ipAddress_s) and client_ipAddress_s !in (AllowedIPs)
| extend oktaDeviceId_ = tostring(parse_json(tostring(parse_json(target_s)[0].detailEntry)).oktaDeviceId), NewDevice_osPlatform = tostring(parse_json(tostring(parse_json(target_s)[0].detailEntry)).osPlatform), NewDevice_osVersion = tostring(parse_json(tostring(parse_json(target_s)[0].detailEntry)).osVersion), displayName_ = tostring(parse_json(target_s)[0].displayName)
| extend Location = strcat(client_geographicalContext_city_s, " | ", client_geographicalContext_state_s," | ", client_geographicalContext_country_s)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by actor_alternateId_s, actor_displayName_s, client_userAgent_os_s, client_ipAddress_s, displayMessage_s, outcome_result_s,
outcome_reason_s, column_ifexists('debugContext_debugData_logOnlySecurityData_s', ""), column_ifexists('debugContext_debugData_threatSuspected_s',""), client_userAgent_rawUserAgent_s,client_userAgent_browser_s, severity_s, NewDevice_osPlatform, NewDevice_osVersion, eventType_s, Location ;
let ThreatInsightEvents = OktaSSO
| where eventType_s in (ThreatInsightOperations)
| extend SuspiciousIP = actor_displayName_s
| project TimeGenerated, column_ifexists('debugContext_debugData_threatDetections_s', ""), client_userAgent_rawUserAgent_s, severity_s, outcome_result_s, eventType_s, displayMessage_s, SuspiciousIP, transaction_id_s;
DeviceRegistrations
| join kind=inner (ThreatInsightEvents) on $left.client_ipAddress_s == $right.SuspiciousIP
| project TimeGenerated, actor_alternateId_s, actor_displayName_s, client_ipAddress_s, client_userAgent_os_s, client_userAgent_rawUserAgent_s, client_userAgent_browser_s, displayMessage_s, outcome_result_s, outcome_reason_s, severity_s, NewDevice_osPlatform, NewDevice_osVersion, eventType_s, Location, SuspiciousIP, transaction_id_s
| extend AccountName = tostring(split(actor_alternateId_s, "@")[0]), AccountUPNSuffix = tostring(split(actor_alternateId_s, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: client_ipAddress_s
customDetails:
ActorDisplayName: actor_displayName_s
ActorDeviceOS: NewDevice_osPlatform
ActorDeviceOSVersion: NewDevice_osVersion
UserAgent: client_userAgent_rawUserAgent_s
Location: Location
alertDetailsOverride:
alertDisplayNameFormat: 'Device registration from Malicious IP {{client_ipAddress_s}}'
alertDescriptionFormat: 'User {{actor_displayName_s}} ({{actor_alternateId_s}}) registered a device from {{client_ipAddress_s}} and the IP is associated with malicious activity in Okta ThreatInsight.'
version: 1.1.2
kind: Scheduled
Stages and Predicates
Parameters
let Events = dynamic(["device.enrollment.create"]);
let ThreatInsightOperations = dynamic(["security.threat.detected", "security.attack.start", "security.attack.end" ]);
let AllowedIPs = dynamic([]);
let DeviceRegistrations is inlined into the numbered stages below.
Let Binding: ThreatInsightEvents
let ThreatInsightEvents = OktaSSO
| where eventType_s in (ThreatInsightOperations)
| extend SuspiciousIP = actor_displayName_s
| project TimeGenerated, column_ifexists('debugContext_debugData_threatDetections_s', ""), client_userAgent_rawUserAgent_s, severity_s, outcome_result_s, eventType_s, displayMessage_s, SuspiciousIP, transaction_id_s;
Stages 1 to 7 define let DeviceRegistrations as the rule's main pipeline source. Stages 8 to 10 use it.
Stage 1: source
OktaSSO
Stage 2: where
| where eventType_s in (Events)
Stage 3: where
| where outcome_result_s == "SUCCESS"
Stage 4: where
| where isnotempty(client_ipAddress_s) and client_ipAddress_s !in (AllowedIPs)
Stage 5: extend
| extend oktaDeviceId_ = tostring(parse_json(tostring(parse_json(target_s)[0].detailEntry)).oktaDeviceId), NewDevice_osPlatform = tostring(parse_json(tostring(parse_json(target_s)[0].detailEntry)).osPlatform), NewDevice_osVersion = tostring(parse_json(tostring(parse_json(target_s)[0].detailEntry)).osVersion), displayName_ = tostring(parse_json(target_s)[0].displayName)
Stage 6: extend
| extend Location = strcat(client_geographicalContext_city_s, " | ", client_geographicalContext_state_s," | ", client_geographicalContext_country_s)
Stage 7: summarize
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by actor_alternateId_s, actor_displayName_s, client_userAgent_os_s, client_ipAddress_s, displayMessage_s, outcome_result_s,
outcome_reason_s, column_ifexists('debugContext_debugData_logOnlySecurityData_s', ""), column_ifexists('debugContext_debugData_threatSuspected_s',""), client_userAgent_rawUserAgent_s,client_userAgent_browser_s, severity_s, NewDevice_osPlatform, NewDevice_osVersion, eventType_s, Location
Stage 8: join
DeviceRegistrations
| join kind=inner (ThreatInsightEvents) on $left.client_ipAddress_s == $right.SuspiciousIP
Stage 9: project
project Location, NewDevice_osPlatform, NewDevice_osVersion, SuspiciousIP, TimeGenerated, actor_alternateId_s, actor_displayName_s, client_ipAddress_s, client_userAgent_browser_s, client_userAgent_os_s, client_userAgent_rawUserAgent_s, displayMessage_s, eventType_s, outcome_reason_s, outcome_result_s, severity_s, transaction_id_s
Stage 10: extend
extend AccountName, AccountUPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
client_ipAddress_s | is_not_null | field:"client_ipAddress_s" kind:is_not_null | |
eventType_s | in |
| field:"okta::eventType" kind:in |
outcome_result_s | eq |
| field:"okta::outcome.result" kind:eq value:"SUCCESS" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
client_ipAddress_s | eq | [] | excludes:client_ipAddress_s field:"client_ipAddress_s" value:"[]" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Location | project |
NewDevice_osPlatform | project |
NewDevice_osVersion | project |
SuspiciousIP | project |
TimeGenerated | project |
actor_alternateId_s | project |
actor_displayName_s | project |
client_ipAddress_s | project |
client_userAgent_browser_s | project |
client_userAgent_os_s | project |
client_userAgent_rawUserAgent_s | project |
displayMessage_s | project |
eventType_s | project |
outcome_reason_s | project |
outcome_result_s | project |
severity_s | project |
transaction_id_s | project |
AccountName | extend |
AccountUPNSuffix | extend |
Forescout-DNS_Sniff_Event_Monitor
#This rule creates an incident when more than certain number of Dnsniff events are generated from a host
Rule Body
id: d272e277-f285-4dbc-ae2d-7f65ba64a79e
name: Forescout-DNS_Sniff_Event_Monitor
description: 'This rule creates an incident when more than certain number of Dnsniff events are generated from a host'
severity: Medium
requiredDataConnectors: []
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query:
ForescoutHostProperties_CL
| extend d = parse_json(HostProperties)
| where d.DnsniffEvent matches regex "DNS Query Type:.A;DNS Query/Response:.Query"
| extend ipaddress = iif(isnotempty(Ipv4Addr), Ipv4Addr, (iif(isnotempty(Ipv6Addr), Ipv6Addr, "")))
| where isnotempty(ipaddress) and isnotempty(EmIpAddr)
| summarize NumEvents_d =count() by ipaddress, EmIpAddr
| where NumEvents_d > 2
| sort by NumEvents_d asc
| project NumEvents_d, ipaddress, EmIpAddr
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ipaddress
customDetails:
Ip: ipaddress
NumEvents: NumEvents_d
EmIp: EmIpAddr
alertDetailsOverride:
alertDisplayNameFormat: Dnsniff-Address-Check
alertDescriptionFormat: Dnsniff-Address-Check alert
version: 2.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
ForescoutHostProperties_CL
Stage 2: extend
| extend d = parse_json(HostProperties)
Stage 3: where
| where d.DnsniffEvent matches regex "DNS Query Type:.A;DNS Query/Response:.Query"
Stage 4: extend
| extend ipaddress = iif(isnotempty(Ipv4Addr), Ipv4Addr, (iif(isnotempty(Ipv6Addr), Ipv6Addr, "")))
Stage 5: where
| where isnotempty(ipaddress) and isnotempty(EmIpAddr)
Stage 6: summarize
| summarize NumEvents_d =count() by ipaddress, EmIpAddr
Stage 7: where
| where NumEvents_d > 2
Stage 8: sort
| sort by NumEvents_d asc
Stage 9: project
| project NumEvents_d, ipaddress, EmIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EmIpAddr | is_not_null | field:"EmIpAddr" kind:is_not_null | |
NumEvents_d | gt |
| field:"NumEvents_d" kind:gt value:"2" |
d.DnsniffEvent | regex_match |
| field:"d.DnsniffEvent" kind:regex_match value:"DNS Query Type:.A;DNS Query/Response:.Query" |
ipaddress | is_not_null | field:"ipaddress" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
EmIpAddr | project |
NumEvents_d | project |
ipaddress | project |
Google DNS - Malicous Python packages
#'Detects requests to resources with malicious Python packages.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 75491db8-eaf7-40bb-a46a-279872cc82f5
name: Google DNS - Malicous Python packages
description: |
'Detects requests to resources with malicious Python packages.'
severity: High
requiredDataConnectors:
- connectorId: GCPDNSDataConnector
dataTypes:
- GCPCloudDNS
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1195
query: |
GCPCloudDNS
| where Query has_any ('psec.forward.io.global.prod.fastly.net', 'b0a0374cd1cb4305002e.d.requestbin.net', 'tornadodomain.000webhostapp.com', 'yxznlysc47wvrb9r9z211e1jbah15q')
| extend DNSCustomEntity = Query, IPCustomEntity = SrcIpAddr
entityMappings:
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: DNSCustomEntity
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
GCPCloudDNS
Stage 2: where
| where Query has_any ('psec.forward.io.global.prod.fastly.net', 'b0a0374cd1cb4305002e.d.requestbin.net', 'tornadodomain.000webhostapp.com', 'yxznlysc47wvrb9r9z211e1jbah15q')
Stage 3: extend
| extend DNSCustomEntity = Query, IPCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Query | match |
| field:"Query" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DNSCustomEntity | extend |
IPCustomEntity | extend |
Solorigate Named Pipe
#Identifies a match across various data feeds for named pipe IOCs related to the Solorigate incident. For the sysmon events required for this detection, logging for Named Pipe Events needs to be configured in Sysmon config (Event ID 17 and Event ID 18) Reference: https://techcommunity.microsoft.com/t5/azure-sentinel/solarwinds-post-compromise-hunting-with-azure-sentinel/ba-p/1995095
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation | |
| Stealth |
Telemetry Coverage
Rule Body
id: 11b4c19d-2a79-4da3-af38-b067e1273dee
name: Solorigate Named Pipe
description: |
'Identifies a match across various data feeds for named pipe IOCs related to the Solorigate incident.
For the sysmon events required for this detection, logging for Named Pipe Events needs to be configured in Sysmon config (Event ID 17 and Event ID 18)
Reference: https://techcommunity.microsoft.com/t5/azure-sentinel/solarwinds-post-compromise-hunting-with-azure-sentinel/ba-p/1995095'
severity: High
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsForwardedEvents
dataTypes:
- WindowsEvent
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
- PrivilegeEscalation
relevantTechniques:
- T1055
tags:
- Solorigate
- NOBELIUM
query: |
(union isfuzzy=true
(Event
| where Source == "Microsoft-Windows-Sysmon"
| where EventID in (17,18)
| where EventData has '583da945-62af-10e8-4902-a8f205c72b2e'
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, MG, ManagementGroupName, _ResourceId)
| extend PipeName = column_ifexists("PipeName", "")
| extend Account = User
| extend AccountName = tostring(split(User, @"\")[1]), AccountNTDomain = tostring(split(User, @"\")[0])
),
(
SecurityEvent
| where EventID == '5145'
// %%4418 looks for presence of CreatePipeInstance value
| where AccessList has '%%4418'
| where RelativeTargetName has '583da945-62af-10e8-4902-a8f205c72b2e'
| extend AccountName = SubjectUserName, AccountNTDomain = SubjectDomainName
),
(
WindowsEvent
| where EventID == '5145' and EventData has '%%4418' and EventData has '583da945-62af-10e8-4902-a8f205c72b2e'
// %%4418 looks for presence of CreatePipeInstance value
| extend AccessList= tostring(EventData.AccessList)
| where AccessList has '%%4418'
| extend RelativeTargetName= tostring(EventData.RelativeTargetName)
| where RelativeTargetName has '583da945-62af-10e8-4902-a8f205c72b2e'
| extend Account = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend AccountName = tostring(EventData.SubjectUserName), AccountNTDomain = tostring(EventData.SubjectDomainName)
)
)
| 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: Account
fieldMappings:
- identifier: FullName
columnName: Account
- identifier: Name
columnName: AccountName
- identifier: NTDomain
columnName: AccountNTDomain
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: NTDomain
columnName: HostNameDomain
version: 1.2.5
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - 0-day Vulnerability" ]
Stages and Predicates
Union isfuzzy=true (3 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: Event, SecurityEvent, WindowsEvent
Leg 1: Event
Event
| where Source == "Microsoft-Windows-Sysmon"
| where EventID in (17,18)
| where EventData has '583da945-62af-10e8-4902-a8f205c72b2e'
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, MG, ManagementGroupName, _ResourceId)
| extend PipeName = column_ifexists("PipeName", "")
| extend Account = User
| extend AccountName = tostring(split(User, @"\")[1]), AccountNTDomain = tostring(split(User, @"\")[0])
Leg 2: SecurityEvent
SecurityEvent
| where EventID == '5145'
| where AccessList has '%%4418'
| where RelativeTargetName has '583da945-62af-10e8-4902-a8f205c72b2e'
| extend AccountName = SubjectUserName, AccountNTDomain = SubjectDomainName
Leg 3: WindowsEvent
WindowsEvent
| where EventID == '5145' and EventData has '%%4418' and EventData has '583da945-62af-10e8-4902-a8f205c72b2e'
| extend AccessList= tostring(EventData.AccessList)
| where AccessList has '%%4418'
| extend RelativeTargetName= tostring(EventData.RelativeTargetName)
| where RelativeTargetName has '583da945-62af-10e8-4902-a8f205c72b2e'
| extend Account = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend AccountName = tostring(EventData.SubjectUserName), AccountNTDomain = tostring(EventData.SubjectDomainName)
Applied to the Combined Result
| 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 |
|---|---|---|---|
AccessList | match |
| field:"AccessList" kind:match value:"%%4418" |
EventData | match |
| field:"EventData" kind:match |
EventID | eq |
| field:"EventID" kind:eq value:"5145" |
EventID | in |
| field:"EventID" kind:in |
RelativeTargetName | match |
| field:"RelativeTargetName" kind:match value:"583da945-62af-10e8-4902-a8f205c72b2e" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
EventData | extend |
Key | extend |
Value | extend |
PipeName | extend |
Account | extend |
AccountNTDomain | extend |
AccountName | extend |
AccessList | extend |
RelativeTargetName | extend |
HostName | extend |
HostNameDomain | extend |
Ubiquiti - Unknown MAC Joined AP
#'Detects when device with unseen MAC Address joined AP.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 9757cee3-1a6c-4d8e-a968-3b7e48ded690
name: Ubiquiti - Unknown MAC Joined AP
description: |
'Detects when device with unseen MAC Address joined AP.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CustomLogsAma
dataTypes:
- Ubiquiti_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1133
query: |
let lbperiod = 14d;
let lbperiod_24h = 24h;
let known_macs = UbiquitiAuditEvent
| where TimeGenerated between (ago(lbperiod) .. ago(lbperiod_24h))
| where DvcAction =~ 'JOIN'
| summarize makeset(SrcMacAddr);
UbiquitiAuditEvent
| where DvcAction =~ 'JOIN'
| where SrcMacAddr !in (known_macs)
| extend Device = SrcMacAddr
| extend HostCustomEntity = Device
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: HostCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let lbperiod = 14d;
let lbperiod_24h = 24h;
Let Binding: known_macs
let known_macs = UbiquitiAuditEvent
| where TimeGenerated between (ago(lbperiod) .. ago(lbperiod_24h))
| where DvcAction =~ 'JOIN'
| summarize makeset(SrcMacAddr);
Stage 1: source
UbiquitiAuditEvent
Stage 2: where
| where DvcAction =~ 'JOIN'
Stage 3: where
| where SrcMacAddr !in (known_macs)
Stage 4: extend
| extend Device = SrcMacAddr
Stage 5: extend
| extend HostCustomEntity = Device
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DvcAction | eq |
| field:"DvcAction" kind:eq value:"JOIN" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
SrcMacAddr | eq | known_macs | excludes:SrcMacAddr field:"SrcMacAddr" value:"known_macs" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Device | extend |
HostCustomEntity | extend |
Varonis - Low severity alerts detected
#'Identifies low-severity alerts generated by Varonis that indicate lower-risk activity supporting ongoing analysis, threat correlation, and security monitoring.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Collection | |
| Exfiltration |
Rule Body
id: 9b1f3e7a-6c42-4d18-a5e9-2b7f8c0d4e61
name: Varonis - Low severity alerts detected
description: |
'Identifies low-severity alerts generated by Varonis that indicate lower-risk activity supporting ongoing analysis, threat correlation, and security monitoring.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: VaronisSaaSAlertsPush
dataTypes:
- VaronisAlertsV2_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Collection
- Exfiltration
relevantTechniques:
- T1530
- T1213
- T1567
query: |
VaronisAlertsV2_CL
| where AlertSeverity_s == "Low"
eventGroupingSettings:
aggregationKind: AlertPerResult
alertDetailsOverride:
alertDisplayNameFormat: 'Varonis alert: {{ThreatDetectionPolicyName_s}} ({{AlertSeverity_s}})'
alertDescriptionFormat: 'Varonis detected activity for policy {{ThreatDetectionPolicyName_s}} in category {{AlertCategory_s}}. Users: {{UserNames_s}}.'
customDetails:
VaronisAlertGuid: AlertId_g
AlertCategory: AlertCategory_s
MitreTactic: MitreTacticName_s
Status: Status_s
EventsCount: EventsCount_d
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserNames_s
- identifier: Name
columnName: UserSamAccountNames_s
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: DeviceNames_s
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
kind: Scheduled
version: 1.0.1
Stages and Predicates
Stage 1: source
VaronisAlertsV2_CL
Stage 2: where
| where AlertSeverity_s == "Low"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AlertSeverity_s | eq |
| field:"AlertSeverity_s" kind:eq value:"Low" |