diff --git a/collectors/azure/collector.js b/collectors/azure/collector.js index 86db0bd8af..e03fb633c3 100644 --- a/collectors/azure/collector.js +++ b/collectors/azure/collector.js @@ -181,7 +181,7 @@ var calls = { }, databaseAccounts: { list: { - url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.DocumentDB/databaseAccounts?api-version=2020-04-01' + url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.DocumentDB/databaseAccounts?api-version=2020-06-01-preview' } } }; diff --git a/collectors/google/collector.js b/collectors/google/collector.js index cb6e055886..4625446237 100644 --- a/collectors/google/collector.js +++ b/collectors/google/collector.js @@ -259,7 +259,7 @@ var collect = function(GoogleConfig, settings, callback) { helpers.authenticate(GoogleConfig) .then(client => { - + async.eachOfLimit(calls, 10, function(call, service, serviceCb) { if (!collection[service]) collection[service] = {}; diff --git a/config_example.js b/config_example.js index c89d6c26b7..f4380f0b4f 100644 --- a/config_example.js +++ b/config_example.js @@ -11,6 +11,14 @@ module.exports = { // session_token: process.env.AWS_SESSION_TOKEN || '', // plugins_remediate: ['bucketEncryptionInTransit'] }, + aws_remediate: { + // OPTION 1: If using a credential JSON file, enter the path below + // credential_file: '/path/to/file.json', + // OPTION 2: If using hard-coded credentials, enter them below + // access_key: process.env.AWS_ACCESS_KEY_ID || '', + // secret_access_key: process.env.AWS_SECRET_ACCESS_KEY || '', + // session_token: process.env.AWS_SESSION_TOKEN || '', + }, azure: { // OPTION 1: If using a credential JSON file, enter the path below // credential_file: '/path/to/file.json', @@ -20,6 +28,15 @@ module.exports = { // directory_id: process.env.AZURE_DIRECTORY_ID || '', // subscription_id: process.env.AZURE_SUBSCRIPTION_ID || '' }, + azure_remediate: { + // OPTION 1: If using a credential JSON file, enter the path below + // credential_file: '/path/to/file.json', + // OPTION 2: If using hard-coded credentials, enter them below + // application_id: process.env.AZURE_APPLICATION_ID || '', + // key_value: process.env.AZURE_KEY_VALUE || '', + // directory_id: process.env.AZURE_DIRECTORY_ID || '', + // subscription_id: process.env.AZURE_SUBSCRIPTION_ID || '' + }, google: { // OPTION 1: If using a credential JSON file, enter the path below // credential_file: process.env.GOOGLE_APPLICATION_CREDENTIALS || '/path/to/file.json', diff --git a/engine.js b/engine.js index 2b9cea1235..939d2132d8 100644 --- a/engine.js +++ b/engine.js @@ -2,7 +2,17 @@ var async = require('async'); var exports = require('./exports.js'); var suppress = require('./postprocess/suppress.js'); var output = require('./postprocess/output.js'); - +var azureHelper = require('./helpers/azure/auth.js'); + +function runAuth(settings, remediateConfig, callback) { + if (settings.cloud && settings.cloud == 'azure') { + azureHelper.login(remediateConfig, function(err, loginData) { + if (err) return (callback(err)); + remediateConfig.token = loginData.token; + return callback(); + }); + } else callback(); +} /** * The main function to execute CloudSploit scans. * @param cloudConfig The configuration for the cloud provider. @@ -138,84 +148,102 @@ var engine = function(cloudConfig, settings) { console.log('INFO: Analysis complete. Scan report to follow...'); var maximumStatus = 0; - - async.mapValuesLimit(plugins, 10, function(plugin, key, pluginDone) { - if (skippedPlugins.indexOf(key) > -1) return pluginDone(null, 0); - - var postRun = function(err, results) { - if (err) return console.log(`ERROR: ${err}`); - if (!results || !results.length) { - console.log(`Plugin ${plugin.title} returned no results. There may be a problem with this plugin.`); - } else { - for (var r in results) { - // If we have suppressed this result, then don't process it - // so that it doesn't affect the return code. - if (suppressionFilter([key, results[r].region || 'any', results[r].resource || 'any'].join(':'))) { - continue; - } - - var complianceMsg = []; - if (settings.compliance && settings.compliance.length) { - settings.compliance.forEach(function(c) { - if (plugin.compliance && plugin.compliance[c]) { - complianceMsg.push(`${c.toUpperCase()}: ${plugin.compliance[c]}`); - } - }); - } - complianceMsg = complianceMsg.join('; '); - if (!complianceMsg.length) complianceMsg = null; - - // Write out the result (to console or elsewhere) - outputHandler.writeResult(results[r], plugin, key, complianceMsg); - - // Add this to our tracking for the worst status to calculate - // the exit code - maximumStatus = Math.max(maximumStatus, results[r].status); - // Remediation - if (settings.remediate && settings.remediate.length) { - if (settings.remediate.indexOf(key) > -1) { - if (results[r].status === 2) { - var resource = results[r].resource; - var event = {}; - event.region = results[r].region; - event['remediation_file'] = {}; - event['remediation_file'] = initializeFile(event['remediation_file'], 'execute', key, resource); - plugin.remediate(cloudConfig, collection, event, resource, (err, result) => { - if (err) return console.log(err); - return console.log(result); - }); + + function executePlugins(cloudRemediateConfig) { + async.mapValuesLimit(plugins, 10, function(plugin, key, pluginDone) { + if (skippedPlugins.indexOf(key) > -1) return pluginDone(null, 0); + + var postRun = function(err, results) { + if (err) return console.log(`ERROR: ${err}`); + if (!results || !results.length) { + console.log(`Plugin ${plugin.title} returned no results. There may be a problem with this plugin.`); + } else { + for (var r in results) { + // If we have suppressed this result, then don't process it + // so that it doesn't affect the return code. + if (suppressionFilter([key, results[r].region || 'any', results[r].resource || 'any'].join(':'))) { + continue; + } + + var complianceMsg = []; + if (settings.compliance && settings.compliance.length) { + settings.compliance.forEach(function(c) { + if (plugin.compliance && plugin.compliance[c]) { + complianceMsg.push(`${c.toUpperCase()}: ${plugin.compliance[c]}`); + } + }); + } + complianceMsg = complianceMsg.join('; '); + if (!complianceMsg.length) complianceMsg = null; + + // Write out the result (to console or elsewhere) + outputHandler.writeResult(results[r], plugin, key, complianceMsg); + + // Add this to our tracking for the worst status to calculate + // the exit code + maximumStatus = Math.max(maximumStatus, results[r].status); + // Remediation + if (settings.remediate && settings.remediate.length) { + if (settings.remediate.indexOf(key) > -1) { + if (results[r].status === 2) { + var resource = results[r].resource; + var event = {}; + event.region = results[r].region; + event['remediation_file'] = {}; + event['remediation_file'] = initializeFile(event['remediation_file'], 'execute', key, resource); + plugin.remediate(cloudRemediateConfig, collection, event, resource, (err, result) => { + if (err) return console.log(err); + return console.log(result); + }); + } } } } + } - + setTimeout(function() { pluginDone(err, maximumStatus); }, 0); + }; + + if (plugin.asl) { + console.log(`INFO: Using custom ASL for plugin: ${plugin.title}`); + // Inject APIs and resource maps + plugin.asl.apis = plugin.apis; + var aslConfig = require('./helpers/asl/config.json'); + var aslVersion = plugin.asl.version ? plugin.asl.version : aslConfig.current_version; + let aslRunner; + try { + aslRunner = require(`./helpers/asl/asl-${aslVersion}.js`); + + } catch (e) { + postRun('Error: ASL: Wrong ASL Version: ', e); + } + + aslRunner(collection, plugin.asl, resourceMap, postRun); + } else { + plugin.run(collection, settings, postRun); } - setTimeout(function() { pluginDone(err, maximumStatus); }, 0); - }; - - if (plugin.asl) { - console.log(`INFO: Using custom ASL for plugin: ${plugin.title}`); - // Inject APIs and resource maps - plugin.asl.apis = plugin.apis; - var aslConfig = require('./helpers/asl/config.json'); - var aslVersion = plugin.asl.version ? plugin.asl.version : aslConfig.current_version; - var aslRunner = require(`./helpers/asl/asl-${aslVersion}.js`); - aslRunner(collection, plugin.asl, resourceMap, postRun); - } else { - plugin.run(collection, settings, postRun); - } - }, function(err) { - if (err) return console.log(err); - // console.log(JSON.stringify(collection, null, 2)); - outputHandler.close(); - if (settings.exit_code) { - // The original cloudsploit always has a 0 exit code. With this option, we can have - // the exit code depend on the results (useful for integration with CI systems) - console.log(`INFO: Exiting with exit code: ${maximumStatus}`); - process.exitCode = maximumStatus; - } - console.log('INFO: Scan complete'); - }); + }, function(err) { + if (err) return console.log(err); + // console.log(JSON.stringify(collection, null, 2)); + outputHandler.close(); + if (settings.exit_code) { + // The original cloudsploit always has a 0 exit code. With this option, we can have + // the exit code depend on the results (useful for integration with CI systems) + console.log(`INFO: Exiting with exit code: ${maximumStatus}`); + process.exitCode = maximumStatus; + } + console.log('INFO: Scan complete'); + }); + } + + if (settings.remediate && settings.remediate.length && cloudConfig.remediate) { + runAuth(settings, cloudConfig.remediate, function(err) { + if (err) return console.log(err); + executePlugins(cloudConfig.remediate); + }); + } else { + executePlugins(cloudConfig); + } }); }; diff --git a/helpers/asl/asl-1.js b/helpers/asl/asl-1.js index 81017854a6..423e1976e5 100644 --- a/helpers/asl/asl-1.js +++ b/helpers/asl/asl-1.js @@ -43,6 +43,18 @@ var transform = function(val, transformation) { }; var compositeResult = function(inputResultsArr, resource, region, results, logical) { + let failingResults = []; + let passingResults = []; + inputResultsArr.forEach(localResult => { + if (localResult.status === 2) { + failingResults.push(localResult.message); + } + + if (localResult.status === 0) { + passingResults.push(localResult.message); + } + }); + if (!logical) { results.push({ status: inputResultsArr[0].status, @@ -51,42 +63,34 @@ var compositeResult = function(inputResultsArr, resource, region, results, logic region: region }); } else if (logical === 'AND') { - var failingResult = inputResultsArr.find(localResult => { - return localResult.status === 2; - }); - - if (failingResult) { + if (failingResults && failingResults.length) { results.push({ - status: failingResult.status, + status: 2, resource: resource, - message: failingResult.message, + message: failingResults.join(' and '), region: region }); } else { results.push({ status: 0, resource: resource, - message: 'All conditions passed', + message: passingResults.join(' and '), region: region }); } } else { - var passingResult = inputResultsArr.find(localResult => { - return localResult.status === 0; - }); - - if (passingResult) { + if (passingResults && passingResults.length) { results.push({ - status: passingResult.status, + status: 0, resource: resource, - message: passingResult.message, + message: passingResults.join(' and '), region: region }); } else { results.push({ status: 2, resource: resource, - message: 'All conditions failed', + message: failingResults.join(' and '), region: region }); } @@ -99,14 +103,14 @@ function evaluateCondition(obj, condition, inputResultsArr){ } var validate = function(obj, condition, inputResultsArr) { - var result = 0; - var message = []; - var override = false; + let result = 0; + let message = []; + let override = false; // Extract the values for the conditions if (condition.property) { - var conditionResult = 0; - var property; + let conditionResult = 0; + let property; if (condition.property.length === 1) property = condition.property[0]; else if (condition.property.length > 1) property = condition.property; @@ -251,14 +255,64 @@ var validate = function(obj, condition, inputResultsArr) { return resultObj; }; +var runConditions = function(input, data, results, resourcePath, resourceName, region) { + let dataToValidate; + let newPath; + let newData; + let validated; + let parsedResource; + + let inputResultsArr = []; + let logical; + let localInput = JSON.parse(JSON.stringify(input)); + localInput.conditions.forEach(condition => { + logical = condition.logical; + if (condition.property && condition.property.indexOf('[*]') > -1) { + dataToValidate = parse(data, condition.property); + newPath = dataToValidate[1]; + newData = dataToValidate[0]; + condition.property = newPath; + + condition.validated = evaluateCondition(newData, condition, inputResultsArr); + parsedResource = parse(data, resourcePath)[0]; + if (typeof parsedResource !== 'string') parsedResource = null; + } else { + dataToValidate = parse(data, condition.property); + if (dataToValidate.length === 1) { + validated = evaluateCondition(data, condition, inputResultsArr); + parsedResource = parse(data, resourcePath)[0]; + if (typeof parsedResource !== 'string') parsedResource = null; + } else { + newPath = dataToValidate[1]; + newData = dataToValidate[0]; + condition.property = newPath; + newData.forEach(element =>{ + condition.validated = evaluateCondition(element, condition, inputResultsArr); + parsedResource = parse(data, resourcePath)[0]; + if (typeof parsedResource !== 'string') parsedResource = null; + + results.push({ + status: validated.status, + resource: parsedResource ? parsedResource : resourceName, + message: validated.message, + region: region + }); + }); + } + } + }); + + compositeResult(inputResultsArr, parsedResource, region, results, logical); +}; + var asl = function(source, input, resourceMap, callback) { if (!source || !input) return callback('No source or input provided'); if (!input.apis || !input.apis[0]) return callback('No APIs provided for input'); if (!input.conditions || !input.conditions.length) return callback('No conditions provided for input'); - var service = input.conditions[0].service; - var api = input.conditions[0].api; - var resourcePath; + let service = input.conditions[0].service; + let api = input.conditions[0].api; + let resourcePath; if (resourceMap && resourceMap[service] && resourceMap[service][api]) { @@ -268,15 +322,9 @@ var asl = function(source, input, resourceMap, callback) { if (!source[service]) return callback(`Source data did not contain service: ${service}`); if (!source[service][api]) return callback(`Source data did not contain API: ${api}`); - var results = []; - var dataToValidate; - var newData; - var newPath; - var validated; - var parsedResource; - - for (var region in source[service][api]) { - var regionVal = source[service][api][region]; + let results = []; + for (let region in source[service][api]) { + let regionVal = source[service][api][region]; if (typeof regionVal !== 'object') continue; if (regionVal.err) { results.push({ @@ -293,58 +341,11 @@ var asl = function(source, input, resourceMap, callback) { }); } else { regionVal.data.forEach(function(regionData) { - dataToValidate = parse(regionData, input.conditions.property); - var inputResultsArr = []; - var logical; - var localInput = JSON.parse(JSON.stringify(input)); - localInput.conditions.forEach(condition => { - logical = condition.logical; - if (dataToValidate.length === 1) { - validated = evaluateCondition(regionData, condition, inputResultsArr); - parsedResource = parse(regionData, resourcePath)[0]; - if (typeof parsedResource !== 'string') parsedResource = null; - - } else { - newPath = dataToValidate[1]; - newData = dataToValidate[0]; - condition.property = newPath; - newData.forEach(element => { - validated = evaluateCondition(element, condition, inputResultsArr); - parsedResource = parse(newData, resourcePath)[0]; - if (typeof parsedResource !== 'string') parsedResource = null; - - }); - } - }); - compositeResult(inputResultsArr, parsedResource, region, results, logical); + runConditions(input, regionData, results, resourcePath, '', region); }); } } else if (regionVal.data && Object.keys(regionVal.data).length > 0) { - dataToValidate = parse(regionVal.data, input.conditions.property); - let inputResultsArr = []; - let logical; - let localInput = JSON.parse(JSON.stringify(input)); - localInput.conditions.forEach(condition => { - logical = condition.logical; - if (dataToValidate.length === 1) { - validated = evaluateCondition(regionVal.data, condition, inputResultsArr); - parsedResource = parse(regionVal.data, resourcePath)[0]; - if (typeof parsedResource !== 'string') parsedResource = null; - - } else { - newPath = dataToValidate[1]; - newData = dataToValidate[0]; - condition.property = newPath; - newData.forEach(element => { - validated = evaluateCondition(element, condition, inputResultsArr); - parsedResource = parse(newData, resourcePath)[0]; - if (typeof parsedResource !== 'string') parsedResource = null; - - }); - } - }); - compositeResult(inputResultsArr, parsedResource, region, results, logical); - + runConditions(input, regionVal.data, results, resourcePath, '', region); } else { if (!Object.keys(regionVal).length) { results.push({ @@ -353,8 +354,8 @@ var asl = function(source, input, resourceMap, callback) { region: region }); } else { - for (var resourceName in regionVal) { - var resourceObj = regionVal[resourceName]; + for (let resourceName in regionVal) { + let resourceObj = regionVal[resourceName]; if (resourceObj.err) { results.push({ status: 3, @@ -370,47 +371,7 @@ var asl = function(source, input, resourceMap, callback) { region: region }); } else { - var inputResultsArr = []; - var logical; - var localInput = JSON.parse(JSON.stringify(input)); - localInput.conditions.forEach(condition => { - logical = condition.logical; - if (condition.property && condition.property.indexOf('[*]') > -1) { - dataToValidate = parse(resourceObj.data, condition.property); - newPath = dataToValidate[1]; - newData = dataToValidate[0]; - condition.property = newPath; - - condition.validated = evaluateCondition(newData, condition, inputResultsArr); - parsedResource = parse(resourceObj.data, resourcePath)[0]; - if (typeof parsedResource !== 'string') parsedResource = null; - } else { - dataToValidate = parse(resourceObj.data, condition.property); - if (dataToValidate.length === 1) { - validated = evaluateCondition(resourceObj.data, condition, inputResultsArr); - parsedResource = parse(resourceObj.data, resourcePath)[0]; - if (typeof parsedResource !== 'string') parsedResource = null; - } else { - newPath = dataToValidate[1]; - newData = dataToValidate[0]; - condition.property = newPath; - newData.forEach(element =>{ - condition.validated = evaluateCondition(element, condition, inputResultsArr); - parsedResource = parse(resourceObj.data, resourcePath)[0]; - if (typeof parsedResource !== 'string') parsedResource = null; - - results.push({ - status: validated.status, - resource: parsedResource ? parsedResource : resourceName, - message: validated.message, - region: region - }); - }); - } - } - }); - console.log(inputResultsArr); - compositeResult(inputResultsArr, parsedResource ? parsedResource : resourceName, region, results, logical); + runConditions(input, resourceObj.data, results, resourcePath, resourceName, region); } } } diff --git a/helpers/aws/functions.js b/helpers/aws/functions.js index 21f4e3e5e3..8a2aeff94d 100644 --- a/helpers/aws/functions.js +++ b/helpers/aws/functions.js @@ -291,6 +291,8 @@ function extractStatementPrincipals(statement) { return [principal]; } + if (!principal.AWS) return response; + var awsPrincipals = principal.AWS; if (!Array.isArray(awsPrincipals)) { awsPrincipals = [awsPrincipals]; @@ -349,7 +351,7 @@ function filterDenyPermissionsByPrincipal(permissionsMap, principal) { return response; } -function isValidCondition(statement, allowedConditionKeys, iamConditionOperators, fetchConditionPrincipals) { +function isValidCondition(statement, allowedConditionKeys, iamConditionOperators, fetchConditionPrincipals, accountId) { if (statement.Condition && statement.Effect) { var effect = statement.Effect; var values = []; @@ -360,10 +362,13 @@ function isValidCondition(statement, allowedConditionKeys, iamConditionOperators var subCondition = statement.Condition[operator]; for (var key of Object.keys(subCondition)) { if (!allowedConditionKeys.some(conditionKey=> key.includes(conditionKey))) return false; - var value = subCondition[key]; if (iamConditionOperators.string[effect].includes(defaultOperator) || iamConditionOperators.arn[effect].includes(defaultOperator)) { + if (key === 'kms:CallerAccount' && typeof value === 'string' && effect === 'Allow' && value === accountId) { + values.push(value); + return values; + } if (!value.length || value === '*') return false; else if (/^[0-9]{12}$/.test(value) || /^arn:aws:(iam|sts)::.+/.test(value)) values.push(value); } else if (defaultOperator === 'Bool') { diff --git a/helpers/azure/auth.js b/helpers/azure/auth.js index db4584f883..8803901307 100644 --- a/helpers/azure/auth.js +++ b/helpers/azure/auth.js @@ -92,7 +92,7 @@ module.exports = { headers: headers, body: params.body ? JSON.stringify(params.body) : null }, function(error, response, body) { - if (response && response.statusCode === 200 && body) { + if (response && [200, 202].includes(response.statusCode) && body) { try { body = JSON.parse(body); } catch (e) { diff --git a/helpers/google/index.js b/helpers/google/index.js index b229303fc9..ccc678f6b8 100644 --- a/helpers/google/index.js +++ b/helpers/google/index.js @@ -7,6 +7,33 @@ const {JWT} = require('google-auth-library'); var async = require('async'); +var getProjects = async function(client, filter, callback) { + const cloudresourcemanager = google.cloudresourcemanager('v1'); + + const request = { + auth: client, + filter: `lifecycleState: ${filter}` + }; + + let response; + let projects = []; + + do { + if (response && response.nextPageToken) { + request.pageToken = response.nextPageToken; + } + response = (await cloudresourcemanager.projects.list(request)).data; + const projectsPage = response.projects; + if (projectsPage) { + for (let i = 0; i < projectsPage.length; i++) { + projects.push(projectsPage[i]); + } + } + } while (response.nextPageToken); + + return callback(projects); +}; + var regions = function() { return regRegions; }; @@ -20,6 +47,23 @@ var authenticate = async function(GoogleConfig) { return client; }; +var projectsList = function(client, cb) { + getProjects(client, 'ACTIVE', function(projects) { + cb(projects); + }); +}; + +var deletedProjectsList = function(client, cb) { + let deletedProjects = []; + + getProjects(client, 'DELETE_REQUESTED', function(delReqProjects) { + getProjects(client, 'DELETE_IN_PROGRESS', function(delInProgress) { + deletedProjects = delReqProjects.concat(delInProgress); + cb(deletedProjects); + }); + }); +}; + var processCall = function(GoogleConfig, collection, settings, regions, call, service, client, serviceCb) { // Loop through each of the service's functions if (call.manyApi) { @@ -333,7 +377,9 @@ var helpers = { regions: regions, MAX_REGIONS_AT_A_TIME: 6, authenticate: authenticate, - processCall: processCall + processCall: processCall, + projectsList: projectsList, + deletedProjectsList: deletedProjectsList }; for (var s in shared) helpers[s] = shared[s]; diff --git a/index.js b/index.js index 8b08674bce..7b5d564fad 100755 --- a/index.js +++ b/index.js @@ -205,5 +205,42 @@ if (config.credentials.aws.credential_file) { process.exit(1); } +if (settings.remediate && settings.remediate.length) { + if (!config.credentials[`${settings.cloud}_remediate`]) { + console.error('ERROR: No credentials provided for remediation.'); + process.exit(1); + } + if (config.credentials.aws_remediate && config.credentials.aws_remediate.credential_file) { + cloudConfig.remediate = loadHelperFile(config.credentials.aws_remediate.credential_file); + if (!cloudConfig.remediate || !cloudConfig.remediate.accessKeyId || !cloudConfig.remediate.secretAccessKey) { + console.error('ERROR: AWS credential file for remediation does not have accessKeyId or secretAccessKey properties'); + process.exit(1); + } + } else if (config.credentials.aws_remediate && config.credentials.aws_remediate.access_key) { + checkRequiredKeys(config.credentials.aws_remediate, ['secret_access_key']); + cloudConfig.remediate = { + accessKeyId: config.credentials.aws_remediate.access_key, + secretAccessKey: config.credentials.aws_remediate.secret_access_key, + sessionToken: config.credentials.aws_remediate.session_token + }; + } else if (config.credentials.azure_remediate && config.credentials.azure_remediate.credential_file) { + cloudConfig.remediate = loadHelperFile(config.credentials.azure_remediate.credential_file); + if (!cloudConfig.remediate || !cloudConfig.remediate.ApplicationID || !cloudConfig.remediate.KeyValue || !cloudConfig.remediate.DirectoryID || !cloudConfig.remediate.SubscriptionID) { + console.error('ERROR: Azure credential file for remediation does not have ApplicationID, KeyValue, DirectoryID, or SubscriptionID'); + process.exit(1); + } + } else if (config.credentials.azure_remediate && config.credentials.azure_remediate.application_id) { + checkRequiredKeys(config.credentials.azure_remediate, ['key_value', 'directory_id', 'subscription_id']); + cloudConfig.remediate = { + ApplicationID: config.credentials.azure_remediate.application_id, + KeyValue: config.credentials.azure_remediate.key_value, + DirectoryID: config.credentials.azure_remediate.directory_id, + SubscriptionID: config.credentials.azure_remediate.subscription_id + }; + } else { + console.error('ERROR: Config file does not contain any valid credential configs for remediation.'); + process.exit(1); + } +} // Now execute the scans using the defined configuration information. engine(cloudConfig, settings); diff --git a/plugins/aws/es/esEncryptedDomain.js b/plugins/aws/es/esEncryptedDomain.js index 7151eed8af..417ae58c7c 100644 --- a/plugins/aws/es/esEncryptedDomain.js +++ b/plugins/aws/es/esEncryptedDomain.js @@ -9,6 +9,26 @@ module.exports = { link: 'https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/encryption-at-rest.html', recommended_action: 'Ensure encryption-at-rest is enabled for all ElasticSearch domains.', apis: ['ES:listDomainNames', 'ES:describeElasticsearchDomain'], + remediation_description: 'ES domain will be encrypted with KMS.', + remediation_min_version: '202102151900', + apis_remediate: ['ES:listDomainNames', 'KMS:listKeys', 'KMS:describeKey'], + remediation_inputs: { + kmsKeyIdForES: { + name: '(Optional) KMS Key Id For ElasticSearch', + description: 'KMS Key Id that will be used to encrypt ElasticSearch domain', + regex: '^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}$', + required: false + } + }, + actions: { + remediate: ['ES:updateElasticsearchDomainConfig'], + rollback: ['ES:updateElasticsearchDomainConfig'] + }, + permissions: { + remediate: ['es:UpdateElasticsearchDomainConfig'], + rollback: ['es:UpdateElasticsearchDomainConfig'] + }, + realtime_triggers: ['es:CreateElasticsearchDomain', 'es:UpdateElasticsearchDomainConfig'], run: function(cache, settings, callback) { var results = []; @@ -62,5 +82,68 @@ module.exports = { }, function() { callback(null, results, source); }); + }, + remediate: function(config, cache, settings, resource, callback) { + var putCall = this.actions.remediate; + var pluginName = 'esEncryptedDomain'; + let defaultKeyDesc = 'Default master key that protects my Elasticsearch data when no other key is defined'; + var domainNameArr = resource.split(':'); + var domain = domainNameArr[domainNameArr.length - 1].split('/')[1]; + + // find the location of the domain needing to be remediated + var domainLocation = domainNameArr[3]; + + // add the location of the domain to the config + config.region = domainLocation; + + var params = {}; + + // create the params necessary for the remediation + if (settings.input && + settings.input.kmsKeyIdForES) { + params = { + DomainName: domain, + EncryptionAtRestOptions: { + 'Enabled': true, + 'KmsKeyId': settings.input.kmsKeyIdForES + }, + }; + } else { + let defaultKmsKeyId = helpers.getDefaultKeyId(cache, config.region, defaultKeyDesc); + if (!defaultKmsKeyId) return callback(`No default ElasticSearch key for the region ${config.region}`); + params = { + DomainName: domain, + EncryptionAtRestOptions: { + 'Enabled': true, + 'KmsKeyId': defaultKmsKeyId + }, + }; + } + + var remediation_file = settings.remediation_file; + remediation_file['pre_remediate']['actions'][pluginName][resource] = { + 'Encryption': 'Disabled', + 'ES': resource + }; + + // passes the config, put call, and params to the remediate helper function + helpers.remediatePlugin(config, putCall[0], params, function(err) { + if (err) { + remediation_file['remediate']['actions'][pluginName]['error'] = err; + return callback(err, null); + } + + let action = params; + action.action = putCall; + + remediation_file['post_remediate']['actions'][pluginName][resource] = action; + remediation_file['remediate']['actions'][pluginName][resource] = { + 'Action': 'ENCRYPTED', + 'ES': domain + }; + + settings.remediation_file = remediation_file; + return callback(null, action); + }); } }; diff --git a/plugins/aws/es/esExposedDomain.js b/plugins/aws/es/esExposedDomain.js index 845cbe3201..df85f8c883 100644 --- a/plugins/aws/es/esExposedDomain.js +++ b/plugins/aws/es/esExposedDomain.js @@ -41,7 +41,7 @@ module.exports = { if (!describeElasticsearchDomain || describeElasticsearchDomain.err || - !describeElasticsearchDomain.data || + !describeElasticsearchDomain.data || !describeElasticsearchDomain.data.DomainStatus) { helpers.addResult( results, 3, @@ -49,26 +49,26 @@ module.exports = { return cb(); } - var goodStatements = []; + var exposed; if (describeElasticsearchDomain.data.DomainStatus.AccessPolicies) { - var accessPolicies = JSON.parse(describeElasticsearchDomain.data.DomainStatus.AccessPolicies); + var statements = helpers.normalizePolicyDocument(describeElasticsearchDomain.data.DomainStatus.AccessPolicies); - if (accessPolicies.Statement && accessPolicies.Statement.length) { - accessPolicies.Statement.forEach(statement => { - if (statement.Principal && statement.Principal.AWS && statement.Principal.AWS != '*') { - goodStatements.push(statement); - } - }); - - if (goodStatements.length === accessPolicies.Statement.length) { - helpers.addResult(results, 0, - 'Domain :' + domain.DomainName + ': is not exposed to all AWS accounts', - region, resource); - } else { + if (statements && statements.length) { + for (let statement of statements) { + var statementPrincipals = helpers.extractStatementPrincipals(statement); + exposed = statementPrincipals.find(principal => principal == '*'); + if (exposed) break; + } + + if (exposed) { helpers.addResult(results, 2, 'Domain :' + domain.DomainName + ': is exposed to all AWS accounts', region, resource); + } else { + helpers.addResult(results, 0, + 'Domain :' + domain.DomainName + ': is not exposed to all AWS accounts', + region, resource); } } else { helpers.addResult(results, 2, @@ -83,7 +83,7 @@ module.exports = { }, function() { rcb(); }); - + }, function() { callback(null, results, source); }); diff --git a/plugins/aws/es/esExposedDomain.spec.js b/plugins/aws/es/esExposedDomain.spec.js index f809b5c5e4..536f3e7162 100644 --- a/plugins/aws/es/esExposedDomain.spec.js +++ b/plugins/aws/es/esExposedDomain.spec.js @@ -261,7 +261,7 @@ const createNullCache = () => { describe('esExposedDomain', function () { describe('run', function () { it('should FAIL if domain is exposed to all AWS accounts', function (done) { - const cache = createCache([domainNames[1]], domains[0]); + const cache = createCache([domainNames[1]], domains[1]); esExposedDomain.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(2); diff --git a/plugins/aws/es/esNodeToNodeEncryption.js b/plugins/aws/es/esNodeToNodeEncryption.js index 9a81447b18..f6153807cd 100644 --- a/plugins/aws/es/esNodeToNodeEncryption.js +++ b/plugins/aws/es/esNodeToNodeEncryption.js @@ -9,6 +9,18 @@ module.exports = { link: 'https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/ntn.html', recommended_action: 'Ensure node-to-node encryption is enabled for all ElasticSearch domains.', apis: ['ES:listDomainNames', 'ES:describeElasticsearchDomain'], + remediation_description: 'ES domain will be configured to use node-to-node encryption.', + remediation_min_version: '202102152200', + apis_remediate: ['ES:listDomainNames'], + actions: { + remediate: ['ES:updateElasticsearchDomainConfig'], + rollback: ['ES:updateElasticsearchDomainConfig'] + }, + permissions: { + remediate: ['es:UpdateElasticsearchDomainConfig'], + rollback: ['es:UpdateElasticsearchDomainConfig'] + }, + realtime_triggers: ['es:CreateElasticsearchDomain', 'es:UpdateElasticsearchDomainConfig'], run: function(cache, settings, callback) { var results = []; @@ -62,5 +74,50 @@ module.exports = { }, function() { callback(null, results, source); }); + }, + remediate: function(config, cache, settings, resource, callback) { + var putCall = this.actions.remediate; + var pluginName = 'esNodeToNodeEncryption'; + var domainNameArr = resource.split(':'); + var domain = domainNameArr[domainNameArr.length - 1].split('/')[1]; + + // find the location of the domain needing to be remediated + var domainLocation = domainNameArr[3]; + + // add the location of the domain to the config + config.region = domainLocation; + + // create the params necessary for the remediation + var params = { + DomainName: domain, + NodeToNodeEncryptionOptions: { + Enabled: true + }, + }; + + var remediation_file = settings.remediation_file; + remediation_file['pre_remediate']['actions'][pluginName][resource] = { + 'NodeToNodeEncryption': 'Disabled', + 'ES': resource + }; + + // passes the config, put call, and params to the remediate helper function + helpers.remediatePlugin(config, putCall[0], params, function(err) { + if (err) { + remediation_file['remediate']['actions'][pluginName]['error'] = err; + return callback(err, null); + } + + let action = params; + action.action = putCall; + remediation_file['post_remediate']['actions'][pluginName][resource] = action; + remediation_file['remediate']['actions'][pluginName][resource] = { + 'Action': 'NodeToNodeENCRYPTED', + 'ES': domain + }; + + settings.remediation_file = remediation_file; + return callback(null, action); + }); } }; diff --git a/plugins/aws/iam/accessKeysRotated.js b/plugins/aws/iam/accessKeysRotated.js index d72395a1f1..846b82e82c 100644 --- a/plugins/aws/iam/accessKeysRotated.js +++ b/plugins/aws/iam/accessKeysRotated.js @@ -32,18 +32,6 @@ module.exports = { default: 90 } }, - asl: { - conditions: [ - { - service: 'iam', - api: 'generateCredentialReport', - property: 'access_key_1_last_rotated', - transform: 'DAYSFROM', - op: 'GT', - value: 90 - } - ] - }, run: function(cache, settings, callback) { var config = { diff --git a/plugins/aws/iam/trustedCrossAccountRoles.js b/plugins/aws/iam/trustedCrossAccountRoles.js index 33ae4573e1..e31de47b89 100644 --- a/plugins/aws/iam/trustedCrossAccountRoles.js +++ b/plugins/aws/iam/trustedCrossAccountRoles.js @@ -14,11 +14,23 @@ module.exports = { description: 'A comma-separated list of trusted cross account principals', regex: '^.*$', default: '' + }, + whitelisted_aws_account_principals_regex: { + name: 'Whitelisted AWS Account Principals Regex', + description: 'If set, plugin will compare cross account principals against this regex instead of otherwise given comma-separated list' + + 'Example regex: ^arn:aws:iam::(111111111111|222222222222|):.+$', + regex: '^.*$', + default: '' } }, run: function(cache, settings, callback) { - var whitelisted_aws_account_principals = settings.whitelisted_aws_account_principals || this.settings.whitelisted_aws_account_principals.default; + var config= { + whitelisted_aws_account_principals : settings.whitelisted_aws_account_principals || this.settings.whitelisted_aws_account_principals.default, + whitelisted_aws_account_principals_regex : settings.whitelisted_aws_account_principals_regex || this.settings.whitelisted_aws_account_principals_regex.default + }; + var makeRegexBased = (config.whitelisted_aws_account_principals_regex.length) ? true : false; + config.whitelisted_aws_account_principals_regex = new RegExp(config.whitelisted_aws_account_principals_regex); var results = []; var source = {}; @@ -63,7 +75,10 @@ module.exports = { var principals = helpers.crossAccountPrincipal(statement.Principal, accountId, true); if (principals.length) { principals.forEach(principal => { - if (!whitelisted_aws_account_principals.includes(principal) && + if (makeRegexBased) { + if (!config.whitelisted_aws_account_principals_regex.test(principal) && + !restrictedAccountPrincipals.includes(principal)) restrictedAccountPrincipals.push(principal); + } else if (!config.whitelisted_aws_account_principals.includes(principal) && !restrictedAccountPrincipals.includes(principal)) restrictedAccountPrincipals.push(principal); }); } @@ -72,7 +87,7 @@ module.exports = { if (crossAccountRole && !restrictedAccountPrincipals.length) { helpers.addResult(results, 0, - `Cross-account role "${role.RoleName}" contains trusted account pricipals only`, + `Cross-account role "${role.RoleName}" contains trusted account principals only`, 'global', role.Arn); } else if (crossAccountRole) { helpers.addResult(results, 2, diff --git a/plugins/aws/iam/trustedCrossAccountRoles.spec.js b/plugins/aws/iam/trustedCrossAccountRoles.spec.js index f737f2eb55..1e650c4300 100644 --- a/plugins/aws/iam/trustedCrossAccountRoles.spec.js +++ b/plugins/aws/iam/trustedCrossAccountRoles.spec.js @@ -114,6 +114,16 @@ describe('trustedCrossAccountRoles', function () { }); }); + it('should PASS if cross-account role contains trusted account IDs validated againt whitelisted account regex', function (done) { + const cache = createCache([roles[1]]); + trustedCrossAccountRoles.run(cache, { whitelisted_aws_account_principals_regex:'^arn:aws:iam::123456654321:.+$' }, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + it('should PASS if no IAM roles found', function (done) { const cache = createCache([]); trustedCrossAccountRoles.run(cache, {}, (err, results) => { diff --git a/plugins/aws/kms/kmsKeyPolicy.js b/plugins/aws/kms/kmsKeyPolicy.js index 913aa0ef61..db15c0a800 100644 --- a/plugins/aws/kms/kmsKeyPolicy.js +++ b/plugins/aws/kms/kmsKeyPolicy.js @@ -7,7 +7,7 @@ module.exports = { more_info: 'KMS key policies should be designed to limit the number of users who can perform encrypt and decrypt operations. Each application should use its own key to avoid over exposure.', recommended_action: 'Modify the KMS key policy to remove any wildcards and limit the number of users and roles that can perform encrypt and decrypt operations using the key.', link: 'http://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html', - apis: ['KMS:listKeys', 'STS:getCallerIdentity', 'KMS:getKeyPolicy'], + apis: ['KMS:listKeys', 'STS:getCallerIdentity', 'KMS:getKeyPolicy', 'KMS:describeKey'], settings: { kms_key_policy_max_user_count: { name: 'KMS Key Policy Max User Count', @@ -42,6 +42,12 @@ module.exports = { '3. Bool values are set to "true" with "Allow" and "false" with "Deny"', regex: '^.*$', default: 'aws:PrincipalArn,aws:PrincipalAccount,aws:PrincipalOrgID,aws:SourceAccount,aws:SourceArn,kms:CallerAccount' + }, + kms_ignore_aws_managed_keys: { + name: 'KMS Ignore AWS-managed Keys', + description: 'If set to true, ignore key policy for AWS-managed KMS keys', + regex: '^(true|false)$', + default: 'false' } }, @@ -51,7 +57,8 @@ module.exports = { kms_key_policy_max_third_parties_count: settings.kms_key_policy_max_third_parties_count || this.settings.kms_key_policy_max_third_parties_count.default, kms_key_policy_whitelisted_account_ids: settings.kms_key_policy_whitelisted_account_ids || this.settings.kms_key_policy_whitelisted_account_ids.default, kms_key_policy_whitelisted_policy_ids: settings.kms_key_policy_whitelisted_policy_ids || this.settings.kms_key_policy_whitelisted_policy_ids.default, - kms_key_policy_condition_keys: settings.kms_key_policy_condition_keys || this.settings.kms_key_policy_condition_keys.default + kms_key_policy_condition_keys: settings.kms_key_policy_condition_keys || this.settings.kms_key_policy_condition_keys.default, + kms_ignore_aws_managed_keys: settings.kms_ignore_aws_managed_keys || this.settings.kms_ignore_aws_managed_keys.default }; if (config.kms_key_policy_whitelisted_account_ids && config.kms_key_policy_whitelisted_account_ids.length) { @@ -66,6 +73,8 @@ module.exports = { config.kms_key_policy_whitelisted_policy_ids = []; } + config.kms_ignore_aws_managed_keys = (config.kms_ignore_aws_managed_keys == 'true'); + var allowedConditionKeys = config.kms_key_policy_condition_keys.split(','); allowedConditionKeys.push('kms:CallerAccount', 'kms:ViaService'); @@ -97,6 +106,24 @@ module.exports = { } async.each(listKeys.data, function(kmsKey, kcb){ + if (config.kms_ignore_aws_managed_keys) { + var describeKey = helpers.addSource(cache, source, + ['kms', 'describeKey', region, kmsKey.KeyId]); + + if (!describeKey || describeKey.err || !describeKey.data || !describeKey.data.KeyMetadata) { + helpers.addResult(results, 3, `Unable to query for KMS Key: ${helpers.addError(describeKey)}`, region); + return kcb(); + } + + let keyLevel = helpers.getEncryptionLevel(describeKey.data.KeyMetadata, helpers.ENCRYPTION_LEVELS); + + if (keyLevel == 2) { + helpers.addResult(results, 0, + 'KMS key is AWS-managed', region, kmsKey.KeyArn); + return kcb(); + } + } + var getKeyPolicy = helpers.addSource(cache, source, ['kms', 'getKeyPolicy', region, kmsKey.KeyId]); @@ -150,7 +177,7 @@ module.exports = { var conditionalCaller = null; if (statement.Condition) { - conditionalCaller = helpers.isValidCondition(statement, allowedConditionKeys, helpers.IAM_CONDITION_OPERATORS, true); + conditionalCaller = helpers.isValidCondition(statement, allowedConditionKeys, helpers.IAM_CONDITION_OPERATORS, true, accountId); } // Check for wildcards without condition @@ -160,7 +187,7 @@ module.exports = { for (var caller of conditionalCaller) { if (caller !== accountId && config.kms_key_policy_whitelisted_account_ids.indexOf(caller) === -1) { - thirdPartyTrusted += 1; + thirdPartyTrusted += 1; } } } else if (!conditionalCaller) { @@ -199,11 +226,11 @@ module.exports = { helpers.addResult(results, 2, 'Key trusts ' + wildcardTrusted + ' principals with wildcards', region, kmsKey.KeyArn, custom); } - + if (!found){ helpers.addResult(results, 0, 'Key policy is sufficient', region, kmsKey.KeyArn, custom); } - + kcb(); }, function(){ rcb(); @@ -212,4 +239,4 @@ module.exports = { callback(null, results, source); }); } -}; +}; \ No newline at end of file diff --git a/plugins/aws/xray/xrayEncryptionEnabled.js b/plugins/aws/xray/xrayEncryptionEnabled.js index 95c64fe367..891cc89da9 100644 --- a/plugins/aws/xray/xrayEncryptionEnabled.js +++ b/plugins/aws/xray/xrayEncryptionEnabled.js @@ -17,7 +17,7 @@ module.exports = { default: 'awskms', } }, - remediation_description: 'Encryption for the affected Cloud trails will be enabled.', + remediation_description: 'Encryption for the affected XRay traces will be enabled.', remediation_min_version: '202011271430', apis_remediate: ['XRay:getEncryptionConfig', 'KMS:listKeys', 'KMS:describeKey'], actions: { diff --git a/plugins/azure/appservice/authEnabled.js b/plugins/azure/appservice/authEnabled.js index 646ece2dae..045d136c13 100644 --- a/plugins/azure/appservice/authEnabled.js +++ b/plugins/azure/appservice/authEnabled.js @@ -9,6 +9,12 @@ module.exports = { recommended_action: 'Enable App Service Authentication for all App Services.', link: 'https://docs.microsoft.com/en-us/azure/app-service/overview-authentication-authorization', apis: ['webApps:list', 'webApps:getAuthSettings'], + remediation_min_version: '202104011300', + remediation_description: 'The App Service Authentication option will be enabled for the web app', + apis_remediate: ['webApps:list'], + actions: {remediate:['webApps:updateAuthSettings'], rollback:['webApps:updateAuthSettings']}, + permissions: {remediate: ['webApps:updateAuthSettings'], rollback: ['webApps:updateAuthSettings']}, + realtime_triggers: ['microsoftweb:sites:write'], compliance: { hipaa: 'HIPAA requires all application access to be restricted to known users ' + 'for auditing and security controls.', @@ -44,7 +50,7 @@ module.exports = { const authSettings = helpers.addSource( cache, source, ['webApps', 'getAuthSettings', location, webApp.id] ); - + if (!authSettings || authSettings.err || !authSettings.data) { helpers.addResult(results, 3, 'Unable to query App Service: ' + helpers.addError(authSettings), @@ -63,5 +69,48 @@ module.exports = { // Global checking goes here callback(null, results, source); }); + }, + remediate: function(config, cache, settings, resource, callback) { + var remediation_file = settings.remediation_file; + var putCall = this.actions.remediate; + + // inputs specific to the plugin + var pluginName = 'authEnabled'; + var baseUrl = 'https://management.azure.com/{resource}/config/authsettings?api-version=2019-08-01'; + var method = 'PUT'; + + // for logging purposes + var webAppNameArr = resource.split('/'); + var webAppName = webAppNameArr[webAppNameArr.length - 1]; + + // create the params necessary for the remediation + if (settings.region) { + var body = { + 'location': settings.region, + 'properties': { + 'enabled': true + } + }; + + // logging + remediation_file['pre_remediate']['actions'][pluginName][resource] = { + 'AppAuthentication': 'Disabled', + 'WebApp': webAppName + }; + + helpers.remediatePlugin(config, method, body, baseUrl, resource, remediation_file, putCall, pluginName, function(err, action) { + if (err) return callback(err); + if (action) action.action = putCall; + + remediation_file['post_remediate']['actions'][pluginName][resource] = action; + remediation_file['remediate']['actions'][pluginName][resource] = { + 'Action': 'Enabled' + }; + + callback(null, action); + }); + } else { + callback('No region found'); + } } }; diff --git a/plugins/azure/appservice/http20Enabled.js b/plugins/azure/appservice/http20Enabled.js index 8a710f9d1d..43a4f347ec 100644 --- a/plugins/azure/appservice/http20Enabled.js +++ b/plugins/azure/appservice/http20Enabled.js @@ -9,6 +9,12 @@ module.exports = { recommended_action: 'Enable HTTP 2.0 support in the general settings for all App Services', link: 'https://azure.microsoft.com/en-us/blog/announcing-http-2-support-in-azure-app-service/', apis: ['webApps:list', 'webApps:listConfigurations'], + remediation_min_version: '202103311945', + remediation_description: 'The HTTP 2.0 option will be enabled for the web app', + apis_remediate: ['webApps:list'], + actions: {remediate:['webApps:updateConfiguration'], rollback:['webApps:updateConfiguration']}, + permissions: {remediate: ['webApps:updateConfiguration'], rollback: ['webApps:updateConfiguration']}, + realtime_triggers: ['microsoftweb:sites:write'], run: function(cache, settings, callback) { const results = []; @@ -57,5 +63,51 @@ module.exports = { // Global checking goes here callback(null, results, source); }); + }, + + remediate: function(config, cache, settings, resource, callback) { + var remediation_file = settings.remediation_file; + var putCall = this.actions.remediate; + + // inputs specific to the plugin + var pluginName = 'http20Enabled'; + var baseUrl = 'https://management.azure.com/{resource}/config/web?api-version=2019-08-01'; + var method = 'PATCH'; + + // for logging purposes + var webAppNameArr = resource.split('/'); + var webAppName = webAppNameArr[webAppNameArr.length - 1]; + + // create the params necessary for the remediation + if (settings.region) { + var body = { + 'location': settings.region, + 'properties': { + 'http20Enabled': true + } + + }; + + // logging + remediation_file['pre_remediate']['actions'][pluginName][resource] = { + 'Http2.0': 'Disabled', + 'WebApp': webAppName + }; + + helpers.remediatePlugin(config, method, body, baseUrl, resource, remediation_file, putCall, pluginName, function(err, action) { + if (err) return callback(err); + if (action) action.action = putCall; + + + remediation_file['post_remediate']['actions'][pluginName][resource] = action; + remediation_file['remediate']['actions'][pluginName][resource] = { + 'Action': 'Enabled' + }; + + callback(null, action); + }); + } else { + callback('No region found'); + } } }; diff --git a/plugins/azure/appservice/identityEnabled.js b/plugins/azure/appservice/identityEnabled.js index 4c554de577..38eac3ed9c 100644 --- a/plugins/azure/appservice/identityEnabled.js +++ b/plugins/azure/appservice/identityEnabled.js @@ -10,6 +10,12 @@ module.exports = { recommended_action: 'Enable system or user-assigned identities for all App Services and avoid storing credentials in code.', link: 'https://docs.microsoft.com/en-us/azure/app-service/overview-managed-identity', apis: ['webApps:list'], + remediation_min_version: '202101041500', + remediation_description: 'The web app will be assigned a system manager identity', + apis_remediate: ['webApps:list'], + actions: {remediate:['webApps:update'], rollback:['webApps:update']}, + permissions: {remediate: ['webApps:update'], rollback: ['webApps:update']}, + realtime_triggers: ['microsoftweb:sites:write'], run: function(cache, settings, callback) { const results = []; @@ -48,5 +54,50 @@ module.exports = { // Global checking goes here callback(null, results, source); }); + }, + remediate: function(config, cache, settings, resource, callback) { + var remediation_file = settings.remediation_file; + var putCall = this.actions.remediate; + + // inputs specific to the plugin + var pluginName = 'identityEnabled'; + var baseUrl = 'https://management.azure.com/{resource}?api-version=2019-08-01'; + var method = 'PATCH'; + + // for logging purposes + var webAppNameArr = resource.split('/'); + var webAppName = webAppNameArr[webAppNameArr.length - 1]; + + // create the params necessary for the remediation + if (settings.region) { + var body = { + 'location': settings.region, + 'identity': { + 'type': 'SystemAssigned' + } + + }; + + // logging + remediation_file['pre_remediate']['actions'][pluginName][resource] = { + 'ManagedIdentity': 'Disabled', + 'WebApp': webAppName + }; + + helpers.remediatePlugin(config, method, body, baseUrl, resource, remediation_file, putCall, pluginName, function(err, action) { + if (err) return callback(err); + if (action) action.action = putCall; + + + remediation_file['post_remediate']['actions'][pluginName][resource] = action; + remediation_file['remediate']['actions'][pluginName][resource] = { + 'Action': 'Enabled' + }; + + callback(null, action); + }); + } else { + callback('No region found'); + } } }; diff --git a/plugins/azure/mysqlserver/enforceMySQLSSLConnection.js b/plugins/azure/mysqlserver/enforceMySQLSSLConnection.js index 61dd9aea42..a1110f3387 100644 --- a/plugins/azure/mysqlserver/enforceMySQLSSLConnection.js +++ b/plugins/azure/mysqlserver/enforceMySQLSSLConnection.js @@ -9,6 +9,12 @@ module.exports = { recommended_action: 'Ensure the connection security of each Azure Database for MySQL is configured to enforce SSL connections.', link: 'https://docs.microsoft.com/en-us/azure/mysql/concepts-ssl-connection-security', apis: ['servers:listMysql'], + remediation_min_version: '202103302200', + remediation_description: 'The SSL enforcement option will be enabled for the affected MySQL servers', + apis_remediate: ['servers:listMysql'], + actions: {remediate:['servers:update'], rollback:['servers:update']}, + permissions: {remediate: ['servers:update'], rollback: ['server:update']}, + realtime_triggers: ['microsoftdbformysql:servers:write'], compliance: { hipaa: 'HIPAA requires all data to be transmitted over secure channels. ' + 'MySQL SSL connection should be used to ensure internal ' + @@ -54,5 +60,49 @@ module.exports = { // Global checking goes here callback(null, results, source); }); + }, + + remediate: function(config, cache, settings, resource, callback) { + var remediation_file = settings.remediation_file; + var putCall = this.actions.remediate; + + // inputs specific to the plugin + var pluginName = 'enforceMySQLSSLConnection'; + var baseUrl = 'https://management.azure.com{resource}?api-version=2017-12-01'; + var method = 'PATCH'; + + // for logging purposes + var serverNameArr = resource.split('/'); + var serverName = serverNameArr[serverNameArr.length - 1]; + + // create the params necessary for the remediation + if (settings.region) { + var body = { + 'properties': { + 'sslEnforcement': 'Enabled' + } + }; + + // logging + remediation_file['pre_remediate']['actions'][pluginName][resource] = { + 'SSLEnforcement': 'Disabled', + 'Server': serverName + }; + + helpers.remediatePlugin(config, method, body, baseUrl, resource, remediation_file, putCall, pluginName, function(err, action) { + if (err) return callback(err); + if (action) action.action = putCall; + + + remediation_file['post_remediate']['actions'][pluginName][resource] = action; + remediation_file['remediate']['actions'][pluginName][resource] = { + 'Action': 'Enabled' + }; + + callback(null, action); + }); + } else { + callback('No region found'); + } } -}; +}; \ No newline at end of file diff --git a/plugins/azure/postgresqlserver/activeDirectoryAdminEnabled.js b/plugins/azure/postgresqlserver/activeDirectoryAdminEnabled.js index c7d90a6581..baca60cda6 100644 --- a/plugins/azure/postgresqlserver/activeDirectoryAdminEnabled.js +++ b/plugins/azure/postgresqlserver/activeDirectoryAdminEnabled.js @@ -2,7 +2,7 @@ const async = require('async'); const helpers = require('../../../helpers/azure'); module.exports = { - title: 'Azure Active Directory Admin Cofigured', + title: 'Azure Active Directory Admin Configured', category: 'PostgreSQL Server', description: 'Ensures that Active Directory admin is set up on all PostgreSQL servers.', more_info: 'Using Azure Active Directory authentication allows key rotation and permission management to be managed in one location for all servers. This can be done are configuring an Active Directory administrator.', diff --git a/plugins/azure/postgresqlserver/enforcePostgresSSLConnection.js b/plugins/azure/postgresqlserver/enforcePostgresSSLConnection.js index 2e40ef3051..66040385ab 100644 --- a/plugins/azure/postgresqlserver/enforcePostgresSSLConnection.js +++ b/plugins/azure/postgresqlserver/enforcePostgresSSLConnection.js @@ -9,6 +9,12 @@ module.exports = { recommended_action: 'Ensure the connection security settings of each PostgreSQL server are configured to enforce SSL connections.', link: 'https://docs.microsoft.com/en-us/azure/postgresql/concepts-ssl-connection-security', apis: ['servers:listPostgres'], + remediation_min_version: '202101041600', + remediation_description: 'The SSL enforcement option will be enabled for the affected PostreSQL servers', + apis_remediate: ['servers:listPostgres'], + actions: {remediate:['servers:update'], rollback:['servers:update']}, + permissions: {remediate: ['servers:update'], rollback: ['server:update']}, + realtime_triggers: ['microsoftdbforpostgresql:servers:write'], compliance: { hipaa: 'HIPAA requires all data to be transmitted over secure channels. ' + 'PostgreSQL SSL connection should be used to ensure internal ' + @@ -56,5 +62,48 @@ module.exports = { // Global checking goes here callback(null, results, source); }); + }, + remediate: function(config, cache, settings, resource, callback) { + var remediation_file = settings.remediation_file; + var putCall = this.actions.remediate; + + // inputs specific to the plugin + var pluginName = 'enforcePostgresSSLConnection'; + var baseUrl = 'https://management.azure.com{resource}?api-version=2017-12-01'; + var method = 'PATCH'; + + // for logging purposes + var serverNameArr = resource.split('/'); + var serverName = serverNameArr[serverNameArr.length - 1]; + + // create the params necessary for the remediation + if (settings.region) { + var body = { + 'properties': { + 'sslEnforcement': 'Enabled' + } + }; + + // logging + remediation_file['pre_remediate']['actions'][pluginName][resource] = { + 'SSLEnforcement': 'Disabled', + 'Server': serverName + }; + + helpers.remediatePlugin(config, method, body, baseUrl, resource, remediation_file, putCall, pluginName, function(err, action) { + if (err) return callback(err); + if (action) action.action = putCall; + + + remediation_file['post_remediate']['actions'][pluginName][resource] = action; + remediation_file['remediate']['actions'][pluginName][resource] = { + 'Action': 'Enabled' + }; + + callback(null, action); + }); + } else { + callback('No region found'); + } } }; diff --git a/plugins/azure/sqlserver/sqlServerTlsVersion.js b/plugins/azure/sqlserver/sqlServerTlsVersion.js index a990c770db..84095e58da 100644 --- a/plugins/azure/sqlserver/sqlServerTlsVersion.js +++ b/plugins/azure/sqlserver/sqlServerTlsVersion.js @@ -17,6 +17,12 @@ module.exports = { default: '1.2' } }, + remediation_min_version: '202104012200', + remediation_description: 'TLS version 1.2 will be set for the affected SQL server', + apis_remediate: ['servers:listSql'], + actions: {remediate:['servers:update'], rollback:['servers:update']}, + permissions: {remediate: ['servers:update'], rollback: ['servers:update']}, + realtime_triggers: ['microsoftsql:servers:write'], run: function(cache, settings, callback) { var results = []; @@ -70,5 +76,49 @@ module.exports = { }, function() { callback(null, results, source); }); + }, + remediate: function(config, cache, settings, resource, callback) { + var remediation_file = settings.remediation_file; + var putCall = this.actions.remediate; + + // inputs specific to the plugin + var pluginName = 'sqlServerTlsVersion'; + var baseUrl = 'https://management.azure.com/{resource}?api-version=2020-08-01-preview'; + var method = 'PATCH'; + + // for logging purposes + var serverNameArr = resource.split('/'); + var serverName = serverNameArr[serverNameArr.length - 1]; + + // create the params necessary for the remediation + if (settings.region) { + var body = { + 'location': settings.region, + 'properties': { + 'minimalTlsVersion': '1.2' + } + }; + + // logging + remediation_file['pre_remediate']['actions'][pluginName][resource] = { + 'TLS1.2': 'Disabled', + 'Server': serverName + }; + + helpers.remediatePlugin(config, method, body, baseUrl, resource, remediation_file, putCall, pluginName, function(err, action) { + if (err) return callback(err); + if (action) action.action = putCall; + + + remediation_file['post_remediate']['actions'][pluginName][resource] = action; + remediation_file['remediate']['actions'][pluginName][resource] = { + 'Action': 'Enabled' + }; + + callback(null, action); + }); + } else { + callback('No region found'); + } } }; \ No newline at end of file diff --git a/plugins/azure/storageaccounts/blobSoftDeletionEnabled.js b/plugins/azure/storageaccounts/blobSoftDeletionEnabled.js index 71e5344cf3..261ca2152a 100644 --- a/plugins/azure/storageaccounts/blobSoftDeletionEnabled.js +++ b/plugins/azure/storageaccounts/blobSoftDeletionEnabled.js @@ -4,7 +4,7 @@ const helpers = require('../../../helpers/azure'); module.exports = { title: 'Blobs Soft Deletion Enabled', category: 'Storage Accounts', - description: 'Ensure that soft delete feature is enabled for all Microdoft Storage Account blobs.', + description: 'Ensure that soft delete feature is enabled for all Microsoft Storage Account blobs.', more_info: 'When soft delete for blobs is enabled for a storage account, blobs, blob versions, and snapshots in that storage account may be recovered after they are deleted, within a retention period that you specify.', recommended_action: 'Enable soft delete for blobs and set deletion retention policy to keep blobs for more than desired number of days', link: 'https://docs.microsoft.com/en-us/azure/storage/blobs/soft-delete-blob-overview', diff --git a/plugins/google/storage/bucketRetentionPolicy.js b/plugins/google/storage/bucketRetentionPolicy.js index 8d32ed07aa..77296d242f 100644 --- a/plugins/google/storage/bucketRetentionPolicy.js +++ b/plugins/google/storage/bucketRetentionPolicy.js @@ -4,7 +4,7 @@ var helpers = require('../../../helpers/google'); module.exports = { title: 'Storage Bucket Retention Policy', category: 'Storage', - description: 'Ensures bucket retention policy is set and locked to prevent deletion or updation of bucket objects or retention policy.', + description: 'Ensures bucket retention policy is set and locked to prevent deleting or updating of bucket objects or retention policy.', more_info: 'Configuring retention policy for bucket prevents accidental deletion as well as modification of bucket objects. This retention policy should also be locked to prevent policy deletion.', link: 'https://cloud.google.com/storage/docs/bucket-lock?_ga=2.221806616.-1645770163.1613190642', recommended_action: 'Modify bucket to configure retention policy and lock retention policy.', diff --git a/plugins/google/vpcnetwork/openCassandra.js b/plugins/google/vpcnetwork/openCassandra.js index 3d44a5ceb0..6968fcd763 100644 --- a/plugins/google/vpcnetwork/openCassandra.js +++ b/plugins/google/vpcnetwork/openCassandra.js @@ -2,7 +2,7 @@ var async = require('async'); var helpers = require('../../../helpers/google'); module.exports = { - title: 'Open Oracle', + title: 'Open Cassandra', category: 'VPC Network', description: 'Determines if TCP port 7001 for Cassandra is open to the public', more_info: 'While some ports such as HTTP and HTTPS are required to be open to the public to function properly, more sensitive services such as Cassandra should be restricted to known IP addresses.', diff --git a/plugins/google/vpcnetwork/openMongo.js b/plugins/google/vpcnetwork/openMongo.js index f50d4d1a68..c12a244666 100644 --- a/plugins/google/vpcnetwork/openMongo.js +++ b/plugins/google/vpcnetwork/openMongo.js @@ -2,9 +2,9 @@ var async = require('async'); var helpers = require('../../../helpers/google'); module.exports = { - title: 'Open MySQL', + title: 'Open MongoDB', category: 'VPC Network', - description: 'Determines if TCP port 27017 for Mongo is open to the public', + description: 'Determines if TCP port 27017 for MongoDB is open to the public', more_info: 'While some ports such as HTTP and HTTPS are required to be open to the public to function properly, more sensitive services such as Mongo should be restricted to known IP addresses.', link: 'https://cloud.google.com/vpc/docs/using-firewalls', recommended_action: 'Restrict TCP ports 27017 to known IP addresses.', diff --git a/plugins/google/vpcnetwork/openMsSQL.js b/plugins/google/vpcnetwork/openMsSQL.js index 46b27187d1..4795e88f00 100644 --- a/plugins/google/vpcnetwork/openMsSQL.js +++ b/plugins/google/vpcnetwork/openMsSQL.js @@ -2,7 +2,7 @@ var async = require('async'); var helpers = require('../../../helpers/google'); module.exports = { - title: 'Open MySQL', + title: 'Open MSSQL', category: 'VPC Network', description: 'Determines if TCP port 1433 for MSSQL is open to the public.', more_info: 'While some ports such as HTTP and HTTPS are required to be open to the public to function properly, more sensitive services such as MSSQL should be restricted to known IP addresses.',