Detection rules › Kusto
CloudNGFW By Palo Alto Networks - possible internal to external port scanning
'Identifies a list of internal Source IPs (10.x.x.x Hosts) that have triggered 10 or more non-graceful tcp server resets from one or more Destination IPs which results in an "app = incomplete" designation. The server resets coupled with an "Incomplete" app designation can be an indication of internal to external port scanning or probing attack. References: https://knowledgebase.paloaltonetworks.com/KCSArticleDetail?id=kA10g000000ClUvCAK https://knowledgebase.paloaltonetworks.com/KCSArticleDetail?id=kA10g000000ClTaCAK'
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Discovery | T1046 Network Service Discovery |
Rule body kusto
id: 5b72f527-e3f6-4a00-9908-8e4fee14da9f
name: CloudNGFW By Palo Alto Networks - possible internal to external port scanning
description: |
'Identifies a list of internal Source IPs (10.x.x.x Hosts) that have triggered 10 or more non-graceful tcp server resets from one or more Destination IPs which results in an "app = incomplete" designation. The server resets coupled with an "Incomplete" app designation can be an indication of internal to external port scanning or probing attack.
References: https://knowledgebase.paloaltonetworks.com/KCSArticleDetail?id=kA10g000000ClUvCAK
https://knowledgebase.paloaltonetworks.com/KCSArticleDetail?id=kA10g000000ClTaCAK'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: AzureCloudNGFWByPaloAltoNetworks
dataTypes:
- fluentbit_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Discovery
relevantTechniques:
- T1046
query: |
fluentbit_CL
| where ident_s == "TRAFFIC"
| where isnotempty(FirewallName_s)
| extend message = parse_json(Message)
| extend DestinationPort = tostring(message.dport)
| extend SourceIP = tostring(message.src_ip)
| extend DestinationIP = tostring(message.dst_ip)
| extend Application = tostring(message.app)
| extend Protocol = tostring(message.proto)
| extend Action = tostring(message.action)
| where isnotempty(DestinationPort) and message.action !in ("reset-both", "deny")
| where DestinationPort !in ("443", "53", "389", "80", "0", "880", "8888", "8080")
| where message.app == "incomplete"
| where toint(DestinationPort) !between (49512 .. 65535)
| where message.dst_ip !startswith "10."
| extend Reason = coalesce(column_ifexists("Reason", ""), tostring(message.session_end_reason), "")
| where Reason !has "aged-out"
| where Reason !has "tcp-fin"
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), count() by FirewallName_s, SourceIP, Application, Reason, DestinationPort, Protocol, ident_s, Action, DestinationIP
| where count_ >= 10
| summarize StartTime = min(StartTime), EndTime = max(EndTime), makeset(DestinationIP), totalcount = sum(count_) by FirewallName_s, SourceIP, Application, Reason, DestinationPort, Protocol, ident_s, Action
| extend IPAddress = SourceIP
| extend HostName = tostring(split(FirewallName_s, ".")[0]), DomainIndex = toint(indexof(FirewallName_s, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(FirewallName_s, DomainIndex + 1), FirewallName_s)
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: FirewallName_s
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: HostNameDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
version: 1.0.6
kind: Scheduled
Stages and Predicates
Stage 1: source
fluentbit_CL
Stage 2: where
| where ident_s == "TRAFFIC"
Stage 3: where
| where isnotempty(FirewallName_s)
Stage 4: extend (7 consecutive steps)
| extend message = parse_json(Message)
| extend DestinationPort = tostring(message.dport)
| extend SourceIP = tostring(message.src_ip)
| extend DestinationIP = tostring(message.dst_ip)
| extend Application = tostring(message.app)
| extend Protocol = tostring(message.proto)
| extend Action = tostring(message.action)
Stage 5: where
| where isnotempty(DestinationPort) and message.action !in ("reset-both", "deny")
Stage 6: where
| where DestinationPort !in ("443", "53", "389", "80", "0", "880", "8888", "8080")
Stage 7: where
| where message.app == "incomplete"
Stage 8: where
| where toint(DestinationPort) !between (49512 .. 65535)
Stage 9: where
| where message.dst_ip !startswith "10."
Stage 10: extend
| extend Reason = coalesce(column_ifexists("Reason", ""), tostring(message.session_end_reason), "")
Stage 11: where
| where Reason !has "aged-out"
Stage 12: where
| where Reason !has "tcp-fin"
Stage 13: summarize
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), count() by FirewallName_s, SourceIP, Application, Reason, DestinationPort, Protocol, ident_s, Action, DestinationIP
Stage 14: where
| where count_ >= 10
Stage 15: summarize
| summarize StartTime = min(StartTime), EndTime = max(EndTime), makeset(DestinationIP), totalcount = sum(count_) by FirewallName_s, SourceIP, Application, Reason, DestinationPort, Protocol, ident_s, Action
Stage 16: extend (3 consecutive steps)
| extend IPAddress = SourceIP
| extend HostName = tostring(split(FirewallName_s, ".")[0]), DomainIndex = toint(indexof(FirewallName_s, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(FirewallName_s, DomainIndex + 1), FirewallName_s)
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
action | in | deny, reset-both |
DestinationPort | in | 0, 389, 443, 53, 80, 8080, 880, 8888 |
dst_ip | starts_with | 10. |
Reason | match | aged-out |
Reason | match | tcp-fin |
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 |
|---|---|---|
DestinationPort | is_not_null | |
FirewallName_s | is_not_null | |
app | eq |
|
count_ | ge |
|
ident_s | eq |
|
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 |
|---|---|
Action | summarize |
Application | summarize |
DestinationPort | summarize |
EndTime | summarize |
FirewallName_s | summarize |
Protocol | summarize |
Reason | summarize |
SourceIP | summarize |
StartTime | summarize |
ident_s | summarize |
totalcount | summarize |
IPAddress | extend |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |