Detection rules › Kusto

Kusto rules: 38

Azure DevOps New Extension Added

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

'Extensions add additional features to Azure DevOps. An attacker could use a malicious extension to conduct malicious activity. This query looks for new extensions that are not from a configurable list of approved publishers.'

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Rule Body

id: bf07ca9c-e408-443a-8939-6860a45a929e
name: Azure DevOps New Extension Added
description: |
  'Extensions add additional features to Azure DevOps. An attacker could use a malicious extension to conduct malicious activity. 
  This query looks for new extensions that are not from a configurable list of approved publishers.'
severity: Low
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1505
query: |
  let allowed_publishers = dynamic([]);
  ADOAuditLogs
  | where OperationName =~ "Extension.Installed"
  | extend ExtensionName = tostring(Data.ExtensionName)
  | extend PublisherName = tostring(Data.PublisherName)
  | where PublisherName !in (allowed_publishers)
  | project-reorder TimeGenerated, OperationName, ExtensionName, PublisherName, ActorUPN, IpAddress, UserAgent, ScopeDisplayName, Data
  | extend timestamp = TimeGenerated
  | extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ActorUPN
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IpAddress
version: 1.0.4
kind: Scheduled

Stages and Predicates

Parameters

let allowed_publishers = dynamic([]);

Stage 1: source

ADOAuditLogs

Stage 2: where

| where OperationName =~ "Extension.Installed"

Stage 3: extend

| extend ExtensionName = tostring(Data.ExtensionName)

Stage 4: extend

| extend PublisherName = tostring(Data.PublisherName)

Stage 5: where

| where PublisherName !in (allowed_publishers)

Stage 6: project-reorder

| project-reorder TimeGenerated, OperationName, ExtensionName, PublisherName, ActorUPN, IpAddress, UserAgent, ScopeDisplayName, Data

Stage 7: extend

| extend timestamp = TimeGenerated

Stage 8: extend

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

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
PublisherNameeq[]excludes:PublisherName field:"PublisherName" value:"[]"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ExtensionNameextend
PublisherNameextend
timestampextend
AccountNameextend
AccountUPNSuffixextend

Backup Repository Settings Updated

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

Detects when backup repository settings are updated in Veeam Backup & Replication. This might indicate configuration changes that require review.

Rule Body

