From a9a19d68b3f3f7661dd77eb916078d0bf837b0fa Mon Sep 17 00:00:00 2001 From: Bryan Latten Date: Mon, 1 Jun 2020 17:17:45 -0400 Subject: [PATCH] Minor refactor: including airbnb-base eslint --- .eslintrc.json | 6 +- Dockerfile | 2 +- index.js | 89 ++-- lib/policy.js | 457 ---------------- lib/policyengine.js | 414 +++++++++++++++ package-lock.json | 1054 ++++++++++++++++++++++++++----------- package.json | 13 +- test/policy.js | 509 ------------------ test/policyengine.test.js | 463 ++++++++++++++++ 9 files changed, 1688 insertions(+), 1319 deletions(-) delete mode 100644 lib/policy.js create mode 100644 lib/policyengine.js delete mode 100644 test/policy.js create mode 100644 test/policyengine.test.js diff --git a/.eslintrc.json b/.eslintrc.json index dafaffd..7db9e44 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,5 +1,9 @@ { - "extends": "eslint:recommended", + "extends": "airbnb-base", + "env": { + "browser": false, + "node": true + }, "parser": "babel-eslint", "globals": { "require": true, diff --git a/Dockerfile b/Dockerfile index 1a4181a..2cd940d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM mhart/alpine-node:10 as base +FROM mhart/alpine-node:12 as base # Ensure application code makes it into the /app directory COPY ./ /app/ diff --git a/index.js b/index.js index fb61e49..e4808cb 100644 --- a/index.js +++ b/index.js @@ -1,20 +1,16 @@ #!/usr/bin/env node -/** - * Module dependencies. - */ - +const clc = require('cli-color'); +const fs = require('fs'); const getStdin = require('get-stdin'); +const path = require('path'); const program = require('commander'); const yaml = require('js-yaml'); -const fs = require('fs'); -const path = require('path'); +const policyEngine = require('./lib/policyengine.js'); const DEFAULT_POLICY = 'default_policy.yaml'; -var clc = require('cli-color'); - program .description('Checks a Docker image\'s properties against a policy') .option('-p, --policy ', 'image policy, defaults to ./default_policy.yaml') @@ -26,11 +22,10 @@ program .option('-r, --range ', 'low-high ports that are allowed') .option('--layers_max ', 'maximum number of filesystem layers') .option('--layers_warning ', 'warning number of filesystem layers') - .parse(process.argv); -var policyFile = program.policy || `./${DEFAULT_POLICY}`; -var policyPath = path.resolve(policyFile) +const policyFile = program.policy || `./${DEFAULT_POLICY}`; +const policyPath = path.resolve(policyFile); if (!fs.existsSync(policyPath)) { console.log('[%s] policy does not exist: %s', clc.redBright('Error'), clc.whiteBright(policyFile)); @@ -43,43 +38,40 @@ if (fs.statSync(policyPath).isDirectory()) { process.exit(1); } -getStdin().then(str => { - - if(!str.trim()) { - console.log('\n--- Docker inspect output required ---'); - program.help() +getStdin().then((str) => { + if (!str.trim()) { + console.log('\n--- Docker inspect output required ---\n'); + program.outputHelp(); process.exit(1); } - var input = JSON.parse(str); + const input = JSON.parse(str); if (!Array.isArray(input) || input[0] === undefined) { console.log('[%s] malformed input detected', clc.redBright('Error')); process.exit(1); } - var container = input[0]; + const container = input[0]; - console.log("\nScanning <%s>", container.Id); - console.log("Docker Build: " + container.DockerVersion); - console.log("Parent: " + container.Parent); - console.log("\nUsing policy <%s>\n", policyFile); + console.log('\nScanning <%s>', container.Id); + console.log(`Docker Build: ${container.DockerVersion}`); + console.log(`Parent: ${container.Parent}`); + console.log('\nUsing policy <%s>\n', policyFile); - var policy = require("./lib/policy.js")(); - var loadedPolicy; + let loadedPolicy; try { - loadedPolicy = yaml.safeLoad(fs.readFileSync(policyPath, 'utf8')) - } - catch (err) { + loadedPolicy = yaml.safeLoad(fs.readFileSync(policyPath, 'utf8')); + } catch (err) { console.log('[%s] unable to parse policy YAML:', clc.redBright('Error'), err.reason); process.exit(1); } - var overrideMsgs = []; - loadedPolicy = policy.applyOverrides(loadedPolicy, program, overrideMsgs); + const overrideMsgs = []; + loadedPolicy = policyEngine.applyOverrides(loadedPolicy, program, overrideMsgs); - overrideMsgs.forEach(function(text) { + overrideMsgs.forEach((text) => { console.log('<%s> %s', clc.cyanBright('Policy Override'), text); }); @@ -87,39 +79,36 @@ getStdin().then(str => { console.log(''); // Inserts a new line } - var testStatus = policy.execute(loadedPolicy, container); + const testStatus = policyEngine.execute(loadedPolicy, container); - testStatus.getMessages().forEach(function(msg) { + testStatus.getMessages().forEach((msg) => { + const severity = msg[0]; + const text = msg[1]; - var severity = msg[0]; - var text = msg[1]; - - switch(severity) { + switch (severity) { case 'exception': - console.log("[%s] %s", clc.magenta('EXCEPTION'), text); + console.log('[%s] %s', clc.magenta('EXCEPTION'), text); break; case 'success': - console.log("[%s] %s", clc.whiteBright('PASS'), text); + console.log('[%s] %s', clc.whiteBright('PASS'), text); break; case 'failure': - console.log("[%s] %s", clc.red('FAIL'), text); + console.log('[%s] %s', clc.red('FAIL'), text); break; case 'warning': - console.log("[%s] %s", clc.yellowBright('WARN'), text); + console.log('[%s] %s', clc.yellowBright('WARN'), text); break; default: - console.log("[%s %s", clc.blue('UNKOWN'), text); + console.log('[%s %s', clc.blue('UNKOWN'), text); break; - } // switch severity - + } }); - var testIsPassing = testStatus.isPassing(); - var testMessage = (testIsPassing) - ? clc.green('PASS') - : clc.redBright('FAIL'); + const testIsPassing = testStatus.isPassing(); + const testMessage = (testIsPassing) + ? clc.green('PASS') + : clc.redBright('FAIL'); - console.log("\nStatus [%s]\n", testMessage); + console.log('\nStatus [%s]\n', testMessage); process.exit(testIsPassing ? 0 : 1); - -}); // getStdin +}); diff --git a/lib/policy.js b/lib/policy.js deleted file mode 100644 index e85043a..0000000 --- a/lib/policy.js +++ /dev/null @@ -1,457 +0,0 @@ -module.exports = function() { - - return { - - emptyPolicy: function() { - - return { - size: {}, - labels: {}, - env_keys: {}, - volumes: {}, - ports: {}, - healthcheck: {} - }; - - }, // emptyPolicy - - // Convenience function to list out all test functions - enumerateTests: function() { - - return [ - 'validateLabels', - 'validateEnvKeys', - 'validateVolumes', - 'validatePortRequirement', - 'validatePortRange', - 'validateContainerSize', - 'validateHealthCheck', - 'validateLayerCount' - ]; - - }, // enumerateTests - - // Override pieces of policy with specialized inputs - applyOverrides: function(existingPolicy, inputs, msgs) { - - var availableOverrides = ['max', 'warning', 'labels', 'envs', 'range', 'layers_max', 'layers_warning']; - var overrides = {}; - var policy = this.emptyPolicy(); - - // NOTE: deep copy is necessary to prevent object reference copies - var existingClone = JSON.parse(JSON.stringify(existingPolicy)); - Object.assign(policy, existingClone); - - // Filter out null values - availableOverrides.forEach(function(item) { - if (inputs[item]) { - overrides[item] = inputs[item]; - } - }); - - policy.layers = policy.layers || {}; - - this._checkOverridePolicy(overrides.max, policy.size, 'max', 'max image size', msgs); - this._checkOverridePolicy(overrides.warning, policy.size, 'warning', 'warning image size', msgs); - this._checkOverridePolicy(overrides.range, policy.ports, 'range', 'port range', msgs); - this._checkOverridePolicy(overrides.layers_max, policy.layers, 'max', 'max layer count', msgs); - this._checkOverridePolicy(overrides.layers_warning, policy.layers, 'warning', 'warning layer count', msgs); - - this._checkOverrideSplitPolicy(overrides.labels, policy.labels, 'labels', msgs); - this._checkOverrideSplitPolicy(overrides.envs, policy.env_keys, 'env keys', msgs); - - return policy; - - }, // applyOverrides - - // Convenience function to run all included tests - execute: function(policy, input) { - - var msgs = this.msgs(); // Holds all validation messages and their severities - var passing = true; - var that = this; - - this.enumerateTests().forEach(function(test) { - var result = that[test](policy, input, msgs); - passing = passing && result; - }); - - return { - isPassing: function() { - return passing; - }, - getMessages: function() { - return msgs.messages; - } - }; - - }, // execute - - validateLabels: function(policy, input, msgs) { - - var labels = this._getObjectPropertyKeys(input.Config, 'Labels'); - - var disallowedLabels = (policy.labels) - ? policy.labels.disallow - : []; - - var failedLabels = []; - - labels.forEach(function(element) { - if (disallowedLabels.indexOf(element) !== -1) { - failedLabels.push(element); - } - }); - - var success = (failedLabels.length === 0 ); - - if (success) { - msgs.addSuccess('labels validated'); - } else { - msgs.addFailure('disallowed labels present: ' + failedLabels.join(', ')); - } - - return success; - - }, // validateLabels - - validateEnvKeys: function(policy, input, msgs) { - - var disallowedEnvKeys = (policy.env_keys) - ? policy.env_keys.disallow - : []; - - var envKeys = []; - var envsShell = (input.Config && input.Config.Env) - ? input.Config.Env - : []; - - var failedEnvKeys = []; - - // Environment variables are in shell syntax (key=value), and need to be extracted - envsShell.forEach(function callback(value) { - var element = value.split('=')[0]; - envKeys.push(element); - }); - - envKeys.forEach(function(element) { - if (disallowedEnvKeys.indexOf(element) !== -1) { - failedEnvKeys.push(element); - } - }); - - var success = (failedEnvKeys.length === 0 ); - if (success) { - msgs.addSuccess('env keys validated'); - } else { - msgs.addFailure('disallowed env keys present: ' + failedEnvKeys.join(', ')); - } - - return success; - - }, // validateEnvKeys - - validateVolumes: function(policy, input, msgs) { - - var volumes = this._getObjectPropertyKeys(input.Config, 'Volumes'); - - // Currently only a simple flag on or off - var disallowedVolumes = (policy.volumes) - ? !!policy.volumes.disallowed - : false; - - var hasVolumes = (volumes.length > 0); - var failed = (disallowedVolumes && volumes.length); - - if (failed) { - msgs.addFailure('volumes are disallowed: ' + volumes.join(', ')); - return false; - } - - var msg = "volumes "; - - if (disallowedVolumes) { - msg += "not allowed, none defined"; - } else if (hasVolumes) { - msg += "allowed: " + volumes.join(', '); - } else { - msg += "not in use"; - } - - msgs.addSuccess(msg); - - return true; - - }, // validateVolumes - - validatePortRequirement: function(policy, input, msgs) { - - var portsRequired = (policy.ports) - ? !!policy.ports.required - : false; - - var ports = this._getObjectPropertyKeys(input.ContainerConfig, 'ExposedPorts'); - var portsAvailable = (ports.length > 0); - - if (portsRequired) { - - if (!portsAvailable) { - msgs.addFailure('exposed port(s) required'); - return false; - } - - msgs.addSuccess('exposed ports required, detected'); - return true; - - } // if portsRequired - - if (portsAvailable) { - msgs.addSuccess('exposed ports allowed, detected'); - } else { - msgs.addSuccess('exposed ports allowed, none detected'); - } - return true - - }, // validatePortRequirement - - validatePortRange: function(policy, input, msgs) { - - var portsRange = (policy.ports) - ? policy.ports.range - : null; - - if (!portsRange) { - return true; - } - - var ports = this._getObjectPropertyKeys(input.ContainerConfig, 'ExposedPorts'); - var split = portsRange.split('-'); - var lowerRange = parseInt(split[0], 10); - var upperRange = parseInt(split[1], 10); - - if (upperRange < lowerRange) { - msgs.addException('invalid port range specified: ' + lowerRange + '-' + upperRange); - return false; - } - - var failedPorts = []; - var portNumbers = []; - - // Do not enforce port requirement from here, instead, using port: required policy - if (!ports.length) { - msgs.addSuccess('no exposed ports for range check [' + portsRange + ']'); - return true; - } - - ports.forEach(function(element) { - - var portNumber = element.split('/')[0]; - portNumbers.push(portNumber); - - if (portNumber < lowerRange || portNumber > upperRange) { - failedPorts.push(portNumber); - } - - }); - - var failed = (failedPorts.length > 0); - - if (!failed) { - msgs.addSuccess('ports ' + portNumbers.join(', ') + ' within range [' + portsRange + ']'); - return true; - } - - var msg = ' ' + failedPorts.join(', ') + ' outside of required range [' + portsRange + ']'; - msg = ((failedPorts.length > 1) ? 'ports' : 'port') + msg; - - msgs.addFailure(msg); - - return !failed; - - }, // validatePortRange - - validateContainerSize: function(policy, input, msgs) { - - var maxSize = (policy.size && policy.size.max) - ? parseInt(policy.size.max, 10) - : null; - - var warningSize = (policy.size && policy.size.warning) - ? parseInt(policy.size.warning, 10) - : null; - - if (warningSize && maxSize && warningSize >= maxSize) { - msgs.addException('Invalid policy: warning size (' + warningSize + 'MB) must be less max size (' + maxSize + 'MB)'); - return false; - } - - // NOTE: this is reported in B, convert to MB - var containerSize = Math.ceil(input.Size / 1000000); - - var failedWarningSize = (warningSize && containerSize >= warningSize); - - if (!maxSize) { - - if (failedWarningSize) { - msgs.addWarning(containerSize + "MB container size, recommend < " + warningSize + "MB"); - } - else { - msgs.addSuccess('no max container size limit specified'); - } - - return true; - - } // if !maxSize - - if (containerSize > maxSize) { - msgs.addFailure(containerSize + 'MB container size, exceeded ' + maxSize + 'MB maximum'); - return false; - } - - if (failedWarningSize) { - msgs.addWarning(containerSize + 'MB container size, recommend < ' + warningSize + 'MB'); - } - else { - msgs.addSuccess(containerSize + "MB container size, maximum: " + maxSize + "MB"); - } - - return true; - - }, // validateContainerSize - - validateHealthCheck: function(policy, input, msgs) { - - var isDisallowed = (policy.healthcheck) - ? !!policy.healthcheck.disallowed - : false; - - var healthcheck = (input.ContainerConfig && input.ContainerConfig.Healthcheck) - ? input.ContainerConfig.Healthcheck - : null; - - // Dockerfile spec outlines a "none" option - var healthTest = (healthcheck && healthcheck.Test && Array.isArray(healthcheck.Test)) - ? healthcheck.Test - : null; - - var noneSpecified = (healthTest && healthTest[0].toLowerCase() === 'none'); - var noCheck = (!healthcheck || noneSpecified); - - if (noCheck) { - msgs.addSuccess('no healthcheck specified'); - return true; - } - - if (isDisallowed) { - msgs.addFailure('healthcheck is disallowed'); - return false; - } - - msgs.addSuccess('healthcheck specified, allowed') - return true; - - }, // validateHealthCheck - - validateLayerCount: function(policy, input, msgs) { - - var maxCount = (policy.layers && policy.layers.max) - ? parseInt(policy.layers.max, 10) - : null; - - var warningCount = (policy.layers && policy.layers.warning) - ? parseInt(policy.layers.warning, 10) - : null; - - if (warningCount && maxCount && warningCount >= maxCount) { - msgs.addException('Invalid policy: layer count warning (' + warningCount + ') must be less max count (' + maxCount + ')'); - return false; - } - - var layers = (input.RootFS) - ? input.RootFS.Layers - : []; - - var layerCount = layers.length; - var failedWarningCount = (warningCount && layerCount >= warningCount); - - if (!maxCount) { - - if (failedWarningCount) { - msgs.addWarning(layerCount + " filesystem layers, recommended < " + warningCount); - } - else { - msgs.addSuccess('no maximum container layer count specified'); - } - - return true; - - } // if !maxCount - - if (layerCount > maxCount) { - msgs.addFailure(layerCount + ' filesystem layers, maxmimum: ' + maxCount ); - return false; - } - - if (failedWarningCount) { - msgs.addWarning(layerCount + ' filesystem layers, recommended < ' + warningCount); - } - else { - msgs.addSuccess(layerCount + " filesystem layers, maximum: " + maxCount); - } - - return true; - }, // validateLayerCount - - // Produces a container to hold ordered messages and their severities - msgs: function() { - return { - messages: [], - addSuccess: function(message) { - this.messages.push(['success',message]); - }, - addFailure: function(message) { - this.messages.push(['failure',message]); - }, - addWarning: function(message) { - this.messages.push(['warning',message]); - }, - addException: function(message) { - this.messages.push(['exception',message]); - } - }; - }, - - _checkOverridePolicy: function(override, destination, property, title, msgs) { - - if (!override) { - return; - } - - destination[property] = override; - - msgs.push(title + ': ' + override); - - }, // _checkOverridePolicy - - _checkOverrideSplitPolicy: function(override, destination, title, msgs) { - - if (!override) { - return; - } - - var addedValues = override.split(','); - destination.disallow = addedValues; - msgs.push('disallowed ' + title + ': ' + addedValues.join(', ')); - - }, // _checkOverrideSplitPolicy - - // Returns empty array when property is non-existant - _getObjectPropertyKeys: function(target, property) { - - return (target && target[property] ) - ? Object.keys(target[property]) - : []; - - } // _getObjectPropertyKeys - }; -}; diff --git a/lib/policyengine.js b/lib/policyengine.js new file mode 100644 index 0000000..6bc5042 --- /dev/null +++ b/lib/policyengine.js @@ -0,0 +1,414 @@ +const policyEngine = { + emptyPolicy: () => ( + { + size: {}, + labels: {}, + env_keys: {}, + volumes: {}, + ports: {}, + healthcheck: {}, + } + ), + + // Convenience function to list out all test functions + enumerateTests() { + return [ + 'validateLabels', + 'validateEnvKeys', + 'validateVolumes', + 'validatePortRequirement', + 'validatePortRange', + 'validateContainerSize', + 'validateHealthCheck', + 'validateLayerCount', + ]; + }, // enumerateTests + + // Override pieces of policy with specialized inputs + applyOverrides(existingPolicy, inputs, msgs) { + const availableOverrides = ['max', 'warning', 'labels', 'envs', 'range', 'layers_max', 'layers_warning']; + const overrides = {}; + const policy = policyEngine.emptyPolicy(); + + // NOTE: deep copy is necessary to prevent object reference copies + const existingClone = JSON.parse(JSON.stringify(existingPolicy)); + Object.assign(policy, existingClone); + + // Filter out null values + availableOverrides.forEach((item) => { + if (inputs[item]) { + overrides[item] = inputs[item]; + } + }); + + policy.layers = policy.layers || {}; + + policyEngine.checkOrOverridePolicy(overrides.max, policy.size, 'max', 'max image size', msgs); + policyEngine.checkOrOverridePolicy(overrides.warning, policy.size, 'warning', 'warning image size', msgs); + policyEngine.checkOrOverridePolicy(overrides.range, policy.ports, 'range', 'port range', msgs); + policyEngine.checkOrOverridePolicy(overrides.layers_max, policy.layers, 'max', 'max layer count', msgs); + policyEngine.checkOrOverridePolicy(overrides.layers_warning, policy.layers, 'warning', 'warning layer count', msgs); + + policyEngine.checkOrOverrideSplitPolicy(overrides.labels, policy.labels, 'labels', msgs); + policyEngine.checkOrOverrideSplitPolicy(overrides.envs, policy.env_keys, 'env keys', msgs); + + return policy; + }, + + // Convenience function to run all included tests + execute(policy, input) { + const msgs = policyEngine.msgs(); // Holds all validation messages and their severities + let passing = true; + const that = this; + + policyEngine.enumerateTests().forEach((test) => { + const result = that[test](policy, input, msgs); + passing = passing && result; + }); + + return { + isPassing: () => passing, + getMessages: () => msgs.messages, + }; + }, + + validateLabels(policy, input, msgs) { + const labels = policyEngine.getObjectPropertyKeys(input.Config, 'Labels'); + + const disallowedLabels = (policy.labels) + ? policy.labels.disallow + : []; + + const failedLabels = []; + + labels.forEach((element) => { + if (disallowedLabels.indexOf(element) !== -1) { + failedLabels.push(element); + } + }); + + const success = (failedLabels.length === 0); + + if (success) { + msgs.addSuccess('labels validated'); + } else { + msgs.addFailure(`disallowed labels present: ${failedLabels.join(', ')}`); + } + + return success; + }, + + validateEnvKeys(policy, input, msgs) { + const disallowedEnvKeys = (policy.env_keys) + ? policy.env_keys.disallow + : []; + + const envKeys = []; + const envsShell = (input.Config && input.Config.Env) + ? input.Config.Env + : []; + + const failedEnvKeys = []; + + // Environment variables are in shell syntax (key=value), and need to be extracted + envsShell.forEach((value) => { + const element = value.split('=')[0]; + envKeys.push(element); + }); + + envKeys.forEach((element) => { + if (disallowedEnvKeys.indexOf(element) !== -1) { + failedEnvKeys.push(element); + } + }); + + const success = (failedEnvKeys.length === 0); + if (success) { + msgs.addSuccess('env keys validated'); + } else { + msgs.addFailure(`disallowed env keys present: ${failedEnvKeys.join(', ')}`); + } + + return success; + }, + + validateVolumes(policy, input, msgs) { + const volumes = policyEngine.getObjectPropertyKeys(input.Config, 'Volumes'); + + // Currently only a simple flag on or off + const disallowedVolumes = (policy.volumes) + ? !!policy.volumes.disallowed + : false; + + const hasVolumes = (volumes.length > 0); + const failed = (disallowedVolumes && volumes.length); + + if (failed) { + msgs.addFailure(`volumes are disallowed: ${volumes.join(', ')}`); + return false; + } + + let msg = 'volumes '; + + if (disallowedVolumes) { + msg += 'not allowed, none defined'; + } else if (hasVolumes) { + msg += `allowed: ${volumes.join(', ')}`; + } else { + msg += 'not in use'; + } + + msgs.addSuccess(msg); + + return true; + }, + + validatePortRequirement(policy, input, msgs) { + const portsRequired = (policy.ports) + ? !!policy.ports.required + : false; + + const ports = policyEngine.getObjectPropertyKeys(input.ContainerConfig, 'ExposedPorts'); + const portsAvailable = (ports.length > 0); + + if (portsRequired) { + if (!portsAvailable) { + msgs.addFailure('exposed port(s) required'); + return false; + } + + msgs.addSuccess('exposed ports required, detected'); + return true; + } + + if (portsAvailable) { + msgs.addSuccess('exposed ports allowed, detected'); + } else { + msgs.addSuccess('exposed ports allowed, none detected'); + } + return true; + }, + + validatePortRange(policy, input, msgs) { + const portsRange = (policy.ports) + ? policy.ports.range + : null; + + if (!portsRange) { + return true; + } + + const ports = policyEngine.getObjectPropertyKeys(input.ContainerConfig, 'ExposedPorts'); + const split = portsRange.split('-'); + const lowerRange = parseInt(split[0], 10); + const upperRange = parseInt(split[1], 10); + + if (upperRange < lowerRange) { + msgs.addException(`invalid port range specified: ${lowerRange}-${upperRange}`); + return false; + } + + const failedPorts = []; + const portNumbers = []; + + // Do not enforce port requirement from here, instead, using port: required policy + if (!ports.length) { + msgs.addSuccess(`no exposed ports for range check [${portsRange}]`); + return true; + } + + ports.forEach((element) => { + const portNumber = element.split('/')[0]; + portNumbers.push(portNumber); + + if (portNumber < lowerRange || portNumber > upperRange) { + failedPorts.push(portNumber); + } + }); + + const failed = (failedPorts.length > 0); + + if (!failed) { + msgs.addSuccess(`ports ${portNumbers.join(', ')} within range [${portsRange}]`); + return true; + } + + let msg = ` ${failedPorts.join(', ')} outside of required range [${portsRange}]`; + msg = ((failedPorts.length > 1) ? 'ports' : 'port') + msg; + + msgs.addFailure(msg); + + return !failed; + }, + + validateContainerSize(policy, input, msgs) { + const maxSize = (policy.size && policy.size.max) + ? parseInt(policy.size.max, 10) + : null; + + const warningSize = (policy.size && policy.size.warning) + ? parseInt(policy.size.warning, 10) + : null; + + if (warningSize && maxSize && warningSize >= maxSize) { + msgs.addException(`Invalid policy: warning size (${warningSize}MB) must be less max size (${maxSize}MB)`); + return false; + } + + // NOTE: this is reported in B, convert to MB + const containerSize = Math.ceil(input.Size / 1000000); + + const failedWarningSize = (warningSize && containerSize >= warningSize); + + if (!maxSize) { + if (failedWarningSize) { + msgs.addWarning(`${containerSize}MB container size, recommend < ${warningSize}MB`); + } else { + msgs.addSuccess('no max container size limit specified'); + } + + return true; + } // if !maxSize + + if (containerSize > maxSize) { + msgs.addFailure(`${containerSize}MB container size, exceeded ${maxSize}MB maximum`); + return false; + } + + if (failedWarningSize) { + msgs.addWarning(`${containerSize}MB container size, recommend < ${warningSize}MB`); + } else { + msgs.addSuccess(`${containerSize}MB container size, maximum: ${maxSize}MB`); + } + + return true; + }, + + validateHealthCheck(policy, input, msgs) { + const isDisallowed = (policy.healthcheck) + ? !!policy.healthcheck.disallowed + : false; + + const healthcheck = (input.ContainerConfig && input.ContainerConfig.Healthcheck) + ? input.ContainerConfig.Healthcheck + : null; + + // Dockerfile spec outlines a "none" option + const healthTest = (healthcheck && healthcheck.Test && Array.isArray(healthcheck.Test)) + ? healthcheck.Test + : null; + + const noneSpecified = (healthTest && healthTest[0].toLowerCase() === 'none'); + const noCheck = (!healthcheck || noneSpecified); + + if (noCheck) { + msgs.addSuccess('no healthcheck specified'); + return true; + } + + if (isDisallowed) { + msgs.addFailure('healthcheck is disallowed'); + return false; + } + + msgs.addSuccess('healthcheck specified, allowed'); + return true; + }, + + validateLayerCount(policy, input, msgs) { + const maxCount = (policy.layers && policy.layers.max) + ? parseInt(policy.layers.max, 10) + : null; + + const warningCount = (policy.layers && policy.layers.warning) + ? parseInt(policy.layers.warning, 10) + : null; + + if (warningCount && maxCount && warningCount >= maxCount) { + msgs.addException(`Invalid policy: layer count warning (${warningCount}) must be less max count (${maxCount})`); + return false; + } + + const layers = (input.RootFS) + ? input.RootFS.Layers + : []; + + const layerCount = layers.length; + const failedWarningCount = (warningCount && layerCount >= warningCount); + + if (!maxCount) { + if (failedWarningCount) { + msgs.addWarning(`${layerCount} filesystem layers, recommended < ${warningCount}`); + } else { + msgs.addSuccess('no maximum container layer count specified'); + } + + return true; + } // if !maxCount + + if (layerCount > maxCount) { + msgs.addFailure(`${layerCount} filesystem layers, maxmimum: ${maxCount}`); + return false; + } + + if (failedWarningCount) { + msgs.addWarning(`${layerCount} filesystem layers, recommended < ${warningCount}`); + } else { + msgs.addSuccess(`${layerCount} filesystem layers, maximum: ${maxCount}`); + } + + return true; + }, + + /** + * Produces a container to hold ordered messages and their severities + */ + msgs: () => ( + { + messages: [], + addSuccess(message) { + this.messages.push(['success', message]); + }, + addFailure(message) { + this.messages.push(['failure', message]); + }, + addWarning(message) { + this.messages.push(['warning', message]); + }, + addException(message) { + this.messages.push(['exception', message]); + }, + } + ), + + checkOrOverridePolicy(override, destination, property, title, msgs) { + if (!override) { + return; + } + + destination[property] = override; + + msgs.push(`${title}: ${override}`); + }, + + checkOrOverrideSplitPolicy(override, destination, title, msgs) { + if (!override) { + return; + } + + const addedValues = override.split(','); + destination.disallow = addedValues; + msgs.push(`disallowed ${title}: ${addedValues.join(', ')}`); + }, + + /** + * Returns empty array when property is non-existant + */ + getObjectPropertyKeys(target, property) { + return (target && target[property]) + ? Object.keys(target[property]) + : []; + }, +}; + +module.exports = policyEngine; diff --git a/package-lock.json b/package-lock.json index 9b34f36..eb0483d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -154,28 +154,37 @@ } }, "@sinonjs/commons": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.1.tgz", - "integrity": "sha512-Debi3Baff1Qu1Unc3mjJ96MgpbwTn43S1+9yJ0llWygPwDNu2aaWBD6yc9y/Z8XDRNhx7U+u2UDg2OGQXkclUQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.0.tgz", + "integrity": "sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q==", "dev": true, "requires": { "type-detect": "4.0.8" } }, + "@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, "@sinonjs/formatio": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-4.0.1.tgz", - "integrity": "sha512-asIdlLFrla/WZybhm0C8eEzaDNNrzymiTqHMeJl6zPW2881l3uuVRpm0QlRQEjqYWv6CcKMGYME3LbrLJsORBw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-5.0.1.tgz", + "integrity": "sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==", "dev": true, "requires": { "@sinonjs/commons": "^1", - "@sinonjs/samsam": "^4.2.0" + "@sinonjs/samsam": "^5.0.2" } }, "@sinonjs/samsam": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-4.2.2.tgz", - "integrity": "sha512-z9o4LZUzSD9Hl22zV38aXNykgFeVj8acqfFabCY6FY83n/6s/XwNJyYYldz6/9lBJanpno9h+oL6HTISkviweA==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.0.3.tgz", + "integrity": "sha512-QucHkc2uMJ0pFGjJUDP3F9dq5dx8QIaqISl9QgwLOh6P9yv877uONPGXh/OH/0zmM3tW1JjuJltAZV2l7zU+uQ==", "dev": true, "requires": { "@sinonjs/commons": "^1.6.0", @@ -189,25 +198,31 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, "acorn": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz", + "integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==", "dev": true }, "acorn-jsx": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", - "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", "dev": true }, "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", "dev": true, "requires": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" @@ -220,12 +235,20 @@ "dev": true }, "ansi-escapes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", - "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", "dev": true, "requires": { - "type-fest": "^0.8.1" + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } } }, "ansi-regex": { @@ -260,6 +283,27 @@ "sprintf-js": "~1.0.2" } }, + "array-includes": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", + "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0", + "is-string": "^1.0.5" + } + }, + "array.prototype.flat": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", + "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, "assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -411,9 +455,9 @@ } }, "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", "dev": true }, "cliui": { @@ -427,16 +471,10 @@ "wrap-ansi": "^5.1.0" }, "dependencies": { - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, "string-width": { @@ -449,6 +487,15 @@ "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^5.1.0" } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } } } }, @@ -468,9 +515,9 @@ "dev": true }, "commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==" }, "concat-map": { "version": "0.0.1", @@ -478,6 +525,18 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "confusing-browser-globals": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.9.tgz", + "integrity": "sha512-KbS1Y0jMtyPgIxjO7ZzMAuUpAKMt1SzCL9fsrKsX6b0zJPTaT0SiSPmewwVZg9UAO83HVIlEhZF84LIjZ0lmAw==", + "dev": true + }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -489,14 +548,6 @@ "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } } }, "d": { @@ -563,11 +614,20 @@ } }, "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, "es-abstract": { "version": "1.17.5", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", @@ -689,14 +749,159 @@ "v8-compile-cache": "^2.0.3" }, "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, "globals": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz", - "integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==", + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", "dev": true, "requires": { "type-fest": "^0.8.1" } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-json-comments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz", + "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==", + "dev": true + } + } + }, + "eslint-config-airbnb-base": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.1.0.tgz", + "integrity": "sha512-+XCcfGyCnbzOnktDVhwsCAx+9DmrzEmuwxyHUJpw+kqBVT744OUBrB09khgFKlK1lshVww6qXGsYPZpavoNjJw==", + "dev": true, + "requires": { + "confusing-browser-globals": "^1.0.9", + "object.assign": "^4.1.0", + "object.entries": "^1.1.1" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz", + "integrity": "sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.13.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-module-utils": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz", + "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "pkg-dir": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "2.20.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.20.2.tgz", + "integrity": "sha512-FObidqpXrR8OnCh4iNsxy+WACztJLXAHBO5hK79T1Hc77PgQZkyDGA5Ag9xAvRpglvLNxhH/zSmZ70/pZ31dHg==", + "dev": true, + "requires": { + "array-includes": "^3.0.3", + "array.prototype.flat": "^1.2.1", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.2", + "eslint-module-utils": "^2.4.1", + "has": "^1.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.0", + "read-pkg-up": "^2.0.0", + "resolve": "^1.12.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true } } }, @@ -726,13 +931,13 @@ "dev": true }, "espree": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", - "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", "dev": true, "requires": { - "acorn": "^7.1.0", - "acorn-jsx": "^5.1.0", + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", "eslint-visitor-keys": "^1.1.0" } }, @@ -742,12 +947,20 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", "dev": true, "requires": { - "estraverse": "^4.0.0" + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", + "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", + "dev": true + } } }, "esrecurse": { @@ -807,9 +1020,9 @@ } }, "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", "dev": true }, "fast-json-stable-stringify": { @@ -825,9 +1038,9 @@ "dev": true }, "figures": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz", - "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, "requires": { "escape-string-regexp": "^1.0.5" @@ -881,9 +1094,9 @@ } }, "flatted": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", - "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", "dev": true }, "fs.realpath": { @@ -893,9 +1106,9 @@ "dev": true }, "fsevents": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", - "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", "dev": true, "optional": true }, @@ -929,9 +1142,9 @@ "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==" }, "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -943,9 +1156,9 @@ } }, "glob-parent": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", - "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", "dev": true, "requires": { "is-glob": "^4.0.1" @@ -957,6 +1170,12 @@ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, "growl": { "version": "1.10.5", "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", @@ -990,6 +1209,12 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1038,26 +1263,122 @@ "dev": true }, "inquirer": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.3.tgz", - "integrity": "sha512-+OiOVeVydu4hnCGLCSX+wedovR/Yzskv9BFqUNNKq9uU2qg7LCcCo3R86S2E7WLo0y/x2pnEZfZe1CoYnORUAw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", + "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", "dev": true, "requires": { "ansi-escapes": "^4.2.1", - "chalk": "^2.4.2", + "chalk": "^3.0.0", "cli-cursor": "^3.1.0", "cli-width": "^2.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.15", "mute-stream": "0.0.8", - "run-async": "^2.2.0", + "run-async": "^2.4.0", "rxjs": "^6.5.3", "string-width": "^4.1.0", - "strip-ansi": "^5.1.0", + "strip-ansi": "^6.0.0", "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1092,9 +1413,9 @@ "dev": true }, "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, "is-glob": { @@ -1126,6 +1447,12 @@ "has": "^1.0.3" } }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, "is-symbol": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", @@ -1154,9 +1481,9 @@ "dev": true }, "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -1196,6 +1523,18 @@ "type-check": "~0.3.2" } }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -1227,15 +1566,6 @@ "chalk": "^2.4.2" } }, - "lolex": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", - "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, "lru-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", @@ -1290,9 +1620,9 @@ } }, "mocha": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.1.tgz", - "integrity": "sha512-3qQsu3ijNS3GkWcccT5Zw0hf/rWvu1fTN9sPvEd81hlwsr30GX2GcDSSoBxo24IR8FelmrAydGC6/1J5QQP4WA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz", + "integrity": "sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ==", "dev": true, "requires": { "ansi-colors": "3.2.3", @@ -1308,7 +1638,7 @@ "js-yaml": "3.13.1", "log-symbols": "3.0.0", "minimatch": "3.0.4", - "mkdirp": "0.5.3", + "mkdirp": "0.5.5", "ms": "2.1.1", "node-environment-flags": "1.0.6", "object.assign": "4.1.0", @@ -1330,33 +1660,14 @@ "ms": "^2.1.1" } }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", "dev": true, "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "mkdirp": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", - "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", - "dev": true, - "requires": { - "minimist": "^1.2.5" + "argparse": "^1.0.7", + "esprima": "^4.0.0" } }, "ms": { @@ -1365,12 +1676,6 @@ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, "supports-color": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", @@ -1412,16 +1717,15 @@ "dev": true }, "nise": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/nise/-/nise-3.0.1.tgz", - "integrity": "sha512-fYcH9y0drBGSoi88kvhpbZEsenX58Yr+wOJ4/Mi1K4cy+iGP/a73gNoyNhu5E9QxPdgTlVChfIaAlnyOy/gHUA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.3.tgz", + "integrity": "sha512-EGlhjm7/4KvmmE6B/UFsKh7eHykRl9VH+au8dduHLCyWUO/hr7+N+WtTvDUwc9zHuM1IaIJs/0lQ6Ag1jDkQSg==", "dev": true, "requires": { "@sinonjs/commons": "^1.7.0", - "@sinonjs/formatio": "^4.0.1", + "@sinonjs/fake-timers": "^6.0.0", "@sinonjs/text-encoding": "^0.7.1", "just-extend": "^4.0.2", - "lolex": "^5.0.1", "path-to-regexp": "^1.7.0" } }, @@ -1433,14 +1737,18 @@ "requires": { "object.getownpropertydescriptors": "^2.0.3", "semver": "^5.7.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" } }, "normalize-path": { @@ -1473,6 +1781,17 @@ "object-keys": "^1.0.11" } }, + "object.entries": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.2.tgz", + "integrity": "sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "has": "^1.0.3" + } + }, "object.getownpropertydescriptors": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", @@ -1483,6 +1802,18 @@ "es-abstract": "^1.17.0-next.1" } }, + "object.values": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", + "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1554,6 +1885,15 @@ "callsites": "^3.0.0" } }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -1587,6 +1927,15 @@ "isarray": "0.0.1" } }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + } + }, "pathval": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", @@ -1599,6 +1948,66 @@ "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", "dev": true }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + } + } + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -1617,11 +2026,71 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, - "ramda": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz", - "integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==", - "dev": true + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + } + } }, "readdirp": { "version": "3.2.0", @@ -1685,18 +2154,15 @@ } }, "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "dev": true, - "requires": { - "is-promise": "^2.1.0" - } + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true }, "rxjs": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", - "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", + "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", "dev": true, "requires": { "tslib": "^1.9.0" @@ -1709,9 +2175,9 @@ "dev": true }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true }, "set-blocking": { @@ -1736,23 +2202,23 @@ "dev": true }, "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, "sinon": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-8.1.1.tgz", - "integrity": "sha512-E+tWr3acRdoe1nXbHMu86SSqA1WGM7Yw3jZRLvlCMnXwTHP8lgFFVn5BnKnF26uc5SfZ3D7pA9sN7S3Y2jG4Ew==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.0.2.tgz", + "integrity": "sha512-0uF8Q/QHkizNUmbK3LRFqx5cpTttEVXudywY9Uwzy8bTfZUhljZ7ARzSxnRHWYWtVTeh4Cw+tTb3iU21FQVO9A==", "dev": true, "requires": { - "@sinonjs/commons": "^1.7.0", - "@sinonjs/formatio": "^4.0.1", - "@sinonjs/samsam": "^4.2.2", + "@sinonjs/commons": "^1.7.2", + "@sinonjs/fake-timers": "^6.0.1", + "@sinonjs/formatio": "^5.0.1", + "@sinonjs/samsam": "^5.0.3", "diff": "^4.0.2", - "lolex": "^5.1.2", - "nise": "^3.0.1", + "nise": "^4.0.1", "supports-color": "^7.1.0" }, "dependencies": { @@ -1788,14 +2254,6 @@ "ansi-styles": "^3.2.0", "astral-regex": "^1.0.0", "is-fullwidth-code-point": "^2.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - } } }, "source-map": { @@ -1804,43 +2262,57 @@ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "dev": true, "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" } }, "string.prototype.trimend": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.0.tgz", - "integrity": "sha512-EEJnGqa/xNfIg05SxiPSqRS7S9qwDhYts1TSLR1BQfYUfPe1stofgGKvwERK9+9yf+PpfBMlpBaCHucXGPQfUA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", "dev": true, "requires": { "define-properties": "^1.1.3", @@ -1870,9 +2342,9 @@ } }, "string.prototype.trimstart": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.0.tgz", - "integrity": "sha512-iCP8g01NFYiiBOnwG1Xc3WZLyoo+RuBymwIlWncShXDDJYWN6DbnM3odslBJdgCdRlq94B5s63NWAZlcn2CS4w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", "dev": true, "requires": { "define-properties": "^1.1.3", @@ -1880,26 +2352,32 @@ } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^3.0.0" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "dev": true } } }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true }, "supports-color": { @@ -1923,16 +2401,10 @@ "string-width": "^3.0.0" }, "dependencies": { - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, "string-width": { @@ -1945,6 +2417,15 @@ "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^5.1.0" } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } } } }, @@ -1994,9 +2475,9 @@ } }, "tslib": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", "dev": true }, "type": { @@ -2035,11 +2516,21 @@ } }, "v8-compile-cache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", - "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", + "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", "dev": true }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -2062,39 +2553,6 @@ "dev": true, "requires": { "string-width": "^1.0.2 || 2" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } } }, "word-wrap": { @@ -2114,16 +2572,10 @@ "strip-ansi": "^5.0.0" }, "dependencies": { - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, "string-width": { @@ -2136,6 +2588,15 @@ "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^5.1.0" } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } } } }, @@ -2178,16 +2639,10 @@ "yargs-parser": "^13.1.2" }, "dependencies": { - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, "string-width": { @@ -2200,6 +2655,15 @@ "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^5.1.0" } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } } } }, diff --git a/package.json b/package.json index 3626843..48988df 100644 --- a/package.json +++ b/package.json @@ -6,20 +6,21 @@ "test": "mocha" }, "engines": { - "node": ">=10.0.0" + "node": ">=12.0.0" }, "dependencies": { "cli-color": "^2.0.0", - "commander": "^4.1.1", + "commander": "^5.1.0", "get-stdin": "^7.0.0", - "js-yaml": "^3.13.1" + "js-yaml": "^3.14.0" }, "devDependencies": { "babel-eslint": "^10.1.0", "chai": "^4.2.0", "eslint": "^6.8.0", - "mocha": "^7.1.1", - "ramda": "^0.26.1", - "sinon": "^8.1.1" + "eslint-config-airbnb-base": "^14.1.0", + "eslint-plugin-import": "^2.20.2", + "mocha": "^7.2.0", + "sinon": "^9.0.2" } } diff --git a/test/policy.js b/test/policy.js deleted file mode 100644 index 111bb5c..0000000 --- a/test/policy.js +++ /dev/null @@ -1,509 +0,0 @@ -"use strict"; - -var assert = require('assert'); -var policy = require('../lib/policy.js')(); -var yaml = require('js-yaml'); -var fs = require('fs'); -var R = require('ramda'); -var defaultPolicy = yaml.safeLoad(fs.readFileSync('./default_policy.yaml', 'utf8')); -var emptyContainer = JSON.parse(fs.readFileSync(__dirname + '/fixtures/empty_container.json', 'utf8'))[0]; -var failingContainer = JSON.parse(fs.readFileSync(__dirname + '/fixtures/failing_container.json', 'utf8'))[0]; - -it('succeeds with an empty policy', -function() { - var testPolicy = {} - var testContainer = {}; - - var result = policy.execute(testPolicy, testContainer); - assert(result.isPassing()); - - // Ensure message count matches test count - assert(policy.enumerateTests.length === result.getMessages.length); -}); - -it('succeeds with an empty policy, empty container fixture', -function() { - var testPolicy = {} - var testContainer = emptyContainer; - - var result = policy.execute(testPolicy, testContainer); - assert(result.isPassing()); - - // Ensure message count matches test count - assert(policy.enumerateTests.length === result.getMessages.length); -}); - -it('fails with violations of the default policy', -function() { - - var testPolicy = defaultPolicy; - var result = policy.execute(testPolicy, failingContainer); - - assert(!result.isPassing()); - - // Ensure message count matches test count - assert(policy.enumerateTests().length === result.getMessages().length); -}); - -it('succeeds without disallowed labels', -function() { - var testPolicy = { labels: { disallow: ['com.swipely.iam-docker.iam-profile', 'ABCDEF'] }}; - var testContainer = { Config: { Labels: { OTHER_ROLE: 12345}}}; - - var result = policy.validateLabels(testPolicy, testContainer, policy.msgs()); - assert(result); -}); - -it('fails with disallowed labels', -function() { - var testPolicy = { labels: { disallow: ['ABCDEF', 'com.swipely.iam-docker.iam-profile']}}; - var testContainer = { Config: { Labels: { 'com.swipely.iam-docker.iam-profile': 12345}}}; - - var result = policy.validateLabels(testPolicy, testContainer, policy.msgs()); - assert(!result); -}); - -it('fails with another disallowed label', -function() { - var testPolicy = { labels: { disallow: ['com.swipely.iam-docker.msi-explicit-identity']}}; - var testContainer = { Config: { Labels: { 'com.swipely.iam-docker.msi-explicit-identity': 12345}}}; - - var result = policy.validateLabels(testPolicy, testContainer, policy.msgs()); - assert(!result); -}); - -it('succeeds without disallowed env keys', -function() { - - var testPolicy = { labels: { disallow: ['IAM_ROLE', 'ABCDEF'] }}; - var testContainer = { Config: { Env: ['OTHER_ROLE=12345', 'OTHER_OTHER_ROLE=67890']}}; - - var result = policy.validateEnvKeys(testPolicy, testContainer, policy.msgs()); - assert(result); -}); - -it('fails with disallowed env keys', -function() { - - var testPolicy = { env_keys: { disallow: ['IAM_ROLE', 'ABCDEF'] }}; - var testContainer = { Config: { Env: ['IAM_ROLE=12345']}}; - - var result = policy.validateEnvKeys(testPolicy, testContainer, policy.msgs()); - assert(!result); -}); - -it('fails with another disallowed env key', -function() { - - var testPolicy = { env_keys: { disallow: ['AGENT_FILL'] }}; - var testContainer = { Config: { Env: ['AGENT_FILL=true']}}; - - var result = policy.validateEnvKeys(testPolicy, testContainer, policy.msgs()); - assert(!result); -}); - -it('succeeds with volumes, default volume restriction (none)', -function() { - var testPolicy = {}; - var testContainer = { Config: { Volumes: { '/myvolume': {}, '/another-volume': {}}}}; - - var result = policy.validateVolumes(testPolicy, testContainer, policy.msgs()); - assert(result); -}); - -it('succeeds without volumes, without restrictions', -function() { - var testPolicy = { volumes: { disallowed: false }}; - var testContainer = { Config: { Volumes: {}}}; - - var result = policy.validateVolumes(testPolicy, testContainer, policy.msgs()); - assert(result); -}); - -it('succeeds with volumes, without restrictions', -function() { - var testPolicy = { volumes: { disallowed: false }}; - var testContainer = { Config: { Volumes: { '/myvolume': {}, '/another-volume': {}}}}; - - var result = policy.validateVolumes(testPolicy, testContainer, policy.msgs()); - assert(result); -}); - -it('succeeds without volumes, with restrictions', -function() { - var testPolicy = { volumes: { disallowed: true }}; - var testContainer = { Config: { Volumes: {}}}; - - var result = policy.validateVolumes(testPolicy, testContainer, policy.msgs()); - assert(result); -}); - -it('fail with volumes, with restrictions', -function() { - var testPolicy = { volumes: { disallowed: true }}; - var testContainer = { Config: { Volumes: { '/myvolume': {}, '/another-volume': {}}}}; - - var result = policy.validateVolumes(testPolicy, testContainer, policy.msgs()); - assert(!result); -}); - -it('succeeds without ports, without requirement', -function() { - var testPolicy = { ports: { required: false }}; - var testContainer = {}; - - var result = policy.validatePortRequirement(testPolicy, testContainer, policy.msgs()); - assert(result); -}); - -it('fails without ports, with requirement', -function() { - var testPolicy = { ports: { required: true }}; - var testContainer = {}; - - var result = policy.validatePortRequirement(testPolicy, testContainer, policy.msgs()); - assert(!result); -}); - -it('succeeds with ports, with requirement', -function() { - var testPolicy = { ports: { required: true }}; - var testContainer = { ContainerConfig: { ExposedPorts: { '8080/tcp': {}, '8081/tcp': {}}}}; - - var result = policy.validatePortRequirement(testPolicy, testContainer, policy.msgs()); - assert(result); -}); - -it('succeeds with ports, without requirement', -function() { - var testPolicy = { ports: { required: false }}; - var testContainer = { ContainerConfig: { ExposedPorts: { '8080/tcp': {}, '8081/tcp': {}}}}; - - var result = policy.validatePortRequirement(testPolicy, testContainer, policy.msgs()); - assert(result); -}); - -it('succeeds with ports, without range requirement', -function() { - var testPolicy = { ports: { range: null }}; - var testContainer = { ContainerConfig: { ExposedPorts: { '8080/tcp': {}, '8081/tcp': {}}}}; - - var result = policy.validatePortRange(testPolicy, testContainer, policy.msgs()); - assert(result); -}); - -it('succeeds with ports, within range requirement', -function() { - var testPolicy = { ports: { range: '1-8082' }}; - var testContainer = { ContainerConfig: { ExposedPorts: { '8080/tcp': {}, '8081/tcp': {}}}}; - - var result = policy.validatePortRange(testPolicy, testContainer, policy.msgs()); - assert(result); -}); - -it('fails with ports, with an invalid port range', -function() { - var testPolicy = { ports: { range: '8081-8080' }}; - var testContainer = { ContainerConfig: { ExposedPorts: { '8080/tcp': {}, '8081/tcp': {}}}}; - - var result = policy.validatePortRange(testPolicy, testContainer, policy.msgs()); - assert(!result); -}); - -// NOTE: this is correct, since a required port is part of validatePortRequirement -it('succeeds without a port, with of port range', -function() { - var testPolicy = { ports: { range: '1-100' }}; - var testContainer = { ContainerConfig: { ExposedPorts: {}}}; - - var result = policy.validatePortRange(testPolicy, testContainer, policy.msgs()); - assert(result); -}); - -it('succeeds without an input port, with a port range', -function() { - var testPolicy = { ports: { range: '1-100' }}; - var testContainer = { ContainerConfig: {} }; - - var result = policy.validatePortRange(testPolicy, testContainer, policy.msgs()); - assert(result); -}); - -it('fails with a port, outside of port range', -function() { - var testPolicy = { ports: { range: '1-100' }}; - var testContainer = { ContainerConfig: { ExposedPorts: { '101/tcp': {}}}}; - - var result = policy.validatePortRange(testPolicy, testContainer, policy.msgs()); - assert(!result); -}); - -it('fails with ports, one outside of port range', -function() { - var testPolicy = { ports: { range: '1-100' }}; - var testContainer = { ContainerConfig: { ExposedPorts: { '50/tcp': {}, '101/tcp': {}}}}; - - var result = policy.validatePortRange(testPolicy, testContainer, policy.msgs()); - assert(!result); -}); - -it('fails with ports, both outside of port range', -function() { - var testPolicy = { ports: { range: '1-100' }}; - var testContainer = { ContainerConfig: { ExposedPorts: { '101/tcp': {}, '102/tcp': {}}}}; - - var result = policy.validatePortRange(testPolicy, testContainer, policy.msgs()); - assert(!result); -}); - -it('succeeds at max size limit', -function() { - var testPolicy = { size: { max: '10' }}; - var testContainer = { Size: 10000000}; - - var result = policy.validateContainerSize(testPolicy, testContainer, policy.msgs()); - assert(result); -}); - -it('fails over max size limit', -function() { - var testPolicy = { size: { max: '10' }}; - var testContainer = { Size: 10000001 }; - - var result = policy.validateContainerSize(testPolicy, testContainer, policy.msgs()); - assert(!result); -}); - -it('fails when warning >= max size', -function() { - var testPolicy = { size: { max: 10, warning: 11 }}; - var testContainer = { Size: 10000000 }; - var msgs = policy.msgs(); - var result = policy.validateContainerSize(testPolicy, testContainer, msgs); - - assert(msgs.messages.length === 1); - assert(msgs.messages.shift()[0] === 'exception'); // Ensure that single message is an exception - assert(!result); -}); - -it('succeeds over warning, but under max size limit', -function() { - var testPolicy = { size: { max: 10, warning: 5 }}; - var testContainer = { Size: 5000001}; - var msgs = policy.msgs(); - var result = policy.validateContainerSize(testPolicy, testContainer, msgs); - - assert(msgs.messages.length === 1); - assert(msgs.messages.shift()[0] === 'warning'); // Ensure that single message is a warning - assert(result); -}); - -it('succeeds over warning, with no max size limit', -function() { - var testPolicy = { size: { warning: 5 }}; - var testContainer = { Size: 5000001}; - var msgs = policy.msgs(); - var result = policy.validateContainerSize(testPolicy, testContainer, msgs); - - assert(msgs.messages.length === 1); - assert(msgs.messages.shift()[0] === 'warning'); // Ensure that single message is a warning - assert(result); -}); - -it('succeeds without healthcheck, with restriction', -function() { - var testPolicy = { healthcheck: { disallowed: true }}; - var testContainer = {}; - var msgs = policy.msgs(); - var result = policy.validateHealthCheck(testPolicy, testContainer, msgs); - - assert(result); -}); - -it('succeeds with "none" healthcheck, with restriction', -function() { - var testPolicy = { healthcheck: { disallowed: true }}; - var testContainer = { ContainerConfig: { Healthcheck: { Test: ['NONE']}}}; - var msgs = policy.msgs(); - var result = policy.validateHealthCheck(testPolicy, testContainer, msgs); - - assert(result); -}); - -it('succeeds with healthcheck, without restriction', -function() { - var testPolicy = { healthcheck: { disallowed: false }}; - var testContainer = { ContainerConfig: { Healthcheck: { Test: ['CMD-SHELL', 'ls -l']}}}; - var msgs = policy.msgs(); - var result = policy.validateHealthCheck(testPolicy, testContainer, msgs); - - assert(result); -}); - -it('succeeds with healthcheck, with default (not restricted)', -function() { - var testPolicy = { healthcheck: { disallowed: false }}; - var testContainer = { ContainerConfig: { Healthcheck: { Test: ['CMD-SHELL', 'ls -l']}}}; - var msgs = policy.msgs(); - var result = policy.validateHealthCheck(testPolicy, testContainer, msgs); - - assert(result); -}); - -it('fails with healthcheck, with restriction', -function() { - var testPolicy = { healthcheck: { disallowed: true }}; - var testContainer = { ContainerConfig: { Healthcheck: { Test: ['CMD-SHELL', 'ls -l']}}}; - var msgs = policy.msgs(); - var result = policy.validateHealthCheck(testPolicy, testContainer, msgs); - - assert(!result); -}); - -it('succeeds at max layer count', -function() { - var testContainer = R.clone(failingContainer); - assert(testContainer.RootFS.Layers.length > 0); - - var testPolicy = { layers: { max: testContainer.RootFS.Layers.length }}; - - var result = policy.validateLayerCount(testPolicy, testContainer, policy.msgs()); - assert(result); -}); - -it('fails over max size limit', -function() { - var testPolicy = { layers: { max: '1' }}; - assert(emptyContainer.RootFS.Layers.length > 1); - - var result = policy.validateLayerCount(testPolicy, emptyContainer, policy.msgs()); - assert(!result); -}); - -it('fails when warning >= max layer count', -function() { - var testPolicy = { layers: { max: 10, warning: 11 }}; - var msgs = policy.msgs(); - var result = policy.validateLayerCount(testPolicy, emptyContainer, msgs); - - assert(msgs.messages.length === 1); - assert(msgs.messages.shift()[0] === 'exception'); // Ensure that single message is an exception - assert(!result); -}); - -it('succeeds over warning, but under max size limit', -function() { - var newMax = (emptyContainer.RootFS.Layers.length + 1); - var newWarning = 1; - var testPolicy = { layers: { max: newMax, warning: newWarning }}; - assert(emptyContainer.RootFS.Layers.length < newMax && emptyContainer.RootFS.Layers.length > newWarning); - - var msgs = policy.msgs(); - var result = policy.validateLayerCount(testPolicy, emptyContainer, msgs); - - // Ensure there is only a single message in the stack - assert(msgs.messages.length === 1); - - // Ensure that single message is a warning - assert(msgs.messages.shift()[0] === 'warning'); - assert(result); -}); - -it('succeeds over warning, with no max size limit', -function() { - var newWarning = 1; - var testPolicy = { layers: { warning: newWarning }}; - assert(emptyContainer.RootFS.Layers.length > newWarning); - var testContainer = R.clone(emptyContainer); - var msgs = policy.msgs(); - var result = policy.validateLayerCount(testPolicy, testContainer, msgs); - - assert(msgs.messages.length === 1); - assert(msgs.messages.shift()[0] === 'warning'); // Ensure that single message is a warning - assert(result); -}); - -it('adds all overrides against an empty policy', -function() { - var myPolicy = {}; - var maxSize = 50; - var warningSize = 25; - var disallowedLabels = 'ABC,DEF'; - var disallowedEnvs = 'IAM,ROLE'; - var maxLayers = 50; - var warningLayers = 10; - - var inputs = { - max: maxSize, - warning: warningSize, - labels: disallowedLabels, - envs: disallowedEnvs, - layers_max: maxLayers, - layers_warning: warningLayers - }; - - var newPolicy = policy.applyOverrides(myPolicy, inputs, []); - - assert(maxSize === newPolicy.size.max); - assert(warningSize === newPolicy.size.warning); - assert.deepEqual(disallowedLabels.split(','), newPolicy.labels.disallow); - assert.deepEqual(disallowedEnvs.split(','), newPolicy.env_keys.disallow); -}); - -it('overrides existing policy', -function() { - var myPolicy = { - size: { - max: 654, - warning: 321 - }, - env_keys: { - disallow: [ 'KEY1', 'KEY2' ] - }, - labels: { - disallow: [ 'LABEL1', 'LABEL2' ] - }, - ports: { - range: '1-10000' - }, - layers: { - max: 100, - warning: 1 - } - }; - - var maxSize = 9876; - var warningSize = 5432; - var disallowedLabels = 'UPDATED,LABELS'; - var disallowedEnvs = 'ENVROLES,HAVEBEENUPDATED'; - var newRange = '1-10'; - var maxLayers = 50; - var warningLayers = 10; - - var inputs = { - max: maxSize, - warning: warningSize, - labels: disallowedLabels, - envs: disallowedEnvs, - range: newRange, - layers_max: maxLayers, - layers_warning: warningLayers - }; - - var newPolicy = policy.applyOverrides(myPolicy, inputs, []); - - assert.strictEqual(maxSize, newPolicy.size.max); - assert.strictEqual(warningSize, newPolicy.size.warning); - assert.notStrictEqual(myPolicy.size.max, newPolicy.size.max); - assert.notStrictEqual(myPolicy.size.warning, newPolicy.size.warning); - assert.strictEqual(newRange, newPolicy.ports.range); - - assert.deepEqual(disallowedLabels.split(','), newPolicy.labels.disallow); - assert.notDeepEqual(myPolicy.labels.disallow, newPolicy.labels.disallow); - - assert.deepEqual(disallowedEnvs.split(','), newPolicy.env_keys.disallow); - assert.notDeepEqual(myPolicy.env_keys.disallow, newPolicy.env_keys.disallow); - - assert.strictEqual(maxLayers, newPolicy.layers.max); - assert.strictEqual(warningLayers, newPolicy.layers.warning); -}); diff --git a/test/policyengine.test.js b/test/policyengine.test.js new file mode 100644 index 0000000..62c96bb --- /dev/null +++ b/test/policyengine.test.js @@ -0,0 +1,463 @@ +const assert = require('assert'); +const fs = require('fs'); +const yaml = require('js-yaml'); + +const policyEngine = require('../lib/policyengine.js'); + +const defaultPolicy = yaml.safeLoad(fs.readFileSync('./default_policy.yaml', 'utf8')); +const emptyContainer = JSON.parse(fs.readFileSync(`${__dirname}/fixtures/empty_container.json`, 'utf8'))[0]; +const failingContainer = JSON.parse(fs.readFileSync(`${__dirname}/fixtures/failing_container.json`, 'utf8'))[0]; + +it('succeeds with an empty policy', () => { + const testPolicy = {}; + const testContainer = {}; + + const result = policyEngine.execute(testPolicy, testContainer); + assert(result.isPassing()); + + // Ensure message count matches test count + assert(policyEngine.enumerateTests.length === result.getMessages.length); +}); + +it('succeeds with an empty policy, empty container fixture', () => { + const testPolicy = {}; + const testContainer = emptyContainer; + + const result = policyEngine.execute(testPolicy, testContainer); + assert(result.isPassing()); + + // Ensure message count matches test count + assert(policyEngine.enumerateTests.length === result.getMessages.length); +}); + +it('fails with violations of the default policy', () => { + const testPolicy = defaultPolicy; + const result = policyEngine.execute(testPolicy, failingContainer); + + assert(!result.isPassing()); + + // Ensure message count matches test count + assert(policyEngine.enumerateTests().length === result.getMessages().length); +}); + +it('succeeds without disallowed labels', () => { + const testPolicy = { labels: { disallow: ['com.swipely.iam-docker.iam-profile', 'ABCDEF'] } }; + const testContainer = { Config: { Labels: { OTHER_ROLE: 12345 } } }; + + const result = policyEngine.validateLabels(testPolicy, testContainer, policyEngine.msgs()); + assert(result); +}); + +it('fails with disallowed labels', () => { + const testPolicy = { labels: { disallow: ['ABCDEF', 'com.swipely.iam-docker.iam-profile'] } }; + const testContainer = { Config: { Labels: { 'com.swipely.iam-docker.iam-profile': 12345 } } }; + + const result = policyEngine.validateLabels(testPolicy, testContainer, policyEngine.msgs()); + assert(!result); +}); + +it('fails with another disallowed label', () => { + const testPolicy = { labels: { disallow: ['com.swipely.iam-docker.msi-explicit-identity'] } }; + const testContainer = { Config: { Labels: { 'com.swipely.iam-docker.msi-explicit-identity': 12345 } } }; + + const result = policyEngine.validateLabels(testPolicy, testContainer, policyEngine.msgs()); + assert(!result); +}); + +it('succeeds without disallowed env keys', () => { + const testPolicy = { labels: { disallow: ['IAM_ROLE', 'ABCDEF'] } }; + const testContainer = { Config: { Env: ['OTHER_ROLE=12345', 'OTHER_OTHER_ROLE=67890'] } }; + + const result = policyEngine.validateEnvKeys(testPolicy, testContainer, policyEngine.msgs()); + assert(result); +}); + +it('fails with disallowed env keys', () => { + const testPolicy = { env_keys: { disallow: ['IAM_ROLE', 'ABCDEF'] } }; + const testContainer = { Config: { Env: ['IAM_ROLE=12345'] } }; + + const result = policyEngine.validateEnvKeys(testPolicy, testContainer, policyEngine.msgs()); + assert(!result); +}); + +it('fails with another disallowed env key', () => { + const testPolicy = { env_keys: { disallow: ['AGENT_FILL'] } }; + const testContainer = { Config: { Env: ['AGENT_FILL=true'] } }; + + const result = policyEngine.validateEnvKeys(testPolicy, testContainer, policyEngine.msgs()); + assert(!result); +}); + +it('succeeds with volumes, default volume restriction (none)', () => { + const testPolicy = {}; + const testContainer = { Config: { Volumes: { '/myvolume': {}, '/another-volume': {} } } }; + + const result = policyEngine.validateVolumes(testPolicy, testContainer, policyEngine.msgs()); + assert(result); +}); + +it('succeeds without volumes, without restrictions', () => { + const testPolicy = { volumes: { disallowed: false } }; + const testContainer = { Config: { Volumes: {} } }; + + const result = policyEngine.validateVolumes(testPolicy, testContainer, policyEngine.msgs()); + assert(result); +}); + +it('succeeds with volumes, without restrictions', () => { + const testPolicy = { volumes: { disallowed: false } }; + const testContainer = { Config: { Volumes: { '/myvolume': {}, '/another-volume': {} } } }; + + const result = policyEngine.validateVolumes(testPolicy, testContainer, policyEngine.msgs()); + assert(result); +}); + +it('succeeds without volumes, with restrictions', () => { + const testPolicy = { volumes: { disallowed: true } }; + const testContainer = { Config: { Volumes: {} } }; + + const result = policyEngine.validateVolumes(testPolicy, testContainer, policyEngine.msgs()); + assert(result); +}); + +it('fail with volumes, with restrictions', () => { + const testPolicy = { volumes: { disallowed: true } }; + const testContainer = { Config: { Volumes: { '/myvolume': {}, '/another-volume': {} } } }; + + const result = policyEngine.validateVolumes(testPolicy, testContainer, policyEngine.msgs()); + assert(!result); +}); + +it('succeeds without ports, without requirement', () => { + const testPolicy = { ports: { required: false } }; + const testContainer = {}; + + const result = policyEngine.validatePortRequirement(testPolicy, testContainer, policyEngine.msgs()); + assert(result); +}); + +it('fails without ports, with requirement', () => { + const testPolicy = { ports: { required: true } }; + const testContainer = {}; + + const result = policyEngine.validatePortRequirement(testPolicy, testContainer, policyEngine.msgs()); + assert(!result); +}); + +it('succeeds with ports, with requirement', () => { + const testPolicy = { ports: { required: true } }; + const testContainer = { ContainerConfig: { ExposedPorts: { '8080/tcp': {}, '8081/tcp': {} } } }; + + const result = policyEngine.validatePortRequirement(testPolicy, testContainer, policyEngine.msgs()); + assert(result); +}); + +it('succeeds with ports, without requirement', () => { + const testPolicy = { ports: { required: false } }; + const testContainer = { ContainerConfig: { ExposedPorts: { '8080/tcp': {}, '8081/tcp': {} } } }; + + const result = policyEngine.validatePortRequirement(testPolicy, testContainer, policyEngine.msgs()); + assert(result); +}); + +it('succeeds with ports, without range requirement', () => { + const testPolicy = { ports: { range: null } }; + const testContainer = { ContainerConfig: { ExposedPorts: { '8080/tcp': {}, '8081/tcp': {} } } }; + + const result = policyEngine.validatePortRange(testPolicy, testContainer, policyEngine.msgs()); + assert(result); +}); + +it('succeeds with ports, within range requirement', () => { + const testPolicy = { ports: { range: '1-8082' } }; + const testContainer = { ContainerConfig: { ExposedPorts: { '8080/tcp': {}, '8081/tcp': {} } } }; + + const result = policyEngine.validatePortRange(testPolicy, testContainer, policyEngine.msgs()); + assert(result); +}); + +it('fails with ports, with an invalid port range', () => { + const testPolicy = { ports: { range: '8081-8080' } }; + const testContainer = { ContainerConfig: { ExposedPorts: { '8080/tcp': {}, '8081/tcp': {} } } }; + + const result = policyEngine.validatePortRange(testPolicy, testContainer, policyEngine.msgs()); + assert(!result); +}); + +// NOTE: this is correct, since a required port is part of validatePortRequirement +it('succeeds without a port, with of port range', () => { + const testPolicy = { ports: { range: '1-100' } }; + const testContainer = { ContainerConfig: { ExposedPorts: {} } }; + + const result = policyEngine.validatePortRange(testPolicy, testContainer, policyEngine.msgs()); + assert(result); +}); + +it('succeeds without an input port, with a port range', () => { + const testPolicy = { ports: { range: '1-100' } }; + const testContainer = { ContainerConfig: {} }; + + const result = policyEngine.validatePortRange(testPolicy, testContainer, policyEngine.msgs()); + assert(result); +}); + +it('fails with a port, outside of port range', () => { + const testPolicy = { ports: { range: '1-100' } }; + const testContainer = { ContainerConfig: { ExposedPorts: { '101/tcp': {} } } }; + + const result = policyEngine.validatePortRange(testPolicy, testContainer, policyEngine.msgs()); + assert(!result); +}); + +it('fails with ports, one outside of port range', () => { + const testPolicy = { ports: { range: '1-100' } }; + const testContainer = { ContainerConfig: { ExposedPorts: { '50/tcp': {}, '101/tcp': {} } } }; + + const result = policyEngine.validatePortRange(testPolicy, testContainer, policyEngine.msgs()); + assert(!result); +}); + +it('fails with ports, both outside of port range', () => { + const testPolicy = { ports: { range: '1-100' } }; + const testContainer = { ContainerConfig: { ExposedPorts: { '101/tcp': {}, '102/tcp': {} } } }; + + const result = policyEngine.validatePortRange(testPolicy, testContainer, policyEngine.msgs()); + assert(!result); +}); + +it('succeeds at max size limit', () => { + const testPolicy = { size: { max: '10' } }; + const testContainer = { Size: 10000000 }; + + const result = policyEngine.validateContainerSize(testPolicy, testContainer, policyEngine.msgs()); + assert(result); +}); + +it('fails over max size limit', () => { + const testPolicy = { size: { max: '10' } }; + const testContainer = { Size: 10000001 }; + + const result = policyEngine.validateContainerSize(testPolicy, testContainer, policyEngine.msgs()); + assert(!result); +}); + +it('fails when warning >= max size', () => { + const testPolicy = { size: { max: 10, warning: 11 } }; + const testContainer = { Size: 10000000 }; + const msgs = policyEngine.msgs(); + const result = policyEngine.validateContainerSize(testPolicy, testContainer, msgs); + + assert(msgs.messages.length === 1); + assert(msgs.messages.shift()[0] === 'exception'); // Ensure that single message is an exception + assert(!result); +}); + +it('succeeds over warning, but under max size limit', () => { + const testPolicy = { size: { max: 10, warning: 5 } }; + const testContainer = { Size: 5000001 }; + const msgs = policyEngine.msgs(); + const result = policyEngine.validateContainerSize(testPolicy, testContainer, msgs); + + assert(msgs.messages.length === 1); + assert(msgs.messages.shift()[0] === 'warning'); // Ensure that single message is a warning + assert(result); +}); + +it('succeeds over warning, with no max size limit', () => { + const testPolicy = { size: { warning: 5 } }; + const testContainer = { Size: 5000001 }; + const msgs = policyEngine.msgs(); + const result = policyEngine.validateContainerSize(testPolicy, testContainer, msgs); + + assert(msgs.messages.length === 1); + assert(msgs.messages.shift()[0] === 'warning'); // Ensure that single message is a warning + assert(result); +}); + +it('succeeds without healthcheck, with restriction', () => { + const testPolicy = { healthcheck: { disallowed: true } }; + const testContainer = {}; + const msgs = policyEngine.msgs(); + const result = policyEngine.validateHealthCheck(testPolicy, testContainer, msgs); + + assert(result); +}); + +it('succeeds with "none" healthcheck, with restriction', () => { + const testPolicy = { healthcheck: { disallowed: true } }; + const testContainer = { ContainerConfig: { Healthcheck: { Test: ['NONE'] } } }; + const msgs = policyEngine.msgs(); + const result = policyEngine.validateHealthCheck(testPolicy, testContainer, msgs); + + assert(result); +}); + +it('succeeds with healthcheck, without restriction', () => { + const testPolicy = { healthcheck: { disallowed: false } }; + const testContainer = { ContainerConfig: { Healthcheck: { Test: ['CMD-SHELL', 'ls -l'] } } }; + const msgs = policyEngine.msgs(); + const result = policyEngine.validateHealthCheck(testPolicy, testContainer, msgs); + + assert(result); +}); + +it('succeeds with healthcheck, with default (not restricted)', () => { + const testPolicy = { healthcheck: { disallowed: false } }; + const testContainer = { ContainerConfig: { Healthcheck: { Test: ['CMD-SHELL', 'ls -l'] } } }; + const msgs = policyEngine.msgs(); + const result = policyEngine.validateHealthCheck(testPolicy, testContainer, msgs); + + assert(result); +}); + +it('fails with healthcheck, with restriction', () => { + const testPolicy = { healthcheck: { disallowed: true } }; + const testContainer = { ContainerConfig: { Healthcheck: { Test: ['CMD-SHELL', 'ls -l'] } } }; + const msgs = policyEngine.msgs(); + const result = policyEngine.validateHealthCheck(testPolicy, testContainer, msgs); + + assert(!result); +}); + +it('succeeds at max layer count', () => { + const testContainer = { ...failingContainer }; + assert(testContainer.RootFS.Layers.length > 0); + + const testPolicy = { layers: { max: testContainer.RootFS.Layers.length } }; + + const result = policyEngine.validateLayerCount(testPolicy, testContainer, policyEngine.msgs()); + assert(result); +}); + +it('fails over max size limit', () => { + const testPolicy = { layers: { max: '1' } }; + assert(emptyContainer.RootFS.Layers.length > 1); + + const result = policyEngine.validateLayerCount(testPolicy, emptyContainer, policyEngine.msgs()); + assert(!result); +}); + +it('fails when warning >= max layer count', () => { + const testPolicy = { layers: { max: 10, warning: 11 } }; + const msgs = policyEngine.msgs(); + const result = policyEngine.validateLayerCount(testPolicy, emptyContainer, msgs); + + assert(msgs.messages.length === 1); + assert(msgs.messages.shift()[0] === 'exception'); // Ensure that single message is an exception + assert(!result); +}); + +it('succeeds over warning, but under max size limit', () => { + const newMax = (emptyContainer.RootFS.Layers.length + 1); + const newWarning = 1; + const testPolicy = { layers: { max: newMax, warning: newWarning } }; + const layerCount = emptyContainer.RootFS.Layers.length; + assert(layerCount < newMax); + assert(layerCount > newWarning); + + const msgs = policyEngine.msgs(); + const result = policyEngine.validateLayerCount(testPolicy, emptyContainer, msgs); + + // Ensure there is only a single message in the stack + assert(msgs.messages.length === 1); + + // Ensure that single message is a warning + assert(msgs.messages.shift()[0] === 'warning'); + assert(result); +}); + +it('succeeds over warning, with no max size limit', () => { + const newWarning = 1; + const testPolicy = { layers: { warning: newWarning } }; + assert(emptyContainer.RootFS.Layers.length > newWarning); + const testContainer = { ...emptyContainer }; + const msgs = policyEngine.msgs(); + const result = policyEngine.validateLayerCount(testPolicy, testContainer, msgs); + + assert(msgs.messages.length === 1); + assert(msgs.messages.shift()[0] === 'warning'); // Ensure that single message is a warning + assert(result); +}); + +it('adds all overrides against an empty policy', () => { + const myPolicy = {}; + const maxSize = 50; + const warningSize = 25; + const disallowedLabels = 'ABC,DEF'; + const disallowedEnvs = 'IAM,ROLE'; + const maxLayers = 50; + const warningLayers = 10; + + const inputs = { + max: maxSize, + warning: warningSize, + labels: disallowedLabels, + envs: disallowedEnvs, + layers_max: maxLayers, + layers_warning: warningLayers, + }; + + const newPolicy = policyEngine.applyOverrides(myPolicy, inputs, []); + + assert(maxSize === newPolicy.size.max); + assert(warningSize === newPolicy.size.warning); + assert.deepEqual(disallowedLabels.split(','), newPolicy.labels.disallow); + assert.deepEqual(disallowedEnvs.split(','), newPolicy.env_keys.disallow); +}); + +it('overrides existing policy', () => { + const myPolicy = { + size: { + max: 654, + warning: 321, + }, + env_keys: { + disallow: ['KEY1', 'KEY2'], + }, + labels: { + disallow: ['LABEL1', 'LABEL2'], + }, + ports: { + range: '1-10000', + }, + layers: { + max: 100, + warning: 1, + }, + }; + + const maxSize = 9876; + const warningSize = 5432; + const disallowedLabels = 'UPDATED,LABELS'; + const disallowedEnvs = 'ENVROLES,HAVEBEENUPDATED'; + const newRange = '1-10'; + const maxLayers = 50; + const warningLayers = 10; + + const inputs = { + max: maxSize, + warning: warningSize, + labels: disallowedLabels, + envs: disallowedEnvs, + range: newRange, + layers_max: maxLayers, + layers_warning: warningLayers, + }; + + const newPolicy = policyEngine.applyOverrides(myPolicy, inputs, []); + + assert.strictEqual(maxSize, newPolicy.size.max); + assert.strictEqual(warningSize, newPolicy.size.warning); + assert.notStrictEqual(myPolicy.size.max, newPolicy.size.max); + assert.notStrictEqual(myPolicy.size.warning, newPolicy.size.warning); + assert.strictEqual(newRange, newPolicy.ports.range); + + assert.deepEqual(disallowedLabels.split(','), newPolicy.labels.disallow); + assert.notDeepEqual(myPolicy.labels.disallow, newPolicy.labels.disallow); + + assert.deepEqual(disallowedEnvs.split(','), newPolicy.env_keys.disallow); + assert.notDeepEqual(myPolicy.env_keys.disallow, newPolicy.env_keys.disallow); + + assert.strictEqual(maxLayers, newPolicy.layers.max); + assert.strictEqual(warningLayers, newPolicy.layers.warning); +});