diff --git a/exports.js b/exports.js index 2795a4d3d0..8e8b52b650 100644 --- a/exports.js +++ b/exports.js @@ -732,10 +732,14 @@ module.exports = { 'blobServiceEncryption' : require(__dirname + '/plugins/azure/storageaccounts/blobServiceEncryption.js'), 'trustedMsAccessEnabled' : require(__dirname + '/plugins/azure/storageaccounts/trustedMsAccessEnabled.js'), 'blobSoftDeletionEnabled' : require(__dirname + '/plugins/azure/storageaccounts/blobSoftDeletionEnabled.js'), + 'containerSoftDeletionEnabled' : require(__dirname + '/plugins/azure/storageaccounts/containerSoftDeletionEnabled.js'), + 'blobVersioningEnabled' : require(__dirname + '/plugins/azure/storageaccounts/blobVersioningEnabled.js'), 'storageAccountKeyRotationReminder': require(__dirname + '/plugins/azure/storageaccounts/storageAccountKeyRotationReminder.js'), 'storageAccountKeyRotation' : require(__dirname + '/plugins/azure/storageaccounts/storageAccountKeyRotation.js'), 'sharedKeyAccessDisabled' : require(__dirname + '/plugins/azure/storageaccounts/sharedKeyAccessDisabled.js'), 'storageAccountEntraIdAuthDefault': require(__dirname + '/plugins/azure/storageaccounts/storageAccountEntraIdAuthDefault.js'), + 'crossTenantReplicationDisabled': require(__dirname + '/plugins/azure/storageaccounts/crossTenantReplicationDisabled.js'), + 'blobAnonymousAccessDisabled' : require(__dirname + '/plugins/azure/storageaccounts/blobAnonymousAccessDisabled.js'), 'geoRedundantStorage' : require(__dirname + '/plugins/azure/storageaccounts/geoRedundantStorage.js'), 'fileShareSoftDeletionEnabled' : require(__dirname + '/plugins/azure/storageaccounts/fileShareSoftDeletionEnabled.js'), 'fileShareSmbProtocolVersion' : require(__dirname + '/plugins/azure/storageaccounts/fileShareSmbProtocolVersion.js'), @@ -785,8 +789,10 @@ module.exports = { 'redisCacheVNetIntegrated' : require(__dirname + '/plugins/azure/redisCache/redisCacheVNetIntegrated.js'), 'multipleSubnets' : require(__dirname + '/plugins/azure/virtualnetworks/multipleSubnets.js'), + 'subnetNetworkSecurityGroup' : require(__dirname + '/plugins/azure/virtualnetworks/subnetNetworkSecurityGroup.js'), 'ddosStandardProtectionEnabled' : require(__dirname + '/plugins/azure/virtualnetworks/ddosStandardProtectionEnabled.js'), 'noNetworkGatewaysInUse' : require(__dirname + '/plugins/azure/virtualnetworks/noNetworkGatewaysInUse.js'), + 'vpnGatewayEntraIdAuth' : require(__dirname + '/plugins/azure/virtualnetworks/vpnGatewayEntraIdAuth.js'), 'virtualNetworkPeering' : require(__dirname + '/plugins/azure/virtualnetworks/virtualNetworkPeering.js'), 'noGatewayConnections' : require(__dirname + '/plugins/azure/virtualnetworks/noGatewayConnections.js'), 'managedNatGateway' : require(__dirname + '/plugins/azure/virtualnetworks/managedNatGateway.js'), @@ -849,6 +855,7 @@ module.exports = { 'bastionHostHasTags' : require(__dirname + '/plugins/azure/bastion/bastionHostHasTags.js'), 'logProfileArchiveData' : require(__dirname + '/plugins/azure/monitor/logProfileArchiveData.js'), + 'appInsightsConfigured' : require(__dirname + '/plugins/azure/monitor/appInsightsConfigured.js'), 'logAnalyticsWorkspacePublic' : require(__dirname + '/plugins/azure/monitor/logAnalyticsWorkspacePublic.js'), 'monitorLogsEnabled' : require(__dirname + '/plugins/azure/monitor/monitorLogsEnabled.js'), 'diagnosticsCapturedCategories' : require(__dirname + '/plugins/azure/monitor/diagnosticsCapturedCategories.js'), @@ -857,6 +864,7 @@ module.exports = { 'securityPolicyAlertsEnabled' : require(__dirname + '/plugins/azure/logalerts/securityPolicyAlertsEnabled.js'), 'nsgLoggingEnabled' : require(__dirname + '/plugins/azure/logalerts/nsgLoggingEnabled.js'), + 'serviceHealthAlertEnabled' : require(__dirname + '/plugins/azure/logalerts/serviceHealthAlertEnabled.js'), 'sqlServerFirewallRuleEnabled' : require(__dirname + '/plugins/azure/logalerts/sqlServerFirewallRuleEnabled.js'), 'virtualNetworkRuleEnabled' : require(__dirname + '/plugins/azure/logalerts/virtualNetworkRuleEnabled.js'), 'securitySolutionLogging' : require(__dirname + '/plugins/azure/logalerts/securitySolutionLogging.js'), @@ -1065,7 +1073,10 @@ module.exports = { 'passwordRequiresUppercase' : require(__dirname + '/plugins/azure/entraid/passwordRequiresUppercase.js'), 'minPasswordLength' : require(__dirname + '/plugins/azure/entraid/minPasswordLength.js'), 'ensureNoGuestUser' : require(__dirname + '/plugins/azure/entraid/ensureNoGuestUser.js'), + 'securityDefaultsEnabled' : require(__dirname + '/plugins/azure/entraid/securityDefaultsEnabled.js'), 'userAccessAdminRestricted' : require(__dirname + '/plugins/azure/entraid/userAccessAdminRestricted.js'), + 'disabledUserRoleAssignments' : require(__dirname + '/plugins/azure/entraid/disabledUserRoleAssignments.js'), + 'resourceLockAdminRole' : require(__dirname + '/plugins/azure/entraid/resourceLockAdminRole.js'), 'subscriptionOwnerCount' : require(__dirname + '/plugins/azure/entraid/subscriptionOwnerCount.js'), 'noCustomOwnerRoles' : require(__dirname + '/plugins/azure/entraid/noCustomOwnerRoles.js'), 'appOrgnaizationalDirectoryAccess' : require(__dirname + '/plugins/azure/entraid/appOrgnaizationalDirectoryAccess.js'), @@ -1099,6 +1110,9 @@ module.exports = { 'keyVaultSecretExpiry' : require(__dirname + '/plugins/azure/keyvaults/keyVaultSecretExpiry.js'), 'keyVaultSecretExpiryNonRbac' : require(__dirname + '/plugins/azure/keyvaults/keyVaultSecretExpiryNonRbac.js'), 'keyVaultKeyExpiry' : require(__dirname + '/plugins/azure/keyvaults/keyVaultKeyExpiry.js'), + 'keyVaultRbacEnabled' : require(__dirname + '/plugins/azure/keyvaults/keyVaultRbacEnabled.js'), + 'keyVaultKeyRotation' : require(__dirname + '/plugins/azure/keyvaults/keyVaultKeyRotation.js'), + 'certificateValidityPeriod' : require(__dirname + '/plugins/azure/keyvaults/certificateValidityPeriod.js'), 'keyVaultKeyExpiryNonRbac' : require(__dirname + '/plugins/azure/keyvaults/keyVaultKeyExpiryNonRbac.js'), 'allowedCertificateKeyTypes' : require(__dirname + '/plugins/azure/keyvaults/allowedCertificateKeyTypes.js'), 'appTierCmkInUse' : require(__dirname + '/plugins/azure/keyvaults/appTierCmkInUse.js'), @@ -1135,6 +1149,7 @@ module.exports = { 'enableDefenderForCosmosDB' : require(__dirname + '/plugins/azure/defender/enableDefenderForCosmosDB.js'), 'enableDefenderForSqlServersVMs': require(__dirname + '/plugins/azure/defender/enableDefenderForSqlServersVMs.js'), 'highSeverityAlertsEnabled' : require(__dirname + '/plugins/azure/defender/highSeverityAlertsEnabled.js'), + 'attackPathNotificationsEnabled': require(__dirname + '/plugins/azure/defender/attackPathNotificationsEnabled.js'), 'standardPricingEnabled' : require(__dirname + '/plugins/azure/defender/standardPricingEnabled.js'), 'monitorExternalAccounts' : require(__dirname + '/plugins/azure/defender/monitorExternalAccounts.js'), 'monitorIpForwarding' : require(__dirname + '/plugins/azure/defender/monitorIpForwarding.js'), @@ -1151,6 +1166,7 @@ module.exports = { 'securityContactsEnabled' : require(__dirname + '/plugins/azure/defender/securityContactsEnabled.js'), 'agWafEnabled' : require(__dirname + '/plugins/azure/applicationGateway/agWafEnabled'), + 'agHttp2Enabled' : require(__dirname + '/plugins/azure/applicationGateway/agHttp2Enabled'), 'applicationGatewayHasTags' : require(__dirname + '/plugins/azure/applicationGateway/applicationGatewayHasTags.js'), 'agSecurityLoggingEnabled' : require(__dirname + '/plugins/azure/applicationGateway/agSecurityLoggingEnabled.js'), 'agSslPolicy' : require(__dirname + '/plugins/azure/applicationGateway/agSslPolicy'), @@ -1165,6 +1181,7 @@ module.exports = { 'rgHasTags' : require(__dirname + '/plugins/azure/resourceGroup/rgHasTags.js'), 'wafPolicyHasTags' : require(__dirname + '/plugins/azure/waf/wafPolicyHasTags.js'), + 'wafPolicyBotProtection' : require(__dirname + '/plugins/azure/waf/wafPolicyBotProtection.js'), 'recoveryVaultByokEncrypted' : require(__dirname + '/plugins/azure/recoveryService/recoveryVaultByokEncrypted.js'), 'recoveryVaultLoggingEnabled' : require(__dirname + '/plugins/azure/recoveryService/recoveryVaultLoggingEnabled.js'), diff --git a/plugins/azure/applicationGateway/agHttp2Enabled.js b/plugins/azure/applicationGateway/agHttp2Enabled.js new file mode 100644 index 0000000000..1d1fe0084a --- /dev/null +++ b/plugins/azure/applicationGateway/agHttp2Enabled.js @@ -0,0 +1,52 @@ +const async = require('async'); +const helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'Application Gateway HTTP2 Enabled', + category: 'Application Gateway', + domain: 'Network Access Control', + severity: 'Low', + description: 'Ensures that HTTP2 is enabled for Application Gateways.', + more_info: 'HTTP2 support is available to clients that connect to application gateway listeners and provides improved performance and efficiency over HTTP1.1. Clients and backend services that do not support HTTP2 fall back to HTTP1.1.', + recommended_action: 'Enable HTTP2 from the configuration settings of the application gateway.', + link: 'https://learn.microsoft.com/en-us/azure/application-gateway/configuration-overview', + apis: ['applicationGateway:listAll'], + realtime_triggers: ['microsoftnetwork:applicationgateways:write', 'microsoftnetwork:applicationgateways:delete'], + + run: function(cache, settings, callback) { + const results = []; + const source = {}; + const locations = helpers.locations(settings.govcloud); + + async.each(locations.applicationGateway, (location, rcb) => { + var appGateways = helpers.addSource(cache, source, + ['applicationGateway', 'listAll', location]); + + if (!appGateways) return rcb(); + + if (appGateways.err || !appGateways.data) { + helpers.addResult(results, 3, 'Unable to query for Application Gateway: ' + helpers.addError(appGateways), location); + return rcb(); + } + + if (!appGateways.data.length) { + helpers.addResult(results, 0, 'No existing Application Gateway found', location); + return rcb(); + } + + for (let appGateway of appGateways.data) { + if (!appGateway.id) continue; + + if (appGateway.enableHttp2) { + helpers.addResult(results, 0, 'HTTP2 is enabled for Application Gateway', location, appGateway.id); + } else { + helpers.addResult(results, 2, 'HTTP2 is not enabled for Application Gateway', location, appGateway.id); + } + } + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/applicationGateway/agHttp2Enabled.spec.js b/plugins/azure/applicationGateway/agHttp2Enabled.spec.js new file mode 100644 index 0000000000..8f770be6ef --- /dev/null +++ b/plugins/azure/applicationGateway/agHttp2Enabled.spec.js @@ -0,0 +1,106 @@ +var expect = require('chai').expect; +var agHttp2Enabled = require('./agHttp2Enabled'); + +const appGateways = [ + { + 'name': 'test-ag', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Network/applicationGateways/test-ag', + 'type': 'Microsoft.Network/applicationGateways', + 'location': 'eastus', + 'enableHttp2': true + }, + { + 'name': 'test-ag', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Network/applicationGateways/test-ag', + 'type': 'Microsoft.Network/applicationGateways', + 'location': 'eastus', + 'enableHttp2': false + }, + { + 'name': 'test-ag', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Network/applicationGateways/test-ag', + 'type': 'Microsoft.Network/applicationGateways', + 'location': 'eastus' + } +]; + +const createCache = (appGateways) => { + return { + applicationGateway: { + listAll: { + 'eastus': { + data: appGateways + } + } + } + }; +}; + +const createErrorCache = () => { + return { + applicationGateway: { + listAll: { + 'eastus': {} + } + } + }; +}; + +describe('agHttp2Enabled', function () { + describe('run', function () { + it('should give passing result if no application gateways found', function (done) { + const cache = createCache([]); + agHttp2Enabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('No existing Application Gateway found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give unknown result if unable to query for application gateways', function (done) { + const cache = createErrorCache(); + agHttp2Enabled.run(cache, {}, (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 Application Gateway'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if HTTP2 is enabled for application gateway', function (done) { + const cache = createCache([appGateways[0]]); + agHttp2Enabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('HTTP2 is enabled for Application Gateway'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if HTTP2 is not enabled for application gateway', function (done) { + const cache = createCache([appGateways[1]]); + agHttp2Enabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('HTTP2 is not enabled for Application Gateway'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if HTTP2 setting does not exist for application gateway', function (done) { + const cache = createCache([appGateways[2]]); + agHttp2Enabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('HTTP2 is not enabled for Application Gateway'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); +}); diff --git a/plugins/azure/defender/attackPathNotificationsEnabled.js b/plugins/azure/defender/attackPathNotificationsEnabled.js new file mode 100644 index 0000000000..ff17bc76f7 --- /dev/null +++ b/plugins/azure/defender/attackPathNotificationsEnabled.js @@ -0,0 +1,58 @@ +const async = require('async'); +const helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'Attack Path Notifications Enabled', + category: 'Defender', + domain: 'Management and Governance', + severity: 'Low', + description: 'Ensures that email notifications for attack paths are enabled for the subscription.', + more_info: 'Enabling attack path email notifications ensures that the subscription owner or other designated security contact is notified of new attack paths detected by Microsoft Defender for Cloud, allowing for quick mitigation of the associated risks.', + recommended_action: 'Enable email notifications for attack paths from the Microsoft Defender for Cloud email notifications settings.', + link: 'https://learn.microsoft.com/en-us/azure/defender-for-cloud/configure-email-notifications', + apis: ['securityContactv3:listAll'], + realtime_triggers: ['microsoftsecurity:securitycontacts:write', 'microsoftsecurity:securitycontacts:delete'], + + run: function(cache, settings, callback) { + const results = []; + const source = {}; + const locations = helpers.locations(settings.govcloud); + + async.each(locations.securityContactv3, (location, rcb) => { + var securityContacts = helpers.addSource(cache, source, + ['securityContactv3', 'listAll', location]); + + if (!securityContacts) return rcb(); + + if (securityContacts.err || !securityContacts.data) { + helpers.addResult(results, 3, + 'Unable to query for security contacts: ' + helpers.addError(securityContacts), location); + return rcb(); + } + + if (!securityContacts.data.length) { + helpers.addResult(results, 2, 'No existing security contacts found', location); + return rcb(); + } + + for (let contact of securityContacts.data) { + if (!contact.id) continue; + + var attackPathSource = contact.notificationsSources ? + contact.notificationsSources.find(notifSource => notifSource.sourceType && + notifSource.sourceType.toLowerCase() === 'attackpath') : null; + + if (attackPathSource && attackPathSource.minimalRiskLevel) { + helpers.addResult(results, 0, + `Attack path email notifications are enabled with minimum risk level ${attackPathSource.minimalRiskLevel}`, location, contact.id); + } else { + helpers.addResult(results, 2, 'Attack path email notifications are not enabled', location, contact.id); + } + } + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/defender/attackPathNotificationsEnabled.spec.js b/plugins/azure/defender/attackPathNotificationsEnabled.spec.js new file mode 100644 index 0000000000..3ba6601620 --- /dev/null +++ b/plugins/azure/defender/attackPathNotificationsEnabled.spec.js @@ -0,0 +1,117 @@ +var expect = require('chai').expect; +var attackPathNotificationsEnabled = require('./attackPathNotificationsEnabled'); + +const securityContacts = [ + { + 'name': 'default', + 'id': '/subscriptions/123/providers/Microsoft.Security/securityContacts/default', + 'type': 'Microsoft.Security/securityContacts', + 'notificationsSources': [ + { + 'minimalRiskLevel': 'High', + 'sourceType': 'AttackPath' + }, + { + 'minimalSeverity': 'High', + 'sourceType': 'Alert' + } + ] + }, + { + 'name': 'default', + 'id': '/subscriptions/123/providers/Microsoft.Security/securityContacts/default', + 'type': 'Microsoft.Security/securityContacts', + 'notificationsSources': [ + { + 'minimalSeverity': 'High', + 'sourceType': 'Alert' + } + ] + }, + { + 'name': 'default', + 'id': '/subscriptions/123/providers/Microsoft.Security/securityContacts/default', + 'type': 'Microsoft.Security/securityContacts' + } +]; + +const createCache = (securityContacts) => { + return { + securityContactv3: { + listAll: { + 'global': { + data: securityContacts + } + } + } + }; +}; + +const createErrorCache = () => { + return { + securityContactv3: { + listAll: { + 'global': {} + } + } + }; +}; + +describe('attackPathNotificationsEnabled', function () { + describe('run', function () { + it('should give failing result if no security contacts found', function (done) { + const cache = createCache([]); + attackPathNotificationsEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('No existing security contacts found'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give unknown result if unable to query for security contacts', function (done) { + const cache = createErrorCache(); + attackPathNotificationsEnabled.run(cache, {}, (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 security contacts'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give passing result if attack path notifications are enabled', function (done) { + const cache = createCache([securityContacts[0]]); + attackPathNotificationsEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Attack path email notifications are enabled with minimum risk level High'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give failing result if attack path notification source is not present', function (done) { + const cache = createCache([securityContacts[1]]); + attackPathNotificationsEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Attack path email notifications are not enabled'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give failing result if no notification sources are configured', function (done) { + const cache = createCache([securityContacts[2]]); + attackPathNotificationsEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Attack path email notifications are not enabled'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + }); +}); diff --git a/plugins/azure/entraid/disabledUserRoleAssignments.js b/plugins/azure/entraid/disabledUserRoleAssignments.js new file mode 100644 index 0000000000..81f67e822c --- /dev/null +++ b/plugins/azure/entraid/disabledUserRoleAssignments.js @@ -0,0 +1,66 @@ +const async = require('async'); +const helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'Disabled User Role Assignments', + category: 'Entra ID', + domain: 'Identity and Access Management', + severity: 'Medium', + description: 'Ensures that disabled user accounts do not have role assignments.', + more_info: 'Disabled user accounts retain their role assignments by default. Removing role assignments from disabled accounts ensures that access is revoked when an account is blocked and enforces the principle of least privilege.', + recommended_action: 'Remove role assignments from disabled user accounts.', + link: 'https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-remove', + apis: ['users:list', 'aad:listRoleAssignments'], + + run: function(cache, settings, callback) { + const results = []; + const source = {}; + const locations = helpers.locations(settings.govcloud); + + async.each(locations.aad, function(location, rcb) { + + const users = helpers.addSource(cache, source, + ['users', 'list', location]); + + if (!users) return rcb(); + + if (users.err || !users.data) { + helpers.addResult(results, 3, 'Unable to query for users: ' + helpers.addError(users), location); + return rcb(); + } + + const roleAssignments = helpers.addSource(cache, source, + ['aad', 'listRoleAssignments', location]); + + if (!roleAssignments) return rcb(); + + if (roleAssignments.err || !roleAssignments.data) { + helpers.addResult(results, 3, 'Unable to query for role assignments: ' + helpers.addError(roleAssignments), location); + return rcb(); + } + + var disabledUsers = users.data.filter(user => user.id && user.accountEnabled === false); + + if (!disabledUsers.length) { + helpers.addResult(results, 0, 'No disabled user accounts found', location); + return rcb(); + } + + var assignedPrincipals = roleAssignments.data.filter(roleAssignment => roleAssignment.principalId) + .map(roleAssignment => roleAssignment.principalId); + + disabledUsers.forEach(user => { + if (assignedPrincipals.includes(user.id)) { + helpers.addResult(results, 2, 'Disabled user account has role assignments', location, user.id); + } else { + helpers.addResult(results, 0, 'Disabled user account does not have role assignments', location, user.id); + } + }); + + rcb(); + }, function() { + // Global checking goes here + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/entraid/disabledUserRoleAssignments.spec.js b/plugins/azure/entraid/disabledUserRoleAssignments.spec.js new file mode 100644 index 0000000000..0280dc32c9 --- /dev/null +++ b/plugins/azure/entraid/disabledUserRoleAssignments.spec.js @@ -0,0 +1,118 @@ +var expect = require('chai').expect; +var disabledUserRoleAssignments = require('./disabledUserRoleAssignments.js'); + +const users = [ + { + "id": "bb19fc40-d2c4-40a7-a990-9399ee840888", + "displayName": "Enabled User", + "userPrincipalName": "enabled@example.com", + "userType": "Member", + "accountEnabled": true + }, + { + "id": "1d50af91-73f6-45f8-95ff-d21aec6d5d56", + "displayName": "Disabled User With Roles", + "userPrincipalName": "disabled1@example.com", + "userType": "Member", + "accountEnabled": false + }, + { + "id": "8a4c3288-1317-42ef-a8f7-ec60ab455e0a", + "displayName": "Disabled User Without Roles", + "userPrincipalName": "disabled2@example.com", + "userType": "Member", + "accountEnabled": false + } +]; + +const roleAssignments = [ + { + "id": "/subscriptions/123/providers/Microsoft.Authorization/roleAssignments/0d25e3ef-59f3-4a95-9c4f-471b97cdeae9", + "name": "0d25e3ef-59f3-4a95-9c4f-471b97cdeae9", + "roleDefinitionId": "/subscriptions/123/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c", + "principalId": "1d50af91-73f6-45f8-95ff-d21aec6d5d56", + "principalType": "User", + "scope": "/subscriptions/123" + } +]; + +const createCache = (userList, assignments, usersErr, assignmentsErr) => { + return { + users: { + list: { + 'global': { + data: userList, + err: usersErr + } + } + }, + aad: { + listRoleAssignments: { + 'global': { + data: assignments, + err: assignmentsErr + } + } + } + }; +}; + +describe('disabledUserRoleAssignments', function () { + describe('run', function () { + + it('should give unknown result if unable to query for users', function (done) { + const cache = createCache(null, roleAssignments, ['error'], null); + disabledUserRoleAssignments.run(cache, {}, (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 users'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give unknown result if unable to query for role assignments', function (done) { + const cache = createCache(users, null, null, ['error']); + disabledUserRoleAssignments.run(cache, {}, (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 role assignments'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give passing result if no disabled user accounts found', function (done) { + const cache = createCache([users[0]], roleAssignments, null, null); + disabledUserRoleAssignments.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('No disabled user accounts found'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give passing result if disabled user account does not have role assignments', function (done) { + const cache = createCache([users[2]], roleAssignments, null, null); + disabledUserRoleAssignments.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Disabled user account does not have role assignments'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give failing result if disabled user account has role assignments', function (done) { + const cache = createCache([users[1]], roleAssignments, null, null); + disabledUserRoleAssignments.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Disabled user account has role assignments'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + }); +}); diff --git a/plugins/azure/entraid/resourceLockAdminRole.js b/plugins/azure/entraid/resourceLockAdminRole.js new file mode 100644 index 0000000000..ee2194f8a7 --- /dev/null +++ b/plugins/azure/entraid/resourceLockAdminRole.js @@ -0,0 +1,54 @@ +const async = require('async'); +const helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'Resource Lock Administrator Role', + category: 'Entra ID', + domain: 'Identity and Access Management', + severity: 'Low', + description: 'Ensures that a custom role is assigned permissions for administering resource locks.', + more_info: 'Resource locks prevent inadvertent modification or deletion of resources. Managing locks requires the Microsoft.Authorization/locks permission, which is otherwise only available through broad roles such as Owner or User Access Administrator. Creating a custom role limited to lock administration follows the principle of least privilege.', + recommended_action: 'Create a custom role granting the Microsoft.Authorization/locks permission and assign it to the members responsible for administering resource locks.', + link: 'https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/lock-resources', + apis: ['roleDefinitions:list'], + + run: function(cache, settings, callback) { + const results = []; + const source = {}; + const locations = helpers.locations(settings.govcloud); + + async.each(locations.roleDefinitions, function(location, rcb) { + + const roleDefinitions = helpers.addSource(cache, source, + ['roleDefinitions', 'list', location]); + + if (!roleDefinitions) return rcb(); + + if (roleDefinitions.err || !roleDefinitions.data) { + helpers.addResult(results, 3, 'Unable to query for role definitions: ' + helpers.addError(roleDefinitions), location); + return rcb(); + } + + if (!roleDefinitions.data.length) { + helpers.addResult(results, 0, 'No role definitions found', location); + return rcb(); + } + + var lockRole = roleDefinitions.data.find(roleDefinition => roleDefinition.roleType && + roleDefinition.roleType.toLowerCase() === 'customrole' && + (roleDefinition.permissions || []).some(permission => (permission.actions || []).some(action => + action.toLowerCase().startsWith('microsoft.authorization/locks')))); + + if (lockRole) { + helpers.addResult(results, 0, 'Custom role for administering resource locks exists', location, lockRole.id); + } else { + helpers.addResult(results, 2, 'No custom role for administering resource locks found', location); + } + + rcb(); + }, function() { + // Global checking goes here + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/entraid/resourceLockAdminRole.spec.js b/plugins/azure/entraid/resourceLockAdminRole.spec.js new file mode 100644 index 0000000000..5aae64a3de --- /dev/null +++ b/plugins/azure/entraid/resourceLockAdminRole.spec.js @@ -0,0 +1,111 @@ +var expect = require('chai').expect; +var resourceLockAdminRole = require('./resourceLockAdminRole.js'); + +const roleDefinitions = [ + { + "id": "/subscriptions/123/providers/Microsoft.Authorization/roleDefinitions/11111111-1111-1111-1111-111111111111", + "roleName": "Resource Lock Administrator", + "roleType": "CustomRole", + "permissions": [ + { + "actions": ["Microsoft.Authorization/locks/*"], + "notActions": [] + } + ] + }, + { + "id": "/subscriptions/123/providers/Microsoft.Authorization/roleDefinitions/22222222-2222-2222-2222-222222222222", + "roleName": "Owner2", + "roleType": "CustomRole", + "permissions": [ + { + "actions": ["*"], + "notActions": [] + } + ] + }, + { + "id": "/subscriptions/123/providers/Microsoft.Authorization/roleDefinitions/33333333-3333-3333-3333-333333333333", + "roleName": "Locks Contributor", + "roleType": "BuiltInRole", + "permissions": [ + { + "actions": ["Microsoft.Authorization/locks/*"], + "notActions": [] + } + ] + } +]; + +const createCache = (definitions, err) => { + return { + roleDefinitions: { + list: { + 'global': { + data: definitions, + err: err + } + } + } + }; +}; + +describe('resourceLockAdminRole', function () { + describe('run', function () { + + it('should give unknown result if unable to query for role definitions', function (done) { + const cache = createCache(null, ['error']); + resourceLockAdminRole.run(cache, {}, (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 role definitions'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give passing result if no role definitions found', function (done) { + const cache = createCache([], null); + resourceLockAdminRole.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('No role definitions found'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give passing result if a custom role for administering resource locks exists', function (done) { + const cache = createCache([roleDefinitions[0]], null); + resourceLockAdminRole.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Custom role for administering resource locks exists'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give failing result if only a wildcard custom role grants lock permissions', function (done) { + const cache = createCache([roleDefinitions[1]], null); + resourceLockAdminRole.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('No custom role for administering resource locks found'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give failing result if only a built-in role grants lock permissions', function (done) { + const cache = createCache([roleDefinitions[2]], null); + resourceLockAdminRole.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('No custom role for administering resource locks found'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + }); +}); diff --git a/plugins/azure/entraid/securityDefaultsEnabled.js b/plugins/azure/entraid/securityDefaultsEnabled.js new file mode 100644 index 0000000000..1cac1e0538 --- /dev/null +++ b/plugins/azure/entraid/securityDefaultsEnabled.js @@ -0,0 +1,49 @@ +const async = require('async'); +const helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'Security Defaults Enabled', + category: 'Entra ID', + domain: 'Identity and Access Management', + severity: 'Medium', + description: 'Ensures that security defaults are enabled in Microsoft Entra ID.', + more_info: 'Security defaults are preconfigured identity security settings that require all users and administrators to register for multi-factor authentication, challenge users with multi-factor authentication when needed and block legacy authentication protocols. Enabling security defaults provides a basic level of identity protection at no extra cost.', + recommended_action: 'Enable security defaults from Microsoft Entra ID properties.', + link: 'https://learn.microsoft.com/en-us/entra/fundamentals/security-defaults', + apis: ['securityDefaultsPolicy:get'], + + run: function(cache, settings, callback) { + const results = []; + const source = {}; + const locations = helpers.locations(settings.govcloud); + + async.each(locations.securityDefaultsPolicy, function(location, rcb) { + + const policy = helpers.addSource(cache, source, + ['securityDefaultsPolicy', 'get', location]); + + if (!policy) return rcb(); + + if (policy.err || !policy.data) { + helpers.addResult(results, 3, 'Unable to query for security defaults policy: ' + helpers.addError(policy), location); + return rcb(); + } + + if (!policy.data.length) { + helpers.addResult(results, 0, 'No existing security defaults policy found', location); + return rcb(); + } + + if (policy.data[0].isEnabled) { + helpers.addResult(results, 0, 'Security defaults are enabled', location); + } else { + helpers.addResult(results, 2, 'Security defaults are not enabled', location); + } + + rcb(); + }, function() { + // Global checking goes here + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/entraid/securityDefaultsEnabled.spec.js b/plugins/azure/entraid/securityDefaultsEnabled.spec.js new file mode 100644 index 0000000000..6ee1eeb013 --- /dev/null +++ b/plugins/azure/entraid/securityDefaultsEnabled.spec.js @@ -0,0 +1,79 @@ +var expect = require('chai').expect; +var securityDefaultsEnabled = require('./securityDefaultsEnabled.js'); + +const policies = [ + { + "id": "00000000-0000-0000-0000-000000000005", + "displayName": "Security Defaults", + "description": "Security defaults is a set of basic identity security mechanisms recommended by Microsoft.", + "isEnabled": true + }, + { + "id": "00000000-0000-0000-0000-000000000005", + "displayName": "Security Defaults", + "description": "Security defaults is a set of basic identity security mechanisms recommended by Microsoft.", + "isEnabled": false + } +]; + +const createCache = (policy, err) => { + return { + securityDefaultsPolicy: { + get: { + 'global': { + data: policy, + err: err + } + } + } + }; +}; + +describe('securityDefaultsEnabled', function () { + describe('run', function () { + + it('should give unknown result if unable to query for security defaults policy', function (done) { + const cache = createCache(null, ['error']); + securityDefaultsEnabled.run(cache, {}, (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 security defaults policy'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give passing result if no security defaults policy found', function (done) { + const cache = createCache([], null); + securityDefaultsEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('No existing security defaults policy found'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give passing result if security defaults are enabled', function (done) { + const cache = createCache([policies[0]], null); + securityDefaultsEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Security defaults are enabled'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give failing result if security defaults are not enabled', function (done) { + const cache = createCache([policies[1]], null); + securityDefaultsEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Security defaults are not enabled'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + }); +}); diff --git a/plugins/azure/keyvaults/certificateValidityPeriod.js b/plugins/azure/keyvaults/certificateValidityPeriod.js new file mode 100644 index 0000000000..25b9f52be1 --- /dev/null +++ b/plugins/azure/keyvaults/certificateValidityPeriod.js @@ -0,0 +1,86 @@ +var async = require('async'); +var helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'Certificate Validity Period', + category: 'Key Vaults', + domain: 'Application Integration', + severity: 'Medium', + description: 'Ensures that Key Vault certificates have a validity period of 12 months or less.', + more_info: 'Limiting certificate validity reduces the risk of misuse if a certificate is compromised and helps ensure timely renewal, improving overall security and reliability.', + recommended_action: 'Modify the certificate issuance policy and set the validity period to 12 months or less.', + link: 'https://learn.microsoft.com/en-us/azure/key-vault/certificates/create-certificate-scenarios', + apis: ['vaults:list', 'vaults:getCertificates', 'getCertificatePolicy:get'], + settings: { + certificate_validity_period_fail: { + name: 'Certificate Validity Period Fail', + description: 'Return a failing result if the certificate validity period, in months, is greater than this number', + regex: '^[1-9][0-9]{0,2}$', + default: '12' + } + }, + realtime_triggers: ['microsoftkeyvault:vaults:write', 'microsoftkeyvault:vaults:delete'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var locations = helpers.locations(settings.govcloud); + var config = { + certificate_validity_period_fail: parseInt(settings.certificate_validity_period_fail || this.settings.certificate_validity_period_fail.default) + }; + + async.each(locations.vaults, function(location, rcb) { + var vaults = helpers.addSource(cache, source, + ['vaults', 'list', location]); + + if (!vaults) return rcb(); + + if (vaults.err || !vaults.data) { + helpers.addResult(results, 3, 'Unable to query for Key Vaults: ' + helpers.addError(vaults), location); + return rcb(); + } + + if (!vaults.data.length) { + helpers.addResult(results, 0, 'No Key Vaults found', location); + return rcb(); + } + + vaults.data.forEach((vault) => { + var certificates = helpers.addSource(cache, source, + ['vaults', 'getCertificates', location, vault.id]); + + if (!certificates || certificates.err || !certificates.data) { + helpers.addResult(results, 3, 'Unable to query for Key Vault certificates: ' + helpers.addError(certificates), location, vault.id); + } else if (!certificates.data.length) { + helpers.addResult(results, 0, 'No Key Vault Certificates found', location, vault.id); + } else { + certificates.data.forEach((certificate) => { + var certificatePolicy = helpers.addSource(cache, source, + ['getCertificatePolicy', 'get', location, certificate.id]); + + if (!certificatePolicy || certificatePolicy.err || !certificatePolicy.data) { + helpers.addResult(results, 3, 'Unable to query for Certificate Policy: ' + helpers.addError(certificatePolicy), location, certificate.id); + return; + } + + var validityMonths = certificatePolicy.data.x509_props ? certificatePolicy.data.x509_props.validity_months : undefined; + + if (validityMonths === undefined || validityMonths === null) { + helpers.addResult(results, 3, 'Unable to determine certificate validity period', location, certificate.id); + } else if (validityMonths <= config.certificate_validity_period_fail) { + helpers.addResult(results, 0, + `Certificate validity period is set to ${validityMonths} months`, location, certificate.id); + } else { + helpers.addResult(results, 2, + `Certificate validity period is set to ${validityMonths} months which is greater than ${config.certificate_validity_period_fail}`, location, certificate.id); + } + }); + } + }); + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/keyvaults/certificateValidityPeriod.spec.js b/plugins/azure/keyvaults/certificateValidityPeriod.spec.js new file mode 100644 index 0000000000..08007bcbf0 --- /dev/null +++ b/plugins/azure/keyvaults/certificateValidityPeriod.spec.js @@ -0,0 +1,157 @@ +var expect = require('chai').expect; +var certificateValidityPeriod = require('./certificateValidityPeriod'); + +const vaults = [ + { + 'name': 'test-vault', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.KeyVault/vaults/test-vault', + 'type': 'Microsoft.KeyVault/vaults', + 'location': 'eastus' + } +]; + +const certificates = [ + { + 'id': 'https://test-vault.vault.azure.net/certificates/test-cert' + } +]; + +const certificatePolicies = [ + { + 'id': 'https://test-vault.vault.azure.net/certificates/test-cert/policy', + 'x509_props': { + 'subject': 'CN=test.com', + 'validity_months': 12 + } + }, + { + 'id': 'https://test-vault.vault.azure.net/certificates/test-cert/policy', + 'x509_props': { + 'subject': 'CN=test.com', + 'validity_months': 24 + } + }, + { + 'id': 'https://test-vault.vault.azure.net/certificates/test-cert/policy', + 'x509_props': { + 'subject': 'CN=test.com' + } + } +]; + +const createCache = (vaults, certificates, certificatePolicy) => { + const vaultId = vaults && vaults.length ? vaults[0].id : null; + const certId = certificates && certificates.length ? certificates[0].id : null; + const certPolicyObj = {}; + if (certId) { + certPolicyObj[certId] = certificatePolicy ? { data: certificatePolicy } : {}; + } + return { + vaults: { + list: { + 'eastus': { + data: vaults + } + }, + getCertificates: { + 'eastus': vaultId ? { [vaultId]: { data: certificates } } : {} + } + }, + getCertificatePolicy: { + get: { + 'eastus': certPolicyObj + } + } + }; +}; + +const createErrorCache = () => { + return { + vaults: { + list: { + 'eastus': {} + } + } + }; +}; + +describe('certificateValidityPeriod', function () { + describe('run', function () { + it('should give passing result if no Key Vaults found', function (done) { + const cache = createCache([], null, null); + certificateValidityPeriod.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('No Key Vaults found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give unknown result if unable to query for Key Vaults', function (done) { + const cache = createErrorCache(); + certificateValidityPeriod.run(cache, {}, (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 Key Vaults'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if no certificates found', function (done) { + const cache = createCache(vaults, [], null); + certificateValidityPeriod.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('No Key Vault Certificates found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if validity period is within the configured limit', function (done) { + const cache = createCache(vaults, certificates, certificatePolicies[0]); + certificateValidityPeriod.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Certificate validity period is set to 12 months'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if validity period exceeds the configured limit', function (done) { + const cache = createCache(vaults, certificates, certificatePolicies[1]); + certificateValidityPeriod.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Certificate validity period is set to 24 months which is greater than 12'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give unknown result if validity period cannot be determined', function (done) { + const cache = createCache(vaults, certificates, certificatePolicies[2]); + certificateValidityPeriod.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(3); + expect(results[0].message).to.include('Unable to determine certificate validity period'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give unknown result if unable to query for certificate policy', function (done) { + const cache = createCache(vaults, certificates, null); + certificateValidityPeriod.run(cache, {}, (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 Certificate Policy'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); +}); diff --git a/plugins/azure/keyvaults/keyVaultKeyRotation.js b/plugins/azure/keyvaults/keyVaultKeyRotation.js new file mode 100644 index 0000000000..fec54dc296 --- /dev/null +++ b/plugins/azure/keyvaults/keyVaultKeyRotation.js @@ -0,0 +1,82 @@ +var async = require('async'); +var helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'Key Vault Key Rotation Enabled', + category: 'Key Vaults', + domain: 'Application Integration', + severity: 'Medium', + description: 'Ensures that automatic key rotation is enabled for Key Vault keys.', + more_info: 'A key rotation policy generates a new key version automatically at a configured frequency. Rotating keys without manual intervention reduces the risk of a key being used beyond its recommended cryptoperiod.', + recommended_action: 'Configure a rotation policy with a rotate action for each key from the Key Vault key rotation policy settings.', + link: 'https://learn.microsoft.com/en-us/azure/key-vault/keys/how-to-configure-key-rotation', + apis: ['vaults:list', 'vaults:listKeys', 'getKey:get'], + realtime_triggers: ['microsoftkeyvault:vaults:write', 'microsoftkeyvault:vaults:delete', 'microsoftkeyvault:vaults:keys:write', 'microsoftkeyvault:vaults:keys:delete'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var locations = helpers.locations(settings.govcloud); + + async.each(locations.vaults, function(location, rcb) { + var vaults = helpers.addSource(cache, source, + ['vaults', 'list', location]); + + if (!vaults) return rcb(); + + if (vaults.err || !vaults.data) { + helpers.addResult(results, 3, 'Unable to query for Key Vaults: ' + helpers.addError(vaults), location); + return rcb(); + } + + if (!vaults.data.length) { + helpers.addResult(results, 0, 'No existing Key Vaults found', location); + return rcb(); + } + + vaults.data.forEach(function(vault) { + var keys = helpers.addSource(cache, source, + ['vaults', 'listKeys', location, vault.id]); + + if (!keys || keys.err || !keys.data) { + helpers.addResult(results, 3, 'Unable to query for Key Vault keys: ' + helpers.addError(keys), location, vault.id); + return; + } + + if (!keys.data.length) { + helpers.addResult(results, 0, 'No existing Key Vault keys found', location, vault.id); + return; + } + + keys.data.forEach(function(key) { + if (!key.id) return; + + var keyData = helpers.addSource(cache, source, + ['getKey', 'get', location, key.id]); + + if (!keyData || keyData.err || !keyData.data) { + helpers.addResult(results, 3, 'Unable to query for Key Vault key: ' + helpers.addError(keyData), location, key.id); + return; + } + + var rotationPolicy = keyData.data.rotationPolicy; + + var rotationEnabled = rotationPolicy && rotationPolicy.lifetimeActions && + rotationPolicy.lifetimeActions.some(lifetimeAction => lifetimeAction.action && + lifetimeAction.action.type && lifetimeAction.action.type.toLowerCase() === 'rotate' && + lifetimeAction.trigger && (lifetimeAction.trigger.timeAfterCreate || lifetimeAction.trigger.timeBeforeExpiry)); + + if (rotationEnabled) { + helpers.addResult(results, 0, 'Key Vault key has automatic rotation enabled', location, key.id); + } else { + helpers.addResult(results, 2, 'Key Vault key does not have automatic rotation enabled', location, key.id); + } + }); + }); + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/keyvaults/keyVaultKeyRotation.spec.js b/plugins/azure/keyvaults/keyVaultKeyRotation.spec.js new file mode 100644 index 0000000000..5b97611ddd --- /dev/null +++ b/plugins/azure/keyvaults/keyVaultKeyRotation.spec.js @@ -0,0 +1,179 @@ +var expect = require('chai').expect; +var keyVaultKeyRotation = require('./keyVaultKeyRotation'); + +const vaults = [ + { + 'name': 'test-vault', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.KeyVault/vaults/test-vault', + 'type': 'Microsoft.KeyVault/vaults', + 'location': 'eastus' + } +]; + +const keyId = '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.KeyVault/vaults/test-vault/keys/test-key'; + +const keys = [ + { 'id': keyId }, + { 'id': keyId }, + { 'id': keyId } +]; + +const keyDetails = [ + { + 'rotationPolicy': { + 'lifetimeActions': [ + { + 'trigger': { 'timeAfterCreate': 'P90D' }, + 'action': { 'type': 'rotate' } + }, + { + 'trigger': { 'timeBeforeExpiry': 'P30D' }, + 'action': { 'type': 'notify' } + } + ], + 'attributes': { 'expiryTime': 'P2Y' } + } + }, + { + 'rotationPolicy': { + 'lifetimeActions': [ + { + 'trigger': { 'timeBeforeExpiry': 'P30D' }, + 'action': { 'type': 'notify' } + } + ] + } + }, + {} +]; + +const createCache = (vaults, keys, keysErr, detailIndex) => { + const vaultId = vaults && vaults.length ? vaults[0].id : null; + const keyObj = {}; + const detailObj = {}; + if (vaultId) { + keyObj[vaultId] = keysErr ? { err: keysErr } : { data: keys }; + if (keys && keys.length && detailIndex !== undefined && keyDetails[detailIndex]) { + detailObj[keyId] = { data: keyDetails[detailIndex] }; + } + } + return { + vaults: { + list: { + 'eastus': { + data: vaults + } + }, + listKeys: { + 'eastus': keyObj + } + }, + getKey: { + get: { + 'eastus': detailObj + } + } + }; +}; + +const createErrorCache = () => { + return { + vaults: { + list: { + 'eastus': {} + } + } + }; +}; + +describe('keyVaultKeyRotation', function () { + describe('run', function () { + it('should give passing result if no Key Vaults found', function (done) { + const cache = createCache([], null); + keyVaultKeyRotation.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('No existing Key Vaults found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give unknown result if unable to query for Key Vaults', function (done) { + const cache = createErrorCache(); + keyVaultKeyRotation.run(cache, {}, (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 Key Vaults'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give unknown result if unable to query for Key Vault keys', function (done) { + const cache = createCache(vaults, null, ['ForbiddenByRbac']); + keyVaultKeyRotation.run(cache, {}, (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 Key Vault keys'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if no keys found in vault', function (done) { + const cache = createCache(vaults, []); + keyVaultKeyRotation.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('No existing Key Vault keys found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if key has automatic rotation enabled', function (done) { + const cache = createCache(vaults, [keys[0]], null, 0); + keyVaultKeyRotation.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Key Vault key has automatic rotation enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if key only has a notify action', function (done) { + const cache = createCache(vaults, [keys[1]], null, 1); + keyVaultKeyRotation.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Key Vault key does not have automatic rotation enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if key has no rotation policy', function (done) { + const cache = createCache(vaults, [keys[2]], null, 2); + keyVaultKeyRotation.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Key Vault key does not have automatic rotation enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give unknown result if unable to query for the key', function (done) { + const cache = createCache(vaults, [keys[0]]); + keyVaultKeyRotation.run(cache, {}, (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 Key Vault key'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); +}); diff --git a/plugins/azure/keyvaults/keyVaultRbacEnabled.js b/plugins/azure/keyvaults/keyVaultRbacEnabled.js new file mode 100644 index 0000000000..dc06d5721d --- /dev/null +++ b/plugins/azure/keyvaults/keyVaultRbacEnabled.js @@ -0,0 +1,52 @@ +var async = require('async'); +var helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'Key Vault RBAC Enabled', + category: 'Key Vaults', + domain: 'Application Integration', + severity: 'Medium', + description: 'Ensures that Azure Role-Based Access Control is enabled for Key Vaults.', + more_info: 'Azure RBAC provides fine-grained access management of keys, secrets and certificates in a Key Vault, and allows permissions to be managed in one place across all key vaults. Vault access policies, the alternative, offer coarser control and cannot be centrally audited in the same way.', + recommended_action: 'Enable Azure role-based access control from the access configuration settings of the Key Vault.', + link: 'https://learn.microsoft.com/en-us/azure/key-vault/general/rbac-guide', + apis: ['vaults:list'], + realtime_triggers: ['microsoftkeyvault:vaults:write', 'microsoftkeyvault:vaults:delete'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var locations = helpers.locations(settings.govcloud); + + async.each(locations.vaults, function(location, rcb) { + var vaults = helpers.addSource(cache, source, + ['vaults', 'list', location]); + + if (!vaults) return rcb(); + + if (vaults.err || !vaults.data) { + helpers.addResult(results, 3, 'Unable to query for Key Vaults: ' + helpers.addError(vaults), location); + return rcb(); + } + + if (!vaults.data.length) { + helpers.addResult(results, 0, 'No existing Key Vaults found', location); + return rcb(); + } + + for (let vault of vaults.data) { + if (!vault.id) continue; + + if (vault.enableRbacAuthorization) { + helpers.addResult(results, 0, 'Key Vault has RBAC authorization enabled', location, vault.id); + } else { + helpers.addResult(results, 2, 'Key Vault does not have RBAC authorization enabled', location, vault.id); + } + } + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/keyvaults/keyVaultRbacEnabled.spec.js b/plugins/azure/keyvaults/keyVaultRbacEnabled.spec.js new file mode 100644 index 0000000000..4c9f386dd5 --- /dev/null +++ b/plugins/azure/keyvaults/keyVaultRbacEnabled.spec.js @@ -0,0 +1,106 @@ +var expect = require('chai').expect; +var keyVaultRbacEnabled = require('./keyVaultRbacEnabled'); + +const vaults = [ + { + 'name': 'test-vault', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.KeyVault/vaults/test-vault', + 'type': 'Microsoft.KeyVault/vaults', + 'location': 'eastus', + 'enableRbacAuthorization': true + }, + { + 'name': 'test-vault', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.KeyVault/vaults/test-vault', + 'type': 'Microsoft.KeyVault/vaults', + 'location': 'eastus', + 'enableRbacAuthorization': false + }, + { + 'name': 'test-vault', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.KeyVault/vaults/test-vault', + 'type': 'Microsoft.KeyVault/vaults', + 'location': 'eastus' + } +]; + +const createCache = (vaults) => { + return { + vaults: { + list: { + 'eastus': { + data: vaults + } + } + } + }; +}; + +const createErrorCache = () => { + return { + vaults: { + list: { + 'eastus': {} + } + } + }; +}; + +describe('keyVaultRbacEnabled', function () { + describe('run', function () { + it('should give passing result if no Key Vaults found', function (done) { + const cache = createCache([]); + keyVaultRbacEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('No existing Key Vaults found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give unknown result if unable to query for Key Vaults', function (done) { + const cache = createErrorCache(); + keyVaultRbacEnabled.run(cache, {}, (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 Key Vaults'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if Key Vault has RBAC authorization enabled', function (done) { + const cache = createCache([vaults[0]]); + keyVaultRbacEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Key Vault has RBAC authorization enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if Key Vault does not have RBAC authorization enabled', function (done) { + const cache = createCache([vaults[1]]); + keyVaultRbacEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Key Vault does not have RBAC authorization enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if RBAC authorization setting is not present', function (done) { + const cache = createCache([vaults[2]]); + keyVaultRbacEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Key Vault does not have RBAC authorization enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); +}); diff --git a/plugins/azure/logalerts/serviceHealthAlertEnabled.js b/plugins/azure/logalerts/serviceHealthAlertEnabled.js new file mode 100644 index 0000000000..0211ca0e59 --- /dev/null +++ b/plugins/azure/logalerts/serviceHealthAlertEnabled.js @@ -0,0 +1,55 @@ +var async = require('async'); +var helpers = require('../../../helpers/azure/'); + +module.exports = { + title: 'Service Health Alert Enabled', + category: 'Log Alerts', + domain: 'Management and Governance', + severity: 'Medium', + description: 'Ensures that an activity log alert exists for Service Health events.', + more_info: 'Service Health events cover service issues, planned maintenance and security advisories that affect the Azure services and regions in use. An activity log alert for Service Health provides visibility into these changes so that they can be acted on in time.', + recommended_action: 'Add an activity log alert that monitors Service Health events for the subscription and sends notifications to an action group.', + link: 'https://learn.microsoft.com/en-us/azure/service-health/alerts-activity-log-service-notifications-portal', + apis: ['activityLogAlerts:listBySubscriptionId'], + realtime_triggers: ['microsoftinsights:activitylogalerts:write', 'microsoftinsights:activitylogalerts:delete'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var locations = helpers.locations(settings.govcloud); + + async.each(locations.activityLogAlerts, function(location, rcb) { + + var activityLogAlerts = helpers.addSource(cache, source, + ['activityLogAlerts', 'listBySubscriptionId', location]); + + if (!activityLogAlerts) return rcb(); + + if (activityLogAlerts.err || !activityLogAlerts.data) { + helpers.addResult(results, 3, 'Unable to query for Activity Alerts: ' + helpers.addError(activityLogAlerts), location); + return rcb(); + } + + if (!activityLogAlerts.data.length) { + helpers.addResult(results, 2, 'No existing Activity Alerts found', location); + return rcb(); + } + + var serviceHealthAlert = activityLogAlerts.data.find(alert => alert.enabled && + alert.condition && alert.condition.allOf && + alert.condition.allOf.some(condition => condition.field && + condition.field.toLowerCase() === 'category' && + condition.equals && condition.equals.toLowerCase() === 'servicehealth')); + + if (serviceHealthAlert) { + helpers.addResult(results, 0, 'Log Alert for Service Health is enabled', location, serviceHealthAlert.id); + } else { + helpers.addResult(results, 2, 'Log Alert for Service Health is not enabled', location); + } + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/logalerts/serviceHealthAlertEnabled.spec.js b/plugins/azure/logalerts/serviceHealthAlertEnabled.spec.js new file mode 100644 index 0000000000..027155adfa --- /dev/null +++ b/plugins/azure/logalerts/serviceHealthAlertEnabled.spec.js @@ -0,0 +1,118 @@ +var expect = require('chai').expect; +var serviceHealthAlertEnabled = require('./serviceHealthAlertEnabled.js'); + +const activityLogAlerts = [ + { + "id": "/subscriptions/123/resourceGroups/test/providers/microsoft.insights/activityLogAlerts/ServiceHealthAlert", + "name": "ServiceHealthAlert", + "type": "Microsoft.Insights/ActivityLogAlerts", + "location": "global", + "scopes": ["/subscriptions/123"], + "condition": { + "allOf": [ + { "field": "category", "equals": "ServiceHealth" } + ] + }, + "enabled": true + }, + { + "id": "/subscriptions/123/resourceGroups/test/providers/microsoft.insights/activityLogAlerts/AdministrativeAlert", + "name": "AdministrativeAlert", + "type": "Microsoft.Insights/ActivityLogAlerts", + "location": "global", + "scopes": ["/subscriptions/123"], + "condition": { + "allOf": [ + { "field": "category", "equals": "Administrative" }, + { "field": "Status", "equals": "Succeeded" } + ] + }, + "enabled": true + }, + { + "id": "/subscriptions/123/resourceGroups/test/providers/microsoft.insights/activityLogAlerts/DisabledServiceHealthAlert", + "name": "DisabledServiceHealthAlert", + "type": "Microsoft.Insights/ActivityLogAlerts", + "location": "global", + "scopes": ["/subscriptions/123"], + "condition": { + "allOf": [ + { "field": "category", "equals": "ServiceHealth" } + ] + }, + "enabled": false + } +]; + +const createCache = (alerts, err) => { + return { + activityLogAlerts: { + listBySubscriptionId: { + 'global': { + data: alerts, + err: err + } + } + } + }; +}; + +describe('serviceHealthAlertEnabled', function () { + describe('run', function () { + + it('should give unknown result if unable to query for activity alerts', function (done) { + const cache = createCache(null, ['error']); + serviceHealthAlertEnabled.run(cache, {}, (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 Activity Alerts'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give failing result if no existing activity alerts found', function (done) { + const cache = createCache([], null); + serviceHealthAlertEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('No existing Activity Alerts found'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give passing result if a Service Health alert is enabled', function (done) { + const cache = createCache([activityLogAlerts[0]], null); + serviceHealthAlertEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Log Alert for Service Health is enabled'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give failing result if no Service Health alert exists', function (done) { + const cache = createCache([activityLogAlerts[1]], null); + serviceHealthAlertEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Log Alert for Service Health is not enabled'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give failing result if the Service Health alert is disabled', function (done) { + const cache = createCache([activityLogAlerts[2]], null); + serviceHealthAlertEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Log Alert for Service Health is not enabled'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + }); +}); diff --git a/plugins/azure/monitor/appInsightsConfigured.js b/plugins/azure/monitor/appInsightsConfigured.js new file mode 100644 index 0000000000..1949909e3e --- /dev/null +++ b/plugins/azure/monitor/appInsightsConfigured.js @@ -0,0 +1,45 @@ +var async = require('async'); +var helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'Application Insights Configured', + category: 'Monitor', + domain: 'Management and Governance', + severity: 'Low', + description: 'Ensures that Application Insights is configured for the subscription.', + more_info: 'Application Insights collects application metrics, telemetry and trace logging data. This data supports proactive monitoring of application performance and provides the detail needed to identify the source of an incident during a reactive investigation.', + recommended_action: 'Create an Application Insights resource and associate it with a Log Analytics workspace.', + link: 'https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview', + apis: ['appInsights:list'], + realtime_triggers: ['microsoftinsights:components:write', 'microsoftinsights:components:delete'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var locations = helpers.locations(settings.govcloud); + + async.each(locations.appInsights, function(location, rcb) { + + var appInsights = helpers.addSource(cache, source, + ['appInsights', 'list', location]); + + if (!appInsights) return rcb(); + + if (appInsights.err || !appInsights.data) { + helpers.addResult(results, 3, 'Unable to query for Application Insights: ' + helpers.addError(appInsights), location); + return rcb(); + } + + if (!appInsights.data.length) { + helpers.addResult(results, 2, 'Application Insights is not configured for the subscription', location); + return rcb(); + } + + helpers.addResult(results, 0, 'Application Insights is configured for the subscription', location); + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/monitor/appInsightsConfigured.spec.js b/plugins/azure/monitor/appInsightsConfigured.spec.js new file mode 100644 index 0000000000..4930f5ba30 --- /dev/null +++ b/plugins/azure/monitor/appInsightsConfigured.spec.js @@ -0,0 +1,65 @@ +var expect = require('chai').expect; +var appInsightsConfigured = require('./appInsightsConfigured.js'); + +const components = [ + { + "id": "/subscriptions/123/resourceGroups/test/providers/microsoft.insights/components/test-insights", + "name": "test-insights", + "type": "microsoft.insights/components", + "location": "eastus", + "kind": "web", + "applicationId": "test-insights", + "provisioningState": "Succeeded" + } +]; + +const createCache = (insights, err) => { + return { + appInsights: { + list: { + 'global': { + data: insights, + err: err + } + } + } + }; +}; + +describe('appInsightsConfigured', function () { + describe('run', function () { + + it('should give unknown result if unable to query for application insights', function (done) { + const cache = createCache(null, ['error']); + appInsightsConfigured.run(cache, {}, (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 Application Insights'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give failing result if application insights is not configured', function (done) { + const cache = createCache([], null); + appInsightsConfigured.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Application Insights is not configured for the subscription'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + + it('should give passing result if application insights is configured', function (done) { + const cache = createCache(components, null); + appInsightsConfigured.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Application Insights is configured for the subscription'); + expect(results[0].region).to.equal('global'); + done(); + }); + }); + }); +}); diff --git a/plugins/azure/storageaccounts/blobAnonymousAccessDisabled.js b/plugins/azure/storageaccounts/blobAnonymousAccessDisabled.js new file mode 100644 index 0000000000..3276e42f4b --- /dev/null +++ b/plugins/azure/storageaccounts/blobAnonymousAccessDisabled.js @@ -0,0 +1,57 @@ +const async = require('async'); +const helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'Blob Anonymous Access Disabled', + category: 'Storage Accounts', + domain: 'Storage', + severity: 'High', + description: 'Ensures that anonymous access to blob data is disabled for Microsoft Azure Storage Accounts.', + more_info: 'When Allow Blob Anonymous Access is enabled, blobs can be accessed by adding the blob name to the URL without authentication. An attacker can enumerate blobs using methods such as brute force and access them, resulting in exfiltration of data.', + recommended_action: 'Disable Allow Blob Anonymous Access from the configuration settings of the storage account.', + link: 'https://learn.microsoft.com/en-us/azure/storage/blobs/anonymous-read-access-prevent', + apis: ['storageAccounts:list'], + realtime_triggers: ['microsoftstorage:storageaccounts:write', 'microsoftstorage:storageaccounts:delete'], + + run: function(cache, settings, callback) { + const results = []; + const source = {}; + const locations = helpers.locations(settings.govcloud); + + 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 => { + if (!storageAccount.id) return; + + if (!storageAccount.allowBlobPublicAccess) { + helpers.addResult(results, 0, + 'Storage Account has blob anonymous access disabled', + location, storageAccount.id); + } else { + helpers.addResult(results, 2, + 'Storage Account has blob anonymous access enabled', + location, storageAccount.id); + } + }); + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/storageaccounts/blobAnonymousAccessDisabled.spec.js b/plugins/azure/storageaccounts/blobAnonymousAccessDisabled.spec.js new file mode 100644 index 0000000000..249799de37 --- /dev/null +++ b/plugins/azure/storageaccounts/blobAnonymousAccessDisabled.spec.js @@ -0,0 +1,106 @@ +var expect = require('chai').expect; +var blobAnonymousAccessDisabled = require('./blobAnonymousAccessDisabled'); + +const storageAccounts = [ + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Storage/storageAccounts/acc', + 'name': 'acc', + 'type': 'Microsoft.Storage/storageAccounts', + 'location': 'eastus', + 'allowBlobPublicAccess': false + }, + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Storage/storageAccounts/acc', + 'name': 'acc', + 'type': 'Microsoft.Storage/storageAccounts', + 'location': 'eastus', + 'allowBlobPublicAccess': true + }, + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Storage/storageAccounts/acc', + 'name': 'acc', + 'type': 'Microsoft.Storage/storageAccounts', + 'location': 'eastus' + } +]; + +const createCache = (storageAccounts) => { + return { + storageAccounts: { + list: { + 'eastus': { + data: storageAccounts + } + } + } + }; +}; + +const createErrorCache = () => { + return { + storageAccounts: { + list: { + 'eastus': {} + } + } + }; +}; + +describe('blobAnonymousAccessDisabled', function () { + describe('run', function () { + it('should give passing result if no storage accounts found', function (done) { + const cache = createCache([]); + blobAnonymousAccessDisabled.run(cache, {}, (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(); + }); + }); + + it('should give unknown result if unable to query for storage accounts', function (done) { + const cache = createErrorCache(); + blobAnonymousAccessDisabled.run(cache, {}, (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(); + }); + }); + + it('should give passing result if blob anonymous access is disabled', function (done) { + const cache = createCache([storageAccounts[0]]); + blobAnonymousAccessDisabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('has blob anonymous access disabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if blob anonymous access is enabled', function (done) { + const cache = createCache([storageAccounts[1]]); + blobAnonymousAccessDisabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('has blob anonymous access enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if allowBlobPublicAccess is not set', function (done) { + const cache = createCache([storageAccounts[2]]); + blobAnonymousAccessDisabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('has blob anonymous access disabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); +}); diff --git a/plugins/azure/storageaccounts/blobVersioningEnabled.js b/plugins/azure/storageaccounts/blobVersioningEnabled.js new file mode 100644 index 0000000000..687282717c --- /dev/null +++ b/plugins/azure/storageaccounts/blobVersioningEnabled.js @@ -0,0 +1,64 @@ +const async = require('async'); +const helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'Blob Versioning Enabled', + category: 'Storage Accounts', + domain: 'Storage', + severity: 'Medium', + description: 'Ensures that blob versioning is enabled for Microsoft Storage Account blob service.', + more_info: 'Enabling blob versioning allows for the automatic retention of previous versions of blobs, allowing data to be recovered in the event of accidental modification or deletion.', + recommended_action: 'Enable versioning for blobs on the storage account blob service.', + link: 'https://learn.microsoft.com/en-us/azure/storage/blobs/versioning-overview', + apis: ['storageAccounts:list', 'blobServices:getServiceProperties'], + realtime_triggers: ['microsoftstorage:storageaccounts:write', 'microsoftstorage:storageaccounts:delete', 'microsoftstorage:storageaccounts:blobservices:write'], + + run: function(cache, settings, callback) { + const results = []; + const source = {}; + const locations = helpers.locations(settings.govcloud); + + 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.isVersioningEnabled) { + helpers.addResult(results, 0, + 'Blob versioning is enabled for Storage Account', + location, storageAccount.id); + } else { + helpers.addResult(results, 2, + 'Blob versioning is not enabled for Storage Account', + location, storageAccount.id); + } + } + }); + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/storageaccounts/blobVersioningEnabled.spec.js b/plugins/azure/storageaccounts/blobVersioningEnabled.spec.js new file mode 100644 index 0000000000..ea3af167e0 --- /dev/null +++ b/plugins/azure/storageaccounts/blobVersioningEnabled.spec.js @@ -0,0 +1,131 @@ +var expect = require('chai').expect; +var blobVersioningEnabled = require('./blobVersioningEnabled'); + +const storageAccounts = [ + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Storage/storageAccounts/acc', + 'name': 'acc', + 'type': 'Microsoft.Storage/storageAccounts', + 'location': 'eastus' + } +]; + +const serviceProperties = [ + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Storage/storageAccounts/acc/blobServices/default', + 'isVersioningEnabled': true + }, + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Storage/storageAccounts/acc/blobServices/default', + 'isVersioningEnabled': false + }, + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Storage/storageAccounts/acc/blobServices/default' + } +]; + +const createCache = (storageAccounts, serviceProps, servicePropsErr) => { + const accountId = storageAccounts && storageAccounts.length ? storageAccounts[0].id : null; + const propsObj = {}; + if (accountId) { + if (servicePropsErr) { + propsObj[accountId] = { err: servicePropsErr }; + } else if (serviceProps) { + propsObj[accountId] = { data: serviceProps }; + } + } + return { + storageAccounts: { + list: { + 'eastus': { + data: storageAccounts + } + } + }, + blobServices: { + getServiceProperties: { + 'eastus': propsObj + } + } + }; +}; + +const createErrorCache = () => { + return { + storageAccounts: { + list: { + 'eastus': {} + } + } + }; +}; + +describe('blobVersioningEnabled', function () { + describe('run', function () { + it('should give passing result if no storage accounts found', function (done) { + const cache = createCache([], null); + blobVersioningEnabled.run(cache, {}, (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(); + }); + }); + + it('should give unknown result if unable to query for storage accounts', function (done) { + const cache = createErrorCache(); + blobVersioningEnabled.run(cache, {}, (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(); + }); + }); + + it('should give unknown result if unable to get blob service properties', function (done) { + const cache = createCache(storageAccounts, null, ['Forbidden']); + blobVersioningEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(3); + expect(results[0].message).to.include('Unable to get blob service properties'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if blob versioning is enabled', function (done) { + const cache = createCache(storageAccounts, serviceProperties[0]); + blobVersioningEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Blob versioning is enabled for Storage Account'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if blob versioning is disabled', function (done) { + const cache = createCache(storageAccounts, serviceProperties[1]); + blobVersioningEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Blob versioning is not enabled for Storage Account'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if versioning setting is not present', function (done) { + const cache = createCache(storageAccounts, serviceProperties[2]); + blobVersioningEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Blob versioning is not enabled for Storage Account'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); +}); diff --git a/plugins/azure/storageaccounts/containerSoftDeletionEnabled.js b/plugins/azure/storageaccounts/containerSoftDeletionEnabled.js new file mode 100644 index 0000000000..b939051eb5 --- /dev/null +++ b/plugins/azure/storageaccounts/containerSoftDeletionEnabled.js @@ -0,0 +1,86 @@ +const async = require('async'); +const helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'Container Soft Deletion Enabled', + category: 'Storage Accounts', + domain: 'Storage', + severity: 'Medium', + description: 'Ensures that soft delete feature is enabled for all Microsoft Storage Account containers.', + more_info: 'When soft delete for containers is enabled for a storage account, deleted containers may be recovered after they are deleted, within a retention period that you specify.', + recommended_action: 'Enable soft delete for containers and set deletion retention policy to keep containers for more than desired number of days', + link: 'https://learn.microsoft.com/en-us/azure/storage/blobs/soft-delete-container-overview', + apis: ['storageAccounts:list', 'blobServices:getServiceProperties'], + settings: { + keep_deleted_containers_for_days: { + name: 'Keep Deleted Containers for Days', + description: 'Number of days that a container is marked for deletion persists until it is permanently deleted', + regex: '^[1-9]{1}[0-9]{0,3}$', + default: '7' + } + }, + realtime_triggers: ['microsoftstorage:storageaccounts:write', 'microsoftstorage:storageaccounts:delete', 'microsoftstorage:storageaccounts:blobservices:write'], + + run: function(cache, settings, callback) { + const results = []; + const source = {}; + const locations = helpers.locations(settings.govcloud); + + const config = { + keepForDays: parseInt(settings.keep_deleted_containers_for_days || this.settings.keep_deleted_containers_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.containerDeleteRetentionPolicy && + getServiceProperties.data.containerDeleteRetentionPolicy.enabled && + getServiceProperties.data.containerDeleteRetentionPolicy.days) { + const retentionDays = getServiceProperties.data.containerDeleteRetentionPolicy.days; + + if (retentionDays >= config.keepForDays) { + helpers.addResult(results, 0, + `Containers deletion policy is configured to persist deleted containers for ${retentionDays} of ${config.keepForDays} days desired limit`, + location, storageAccount.id); + } else { + helpers.addResult(results, 2, + `Containers deletion policy is configured to persist deleted containers for ${retentionDays} of ${config.keepForDays} days desired limit`, + location, storageAccount.id); + } + } else { + helpers.addResult(results, 2, + 'Containers soft delete feature is not enabled for Storage Account', + location, storageAccount.id); + } + } + }); + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/storageaccounts/containerSoftDeletionEnabled.spec.js b/plugins/azure/storageaccounts/containerSoftDeletionEnabled.spec.js new file mode 100644 index 0000000000..83799f66a5 --- /dev/null +++ b/plugins/azure/storageaccounts/containerSoftDeletionEnabled.spec.js @@ -0,0 +1,154 @@ +var expect = require('chai').expect; +var containerSoftDeletionEnabled = require('./containerSoftDeletionEnabled'); + +const storageAccounts = [ + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Storage/storageAccounts/acc', + 'name': 'acc', + 'type': 'Microsoft.Storage/storageAccounts', + 'location': 'eastus' + } +]; + +const serviceProperties = [ + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Storage/storageAccounts/acc/blobServices/default', + 'containerDeleteRetentionPolicy': { + 'enabled': true, + 'days': 30 + } + }, + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Storage/storageAccounts/acc/blobServices/default', + 'containerDeleteRetentionPolicy': { + 'enabled': true, + 'days': 3 + } + }, + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Storage/storageAccounts/acc/blobServices/default', + 'containerDeleteRetentionPolicy': { + 'enabled': false + } + }, + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Storage/storageAccounts/acc/blobServices/default' + } +]; + +const createCache = (storageAccounts, serviceProps, servicePropsErr) => { + const accountId = storageAccounts && storageAccounts.length ? storageAccounts[0].id : null; + const propsObj = {}; + if (accountId) { + if (servicePropsErr) { + propsObj[accountId] = { err: servicePropsErr }; + } else if (serviceProps) { + propsObj[accountId] = { data: serviceProps }; + } + } + return { + storageAccounts: { + list: { + 'eastus': { + data: storageAccounts + } + } + }, + blobServices: { + getServiceProperties: { + 'eastus': propsObj + } + } + }; +}; + +const createErrorCache = () => { + return { + storageAccounts: { + list: { + 'eastus': {} + } + } + }; +}; + +describe('containerSoftDeletionEnabled', function () { + describe('run', function () { + it('should give passing result if no storage accounts found', function (done) { + const cache = createCache([], null); + containerSoftDeletionEnabled.run(cache, {}, (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(); + }); + }); + + it('should give unknown result if unable to query for storage accounts', function (done) { + const cache = createErrorCache(); + containerSoftDeletionEnabled.run(cache, {}, (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(); + }); + }); + + it('should give unknown result if unable to get blob service properties', function (done) { + const cache = createCache(storageAccounts, null, ['Forbidden']); + containerSoftDeletionEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(3); + expect(results[0].message).to.include('Unable to get blob service properties'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if retention days meet the desired limit', function (done) { + const cache = createCache(storageAccounts, serviceProperties[0]); + containerSoftDeletionEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('persist deleted containers for 30 of 7 days desired limit'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if retention days are less than the desired limit', function (done) { + const cache = createCache(storageAccounts, serviceProperties[1]); + containerSoftDeletionEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('persist deleted containers for 3 of 7 days desired limit'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if soft delete is disabled', function (done) { + const cache = createCache(storageAccounts, serviceProperties[2]); + containerSoftDeletionEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Containers soft delete feature is not enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if no container delete retention policy exists', function (done) { + const cache = createCache(storageAccounts, serviceProperties[3]); + containerSoftDeletionEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Containers soft delete feature is not enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); +}); diff --git a/plugins/azure/storageaccounts/crossTenantReplicationDisabled.js b/plugins/azure/storageaccounts/crossTenantReplicationDisabled.js new file mode 100644 index 0000000000..e267ddc322 --- /dev/null +++ b/plugins/azure/storageaccounts/crossTenantReplicationDisabled.js @@ -0,0 +1,57 @@ +const async = require('async'); +const helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'Storage Account Cross Tenant Replication Disabled', + category: 'Storage Accounts', + domain: 'Storage', + severity: 'Medium', + description: 'Ensures that cross tenant replication is not enabled for Microsoft Azure Storage Accounts.', + more_info: 'Cross tenant replication allows data to be replicated across different Azure tenant boundaries. Disabling this setting minimizes the risk of unauthorized data access, data leakage, and inadvertent replication of data outside the organization\'s tenant.', + recommended_action: 'Disable Allow cross-tenant replication from the object replication settings of the storage account.', + link: 'https://learn.microsoft.com/en-us/azure/storage/blobs/object-replication-overview', + apis: ['storageAccounts:list'], + realtime_triggers: ['microsoftstorage:storageaccounts:write', 'microsoftstorage:storageaccounts:delete'], + + run: function(cache, settings, callback) { + const results = []; + const source = {}; + const locations = helpers.locations(settings.govcloud); + + 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 => { + if (!storageAccount.id) return; + + if (!storageAccount.allowCrossTenantReplication) { + helpers.addResult(results, 0, + 'Storage Account has cross tenant replication disabled', + location, storageAccount.id); + } else { + helpers.addResult(results, 2, + 'Storage Account has cross tenant replication enabled', + location, storageAccount.id); + } + }); + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/storageaccounts/crossTenantReplicationDisabled.spec.js b/plugins/azure/storageaccounts/crossTenantReplicationDisabled.spec.js new file mode 100644 index 0000000000..40ee692cac --- /dev/null +++ b/plugins/azure/storageaccounts/crossTenantReplicationDisabled.spec.js @@ -0,0 +1,106 @@ +var expect = require('chai').expect; +var crossTenantReplicationDisabled = require('./crossTenantReplicationDisabled'); + +const storageAccounts = [ + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Storage/storageAccounts/acc', + 'name': 'acc', + 'type': 'Microsoft.Storage/storageAccounts', + 'location': 'eastus', + 'allowCrossTenantReplication': false + }, + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Storage/storageAccounts/acc', + 'name': 'acc', + 'type': 'Microsoft.Storage/storageAccounts', + 'location': 'eastus', + 'allowCrossTenantReplication': true + }, + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Storage/storageAccounts/acc', + 'name': 'acc', + 'type': 'Microsoft.Storage/storageAccounts', + 'location': 'eastus' + } +]; + +const createCache = (storageAccounts) => { + return { + storageAccounts: { + list: { + 'eastus': { + data: storageAccounts + } + } + } + }; +}; + +const createErrorCache = () => { + return { + storageAccounts: { + list: { + 'eastus': {} + } + } + }; +}; + +describe('crossTenantReplicationDisabled', function () { + describe('run', function () { + it('should give passing result if no storage accounts found', function (done) { + const cache = createCache([]); + crossTenantReplicationDisabled.run(cache, {}, (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(); + }); + }); + + it('should give unknown result if unable to query for storage accounts', function (done) { + const cache = createErrorCache(); + crossTenantReplicationDisabled.run(cache, {}, (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(); + }); + }); + + it('should give passing result if cross tenant replication is disabled', function (done) { + const cache = createCache([storageAccounts[0]]); + crossTenantReplicationDisabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('has cross tenant replication disabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if cross tenant replication is enabled', function (done) { + const cache = createCache([storageAccounts[1]]); + crossTenantReplicationDisabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('has cross tenant replication enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if allowCrossTenantReplication is not set', function (done) { + const cache = createCache([storageAccounts[2]]); + crossTenantReplicationDisabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('has cross tenant replication disabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); +}); diff --git a/plugins/azure/virtualnetworks/subnetNetworkSecurityGroup.js b/plugins/azure/virtualnetworks/subnetNetworkSecurityGroup.js new file mode 100644 index 0000000000..c988e20e07 --- /dev/null +++ b/plugins/azure/virtualnetworks/subnetNetworkSecurityGroup.js @@ -0,0 +1,68 @@ +var async = require('async'); +var helpers = require('../../../helpers/azure'); + +var exemptSubnets = ['gatewaysubnet', 'azurefirewallsubnet', 'azurefirewallmanagementsubnet', 'routeserversubnet']; + +module.exports = { + title: 'Subnet Network Security Group Association', + category: 'Virtual Networks', + domain: 'Network Access Control', + severity: 'Medium', + description: 'Ensures that virtual network subnets are associated with a network security group.', + more_info: 'Network security groups filter inbound and outbound traffic for a subnet using security rules. Subnets without an associated network security group do not have this filtering in place and can expose their resources to unauthorized access.', + recommended_action: 'Associate a network security group with each subnet from the subnet security settings.', + link: 'https://learn.microsoft.com/en-us/azure/virtual-network/network-security-groups-overview', + apis: ['virtualNetworks:listAll'], + realtime_triggers: ['microsoftnetwork:virtualnetworks:write', 'microsoftnetwork:virtualnetworks:delete', 'microsoftnetwork:virtualnetworks:subnets:write', 'microsoftnetwork:virtualnetworks:subnets:delete'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var locations = helpers.locations(settings.govcloud); + + async.each(locations.virtualNetworks, function(location, rcb) { + var virtualNetworks = helpers.addSource(cache, source, + ['virtualNetworks', 'listAll', location]); + + if (!virtualNetworks) return rcb(); + + if (virtualNetworks.err || !virtualNetworks.data) { + helpers.addResult(results, 3, 'Unable to query for Virtual Networks: ' + helpers.addError(virtualNetworks), location); + return rcb(); + } + + if (!virtualNetworks.data.length) { + helpers.addResult(results, 0, 'No existing Virtual Networks found', location); + return rcb(); + } + + var found = false; + + for (let virtualNetwork of virtualNetworks.data) { + if (!virtualNetwork.subnets || !virtualNetwork.subnets.length) continue; + + for (let subnet of virtualNetwork.subnets) { + if (!subnet.id || !subnet.properties) continue; + + if (subnet.name && exemptSubnets.includes(subnet.name.toLowerCase())) continue; + + found = true; + + if (subnet.properties.networkSecurityGroup && subnet.properties.networkSecurityGroup.id) { + helpers.addResult(results, 0, 'Subnet has a network security group associated', location, subnet.id); + } else { + helpers.addResult(results, 2, 'Subnet does not have a network security group associated', location, subnet.id); + } + } + } + + if (!found) { + helpers.addResult(results, 0, 'No existing subnets found', location); + } + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/virtualnetworks/subnetNetworkSecurityGroup.spec.js b/plugins/azure/virtualnetworks/subnetNetworkSecurityGroup.spec.js new file mode 100644 index 0000000000..2826e198de --- /dev/null +++ b/plugins/azure/virtualnetworks/subnetNetworkSecurityGroup.spec.js @@ -0,0 +1,153 @@ +var expect = require('chai').expect; +var subnetNetworkSecurityGroup = require('./subnetNetworkSecurityGroup'); + +const virtualNetworks = [ + { + 'name': 'test-vnet', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Network/virtualNetworks/test-vnet', + 'type': 'Microsoft.Network/virtualNetworks', + 'location': 'eastus', + 'subnets': [ + { + 'name': 'default', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/default', + 'properties': { + 'networkSecurityGroup': { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Network/networkSecurityGroups/test-nsg' + } + } + } + ] + }, + { + 'name': 'test-vnet', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Network/virtualNetworks/test-vnet', + 'type': 'Microsoft.Network/virtualNetworks', + 'location': 'eastus', + 'subnets': [ + { + 'name': 'default', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/default', + 'properties': {} + } + ] + }, + { + 'name': 'test-vnet', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Network/virtualNetworks/test-vnet', + 'type': 'Microsoft.Network/virtualNetworks', + 'location': 'eastus', + 'subnets': [ + { + 'name': 'GatewaySubnet', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/GatewaySubnet', + 'properties': {} + } + ] + }, + { + 'name': 'test-vnet', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Network/virtualNetworks/test-vnet', + 'type': 'Microsoft.Network/virtualNetworks', + 'location': 'eastus', + 'subnets': [ + { + 'name': 'AzureBastionSubnet', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/AzureBastionSubnet', + 'properties': {} + } + ] + } +]; + +const createCache = (virtualNetworks) => { + return { + virtualNetworks: { + listAll: { + 'eastus': { + data: virtualNetworks + } + } + } + }; +}; + +const createErrorCache = () => { + return { + virtualNetworks: { + listAll: { + 'eastus': {} + } + } + }; +}; + +describe('subnetNetworkSecurityGroup', function () { + describe('run', function () { + it('should give passing result if no virtual networks found', function (done) { + const cache = createCache([]); + subnetNetworkSecurityGroup.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('No existing Virtual Networks found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give unknown result if unable to query for virtual networks', function (done) { + const cache = createErrorCache(); + subnetNetworkSecurityGroup.run(cache, {}, (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 Virtual Networks'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if subnet has a network security group associated', function (done) { + const cache = createCache([virtualNetworks[0]]); + subnetNetworkSecurityGroup.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Subnet has a network security group associated'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if subnet does not have a network security group associated', function (done) { + const cache = createCache([virtualNetworks[1]]); + subnetNetworkSecurityGroup.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Subnet does not have a network security group associated'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should not evaluate subnets that do not support network security groups', function (done) { + const cache = createCache([virtualNetworks[2]]); + subnetNetworkSecurityGroup.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('No existing subnets found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if bastion subnet does not have a network security group associated', function (done) { + const cache = createCache([virtualNetworks[3]]); + subnetNetworkSecurityGroup.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Subnet does not have a network security group associated'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); +}); diff --git a/plugins/azure/virtualnetworks/vpnGatewayEntraIdAuth.js b/plugins/azure/virtualnetworks/vpnGatewayEntraIdAuth.js new file mode 100644 index 0000000000..19cd37b317 --- /dev/null +++ b/plugins/azure/virtualnetworks/vpnGatewayEntraIdAuth.js @@ -0,0 +1,75 @@ +var async = require('async'); +var helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'VPN Gateway Entra ID Authentication', + category: 'Virtual Networks', + domain: 'Network Access Control', + severity: 'Medium', + description: 'Ensures that VPN gateway point-to-site configuration uses Entra ID authentication only.', + more_info: 'Entra ID authentication provides centralized identity management and conditional access for point-to-site VPN users. Allowing certificate or RADIUS authentication relies on static credentials that are harder to rotate, revoke and audit.', + recommended_action: 'In the point-to-site configuration of the VPN gateway, set the authentication type to Entra ID only and remove Azure certificate and RADIUS authentication.', + link: 'https://learn.microsoft.com/en-us/azure/vpn-gateway/point-to-site-about', + apis: ['resourceGroups:list', 'virtualNetworkGateways:listByResourceGroup'], + realtime_triggers: ['microsoftnetwork:virtualnetworkgateways:write', 'microsoftnetwork:virtualnetworkgateways:delete'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var locations = helpers.locations(settings.govcloud); + + async.each(locations.virtualNetworkGateways, function(location, rcb) { + var resourceGroups = helpers.addSource(cache, source, + ['resourceGroups', 'list', location]); + + if (!resourceGroups) return rcb(); + + if (resourceGroups.err || !resourceGroups.data) { + helpers.addResult(results, 3, 'Unable to query for resource groups: ' + helpers.addError(resourceGroups), location); + return rcb(); + } + + if (!resourceGroups.data.length) { + helpers.addResult(results, 0, 'No existing resource groups found', location); + return rcb(); + } + + var found = false; + + resourceGroups.data.forEach(resourceGroup => { + var virtualNetworkGateways = helpers.addSource(cache, source, + ['virtualNetworkGateways', 'listByResourceGroup', location, resourceGroup.id]); + + if (!virtualNetworkGateways || virtualNetworkGateways.err || !virtualNetworkGateways.data) { + helpers.addResult(results, 3, 'Unable to query for virtual network gateways: ' + helpers.addError(virtualNetworkGateways), location, resourceGroup.id); + return; + } + + for (let gateway of virtualNetworkGateways.data) { + if (!gateway.gatewayType || gateway.gatewayType.toLowerCase() !== 'vpn') continue; + + found = true; + + var authTypes = gateway.vpnClientConfiguration && gateway.vpnClientConfiguration.vpnAuthenticationTypes ? + gateway.vpnClientConfiguration.vpnAuthenticationTypes : []; + + if (!authTypes.length) { + helpers.addResult(results, 0, 'VPN gateway does not have point-to-site configuration enabled', location, gateway.id); + } else if (authTypes.length === 1 && authTypes[0].toLowerCase() === 'aad') { + helpers.addResult(results, 0, 'VPN gateway point-to-site configuration is using Entra ID authentication only', location, gateway.id); + } else { + helpers.addResult(results, 2, 'VPN gateway point-to-site configuration is not using Entra ID authentication only', location, gateway.id); + } + } + }); + + if (!found) { + helpers.addResult(results, 0, 'No existing VPN gateways found', location); + } + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/virtualnetworks/vpnGatewayEntraIdAuth.spec.js b/plugins/azure/virtualnetworks/vpnGatewayEntraIdAuth.spec.js new file mode 100644 index 0000000000..f60cf78532 --- /dev/null +++ b/plugins/azure/virtualnetworks/vpnGatewayEntraIdAuth.spec.js @@ -0,0 +1,152 @@ +var expect = require('chai').expect; +var vpnGatewayEntraIdAuth = require('./vpnGatewayEntraIdAuth'); + +const resourceGroups = [ + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group', + 'name': 'aqua-resource-group', + 'type': 'Microsoft.Resources/resourceGroups', + 'location': 'eastus' + } +]; + +const virtualNetworkGateways = [ + { + 'name': 'test-gateway', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Network/virtualNetworkGateways/test-gateway', + 'type': 'Microsoft.Network/virtualNetworkGateways', + 'gatewayType': 'Vpn', + 'vpnClientConfiguration': { + 'vpnAuthenticationTypes': ['AAD'] + } + }, + { + 'name': 'test-gateway', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Network/virtualNetworkGateways/test-gateway', + 'type': 'Microsoft.Network/virtualNetworkGateways', + 'gatewayType': 'Vpn', + 'vpnClientConfiguration': { + 'vpnAuthenticationTypes': ['AAD', 'Certificate'] + } + }, + { + 'name': 'test-gateway', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Network/virtualNetworkGateways/test-gateway', + 'type': 'Microsoft.Network/virtualNetworkGateways', + 'gatewayType': 'Vpn' + }, + { + 'name': 'test-gateway', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Network/virtualNetworkGateways/test-gateway', + 'type': 'Microsoft.Network/virtualNetworkGateways', + 'gatewayType': 'ExpressRoute' + } +]; + +const createCache = (resourceGroups, virtualNetworkGateways) => { + let groups = {}; + let gateways = {}; + + if (resourceGroups) { + groups['data'] = resourceGroups; + if (resourceGroups.length && virtualNetworkGateways) { + gateways[resourceGroups[0].id] = { + 'data': virtualNetworkGateways + }; + } + } + + return { + resourceGroups: { + list: { + 'eastus': groups + } + }, + virtualNetworkGateways: { + listByResourceGroup: { + 'eastus': gateways + } + } + }; +}; + +describe('vpnGatewayEntraIdAuth', function () { + describe('run', function () { + it('should give passing result if no resource groups found', function (done) { + const cache = createCache([], null); + vpnGatewayEntraIdAuth.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('No existing resource groups found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give unknown result if unable to query for resource groups', function (done) { + const cache = createCache(null, null); + vpnGatewayEntraIdAuth.run(cache, {}, (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 resource groups'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if no VPN gateways found', function (done) { + const cache = createCache(resourceGroups, []); + vpnGatewayEntraIdAuth.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('No existing VPN gateways found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if VPN gateway uses Entra ID authentication only', function (done) { + const cache = createCache(resourceGroups, [virtualNetworkGateways[0]]); + vpnGatewayEntraIdAuth.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('VPN gateway point-to-site configuration is using Entra ID authentication only'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if VPN gateway uses more than one authentication type', function (done) { + const cache = createCache(resourceGroups, [virtualNetworkGateways[1]]); + vpnGatewayEntraIdAuth.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('VPN gateway point-to-site configuration is not using Entra ID authentication only'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if VPN gateway does not have point-to-site configuration', function (done) { + const cache = createCache(resourceGroups, [virtualNetworkGateways[2]]); + vpnGatewayEntraIdAuth.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('VPN gateway does not have point-to-site configuration enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if gateway is not a VPN gateway', function (done) { + const cache = createCache(resourceGroups, [virtualNetworkGateways[3]]); + vpnGatewayEntraIdAuth.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('No existing VPN gateways found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); +}); diff --git a/plugins/azure/waf/wafPolicyBotProtection.js b/plugins/azure/waf/wafPolicyBotProtection.js new file mode 100644 index 0000000000..f053fa629b --- /dev/null +++ b/plugins/azure/waf/wafPolicyBotProtection.js @@ -0,0 +1,66 @@ +const async = require('async'); +const helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'WAF Policy Bot Protection', + category: 'Application Gateway', + domain: 'Network Access Control', + severity: 'Low', + description: 'Ensure that Bot Protection for Azure Application Gateway WAF policy is enabled.', + more_info: 'Azure Web Application Firewall (WAF) for Application Gateway provides bot rules to block or log requests from known malicious IP addresses identified through the Microsoft Threat Intelligence feed. Enabling the bot manager rule set reduces exposure to automated attacks that scrape, scan and search for application vulnerabilities.', + recommended_action: 'Modify Application Gateway WAF policy and add the bot manager rule set in managed rules.', + link: 'https://learn.microsoft.com/en-us/azure/web-application-firewall/ag/bot-protection', + apis: ['wafPolicies:listAll'], + realtime_triggers: ['microsoftnetwork:applicationgatewaywebapplicationfirewallpolicies:write', 'microsoftnetwork:applicationgatewaywebapplicationfirewallpolicies:delete'], + + run: function(cache, settings, callback) { + const results = []; + const source = {}; + const locations = helpers.locations(settings.govcloud); + + async.each(locations.wafPolicies, (location, rcb) => { + + var wafPolicies = helpers.addSource(cache, source, + ['wafPolicies', 'listAll', location]); + + if (!wafPolicies) return rcb(); + + if (wafPolicies.err || !wafPolicies.data) { + helpers.addResult(results, 3, 'Unable to query for WAF policies: ' + helpers.addError(wafPolicies), location); + return rcb(); + } + + if (!wafPolicies.data.length) { + helpers.addResult(results, 0, 'No existing WAF policies found', location); + return rcb(); + } + + for (let policy of wafPolicies.data) { + if (!policy.id) continue; + + var botRuleSet = policy.managedRules && policy.managedRules.managedRuleSets ? + policy.managedRules.managedRuleSets.find(ruleSet => ruleSet.ruleSetType && + ruleSet.ruleSetType.toLowerCase() == 'microsoft_botmanagerruleset') : null; + + if (!botRuleSet) { + helpers.addResult(results, 2, 'WAF policy does not have bot protection enabled', location, policy.id); + continue; + } + + var badBotsDisabled = (botRuleSet.ruleGroupOverrides || []).some(override => override.ruleGroupName && + override.ruleGroupName.toLowerCase().indexOf('badbots') > -1 && + override.rules && override.rules.some(rule => rule.state && rule.state.toLowerCase() == 'disabled')); + + if (badBotsDisabled) { + helpers.addResult(results, 2, 'WAF policy has malicious bot rules disabled', location, policy.id); + } else { + helpers.addResult(results, 0, 'WAF policy has bot protection enabled', location, policy.id); + } + } + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/waf/wafPolicyBotProtection.spec.js b/plugins/azure/waf/wafPolicyBotProtection.spec.js new file mode 100644 index 0000000000..2a4fe67bb6 --- /dev/null +++ b/plugins/azure/waf/wafPolicyBotProtection.spec.js @@ -0,0 +1,160 @@ +var expect = require('chai').expect; +var wafPolicyBotProtection = require('./wafPolicyBotProtection'); + +const wafPolicies = [ + { + 'name': 'test-policy', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies/test-policy', + 'type': 'Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies', + 'location': 'eastus', + 'managedRules': { + 'managedRuleSets': [ + { + 'ruleSetType': 'OWASP', + 'ruleSetVersion': '3.2' + }, + { + 'ruleSetType': 'Microsoft_BotManagerRuleSet', + 'ruleSetVersion': '1.0' + } + ] + } + }, + { + 'name': 'test-policy', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies/test-policy', + 'type': 'Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies', + 'location': 'eastus', + 'managedRules': { + 'managedRuleSets': [ + { + 'ruleSetType': 'OWASP', + 'ruleSetVersion': '3.2' + } + ] + } + }, + { + 'name': 'test-policy', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies/test-policy', + 'type': 'Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies', + 'location': 'eastus', + 'managedRules': { + 'managedRuleSets': [ + { + 'ruleSetType': 'Microsoft_BotManagerRuleSet', + 'ruleSetVersion': '1.0', + 'ruleGroupOverrides': [ + { + 'ruleGroupName': 'BadBots', + 'rules': [ + { + 'ruleId': 'Bot100100', + 'state': 'Disabled' + } + ] + } + ] + } + ] + } + }, + { + 'name': 'test-policy', + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies/test-policy', + 'type': 'Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies', + 'location': 'eastus' + } +]; + +const createCache = (wafPolicies) => { + return { + wafPolicies: { + listAll: { + 'eastus': { + data: wafPolicies + } + } + } + }; +}; + +const createErrorCache = () => { + return { + wafPolicies: { + listAll: { + 'eastus': {} + } + } + }; +}; + +describe('wafPolicyBotProtection', function () { + describe('run', function () { + it('should give passing result if no WAF policies found', function (done) { + const cache = createCache([]); + wafPolicyBotProtection.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('No existing WAF policies found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give unknown result if unable to query for WAF policies', function (done) { + const cache = createErrorCache(); + wafPolicyBotProtection.run(cache, {}, (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 WAF policies'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if WAF policy has bot protection enabled', function (done) { + const cache = createCache([wafPolicies[0]]); + wafPolicyBotProtection.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('WAF policy has bot protection enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if WAF policy does not have bot manager rule set', function (done) { + const cache = createCache([wafPolicies[1]]); + wafPolicyBotProtection.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('WAF policy does not have bot protection enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if WAF policy has malicious bot rules disabled', function (done) { + const cache = createCache([wafPolicies[2]]); + wafPolicyBotProtection.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('WAF policy has malicious bot rules disabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if WAF policy does not have managed rules', function (done) { + const cache = createCache([wafPolicies[3]]); + wafPolicyBotProtection.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('WAF policy does not have bot protection enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); +});