@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
JavaScript
"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"]);
})();