Skip to main content

Azure Firewall KQL Queries

When I work with Azure Firewall logs in Log Analytics, the first problem is always the raw AzureDiagnostics table — logs arrive as unstructured message strings that need parsing before they're useful. I built these queries to handle that parsing and surface traffic in a readable, filterable format.

All queries target the AzureDiagnostics table and work with both AzureFirewallNetworkRule and AzureFirewallApplicationRule log categories.

I put these together for a few recurring situations: understanding what's traversing the firewall during a migration, identifying sources of unexpected traffic, auditing overly permissive rules, and building filtered views for security reviews. I've noted when I reach for each one.

Prerequisites

Ensure diagnostic settings on your Azure Firewall are configured to send AzureFirewallNetworkRule and AzureFirewallApplicationRule logs to a Log Analytics workspace.


List Application Gateway Hostnames

I use this Azure Resource Graph query to enumerate all hostnames configured across Application Gateway HTTP listeners in my environment. Run it in the Azure Resource Graph Explorer — not Log Analytics.

Resources
| where type == "microsoft.network/applicationgateways"
| where subscriptionId in~ (
"<subscription-id-1>",
"<subscription-id-2>",
"<subscription-id-3>"
)
| extend httpListeners = properties.httpListeners
| mv-expand listener = httpListeners
| mv-expand hostname = listener.properties.hostNames
| project
subscriptionId,
resourceGroup,
name,
listenerName = listener.name,
hostname = tostring(hostname),
location
| distinct hostname

Replace <subscription-id-1> etc. with your actual subscription IDs, or remove the where subscriptionId in~ filter to query all subscriptions in your tenant.


List Distinct Source IPs

My starting point when I don't know what's in the environment — this gives me every unique source IP seen in the logs over the selected time range. Good for building a filter list or spotting addresses that shouldn't be there.

AzureDiagnostics
| where Category in ("AzureFirewallNetworkRule", "AzureFirewallApplicationRule")
| extend msg_original = msg_s

// Normalize common message patterns
| extend msg_s = replace_regex(msg_s, @'\. Action: Deny\. Reason: SNI TLS extension was missing\.',
' to no_data:no_data. Action: Deny. Rule Collection: default behavior. Rule: SNI TLS extension missing')
| extend msg_s = replace_regex(msg_s, @'No rule matched\. Proceeding with default action',
'Rule Collection: default behavior. Rule: no rule matched')

// Extract Web Category if present
| extend WebCategory = extract(@"Web Category: ([^.]+)", 1, msg_s)
| extend msg_s = replace_regex(msg_s, @'\. Web Category:[^.]*', '')

// Extract Rule Collection and Rule
| extend RuleCollection_temp = extract(@"Rule Collection: ([^.]+)", 1, msg_s)
| extend Rule = extract(@"Rule Collection: [^.]+\. Rule: ([^.]+)", 1, msg_s)
| extend msg_s = replace_regex(msg_s, @'\. Rule Collection:[^.]*', '')

// Extract Rule Collection Group
| extend RuleCollectionGroup = extract(@"Rule Collection Group: ([^.]+)", 1, msg_s)
| extend msg_s = replace_regex(msg_s, @'\. Rule Collection Group:[^.]*', '')

// Extract Policy
| extend Policy_temp = extract(@"Policy: ([^.]+)", 1, msg_s)
| extend msg_s = replace_regex(msg_s, @'\. Policy:[^.]*', '')

// Parse the main traffic pattern
| parse msg_s with Protocol " request from " SourceAddress " to " TargetAddress ". Action: " ActionTemp

// Parse source IP and port
| extend SourceIP = case(
SourceAddress contains ":",
tostring(split(SourceAddress, ":")[0]),
SourceAddress
)
| distinct SourceIP

List Distinct Destinations

Same idea as above, but focused on the destination side. I use this alongside the source IP query to build a quick map of what's talking to what before I dig into the full traffic log.

AzureDiagnostics
| where Category in ("AzureFirewallNetworkRule", "AzureFirewallApplicationRule")
| extend msg_original = msg_s

// Normalize common message patterns
| extend msg_s = replace_regex(msg_s, @'\. Action: Deny\. Reason: SNI TLS extension was missing\.',
' to no_data:no_data. Action: Deny. Rule Collection: default behavior. Rule: SNI TLS extension missing')
| extend msg_s = replace_regex(msg_s, @'No rule matched\. Proceeding with default action',
'Rule Collection: default behavior. Rule: no rule matched')