id: f78e0bb9-2878-4e36-b9c9-6141bff8d3dd
name: Backup Repository Settings Updated
description: Detects when backup repository settings are updated in Veeam Backup &
  Replication. This might indicate configuration changes that require review.
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 == 28100\n| extend RepositoryName\
  \ =  extract(\"RepositoryName=\\\"(?<RepositoryName>[^\\\"]*)\\\"\", 1, SyslogMessage)\n\
  | project\n    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource\
  \ = original_host,\n    EventId = instanceId,\n    [\"User Name\"] = user,\n   \
  \ [\"Object Name\"] = RepositoryName,\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 == 28100

Stage 3: extend

| extend RepositoryName =  extract("RepositoryName=\"(?<RepositoryName>[^\"]*)\"", 1, SyslogMessage)

Stage 4: project

| project
    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
    DataSource = original_host,
    EventId = instanceId,
    ["User Name"] = user,
    ["Object Name"] = RepositoryName,
    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
User Nameproject

BloodHound Attack Path Finding - Website Contributor Role on Tier Zero Resource

#

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 - Website Contributor Role on Tier Zero Resource

Rule Body

id: a1052a3e-d955-4a0b-9f96-179393e7741a
name: BloodHound Attack Path Finding - Website Contributor Role on Tier Zero Resource
description: BloodHound Attack Path Finding - Website Contributor Role on Tier Zero
  Resource
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 == "AzureT0WebsiteContributor"
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 == "AzureT0WebsiteContributor"

Indicators

These rows show field, operator, and value matches.

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

Cognni Incidents for Highly Sensitive Legal Information

#

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
5h
Source
github.com/Azure/Azure-Sentinel

'Display incidents in which highly sensitive legal information was placed at risk by user sharing.'

MITRE ATT&CK Coverage

TacticTechniques
Collection

Rule Body

id: 4f45f43b-3a4b-491b-9cbe-d649603384aa
name: Cognni Incidents for Highly Sensitive Legal Information
description: |
  'Display incidents in which highly sensitive legal information was placed at risk by user sharing.'
severity: High
requiredDataConnectors:
  - connectorId: CognniSentinelDataConnector
    dataTypes:
      - CognniIncidents_CL
queryFrequency: 5h
queryPeriod: 5h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Collection
relevantTechniques:
  - T1530
query: |
  let highRisk = 3;
  let legal = 'Legal Information';
  CognniIncidents_CL 
  | where Severity == highRisk
  | where informationType_s == legal
  | where TimeGenerated >= ago(5h)
  | extend AccountCustomEntity = userId_s
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let highRisk = 3;
let legal = 'Legal Information';

Stage 1: source

CognniIncidents_CL

Stage 2: where

| where Severity == highRisk

Stage 3: where

| where informationType_s == legal

Stage 4: where

| where TimeGenerated >= ago(5h)

Stage 5: extend

| extend AccountCustomEntity = userId_s

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend

CTE - License Expiring Critically (7 Days)

#
Severity
high
Time window
2d
Source
github.com/Azure/Azure-Sentinel

A Cybereinforce license - paid or trial - is expiring within 7 days. Urgent renewal action is needed to avoid a lapse in protection. Emitted once per license term by the daily tenant health-check job (api/src/functions/tenantHealthChecks.js); re-arms automatically on renewal.

Rule Body

id: c32e5873-00b7-45fc-9a7e-43e9d0018978
name: CTE - License Expiring Critically (7 Days)
description: |
  A Cybereinforce license - paid or trial - is expiring within 7 days. Urgent renewal action is needed to avoid a lapse in protection. Emitted once per license term by the daily tenant health-check job (api/src/functions/tenantHealthChecks.js); re-arms automatically on renewal.
severity: High
requiredDataConnectors:
  - connectorId: cybereinforce_cte
    dataTypes:
      - CybereinforceCTE_CL
queryFrequency: 24h
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
tactics:
  []
relevantTechniques: []
query: |
  CybereinforceCTE_CL
  | where EventType == "license.expiring_critical"
  | extend d = parse_json(Details)
  | extend DaysRemaining = toint(d.daysRemaining), EndAt = tostring(d.endAt), Plan = tostring(d.plan), AppName = "Cybereinforce"
  | project TimeGenerated, TenantId, DaysRemaining, EndAt, Plan, AppName, Severity, Category, Source
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: AppName
      - identifier: InstanceName
        columnName: TenantId
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CybereinforceCTE_CL

Stage 2: where

| where EventType == "license.expiring_critical"

Stage 3: extend

| extend d = parse_json(Details)

Stage 4: extend

| extend DaysRemaining = toint(d.daysRemaining), EndAt = tostring(d.endAt), Plan = tostring(d.plan), AppName = "Cybereinforce"

Stage 5: project

| project TimeGenerated, TenantId, DaysRemaining, EndAt, Plan, AppName, Severity, Category, Source

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventTypeeq
  • license.expiring_critical
field:"EventType" kind:eq value:"license.expiring_critical"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AppNameproject
Categoryproject
DaysRemainingproject
EndAtproject
Planproject
Severityproject
Sourceproject
TenantIdproject
TimeGeneratedproject

CyberArkEPM - Attack attempt not blocked

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

'This rule triggers on attack attempt which was not blocked by CyberArkEPM.'

MITRE ATT&CK Coverage

TacticTechniques
Execution

Rule Body

id: 8e8978a2-9188-4187-8909-5ea00507bf16
name: CyberArkEPM - Attack attempt not blocked
description: |
  'This rule triggers on attack attempt which was not blocked by CyberArkEPM.'
severity: High
requiredDataConnectors:
  - connectorId: CyberArkEPM
    dataTypes:
      - CyberArkEPM
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1204
query: | 
  CyberArkEPM
  | where EventSubType =~ 'AttackAttempt'
  | where ThreatProtectionAction =~ 'Detect'
  | project EventEndTime, EventMessage, ActorUsername, ActingProcessFileInternalName, Evidences
  | extend AccountCustomEntity = ActorUsername
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CyberArkEPM

Stage 2: where

| where EventSubType =~ 'AttackAttempt'

Stage 3: where

| where ThreatProtectionAction =~ 'Detect'

Stage 4: project

| project EventEndTime, EventMessage, ActorUsername, ActingProcessFileInternalName, Evidences

Stage 5: extend

| extend AccountCustomEntity = ActorUsername

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActingProcessFileInternalNameproject
ActorUsernameproject
EventEndTimeproject
EventMessageproject
Evidencesproject
AccountCustomEntityextend

CyberArkEPM - Multiple attack types

#
Severity
high
Time window
10m
Group by
ActorUsername
Source
github.com/Azure/Azure-Sentinel

'This rule triggers on multiple attack attemts triggered by same user.'

MITRE ATT&CK Coverage

TacticTechniques
Execution

Rule Body

id: c02f96b4-057b-4e63-87af-6376ef7a081b
name: CyberArkEPM - Multiple attack types
description: |
  'This rule triggers on multiple attack attemts triggered by same user.'
severity: High
requiredDataConnectors:
  - connectorId: CyberArkEPM
    dataTypes:
      - CyberArkEPM
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1204
query: | 
  CyberArkEPM
  | where EventSubType =~ 'AttackAttempt'
  | summarize LatestAttackTime=max(EventEndTime), att=makeset(EventMessage) by ActorUsername
  | where array_length(att) > 1
  | extend AccountCustomEntity = ActorUsername
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CyberArkEPM

Stage 2: where

| where EventSubType =~ 'AttackAttempt'

Stage 3: summarize

| summarize LatestAttackTime=max(EventEndTime), att=makeset(EventMessage) by ActorUsername

Stage 4: where

| where array_length(att) > 1

Stage 5: extend

| extend AccountCustomEntity = ActorUsername

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActorUsernamesummarize
LatestAttackTimesummarize
attsummarize
AccountCustomEntityextend

Disks Alerts From Prancer

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

'High severity disk alerts found by Prancer.'

MITRE ATT&CK Coverage

TacticTechniques
Reconnaissance

Rule Body

id: 8c484ef9-d758-4827-9920-f4f77158f03e
name: Disks Alerts From Prancer
description: |
  'High severity disk alerts found by Prancer.'
severity: High
requiredDataConnectors:
- connectorId: PrancerLogData
  dataTypes:
    - prancer_CL
queryFrequency: 5h
queryPeriod: 5h
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: SingleAlert
status: Available
tactics:
  - Reconnaissance
relevantTechniques:
  - T1595
query: |
  union prancer_CL
  | where deviceProduct_s == 'azure'
  | where parse_json(data_data_snapshots_s)[0].type == 'Microsoft.Compute/disks'
  | where data_data_severity_s == 'High' and data_data_result_s == 'failed'
  | extend snapshot = parse_json(data_data_snapshots_s)
  | mv-expand snapshot 
  | extend
    id = tostring(snapshot.id),
    structure = tostring(snapshot.structure),
    reference = tostring(snapshot.reference),
    source = tostring(snapshot.source),
    collection = tostring(snapshot.collection),
    type = tostring(snapshot.type),
    region = tostring(snapshot.region),
    resourceTypes = tostring(snapshot.resourceTypes),
    path = tostring(snapshot.path)
customDetails:
entityMappings:
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: path
alertDetailsOverride:
  alertDisplayNameFormat: "{{data_data_message_s}}"  
  alertDescriptionFormat: "{{data_data_description_s}}"
  alertSeverityColumnName: "{{data_data_severity_s}}"
  alertDynamicProperties:
    - alertProperty: RemediationSteps
      value: data_data_remediation_description_s
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: union

union of 1 branches

Stage 2: source

prancer_CL

Stage 3: where

where deviceProduct_s == "azure"

Stage 4: where

where parse_json(data_data_snapshots_s)[0].type == "Microsoft.Compute/disks"

Stage 5: where

where data_data_result_s == "failed" and data_data_severity_s == "High"

Stage 6: extend

extend snapshot

Stage 7: mv-expand

mv-expand snapshot

Stage 8: extend

extend collection, id, path, reference, region, resourceTypes, source, structure, type

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
data_data_result_seq
  • failed
field:"data_data_result_s" kind:eq value:"failed"
data_data_severity_seq
  • High
field:"data_data_severity_s" kind:eq value:"High"
data_data_snapshots_s[0].typeeq
  • Microsoft.Compute/disks transforms: kusto_accessor_expr:{"expression":"parse_json(data_data_snapshots_s)[0].type","field":"data_data_snapshots_s[0].type"}, kusto_dynamic_path
field:"data_data_snapshots_s[0].type" kind:eq value:"Microsoft.Compute/disks"
deviceProduct_seq
  • azure
field:"deviceProduct_s" kind:eq value:"azure"

Output Fields

These fields are emitted when the rule matches.

FieldSource
snapshotextend
collectionextend
idextend
pathextend
referenceextend
regionextend
resourceTypesextend
sourceextend
structureextend
typeextend

DomainTools Newly Observed Hostnames Feeds Ingested

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

Creates an incident whenever DomainTools Newly Observed Hostnames new Feeds (domains) are ingested in the last 1 hour.

Rule Body

id: f8c223c1-1a28-4b28-8e9f-5e36b610916b
name: DomainTools Newly Observed Hostnames Feeds Ingested
description: Creates an incident whenever DomainTools Newly Observed Hostnames new Feeds (domains) are ingested in the last 1 hour.
kind: Scheduled
severity: Medium
requiredDataConnectors:
  - connectorId: DomainTools
    dataTypes:
      - DomainToolsThreatIntelDomains_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: "DomainToolsThreatIntelDomains(1h, \"noh\")"
entityMappings:
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: DomainCustomEntity
eventGroupingSettings:
  aggregationKind: SingleAlert
version: 1.0.0

Stages and Predicates

Stage 1: source

DomainToolsThreatIntelDomains(1h, "noh")

Highly Sensitive Password Accessed

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

'This rule will monitor access to highly sensitive passwords. Within the Watchlist called 'LastPass' define passwords which are deemed highly sensitive (such as password to a high privileged application). When an activity is observed against such password, an incident is created.'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access
Discovery

Rule Body

id: b39e6482-ab7e-4817-813d-ec910b64b26e
name: Highly Sensitive Password Accessed
description: |
  'This rule will monitor access to highly sensitive passwords.
  Within the Watchlist called 'LastPass' define passwords which are deemed highly sensitive (such as password to a high privileged application).
  When an activity is observed against such password, an incident is created.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: LastPass
    dataTypes:
      - LastPassNativePoller_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics:
  - CredentialAccess
  - Discovery
relevantTechniques:
 - T1555
 - T1087
query: |
  let watchlist = (_GetWatchlist("LastPass") | project name);
  LastPassNativePoller_CL
  | where Data_s in (watchlist)
  | extend AccountCustomEntity = Username_s, IPCustomEntity = IP_Address_s, URLCustomEntity = Data_s
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: URLCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Let Binding: watchlist used in Stage 2

let watchlist = (_GetWatchlist("LastPass") | project name);

Stage 1: source

LastPassNativePoller_CL

Stage 2: where

| where Data_s in (watchlist)

Stage 3: extend

| extend AccountCustomEntity = Username_s, IPCustomEntity = IP_Address_s, URLCustomEntity = Data_s

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Data_sin
  • watchlist
field:"Data_s" kind:in value:"watchlist"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend
IPCustomEntityextend
URLCustomEntityextend

Hijack Execution Flow - DLL Side-Loading

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

This detection tries to identify all DLLs loaded by "high integrity" processes and cross-checks the DLL paths against FileCreate/FileModify events of the same DLL by a medium integrity process. Of course, we need to do some magic to filter out false positives as much as possible. So any FileCreate/FileModify done by "NT Authoriy\System" and the "RID 500" users aren't interesting. Also, we only want to see the FileCreate/FileModify actions which are performed with a default or limited token elevation. If done with a full elevated token, the user is apparently admin already.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 3084b487-fad6-4000-9544-6085b9657290
name: Hijack Execution Flow - DLL Side-Loading
description: |
  This detection tries to identify all DLLs loaded by "high integrity" processes and cross-checks the DLL paths against FileCreate/FileModify events of the same DLL by a medium integrity process.
  Of course, we need to do some magic to filter out false positives as much as possible. So any FileCreate/FileModify done by "NT Authoriy\System" and the "RID 500" users aren't interesting.
  Also, we only want to see the FileCreate/FileModify actions which are performed with a default or limited token elevation. If done with a full elevated token, the user is apparently admin already.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceFileEvents
      - DeviceImageLoadEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - PrivilegeEscalation
  - DefenseEvasion
relevantTechniques:
  - T1574.002
query: |
  let imls = materialize(
      DeviceImageLoadEvents
      | where InitiatingProcessIntegrityLevel in ("High", "System") and FileName !endswith ".exe"
      | project FolderPath=tolower(FolderPath), InitiatingProcessFileName, InitiatingProcessIntegrityLevel, DeviceId, DeviceName
      | distinct FolderPath, InitiatingProcessFileName, InitiatingProcessIntegrityLevel, DeviceId, DeviceName
  );
  imls
  | join (
      DeviceFileEvents
      | where FolderPath in~ ((imls | project FolderPath)) and ActionType in ("FileCreated", "FileModified") and
      InitiatingProcessIntegrityLevel !in ("High", "System", "") and InitiatingProcessAccountSid != "S-1-5-18" and
      InitiatingProcessTokenElevation in ("TokenElevationTypeDefault", "TokenElevationTypeLimited") and InitiatingProcessAccountSid !endswith "-500"
      | extend FolderPath=tolower(FolderPath)
  ) on FolderPath, DeviceId, DeviceName
  | project-away FolderPath1
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
  - entityType: Account
    fieldMappings:
      - identifier: Sid
        columnName: InitiatingProcessAccountSid
      - identifier: Name
        columnName: InitiatingProcessAccountName
      - identifier: NTDomain
        columnName: InitiatingProcessAccountDomain
  - entityType: Process
    fieldMappings:
      - identifier: CommandLine
        columnName: InitiatingProcessCommandLine
version: 1.0.1
kind: Scheduled

Stages and Predicates

let imls is inlined into the numbered stages below.

Stage 1: source

let imls

Stage 2: source

DeviceImageLoadEvents

Stage 3: where

where not (FileName endswith ".exe") and InitiatingProcessIntegrityLevel in ("High", "System")

Stage 4: project

project DeviceId, DeviceName, FolderPath, InitiatingProcessFileName, InitiatingProcessIntegrityLevel

Stage 5: distinct

distinct DeviceId, DeviceName, FolderPath, InitiatingProcessFileName, InitiatingProcessIntegrityLevel

Stage 6: join

join (DeviceFileEvents) on FolderPath, DeviceId, DeviceName

Stage 7: project-away

project-away FolderPath1

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceIdproject
DeviceNameproject
FolderPathproject
InitiatingProcessFileNameproject
InitiatingProcessIntegrityLevelproject

Hunt for public facing devices via DeviceNetworkEvents

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

Find public facing devices via the DeviceNetworkEvents table.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

References

Telemetry Coverage

Rule Body

DeviceNetworkEvents
| where ActionType contains "InboundConnection"
| where RemoteIPType == "Public"
| distinct DeviceName

Stages and Predicates

Stage 1: source

DeviceNetworkEvents

Stage 2: where

| where ActionType contains "InboundConnection"

Stage 3: where

| where RemoteIPType == "Public"

Stage 4: distinct

| distinct DeviceName

Indicators

These rows show field, operator, and value matches.

Illumio VEN Suspend Detection Rule

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

'Create Microsoft Sentinel Incident When Ven Goes Into Suspended state'

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Rule Body

id: 7379f752-18a2-43ca-8b74-70747dd792f8
name: Illumio VEN Suspend Detection Rule
description: |
  'Create Microsoft Sentinel Incident When Ven Goes Into Suspended state'
severity: High
status: Available
requiredDataConnectors: 
  - connectorId: IllumioSaaSDataConnector
    dataTypes:
      - Illumio_Auditable_Events_CL
  - connectorId: SyslogAma
    datatypes:
      - Syslog      
queryFrequency: 60m
queryPeriod: 60m
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1562
query: |
  Illumio_Auditable_Events_CL
  | union IllumioSyslogAuditEvents  
  | where event_type has 'agent.suspend'
  | extend ipaddress = action.src_ip,
        hostname = created_by.agent.hostname
  | project-away resource_changes, action, version        
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: hostname
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ipaddress        
alertDetailsOverride:
  alertDisplayNameFormat: | 
    Illumio VEN Suspended Incident for {{hostname}}
  alertDescriptionFormat: | 
    Illumio VEN Suspended Incident for {{hostname}} generated at {{TimeGenerated}}
version: 1.0.6
kind: Scheduled

Stages and Predicates

Stage 1: source

Illumio_Auditable_Events_CL

Stage 2: union

union of 1 branches

Stage 3: source

IllumioSyslogAuditEvents

Stage 4: where

where event_type has "agent.suspend"

Stage 5: extend

extend hostname, ipaddress

Stage 6: project-away

project-away action, resource_changes, version

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
event_typematch
  • agent.suspend transforms: term
field:"event_type" kind:match value:"agent.suspend"

Output Fields

These fields are emitted when the rule matches.

FieldSource
hostnameextend
ipaddressextend

Imperva - Request to unexpected destination port

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

'Detects request attempts to unexpected destination ports.'

MITRE ATT&CK Coverage

Rule Body

id: 0ba78922-033c-468c-82de-2974d7b1797d
name: Imperva - Request to unexpected destination port
description: |
  'Detects request attempts to unexpected destination ports.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: ImpervaWAFCloudAPI
    dataTypes:
      - ImpervaWAFCloud
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  let bl_ports = dynamic(['22', '3389']);
  ImpervaWAFCloud
  | where DvcAction !startswith 'REQ_BLOCKED' or DvcAction !startswith 'REQ_BAD_'
  | where DstPortNumber in (bl_ports)
  | extend IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let bl_ports = dynamic(['22', '3389']);

Stage 1: source

ImpervaWAFCloud

Stage 2: where

| where DvcAction !startswith 'REQ_BLOCKED' or DvcAction !startswith 'REQ_BAD_'

Stage 3: where

| where DstPortNumber in (bl_ports)

Stage 4: extend

| extend IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DstPortNumberin
  • 22
  • 3389
field:"DstPortNumber" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
IPCustomEntityextend

Object Storage Settings Updated

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

Detects when object storage settings are updated in Veeam Backup & Replication. This might indicate configuration changes that require review.

Rule Body

id: 1f6897bf-a05a-47b2-a5f9-4ba6fd34f715
name: Object Storage Settings Updated
description: Detects when object storage settings are updated in Veeam Backup & Replication.
  This might indicate configuration changes that require review.
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 == 28970\n| extend Name =  extract(\"\
  Name=\\\"(?<Name>[^\\\"]*)\\\"\", 1, SyslogMessage)\n| project\n    Date = format_datetime(TimeGenerated,\
  \ 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n    EventId = instanceId,\n\
  \    [\"User Name\"] = user,\n    [\"Data SourceName\"] = 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 == 28970

Stage 3: extend

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

Stage 4: project

| project
    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
    DataSource = original_host,
    EventId = instanceId,
    ["User Name"] = user,
    ["Data SourceName"] = 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
Data SourceNameproject
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
User Nameproject

Remote File Creation with PsExec

#
Status
available
Severity
high
Time window
10m
Group by
DeviceId, DeviceName, InitiatingProcessFileName, TimeWindow
Source
github.com/Azure/Azure-Sentinel

This query was originally published in the threat analytics report, Ryuk ransomware. There is also a related blog. Ryuk is human-operated ransomware. Much like DoppelPaymer ransomware, Ryuk is spread manually, often on networks that are already infected with Trickbot. Ryuk operators use PsExec to manually spread the ransomware to other devices. The following query detects remote file creation events that might indicate an active attack. The See also section below lists links to other queries associated with Ryuk ransomware. References: https://www.microsoft.com/security/blog/2020/03/05/human-operated-ransomware-attacks-a-preventable-disaster/ https://www.microsoft.com/en-us/wdsi/threats/malware-encyclopedia-description?Name=Ransom:Win32/Ryuk.AA https://www.microsoft.com/security/blog/2020/03/05/human-operated-ransomware-attacks-a-preventable-disaster/ https://docs.microsoft.com/sysinternals/downloads/psexec

MITRE ATT&CK Coverage

TacticTechniques
Lateral Movement

Telemetry Coverage

Rule Body

id: 35ab0d58-baab-4154-87ed-fa2f69797e9e
name: Remote File Creation with PsExec
description: |
  This query was originally published in the threat analytics report, Ryuk ransomware. There is also a related blog.
  Ryuk is human-operated ransomware. Much like DoppelPaymer ransomware, Ryuk is spread manually, often on networks that are already infected with Trickbot.
  Ryuk operators use PsExec to manually spread the ransomware to other devices.
  The following query detects remote file creation events that might indicate an active attack.
  The See also section below lists links to other queries associated with Ryuk ransomware.
  References:
  https://www.microsoft.com/security/blog/2020/03/05/human-operated-ransomware-attacks-a-preventable-disaster/
  https://www.microsoft.com/en-us/wdsi/threats/malware-encyclopedia-description?Name=Ransom:Win32/Ryuk.AA
  https://www.microsoft.com/security/blog/2020/03/05/human-operated-ransomware-attacks-a-preventable-disaster/
  https://docs.microsoft.com/sysinternals/downloads/psexec
severity: High
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceFileEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - LateralMovement
relevantTechniques:
  - T1570
tags:
  - Ryuk
  - Ransomware
  - PsExec
query: |
  // Find PsExec creating multiple files on remote machines in a 10-minute window
  DeviceFileEvents
  // Looking for PsExec by accepteula command flag
  | where InitiatingProcessCommandLine has "accepteula"
  // Remote machines and file is exe
  | where FolderPath has "\\\\" and FileName endswith ".exe"
  | extend Exe = countof(InitiatingProcessCommandLine, ".exe")
  // Checking to see if command line has 2 .exe or .bat
  | where InitiatingProcessCommandLine !has ".ps1" and Exe > 1 or
  InitiatingProcessCommandLine has ".bat"
  // Exclusions: Remove the following line to widen scope of AHQ
  | where not(InitiatingProcessCommandLine has_any("batch", "auditpol",
  "script", "scripts", "illusive", "rebootrequired"))
  | summarize FileCount = dcount(FolderPath), make_set(SHA1, 100000), make_set(FolderPath, 100000),
  make_set(FileName, 100000), make_set(InitiatingProcessCommandLine, 100000) by DeviceId, DeviceName,
  TimeWindow=bin(TimeGenerated, 10m), InitiatingProcessFileName
  | where FileCount > 4
  | extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
  | extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

DeviceFileEvents

Stage 2: where

| where InitiatingProcessCommandLine has "accepteula"

Stage 3: where

| where FolderPath has "\\\\" and FileName endswith ".exe"

Stage 4: extend

| extend Exe = countof(InitiatingProcessCommandLine, ".exe")

Stage 5: where

| where InitiatingProcessCommandLine !has ".ps1" and Exe > 1 or
InitiatingProcessCommandLine has ".bat"

Stage 6: where

| where not(InitiatingProcessCommandLine has_any("batch", "auditpol",
"script", "scripts", "illusive", "rebootrequired"))

Stage 7: summarize

| summarize FileCount = dcount(FolderPath), make_set(SHA1, 100000), make_set(FolderPath, 100000),
make_set(FileName, 100000), make_set(InitiatingProcessCommandLine, 100000) by DeviceId, DeviceName,
TimeWindow=bin(TimeGenerated, 10m), InitiatingProcessFileName

Stage 8: where

| where FileCount > 4

Stage 9: extend

| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
HostName =
ifDeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))
elseDeviceName

