Detection rules › Kusto

GCP Security Command Center - Detect Firewall rules allowing unrestricted high-risk ports

Status
available
Severity
high
Time window
1h
Group by
Description, ExternalUri, FindingName, FirewallName, ProjectName, ResourceName, Severity
Source
github.com/Azure/Azure-Sentinel

This query detects GCP Firewall rules that allow unrestricted (0.0.0.0/0) ingress to high-risk ports using Google Cloud Security Command Center OPEN_FIREWALL findings. Publicly exposed management, database, and service ports (e.g., RDP 3389, SSH 22, SQL 1433/3306) significantly increase the risk of brute-force attacks, exploitation, and lateral movement.

MITRE ATT&CK coverage

TacticTechniques
Initial AccessT1133 External Remote Services
DiscoveryT1046 Network Service Discovery
Lateral MovementT1021 Remote Services

Rule body kusto

id: f4f92ca4-6ebe-4f2a-90e5-b0d04b709651
name: GCP Security Command Center - Detect Firewall rules allowing unrestricted high-risk ports
description: |
  This query detects GCP Firewall rules that allow unrestricted (0.0.0.0/0) ingress to high-risk ports using Google Cloud Security Command Center OPEN_FIREWALL findings.
  Publicly exposed management, database, and service ports (e.g., RDP 3389, SSH 22, SQL 1433/3306) significantly increase the risk of brute-force attacks, exploitation, and lateral movement.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: GoogleSCCDefinition
    dataTypes:
      - GoogleCloudSCC
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - LateralMovement
  - Discovery
relevantTechniques:
  - T1133
  - T1021
  - T1046
tags:
  - PCI-DSS v3.2.1 1.2.1
query: |
  let HighRiskPorts = dynamic([3389,20,23,110,143,3306,8080,1433,9200,9300,25,445,135,21,1434,4333,5432,5500,5601,22,3000,5000,8088,8888]);
  GoogleCloudSCC
  | where tostring(Findings.state) == "ACTIVE"
  | extend FindingCategory = tostring(Findings.category)
  | where FindingCategory == "OPEN_FIREWALL"
  | extend FindingsJson = parse_json(Findings)
  | extend SourcePropertiesJson = parse_json(tostring(FindingsJson.sourceProperties))
  | extend IpRulesJson = parse_json(tostring(FindingsJson.ipRules))
  | where tostring(FindingsJson.state) == "ACTIVE"
  | where tostring(SourcePropertiesJson.ExternalSourceRanges) contains "0.0.0.0/0"
  | extend AllowedIpRules = parse_json(tostring(IpRulesJson.allowed.ipRules))
  | mv-expand IpRule = AllowedIpRules
  | extend PortRanges = parse_json(tostring(IpRule.portRanges))
  | mv-expand PortRange = PortRanges
  | extend MinPort = toint(PortRange.min), MaxPort = toint(PortRange.max)
  | where MinPort in (HighRiskPorts) or MaxPort in (HighRiskPorts)
  | extend 
      ResourceName = tostring(FindingsJson.resourceName),
      FindingName = tostring(FindingsJson.name),
      Protocol = tostring(IpRule.protocol),
      Severity = tostring(FindingsJson.severity),
      Description = tostring(FindingsJson.description),
      ExternalUri = tostring(FindingsJson.externalUri),
      AttackExposureScore = todouble(FindingsJson.attackExposure.score),
      ProjectName = extract(@"projects/([^/]+)", 1, tostring(FindingsJson.resourceName)),
      FirewallName = extract(@"firewalls/([^/]+)", 1, tostring(FindingsJson.resourceName))
  | extend PortInfo = case(
      MinPort == MaxPort, tostring(MinPort),
      strcat(tostring(MinPort), "-", tostring(MaxPort))
  )
  | summarize 
      TimeGenerated = max(TimeGenerated), 
      OpenHighRiskPorts = make_set(PortInfo),
      Protocols = make_set(Protocol),
      AttackExposureScore = max(AttackExposureScore)
      by ProjectName, FirewallName, ResourceName, FindingName, Severity, Description, ExternalUri
  | extend OpenHighRiskPorts = strcat_array(OpenHighRiskPorts, ", ")
  | extend Protocols = strcat_array(Protocols, ", ")
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: ResourceName
customDetails:
  ProjectName: ProjectName
  FirewallName: FirewallName
  FindingId: FindingName
  OpenHighRiskPorts: OpenHighRiskPorts
  Protocols: Protocols
  AttackExposureScore: AttackExposureScore
  ExternalUri: ExternalUri
alertDetailsOverride:
  alertDisplayNameFormat: "GCP Firewall {{FirewallName}} allows unrestricted high-risk ports"
  alertDescriptionFormat: |-
    GCP Firewall {{FirewallName}} in project {{ProjectName}} allows unrestricted (0.0.0.0/0) ingress to high-risk ports: {{OpenHighRiskPorts}}.
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let HighRiskPorts = dynamic([3389,20,23,110,143,3306,8080,1433,9200,9300,25,445,135,21,1434,4333,5432,5500,5601,22,3000,5000,8088,8888]);