// Extract Web Category if present
| extend WebCategory = extract(@"Web Category: ([^.]+)", 1, msg_s)
| extend msg_s = replace_regex(msg_s, @'\. Web Category:[^.]*', '')

// Extract Rule Collection and Rule
| extend RuleCollection_temp = extract(@"Rule Collection: ([^.]+)", 1, msg_s)
| extend Rule = extract(@"Rule Collection: [^.]+\. Rule: ([^.]+)", 1, msg_s)
| extend msg_s = replace_regex(msg_s, @'\. Rule Collection:[^.]*', '')

// Extract Rule Collection Group
| extend RuleCollectionGroup = extract(@"Rule Collection Group: ([^.]+)", 1, msg_s)
| extend msg_s = replace_regex(msg_s, @'\. Rule Collection Group:[^.]*', '')

// Extract Policy
| extend Policy_temp = extract(@"Policy: ([^.]+)", 1, msg_s)
| extend msg_s = replace_regex(msg_s, @'\. Policy:[^.]*', '')

// Parse the main traffic pattern
| parse msg_s with Protocol " request from " SourceAddress " to " TargetAddress ". Action: " ActionTemp

// Parse target IP and port
| extend Target = case(
TargetAddress contains ":",
tostring(split(TargetAddress, ":")[0]),
TargetAddress
)
| distinct Target

List Distinct Target Ports

I run this early in a security review to understand what traffic profiles exist — whether I'm seeing expected ports like 443 and 1433 or something unexpected. It's also useful for pre-migration dependency mapping when I need to catalog which ports are actively in use.

AzureDiagnostics
| where Category in ("AzureFirewallNetworkRule", "AzureFirewallApplicationRule")
| extend msg_original = msg_s

// Normalize common message patterns
| extend msg_s = replace_regex(msg_s, @'\. Action: Deny\. Reason: SNI TLS extension was missing\.',
' to no_data:no_data. Action: Deny. Rule Collection: default behavior. Rule: SNI TLS extension missing')
| extend msg_s = replace_regex(msg_s, @'No rule matched\. Proceeding with default action',
'Rule Collection: default behavior. Rule: no rule matched')

// Extract Web Category if present
| extend WebCategory = extract(@"Web Category: ([^.]+)", 1, msg_s)
| extend msg_s = replace_regex(msg_s, @'\. Web Category:[^.]*', '')

// Extract Rule Collection and Rule
| extend RuleCollection_temp = extract(@"Rule Collection: ([^.]+)", 1, msg_s)
| extend Rule = extract(@"Rule Collection: [^.]+\. Rule: ([^.]+)", 1, msg_s)
| extend msg_s = replace_regex(msg_s, @'\. Rule Collection:[^.]*', '')

// Extract Rule Collection Group
| extend RuleCollectionGroup = extract(@"Rule Collection Group: ([^.]+)", 1, msg_s)
| extend msg_s = replace_regex(msg_s, @'\. Rule Collection Group:[^.]*', '')

// Extract Policy
| extend Policy_temp = extract(@"Policy: ([^.]+)", 1, msg_s)
| extend msg_s = replace_regex(msg_s, @'\. Policy:[^.]*', '')

// Parse the main traffic pattern
| parse msg_s with Protocol " request from " SourceAddress " to " TargetAddress ". Action: " ActionTemp

// Parse target port
| extend TargetPort = case(
TargetAddress contains ":" and array_length(split(TargetAddress, ":")) >= 2,
tostring(split(TargetAddress, ":")[1]),
""
)
| distinct TargetPort

Azure Firewall Traffic Log

This is the main query I use for detailed traffic analysis. It parses and normalises the raw log entries into clean columns, and lets me filter by source IP, destination IP, target port, or action. Most of my security review and migration work starts here.

// Optional filters — replace with specific values or leave as empty dynamic arrays to return all traffic
let sourceIPs = dynamic([]); // e.g. dynamic(["10.0.0.1", "10.0.0.2"])
let targetIPs = dynamic([]); // e.g. dynamic(["8.8.8.8", "1.1.1.1"])
let targetPorts = dynamic([]); // e.g. dynamic(["443", "80", "22"])

AzureDiagnostics
| where Category in ("AzureFirewallNetworkRule", "AzureFirewallApplicationRule")
| extend msg_original = msg_s