Stage 10: extend

| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
DnsDomain =
ifDeviceName has "."substring(DeviceName, (indexof(DeviceName, '.') + 1))
else""

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Exegt
  • 1 transforms: number
field:"Exe" kind:gt value:"1"
FileCountgt
  • 4 transforms: number
field:"FileCount" kind:gt value:"4"
FileNameends_with
  • .exe corpus 2 (kusto 2)
field:"file_name" kind:ends_with value:".exe"
FolderPathmatch
  • \\ transforms: term
field:"TargetFilename" kind:match value:"\\"
InitiatingProcessCommandLinematch
  • .bat transforms: term
  • accepteula transforms: term corpus 3 (kusto 3)
field:"CommandLine" kind:match

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
InitiatingProcessCommandLinematchbatch, auditpol, script, scripts, illusive, rebootrequiredexcludes:InitiatingProcessCommandLine

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceIdsummarize
DeviceNamesummarize
FileCountsummarize
InitiatingProcessFileNamesummarize
TimeWindowsummarize
HostNameextend
DnsDomainextend

Response rows stateful anomaly on database

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

'Goal: To detect anomalous data exfiltration. This query detects SQL queries that accessed a large number of rows, which is significantly higher than normal for this database. The calculation is made inside recent time window (defined by 'detectionWindow' parameter), and the anomaly is calculated based on previous training window (defined by 'trainingWindow' parameter). The user can set the minimal threshold for anomaly by changing the threshold parameters volThresholdZ and volThresholdQ (higher thresholds will detect only more severe anomalies).'

