Detection rules › Kusto

Port Scan

Status
available
Severity
medium
Time window
30s
Group by
Fqdn, SourceIp
Source
github.com/Azure/Azure-Sentinel

'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

Rule body

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

Stage 0: let

let MinimumDifferentPortsThreshold = 100;
let BinTime = 30s;
let KnownScannerIPs = dynamic([]); 

Stage 1: union

union of 3 branches

Stage 2: source

AZFWApplicationRule

Stage 3: where

| where SourceIp !in (KnownScannerIPs)

Stage 4: summarize

| summarize AlertTimedCountPortsInBinTime = dcount(DestinationPort) by SourceIp, bin(TimeGenerated, BinTime), Fqdn

Stage 5: where

| where AlertTimedCountPortsInBinTime > MinimumDifferentPortsThreshold

Stage 6: source

AZFWNetworkRule

Stage 7: where

| where SourceIp !in (KnownScannerIPs)

Stage 8: extend

| extend Fqdn = DestinationIp

Stage 9: summarize

| summarize AlertTimedCountPortsInBinTime = dcount(DestinationPort) by SourceIp, bin(TimeGenerated, BinTime), Fqdn

Stage 10: where

| where AlertTimedCountPortsInBinTime > MinimumDifferentPortsThreshold

Stage 11: source

AzureDiagnostics

Stage 12: where

| where OperationName == "AzureFirewallApplicationRuleLog" or OperationName == "AzureFirewallNetworkRuleLog"

Stage 13: parse

| parse msg_s with * "from " SourceIp ":" SourcePort:int " to " Fqdn ":" DestinationPort:int *

Stage 14: where

| where isnotempty(Fqdn) and isnotempty(SourceIp)

Stage 15: where

| where SourceIp !in (KnownScannerIPs)

Stage 16: summarize

| summarize AlertTimedCountPortsInBinTime = dcount(DestinationPort) by SourceIp, bin(TimeGenerated, BinTime), Fqdn

Stage 17: where

| where AlertTimedCountPortsInBinTime > MinimumDifferentPortsThreshold

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AlertTimedCountPortsInBinTimegt
  • 100
field:"AlertTimedCountPortsInBinTime" kind:gt value:"100"
Fqdnis_not_null
  • (no value, null check)
field:"Fqdn" kind:is_not_null
OperationNameeq
  • AzureFirewallApplicationRuleLog
  • AzureFirewallNetworkRuleLog
field:"OperationName" kind:eq
SourceIpis_not_null
  • (no value, null check)
field:"src_ip" kind:is_not_null

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
SourceIpeq[]excludes:SourceIp field:"SourceIp" value:"[]"

Output fields

These fields are emitted when the rule matches.

FieldSource
AlertTimedCountPortsInBinTimesummarize
Fqdnsummarize
SourceIpsummarize