Detection rules › Kusto

GCP Security Command Center - Detect Open/Unrestricted API Keys

Status
available
Severity
medium
Time window
1h
Group by
ProjectId, ProjectName, ResourceName, Severity
Source
github.com/Azure/Azure-Sentinel

Detects Google Cloud projects that have API keys with unrestricted API access using Security Command Center API_KEY_APIS_UNRESTRICTED findings. These findings indicate API keys that are not restricted to specific APIs and may allow broader access than intended.

MITRE ATT&CK coverage

TacticTechniques
Initial AccessT1190 Exploit Public-Facing Application
Credential AccessT1552 Unsecured Credentials

Rule body kusto

id: d8e30113-373a-4f49-a0ad-1a5d8b95b729
name: GCP Security Command Center - Detect Open/Unrestricted API Keys
description: |
  Detects Google Cloud projects that have API keys with unrestricted API access using Security Command Center API_KEY_APIS_UNRESTRICTED findings.
  These findings indicate API keys that are not restricted to specific APIs and may allow broader access than intended.
severity: Medium
status: Available
requiredDataConnectors:                                                                                                                                                                                                                                                                                                                                                        
  - connectorId: GoogleSCCDefinition
    dataTypes:
      - GoogleCloudSCC
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - CredentialAccess
relevantTechniques:
  - T1190
  - T1552
tags:
  - CIS GCP Foundation 3.0 1.14
  - NIST 800-53 R5 PL-8, SA-8
  - PCI-DSS v4.0 2.2.2, 6.2.1
  - ISO-27001 v2022 A.8.27
  - Cloud Controls Matrix 4 DSP-07
  - NIST Cybersecurity Framework 1.0 PR-IP-2
  - CIS Controls 8.0 16.10
query: |
 GoogleCloudSCC
  | where tostring(Findings.state) == "ACTIVE"
  | extend FindingCategory = tostring(Findings.category)
  | where FindingCategory == "API_KEY_APIS_UNRESTRICTED"
  | extend FindingsJson = parse_json(Findings), FindingsResourceJson = parse_json(FindingsResource)
  | extend ResourceName = tostring(FindingsJson.resourceName)
  | extend ProjectId = extract(@"projects/([^/]+)", 1, ResourceName)
  | extend ProjectName = tostring(FindingsResourceJson.displayName)
  | extend Severity = tostring(FindingsJson.severity),
           FindingName = tostring(FindingsJson.name),
           ExternalUri = tostring(FindingsJson.externalUri),
           Description = tostring(FindingsJson.description)
  // produce one row per project that has unrestricted API key findings
  | summarize TimeGenerated = max(TimeGenerated),
              FindingsCount = count(),
              ExternalUri = any(ExternalUri),
              Description = any(Description)
    by ProjectId, ProjectName, Severity, ResourceName
  | project TimeGenerated, ProjectId, ProjectName, ResourceName, Severity, ExternalUri, Description
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: ProjectName
      - identifier: AppId
        columnName: ProjectId
customDetails:
  ResourceName: ResourceName
  ExternalUri: ExternalUri
  Description: Description
alertDetailsOverride:
  alertDisplayNameFormat: "GCP project {{ProjectName}} has unrestricted API key(s)"
  alertDescriptionFormat: |-
    Project {{ProjectName}} ({{ProjectId}}) has {{ResourceName}} with unrestricted API keys (API_KEY_APIS_UNRESTRICTED). Review API key restrictions, rotate or remove keys as appropriate, and apply API restrictions to keys.
version: 1.0.0
kind: Scheduled

Stages and Predicates

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 == "API_KEY_APIS_UNRESTRICTED"

Stage 5: extend (5 consecutive steps)

| extend FindingsJson = parse_json(Findings), FindingsResourceJson = parse_json(FindingsResource)
| extend ResourceName = tostring(FindingsJson.resourceName)
| extend ProjectId = extract(@"projects/([^/]+)", 1, ResourceName)
| extend ProjectName = tostring(FindingsResourceJson.displayName)
| extend Severity = tostring(FindingsJson.severity),
          FindingName = tostring(FindingsJson.name),
          ExternalUri = tostring(FindingsJson.externalUri),
          Description = tostring(FindingsJson.description)

Stage 6: summarize

| summarize TimeGenerated = max(TimeGenerated),
             FindingsCount = count(),
             ExternalUri = any(ExternalUri),
             Description = any(Description)
   by ProjectId, ProjectName, Severity, ResourceName

Stage 7: project

| project TimeGenerated, ProjectId, ProjectName, ResourceName, Severity, ExternalUri, Description

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
FindingCategoryeq
  • API_KEY_APIS_UNRESTRICTED 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
Descriptionproject
ExternalUriproject
ProjectIdproject
ProjectNameproject
ResourceNameproject
Severityproject
TimeGeneratedproject