Detection rules › Kusto

Kusto rules: 2d

Azure Diagnostic settings removed from a resource

#
Severity
medium
Time window
2h
Group by
$left.MonitoredResourcePath, $right._ResourceId, CorrelationId, OperationNameValue, _ResourceId
Author
KennethMLdk
Source
github.com/Azure/Azure-Sentinel

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

Telemetry Coverage

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.

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AadUserIdproject
ActivityStatusValueproject
Callerproject
CallerIpAddressproject
CorrelationIdproject
MonitoredResourcePathproject
Nameproject
OperationNameproject
OperationNameValueproject
Propertiesproject
Resourceproject
ResourceGroupproject
TimeGeneratedproject
UPNSuffixproject
_ResourceIdproject

Best Practice Compliance Check Not Passed

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

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.

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.

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

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.

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

Cisco SEG - Unexpected link

#

Cloudflare - Multiple user agents for single source

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

'Detects requests with different user agents from one source in short timeframe.'

MITRE ATT&CK Coverage

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.

FieldKindValuesSearch
HttpUserAgentOriginalis_not_null
  • (no value, null check)
field:"HttpUserAgentOriginal" kind:is_not_null
d_uagt
  • 10 transforms: number
field:"d_ua" kind:gt value:"10"

Output Fields

These fields are emitted when the rule matches.

FieldSource
SrcIpAddrsummarize
d_uasummarize

Cortex XDR Incident - Low

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

'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.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Statuscontainsresolved_autoexcludes:Status field:"Status" value:"resolved_auto"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertCategoriesproject
AlertCountproject
CreationTimeproject
Descriptionproject
HostCountproject
Hosts_sproject
IncidentIdproject
IncidentSourcesproject
ModificationTimeproject
Severityproject
Statusproject
TimeGeneratedproject
UserCountproject
Users_sproject
XdrUrlproject

Detecting Macro Invoking ShellBrowserWindow COM Objects

#
Status
available
Severity
medium
Time window
1h
Group by
CommandLine, Computer, Description, EventID, Image, ParentCommandLine, ParentImage, ParentProcessGuid, ParentUser, ProcessGuid, User
Source
github.com/Azure/Azure-Sentinel

This query detects a macro invoking ShellBrowserWindow COM Objects evade naive parent/child Office detection rules.

MITRE ATT&CK Coverage

Telemetry Coverage

ProviderRecord / event type
SysmonEvent 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.

FieldKindValuesSearch
CommandLinematch
  • {c08afd90-f2a1-11d1-8455-00a0c91f3880} transforms: term
field:"CommandLine" kind:match value:"{c08afd90-f2a1-11d1-8455-00a0c91f3880}"
EventIDeq
  • 1 corpus 12 (kusto 11, elastic 1)
field:"EventID" kind:eq value:"1"
EventLogeq
  • Microsoft-Windows-Sysmon/Operational corpus 7 (kusto 7)
field:"EventLog" kind:eq value:"Microsoft-Windows-Sysmon/Operational"
Imagematch
  • rundll32.exe transforms: term
field:"Image" kind:match value:"rundll32.exe"
ParentImagematch
  • svchost.exe transforms: term
field:"ParentImage" kind:match value:"svchost.exe"

Output Fields

These fields are emitted when the rule matches.

FieldSource
CommandLinesummarize
Computersummarize
Descriptionsummarize
EndTimesummarize
EventIDsummarize
Imagesummarize
ParentCommandLinesummarize
ParentImagesummarize
ParentProcessGuidsummarize
ParentUsersummarize
ProcessGuidsummarize
StartTimesummarize
Usersummarize
DnsDomainextend
HostNameextend

GCP Security Command Center - Detect Open/Unrestricted API Keys

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

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

TacticTechniques
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.

FieldKindValuesSearch
FindingCategoryeq
  • API_KEY_APIS_UNRESTRICTED
field:"FindingCategory" kind:eq value:"API_KEY_APIS_UNRESTRICTED"
Findings.stateeq
  • ACTIVE transforms: tostring, kusto_dynamic_path
field:"Findings.state" kind:eq value:"ACTIVE"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Descriptionproject
ExternalUriproject
ProjectIdproject
ProjectNameproject
ResourceNameproject
Severityproject
TimeGeneratedproject

General Settings Updated

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

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
UserNameproject

Guardian- Invisible Text Policy Violation Detection

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

'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.

Malware in the recycle bin (Normalized Process Events)

#
Severity
medium
Time window
1d
Author
Yuval Naor
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
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.

FieldKindValuesSearch
CommandLinematch
  • recycler transforms: term