Stage 1: source

GoogleCloudSCC

Stage 2: where

| where tostring(Findings.state) == "ACTIVE"

Stage 3: extend

| extend FindingCategory = tostring(Findings.category)

Stage 4: where

| where FindingCategory == "OPEN_FIREWALL"

Stage 5: extend (3 consecutive steps)

| extend FindingsJson = parse_json(Findings)
| extend SourcePropertiesJson = parse_json(tostring(FindingsJson.sourceProperties))
| extend IpRulesJson = parse_json(tostring(FindingsJson.ipRules))

Stage 6: where

| where tostring(FindingsJson.state) == "ACTIVE"

Stage 7: where

| where tostring(SourcePropertiesJson.ExternalSourceRanges) contains "0.0.0.0/0"

Stage 8: extend

| extend AllowedIpRules = parse_json(tostring(IpRulesJson.allowed.ipRules))

Stage 9: mv-expand

| mv-expand IpRule = AllowedIpRules

Stage 10: extend

| extend PortRanges = parse_json(tostring(IpRule.portRanges))

Stage 11: mv-expand

| mv-expand PortRange = PortRanges

Stage 12: extend

| extend MinPort = toint(PortRange.min), MaxPort = toint(PortRange.max)

Stage 13: where

| where MinPort in (HighRiskPorts) or MaxPort in (HighRiskPorts)

Stage 14: extend

| extend 
    ResourceName = tostring(FindingsJson.resourceName),
    FindingName = tostring(FindingsJson.name),
    Protocol = tostring(IpRule.protocol),
    Severity = tostring(FindingsJson.severity),
    Description = tostring(FindingsJson.description),
    ExternalUri = tostring(FindingsJson.externalUri),
    AttackExposureScore = todouble(FindingsJson.attackExposure.score),
    ProjectName = extract(@"projects/([^/]+)", 1, tostring(FindingsJson.resourceName)),
    FirewallName = extract(@"firewalls/([^/]+)", 1, tostring(FindingsJson.resourceName))

Stage 15: extend

| extend PortInfo = case(
    MinPort == MaxPort, tostring(MinPort),
    strcat(tostring(MinPort), "-", tostring(MaxPort))
)
PortInfo =
ifMinPort == "MaxPort"tostring(MinPort)
elsestrcat(tostring(MinPort), "-", tostring(MaxPort))

Stage 16: summarize

| summarize 
    TimeGenerated = max(TimeGenerated), 
    OpenHighRiskPorts = make_set(PortInfo),
    Protocols = make_set(Protocol),
    AttackExposureScore = max(AttackExposureScore)
    by ProjectName, FirewallName, ResourceName, FindingName, Severity, Description, ExternalUri

Stage 17: extend

| extend OpenHighRiskPorts = strcat_array(OpenHighRiskPorts, ", ")

Stage 18: extend

| extend Protocols = strcat_array(Protocols, ", ")

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
ExternalSourceRangescontains
  • 0.0.0.0/0 transforms: tostring
FindingCategoryeq
  • OPEN_FIREWALL transforms: cased
MaxPortin
  • 110 transforms: cased
  • 135 transforms: cased
  • 143 transforms: cased
  • 1433 transforms: cased
  • 1434 transforms: cased
  • 20 transforms: cased
  • 21 transforms: cased
  • 22 transforms: cased
  • 23 transforms: cased
  • 25 transforms: cased
  • 3000 transforms: cased
  • 3306 transforms: cased
  • 3389 transforms: cased
  • 4333 transforms: cased
  • 445 transforms: cased
  • 5000 transforms: cased
  • 5432 transforms: cased
  • 5500 transforms: cased
  • 5601 transforms: cased
  • 8080 transforms: cased
  • 8088 transforms: cased
  • 8888 transforms: cased
  • 9200 transforms: cased
  • 9300 transforms: cased
MinPortin
  • 110 transforms: cased
  • 135 transforms: cased
  • 143 transforms: cased
  • 1433 transforms: cased
  • 1434 transforms: cased
  • 20 transforms: cased
  • 21 transforms: cased
  • 22 transforms: cased
  • 23 transforms: cased
  • 25 transforms: cased
  • 3000 transforms: cased
  • 3306 transforms: cased
  • 3389 transforms: cased
  • 4333 transforms: cased
  • 445 transforms: cased
  • 5000 transforms: cased
  • 5432 transforms: cased
  • 5500 transforms: cased
  • 5601 transforms: cased
  • 8080 transforms: cased
  • 8088 transforms: cased
  • 8888 transforms: cased
  • 9200 transforms: cased
  • 9300 transforms: cased
stateeq
  • ACTIVE transforms: tostring, 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
AttackExposureScoresummarize
Descriptionsummarize
ExternalUrisummarize
FindingNamesummarize
FirewallNamesummarize
OpenHighRiskPortsextend
ProjectNamesummarize
Protocolsextend
ResourceNamesummarize
Severitysummarize
TimeGeneratedsummarize