@pulumi/compliance-policies-unit-test-helpers
Version:
This repository contains a growing set of Compliance Policies to validate your infrastructure using Pulumi's Crossguard Policy-as-Code framework.
967 lines (966 loc) • 42.9 kB
JavaScript
;
// 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);
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.assertPolicyManagerVersion = exports.assertCodeQuality = exports.assertSelectionEnforcementLevel = exports.assertNoDoubleSelection = exports.assertExpectedRemainingPolicyCount = exports.assertHasNoRemainingPolicies = exports.assertHasAllRemainingPolicies = exports.assertHasRemainingPolicies = exports.assertHasRegisteredPolicies = exports.assertResourcePolicyRegistrationDetails = exports.assertResourcePolicyDescription = exports.assertResourcePolicyEnforcementLevel = exports.assertResourcePolicyName = exports.assertResourcePolicyIsRegistered = exports.assertHasStackViolation = exports.assertNoStackViolations = exports.assertNoResourceViolations = exports.assertHasResourceViolation = exports.daysFromNow = exports.createStackValidationArgs = exports.createResourceValidationArgs = void 0;
__exportStar(require("./version"), exports);
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const policymanager = require("@pulumi/compliance-policy-manager");
const assert = require("assert");
const empytOptions = {
protect: false,
ignoreChanges: [],
aliases: [],
customTimeouts: {
createSeconds: 0,
updateSeconds: 0,
deleteSeconds: 0,
},
additionalSecretOutputs: [],
};
/**
* The function createResourceValidationArgs() will create a ResourceValidationArgs using the `type` from
* the specified `resourceClass` and `props` returned from the specified `argsFactory`.
* The return type of the `argsFactory` is the unwrapped args bag for the resource, inferred
* from the resource's constructor parameters.
*
* @param resourceClass The provider resource class.
* @param args The resource class arguments to create the resource.
* @param config The policy configuration.
* @returns A resource validation used to assert against a policy.
*/
function createResourceValidationArgs(resourceClass, // eslint-disable-line @typescript-eslint/no-shadow
args, config, resourceName) {
const type = resourceClass.__pulumiType;
if (typeof type !== "string") {
assert.fail("Could not determine Pulumi type from resourceClass.");
}
return {
type: type,
props: args,
urn: "unknown",
name: resourceName || "unknown",
opts: empytOptions,
isType: (cls) => isTypeOf(type, resourceClass),
asType: (cls) => isTypeOf(type, cls) ? args : undefined,
getConfig: () => (config || {}),
};
}
exports.createResourceValidationArgs = createResourceValidationArgs;
/**
* The function createStackValidationArgs() will create a StackValidationArgs, simulating a stack that has a
* single resource with the provided type and properties.
*
* @param resourceClass The provider resource class.
* @param props The outputs of the resource.
* @param config The stack validation configuration.
* @returns A stack validation used to assert against a stack validation policy.
*/
function createStackValidationArgs(resourceClass, props, config, resourceName) {
const type = resourceClass.__pulumiType;
if (typeof type !== "string") {
assert.fail("Could not determine Pulumi type from resourceClass.");
}
const testResource = {
type: type,
props: props,
urn: "unknown",
name: resourceName || "unknown",
opts: empytOptions,
dependencies: [],
propertyDependencies: {},
isType: (cls) => isTypeOf(type, cls),
asType: (cls) => isTypeOf(type, cls) ? props : undefined,
};
return {
resources: [testResource],
getConfig: () => (config || {}),
};
}
exports.createStackValidationArgs = createStackValidationArgs;
/**
* The function runResourcePolicy() will run some basic checks for a policy's metadata, and then
* execute its rules with the provided type and properties.
*
* @param resPolicy A resource validation policy to evaluate.
* @param args The resource validation policy arguments.
* @returns A promise of a policy violation.
*/
function runResourcePolicy(resPolicy, args) {
return __awaiter(this, void 0, void 0, function* () {
const violations = [];
const report = (message, urn) => {
violations.push({ message: message, urn: urn });
};
const validations = Array.isArray(resPolicy.validateResource)
? resPolicy.validateResource
: [resPolicy.validateResource];
for (const validation of validations) {
if (!validation) {
throw Error("validateResource must be a function or array of functions.");
}
yield Promise.resolve(validation(args, report));
}
return violations;
});
}
/**
* The function runStackPolicy() will run some basic checks for a policy's metadata, and then
* execute its rules with the provided type and properties.
*
* @param stackPolicy A stack validation policy to evaluate.
* @param args The stack validation policy arguments.
* @returns A promise of a policy violation.
*/
function runStackPolicy(stackPolicy, args) {
return __awaiter(this, void 0, void 0, function* () {
const violations = [];
const report = (message, urn) => {
violations.push({ message: message, urn: urn });
};
yield Promise.resolve(stackPolicy.validateStack(args, report));
return violations;
});
}
/**
* The function assertNoViolations() runs the policy and confirms no violations were found.
*
* @param allViolations An array of all policy violations.
*/
function assertNoViolations(allViolations) {
if (allViolations && allViolations.length > 0) {
for (const violation of allViolations) {
const urnSuffix = violation.urn ? `(URN=${violation.urn})` : "";
console.log(`VIOLATION: ${violation.message} ${urnSuffix}`);
}
assert.fail("got violations but wasn't expecting any.");
}
}
/**
* The function assertHasViolation() runs the policy and confirms the expected violation is reported.
*
* @param allViolations An array of all policy violations.
* @param wantViolation An array of expected policy violations.
*/
function assertHasViolation(allViolations, wantViolation) {
if (!allViolations || allViolations.length === 0) {
assert.fail("no violations reported, but expected one");
}
else {
for (const reportedViolation of allViolations) {
// If we expect a specific URN, require that in the reported violation.
// The converse is not true, we allow test authors to omit the URN
// even if it is included in the matched violation.
if (wantViolation.urn && !reportedViolation.urn) {
continue;
}
const messageMatches = reportedViolation.message.indexOf(wantViolation.message) !== -1;
let urnMatches = true;
if (reportedViolation.urn && wantViolation.urn) {
urnMatches = reportedViolation.urn.indexOf(wantViolation.urn) !== -1;
}
if (messageMatches && urnMatches) {
// Success! We found the violation we were looking for.
return;
}
}
// Print all reported violations for easier debugging of failing tests.
console.log("Reported Violations:");
for (const reported of allViolations) {
console.log(`urn: ${reported.urn} - message: ${reported.message}`);
}
assert.fail(`violation with substrings message:'${wantViolation.message}' urn:'${wantViolation.urn}' not found.'`);
}
}
/**
* The function daysFromNow() returns "now", d days in the future or past.
*
* @param days Number of days.
* @returns A Date object.
*/
function daysFromNow(days) {
const date = new Date();
date.setDate(date.getDate() + days);
return date;
}
exports.daysFromNow = daysFromNow;
/**
* The function asserts the resource provided reports a violation.
*
* @param resPolicy A resource validation policy.
* @param args Arguments for the resource validation policy.
* @param wantViolation An expected policy violation.
*/
function assertHasResourceViolation(resPolicy, args, wantViolation) {
return __awaiter(this, void 0, void 0, function* () {
const allViolations = yield runResourcePolicy(resPolicy, args);
assertHasViolation(allViolations, wantViolation);
});
}
exports.assertHasResourceViolation = assertHasResourceViolation;
/**
* The function asserts the resource provided does NOT reports any violation.
*
* @param resPolicy A resource validation policy.
* @param args Arguments for the resource validation policy.
*/
function assertNoResourceViolations(resPolicy, args) {
return __awaiter(this, void 0, void 0, function* () {
const allViolations = yield runResourcePolicy(resPolicy, args);
assertNoViolations(allViolations);
});
}
exports.assertNoResourceViolations = assertNoResourceViolations;
/**
* The function asserts the stack provided does NOT report any violation.
*
* @param stackPolicy A stack validation policy.
* @param args Arguments for the stack validation policy.
*/
function assertNoStackViolations(stackPolicy, args) {
return __awaiter(this, void 0, void 0, function* () {
const allViolations = yield runStackPolicy(stackPolicy, args);
assertNoViolations(allViolations);
});
}
exports.assertNoStackViolations = assertNoStackViolations;
/**
* The function asserts the stack provided report a violation.
*
* @param stackPolicy A stack validation policy.
* @param args Arguments for the stack validation policy.
* @param wantViolation An expected violation.
*/
function assertHasStackViolation(stackPolicy, args, wantViolation) {
return __awaiter(this, void 0, void 0, function* () {
const allViolations = yield runStackPolicy(stackPolicy, args);
assertHasViolation(allViolations, wantViolation);
});
}
exports.assertHasStackViolation = assertHasStackViolation;
/**
* The function asserts the policy has been registered.
*
* @param policy A resource validation policy.
*/
function assertResourcePolicyIsRegistered(policy) {
if (!policymanager.policyManager.getPolicyByName(policy.name)) {
assert.fail(`Policy ${policy.name} is not registered.`);
}
}
exports.assertResourcePolicyIsRegistered = assertResourcePolicyIsRegistered;
/**
* The function asserts the policy name is as expected.
*
* @param policy A resource validation policy.
* @param name The resource validation policy expected name.
*/
function assertResourcePolicyName(policy, name) {
/**
* Check the policy name follows the Compliance Policies expected pattern.
*/
const localRE = /([a-z]{1}[\da-z\-]+[\da-z]{1})/g;
/**
* Check the policy name is compliant with the Pulumi Cloud service.
*/
const serviceRE = /^[a-zA-Z0-9\-_\.]{1,300}$/;
if (!isLowerCase(policy.name)) {
assert.fail(`Policy name '${policy.name}' should be in lower case.`);
}
if (policy.name !== name) {
assert.fail(`Policy name '${policy.name}' isn't matching the expected name '${name}'.`);
}
let nameMatch = policy.name.match(localRE);
if (!nameMatch) {
assert.fail(`Policy name '${policy.name}' should match '${localRE}' (#1)`);
}
else {
if (nameMatch.length !== 1) {
assert.fail(`Policy name '${policy.name}' should match '${localRE}' (#2)`);
}
}
nameMatch = policy.name.match(serviceRE);
if (!nameMatch) {
assert.fail(`Policy name '${policy.name}' should match '${serviceRE}' so it's accepted by the Pulumi service (#1)`);
}
else {
if (nameMatch.length !== 1) {
assert.fail(`Policy name '${policy.name}' should match '${serviceRE}' so it's accepted by the Pulumi service (#2)`);
}
}
/**
* @link https://github.com/pulumi/pulumi-service/issues/14939
*/
if (policy.name.length > 300) {
assert.fail(`Policy name '${policy.name}' can't be more than 300 characters long.`);
}
}
exports.assertResourcePolicyName = assertResourcePolicyName;
/**
* The function asserts the policy has the correct enforcementLevel.
*
* @param policy A resource validation policy.
*/
function assertResourcePolicyEnforcementLevel(policy) {
if (policy.enforcementLevel !== "advisory") {
assert.fail(`Policy name '${policy.name}' should have its enforcementLevel set to 'advisory'.`);
}
}
exports.assertResourcePolicyEnforcementLevel = assertResourcePolicyEnforcementLevel;
/**
* The function asserts the policy has a sentence as a description.
*
* @param policy A resource validation policy.
*/
function assertResourcePolicyDescription(policy) {
if (!policy.description) {
assert.fail(`Policy name '${policy.name}' should have a description.`);
}
else {
const sentenceEndGrouping = /([.?!])(?:\s+|$)/gmu;
const puntuations = policy.description.match(sentenceEndGrouping);
if (puntuations === null) {
assert.fail(`The policy '${policy.name}' description requires a complete sentence.`);
}
else {
const sentences = policy.description.split(/[.?!](?:\s+|$)/u).map((sentence, idx) => {
// re-add the punctuation back to the sentence
if (puntuations[idx]) {
return `${sentence}${puntuations[idx]}`;
}
return sentence;
});
const re = /^[A-Z].*[.?!]/gu;
for (let x = 0; x < sentences.length; x++) {
if (sentences[x]) {
const descriptionMatch = sentences[x].match(re);
if (!descriptionMatch) {
assert.fail(`The policy '${policy.name}' description requires a complete sentence.`);
}
}
}
}
}
}
exports.assertResourcePolicyDescription = assertResourcePolicyDescription;
/**
* The function asserts the policy has the expected policy metadata.
*
* @param policy A resource validation policy.
* @param metadata The expected policy metadata.
*/
function assertResourcePolicyRegistrationDetails(policy, metadata) {
const registeredPolicy = policymanager.policyManager.getPolicyByName(policy.name);
if (!registeredPolicy) {
assert.fail(`Policy ${policy.name} is not registered.`);
}
if (registeredPolicy) {
/**
* Perform checks on Frameworks.
*/
if (registeredPolicy.policyMetadata.frameworks && metadata.frameworks
&& registeredPolicy.policyMetadata.frameworks.length && metadata.frameworks.length) {
if (!compareArray(registeredPolicy.policyMetadata.frameworks, metadata.frameworks)) {
assert.fail(`Policy ${policy.name} 'frameworks' don't match.`);
}
}
else {
if ((!registeredPolicy.policyMetadata.frameworks && metadata.frameworks) ||
(registeredPolicy.policyMetadata.frameworks && !metadata.frameworks)) {
assert.fail(`Policy ${policy.name} 'frameworks' don't match.`);
}
}
/**
* Perform checks on Services.
*/
if (registeredPolicy.policyMetadata.vendors && !registeredPolicy.policyMetadata.vendors.includes("kubernetes")) {
if (registeredPolicy.policyMetadata.services && registeredPolicy.policyMetadata.services.length) {
if (registeredPolicy.policyMetadata.services.length > 1) {
assert.fail(`Policy ${policy.name} should be associated to one service only.`);
}
}
}
if (registeredPolicy.policyMetadata.services && metadata.services
&& registeredPolicy.policyMetadata.services.length && metadata.services.length) {
if (!compareArray(registeredPolicy.policyMetadata.services, metadata.services)) {
assert.fail(`Policy ${policy.name} 'services' don't match.`);
}
}
else {
if ((!registeredPolicy.policyMetadata.services && metadata.services) ||
(registeredPolicy.policyMetadata.services && !metadata.services)) {
assert.fail(`Policy ${policy.name} 'services' don't match.`);
}
}
/**
* Perform checks on Severities.
*/
if (registeredPolicy.policyMetadata.severity) {
switch (registeredPolicy.policyMetadata.severity) {
case "low":
case "medium":
case "high":
case "critical":
break;
default:
assert.fail(`Policy ${policy.name} 'severity' isn't valid ('low', 'medium', 'high', 'critical').`);
}
}
if (registeredPolicy.policyMetadata.severity && metadata.severity) {
if (registeredPolicy.policyMetadata.severity !== metadata.severity) {
assert.fail(`Policy ${policy.name} 'severity' don't match.`);
}
}
else {
if ((!registeredPolicy.policyMetadata.severity && metadata.severity) ||
(registeredPolicy.policyMetadata.severity && !metadata.severity)) {
assert.fail(`Policy ${policy.name} 'severity' don't match.`);
}
}
/**
* Perform checks on Topics.
*/
if (registeredPolicy.policyMetadata.topics && metadata.topics
&& registeredPolicy.policyMetadata.topics.length && metadata.topics.length) {
if (!compareArray(registeredPolicy.policyMetadata.topics, metadata.topics)) {
assert.fail(`Policy ${policy.name} 'topics' don't match.`);
}
}
else {
if ((!registeredPolicy.policyMetadata.topics && metadata.topics) ||
(registeredPolicy.policyMetadata.topics && !metadata.topics)) {
assert.fail(`Policy ${policy.name} 'topics' don't match.`);
}
}
/**
* Perform checks on Vendors.
*/
if (registeredPolicy.policyMetadata.vendors && registeredPolicy.policyMetadata.vendors.length) {
if (registeredPolicy.policyMetadata.vendors.length > 1) {
assert.fail(`Policy ${policy.name} should be associated to one vendor only.`);
}
}
if (registeredPolicy.policyMetadata.vendors && metadata.vendors
&& registeredPolicy.policyMetadata.vendors.length && metadata.vendors.length) {
if (!compareArray(registeredPolicy.policyMetadata.vendors, metadata.vendors)) {
assert.fail(`Policy ${policy.name} 'vendors' don't match.`);
}
}
else {
if ((!registeredPolicy.policyMetadata.vendors && metadata.vendors) ||
(registeredPolicy.policyMetadata.vendors && !metadata.vendors)) {
assert.fail(`Policy ${policy.name} 'vendors' don't match.`);
}
}
}
}
exports.assertResourcePolicyRegistrationDetails = assertResourcePolicyRegistrationDetails;
/**
* The function asserts some policies have been registered.
*/
function assertHasRegisteredPolicies() {
if (policymanager.policyManager.getSelectionStats().policyCount === 0) {
assert.fail(`Didn't find any registered policies.`);
}
}
exports.assertHasRegisteredPolicies = assertHasRegisteredPolicies;
/**
* The function asserts 1 or more policies are up for selection.
*/
function assertHasRemainingPolicies() {
if (policymanager.policyManager.getSelectionStats().remainingPolicyCount === 0) {
assert.fail(`Didn't find any remaining policies.`);
}
}
exports.assertHasRemainingPolicies = assertHasRemainingPolicies;
/**
* The function asserts the number of selectable policies is the same as the total of all registered policies.
*/
function assertHasAllRemainingPolicies() {
if (policymanager.policyManager.getSelectionStats().policyCount < 1) {
assert.fail(`Registered policies count and remaining policies count don't match. ${policymanager.policyManager.getSelectionStats().remainingPolicyCount} policies but ${policymanager.policyManager.getSelectionStats().policyCount} are registered.`);
}
if (policymanager.policyManager.getSelectionStats().remainingPolicyCount !== policymanager.policyManager.getSelectionStats().policyCount) {
assert.fail(`Registered policies count and remaining policies count don't match. ${policymanager.policyManager.getSelectionStats().remainingPolicyCount} policies but ${policymanager.policyManager.getSelectionStats().policyCount} are registered.`);
}
}
exports.assertHasAllRemainingPolicies = assertHasAllRemainingPolicies;
/**
* The function asserts no more policies are selectable.
*/
function assertHasNoRemainingPolicies() {
if (policymanager.policyManager.getSelectionStats().remainingPolicyCount !== 0) {
assert.fail(`Found remaining policies but expected none.`);
}
}
exports.assertHasNoRemainingPolicies = assertHasNoRemainingPolicies;
/**
* The function asserts an expected number of policies is selectable.
*
* @param expectedtedRemainingPolicyCount The expected number of remaining policies.
*/
function assertExpectedRemainingPolicyCount(expectedtedRemainingPolicyCount) {
if (policymanager.policyManager.getSelectionStats().remainingPolicyCount !== expectedtedRemainingPolicyCount) {
assert.fail(`Expected remaining policy counts don't match. Found ${policymanager.policyManager.getSelectionStats().remainingPolicyCount} but expected ${expectedtedRemainingPolicyCount}.`);
}
}
exports.assertExpectedRemainingPolicyCount = assertExpectedRemainingPolicyCount;
/**
* The function asserts no policies are selected twice.
*
* @param filterPolicy A policy selection filter.
*/
function assertNoDoubleSelection(filterPolicy) {
policymanager.policyManager.resetPolicySelector();
const firstSelection = policymanager.policyManager.selectPolicies(filterPolicy);
const secondSelection = policymanager.policyManager.selectPolicies(filterPolicy);
if (secondSelection.length > 0) {
assert.fail(`Some policies haven't been returned after they'd been already selected.`);
}
policymanager.policyManager.resetPolicySelector();
}
exports.assertNoDoubleSelection = assertNoDoubleSelection;
/**
* The function asserts the enforcementLevel is applied when selecting policies.
*
* @param filterPolicy A policy selection filter.
* @param enforcementLevel An expected policy enforcement level.
*/
function assertSelectionEnforcementLevel(filterPolicy, enforcementLevel) {
policymanager.policyManager.resetPolicySelector();
const policySelection = policymanager.policyManager.selectPolicies(filterPolicy, enforcementLevel);
policySelection.forEach((policy) => {
if (policy.enforcementLevel !== enforcementLevel) {
assert.fail(`Policy enforcementLevel not set on during policy selection.`);
}
});
policymanager.policyManager.resetPolicySelector();
}
exports.assertSelectionEnforcementLevel = assertSelectionEnforcementLevel;
/**
* An array of allowed policy verbs. This is used to improve consistency.
*/
const allowedPolicyVerbs = [
"missing",
"disallow",
"enable",
"disable",
"configure",
"enforce",
];
/**
* This function assert the code quality of a policy source file.
*
* @param suiteName The MochaJS suite name.
* @param suiteFile The MochaJS source file for the current suite. Using `__filename` is usually sufficient.
*/
function assertCodeQuality(suiteName, suiteFile) {
var _a;
const sourceFileDetails = parseSourceFile(suiteName, suiteFile);
// https://astexplorer.net/#/gist/8542f6a83839e9db21d7c27bc482e828/7a1341ec228bcd5d574324039ff7fab84244dd6f
if (sourceFileDetails.error) {
assert.fail(sourceFileDetails.error);
}
if (!sourceFileDetails.policyVarName) {
assert.fail("Unable to determine policy variable name.");
}
if (!sourceFileDetails.parserResults) {
assert.fail(`Failed to parse file ${sourceFileDetails.sourceFile}.`);
}
const parserResults = sourceFileDetails.parserResults;
let policyDetails = {};
for (let codeBlockIndex = 0; codeBlockIndex < parserResults.program.body.length; codeBlockIndex++) {
const node = parserResults.program.body[codeBlockIndex];
if (node.type === "ExportNamedDeclaration") {
policyDetails = getPolicyDetails(node, sourceFileDetails.policyVarName);
if (!policyDetails.error && !policyDetails.name) {
/*
* No error is reported, so the processed "ExportNamedDeclaration" is not the one
* for the current policy. Let's continue to the next one then.
*/
continue;
}
policyDetails.sourceFileDetails = sourceFileDetails;
break;
}
}
if (!policyDetails.error && !policyDetails.name) {
assert.fail(`Unable to locate the policy code in ${sourceFileDetails.sourceFile}`);
}
if (policyDetails.error) {
assert.fail(policyDetails.error);
}
if (!((_a = policyDetails.comment) === null || _a === void 0 ? void 0 : _a.includes(policyDetails.description))) {
assert.fail("The jsDoc description isn't matching the policy's description.");
}
if (!policyDetails.comment.toLowerCase().includes(`@severity ${policyDetails.severity}`)) {
assert.fail("The policy's severity isn't matching the one in the jsDoc comment.");
}
if (!policyDetails.comment.toLowerCase().includes(`@topics ${policyDetails.topics || "none"}`)) {
assert.fail("The policy's topics list isn't matching the one in the jsDoc comment.");
}
if (!policyDetails.comment.toLowerCase().includes(`@frameworks ${policyDetails.frameworks || "none"}`)) {
assert.fail("The policy's frameworks list isn't matching the one in the jsDoc comment.");
}
checkPolicyVerbDetails(policyDetails);
if (policyDetails.error) {
assert.fail(policyDetails.error);
}
}
exports.assertCodeQuality = assertCodeQuality;
/**
* This function takes a suite name and returns parsed source file.
*
* @param suiteName The MochaJS suite name.
* @returns The parsed corresponding source file.
*/
function parseSourceFile(suiteName, suiteFile) {
const sourceFileDetails = {};
sourceFileDetails.suiteName = suiteName;
sourceFileDetails.suiteFile = suiteFile;
if (!suiteName) {
sourceFileDetails.error = "The test suite name isn't present.";
return sourceFileDetails;
}
if (!suiteFile) {
sourceFileDetails.error = "The test suite filename isn't present.";
return sourceFileDetails;
}
const splitResourceSuiteName = suiteName.split("."); // [ 'aws', 'alb', 'Listener', 'disallowUnencryptedTraffic' ]
sourceFileDetails.policyVarName = splitResourceSuiteName[splitResourceSuiteName.length - 1]; // 'disallowUnencryptedTraffic' = [ 'aws', 'alb', 'Listener', 'disallowUnencryptedTraffic' ]
const relativeSpecFile = splitResourceSuiteName.join("/") + ".spec.ts"; // "aws/alb/Listener/disallowUnencryptedTraffic.spec.ts"
const testsBasePath = suiteFile.replace(`/${relativeSpecFile}`, ""); // "/home/aureq/work/github.com/pulumi/policy-packs/policies/tests"
const policiesBasePath = path.dirname(testsBasePath); // "/home/aureq/work/github.com/pulumi/policy-packs/policies"
const splitSuite = suiteFile.replace(`${testsBasePath}/`, "").replace(".spec.ts", "").split("/"); // [ 'aws', 'alb', 'Listener', 'disallowUnencryptedTraffic' ]
/**
* Ensure the suite is located in the correct file.
*/
if (!compareArray(splitSuite, splitResourceSuiteName)) {
sourceFileDetails.error = `The test suite named '${suiteName}' isn't located in the correct spec file '${suiteFile}'. It should be '${testsBasePath}/${relativeSpecFile}.spec.ts'.`;
return sourceFileDetails;
}
if (!sourceFileDetails.policyVarName) {
sourceFileDetails.error = "Unable to determine the policy variable name. Is the suite name in the correct format?";
return sourceFileDetails;
}
sourceFileDetails.sourceFile = `${policiesBasePath}/${splitResourceSuiteName.join("/")}.ts`;
sourceFileDetails.parserResults = parser.parse(fs.readFileSync(sourceFileDetails.sourceFile, "utf-8"), {
attachComment: true,
sourceType: "module",
sourceFilename: sourceFileDetails.sourceFile,
plugins: [
"typescript",
],
});
return sourceFileDetails;
}
/**
* This functions extracts the necessary details related to the current registered policy.
*
* @param objectExpression An object expression as it is provided to `policymanager.policyManager.registerPolicy()`.
* @returns An array of information related to the current policy.
*/
function getPolicyDetails(node, policyVarName) {
const policyDetails = {};
if (!node.declaration || node.declaration.type !== "VariableDeclaration") {
return policyDetails;
}
if (node.declaration.declarations.length !== 1) {
return policyDetails;
}
if (node.declaration.declarations[0].id.type === "Identifier") {
if (node.declaration.declarations[0].id.name === policyVarName) {
/*
* We have a match with the exported variable so we're going to process it.
*/
if (!node.leadingComments) {
policyDetails.error = "The policy is missing its jsDoc comment.";
return policyDetails;
}
const jsDocDetails = getPolicyComment(node);
if (jsDocDetails.error) {
assert.fail(jsDocDetails.error);
}
policyDetails.comment = jsDocDetails.comment;
/*
* This will most likely blow up in my face sooner or later because this
* feels way too brittle. I just don't know how to parse this in a more
* flexible way - for now.
*/
if (node.declaration.declarations[0].init &&
node.declaration.declarations[0].init.type === "CallExpression" &&
node.declaration.declarations[0].init.callee.type === "MemberExpression" &&
node.declaration.declarations[0].init.callee.property.type === "Identifier" &&
node.declaration.declarations[0].init.callee.property.name === "registerPolicy" &&
node.declaration.declarations[0].init.arguments[0].type === "ObjectExpression") {
const objectExpression = node.declaration.declarations[0].init.arguments[0];
for (let oepIndex = 0; oepIndex < objectExpression.properties.length; oepIndex++) {
if (objectExpression.properties[oepIndex].type !== "ObjectProperty") {
continue;
}
const objectProperty = objectExpression.properties[oepIndex];
switch (objectProperty.value.type) {
case "ObjectExpression": // the `resourceValidationPolicy`
if (objectProperty.key.type === "Identifier" && objectProperty.key.name.includes("resourceValidationPolicy")) {
const p = getPolicyCodeDetails(objectProperty.value.properties);
policyDetails.name = p.name;
policyDetails.description = p.description;
}
break;
case "ArrayExpression": // vendors[] || services[] || frameworks[] || topics[]
if (objectProperty.key.type === "Identifier") {
switch (objectProperty.key.name) {
case "frameworks":
policyDetails.frameworks = extractArrayToString(objectProperty.value);
break;
case "topics":
policyDetails.topics = extractArrayToString(objectProperty.value);
break;
default:
break;
}
}
break;
case "StringLiteral": // Policy Severity
policyDetails.severity = objectProperty.value.value.toLowerCase(); // this needs to be lowercase as the value is made case insensitive
break;
default:
continue;
}
}
}
}
}
return policyDetails;
}
/**
* From an ArrayExpression object, returns a string of the values.
*
* @param arrayExpressionObject The ArrayExpression containing the multiple strings.
* @returns A string representing the values of the provided ArrayExpression, or "none" is there was no values.
*/
function extractArrayToString(arrayExpressionObject) {
const items = [];
for (let i = 0; i < arrayExpressionObject.elements.length; i++) {
const element = arrayExpressionObject.elements[i];
if (!element || element.type !== "StringLiteral") {
continue;
}
items.push(element.value.toLowerCase());
}
return items.sort().join(", ").toLowerCase();
}
/**
* This function checks for the policy verb (missing, disallow...) and the consistency of the policy variable name.
*
* @param policyDetails The `policyDetails` to process. The `policyDetails` should have `.sourceFileDetails` set.
* @returns An updated `policyDetails`.
*/
function checkPolicyVerbDetails(policyDetails) {
if (!policyDetails.sourceFileDetails || !policyDetails.sourceFileDetails.suiteName || !policyDetails.name) {
policyDetails.error = "Missing or incomplete policy details. Unable to process.";
return policyDetails;
}
const splitResourceSuiteName = policyDetails.sourceFileDetails.suiteName.split(".");
splitResourceSuiteName.pop();
const basePolicyName = splitResourceSuiteName.join("-").toLowerCase();
const policyName = policyDetails.name.replace(`${basePolicyName}-`, "");
const splitPolicyName = policyName.split("-");
if (!allowedPolicyVerbs.includes(splitPolicyName[0])) {
policyDetails.error = `The policy verb '${splitPolicyName[0]}' is not allowed.`;
return policyDetails;
}
/*
* intentionally skip the policy verb as it's lower case in the policy
* variable name.
*/
const splitPolicyWords = [...splitPolicyName];
for (let wordIndex = 1; wordIndex < splitPolicyWords.length; wordIndex++) {
splitPolicyWords[wordIndex] = capitalize(splitPolicyWords[wordIndex]);
}
const computedPolicyVarName = splitPolicyWords.join("");
if (computedPolicyVarName !== policyDetails.sourceFileDetails.policyVarName) {
policyDetails.error = `The policy variable name ${policyDetails.sourceFileDetails.policyVarName} doesn't match with the expected variable name ${computedPolicyVarName}.`;
return policyDetails;
}
return policyDetails;
}
/**
* This function get the policy code details (name and description).
*
* @param properties The properties array that's passed to `resourceValidationPolicy`.
* @returns An array of strings. [name, description].
*/
function getPolicyCodeDetails(properties) {
const policyDetails = {};
properties.forEach((property) => {
if (property.type !== "ObjectProperty" || property.key.type !== "Identifier") {
return;
}
switch (property.key.name) {
case "name":
if (property.value.type === "StringLiteral") {
policyDetails.name = property.value.value;
}
break;
case "description":
if (property.value.type === "StringLiteral") {
policyDetails.description = property.value.value;
}
break;
default:
}
});
return policyDetails;
}
/**
* Gets the jsDoc comment block for the current code block.
*
* @param comments An array of Comment containing the leading comments of the current code block.
* @returns Returns the single jsDoc comment block found otherwise will `assert.fail()`.
*/
function getPolicyComment(node) {
const policyCommentDetails = {};
if (!node.leadingComments) {
policyCommentDetails.error = "The policy should have a jsDoc comment block to describe the policy.";
return policyCommentDetails;
}
let jsDocCommentBlockIndex = -1;
let jsDocCommentBlocksCount = 0;
let commentBlocksCount = 0;
let commentLinesCount = 0;
for (let commentIndex = 0; commentIndex < node.leadingComments.length; commentIndex++) {
const comment = node.leadingComments[commentIndex];
if (comment.type === "CommentBlock") {
if (comment.value.startsWith("*\n")) {
/*
* This appears to be a proper jsDoc comment block because it starts
* with '/**' (@babel/parser remove the initial '/*')
*/
jsDocCommentBlocksCount++;
jsDocCommentBlockIndex = commentIndex;
}
else {
/*
* this is a generic comment block and not jsDoc one.
*/
commentBlocksCount++;
}
}
else {
commentLinesCount++;
}
}
if (commentLinesCount > 0) {
policyCommentDetails.error = "The policy should not use comment lines leading to the policy declaration.";
return policyCommentDetails;
}
if (commentBlocksCount > 0) {
policyCommentDetails.error = "The policy should not use comment blocks leading to the policy declaration.";
return policyCommentDetails;
}
if (jsDocCommentBlocksCount > 1) {
policyCommentDetails.error = "The policy should not use more than one jsDoc comment blocks leading to the policy declaration.";
return policyCommentDetails;
}
policyCommentDetails.comment = node.leadingComments[jsDocCommentBlockIndex].value;
return policyCommentDetails;
}
/**
* This function asserts the supplied Policy Manager package version is identical
* to the one included in this package.
*
* @param version The Policy Manager package version.
*/
function assertPolicyManagerVersion(version) {
if (version !== policymanager.version) {
assert.fail(`The 'unit-test-helpers' (${policymanager.version}) and your package (${version}) should depend on the same version of 'policy-manager'`);
}
}
exports.assertPolicyManagerVersion = assertPolicyManagerVersion;
/**
* This function converts the 1st character to a upper case character.
*
* @param str The input string.
* @returns A capitalized string.
*/
function capitalize(str) {
return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function (match, index) {
if (+match === 0) {
return ""; // or if (/\s+/.test(match)) for white spaces
}
return index === 0 ? match.toUpperCase() : match.toLowerCase();
});
}
/**
* Determine whether the given `input` is a string in lowercase.
*
* @param input A string to evaluate.
* @returns `true` if the provided input is all lower case, otherwise `false`.
*/
function isLowerCase(input) {
return input === String(input).toLowerCase();
}
/**
* This function compares 2 arrays.
* See for context: https://stackoverflow.com/questions/7837456/how-to-compare-arrays-in-javascript/.
*
* @param array1 First array to compare.
* @param array2 Second to compare.
* @returns `true` is the 2 arrays are identical, otherwise `false`.
*/
function compareArray(array1, array2) {
const array2Sorted = array2.slice().sort();
return (array1.length === array2.length && array1.slice().sort().every((value, index) => {
return value === array2Sorted[index];
}));
}
/**
* Helper to check if `type` is the type of `resourceClass`.
*
* @param type A resource type as a string.
* @param resourceClass A resource class.
* @returns `true` if the `type` and the resource class match, otherwise `false`.
*/
function isTypeOf(type, resourceClass) {
const isInstance = resourceClass.isInstance;
return isInstance &&
typeof isInstance === "function" &&
isInstance({ __pulumiType: type }) === true;
}