Detection rules › Kusto

Kusto rules: 17

AWSCloudTrail - Privilege escalation via EC2 policy

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

Detects inline IAM policy updates that grant broad EC2 permissions or EC2 actions paired with IAM pass-role capability. This policy pattern can enable launch and control of compute resources as a privilege escalation path.

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: a2b2a984-c820-4d93-830e-139bffd81fa3
name: AWSCloudTrail - Privilege escalation via EC2 policy
description: |
  Detects inline IAM policy updates that grant broad EC2 permissions or EC2 actions paired with IAM pass-role
  capability. This policy pattern can enable launch and control of compute resources as a privilege escalation path.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1098.003
query: |
    AWSCloudTrail
      | where EventName in ("PutUserPolicy","PutRolePolicy","PutGroupPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
      | extend PolicyName = tostring(parse_json(RequestParameters).policyName)
      | extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement
      | mvexpand Statement
      | extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition)
      | extend Action = tostring(Action)
      | where Effect =~ "Allow" and ((((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "ec2:*") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "ec2:RunInstances") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "ec2:Run*")) or (Action contains "ec2:*") or (Action contains "ec2:ListInstances" and Action contains "ec2:StartInstance" and Action contains "ec2:ModifyInstanceAttribute") or (Action contains "ec2:List*" and Action contains "ec2:Start*" and Action contains "ec2:Modify*")) and Resource == "*" and Condition == ""
      | extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
      | extend UserName = tostring(split(UserIdentityArn, '/')[-1])
      | extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
      | extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
        AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
      | distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, UserIdentityArn, UserIdentityUserName, RecipientAccountId, AccountName, AccountUPNSuffix
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
  PolicyName: PolicyName
  EventName: EventName
  UserIdentityArn: UserIdentityArn
  RecipientAccountId: RecipientAccountId
alertDetailsOverride:
  alertDisplayNameFormat: 'AWS EC2 privilege escalation policy update by {{AccountName}}'
  alertDescriptionFormat: 'Detected {{EventName}} Event, updating inline EC2 escalation policy {{PolicyName}} in account {{RecipientAccountId}}.'
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

AWSCloudTrail

Stage 2: where

| where EventName in ("PutUserPolicy","PutRolePolicy","PutGroupPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)

Stage 3: extend

| extend PolicyName = tostring(parse_json(RequestParameters).policyName)

Stage 4: extend

| extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement

Stage 5: mv-expand

| mvexpand Statement

Stage 6: extend

| extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition)

Stage 7: extend

| extend Action = tostring(Action)

Stage 8: where

| where Effect =~ "Allow" and ((((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "ec2:*") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "ec2:RunInstances") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "ec2:Run*")) or (Action contains "ec2:*") or (Action contains "ec2:ListInstances" and Action contains "ec2:StartInstance" and Action contains "ec2:ModifyInstanceAttribute") or (Action contains "ec2:List*" and Action contains "ec2:Start*" and Action contains "ec2:Modify*")) and Resource == "*" and Condition == ""

Stage 9: extend (4 consecutive steps)

| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
| extend UserName = tostring(split(UserIdentityArn, '/')[-1])
| extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
| extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
    AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")

Stage 10: distinct

| distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, UserIdentityArn, UserIdentityUserName, RecipientAccountId, AccountName, AccountUPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actioncontains
  • ec2:*
  • ec2:List*
  • ec2:ListInstances
  • ec2:Modify*
  • ec2:ModifyInstanceAttribute
  • ec2:Run*
  • ec2:RunInstances
  • ec2:Start*
  • ec2:StartInstance
  • iam:*
  • iam:PassRole
field:"Action" kind:contains
Effecteq
  • Allow
field:"Effect" kind:eq value:"Allow"
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
  • PutGroupPolicy corpus 12 (kusto 11, panther 1)
  • PutRolePolicy corpus 12 (kusto 11, panther 1)
  • PutUserPolicy corpus 12 (kusto 11, panther 1)
field:"aws::eventName" kind:in
Resourceeq
  • *
field:"Resource" kind:eq value:"*"

Output Fields

These fields are emitted when the rule matches.

FieldSource
PolicyNameextend
Statementextend
Actionextend
Conditionextend
Effectextend
Resourceextend
UserIdentityArnextend
UserNameextend
AccountNameextend
AccountUPNSuffixextend

BloodHound Attack Path Finding - ReadLapsPassword Privileges on Tier Zero Objects

#

This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

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

BloodHound Attack Path Finding - ReadLapsPassword Privileges on Tier Zero Objects

Rule Body

id: 1a7721b8-028f-47d2-a1aa-dbb307aefb27
name: BloodHound Attack Path Finding - ReadLapsPassword Privileges on Tier Zero Objects
description: BloodHound Attack Path Finding - ReadLapsPassword Privileges on Tier
  Zero Objects
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BloodHoundEnterprise
    dataTypes:
      - BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: BHEAttackPathsData_CL | where Finding == "T0ReadLAPS"
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: domain_name
customDetails:
  Description: ShortDescription
  Remediation: ShortRemediation
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

BHEAttackPathsData_CL

Stage 2: where

| where Finding == "T0ReadLAPS"

Indicators

These rows show field, operator, and value matches.

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

Cisco SDWAN - Monitor Critical IPs

#
Status
available
Severity
high
Time window
3h
Group by
SourceIP
Source
github.com/Azure/Azure-Sentinel

'This analytic rule will monitor critical IPs in Syslog and Netflow Data.'

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: a62a207e-62be-4a74-acab-4466d5b3854f
name: Cisco SDWAN - Monitor Critical IPs
description: |
  'This analytic rule will monitor critical IPs in Syslog and Netflow Data.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: CiscoSDWAN
    dataTypes:
      - CiscoSyslogUTD
  - connectorId: CiscoSDWAN
    dataTypes:
      - CiscoSDWANNetflow
queryFrequency: 3h
queryPeriod: 3h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  CiscoSyslogUTD
  | union (CiscoSDWANNetflow)
  | where isnotempty(SourceIP) or isnotempty(NetflowFwSrcAddrIpv4)
  | extend SourceIP = coalesce(SourceIP, NetflowFwSrcAddrIpv4)
  | where ipv4_is_in_any_range(SourceIP, "Enter comma-separated IPs")
  | summarize count() by SourceIP
incidentConfiguration:
  createIncident: true
eventGroupingSettings:
    aggregationKind: AlertPerResult
customDetails:
  "critical_ip": "SourceIP"
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

CiscoSyslogUTD

Stage 2: union

union of 1 branches

Stage 3: source

CiscoSDWANNetflow

Stage 4: where

where (isnotempty(NetflowFwSrcAddrIpv4) or isnotempty(SourceIP))

Stage 5: extend

extend SourceIP

Stage 6: where

where /* macro: ipv4_is_in_any_range(SourceIP, "Enter comma-separated IPs") */

Stage 7: summarize

summarize by SourceIP

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
NetflowFwSrcAddrIpv4is_not_null
  • (no value, null check)
field:"NetflowFwSrcAddrIpv4" kind:is_not_null
SourceIPis_not_null
  • (no value, null check)
field:"SourceIP" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
SourceIPsummarize

Cisco SEG - Malicious attachment not blocked

#
Status
available
Severity
high
Time window
10m
Source
github.com/Azure/Azure-Sentinel

'Detects mails with malicious attachments which were not blocked.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 236e872c-31d1-4b45-ac2a-fda3af465c97
name: Cisco SEG - Malicious attachment not blocked
description: |
  'Detects mails with malicious attachments which were not blocked.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  CiscoSEGEvent
  | where NetworkDirection =~ 'Incoming'
  | where SimplifiedDeviceAction =~ 'DELIVERED'
  | where tostring(AdditionalFields) has 'ESAAMPVerdict'
  | extend amp_verdict = extract(@'ESAAMPVerdict":"(NOT_EVALUATED|CLEAN|FA_PENDING|UNKNOWN|SKIPPED|UNSCANNABLE|LOW_RISK|MALICIOUS)"', 1, tostring(AdditionalFields))
  | where amp_verdict =~ 'MALICIOUS'
  | extend AccountCustomEntity = DstUserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.3
kind: Scheduled

Stages and Predicates

Stage 1: source

CiscoSEGEvent

Stage 2: where

| where NetworkDirection =~ 'Incoming'

Stage 3: where

| where SimplifiedDeviceAction =~ 'DELIVERED'

Stage 4: where

| where tostring(AdditionalFields) has 'ESAAMPVerdict'

Stage 5: extend

| extend amp_verdict = extract(@'ESAAMPVerdict":"(NOT_EVALUATED|CLEAN|FA_PENDING|UNKNOWN|SKIPPED|UNSCANNABLE|LOW_RISK|MALICIOUS)"', 1, tostring(AdditionalFields))

Stage 6: where

| where amp_verdict =~ 'MALICIOUS'

Stage 7: extend

| extend AccountCustomEntity = DstUserName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
amp_verdictextend
AccountCustomEntityextend

Cloudflare - Bad client IP

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

'Detects requests from IP with bad reputation index.'

MITRE ATT&CK Coverage

Rule Body