field:"CommandLine" kind:match value:"recycler"
FileNamein
  • cmd.exe
  • ftp.exe
  • msiexec.exe
  • powershell.exe
  • regsvr32.exe
  • rundll32.exe
  • schtasks.exe
field:"FileName" kind:in
Processmatch
  • cmd.exe transforms: term
  • ftp.exe transforms: term
  • msiexec.exe transforms: term
  • powershell.exe transforms: term corpus 2 (kusto 2)
  • regsvr32.exe transforms: term
  • rundll32.exe transforms: term
  • schtasks.exe transforms: term
field:"Process" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActingProcessNameproject
CommandLineproject
Dvcproject
EventProductproject
EventVendorproject
FileNameproject
Processproject
TimeGeneratedproject
Userproject
AccountNTDomainextend
AccountNameextend
HostNameextend
HostNameDomainextend

Objects for Job Deleted

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

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
Job Nameproject
MessageDetailsproject
Severityproject
User Nameproject

Power Platform - Possibly compromised user accesses Power Platform services

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

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

TacticTechniques
Initial Access
Lateral Movement

Telemetry Coverage

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 =
ifAppId =~ ppac_appid"Power Platform Admin Center"
elifAppId =~ power_apps_appid"Power Apps"
elifAppId =~ power_automate_appid"Power Automate"
else"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 =
ifAffectedPlatform == "Power Apps"int(27593)
elifAffectedPlatform == "Power Automate"int(27592)
else0

Stage 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.

FieldKindValuesSearch
AppIdin
  • 065d9450-1e87-434e-ac2f-69af271549ed
  • 6204c1d1-4712-4c46-a7d9-3ed63d992682
  • a8f7a65c-f5ba-4859-b2d6-df772c264e9d
field:"AppId" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
AffectedPlatformproject
AppDisplayNameproject
AppIdproject
CloudAppIdproject
IPAddressproject
Identityproject
RiskEventTypesproject
RiskEventTypes_V2project
Severityproject
TimeGeneratedproject
UPNSuffixproject
UniqueTokenIdentifierproject
UserIdproject
UserPrincipalNameproject

RecordedFuture Threat Hunting IP All Actors

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

Recorded Future Threat Hunting IP correlation for all actors.

MITRE ATT&CK Coverage

TacticTechniques
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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Idsummarize

Risky user signin observed in non-Microsoft network device

#
Severity
medium
Time window
1d
Group by
ConditionalAccessStatus, IPAddress, IsRisky, ResourceDisplayName, ResultType, RiskDetail, RiskLevelAggregated, RiskLevelDuringSignIn, RiskState, SourceSystem, UserPrincipalName
Author
Arjun Trivedi
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
Command & Control

Telemetry Coverage

PlatformRecord / event type
Entra IDSigninLogs sign-in event: Successful risky sign-in
Microsoft SentinelCommonSecurityLog CEF event: Allowed third-party network activity

Rules Detecting the Same Action

These rules filter on the same operation.

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.

FieldKindValuesSearch
DeviceActionne
  • Block
field:"DeviceAction" kind:ne value:"Block"
DeviceProductstarts_with
  • FireWall
  • FortiGate
  • NSSWeblog
  • PAN
  • URL
  • VPN
field:"DeviceProduct" kind:starts_with
DeviceVendormatch
  • Check Point transforms: term
  • Fortinet transforms: term
  • Palo Alto Networks transforms: term
  • Zscaler transforms: term
field:"DeviceVendor" kind:match
RequestURLis_not_null
  • (no value, null check)
field:"RequestURL" kind:is_not_null
ResultTypeeq
  • 0 corpus 8 (kusto 8)
field:"ResultType" kind:eq value:"0"
RiskStateeq
  • atRisk corpus 2 (kusto 2)
field:"RiskState" kind:eq value:"atRisk"
SourceUserNameis_not_null
  • (no value, null check)
field:"SourceUserName" kind:is_not_null
Timediffge
  • 0 transforms: number
field:"Timediff" kind:ge value:"0"
Timediffle
  • 1 transforms: number
field:"Timediff" kind:le value:"1"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AppDisplayNamesummarize
ClientAppUsedsummarize
ConditionalAccessStatussummarize
CorrelationIdsummarize
IPAddresssummarize
IsRiskysummarize
ResourceDisplayNamesummarize
ResultTypesummarize
RiskDetailsummarize
RiskEventTypessummarize
RiskLevelAggregatedsummarize
RiskLevelDuringSignInsummarize
RiskStatesummarize
Signin_Timesummarize
SourceSystemsummarize
UserAgentsummarize
UserPrincipalNamesummarize
Timediffextend

