-
Notifications
You must be signed in to change notification settings - Fork 725
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: CAI Monitoring Cloud Function (#1015)
Co-authored-by: Grant Sorbo <[email protected]> Co-authored-by: Daniel Andrade <[email protected]>
- Loading branch information
1 parent
8a4c106
commit 141f067
Showing
24 changed files
with
1,702 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
/** | ||
* 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. | ||
*/ | ||
|
||
module "kms" { | ||
source = "terraform-google-modules/kms/google" | ||
version = "~> 2.1" | ||
|
||
project_id = module.scc_notifications.project_id | ||
keyring = "krg-cai-monitoring" | ||
location = local.default_region | ||
keys = ["key-cai-monitoring"] | ||
prevent_destroy = !var.cai_monitoring_kms_force_destroy | ||
} | ||
|
||
module "cai_monitoring" { | ||
source = "../../modules/cai-monitoring" | ||
|
||
org_id = local.org_id | ||
billing_account = local.billing_account | ||
project_id = module.scc_notifications.project_id | ||
location = local.default_region | ||
enable_cmek = true | ||
encryption_key = module.kms.keys["key-cai-monitoring"] | ||
impersonate_sa_email = local.org_step_terraform_service_account_email | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
# Cloud Asset Inventory Notification | ||
Uses Google Cloud Asset Inventory to create a feed of IAM Policy change events, then process them to detect when a roles (from a preset list) is given to a member (service account, user or group). Then generates a SCC Finding with the member, role, resource where it was granted and the time that was granted. | ||
|
||
## Usage | ||
|
||
```hcl | ||
module "secure_cai_notification" { | ||
source = "terraform-google-modules/terraform-example-foundation/google//1-org/modules/cai-monitoring" | ||
org_id = <ORG ID> | ||
billing_account = <BILLING ACCOUNT ID> | ||
project_id = <PROJECT ID> | ||
region = <REGION> | ||
encryption_key = <CMEK KEY> | ||
labels = <LABELS> | ||
impersonate_sa_email = <SA TO IMPERSONATE> | ||
roles_to_monitor = <ROLES TO MONITOR> | ||
} | ||
``` | ||
|
||
<!-- BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK --> | ||
## Inputs | ||
|
||
| Name | Description | Type | Default | Required | | ||
|------|-------------|------|---------|:--------:| | ||
| billing\_account | The ID of the billing account to associate projects with. | `string` | n/a | yes | | ||
| enable\_cmek | The KMS Key to Encrypt Artifact Registry repository, Cloud Storage Bucket and Pub/Sub. | `bool` | `false` | no | | ||
| encryption\_key | The KMS Key to Encrypt Artifact Registry repository, Cloud Storage Bucket and Pub/Sub. | `string` | `null` | no | | ||
| impersonate\_sa\_email | The Service Account email who will execute terraform code. | `string` | n/a | yes | | ||
| labels | Labels to be assigned to resources. | `map(any)` | `{}` | no | | ||
| location | Default location to create resources where applicable. | `string` | `"us-central1"` | no | | ||
| org\_id | GCP Organization ID | `string` | n/a | yes | | ||
| project\_id | The Project ID where the resources will be created | `string` | n/a | yes | | ||
| random\_suffix | Adds a suffix of 4 random characters to the created resources names. | `bool` | `true` | no | | ||
| roles\_to\_monitor | List of roles that will save a SCC Finding if granted to any member (service account, user or group) on an update in the IAM Policy. | `list(string)` | <pre>[<br> "roles/owner",<br> "roles/editor",<br> "roles/resourcemanager.organizationAdmin",<br> "roles/compute.networkAdmin",<br> "roles/compute.orgFirewallPolicyAdmin"<br>]</pre> | no | | ||
|
||
## Outputs | ||
|
||
| Name | Description | | ||
|------|-------------| | ||
| artifact\_registry\_name | Artifact Registry Repo to store the Cloud Function image. | | ||
| asset\_feed\_name | Organization Asset Feed. | | ||
| bucket\_name | Storage bucket where the source code is. | | ||
| function\_uri | URI of the Cloud Function. | | ||
| scc\_source | SCC Findings Source. | | ||
| topic\_name | Pub/Sub Topic for the Asset Feed. | | ||
|
||
<!-- END OF PRE-COMMIT-TERRAFORM DOCS HOOK --> | ||
|
||
## Requirements | ||
|
||
### Software | ||
|
||
The following dependencies must be available: | ||
|
||
* [Terraform](https://www.terraform.io/downloads.html) >= 1.3 | ||
* [Terraform Provider for GCP](https://github.com/terraform-providers/terraform-provider-google) < 5.0 | ||
|
||
### APIs | ||
|
||
A project with the following APIs enabled must be used to host the resources of this module: | ||
|
||
* Project | ||
* Google Cloud Key Management Service: `cloudkms.googleapis.com` | ||
* Cloud Resource Manager API: `cloudresourcemanager.googleapis.com` | ||
* Cloud Functions API: `cloudfunctions.googleapis.com` | ||
* Cloud Build API: `cloudbuild.googleapis.com` | ||
* Cloud Asset API`cloudasset.googleapis.com` | ||
* Clouod Pub/Sub API: `pubsub.googleapis.com` | ||
* Identity and Access Management (IAM) API: `iam.googleapis.com` | ||
* Cloud Billing API: `cloudbilling.googleapis.com` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
/** | ||
* 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. | ||
*/ | ||
|
||
'use strict' | ||
|
||
// Import | ||
const uuid4 = require('uuid4') | ||
const moment = require('moment') | ||
|
||
// SCC client | ||
const { SecurityCenterClient } = require('@google-cloud/security-center'); | ||
const client = new SecurityCenterClient(); | ||
|
||
// Environment variables | ||
const sourceId = process.env.SOURCE_ID; | ||
const searchroles = process.env.ROLES.split(","); | ||
|
||
// Variables | ||
const sccConfig = { | ||
category: 'PRIVILEGED_ROLE_GRANTED', | ||
findingClass: 'VULNERABILITY', | ||
severity: 'MEDIUM' | ||
}; | ||
|
||
// Exported function | ||
exports.caiMonitoring = message => { | ||
try { | ||
var event = parseMessage(message); | ||
|
||
// This validate is specific for the CAI Monitoring scenario. | ||
// If you want to use this Cloud Function for other purpose, please change this validate function. | ||
validateEvent(event); | ||
|
||
// From this part of the code until the end of the function is specific for the CAI Monitoring scenario | ||
var bindings = getRoleBindings(event.asset); | ||
|
||
// If the list is not empty, search for the same member and role on the prior asset. | ||
// Get only the new bindings that is not on the prior asset and create a new finding. | ||
if (bindings.length > 0) { | ||
var delta = bindingDiff(bindings, getRoleBindings(event.priorAsset)); | ||
|
||
if (delta.length > 0) { | ||
// Map of extra properties to save on the finding with field name and value | ||
var extraProperties = { | ||
iamBindings: delta | ||
}; | ||
|
||
createFinding( | ||
event.asset.updateTime, | ||
event.asset.name, | ||
extraProperties | ||
); | ||
} | ||
} | ||
} catch (error) { | ||
console.warn(`Skipping executing with message: ${error.message}`); | ||
} | ||
} | ||
|
||
/** | ||
* Parse the message received on the Cloud Function to a JSON. | ||
* | ||
* @param {any} message Message from Cloud Function | ||
* @returns {JSON} Json object from the message | ||
* @exception If some error happens while parsing, it will log the error and finish the execution | ||
*/ | ||
function parseMessage(message) { | ||
// If message data is missing, log a warning and exit. | ||
if (!(message && message.data)) { | ||
throw new Error(`Missing required fields (message or message.data)`); | ||
} | ||
|
||
// Extract the event data from the message | ||
var event = JSON.parse(Buffer.from(message.data, 'base64').toString()); | ||
|
||
return event; | ||
} | ||
|
||
/** | ||
* Validate if the asset is from Organizations and have the iamPolicy and bindings field. | ||
* | ||
* @param {any} asset Asset JSON. | ||
* @exception If the asset is not valid it will throw the corresponding error. | ||
*/ | ||
function validateEvent(event) { | ||
// If the asset is not present, throw an error. | ||
if (!(event.asset && event.asset.iamPolicy && event.asset.iamPolicy.bindings)) { | ||
throw new Error(`Missing required fields (asset or asset.iamPolicy or asset.iamPolicy.bindings)`); | ||
} | ||
|
||
// If event priorAsset is missing and assetType is Project, is a new project creation, log a warning and exit | ||
if (!(event.priorAsset && event.priorAsset.iamPolicy) && event.asset.assetType === "cloudresourcemanager.googleapis.com/Project") { | ||
var name = event.asset.name.split("/"); | ||
throw new Error(`Creating project ${name[name.length - 1]}, prior asset is empty`); | ||
} | ||
} | ||
|
||
/** | ||
* Return an array of all members that have overprivileged roles. | ||
* If there's no member, it will return an empty array. | ||
* | ||
* @param {Asset} asset The asset to find members with selected permissions | ||
* @returns {Array} The array of found bindings ({member: String, role: String, action: String('ADD')}) sorted by role | ||
*/ | ||
function getRoleBindings(asset) { | ||
try { | ||
var foundRoles = []; | ||
var bindings = asset.iamPolicy.bindings; | ||
|
||
// Check for bindings that include the list of roles | ||
bindings.forEach(binding => { | ||
if (searchroles.includes(binding.role)) { | ||
binding.members.forEach(member => { | ||
foundRoles.push({ | ||
member: member, | ||
role: binding.role, | ||
action: 'ADD' | ||
}); | ||
}); | ||
} | ||
}); | ||
|
||
return foundRoles; | ||
} catch (error) { | ||
console.warn(`Returning empty bindings with message: ${error.message}`); | ||
return []; | ||
} | ||
} | ||
|
||
/** | ||
* Return an array of the members difference between the actual and the prior bindings. | ||
* If there's no member, it will return an empty array. | ||
* | ||
* @param {Array} bindings Array of the actual binding | ||
* @param {Array} priorBindings Array of the prior binding | ||
* @returns {Array} The difference array between actual and prior bindings | ||
*/ | ||
function bindingDiff(bindings, priorBindings) { | ||
return bindings.filter(actual => !priorBindings.some(prior => (prior.member === actual.member && prior.role === actual.role))); | ||
} | ||
|
||
/** | ||
* Convert string date to google.protobuf.Timestamp format | ||
* | ||
* @param {string} dateTimeStr date time format as a String. (e.g. 2019-02-15T10:23:13Z) | ||
*/ | ||
function parseStrTime(dateTimeStr) { | ||
const dateTimeStrInMillis = moment.utc(dateTimeStr).valueOf() | ||
return { | ||
seconds: Math.trunc(dateTimeStrInMillis / 1000), | ||
nanos: Math.trunc((dateTimeStrInMillis % 1000) * 1000000) | ||
} | ||
} | ||
|
||
/** | ||
* Create the new SCC finding | ||
* | ||
* @param {string} updateTime The time that the asset was changed. | ||
* @param {string} resourceName The resource where the role was given. | ||
* @param {Any} extraProperties A key/value map with properties to save on the finding ({fieldName: fieldValue}) | ||
*/ | ||
async function createFinding(updateTime, resourceName, extraProperties) { | ||
const [newFinding] = await client.createFinding( | ||
{ | ||
parent: sourceId, | ||
findingId: uuid4().replace(/-/g, ''), | ||
finding: { | ||
... { | ||
state: 'ACTIVE', | ||
resourceName: resourceName, | ||
category: sccConfig.category, | ||
eventTime: parseStrTime(updateTime), | ||
findingClass: sccConfig.findingClass, | ||
severity: sccConfig.severity | ||
}, | ||
...extraProperties | ||
} | ||
} | ||
); | ||
|
||
console.log('New finding created: %j', newFinding); | ||
} |
Oops, something went wrong.