MITRE ATT&CK Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 9851c360-5fd5-4bae-a117-b66d8476bf5e
name: Response rows stateful anomaly on database
description: |
  'Goal: To detect anomalous data exfiltration. This query detects SQL queries that accessed a large number of rows, which is significantly higher than normal for this database.
   The calculation is made inside recent time window (defined by 'detectionWindow' parameter), and the anomaly is calculated based on previous training window (defined by 'trainingWindow' parameter). The user can set the minimal threshold for anomaly by changing the threshold parameters volThresholdZ and volThresholdQ (higher thresholds will detect only more severe anomalies).'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureSql
    dataTypes:
      - AzureDiagnostics
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Collection
relevantTechniques:
  - T1213.006 
tags:
  - SQL
query: |
    let volumeThresholdZ = 3.0;                     // Minimal threshold for the Zscore to trigger anomaly (number of standard deviations above mean). If set higher, only very significant alerts will fire.
    let volumeThresholdQ = volumeThresholdZ;        // Minimal threshold for the Qscore to trigger anomaly (number of Inter-Percentile Ranges above high percentile). If set higher, only very significant alerts will fire.
    let volumeThresholdHardcoded = 500;             // Minimal value for the volume metric to trigger anomaly.
    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 monitoredColumn = 'ResponseRows';           // The name of the column for volumetric anomalies.
    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
        | extend QuantityColumn = column_ifexists(monitoredColumn, 0)
        | extend WindowType = case( TimeGenerated >= ago(detectionWindow), 'detection',
                                               (ago(trainingWindow) <= TimeGenerated and TimeGenerated < ago(detectionWindow)), 'training', 'other')
        | where WindowType in ('detection', 'training'));
    let trainingSet =
        processedData
        | where WindowType == 'training'
        | summarize AvgVal = round(avg(QuantityColumn), 2), StdVal = round(stdev(QuantityColumn), 2), N = count(),
                    P99Val = round(percentile(QuantityColumn, 99), 2), P50Val = round(percentile(QuantityColumn, 50), 2)
          by Database;
    processedData
    | where WindowType == 'detection'
    | join kind = inner (trainingSet) on Database
    | extend ZScoreVal = iff(N >= 20, round(todouble(QuantityColumn - AvgVal) / todouble(StdVal + 1), 2), 0.00),
             QScoreVal = iff(N >= 20, round(todouble(QuantityColumn - P99Val) / todouble(P99Val - P50Val + 1), 2), 0.00)
    | extend IsVolumeAnomalyOnVal = iff((ZScoreVal > volumeThresholdZ and QScoreVal > volumeThresholdQ and QuantityColumn > volumeThresholdHardcoded), true, false), AnomalyScore = round((ZScoreVal + QScoreVal)/2, 0)
    | where IsVolumeAnomalyOnVal == 'true'
    | project TimeGenerated, Database, PrincipalName, ClientIp, HostName, ApplicationName, ActionName, Statement,
              IsSuccess, ResponseRows, AffectedRows, IsVolumeAnomalyOnVal, AnomalyScore, ResourceId
    | extend Name = tostring(split(PrincipalName,'@',0)[0]), UPNSuffix = tostring(split(PrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: ApplicationName
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: ResourceId
alertDetailsOverride:
  alertDisplayNameFormat: 'Response rows anomaly on database {{Database}}'
  alertDescriptionFormat: |
    'An anomaly was detected on database {{Database}} with {{ResponseRows}} response rows in a statement, which is significantly higher than normal for this database. Investigate the database activity for potential malicious data changes or deletions.'
version: 1.1.3
kind: Scheduled

Stages and Predicates

Parameters

let volumeThresholdZ = 3.0;
let volumeThresholdQ = volumeThresholdZ;
let volumeThresholdHardcoded = 500;
let detectionWindow = 1h;
let trainingWindow = detectionWindow + 14d;
let monitoredColumn = 'ResponseRows';

let processedData is inlined into the numbered stages below.

Let Binding: trainingSet used in Stage 9

let trainingSet = processedData
    | where WindowType == 'training'
    | summarize AvgVal = round(avg(QuantityColumn), 2), StdVal = round(stdev(QuantityColumn), 2), N = count(),
                P99Val = round(percentile(QuantityColumn, 99), 2), P50Val = round(percentile(QuantityColumn, 50), 2)
      by Database;

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

Stage 5: extend

| extend QuantityColumn = column_ifexists(monitoredColumn, 0)

Stage 6: extend

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

Stage 7: where

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

Stage 8: where

processedData
| where WindowType == 'detection'

Stage 9: join

| join kind = inner (trainingSet) on Database

Stage 10: extend

extend QScoreVal, ZScoreVal
QScoreVal =
ifN >= 20round((todouble((QuantityColumn - P99Val)) / todouble(((P99Val - P50Val) + 1))), 2)
else0.0
ZScoreVal =
ifN >= 20round((todouble((QuantityColumn - AvgVal)) / todouble((StdVal + 1))), 2)
else0.0

Stage 11: extend

extend AnomalyScore, IsVolumeAnomalyOnVal
IsVolumeAnomalyOnVal =
if(ZScoreVal > volumeThresholdZ and QScoreVal > volumeThresholdQ) and QuantityColumn > volumeThresholdHardcodedtrue
elsefalse

Stage 12: where

where IsVolumeAnomalyOnVal == true

Stage 13: project

project ActionName, AffectedRows, AnomalyScore, ApplicationName, ClientIp, Database, HostName, IsSuccess, IsVolumeAnomalyOnVal, PrincipalName, ResourceId, ResponseRows, Statement, TimeGenerated

Stage 14: extend

extend Name, UPNSuffix

Stage 15: summarize aggregation inside the join branch

summarize by Database

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Databasesummarize

Tailscale: Auth key created

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

Identifies when a new Tailscale auth key is generated. Auth keys allow unattended device enrollment into the tailnet - confirm it was expected and revoke if not.

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Rule Body

id: 6b052c8d-5de8-eab0-1956-69a297765a32
name: "Tailscale: Auth key created"
description: |
  Identifies when a new Tailscale auth key is generated. Auth keys allow unattended device enrollment into the tailnet - confirm it was expected and revoke if not.
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: TailscaleCCF
    dataTypes:
      - Tailscale_Audit_CL
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1098
query: |
  Tailscale_Audit_CL
        | where Action == "CREATE"
        | where tostring(Target.type) == "AUTH_KEY"
        | extend ActorLogin = tostring(Actor.loginName)
        | extend KeyDescription = tostring(New.description)
        | extend Reusable = tostring(New.reusable)
        | extend Ephemeral = tostring(New.ephemeral)
        | project TimeGenerated, ActorLogin, KeyDescription, Reusable, Ephemeral, Origin, New
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ActorLogin
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

Stage 1: source

Tailscale_Audit_CL

Stage 2: where

| where Action == "CREATE"

Stage 3: where

| where tostring(Target.type) == "AUTH_KEY"

Stage 4: extend (4 consecutive steps)

| extend ActorLogin = tostring(Actor.loginName)
| extend KeyDescription = tostring(New.description)
| extend Reusable = tostring(New.reusable)
| extend Ephemeral = tostring(New.ephemeral)

Stage 5: project

| project TimeGenerated, ActorLogin, KeyDescription, Reusable, Ephemeral, Origin, New

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actioneq
  • CREATE
field:"Action" kind:eq value:"CREATE"
Target.typeeq
  • AUTH_KEY transforms: tostring, kusto_dynamic_path
field:"Target.type" kind:eq value:"AUTH_KEY"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActorLoginproject
Ephemeralproject
KeyDescriptionproject
Newproject
Originproject
Reusableproject
TimeGeneratedproject

TI Map IP Entity to Azure SQL Security Audit Events

#
Severity
medium
Time window
14d
Group by
$left.TI_ipEntity, $right.ClientIP, ClientIP, IndicatorId
Source
github.com/Azure/Azure-Sentinel

This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in SQL Security Audit Events.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: d0aa8969-1bbe-4da3-9e76-09e5f67c9d85
name: TI Map IP Entity to Azure SQL Security Audit Events
description: |
  This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in SQL Security Audit Events.
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: AzureSql
    dataTypes:
      - AzureDiagnostics
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h; // Look back 1 hour for AzureDiagnostics logs
  let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
  // Fetch threat intelligence indicators related to IP addresses
  let IP_Indicators = ThreatIntelligenceIndicator
    | where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP)
    | where TimeGenerated >= ago(ioc_lookBack)
    | extend TI_ipEntity = iff(isnotempty(NetworkIP), NetworkIP, NetworkDestinationIP)
    | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
    | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(EmailSourceIpAddress), EmailSourceIpAddress, TI_ipEntity)
    | where ipv4_is_private(TI_ipEntity) == false and  TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
    | where Active == true and ExpirationDateTime > now();
  // Perform a join between IP indicators and AzureDiagnostics logs for SQL Security Audit events
  IP_Indicators
    // Use innerunique to keep performance fast and result set low, as we only need one match to indicate potential malicious activity that needs investigation
    | join kind=innerunique (
        AzureDiagnostics
        | where TimeGenerated >= ago(dt_lookBack)
        | where ResourceProvider == 'MICROSOFT.SQL'
        | where Category == 'SQLSecurityAuditEvents'
        | extend SQLSecurityAuditEvents_TimeGenerated = TimeGenerated
        | extend ClientIP = column_ifexists("client_ip_s", "Not Available")
        | extend Action = column_ifexists("action_name_s", "Not Available")
        | extend Application = column_ifexists("application_name_s", "Not Available")
        | extend HostName = column_ifexists("host_name_s", "Not Available")
    )
    on $left.TI_ipEntity == $right.ClientIP
    // Filter out logs that occurred after the expiration of the corresponding indicator
    | where SQLSecurityAuditEvents_TimeGenerated < ExpirationDateTime
    // Group the results by IndicatorId and ClientIP, and keep the log entry with the latest timestamp
    | summarize SQLSecurityAuditEvents_TimeGenerated = arg_max(SQLSecurityAuditEvents_TimeGenerated, *) by IndicatorId, ClientIP
    // Select the desired output fields
    | project SQLSecurityAuditEvents_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore,
      TI_ipEntity, ResourceId, ClientIP, Action, Application, HostName, NetworkIP, NetworkDestinationIP, NetworkSourceIP, EmailSourceIpAddress, Type
    // Rename the timestamp field
    | extend timestamp = SQLSecurityAuditEvents_TimeGenerated
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIP
version: 1.3.2
kind: Scheduled

Stages and Predicates

Parameters

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

let IP_Indicators is inlined into the numbered stages below.

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

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

| where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP)

Stage 3: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 4: extend (3 consecutive steps)

| extend TI_ipEntity = iff(isnotempty(NetworkIP), NetworkIP, NetworkDestinationIP)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(EmailSourceIpAddress), EmailSourceIpAddress, TI_ipEntity)
TI_ipEntity =
ifisnotempty(NetworkIP)NetworkIP
elseNetworkDestinationIP

Stage 5: where

| where ipv4_is_private(TI_ipEntity) == false and  TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."

Stage 6: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId

Stage 7: where

| where Active == true and ExpirationDateTime > now()

Stage 8: join

IP_Indicators
| join kind=innerunique (
      AzureDiagnostics
      | where TimeGenerated >= ago(dt_lookBack)
      | where ResourceProvider == 'MICROSOFT.SQL'
      | where Category == 'SQLSecurityAuditEvents'
      | extend SQLSecurityAuditEvents_TimeGenerated = TimeGenerated
      | extend ClientIP = column_ifexists("client_ip_s", "Not Available")
      | extend Action = column_ifexists("action_name_s", "Not Available")
      | extend Application = column_ifexists("application_name_s", "Not Available")
      | extend HostName = column_ifexists("host_name_s", "Not Available")
  )
  on $left.TI_ipEntity == $right.ClientIP

Stage 9: where

where SQLSecurityAuditEvents_TimeGenerated < ExpirationDateTime

Stage 10: summarize

summarize SQLSecurityAuditEvents_TimeGenerated by IndicatorId, ClientIP

Stage 11: project

project Action, ActivityGroupNames, Application, ClientIP, ConfidenceScore, Description, EmailSourceIpAddress, ExpirationDateTime, HostName, IndicatorId, NetworkDestinationIP, NetworkIP, NetworkSourceIP, ResourceId, SQLSecurityAuditEvents_TimeGenerated, TI_ipEntity, ThreatType, Type, Url

Stage 12: extend

extend timestamp

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
TI_ipEntitycidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:TI_ipEntity
TI_ipEntitystarts_with127.excludes:TI_ipEntity field:"TI_ipEntity" value:"127."
TI_ipEntitystarts_with::excludes:TI_ipEntity field:"TI_ipEntity" value:"::"
TI_ipEntitystarts_withfe80excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionproject
ActivityGroupNamesproject
Applicationproject
ClientIPproject
ConfidenceScoreproject
Descriptionproject
EmailSourceIpAddressproject
ExpirationDateTimeproject
HostNameproject
IndicatorIdproject
NetworkDestinationIPproject
NetworkIPproject
NetworkSourceIPproject
ResourceIdproject
SQLSecurityAuditEvents_TimeGeneratedproject
TI_ipEntityproject
ThreatTypeproject
Typeproject
Urlproject
timestampextend

