Detection rules › Kusto

Kusto rules: 08

Awake Security - High Severity Matches By Device

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

This query searches for devices with high severity event(s).

Rule Body

id: d5e012c2-29ba-4a02-a813-37b928aafe2d
name: Awake Security - High Severity Matches By Device
description: This query searches for devices with high severity event(s).
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: |
  CommonSecurityLog
  | where DeviceVendor == "Arista Networks" and DeviceProduct == "Awake Security" and toint(LogSeverity) > 6
  | summarize StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), Models=make_set(Activity), ASPMatchURLs=make_set(DeviceCustomString2), SourceIPs=make_set(SourceIP),
    DestinationIPs=make_set(DestinationIP), ModelMatchCount=sum(EventCount), MaxSeverity=max(todecimal  (LogSeverity)) by SourceHostName
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: Awake Security - High Severity Matches On Device {{SourceHostName}}
  alertDescriptionFormat: |
    Device {{SourceHostName}} matched the following high-severity Awake model(s):

    {{Models}}

    The destination IPs associated with these matches were:

    {{DestinationIPs}}
  alertTacticsColumnName: null
  alertSeverityColumnName: MaxSeverity
customDetails:
  Matched_Models: Models
  Matches_ASP_URLs: ASPMatchURLs
  Device: SourceHostName
  Matches_Count: ModelMatchCount
  Matches_Max_Severity: MaxSeverity
  Matches_Dest_IPs: DestinationIPs
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SourceHostName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIPs
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: true
    lookbackDuration: 3d
    matchingMethod: Selected
    groupByEntities:
      - Host
    groupByAlertDetails: []
    groupByCustomDetails:
      - Device
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

CommonSecurityLog

Stage 2: where

| where DeviceVendor == "Arista Networks" and DeviceProduct == "Awake Security" and toint(LogSeverity) > 6

Stage 3: summarize

| summarize StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), Models=make_set(Activity), ASPMatchURLs=make_set(DeviceCustomString2), SourceIPs=make_set(SourceIP),
  DestinationIPs=make_set(DestinationIP), ModelMatchCount=sum(EventCount), MaxSeverity=max(todecimal  (LogSeverity)) by SourceHostName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ASPMatchURLssummarize
DestinationIPssummarize
EndTimesummarize
MaxSeveritysummarize
ModelMatchCountsummarize
Modelssummarize
SourceHostNamesummarize
SourceIPssummarize
StartTimesummarize

AWSCloudTrail - S3 object publicly exposed

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

Detects S3 object ACL changes that expose objects to public or broadly authenticated users. This can result in unintended data disclosure from object-level permissions.

MITRE ATT&CK Coverage

TacticTechniques
Exfiltration

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 09f2a28b-3286-4268-9e2f-33805f104e5d
name: AWSCloudTrail - S3 object publicly exposed
description: |
  Detects S3 object ACL changes that expose objects to public or broadly authenticated users.
  This can result in unintended data disclosure from object-level permissions.
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 == "PutObjectAcl" and isempty(ErrorCode) and isempty(ErrorMessage)
      | 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")
      | 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]), "")
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 object public ACL exposure by {{AccountName}}'
  alertDescriptionFormat: 'Detected {{EventName}} event from {{SourceIpAddress}} applying public object ACL settings in account {{RecipientAccountId}}.'
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

AWSCloudTrail

Stage 2: where

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

Stage 3: extend

| 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")

Stage 8: 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]), "")

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
  • PutObjectAcl
field:"aws::eventName" kind:eq value:"PutObjectAcl"
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
Grantextend
cannedaclextend
URIextend
typeextend
xamzaclextend
UserIdentityArnextend
UserNameextend
AccountNameextend
AccountUPNSuffixextend

AWSCloudTrail - Unauthorized EC2 Instance Setup Attempt

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

Detects an unauthorized attempt to launch an EC2 instance by a user without the necessary permissions. This may indicate an attempt to provision malicious infrastructure within the AWS subscription. Reference: https://stratus-red-team.cloud/attack-techniques/AWS/aws.execution.ec2-launch-unusual-instances/

MITRE ATT&CK Coverage

TacticTechniques
Resource Development

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: f7210a45-12a4-4d02-b59e-f23476827a4b
name: AWSCloudTrail - Unauthorized EC2 Instance Setup Attempt
description: |
  Detects an unauthorized attempt to launch an EC2 instance by a user without the necessary permissions. This may indicate an attempt to provision malicious infrastructure within the AWS subscription. Reference: https://stratus-red-team.cloud/attack-techniques/AWS/aws.execution.ec2-launch-unusual-instances/
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - ResourceDevelopment
relevantTechniques:
  - T1583
query: |
 AWSCloudTrail
 | where EventName == "RunInstances"
 | where ErrorCode == "Client.UnauthorizedOperation"
 | project TimeGenerated, AwsEventId, EventTypeName, UserIdentityPrincipalid, UserIdentityType,  UserIdentityAccountId, SessionMfaAuthenticated, SessionCreationDate, SessionIssuerUserName, SourceIpAddress, AWSRegion, UserAgent, RequestParameters, ErrorMessage
 | extend UserName = substring(UserIdentityPrincipalid, indexof_regex(UserIdentityPrincipalid, ":") + 1)
 | extend Name = split(UserName,'@')[0],UpnSuffix = split(UserName,'@')[1]
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UpnSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
  AWSRegion: AWSRegion
  UserAgent: UserAgent
  ErrorMessage: ErrorMessage
  UserIdentityType: UserIdentityType
alertDetailsOverride:
  alertDisplayNameFormat: Unauthorized EC2 instance launch attempt by {{UserIdentityPrincipalid}} in {{AWSRegion}}
  alertDescriptionFormat: User {{UserIdentityPrincipalid}} (type {{UserIdentityType}}) attempted an unauthorized EC2 instance launch in region {{AWSRegion}}.
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

AWSCloudTrail

Stage 2: where

| where EventName == "RunInstances"

Stage 3: where

| where ErrorCode == "Client.UnauthorizedOperation"

Stage 4: project

| project TimeGenerated, AwsEventId, EventTypeName, UserIdentityPrincipalid, UserIdentityType,  UserIdentityAccountId, SessionMfaAuthenticated, SessionCreationDate, SessionIssuerUserName, SourceIpAddress, AWSRegion, UserAgent, RequestParameters, ErrorMessage

Stage 5: extend

| extend UserName = substring(UserIdentityPrincipalid, indexof_regex(UserIdentityPrincipalid, ":") + 1)

Stage 6: extend

| extend Name = split(UserName,'@')[0],UpnSuffix = split(UserName,'@')[1]

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ErrorCodeeq
  • Client.UnauthorizedOperation
field:"aws::errorCode" kind:eq value:"Client.UnauthorizedOperation"
EventNameeq
  • RunInstances corpus 2 (kusto 1, panther 1)
field:"aws::eventName" kind:eq value:"RunInstances"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AWSRegionproject
AwsEventIdproject
ErrorMessageproject
EventTypeNameproject
RequestParametersproject
SessionCreationDateproject
SessionIssuerUserNameproject
SessionMfaAuthenticatedproject
SourceIpAddressproject
TimeGeneratedproject
UserAgentproject
UserIdentityAccountIdproject
UserIdentityPrincipalidproject
UserIdentityTypeproject
UserNameextend
Nameextend
UpnSuffixextend

BloodHound Attack Path Finding - Non-Tier Zero Principal Trusted for Unconstrained Delegation

#

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

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

BloodHound Attack Path Finding - Non-Tier Zero Principal Trusted for Unconstrained Delegation

Rule Body

id: bc26d218-89a4-40ba-88b6-76e5141b5731
name: BloodHound Attack Path Finding - Non-Tier Zero Principal Trusted for Unconstrained
  Delegation
description: BloodHound Attack Path Finding - Non-Tier Zero Principal Trusted for
  Unconstrained Delegation
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BloodHoundEnterprise
    dataTypes:
      - BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: BHEAttackPathsData_CL | where Finding == "T0CoerceToTGT"
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: domain_name
customDetails:
  Description: ShortDescription
  Remediation: ShortRemediation
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

BHEAttackPathsData_CL

Stage 2: where

| where Finding == "T0CoerceToTGT"

Indicators

These rows show field, operator, and value matches.

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

CiscoISE - Command executed with the highest privileges by new user

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

'Detects command execution with PrivilegeLevel - 15 by user for wich there was no such activity detected earlier.'

MITRE ATT&CK Coverage

Rule Body

id: e71890a2-5f61-4790-b1ed-cf1d92d3e398
name: CiscoISE - Command executed with the highest privileges by new user
description: |
  'Detects command execution with PrivilegeLevel - 15 by user for wich there was no such activity detected earlier.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Persistence
  - PrivilegeEscalation
  - DefenseEvasion
  - Execution
relevantTechniques:
  - T1133
  - T1548
  - T1059
query: |
  let lbperiod = 14d;
  let lbtime = 15m;
  let knownPrivUsers =
  CiscoISEEvent
  | where TimeGenerated between (ago(lbperiod) .. ago(lbtime))
  | where PrivilegeLevel == '15'
  | summarize makelist(DstUserName)
  ;
  CiscoISEEvent
  | where TimeGenerated > ago(lbtime)
  | where PrivilegeLevel == '15'
  | where DstUserName !in (knownPrivUsers)
  | project TimeGenerated, SrcIpAddr, DstIpAddr, DstUserName, CmdSet
  | extend AccountCustomEntity = DstUserName
  | extend IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountCustomEntity
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.4
kind: Scheduled

Stages and Predicates

Parameters

let lbperiod = 14d;
let lbtime = 15m;

Let Binding: knownPrivUsers used in Stage 4

let knownPrivUsers = CiscoISEEvent
| where TimeGenerated between (ago(lbperiod) .. ago(lbtime))
| where PrivilegeLevel == '15'
| summarize makelist(DstUserName);

Stage 1: source

CiscoISEEvent

Stage 2: where

| where TimeGenerated > ago(lbtime)

Stage 3: where

| where PrivilegeLevel == '15'

Stage 4: where

| where DstUserName !in (knownPrivUsers)

Stage 5: project

| project TimeGenerated, SrcIpAddr, DstIpAddr, DstUserName, CmdSet

Stage 6: extend

| extend AccountCustomEntity = DstUserName

Stage 7: extend

| extend IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DstUserNameeqknownPrivUsersexcludes:DstUserName field:"DstUserName" value:"knownPrivUsers"

Output Fields

These fields are emitted when the rule matches.

FieldSource
CmdSetproject
DstIpAddrproject
DstUserNameproject
SrcIpAddrproject
TimeGeneratedproject
AccountCustomEntityextend
IPCustomEntityextend

Cloud Gateway Pool Deleted

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

Detects when a cloud gateway pool is deleted from Veeam Backup & Replication. This might indicate unauthorized removal of critical components.

Rule Body

