Detection rules › Kusto
Kusto rules: 2d
Azure Diagnostic settings removed from a resource
#This query looks for diagnostic settings that are removed from a resource. This could indicate an attacker or malicious internal trying to evade detection before malicious act is performed. If the diagnostic settings are being deleted as part of a parent resource deletion, the event is ignores.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Activity Log event Microsoft.Insights/DiagnosticSettings/Delete: Delete a resource diagnostic setting |
Rule Body
id: 6e95aef3-a1e0-4063-8e74-cd59aa59f245
name: Azure Diagnostic settings removed from a resource
description: |
'This query looks for diagnostic settings that are removed from a resource.
This could indicate an attacker or malicious internal trying to evade detection before malicious act is performed.
If the diagnostic settings are being deleted as part of a parent resource deletion, the event is ignores.'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActivity
dataTypes:
- AzureActivity
queryFrequency: 1h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1562.008
query: |
AzureActivity
| where OperationNameValue =~ "MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE"
| summarize
TimeGenerated = arg_max(TimeGenerated, Properties),
ActivityStatusValue = make_set(ActivityStatusValue, 5),
take_any(Caller, CallerIpAddress, OperationName, ResourceGroup, Resource)
by CorrelationId, _ResourceId, OperationNameValue
| extend ResourceHierarchy = split(_ResourceId, "/")
| extend MonitoredResourcePath = strcat_array(array_slice(ResourceHierarchy, 0, array_length(ResourceHierarchy)-5), "/")
| join kind=leftanti (
AzureActivity
| where OperationNameValue !~ "MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE" and OperationNameValue endswith "/DELETE" and ActivityStatusValue has_any ("Success", "Succeeded")
| project _ResourceId
) on $left.MonitoredResourcePath == $right._ResourceId
| extend
Name = iif(Caller has "@", tostring(split(Caller, "@")[0]), ""),
UPNSuffix = iif(Caller has "@", tostring(split(Caller, "@")[1]), ""),
AadUserId = iif(Caller has "@", "", Caller)
| project TimeGenerated, Caller, CallerIpAddress, OperationNameValue, OperationName, ActivityStatusValue, ResourceGroup, MonitoredResourcePath, Resource, Properties, Name, UPNSuffix, AadUserId, _ResourceId, CorrelationId
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Caller
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: AadUserId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: CallerIpAddress
version: 1.0.3
kind: Scheduled
metadata:
source:
kind: Community
author:
name: KennethMLdk
support:
tier: Community
categories:
domains: [ "Security - Others", "Platform" ]
Stages and Predicates
Stage 1: source
AzureActivity
Stage 2: where
| where OperationNameValue =~ "MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE"
Stage 3: summarize
| summarize
TimeGenerated = arg_max(TimeGenerated, Properties),
ActivityStatusValue = make_set(ActivityStatusValue, 5),
take_any(Caller, CallerIpAddress, OperationName, ResourceGroup, Resource)
by CorrelationId, _ResourceId, OperationNameValue
Stage 4: extend
| extend ResourceHierarchy = split(_ResourceId, "/")
Stage 5: extend
| extend MonitoredResourcePath = strcat_array(array_slice(ResourceHierarchy, 0, array_length(ResourceHierarchy)-5), "/")
Stage 6: join (negated)
join kind=leftanti (AzureActivity) on $left.MonitoredResourcePath, $right._ResourceId
Stage 7: extend
extend AadUserId, Name, UPNSuffix
Stage 8: project
project AadUserId, ActivityStatusValue, Caller, CallerIpAddress, CorrelationId, MonitoredResourcePath, Name, OperationName, OperationNameValue, Properties, Resource, ResourceGroup, TimeGenerated, UPNSuffix, _ResourceId
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationNameValue | eq |
| field:"azure_ad::operation_name_value" kind:eq value:"MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
ActivityStatusValue | match | Success, Succeeded | excludes:ActivityStatusValue field:"ActivityStatusValue" value:"Success" field:"ActivityStatusValue" value:"Succeeded" |
OperationNameValue | ends_with | /DELETE | excludes:OperationNameValue field:"OperationNameValue" value:"/DELETE" |
OperationNameValue | ne | MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE | excludes:OperationNameValue field:"OperationNameValue" value:"MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AadUserId | project |
ActivityStatusValue | project |
Caller | project |
CallerIpAddress | project |
CorrelationId | project |
MonitoredResourcePath | project |
Name | project |
OperationName | project |
OperationNameValue | project |
Properties | project |
Resource | project |
ResourceGroup | project |
TimeGenerated | project |
UPNSuffix | project |
_ResourceId | project |
Best Practice Compliance Check Not Passed
#Detects when a security best practice does not pass a compliance check in Veeam Security & Compliance Analyzer.
Rule Body
id: f920ac64-dfd0-4dea-9b7c-acecf1ea2b28
name: Best Practice Compliance Check Not Passed
description: Detects when a security best practice does not pass a compliance check
in Veeam Security & Compliance Analyzer.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: VeeamCustomTablesDataConnector
dataTypes:
- VeeamSecurityComplianceAnalyzer_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "VeeamSecurityComplianceAnalyzer_CL\r\n| where Status != \"OK\""
version: 1.0.1
kind: Scheduled
customDetails:
VbrHostName: VbrHostName
Status: Status
BestPracticeName: BestPractice
TenantId: TenantId
Id: Id
Note: Note
Stages and Predicates
Stage 1: source
VeeamSecurityComplianceAnalyzer_CL
Stage 2: where
| where Status != "OK"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Status | ne |
| field:"Status" kind:ne value:"OK" |
BloodHound Attack Path Finding - Large Default Groups With ForceChangePassword Privileges
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
BloodHound Attack Path Finding - Large Default Groups With ForceChangePassword Privileges
Rule Body
id: b4f835f1-3ce1-4009-9dcd-816ba4107e9b
name: BloodHound Attack Path Finding - Large Default Groups With ForceChangePassword
Privileges
description: BloodHound Attack Path Finding - Large Default Groups With ForceChangePassword
Privileges
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 == "LargeDefaultGroupsForceChangePassword"
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 == "LargeDefaultGroupsForceChangePassword"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"LargeDefaultGroupsForceChangePassword" |
Cisco SEG - Unexpected link
#'Detects mails with suspicious links.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 9cb4a02d-3708-42ba-b33b-0fdd360ce4b6
name: Cisco SEG - Unexpected link
description: |
'Detects mails with suspicious links.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1566
query: |
let u_cat = dynamic(['Unclassified']);
CiscoSEGEvent
| where NetworkDirection =~ 'Incoming'
| where tostring(AdditionalFields) has 'ESAURLDetails'
| extend link_cat = extract(@"'Category': '(.*?)'", 1, tostring(AdditionalFields))
| where link_cat in~ (u_cat)
| extend AccountCustomEntity = DstUserName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.3
kind: Scheduled
Stages and Predicates
Parameters
let u_cat = dynamic(['Unclassified']);
Stage 1: source
CiscoSEGEvent
Stage 2: where
| where NetworkDirection =~ 'Incoming'
Stage 3: where
| where tostring(AdditionalFields) has 'ESAURLDetails'
Stage 4: extend
| extend link_cat = extract(@"'Category': '(.*?)'", 1, tostring(AdditionalFields))
Stage 5: where
| where link_cat in~ (u_cat)
Stage 6: extend
| extend AccountCustomEntity = DstUserName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AdditionalFields | match |
| field:"AdditionalFields" kind:match value:"ESAURLDetails" |
NetworkDirection | eq |
| field:"NetworkDirection" kind:eq value:"Incoming" |
link_cat | in |
| field:"link_cat" kind:in value:"u_cat" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
link_cat | extend |
AccountCustomEntity | extend |
Cloudflare - Multiple user agents for single source
#'Detects requests with different user agents from one source in short timeframe.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: fc50076a-0275-43d5-b9dd-38346c061f67
name: Cloudflare - Multiple user agents for single source
description: |
'Detects requests with different user agents from one source in short timeframe.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CloudflareDataConnector
dataTypes:
- Cloudflare
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
let threshold = 10;
Cloudflare
| where isnotempty(HttpUserAgentOriginal)
| summarize d_ua = dcount(HttpUserAgentOriginal) by SrcIpAddr, bin(TimeGenerated, 3m)
| where d_ua > threshold
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 10;
Stage 1: source
Cloudflare
Stage 2: where
| where isnotempty(HttpUserAgentOriginal)
Stage 3: summarize
| summarize d_ua = dcount(HttpUserAgentOriginal) by SrcIpAddr, bin(TimeGenerated, 3m)
Stage 4: where
| where d_ua > threshold
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
HttpUserAgentOriginal | is_not_null | field:"HttpUserAgentOriginal" kind:is_not_null | |
d_ua | gt |
| field:"d_ua" kind:gt value:"10" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SrcIpAddr | summarize |
d_ua | summarize |
Cortex XDR Incident - Low
#'A new incident was created in the Cortex XDR portal with a severity "Low". Click on the events for incident details. '
Rule Body
id: 0d3fbeba-f9c9-40c7-9b71-8afb1816d7b2
name: Cortex XDR Incident - Low
description: |
'A new incident was created in the Cortex XDR portal with a severity "Low". Click on the events for incident details. '
severity: Low
requiredDataConnectors:
- connectorId: CortexXDRDataConnector
dataTypes:
- PaloAltoCortexXDR_Incidents_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 5h
suppressionEnabled: false
tactics: []
relevantTechniques: []
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: SingleAlert
alertDetailsOverride: null
customDetails: null
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Users_s
- entityType: URL
fieldMappings:
- identifier: Url
columnName: XdrUrl
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Hosts_s
sentinelEntitiesMappings: null
query: |
PaloAltoCortexXDR_Incidents
| where Severity contains "low"
| where Status !contains "resolved_auto"
// the below line filters for incidents that has at least one user or one host in it. Comment the below line if you want to generated incidents that have 0 user or 0 host as well
| where HostCount > 0 or UserCount > 0
| extend Hosts_s = tostring(Hosts), Users_s = tostring(Users)
| project TimeGenerated, IncidentId, Status, Severity, IncidentSources, Description, XdrUrl, Hosts_s, Users_s, AlertCategories, HostCount, UserCount, AlertCount, CreationTime, ModificationTime
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
PaloAltoCortexXDR_Incidents
Stage 2: where
| where Severity contains "low"
Stage 3: where
| where Status !contains "resolved_auto"
Stage 4: where
| where HostCount > 0 or UserCount > 0
Stage 5: extend
| extend Hosts_s = tostring(Hosts), Users_s = tostring(Users)
Stage 6: project
| project TimeGenerated, IncidentId, Status, Severity, IncidentSources, Description, XdrUrl, Hosts_s, Users_s, AlertCategories, HostCount, UserCount, AlertCount, CreationTime, ModificationTime
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
HostCount | gt |
| field:"HostCount" kind:gt value:"0" |
Severity | contains |
| field:"Severity" kind:contains value:"low" |
UserCount | gt |
| field:"UserCount" kind:gt value:"0" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Status | contains | resolved_auto | excludes:Status field:"Status" value:"resolved_auto" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertCategories | project |
AlertCount | project |
CreationTime | project |
Description | project |
HostCount | project |
Hosts_s | project |
IncidentId | project |
IncidentSources | project |
ModificationTime | project |
Severity | project |
Status | project |
TimeGenerated | project |
UserCount | project |
Users_s | project |
XdrUrl | project |
Detecting Macro Invoking ShellBrowserWindow COM Objects
#This query detects a macro invoking ShellBrowserWindow COM Objects evade naive parent/child Office detection rules.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Lateral Movement |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Sysmon | Event ID 1: Process creation |
Rule Body
id: e7470b35-0128-4508-bfc9-e01cfb3c2eb7
name: Detecting Macro Invoking ShellBrowserWindow COM Objects
description: |
'This query detects a macro invoking ShellBrowserWindow COM Objects evade naive parent/child Office detection rules.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- LateralMovement
relevantTechniques:
- T1021.003
query: |
Event
| where EventLog =~ "Microsoft-Windows-Sysmon/Operational" and EventID==1
| parse EventData with * 'Image">' Image "<" * 'CommandLine">' CommandLine "<" * 'ParentImage">' ParentImage "<" *
| where ParentImage has "svchost.exe" and Image has "rundll32.exe" and CommandLine has "{c08afd90-f2a1-11d1-8455-00a0c91f3880}"
| parse EventData with * 'ProcessGuid">' ProcessGuid "<" * 'Description">' Description "<" * 'CurrentDirectory">' CurrentDirectory "<" * 'User">' User "<" * 'LogonGuid">' LogonGuid "<" * 'ParentProcessGuid">' ParentProcessGuid "<" * 'ParentImage">' ParentImage "<" * 'ParentCommandLine">' ParentCommandLine "<" * 'ParentUser">' ParentUser "<" *
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, User, ParentImage, ParentProcessGuid, ParentCommandLine, ParentUser, Image, ProcessGuid, CommandLine, Description
| extend HostName = iif(Computer has '.',substring(Computer,0,indexof(Computer,'.')),Computer) , DnsDomain = iif(Computer has '.',substring(Computer,indexof(Computer,'.')+1),'')
entityMappings:
- entityType: Process
fieldMappings:
- identifier: CommandLine
columnName: CommandLine
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
- entityType: Account
fieldMappings:
- identifier: Name
columnName: User
version: 1.0.4
kind: Scheduled
Stages and Predicates
Stage 1: source
Event
Stage 2: where
| where EventLog =~ "Microsoft-Windows-Sysmon/Operational" and EventID==1
Stage 3: parse
| parse EventData with * 'Image">' Image "<" * 'CommandLine">' CommandLine "<" * 'ParentImage">' ParentImage "<" *
Stage 4: where
| where ParentImage has "svchost.exe" and Image has "rundll32.exe" and CommandLine has "{c08afd90-f2a1-11d1-8455-00a0c91f3880}"
Stage 5: parse
| parse EventData with * 'ProcessGuid">' ProcessGuid "<" * 'Description">' Description "<" * 'CurrentDirectory">' CurrentDirectory "<" * 'User">' User "<" * 'LogonGuid">' LogonGuid "<" * 'ParentProcessGuid">' ParentProcessGuid "<" * 'ParentImage">' ParentImage "<" * 'ParentCommandLine">' ParentCommandLine "<" * 'ParentUser">' ParentUser "<" *
Stage 6: summarize
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, User, ParentImage, ParentProcessGuid, ParentCommandLine, ParentUser, Image, ProcessGuid, CommandLine, Description
Stage 7: extend
| extend HostName = iif(Computer has '.',substring(Computer,0,indexof(Computer,'.')),Computer) , DnsDomain = iif(Computer has '.',substring(Computer,indexof(Computer,'.')+1),'')
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
CommandLine | match |
| field:"CommandLine" kind:match value:"{c08afd90-f2a1-11d1-8455-00a0c91f3880}" |
EventID | eq |
| field:"EventID" kind:eq value:"1" |
EventLog | eq |
| field:"EventLog" kind:eq value:"Microsoft-Windows-Sysmon/Operational" |
Image | match |
| field:"Image" kind:match value:"rundll32.exe" |
ParentImage | match |
| field:"ParentImage" kind:match value:"svchost.exe" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CommandLine | summarize |
Computer | summarize |
Description | summarize |
EndTime | summarize |
EventID | summarize |
Image | summarize |
ParentCommandLine | summarize |
ParentImage | summarize |
ParentProcessGuid | summarize |
ParentUser | summarize |
ProcessGuid | summarize |
StartTime | summarize |
User | summarize |
DnsDomain | extend |
HostName | extend |
GCP Security Command Center - Detect Open/Unrestricted API Keys
#Detects Google Cloud projects that have API keys with unrestricted API access using Security Command Center API_KEY_APIS_UNRESTRICTED findings. These findings indicate API keys that are not restricted to specific APIs and may allow broader access than intended.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access |
Rule Body
id: d8e30113-373a-4f49-a0ad-1a5d8b95b729
name: GCP Security Command Center - Detect Open/Unrestricted API Keys
description: |
Detects Google Cloud projects that have API keys with unrestricted API access using Security Command Center API_KEY_APIS_UNRESTRICTED findings.
These findings indicate API keys that are not restricted to specific APIs and may allow broader access than intended.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: GoogleSCCDefinition
dataTypes:
- GoogleCloudSCC
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- CredentialAccess
relevantTechniques:
- T1190
- T1552
tags:
- CIS GCP Foundation 3.0 1.14
- NIST 800-53 R5 PL-8, SA-8
- PCI-DSS v4.0 2.2.2, 6.2.1
- ISO-27001 v2022 A.8.27
- Cloud Controls Matrix 4 DSP-07
- NIST Cybersecurity Framework 1.0 PR-IP-2
- CIS Controls 8.0 16.10
query: |
GoogleCloudSCC
| where tostring(Findings.state) == "ACTIVE"
| extend FindingCategory = tostring(Findings.category)
| where FindingCategory == "API_KEY_APIS_UNRESTRICTED"
| extend FindingsJson = parse_json(Findings), FindingsResourceJson = parse_json(FindingsResource)
| extend ResourceName = tostring(FindingsJson.resourceName)
| extend ProjectId = extract(@"projects/([^/]+)", 1, ResourceName)
| extend ProjectName = tostring(FindingsResourceJson.displayName)
| extend Severity = tostring(FindingsJson.severity),
FindingName = tostring(FindingsJson.name),
ExternalUri = tostring(FindingsJson.externalUri),
Description = tostring(FindingsJson.description)
// produce one row per project that has unrestricted API key findings
| summarize TimeGenerated = max(TimeGenerated),
FindingsCount = count(),
ExternalUri = any(ExternalUri),
Description = any(Description)
by ProjectId, ProjectName, Severity, ResourceName
| project TimeGenerated, ProjectId, ProjectName, ResourceName, Severity, ExternalUri, Description
entityMappings:
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: ProjectName
- identifier: AppId
columnName: ProjectId
customDetails:
ResourceName: ResourceName
ExternalUri: ExternalUri
Description: Description
alertDetailsOverride:
alertDisplayNameFormat: "GCP project {{ProjectName}} has unrestricted API key(s)"
alertDescriptionFormat: |-
Project {{ProjectName}} ({{ProjectId}}) has {{ResourceName}} with unrestricted API keys (API_KEY_APIS_UNRESTRICTED). Review API key restrictions, rotate or remove keys as appropriate, and apply API restrictions to keys.
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
GoogleCloudSCC
Stage 2: where
| where tostring(Findings.state) == "ACTIVE"
Stage 3: extend
| extend FindingCategory = tostring(Findings.category)
Stage 4: where
| where FindingCategory == "API_KEY_APIS_UNRESTRICTED"
Stage 5: extend (5 consecutive steps)
| extend FindingsJson = parse_json(Findings), FindingsResourceJson = parse_json(FindingsResource)
| extend ResourceName = tostring(FindingsJson.resourceName)
| extend ProjectId = extract(@"projects/([^/]+)", 1, ResourceName)
| extend ProjectName = tostring(FindingsResourceJson.displayName)
| extend Severity = tostring(FindingsJson.severity),
FindingName = tostring(FindingsJson.name),
ExternalUri = tostring(FindingsJson.externalUri),
Description = tostring(FindingsJson.description)
Stage 6: summarize
| summarize TimeGenerated = max(TimeGenerated),
FindingsCount = count(),
ExternalUri = any(ExternalUri),
Description = any(Description)
by ProjectId, ProjectName, Severity, ResourceName
Stage 7: project
| project TimeGenerated, ProjectId, ProjectName, ResourceName, Severity, ExternalUri, Description
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
FindingCategory | eq |
| field:"FindingCategory" kind:eq value:"API_KEY_APIS_UNRESTRICTED" |
Findings.state | eq |
| field:"Findings.state" kind:eq value:"ACTIVE" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Description | project |
ExternalUri | project |
ProjectId | project |
ProjectName | project |
ResourceName | project |
Severity | project |
TimeGenerated | project |
General Settings Updated
#Detects when Veeam Backup & Replication general settings are updated. This might indicate configuration changes that require review.
Rule Body
id: 14003a45-da0b-47dc-8e20-9711ba7b5112
name: General Settings Updated
description: Detects when Veeam Backup & Replication general settings are updated.
This might indicate configuration changes that require review.
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 == 31000\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 == 31000
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.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"31000" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Severity | project |
UserName | project |
Guardian- Invisible Text Policy Violation Detection
#'This alert creates an incident when Invisible Text Policy Violation detected from the Guardian.'
Rule Body
id: 1e31a465-e25d-4810-881b-8a79e0de9379
name: Guardian- Invisible Text Policy Violation Detection
description: |
'This alert creates an incident when Invisible Text Policy Violation detected from the Guardian.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: BoschAIShield
dataTypes:
- Guardian
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: |
Guardian
| where PolicyViolatedControlFeature =~ 'Invisible Text'
| where Severity =~ 'Low'
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- Invisible Text Policy Violation detection
alertDescriptionFormat: |
This query detects Invisible Text 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 =~ 'Invisible Text'
Stage 3: where
| where Severity =~ 'Low'
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
PolicyViolatedControlFeature | eq |
| field:"PolicyViolatedControlFeature" kind:eq value:"Invisible Text" |
Severity | eq |
| field:"Severity" kind:eq value:"Low" |
Malware in the recycle bin (Normalized Process Events)
#Identifies malware that has been hidden in the recycle bin. To use this analytics rule, make sure you have deployed the ASIM normalization parsers
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Stealth |
Telemetry Coverage
Rule Body
id: 61988db3-0565-49b5-b8e3-747195baac6e
name: Malware in the recycle bin (Normalized Process Events)
description: |
'Identifies malware that has been hidden in the recycle bin.
To use this analytics rule, make sure you have deployed the [ASIM normalization parsers](https://aka.ms/ASimProcessEvent)'
severity: Medium
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1564
tags:
- Id: b8266f81-2715-41a6-9062-42486cbc9c73
version: 1.0.0
- Schema: ASIMProcessEvent
SchemaVersion: 0.1.0
query: |
let procList = dynamic(["cmd.exe","ftp.exe","schtasks.exe","powershell.exe","rundll32.exe","regsvr32.exe","msiexec.exe"]);
imProcessCreate
| where CommandLine has "recycler"
| where Process has_any (procList)
| extend FileName = tostring(split(Process, '\\')[-1])
| where FileName in~ (procList)
| project TimeGenerated, Dvc, User, Process, FileName, CommandLine, ActingProcessName, EventVendor, EventProduct
| extend AccountName = tostring(split(User, @'\')[1]), AccountNTDomain = tostring(split(User, @'\')[0])
| extend HostName = tostring(split(Dvc, ".")[0]), DomainIndex = toint(indexof(Dvc, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Dvc, DomainIndex + 1), Dvc)
| project-away DomainIndex
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: User
- identifier: Name
columnName: AccountName
- identifier: NTDomain
columnName: AccountNTDomain
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Dvc
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: HostNameDomain
version: 1.2.5
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Yuval Naor
support:
tier: Community
categories:
domains: [ "Security - Threat Protection" ]
Stages and Predicates
Parameters
let procList = dynamic(["cmd.exe","ftp.exe","schtasks.exe","powershell.exe","rundll32.exe","regsvr32.exe","msiexec.exe"]);
Stage 1: source
imProcessCreate
Stage 2: where
| where CommandLine has "recycler"
Stage 3: where
| where Process has_any (procList)
Stage 4: extend
| extend FileName = tostring(split(Process, '\\')[-1])
Stage 5: where
| where FileName in~ (procList)
Stage 6: project
| project TimeGenerated, Dvc, User, Process, FileName, CommandLine, ActingProcessName, EventVendor, EventProduct
Stage 7: extend (3 consecutive steps)
| extend AccountName = tostring(split(User, @'\')[1]), AccountNTDomain = tostring(split(User, @'\')[0])
| extend HostName = tostring(split(Dvc, ".")[0]), DomainIndex = toint(indexof(Dvc, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Dvc, DomainIndex + 1), Dvc)
Stage 8: project-away
| project-away DomainIndex
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
CommandLine | match |
| field:"CommandLine" kind:match value:"recycler" |
FileName | in |
| field:"FileName" kind:in |
Process | match |
| field:"Process" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActingProcessName | project |
CommandLine | project |
Dvc | project |
EventProduct | project |
EventVendor | project |
FileName | project |
Process | project |
TimeGenerated | project |
User | project |
AccountNTDomain | extend |
AccountName | extend |
HostName | extend |
HostNameDomain | extend |
Objects for Job Deleted
#Detects when objects are deleted from the job. This might indicate unauthorized removal of critical components.
Rule Body
id: 5367e8fc-a150-468f-84f2-90ac1dabef15
name: Objects for Job Deleted
description: Detects when objects are deleted from the job. 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 == 32120\n| extend JobName = extract(\"\
JobName=\\\"([^\\\"]*)\\\"\", 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 [\"Job Name\"] = JobName,\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 == 32120
Stage 3: extend
| extend JobName = extract("JobName=\"([^\"]*)\"", 1, SyslogMessage)
Stage 4: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
["User Name"] = user,
["Job Name"] = JobName,
MessageDetails = Description,
Severity = SeverityDescription
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"32120" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
Job Name | project |
MessageDetails | project |
Severity | project |
User Name | project |
Power Platform - Possibly compromised user accesses Power Platform services
#Identifies user accounts flagged at risk in Microsoft Entra Identity Protection and correlates these users with sign-in activity in Power Platform, including Power Apps, Power Automate and Power Platform Admin Center.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Lateral Movement |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Entra ID | SigninLogs sign-in event: Sign-in naming a target application |
Rule Body
id: 54d48840-1c64-4399-afee-ad39a069118d
kind: Scheduled
name: Power Platform - Possibly compromised user accesses Power Platform services
description: Identifies user accounts flagged at risk in Microsoft Entra Identity
Protection and correlates these users with sign-in activity in Power Platform, including
Power Apps, Power Automate and Power Platform Admin Center.
severity: High
status: Available
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- LateralMovement
relevantTechniques:
- T1078
- T1210
query: |
let power_automate_appid = "6204c1d1-4712-4c46-a7d9-3ed63d992682";
let power_apps_appid = "a8f7a65c-f5ba-4859-b2d6-df772c264e9d";
let ppac_appid = "065d9450-1e87-434e-ac2f-69af271549ed";
let query_frequency = 1h;
SigninLogs
| where ingestion_time() >= ago(query_frequency)
| where array_length(todynamic(RiskEventTypes)) != 0 or array_length(todynamic(RiskEventTypes_V2)) != 0
| where AppId in (power_automate_appid, power_apps_appid, ppac_appid)
| extend AffectedPlatform = case(
AppId == ppac_appid,
"Power Platform Admin Center",
AppId == power_apps_appid,
"Power Apps",
AppId == power_automate_appid,
"Power Automate",
"Unknown"
)
| extend
Severity = iif(AffectedPlatform in ("Power Apps", "Power Automate"), "Medium", "High"),
CloudAppId = case(AffectedPlatform == "Power Apps", int(27593), AffectedPlatform == "Power Automate", int(27592), 0),
AccountName = tostring(split(UserPrincipalName, '@')[0]),
UPNSuffix = tostring(split(UserPrincipalName, '@')[1])
| project
TimeGenerated,
UserId,
UniqueTokenIdentifier,
Identity,
RiskEventTypes,
RiskEventTypes_V2,
UserPrincipalName,
AppId,
AppDisplayName,
AffectedPlatform,
IPAddress,
Severity,
CloudAppId,
AccountName,
UPNSuffix
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: AffectedPlatform
- identifier: AppId
columnName: AppId
alertDetailsOverride:
alertDisplayNameFormat: 'Risky user sign-in activity in {{{AffectedPlatform}} '
alertDescriptionFormat: The user {{UserPrincipalName}} has sign-in risk events associated
and successfully signed in to {{{AffectedPlatform}} from {{IPAddress}}
alertSeverityColumnName: Severity
customDetails:
RiskEventTypes: RiskEventTypes
RiskEventTypes_V2: RiskEventTypes_V2
version: 3.0.0
Stages and Predicates
Parameters
let power_automate_appid = "6204c1d1-4712-4c46-a7d9-3ed63d992682";
let power_apps_appid = "a8f7a65c-f5ba-4859-b2d6-df772c264e9d";
let ppac_appid = "065d9450-1e87-434e-ac2f-69af271549ed";
let query_frequency = 1h;
Stage 1: source
SigninLogs
Stage 2: where
| where ingestion_time() >= ago(query_frequency)
Stage 3: where
| where array_length(todynamic(RiskEventTypes)) != 0 or array_length(todynamic(RiskEventTypes_V2)) != 0
Stage 4: where
| where AppId in (power_automate_appid, power_apps_appid, ppac_appid)
Stage 5: extend
| extend AffectedPlatform = case(
AppId == ppac_appid,
"Power Platform Admin Center",
AppId == power_apps_appid,
"Power Apps",
AppId == power_automate_appid,
"Power Automate",
"Unknown"
)
AffectedPlatform =AppId =~ ppac_appid"Power Platform Admin Center"AppId =~ power_apps_appid"Power Apps"AppId =~ power_automate_appid"Power Automate""Unknown"Stage 6: extend
| extend
Severity = iif(AffectedPlatform in ("Power Apps", "Power Automate"), "Medium", "High"),
CloudAppId = case(AffectedPlatform == "Power Apps", int(27593), AffectedPlatform == "Power Automate", int(27592), 0),
AccountName = tostring(split(UserPrincipalName, '@')[0]),
UPNSuffix = tostring(split(UserPrincipalName, '@')[1])
CloudAppId =AffectedPlatform == "Power Apps"int(27593)AffectedPlatform == "Power Automate"int(27592)0Stage 7: project
| project
TimeGenerated,
UserId,
UniqueTokenIdentifier,
Identity,
RiskEventTypes,
RiskEventTypes_V2,
UserPrincipalName,
AppId,
AppDisplayName,
AffectedPlatform,
IPAddress,
Severity,
CloudAppId,
AccountName,
UPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AppId | in |
| field:"AppId" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
AffectedPlatform | project |
AppDisplayName | project |
AppId | project |
CloudAppId | project |
IPAddress | project |
Identity | project |
RiskEventTypes | project |
RiskEventTypes_V2 | project |
Severity | project |
TimeGenerated | project |
UPNSuffix | project |
UniqueTokenIdentifier | project |
UserId | project |
UserPrincipalName | project |
RecordedFuture Threat Hunting IP All Actors
#Recorded Future Threat Hunting IP correlation for all actors.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control | |
| Exfiltration |
Telemetry Coverage
Rule Body
id: e31bc14e-2b4c-42a4-af34-5bfd7d768aea
name: RecordedFuture Threat Hunting IP All Actors
description: |
'Recorded Future Threat Hunting IP correlation for all actors.'
severity: Medium
requiredDataConnectors:
- connectorId: ThreatIntelligenceUploadIndicatorsAPI
dataTypes:
- ThreatIntelIndicators
queryFrequency: 15m
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
- CommandAndControl
relevantTechniques:
- T1041
- T1568
customDetails:
ActorInformation: RecordedFuturePortalLink
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 1h
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: AlertPerResult
alertDetailsOverride:
alertDisplayNameFormat: '{{Description}}'
alertDescriptionFormat: '**{{Description}}**\n\nCorrelation found on {{NetworkIP}} from the {{Type}} table.\n'
alertDynamicProperties:
- alertProperty: AlertLink
value: RecordedFuturePortalLink
query: |
let ioc_lookBack = 1d;
// The source table (_Im_NetworkSession) is a ASIM parser table, but can be replaced by any infrastructure table containing ip data.
// The following workbook: Recorded Future - IP Correlation will help researching available data and selecting tables and columns
_Im_NetworkSession
| where isnotempty(DstIpAddr)
| join kind=inner (
ThreatIntelIndicators
// Only look for IOCs
| where ObservableKey == 'ipv4-addr:value'
| where isnotempty(ObservableValue)
// Only look at Recorded Future Threat Hunt Indicators.
| where Data.description startswith "Recorded Future - Threat Hunt"
// Only work with the latest indicators
| where TimeGenerated >= ago(ioc_lookBack)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id
| where IsActive == true and ValidUntil > now()
) on $left.DstIpAddr == $right.ObservableValue
// select column from the source table to match with Recorded Future ThreatIntelIndicators $left.DstIpAddr
| mv-expand Label=Data.labels
| extend RecordedFuturePortalLink = parse_json(tostring(Label)).RecordedFuturePortalLink
| project NetworkIP=ObservableValue, Description=Data.description, Type, TimeGenerated, RecordedFuturePortalLink
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: NetworkIP
version: 1.1.1
kind: Scheduled
Stages and Predicates
Parameters
let ioc_lookBack = 1d;
Stage 1: source
_Im_NetworkSession
Stage 2: where
| where isnotempty(DstIpAddr)
Stage 3: join
| join kind=inner (
ThreatIntelIndicators
| where ObservableKey == 'ipv4-addr:value'
| where isnotempty(ObservableValue)
| where Data.description startswith "Recorded Future - Threat Hunt"
| where TimeGenerated >= ago(ioc_lookBack)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id
| where IsActive == true and ValidUntil > now()
) on $left.DstIpAddr == $right.ObservableValue
Stage 4: mv-expand
mv-expand Label
Stage 5: extend
extend RecordedFuturePortalLink
Stage 6: project
project Description, NetworkIP, RecordedFuturePortalLink, TimeGenerated, Type
Stage 7: summarize aggregation inside the join branch
summarize by Id
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Data.description | starts_with |
| field:"Data.description" kind:starts_with value:"Recorded Future - Threat Hunt" |
DstIpAddr | is_not_null | field:"DstIpAddr" kind:is_not_null | |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
ObservableKey | eq |
| field:"ObservableKey" kind:eq value:"ipv4-addr:value" |
ObservableValue | is_not_null | field:"ObservableValue" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Id | summarize |
Risky user signin observed in non-Microsoft network device
#This content is utilized to identify instances of successful login by risky users, who have been observed engaging in potentially suspicious network activity on non-Microsoft network devices.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Entra ID | SigninLogs sign-in event: Successful risky sign-in |
| Microsoft Sentinel | CommonSecurityLog CEF event: Allowed third-party network activity |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Suspicious Continuous OAuth Token Usage (Kusto)
- Anomalous sign-in location by user account and authenticating application (Kusto)
- Anomalous Single Factor Signin (Kusto)
- Authentications of Privileged Accounts Outside of Expected Controls (Kusto)
- Azure Portal sign in from another Azure Tenant (Kusto)
- Azure Service Principal Sign-In Followed by Arc Cluster Credential Access (Elastic)
- Azure SignIn via Legacy Authentication Protocol (Panther)
- Cisco - firewall block but success logon to Microsoft Entra ID (Kusto)
Rule Body
id: 042f2801-a375-4cfd-bd29-041fc7ed88a0
name: Risky user signin observed in non-Microsoft network device
description: |
'This content is utilized to identify instances of successful login by risky users, who have been observed engaging in potentially suspicious network activity on non-Microsoft network devices.'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
- connectorId: PaloAltoNetworks
dataTypes:
- CommonSecurityLog (PaloAlto)
- connectorId: Fortinet
dataTypes:
- CommonSecurityLog (Fortinet)
- connectorId: CheckPoint
dataTypes:
- CommonSecurityLog (CheckPoint)
- connectorId: Zscaler
dataTypes:
- CommonSecurityLog (Zscaler)
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
query: |
SigninLogs
//Find risky Signin
| where RiskState == "atRisk" and ResultType == 0
| extend Signin_Time = TimeGenerated
| summarize
AppDisplayName=make_set(AppDisplayName),
ClientAppUsed=make_set(ClientAppUsed),
UserAgent=make_set(UserAgent),
CorrelationId=make_set(CorrelationId),
Signin_Time= min(Signin_Time),
RiskEventTypes=make_set(RiskEventTypes)
by
ConditionalAccessStatus,
IPAddress,
IsRisky,
ResourceDisplayName,
RiskDetail,
ResultType,
RiskLevelAggregated,
RiskLevelDuringSignIn,
RiskState,
UserPrincipalName=tostring(tolower(UserPrincipalName)),
SourceSystem
| join kind=inner (
CommonSecurityLog
| where DeviceVendor has_any ("Palo Alto Networks", "Fortinet", "Check Point", "Zscaler")
| where DeviceProduct startswith "FortiGate" or DeviceProduct startswith "PAN" or DeviceProduct startswith "VPN" or DeviceProduct startswith "FireWall" or DeviceProduct startswith "NSSWeblog" or DeviceProduct startswith "URL"
| where DeviceAction != "Block"
| where isnotempty(RequestURL)
| where isnotempty(SourceUserName)
| extend SourceUserName = tolower(SourceUserName)
| summarize
min(TimeGenerated),
max(TimeGenerated),
Activity=make_set(Activity)
by DestinationHostName, DestinationIP, RequestURL, SourceUserName=tostring(tolower(SourceUserName)),DeviceVendor,DeviceProduct
| extend 3p_observed_Time= min_TimeGenerated,Name = tostring(split(SourceUserName,"@")[0]),UPNSuffix =tostring(split(SourceUserName,"@")[1]))
on $left.IPAddress == $right.DestinationIP and $left.UserPrincipalName == $right.SourceUserName
| extend Timediff = datetime_diff('day', 3p_observed_Time, Signin_Time)
| where Timediff <= 1 and Timediff >= 0
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserPrincipalName
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: DestinationIP
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: DestinationHostName
kind: Scheduled
version: 1.0.6
metadata:
source:
kind: Community
author:
name: Arjun Trivedi
support:
tier: Community
categories:
domains: [ "Security - Threat Protection" ]
Stages and Predicates
Stage 1: source
SigninLogs
Stage 2: where
| where RiskState == "atRisk" and ResultType == 0
Stage 3: extend
| extend Signin_Time = TimeGenerated
Stage 4: summarize
| summarize
AppDisplayName=make_set(AppDisplayName),
ClientAppUsed=make_set(ClientAppUsed),
UserAgent=make_set(UserAgent),
CorrelationId=make_set(CorrelationId),
Signin_Time= min(Signin_Time),
RiskEventTypes=make_set(RiskEventTypes)
by
ConditionalAccessStatus,
IPAddress,
IsRisky,
ResourceDisplayName,
RiskDetail,
ResultType,
RiskLevelAggregated,
RiskLevelDuringSignIn,
RiskState,
UserPrincipalName=tostring(tolower(UserPrincipalName)),
SourceSystem
Stage 5: join
| join kind=inner (
CommonSecurityLog
| where DeviceVendor has_any ("Palo Alto Networks", "Fortinet", "Check Point", "Zscaler")
| where DeviceProduct startswith "FortiGate" or DeviceProduct startswith "PAN" or DeviceProduct startswith "VPN" or DeviceProduct startswith "FireWall" or DeviceProduct startswith "NSSWeblog" or DeviceProduct startswith "URL"
| where DeviceAction != "Block"
| where isnotempty(RequestURL)
| where isnotempty(SourceUserName)
| extend SourceUserName = tolower(SourceUserName)
| summarize
min(TimeGenerated),
max(TimeGenerated),
Activity=make_set(Activity)
by DestinationHostName, DestinationIP, RequestURL, SourceUserName=tostring(tolower(SourceUserName)),DeviceVendor,DeviceProduct
| extend 3p_observed_Time= min_TimeGenerated,Name = tostring(split(SourceUserName,"@")[0]),UPNSuffix =tostring(split(SourceUserName,"@")[1]))
on $left.IPAddress == $right.DestinationIP and $left.UserPrincipalName == $right.SourceUserName
Stage 6: extend
extend Timediff
Stage 7: where
where Timediff >= 0 and Timediff <= 1
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DeviceAction | ne |
| field:"DeviceAction" kind:ne value:"Block" |
DeviceProduct | starts_with |
| field:"DeviceProduct" kind:starts_with |
DeviceVendor | match |
| field:"DeviceVendor" kind:match |
RequestURL | is_not_null | field:"RequestURL" kind:is_not_null | |
ResultType | eq |
| field:"ResultType" kind:eq value:"0" |
RiskState | eq |
| field:"RiskState" kind:eq value:"atRisk" |
SourceUserName | is_not_null | field:"SourceUserName" kind:is_not_null | |
Timediff | ge |
| field:"Timediff" kind:ge value:"0" |
Timediff | le |
| field:"Timediff" kind:le value:"1" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AppDisplayName | summarize |
ClientAppUsed | summarize |
ConditionalAccessStatus | summarize |
CorrelationId | summarize |
IPAddress | summarize |
IsRisky | summarize |
ResourceDisplayName | summarize |
ResultType | summarize |
RiskDetail | summarize |
RiskEventTypes | summarize |
RiskLevelAggregated | summarize |
RiskLevelDuringSignIn | summarize |
RiskState | summarize |
Signin_Time | summarize |
SourceSystem | summarize |
UserAgent | summarize |
UserPrincipalName | summarize |
Timediff | extend |
Rouge RDP: Suspicious File Creation
#Below query detects file creations of mstsc.exe where it also makes a network connection to a public IP address. This behavior is an indication of Rogue RDP.
False Positives: Copying files to the local machine over RDP may cause false positives.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Defense Impairment | |
| Credential Access |
References
Telemetry Coverage
Rule Body
// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
// Description: Detect file creations of mstsc.exe where it also makes a network connection to a public IP address. This behavior is an indication of Rogue RDP.
//
// Query parameters:
// there might be more file types that can be leveraged
// adjust file types to monitor
// consider DLL sideloading opportunities if you want to filter based on folders.
let file_types = dynamic([".dll", ".exe", ".cpl", ".pif", ".com", ".js", ".vbs", ".wsh", ".vbe", ".jse", ".bat", ".cmd", ".lnk", ".url", ".png", ".hta", ".svg"]);
let _query_period = 7d;
DeviceNetworkEvents
| where Timestamp > ago(_query_period)
| where ActionType in ("ConnectionSuccess")
| where InitiatingProcessFileName =~ "mstsc.exe"
| where RemoteIPType == "Public"
| project Timestamp, DeviceId, DeviceName, InitiatingProcessFileName=tolower(InitiatingProcessFileName), InitiatingProcessId, RemoteIP, RemoteUrl, RemotePort, NetworkTimestamp = Timestamp
| join kind=inner (
DeviceFileEvents
| where Timestamp > ago(_query_period)
| where ActionType in ("FileCreated", "FileModified")
| where InitiatingProcessFileName =~ "mstsc.exe"
| where FileName has_any (file_types)
| extend InitiatingProcessFileName=tolower(InitiatingProcessFileName), FileTimestamp = Timestamp
) on InitiatingProcessFileName, DeviceId // InitiatingProcessId or InitiatingProcessUniqueId can be used as well but be mindful of multiple connections and telemetry sampling.
// adjust time between RDP connection and file creation
| where datetime_diff('minute', FileTimestamp, NetworkTimestamp) < 60
Stages and Predicates
Parameters
let file_types = dynamic([".dll", ".exe", ".cpl", ".pif", ".com", ".js", ".vbs", ".wsh", ".vbe", ".jse", ".bat", ".cmd", ".lnk", ".url", ".png", ".hta", ".svg"]);
let _query_period = 7d;
Stage 1: source
DeviceNetworkEvents
Stage 2: where
| where Timestamp > ago(_query_period)
Stage 3: where
| where ActionType in ("ConnectionSuccess")
Stage 4: where
| where InitiatingProcessFileName =~ "mstsc.exe"
Stage 5: where
| where RemoteIPType == "Public"
Stage 6: project
| project Timestamp, DeviceId, DeviceName, InitiatingProcessFileName=tolower(InitiatingProcessFileName), InitiatingProcessId, RemoteIP, RemoteUrl, RemotePort, NetworkTimestamp = Timestamp
Stage 7: join
| join kind=inner (
DeviceFileEvents
| where Timestamp > ago(_query_period)
| where ActionType in ("FileCreated", "FileModified")
| where InitiatingProcessFileName =~ "mstsc.exe"
| where FileName has_any (file_types)
| extend InitiatingProcessFileName=tolower(InitiatingProcessFileName), FileTimestamp = Timestamp
) on InitiatingProcessFileName, DeviceId
Stage 8: where FileTimestamp - NetworkTimestamp < 1h
where FileTimestamp - NetworkTimestamp < 1h
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionType | in |
| field:"ActionType" kind:in |
FileName | match |
| field:"FileName" kind:match |
InitiatingProcessFileName | eq |
| field:"process_basename" kind:eq |
RemoteIPType | eq |
| field:"RemoteIPType" kind:eq value:"Public" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceId | project |
DeviceName | project |
InitiatingProcessFileName | project |
InitiatingProcessId | project |
NetworkTimestamp | project |
RemoteIP | project |
RemotePort | project |
RemoteUrl | project |
Timestamp | project |
SFTP File transfer above threshold
#'Identifies SFTP File Transfers above certain threshold in a 15min time period. It requires SFTP VERBOSE loglevel to be enabled. Please note that entity mapping for arrays is not supported, so when there is a single value in an array, we will pull that value from the array as a single string to populate the entity to support entity mapping features within Sentinel. Additionally, if the array is multivalued, we will input a string to indicate this with a unique hash so that matching will not occur.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Rule Body
id: bb6a74c8-889d-4c6e-8412-7d5efe33f4ed
name: SFTP File transfer above threshold
description: |
'Identifies SFTP File Transfers above certain threshold in a 15min time period. It requires SFTP VERBOSE loglevel to be enabled.
Please note that entity mapping for arrays is not supported, so when there is a single value in an array, we will pull that value from the array as a single string to populate the entity to support entity mapping features within Sentinel. Additionally, if the array is multivalued, we will input a string to indicate this with a unique hash so that matching will not occur.'
severity: Medium
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
relevantTechniques:
- T1020
query: |
let threshold = 50;
Syslog
| where ProcessName has "sftp"
and SyslogMessage has "close "
and SyslogMessage has " bytes read "
| parse SyslogMessage with "close \"" filepath "\" bytes read " readbytes: int " written " writtenbytes: int
| join kind=leftouter (
Syslog
| where ProcessName has "sftp" and SyslogMessage has "session opened for"
| parse SyslogMessage with "session opened for local user " username: string " from [" src_ip "]"
| project username, src_ip, ProcessID
)
on ProcessID
| project-away ProcessID1
| summarize count_distinct_filepath=dcount(filepath), fileslist=make_set(filepath) by Computer, username, src_ip, bin(TimeGenerated, 15m)
| where count_distinct_filepath >= threshold
| extend FileSample = iff(array_length(fileslist) == 1, tostring(fileslist[0]), strcat("SeeFilesListField","_", tostring(hash(tostring(fileslist)))))
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: username
- entityType: IP
fieldMappings:
- identifier: Address
columnName: src_ip
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Computer
- entityType: File
fieldMappings:
- identifier: Name
columnName: FileSample
customDetails:
TransferCount: count_distinct_filepath
FilesList: fileslist
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 5h
matchingMethod: Selected
groupByEntities:
- Account
- Host
groupByAlertDetails: []
groupByCustomDetails: []
alertDetailsOverride:
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 50;
Stage 1: source
Syslog
Stage 2: where
| where ProcessName has "sftp"
and SyslogMessage has "close "
and SyslogMessage has " bytes read "
Stage 3: parse
| parse SyslogMessage with "close \"" filepath "\" bytes read " readbytes: int " written " writtenbytes: int
Stage 4: join
| join kind=leftouter (
Syslog
| where ProcessName has "sftp" and SyslogMessage has "session opened for"
| parse SyslogMessage with "session opened for local user " username: string " from [" src_ip "]"
| project username, src_ip, ProcessID
)
on ProcessID
Stage 5: project-away
project-away ProcessID1
Stage 6: summarize
summarize count_distinct_filepath, fileslist by Computer, username, src_ip
Stage 7: where
where count_distinct_filepath >= 50
Stage 8: extend
extend FileSample
FileSample =fileslist == 1tostring(fileslist[0])strcat("SeeFilesListField", "_", tostring(hash(tostring(fileslist))))Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ProcessName | match |
| field:"process_name" kind:match value:"sftp" |
SyslogMessage | match |
| field:"SyslogMessage" kind:match |
count_distinct_filepath | ge |
| field:"count_distinct_filepath" kind:ge value:"50" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Computer | summarize |
count_distinct_filepath | summarize |
fileslist | summarize |
src_ip | summarize |
username | summarize |
FileSample | extend |
SUNBURST and SUPERNOVA backdoor hashes (Normalized File Events)
#Identifies SolarWinds SUNBURST and SUPERNOVA backdoor file hash IOCs in File Events To use this analytics rule, make sure you have deployed the ASIM normalization parsers References: - https://www.fireeye.com/blog/threat-research/2020/12/evasive-attacker-leverages-solarwinds-supply-chain-compromises-with-sunburst-backdoor.html - https://gist.github.com/olafhartong/71ffdd4cab4b6acd5cbcd1a0691ff82f
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Persistence |
Telemetry Coverage
Rule Body
id: bc5ffe2a-84d6-48fe-bc7b-1055100469bc
name: SUNBURST and SUPERNOVA backdoor hashes (Normalized File Events)
description: |
Identifies SolarWinds SUNBURST and SUPERNOVA backdoor file hash IOCs in File Events
To use this analytics rule, make sure you have deployed the [ASIM normalization parsers](https://aka.ms/ASimFileEvent)
References:
- https://www.fireeye.com/blog/threat-research/2020/12/evasive-attacker-leverages-solarwinds-supply-chain-compromises-with-sunburst-backdoor.html
- https://gist.github.com/olafhartong/71ffdd4cab4b6acd5cbcd1a0691ff82f
severity: High
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Execution
- Persistence
- InitialAccess
relevantTechniques:
- T1195
- T1059
- T1546
tags:
- Id: a3c144f9-8051-47d4-ac29-ffb0c312c910
version: 1.0.0
query: |
let SunburstMD5=dynamic(["b91ce2fa41029f6955bff20079468448","02af7cec58b9a5da1c542b5a32151ba1","2c4a910a1299cdae2a4e55988a2f102e","846e27a652a5e1bfbd0ddd38a16dc865","4f2eb62fa529c0283b28d05ddd311fae"]);
let SupernovaMD5="56ceb6d0011d87b6e4d7023d7ef85676";
imFileEvent
| where TargetFileMD5 in (SunburstMD5) or TargetFileMD5 in (SupernovaMD5)
| extend AccountName = tostring(split(User, @'\')[1]), AccountNTDomain = tostring(split(User, @'\')[0])
| extend AlgorithmType = "MD5"
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: User
- identifier: Name
columnName: AccountName
- identifier: NTDomain
columnName: AccountNTDomain
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Dvc
- identifier: HostName
columnName: DvcHostname
- identifier: DnsDomain
columnName: DvcDomain
- entityType: FileHash
fieldMappings:
- identifier: Algorithm
columnName: AlgorithmType
- identifier: Value
columnName: TargetFileMD5
version: 1.0.7
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Yaron
support:
tier: Community
categories:
domains: [ "Security - Threat Intelligence" ]
Stages and Predicates
Parameters
let SupernovaMD5 = "56ceb6d0011d87b6e4d7023d7ef85676";
Let Binding: SunburstMD5
let SunburstMD5 = dynamic(["b91ce2fa41029f6955bff20079468448","02af7cec58b9a5da1c542b5a32151ba1","2c4a910a1299cdae2a4e55988a2f102e","846e27a652a5e1bfbd0ddd38a16dc865","4f2eb62fa529c0283b28d05ddd311fae"]);
Stage 1: source
imFileEvent
Stage 2: where
| where TargetFileMD5 in (SunburstMD5) or TargetFileMD5 in (SupernovaMD5)
Stage 3: extend
| extend AccountName = tostring(split(User, @'\')[1]), AccountNTDomain = tostring(split(User, @'\')[0])
Stage 4: extend
| extend AlgorithmType = "MD5"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
TargetFileMD5 | in |
| field:"md5" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountNTDomain | extend |
AccountName | extend |
AlgorithmType | extend |
TI Map IP entity to AppServiceHTTPLogs
#Identifies a match in AppServiceHTTPLogs from any IP IOC from TI
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: 206277b1-9a2c-4c62-9ee8-a4c888810d3c
name: TI Map IP entity to AppServiceHTTPLogs
description: |
Identifies a match in AppServiceHTTPLogs from any IP IOC from TI
severity: Medium
requiredDataConnectors:
- 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; // Look back 1 hour for AppServiceHTTPLogs
let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
// Fetch threat intelligence indicators related to IP addresses
let IP_Indicators = ThreatIntelIndicators
//extract key part of kv pair
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
| where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")
| extend NetworkSourceIP = ObservableValue
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
// Filter out indicators without relevant IP address fields
| where TimeGenerated >= ago(ioc_lookBack)
// Filtering out rows where the Confidence Score is less than 50 as they would not have an Alert Priority label.
| where Confidence > 50
// Select the IP entity based on availability of different IP fields
| extend TI_ipEntity = iff(isnotempty(NetworkSourceIP), NetworkSourceIP, NetworkSourceIP)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
// Determine Severity based on ConfidenceScore
| extend Severity = case(Confidence > 82, "High",
Confidence > 74, "Medium",
"Low")
// Exclude local addresses using the ipv4_is_private operator and filtering out specific address prefixes
| 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 Id, ObservableValue
| where IsActive and (ValidUntil > now() or isempty(ValidUntil));
// Perform a join between IP indicators and AppServiceHTTPLogs to identify potential malicious activity
IP_Indicators
| project-reorder *, IsActive, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity, Severity
// 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 (
AppServiceHTTPLogs | where TimeGenerated >= ago(dt_lookBack)
| where isnotempty(CIp)
| extend WebApp = split(_ResourceId, '/')[8]
| extend AppService_TimeGenerated = TimeGenerated // Rename time column for clarity
)
on $left.TI_ipEntity == $right.CIp
// Filter out logs that occurred after the expiration of the corresponding indicator
| where AppService_TimeGenerated < ValidUntil
// Group the results by IndicatorId and CIp, and keep the log entry with the latest timestamp
| summarize AppService_TimeGenerated = arg_max(AppService_TimeGenerated, *) by Id, CIp
// Select the desired output fields
| extend Description = tostring(parse_json(Data).description)
| extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
| project AppService_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence, TI_ipEntity, CsUsername, WebApp = split(_ResourceId, '/')[8], CIp, CsHost, NetworkSourceIP, _ResourceId, Type, Url, Severity
// Extract hostname and DNS domain from the CsHost field
| extend HostName = tostring(split(CsHost, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(CsHost, '.'), 1, -1), '.'))
// Rename the timestamp field
| extend timestamp = AppService_TimeGenerated
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
- entityType: Account
fieldMappings:
- identifier: Name
columnName: CsUsername
- entityType: IP
fieldMappings:
- identifier: Address
columnName: CIp
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
- entityType: AzureResource
fieldMappings:
- identifier: ResourceId
columnName: _ResourceId
alertDetailsOverride:
alertSeverityColumnName: Severity
version: 1.5.9
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 11 define let IP_Indicators as the rule's main pipeline source. Stages 12 to 20 use it.
Stage 1: source
ThreatIntelIndicators
Stage 2: extend
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
Stage 3: where
| where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")
Stage 4: extend
| extend NetworkSourceIP = ObservableValue
Stage 5: extend
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
Stage 6: where
| where TimeGenerated >= ago(ioc_lookBack)
Stage 7: where
| where Confidence > 50
Stage 8: extend (4 consecutive steps)
| extend TI_ipEntity = iff(isnotempty(NetworkSourceIP), NetworkSourceIP, NetworkSourceIP)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
| extend Severity = case(Confidence > 82, "High",
Confidence > 74, "Medium",
"Low")
TI_ipEntity =isnotempty(NetworkSourceIP)NetworkSourceIPNetworkSourceIPStage 9: where
| where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
Stage 10: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
Stage 11: where
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
Stage 12: project-reorder
IP_Indicators
| project-reorder *, IsActive, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity, Severity
Stage 13: join
| join kind=innerunique (
AppServiceHTTPLogs | where TimeGenerated >= ago(dt_lookBack)
| where isnotempty(CIp)
| extend WebApp = split(_ResourceId, '/')[8]
| extend AppService_TimeGenerated = TimeGenerated
)
on $left.TI_ipEntity == $right.CIp
Stage 14: where
where AppService_TimeGenerated < ValidUntil
Stage 15: summarize
summarize AppService_TimeGenerated by Id, CIp
Stage 16: extend
extend Description
Stage 17: extend
extend ActivityGroupNames
Stage 18: project
project ActivityGroupNames, AppService_TimeGenerated, CIp, Confidence, CsHost, CsUsername, Description, Id, NetworkSourceIP, Severity, TI_ipEntity, Type, Url, ValidUntil, WebApp, _ResourceId
Stage 19: extend
extend DnsDomain, HostName
Stage 20: extend
extend timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
CIp | is_not_null | field:"CIp" kind:is_not_null | |
Confidence | gt |
| field:"Confidence" kind:gt value:"50" |
IndicatorType | in |
| field:"IndicatorType" kind:in |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
ValidUntil | is_null | field:"ValidUntil" kind:is_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
TI_ipEntity | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 | excludes:TI_ipEntity |
TI_ipEntity | starts_with | 127. | excludes:TI_ipEntity field:"TI_ipEntity" value:"127." |
TI_ipEntity | starts_with | :: | excludes:TI_ipEntity field:"TI_ipEntity" value:"::" |
TI_ipEntity | starts_with | fe80 | excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
AppService_TimeGenerated | project |
CIp | project |
Confidence | project |
CsHost | project |
CsUsername | project |
Description | project |
Id | project |
NetworkSourceIP | project |
Severity | project |
TI_ipEntity | project |
Type | project |
Url | project |
ValidUntil | project |
WebApp | project |
_ResourceId | project |
DnsDomain | extend |
HostName | extend |
timestamp | extend |
TI Map IP entity to AzureNetworkAnalytics_CL (NSG Flow Logs)
#Identifies a match in AzureNetworkAnalytics_CL (NSG Flow Logs) from any IP IOC from TI that was Allowed
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: 929160b7-4449-4307-a3f9-bb742d1b8f01
name: TI Map IP entity to AzureNetworkAnalytics_CL (NSG Flow Logs)
description: |
Identifies a match in AzureNetworkAnalytics_CL (NSG Flow Logs) from any IP IOC from TI that was Allowed
severity: Medium
requiredDataConnectors:
- 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; // Look back 1 hour for AzureNetworkAnalytics_CL logs
let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
// Fetch threat intelligence indicators related to IP addresses
let IP_Indicators = ThreatIntelIndicators
//extract key part of kv pair
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
| where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")
| extend NetworkSourceIP = toupper(ObservableValue)
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
| where TimeGenerated >= ago(ioc_lookBack)
| extend TI_ipEntity = iff(isnotempty(NetworkSourceIP), NetworkSourceIP, NetworkSourceIP)
| 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 Id, ObservableValue
| where IsActive and (ValidUntil > now() or isempty(ValidUntil));
// Perform a join between IP indicators and AzureNetworkAnalytics_CL logs for NSG Flow information
IP_Indicators
| project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity
// 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 (
AzureNetworkAnalytics_CL
| where TimeGenerated >= ago(dt_lookBack)
| extend AzureNetworkAnalytics_CL_TimeGenerated = TimeGenerated
| extend PIPs = split(PublicIPs_s, '|', 0)
| extend PIP = tostring(PIPs[0])
)
on $left.TI_ipEntity == $right.PIP
// Filter out logs that occurred after the expiration of the corresponding indicator
| where AzureNetworkAnalytics_CL_TimeGenerated < ValidUntil
// Filter out NSG Flow logs that are not allowed (FlowStatus_s == "A")
| where FlowStatus_s == "A"
// Group the results by IndicatorId and PIP (Public IP), and keep the log entry with the latest timestamp
| summarize AzureNetworkAnalytics_CL_TimeGenerated = arg_max(AzureNetworkAnalytics_CL_TimeGenerated, *) by Id, PIP
// Select the desired output fields
| extend Description = tostring(parse_json(Data).description)
| extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
| project AzureNetworkAnalytics_CL_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence,
TI_ipEntity, Computer, FlowDirection_s, FlowStatus_s, FlowType_s, SrcPublicIPs_s, DestPublicIPs_s, PublicIPs_s, L7Protocol_s, DestPort_d, NetworkSourceIP, Type
// Extract hostname and DNS domain from the Computer field
| extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
// Rename the timestamp field
| extend timestamp = AzureNetworkAnalytics_CL_TimeGenerated
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: TI_ipEntity
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
version: 1.4.7
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let IP_Indicators is inlined into the numbered stages below.
Stages 1 to 11 define let IP_Indicators as the rule's main pipeline source. Stages 12 to 21 use it.
Stage 1: source
ThreatIntelIndicators
Stage 2: extend
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
Stage 3: where
| where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")
Stage 4: extend
| extend NetworkSourceIP = toupper(ObservableValue)
Stage 5: extend
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
Stage 6: where
| where TimeGenerated >= ago(ioc_lookBack)
Stage 7: extend
| extend TI_ipEntity = iff(isnotempty(NetworkSourceIP), NetworkSourceIP, NetworkSourceIP)
TI_ipEntity =isnotempty(NetworkSourceIP)NetworkSourceIPNetworkSourceIPStage 8: extend
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
TI_ipEntity =isempty(TI_ipEntity) and isnotempty(NetworkSourceIP)NetworkSourceIPTI_ipEntityStage 9: where
| where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
Stage 10: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
Stage 11: where
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
Stage 12: project-reorder
IP_Indicators
| project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity
Stage 13: join
| join kind=innerunique (
AzureNetworkAnalytics_CL
| where TimeGenerated >= ago(dt_lookBack)
| extend AzureNetworkAnalytics_CL_TimeGenerated = TimeGenerated
| extend PIPs = split(PublicIPs_s, '|', 0)
| extend PIP = tostring(PIPs[0])
)
on $left.TI_ipEntity == $right.PIP
Stage 14: where
where AzureNetworkAnalytics_CL_TimeGenerated < ValidUntil
Stage 15: where
where FlowStatus_s == "A"
Stage 16: summarize
summarize AzureNetworkAnalytics_CL_TimeGenerated by Id, PIP
Stage 17: extend
extend Description
Stage 18: extend
extend ActivityGroupNames
Stage 19: project
project ActivityGroupNames, AzureNetworkAnalytics_CL_TimeGenerated, Computer, Confidence, Description, DestPort_d, DestPublicIPs_s, FlowDirection_s, FlowStatus_s, FlowType_s, Id, L7Protocol_s, NetworkSourceIP, PublicIPs_s, SrcPublicIPs_s, TI_ipEntity, Type, ValidUntil
Stage 20: extend
extend DnsDomain, HostName
Stage 21: extend
extend timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
FlowStatus_s | eq |
| field:"FlowStatus_s" kind:eq value:"A" |
IndicatorType | in |
| field:"IndicatorType" kind:in |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
ValidUntil | is_null | field:"ValidUntil" kind:is_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
TI_ipEntity | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 | excludes:TI_ipEntity |
TI_ipEntity | starts_with | 127. | excludes:TI_ipEntity field:"TI_ipEntity" value:"127." |
TI_ipEntity | starts_with | :: | excludes:TI_ipEntity field:"TI_ipEntity" value:"::" |
TI_ipEntity | starts_with | fe80 | excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
AzureNetworkAnalytics_CL_TimeGenerated | project |
Computer | project |
Confidence | project |
Description | project |
DestPort_d | project |
DestPublicIPs_s | project |
FlowDirection_s | project |
FlowStatus_s | project |
FlowType_s | project |
Id | project |
L7Protocol_s | project |
NetworkSourceIP | project |
PublicIPs_s | project |
SrcPublicIPs_s | project |
TI_ipEntity | project |
Type | project |
ValidUntil | project |
DnsDomain | extend |
HostName | extend |
timestamp | extend |
TI Map IP Entity to DeviceNetworkEvents
#Identifies a match in DeviceNetworkEvents Event data from any IP Indicator from TI.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceNetworkEvents action any: Network activity |
Rule Body
id: 2474343c-9135-42ec-9c40-a1bace43da5c
name: TI Map IP Entity to DeviceNetworkEvents
description: |
'Identifies a match in DeviceNetworkEvents Event data from any IP Indicator from TI.'
severity: Medium
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceNetworkEvents
- 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 DeviceNetworkEvents_ = DeviceNetworkEvents
| where isnotempty(RemoteIP)
| where TimeGenerated > ago(dt_lookBack)
| where ActionType !has "ConnectionFailed"
| extend isPrivate = ipv4_is_private(RemoteIP)
| where isPrivate != true;
let IPs = DeviceNetworkEvents_ | distinct RemoteIP | summarize make_list(RemoteIP);
ThreatIntelIndicators
//extract key part of kv pair
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
| where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")
| extend NetworkSourceIP = toupper(ObservableValue)
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
| where TimeGenerated >= ago(ioc_lookBack)
| extend TI_ipEntity = NetworkSourceIP
| where TI_ipEntity in (IPs)
| 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;"
| join kind=innerunique (DeviceNetworkEvents_) on $left.TI_ipEntity == $right.RemoteIP
| summarize TimeGenerated = arg_max(TimeGenerated, *) by Id, TI_ipEntity, DeviceName
| extend timestamp = TimeGenerated, Name = tostring(split(InitiatingProcessAccountUpn, '@', 0)[0]), UPNSuffix = tostring(split(InitiatingProcessAccountUpn, '@', 1)[0])
| project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: TI_ipEntity
- entityType: URL
fieldMappings:
- identifier: Url
columnName: RemoteUrl
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: DeviceName
version: 1.0.4
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
Let Binding: DeviceNetworkEvents_
let DeviceNetworkEvents_ = DeviceNetworkEvents
| where isnotempty(RemoteIP)
| where TimeGenerated > ago(dt_lookBack)
| where ActionType !has "ConnectionFailed"
| extend isPrivate = ipv4_is_private(RemoteIP)
| where isPrivate != true;
Let Binding: IPs
let IPs = DeviceNetworkEvents_ | distinct RemoteIP | summarize make_list(RemoteIP);
Stage 1: source
let DeviceNetworkEvents_
Stage 2: source
let IPs
Stage 3: source
ThreatIntelIndicators
Stage 4: extend
extend IndicatorType
Stage 5: where
where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")
Stage 6: extend
extend NetworkSourceIP
Stage 7: extend
extend TrafficLightProtocolLevel
Stage 8: where
where TimeGenerated >= ago(1209600s)
Stage 9: extend
extend TI_ipEntity
Stage 10: where
where TI_ipEntity == "IPs"
Stage 11: summarize
summarize LatestIndicatorTime by Id, ObservableValue
Stage 12: where
where (isempty(ValidUntil) or ValidUntil > now()) and IsActive == true
Stage 13: extend
extend Description
Stage 14: where
where not (Description contains_cs "State: falsepos;") and not (Description contains_cs "State: inactive;")
Stage 15: join
join kind=innerunique (DeviceNetworkEvents_) on $left.TI_ipEntity, $right.RemoteIP
Stage 16: summarize
summarize TimeGenerated by Id, TI_ipEntity, DeviceName
Stage 17: extend
extend Name, UPNSuffix, timestamp
Stage 18: project-reorder
project-reorder
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
IndicatorType | in |
| field:"IndicatorType" kind:in |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
RemoteIP | is_not_null | field:"RemoteIP" kind:is_not_null | |
TI_ipEntity | in |
| field:"TI_ipEntity" kind:in value:"IPs" |
ValidUntil | is_null | field:"ValidUntil" kind:is_null | |
isPrivate | ne |
| field:"isPrivate" kind:ne value:"true" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Description | contains | State: falsepos; | excludes:Description field:"Description" value:"State: falsepos;" |
Description | contains | State: inactive; | excludes:Description field:"Description" value:"State: inactive;" |
ActionType | match | ConnectionFailed | excludes:ActionType field:"ActionType" value:"ConnectionFailed" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceName | summarize |
Id | summarize |
TI_ipEntity | summarize |
TimeGenerated | summarize |
Name | extend |
UPNSuffix | extend |
timestamp | extend |
TI Map IP entity to OfficeActivity
#This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in OfficeActivity.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: f50280e5-5eb1-4e95-99fd-9d584a987bdd
name: TI Map IP entity to OfficeActivity
description: |
This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in OfficeActivity.
severity: Medium
requiredDataConnectors:
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelIndicators
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelIndicators
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelIndicators
- connectorId: Office365
dataTypes:
- OfficeActivity
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
query: |
let dt_lookBack = 1h; // Look back 1 hour for OfficeActivity events
let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
let OfficeActivity_ = materialize(OfficeActivity
| where isnotempty(ClientIP)
| where TimeGenerated >= ago(dt_lookBack)
| extend ClientIPValues = extract_all(@'\[?(::ffff:)?(?P<IPAddress>(\d+\.\d+\.\d+\.\d+)|[^\]%]+)(%\d+)?\]?([-:](?P<Port>\d+))?', dynamic(["IPAddress", "Port"]), ClientIP)[0]
| extend IPAddress = iff(array_length(ClientIPValues) > 0, tostring(ClientIPValues[0]), '')
| project-rename OfficeActivity_TimeGenerated = TimeGenerated);
let ActivityIPs = OfficeActivity_ | summarize IPs = make_list(IPAddress);
// Fetch threat intelligence indicators related to IP addresses
let IP_Indicators = materialize(ThreatIntelIndicators
//extract key part of kv pair
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
| where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")
| extend NetworkSourceIP = toupper(ObservableValue)
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
| where TimeGenerated >= ago(ioc_lookBack)
| extend TI_ipEntity = NetworkSourceIP
| where TI_ipEntity in (ActivityIPs)
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
| 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;");
IP_Indicators
| project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity
// 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 (OfficeActivity_)
on $left.TI_ipEntity == $right.IPAddress
// Filter out OfficeActivity events that occurred after the expiration of the corresponding indicator
| where OfficeActivity_TimeGenerated < ValidUntil
// Group the results by IndicatorId and keep the OfficeActivity event with the latest timestamp
| summarize OfficeActivity_TimeGenerated = arg_max(OfficeActivity_TimeGenerated, *) by Id
// Select the desired output fields
| 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, TI_ipEntity, ClientIP, UserId, Operation, ResultStatus, RecordType, OfficeObjectId, NetworkSourceIP, Type, Url
| extend timestamp = OfficeActivity_TimeGenerated, Name = tostring(split(UserId, '@', 0)[0]), UPNSuffix = tostring(split(UserId, '@', 1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserId
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: TI_ipEntity
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
version: 1.4.8
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let IP_Indicators is inlined into the numbered stages below.
Let Binding: OfficeActivity_
let OfficeActivity_ = materialize(OfficeActivity
| where isnotempty(ClientIP)
| where TimeGenerated >= ago(dt_lookBack)
| extend ClientIPValues = extract_all(@'\[?(::ffff:)?(?P<IPAddress>(\d+\.\d+\.\d+\.\d+)|[^\]%]+)(%\d+)?\]?([-:](?P<Port>\d+))?', dynamic(["IPAddress", "Port"]), ClientIP)[0]
| extend IPAddress = iff(array_length(ClientIPValues) > 0, tostring(ClientIPValues[0]), '')
| project-rename OfficeActivity_TimeGenerated = TimeGenerated);
Let Binding: ActivityIPs
let ActivityIPs = OfficeActivity_ | summarize IPs = make_list(IPAddress);
Stages 1 to 13 define let IP_Indicators as the rule's main pipeline source. Stages 14 to 21 use it.
Stage 1: source
ThreatIntelIndicators
Stage 2: extend
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
Stage 3: where
| where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")
Stage 4: extend
| extend NetworkSourceIP = toupper(ObservableValue)
Stage 5: extend
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
Stage 6: where
| where TimeGenerated >= ago(ioc_lookBack)
Stage 7: extend
| extend TI_ipEntity = NetworkSourceIP
Stage 8: where
| where TI_ipEntity in (ActivityIPs)
Stage 9: extend
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
Url =ObservableKey == "url:value"ObservableValue""Stage 10: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
Stage 11: where
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
Stage 12: extend
| extend Description = tostring(parse_json(Data).description)
Stage 13: where
| where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"
Stage 14: project-reorder
IP_Indicators
| project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity
Stage 15: join
| join kind=innerunique (OfficeActivity_)
on $left.TI_ipEntity == $right.IPAddress
Stage 16: where
where OfficeActivity_TimeGenerated < ValidUntil
Stage 17: summarize
summarize OfficeActivity_TimeGenerated by Id
Stage 18: extend
extend Description
Stage 19: extend
extend ActivityGroupNames
Stage 20: project
project ActivityGroupNames, ClientIP, Confidence, Description, Id, NetworkSourceIP, OfficeActivity_TimeGenerated, OfficeObjectId, Operation, RecordType, ResultStatus, TI_ipEntity, Type, Url, UserId, ValidUntil
Stage 21: extend
extend Name, UPNSuffix, timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ClientIP | is_not_null | field:"ClientIP" kind:is_not_null | |
IndicatorType | in |
| field:"IndicatorType" kind:in |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
TI_ipEntity | in |
| field:"TI_ipEntity" kind:in value:"ActivityIPs" |
ValidUntil | is_null | field:"ValidUntil" kind:is_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Description | contains | State: falsepos; | excludes:Description field:"Description" value:"State: falsepos;" |
Description | contains | State: inactive; | excludes:Description field:"Description" value:"State: inactive;" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
ClientIP | project |
Confidence | project |
Description | project |
Id | project |
NetworkSourceIP | project |
OfficeActivity_TimeGenerated | project |
OfficeObjectId | project |
Operation | project |
RecordType | project |
ResultStatus | project |
TI_ipEntity | project |
Type | project |
Url | project |
UserId | project |
ValidUntil | project |
Name | extend |
UPNSuffix | extend |
timestamp | extend |
User account created without expected attributes defined
#This query looks for accounts being created that do not have attributes populated that are commonly populated in the tenant. Attackers may attempt to add accounts as a means of establishing persistant access to an environment, looking for anomalies in created accounts may help identify illegitimately created accounts. Created accounts should be investigated to ensure they were legitimated created. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#accounts-not-following-naming-policies
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Authentication Method Changed for Privileged Account (Kusto)
- Account Created and Deleted in Short Timeframe (Kusto)
- Account Created And Deleted Within A Close Time Frame (Sigma)
- Account created from non-approved sources (Kusto)
- Account created or deleted by non-approved user (Kusto)
- Addition of a Temporary Access Pass to a Privileged Account (Kusto)
- Authentication Method Changed for Privileged Account (Kusto)
- Authentication Methods Changed for Privileged Account (Kusto)
Rule Body
id: dc99e38c-f4e9-4837-94d7-353ac0b01a77
name: User account created without expected attributes defined
description: |
'This query looks for accounts being created that do not have attributes populated that are commonly populated in the tenant.
Attackers may attempt to add accounts as a means of establishing persistant access to an environment, looking for anomalies in created accounts may help identify illegitimately created accounts.
Created accounts should be investigated to ensure they were legitimated created.
Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#accounts-not-following-naming-policies'
severity: Low
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
relevantTechniques:
- T1136.003
tags:
- AADSecOpsGuide
query: |
let threshold = 10;
let default_ad_attributes = dynamic(["LastDirSyncTime", "StsRefreshTokensValidFrom", "Included Updated Properties", "AccountEnabled", "Action Client Name", "SourceAnchor"]);
let addUsers = AuditLogs
| where OperationName =~ "Add user"
| where Result =~ "success"
| extend AccountProperties = TargetResources[0].modifiedProperties
| mv-expand AccountProperties
;
addUsers
| evaluate bag_unpack(AccountProperties) : (displayName:string, oldValue: string, newValue: string , TenantId : string, SourceSystem : string, TimeGenerated : datetime, ResourceId : string, OperationName : string, OperationVersion : string, Category : string, ResultType : string, ResultSignature : string, ResultDescription : string, DurationMs : long, CorrelationId : string, Resource : string, ResourceGroup : string, ResourceProvider : string, Identity : string, Level : string, Location : string, AdditionalDetails : dynamic, Id : string, InitiatedBy : dynamic, LoggedByService : string, Result : string, ResultReason : string, TargetResources : dynamic, AADTenantId : string, ActivityDisplayName : string, ActivityDateTime : datetime, AADOperationType : string, Type : string)
| extend displayName = column_ifexists("displayName", "Unknown Value")
| summarize count() by displayName, TenantId
| where displayName !in (default_ad_attributes)
| top threshold by count_ desc
| summarize make_set(displayName) by TenantId
| join kind=inner (
addUsers
| extend CreatingUserPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend CreatingAadUserId = tostring(InitiatedBy.user.id)
| extend CreatingUserIPAddress = tostring(InitiatedBy.user.ipAddress)
| extend CreatedUserPrincipalName = tostring(TargetResources[0].userPrincipalName)
| extend PropName = tostring(AccountProperties.displayName))
on TenantId
| summarize makeset(PropName) by TimeGenerated, CorrelationId, CreatedUserPrincipalName, CreatingUserPrincipalName, CreatingAadUserId, CreatingUserIPAddress, tostring(set_displayName)
| extend missing_props = set_difference(todynamic(set_displayName), set_PropName)
| where array_length(missing_props) > 0
| join kind=innerunique (
AuditLogs
| where Result =~ "success"
| where OperationName =~ "Add user"
| extend CreatedUserPrincipalName = tostring(TargetResources[0].userPrincipalName))
on CorrelationId, CreatedUserPrincipalName
| extend ExpectedProperties = set_displayName
| project-away set_displayName, set_PropName
| extend InitiatingAccountName = tostring(split(CreatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(CreatingUserPrincipalName, "@")[1])
| extend TargetAccountName = tostring(split(CreatedUserPrincipalName, "@")[0]), TargetAccountUPNSuffix = tostring(split(CreatedUserPrincipalName, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: CreatingUserPrincipalName
- identifier: Name
columnName: InitiatingAccountName
- identifier: UPNSuffix
columnName: InitiatingAccountUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: CreatingAadUserId
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: CreatedUserPrincipalName
- identifier: Name
columnName: TargetAccountName
- identifier: UPNSuffix
columnName: TargetAccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: CreatingUserIPAddress
version: 1.1.0
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others" ]
Stages and Predicates
Parameters
let threshold = 10;
let default_ad_attributes = dynamic(["LastDirSyncTime", "StsRefreshTokensValidFrom", "Included Updated Properties", "AccountEnabled", "Action Client Name", "SourceAnchor"]);
let addUsers is inlined into the numbered stages below.
Stages 1 to 5 define let addUsers as the rule's main pipeline source. Stages 6 to 20 use it.
Stage 1: source
AuditLogs
Stage 2: where
| where OperationName =~ "Add user"
Stage 3: where
| where Result =~ "success"
Stage 4: extend
| extend AccountProperties = TargetResources[0].modifiedProperties
Stage 5: mv-expand
| mv-expand AccountProperties
Stage 6: evaluate
addUsers
| evaluate bag_unpack(AccountProperties) : (displayName:string, oldValue: string, newValue: string , TenantId : string, SourceSystem : string, TimeGenerated : datetime, ResourceId : string, OperationName : string, OperationVersion : string, Category : string, ResultType : string, ResultSignature : string, ResultDescription : string, DurationMs : long, CorrelationId : string, Resource : string, ResourceGroup : string, ResourceProvider : string, Identity : string, Level : string, Location : string, AdditionalDetails : dynamic, Id : string, InitiatedBy : dynamic, LoggedByService : string, Result : string, ResultReason : string, TargetResources : dynamic, AADTenantId : string, ActivityDisplayName : string, ActivityDateTime : datetime, AADOperationType : string, Type : string)
Stage 7: extend
| extend displayName = column_ifexists("displayName", "Unknown Value")
Stage 8: summarize
| summarize count() by displayName, TenantId
Stage 9: where
| where displayName !in (default_ad_attributes)
Stage 10: top
| top threshold by count_ desc
Stage 11: summarize
| summarize make_set(displayName) by TenantId
Stage 12: join
| join kind=inner (
addUsers
| extend CreatingUserPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend CreatingAadUserId = tostring(InitiatedBy.user.id)
| extend CreatingUserIPAddress = tostring(InitiatedBy.user.ipAddress)
| extend CreatedUserPrincipalName = tostring(TargetResources[0].userPrincipalName)
| extend PropName = tostring(AccountProperties.displayName))
on TenantId
Stage 13: summarize
summarize by TimeGenerated, CorrelationId, CreatedUserPrincipalName, CreatingUserPrincipalName, CreatingAadUserId, CreatingUserIPAddress
Stage 14: extend
extend missing_props
Stage 15: where
where missing_props > 0
Stage 16: join
| join kind=innerunique (
AuditLogs
| where Result =~ "success"
| where OperationName =~ "Add user"
| extend CreatedUserPrincipalName = tostring(TargetResources[0].userPrincipalName))
on CorrelationId, CreatedUserPrincipalName
Stage 17: extend
extend ExpectedProperties
Stage 18: project-away
project-away set_PropName, set_displayName
Stage 19: extend
extend InitiatingAccountName, InitiatingAccountUPNSuffix
Stage 20: extend
extend TargetAccountName, TargetAccountUPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | eq |
| field:"OperationName" kind:eq value:"Add user" |
Result | eq |
| field:"Result" kind:eq value:"success" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
displayName | in | AccountEnabled, Action Client Name, Included Updated Properties, LastDirSyncTime, SourceAnchor, StsRefreshTokensValidFrom | excludes:displayName |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CorrelationId | summarize |
CreatedUserPrincipalName | summarize |
CreatingAadUserId | summarize |
CreatingUserIPAddress | summarize |
CreatingUserPrincipalName | summarize |
TimeGenerated | summarize |
missing_props | extend |
ExpectedProperties | extend |
InitiatingAccountName | extend |
InitiatingAccountUPNSuffix | extend |
TargetAccountName | extend |
TargetAccountUPNSuffix | extend |
VMware ESXi - New VM started
#'Detects when new VM was started.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 0f4a80de-344f-47c0-bc19-cb120c59b6f0
name: VMware ESXi - New VM started
description: |
'Detects when new VM was started.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SyslogAma
datatypes:
- Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |
let a_vm =
VMwareESXi
| where SyslogMessage has ('VmPoweredOnEvent')
| extend DstHostname = extract(@'\[\d+\]\s+\[(.*?)\s+on', 1, SyslogMessage)
| summarize vm_l = makeset(DstHostname)
| extend k=1;
VMwareESXi
| where SyslogMessage has ('VmPoweredOnEvent')
| extend DstHostname = extract(@'\[\d+\]\s+\[(.*?)\s+on', 1, SyslogMessage)
| extend k = 1
| join (a_vm) on k
| where vm_l !has DstHostname
| extend HostCustomEntity = DstHostname
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: HostCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Let Binding: a_vm
let a_vm = VMwareESXi
| where SyslogMessage has ('VmPoweredOnEvent')
| extend DstHostname = extract(@'\[\d+\]\s+\[(.*?)\s+on', 1, SyslogMessage)
| summarize vm_l = makeset(DstHostname)
| extend k=1;
Stage 1: source
VMwareESXi
Stage 2: where
| where SyslogMessage has ('VmPoweredOnEvent')
Stage 3: extend
| extend DstHostname = extract(@'\[\d+\]\s+\[(.*?)\s+on', 1, SyslogMessage)
Stage 4: extend
| extend k = 1
Stage 5: join
| join (a_vm) on k
Stage 6: where
where vm_l =~ DstHostname
Stage 7: extend
extend HostCustomEntity
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
SyslogMessage | match |
| field:"SyslogMessage" kind:match value:"VmPoweredOnEvent" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DstHostname | extend |
k | extend |
HostCustomEntity | extend |
VMware ESXi - Unexpected disk image
#'Detects unexpected disk image for VM.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: 395c5560-ddc2-45b2-aafe-2e3f64528d3d
name: VMware ESXi - Unexpected disk image
description: |
'Detects unexpected disk image for VM.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SyslogAma
datatypes:
- Syslog
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1496
query: |
let img_ =
VMwareESXi
| where SyslogMessage has ('Matched discovered VM')
| extend DstHostname = extract(@'vim.VirtualMachine\S+,(.*?)\]', 1, SyslogMessage)
| extend kImageName = extract(@'ds:///vmfs/volumes/(.*)/(.*?),', 2, SyslogMessage)
| summarize img_lst = makeset(kImageName) by DstHostname;
VMwareESXi
| where SyslogMessage has ('Matched discovered VM')
| extend DstHostname = extract(@'vim.VirtualMachine\S+,(.*?)\]', 1, SyslogMessage)
| extend ImageName = extract(@'ds:///vmfs/volumes/(.*)/(.*?),', 2, SyslogMessage)
| join (img_) on DstHostname
| where img_lst !has ImageName
| extend HostCustomEntity = DstHostname
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: HostCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Let Binding: img_
let img_ = VMwareESXi
| where SyslogMessage has ('Matched discovered VM')
| extend DstHostname = extract(@'vim.VirtualMachine\S+,(.*?)\]', 1, SyslogMessage)
| extend kImageName = extract(@'ds:///vmfs/volumes/(.*)/(.*?),', 2, SyslogMessage)
| summarize img_lst = makeset(kImageName) by DstHostname;
Stage 1: source
VMwareESXi
Stage 2: where
| where SyslogMessage has ('Matched discovered VM')
Stage 3: extend
| extend DstHostname = extract(@'vim.VirtualMachine\S+,(.*?)\]', 1, SyslogMessage)
Stage 4: extend
| extend ImageName = extract(@'ds:///vmfs/volumes/(.*)/(.*?),', 2, SyslogMessage)
Stage 5: join
| join (img_) on DstHostname
Stage 6: where
where img_lst =~ ImageName
Stage 7: extend
extend HostCustomEntity
Stage 8: summarize aggregation inside the join branch
summarize by DstHostname
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
SyslogMessage | match |
| field:"SyslogMessage" kind:match value:"Matched discovered VM" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DstHostname | summarize |