Detection rules › Kusto

Kusto rules: 2l

Abnormal Security - High-risk email attack detected

#
Status
available
Severity
high
Time window
1h
Source
github.com/Azure/Azure-Sentinel

'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

TacticTechniques
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.

FieldKindValuesSearch
abx_body_abx_body_attack_type_sin
  • Extortion
  • Internal-to-Internal Attacks (Email Account Takeover)
  • Invoice/Payment Fraud (BEC)
  • Malware
  • Phishing: Credential
  • Phishing: Sensitive Data
  • Scam
  • Social Engineering (BEC)
field:"abx_body_abx_body_attack_type_s" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
AttackScoreproject
AttackTypeproject
MessageIdproject
PortalUrlproject
RecipientEmailproject
RemediationStatusproject
SenderEmailproject
SenderIPproject
SenderNameproject
Subjectproject
TimeGeneratedproject

AD FS Remote HTTP Network Connection

#
Status
available
Severity
medium
Time window
1d
Group by
Computer
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
Collection

Telemetry Coverage

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 used in Stages 1, 4

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.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DestinationIpin0:0:0:0:0:0:0:1, ::1excludes: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.

FieldSource
RenderedDescriptionextend
EventDataextend
Keyextend
Valueextend
RuleNameextend
TechniqueIdextend
TechniqueNameextend
DestinationIpextend
DestinationPortextend
Imageextend
Initiatedextend
SourceIpextend
processextend
Operationextend
DomainIndexextend
HostNameextend
HostNameDomainextend
AccountNTDomainextend
AccountNameextend

AV detections related to Ukraine threats

#
Status
available
Severity
high
Time window
1d
Source
github.com/Azure/Azure-Sentinel

'This query looks for Microsoft Defender AV detections for malware observed in relation to the war in Ukraine. Ref: https://msrc-blog.microsoft.com/2022/02/28/analysis-resources-cyber-threat-activity-ukraine/ '

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: b6685757-3ed1-4b05-a5bd-2cacadc86c2a
name: AV detections related to Ukraine threats
description: |
  'This query looks for Microsoft Defender AV detections for malware observed in relation to the war in Ukraine.
    Ref: https://msrc-blog.microsoft.com/2022/02/28/analysis-resources-cyber-threat-activity-ukraine/ '