id: 73e1d89b-79a6-4ab0-b3bb-5e564fcf10f1
name: Cloud Gateway Pool Deleted
description: Detects when a cloud gateway pool is deleted from Veeam Backup & Replication.
  This might indicate unauthorized removal of critical components.
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 24143\n| project\n    Date\
  \ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n\
  \    EventId = instanceId,\n    [\"User Name\"] = user,\n  MessageDetails = Description,\n\
  \   Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 24143

Stage 3: project

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
User Nameproject

Cyble Vision Alerts Bitbucket

#

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

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

'Detects exposed secrets in Bitbucket repositories using the Alerts_bit_bucket parser. Creates one incident per matched secret. Includes decoder, detector, commit, file, and repository context for SOC triage.'

MITRE ATT&CK Coverage

Rule Body

id: f3c25011-4509-41c8-be27-35d891531c39
name: Cyble Vision Alerts Bitbucket
description: |
  'Detects exposed secrets in Bitbucket repositories using the Alerts_bit_bucket parser. Creates one incident per matched secret. Includes decoder, detector, commit, file, and repository context for SOC triage.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CybleVisionAlerts
  dataTypes:
  - CybleVisionAlerts_CL
enabled: true
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: GreaterThan
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics:
- CredentialAccess
- Exfiltration
- Discovery
relevantTechniques:
- T1552
- T1537
- T1083
query: |
  Alerts_bit_bucket 
  | where Service == "bit_bucket" 
  | extend MappedSeverity = Severity
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
alertDetailsOverride:
  alertDisplayNameFormat: Secret Exposure in Bitbucket {{BB_File}} ({{BB_DetectorName}})
  alertDescriptionFormat: |
    A sensitive secret was detected in Bitbucket repository {{BB_Repository}}. File {{BB_File}}:{{BB_Line}}. Investigator should verify exposure, rotate credentials and remediate impacted systems.
customDetails:
  MappedSeverity: Severity
  Status: Status
  AlertID: AlertID
  Service: Service
  BB_DetectorName: BB_DetectorName
  BB_Raw: BB_Raw
  BB_File: BB_File
  BB_Line: BB_Line
  BB_Commit: BB_Commit
  BB_Email: BB_Email
  BB_Repository: BB_Repository
  BB_Link: BB_Link
  BB_Verified: BB_Verified
  BB_RotationGuide: BB_RotationGuide
entityMappings:
- entityType: Url
  fieldMappings:
  - identifier: Url
    columnName: BB_Repository
- entityType: Url
  fieldMappings:
  - identifier: Url
    columnName: BB_Link
- entityType: File
  fieldMappings:
  - identifier: Name
    columnName: BB_File
  - identifier: Directory
    columnName: KeywordName
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

Alerts_bit_bucket

Stage 2: where

| where Service == "bit_bucket"

Stage 3: extend

| extend MappedSeverity = Severity

Indicators

These rows show field, operator, and value matches.

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

Output Fields

These fields are emitted when the rule matches.

FieldSource
MappedSeverityextend

Execution attempts 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 detects batches of distinct SQL queries that execute (or attempt to) commands that could indicate potential security issues - such as attempts to execute shell commands (e.g. for running illegitimate code).'

MITRE ATT&CK Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 3367fd5e-44b3-4746-a9a5-dc15c8202490
name: Execution attempts stateful anomaly on database
description: |
  'This query detects batches of distinct SQL queries that execute (or attempt to) commands that could indicate potential security issues - such as attempts to execute shell commands (e.g. for running illegitimate code).'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureSql
    dataTypes:
      - AzureDiagnostics
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Execution
relevantTechniques:
  - T1059
  - T1059.001
  - T1059.003
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 hotwords = pack_array('xp_cmdshell', 'ps.exe', 'powershell', 'cmd.exe', 'msiexec', '<script>'); // List of monitored hot words.
    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)
        | extend hasHotword = iff(Statement has_any (hotwords), 1, 0)
        | summarize countEvents = count(), countStatements = dcount(Statement)
            , countStatementsWithHotwords = dcountif(Statement, hasHotword == 1)
            , countFailedStatementsWithHotwords = dcountif(Statement, (hasHotword == 1) and (Error > 0))
            , countSuccessfulStatementsWithHotwords = dcountif(Statement, ((hasHotword == 1)) and (Error == 0))
            , anyMonitoredStatement = anyif(Statement, (hasHotword == 1))
            , anySuccessfulMonitoredStatement = anyif(Statement, (hasHotword == 1) and (Error == 0))
            , anyInfo = anyif(AdditionalInfo, hasHotword == 1)
            , hotWord = anyif(extract(strcat_array(hotwords, '|'), 0, tolower(Statement)), hasHotword == 1)
            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 countSlicesWithHotwords = dcountif(timeSlice, countStatementsWithHotwords >= monitoredStatementsThreshold)
            by Database;
    processedData
    | where WindowType == 'detection' 
    | join kind = inner (trainingSet) on Database
    | extend IsHotwordAnomalyOnStatement = iff(((countStatementsWithHotwords >= monitoredStatementsThreshold) and (countSlicesWithHotwords <= trainingSlicesThreshold)), true, false)
        , anomalyScore = round(countStatementsWithHotwords/monitoredStatementsThreshold, 0)
    | where IsHotwordAnomalyOnStatement == 'true'
    | project TimeGenerated = timeSlice, Database, ClientIp, ApplicationName, PrincipalName, HostName, ResourceId, countEvents, countStatements, countStatementsWithHotwords, anyMonitoredStatement, anyInfo, anomalyScore, hotWord
    | 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
customDetails:
  HotWords: hotWord
alertDetailsOverride:
  alertDisplayNameFormat: 'Execution attempts stateful anomaly on database {{Database}}'
  alertDescriptionFormat: 'An anomaly was detected on database {{Database}} with {{countStatementsWithHotwords}} statements with monitored hot words (xp_cmdshell, powershell, cmd.exe etc.) in the last hour. Investigate the database activity for potential malicious attempts to execute shell commands or run illegitimate code.'
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 hotwords = pack_array('xp_cmdshell', 'ps.exe', 'powershell', 'cmd.exe', 'msiexec', '<script>');

let processedData is inlined into the numbered stages below.

Let Binding: trainingSet used in Stage 10

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

Stages 1 to 8 define let processedData as the rule's main pipeline source. Stages 9 to 14 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: extend

| extend hasHotword = iff(Statement has_any (hotwords), 1, 0)
hasHotword =
ifStatement has "hotwords"1
else0

Stage 6: summarize

| summarize countEvents = count(), countStatements = dcount(Statement)
        , countStatementsWithHotwords = dcountif(Statement, hasHotword == 1)
        , countFailedStatementsWithHotwords = dcountif(Statement, (hasHotword == 1) and (Error > 0))
        , countSuccessfulStatementsWithHotwords = dcountif(Statement, ((hasHotword == 1)) and (Error == 0))
        , anyMonitoredStatement = anyif(Statement, (hasHotword == 1))
        , anySuccessfulMonitoredStatement = anyif(Statement, (hasHotword == 1) and (Error == 0))
        , anyInfo = anyif(AdditionalInfo, hasHotword == 1)
        , hotWord = anyif(extract(strcat_array(hotwords, '|'), 0, tolower(Statement)), hasHotword == 1)
        by Database, ClientIp, ApplicationName, PrincipalName, timeSlice,HostName,ResourceId

Stage 7: 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 8: where

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

Stage 9: where

processedData
| where WindowType == 'detection'

Stage 10: join

| join kind = inner (trainingSet) on Database

Stage 11: extend

extend IsHotwordAnomalyOnStatement, anomalyScore
IsHotwordAnomalyOnStatement =
ifcountStatementsWithHotwords >= monitoredStatementsThreshold and countSlicesWithHotwords <= trainingSlicesThresholdtrue
elsefalse

Stage 12: where

where IsHotwordAnomalyOnStatement == true

Stage 13: project

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

Stage 14: 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
countStatementsWithHotwordsproject
hotWordproject
Nameextend
UPNSuffixextend

GCP Audit Logs - Storage Bucket Made Public

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

Detects when a Google Cloud Storage bucket is made publicly accessible by granting permissions to allUsers or allAuthenticatedUsers. Making buckets public can expose sensitive data to unauthorized access and may indicate a misconfiguration or malicious activity. Adversaries may make buckets public to exfiltrate data or as part of a data exposure attack. This rule monitors setIamPermissions operations that add public access roles to storage buckets.

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 3a8d7f9e-4b2c-4e5d-8c6b-9f1a3d5e8c7b
name: GCP Audit Logs - Storage Bucket Made Public
description: |
  'Detects when a Google Cloud Storage bucket is made publicly accessible by granting permissions to allUsers or allAuthenticatedUsers.
  Making buckets public can expose sensitive data to unauthorized access and may indicate a misconfiguration or malicious activity.
  Adversaries may make buckets public to exfiltrate data or as part of a data exposure attack.
  This rule monitors setIamPermissions operations that add public access roles to storage buckets.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: GCPAuditLogsDefinition
    dataTypes:
      - GCPAuditLogs
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
  - InitialAccess
  - Exfiltration
relevantTechniques:
  - T1530
  - T1078.004
  - T1567.002
tags:
  - GCP
  - Storage
  - Data Exposure
  - Cloud Security
query: |
  GCPAuditLogs
  | where ServiceName == "storage.googleapis.com"
  | where MethodName == "storage.setIamPermissions"
  | where GCPResourceType == "gcs_bucket"
  | extend 
      ServiceDataJson = parse_json(ServiceData),
      RequestMetadataJson = parse_json(RequestMetadata),
      AuthInfoJson = parse_json(AuthenticationInfo),
      AuthzInfoJson = parse_json(AuthorizationInfo)
  | extend PolicyDelta = ServiceDataJson.policyDelta.bindingDeltas
  | mv-expand PolicyDelta
  | extend 
      Action = tostring(PolicyDelta.action),
      Member = tostring(PolicyDelta.member),
      Role = tostring(PolicyDelta.role)
  | where Action == "ADD"
  | where Member in~ ("allUsers", "allAuthenticatedUsers")
  | extend 
      BucketName = extract(@"buckets/([^/]+)", 1, GCPResourceName),
      CallerIpAddress = tostring(RequestMetadataJson.callerIp),
      UserAgent = tostring(RequestMetadataJson.callerSuppliedUserAgent),
      AuthEmail = tostring(AuthInfoJson.principalEmail),
      Permission = tostring(AuthzInfoJson[0].permission),
      PermissionGranted = tostring(AuthzInfoJson[0].granted)
  | extend 
      PublicAccessType = case(
          Member =~ "allUsers", "Public to Everyone",
          Member =~ "allAuthenticatedUsers", "Public to All Authenticated Users",
          "Unknown"),
      AccountName = tostring(split(PrincipalEmail, "@")[0]), 
      AccountUPNSuffix = tostring(split(PrincipalEmail, "@")[1])
  | project TimeGenerated,
            PrincipalEmail,
            AuthEmail,
            ProjectId,
            BucketName,
            ResourceName = GCPResourceName,
            PublicAccessType,
            Member,
            Role,
            CallerIpAddress,
            UserAgent,
            MethodName,
            ServiceName,
            Severity,
            Permission,
            PermissionGranted,
            LogName,
            InsertId,
            AccountName,
            AccountUPNSuffix
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: PrincipalEmail
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CallerIpAddress
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: ProjectId
      - identifier: InstanceName
        columnName: ResourceName
