Detection rules › Kusto

Kusto rules: 01

Account Created and Deleted in Short Timeframe

#
Status
available
Severity
high
Time window
7d
Group by
UserId
Source
github.com/Azure/Azure-Sentinel

Search for user principal name (UPN) events. Look for accounts created and then deleted within 7 days. Attackers may create an account for their use, and then remove the account when no longer needed. Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#short-lived-account

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: bb616d82-108f-47d3-9dec-9652ea0d3bf6
name: Account Created and Deleted in Short Timeframe
description: |
  'Search for user principal name (UPN) events. Look for accounts created and then deleted within 7 days. Attackers may create an account for their use, and then remove the account when no longer needed.
  Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#short-lived-account'
severity: High
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
queryFrequency: 1h
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
relevantTechniques:
  - T1078.004
tags:
  - AADSecOpsGuide
query: |
  let queryfrequency = 1h;
  let queryperiod = 7d; // Increased queryperiod to 7 days to mitigate timing-based bypasses
  AuditLogs
  | where TimeGenerated > ago(queryfrequency)
  | where OperationName =~ "Delete user"
  | mv-apply TargetResource = TargetResources on 
    (
        where TargetResource.type == "User"
        // Normalize TargetUserPrincipalName by removing optional 32-char hex prefixes and underscores
        // Making extraction case-insensitive, and converting the result to lowercase for consistency
        | extend TargetUserPrincipalName = tolower(extract(@"(?i)([a-f0-9]{32})?_?(.*)", 2, tostring(TargetResource.userPrincipalName)))
        // Standardize UserId to ensure consistent, immutable identification
        | extend UserId = tostring(TargetResource.id)
    )
  | extend DeletedByApp = tostring(InitiatedBy.app.displayName),
  DeletedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId),
  DeletedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName),
  DeletedByAadUserId = tostring(InitiatedBy.user.id),
  DeletedByIPAddress = tostring(InitiatedBy.user.ipAddress)
  | project Deletion_TimeGenerated = TimeGenerated, TargetUserPrincipalName, UserId, DeletedByApp, DeletedByAppServicePrincipalId, DeletedByUserPrincipalName, DeletedByAadUserId, DeletedByIPAddress, 
  Deletion_AdditionalDetails = AdditionalDetails, Deletion_InitiatedBy = InitiatedBy, Deletion_TargetResources = TargetResources
  | join kind=inner (
      AuditLogs
      | where TimeGenerated > ago(queryperiod)
      | where OperationName =~ "Add user"      
      | mv-apply TargetResource = TargetResources on 
        (
            where TargetResource.type == "User"
            // Normalize TargetUserPrincipalName by removing optional 32-char hex prefixes and underscores
            // Making extraction case-insensitive, and converting the result to lowercase for consistency
            | extend TargetUserPrincipalName = tolower(extract(@"(?i)([a-f0-9]{32})?_?(.*)", 2, tostring(TargetResource.userPrincipalName)))
            // Standardize UserId to ensure consistent, immutable identification
            | extend UserId = tostring(TargetResource.id)
        )
      | project-rename Creation_TimeGenerated = TimeGenerated
  ) on UserId
  | extend TimeDelta = Deletion_TimeGenerated - Creation_TimeGenerated
  | where  TimeDelta between (time(0s) .. queryperiod)
  | extend CreatedByApp = tostring(InitiatedBy.app.displayName),
  CreatedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId),
  CreatedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName),
  CreatedByAadUserId = tostring(InitiatedBy.user.id),
  CreatedByIPAddress = tostring(InitiatedBy.user.ipAddress)
  | project Creation_TimeGenerated, Deletion_TimeGenerated, TimeDelta, UserId, TargetUserPrincipalName, DeletedByApp, DeletedByAppServicePrincipalId, DeletedByUserPrincipalName, DeletedByAadUserId, DeletedByIPAddress, 
  CreatedByApp, CreatedByAppServicePrincipalId, CreatedByUserPrincipalName, CreatedByAadUserId, CreatedByIPAddress, Creation_AdditionalDetails = AdditionalDetails, Creation_InitiatedBy = InitiatedBy, Creation_TargetResources = TargetResources, Deletion_AdditionalDetails, Deletion_InitiatedBy, Deletion_TargetResources
  | extend TargetName = tostring(split(TargetUserPrincipalName,'@',0)[0]), TargetUPNSuffix = tostring(split(TargetUserPrincipalName,'@',1)[0])
  | extend CreatedByName = tostring(split(CreatedByUserPrincipalName,'@',0)[0]), CreatedByUPNSuffix = tostring(split(CreatedByUserPrincipalName,'@',1)[0])
  | extend DeletedByName = tostring(split(DeletedByUserPrincipalName,'@',0)[0]), DeletedByUPNSuffix = tostring(split(DeletedByUserPrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: TargetUserPrincipalName
      - identifier: Name
        columnName: TargetName
      - identifier: UPNSuffix
        columnName: TargetUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: CreatedByUserPrincipalName
      - identifier: Name
        columnName: CreatedByName
      - identifier: UPNSuffix
        columnName: CreatedByUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: CreatedByAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: DeletedByUserPrincipalName
      - identifier: Name
        columnName: DeletedByName
      - identifier: UPNSuffix
        columnName: DeletedByUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: DeletedByAadUserId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CreatedByIPAddress
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: DeletedByIPAddress
version: 1.1.2
kind: Scheduled

Stages and Predicates

Parameters

let queryfrequency = 1h;
let queryperiod = 7d;

Stage 1: source

AuditLogs

Stage 2: where

| where TimeGenerated > ago(queryfrequency)

Stage 3: where

| where OperationName =~ "Delete user"

Stage 4: kusto:mv-apply

| mv-apply TargetResource = TargetResources on 
  (
      where TargetResource.type == "User"
      | extend TargetUserPrincipalName = tolower(extract(@"(?i)([a-f0-9]{32})?_?(.*)", 2, tostring(TargetResource.userPrincipalName)))
      | extend UserId = tostring(TargetResource.id)
  )

Stage 5: extend

| extend DeletedByApp = tostring(InitiatedBy.app.displayName),
DeletedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId),
DeletedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName),
DeletedByAadUserId = tostring(InitiatedBy.user.id),
DeletedByIPAddress = tostring(InitiatedBy.user.ipAddress)

Stage 6: project

| project Deletion_TimeGenerated = TimeGenerated, TargetUserPrincipalName, UserId, DeletedByApp, DeletedByAppServicePrincipalId, DeletedByUserPrincipalName, DeletedByAadUserId, DeletedByIPAddress, 
Deletion_AdditionalDetails = AdditionalDetails, Deletion_InitiatedBy = InitiatedBy, Deletion_TargetResources = TargetResources

Stage 7: join

| join kind=inner (
    AuditLogs
    | where TimeGenerated > ago(queryperiod)
    | where OperationName =~ "Add user"      
    | mv-apply TargetResource = TargetResources on 
      (
          where TargetResource.type == "User"
          | extend TargetUserPrincipalName = tolower(extract(@"(?i)([a-f0-9]{32})?_?(.*)", 2, tostring(TargetResource.userPrincipalName)))
          | extend UserId = tostring(TargetResource.id)
      )
    | project-rename Creation_TimeGenerated = TimeGenerated
) on UserId

Stage 8: extend

extend TimeDelta

Stage 9: where

where /* macro: (TimeDelta between (time(0s) .. 7d)) */

Stage 10: extend

extend CreatedByAadUserId, CreatedByApp, CreatedByAppServicePrincipalId, CreatedByIPAddress, CreatedByUserPrincipalName

Stage 11: project

project CreatedByAadUserId, CreatedByApp, CreatedByAppServicePrincipalId, CreatedByIPAddress, CreatedByUserPrincipalName, Creation_AdditionalDetails, Creation_InitiatedBy, Creation_TargetResources, Creation_TimeGenerated, DeletedByAadUserId, DeletedByApp, DeletedByAppServicePrincipalId, DeletedByIPAddress, DeletedByUserPrincipalName, Deletion_AdditionalDetails, Deletion_InitiatedBy, Deletion_TargetResources, Deletion_TimeGenerated, TargetUserPrincipalName, TimeDelta, UserId

Stage 12: extend (3 consecutive steps)

extend CreatedByName, CreatedByUPNSuffix, DeletedByName, DeletedByUPNSuffix, TargetName, TargetUPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
OperationNameeq
  • Add user
  • Delete user corpus 3 (kusto 3)
field:"OperationName" kind:eq
TargetResource.typeeq
  • User transforms: kusto_dynamic_path
field:"TargetResource.type" kind:eq value:"User"

Output Fields

These fields are emitted when the rule matches.

FieldSource
CreatedByAadUserIdproject
CreatedByAppproject
CreatedByAppServicePrincipalIdproject
CreatedByIPAddressproject
CreatedByUserPrincipalNameproject
Creation_AdditionalDetailsproject
Creation_InitiatedByproject
Creation_TargetResourcesproject
Creation_TimeGeneratedproject
DeletedByAadUserIdproject
DeletedByAppproject
DeletedByAppServicePrincipalIdproject
DeletedByIPAddressproject
DeletedByUserPrincipalNameproject
Deletion_AdditionalDetailsproject
Deletion_InitiatedByproject
Deletion_TargetResourcesproject
Deletion_TimeGeneratedproject
TargetUserPrincipalNameproject
TimeDeltaproject
UserIdproject
TargetNameextend
TargetUPNSuffixextend
CreatedByNameextend
CreatedByUPNSuffixextend
DeletedByNameextend
DeletedByUPNSuffixextend

Acronis - Multiple Inboxes with Malicious Content Detected

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

Many inboxes containing malicious content could indicate a potential ongoing phishing attack.

MITRE ATT&CK Coverage

Rule Body

id: 5090ad7b-4b47-4cab-9015-bffb43aecde8
name: Acronis - Multiple Inboxes with Malicious Content Detected
description: Many inboxes containing malicious content could indicate a potential ongoing phishing attack.
severity: Medium
query: |
    CommonSecurityLog
    | where DeviceVendor == "Acronis"
    | where DeviceEventClassID in
    ("MaliciousURLDetectedInM365MailboxBackup",
    "MalwareDetectedInM365MailboxBackup",
    "MaliciousEmailDetectedPerceptionPointWarning")
    | summarize MaliciousContent = count() by DeviceName
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 2
tactics:
    - InitialAccess
relevantTechniques:
    - T1566.002
    - T1566.001
incidentConfiguration:
    createIncident: true
    groupingConfiguration:
        enabled: true
        reopenClosedIncident: true
        lookbackDuration: P1D
        matchingMethod: AnyAlert
eventGroupingSettings:
    aggregationKind: SingleAlert
customDetails:
  DeviceName: DeviceName
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DeviceName
version: 1.0.0
kind: Scheduled
requiredDataConnectors: []

Stages and Predicates

Stage 1: source

CommonSecurityLog

Stage 2: where

| where DeviceVendor == "Acronis"

Stage 3: where

| where DeviceEventClassID in
("MaliciousURLDetectedInM365MailboxBackup",
"MalwareDetectedInM365MailboxBackup",
"MaliciousEmailDetectedPerceptionPointWarning")

Stage 4: summarize

| summarize MaliciousContent = count() by DeviceName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DeviceEventClassIDin
  • MaliciousEmailDetectedPerceptionPointWarning
  • MaliciousURLDetectedInM365MailboxBackup
  • MalwareDetectedInM365MailboxBackup
field:"DeviceEventClassID" kind:in
DeviceVendoreq
  • Acronis
field:"DeviceVendor" kind:eq value:"Acronis"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceNamesummarize
MaliciousContentsummarize

AWS role with admin privileges

#

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

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

The policy detects the creation of new AWS roles with administrative privileges. The policy configuration allows limiting the policy to specific accounts.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 734c00a0-a95b-44dd-9b69-d926ed44256d
name: AWS role with admin privileges
kind: Scheduled
description: The policy detects the creation of new AWS roles with administrative privileges. The policy configuration allows limiting the policy to specific accounts.
severity: High
status: Available
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |-
  Authomize_v2_CL
  | where ingestion_time() >= ago(30m)
  | extend EventID = id_s, Policy = policy_name_s, Severity = severity_s,Description = description_s,Recommendation = recommendation_s,URL = url_s,Tactics = tactics_s  
  | where Policy has "AWS role with admin privileges"
  | project  EventID, Policy, Severity, Description, Recommendation, URL, Category, Tactics
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: URL
requiredDataConnectors:
- connectorId: Authomize
  dataTypes: [ "Authomize_v2_CL" ]
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
  alertnameFormat: Alert from Authomize - AWS role with admin privileges
  alertDescriptionFormat: AWS role with admin privileges. The policy detects the creation of new AWS roles with administrative privileges. The policy configuration allows limiting the policy to specific accounts.
  alertSeverity: Severity
  alertTactics: Tactics
  alertDynamicProperties:
  - alertProperty: AlertLink
    value: URL
