1-org/modules/cai-monitoring/function-source/index.js (108 lines of code) (raw):
/**
* 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);
}