TIE Password issues

#
Severity
low
Time window
2h
Group by
Severity
Source
github.com/Azure/Azure-Sentinel

'Searches for triggered Indicators of Exposures related to password issues.'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: 87af910a-e9c0-4c96-8045-f778ba405251
name: TIE Password issues
description: |
  'Searches for triggered Indicators of Exposures related to password issues.'
severity: Low
requiredDataConnectors:
  - connectorId: TenableIE
    dataTypes:
      - Tenable_IE_CL
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  // For the query to work properly, make sure you have imported the afad_parser.yaml parser into the workspace
  // Retrieve the parser here: https://aka.ms/sentinel-TenableApp-afad-parser
  // Then, create the Kusto Function with alias afad_parser
  let SeverityTable=datatable(Severity:string,Level:int) [
  "low", 1,
  "medium", 2,
  "high", 3,
  "critical", 4
  ];
  let codeNameList = datatable(Codename:string)["C-CLEARTEXT-PASSWORD", "C-PASSWORD-DONT-EXPIRE", "C-USER-REVER-PWDS", "C-PASSWORD-POLICY", "C-USER-PASSWORD", "C-KRBTGT-PASSWORD", "C-AAD-SSO-PASSWORD", "C-REVER-PWD-GPO"];
  afad_parser
  | where MessageType == 0 and Codename in~ (codeNameList)
  | lookup kind=leftouter SeverityTable on Severity
  | order by Level
  | extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 1.0.1