Rouge RDP: Suspicious File Creation

#
Group by
DeviceId, InitiatingProcessFileName
Author
Cyb3rMonk
Source
github.com/Cyb3r-Monk/Threat-Hunting-and-Detection

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

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.

FieldKindValuesSearch
ActionTypein
  • ConnectionSuccess corpus 4 (kusto 4)
  • FileCreated
  • FileModified
field:"ActionType" kind:in
FileNamematch
  • .bat transforms: term
  • .cmd transforms: term
  • .com transforms: term
  • .cpl transforms: term
  • .dll transforms: term
  • .exe transforms: term
  • .hta transforms: term
  • .js transforms: term
  • .jse transforms: term
  • .lnk transforms: term
  • .pif transforms: term
  • .png transforms: term
  • .svg transforms: term
  • .url transforms: term
  • .vbe transforms: term
  • .vbs transforms: term
  • .wsh transforms: term
field:"FileName" kind:match
InitiatingProcessFileNameeq
  • mstsc.exe corpus 8 (elastic 5, splunk 2, kusto 1)
  • mstsc.exe
field:"process_basename" kind:eq
RemoteIPTypeeq
  • Public corpus 2 (kusto 2)
field:"RemoteIPType" kind:eq value:"Public"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceIdproject
DeviceNameproject
InitiatingProcessFileNameproject
InitiatingProcessIdproject
NetworkTimestampproject
RemoteIPproject
RemotePortproject
RemoteUrlproject
Timestampproject

SFTP File transfer above threshold

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

'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

TacticTechniques
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 =
iffileslist == 1tostring(fileslist[0])
elsestrcat("SeeFilesListField", "_", tostring(hash(tostring(fileslist))))

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ProcessNamematch
  • sftp transforms: term
field:"process_name" kind:match value:"sftp"
SyslogMessagematch
  • bytes read transforms: term
  • close transforms: term
  • session opened for transforms: term
field:"SyslogMessage" kind:match
count_distinct_filepathge
  • 50 transforms: number
field:"count_distinct_filepath" kind:ge value:"50"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Computersummarize
count_distinct_filepathsummarize
fileslistsummarize
src_ipsummarize
usernamesummarize
FileSampleextend

SUNBURST and SUPERNOVA backdoor hashes (Normalized File Events)

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

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

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 used in Stage 2

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.

FieldKindValuesSearch
TargetFileMD5in
  • 02af7cec58b9a5da1c542b5a32151ba1 corpus 2 (kusto 2)
  • 2c4a910a1299cdae2a4e55988a2f102e corpus 2 (kusto 2)
  • 4f2eb62fa529c0283b28d05ddd311fae corpus 2 (kusto 2)
  • 56ceb6d0011d87b6e4d7023d7ef85676 corpus 2 (kusto 2)
  • 846e27a652a5e1bfbd0ddd38a16dc865 corpus 2 (kusto 2)
  • b91ce2fa41029f6955bff20079468448 corpus 2 (kusto 2)
field:"md5" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNTDomainextend
AccountNameextend
AlgorithmTypeextend

TI Map IP entity to AppServiceHTTPLogs

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

Identifies a match in AppServiceHTTPLogs from any IP IOC from TI

MITRE ATT&CK Coverage

TacticTechniques
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 =
ifisnotempty(NetworkSourceIP)NetworkSourceIP
elseNetworkSourceIP

Stage 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.

FieldKindValuesSearch
CIpis_not_null
  • (no value, null check)
field:"CIp" kind:is_not_null
Confidencegt
  • 50 transforms: number
field:"Confidence" kind:gt value:"50"
IndicatorTypein
  • ipv4-addr
  • ipv6-addr
  • network-traffic
field:"IndicatorType" kind:in
IsActiveeq
  • true transforms: boolean
field:"IsActive" kind:eq value:"true"
ValidUntilis_null
  • (no value, null check)
field:"ValidUntil" kind:is_null

Exclusions

The rule actively suppresses these predicates.

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

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
AppService_TimeGeneratedproject
CIpproject
Confidenceproject
CsHostproject
CsUsernameproject
Descriptionproject
Idproject
NetworkSourceIPproject
Severityproject
TI_ipEntityproject
Typeproject
Urlproject
ValidUntilproject
WebAppproject
_ResourceIdproject
DnsDomainextend
HostNameextend
timestampextend

TI Map IP entity to AzureNetworkAnalytics_CL (NSG Flow Logs)

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

Identifies a match in AzureNetworkAnalytics_CL (NSG Flow Logs) from any IP IOC from TI that was Allowed

MITRE ATT&CK Coverage

