diff --git a/eventarc/audit_iam/main.py b/eventarc/audit_iam/main.py new file mode 100644 index 000000000000..e7c9a603b986 --- /dev/null +++ b/eventarc/audit_iam/main.py @@ -0,0 +1,71 @@ +# Copyright 2023 Google LLC. +# +# 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. + +# [START eventarc_audit_iam_server] +import json +import os + +from cloudevents.http import from_http +from flask import Flask, request +from google.events.cloud.audit import LogEntryData + +app = Flask(__name__) +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080))) + +# [END eventarc_audit_iam_server] + + +# [START eventarc_audit_iam_handler] +@app.route("/", methods=["POST"]) +def index(): + # Transform the HTTP request into a CloudEvent + event = from_http(request.headers, request.get_data()) + + # Extract the LogEntryData from the CloudEvent + # The LogEntryData type is described at https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry + # re-serialize to json, to convert the json-style 'lowerCamelCase' names to the protobuf-style 'snake_case' equivalents. + # ignore_unknown_fields is needed to skip the '@type' fields. + log_entry = LogEntryData.from_json( + json.dumps(event.get_data()), ignore_unknown_fields=True + ) + + # Ensure that this event is for service accout key creation, and succeeded. + if log_entry.proto_payload.service_name != "iam.googleapis.com": + return ("Received event was not from IAM.", 400) + if log_entry.proto_payload.status.code != 0: + return ("Key creation failed, not reporting.", 204) + + # Extract relevant fields from the audit log entry. + # Identify the user that requested key creation + user = log_entry.proto_payload.authentication_info.principal_email + + # Extract the resource name from the CreateServiceAccountKey request + # For details of this type, see https://cloud.google.com/iam/docs/reference/rpc/google.iam.admin.v1#createserviceaccountkeyrequest + service_account = log_entry.proto_payload.request["name"] + + # The response is of type google.iam.admin.v1.ServiceAccountKey, + # which is described at https://cloud.google.com/iam/docs/reference/rpc/google.iam.admin.v1#google.iam.admin.v1.ServiceAccountKey + # This key path can be used with gcloud to disable/delete the key: + # e.g. gcloud iam service-accounts keys disable ${keypath} + keypath = log_entry.proto_payload.response["name"] + + print(f"New Service Account Key created for {service_account} by {user}: {keypath}") + return ( + f"New Service Account Key created for {service_account} by {user}: {keypath}", + 200, + ) + + +# [END eventarc_audit_iam_handler] diff --git a/eventarc/audit_iam/main_test.py b/eventarc/audit_iam/main_test.py new file mode 100644 index 000000000000..57e036e1fc82 --- /dev/null +++ b/eventarc/audit_iam/main_test.py @@ -0,0 +1,64 @@ +# Copyright 2023 Google LLC. +# +# 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. +from uuid import uuid4 + +from cloudevents.conversion import to_binary +from cloudevents.http import CloudEvent + +import pytest + +import main + + +ce_attributes = { + "id": str(uuid4), + "type": "com.pytest.sample.event", + "source": "", + "specversion": "1.0", +} + + +@pytest.fixture +def client(request): + main.app.testing = True + return main.app.test_client() + + +def test_endpoint(client, capsys): + rawobj = { + "protoPayload": { + "@type": "....", + "service_name": "iam.googleapis.com", + "status": { + "code": 0, + }, + "authenticationInfo": {"principalEmail": "user@example.com"}, + "request": { + "name": "projects/-/serviceAccounts/service-account@my-project.iam.gserviceaccount.com" + }, + "response": { + "name": "projects/my-project/serviceAccounts/service-account@my-project.iam.gserviceaccount.com/keys/deadbeef" + }, + }, + } + event = CloudEvent(ce_attributes, rawobj) + headers, body = to_binary(event) + + r = client.post("/", headers=headers, data=body) + assert ( + "New Service Account Key created for projects/-/serviceAccounts/service-account@" + in r.text + ) + + assert "by user@example.com" in r.text diff --git a/eventarc/audit_iam/requirements-test.txt b/eventarc/audit_iam/requirements-test.txt new file mode 100644 index 000000000000..c2845bffbe89 --- /dev/null +++ b/eventarc/audit_iam/requirements-test.txt @@ -0,0 +1 @@ +pytest==7.0.1 diff --git a/eventarc/audit_iam/requirements.txt b/eventarc/audit_iam/requirements.txt new file mode 100644 index 000000000000..de430fcbd6c7 --- /dev/null +++ b/eventarc/audit_iam/requirements.txt @@ -0,0 +1,5 @@ +Flask==2.1.0 +gunicorn==20.1.0 +google-events==0.10.0 +cloudevents==1.9.0 +googleapis-common-protos==1.59.0 \ No newline at end of file