UNPKG

@pulumi/compliance-policy-manager

Version:

This repository contains a growing set of Compliance Policies to validate your infrastructure using Pulumi's Crossguard Policy-as-Code framework.

733 lines (732 loc) 32.2 kB
"use strict"; // Copyright 2016-2024, Pulumi Corporation. // // 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. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.valToBoolean = exports.policyManager = exports.PolicyManager = exports.loadPlugins = void 0; __exportStar(require("./version"), exports); const plugin_1 = require("./plugin"); var plugin_2 = require("./plugin"); Object.defineProperty(exports, "loadPlugins", { enumerable: true, get: function () { return plugin_2.loadPlugins; } }); ; /** * Class to manage policies. */ class PolicyManager { constructor() { /** * An array containing all registered policies. */ this.allPolicies = []; /** * An array containing all registered policy names. This is used to * prevent duplicated names since the Pulumi service requires a * unique name for each policy. */ this.allNames = []; /** * A `Record` that contains policies arranged per vendor. */ this.vendors = {}; /** * A `Record` that contains policies arranged per service. */ this.services = {}; /** * A `Record` that contains policies arranged per framework. */ this.frameworks = {}; /** * A `Record` that contains policies arranged per topic. */ this.topics = {}; /** * A `Record` that contains policies arranged per severity. */ this.severities = {}; /** * An array of registered policies that haven't been returned yet via `selectPolicies()`. * This is needed to ensure users get policies only once so the Pulumi service doesn't * complain about duplicated policies. */ this.remainingPolicies = []; /** * An array of policy names that have been returned via `selectPolicies()`. * This is used to return by `getStats()` so it's possible to know the list * of policies that have been selected. */ this.selectedPolicies = []; /** * An array containing the list of registered modules. */ this.registeredModules = []; /** * A Pulumi managed policy configuration schema. This policy schema, when associated * to a policy, offers a basic policy configuration scheme, which in turn can be * leveraged inside the policy itself. * * See `shouldEvalPolicy()` for more information. */ this.policyConfigSchema = { properties: { includeFor: { type: "array", description: "A list of resource names or regular expressions for resources that must be included from this policy evaluation.", }, excludeFor: { type: "array", description: "A list of resource names or regular expressions for resources that should be excluded from this policy evaluation.", }, ignoreCase: { type: "boolean", description: "Case-insensitive search.", }, }, }; } /** * This function determines if a policy should be evaluated for the given resource based on the * Policy configuration. The user supplied list of matching names provided via the Policy * Configuration is checked against the resource name. * * This function checks first for explicit inclusions, then for explicit exclusions and finally * returns `true` if no matches occured. * * Note: This functon is primarily intented to be used within a policy. * * @param args The argument `ResourceValidationArgs` provided during the policy evaluation. * @returns `true` if the policy should be evaluated for the given resource, `false` otherwise. */ shouldEvalPolicy(args) { const polConfig = args.getConfig(); if (polConfig.includeFor && polConfig.includeFor.length > 0) { for (let i = 0; i < polConfig.includeFor.length; i++) { const expression = polConfig.includeFor[i]; // received and empty element, nothing to do. if (expression.length < 1) { continue; } let flag = undefined; if (polConfig.ignoreCase === true) { flag = "i"; } try { const re = new RegExp(expression, flag); const result = re.exec(args.name); if (result !== null) { return true; } } catch (e) { console.error(`Error evaluating regular expression ${e}`); console.error(`Regular expression: '${expression}'`); } } } if (polConfig.excludeFor && polConfig.excludeFor.length > 0) { for (let i = 0; i < polConfig.excludeFor.length; i++) { const expression = polConfig.excludeFor[i]; // received and empty element, nothing to do. if (expression.length < 1) { continue; } try { const re = new RegExp(expression, polConfig.ignoreCase ? "i" : undefined); const result = re.exec(args.name); if (result !== null) { /** * It's currently not possible to show the policy name as this is not exposed within the policy. * TODO: Once the policy can be exposed, then display the policy name alongside the resource name. */ if (polConfig.logExcludedResources === true) { console.warn(`The resource '${args.name}' configuration will not be evaluated for policy violation. (urn: '${args.urn}', type: '${args.type}')`); } return false; } } catch (e) { console.error(`Error evaluating regular expression ${e}`); console.error(`Regular expression: '${expression}'`); } } } return true; } /** * The function `getSelectionStats()` returns statistics about the number of registered * policies as well as the names and count of already selected policies and the number * of policies that haven't been selected yet. * * @returns Returns a populated `PolicyManagerStats`. */ getSelectionStats() { return { policyCount: this.allPolicies.length, remainingPolicyCount: this.remainingPolicies.length, selectedPoliciesCount: this.selectedPolicies.length, selectedPolicies: [...this.selectedPolicies], registeredModules: [...this.registeredModules], }; } /** * This function `displaySelectionStats()` displays general statistics about policies * that have been returned by `selectPolicies()` and how many remain in the pool. * Additional information about registered policy modules are displayed too. * * @returns No value is returned. */ displaySelectionStats(args) { const stats = this.getSelectionStats(); let message = ""; if (args.displayGeneralStats) { message += `Total registered policies: ${stats.policyCount}\n`; message += `Selected policies: ${stats.selectedPoliciesCount}\n`; message += `Remaining (unselected) policies: ${stats.remainingPolicyCount}\n`; } if (args.displayModuleInformation) { if (args.displayGeneralStats) { message += "---\n"; } message += "Included policy packages:\n"; for (let x = 0; x < stats.registeredModules.length; x++) { message += ` ${stats.registeredModules[x].name}: ${stats.registeredModules[x].version}\n`; } } if (args.displaySelectedPolicyNames) { if (args.displayGeneralStats || args.displayModuleInformation) { message += "---\n"; } /** * Sort policies by name so the output is consistent. */ stats.selectedPolicies.sort((policyA, policyB) => { if (policyA.name < policyB.name) { return -1; } if (policyA > policyB) { return 1; } return 0; }); message += "Selected policies:\n"; for (let x = 0; x < stats.selectedPolicies.length; x++) { message += ` ${stats.selectedPolicies[x].name}: enforcementLevel: ${stats.selectedPolicies[x].enforcementLevel}\n`; } } console.error(message); return; } /** * When executing the policy selector, it's crucial for the function to return each policy * exactly once. This ensures that the Pulumi service doesn't return an error related to * duplicated policies when a Policy Pack is published. * * The purpose of this function is to reset the policy filter, enabling a fresh start. * Consequently, when you invoke `selectPolicies()`, it will take into account all the * registered policies including the ones previously selected. This may add previously * selected policies to your Policy Pack. * * This function for unit tests purpose and most users/developers shouldn't use it. */ resetPolicySelector() { this.remainingPolicies = [...this.allPolicies]; this.selectedPolicies = []; } /** * This function returns a resource policy information by providing the policy * name. * * This function for unit tests purpose and most users/developers shouldn't use it. * * **Note**: The returned policy is not removed from the pool of available policies. * If you want to select an individual policy, then you should be using * `selectPolicyByName()` instead. * * @param name The policy name to search for and return. * @returns The PolicyInfo if found, otherwise `undefined`. */ getPolicyByName(name) { if (!name) { return undefined; } const match = this.allPolicies.find((pol) => { if (pol.policyName === name) { return true; } return false; }); if (!match) { return undefined; } return match; } /** * This function searches for a policy based on the provided `name`. If the * policy is found, then it is removed from the pool of available policies * and the policy is returned. If not found, the `undefined` is returned. * * @param name The policy name to search for and return. * @param enforcementLevel The desired policy enforcement Level. Valid values are `advisory`, `mandatory` and `disabled`. * @returns A `ResourceValidationPolicy` policy that matched the supplied `name` or `undefined` if the policy wasn't found in the pool of `remainingPolicies`. */ selectPolicyByName(name, enforcementLevel) { if (!name) { return undefined; } const policyIndex = this.remainingPolicies.findIndex((candidate) => { if (candidate.policyName === name) { return true; } return false; }); if (policyIndex >= 0) { /* * We need to deep clone the entire policy to avoid changing * the enforcement level set by the policy developer. However, * It's not possible to use `structuredClone()` to clone because * the policy code cannot be serialized. So instead, we manually * assign each value and set the enforcementLevel last. */ const pol = { name: this.remainingPolicies[policyIndex].resourceValidationPolicy.name, description: this.remainingPolicies[policyIndex].resourceValidationPolicy.description, configSchema: this.remainingPolicies[policyIndex].resourceValidationPolicy.configSchema, validateResource: this.remainingPolicies[policyIndex].resourceValidationPolicy.validateResource, enforcementLevel: this.remainingPolicies[policyIndex].resourceValidationPolicy.enforcementLevel, }; if (enforcementLevel === "advisory" || enforcementLevel === "mandatory" || enforcementLevel === "disabled") { pol.enforcementLevel = enforcementLevel; } /* * We also take the opportunity to capture the policy name and * store it if the user wants to know which policies have been * used to create their Policy Pack. */ this.selectedPolicies.push(pol); /* * We remove the selected policy from the pool of available policies. */ this.remainingPolicies.splice(policyIndex, 1); return pol; } return undefined; } /** * Takes an array of policy names and set the desired enforcement level on each policy. * If a provided policy name has alread been selected, then the matching policy is not * returned as part of the result. * * @param names An array of policy names. * @param enforcementLevel The desired enforcement level for those policies. * @returns An array of policies. */ selectPoliciesByName(names, enforcementLevel) { const policies = []; for (let x = 0; x < names.length; x++) { const result = this.selectPolicyByName(names[x], enforcementLevel); if (!result) { continue; } policies.push(result); } return policies; } /** * Select policies based on criterias provided as arguments. The selectiopn filter only * returns policies that match selection criterias. Effectively, this function performs * an `or` operation within each selection criteria, and an `and` operation between * selection criterias. * * You may also provide an array of cherry-picked polcies. The function takes care of * removing duplicates as well as ignoring already selected policies from previous calls. * * Note: Criterias are all case-insensitive. * Note: Call `resetPolicyfilter()` to reset the selection filter and consider all * policies again. * * @param args A bag of options containing the selection criterias, or an array of cherry-picked policies. * @param enforcementLevel The desired policy enforcement Level. Valid values are `advisory`, `mandatory` and `disabled`. * @returns An array of ResourceValidationPolicy policies that matched with the selection criterias. */ selectPolicies(args, enforcementLevel) { const results = []; /* * We make a deep copy to avoid interactions between 2 * variables but on the same array. */ let matches = [...this.remainingPolicies]; // let matches: PolicyInfo[] = []; if (typeof args === "object" && ("vendors" in args || "services" in args || "frameworks" in args || "severities" in args || "topics" in args)) { /* * We have a `FilterPolicyArgs` type */ if (args.vendors && args.vendors.length > 0 && matches.length > 0) { let tmpMatches = []; for (let x = 0; x < args.vendors.length; x++) { const vendorName = args.vendors[x].toLowerCase(); tmpMatches = tmpMatches.concat(matches.filter((candidatePolicy) => { if (!this.vendors[vendorName]) { return false; } const findResult = this.vendors[vendorName].find((vendorPolicy) => { return vendorPolicy.policyName === candidatePolicy.policyName; }); if (findResult) { return true; } return false; })); } matches = tmpMatches; } if (args.services && args.services.length > 0 && matches.length > 0) { let tmpMatches = []; for (let x = 0; x < args.services.length; x++) { const serviceName = args.services[x].toLowerCase(); tmpMatches = tmpMatches.concat(matches.filter((candidatePolicy) => { if (!this.services[serviceName]) { return false; } const findResult = this.services[serviceName].find((servicePolicy) => { return servicePolicy.policyName === candidatePolicy.policyName; }); if (findResult) { return true; } return false; })); } matches = tmpMatches; } if (args.frameworks && args.frameworks.length > 0 && matches.length > 0) { let tmpMatches = []; for (let x = 0; x < args.frameworks.length; x++) { const frameworkName = args.frameworks[x].toLowerCase(); tmpMatches = tmpMatches.concat(matches.filter((candidatePolicy) => { if (!this.frameworks[frameworkName]) { return false; } const findResult = this.frameworks[frameworkName].find((frameworkPolicy) => { return frameworkPolicy.policyName === candidatePolicy.policyName; }); if (findResult) { return true; } return false; })); } matches = tmpMatches; } if (args.topics && args.topics.length > 0 && matches.length > 0) { let tmpMatches = []; for (let x = 0; x < args.topics.length; x++) { const topicName = args.topics[x].toLowerCase(); tmpMatches = tmpMatches.concat(matches.filter((candidatePolicy) => { if (!this.topics[topicName]) { return false; } const findResult = this.topics[topicName].find((topicPolicy) => { return topicPolicy.policyName === candidatePolicy.policyName; }); if (findResult) { return true; } return false; })); } matches = tmpMatches; } if (args.severities && args.severities.length > 0 && matches.length > 0) { let tmpMatches = []; for (let x = 0; x < args.severities.length; x++) { const severityName = args.severities[x].toLowerCase(); tmpMatches = tmpMatches.concat(matches.filter((candidatePolicy) => { if (!this.severities[severityName]) { return false; } const findResult = this.severities[severityName].find((severityPolicy) => { return severityPolicy.policyName === candidatePolicy.policyName; }); if (findResult) { return true; } return false; })); } matches = tmpMatches; } if ((!args.vendors || args.vendors.length <= 0) && (!args.services || args.services.length <= 0) && (!args.frameworks || args.frameworks.length <= 0) && (!args.topics || args.topics.length <= 0) && (!args.severities || args.severities.length <= 0)) { /** * no selection criteria were supplied, so we need to * return an empty selection. */ matches = []; } } else if (typeof args === "object" && "length" in args) { /* * We have an `array` type */ let tmpMatches = []; for (let x = 0; x < args.length; x++) { const policyName = args[x].name; tmpMatches = tmpMatches.concat(matches.filter((candidatePolicy) => { return candidatePolicy.policyName === policyName; })); } /* * As we now have all the user supplied policies that were still available * in the remaining pool, we need to remove any duplicates. */ tmpMatches = tmpMatches.filter((value, index, self) => { return index === self.findIndex((obj) => (obj.policyName === value.policyName)); }); matches = tmpMatches; } else { /** * We can't be 100% sure of the type, so we assume we got an empty * `FilterPolicyArgs` and simply return no selection. */ matches = []; } /** * Right now, `matches` contains all the selected policies but we need * to remove any duplciates from it before returning the result to the * user. * Note: don't user `.forEach()` and `.splice()` otherwise you'll get * an unpredictable result. */ const matchSeen = {}; matches = matches.filter((m) => { if (!matchSeen[m.policyName]) { matchSeen[m.policyName] = true; return true; } return false; }); /* * At this point, `matches` contains only the polcies that have matches * the user submitted criterias or their cherry-picking selection. We * should remove them from the `this.remainingPolicies[]` to avoid * duplicates when calling the next `filterPolicies()`. */ matches.forEach((match) => { const matchIndex = this.remainingPolicies.findIndex((candidate) => { if (candidate.policyName === match.policyName) { return true; } return false; }); if (matchIndex >= 0) { this.remainingPolicies.splice(matchIndex, 1); } }); /* * Now `matches` only contains policies that haven't been selected before. */ matches.forEach((match) => { /* * We need to deep clone the entire policy to avoid changing * the enforcement level set by the policy developer. However, * It's not possible to use `structuredClone()` to clone because * the policy code cannot be serialized. So instead, we manually * assign each value and set the enforcementLevel last. */ const pol = { name: match.resourceValidationPolicy.name, description: match.resourceValidationPolicy.description, configSchema: match.resourceValidationPolicy.configSchema, validateResource: match.resourceValidationPolicy.validateResource, enforcementLevel: match.resourceValidationPolicy.enforcementLevel, }; if (enforcementLevel === "advisory" || enforcementLevel === "mandatory" || enforcementLevel === "disabled") { pol.enforcementLevel = enforcementLevel; } results.push(pol); /* * We also take the opportunity to capture the policy name and * store it if the user wants to know which policies have been * used to create their Policy Pack. */ this.selectedPolicies.push(pol); }); return results; } /** * Register a new policy into the pool of policies. The policy name must be * unique to the pool of policies already registered or an exception is thrown. * * This function is used if you are authoring your own Compliance Policies. * * @param args An object containing the policy to register as well as its additional attributes. * @returns a `ResourceValidationPolicy` object. */ registerPolicy(args) { var _a, _b, _c, _d; if (this.allNames.includes(args.resourceValidationPolicy.name)) { throw new Error(`Another policy with the name '${args.resourceValidationPolicy.name}' already exists. Either register the policy only once, or ensure policy names are unique.`); } this.allNames.push(args.resourceValidationPolicy.name); const policyInfo = { policyName: args.resourceValidationPolicy.name, resourceValidationPolicy: args.resourceValidationPolicy, policyMetadata: { frameworks: args.frameworks, services: args.services, severity: args.severity, topics: args.topics, vendors: args.vendors, }, }; this.allPolicies.push(policyInfo); /* * We make a deep copy of the array below to ensure `this.allPolicies` * is saved in its own instance. This is necessary to ensure * `this.allPolicies` is never affected by operations done on * `this.remainingPolicies`. */ this.remainingPolicies = [...this.allPolicies]; (_a = args.vendors) === null || _a === void 0 ? void 0 : _a.forEach((vendorName) => { vendorName = vendorName.toLowerCase(); if (this.vendors[vendorName] === undefined) { this.vendors[vendorName] = [policyInfo]; } else { this.vendors[vendorName].push(policyInfo); } }); (_b = args.services) === null || _b === void 0 ? void 0 : _b.forEach((serviceName) => { serviceName = serviceName.toLowerCase(); if (this.services[serviceName] === undefined) { this.services[serviceName] = [policyInfo]; } else { this.services[serviceName].push(policyInfo); } }); (_c = args.frameworks) === null || _c === void 0 ? void 0 : _c.forEach((frameworkName) => { frameworkName = frameworkName.toLowerCase(); if (this.frameworks[frameworkName] === undefined) { this.frameworks[frameworkName] = [policyInfo]; } else { this.frameworks[frameworkName].push(policyInfo); } }); (_d = args.topics) === null || _d === void 0 ? void 0 : _d.forEach((topicName) => { topicName = topicName.toLowerCase(); if (this.topics[topicName] === undefined) { this.topics[topicName] = [policyInfo]; } else { this.topics[topicName].push(policyInfo); } }); if (args.severity && args.severity.length > 0) { const severityName = args.severity.toLowerCase(); if (this.severities[severityName] === undefined) { this.severities[severityName] = [policyInfo]; } else { this.severities[args.severity].push(policyInfo); } } return args.resourceValidationPolicy; } /** * This function is used by policy module to register information about themselves. * This can be later used to display statistics about included packages as part of * a policy-pack. * * This function is to be used if you are authoring your own Compliance Policies. * * @param name Name of the policy module as stored in `package.json` * @param version The module version as stored in `package.json` * @returns returns the package version as a string */ registerPolicyModule(name, version) { this.registeredModules.push({ name: name, version: version, }); return version; } } exports.PolicyManager = PolicyManager; /** * An instance of the `PolicyManager` class. * * Use this instance to manipulate (register, select...) policies. */ exports.policyManager = new PolicyManager(); /** * The function `valToBoolean()` is a helper because some boolean properties * require a string type instead of a boolean type. * The idea for this function is to allow compatibility across multiple versions * of the same provider in case a property type changes from string to boolean. * * @link https://github.com/pulumi/pulumi-aws/issues/2257 * @param val A value to convert into a boolean. * @returns The boolean value, or `undefined` is the conversion isn't possible. */ // Help to help with boolean stored as string // see https://github.com/pulumi/pulumi-aws/issues/2257 function valToBoolean(val) { switch (typeof val) { case "undefined": return undefined; case "boolean": return val; case "string": switch (val.toLowerCase()) { case "false": return false; case "true": return true; default: return undefined; } default: return undefined; } } exports.valToBoolean = valToBoolean; (() => { // This anonymous function is used to dynamically load official // policies that match the pattern shown below. During the loading // process, this policy-manager version is compared to the one // included in the policy package. An exception is thrown if the // values don't match. // This is done to ensure that only a single policy-manager exists // and all policies are registered in that unique instance. (0, plugin_1.loadPlugins)(["@pulumi/*-compliance-policies"]); })();