diff --git a/collectors/azure/collector.js b/collectors/azure/collector.js index cefaa4e925..a694b408ed 100644 --- a/collectors/azure/collector.js +++ b/collectors/azure/collector.js @@ -222,6 +222,11 @@ var postcalls = { reliesOnPath: 'storageAccounts.list', properties: ['id'], url: 'https://management.azure.com/{id}/blobServices?api-version=2019-06-01' + }, + getServiceProperties: { + reliesOnPath: 'storageAccounts.list', + properties: ['id'], + url: 'https://management.azure.com/{id}/blobServices/default?api-version=2019-06-01' } }, fileShares: { diff --git a/exports.js b/exports.js index 364e127dda..b745482964 100644 --- a/exports.js +++ b/exports.js @@ -335,6 +335,7 @@ module.exports = { 'storageAccountsAADEnabled' : require(__dirname + '/plugins/azure/storageaccounts/storageAccountsAADEnabled.js'), 'blobServiceEncryption' : require(__dirname + '/plugins/azure/storageaccounts/blobServiceEncryption.js'), 'trustedMsAccessEnabled' : require(__dirname + '/plugins/azure/storageaccounts/trustedMsAccessEnabled.js'), + 'blobSoftDeletionEnabled' : require(__dirname + '/plugins/azure/storageaccounts/blobSoftDeletionEnabled.js'), 'blobContainersPrivateAccess' : require(__dirname + '/plugins/azure/blobservice/blobContainersPrivateAccess.js'), 'blobServiceImmutable' : require(__dirname + '/plugins/azure/blobservice/blobServiceImmutable.js'), diff --git a/plugins/azure/storageaccounts/blobSoftDeletionEnabled.js b/plugins/azure/storageaccounts/blobSoftDeletionEnabled.js new file mode 100644 index 0000000000..71e5344cf3 --- /dev/null +++ b/plugins/azure/storageaccounts/blobSoftDeletionEnabled.js @@ -0,0 +1,84 @@ +const async = require('async'); +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.', + 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', + apis: ['storageAccounts:list', 'blobServices:getServiceProperties'], + settings: { + keep_deleted_blobs_for_days: { + name: 'Keep Deleted Blobs for Days', + description: 'Number of days that a blob is marked for deletion persists until it is permanently deleted', + regex: '^[1-9]{1}[0-9]{0,3}$', + default: '30' + } + }, + + run: function(cache, settings, callback) { + const results = []; + const source = {}; + const locations = helpers.locations(settings.govcloud); + + const config = { + keepForDays: parseInt(settings.keep_deleted_blobs_for_days || this.settings.keep_deleted_blobs_for_days.default) + }; + + async.each(locations.storageAccounts, function(location, rcb) { + const storageAccounts = helpers.addSource( + cache, source, ['storageAccounts', 'list', location]); + + if (!storageAccounts) return rcb(); + + if (storageAccounts.err || !storageAccounts.data) { + helpers.addResult(results, 3, + 'Unable to query for storage accounts: ' + helpers.addError(storageAccounts), location); + return rcb(); + } + + if (!storageAccounts.data.length) { + helpers.addResult(results, 0, 'No storage accounts found', location); + return rcb(); + } + + storageAccounts.data.forEach(storageAccount => { + const getServiceProperties = helpers.addSource(cache, source, + ['blobServices', 'getServiceProperties', location, storageAccount.id]); + + if (!getServiceProperties || getServiceProperties.err || !getServiceProperties.data) { + helpers.addResult(results, 3, + `Unable to get blob service properties: ${helpers.addError(getServiceProperties)}`, + location, storageAccount.id); + } else { + if (getServiceProperties.data.deleteRetentionPolicy && + getServiceProperties.data.deleteRetentionPolicy.enabled && + getServiceProperties.data.deleteRetentionPolicy.days) { + const retentionDays = getServiceProperties.data.deleteRetentionPolicy.days; + + if (retentionDays >= config.keepForDays) { + helpers.addResult(results, 0, + `Blobs deletion policy is configured to persist deleted blobs for ${retentionDays} of ${config.keepForDays} days desired limit`, + location, storageAccount.id); + } else { + helpers.addResult(results, 2, + `Blobs deletion policy is configured to persist deleted blobs for ${retentionDays} of ${config.keepForDays} days desired limit`, + location, storageAccount.id); + } + } else { + helpers.addResult(results, 2, + 'Blobs soft delete feature is not enabled for Storage Account', + location, storageAccount.id); + } + } + }); + + rcb(); + }, function() { + // Global checking goes here + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/storageaccounts/blobSoftDeletionEnabled.spec.js b/plugins/azure/storageaccounts/blobSoftDeletionEnabled.spec.js new file mode 100644 index 0000000000..09473aaf3d --- /dev/null +++ b/plugins/azure/storageaccounts/blobSoftDeletionEnabled.spec.js @@ -0,0 +1,143 @@ +var expect = require('chai').expect; +var blobSoftDeletionEnabled = require('./blobSoftDeletionEnabled'); + +const storageAccounts = [ + { + "id": "/subscriptions/123/resourceGroups/test-group/providers/Microsoft.Storage/storageAccounts/test-account" + } +]; + +const getServiceProperties = [ + { + "sku": { "name": "Standard_LRS", "tier": "Standard" }, + "id": "/subscriptions/123/resourceGroups/test-group/providers/Microsoft.Storage/storageAccounts/test-account/blobServices/default", + "name": "default", + "type": "Microsoft.Storage/storageAccounts/blobServices", + "cors": { "corsRules": [] }, + "deleteRetentionPolicy": { "enabled": true, "days": 30 } + }, + { + "sku": { "name": "Standard_LRS", "tier": "Standard" }, + "id": "/subscriptions/123/resourceGroups/test-group/providers/Microsoft.Storage/storageAccounts/csb100320011cd09016/blobServices/default", + "name": "default", + "type": "Microsoft.Storage/storageAccounts/blobServices", + "cors": { "corsRules": [] }, + "deleteRetentionPolicy": { "enabled": false } + } +]; + + +const createCache = (list, err, getProperties, geterr) => { + const id = (list && list.length) ? list[0].id : null; + return { + storageAccounts: { + list: { + 'eastus': { + err: err, + data: list + } + } + }, + blobServices: { + getServiceProperties: { + 'eastus': { + [id]: { + err: geterr, + data: getProperties + } + } + } + } + } +}; + +describe('blobSoftDeletionEnabled', function() { + describe('run', function() { + it('should give passing result if no Storage Accounts', 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 storage accounts found'); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache( + [] + ); + + blobSoftDeletionEnabled.run(cache, {}, callback); + }) + + it('should give failing result if Blobs soft delete feature is not enabled for Storage Account', 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('Blobs soft delete feature is not enabled for Storage Account'); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache( + storageAccounts, + null, + getServiceProperties[1] + ); + + blobSoftDeletionEnabled.run(cache, {}, callback); + }); + + it('should give failing result if Blobs deletion policy is configured to persist deleted blobs for less days than desired limit', 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('Blobs deletion policy is configured to persist deleted blobs'); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache( + storageAccounts, + null, + getServiceProperties[0] + ); + + blobSoftDeletionEnabled.run(cache, { keep_deleted_blobs_for_days: '50' }, callback); + }); + + it('should give passing result if Blobs deletion policy is configured to persist deleted blobs for more days than desired limit', 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('Blobs deletion policy is configured to persist deleted blobs'); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache( + storageAccounts, + null, + getServiceProperties[0] + ); + + blobSoftDeletionEnabled.run(cache, { keep_deleted_blobs_for_days: '20' }, callback); + }); + + it('should give unknown result if unable to query for Storage Accounts', function(done) { + const callback = (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(3); + expect(results[0].message).to.include('Unable to query for storage accounts'); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache( + storageAccounts, + { message: "Unable to list Storage Accounts" }, + ); + + blobSoftDeletionEnabled.run(cache, {}, callback); + }); + }) +}) \ No newline at end of file