// Normalize common message patterns
| extend msg_s = replace_regex(msg_s, @'\. Action: Deny\. Reason: SNI TLS extension was missing\.',
' to no_data:no_data. Action: Deny. Rule Collection: default behavior. Rule: SNI TLS extension missing')
| extend msg_s = replace_regex(msg_s, @'No rule matched\. Proceeding with default action',
'Rule Collection: default behavior. Rule: no rule matched')

// Extract Web Category if present
| extend WebCategory = extract(@"Web Category: ([^.]+)", 1, msg_s)
| extend msg_s = replace_regex(msg_s, @'\. Web Category:[^.]*', '')

// Extract Rule Collection and Rule
| extend RuleCollection_temp = extract(@"Rule Collection: ([^.]+)", 1, msg_s)
| extend Rule = extract(@"Rule Collection: [^.]+\. Rule: ([^.]+)", 1, msg_s)
| extend msg_s = replace_regex(msg_s, @'\. Rule Collection:[^.]*', '')

// Extract Rule Collection Group
| extend RuleCollectionGroup = extract(@"Rule Collection Group: ([^.]+)", 1, msg_s)
| extend msg_s = replace_regex(msg_s, @'\. Rule Collection Group:[^.]*', '')

// Extract Policy
| extend Policy_temp = extract(@"Policy: ([^.]+)", 1, msg_s)
| extend msg_s = replace_regex(msg_s, @'\. Policy:[^.]*', '')

// Parse the main traffic pattern
| parse msg_s with Protocol " request from " SourceAddress " to " TargetAddress ". Action: " ActionTemp

// Parse source IP and port
| extend SourceIP = case(
SourceAddress contains ":",
tostring(split(SourceAddress, ":")[0]),
SourceAddress
)
| extend SourcePort = case(
SourceAddress contains ":" and array_length(split(SourceAddress, ":")) >= 2,
tostring(split(SourceAddress, ":")[1]),
""
)

// Parse target IP and port
| extend Target = case(
TargetAddress contains ":",
tostring(split(TargetAddress, ":")[0]),
TargetAddress
)
| extend TargetPort = case(
TargetAddress contains ":" and array_length(split(TargetAddress, ":")) >= 2,
tostring(split(TargetAddress, ":")[1]),
""
)

// Clean up Action field
| extend Action = case(
ActionTemp contains ".",
tostring(split(ActionTemp, ".")[0]),
ActionTemp
)

// Handle Rule Collection format (Policy:RuleCollectionGroup:RuleCollection)
| extend ParsedRuleCollection = split(RuleCollection_temp, ":")
| extend
Policy = coalesce(
case(array_length(ParsedRuleCollection) >= 1, tostring(ParsedRuleCollection[0]), ""),
Policy_temp
),
RuleCollectionGroup = coalesce(
case(array_length(ParsedRuleCollection) >= 2, tostring(ParsedRuleCollection[1]), ""),
RuleCollectionGroup
),
RuleCollection = coalesce(
case(array_length(ParsedRuleCollection) >= 3, tostring(ParsedRuleCollection[2]), ""),
case(array_length(ParsedRuleCollection) == 1, tostring(ParsedRuleCollection[0]), ""),
RuleCollection_temp
)

// Apply filters — only filter if lists are non-empty
| where array_length(sourceIPs) == 0 or SourceIP in (sourceIPs)
| where array_length(targetIPs) == 0 or Target in (targetIPs)
| where array_length(targetPorts) == 0 or TargetPort in (targetPorts)

// Additional filters (uncomment as needed)
// | where Protocol == "TCP"
// | where Action == "Allow"
// | where Action == "Deny"

// Project final columns
| project
TimeGenerated,
Protocol,
SourceIP,
SourcePort = case(SourcePort == "", "N/A", SourcePort),
Target,
TargetPort = case(TargetPort == "", "N/A", TargetPort),
Action,
OperationName,
Policy = case(Policy == "", "N/A", Policy),
RuleCollectionGroup = case(RuleCollectionGroup == "", "N/A", RuleCollectionGroup),
RuleCollection = case(RuleCollection == "", "N/A", RuleCollection),
Rule = case(Rule == "", "N/A", Rule),
WebCategory = case(WebCategory == "", "N/A", WebCategory),
OriginalMessage = msg_original

| order by TimeGenerated desc

Optional aggregations

Uncomment one of the following summarize clauses at the end of the query above to switch from a raw log view to an aggregated view.

