diff --git a/collectors/aws/collector.js b/collectors/aws/collector.js index fa30b93079..d588dd5dcf 100644 --- a/collectors/aws/collector.js +++ b/collectors/aws/collector.js @@ -838,6 +838,12 @@ var postcalls = [ reliesOnCall: 'listIdentities', override: true, rateLimit: 1000 + }, + getIdentityVerificationAttributes: { + reliesOnService: 'ses', + reliesOnCall: 'listIdentities', + override: true, + rateLimit: 1000 } }, SNS: { diff --git a/collectors/aws/ses/getIdentityVerificationAttributes.js b/collectors/aws/ses/getIdentityVerificationAttributes.js new file mode 100644 index 0000000000..8d38d5d6b4 --- /dev/null +++ b/collectors/aws/ses/getIdentityVerificationAttributes.js @@ -0,0 +1,15 @@ +var AWS = require('aws-sdk'); + +module.exports = function(AWSConfig, collection, callback) { + var ses = new AWS.SES(AWSConfig); + + ses.getIdentityVerificationAttributes({Identities: collection.ses.listIdentities[AWSConfig.region].data}, function(err, data){ + if (err) { + collection.ses.getIdentityVerificationAttributes[AWSConfig.region].err = err; + } + + collection.ses.getIdentityVerificationAttributes[AWSConfig.region].data = data; + + callback(); + }); +}; \ No newline at end of file diff --git a/exports.js b/exports.js index b50c9d9a75..16da354a80 100644 --- a/exports.js +++ b/exports.js @@ -173,6 +173,7 @@ module.exports = { 'notebookDirectInternetAccess' : require(__dirname + '/plugins/aws/sagemaker/notebookDirectInternetAccess.js'), 'dkimEnabled' : require(__dirname + '/plugins/aws/ses/dkimEnabled.js'), + 'sesValidatedDomains' : require(__dirname + '/plugins/aws/ses/sesValidatedDomains.js'), 'topicPolicies' : require(__dirname + '/plugins/aws/sns/topicPolicies.js'), 'sqsCrossAccount' : require(__dirname + '/plugins/aws/sqs/sqsCrossAccount.js'), diff --git a/plugins/aws/ses/sesValidatedDomains.js b/plugins/aws/ses/sesValidatedDomains.js new file mode 100644 index 0000000000..1f59cdd876 --- /dev/null +++ b/plugins/aws/ses/sesValidatedDomains.js @@ -0,0 +1,74 @@ +var async = require('async'); +var helpers = require('../../../helpers/aws'); + +module.exports = { + title: 'SES Validated Domains Flagged', + category: 'SES', + description: 'Ensures that only Email Identities are used for verification of identities.', + more_info: 'SES allows for either domains or email addresses to be valid identities. This checks to ensure that only emails have been verified.', + recommended_action: 'Remove any domains', + link: 'https://docs.aws.amazon.com/ses/latest/DeveloperGuide/remove-verified-domain.html', + apis: ['SES:listIdentities', 'SES:getIdentityVerificationAttributes'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var regions = helpers.regions(settings); + + async.each(regions.ses, function(region, rcb){ + var listIdentities = helpers.addSource(cache, source, + ['ses', 'listIdentities', region]); + + if (!listIdentities) return rcb(); + + if (listIdentities.err || !listIdentities.data) { + helpers.addResult(results, 3, + 'Unable to query for SES identities: ' + helpers.addError(listIdentities), region); + return rcb(); + } + + if (!listIdentities.data.length) { + helpers.addResult(results, 0, 'No SES domain identities found', region); + return rcb(); + } + + var getIdentityVerificationAttributes = helpers.addSource(cache, source, + ['ses', 'getIdentityVerificationAttributes', region]); + + if (!getIdentityVerificationAttributes || + getIdentityVerificationAttributes.err || + !getIdentityVerificationAttributes.data) { + helpers.addResult(results, 3, + 'Unable to get SES Verification attributes: ' + helpers.addError(getIdentityVerificationAttributes), region); + return rcb(); + } + + for (i in getIdentityVerificationAttributes.data.VerificationAttributes) { + var identity = getIdentityVerificationAttributes.data.VerificationAttributes[i]; + + if (!identity.VerificationStatus) { + helpers.addResult(results, 2, 'Domain identity exists with unknown status', region, i); + } else if (identity.VerificationStatus == 'Success') { + helpers.addResult(results, 2, + 'Domain identity exists and is verified', region, i); + } else if (identity.VerificationStatus == 'Pending') { + helpers.addResult(results, 2, + 'Domain identity exists and has verification that is pending', region, i); + } else if (identity.VerificationStatus == 'Failed') { + helpers.addResult(results, 2, + 'Domain identity exists but verification failed', region, i); + } else if (identity.VerificationStatus == 'TemporaryFailure') { + helpers.addResult(results, 2, + 'Domain identity exists but verification temporarily failed', region, i); + } else { + helpers.addResult(results, 2, 'Domain identity exists with unknown status', region, i); + + } + } + + rcb(); + }, function(){ + callback(null, results, source); + }); + } +}; \ No newline at end of file diff --git a/plugins/aws/ses/sesValidatedDomains.spec.js b/plugins/aws/ses/sesValidatedDomains.spec.js new file mode 100644 index 0000000000..8fe1d8597d --- /dev/null +++ b/plugins/aws/ses/sesValidatedDomains.spec.js @@ -0,0 +1,126 @@ +var assert = require('assert'); +var expect = require('chai').expect; +var ses = require('./sesValidatedDomains'); + +const createCache = (lData, gData) => { + return { + ses: { + listIdentities: { + 'us-east-1': { + err: null, + data: lData + } + }, + getIdentityVerificationAttributes: { + 'us-east-1': { + err: null, + data: gData + } + } + + } + } +}; + +describe('sesValidatedDomains', function () { + describe('run', function () { + it('should give passing result if no domain identities are found', function (done) { + const callback = (err, results) => { + expect(results.length).to.equal(1) + expect(results[0].status).to.equal(0) + expect(results[0].message).to.include('No SES domain identities found') + done() + }; + + const cache = createCache( + [], + [] + ); + + ses.run(cache, {}, callback); + }) + }); + + describe('run', function () { + it('should give error result if verified domain identities exist', function (done) { + const callback = (err, results) => { + expect(results.length).to.equal(1) + expect(results[0].status).to.equal(2) + expect(results[0].message).to.include('Domain identity exists and is verified') + done() + }; + + const cache = createCache( + [{ + "Identities": [ + "test@test.com" + ] + }], + { + "VerificationAttributes": [ + { + "VerificationStatus": "Success" + } + ] + } + ); + + ses.run(cache, {}, callback); + }) + }) + + describe('run', function () { + it('should give warning result if verified domain identities are pending', function (done) { + const callback = (err, results) => { + expect(results.length).to.equal(1) + expect(results[0].status).to.equal(2) + expect(results[0].message).to.include('Domain identity exists and has verification that is pending') + done() + }; + + const cache = createCache( + [{ + "Identities": [ + "test@test.com" + ] + }], + { + "VerificationAttributes": [ + { + "VerificationStatus": "Pending" + } + ] + } + ); + + ses.run(cache, {}, callback); + }) + }) + + describe('run', function () { + it('should give warning result if domain identity exists, but verification has not yet been requested', function (done) { + const callback = (err, results) => { + expect(results.length).to.equal(1) + expect(results[0].status).to.equal(2) + expect(results[0].message).to.include('Domain identity exists with unknown status') + done() + }; + + const cache = createCache( + [{ + "Identities": [ + "test@test.com" + ] + }], + { + "VerificationAttributes": [ + {} + ] + } + ); + + ses.run(cache, {}, callback); + }) + }) + +}) \ No newline at end of file