id: a7ce6135-9d55-4f14-b058-adc2e920a4fa
name: Cloudflare - Bad client IP
description: |
  'Detects requests from IP with bad reputation index.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CloudflareDataConnector
    dataTypes:
      - Cloudflare
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  let ip_reputation = dynamic(['unknown', 'badHost', 'greylist', 'securityScanner', 'scan', 'tor']);
  Cloudflare
  | where ClientIPClass in~ (ip_reputation)
  | extend CompleteUrl = strcat(HttpRequestHeaderHost,ClientRequestPath)
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: CompleteUrl
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let ip_reputation = dynamic(['unknown', 'badHost', 'greylist', 'securityScanner', 'scan', 'tor']);

Stage 1: source

Cloudflare

Stage 2: where

| where ClientIPClass in~ (ip_reputation)

Stage 3: extend

| extend CompleteUrl = strcat(HttpRequestHeaderHost,ClientRequestPath)

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ClientIPClassin
  • badHost
  • greylist
  • scan
  • securityScanner
  • tor
  • unknown
field:"ClientIPClass" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
CompleteUrlextend

Copilot - Plugin Tampering (Enable and Disable Within 5 Minutes)

#
Status
available
Severity
medium
Time window
5m
Group by
ActorName, SrcIpAddr
Source
github.com/Azure/Azure-Sentinel

'Detects when a user enables and disables Copilot plugins within a 5-minute window. This behavior often indicates probing for security controls or living-off-Copilot techniques. This rule identifies discovery and defense evasion activities where users rapidly toggle plugin states, potentially testing security boundaries.'

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment
Discovery

Rule Body

id: d4e5f6a7-b8c9-40d1-e2f3-a4b5c6d7e8f9
name: Copilot - Plugin Tampering (Enable and Disable Within 5 Minutes)
description: |
  'Detects when a user enables and disables Copilot plugins within a 5-minute window. This behavior often indicates probing for security controls or living-off-Copilot techniques.
  This rule identifies discovery and defense evasion activities where users rapidly toggle plugin states, potentially testing security boundaries.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftCopilot
    dataTypes:
      - CopilotActivity
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
  - DefenseEvasion
relevantTechniques:
  - T1087
  - T1562
query: |
  CopilotActivity
  | where RecordType in ("EnableCopilotPlugin","DisableCopilotPlugin")
  | summarize
      Actions = make_set(RecordType),
      FirstSeen = min(TimeGenerated),
      LastSeen = max(TimeGenerated)
    by ActorName, SrcIpAddr
  | where Actions has "EnableCopilotPlugin" and Actions has "DisableCopilotPlugin"
  | where LastSeen - FirstSeen < 5m
  | extend AccountCustomEntity = ActorName, IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountCustomEntity
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CopilotActivity

Stage 2: where

| where RecordType in ("EnableCopilotPlugin","DisableCopilotPlugin")

Stage 3: summarize

| summarize
    Actions = make_set(RecordType),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated)
  by ActorName, SrcIpAddr

Stage 4: where

| where Actions has "EnableCopilotPlugin" and Actions has "DisableCopilotPlugin"

Stage 5: where LastSeen - FirstSeen < 5m

| where LastSeen - FirstSeen < 5m

Stage 6: extend

| extend AccountCustomEntity = ActorName, IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actionsmatch
  • DisableCopilotPlugin transforms: term
  • EnableCopilotPlugin transforms: term
field:"Actions" kind:match
RecordTypein
  • DisableCopilotPlugin
  • EnableCopilotPlugin
field:"RecordType" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionssummarize
ActorNamesummarize
FirstSeensummarize
LastSeensummarize
SrcIpAddrsummarize
AccountCustomEntityextend
IPCustomEntityextend

Cyble Vision Alerts Website Defacement Content

#

This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

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

'Triggers when monitored websites show new or suspicious content referencing known defacement patterns. Supports investigation into potential web compromise incidents.'

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: 91a00e4f-3edb-49e9-ba6f-cec87a5bd2f8
name: Cyble Vision Alerts Website Defacement Content
description: |
  'Triggers when monitored websites show new or suspicious content referencing known defacement patterns. Supports investigation into potential web compromise incidents.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CybleVisionAlerts
  dataTypes:
  - CybleVisionAlerts_CL
enabled: true
queryfrequency: 30m
queryPeriod: 30m
triggerOperator: GreaterThan
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics:
- Impact
relevantTechniques:
- T1491
query: |
  Alerts_defacement_content  
  | where Service == "defacement_content" 
  | extend MappedSeverity = Severity
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
  alertDetailsOverride:
  alertDisplayNameFormat: Defacement Activity Detected {{DC_DomainName}}
  alertDescriptionFormat: |
    New or modified content was detected on website {{DC_DomainName}}. URL {{DC_URL}}. This may indicate unauthorized website updates or defacement activity.
customDetails:
  MappedSeverity: Severity
  Status: Status
  AlertID: AlertID
  Service: Service
  DC_URL: DC_URL
  DC_DomainName: DC_DomainName
  DC_Keywords: DC_Keywords
  DC_RecordID: DC_RecordID
entityMappings:
- entityType: Url
  fieldMappings:
  - identifier: Url
    columnName: DC_URL
- entityType: Host
  fieldMappings:
  - identifier: HostName
    columnName: DC_DomainName
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

Alerts_defacement_content

Stage 2: where

| where Service == "defacement_content"

Stage 3: extend

| extend MappedSeverity = Severity

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Serviceeq
  • defacement_content
field:"Service" kind:eq value:"defacement_content"

Output Fields

These fields are emitted when the rule matches.

FieldSource
MappedSeverityextend

Dataverse - Login by a sensitive privileged user

#
Status
available
Severity
high
Time window
14d
Group by
$left.UserPrincipalName, $right.UserId, UserId
Source
github.com/Azure/Azure-Sentinel

Identifies Dataverse and Dynamics 365 logons by sensitive users.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: f327816b-9328-4b17-9290-a02adc2f4928
kind: Scheduled
name: Dataverse - Login by a sensitive privileged user
description: Identifies Dataverse and Dynamics 365 logons by sensitive users.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: Dataverse
    dataTypes:
      - DataverseActivity
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - CredentialAccess
  - PrivilegeEscalation
relevantTechniques:
  - T1133
  - T1190
  - T1078
  - T1212
query: |
  # Sensitive users are marked in the VIP Users watchlist using the Tags field.
  # Enter the tags values to monitor
  let monitored_tags = dynamic(["DataverseSensitive"]);
  let query_frequency = 1h;
  let sensitive_users = MSBizAppsVIPUsers()
      | where Tags in (monitored_tags);
  sensitive_users
  | join kind=inner (DataverseActivity
      | where TimeGenerated >= ago(query_frequency)
      | where Message == "UserSignIn")
      on $left.UserPrincipalName == $right.UserId
  | summarize FirstSeen = arg_max(TimeGenerated, *) by UserId
  | extend
      CloudAppId = int(32780),
      AccountName = tostring(split(UserId, '@')[0]),
      UPNSuffix = tostring(split(UserId, '@')[1])
  | project
      FirstSeen,
      UserId,
      ClientIp,
      UserAgent,
      InstanceUrl,
      CloudAppId,
      AccountName,
      UPNSuffix
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
      - identifier: InstanceName
        columnName: InstanceUrl
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
alertDetailsOverride:
  alertDisplayNameFormat: 'Dataverse - Sensitive user logged in in at {{InstanceUrl}} '
  alertDescriptionFormat: A user marked as sensitive for Dataverse in the VIPUsers
    watchlist signed in at {{InstanceUrl}}.
version: 3.2.0

Stages and Predicates

Parameters

let monitored_tags = dynamic(["DataverseSensitive"]);
let query_frequency = 1h;

let sensitive_users is inlined into the numbered stages below.

Stages 1 to 2 define let sensitive_users as the rule's main pipeline source. Stages 3 to 6 use it.

Stage 1: source

MSBizAppsVIPUsers()

Stage 2: where

| where Tags in (monitored_tags)

Stage 3: join

sensitive_users
| join kind=inner (DataverseActivity
    | where TimeGenerated >= ago(query_frequency)
    | where Message == "UserSignIn")
    on $left.UserPrincipalName == $right.UserId

Stage 4: summarize

summarize FirstSeen by UserId

Stage 5: extend

extend AccountName, CloudAppId, UPNSuffix

Stage 6: project

project AccountName, ClientIp, CloudAppId, FirstSeen, InstanceUrl, UPNSuffix, UserAgent, UserId

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
ClientIpproject
CloudAppIdproject
FirstSeenproject
InstanceUrlproject
UPNSuffixproject
UserAgentproject
UserIdproject

Dataverse - New user agent type that was not used with Office 365

#
Status
available
Severity
low
Time window
14d
Group by
InstanceUrl, UserAgent, UserId
Source
github.com/Azure/Azure-Sentinel

