Detection rules › Kusto
Port Scan
'Identifies a source IP scanning multiple open ports on Azure Firewall. This can indicate malicious scanning of ports by an attacker, trying to reveal open ports in the organization that can be compromised for initial access. Configurable Parameters: - Port scan time - the time range to look for multiple ports scanned. Default is set to 30 seconds. - Minimum different ports threshold - alert only if more than this number of ports scanned. Default is set to 100.'
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance | T1595.001 Active Scanning: Scanning IP Blocks |
| Discovery | T1046 Network Service Discovery |
Rule body kusto
id: b2c5907b-1040-4692-9802-9946031017e8
name: Port Scan
description: |
'Identifies a source IP scanning multiple open ports on Azure Firewall. This can indicate malicious scanning of ports by an attacker, trying to reveal open ports in the organization that can be compromised for initial access.
Configurable Parameters:
- Port scan time - the time range to look for multiple ports scanned. Default is set to 30 seconds.
- Minimum different ports threshold - alert only if more than this number of ports scanned. Default is set to 100.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AzureFirewall
dataTypes:
- AzureDiagnostics
- AZFWApplicationRule
- AZFWNetworkRule
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Discovery
- Reconnaissance
relevantTechniques:
- T1046
- T1595.001
query: |
let MinimumDifferentPortsThreshold = 100;
let BinTime = 30s;
// Exclude known benign scanner IPs in your environment to reduce noise
let KnownScannerIPs = dynamic([]);
union isfuzzy=true(
AZFWApplicationRule
| where SourceIp !in (KnownScannerIPs)
| summarize AlertTimedCountPortsInBinTime = dcount(DestinationPort) by SourceIp, bin(TimeGenerated, BinTime), Fqdn
| where AlertTimedCountPortsInBinTime > MinimumDifferentPortsThreshold),
(AZFWNetworkRule
| where SourceIp !in (KnownScannerIPs)
| extend Fqdn = DestinationIp
| summarize AlertTimedCountPortsInBinTime = dcount(DestinationPort) by SourceIp, bin(TimeGenerated, BinTime), Fqdn
| where AlertTimedCountPortsInBinTime > MinimumDifferentPortsThreshold),
(AzureDiagnostics
| where OperationName == "AzureFirewallApplicationRuleLog" or OperationName == "AzureFirewallNetworkRuleLog"
| parse msg_s with * "from " SourceIp ":" SourcePort:int " to " Fqdn ":" DestinationPort:int *
| where isnotempty(Fqdn) and isnotempty(SourceIp)
| where SourceIp !in (KnownScannerIPs)
| summarize AlertTimedCountPortsInBinTime = dcount(DestinationPort) by SourceIp, bin(TimeGenerated, BinTime), Fqdn
| where AlertTimedCountPortsInBinTime > MinimumDifferentPortsThreshold)
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIp
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Fqdn
alertDetailsOverride:
alertDisplayNameFormat: 'Port Scan Detected from {{SourceIp}}'
alertDescriptionFormat: 'Source IP {{SourceIp}} has scanned {{AlertTimedCountPortsInBinTime}} different ports within a short time frame, which may indicate reconnaissance activity.'
version: 1.1.3
kind: Scheduled
Stages and Predicates
Parameters
let MinimumDifferentPortsThreshold = 100;
let BinTime = 30s;
let KnownScannerIPs = dynamic([]);
union isfuzzy=true (3 sources)
Each leg below queries one source; the rule matches if any leg does. Sources: AZFWApplicationRule, AZFWNetworkRule, AzureDiagnostics
Leg 1: AZFWApplicationRule
AZFWApplicationRule
| where SourceIp !in (KnownScannerIPs)
| summarize AlertTimedCountPortsInBinTime = dcount(DestinationPort) by SourceIp, bin(TimeGenerated, BinTime), Fqdn
| where AlertTimedCountPortsInBinTime > MinimumDifferentPortsThreshold
Leg 2: AZFWNetworkRule
AZFWNetworkRule
| where SourceIp !in (KnownScannerIPs)
| extend Fqdn = DestinationIp
| summarize AlertTimedCountPortsInBinTime = dcount(DestinationPort) by SourceIp, bin(TimeGenerated, BinTime), Fqdn
| where AlertTimedCountPortsInBinTime > MinimumDifferentPortsThreshold
Leg 3: AzureDiagnostics
AzureDiagnostics
| where OperationName == "AzureFirewallApplicationRuleLog" or OperationName == "AzureFirewallNetworkRuleLog"
| parse msg_s with * "from " SourceIp ":" SourcePort:int " to " Fqdn ":" DestinationPort:int *
| where isnotempty(Fqdn) and isnotempty(SourceIp)
| where SourceIp !in (KnownScannerIPs)
| summarize AlertTimedCountPortsInBinTime = dcount(DestinationPort) by SourceIp, bin(TimeGenerated, BinTime), Fqdn
| where AlertTimedCountPortsInBinTime > MinimumDifferentPortsThreshold
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
SourceIp | eq | [] |
SourceIp | eq | [] |
SourceIp | eq | [] |
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
AlertTimedCountPortsInBinTime | gt |
|
Fqdn | is_not_null | |
OperationName | eq |
|
SourceIp | is_not_null |
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
AlertTimedCountPortsInBinTime | summarize |
Fqdn | summarize |
SourceIp | summarize |