customDetails:
  AuthomizeEventID: EventID
  EventName: Policy
  EventDescription: Description
  EventRecommendation: Recommendation
  ReferencedURL: URL
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.2

Stages and Predicates

Stage 1: source

Authomize_v2_CL

Stage 2: where

| where ingestion_time() >= ago(30m)

Stage 3: extend

| extend EventID = id_s, Policy = policy_name_s, Severity = severity_s,Description = description_s,Recommendation = recommendation_s,URL = url_s,Tactics = tactics_s

Stage 4: where

| where Policy has "AWS role with admin privileges"

Stage 5: project

| project  EventID, Policy, Severity, Description, Recommendation, URL, Category, Tactics

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Policymatch
  • AWS role with admin privileges transforms: term
field:"policy_name_s" kind:match value:"AWS role with admin privileges"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Categoryproject
Descriptionproject
EventIDproject
Policyproject
Recommendationproject
Severityproject
Tacticsproject
URLproject

AWSCloudTrail - Login to AWS Management Console without MFA

#
Status
available
Severity
low
Time window
1d
Group by
AWSRegion, AccountName, AccountUPNSuffix, EventName, EventTypeName, LoginResult, MFAUsed, RecipientAccountId, SessionMfaAuthenticated, SourceIpAddress, UserAgent, UserIdentityAccountId, UserIdentityPrincipalid, UserIdentityUserName, indexId
Source
github.com/Azure/Azure-Sentinel

Identifies successful AWS Management Console sign-ins where CloudTrail records a ConsoleLogin event without multi-factor authentication. The rule looks for logins where MFAUsed is not Yes and the console response is not Failure, which can indicate credential misuse or weak account protection.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Persistence
Privilege Escalation
Stealth

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: d25b1998-a592-4bc5-8a3a-92b39eedb1bc
name: AWSCloudTrail - Login to AWS Management Console without MFA
description: |
  Identifies successful AWS Management Console sign-ins where CloudTrail records a ConsoleLogin event without
  multi-factor authentication. The rule looks for logins where MFAUsed is not Yes and the console response is not
  Failure, which can indicate credential misuse or weak account protection.
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
  - connectorId: AWSS3
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - PrivilegeEscalation
  - Persistence
  - InitialAccess
relevantTechniques:
  - T1078
query: |
  AWSCloudTrail
  | where EventName =~ "ConsoleLogin"
  | extend MFAUsed = tostring(parse_json(AdditionalEventData).MFAUsed), LoginResult = tostring(parse_json(ResponseElements).ConsoleLogin), indexId = indexof(tostring(UserIdentityPrincipalid),":")
  | where MFAUsed !~ "Yes" and LoginResult !~ "Failure"
  | where SessionIssuerUserName !contains "AWSReservedSSO"
  | extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
  | extend UserName = tostring(split(UserIdentityArn, '/')[-1])
  | extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
  | extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
    AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
  | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by EventName, EventTypeName, LoginResult, MFAUsed, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId,  UserIdentityPrincipalid, UserAgent,
  UserIdentityUserName, SessionMfaAuthenticated, SourceIpAddress, AWSRegion, indexId
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
  EventName: EventName
  LoginResult: LoginResult
  MFAUsed: MFAUsed
  AWSRegion: AWSRegion
  UserAgent: UserAgent
alertDetailsOverride:
  alertDisplayNameFormat: 'AWS Management Console sign-in without MFA: {{AccountName}} from {{SourceIpAddress}}'
  alertDescriptionFormat: 'Successful ConsoleLogin without MFA for {{AccountName}} from {{SourceIpAddress}} in account {{RecipientAccountId}}.'
version: 1.0.6
kind: Scheduled

Stages and Predicates

Stage 1: source

AWSCloudTrail

Stage 2: where

| where EventName =~ "ConsoleLogin"

Stage 3: extend

| extend MFAUsed = tostring(parse_json(AdditionalEventData).MFAUsed), LoginResult = tostring(parse_json(ResponseElements).ConsoleLogin), indexId = indexof(tostring(UserIdentityPrincipalid),":")

Stage 4: where

| where MFAUsed !~ "Yes" and LoginResult !~ "Failure"

Stage 5: where

| where SessionIssuerUserName !contains "AWSReservedSSO"

Stage 6: extend (4 consecutive steps)

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

Stage 7: summarize

| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by EventName, EventTypeName, LoginResult, MFAUsed, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId,  UserIdentityPrincipalid, UserAgent,
UserIdentityUserName, SessionMfaAuthenticated, SourceIpAddress, AWSRegion, indexId

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
SessionIssuerUserNamecontainsAWSReservedSSOexcludes:SessionIssuerUserName field:"SessionIssuerUserName" value:"AWSReservedSSO"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AWSRegionsummarize
AccountNamesummarize
AccountUPNSuffixsummarize
EndTimeUtcsummarize
EventNamesummarize
EventTypeNamesummarize
LoginResultsummarize
MFAUsedsummarize
RecipientAccountIdsummarize
SessionMfaAuthenticatedsummarize
SourceIpAddresssummarize
StartTimeUtcsummarize
UserAgentsummarize
UserIdentityAccountIdsummarize
UserIdentityPrincipalidsummarize
UserIdentityUserNamesummarize
indexIdsummarize

AWSCloudTrail - S3 bucket exposed via ACL

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

Detects S3 bucket ACL changes that grant public or broadly authenticated access. Overly permissive ACL assignments can expose bucket contents to unauthorized users.

MITRE ATT&CK Coverage

TacticTechniques
Exfiltration

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 6b9b4ee6-f4c1-4b86-8c8c-beb0bb59ae44
name: AWSCloudTrail - S3 bucket exposed via ACL
description: |
  Detects S3 bucket ACL changes that grant public or broadly authenticated access. Overly permissive ACL
  assignments can expose bucket contents to unauthorized users.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1537
query: |
  AWSCloudTrail
  | where EventName == "PutBucketAcl" and isempty(ErrorCode) and isempty(ErrorMessage)
  | extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
  | extend UserName = tostring(split(UserIdentityArn, '/')[-1])
  | extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
  | extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
    AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
  | extend Grant = parse_json(tostring((parse_json(RequestParameters).AccessControlPolicy))).AccessControlList.Grant
  | mvexpand Grant
  | extend cannedacl = parse_json(tostring((parse_json(RequestParameters))))
  | extend URI = parse_json(Grant).Grantee.URI, type = parse_json(Grant).Grantee.["xsi:type"], xamzacl = parse_json(cannedacl).["x-amz-acl"]
  | where (type == "Group" and (URI endswith "AllUsers" or URI endswith "AuthenticatedUsers"))
    or xamzacl in ("authenticated-read","public-read","public-read-write")
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
  EventName: EventName
  URI: URI
  xamzacl: xamzacl
  RecipientAccountId: RecipientAccountId
alertDetailsOverride:
  alertDisplayNameFormat: 'AWS S3 bucket ACL exposure change by {{AccountName}}'
  alertDescriptionFormat: 'Detected {{EventName}} from {{SourceIpAddress}} applying public or broad ACL on S3 bucket in account {{RecipientAccountId}}.'
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

AWSCloudTrail

Stage 2: where

| where EventName == "PutBucketAcl" and isempty(ErrorCode) and isempty(ErrorMessage)

Stage 3: extend (5 consecutive steps)

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

Stage 4: mv-expand

| mvexpand Grant

Stage 5: extend

| extend cannedacl = parse_json(tostring((parse_json(RequestParameters))))

Stage 6: extend

| extend URI = parse_json(Grant).Grantee.URI, type = parse_json(Grant).Grantee.["xsi:type"], xamzacl = parse_json(cannedacl).["x-amz-acl"]

Stage 7: where

| where (type == "Group" and (URI endswith "AllUsers" or URI endswith "AuthenticatedUsers"))
  or xamzacl in ("authenticated-read","public-read","public-read-write")

Indicators

These rows show field, operator, and value matches.

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
EventNameeq
  • PutBucketAcl
field:"aws::eventName" kind:eq value:"PutBucketAcl"
URIends_with
  • AllUsers
  • AuthenticatedUsers
field:"URI" kind:ends_with
typeeq
  • Group
field:"type" kind:eq value:"Group"
xamzaclin
  • authenticated-read
  • public-read
  • public-read-write
field:"xamzacl" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
UserIdentityArnextend
UserNameextend
AccountNameextend
AccountUPNSuffixextend
Grantextend
cannedaclextend
URIextend
typeextend
xamzaclextend

CertUtil Used for File Download (Living off the Land)

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

Detects certutil.exe being used to download files from remote URLs via the -urlcache or -verifyctl flags. CertUtil is a signed Windows binary (LOLBin) that attackers abuse to download payloads while bypassing application whitelisting and network controls. Tune AllowlistedDomains to match your PKI/CA infrastructure.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 4a9d3c2e-7f1b-4e58-9a0c-2d5b8e3f1a7c
name: CertUtil Used for File Download (Living off the Land)
description: |
  Detects certutil.exe being used to download files from remote URLs via the
  -urlcache or -verifyctl flags. CertUtil is a signed Windows binary (LOLBin)
  that attackers abuse to download payloads while bypassing application
  whitelisting and network controls.
  Tune AllowlistedDomains to match your PKI/CA infrastructure.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceProcessEvents
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
  - DefenseEvasion
relevantTechniques:
  - T1105
  - T1218
  - T1140
query: |
  let Lookback = 1h;
  let AllowlistedDomains = dynamic([
    "windowsupdate.microsoft.com",
    "download.microsoft.com",
    "dl.delivery.mp.microsoft.com",
    "ctldl.windowsupdate.com"
  ]);
  let MDE_Results =
    DeviceProcessEvents
    | where Timestamp >= ago(Lookback)
    | where FileName =~ "certutil.exe"
    | where ProcessCommandLine has_any ("-urlcache", "-verifyctl", "-decode", "-decodehex")
    | where ProcessCommandLine has_any ("http://", "https://", "ftp://")
    | where not(ProcessCommandLine has_any (AllowlistedDomains))
    | extend
        AccountName   = InitiatingProcessAccountName,
        AccountDomain = InitiatingProcessAccountDomain,
        HostName      = DeviceName,
        EventTime     = Timestamp
    | project EventTime, HostName, AccountName, AccountDomain,
        ProcessCommandLine, InitiatingProcessFileName;
  let SecEvent_Results =
    SecurityEvent
    | where TimeGenerated >= ago(Lookback)
    | where EventID == 4688
    | where NewProcessName endswith "\\certutil.exe"
    | where CommandLine has_any ("-urlcache", "-verifyctl", "-decode", "-decodehex")
    | where CommandLine has_any ("http://", "https://", "ftp://")
    | where not(CommandLine has_any (AllowlistedDomains))
    | extend
        AccountName   = SubjectUserName,
        AccountDomain = SubjectDomainName,
        HostName      = Computer,
        EventTime     = TimeGenerated,
        ProcessCommandLine = CommandLine,
        InitiatingProcessFileName = ParentProcessName
    | project EventTime, HostName, AccountName, AccountDomain,
        ProcessCommandLine, InitiatingProcessFileName;
  union MDE_Results, SecEvent_Results
  | sort by EventTime desc
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: HostName
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: NTDomain
        columnName: AccountDomain
  - entityType: Process
    fieldMappings:
      - identifier: CommandLine
        columnName: ProcessCommandLine
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let Lookback = 1h;

Let Binding: AllowlistedDomains

let AllowlistedDomains = dynamic([
  "windowsupdate.microsoft.com",
  "download.microsoft.com",
  "dl.delivery.mp.microsoft.com",
  "ctldl.windowsupdate.com"
]);