Identifies users accessing Dynamics with a User Agent that has not been seen in any Office 365 workloads in the last 14 days.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 094b3c0a-1f63-42f7-9535-c8c7b7198328
kind: Scheduled
name: Dataverse - New user agent type that was not used with Office 365
description: Identifies users accessing Dynamics with a User Agent that has not been
  seen in any Office 365 workloads in the last 14 days.
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: Dataverse
    dataTypes:
      - DataverseActivity
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  let query_lookback = 14d;
  let query_frequency = 1h;
  let known_useragents = dynamic([
      // Enter known user agents to exclude.
      // example:
      // "Agent1", "Agent2", "Agent3"
      ]);
  DataverseActivity
  | where TimeGenerated > ago(query_frequency)
  | where not (UserId has_any ("@onmicrosoft.com", "@microsoft.com", "Unknown"))
  | where isnotempty(UserAgent)
  | where UserAgent !in~ (known_useragents)
  | where UserAgent !hasprefix "azure-logic-apps" and UserAgent !hasprefix "PowerApps"
  | join kind = leftanti (
      OfficeActivity
      | where TimeGenerated between(ago(query_lookback) .. ago(query_frequency))
      | where isnotempty(UserAgent)
      | summarize by UserAgent)
      on UserAgent
  // Exclude user agents with a render agent to reduce noise.
  | join kind = leftanti(
      DataverseActivity
      | where TimeGenerated > ago(query_frequency)
      | where UserAgent has_any ("Gecko", "WebKit", "Presto", "Trident", "EdgeHTML", "Blink"))
      on UserAgent
  | summarize
      FirstSeen = min(TimeGenerated),
      LatestIP = arg_max(ClientIp, TimeGenerated)
      by UserAgent, UserId, InstanceUrl
  | extend
      AccountName = tostring(split(UserId, '@')[0]),
      UPNSuffix = tostring(split(UserId, '@')[1]),
      CloudAppId = int(32780)
  | project
      FirstSeen,
      UserId,
      UserAgent,
      LatestIP,
      InstanceUrl,
      CloudAppId,
      AccountName,
      UPNSuffix
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: LatestIP
version: 3.2.0

Stages and Predicates

Parameters

let query_lookback = 14d;
let query_frequency = 1h;

Let Binding: known_useragents used in Stage 5

let known_useragents = dynamic([
    ]);

Stage 1: source

DataverseActivity

Stage 2: where

| where TimeGenerated > ago(query_frequency)

Stage 3: where

| where not (UserId has_any ("@onmicrosoft.com", "@microsoft.com", "Unknown"))

Stage 4: where

| where isnotempty(UserAgent)

Stage 5: where

| where UserAgent !in~ (known_useragents)

Stage 6: where

| where UserAgent !hasprefix "azure-logic-apps" and UserAgent !hasprefix "PowerApps"

Stage 7: join (negated)

join kind=leftanti (OfficeActivity) on UserAgent

Stage 8: join (negated)

join kind=leftanti (DataverseActivity) on UserAgent

Stage 9: summarize

summarize FirstSeen, LatestIP by UserAgent, UserId, InstanceUrl

Stage 10: extend

extend AccountName, CloudAppId, UPNSuffix

Stage 11: project

project AccountName, CloudAppId, FirstSeen, InstanceUrl, LatestIP, UPNSuffix, UserAgent, UserId

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
UserAgentis_not_null
  • (no value, null check)
field:"UserAgent" kind:is_not_null

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
UserIdmatch@onmicrosoft.com, @microsoft.com, Unknownexcludes:UserId field:"UserId" value:"@onmicrosoft.com" field:"UserId" value:"@microsoft.com" field:"UserId" value:"Unknown"
UserAgenteq[]excludes:UserAgent field:"UserAgent" value:"[]"
UserAgentstarts_withPowerAppsexcludes:UserAgent field:"UserAgent" value:"PowerApps"
UserAgentstarts_withazure-logic-appsexcludes:UserAgent field:"UserAgent" value:"azure-logic-apps"
UserAgentis_not_null(no value, null check)excludes:UserAgent
UserAgentmatchGecko, WebKit, Presto, Trident, EdgeHTML, Blinkexcludes:UserAgent

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
CloudAppIdproject
FirstSeenproject
InstanceUrlproject
LatestIPproject
UPNSuffixproject
UserAgentproject
UserIdproject

Digital Guardian - Bulk exfiltration to external domain

#
Status
available
Severity
medium
Time window
30m
Group by
DstUserName, SrcUserName
Source
github.com/Azure/Azure-Sentinel

'Detects bulk exfiltration to external domain.'

MITRE ATT&CK Coverage

Rule Body

id: 5f75a873-b524-4ba5-a3b8-2c20db517148
name: Digital Guardian - Bulk exfiltration to external domain
description: |
  'Detects bulk exfiltration to external domain.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1048
query: |
  let threshold = 10;
  let corp_domain = dynamic(['example.com']);
  DigitalGuardianDLPEvent
  | where NetworkApplicationProtocol =~ 'SMTP'
  | where isnotempty(inspected_document)
  | extend s_domain = extract(@'@(.*)', 1, SrcUserName)
  | extend d_domain = extract(@'@(.*)', 1, DstUserName)
  | where s_domain in~ (corp_domain)
  | where d_domain !in (corp_domain)
  | summarize f = dcount(inspected_document) by SrcUserName, DstUserName, bin(TimeGenerated, 30m)
  | where  f >= threshold
  | extend AccountCustomEntity = SrcUserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 10;
let corp_domain = dynamic(['example.com']);

Stage 1: source

DigitalGuardianDLPEvent

Stage 2: where

| where NetworkApplicationProtocol =~ 'SMTP'

Stage 3: where

| where isnotempty(inspected_document)

Stage 4: extend

| extend s_domain = extract(@'@(.*)', 1, SrcUserName)

Stage 5: extend

| extend d_domain = extract(@'@(.*)', 1, DstUserName)

Stage 6: where

| where s_domain in~ (corp_domain)

Stage 7: where

| where d_domain !in (corp_domain)

Stage 8: summarize

| summarize f = dcount(inspected_document) by SrcUserName, DstUserName, bin(TimeGenerated, 30m)

Stage 9: where

| where  f >= threshold

Stage 10: extend

| extend AccountCustomEntity = SrcUserName

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
d_domaineqcorp_domainexcludes:d_domain field:"d_domain" value:"corp_domain"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstUserNamesummarize
SrcUserNamesummarize
fsummarize
AccountCustomEntityextend

GCP Audit Logs - Data Access Logging Exemption Added for Principal

#
Status
available
Severity
high
Time window
1h
Group by
AuthEmail, CallerIpAddress, ExemptedAccountName, ExemptedMember, GCPResourceName, LogName, MethodName, PrincipalEmail, ProjectId, ServiceName, UserAgent
Source
github.com/Azure/Azure-Sentinel

Detects when a principal (user or service account) is exempted from GCP data access audit logging. This is a critical security event as it reduces visibility into privileged operations and may indicate an attempt to hide malicious activity. Adversaries may exempt their accounts from audit logging to evade detection while performing reconnaissance, privilege escalation, or data exfiltration. This rule monitors SetIamPolicy operations that add audit log exemptions for ADMIN_READ, DATA_READ, or DATA_WRITE log types.

MITRE ATT&CK Coverage

Telemetry Coverage

PlatformRecord / event type
GCPSetIamPolicy

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: b7da45ce-fcc8-43c7-a37c-c08454579d26
name: GCP Audit Logs - Data Access Logging Exemption Added for Principal
description: |
  'Detects when a principal (user or service account) is exempted from GCP data access audit logging.
  This is a critical security event as it reduces visibility into privileged operations and may indicate an attempt to hide malicious activity.
  Adversaries may exempt their accounts from audit logging to evade detection while performing reconnaissance, privilege escalation, or data exfiltration.
  This rule monitors SetIamPolicy operations that add audit log exemptions for ADMIN_READ, DATA_READ, or DATA_WRITE log types.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: GCPAuditLogsDefinition
    dataTypes:
      - GCPAuditLogs
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - PrivilegeEscalation
relevantTechniques:
  - T1562.008
  - T1078.004
tags:
  - Cloud Security
  - Audit Logging
  - Defense Evasion
query: |
 GCPAuditLogs
  | where ServiceName == "cloudresourcemanager.googleapis.com"
  | where MethodName == "SetIamPolicy"
  | where GCPResourceType == "project" and Severity == "NOTICE"
  | where isnotempty(ServiceData)
  | extend ServiceDataJson = parse_json(ServiceData)
  | extend PolicyDelta = ServiceDataJson.policyDelta.auditConfigDeltas
  | where isnotempty(PolicyDelta)
  | mv-expand ConfigDelta = PolicyDelta
  | where ConfigDelta.action == "ADD"
  | extend LogType = tostring(ConfigDelta.logType)
  | where LogType in ("ADMIN_READ", "DATA_READ", "DATA_WRITE")
  | extend 
      ExemptedMember = tostring(ConfigDelta.exemptedMember),
      ServiceAffected = tostring(ConfigDelta.service),
      RequestMetadataJson = parse_json(RequestMetadata),
      AuthInfoJson = parse_json(AuthenticationInfo)
  | where isnotempty(ExemptedMember)
  | extend 
      CallerIpAddress = tostring(RequestMetadataJson.callerIp),
      UserAgent = tostring(RequestMetadataJson.callerSuppliedUserAgent),
      AuthEmail = tostring(AuthInfoJson.principalEmail),
      ExemptedAccountName = tostring(split(ExemptedMember, ":")[1])
  | summarize 
      ExemptedLogTypes = make_set(LogType, 10),
      ExemptedServices = make_set(ServiceAffected, 50),
      FirstExemption = min(TimeGenerated),
      LastExemption = max(TimeGenerated)
      by PrincipalEmail, ProjectId, GCPResourceName, ExemptedMember, 
         CallerIpAddress, UserAgent, LogName, ExemptedAccountName, MethodName, ServiceName, AuthEmail
  | extend
      AccountName = tostring(split(PrincipalEmail, "@")[0]), 
      AccountUPNSuffix = tostring(split(PrincipalEmail, "@")[1])
  | project TimeGenerated = LastExemption,
      PrincipalEmail,
      ProjectId,
      ResourceName = GCPResourceName,
      ExemptedMember,
      ExemptedAccountName,
      ExemptedLogTypes,
      ExemptedServices,
      FirstExemption,
      LastExemption,
      CallerIpAddress,
      UserAgent,
      AuthEmail,
      MethodName,
      ServiceName,
      LogName,
      AccountName,
      AccountUPNSuffix
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: PrincipalEmail
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CallerIpAddress
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: ProjectId
      - identifier: InstanceName
        columnName: ResourceName