customDetails:
  ProjectId: ProjectId
  BucketName: BucketName
  ResourceName: ResourceName
  PublicAccessType: PublicAccessType
  RoleGranted: Role
  UserAgent: UserAgent
  Permission: Permission
alertDetailsOverride:
  alertDisplayNameFormat: "GCP Storage Bucket {{BucketName}} Made Public by {{PrincipalEmail}}"
  alertDescriptionFormat: |-
    User {{PrincipalEmail}} made storage bucket {{BucketName}} publicly accessible in project {{ProjectId}}.
    This may expose sensitive data to unauthorized access. Investigate immediately to determine if this action was authorized and assess potential data exposure.
    Review bucket contents and access logs for any unauthorized access attempts.
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

GCPAuditLogs

Stage 2: where

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

Stage 3: where

| where MethodName == "storage.setIamPermissions"

Stage 4: where

| where GCPResourceType == "gcs_bucket"

Stage 5: extend

| extend 
    ServiceDataJson = parse_json(ServiceData),
    RequestMetadataJson = parse_json(RequestMetadata),
    AuthInfoJson = parse_json(AuthenticationInfo),
    AuthzInfoJson = parse_json(AuthorizationInfo)

Stage 6: extend

| extend PolicyDelta = ServiceDataJson.policyDelta.bindingDeltas

Stage 7: mv-expand

| mv-expand PolicyDelta

Stage 8: extend

| extend 
    Action = tostring(PolicyDelta.action),
    Member = tostring(PolicyDelta.member),
    Role = tostring(PolicyDelta.role)

Stage 9: where

| where Action == "ADD"

Stage 10: where

| where Member in~ ("allUsers", "allAuthenticatedUsers")

Stage 11: extend

| extend 
    BucketName = extract(@"buckets/([^/]+)", 1, GCPResourceName),
    CallerIpAddress = tostring(RequestMetadataJson.callerIp),
    UserAgent = tostring(RequestMetadataJson.callerSuppliedUserAgent),
    AuthEmail = tostring(AuthInfoJson.principalEmail),
    Permission = tostring(AuthzInfoJson[0].permission),
    PermissionGranted = tostring(AuthzInfoJson[0].granted)

Stage 12: extend

| extend 
    PublicAccessType = case(
        Member =~ "allUsers", "Public to Everyone",
        Member =~ "allAuthenticatedUsers", "Public to All Authenticated Users",
        "Unknown"),
    AccountName = tostring(split(PrincipalEmail, "@")[0]), 
    AccountUPNSuffix = tostring(split(PrincipalEmail, "@")[1])
PublicAccessType =
ifMember =~ "allUsers""Public to Everyone"
elifMember =~ "allAuthenticatedUsers""Public to All Authenticated Users"
else"Unknown"

Stage 13: project

| project TimeGenerated,
          PrincipalEmail,
          AuthEmail,
          ProjectId,
          BucketName,
          ResourceName = GCPResourceName,
          PublicAccessType,
          Member,
          Role,
          CallerIpAddress,
          UserAgent,
          MethodName,
          ServiceName,
          Severity,
          Permission,
          PermissionGranted,
          LogName,
          InsertId,
          AccountName,
          AccountUPNSuffix

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
AccountUPNSuffixproject
AuthEmailproject
BucketNameproject
CallerIpAddressproject
InsertIdproject
LogNameproject
Memberproject
MethodNameproject
Permissionproject
PermissionGrantedproject
PrincipalEmailproject
ProjectIdproject
PublicAccessTypeproject
ResourceNameproject
Roleproject
ServiceNameproject
Severityproject
TimeGeneratedproject
UserAgentproject

Global VM Exclusions Deleted

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

Detects when a VM is removed from global exclusions in Veeam Backup & Replication. This might indicate unauthorized changes.

Rule Body

id: 141d7e3c-a72b-49f7-9e64-57f60c2fa927
name: Global VM Exclusions Deleted
description: Detects when a VM is removed from global exclusions in Veeam Backup &
  Replication. This might indicate unauthorized changes.
severity: Low
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 3h
queryPeriod: 3h
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 40500\n| project\n    Date\
  \ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n\
  \    EventId = instanceId,\n    UserName = user,\n  MessageDetails = Description,\n\
  \    Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 40500

Stage 3: project

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
UserNameproject

GSA - TI URL Entity

#
Status
available
Severity
medium
Time window
14d
Group by
$left.Url, $right.DestinationUrlToLower, DestinationUrl, Id, ObservableValue
Source
github.com/Azure/Azure-Sentinel

This query identifies URL indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in GSA NetworkAccessTraffic.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 347c6cb3-33d2-4753-b7f6-eab946a8cd51
name: GSA - TI URL Entity
description: |
  This query identifies URL indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in GSA NetworkAccessTraffic.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - NetworkAccessTrafficLogs
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let GSANetworkAccessTraffic_ = NetworkAccessTraffic
  | where TimeGenerated >= ago(dt_lookBack)
  | where TrafficType  == "internet"
  | extend DestinationUrlToLower = tolower(DestinationUrl)
  | project-rename GSANetworkAccessTraffic_TimeGenerated = TimeGenerated;
  let GSANetworkAccessTrafficUrls =
      GSANetworkAccessTraffic_
      | where isnotempty(DestinationUrl)
      | extend Url = tolower(DestinationUrl)
      | distinct Url;
  ThreatIntelIndicators
    | extend IndicatorType = replace(@'[\[\]"]', "", tostring(split(ObservableKey, ":", 0)))
    | where IndicatorType == "url"
    | extend Url = ObservableValue
    | extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
    | where TimeGenerated >= ago(ioc_lookBack)
    | extend Url = tolower(Url)
    | where Url in (GSANetworkAccessTrafficUrls)
    | 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;"
    | project-reorder *, Tags, TrafficLightProtocolLevel, Url, Type
    | join kind=innerunique (GSANetworkAccessTraffic_) on $left.Url == $right.DestinationUrlToLower
    | where isempty(ValidUntil) or GSANetworkAccessTraffic_TimeGenerated < ValidUntil
    | extend GSAThreatType = ThreatType
  | summarize GSANetworkAccessTraffic_TimeGenerated = arg_max(GSANetworkAccessTraffic_TimeGenerated, *) by Id, DestinationUrl
  | project-reorder *,
      Tags,
      Confidence,
      TrafficLightProtocolLevel,
      Type,
      UserPrincipalName,
      UserId,
      DeviceId,
      InitiatingProcessName,
      GSAThreatType,
      DestinationIp,
      DestinationFqdn,
      DestinationUrl,
      DestinationPort,
      SourceIp,
      SourcePort,
      HttpUserAgent
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: UserPrincipalName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIp
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: DestinationUrl
  - entityType: Process
    fieldMappings:
      - identifier: CommandLine
        columnName: InitiatingProcessName
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

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

Let Binding: GSANetworkAccessTraffic_ used in Stage 14

let GSANetworkAccessTraffic_ = NetworkAccessTraffic
| where TimeGenerated >= ago(dt_lookBack)
| where TrafficType  == "internet"
| extend DestinationUrlToLower = tolower(DestinationUrl)
| project-rename GSANetworkAccessTraffic_TimeGenerated = TimeGenerated;

Let Binding: GSANetworkAccessTrafficUrls used in Stage 8

let GSANetworkAccessTrafficUrls = GSANetworkAccessTraffic_
    | where isnotempty(DestinationUrl)
    | extend Url = tolower(DestinationUrl)
    | distinct Url;

Stage 1: source

ThreatIntelIndicators

Stage 2: extend

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

Stage 3: where

| where IndicatorType == "url"

Stage 4: extend

| extend Url = ObservableValue

Stage 5: extend

| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)

Stage 6: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 7: extend

| extend Url = tolower(Url)

Stage 8: where

| where Url in (GSANetworkAccessTrafficUrls)

Stage 9: summarize

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

Stage 10: where

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

Stage 11: extend

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

Stage 12: where

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

Stage 13: project-reorder

| project-reorder *, Tags, TrafficLightProtocolLevel, Url, Type

Stage 14: join

| join kind=innerunique (GSANetworkAccessTraffic_) on $left.Url == $right.DestinationUrlToLower

Stage 15: where

where (GSANetworkAccessTraffic_TimeGenerated < ValidUntil or isempty(ValidUntil))

Stage 16: extend

extend GSAThreatType

Stage 17: summarize

summarize GSANetworkAccessTraffic_TimeGenerated by Id, DestinationUrl

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
DestinationUrlsummarize
GSANetworkAccessTraffic_TimeGeneratedsummarize
Idsummarize

Guardian- Privacy Protection PII Policy Violation Detection

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

'This alert creates an incident when Privacy Protection PII Policy Violation detected from the Guardian'

Rule Body

id: d5065600-617b-4a16-b58a-4a46da9c1afd
name: Guardian- Privacy Protection PII Policy Violation Detection
description: |
  'This alert creates an incident when  Privacy Protection PII Policy Violation detected from the Guardian'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: BoschAIShield
    dataTypes:
      - Guardian
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: |
  Guardian
  | where PolicyViolatedControlFeature =~ 'PII Detection'
  | where Severity =~ 'High'
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: NTDomain
        columnName: NTDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
  alertDisplayNameFormat: Guardian-  Privacy Protection PII Policy Violation detection
  alertDescriptionFormat: |
    This query detects Privacy Protection PII Policy Violation detected from the Guardian generated at {{TimeGenerated}}.\n\nPlease check the source for more information and investigate further
  alertTacticsColumnName: null
  alertSeverityColumnName: Severity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

Guardian

Stage 2: where

| where PolicyViolatedControlFeature =~ 'PII Detection'

Stage 3: where

| where Severity =~ 'High'

Indicators

These rows show field, operator, and value matches.

High Number of Urgent Vulnerabilities Detected

#
Status
available
Severity
medium
Time window
1h
Group by
IPAddress, NetBios
Source
github.com/Azure/Azure-Sentinel

'This Creates an incident when a host has a high number of Urgent, severity 5, vulnerabilities detected.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 3edb7215-250b-40c0-8b46-79093949242d
name: High Number of Urgent Vulnerabilities Detected
description: |
  'This Creates an incident when a host has a high number of Urgent, severity 5, vulnerabilities detected.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: QualysVMLogsCCPDefinition
    dataTypes:
      - QualysHostDetection
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
query: |
  let threshold = 10;
  QualysHostDetection
  | where Severity == "5"
  | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), count() by NetBios, IPAddress
  | where count_ >= threshold
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: NetBios
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
version: 1.0.3
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 10;

Stage 1: source

QualysHostDetection

Stage 2: where

| where Severity == "5"

Stage 3: summarize

| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), count() by NetBios, IPAddress

Stage 4: where

| where count_ >= threshold

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
EndTimesummarize
IPAddresssummarize
NetBiossummarize
StartTimesummarize

Hunt for compromised browser extensions

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

This hunting rule can be used to find devices using known compromised browser extensions. It is created based on the threat researched linked in the reference section.

MITRE ATT&CK Coverage

TacticTechniques
Persistence

References

Telemetry Coverage

Rule Body

let extensions = datatable (Id:string, VulnVersion:string) [
    "nnpnnpemnckcfdebeekibpiijlicmpom", "2.0.1",
    "kkodiihpgodmdankclfibbiphjkfdenh", "1.16.2",
    "oaikpkmjciadfpddlpjjdapglcihgdle", "1.0.12",
    "dpggmcodlahmljkhlmpgpdcffdaoccni", "1.1.1",
    "acmfnomgphggonodopogfbmkneepfgnh", "4.00",
    "mnhffkhmpnefgklngfmlndmkimimbphc", "4.40",
    "cedgndijpacnfbdggppddacngjfdkaca", "0.0.11",
    "bbdnohkpnbkdkmnkddobeafboooinpla", "1.0.1",
    "egmennebgadmncfjafcemlecimkepcle", "2.2.7",
    "bibjgkidgpfbblifamdlkdlhgihmfohh", "0.1.3",
    "befflofjcniongenjmbkgkoljhgliihe", "2.13.0",
    "pkgciiiancapdlpcbppfkmeaieppikkk", "1.3.7",
    "llimhhconnjiflfimocjggfjdlmlhblm", "1.5.7",
    "oeiomhmbaapihbilkfkhmlajkeegnjhe", "3.18.0",
    "pajkjnmeojmbapicmbpliphjmcekeaac", "24.10.4",
    "ndlbedplllcgconngcnfmkadhokfaaln", "2.22.6",
    "epdjhgbipjpbbhoccdeipghoihibnfja", "1.4",
    "cplhlgabfijoiabgkigdafklbhhdkahj", "1.0.161",
    "jiofmdifioeejeilfkpegipdjiopiekl", "1.1.61",
    "hihblcmlaaademjlakdpicchbjnnnkbo", "3.0.2",
    "llimhhconnjiflfimocjggfjdlmlhblm", "1.5.7",
    "ekpkdmohpdnebfedjjfklhpefgpgaaji", "1.3",
    "epikoohpebngmakjinphfiagogjcnddm", "2.7.3",
    "miglaibdlgminlepgeifekifakochlka", "1.4.5",
    "eanofdhdfbcalhflpbdipkjjkoimeeod", "1.4.9",
    "ogbhbgkiojdollpjbhbamafmedkeockb", "1.8.1",
    "bgejafhieobnfpjlpcjjggoboebonfcg", "1.1.1",
    "igbodamhgjohafcenbcljfegbipdfjpk", "2.3",
    "mbindhfolmpijhodmgkloeeppmkhpmhc", "1.44",
    "hodiladlefdpcbemnbbcpclbmknkiaem", "3.1.3",
    "lbneaaedflankmgmfbmaplggbmjjmbae", "1.3.8",
    "eaijffijbobmnonfhilihbejadplhddo", "2.4",
    "hmiaoahjllhfgebflooeeefeiafpkfde", "1.0.0"
];
DeviceTvmBrowserExtensions
// Find devices using vulnerable extensions
| join kind=inner extensions on $left.ExtensionId == $right.Id
| extend IntVersion = parse_version(ExtensionVersion), IntVulnVursion = parse_version(VulnVersion)
| where IntVersion <= IntVulnVursion and IsActivated == "true"
// Join for more device info
| join kind=inner (
    DeviceInfo 
    | where Timestamp > ago(7d)
) on DeviceId
| distinct DeviceName, DeviceId, BrowserName, ExtensionName, ExtensionDescription, ExtensionVersion, ExtensionRisk, VulnVersion

Stages and Predicates

Let Binding: extensions used in Stage 2

let extensions = datatable (Id:string, VulnVersion:string) [
    "nnpnnpemnckcfdebeekibpiijlicmpom", "2.0.1",
    "kkodiihpgodmdankclfibbiphjkfdenh", "1.16.2",
    "oaikpkmjciadfpddlpjjdapglcihgdle", "1.0.12",
    "dpggmcodlahmljkhlmpgpdcffdaoccni", "1.1.1",
    "acmfnomgphggonodopogfbmkneepfgnh", "4.00",
    "mnhffkhmpnefgklngfmlndmkimimbphc", "4.40",
    "cedgndijpacnfbdggppddacngjfdkaca", "0.0.11",
    "bbdnohkpnbkdkmnkddobeafboooinpla", "1.0.1",
    "egmennebgadmncfjafcemlecimkepcle", "2.2.7",
    "bibjgkidgpfbblifamdlkdlhgihmfohh", "0.1.3",
    "befflofjcniongenjmbkgkoljhgliihe", "2.13.0",
    "pkgciiiancapdlpcbppfkmeaieppikkk", "1.3.7",
    "llimhhconnjiflfimocjggfjdlmlhblm", "1.5.7",
    "oeiomhmbaapihbilkfkhmlajkeegnjhe", "3.18.0",
    "pajkjnmeojmbapicmbpliphjmcekeaac", "24.10.4",
    "ndlbedplllcgconngcnfmkadhokfaaln", "2.22.6",
    "epdjhgbipjpbbhoccdeipghoihibnfja", "1.4",
    "cplhlgabfijoiabgkigdafklbhhdkahj", "1.0.161",
    "jiofmdifioeejeilfkpegipdjiopiekl", "1.1.61",
    "hihblcmlaaademjlakdpicchbjnnnkbo", "3.0.2",
    "llimhhconnjiflfimocjggfjdlmlhblm", "1.5.7",
    "ekpkdmohpdnebfedjjfklhpefgpgaaji", "1.3",
    "epikoohpebngmakjinphfiagogjcnddm", "2.7.3",
    "miglaibdlgminlepgeifekifakochlka", "1.4.5",
    "eanofdhdfbcalhflpbdipkjjkoimeeod", "1.4.9",
    "ogbhbgkiojdollpjbhbamafmedkeockb", "1.8.1",
    "bgejafhieobnfpjlpcjjggoboebonfcg", "1.1.1",
    "igbodamhgjohafcenbcljfegbipdfjpk", "2.3",
    "mbindhfolmpijhodmgkloeeppmkhpmhc", "1.44",
    "hodiladlefdpcbemnbbcpclbmknkiaem", "3.1.3",
    "lbneaaedflankmgmfbmaplggbmjjmbae", "1.3.8",
    "eaijffijbobmnonfhilihbejadplhddo", "2.4",
    "hmiaoahjllhfgebflooeeefeiafpkfde", "1.0.0"
];

Stage 1: source

DeviceTvmBrowserExtensions

Stage 2: join

join kind=inner (extensions) on $left.ExtensionId, $right.Id

Stage 3: extend

extend IntVersion, IntVulnVursion

Stage 4: where

where IntVersion <= IntVulnVursion and IsActivated == true

Stage 5: join

| join kind=inner (
    DeviceInfo 
    | where Timestamp > ago(7d)
) on DeviceId

Stage 6: distinct

distinct BrowserName, DeviceId, DeviceName, ExtensionDescription, ExtensionName, ExtensionRisk, ExtensionVersion, VulnVersion

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
IntVersionextend
IntVulnVursionextend

Insider Risk_Risky User Access By Application

#
Severity
medium
Time window
6h
Source
github.com/Azure/Azure-Sentinel

This alert evaluates Microsoft Entra ID Sign in risk via Machine Learning correlations in the basket operator. The basket threshold is adjustable, and the default is set to .01. There is an optional configuration to configure the percentage rates. The correlations are designed to leverage machine learning to identify patterns of risky user application access. There is an option for configuration of correlations against Microsoft Sentinel watchlists. For more information, see Tutorial: Use risk detections for user sign-ins to trigger Microsoft Entra ID Multi-Factor Authentication or password changes

MITRE ATT&CK Coverage

TacticTechniques
Execution

Telemetry Coverage

Rule Body

id: 15386bba-dc70-463f-a09f-d392e7731c63
name: Insider Risk_Risky User Access By Application
description: |
  'This alert evaluates Microsoft Entra ID Sign in risk via Machine Learning correlations in the basket operator. The basket threshold is adjustable, and the default is set to .01. There is an optional configuration to configure the percentage rates. The correlations are designed to leverage machine learning to identify patterns of risky user application access. There is an option for configuration of correlations against Microsoft Sentinel watchlists. For more information, see [Tutorial: Use risk detections for user sign-ins to trigger Microsoft Entra ID Multi-Factor Authentication or password changes](https://docs.microsoft.com/azure/active-directory/authentication/tutorial-risk-based-sspr-mfa)'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs 
queryFrequency: 6h
queryPeriod: 6h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1204
query: |
  SigninLogs
  | where RiskState == "atRisk"
  | project UserPrincipalName, Location, AppDisplayName, RiskState
  | evaluate basket(0.01) // Adjust & Tune Thresholds within Organzational Requirements
  // | where Percent > 50 // Adjust & Tune Thresholds within Organzational Requirements
  | where UserPrincipalName <> ""
  | where AppDisplayName <> ""
  | project Percent, UserPrincipalName, Location, AppDisplayName, RiskState
  // | lookup kind=inner _GetWatchlist('<Your Watchlist Name>') on $left.UserPrincipalName == $right.SearchKey
  | sort by Percent desc
  | extend AccountName = tostring(split(UserPrincipalName, "@")[0]), AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: true
    lookbackDuration: 3d
    matchingMethod: Selected
    groupByEntities:
      - Account
version: 1.1.4
kind: Scheduled

Stages and Predicates

Stage 1: source

SigninLogs

Stage 2: where

| where RiskState == "atRisk"

Stage 3: project

| project UserPrincipalName, Location, AppDisplayName, RiskState

Stage 4: evaluate

| evaluate basket(0.01)

Stage 5: where

| where UserPrincipalName <> ""

Stage 6: where

| where AppDisplayName <> ""

Stage 7: project

| project Percent, UserPrincipalName, Location, AppDisplayName, RiskState

Stage 8: sort

| sort by Percent desc

Stage 9: extend

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

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
RiskStateeq
  • atRisk corpus 2 (kusto 2)
field:"RiskState" kind:eq value:"atRisk"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AppDisplayNameproject
Locationproject
Percentproject
RiskStateproject
UserPrincipalNameproject
AccountNameextend
AccountUPNSuffixextend

M2131_RecommendedDatatableUnhealthy

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

'This alert is designed to monitor recommended data tables aligned to the Maturity Model for Event Log Management (M-21-31) standard. The alert triggers when a recommended data table hasn't been observed in over 48 hours.'

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Rule Body