Union (2 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: MDE_Results, SecEvent_Results

Leg 1: MDE_Results

Leg 2: SecEvent_Results

Applied to the Combined Result

| sort by EventTime desc

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
CommandLinematch
  • -decode transforms: term
  • -decodehex transforms: term
  • -urlcache transforms: term
  • -verifyctl transforms: term
  • ftp:// transforms: term
  • http:// transforms: term
  • https:// transforms: term
field:"CommandLine" kind:match
EventIDeq
  • 4688
field:"EventID" kind:eq value:"4688"
FileNameeq
  • certutil.exe
field:"FileName" kind:eq value:"certutil.exe"
NewProcessNameends_with
  • \certutil.exe
field:"NewProcessName" kind:ends_with value:"\certutil.exe"
ProcessCommandLinematch
  • -decode transforms: term
  • -decodehex transforms: term
  • -urlcache transforms: term
  • -verifyctl transforms: term
  • ftp:// transforms: term
  • http:// transforms: term
  • https:// transforms: term
field:"ProcessCommandLine" kind:match

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ProcessCommandLinematchwindowsupdate.microsoft.com, download.microsoft.com, dl.delivery.mp.microsoft.com, ctldl.windowsupdate.comexcludes:ProcessCommandLine
CommandLinematchwindowsupdate.microsoft.com, download.microsoft.com, dl.delivery.mp.microsoft.com, ctldl.windowsupdate.comexcludes:CommandLine

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountDomainproject
AccountNameproject
EventTimeproject
HostNameproject
InitiatingProcessFileNameproject
ProcessCommandLineproject

Contrast ADR - DLP SQL Injection Correlation

#

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

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

'Detects successful SQL injection attacks identified by Contrast ADR and correlates them with WAF/DLP logs. This rule identifies critical database security breaches that have bypassed initial defenses and may result in data exfiltration or unauthorized database access.'

MITRE ATT&CK Coverage

Rule Body

id: 1aac7737-d52f-483d-b225-6a27c1b29a9e
name: Contrast ADR - DLP SQL Injection Correlation
description: |
  'Detects successful SQL injection attacks identified by Contrast ADR and correlates them with WAF/DLP logs. This rule identifies critical database security breaches that have bypassed initial defenses and may result in data exfiltration or unauthorized database access.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: ContrastADRCCF
    dataTypes:
      - ContrastADRAttackEvents_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - CredentialAccess
  - Collection
  - Exfiltration
  - CommandAndControl
  - Reconnaissance
  - CredentialAccess
  - LateralMovement
  - Discovery 
relevantTechniques:
  - T1190
  - T1552
  - T1005
  - T1041
  - T1008
  - T1590
  - T1571
  - T1528
  - T1021
  - T1046
query: |
  ContrastADRAttackEvents_CL
  | where result =~ "EXPLOITED" and rule =~ "SQL-INJECTION"
  | project-rename hostname = host_hostname
  //please add your DLP logs table in place of ContrastWAFLogs_CL and hostname colomun in place of hostname below and uncomment the queries below
  //| join kind= inner (ContrastWAFLogs_CL | where TimeGenerated >= ago(5m)) on hostname
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: sourceIp
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: hostname
alertDetailsOverride:
  alertDisplayNameFormat: 'Confirmed active SQL Injection by Contrast ADR on {{request_headers_referer}}  endpoint of {{application_name}} '
  alertDescriptionFormat: 'Confirmed active SQL Injection by Contrast ADR on {{request_headers_referer}}  endpoint of {{application_name}} '
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT30M
    matchingMethod: Selected
    groupByEntities:
      - Host
      - IP
eventGroupingSettings:
  aggregationKind: AlertPerResult
kind: Scheduled
version: 1.0.1

Stages and Predicates

Stage 1: source

ContrastADRAttackEvents_CL

Stage 2: where

| where result =~ "EXPLOITED" and rule =~ "SQL-INJECTION"

Stage 3: project-rename

| project-rename hostname = host_hostname

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
hostnameproject-rename

Firewall errors stateful anomaly on database

#
Status
available
Severity
medium
Time window
14d
Group by
ApplicationName, ClientIp, Database, HostName, PrincipalName, ResourceId, timeSlice
Source
github.com/Azure/Azure-Sentinel

'This query batches of distinct SQL queries that failed with error codes that might indicate malicious attempts to gain illegitimate access to the data. When attacker attempts to scan or gain access to server protected by firewall, he will be blocked by firewall and fail with error code 40615. Thus, if we see a large number of logins with such error codes, this could indicate attempts to gain access.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 20f87813-3de0-4a9f-a8c0-6aaa3187be08
name: Firewall errors stateful anomaly on database
description: |
  'This query batches of distinct SQL queries that failed with error codes that might indicate malicious attempts to gain illegitimate access to the data. When attacker attempts to scan or gain access to server protected by firewall, he will be blocked by firewall and fail with error code 40615. Thus, if we see a large number of logins with such error codes, this could indicate attempts to gain access.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureSql
    dataTypes:
      - AzureDiagnostics
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
relevantTechniques:
  - T1190 
tags:
  - SQL
query: |
    let monitoredStatementsThreshold = 1;           // Minimal number of monitored statements in the slice to trigger an anomaly.
    let trainingSlicesThreshold = 5;                // The maximal amount of slices with monitored statements in the training window before anomaly detection is throttled.
    let timeSliceSize = 1h;                         // The size of the single timeSlice for individual aggregation.
    let detectionWindow = 1h;                       // The size of the recent detection window for detecting anomalies.  
    let trainingWindow = detectionWindow + 14d;     // The size of the training window before the detection window for learning the normal state.
    let monitoredErrors = pack_array(40615);        // List of sql error codes relevant for this detection.
    let processedData = materialize (
        AzureDiagnostics
        | where TimeGenerated >= ago(trainingWindow)
        | where Category == 'SQLSecurityAuditEvents' and action_id_s has_any ("RCM", "BCM") // Keep only SQL affected rows
        | project TimeGenerated, PrincipalName = server_principal_name_s, ClientIp = client_ip_s, HostName = host_name_s, ResourceId,
                  ApplicationName = application_name_s, ActionName = action_name_s, Database = strcat(LogicalServerName_s, '/', database_name_s),
                  IsSuccess = succeeded_s, AffectedRows = affected_rows_d,
                  ResponseRows = response_rows_d, Statement = statement_s,
                  Error = case( additional_information_s has 'error_code', toint(extract("<error_code>([0-9.]+)", 1, additional_information_s))
                        , additional_information_s has 'failure_reason', toint(extract("<failure_reason>Err ([0-9.]+)", 1, additional_information_s))
                        , 0),
                  State = case( additional_information_s has 'error_state', toint(extract("<error_state>([0-9.]+)", 1, additional_information_s))
                        , additional_information_s has 'failure_reason', toint(extract("<failure_reason>Err ([0-9.]+), Level ([0-9.]+)", 2, additional_information_s))
                        , 0),
                  AdditionalInfo = additional_information_s, timeSlice = floor(TimeGenerated, timeSliceSize)
        | summarize countEvents = count(), countStatements = dcount(Statement), countStatementsWithError = dcountif(Statement, Error in (monitoredErrors))
            , anyMonitoredStatement = anyif(Statement, Error in (monitoredErrors)), anyInfo = anyif(AdditionalInfo, Error in (monitoredErrors))
            by Database, ClientIp, ApplicationName, PrincipalName, timeSlice,HostName,ResourceId
        | extend WindowType = case( timeSlice >= ago(detectionWindow), 'detection',
                                               (ago(trainingWindow) <= timeSlice and timeSlice < ago(detectionWindow)), 'training', 'other')
        | where WindowType in ('detection', 'training'));
    let trainingSet =
        processedData
        | where WindowType == 'training'
        | summarize countSlicesWithErrors = dcountif(timeSlice, countStatementsWithError >= monitoredStatementsThreshold)
            by Database;
    processedData
    | where WindowType == 'detection' 
    | join kind = inner (trainingSet) on Database
    | extend IsErrorAnomalyOnStatement = iff(((countStatementsWithError >= monitoredStatementsThreshold) and (countSlicesWithErrors <= trainingSlicesThreshold)), true, false)
        , anomalyScore = round(countStatementsWithError/monitoredStatementsThreshold, 0)
    | where IsErrorAnomalyOnStatement == 'true'
    | project TimeGenerated = timeSlice, Database, ClientIp, ApplicationName, PrincipalName, HostName, ResourceId, countEvents, countStatements, countStatementsWithError, anyMonitoredStatement, anyInfo, anomalyScore
    | extend Name = tostring(split(PrincipalName,'@',0)[0]), UPNSuffix = tostring(split(PrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: ApplicationName
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: ResourceId
alertDetailsOverride:
  alertDisplayNameFormat: 'Firewall errors stateful anomaly on database {{Database}}'
  alertDescriptionFormat: 'An anomaly was detected on database {{Database}} with {{countStatementsWithError}} statements with monitored errors in the last hour. Investigate the database activity for potential malicious attempts to gain illegitimate access to the data.'
version: 1.1.2
kind: Scheduled

Stages and Predicates

Parameters

let monitoredStatementsThreshold = 1;
let trainingSlicesThreshold = 5;
let timeSliceSize = 1h;
let detectionWindow = 1h;
let trainingWindow = detectionWindow + 14d;
let monitoredErrors = pack_array(40615);

let processedData is inlined into the numbered stages below.

Let Binding: trainingSet used in Stage 9

let trainingSet = processedData
    | where WindowType == 'training'
    | summarize countSlicesWithErrors = dcountif(timeSlice, countStatementsWithError >= monitoredStatementsThreshold)
        by Database;

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

Stage 1: source

AzureDiagnostics

Stage 2: where

| where TimeGenerated >= ago(trainingWindow)

Stage 3: where

| where Category == 'SQLSecurityAuditEvents' and action_id_s has_any ("RCM", "BCM")

Stage 4: project

| project TimeGenerated, PrincipalName = server_principal_name_s, ClientIp = client_ip_s, HostName = host_name_s, ResourceId,
              ApplicationName = application_name_s, ActionName = action_name_s, Database = strcat(LogicalServerName_s, '/', database_name_s),
              IsSuccess = succeeded_s, AffectedRows = affected_rows_d,
              ResponseRows = response_rows_d, Statement = statement_s,
              Error = case( additional_information_s has 'error_code', toint(extract("<error_code>([0-9.]+)", 1, additional_information_s))
                    , additional_information_s has 'failure_reason', toint(extract("<failure_reason>Err ([0-9.]+)", 1, additional_information_s))
                    , 0),
              State = case( additional_information_s has 'error_state', toint(extract("<error_state>([0-9.]+)", 1, additional_information_s))
                    , additional_information_s has 'failure_reason', toint(extract("<failure_reason>Err ([0-9.]+), Level ([0-9.]+)", 2, additional_information_s))
                    , 0),
              AdditionalInfo = additional_information_s, timeSlice = floor(TimeGenerated, timeSliceSize)

Stage 5: summarize

| summarize countEvents = count(), countStatements = dcount(Statement), countStatementsWithError = dcountif(Statement, Error in (monitoredErrors))
        , anyMonitoredStatement = anyif(Statement, Error in (monitoredErrors)), anyInfo = anyif(AdditionalInfo, Error in (monitoredErrors))
        by Database, ClientIp, ApplicationName, PrincipalName, timeSlice,HostName,ResourceId

Stage 6: extend

| extend WindowType = case( timeSlice >= ago(detectionWindow), 'detection',
                                           (ago(trainingWindow) <= timeSlice and timeSlice < ago(detectionWindow)), 'training', 'other')
WindowType =
if/* macro: (timeSlice >= ago(detectionWindow)) */'detection'
elif/* macro: (ago(trainingWindow) <= timeSlice) */'training'
else'other'

Stage 7: where

| where WindowType in ('detection', 'training')

Stage 8: where

processedData
| where WindowType == 'detection'

Stage 9: join

| join kind = inner (trainingSet) on Database

Stage 10: extend

extend IsErrorAnomalyOnStatement, anomalyScore
IsErrorAnomalyOnStatement =
ifcountStatementsWithError >= monitoredStatementsThreshold and countSlicesWithErrors <= trainingSlicesThresholdtrue
elsefalse

Stage 11: where

where IsErrorAnomalyOnStatement == true

Stage 12: project

project ApplicationName, ClientIp, Database, HostName, PrincipalName, ResourceId, TimeGenerated, anomalyScore, anyInfo, anyMonitoredStatement, countEvents, countStatements, countStatementsWithError

Stage 13: extend

extend Name, UPNSuffix

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ApplicationNameproject
ClientIpproject
Databaseproject
HostNameproject
PrincipalNameproject
ResourceIdproject
TimeGeneratedproject
anomalyScoreproject
anyInfoproject
anyMonitoredStatementproject
countEventsproject
countStatementsproject
countStatementsWithErrorproject
Nameextend
UPNSuffixextend

Hunt for RDP sessions to unmanaged and non TPM devices

#
Group by
$left.RemoteIP, $right.IPAddress, DeviceId
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

This query can help you find devices performing RDP sessions to unmanaged or non-TPM protected devices.

MITRE ATT&CK Coverage

References

Telemetry Coverage

Rule Body

let no_tpm_devices = (
    ExposureGraphNodes
    // Get device nodes with their inventory ID
    | where NodeLabel == "device"
    | mv-expand EntityIds
    | where EntityIds.type == "DeviceInventoryId"
    // Get interesting properties
    | extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]),
        TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]),
        TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]),
        TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]),
        DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]),
        DeviceId = tostring(EntityIds.id)
    // Search for distinct devices
    | distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated
    // Get Unmanaged devices and device not supporting a TPM
    | where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true")
    | extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported),
        TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated),
        TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled)
);
let no_tpm_device_info = (
    DeviceNetworkInfo
    | where Timestamp > ago(7d)
    // Get latest network info for each device ID
    | summarize arg_max(Timestamp, *) by DeviceId
    | mv-expand todynamic(IPAddresses)
    | extend IPAddress = tostring(IPAddresses.IPAddress)
    // Find no TPM devices and join with their network information
    | join kind=inner no_tpm_devices on DeviceId
    | project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported
);
DeviceNetworkEvents
// Search for RDP connections to non-tpm devices
| where Timestamp > ago(1h)
| where ActionType == "ConnectionSuccess"
| where RemotePort == 3389
// Exclude MDI RDP Connections (known for NNR)
| where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe"
| join kind=inner no_tpm_device_info on $left.RemoteIP == $right.IPAddress
| project-rename RemoteDeviceId = DeviceId1, RemoteDeviceName = DeviceName1, RemoteMacAddress = MacAddress, RemoteDeviceOnboardingStatus = OnboardingStatus, RemoteDeviceTpmActivated = TpmActivated, RemoteDeviceTpmEnabled = TpmEnabled, RemoteDeviceTpmSupported = TpmSupported
| project-away IPAddress