TacticTechniques
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 =
ifisnotempty(NetworkSourceIP)NetworkSourceIP
elseNetworkSourceIP

Stage 8: extend

| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
TI_ipEntity =
ifisempty(TI_ipEntity) and isnotempty(NetworkSourceIP)NetworkSourceIP
elseTI_ipEntity

Stage 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.

Exclusions

The rule actively suppresses these predicates.

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

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
AzureNetworkAnalytics_CL_TimeGeneratedproject
Computerproject
Confidenceproject
Descriptionproject
DestPort_dproject
DestPublicIPs_sproject
FlowDirection_sproject
FlowStatus_sproject
FlowType_sproject
Idproject
L7Protocol_sproject
NetworkSourceIPproject
PublicIPs_sproject
SrcPublicIPs_sproject
TI_ipEntityproject
Typeproject
ValidUntilproject
DnsDomainextend
HostNameextend
timestampextend

TI Map IP Entity to DeviceNetworkEvents

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

Identifies a match in DeviceNetworkEvents Event data from any IP Indicator from TI.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Telemetry Coverage

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_ used in Stages 1, 15

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 used in Stages 2, 10

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.

Exclusions

The rule actively suppresses these predicates.

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

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceNamesummarize
Idsummarize
TI_ipEntitysummarize
TimeGeneratedsummarize
Nameextend
UPNSuffixextend
timestampextend

TI Map IP entity to OfficeActivity

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

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

MITRE ATT&CK Coverage

TacticTechniques
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_ used in Stage 15

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 used in Stage 8

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 =
ifObservableKey == "url:value"ObservableValue
else""

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.

FieldKindValuesSearch
ClientIPis_not_null
  • (no value, null check)
field:"ClientIP" kind:is_not_null
IndicatorTypein
  • ipv4-addr
  • ipv6-addr
  • network-traffic
field:"IndicatorType" kind:in
IsActiveeq
  • true transforms: boolean
field:"IsActive" kind:eq value:"true"
TI_ipEntityin
  • ActivityIPs
field:"TI_ipEntity" kind:in value:"ActivityIPs"
ValidUntilis_null
  • (no value, null check)
field:"ValidUntil" kind:is_null

Exclusions

The rule actively suppresses these predicates.

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

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
ClientIPproject
Confidenceproject
Descriptionproject
Idproject
NetworkSourceIPproject
OfficeActivity_TimeGeneratedproject
OfficeObjectIdproject
Operationproject
RecordTypeproject
ResultStatusproject
TI_ipEntityproject
Typeproject
Urlproject
UserIdproject
ValidUntilproject
Nameextend
UPNSuffixextend
timestampextend

User account created without expected attributes defined

#
Severity
low
Time window
1d
Group by
CorrelationId, CreatedUserPrincipalName, CreatingAadUserId, CreatingUserIPAddress, CreatingUserPrincipalName, TenantId, TimeGenerated, displayName
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
Persistence

Telemetry Coverage

PlatformRecord / event type
AzureAdd user

Rules Detecting the Same Action

These rules filter on the same operation.

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.

FieldKindValuesSearch
OperationNameeq
  • Add user corpus 4 (kusto 4)
field:"OperationName" kind:eq value:"Add user"
Resulteq
  • success corpus 23 (kusto 23)
field:"Result" kind:eq value:"success"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
displayNameinAccountEnabled, Action Client Name, Included Updated Properties, LastDirSyncTime, SourceAnchor, StsRefreshTokensValidFromexcludes:displayName

Output Fields

These fields are emitted when the rule matches.

FieldSource
CorrelationIdsummarize
CreatedUserPrincipalNamesummarize
CreatingAadUserIdsummarize
CreatingUserIPAddresssummarize
CreatingUserPrincipalNamesummarize
TimeGeneratedsummarize
missing_propsextend
ExpectedPropertiesextend
InitiatingAccountNameextend
InitiatingAccountUPNSuffixextend
TargetAccountNameextend
TargetAccountUPNSuffixextend

VMware ESXi - New VM started

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

'Detects when new VM was started.'

MITRE ATT&CK Coverage

TacticTechniques
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 used in Stage 5

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.

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

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstHostnameextend
kextend
HostCustomEntityextend

VMware ESXi - Unexpected disk image

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

'Detects unexpected disk image for VM.'

MITRE ATT&CK Coverage

TacticTechniques
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_ used in Stage 5

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.

FieldKindValuesSearch
SyslogMessagematch
  • Matched discovered VM transforms: term
field:"SyslogMessage" kind:match value:"Matched discovered VM"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstHostnamesummarize

Back to all Kusto rules