id: c61b167a-59ae-42af-bc98-36c78c5acb5c
name: M2131_RecommendedDatatableUnhealthy
description: |
  'This alert is designed to monitor recommended data tables aligned to the Maturity Model for Event Log Management (M-21-31) standard. The alert triggers when a recommended data table hasn't been observed in over 48 hours.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T1082
query: |
  let M2131Mapping = datatable(DataTable:string, MaturityLevel:string)
  [
  "SigninLogs",	"Event Logging (EL0)",
  "AADManagedIdentitySignInLogs", "Event Logging (EL0)",
  "AADServicePrincipalSignInLogs",	"Event Logging (EL0)",
  "StorageBlobLogs",	"Event Logging (EL0)",
  "StorageFileLogs",	"Event Logging (EL0)",
  "AzureMetrics",	"Event Logging (EL0)",
  "AuditLogs",	"Event Logging (EL0)",
  "IdentityInfo",	"Event Logging (EL0)",
  "CommonSecurityLog",	"Event Logging (EL0)",
  "ThreatIntelligenceIndicator",	"Event Logging (EL0)",
  "DeviceNetworkInfo",	"Event Logging (EL0)",
  "DnsEvents",	"Event Logging (EL0)",
  "DeviceNetworkEvents",	"Event Logging (EL0)",
  "AzureDiagnostics",	"Event Logging (EL0)",
  "Usage",	"Event Logging (EL0)",
  "SecurityIncident",	"Event Logging (EL0)",
  "SecurityAlert",	"Event Logging (EL0)",
  "AzureActivity",	"Event Logging (EL0)",
  "Heartbeat",	"Event Logging (EL0)",
  "OfficeActivity",	"Event Logging (EL0)",
  "SecurityEvent",	"Event Logging (EL0)",
  "Syslog",	"Event Logging (EL0)",
  "AWSCloudTrail",	"Event Logging (EL0)",
  "GWorkspaceActivityReports",	"Event Logging (EL0)",
  "AWSGuardDuty",	"Event Logging (EL0)",
  "AWSVPCFlow",	"Event Logging (EL0)",
  "Perf",	"Basic Event Logging (EL1)",
  "SentinelHealth",	"Basic Event Logging (EL1)",
  "DeviceLogonEvents",	"Basic Event Logging (EL1)",
  "DeviceEvents",	"Basic Event Logging (EL1)",
  "DeviceNetworkEvents",	"Basic Event Logging (EL1)",
  "DeviceFileEvents",	"Basic Event Logging (EL1)",
  "DeviceRegistryEvents",	"Basic Event Logging (EL1)",
  "DeviceProcessEvents",	"Basic Event Logging (EL1)",
  "VMConnection",	"Basic Event Logging (EL1)",
  "EmailEvents",	"Basic Event Logging (EL1)",
  "ThreatIntelligenceIndicator",	"Basic Event Logging (EL1)",
  "SecurityRecommendation",	"Basic Event Logging (EL1)",
  "DeviceProcessEvents",	"Basic Event Logging (EL1)",
  "ConfigurationData",	"Basic Event Logging (EL1)",
  "ConfigurationChange",	"Basic Event Logging (EL1)",
  "GatewayDiagnosticLog",	"Basic Event Logging (EL1)",
  "TunnelDiagnosticLog",	"Basic Event Logging (EL1)",
  "IKEDiagnosticLog",	"Basic Event Logging (EL1)",
  "RouteDiagnosticLog",	"Basic Event Logging (EL1)",
  "PS2DiagnosticLog",	"Basic Event Logging (EL1)",
  "Event",	"Basic Event Logging (EL1)",
  "SqlAtpStatus",	"Basic Event Logging (EL1)",
  "ConstainerInstanceLog_CL",	"Basic Event Logging (EL1)",
  "ContainerEvent_CL",	"Basic Event Logging (EL1)",
  "InsightsMetrics",	"Intermediate Event Logging (EL2)",
  "EmailUrlInfo",	"Intermediate Event Logging (EL2)",
  "EmailAttachmentInfo",	"Intermediate Event Logging (EL2)",
  "InformationProtectionLogs_CL",	"Intermediate Event Logging (EL2)",
  "CloudAppEvents",	"Intermediate Event Logging (EL2)",
  "ContainerInventory",	"Intermediate Event Logging (EL2)",
  "Update",	"Advanced Event Logging (EL3)",
  "BehaviorAnalytics",	"Advanced Event Logging (EL3)",
  "Anomalies",	"Advanced Event Logging (EL3)",
  "SecurityRegulatoryCompliance",	"Advanced Event Logging (EL3)"
  ];
  let LastLogTime = Usage
  | summarize LastLog_Time = arg_max(TimeGenerated, *) by DataType;
  Usage
  | summarize last_log = datetime_diff("day",now(), max(TimeGenerated)) by DataType
  | where last_log > 0
  | join kind=inner (LastLogTime) on DataType
  | project DataTable = DataType, ['Last Log Received'] = last_log, LastLog_Time
  | where ['Last Log Received'] > 2
  | join kind=inner (M2131Mapping) on DataTable
  | project-away DataTable1
  | order by ['Last Log Received'] desc
  | extend CloudApplication = DataTable
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: DataTable
version: 1.0.0
kind: Scheduled

Stages and Predicates

Let Binding: M2131Mapping used in Stage 7

let M2131Mapping = datatable(DataTable:string, MaturityLevel:string)
[
"SigninLogs",	"Event Logging (EL0)",
"AADManagedIdentitySignInLogs", "Event Logging (EL0)",
"AADServicePrincipalSignInLogs",	"Event Logging (EL0)",
"StorageBlobLogs",	"Event Logging (EL0)",
"StorageFileLogs",	"Event Logging (EL0)",
"AzureMetrics",	"Event Logging (EL0)",
"AuditLogs",	"Event Logging (EL0)",
"IdentityInfo",	"Event Logging (EL0)",
"CommonSecurityLog",	"Event Logging (EL0)",
"ThreatIntelligenceIndicator",	"Event Logging (EL0)",
"DeviceNetworkInfo",	"Event Logging (EL0)",
"DnsEvents",	"Event Logging (EL0)",
"DeviceNetworkEvents",	"Event Logging (EL0)",
"AzureDiagnostics",	"Event Logging (EL0)",
"Usage",	"Event Logging (EL0)",
"SecurityIncident",	"Event Logging (EL0)",
"SecurityAlert",	"Event Logging (EL0)",
"AzureActivity",	"Event Logging (EL0)",
"Heartbeat",	"Event Logging (EL0)",
"OfficeActivity",	"Event Logging (EL0)",
"SecurityEvent",	"Event Logging (EL0)",
"Syslog",	"Event Logging (EL0)",
"AWSCloudTrail",	"Event Logging (EL0)",
"GWorkspaceActivityReports",	"Event Logging (EL0)",
"AWSGuardDuty",	"Event Logging (EL0)",
"AWSVPCFlow",	"Event Logging (EL0)",
"Perf",	"Basic Event Logging (EL1)",
"SentinelHealth",	"Basic Event Logging (EL1)",
"DeviceLogonEvents",	"Basic Event Logging (EL1)",
"DeviceEvents",	"Basic Event Logging (EL1)",
"DeviceNetworkEvents",	"Basic Event Logging (EL1)",
"DeviceFileEvents",	"Basic Event Logging (EL1)",
"DeviceRegistryEvents",	"Basic Event Logging (EL1)",
"DeviceProcessEvents",	"Basic Event Logging (EL1)",
"VMConnection",	"Basic Event Logging (EL1)",
"EmailEvents",	"Basic Event Logging (EL1)",
"ThreatIntelligenceIndicator",	"Basic Event Logging (EL1)",
"SecurityRecommendation",	"Basic Event Logging (EL1)",
"DeviceProcessEvents",	"Basic Event Logging (EL1)",
"ConfigurationData",	"Basic Event Logging (EL1)",
"ConfigurationChange",	"Basic Event Logging (EL1)",
"GatewayDiagnosticLog",	"Basic Event Logging (EL1)",
"TunnelDiagnosticLog",	"Basic Event Logging (EL1)",
"IKEDiagnosticLog",	"Basic Event Logging (EL1)",
"RouteDiagnosticLog",	"Basic Event Logging (EL1)",
"PS2DiagnosticLog",	"Basic Event Logging (EL1)",
"Event",	"Basic Event Logging (EL1)",
"SqlAtpStatus",	"Basic Event Logging (EL1)",
"ConstainerInstanceLog_CL",	"Basic Event Logging (EL1)",
"ContainerEvent_CL",	"Basic Event Logging (EL1)",
"InsightsMetrics",	"Intermediate Event Logging (EL2)",
"EmailUrlInfo",	"Intermediate Event Logging (EL2)",
"EmailAttachmentInfo",	"Intermediate Event Logging (EL2)",
"InformationProtectionLogs_CL",	"Intermediate Event Logging (EL2)",
"CloudAppEvents",	"Intermediate Event Logging (EL2)",
"ContainerInventory",	"Intermediate Event Logging (EL2)",
"Update",	"Advanced Event Logging (EL3)",
"BehaviorAnalytics",	"Advanced Event Logging (EL3)",
"Anomalies",	"Advanced Event Logging (EL3)",
"SecurityRegulatoryCompliance",	"Advanced Event Logging (EL3)"
];

Let Binding: LastLogTime used in Stage 4

let LastLogTime = Usage
| summarize LastLog_Time = arg_max(TimeGenerated, *) by DataType;

Stage 1: source

Usage

Stage 2: summarize

| summarize last_log = datetime_diff("day",now(), max(TimeGenerated)) by DataType

Stage 3: where

| where last_log > 0

Stage 4: join

| join kind=inner (LastLogTime) on DataType

Stage 5: project

project DataTable, Last Log Received, LastLog_Time

Stage 6: where

where ["Last Log Received"] > 2

Stage 7: join

join kind=inner (M2131Mapping) on DataTable

Stage 8: project-away

project-away DataTable1

Stage 9: sort

sort by Last Log Received

Stage 10: extend

extend CloudApplication

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Last Log Receivedgt
  • 2 transforms: number, kusto_literal_column
field:"Last Log Received" kind:gt value:"2"
last_loggt
  • 0 transforms: number
field:"last_log" kind:gt value:"0"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataTableproject
Last Log Receivedproject
LastLog_Timeproject
CloudApplicationextend

Multi-Factor Authentication User Locked

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

Detects when the allowed number of multi-factor authentication attempts is exceeded for a user.

Rule Body

id: ebdd9cf8-c41c-460e-95d8-e5bc3cd9763e
name: Multi-Factor Authentication User Locked
description: Detects when the allowed number of multi-factor authentication attempts
  is exceeded for a user.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 40206\n| project\n  Date =\
  \ format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n\
  \    EventId = instanceId,\n    UserName = user,\n   MessageDetails = Description,\n\
  \   Severity = SeverityDescription\n| project Date, DataSource, EventId, UserName,MessageDetails,Severity"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 40206

Stage 3: project

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

Stage 4: project

| project Date, DataSource, EventId, UserName,MessageDetails,Severity

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
UserNameproject