let no_tpm_devices = (
    ExposureGraphNodes
    // Get device nodes with their inventory ID
    | where NodeLabel == "device"
    | mv-expand EntityIds
    | where EntityIds.type == "DeviceInventoryId"
    // Get interesting properties
    | extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]),
        TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]),
        TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]),
        TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]),
        DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]),
        DeviceId = tostring(EntityIds.id)
    // Search for distinct devices
    | distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated
    // Get Unmanaged devices and device not supporting a TPM
    | where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true")
    | extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported),
        TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated),
        TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled)
);
let no_tpm_device_info = (
    DeviceNetworkInfo
    | where TimeGenerated > ago(7d)
    // Get latest network info for each device ID
    | summarize arg_max(TimeGenerated, *) by DeviceId
    | mv-expand todynamic(IPAddresses)
    | extend IPAddress = tostring(IPAddresses.IPAddress)
    // Find no TPM devices and join with their network information
    | join kind=inner no_tpm_devices on DeviceId
    | project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported
);
DeviceNetworkEvents
// Search for RDP connections to non-tpm devices
| where TimeGenerated > ago(1h)
| where ActionType == "ConnectionSuccess"
| where RemotePort == 3389
// Exclude MDI RDP Connections (known for NNR)
| where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe"
| join kind=inner no_tpm_device_info on $left.RemoteIP == $right.IPAddress
| project-rename RemoteDeviceId = DeviceId1, RemoteDeviceName = DeviceName1, RemoteMacAddress = MacAddress, RemoteDeviceOnboardingStatus = OnboardingStatus, RemoteDeviceTpmActivated = TpmActivated, RemoteDeviceTpmEnabled = TpmEnabled, RemoteDeviceTpmSupported = TpmSupported
| project-away IPAddress

Stages and Predicates

Let Binding: no_tpm_devices used in Stage 1

let no_tpm_devices = (
    ExposureGraphNodes
    | where NodeLabel == "device"
    | mv-expand EntityIds
    | where EntityIds.type == "DeviceInventoryId"
    | extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]),
        TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]),
        TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]),
        TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]),
        DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]),
        DeviceId = tostring(EntityIds.id)
    | distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated
    | where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true")
    | extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported),
        TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated),
        TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled)
);

Let Binding: no_tpm_device_info used in Stages 2, 8

let no_tpm_device_info = (
    DeviceNetworkInfo
    | where Timestamp > ago(7d)
    | summarize arg_max(Timestamp, *) by DeviceId
    | mv-expand todynamic(IPAddresses)
    | extend IPAddress = tostring(IPAddresses.IPAddress)
    | join kind=inner no_tpm_devices on DeviceId
    | project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported
);

Stage 1: source

let no_tpm_devices

Stage 2: source

let no_tpm_device_info

Stage 3: source

DeviceNetworkEvents

Stage 4: where

where Timestamp > ago(3600s)

Stage 5: where

where ActionType == "ConnectionSuccess"

Stage 6: where

where RemotePort == 3389

Stage 7: where

where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe"

Stage 8: join

join kind=inner (no_tpm_device_info) on $left.RemoteIP, $right.IPAddress

Stage 9: project-rename

project-rename

Stage 10: project-away

project-away IPAddress

Stage 11: summarize aggregation inside the join branch

summarize by DeviceId

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceIdsummarize

Palo Alto Prisma Cloud - Maximum risk score alert

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

'Detects alerts with maximum risk score value.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 119a574d-f37a-403a-a67a-4d6f5083d9cf
name: Palo Alto Prisma Cloud - Maximum risk score alert
description: |
  'Detects alerts with maximum risk score value.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: PaloAltoPrismaCloud
    dataTypes:
      - PaloAltoPrismaCloud
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1133
query: |
  PaloAltoPrismaCloud
  | where Reason =~ 'NEW_ALERT'
  | where Status =~ 'open'
  | where RiskDetailRiskScoreScore == RiskDetailRiskScoreMaxScore
  | extend AccountCustomEntity = UserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

PaloAltoPrismaCloud

Stage 2: where

| where Reason =~ 'NEW_ALERT'

Stage 3: where

| where Status =~ 'open'

Stage 4: where

| where RiskDetailRiskScoreScore == RiskDetailRiskScoreMaxScore

Stage 5: extend

| extend AccountCustomEntity = UserName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend

Prestige ransomware IOCs Oct 2022

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

This query looks for file hashes and AV signatures associated with Prestige ransomware payload.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: bca9c877-2afc-4246-a26d-087ab1cdcd5f
name: Prestige ransomware IOCs Oct 2022
description: |
  'This query looks for file hashes and AV signatures associated with Prestige ransomware payload.'
severity: High
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceEvents
      - DeviceFileEvents
      - DeviceImageLoadEvents
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvents  
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1203
tags:
  - Prestige
  - Ransomware
query: |
  let sha256Hashes = dynamic(["5dd1ca0d471dee41eb3ea0b6ea117810f228354fc3b7b47400a812573d40d91d", "5fc44c7342b84f50f24758e39c8848b2f0991e8817ef5465844f5f2ff6085a57", "6cff0bbd62efe99f381e5cc0c4182b0fb7a9a34e4be9ce68ee6b0d0ea3eee39c"]);
  let signames = dynamic(["Ransom:Win32/Prestige"]);
  (union isfuzzy=true
  (CommonSecurityLog
  | where FileHash in (sha256Hashes)
  | project TimeGenerated, Message, SourceUserID, FileHash, Type
  | extend timestamp = TimeGenerated, Algorithm = "SHA256", AccountNTName = SourceUserID
  ),
  (imFileEvent
  | where TargetFileSHA256 has_any (sha256Hashes)
  | extend AccountNT = ActorUsername, Computer = DvcHostname, IPAddress = SrcIpAddr, CommandLine = ActingProcessCommandLine, FileHash = TargetFileSHA256
  | project Type, TimeGenerated, Computer, AccountNT, IPAddress, CommandLine, FileHash, Algorithm = "SHA256"
  ),
  (Event
  | where Source =~ "Microsoft-Windows-Sysmon"
  | where EventID == 1
  | extend EvData = parse_xml(EventData)
  | extend EventDetail = EvData.DataItem.EventData.Data
  | extend ProcessId = tolong(EventDetail.[3].["#text"]), Image = tostring(EventDetail.[4].["#text"]), CommandLine = tostring(EventDetail.[10].["#text"]), Hashes = tostring(EventDetail.[17].["#text"])
  | extend Hashes = extract_all(@"(?P<key>\w+)=(?P<value>[a-zA-Z0-9]+)", dynamic(["key","value"]), Hashes)
  | extend Hashes = column_ifexists("Hashes", dynamic(["", ""])), CommandLine = column_ifexists("CommandLine", "")
  | mv-expand Hashes
  | where Hashes[0] =~ "SHA256" and Hashes[1] has_any (sha256Hashes)  
  | project TimeGenerated, EventDetail, UserName, Computer, Type, Source, ProcessId, Hashes, CommandLine, Image
  | extend Type = strcat(Type, ": ", Source)
  | extend AccountNT = UserName, InitiatingProcessId = ProcessId
  | extend Process = tostring(split(Image, '\\', -1)[-1]), Algorithm = "SHA256", FileHash = tostring(Hashes[1]) 
  ),
  (DeviceEvents
  | where InitiatingProcessSHA256 has_any (sha256Hashes) or SHA256 has_any (sha256Hashes)
  | project TimeGenerated, ActionType, DeviceId, DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessSHA256, Type
  | extend AccountNT = InitiatingProcessAccountName, Computer = DeviceName
  | extend Algorithm = "SHA256", FileHash = tostring(InitiatingProcessSHA256), CommandLine = InitiatingProcessCommandLine, Image = InitiatingProcessFolderPath
  ),
  (DeviceFileEvents
  | where SHA256 has_any (sha256Hashes)
  | project TimeGenerated, ActionType, DeviceId, DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessSHA256, Type
  | extend AccountNT = InitiatingProcessAccountName, Computer = DeviceName
  | extend Algorithm = "SHA256", FileHash = tostring(InitiatingProcessSHA256), CommandLine = InitiatingProcessCommandLine, Image = InitiatingProcessFolderPath
  ),
  (DeviceImageLoadEvents
  | where SHA256 has_any (sha256Hashes)
  | project TimeGenerated, ActionType, DeviceId, DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessSHA256, Type
  | extend AccountNT = InitiatingProcessAccountName, Computer = DeviceName
  | extend Algorithm = "SHA256", FileHash = tostring(InitiatingProcessSHA256), CommandLine = InitiatingProcessCommandLine, Image = InitiatingProcessFolderPath
  ),
  (SecurityAlert
  | where ProductName == "Microsoft Defender Advanced Threat Protection"
  | extend ThreatName = tostring(parse_json(ExtendedProperties).ThreatName)
  | where isnotempty(ThreatName)
  | where ThreatName has_any (signames)
  | extend Computer = tostring(parse_json(Entities)[0].HostName)
  )
  )
  | extend AccountNTName = tostring(split(AccountNT, "\\")[0]), AccountNTDomain = tostring(split(AccountNT, "\\")[1])
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | project-away DomainIndex
entityMappings:
  - entityType: FileHash
    fieldMappings:
      - identifier: Algorithm
        columnName: Algorithm
      - identifier: Value
        columnName: FileHash
  - entityType: Process
    fieldMappings:
      - identifier: ProcessId
        columnName: InitiatingProcessId
      - identifier: CommandLine
        columnName: CommandLine
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountNT
      - identifier: Name
        columnName: AccountNTName
      - identifier: UPNSuffix
        columnName: AccountNTDomain
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: NTDomain
        columnName: HostNameDomain
version: 1.0.4
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others" ]

Stages and Predicates

Parameters

let signames = dynamic(["Ransom:Win32/Prestige"]);

Let Binding: sha256Hashes

let sha256Hashes = dynamic(["5dd1ca0d471dee41eb3ea0b6ea117810f228354fc3b7b47400a812573d40d91d", "5fc44c7342b84f50f24758e39c8848b2f0991e8817ef5465844f5f2ff6085a57", "6cff0bbd62efe99f381e5cc0c4182b0fb7a9a34e4be9ce68ee6b0d0ea3eee39c"]);

