Detection rules › Kusto

CloudNGFW By Palo Alto Networks - possible internal to external port scanning

Status
available
Severity
low
Time window
1h
Group by
Action, Application, DestinationIP, DestinationPort, FirewallName_s, Protocol, Reason, SourceIP, ident_s
Source
github.com/Azure/Azure-Sentinel

'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

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

FieldKindExcluded values
actionindeny, reset-both
DestinationPortin0, 389, 443, 53, 80, 8080, 880, 8888
dst_ipstarts_with10.
Reasonmatchaged-out
Reasonmatchtcp-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.

FieldKindValues
DestinationPortis_not_null
  • (no value, null check)
FirewallName_sis_not_null
  • (no value, null check)
appeq
  • incomplete transforms: cased
count_ge
  • 10 transforms: cased
ident_seq
  • TRAFFIC transforms: cased

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.

FieldSource
Actionsummarize
Applicationsummarize
DestinationPortsummarize
EndTimesummarize
FirewallName_ssummarize
Protocolsummarize
Reasonsummarize
SourceIPsummarize
StartTimesummarize
ident_ssummarize
totalcountsummarize
IPAddressextend
DomainIndexextend
HostNameextend
HostNameDomainextend