Pathlock TDnR - Outgoing Spool Print Job Events

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

Detects recording of outgoing spool print jobs in SAP, forwarded by Pathlock Threat Detection and Response. Anomalous print job activity may indicate unauthorized printing of sensitive documents, data exfiltration via print-to-file operations, or misuse of SAP output management.

MITRE ATT&CK Coverage

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c60
name: Pathlock TDnR - Outgoing Spool Print Job Events
kind: Scheduled
description: >-
  Detects recording of outgoing spool print jobs in SAP, forwarded by Pathlock Threat Detection
  and Response. Anomalous print job activity may indicate unauthorized printing of sensitive
  documents, data exfiltration via print-to-file operations, or misuse of SAP output management.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1048
query: |
  Pathlock_TDnR_CL
  | where DataSource == "SPOOL_OUTPUT_REQUEST"
  | project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
            Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
            MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Bname
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIp
eventGroupingSettings:
  aggregationKind: SingleAlert
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Stage 1: source

Pathlock_TDnR_CL

Stage 2: where

| where DataSource == "SPOOL_OUTPUT_REQUEST"

Stage 3: project

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

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

SailPointIdentityNowEventType

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

'Created to detect failed events of particular type from SailPointIDN_Events.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 48bb92e2-bad4-4fd4-9684-26cb188299b7
name: SailPointIdentityNowEventType
description: |
  'Created to detect failed events of particular type from SailPointIDN_Events.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: SailPointIdentityNow
    dataTypes:
      - SailPointIDN_Events
  - connectorId: SailPointIdentityNowConnector
    dataTypes:
      - SailPointIDN_Events
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1133
query: |
  declare query_parameters(lbperiod:timespan = 14d, type:string = "ACCESS_ITEM");
  SailPointIDN_Events
  | where TimeGenerated > ago(lbperiod)
  | where EventType == type
  | where Status == "FAILED"
  | sort by Created
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: TechnicalName
version: 1.1.0
kind: Scheduled

Stages and Predicates

Stage 1: source

declare query_parameters(lbperiod:timespan = 14d, type:string = "ACCESS_ITEM");
SailPointIDN_Events

Stage 2: where

| where TimeGenerated > ago(lbperiod)

Stage 3: where

| where EventType == type

Stage 4: where

| where Status == "FAILED"

Stage 5: sort

| sort by Created

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Statuseq
  • FAILED
field:"Status" kind:eq value:"FAILED"

Scale-Out Backup Repository Deleted

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

Detects when a scale-out backup repository is deleted from Veeam Backup & Replication. This might indicate unauthorized removal of critical components.

Rule Body

id: 4dc85727-573c-4722-8173-026e2a1d20db
name: Scale-Out Backup Repository Deleted
description: Detects when a scale-out backup repository is deleted from Veeam Backup
  & Replication. This might indicate unauthorized removal of critical components.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 30200\n| extend Name = extract(\"\
  Name=\\\"([^\\\"]*)\\\"\", 1, SyslogMessage)\n| project\n    Date = format_datetime(TimeGenerated,\
  \ 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n    EventId = instanceId,\n\
  \    UserName = user,\n    [\"Object Name\"] = Name,\n    MessageDetails = Description,\n\
  \    Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 30200

Stage 3: extend

| extend Name = extract("Name=\"([^\"]*)\"", 1, SyslogMessage)

Stage 4: project

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Object Nameproject
Severityproject
UserNameproject

Shadow Credentials Added to Account

#
Group by
DedupEntity
Author
FalconForce
Source
github.com/FalconForceTeam/FalconFriday

This query searches for modifications to the 'msDS-KeyCredentialLink' property in Active Directory, introduced in Windows Server 2016. There are two different events which contain information to detect such changes 5136 and 4662. This detection uses the 5136, which is the preferred event to use.

Known False Positives

  • The property can also be used for legitimate purposes. However, the legitimate use of the property is limited in most environments.

MITRE ATT&CK Coverage

References

Telemetry Coverage

Rule Body

let timeframe = 2*1h;
let RuleId = "0275";
let DedupFields = dynamic(["TimeGenerated", "SubjectUserName", "Computer"]);
SecurityEvent
| where ingestion_time() >= ago(timeframe)
| where EventID == 5136
| extend AttributeName = extract("<Data Name=\"AttributeLDAPDisplayName\">(.*?)</Data>", 1, EventData)
| extend ObjectDN = extract("<Data Name=\"ObjectDN\">(.*?)</Data>", 1, EventData)
| extend SubjectUserName = extract("<Data Name=\"SubjectUserName\">(.*?)</Data>", 1, EventData)
| where AttributeName has "msDS-KeyCredentialLink"
| where not(SubjectUserName endswith "$" and ObjectDN startswith strcat("CN=", replace_string(SubjectUserName, "$", ""), ",")) // Machine account changing its own msDS-KeyCredentialLink.
| extend HostName=tostring(split(Computer,".")[0]),DnsDomain=iif(Computer contains ".", substring(Computer, indexof(Computer, ".") + 1, strlen(Computer)),"")
| extend AccountName=iif(Account contains @"\",tostring(split(Account,@"\")[1]),Account),AccountDomain=iif(Account contains @"\",tostring(split(Account,@"\")[0]),"")
// Begin environment-specific filter.
// End environment-specific filter.
// Begin de-duplication logic.
| extend DedupFieldValues=pack_all()
| mv-apply e=DedupFields to typeof(string) on (
    extend DedupValue=DedupFieldValues[tostring(e)]
    | order by e // Sorting is required to ensure make_list is deterministic.
    | summarize DedupValues=make_list(DedupValue)
)
| extend DedupEntity=strcat_array(DedupValues, "|")
| project-away DedupFieldValues, DedupValues
| join kind=leftanti (
    SecurityAlert
    | where AlertName has RuleId and ProviderName has "ASI"
    | where TimeGenerated >= ago(timeframe)
    | extend DedupEntity = tostring(parse_json(tostring(parse_json(ExtendedProperties)["Custom Details"])).DedupEntity[0])
    | project DedupEntity
) on DedupEntity
// End de-duplication logic.

Stages and Predicates

Parameters

let timeframe = 2*1h;
let RuleId = "0275";
let DedupFields = dynamic(["TimeGenerated", "SubjectUserName", "Computer"]);

Stage 1: source

SecurityEvent

Stage 2: where

| where ingestion_time() >= ago(timeframe)

Stage 3: where

| where EventID == 5136

Stage 4: extend (3 consecutive steps)

| extend AttributeName = extract("<Data Name=\"AttributeLDAPDisplayName\">(.*?)</Data>", 1, EventData)
| extend ObjectDN = extract("<Data Name=\"ObjectDN\">(.*?)</Data>", 1, EventData)
| extend SubjectUserName = extract("<Data Name=\"SubjectUserName\">(.*?)</Data>", 1, EventData)

Stage 5: where

| where AttributeName has "msDS-KeyCredentialLink"

Stage 6: where

| where not(SubjectUserName endswith "$" and ObjectDN startswith strcat("CN=", replace_string(SubjectUserName, "$", ""), ","))

Stage 7: extend (3 consecutive steps)

| extend HostName=tostring(split(Computer,".")[0]),DnsDomain=iif(Computer contains ".", substring(Computer, indexof(Computer, ".") + 1, strlen(Computer)),"")
| extend AccountName=iif(Account contains @"\",tostring(split(Account,@"\")[1]),Account),AccountDomain=iif(Account contains @"\",tostring(split(Account,@"\")[0]),"")
| extend DedupFieldValues=pack_all()

Stage 8: kusto:mv-apply

| mv-apply e=DedupFields to typeof(string) on (
    extend DedupValue=DedupFieldValues[tostring(e)]
    | order by e
    | summarize DedupValues=make_list(DedupValue)
)

Stage 9: extend

| extend DedupEntity=strcat_array(DedupValues, "|")

Stage 10: project-away

| project-away DedupFieldValues, DedupValues

Stage 11: join (negated)

join kind=leftanti (SecurityAlert) on DedupEntity

Stage 12: summarize aggregation inside the mv-apply branch

summarize

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AttributeNamematch
  • msDS-KeyCredentialLink transforms: term
field:"AttributeName" kind:match value:"msDS-KeyCredentialLink"
EventIDeq
  • 5136 corpus 20 (kusto 18, elastic 2)
field:"EventID" kind:eq value:"5136"

Exclusions

The rule actively suppresses these predicates.

Tailscale Premium: Unexpected exit-node egress

#
Status
available
Severity
medium
Time window
1h
Group by
ExitDst, NodeId, Src, SrcNodeName, SrcOs, SrcUser
Source
github.com/Azure/Azure-Sentinel

Identifies when a node sends traffic via an exit node not used in the prior 7-day baseline. First-seen exit destinations from a node may indicate routing-policy drift, data exfiltration, or compromise.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control
Exfiltration

Rule Body

id: c1d2e3f4-1a2b-3c4d-5e6f-7a8b9c0d1e2f
name: "Tailscale Premium: Unexpected exit-node egress"
description: Identifies when a node sends traffic via an exit node not used in the prior 7-day baseline. First-seen exit destinations from a node may indicate routing-policy drift, data exfiltration, or compromise.
description-detailed: |
  Identifies when a node sends traffic via an exit node not used in the prior 7-day baseline. First-seen exit destinations from a specific source may indicate unsanctioned egress, a compromised node pivoting, or a policy misconfiguration. Requires Tailscale Premium or Enterprise.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: TailscalePremiumCCF
    dataTypes:
      - Tailscale_Network_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
  - Exfiltration
relevantTechniques:
  - T1090
  - T1041
query: |
  let baseline = 7d;
  let recent = 1h;
  let recentEgress =
      Tailscale_Network_CL
      | where TimeGenerated > ago(recent)
      | where HasExitTraffic
      | mv-expand t = ExitTraffic
      | extend Src = tostring(t.src), ExitDst = tostring(t.dst), Bytes = tolong(t.txBytes) + tolong(t.rxBytes)
      | summarize FirstSeen = min(TimeGenerated), TotalBytes = sum(Bytes) by NodeId, SrcNodeName, SrcUser, SrcOs, Src, ExitDst;
  let baselineEgress =
      Tailscale_Network_CL
      | where TimeGenerated between (ago(baseline + recent) .. ago(recent))
      | where HasExitTraffic
      | mv-expand t = ExitTraffic
      | extend Src = tostring(t.src), ExitDst = tostring(t.dst)
      | distinct NodeId, Src, ExitDst;
  recentEgress
  | join kind=leftanti baselineEgress on NodeId, Src, ExitDst
  | extend TotalMB = round(TotalBytes / 1024.0 / 1024.0, 2)
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SrcNodeName
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: SrcUser
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: Src
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

Parameters

let baseline = 7d;
let recent = 1h;

let recentEgress is inlined into the numbered stages below.

Let Binding: baselineEgress used in Stage 7

let baselineEgress = Tailscale_Network_CL
    | where TimeGenerated between (ago(baseline + recent) .. ago(recent))
    | where HasExitTraffic
    | mv-expand t = ExitTraffic
    | extend Src = tostring(t.src), ExitDst = tostring(t.dst)
    | distinct NodeId, Src, ExitDst;

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

Stage 1: source

Tailscale_Network_CL

Stage 2: where

| where TimeGenerated > ago(recent)

Stage 3: where

| where HasExitTraffic

Stage 4: mv-expand

| mv-expand t = ExitTraffic

Stage 5: extend

| extend Src = tostring(t.src), ExitDst = tostring(t.dst), Bytes = tolong(t.txBytes) + tolong(t.rxBytes)

Stage 6: summarize

| summarize FirstSeen = min(TimeGenerated), TotalBytes = sum(Bytes) by NodeId, SrcNodeName, SrcUser, SrcOs, Src, ExitDst

Stage 7: join (negated)

join kind=leftanti (baselineEgress) on NodeId, Src, ExitDst

Stage 8: extend

extend TotalMB

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
HasExitTrafficeq
  • true transforms: boolean
field:"HasExitTraffic" kind:eq value:"true"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
HasExitTrafficeqtrueexcludes:HasExitTraffic field:"HasExitTraffic" value:"true"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ExitDstsummarize
FirstSeensummarize
NodeIdsummarize
Srcsummarize
SrcNodeNamesummarize
SrcOssummarize
SrcUsersummarize
TotalBytessummarize
TotalMBextend

Theom High Risks

#

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

"Creates Microsoft Sentinel incidents for high risk Theom alerts."

MITRE ATT&CK Coverage

Rule Body

id: 74b80987-0a62-448c-8779-47b02e17d3cf
name: Theom High Risks
description: |
  "Creates Microsoft Sentinel incidents for high risk Theom alerts."
severity: High
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 == "P2"
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: "Theom Alert ID: {{id_s}} "
  alertDescriptionFormat: "\nSummary: {{summary_s}}  \nAdditional info: {{details_s}}\nPlease investigate further on Theom UI at {{deepLink_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 == "P2"

Indicators

These rows show field, operator, and value matches.

TI Map Email entity to OfficeActivity

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

'Identifies a match in OfficeActivity table from any Email IOC from TI'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 795d43a3-6edc-4c99-971f-00d05841e5ac
name: TI Map Email entity to OfficeActivity
description: |
  'Identifies a match in OfficeActivity table from any Email IOC from TI'
severity: Medium
requiredDataConnectors:
  - connectorId: Office365
    dataTypes:
      - OfficeActivity
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let emailregex = @'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$';
  let OfficeEvents = materialize(
    OfficeActivity
    | where isnotempty(UserId)
    | where TimeGenerated >= ago(dt_lookBack)
    | where UserId matches regex emailregex
    | project-rename  OfficeActivity_TimeGenerated = TimeGenerated);
  let OfficeActivityUPNs = OfficeEvents | distinct UserId = tolower(UserId) | summarize make_list(UserId);
  ThreatIntelIndicators
  | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
  | where IndicatorType == "email-addr"
  | extend EmailSenderAddress = ObservableValue
  | extend EmailSourceDomain = substring(EmailSenderAddress, indexof(EmailSenderAddress, "@") + 1, strlen(EmailSenderAddress) - indexof(EmailSenderAddress, "@") - 1)
  | extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
  | extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
  | where TimeGenerated >= ago(ioc_lookBack) 
  | where tolower(EmailSenderAddress) in (OfficeActivityUPNs)
  | 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;"
  | project-reorder *, TrafficLightProtocolLevel, EmailSenderAddress, EmailSourceDomain, Type
  // using innerunique to keep perf fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
  | join kind=innerunique (OfficeEvents) on $left.EmailSenderAddress == $right.UserId
  | where OfficeActivity_TimeGenerated < ValidUntil
  | summarize OfficeActivity_TimeGenerated = arg_max(OfficeActivity_TimeGenerated, *) by Id, UserId
  | extend Description = tostring(parse_json(Data).description)
  | extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
  | project OfficeActivity_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence, EmailSenderAddress, EmailSourceDomain, UserId, ClientIP, Operation, UserType, RecordType, OfficeWorkload, Parameters, Url
  | extend Name = tostring(split(UserId, '@', 0)[0]), UPNSuffix = tostring(split(UserId, '@', 1)[0])
  | extend timestamp = OfficeActivity_TimeGenerated
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserId
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.2.12
kind: Scheduled

Stages and Predicates

Parameters

let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let emailregex = @'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$';

Let Binding: OfficeEvents used in Stage 12

let OfficeEvents = materialize(
  OfficeActivity
  | where isnotempty(UserId)
  | where TimeGenerated >= ago(dt_lookBack)
  | where UserId matches regex emailregex
  | project-rename  OfficeActivity_TimeGenerated = TimeGenerated);

Let Binding: OfficeActivityUPNs used in Stage 6

let OfficeActivityUPNs = OfficeEvents | distinct UserId = tolower(UserId) | summarize make_list(UserId);

Stage 1: source

ThreatIntelIndicators

Stage 2: extend

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

Stage 3: where

| where IndicatorType == "email-addr"

Stage 4: extend (4 consecutive steps)

| extend EmailSenderAddress = ObservableValue
| extend EmailSourceDomain = substring(EmailSenderAddress, indexof(EmailSenderAddress, "@") + 1, strlen(EmailSenderAddress) - indexof(EmailSenderAddress, "@") - 1)
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")

Stage 5: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 6: where

| where tolower(EmailSenderAddress) in (OfficeActivityUPNs)

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: project-reorder

| project-reorder *, TrafficLightProtocolLevel, EmailSenderAddress, EmailSourceDomain, Type

Stage 12: join

| join kind=innerunique (OfficeEvents) on $left.EmailSenderAddress == $right.UserId

Stage 13: where

where OfficeActivity_TimeGenerated < ValidUntil

Stage 14: summarize

summarize OfficeActivity_TimeGenerated by Id, UserId

Stage 15: extend

extend Description

Stage 16: extend

extend ActivityGroupNames

Stage 17: project

project ActivityGroupNames, ClientIP, Confidence, Description, EmailSenderAddress, EmailSourceDomain, Id, OfficeActivity_TimeGenerated, OfficeWorkload, Operation, Parameters, RecordType, Url, UserId, UserType, ValidUntil

Stage 18: extend

extend Name, UPNSuffix

Stage 19: extend

extend timestamp

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
ClientIPproject
Confidenceproject
Descriptionproject
EmailSenderAddressproject
EmailSourceDomainproject
Idproject
OfficeActivity_TimeGeneratedproject
OfficeWorkloadproject
Operationproject
Parametersproject
RecordTypeproject
Urlproject
UserIdproject
UserTypeproject
ValidUntilproject
Nameextend
UPNSuffixextend
timestampextend

TI Map URL Entity to UrlClickEvents

#
Severity
medium
Time window
14d
Group by
$left.TI_Url, $right.Url, $right.UrlChain, Id, ObservableValue, ReportId
Source
github.com/Azure/Azure-Sentinel

This query identifies any URL indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in UrlClickEvents.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Telemetry Coverage

Rule Body

id: ad4fa1f2-2189-459c-9458-f77d2039d2f5
name: TI Map URL Entity to UrlClickEvents
description: |
  'This query identifies any URL indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in UrlClickEvents.'
severity: Medium
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - UrlClickEvents
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelIndicators
  - 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 UrlClickEvents_ = materialize(UrlClickEvents
      | where TimeGenerated >= ago(dt_lookBack)
      | extend UrlClickEvents_TimeGenerated = TimeGenerated);
  let ChainReportID = UrlClickEvents_
      | mv-expand todynamic(UrlChain)
      | extend UrlChain = tolower(UrlChain)
      | project ReportId, Url, UrlChain;
  // Url is not always in UrlChain, so we need to check both
  let ClickedUrls = 
    (union isfuzzy=false (ChainReportID), (ChainReportID | project Url = UrlChain))
    | distinct Url
    | summarize make_list(Url);
  let TI = materialize(ThreatIntelIndicators
  //extract key part of kv pair
  | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
  | where IndicatorType == "url"
  | extend Url = ObservableValue
    | extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
    | where TimeGenerated >= ago(ioc_lookBack)
    | where tolower(Url) in (ClickedUrls)
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
    | where IsActive and (ValidUntil > now() or isempty(ValidUntil))
    | project-rename TI_Url = Url, TI_Type = Type
    );
  (union isfuzzy=false (TI | join kind=innerunique (ChainReportID) on $left.TI_Url == $right.UrlChain),
    (TI | join kind=innerunique (ChainReportID) on $left.TI_Url == $right.Url))
  | project-away UrlChain
  | join kind=innerunique (UrlClickEvents_) on ReportId
  | where UrlClickEvents_TimeGenerated < ValidUntil
  | summarize UrlClickEvents_TimeGenerated = arg_max(UrlClickEvents_TimeGenerated, *) by Id
  | extend Description = tostring(parse_json(Data).description)
  | extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
  | project UrlClickEvents_TimeGenerated, AccountUpn, Description, ActivityGroupNames, Id, Type, ValidUntil, Confidence, Url, NetworkMessageId
  | extend timestamp = UrlClickEvents_TimeGenerated
  | extend timestamp = UrlClickEvents_TimeGenerated, Name = tostring(split(AccountUpn, '@', 0)), UPNSuffix = tostring(split(AccountUpn, '@', 1))
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountUpn
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.0.5
kind: Scheduled

Stages and Predicates

Parameters

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

Let Binding: UrlClickEvents_

let UrlClickEvents_ = materialize(UrlClickEvents
    | where TimeGenerated >= ago(dt_lookBack)
    | extend UrlClickEvents_TimeGenerated = TimeGenerated);

Let Binding: ChainReportID

let ChainReportID = UrlClickEvents_
    | mv-expand todynamic(UrlChain)
    | extend UrlChain = tolower(UrlChain)
    | project ReportId, Url, UrlChain;

Let Binding: ClickedUrls

let ClickedUrls = (union isfuzzy=false (ChainReportID), (ChainReportID | project Url = UrlChain))
  | distinct Url
  | summarize make_list(Url);

Union isfuzzy=false (2 Sources)

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

Leg 1: TI

TI | join kind=innerunique (ChainReportID) on $left.TI_Url == $right.UrlChain

Leg 2: TI

TI | join kind=innerunique (ChainReportID) on $left.TI_Url == $right.Url

Applied to the Combined Result

| project-away UrlChain
| join kind=innerunique (UrlClickEvents_) on ReportId
| where UrlClickEvents_TimeGenerated < ValidUntil
| summarize UrlClickEvents_TimeGenerated = arg_max(UrlClickEvents_TimeGenerated, *) by Id
| extend Description = tostring(parse_json(Data).description)
| extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
| project UrlClickEvents_TimeGenerated, AccountUpn, Description, ActivityGroupNames, Id, Type, ValidUntil, Confidence, Url, NetworkMessageId
| extend timestamp = UrlClickEvents_TimeGenerated
| extend timestamp = UrlClickEvents_TimeGenerated, Name = tostring(split(AccountUpn, '@', 0)), UPNSuffix = tostring(split(AccountUpn, '@', 1))

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountUpnproject
ActivityGroupNamesproject
Confidenceproject
Descriptionproject
Idproject
NetworkMessageIdproject
Typeproject
Urlproject
UrlClickEvents_TimeGeneratedproject
ValidUntilproject
timestampextend
Nameextend
UPNSuffixextend

UniFi Site Manager: WAN uptime below 99%

#
Status
available
Severity
medium
Time window
1h
Group by
SiteId, SiteName
Source
github.com/Azure/Azure-Sentinel

Identifies when site WAN uptime falls below 99 percent, an SLA-grade availability problem. Combine with the ISP Downtime rule for root-cause attribution.

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: 2bb29f25-833a-5544-4a8e-9bf6c4d8da56
name: "UniFi Site Manager: WAN uptime below 99%"
description: |
  Identifies when site WAN uptime falls below 99 percent, an SLA-grade availability problem. Combine with the ISP Downtime rule for root-cause attribution.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: UniFiSiteManagerConnectorDefinition
    dataTypes:
      - Unifi_SiteManager_Sites_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1499
subTechniques:
  - T1499.002
query: |
  Unifi_SiteManager_Sites_CL
        | where TimeGenerated > ago(1h)
  | extend wanUptime = WanUptimePct
        | where isnotnull(wanUptime) and wanUptime < 99.0
        | summarize arg_max(TimeGenerated, *) by SiteId, SiteName
        | extend Activity = strcat('WAN uptime ', round(wanUptime, 2), '% (below 99% SLA threshold)')
        | project TimeGenerated, SiteId, SiteName, Activity, WanUptimePct = round(wanUptime, 2)
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SiteId
      - identifier: DnsDomain
        columnName: SiteName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.1

Stages and Predicates

Stage 1: source

Unifi_SiteManager_Sites_CL

Stage 2: where

| where TimeGenerated > ago(1h)

Stage 3: extend

| extend wanUptime = WanUptimePct

Stage 4: where

| where isnotnull(wanUptime) and wanUptime < 99.0

Stage 5: summarize

| summarize arg_max(TimeGenerated, *) by SiteId, SiteName

Stage 6: extend

| extend Activity = strcat('WAN uptime ', round(wanUptime, 2), '% (below 99% SLA threshold)')

Stage 7: project

| project TimeGenerated, SiteId, SiteName, Activity, WanUptimePct = round(wanUptime, 2)

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
wanUptimeis_not_null
  • (no value, null check)
field:"wanUptime" kind:is_not_null
wanUptimelt
  • 99.0 transforms: number
field:"WanUptimePct" kind:lt value:"99.0"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Activityproject
SiteIdproject
SiteNameproject
TimeGeneratedproject
WanUptimePctproject

User or Group Deleted

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

Detects when a user or user group is deleted from Veeam Backup & Replication. This might indicate unauthorized removal of critical components.

Rule Body

id: d297961f-f8a3-4f84-865d-ec3a07641699
name: User or Group Deleted
description: Detects when a user or user group is deleted from Veeam Backup & Replication.
  This might indicate unauthorized removal of critical components.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 31400\n| project\n    Date\
  \ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n\
  \    EventId = instanceId,\n    [\"User Name\"] = user,\n  MessageDetails = Description,\n\
  \   Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 31400

Stage 3: project

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
User Nameproject

Vectra AI Detect - Detections with High Severity

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

'Create an incident for high severity malicious behavior detected by Vectra AI (Threat score superior to 7.0). The Severity is a mapping with the Threat score assigned to a detection. It ranges between 0 and 10. The severity_threshold variable can be adjusted as desired.'

MITRE ATT&CK Coverage

Rule Body

id: 39e48890-2c02-487e-aa9e-3ba494061798
name: Vectra AI Detect - Detections with High Severity 
description: |
  'Create an incident for high severity malicious behavior detected by Vectra AI (Threat score superior to 7.0). 
  The Severity is a mapping with the Threat score assigned to a detection. It ranges between 0 and 10. 
  The severity_threshold variable can be adjusted as desired.' 
severity: High
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: |
  // Edit this variable to only keep the tactics where an incident needs to be created (Defaults are: "COMMAND & CONTROL", "BOTNET ACTIVITY", "EXFILTRATION", "LATERAL MOVEMENT", "RECONNAISSANCE")   
  let configured_tactics = dynamic(["COMMAND & CONTROL", "BOTNET ACTIVITY", "EXFILTRATION", "LATERAL MOVEMENT", "RECONNAISSANCE"]);
  //default threshold is 7 (meaning a threat score of 70)
  let severity_threshold = 7.0;
  //Map by default to High Severity in Sentinel
  let Severity = "High";
  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
  | where todecimal(LogSeverity) >= severity_threshold
  | extend Tactic = case( Category == "COMMAND & CONTROL", "CommandAndControl",
                          Category == "BOTNET ACTIVITY" , "Impact",
                          Category == "EXFILTRATION", "Exfiltration",
                          Category == "LATERAL MOVEMENT", "LateralMovement",
                          Category == "RECONNAISSANCE", "Discovery",
                          "UNKNOWN")
  | 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)
  | extend account = extract("account=(.+?);", 1, AdditionalExtensions)
  | extend upn = iff(account matches regex ":", tostring(split(account,":")[1]) ,tostring(split(account,":")[0]))
  | extend name = tostring(split(upn, "@")[0])
  | extend upn_suffix = tostring(split(upn, "@")[1])
  | extend source_entity = case( isnotempty(upn), upn,
                                 isnotempty(SourceHostName), SourceHostName,
                                 "UNKNWON") 
  | where Category in (configured_tactics) 
  | summarize arg_max(threat_score, *) by source_entity, Activity
  | sort by TimeGenerated
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SourceHostName
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: name
      - identifier: UPNSuffix
        columnName: upn_suffix
alertDetailsOverride:
  alertDisplayNameFormat: Vectra AI Detect - {{Activity}} detected
  alertDescriptionFormat: Source entity is {{source_entity}} and category is {{Category}}. Threat score is {{threat_score}}.
  alertSeverityColumnName: Severity
  alertDynamicProperties:
    - alertProperty: AlertLink
      value: vectra_URL
    - alertProperty: ProductName
      value: DeviceProduct
    - alertProperty: ProviderName
      value: DeviceVendor
    - alertProperty: ConfidenceScore
      value: certainty_score
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: true
    lookbackDuration: 7d
    matchingMethod: AllEntities
customDetails:
  AttackType: Activity
  AttackCategory: Category  
version: 1.1.0
kind: Scheduled

Stages and Predicates

Parameters

let configured_tactics = dynamic(["COMMAND & CONTROL", "BOTNET ACTIVITY", "EXFILTRATION", "LATERAL MOVEMENT", "RECONNAISSANCE"]);
let severity_threshold = 7.0;
let Severity = "High";

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: where

| where todecimal(LogSeverity) >= severity_threshold

Stage 11: extend (8 consecutive steps)

| extend Tactic = case( Category == "COMMAND & CONTROL", "CommandAndControl",
                        Category == "BOTNET ACTIVITY" , "Impact",
                        Category == "EXFILTRATION", "Exfiltration",
                        Category == "LATERAL MOVEMENT", "LateralMovement",
                        Category == "RECONNAISSANCE", "Discovery",
                        "UNKNOWN")
| 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)
| extend account = extract("account=(.+?);", 1, AdditionalExtensions)
| extend upn = iff(account matches regex ":", tostring(split(account,":")[1]) ,tostring(split(account,":")[0]))
| extend name = tostring(split(upn, "@")[0])
| extend upn_suffix = tostring(split(upn, "@")[1])
| extend source_entity = case( isnotempty(upn), upn,
                               isnotempty(SourceHostName), SourceHostName,
                               "UNKNWON")
Tactic =
ifCategory == "COMMAND & CONTROL""CommandAndControl"
elifCategory == "BOTNET ACTIVITY""Impact"
elifCategory == "EXFILTRATION""Exfiltration"
elifCategory == "LATERAL MOVEMENT""LateralMovement"
elifCategory == "RECONNAISSANCE""Discovery"
else"UNKNOWN"

Stage 12: where

| where Category in (configured_tactics)

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.

FieldKindValuesSearch
Categoryin
  • BOTNET ACTIVITY
  • COMMAND & CONTROL
  • EXFILTRATION
  • LATERAL MOVEMENT
  • RECONNAISSANCE
field:"Category" kind:in
DeviceEventClassIDne
  • asc
  • audit
  • campaigns
  • health
  • hsc
field:"DeviceEventClassID" kind:ne
DeviceProducteq
  • X Series
field:"DeviceProduct" kind:eq value:"X Series"
DeviceVendoreq
  • Vectra Networks
field:"DeviceVendor" kind:eq value:"Vectra Networks"
LogSeverityge
  • 7.0 transforms: todecimal
field:"LogSeverity" kind:ge value:"7.0"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Activitysummarize
source_entitysummarize

VMware ESXi - Multiple Failed Shell Login via SSH

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

Identifies a failed ESXi Shell login via SSH in a short TimeFrame. This could be suspicious activity especially if this alert is seen triggering many times within a short time frame which could be evidence of a brute-force attack. TriggerThreshold can be adapted.

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: 22d177d5-588c-4f1a-a332-2695f52079bb
name: VMware ESXi - Multiple Failed Shell Login via SSH
description: |
 Identifies a failed ESXi Shell login via SSH in a short TimeFrame. This could be suspicious activity especially if this alert is seen triggering many times within a short time frame which could be evidence of a brute-force attack. TriggerThreshold can be adapted. 
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  // To Adapt to your environment
  let triggerThreshold = 10;
  VMwareESXi
  | where SyslogMessage has "sshd"
  | where SyslogMessage contains "authentication failure"
  | extend SrcUsername = extract(@'\[info\]\s+\[(.*?)\]', 1, SyslogMessage)
  | extend DstHostname = extract(@'\[\d+\]\s+\[(.*?)\s+on', 1, SyslogMessage)
  | extend SrcIpAddr = extract(@'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', 1, SyslogMessage)
  | summarize make_set(SrcUsername),count() by SrcIpAddr,DstHostname
  | where count_ >= triggerThreshold
  | extend HostCustomEntity = DstHostname
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: HostCustomEntity
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let triggerThreshold = 10;

Stage 1: source

VMwareESXi

Stage 2: where

| where SyslogMessage has "sshd"

Stage 3: where

| where SyslogMessage contains "authentication failure"

Stage 4: extend (3 consecutive steps)

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

Stage 5: summarize

| summarize make_set(SrcUsername),count() by SrcIpAddr,DstHostname

Stage 6: where

| where count_ >= triggerThreshold

Stage 7: extend

| extend HostCustomEntity = DstHostname

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstHostnamesummarize
SrcIpAddrsummarize
HostCustomEntityextend

Back to all Kusto rules