severity: High
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - SecurityAlert
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1485
query: |
  let UA_threats = dynamic(["FoxBlade", "WhisperGate", "Lasainraw", "SonicVote", "CaddyWiper", "AprilAxe", "FiberLake", "Industroyer", "DesertBlade"]);
  SecurityAlert
  | where ProviderName =~ "MDATP"
  | extend ThreatFamilyName = tostring(parse_json(ExtendedProperties).ThreatFamilyName)
  | where ThreatFamilyName in~ (UA_threats)
  | extend HostName = tostring(split(CompromisedEntity, ".")[0]), DomainIndex = toint(indexof(CompromisedEntity, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(CompromisedEntity, DomainIndex + 1), CompromisedEntity)
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName 
        columnName: CompromisedEntity
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
version: 1.1.3
kind: Scheduled

Stages and Predicates

Parameters

let UA_threats = dynamic(["FoxBlade", "WhisperGate", "Lasainraw", "SonicVote", "CaddyWiper", "AprilAxe", "FiberLake", "Industroyer", "DesertBlade"]);

Stage 1: source

SecurityAlert

Stage 2: where

| where ProviderName =~ "MDATP"

Stage 3: extend

| extend ThreatFamilyName = tostring(parse_json(ExtendedProperties).ThreatFamilyName)

Stage 4: where

| where ThreatFamilyName in~ (UA_threats)

Stage 5: extend

| extend HostName = tostring(split(CompromisedEntity, ".")[0]), DomainIndex = toint(indexof(CompromisedEntity, '.'))

Stage 6: extend

| extend HostNameDomain = iff(DomainIndex != -1, substring(CompromisedEntity, DomainIndex + 1), CompromisedEntity)
HostNameDomain =
ifDomainIndex != -1substring(CompromisedEntity, (DomainIndex + 1))
elseCompromisedEntity

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ProviderNameeq
  • MDATP
field:"ProviderName" kind:eq value:"MDATP"
ThreatFamilyNamein
  • AprilAxe
  • CaddyWiper
  • DesertBlade
  • FiberLake
  • FoxBlade
  • Industroyer
  • Lasainraw
  • SonicVote
  • WhisperGate
field:"ThreatFamilyName" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
ThreatFamilyNameextend
DomainIndexextend
HostNameextend
HostNameDomainextend

AWSCloudTrail - Privilege escalation with admin managed policy

#
Status
available
Severity
medium
Time window
1d
Source
github.com/Azure/Azure-Sentinel

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

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

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.

FieldKindValuesSearch
ErrorCodeis_null
  • (no value, null check)
field:"aws::errorCode" kind:is_null
ErrorMessageis_null
  • (no value, null check)
field:"aws::errorMessage" kind:is_null
EventNamein
  • AttachGroupPolicy corpus 17 (kusto 15, panther 2)
  • AttachRolePolicy corpus 17 (kusto 15, panther 2)
  • AttachUserPolicy corpus 17 (kusto 15, panther 2)
field:"aws::eventName" kind:in
RequestParameters.policyArncontains
  • Admin transforms: tostring, kusto_accessor_expr:{"expression":"tostring(parse_json(RequestParameters).policyArn)","field":"RequestParameters.policyArn"}, kusto_dynamic_path
field:"RequestParameters.policyArn" kind:contains value:"Admin"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
RequestParameters.policyArncontainsFullAccessexcludes:RequestParameters.policyArn field:"RequestParameters.policyArn" value:"FullAccess"
RequestParameters.policyArnstarts_witharn:aws:iam::aws:policy/AdministratorAccessexcludes:RequestParameters.policyArn field:"RequestParameters.policyArn" value:"arn:aws:iam::aws:policy/AdministratorAccess"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AWSRegionproject
AccountNameproject
AccountUPNSuffixproject
AdditionalEventDataproject
EventNameproject
EventSourceproject
EventTypeNameproject
RecipientAccountIdproject
RequestParametersproject
ResponseElementsproject
SessionMfaAuthenticatedproject
SourceIpAddressproject
TimeGeneratedproject
UserAgentproject
UserIdentityAccountIdproject
UserIdentityArnproject
UserIdentityPrincipalidproject
UserIdentityUserNameproject

Azure DevOps Variable Secret Not Secured

#
Status
available
Severity
medium
Time window
1d
Source
github.com/Azure/Azure-Sentinel

'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

TacticTechniques
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.

FieldKindValuesSearch
Data_Variablesmatch
  • PAT transforms: term
  • cred transforms: term
  • credential transforms: term
  • credentials transforms: term
  • creds transforms: term
  • key transforms: term
  • passwd transforms: term
  • password transforms: term
  • pswd transforms: term
  • pwd transforms: term
  • secret transforms: term
  • secrets transforms: term
field:"Data_Variables" kind:match
OperationNameeq
  • Library.VariableGroupModified
field:"OperationName" kind:eq value:"Library.VariableGroupModified"
Typene
  • AzureKeyVault
field:"Type" kind:ne value:"AzureKeyVault"
VariableGroupNamematch
  • PAT transforms: term
  • cred transforms: term
  • credential transforms: term
  • credentials transforms: term
  • creds transforms: term
  • key transforms: term
  • passwd transforms: term
  • password transforms: term
  • pswd transforms: term
  • pwd transforms: term
  • secret transforms: term
  • secrets transforms: term
field:"VariableGroupName" kind:match

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Data_VariablesmatchIsSecretexcludes:Data_Variables field:"Data_Variables" value:"IsSecret"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Typeextend
VariableGroupIdextend
VariableGroupNameextend
timestampextend
AccountNameextend
AccountUPNSuffixextend

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.

Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

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.

FieldKindValuesSearch
Findingeq
  • T0WriteAccountRestrictions
field:"Finding" kind:eq value:"T0WriteAccountRestrictions"

Box - User logged in as admin

#
Status
available
Severity
medium
Time window
14d
Source
github.com/Azure/Azure-Sentinel

'Detects when user logged in as admin.'

MITRE ATT&CK Coverage

TacticTechniques
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 used in Stage 3

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.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
SourceLogineqadminsexcludes:SourceLogin field:"SourceLogin" value:"admins"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend
IPCustomEntityextend

Box - User role changed to owner

#
Status
available
Severity
medium
Time window
14d
Group by
AccessibleByName, AdditionalDetailsRole, FileDirectory
Source
github.com/Azure/Azure-Sentinel

'Detects when user collaboration role is changed to owner.'

MITRE ATT&CK Coverage

TacticTechniques
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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccessibleByNameproject
FileDirectoryproject
AccountCustomEntityextend

Detect credential add to Connect Sync Application

#
Group by
AppName
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

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

References

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

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 used in Stage 13

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.

FieldKindValuesSearch
AppNamestarts_with
  • ConnectSyncProvisioning_
field:"AppName" kind:starts_with value:"ConnectSyncProvisioning_"
ModifiedProperties.displayNameeq
  • KeyDescription transforms: kusto_dynamic_path
field:"ModifiedProperties.displayName" kind:eq value:"KeyDescription"
OperationNamematch
  • Add service principal transforms: term corpus 4 (kusto 4)
  • Certificates and secrets management transforms: term corpus 5 (kusto 5)
  • Update application transforms: term corpus 3 (kusto 3)
field:"OperationName" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
AdditionalDetailsproject
AppNameproject
CredRemoveTimeGeneratedproject
InitiatedByproject
ModifiedPropertiesproject
NewCredAddTimeGeneratedproject
NewCredentialNamesproject
OldCredentialNamesproject
OperationNameproject

Device Registration from Malicious IP

#
Status
available
Severity
high
Time window
1h
Group by
$left.client_ipAddress_s, $right.SuspiciousIP, Location, NewDevice_osPlatform, NewDevice_osVersion, 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
Source
github.com/Azure/Azure-Sentinel

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

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

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 used in Stage 8

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.

FieldKindValuesSearch
client_ipAddress_sis_not_null
  • (no value, null check)
field:"client_ipAddress_s" kind:is_not_null
eventType_sin
  • Events
  • security.attack.end
  • security.attack.start
  • security.threat.detected
field:"okta::eventType" kind:in
outcome_result_seq
  • SUCCESS corpus 7 (elastic 4, kusto 3)
field:"okta::outcome.result" kind:eq value:"SUCCESS"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
client_ipAddress_seq[]excludes:client_ipAddress_s field:"client_ipAddress_s" value:"[]"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Locationproject
NewDevice_osPlatformproject
NewDevice_osVersionproject
SuspiciousIPproject
TimeGeneratedproject
actor_alternateId_sproject
actor_displayName_sproject
client_ipAddress_sproject
client_userAgent_browser_sproject
client_userAgent_os_sproject
client_userAgent_rawUserAgent_sproject
displayMessage_sproject
eventType_sproject
outcome_reason_sproject
outcome_result_sproject
severity_sproject
transaction_id_sproject
AccountNameextend
AccountUPNSuffixextend

Forescout-DNS_Sniff_Event_Monitor

#
Severity
medium
Time window
5m
Group by
EmIpAddr, ipaddress
Source
github.com/Azure/Azure-Sentinel

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.

FieldKindValuesSearch
EmIpAddris_not_null
  • (no value, null check)
field:"EmIpAddr" kind:is_not_null
NumEvents_dgt
  • 2 transforms: number
field:"NumEvents_d" kind:gt value:"2"
d.DnsniffEventregex_match
  • DNS Query Type:.A;DNS Query/Response:.Query transforms: kusto_dynamic_path
field:"d.DnsniffEvent" kind:regex_match value:"DNS Query Type:.A;DNS Query/Response:.Query"
ipaddressis_not_null
  • (no value, null check)
field:"ipaddress" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
EmIpAddrproject
NumEvents_dproject
ipaddressproject

Google DNS - Malicous Python packages

#
Severity
high
Time window
15m
Source
github.com/Azure/Azure-Sentinel

'Detects requests to resources with malicious Python packages.'

MITRE ATT&CK Coverage

TacticTechniques
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.

FieldKindValuesSearch
Querymatch
  • b0a0374cd1cb4305002e.d.requestbin.net transforms: term
  • psec.forward.io.global.prod.fastly.net transforms: term
  • tornadodomain.000webhostapp.com transforms: term
  • yxznlysc47wvrb9r9z211e1jbah15q transforms: term
field:"Query" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
DNSCustomEntityextend
IPCustomEntityextend

Solorigate Named Pipe

#
Severity
high
Time window
1d
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
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.

FieldKindValuesSearch
AccessListmatch
  • %%4418 transforms: term
field:"AccessList" kind:match value:"%%4418"
EventDatamatch
  • %%4418 transforms: term
  • 583da945-62af-10e8-4902-a8f205c72b2e transforms: term
field:"EventData" kind:match
EventIDeq
  • 5145 corpus 4 (elastic 3, kusto 1)
field:"EventID" kind:eq value:"5145"
EventIDin
  • 17
  • 18
field:"EventID" kind:in
RelativeTargetNamematch
  • 583da945-62af-10e8-4902-a8f205c72b2e transforms: term
field:"RelativeTargetName" kind:match value:"583da945-62af-10e8-4902-a8f205c72b2e"

Output Fields

These fields are emitted when the rule matches.

FieldSource
EventDataextend
Keyextend
Valueextend
PipeNameextend
Accountextend
AccountNTDomainextend
AccountNameextend
AccessListextend
RelativeTargetNameextend
HostNameextend
HostNameDomainextend

Ubiquiti - Unknown MAC Joined AP

#
Status
available
Severity
medium
Time window
14d
Source
github.com/Azure/Azure-Sentinel

'Detects when device with unseen MAC Address joined AP.'

MITRE ATT&CK Coverage

TacticTechniques
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 used in Stage 3

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.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
SrcMacAddreqknown_macsexcludes:SrcMacAddr field:"SrcMacAddr" value:"known_macs"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Deviceextend
HostCustomEntityextend

Varonis - Low severity alerts detected

#
Status
available
Severity
low
Time window
5m
Source
github.com/Azure/Azure-Sentinel

'Identifies low-severity alerts generated by Varonis that indicate lower-risk activity supporting ongoing analysis, threat correlation, and security monitoring.'

MITRE ATT&CK Coverage

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.

Back to all Kusto rules