kind: Scheduled

Stages and Predicates

Let Binding: SeverityTable used in Stage 3

let SeverityTable = datatable(Severity:string,Level:int) [
"low", 1,
"medium", 2,
"high", 3,
"critical", 4
];

Let Binding: codeNameList used in Stage 2

let codeNameList = datatable(Codename:string)["C-CLEARTEXT-PASSWORD", "C-PASSWORD-DONT-EXPIRE", "C-USER-REVER-PWDS", "C-PASSWORD-POLICY", "C-USER-PASSWORD", "C-KRBTGT-PASSWORD", "C-AAD-SSO-PASSWORD", "C-REVER-PWD-GPO"];

Stage 1: source

afad_parser

Stage 2: where

| where MessageType == 0 and Codename in~ (codeNameList)

Stage 3: kusto:lookup

lookup kind=leftouter (SeverityTable) on Severity

Stage 4: sort

sort by Level

Stage 5: extend

extend DnsDomain, HostName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DnsDomainextend
HostNameextend

Tomcat - Put file and get file from same IP address

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

'Detects put or get files from one source in short timeframe'

MITRE ATT&CK Coverage

Rule Body

id: 103d5ada-4874-11ec-81d3-0242ac130003
name: Tomcat - Put file and get file from same IP address
description: |
  'Detects put or get files from one source in short timeframe' 
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    datatypes:
      - Tomcat_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  let p = TomcatEvent
  | where HttpRequestMethod in~ ('POST', 'PUT')
  | sort by EventStartTime asc
  | summarize post_time=min(EventStartTime) by SrcIpAddr, tostring(UrlOriginal);
  TomcatEvent
  | where HttpRequestMethod =~ 'GET'
  | sort by EventStartTime asc
  | summarize get_time=min(EventStartTime) by SrcIpAddr, tostring(UrlOriginal)
  | join kind=innerunique (p) on UrlOriginal, SrcIpAddr
  | extend second = datetime_diff('second',get_time,post_time)
  | where second between (1 .. 300)
  | project second, post_time, get_time, SrcIpAddr, UrlOriginal
  | extend IPCustomEntity = SrcIpAddr, UrlCustomEntity = tostring(UrlOriginal)
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: UrlCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Let Binding: p used in Stage 5

let p = TomcatEvent
| where HttpRequestMethod in~ ('POST', 'PUT')
| sort by EventStartTime asc
| summarize post_time=min(EventStartTime) by SrcIpAddr, tostring(UrlOriginal);

Stage 1: source

TomcatEvent

Stage 2: where

| where HttpRequestMethod =~ 'GET'

Stage 3: sort

| sort by EventStartTime asc

Stage 4: summarize

| summarize get_time=min(EventStartTime) by SrcIpAddr, tostring(UrlOriginal)

Stage 5: join

| join kind=innerunique (p) on UrlOriginal, SrcIpAddr

Stage 6: extend

extend second

Stage 7: where

where second >= 1 and second <= 300

Stage 8: project

project SrcIpAddr, UrlOriginal, get_time, post_time, second

Stage 9: extend

extend IPCustomEntity, UrlCustomEntity

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
SrcIpAddrproject
UrlOriginalproject
get_timeproject
post_timeproject
secondproject
IPCustomEntityextend
UrlCustomEntityextend

Back to all Kusto rules