// Connection counts over time by port
// | summarize Connections = count() by TargetPort, bin(TimeGenerated, 2m)

// Unique source addresses by target port
// | summarize SourceAddresses = make_set(SourceIP), Connections = count() by TargetPort
// | order by Connections desc

// Top target destinations
// | summarize Connections = count() by Target, TargetPort
// | order by Connections desc
// | take 20

Connections Hitting Permissive Rules

When I'm auditing firewall rules and need to justify retiring a permissive rule collection, this is the query I run. Replace <your-rule-collection-name> with the collection you're investigating. It summarises hits by target port and shows the distinct source and destination sets — which makes it easy to see exactly what depends on that rule before I tighten it.

// Optional filters — replace with specific values or leave as empty dynamic arrays to return all traffic
let sourceIPs = dynamic([]); // e.g. dynamic(["10.0.0.1", "10.0.0.2"])
let targetIPs = dynamic([]); // e.g. dynamic(["8.8.8.8", "1.1.1.1"])
let targetPorts = dynamic([]); // e.g. dynamic(["443", "80", "22"])

AzureDiagnostics
| where Category in ("AzureFirewallNetworkRule", "AzureFirewallApplicationRule")
| extend msg_original = msg_s

// Normalize common message patterns
| extend msg_s = replace_regex(msg_s, @'\. Action: Deny\. Reason: SNI TLS extension was missing\.',
' to no_data:no_data. Action: Deny. Rule Collection: default behavior. Rule: SNI TLS extension missing')
| extend msg_s = replace_regex(msg_s, @'No rule matched\. Proceeding with default action',
'Rule Collection: default behavior. Rule: no rule matched')

// Extract Web Category if present
| extend WebCategory = extract(@"Web Category: ([^.]+)", 1, msg_s)
| extend msg_s = replace_regex(msg_s, @'\. Web Category:[^.]*', '')

// Extract Rule Collection and Rule
| extend RuleCollection_temp = extract(@"Rule Collection: ([^.]+)", 1, msg_s)
| extend Rule = extract(@"Rule Collection: [^.]+\. Rule: ([^.]+)", 1, msg_s)
| extend msg_s = replace_regex(msg_s, @'\. Rule Collection:[^.]*', '')

// Extract Rule Collection Group
| extend RuleCollectionGroup = extract(@"Rule Collection Group: ([^.]+)", 1, msg_s)
| extend msg_s = replace_regex(msg_s, @'\. Rule Collection Group:[^.]*', '')

// Extract Policy
| extend Policy_temp = extract(@"Policy: ([^.]+)", 1, msg_s)
| extend msg_s = replace_regex(msg_s, @'\. Policy:[^.]*', '')

// Parse the main traffic pattern
| parse msg_s with Protocol " request from " SourceAddress " to " TargetAddress ". Action: " ActionTemp

// Parse source IP and port
| extend SourceIP = case(
SourceAddress contains ":",
tostring(split(SourceAddress, ":")[0]),
SourceAddress
)
| extend SourcePort = case(
SourceAddress contains ":" and array_length(split(SourceAddress, ":")) >= 2,
tostring(split(SourceAddress, ":")[1]),
""
)

// Parse target IP and port
| extend Target = case(
TargetAddress contains ":",
tostring(split(TargetAddress, ":")[0]),
TargetAddress
)
| extend TargetPort = case(
TargetAddress contains ":" and array_length(split(TargetAddress, ":")) >= 2,
tostring(split(TargetAddress, ":")[1]),
""
)

// Clean up Action field
| extend Action = case(
ActionTemp contains ".",
tostring(split(ActionTemp, ".")[0]),
ActionTemp
)

// Handle Rule Collection format (Policy:RuleCollectionGroup:RuleCollection)
| extend ParsedRuleCollection = split(RuleCollection_temp, ":")
| extend
Policy = coalesce(
case(array_length(ParsedRuleCollection) >= 1, tostring(ParsedRuleCollection[0]), ""),
Policy_temp
),
RuleCollectionGroup = coalesce(
case(array_length(ParsedRuleCollection) >= 2, tostring(ParsedRuleCollection[1]), ""),
RuleCollectionGroup
),
RuleCollection = coalesce(
case(array_length(ParsedRuleCollection) >= 3, tostring(ParsedRuleCollection[2]), ""),
case(array_length(ParsedRuleCollection) == 1, tostring(ParsedRuleCollection[0]), ""),
RuleCollection_temp
)