Union isfuzzy=true (7 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: CommonSecurityLog, imFileEvent, Event, DeviceEvents, DeviceFileEvents, DeviceImageLoadEvents, SecurityAlert

Leg 1: CommonSecurityLog

CommonSecurityLog
| where FileHash in (sha256Hashes)
| project TimeGenerated, Message, SourceUserID, FileHash, Type
| extend timestamp = TimeGenerated, Algorithm = "SHA256", AccountNTName = SourceUserID

Leg 2: imFileEvent

imFileEvent
| where TargetFileSHA256 has_any (sha256Hashes)
| extend AccountNT = ActorUsername, Computer = DvcHostname, IPAddress = SrcIpAddr, CommandLine = ActingProcessCommandLine, FileHash = TargetFileSHA256
| project Type, TimeGenerated, Computer, AccountNT, IPAddress, CommandLine, FileHash, Algorithm = "SHA256"

Leg 3: Event

Event
| where Source =~ "Microsoft-Windows-Sysmon"
| where EventID == 1
| extend EvData = parse_xml(EventData)
| extend EventDetail = EvData.DataItem.EventData.Data
| extend ProcessId = tolong(EventDetail.[3].["#text"]), Image = tostring(EventDetail.[4].["#text"]), CommandLine = tostring(EventDetail.[10].["#text"]), Hashes = tostring(EventDetail.[17].["#text"])
| extend Hashes = extract_all(@"(?P<key>\w+)=(?P<value>[a-zA-Z0-9]+)", dynamic(["key","value"]), Hashes)
| extend Hashes = column_ifexists("Hashes", dynamic(["", ""])), CommandLine = column_ifexists("CommandLine", "")
| mv-expand Hashes
| where Hashes[0] =~ "SHA256" and Hashes[1] has_any (sha256Hashes)  
| project TimeGenerated, EventDetail, UserName, Computer, Type, Source, ProcessId, Hashes, CommandLine, Image
| extend Type = strcat(Type, ": ", Source)
| extend AccountNT = UserName, InitiatingProcessId = ProcessId
| extend Process = tostring(split(Image, '\\', -1)[-1]), Algorithm = "SHA256", FileHash = tostring(Hashes[1])

Leg 4: DeviceEvents

DeviceEvents
| where InitiatingProcessSHA256 has_any (sha256Hashes) or SHA256 has_any (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessSHA256, Type
| extend AccountNT = InitiatingProcessAccountName, Computer = DeviceName
| extend Algorithm = "SHA256", FileHash = tostring(InitiatingProcessSHA256), CommandLine = InitiatingProcessCommandLine, Image = InitiatingProcessFolderPath

Leg 5: DeviceFileEvents

DeviceFileEvents
| where SHA256 has_any (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessSHA256, Type
| extend AccountNT = InitiatingProcessAccountName, Computer = DeviceName
| extend Algorithm = "SHA256", FileHash = tostring(InitiatingProcessSHA256), CommandLine = InitiatingProcessCommandLine, Image = InitiatingProcessFolderPath

Leg 6: DeviceImageLoadEvents

DeviceImageLoadEvents
| where SHA256 has_any (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessSHA256, Type
| extend AccountNT = InitiatingProcessAccountName, Computer = DeviceName
| extend Algorithm = "SHA256", FileHash = tostring(InitiatingProcessSHA256), CommandLine = InitiatingProcessCommandLine, Image = InitiatingProcessFolderPath

Leg 7: SecurityAlert

SecurityAlert
| where ProductName == "Microsoft Defender Advanced Threat Protection"
| extend ThreatName = tostring(parse_json(ExtendedProperties).ThreatName)
| where isnotempty(ThreatName)
| where ThreatName has_any (signames)
| extend Computer = tostring(parse_json(Entities)[0].HostName)

Applied to the Combined Result

| extend AccountNTName = tostring(split(AccountNT, "\\")[0]), AccountNTDomain = tostring(split(AccountNT, "\\")[1])
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| project-away DomainIndex

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIDeq
  • 1 corpus 12 (kusto 11, elastic 1)
field:"EventID" kind:eq value:"1"
FileHashin
  • 5dd1ca0d471dee41eb3ea0b6ea117810f228354fc3b7b47400a812573d40d91d
  • 5fc44c7342b84f50f24758e39c8848b2f0991e8817ef5465844f5f2ff6085a57
  • 6cff0bbd62efe99f381e5cc0c4182b0fb7a9a34e4be9ce68ee6b0d0ea3eee39c
field:"FileHash" kind:in
Hashes[0]eq
  • SHA256 transforms: kusto_dynamic_path
field:"Hashes[0]" kind:eq value:"SHA256"
Hashes[1]match
  • 5dd1ca0d471dee41eb3ea0b6ea117810f228354fc3b7b47400a812573d40d91d transforms: term, kusto_dynamic_path
  • 5fc44c7342b84f50f24758e39c8848b2f0991e8817ef5465844f5f2ff6085a57 transforms: term, kusto_dynamic_path
  • 6cff0bbd62efe99f381e5cc0c4182b0fb7a9a34e4be9ce68ee6b0d0ea3eee39c transforms: term, kusto_dynamic_path
field:"Hashes[1]" kind:match
InitiatingProcessSHA256match
  • 5dd1ca0d471dee41eb3ea0b6ea117810f228354fc3b7b47400a812573d40d91d transforms: term
  • 5fc44c7342b84f50f24758e39c8848b2f0991e8817ef5465844f5f2ff6085a57 transforms: term
  • 6cff0bbd62efe99f381e5cc0c4182b0fb7a9a34e4be9ce68ee6b0d0ea3eee39c transforms: term
field:"InitiatingProcessSHA256" kind:match
ProductNameeq
  • Microsoft Defender Advanced Threat Protection
field:"ProductName" kind:eq value:"Microsoft Defender Advanced Threat Protection"
SHA256match
  • 5dd1ca0d471dee41eb3ea0b6ea117810f228354fc3b7b47400a812573d40d91d transforms: term
  • 5fc44c7342b84f50f24758e39c8848b2f0991e8817ef5465844f5f2ff6085a57 transforms: term
  • 6cff0bbd62efe99f381e5cc0c4182b0fb7a9a34e4be9ce68ee6b0d0ea3eee39c transforms: term
field:"sha256" kind:match
TargetFileSHA256match
  • 5dd1ca0d471dee41eb3ea0b6ea117810f228354fc3b7b47400a812573d40d91d transforms: term
  • 5fc44c7342b84f50f24758e39c8848b2f0991e8817ef5465844f5f2ff6085a57 transforms: term
  • 6cff0bbd62efe99f381e5cc0c4182b0fb7a9a34e4be9ce68ee6b0d0ea3eee39c transforms: term
field:"sha256" kind:match
ThreatNameis_not_null
  • (no value, null check)
field:"ThreatName" kind:is_not_null
ThreatNamematch
  • signames transforms: term
field:"ThreatName" kind:match value:"signames"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActionTypeproject
DeviceIdproject
DeviceNameproject
InitiatingProcessAccountDomainproject
InitiatingProcessAccountNameproject
InitiatingProcessCommandLineproject
InitiatingProcessFileNameproject
InitiatingProcessFolderPathproject
InitiatingProcessIdproject
InitiatingProcessParentFileNameproject
InitiatingProcessSHA256project
TimeGeneratedproject
Typeproject
AccountNTextend
Computerextend
Algorithmextend
CommandLineextend
FileHashextend
Imageextend
ThreatNameextend
AccountNTDomainextend
AccountNTNameextend
HostNameextend
HostNameDomainextend

PROD (TM006.1) - ACL - Modified at Tier Level OU

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

ACL modification of any Tier Model OU.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: f3e2433b-6456-4009-a1cb-0d4eb3a36125
name: 'PROD (TM006.1) - ACL - Modified at Tier Level OU'
description: |
  ACL modification of any Tier Model OU.
severity: High
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1222.001
query: |
  SecurityEvent 
  | where EventID == 5136 
      and (EventData matches regex @"(?i)OU=Domain Controllers"
          or EventData matches regex @"(?i)OU=Tier 0 Member Servers"
          or EventData matches regex @"(?i)OU=Tier 1 Member Servers"
          or EventData matches regex @"(?i)OU=Tier 2 End-User Accounts"
          or EventData matches regex @"(?i)OU=Tier 2 End-User Devices"
          or EventData matches regex @"(?i)OU=Tier 2 End-User Groups"
          or EventData matches regex @"(?i)OU=Tier Model Administration"
          or EventData matches regex @"(?i)OU=Tier Model Computer Quarantine")
      and Activity has "modified" 
      and EventData has '"ObjectClass">organizationalUnit'
      and EventData has '<Data Name="AttributeLDAPDisplayName">nTSecurityDescriptor</Data>'
      and EventData has '"OperationType">%%14674'
  | extend ObjectName = extract("(?i)<Data Name=\"ObjectDN\">(((?i)OU=[^,]+,?)+),[dD][cC]=", 1, EventData)
  | extend ObjectTier = case(
      ObjectName has "Tier 0", "Tier 0",
      ObjectName has "Tier 1", "Tier 1",
      ObjectName has "Tier 2", "Tier 2",
      ObjectName has "Tier Model Administration", "Tier 0",
      ObjectName has "Tier Model Computer Quarantine", "Tier 0",
      ObjectName has "Domain Controllers", "Tier 0",
      "Unknown")
  | extend AttributeLDAP = extract("<Data Name=\"AttributeLDAPDisplayName\">([^<]+)</Data>", 1, EventData)
  | extend AttributeValue = extract("\\[([^\\]]+)\\]", 1, EventData)
  | extend OperationType = iff(EventData has 'nTSecurityDescriptor', "ACL Modified", tostring(OperationType))
  | extend ActivityType = extract("([a-zA-Z]+)\\.$", 1, Activity)
  | extend ObjectClass = extract("<Data Name=\"ObjectClass\">([^<]+)</Data>", 1, EventData)
  | extend Domain = extract("([^.]+\\.[^.]+)$", 1, Computer)
  | project TimeGenerated
     , ObjectName
     , ObjectTier
     , ObjectClass
     , OperationType
     , ActivityType
     , AttributeLDAP
     , Account
     , Domain
     , Computer
     , Channel
     , EventID
     , EventData
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ObjectName
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Computer
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
customDetails:
  ObjectName: ObjectName
  ObjectTier: ObjectTier
  ObjectClass: ObjectClass
  OperationType: OperationType
  ActivityType: ActivityType
  AttributeLDAP: AttributeLDAP
  Account: Account
  Domain: Domain
  Computer: Computer
  Channel: Channel
  EventID: EventID
  EventData: EventData
eventGroupingSettings:
  aggregationKind: AlertPerResult
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
alertDetailsOverride:
  alertDisplayNameFormat: '(TM006.1) A {{ObjectTier}} OU ACL was modified at the following {{ObjectName}} OU'
  alertDescriptionFormat: 'Modification to any Tier Model OU ACL.'
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

SecurityEvent

Stage 2: where

| where EventID == 5136 
    and (EventData matches regex @"(?i)OU=Domain Controllers"
        or EventData matches regex @"(?i)OU=Tier 0 Member Servers"
        or EventData matches regex @"(?i)OU=Tier 1 Member Servers"
        or EventData matches regex @"(?i)OU=Tier 2 End-User Accounts"
        or EventData matches regex @"(?i)OU=Tier 2 End-User Devices"
        or EventData matches regex @"(?i)OU=Tier 2 End-User Groups"
        or EventData matches regex @"(?i)OU=Tier Model Administration"
        or EventData matches regex @"(?i)OU=Tier Model Computer Quarantine")
    and Activity has "modified" 
    and EventData has '"ObjectClass">organizationalUnit'
    and EventData has '<Data Name="AttributeLDAPDisplayName">nTSecurityDescriptor</Data>'
    and EventData has '"OperationType">%%14674'

Stage 3: extend (8 consecutive steps)

| extend ObjectName = extract("(?i)<Data Name=\"ObjectDN\">(((?i)OU=[^,]+,?)+),[dD][cC]=", 1, EventData)
| extend ObjectTier = case(
    ObjectName has "Tier 0", "Tier 0",
    ObjectName has "Tier 1", "Tier 1",
    ObjectName has "Tier 2", "Tier 2",
    ObjectName has "Tier Model Administration", "Tier 0",
    ObjectName has "Tier Model Computer Quarantine", "Tier 0",
    ObjectName has "Domain Controllers", "Tier 0",
    "Unknown")
| extend AttributeLDAP = extract("<Data Name=\"AttributeLDAPDisplayName\">([^<]+)</Data>", 1, EventData)
| extend AttributeValue = extract("\\[([^\\]]+)\\]", 1, EventData)
| extend OperationType = iff(EventData has 'nTSecurityDescriptor', "ACL Modified", tostring(OperationType))
| extend ActivityType = extract("([a-zA-Z]+)\\.$", 1, Activity)
| extend ObjectClass = extract("<Data Name=\"ObjectClass\">([^<]+)</Data>", 1, EventData)
| extend Domain = extract("([^.]+\\.[^.]+)$", 1, Computer)

Stage 4: project

| project TimeGenerated
   , ObjectName
   , ObjectTier
   , ObjectClass
   , OperationType
   , ActivityType
   , AttributeLDAP
   , Account
   , Domain
   , Computer
   , Channel
   , EventID
   , EventData

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Activitymatch
  • modified transforms: term corpus 10 (kusto 10)
field:"Activity" kind:match value:"modified"
EventDatamatch
  • "ObjectClass">organizationalUnit transforms: term corpus 5 (kusto 5)
  • "OperationType">%%14674 transforms: term corpus 7 (kusto 7)
  • <Data Name="AttributeLDAPDisplayName">nTSecurityDescriptor</Data> transforms: term corpus 3 (kusto 3)
field:"EventData" kind:match
EventDataregex_match
  • (?i)OU=Domain Controllers corpus 8 (kusto 8)
  • (?i)OU=Tier 0 Member Servers corpus 8 (kusto 8)
  • (?i)OU=Tier 1 Member Servers corpus 8 (kusto 8)
  • (?i)OU=Tier 2 End-User Accounts corpus 3 (kusto 3)
  • (?i)OU=Tier 2 End-User Devices corpus 3 (kusto 3)
  • (?i)OU=Tier 2 End-User Groups corpus 3 (kusto 3)
  • (?i)OU=Tier Model Administration corpus 3 (kusto 3)
  • (?i)OU=Tier Model Computer Quarantine corpus 3 (kusto 3)
field:"EventData" kind:regex_match
EventIDeq
  • 5136 corpus 20 (kusto 18, elastic 2)
field:"EventID" kind:eq value:"5136"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountproject
ActivityTypeproject
AttributeLDAPproject
Channelproject
Computerproject
Domainproject
EventDataproject
EventIDproject
ObjectClassproject
ObjectNameproject
ObjectTierproject
OperationTypeproject
TimeGeneratedproject

Successful AWS Console Login from IP Address Observed Conducting Password Spray

#
Severity
medium
Time window
1m
Group by
$left.EntityIp, $right.SourceIpAddress, AccountObjectId, AlertName, AlertSeverity, AlertTime, AlertTimeGenerated, EntityIp, ProductName, ProviderName, Tactics, Techniques
Source
github.com/Azure/Azure-Sentinel

This query aims to detect instances of successful AWS console login events followed by multiple failed app logons alerts generated by Microsoft Cloud App Security or password spray alerts generated by Defender Products. Specifically, it focuses on scenarios where the successful login takes place within a 60-minute timeframe of the high-severity alert. The login is considered relevant if it originates from an IP address associated with potential attackers.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Credential Access

Telemetry Coverage

Rule Body

id: 188db479-d50a-4a9c-a041-644bae347d1f
name: Successful AWS Console Login from IP Address Observed Conducting Password Spray
description: |
  'This query aims to detect instances of successful AWS console login events followed by multiple failed app logons alerts generated by Microsoft Cloud App Security or password spray alerts generated by Defender Products.
   Specifically, it focuses on scenarios where the successful login takes place within a 60-minute timeframe of the high-severity alert. 
   The login is considered relevant if it originates from an IP address associated with potential attackers.'
severity: Medium
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
  - connectorId: MicrosoftDefenderAdvancedThreatProtection
    dataTypes:
      - SecurityAlert
  - connectorId: AzureActiveDirectoryIdentityProtection
    dataTypes:
      - SecurityAlert (IPC)
  - connectorId: BehaviorAnalytics
    dataTypes:
      - IdentityInfo
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - SecurityAlert
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - CredentialAccess
relevantTechniques:
  - T1110
  - T1078
query: |
  SecurityAlert 
  // Filtering alerts based on Microsoft product names and Relevent alert names
    | where ProductName in ( "Microsoft Cloud App Security","Azure Active Directory Identity Protection")
    |where AlertName in ("Multiple failed user log on attempts to an app","Password Spray")
  // Parsing and extending the 'Entities' column as JSON objects
    | extend Entities = parse_json(Entities) 
  // Exploring IP entities within the alert entities
    | mv-apply Entity = Entities on 
        ( 
        where Entity.Type == 'ip' 
        | extend EntityIp = tostring(Entity.Address) 
        ) 
  // Exploring account entities within the alert entities
    | mv-apply Entity = Entities on 
        ( 
        where Entity.Type == 'account' 
        | extend AccountObjectId = tostring(Entity.AadUserId)
        )
  // Filtering out alerts with missing IP or account information
    | where isnotempty(EntityIp) and isnotempty(AccountObjectId)
  // Summarizing relevant fields for further analysis
    | summarize 
        by 
        AlertName,
        ProductName,
        ProviderName,
        AlertSeverity,
        EntityIp,
        Tactics,
        Techniques,
        AlertTime= bin(TimeGenerated, 1min),
        AccountObjectId,
        AlertTimeGenerated=TimeGenerated
  // Joining with IdentityInfo to obtain additional account details
    | join kind=inner (
        IdentityInfo
        | where TimeGenerated >= ago(1d)
        | distinct AccountObjectId, AccountUPN=tolower(AccountUPN)
        )
        on AccountObjectId 
        |extend Name = tostring(split(AccountUPN,'@')[0]), UPNSuffix =tostring(split(AccountUPN,'@')[1])
  // Joining with AWSCloudTrail data to correlate AWS console logins
    | join kind=inner (
        AWSCloudTrail
        | where EventName == "ConsoleLogin"
        | extend CTUPN= tolower(tostring(tolower(split(UserIdentityArn, "/", 2)[0])))
        | extend ActionType= tostring(parse_json(ResponseElements).ConsoleLogin)  
        | where ActionType == "Success"
        | extend AWSTime= bin(TimeGenerated, 1min)
        | project
            EventName,
            EventSource,
            EventTypeName,
            RecipientAccountId,
            ResponseElements,
            SessionMfaAuthenticated,
            SourceIpAddress,
            TimeGenerated,
            UserAgent,
            UserIdentityArn,
            UserIdentityType,
            CTUPN,
            AWSTime,
            UserIdentityUserName
        )
        on $left.EntityIp == $right.SourceIpAddress 
  // Filtering login event after the Alert generation time
    | where AlertTimeGenerated between ((AWSTime - 1h)..(AWSTime + 1h))
  // Calculating the time difference between alert generation and AWS login
    | extend timediff = datetime_diff('minute', AlertTimeGenerated, TimeGenerated) 
  // Filtering alerts with a time difference of up to 60 minutes
    | where timediff <= 60
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountUPN
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
   AWSUser: UserIdentityArn
   UserAgent: UserAgent
   AWSUserUPN: CTUPN
kind: Scheduled
version: 1.0.1

Stages and Predicates

Stage 1: source

SecurityAlert

Stage 2: where

| where ProductName in ( "Microsoft Cloud App Security","Azure Active Directory Identity Protection")

Stage 3: where

| where AlertName in ("Multiple failed user log on attempts to an app","Password Spray")

Stage 4: extend

| extend Entities = parse_json(Entities)

Stage 5: kusto:mv-apply

| mv-apply Entity = Entities on 
      ( 
      where Entity.Type == 'ip' 
      | extend EntityIp = tostring(Entity.Address) 
      )

Stage 6: kusto:mv-apply

| mv-apply Entity = Entities on 
      ( 
      where Entity.Type == 'account' 
      | extend AccountObjectId = tostring(Entity.AadUserId)
      )

Stage 7: where

| where isnotempty(EntityIp) and isnotempty(AccountObjectId)

Stage 8: summarize

| summarize 
      by 
      AlertName,
      ProductName,
      ProviderName,
      AlertSeverity,
      EntityIp,
      Tactics,
      Techniques,
      AlertTime= bin(TimeGenerated, 1min),
      AccountObjectId,
      AlertTimeGenerated=TimeGenerated

Stage 9: join

| join kind=inner (
      IdentityInfo
      | where TimeGenerated >= ago(1d)
      | distinct AccountObjectId, AccountUPN=tolower(AccountUPN)
      )
      on AccountObjectId

Stage 10: extend

extend Name, UPNSuffix

Stage 11: join

| join kind=inner (
      AWSCloudTrail
      | where EventName == "ConsoleLogin"
      | extend CTUPN= tolower(tostring(tolower(split(UserIdentityArn, "/", 2)[0])))
      | extend ActionType= tostring(parse_json(ResponseElements).ConsoleLogin)  
      | where ActionType == "Success"
      | extend AWSTime= bin(TimeGenerated, 1min)
      | project
          EventName,
          EventSource,
          EventTypeName,
          RecipientAccountId,
          ResponseElements,
          SessionMfaAuthenticated,
          SourceIpAddress,
          TimeGenerated,
          UserAgent,
          UserIdentityArn,
          UserIdentityType,
          CTUPN,
          AWSTime,
          UserIdentityUserName
      )
      on $left.EntityIp == $right.SourceIpAddress

Stage 12: where

where /* macro: (AlertTimeGenerated between ((AWSTime - 1h) .. (AWSTime + 1h))) */

Stage 13: extend

extend timediff

Stage 14: where

where timediff <= 60

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AccountObjectIdis_not_null
  • (no value, null check)
field:"AccountObjectId" kind:is_not_null
ActionTypeeq
  • Success
field:"ActionType" kind:eq value:"Success"
AlertNamein
  • Multiple failed user log on attempts to an app
  • Password Spray
field:"AlertName" kind:in
Entity.Typeeq
  • account transforms: kusto_dynamic_path
  • ip transforms: kusto_dynamic_path
field:"Entity.Type" kind:eq
EntityIpis_not_null
  • (no value, null check)
field:"EntityIp" kind:is_not_null
EventNameeq
  • ConsoleLogin
field:"EventName" kind:eq value:"ConsoleLogin"
ProductNamein
  • Azure Active Directory Identity Protection corpus 3 (kusto 3)
  • Microsoft Cloud App Security corpus 3 (kusto 3)
field:"ProductName" kind:in
timediffle
  • 60 transforms: number
field:"timediff" kind:le value:"60"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountObjectIdsummarize
AlertNamesummarize
AlertSeveritysummarize
AlertTimesummarize
AlertTimeGeneratedsummarize
EntityIpsummarize
ProductNamesummarize
ProviderNamesummarize
Tacticssummarize
Techniquessummarize
Nameextend
UPNSuffixextend
timediffextend

Theom Insights

#

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

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

"Creates Microsoft Sentinel incidents for Theom insight alerts."

MITRE ATT&CK Coverage

Rule Body

id: d200da84-0191-44ce-ad9e-b85e64c84c89
name: Theom Insights
description: |
  "Creates Microsoft Sentinel incidents for Theom insight alerts."
severity: Low
status : Available
requiredDataConnectors: 
  - connectorId: Theom
    dataTypes:
      - TheomAlerts_CL
queryPeriod: 5m
queryFrequency: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
  - CommandAndControl
  - CredentialAccess
  - DefenseEvasion
  - Discovery
  - Exfiltration
  - Impact
  - Reconnaissance
relevantTechniques:
  - T1592
  - T1589
  - T1070
  - T1552
  - T1619
  - T1119
  - T1560
  - T1530
  - T1213
  - T1001
  - T1041
  - T1537
  - T1485
  - T1486
  - T1565
query: |
  TheomAlerts_CL
   | where priority_s == "P5"
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: "Theom Alert ID: {{id_s}} "
  alertDescriptionFormat: "\nSummary: {{summary_s}}  \nAdditional info: {{details_s}}\nResource ID: {{customProps_AssetName_s}}\n"
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: customProps_AssetName_s
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: deepLink_s
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

TheomAlerts_CL

Stage 2: where

| where priority_s == "P5"

Indicators

These rows show field, operator, and value matches.

TI Map Domain entity to SecurityAlert

#
Severity
medium
Time window
14d
Group by
$left.DomainName, $right.domain, AlertName, Id, ObservableValue
Source
github.com/Azure/Azure-Sentinel

'Identifies a match in SecurityAlert table from any Domain IOC from TI'

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: df88b403-1cb9-49ea-a43d-b6613051cf7f
name: TI Map Domain entity to SecurityAlert
description: |
  'Identifies a match in SecurityAlert table from any Domain IOC from TI'
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: MicrosoftCloudAppSecurity
    dataTypes:
      - SecurityAlert
  - connectorId: AzureSecurityCenter
    dataTypes:
      - SecurityAlert
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let SecurityAlerts = SecurityAlert
  | where TimeGenerated > ago(dt_lookBack)
  | where AlertName != "TI map Domain entity to SecurityAlert"
  | extend domain = todynamic(dynamic_to_json(extract_all(@"(((xn--)?[a-z0-9\-]+\.)+([a-z]+|(xn--[a-z0-9]+)))", dynamic([1]), tolower(Entities))))
  | where isnotempty(domain)
  | mv-expand domain
  | extend domain = tostring(domain)
  | extend EntitiesDynamicArray = parse_json(Entities)
  | mv-apply EntitiesDynamicArray on
      (summarize
          HostName = take_anyif(tostring(EntitiesDynamicArray.HostName), EntitiesDynamicArray.Type == "host"),
          IP_addr = take_anyif(tostring(EntitiesDynamicArray.Address), EntitiesDynamicArray.Type == "ip")
      )
  | extend Alert_TimeGenerated = TimeGenerated
  | extend Alert_Description = Description;
  let AlertDomains = SecurityAlerts
  | distinct domain
  | summarize make_list(domain);
  let Domain_Indicators = materialize(ThreatIntelIndicators
  | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
  | where IndicatorType == "domain-name"
  | extend DomainName = tolower(ObservableValue)
  | where TimeGenerated >= ago(ioc_lookBack)
  | where DomainName in (AlertDomains)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
  | where IsActive and (ValidUntil > now() or isempty(ValidUntil))
  | extend Description = tostring(parse_json(Data).description)
  | where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;");
  Domain_Indicators
  // Using innerunique to keep performance fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
  | join kind=innerunique (SecurityAlerts) on $left.DomainName == $right.domain
  | where Alert_TimeGenerated < ValidUntil
  | summarize Alert_TimeGenerated = arg_max(Alert_TimeGenerated, *) by Id, AlertName
  | extend ActivityGroupNames = extract("ActivityGroup:([^,]+)", 1, tostring(parse_json(Data).labels))
  | extend Url = DomainName
  | project Alert_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, ConfidenceScore, DomainName, AlertName, Alert_Description, ProviderName, AlertSeverity, ConfidenceLevel, HostName, IP_addr, Url, Entities, Type
  | extend timestamp = Alert_TimeGenerated
  | project-reorder *, ConfidenceScore, DomainName, AlertName, Alert_TimeGenerated, ProviderName, AlertSeverity, Entities, Type, timestamp
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IP_addr
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.4.7
kind: Scheduled

Stages and Predicates

Parameters

let dt_lookBack = 1h;
let ioc_lookBack = 14d;

let Domain_Indicators is inlined into the numbered stages below.

Let Binding: SecurityAlerts used in Stage 11

let SecurityAlerts = SecurityAlert
| where TimeGenerated > ago(dt_lookBack)
| where AlertName != "TI map Domain entity to SecurityAlert"
| extend domain = todynamic(dynamic_to_json(extract_all(@"(((xn--)?[a-z0-9\-]+\.)+([a-z]+|(xn--[a-z0-9]+)))", dynamic([1]), tolower(Entities))))
| where isnotempty(domain)
| mv-expand domain
| extend domain = tostring(domain)
| extend EntitiesDynamicArray = parse_json(Entities)
| mv-apply EntitiesDynamicArray on
    (summarize
        HostName = take_anyif(tostring(EntitiesDynamicArray.HostName), EntitiesDynamicArray.Type == "host"),
        IP_addr = take_anyif(tostring(EntitiesDynamicArray.Address), EntitiesDynamicArray.Type == "ip")
    )
| extend Alert_TimeGenerated = TimeGenerated
| extend Alert_Description = Description;

Let Binding: AlertDomains used in Stage 6

let AlertDomains = SecurityAlerts
| distinct domain
| summarize make_list(domain);

Stages 1 to 10 define let Domain_Indicators as the rule's main pipeline source. Stages 11 to 18 use it.

Stage 1: source

ThreatIntelIndicators

Stage 2: extend

| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))

Stage 3: where

| where IndicatorType == "domain-name"

Stage 4: extend

| extend DomainName = tolower(ObservableValue)

Stage 5: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 6: where

| where DomainName in (AlertDomains)

Stage 7: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue

Stage 8: where

| where IsActive and (ValidUntil > now() or isempty(ValidUntil))

Stage 9: extend

| extend Description = tostring(parse_json(Data).description)

Stage 10: where

| where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"

Stage 11: join

Domain_Indicators
| join kind=innerunique (SecurityAlerts) on $left.DomainName == $right.domain

Stage 12: where

where Alert_TimeGenerated < ValidUntil

Stage 13: summarize

summarize Alert_TimeGenerated by Id, AlertName

Stage 14: extend

extend ActivityGroupNames

Stage 15: extend

extend Url

Stage 16: project

project ActivityGroupNames, AlertName, AlertSeverity, Alert_Description, Alert_TimeGenerated, ConfidenceLevel, ConfidenceScore, Description, DomainName, Entities, HostName, IP_addr, Id, ProviderName, Type, Url, ValidUntil

Stage 17: extend

extend timestamp

Stage 18: project-reorder

project-reorder

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DescriptioncontainsState: falsepos;excludes:Description field:"Description" value:"State: falsepos;"
DescriptioncontainsState: inactive;excludes:Description field:"Description" value:"State: inactive;"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
AlertNameproject
AlertSeverityproject
Alert_Descriptionproject
Alert_TimeGeneratedproject
ConfidenceLevelproject
ConfidenceScoreproject
Descriptionproject
DomainNameproject
Entitiesproject
HostNameproject
IP_addrproject
Idproject
ProviderNameproject
Typeproject
Urlproject
ValidUntilproject
timestampextend

Unusual Volume of file deletion by users

#
Status
available
Severity
high
Time window
5m
Group by
AccountDisplayName, ActionType, ApplicationId, SessionId, Time, UserId
Source
github.com/Azure/Azure-Sentinel

This query looks for users performing file deletion activities. Spikes in file deletion observed from risky sign-in sessions are flagged here. This applies to SharePoint and OneDrive users. Audit event and Cloud application identifier references. Reference - https://learn.microsoft.com/microsoft-365/compliance/audit-log-activities?view=o365-worldwide Reference - https://learn.microsoft.com/azure/sentinel/entities-reference#cloud-application-identifiers

MITRE ATT&CK Coverage

TacticTechniques
Impact

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: e5f8e196-3544-4a8b-96a9-17c1b6a49710
name: Unusual Volume of file deletion by users
description: |
  This query looks for users performing file deletion activities. Spikes in file deletion observed from risky sign-in sessions are flagged here.
  This applies to SharePoint and OneDrive users.
  Audit event and Cloud application identifier references.
  Reference - https://learn.microsoft.com/microsoft-365/compliance/audit-log-activities?view=o365-worldwide
  Reference - https://learn.microsoft.com/azure/sentinel/entities-reference#cloud-application-identifiers
severity: High
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - CloudAppEvents
      - AADSignInEventsBeta
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1485
query: |
  let relevantOperations = pack_array("FileDeleted", "FileRecycled", "FileDeletedFirstStageRecycleBin", "FileDeletedSecondStageRecycleBin", "FileVersionsAllMinorsRecycled", "FileVersionRecycled", "FileVersionsAllRecycled");
  let relevantAppIds = pack_array(int(20892), int(15600)); // App Ids for SharePoint and OneDrive
  let timeWindow = 7d;
  let timeNow = now();
  //
  let riskyUsers= // Look for users with risky sign-ins
    SigninLogs    
    | where CreatedDateTime between ((timeNow - timeWindow) .. (timeNow))
    | where isnotempty(UserId) and isnotempty(OriginalRequestId)
    | where ResultType == '0'
    | where RiskLevelDuringSignIn == 'high'
    | project UserId, RiskLevelDuringSignIn, CreatedDateTime, SessionId=OriginalRequestId
    ;
  let hasUsers = isnotempty(toscalar(riskyUsers));
  //
  let deleteEvents = // look for file deletion activity and scope it to risky users
    CloudAppEvents
    | where hasUsers
    | where TimeGenerated between ((timeNow - timeWindow) .. (timeNow))
    | where ApplicationId in (relevantAppIds)
    | where isnotempty(AccountObjectId)
    | where AccountObjectId in (riskyUsers)
    | where ActionType in (relevantOperations)
    | extend SessionId= tostring(RawEventData.AppAccessContext.AADSessionId)
    | where isnotempty(SessionId)
    | project UserId=AccountObjectId, AccountDisplayName, ApplicationId, SessionId, ActionType, TimeGenerated, ReportId
    ;   
   //
  deleteEvents  
  | join kind=leftsemi riskyUsers on UserId, SessionId
  | summarize Count=count() , (Timestamp, ReportId)=arg_min(TimeGenerated, ReportId) by UserId, AccountDisplayName, ApplicationId, ActionType, Time=bin(TimeGenerated, 5m)
  // look for only those scoped users who have generated an increase in file deletion activity.
  | summarize TotalCount= countif(Count > 50), (Timestamp, ReportId)=arg_min(Timestamp, ReportId) by UserId, AccountDisplayName, ApplicationId 
  | where TotalCount >= 3
  | project UserId, AccountDisplayName, ApplicationId, TotalCount, ReportId, Timestamp
  | extend NTDomain = tostring(split(AccountDisplayName,'\\',0)[0]), Name = tostring(split(AccountDisplayName,'\\',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: UserId
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: NTDomain
        columnName: NTDomain
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: ApplicationId
customDetails:
  Count: TotalCount
version: 1.0.3
kind: Scheduled

Stages and Predicates

Parameters

let relevantAppIds = pack_array(int(20892), int(15600));
let timeWindow = 7d;
let timeNow = now();
let hasUsers = isnotempty(toscalar(riskyUsers));

let deleteEvents is inlined into the numbered stages below.

Let Binding: relevantOperations used in Stage 8

let relevantOperations = pack_array("FileDeleted", "FileRecycled", "FileDeletedFirstStageRecycleBin", "FileDeletedSecondStageRecycleBin", "FileVersionsAllMinorsRecycled", "FileVersionRecycled", "FileVersionsAllRecycled");

Let Binding: riskyUsers used in Stages 7, 12

let riskyUsers = SigninLogs    
  | where CreatedDateTime between ((timeNow - timeWindow) .. (timeNow))
  | where isnotempty(UserId) and isnotempty(OriginalRequestId)
  | where ResultType == '0'
  | where RiskLevelDuringSignIn == 'high'
  | project UserId, RiskLevelDuringSignIn, CreatedDateTime, SessionId=OriginalRequestId;

Stage 1: source

let deleteEvents

Stage 2: source

CloudAppEvents

Stage 3: where

where hasUsers == true

Stage 4: where

where /* macro: (TimeGenerated between ((timeNow - 7d) .. timeNow)) */

Stage 5: where

where ApplicationId == "relevantAppIds"

Stage 6: where

where isnotempty(AccountObjectId)

Stage 7: where

where AccountObjectId == "riskyUsers"

Stage 8: where

where ActionType == "relevantOperations"

Stage 9: extend

extend SessionId

Stage 10: where

where isnotempty(SessionId)

Stage 11: project

project AccountDisplayName, ActionType, ApplicationId, ReportId, SessionId, TimeGenerated, UserId

Stage 12: join

join kind=leftsemi (riskyUsers) on UserId, SessionId

Stage 13: summarize

summarize Count, ReportId, Time, Timestamp by UserId, AccountDisplayName, ApplicationId, ActionType, Time

Stage 14: summarize

summarize ReportId, Timestamp, TotalCount by UserId, AccountDisplayName, ApplicationId

Stage 15: where

where TotalCount >= 3

Stage 16: project

project AccountDisplayName, ApplicationId, ReportId, Timestamp, TotalCount, UserId

Stage 17: extend

extend NTDomain, Name

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountDisplayNameproject
ApplicationIdproject
ReportIdproject
Timestampproject
TotalCountproject
UserIdproject
NTDomainextend
Nameextend

Vaikora - Feed outage detection

#

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

Status
available
Severity
low
Time window
12h
Source
github.com/Azure/Azure-Sentinel

Identifies when no Vaikora data has arrived in the Vaikora_AgentSignals_CL table for 12 or more hours, which may indicate a failed playbook, expired API key, or connectivity issue.

Rule Body

id: 5f7789fa-0a6b-4dff-a2da-dfa4b682f3af
name: Vaikora - Feed outage detection
description: |
  Identifies when no Vaikora data has arrived in the Vaikora_AgentSignals_CL table for 12 or more hours, which may indicate a failed playbook, expired API key, or connectivity issue.
severity: Low
requiredDataConnectors:
  - connectorId: VaikoraSecurityCenter
    dataTypes:
      - Vaikora_AgentSignals_CL
queryFrequency: 12h
queryPeriod: 12h
triggerOperator: lt
triggerThreshold: 1
status: Available
tactics: []
relevantTechniques: []
query: |
  Vaikora_AgentSignals_CL
  | where TimeGenerated >= ago(12h)
  | summarize Count = count()
  | where Count == 0
  | extend
      Alert      = "No Vaikora data ingested in the last 12 hours",
      Suggestion = "Check the VaikoraToAzureSecurityCenter Logic App run history and verify the Vaikora API key is valid."
  | project Alert, Suggestion
alertDetailsOverride:
  alertDisplayNameFormat: "Vaikora feed outage detected"
  alertDescriptionFormat: "Vaikora_AgentSignals_CL has received no records in the last 12 hours."
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

Vaikora_AgentSignals_CL

Stage 2: where

| where TimeGenerated >= ago(12h)

Stage 3: summarize

| summarize Count = count()

Stage 4: where

| where Count == 0

Stage 5: extend

| extend
    Alert      = "No Vaikora data ingested in the last 12 hours",
    Suggestion = "Check the VaikoraToAzureSecurityCenter Logic App run history and verify the Vaikora API key is valid."

Stage 6: project

| project Alert, Suggestion

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Counteq
  • 0
field:"Count" kind:eq value:"0"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Alertproject
Suggestionproject

Vaikora - Quarantine rate spike

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

Identifies an abnormal spike in Vaikora for O365 quarantine volume for a tenant. Fires when the hourly count exceeds three times the rolling 7-day baseline, signaling a targeted campaign or model surge worth admin review.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 3b3eb1cd-578d-4198-9b7a-bd7253b0dc9f
name: Vaikora - Quarantine rate spike
description: |
  Identifies an abnormal spike in Vaikora for O365 quarantine volume for a tenant. Fires when the hourly count exceeds three times the rolling 7-day baseline, signaling a targeted campaign or model surge worth admin review.
severity: Medium
requiredDataConnectors:
  - connectorId: VaikoraO365
    dataTypes:
      - VaikoraO365_Quarantine_CL
queryFrequency: 1h
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  let baseline = VaikoraO365_Quarantine_CL
    | where TimeGenerated between (ago(7d) .. ago(1h))
    | summarize HourlyCount = count() by bin(TimeGenerated, 1h), TenantId_s
    | summarize AvgHourlyCount = avg(HourlyCount) by TenantId_s;
  VaikoraO365_Quarantine_CL
  | where TimeGenerated >= ago(1h)
  | summarize RecentCount = count() by TenantId_s
  | join kind=inner baseline on TenantId_s
  | where RecentCount > (AvgHourlyCount * 3.0)
  | where RecentCount >= 10
  | project TimeGenerated = now(), TenantId = TenantId_s, RecentCount, AvgHourlyCount, RatioOverBaseline = (RecentCount / AvgHourlyCount)
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: TenantId
alertDetailsOverride:
  alertDisplayNameFormat: "Vaikora-O365 quarantine spike on tenant {{TenantId}}"
  alertDescriptionFormat: "Quarantine count for the last hour is {{RecentCount}}, which is more than three times the 7-day baseline of {{AvgHourlyCount}}."
version: 1.0.0
kind: Scheduled

Stages and Predicates

Let Binding: baseline used in Stage 4

let baseline = VaikoraO365_Quarantine_CL
  | where TimeGenerated between (ago(7d) .. ago(1h))
  | summarize HourlyCount = count() by bin(TimeGenerated, 1h), TenantId_s
  | summarize AvgHourlyCount = avg(HourlyCount) by TenantId_s;

Stage 1: source

VaikoraO365_Quarantine_CL

Stage 2: where

| where TimeGenerated >= ago(1h)

Stage 3: summarize

| summarize RecentCount = count() by TenantId_s

Stage 4: join

| join kind=inner baseline on TenantId_s

Stage 5: where

where RecentCount > (AvgHourlyCount * 3.0)

Stage 6: where

where RecentCount >= 10

Stage 7: project

project AvgHourlyCount, RatioOverBaseline, RecentCount, TenantId, TimeGenerated

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
RecentCountge
  • 10 transforms: number
field:"RecentCount" kind:ge value:"10"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AvgHourlyCountproject
RatioOverBaselineproject
RecentCountproject
TenantIdproject
TimeGeneratedproject

Vectra Host's Behaviors

#
Status
available
Severity
informational
Time window
5m
Group by
Activity, source_entity
Source
github.com/Azure/Azure-Sentinel

'This analytic rule is looking for new attacker behaviors observed by the Vectra Platform. This rule is focused on host's detections.'

MITRE ATT&CK Coverage

Rule Body

id: 33e3b6da-2660-4cd7-9032-11be76db88d2
name: Vectra Host's Behaviors
description: |
  'This analytic rule is looking for new attacker behaviors observed by the Vectra Platform. This rule is focused on host's detections.'
severity: Informational
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics:
  - CredentialAccess
  - Discovery
  - LateralMovement
  - Collection
  - CommandAndControl
  - Exfiltration
  - Impact
relevantTechniques:
  - T1003
  - T1087
  - T1021
  - T1119
  - T1071
  - T1041
  - T1499
query: |
  CommonSecurityLog
  | where DeviceVendor == "Vectra Networks"
  | where DeviceProduct == "X Series"
  | where DeviceEventClassID != "campaigns"
      and DeviceEventClassID != "hsc"
      and DeviceEventClassID != "audit"
      and DeviceEventClassID != "health"
      and DeviceEventClassID != "asc"
  | extend Category = coalesce(
      column_ifexists("DeviceEventCategory", ""), 
      extract("cat=(.+?)(;|$)", 1, AdditionalExtensions), 
      ""
      )
  | project-rename threat_score = FlexNumber1
  | project-rename certainty_score = FlexNumber2
  | project-rename vectra_URL = DeviceCustomString4
  | project-rename detection_name = DeviceEventClassID
  | project-rename triaged = DeviceCustomString5
  | where triaged != "True" and AdditionalExtensions !has "account"
  | extend source_entity = case(isnotempty(SourceHostName), SourceHostName,
      "UNKNWON")
  | extend level = case(threat_score == 0 and certainty_score == 0, "Info",
      threat_score < 50 and certainty_score < 50, "Low",
      threat_score < 50 and certainty_score >= 50, "Medium", 
      threat_score >= 50 and certainty_score <= 50, "High", 
      threat_score >= 50 and certainty_score >= 50, "Critical",
      "UNKNOWN")
  | extend Severity = case(level == "Info", "Informational",level == "Critical", "High", level)
  | summarize arg_max(threat_score, *) by source_entity, Activity
  | sort by TimeGenerated
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SourceHostName
alertDetailsOverride:
  alertDisplayNameFormat: Vectra AI - {{Activity}} Detected
  alertDescriptionFormat: |
    Entity is a host. Category is {{Category}}. Threat score is {{threat_score}} and certainty score is {{certainty_score}}.
  alertSeverityColumnName: Severity
  alertDynamicProperties:
    - alertProperty: AlertLink
      value: vectra_URL
    - alertProperty: ProductName
      value: DeviceProduct
    - alertProperty: ProviderName
      value: DeviceVendor
    - alertProperty: ConfidenceScore
      value: certainty_score
customDetails:
  AttackType: Activity
  AttackCategory: Category
version: 1.0.5
kind: Scheduled

Stages and Predicates

Stage 1: source

CommonSecurityLog

Stage 2: where

| where DeviceVendor == "Vectra Networks"

Stage 3: where

| where DeviceProduct == "X Series"

Stage 4: where

| where DeviceEventClassID != "campaigns"
    and DeviceEventClassID != "hsc"
    and DeviceEventClassID != "audit"
    and DeviceEventClassID != "health"
    and DeviceEventClassID != "asc"

Stage 5: extend

| extend Category = coalesce(
    column_ifexists("DeviceEventCategory", ""), 
    extract("cat=(.+?)(;|$)", 1, AdditionalExtensions), 
    ""
    )

Stage 6: project-rename

| project-rename threat_score = FlexNumber1

Stage 7: project-rename

| project-rename certainty_score = FlexNumber2

Stage 8: project-rename

| project-rename vectra_URL = DeviceCustomString4

Stage 9: project-rename

| project-rename detection_name = DeviceEventClassID

Stage 10: project-rename

| project-rename triaged = DeviceCustomString5

Stage 11: where

| where triaged != "True" and AdditionalExtensions !has "account"

Stage 12: extend (3 consecutive steps)

| extend source_entity = case(isnotempty(SourceHostName), SourceHostName,
    "UNKNWON")
| extend level = case(threat_score == 0 and certainty_score == 0, "Info",
    threat_score < 50 and certainty_score < 50, "Low",
    threat_score < 50 and certainty_score >= 50, "Medium", 
    threat_score >= 50 and certainty_score <= 50, "High", 
    threat_score >= 50 and certainty_score >= 50, "Critical",
    "UNKNOWN")
| extend Severity = case(level == "Info", "Informational",level == "Critical", "High", level)
source_entity =
ifisnotempty(SourceHostName)SourceHostName
else"UNKNWON"

Stage 13: summarize

| summarize arg_max(threat_score, *) by source_entity, Activity

Stage 14: sort

| sort by TimeGenerated

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
AdditionalExtensionsmatchaccountexcludes:AdditionalExtensions field:"AdditionalExtensions" value:"account"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Activitysummarize
source_entitysummarize

VMware ESXi - Multiple new VMs started

#
Status
available
Severity
medium
Time window
10m
Group by
SrcUsername, k
Source
github.com/Azure/Azure-Sentinel

'Detects when multiple new VMs were started.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: bdea247f-7d17-498c-ac0e-c7e764cbdbbe
name: VMware ESXi - Multiple new VMs started
description: |
  'Detects when multiple new VMs were started.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |
  let threshold = 5;
  let a_vm = 
  VMwareESXi
  | where SyslogMessage has ('VmPoweredOnEvent')
  | extend DstHostname = extract(@'\[\d+\]\s+\[(.*?)\s+on', 1, SyslogMessage)
  | summarize vm_l = make_set(DstHostname,200)
  | extend k=1;
  VMwareESXi
  | where SyslogMessage has ('VmPoweredOnEvent')
  | extend DstHostname = extract(@'\[\d+\]\s+\[(.*?)\s+on', 1, SyslogMessage)
  | extend SrcUsername = extract(@'\[info\]\s+\[(.*?)\]', 1, SyslogMessage)
  | extend k = 1
  | join (a_vm) on k
  | where vm_l !has DstHostname
  | summarize n_vm = make_set(DstHostname,200) by SrcUsername, bin(TimeGenerated, 10m)
  | where array_length(n_vm) >= threshold
  | extend hostname = iff(array_length(n_vm) > 0, n_vm[0], "")
  | extend NTDomain = tostring(split(hostname, '\\', 1)[0]), HostName = tostring(split(hostname, '.', 0)[0])
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: NTDomain
        columnName: NTDomain
version: 1.0.3
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 5;

Let Binding: a_vm used in Stage 4

let a_vm = VMwareESXi
| where SyslogMessage has ('VmPoweredOnEvent')
| extend DstHostname = extract(@'\[\d+\]\s+\[(.*?)\s+on', 1, SyslogMessage)
| summarize vm_l = make_set(DstHostname,200)
| extend k=1;

Stage 1: source

VMwareESXi

Stage 2: where

| where SyslogMessage has ('VmPoweredOnEvent')

Stage 3: extend (3 consecutive steps)

| extend DstHostname = extract(@'\[\d+\]\s+\[(.*?)\s+on', 1, SyslogMessage)
| extend SrcUsername = extract(@'\[info\]\s+\[(.*?)\]', 1, SyslogMessage)
| extend k = 1

Stage 4: join

| join (a_vm) on k

Stage 5: where

where vm_l =~ DstHostname

Stage 6: summarize

summarize n_vm by SrcUsername

Stage 7: where

where n_vm >= 5

Stage 8: extend

extend hostname
hostname =
ifn_vm > 0n_vm[0]
else""

Stage 9: extend

extend HostName, NTDomain

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
SyslogMessagematch
  • VmPoweredOnEvent transforms: term
field:"SyslogMessage" kind:match value:"VmPoweredOnEvent"

Output Fields

These fields are emitted when the rule matches.

FieldSource
SrcUsernamesummarize
n_vmsummarize
hostnameextend
HostNameextend
NTDomainextend

Back to all Kusto rules