Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions plugins/gcpaudit/pkg/gcpaudit/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,7 @@ func (p *Plugin) Extract(req sdk.ExtractRequest, evt sdk.EventReader) error {

case "gcp.policyDelta":
resource := string(p.jdata.Get("resource").Get("type").GetStringBytes())

if resource == "gcs_bucket" {
fsval = p.jdata.Get("protoPayload", "serviceData", "policyDelta", "bindingDeltas")
} else {
fsval = p.jdata.Get("protoPayload", "metadata", "datasetChange", "bindingDeltas")
}
fsval = p.jdata.Get(policyDeltaPath(resource)...)

case "gcp.methodName":
fsval = p.jdata.Get("protoPayload", "methodName")
Expand Down Expand Up @@ -201,3 +196,17 @@ func (p *Plugin) Extract(req sdk.ExtractRequest, evt sdk.EventReader) error {

return nil
}

// policyDeltaPath returns the JSON path that contains the bindingDeltas array
// for a SetIamPolicy audit event on the given resource type. The generic IAM
// AuditData proto (google.iam.v1.PolicyDelta) is emitted under
// protoPayload.serviceData.policyDelta for every resource type that uses the
// shared IAM service. BigQuery's dataset.setIamPolicy is the outlier and emits
// its deltas under protoPayload.metadata.datasetChange as part of the
// BigQueryAuditMetadata. See issue #1351.
Comment on lines +200 to +206

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice improvement over the old logic 🙏 defaulting to serviceData.policyDelta and special-casing BigQuery is the right direction, and I don't see a breaking change here (gcs_bucket and bigquery_dataset keep the same paths).

A couple of caveats about this rationale though:

  • protoPayload.serviceData is officially deprecated by Google (they recommend metadata now), and it's shared by several services, not just BigQuery. So "BigQuery is the outlier" is a bit of an oversimplification - not a blocker, but the comment may not hold as more services migrate to metadata.

  • More importantly, this plugin ingests exported logs via the Pub/Sub sink, and serviceData is known to be stripped on exported entries even when it's populated in Logs Explorer 👉 https://permiso.io/blog/gcp-servicedata-officially-deprecated-actively-dangerous

So for the very resources #1351 is about (project/folder/org/service_account), gcp.policyDelta may still come back empty in practice after this change. Have you confirmed it's populated on a real exported setIamPolicy event? 🤔 If not, we may need metadata as a fallback.

func policyDeltaPath(resource string) []string {
if resource == "bigquery_dataset" {
return []string{"protoPayload", "metadata", "datasetChange", "bindingDeltas"}
}
return []string{"protoPayload", "serviceData", "policyDelta", "bindingDeltas"}
}
57 changes: 57 additions & 0 deletions plugins/gcpaudit/pkg/gcpaudit/extract_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright (C) 2026 The Falco Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package gcpaudit

import (
"reflect"
"testing"
)

// TestPolicyDeltaPath guards against #1351: gcp.policyDelta was returning the
// BigQuery datasetChange path for every non-gcs_bucket resource type, leaving
// the field empty for project, folder, organization, service_account, and any
// other GCP resource that uses the generic IAM SetIamPolicy flow.
func TestPolicyDeltaPath(t *testing.T) {
serviceData := []string{"protoPayload", "serviceData", "policyDelta", "bindingDeltas"}
datasetChange := []string{"protoPayload", "metadata", "datasetChange", "bindingDeltas"}

cases := []struct {
resource string
want []string
}{
{"gcs_bucket", serviceData},
{"project", serviceData},
{"folder", serviceData},
{"organization", serviceData},
{"service_account", serviceData},
// Unknown / future resource types default to the generic IAM path,
// not the BigQuery-specific datasetChange path.
{"unknown_type", serviceData},
{"", serviceData},
{"bigquery_dataset", datasetChange},
}

for _, tc := range cases {
t.Run(tc.resource, func(t *testing.T) {
got := policyDeltaPath(tc.resource)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test covers the path selection nicely, but it stops at policyDeltaPath() and never exercises Extract() with a real event, so it won't catch the case where the selected path exists but is empty on the wire (see my comment on extract.go).

May you add a fixture-based test that feeds a captured exported setIamPolicy event (e.g. project-level) through Extract and asserts gcp.policyDelta is non-empty? That would guard the actual #1351 symptom, not just the routing. 🙏

if !reflect.DeepEqual(got, tc.want) {
t.Errorf("policyDeltaPath(%q) = %v, want %v", tc.resource, got, tc.want)
}
})
}
}
Loading