// Scope to the target rule collection
| where RuleCollection == "<your-rule-collection-name>"

// Apply filters — only filter if lists are non-empty
| where array_length(sourceIPs) == 0 or SourceIP in (sourceIPs)
| where array_length(targetIPs) == 0 or Target in (targetIPs)
| where array_length(targetPorts) == 0 or TargetPort in (targetPorts)

// Additional filters (uncomment as needed)
// | where Protocol == "TCP"
// | where Action == "Allow"
// | where Action == "Deny"

// Project and summarise
| project
TimeGenerated,
Protocol,
SourceIP,
SourcePort = case(SourcePort == "", "N/A", SourcePort),
Target,
TargetPort = case(TargetPort == "", "N/A", TargetPort),
Action,
OperationName,
Policy = case(Policy == "", "N/A", Policy),
RuleCollectionGroup = case(RuleCollectionGroup == "", "N/A", RuleCollectionGroup),
RuleCollection = case(RuleCollection == "", "N/A", RuleCollection),
Rule = case(Rule == "", "N/A", Rule),
WebCategory = case(WebCategory == "", "N/A", WebCategory),
OriginalMessage = msg_original

| order by TimeGenerated desc
| summarize
Sources = make_set(SourceIP),
Targets = make_set(Target),
Hits = count()
by TargetPort, RuleCollection

I use these results to identify traffic that only gets through because of a permissive rule, then write specific rules for the legitimate flows and retire it.


Search Firewall Rules by IP (Azure Resource Graph)

The queries above work against diagnostic logs -- they show what traffic did traverse the firewall. This next query is different: it searches the firewall policy rules themselves to find which rules would match a given IP. I reach for this when someone asks "is there a rule that allows traffic from X to Y?" and I don't want to click through 200 rules in the portal.

Run this in the Azure Resource Graph Explorer, not Log Analytics. It queries the networkresources table to expand all rule collection groups, rule collections, and individual rules, then joins with the firewall policy to get the firewall name and DNS settings.

networkresources
| where type == "microsoft.network/firewallpolicies/rulecollectiongroups"
| extend policyId = tostring(properties.firewallPolicy.id)
| extend policyName = split(policyId, "/")[-1]
| extend ruleCollectionGroupName = name
| mv-expand ruleCollection = properties.ruleCollections
| extend ruleCollectionName = tostring(ruleCollection.name)
| extend ruleCollectionType = tostring(ruleCollection.ruleCollectionType)
| extend ruleCollectionAction = tostring(ruleCollection.action.type)
| extend ruleCollectionPriority = toint(ruleCollection.priority)
| mv-expand rule = ruleCollection.rules
| extend ruleName = tostring(rule.name)
| extend ruleType = tostring(rule.ruleType)
| where ruleType == "NetworkRule"
| extend protocols = rule.ipProtocols
| extend sourceAddresses = rule.sourceAddresses
| extend sourceIpGroups = rule.sourceIpGroups
| extend destinationAddresses = rule.destinationAddresses
| extend destinationIpGroups = rule.destinationIpGroups
| extend destinationPorts = rule.destinationPorts
| join kind= leftouter (resources
| where type == "microsoft.network/firewallpolicies"
| extend dnsServers = tostring(properties.dnsSettings.servers),
dnsProxyEnabled = tostring(properties.dnsSettings.enableProxy),
ruleCollectionGroups = (properties.ruleCollectionGroups)
| mv-expand ruleCollectionGroups
| extend ruleCollectionGroupId = tostring(ruleCollectionGroups.id)
| extend firewallId = properties.firewalls
| mv-expand firewallId
| extend firewallName = tostring(split(firewallId.id, "/")[8])
| project firewallPolicyId = id, firewallPolicyName = name,
dnsServers, dnsProxyEnabled, ruleCollectionGroupId, firewallName)
on $left.id == $right.ruleCollectionGroupId

// --- Filter by source and/or destination IP ---
// Uncomment and modify based on what you're searching for:

// Search by source IP:
// | where sourceAddresses has ("10.65.10.7")
// or ipv4_is_in_any_range("10.65.10.7", todynamic(sourceAddresses))

// Search by destination IP:
// | where destinationAddresses has ("172.16.128.45")
// or ipv4_is_in_any_range("172.16.128.45", todynamic(destinationAddresses))

// Search by destination port:
// | where tostring(destinationPorts) has "443"

// Search by protocol:
// | where tostring(protocols) has "TCP"