customDetails:
  ProjectId: ProjectId
  ExemptedMember: ExemptedMember
  ExemptedAccountName: ExemptedAccountName
  ExemptedLogTypes: ExemptedLogTypes
  ExemptedServices: ExemptedServices
  UserAgent: UserAgent
alertDetailsOverride:
  alertDisplayNameFormat: "GCP Data Access Logging Exemption Added for {{ExemptedAccountName}} by {{PrincipalEmail}} in Service {{ExemptedServices}}"
  alertDescriptionFormat: |-
    Principal {{ExemptedAccountName}} added as exception from Data Access logging in project {{ProjectId}} for Service {{ExemptedServices}}.
    This action reduces audit visibility and may indicate an attempt to evade detection. Verify this change was authorized and investigate any suspicious activity performed by the exempted principal.
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

GCPAuditLogs

Stage 2: where

| where ServiceName == "cloudresourcemanager.googleapis.com"

Stage 3: where

| where MethodName == "SetIamPolicy"

Stage 4: where

| where GCPResourceType == "project" and Severity == "NOTICE"

Stage 5: where

| where isnotempty(ServiceData)

Stage 6: extend

| extend ServiceDataJson = parse_json(ServiceData)

Stage 7: extend

| extend PolicyDelta = ServiceDataJson.policyDelta.auditConfigDeltas

Stage 8: where

| where isnotempty(PolicyDelta)

Stage 9: mv-expand

| mv-expand ConfigDelta = PolicyDelta

Stage 10: where

| where ConfigDelta.action == "ADD"

Stage 11: extend

| extend LogType = tostring(ConfigDelta.logType)

Stage 12: where

| where LogType in ("ADMIN_READ", "DATA_READ", "DATA_WRITE")

Stage 13: extend

| extend 
     ExemptedMember = tostring(ConfigDelta.exemptedMember),
     ServiceAffected = tostring(ConfigDelta.service),
     RequestMetadataJson = parse_json(RequestMetadata),
     AuthInfoJson = parse_json(AuthenticationInfo)

Stage 14: where

| where isnotempty(ExemptedMember)

Stage 15: extend

| extend 
     CallerIpAddress = tostring(RequestMetadataJson.callerIp),
     UserAgent = tostring(RequestMetadataJson.callerSuppliedUserAgent),
     AuthEmail = tostring(AuthInfoJson.principalEmail),
     ExemptedAccountName = tostring(split(ExemptedMember, ":")[1])

Stage 16: summarize

| summarize 
     ExemptedLogTypes = make_set(LogType, 10),
     ExemptedServices = make_set(ServiceAffected, 50),
     FirstExemption = min(TimeGenerated),
     LastExemption = max(TimeGenerated)
     by PrincipalEmail, ProjectId, GCPResourceName, ExemptedMember, 
        CallerIpAddress, UserAgent, LogName, ExemptedAccountName, MethodName, ServiceName, AuthEmail

Stage 17: extend

| extend
     AccountName = tostring(split(PrincipalEmail, "@")[0]), 
     AccountUPNSuffix = tostring(split(PrincipalEmail, "@")[1])

Stage 18: project

| project TimeGenerated = LastExemption,
     PrincipalEmail,
     ProjectId,
     ResourceName = GCPResourceName,
     ExemptedMember,
     ExemptedAccountName,
     ExemptedLogTypes,
     ExemptedServices,
     FirstExemption,
     LastExemption,
     CallerIpAddress,
     UserAgent,
     AuthEmail,
     MethodName,
     ServiceName,
     LogName,
     AccountName,
     AccountUPNSuffix

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
AccountUPNSuffixproject
AuthEmailproject
CallerIpAddressproject
ExemptedAccountNameproject
ExemptedLogTypesproject
ExemptedMemberproject
ExemptedServicesproject
FirstExemptionproject
LastExemptionproject
LogNameproject
MethodNameproject
PrincipalEmailproject
ProjectIdproject
ResourceNameproject
ServiceNameproject
TimeGeneratedproject
UserAgentproject

Generate alerts based on ExtraHop detections recommended for triage

#
Status
available
Severity
medium
Time window
15m
Source
github.com/Azure/Azure-Sentinel

'This analytics rule will generate alerts in Microsoft Sentinel for detections from ExtraHop that are recommended for triage.'

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Rule Body

id: "cd65aebc-7e85-4cbb-9f91-ff0376c5d37d"
name: "Generate alerts based on ExtraHop detections recommended for triage"
description: |
  'This analytics rule will generate alerts in Microsoft Sentinel for detections from ExtraHop that are recommended for triage.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: ExtraHop
    dataTypes:
      - ExtraHopDetections
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: GreaterThan
triggerThreshold: 0
tactics: 
  - Persistence
relevantTechniques:
  - T1546
query: |
    ExtraHopDetections
    | where IsRecommended == true
incidentConfiguration:
    createIncident: false
eventGroupingSettings:
    aggregationKind: AlertPerResult
alertDetailsOverride:
    alertDisplayNameFormat: "Alert from {{EventVendor}} for Detection {{Title}}"
    alertDescriptionFormat: "Alert from {{EventVendor}} for Detection {{Title}} for Id {{Id}}"
    alertTacticsColumnName: "TacticNames"
    alertSeverityColumnName: "Severity"
    alertDynamicProperties:
    - alertProperty: AlertLink
      value: Url
    - alertProperty: Techniques
      value: TechniqueIds
    - alertProperty: ProductName
      value: EventVendor
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SourceHostname
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DestinationHostname
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: DestinationIpAddress
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: SourceUsername
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: DestinationUsername
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

ExtraHopDetections

Stage 2: where

| where IsRecommended == true

Indicators

These rows show field, operator, and value matches.

GitHub - pull request was merged

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

Detect activities when a pull request was merged. This query runs every day and its severity is Medium.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

Rule Body

id: 0b85a077-8ba5-4cb5-90f7-1e882afe10c6
name: GitHub - pull request was merged
description: |
  'Detect activities when a pull request was merged. This query runs every day and its severity is Medium.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |
    GitHubAuditData
    | where Action == "pull_request.merge"
    | project Actor, Action
    | extend AccountName = tostring(split(Actor, "@")[0]), AccountUPNSuffix = tostring(split(Actor, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Actor
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

GitHubAuditData

Stage 2: where

| where Action == "pull_request.merge"

Stage 3: project

| project Actor, Action

Stage 4: extend

| extend AccountName = tostring(split(Actor, "@")[0]), AccountUPNSuffix = tostring(split(Actor, "@")[1])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actioneq
  • pull_request.merge
field:"Action" kind:eq value:"pull_request.merge"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionproject
Actorproject
AccountNameextend
AccountUPNSuffixextend

Global VM Exclusions Added

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

Detects when global VM exclusion are added in Veeam Backup & Replication.

Rule Body

id: c9faf1ee-57dd-4f54-8cd1-fcf35a1aa424
name: Global VM Exclusions Added
description: Detects when global VM exclusion are added in Veeam Backup & Replication.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 40400\n| project\n    Date\
  \ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n\
  \    EventId = instanceId,\n    [\"User Name\"] = user,\n  MessageDetails = Description,\n\
  \   Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 40400

Stage 3: project

| project
    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
    DataSource = original_host,
    EventId = instanceId,
    ["User Name"] = user,
  MessageDetails = Description,
   Severity = SeverityDescription

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
User Nameproject

Idira - Multiple Failed Actions Followed by Success (15m)

#
Severity
medium
Time window
450d
Group by
targetAccount, username
Source
github.com/Azure/Azure-Sentinel

Detects 3+ failed actions against an account followed by a success in a short window, indicating brute-force or credential guessing.

MITRE ATT&CK Coverage

TacticTechniques
Credential AccessNo specific technique

Rule Body

id: bdf3cf98-d64f-4c55-97e4-43483d6d3237
kind: Scheduled
name: Idira - Multiple Failed Actions Followed by Success (15m)
description: >
  Detects 3+ failed actions against an account followed by a success in a short window,
  indicating brute-force or credential guessing.
severity: Medium
tactics:
  - CredentialAccess
queryFrequency: 5M
queryPeriod: 15M
triggerOperator: GreaterThan
triggerThreshold: 0
query: |
  let window = 15m;
  let fails =
      CyberArk_AuditEvents_CL
      | where TimeGenerated > ago(window)
      | where actionType has_any ("Fail","Failed","Denied","Error")
          or message has_any ("fail","failed","denied","unauthorized")
      | summarize Fails=count(), firstFail=min(TimeGenerated), lastFail=max(TimeGenerated) by username, targetAccount;
  let success =
      CyberArk_AuditEvents_CL
      | where TimeGenerated > ago(window)
      | where actionType has_any ("Success","Succeeded","Allow","Allowed")
          or message has_any ("success","allowed")
      | summarize Successes=count(), firstSuccess=min(TimeGenerated) by username, targetAccount;
  fails
  | where Fails >= 3
  | join kind=inner success on username, targetAccount
  | join kind=inner (
      CyberArk_AuditEvents_CL
      | extend cd = parse_json(customData)
      | extend cd_client_ip = tostring(cd.client_ip_address),
               cd_source_ip = tostring(cd.source_ip_address),
               cd_device_os = tostring(cd.device_os),
               cd_browser = tostring(cd.browser_name),
               cd_geo_city = tostring(cd.geoip_city_name),
               cd_geo_country = tostring(cd.geoip_country_name),
               cd_target = coalesce(tostring(cd.target), tostring(cd.target_resource), tostring(cd.new_target))
      | project username, targetAccount, any_cd_client_ip = cd_client_ip, any_cd_source_ip = cd_source_ip,
                any_cd_device_os = cd_device_os, any_cd_browser = cd_browser,
                any_cd_geo_city = cd_geo_city, any_cd_geo_country = cd_geo_country, any_cd_target = cd_target
    ) on username, targetAccount
  | project username, targetAccount, Fails, Successes, firstFail, lastFail, firstSuccess,
            any_cd_source_ip, any_cd_client_ip, any_cd_device_os, any_cd_browser,
            any_cd_geo_city, any_cd_geo_country, any_cd_target
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: username
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: targetAccount
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Parameters

let window = 15m;

let fails is inlined into the numbered stages below.

Let Binding: success used in Stage 6

let success = CyberArk_AuditEvents_CL
    | where TimeGenerated > ago(window)
    | where actionType has_any ("Success","Succeeded","Allow","Allowed")
        or message has_any ("success","allowed")
    | summarize Successes=count(), firstSuccess=min(TimeGenerated) by username, targetAccount;

Stages 1 to 4 define let fails as the rule's main pipeline source. Stages 5 to 8 use it.

Stage 1: source

CyberArk_AuditEvents_CL

Stage 2: where

| where TimeGenerated > ago(window)

Stage 3: where

| where actionType has_any ("Fail","Failed","Denied","Error")
        or message has_any ("fail","failed","denied","unauthorized")

Stage 4: summarize

| summarize Fails=count(), firstFail=min(TimeGenerated), lastFail=max(TimeGenerated) by username, targetAccount

Stage 5: where

fails
| where Fails >= 3

Stage 6: join

| join kind=inner success on username, targetAccount

Stage 7: join

| join kind=inner (
    CyberArk_AuditEvents_CL
    | extend cd = parse_json(customData)
    | extend cd_client_ip = tostring(cd.client_ip_address),
             cd_source_ip = tostring(cd.source_ip_address),
             cd_device_os = tostring(cd.device_os),
             cd_browser = tostring(cd.browser_name),
             cd_geo_city = tostring(cd.geoip_city_name),
             cd_geo_country = tostring(cd.geoip_country_name),
             cd_target = coalesce(tostring(cd.target), tostring(cd.target_resource), tostring(cd.new_target))
    | project username, targetAccount, any_cd_client_ip = cd_client_ip, any_cd_source_ip = cd_source_ip,
              any_cd_device_os = cd_device_os, any_cd_browser = cd_browser,
              any_cd_geo_city = cd_geo_city, any_cd_geo_country = cd_geo_country, any_cd_target = cd_target
  ) on username, targetAccount

Stage 8: project

project Fails, Successes, any_cd_browser, any_cd_client_ip, any_cd_device_os, any_cd_geo_city, any_cd_geo_country, any_cd_source_ip, any_cd_target, firstFail, firstSuccess, lastFail, targetAccount, username

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Failsge
  • 3 transforms: number
field:"Fails" kind:ge value:"3"
actionTypematch
  • Allow transforms: term
  • Allowed transforms: term
  • Denied transforms: term
  • Error transforms: term
  • Fail transforms: term
  • Failed transforms: term
  • Succeeded transforms: term
  • Success transforms: term
field:"actionType" kind:match
messagematch
  • allowed transforms: term
  • denied transforms: term
  • fail transforms: term
  • failed transforms: term
  • success transforms: term
  • unauthorized transforms: term
field:"message" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
Failsproject
Successesproject
any_cd_browserproject
any_cd_client_ipproject
any_cd_device_osproject
any_cd_geo_cityproject
any_cd_geo_countryproject
any_cd_source_ipproject
any_cd_targetproject
firstFailproject
firstSuccessproject
lastFailproject
targetAccountproject
usernameproject

McAfee ePO - Unable to clean or delete infected file

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

Detects when McAfee failed to clean or delete infected file.

MITRE ATT&CK Coverage

TacticTechniques
Stealth
Defense Impairment

Telemetry Coverage

Rule Body

id: 9860e89f-72c8-425e-bac9-4a170798d3ea
name: McAfee ePO - Unable to clean or delete infected file
description: |
  'Detects when McAfee failed to clean or delete infected file.'
severity: High
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562
  - T1070
query: |
  McAfeeEPOEvent
  | where EventId in ('1026', '1028', '1298', '1310', '1055', '2002', '2004', '2009')
  | extend EventMessage = case(EventId == '1026', 'Unable to clean infected file',
                                EventId == '1028', 'Unable to delete infected file',
                                EventId == '1298', 'File infected. Delete failed, quarantine failed',
                                EventId == '1310', 'Multiple extension heuristic detection - delete failed, quarantine failed',
                                EventId == '1055', 'Unable to delete infected file',
                                EventId == '2002', 'Unable to clean infected file',
                                EventId == '2004', 'Unable to delete infected file',
                                'Unable to move infected file to quarantine')
  | project DvcIpAddr, EventId, EventMessage, DstFileName
  | extend IPCustomEntity = DvcIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

McAfeeEPOEvent

Stage 2: where

| where EventId in ('1026', '1028', '1298', '1310', '1055', '2002', '2004', '2009')

Stage 3: extend

| extend EventMessage = case(EventId == '1026', 'Unable to clean infected file',
                              EventId == '1028', 'Unable to delete infected file',
                              EventId == '1298', 'File infected. Delete failed, quarantine failed',
                              EventId == '1310', 'Multiple extension heuristic detection - delete failed, quarantine failed',
                              EventId == '1055', 'Unable to delete infected file',
                              EventId == '2002', 'Unable to clean infected file',
                              EventId == '2004', 'Unable to delete infected file',
                              'Unable to move infected file to quarantine')
EventMessage =
ifEventId == 1026'Unable to clean infected file'
elifEventId == 1028'Unable to delete infected file'
elifEventId == 1298'File infected. Delete failed, quarantine failed'
elifEventId == 1310'Multiple extension heuristic detection - delete failed, quarantine failed'
elifEventId == 1055'Unable to delete infected file'
elifEventId == 2002'Unable to clean infected file'
elifEventId == 2004'Unable to delete infected file'
else'Unable to move infected file to quarantine'

Stage 4: project

| project DvcIpAddr, EventId, EventMessage, DstFileName

Stage 5: extend

| extend IPCustomEntity = DvcIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIdin
  • 1026
  • 1028
  • 1055
  • 1298
  • 1310
  • 2002
  • 2004
  • 2009
field:"EventId" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstFileNameproject
DvcIpAddrproject
EventIdproject
EventMessageproject
IPCustomEntityextend

Pathlock TDnR - RiskTrack Audit Results

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

Detects Pathlock RiskTrack audit results forwarded to Microsoft Sentinel. RiskTrack findings represent completed risk assessments identifying segregation of duties conflicts, critical access violations, and compliance gaps that require remediation or SOC awareness.

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c52
name: Pathlock TDnR - RiskTrack Audit Results
kind: Scheduled
description: >-
  Detects Pathlock RiskTrack audit results forwarded to Microsoft Sentinel. RiskTrack findings
  represent completed risk assessments identifying segregation of duties conflicts, critical
  access violations, and compliance gaps that require remediation or SOC awareness.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T1082
query: |
  Pathlock_TDnR_CL
  | where DataSource == "SAST_RT"
  | project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
            Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
            MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Bname
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIp
eventGroupingSettings:
  aggregationKind: SingleAlert
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Stage 1: source

Pathlock_TDnR_CL

Stage 2: where

| where DataSource == "SAST_RT"

Stage 3: project

| project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
          Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
          MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AffectedUserproject
Areaproject
Bnameproject
CentralTsproject
DataSourceproject
DestIpproject
Eventidproject
Hostnameproject
Instanceproject
LogLineproject
MessageV1project
MessageV2project
MessageV3project
MessageV4project
MsgIdproject
MsgNoproject
MsgTypeproject
Reportproject
SrcIpproject
Subidproject
Sysidproject
Tcodeproject
TimeGeneratedproject

Pathlock TDnR - User-Profile Assignment Changes

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

Detects changes to user-to-profile assignments in SAP (SU01 profile tab), forwarded by Pathlock Threat Detection and Response. Unauthorized profile assignments can grant users broad authorizations outside of the role-based access control framework, bypassing segregation-of-duties controls.

MITRE ATT&CK Coverage

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c75
name: Pathlock TDnR - User-Profile Assignment Changes
kind: Scheduled
description: >-
  Detects changes to user-to-profile assignments in SAP (SU01 profile tab), forwarded by Pathlock
  Threat Detection and Response. Unauthorized profile assignments can grant users broad authorizations
  outside of the role-based access control framework, bypassing segregation-of-duties controls.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
  - Persistence
relevantTechniques:
  - T1548
  - T1098
query: |
  Pathlock_TDnR_CL
  | where DataSource == "USER_PROFILES"
  | project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
            Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
            MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Bname
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIp
eventGroupingSettings:
  aggregationKind: SingleAlert
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Stage 1: source

Pathlock_TDnR_CL

Stage 2: where

| where DataSource == "USER_PROFILES"

Stage 3: project

| project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
          Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
          MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AffectedUserproject
Areaproject
Bnameproject
CentralTsproject
DataSourceproject
DestIpproject
Eventidproject
Hostnameproject
Instanceproject
LogLineproject
MessageV1project
MessageV2project
MessageV3project
MessageV4project
MsgIdproject
MsgNoproject
MsgTypeproject
Reportproject
SrcIpproject
Subidproject
Sysidproject
Tcodeproject
TimeGeneratedproject

Pure Failed Login

#
Severity
high
Group by
HostIP
Source
github.com/Azure/Azure-Sentinel

Detect failed login attacks and delete user

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: ed32b115-5001-43a7-a2bb-f53026db4d97
name: Pure Failed Login
version: 1.0.0
kind: NRT
description: Detect failed login attacks and delete user
severity: High
tactics:
- CredentialAccess
relevantTechniques:
- T1212
query: |-
  Syslog
  | where SyslogMessage has "purity.alert" and SyslogMessage has "invalid username or password"
  | extend Message = SyslogMessage
  | extend ParsedLog = extract_all(@"((?P<process>.*?)\[(?P<processid>.*?)\][\s\S]*?Array name:\s*'(?P<arrayname>\S+)'[\s\S]*?Controller:\s*'?(?P<controller>[^']+)'?[\s\S]*Interface:\s*'(?P<interface>\S+)'.*?User:\s'(?P<login>.*?)'\sLocation: '(?P<location>[^']+)'\sSublocation: '(?P<sublocation>[^']+)\s*(?P<part2log>[\s\S]*))", dynamic(['process', 'processid', 'arrayname', 'controller', 'interface','login', 'location', 'sublocation', 'part2log']), Message)
  | mv-expand ParsedLog
  | extend ResidueLog = tostring(ParsedLog[8])
  | extend Rlog = extract_all(@"[\s\S]*Action:\s'(?P<action>[^']+)'[\s\S]*Method:\s'(?P<method>[^']+)'[\s\S]*Result:\s(?P<result>[^']+)[\s\S]*Description:\s'(?P<description>[^']*)'", dynamic(['action', 'method', 'result', 'description']), ResidueLog)
  | mv-expand Rlog
  | extend PureLogType = ParsedLog[0], PureProcessID = ParsedLog[1], PureArrayName = ParsedLog[2], PureController = ParsedLog[3], PureInterface = ParsedLog[4], PureLogin = ParsedLog [5], PureLocation = ParsedLog [6], PureSublocation = ParsedLog [7], PureAction = Rlog [0], PureMethod = Rlog [1], PureResult = Rlog [2], PureDescription = Rlog [3]
  | project-away ResidueLog, Rlog, ParsedLog
  | summarize count() by tostring(PureLogin), tostring(PureArrayName), HostIP
  | where count_ >= 10
suppressionDuration: 5h
suppressionEnabled: false
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
  alertDynamicProperties: []
entityMappings:
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: HostIP
- entityType: Account
  fieldMappings:
  - identifier: Name
    columnName: PureLogin
- entityType: Host
  fieldMappings:
  - identifier: HostName
    columnName: PureArrayName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []

Stages and Predicates

Stage 1: source

Syslog

Stage 2: where

| where SyslogMessage has "purity.alert" and SyslogMessage has "invalid username or password"

Stage 3: extend

| extend Message = SyslogMessage

Stage 4: extend

| extend ParsedLog = extract_all(@"((?P<process>.*?)\[(?P<processid>.*?)\][\s\S]*?Array name:\s*'(?P<arrayname>\S+)'[\s\S]*?Controller:\s*'?(?P<controller>[^']+)'?[\s\S]*Interface:\s*'(?P<interface>\S+)'.*?User:\s'(?P<login>.*?)'\sLocation: '(?P<location>[^']+)'\sSublocation: '(?P<sublocation>[^']+)\s*(?P<part2log>[\s\S]*))", dynamic(['process', 'processid', 'arrayname', 'controller', 'interface','login', 'location', 'sublocation', 'part2log']), Message)

Stage 5: mv-expand

| mv-expand ParsedLog

Stage 6: extend

| extend ResidueLog = tostring(ParsedLog[8])

Stage 7: extend

| extend Rlog = extract_all(@"[\s\S]*Action:\s'(?P<action>[^']+)'[\s\S]*Method:\s'(?P<method>[^']+)'[\s\S]*Result:\s(?P<result>[^']+)[\s\S]*Description:\s'(?P<description>[^']*)'", dynamic(['action', 'method', 'result', 'description']), ResidueLog)

Stage 8: mv-expand

| mv-expand Rlog

Stage 9: extend

| extend PureLogType = ParsedLog[0], PureProcessID = ParsedLog[1], PureArrayName = ParsedLog[2], PureController = ParsedLog[3], PureInterface = ParsedLog[4], PureLogin = ParsedLog [5], PureLocation = ParsedLog [6], PureSublocation = ParsedLog [7], PureAction = Rlog [0], PureMethod = Rlog [1], PureResult = Rlog [2], PureDescription = Rlog [3]

Stage 10: project-away

| project-away ResidueLog, Rlog, ParsedLog

Stage 11: summarize

| summarize count() by tostring(PureLogin), tostring(PureArrayName), HostIP

Stage 12: where

| where count_ >= 10

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
SyslogMessagematch
  • invalid username or password transforms: term
  • purity.alert transforms: term
field:"SyslogMessage" kind:match
count_ge
  • 10 transforms: number
field:"count_" kind:ge value:"10"

Output Fields

These fields are emitted when the rule matches.

FieldSource
HostIPsummarize

Service Principal Authentication Attempt from New Country

#
Severity
medium
Time window
14d
Group by
Location, ServicePrincipalName
Author
Pete Bryan
Source
github.com/Azure/Azure-Sentinel

Detects when there is a Service Principal login attempt from a country that has not seen a successful login in the previous 14 days. Threat actors may attempt to authenticate with credentials from compromised accounts - monitoring attempts from anomalous locations may help identify these attempts. Authentication attempts should be investigated to ensure the activity was legitimate and if there is other similar activity. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-failed-unusual-sign-ins

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

Rule Body

id: 1baaaf00-655f-4de9-8ff8-312e902cda71
name: Service Principal Authentication Attempt from New Country
description: |
  'Detects when there is a Service Principal login attempt from a country that has not seen a successful login in the previous 14 days.
    Threat actors may attempt to authenticate with credentials from compromised accounts - monitoring attempts from anomalous locations may help identify these attempts.
    Authentication attempts should be investigated to ensure the activity was legitimate and if there is other similar activity.
    Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-failed-unusual-sign-ins'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADServicePrincipalSignInLogs
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078.004
tags:
  - AADSecOpsGuide
query: |
  let known_locations = (
    AADServicePrincipalSignInLogs
    | where TimeGenerated between(ago(14d)..ago(1d))
    | where ResultType == 0
    | summarize by Location);
    AADServicePrincipalSignInLogs
    | where TimeGenerated > ago(1d)
    | where ResultType != 50126
    | where Location !in (known_locations)
    | extend City = tostring(parse_json(LocationDetails).city)
    | extend State = tostring(parse_json(LocationDetails).state)
    | extend Place = strcat(City, " - ", State)
    | extend Result = strcat(tostring(ResultType), " - ", ResultDescription)
    | summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), make_set(Result), make_set(IPAddress), make_set(Place) by ServicePrincipalName, Location
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: ServicePrincipalName
version: 1.0.1
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Pete Bryan
    support:
        tier: Community
    categories:
        domains: [ "Security - Others", "Identity" ]

Stages and Predicates

Let Binding: known_locations used in Stage 4

let known_locations = (
  AADServicePrincipalSignInLogs
  | where TimeGenerated between(ago(14d)..ago(1d))
  | where ResultType == 0
  | summarize by Location);

Stage 1: source

AADServicePrincipalSignInLogs

Stage 2: where

| where TimeGenerated > ago(1d)

Stage 3: where

| where ResultType != 50126

Stage 4: where

| where Location !in (known_locations)

Stage 5: extend (4 consecutive steps)

| extend City = tostring(parse_json(LocationDetails).city)
| extend State = tostring(parse_json(LocationDetails).state)
| extend Place = strcat(City, " - ", State)
| extend Result = strcat(tostring(ResultType), " - ", ResultDescription)

Stage 6: summarize

| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), make_set(Result), make_set(IPAddress), make_set(Place) by ServicePrincipalName, Location

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Locationeqknown_locationsexcludes:Location field:"Location" value:"known_locations"

Output Fields

These fields are emitted when the rule matches.

FieldSource
FirstSeensummarize
LastSeensummarize
Locationsummarize
ServicePrincipalNamesummarize

SFTP File transfer folder count above threshold

#
Severity
medium
Time window
15m
Group by
Computer, ProcessID, src_ip, username
Source
github.com/Azure/Azure-Sentinel

'Identifies SFTP File Transfers with distinct folder count above certain threshold in a 15min time period. It requires SFTP VERBOSE loglevel to be enabled. Please note that entity mapping for arrays is not supported, so when there is a single value in an array, we will pull that value from the array as a single string to populate the entity to support entity mapping features within Sentinel. Additionally, if the array is multivalued, we will input a string to indicate this with a unique hash so that matching will not occur.'

MITRE ATT&CK Coverage

TacticTechniques
Exfiltration

Rule Body

id: 7355434e-09d5-4401-b56d-e03e9379dfb1
name: SFTP File transfer folder count above threshold
description: |
  'Identifies SFTP File Transfers with distinct folder count above certain threshold in a 15min time period.
   It requires SFTP VERBOSE loglevel to be enabled.
   Please note that entity mapping for arrays is not supported, so when there is a single value in an array, we will pull that value from the array as a single string to populate the entity to support entity mapping features within Sentinel. Additionally, if the array is multivalued, we will input a string to indicate this with a unique hash so that matching will not occur.'
severity: Medium
requiredDataConnectors:
  - connectorId: Syslog
    dataTypes:
      - Syslog
  - connectorId: SyslogAma
    dataTypes: 
      - Syslog
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1020
query: |
  let threshold = 10;
  Syslog
  | where ProcessName has "sftp"
      and SyslogMessage has "close "
      and SyslogMessage has " bytes read "
  | parse SyslogMessage with "close \"" filepath "\" bytes read " readbytes: int " written " writtenbytes: int
  | parse kind=regex filepath with dirpath:string "/" filename:string
  | join kind=leftouter (
      Syslog
      | where ProcessName has "sftp" and SyslogMessage has "session opened for"
      | parse SyslogMessage with "session opened for local user " username: string " from [" src_ip "]"
      | project username, src_ip, ProcessID
      )
      on ProcessID
  | project-away ProcessID1
  | summarize count_distinct_dirpath=dcount(dirpath), dirlist=make_set(dirpath) by Computer, username, src_ip, bin(TimeGenerated, 15m)
  | where count_distinct_dirpath >= threshold
  | extend DirSample = iff(array_length(dirlist) == 1, tostring(dirlist[0]), strcat("SeeDirListField","_", tostring(hash(tostring(dirlist)))))
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: username
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: src_ip
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Computer
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: DirSample
customDetails:
  TransferCount: count_distinct_dirpath
  FilesList: dirlist
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: Selected
    groupByEntities:
    - Account
    - Host
    groupByAlertDetails: []
    groupByCustomDetails: []
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 10;

Stage 1: source

Syslog

Stage 2: where

| where ProcessName has "sftp"
    and SyslogMessage has "close "
    and SyslogMessage has " bytes read "

Stage 3: parse

| parse SyslogMessage with "close \"" filepath "\" bytes read " readbytes: int " written " writtenbytes: int

Stage 4: parse

| parse kind=regex filepath with dirpath:string "/" filename:string

Stage 5: join

| join kind=leftouter (
    Syslog
    | where ProcessName has "sftp" and SyslogMessage has "session opened for"
    | parse SyslogMessage with "session opened for local user " username: string " from [" src_ip "]"
    | project username, src_ip, ProcessID
    )
    on ProcessID

Stage 6: project-away

project-away ProcessID1

Stage 7: summarize

summarize count_distinct_dirpath, dirlist by Computer, username, src_ip

Stage 8: where

where count_distinct_dirpath >= 10

Stage 9: extend

extend DirSample
DirSample =
ifdirlist == 1tostring(dirlist[0])
elsestrcat("SeeDirListField", "_", tostring(hash(tostring(dirlist))))

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ProcessNamematch
  • sftp transforms: term
field:"process_name" kind:match value:"sftp"
SyslogMessagematch
  • bytes read transforms: term
  • close transforms: term
  • session opened for transforms: term
field:"SyslogMessage" kind:match
count_distinct_dirpathge
  • 10 transforms: number
field:"count_distinct_dirpath" kind:ge value:"10"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Computersummarize
count_distinct_dirpathsummarize
dirlistsummarize
src_ipsummarize
usernamesummarize
DirSampleextend

Suspicious access of BEC related documents in AWS S3 buckets

#
Severity
medium
Time window
14d
Group by
TimeStamp, UserIdentityPrincipalid
Source
github.com/Azure/Azure-Sentinel

This query looks for users with suspicious spikes in the number of files accessed that relate to topics commonly accessed as part of Business Email Compromise (BEC) attacks. The query looks for access to files in AWS S3 storage that relate to topics such as invoices or payments, and then looks for users accessing these files in significantly higher numbers than in the previous 14 days. Incidents raised by this analytic should be investigated to see if the user accessing these files should be accessing them, and if the volume they accessed them at was related to a legitimate business need. This query contains thresholds to reduce the chance of false positives, these can be adjusted to suit individual environments. In addition false positives could be generated by legitimate, scheduled actions that occur less often than every 14 days, additional exclusions can be added for these actions on username or IP address entities.

MITRE ATT&CK Coverage

TacticTechniques
Collection

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: f3e2d35f-1202-4215-995c-4654ef07d1d8
name: Suspicious access of BEC related documents in AWS S3 buckets
description: |
  'This query looks for users with suspicious spikes in the number of files accessed that relate to topics commonly accessed as part of Business Email Compromise (BEC) attacks.
  The query looks for access to files in AWS S3 storage that relate to topics such as invoices or payments, and then looks for users accessing these files in significantly higher numbers than in the previous 14 days. Incidents raised by this analytic should be investigated to see if the user accessing these files should be accessing them, and if the volume they accessed them at was related to a legitimate business need. 
  This query contains thresholds to reduce the chance of false positives, these can be adjusted to suit individual environments. In addition false positives could be generated by legitimate, scheduled actions that occur less often than every 14 days, additional exclusions can be added for these actions on username or IP address entities.'
severity: Medium
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
relevantTechniques:
  - T1530
eventGroupingSettings:
  aggregationKind: SingleAlert
query: |
  let BEC_Keywords = dynamic([ 'invoice','payment','paycheck','transfer','bank statement','bank details','closing','funds','bank account','account details','remittance','purchase','deposit',"PO#","Zahlung","Rechnung","Paiement", "virement bancaire","Bankuberweisung",'hacked','phishing']);
  // Adjust this threshold based on your environment
  let sensitivity = 2.5;
  let Events = materialize(AWSCloudTrail
  | where TimeGenerated between (ago(14d)..ago(0d))
  | where UserIdentityAccountId != "anonymous"
  | where EventSource startswith "s3."
  | where EventName =~ "GetObject"
  | extend FilePath = tostring(parse_json(RequestParameters).key)
  | where FilePath has_any(BEC_Keywords)
  );
  Events
  | summarize dcount(FilePath) by UserIdentityPrincipalid, bin(startofday(TimeGenerated), 1d)
  | summarize CountOfDocs = make_list(dcount_FilePath, 10000), TimeStamp = make_list(TimeGenerated, 10000) by UserIdentityPrincipalid
  | extend (Anomalies, Score, Baseline) = series_decompose_anomalies(CountOfDocs, sensitivity, -1, 'linefit')
  | mv-expand CountOfDocs to typeof(double), TimeStamp to typeof(datetime), Anomalies to typeof(double),Score to typeof(double), Baseline to typeof(long)
  | where Anomalies > 0
  | project TimeStamp, CountOfDocs, Baseline, Score, Anomalies, UserIdentityPrincipalid
  | join kind=inner(Events | extend TimeStamp = startofday(TimeGenerated)) on TimeStamp, UserIdentityPrincipalid
  | extend Name = iif(UserIdentityUserName contains "@", split(UserIdentityUserName, "@")[0], UserIdentityUserName)
  | extend UPNSuffix = iif(UserIdentityUserName contains "@", split(UserIdentityUserName, "@")[1], "")
  | project-reorder TimeGenerated, UserIdentityType, UserIdentityPrincipalid, UserIdentityUserName, FilePath, EventName, UserAgent, SourceIpAddress, CountOfDocs, Baseline, Score
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserIdentityUserName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: FilePath
customDetails:
  UserType: UserIdentityType
  Event: EventName
  UserAgent: UserAgent
alertDetailsOverride:
  alertDisplayNameFormat: Suspicious access of {{CountOfDocs}} BEC related documents in AWS S3 buckets by {{UserIdentityUserName}}
  alertDescriptionFormat: |
    This query looks for users (in this case {{UserIdentityUserName}}) with suspicious spikes in the number of files accessed (in this case {{CountOfDocs}})that relate to topics commonly accessed as part of Business Email Compromise (BEC) attacks. The query looks for access to files in AWS S3 storage that relate to topics such as invoices or payments, and then looks for users accessing these files in significantly higher numbers than in the previous 14 days. Incidents raised by this analytic should be investigated to see if the user accessing these files should be accessing them, and if the volume they accessed them at was related to a legitimate business need. 
    This query contains thresholds to reduce the chance of false positives, these can be adjusted to suit individual environments. In addition false positives could be generated by legitimate, scheduled actions that occur less often than every 14 days, additional exclusions can be added for these actions on username or IP address entities.
version: 1.0.4
kind: Scheduled

Stages and Predicates

Parameters

let sensitivity = 2.5;

let Events is inlined into the numbered stages below.

Let Binding: BEC_Keywords used in Stage 7

let BEC_Keywords = dynamic([ 'invoice','payment','paycheck','transfer','bank statement','bank details','closing','funds','bank account','account details','remittance','purchase','deposit',"PO#","Zahlung","Rechnung","Paiement", "virement bancaire","Bankuberweisung",'hacked','phishing']);

Stages 1 to 7 define let Events as the rule's main pipeline source. Stages 8 to 17 use it.

Stage 1: source

AWSCloudTrail

Stage 2: where

| where TimeGenerated between (ago(14d)..ago(0d))

Stage 3: where

| where UserIdentityAccountId != "anonymous"

Stage 4: where

| where EventSource startswith "s3."

Stage 5: where

| where EventName =~ "GetObject"

Stage 6: extend

| extend FilePath = tostring(parse_json(RequestParameters).key)

Stage 7: where

| where FilePath has_any(BEC_Keywords)

Stage 8: summarize

Events
| summarize dcount(FilePath) by UserIdentityPrincipalid, bin(startofday(TimeGenerated), 1d)

Stage 9: summarize

| summarize CountOfDocs = make_list(dcount_FilePath, 10000), TimeStamp = make_list(TimeGenerated, 10000) by UserIdentityPrincipalid

The stages below score time-series anomalies (make-series, series_decompose_anomalies).

Stage 10: extend

| extend (Anomalies, Score, Baseline) = series_decompose_anomalies(CountOfDocs, sensitivity, -1, 'linefit')

Stage 11: mv-expand

| mv-expand CountOfDocs to typeof(double), TimeStamp to typeof(datetime), Anomalies to typeof(double),Score to typeof(double), Baseline to typeof(long)

Stage 12: where

| where Anomalies > 0

Stage 13: project

| project TimeStamp, CountOfDocs, Baseline, Score, Anomalies, UserIdentityPrincipalid

Stage 14: join

| join kind=inner(Events | extend TimeStamp = startofday(TimeGenerated)) on TimeStamp, UserIdentityPrincipalid

Stage 15: extend

extend Name

Stage 16: extend

extend UPNSuffix

Stage 17: project-reorder

project-reorder

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Anomaliesgt
  • 0 transforms: number
field:"Anomalies" kind:gt value:"0"
EventNameeq
  • GetObject corpus 3 (splunk 1, kusto 1, panther 1)
  • GetObject
field:"aws::eventName" kind:eq
EventSourcestarts_with
  • s3.
  • s3.
field:"aws::eventSource" kind:starts_with
FilePathmatch
  • Bankuberweisung transforms: term
  • PO# transforms: term
  • Paiement transforms: term
  • Rechnung transforms: term
  • Zahlung transforms: term
  • account details transforms: term
  • bank account transforms: term
  • bank details transforms: term
  • bank statement transforms: term
  • closing transforms: term
  • deposit transforms: term
  • funds transforms: term
  • hacked transforms: term
  • invoice transforms: term
  • paycheck transforms: term
  • payment transforms: term
  • phishing transforms: term
  • purchase transforms: term
  • remittance transforms: term
  • transfer transforms: term
  • virement bancaire transforms: term
field:"FilePath" kind:match
UserIdentityAccountIdne
  • anonymous
field:"UserIdentityAccountId" kind:ne value:"anonymous"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Anomaliesproject
Baselineproject
CountOfDocsproject
Scoreproject
TimeStampproject
UserIdentityPrincipalidproject
Nameextend
UPNSuffixextend

Tailscale: Device key expiring within 7 days

#
Status
available
Severity
medium
Time window
6h
Group by
DeviceId
Source
github.com/Azure/Azure-Sentinel

Identifies tailnet devices whose machine key expires within the next 7 days and where key expiry is not disabled. Surface proactively so renewal can be scheduled rather than forced during an outage.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: b1a2c3d4-1234-5678-90ab-cdef12345001
name: "Tailscale: Device key expiring within 7 days"
description: |
  Identifies tailnet devices whose machine key expires within the next 7 days and where key expiry is not disabled. Surface proactively so renewal can be scheduled rather than forced during an outage.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: TailscaleCCF
    dataTypes:
      - Tailscale_Devices_CL
queryFrequency: 6h
queryPeriod: 6h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |
  Tailscale_Devices_CL
  | where TimeGenerated > ago(6h)
  | summarize arg_max(TimeGenerated, *) by DeviceId
  | where KeyExpiryDisabled == false
  | where isnotnull(Expires)
  | where Expires between (now() .. now() + 7d)
  | extend DaysToExpiry = datetime_diff('day', Expires, now())
  | project TimeGenerated, DeviceName, Hostname, User, Os, DaysToExpiry, Expires, LastSeen
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: User
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

Stage 1: source

Tailscale_Devices_CL

Stage 2: where

| where TimeGenerated > ago(6h)

Stage 3: summarize

| summarize arg_max(TimeGenerated, *) by DeviceId

Stage 4: where

| where KeyExpiryDisabled == false

Stage 5: where

| where isnotnull(Expires)

Stage 6: where

| where Expires between (now() .. now() + 7d)

Stage 7: extend

| extend DaysToExpiry = datetime_diff('day', Expires, now())

Stage 8: project

| project TimeGenerated, DeviceName, Hostname, User, Os, DaysToExpiry, Expires, LastSeen

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DaysToExpiryproject
DeviceNameproject
Expiresproject
Hostnameproject
LastSeenproject
Osproject
TimeGeneratedproject
Userproject

Trend Micro CAS - Multiple infected users

#
Status
available
Severity
high
Time window
15m
Group by
DstUserName, VirusName
Source
github.com/Azure/Azure-Sentinel

'Detects when same malware was detected for multiple user account.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 65c2a6fe-ff7b-46b0-9278-61265f77f3bc
name: Trend Micro CAS - Multiple infected users
description: |
  'Detects when same malware was detected for multiple user account.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: TrendMicroCAS
    dataTypes:
      - TrendMicroCAS
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  TrendMicroCAS
  | where EventType has_all ('virtual', 'analyzer')
  | where isnotempty(VirusName)
  | summarize count() by DstUserName, VirusName, bin(TimeGenerated, 15m)
  | where count_ >= 2
  | extend AccountCustomEntity = DstUserName, MalwareCustomEntity = VirusName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
  - entityType: Malware
    fieldMappings:
      - identifier: Name
        columnName: MalwareCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

TrendMicroCAS

Stage 2: where

| where EventType has_all ('virtual', 'analyzer')

Stage 3: where

| where isnotempty(VirusName)

Stage 4: summarize

| summarize count() by DstUserName, VirusName, bin(TimeGenerated, 15m)

Stage 5: where

| where count_ >= 2

Stage 6: extend

| extend AccountCustomEntity = DstUserName, MalwareCustomEntity = VirusName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventTypematch
  • analyzer transforms: term corpus 2 (kusto 2)
  • virtual transforms: term corpus 2 (kusto 2)
field:"EventType" kind:match
VirusNameis_not_null
  • (no value, null check)
field:"VirusName" kind:is_not_null
count_ge
  • 2 transforms: number
field:"count_" kind:ge value:"2"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstUserNamesummarize
VirusNamesummarize
AccountCustomEntityextend
MalwareCustomEntityextend

User or Group Added

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

Detects when a user or user group is added to Veeam Backup & Replication.

Rule Body

id: 25111c10-44b5-4275-996d-d62f15a7d072
name: User or Group Added
description: Detects when a user or user group is added to Veeam Backup & Replication.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 31200\n| project\n    Date\
  \ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n\
  \    EventId = instanceId,\n    UserName = user,\n  MessageDetails = Description,\n\
  \    Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 31200

Stage 3: project

| project
    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
    DataSource = original_host,
    EventId = instanceId,
    UserName = user,
  MessageDetails = Description,
    Severity = SeverityDescription

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
UserNameproject

Back to all Kusto rules