// Combined example (source + destination + port):
// | where (sourceAddresses has_any (dynamic(["10.0.0.100"]))
// or ipv4_is_in_any_range("10.0.0.100", todynamic(sourceAddresses)))
// and (destinationAddresses has_any (dynamic(["192.168.1.10"]))
// or ipv4_is_in_any_range("192.168.1.10", todynamic(destinationAddresses)))
// and tostring(destinationPorts) has "3389"

| project
FirewallName = firewallName,
FirewallPolicy = policyName,
RuleCollectionGroup = ruleCollectionGroupName,
RuleCollection = ruleCollectionName,
RuleCollectionType = ruleCollectionType,
Rule = ruleName,
Protocol = tostring(protocols),
SourceAddresses = tostring(sourceAddresses),
SourceIpGroups = tostring(sourceIpGroups),
DestinationAddresses = tostring(destinationAddresses),
DestinationIpGroups = tostring(destinationIpGroups),
DestinationPorts = tostring(destinationPorts),
Action = ruleCollectionAction,
Priority = ruleCollectionPriority,
ResourceGroup = resourceGroup,
SubscriptionId = subscriptionId
| order by tostring(FirewallPolicy) asc, toint(Priority) asc, tostring(RuleCollectionGroup) asc

I use this query more often than the log-based ones. When a network change request comes in, I can quickly verify whether a rule already exists before creating a new one, or confirm what's allowing unexpected traffic without waiting for log data to appear.


Firewall Diagnostics Log Filtered by IP Range

When I need to see all firewall traffic for a specific subnet -- during a migration or when investigating unexpected flows -- I use this query. It filters by an IP CIDR range using ipv4_is_in_range and summarises the unique targets per source IP, which gives a quick picture of what a subnet is talking to.

let ipRange = "10.107.23.0/24";
AzureDiagnostics
| where Category == "AzureFirewallNetworkRule" or Category == "AzureFirewallApplicationRule"
| extend msg_original = msg_s
| extend msg_s = replace(@'. Action: Deny. Reason: SNI TLS extension was missing.',
@' to no_data:no_data. Action: Deny. Rule Collection: default behavior. Rule: SNI TLS extension missing', msg_s)
| extend msg_s = replace(@'No rule matched. Proceeding with default action',
@'Rule Collection: default behavior. Rule: no rule matched', msg_s)
| parse msg_s with * " Web Category: " WebCategory
| extend msg_s = replace(@'(. Web Category:).*','', msg_s)
| parse msg_s with * ". Rule Collection: " RuleCollection ". Rule: " Rule
| extend msg_s = replace(@'(. Rule Collection:).*','', msg_s)
| parse msg_s with * ". Rule Collection Group: " RuleCollectionGroup
| extend msg_s = replace(@'(. Rule Collection Group:).*','', msg_s)
| parse msg_s with * ". Policy: " Policy
| extend msg_s = replace(@'(. Policy:).*','', msg_s)
| parse msg_s with Protocol " request from " SourceIP " to " Target ". Action: " Action
| extend
SourceIP = iif(SourceIP contains ":", strcat_array(split(SourceIP, ":", 0), ""), SourceIP),
SourcePort = iif(SourceIP contains ":", strcat_array(split(SourceIP, ":", 1), ""), ""),
Target = iif(Target contains ":", strcat_array(split(Target, ":", 0), ""), Target),
TargetPort = iif(SourceIP contains ":", strcat_array(split(Target, ":", 1), ""), ""),
Action = iif(Action contains ".", strcat_array(split(Action, ".", 0), ""), Action),
Policy = case(RuleCollection contains ":", split(RuleCollection, ":")[0], Policy),
RuleCollectionGroup = case(RuleCollection contains ":", split(RuleCollection, ":")[1], RuleCollectionGroup),
RuleCollection = case(RuleCollection contains ":", split(RuleCollection, ":")[2], RuleCollection)
| where ipv4_is_in_range(SourceIP, ipRange) or ipv4_is_in_range(Target, ipRange)
| project msg_original, TimeGenerated, Protocol, SourceIP, SourcePort, Target, TargetPort,
Action, OperationName, Policy, RuleCollectionGroup, RuleCollection, Rule, WebCategory
| order by TimeGenerated
| summarize Targets = make_set(Target) by SourceIP

Replace ipRange with your target subnet CIDR. I typically run this before and after a migration cutover to verify that traffic patterns match expectations.