diff --git a/helpers/azure/api.js b/helpers/azure/api.js index d47e23c9ea..97c051f9cb 100644 --- a/helpers/azure/api.js +++ b/helpers/azure/api.js @@ -627,7 +627,7 @@ var calls = { }, serviceBus: { listNamespacesBySubscription: { - url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.ServiceBus/namespaces?api-version=2022-10-01-preview' + url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.ServiceBus/namespaces?api-version=2025-05-01-preview' } }, mediaServices:{ @@ -1270,6 +1270,13 @@ var postcalls = { properties: ['id'], url: 'https://management.azure.com/{id}/networkRuleSets/default?api-version=2022-10-01-preview' } + }, + serviceBus: { + getNamespaceNetworkRuleSet: { + reliesOnPath: 'serviceBus.listNamespacesBySubscription', + properties: ['id'], + url: 'https://management.azure.com/{id}/networkRuleSets/default?api-version=2021-11-01' + } } }; diff --git a/helpers/azure/functions.js b/helpers/azure/functions.js index d52565a212..f7216a027d 100644 --- a/helpers/azure/functions.js +++ b/helpers/azure/functions.js @@ -855,6 +855,16 @@ function checkNetworkExposure(cache, source, networkInterfaces, securityGroups, return exposedPath; } + +function isOpenCidrRange(cidr) { + if (!cidr || typeof cidr !== 'string') return false; + + const trimmed = cidr.trim(); + return trimmed === '0.0.0.0/0' || + trimmed === '::/0' || + trimmed === '0.0.0.0'; +} + module.exports = { addResult: addResult, findOpenPorts: findOpenPorts, @@ -868,7 +878,7 @@ module.exports = { remediateOpenPortsHelper: remediateOpenPortsHelper, checkMicrosoftDefender: checkMicrosoftDefender, checkFlexibleServerConfigs:checkFlexibleServerConfigs, - checkNetworkExposure: checkNetworkExposure - + checkNetworkExposure: checkNetworkExposure, + isOpenCidrRange: isOpenCidrRange }; diff --git a/plugins/azure/appConfigurations/appConfigurationPublicAccess.js b/plugins/azure/appConfigurations/appConfigurationPublicAccess.js index 57a7080ec3..655b1822ae 100644 --- a/plugins/azure/appConfigurations/appConfigurationPublicAccess.js +++ b/plugins/azure/appConfigurations/appConfigurationPublicAccess.js @@ -36,8 +36,9 @@ module.exports = { for (let appConfiguration of appConfigurations.data) { if (!appConfiguration.id) continue; - - if (appConfiguration.publicNetworkAccess && appConfiguration.publicNetworkAccess.toLowerCase() === 'disabled') { + const hasPrivateEndpoint = appConfiguration.privateEndpointConnections + && appConfiguration.privateEndpointConnections.length > 0; + if ((appConfiguration.publicNetworkAccess && appConfiguration.publicNetworkAccess.toLowerCase() === 'disabled') || hasPrivateEndpoint) { helpers.addResult(results, 0, 'App Configuration has public network access disabled', location, appConfiguration.id); } else { helpers.addResult(results, 2, 'App Configuration does not have public network access disabled', location, appConfiguration.id); diff --git a/plugins/azure/appConfigurations/appConfigurationPublicAccess.spec.js b/plugins/azure/appConfigurations/appConfigurationPublicAccess.spec.js index 7503b92375..42e710903c 100644 --- a/plugins/azure/appConfigurations/appConfigurationPublicAccess.spec.js +++ b/plugins/azure/appConfigurations/appConfigurationPublicAccess.spec.js @@ -48,6 +48,40 @@ const appConfigurations = [ } } } + }, + { + "type": "Microsoft.AppConfiguration/configurationStores", + "location": "eastus", + "provisioningState": "Succeeded", + "creationDate": "2023-12-27T09:26:54+00:00", + "endpoint": "https://dummy-test-rg.azconfig.io", + "encryption": { + "keyVaultProperties": null + }, + "privateEndpointConnections": [ + { + "id": "/subscriptions/123/resourceGroups/dummy-rg/providers/Microsoft.AppConfiguration/configurationStores/dummy-test-rg/privateEndpointConnections/dummyConnection", + "name": "dummyConnection", + "type": "Microsoft.AppConfiguration/configurationStores/privateEndpointConnections", + "properties": { + "provisioningState": "Succeeded", + "privateEndpoint": { + "id": "/subscriptions/123/resourceGroups/dummy-rg/providers/Microsoft.Network/privateEndpoints/dummyEndpoint" + }, + "privateLinkServiceConnectionState": { + "status": "Approved", + "description": "Auto approved" + } + } + } + ], + "publicNetworkAccess": "Enabled", + "disableLocalAuth": false, + "softDeleteRetentionInDays": 0, + "enablePurgeProtection": false, + "id": "/subscriptions/123/resourceGroups/dummy-rg/providers/Microsoft.AppConfiguration/configurationStores/dummy-test-rg-private", + "name": "dummy-test-rg-private", + "tags": {} } ]; @@ -110,5 +144,16 @@ describe('appConfigurationPublicAccess', function () { done(); }); }); + + it('should give passing result if App Configuration has private endpoint connections', function (done) { + const cache = createCache([appConfigurations[2]]); + appConfigurationPublicAccess.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('App Configuration has public network access disabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); }); }); \ No newline at end of file diff --git a/plugins/azure/appservice/appServicePublicAccess.js b/plugins/azure/appservice/appServicePublicAccess.js index 246ca91c29..cc827f4849 100644 --- a/plugins/azure/appservice/appServicePublicAccess.js +++ b/plugins/azure/appservice/appServicePublicAccess.js @@ -50,14 +50,38 @@ module.exports = { var config = webConfigs.data[0]; - if (config.publicNetworkAccess && config.publicNetworkAccess.toLowerCase() === 'disabled') { + if (config.publicNetworkAccess && + config.publicNetworkAccess.toLowerCase() === 'disabled') { + helpers.addResult(results, 0, 'App Service has public network access disabled', location, webApp.id); + return; } else { - helpers.addResult(results, 2, - 'App Service does not have public network access disabled', - location, webApp.id); + let hasOpenCidr = false; + + if (config.ipSecurityRestrictions && config.ipSecurityRestrictions.length) { + for (let rule of config.ipSecurityRestrictions) { + if (helpers.isOpenCidrRange(rule.ipAddress)) { + hasOpenCidr = true; + break; + } + } + } + + const restricted = + config.ipSecurityRestrictionsDefaultAction && + config.ipSecurityRestrictionsDefaultAction.toLowerCase() === 'deny' && !hasOpenCidr; + + if (restricted) { + helpers.addResult(results, 0, + 'App Service has public network access disabled', + location, webApp.id); + } else { + helpers.addResult(results, 2, + 'App Service does not have public network access disabled', + location, webApp.id); + } } }); diff --git a/plugins/azure/appservice/appServicePublicAccess.spec.js b/plugins/azure/appservice/appServicePublicAccess.spec.js index cd732733d9..d00f2d9583 100644 --- a/plugins/azure/appservice/appServicePublicAccess.spec.js +++ b/plugins/azure/appservice/appServicePublicAccess.spec.js @@ -39,6 +39,32 @@ const listConfigurations = [ { 'id': '/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app-3/config/web', 'name': 'web' + }, + { + 'id': '/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app-4/config/web', + 'name': 'web', + 'publicNetworkAccess': 'Enabled', + 'ipSecurityRestrictionsDefaultAction': 'Deny', + 'ipSecurityRestrictions': [ + { + 'ipAddress': '192.168.1.0/24', + 'action': 'Allow', + 'name': 'AllowInternal' + } + ] + }, + { + 'id': '/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app-5/config/web', + 'name': 'web', + 'publicNetworkAccess': 'Enabled', + 'ipSecurityRestrictionsDefaultAction': 'Deny', + 'ipSecurityRestrictions': [ + { + 'ipAddress': '0.0.0.0/0', + 'action': 'Allow', + 'name': 'AllowAll' + } + ] } ]; @@ -147,5 +173,27 @@ describe('appServicePublicAccess', function () { done(); }); }); + + it('should give passing result if App Service has restricted IP security with no open CIDR', function (done) { + const cache = createCache([webApps[0]], [listConfigurations[3]]); + appServicePublicAccess.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('App Service has public network access disabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if App Service has open CIDR range in IP security restrictions', function (done) { + const cache = createCache([webApps[1]], [listConfigurations[4]]); + appServicePublicAccess.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('App Service does not have public network access disabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); }); }); \ No newline at end of file diff --git a/plugins/azure/automationAccounts/automationAcctPublicAccess.js b/plugins/azure/automationAccounts/automationAcctPublicAccess.js index 47e9d730f5..e47047e966 100644 --- a/plugins/azure/automationAccounts/automationAcctPublicAccess.js +++ b/plugins/azure/automationAccounts/automationAcctPublicAccess.js @@ -47,7 +47,12 @@ module.exports = { } if (Object.prototype.hasOwnProperty.call(describeAcct.data, 'publicNetworkAccess')) { - if (describeAcct.data.publicNetworkAccess) { + const hasActivePrivateEndpoint = describeAcct.data.privateEndpointConnections && + describeAcct.data.privateEndpointConnections.length > 0 && + describeAcct.data.privateEndpointConnections.some(conn => + conn.properties && conn.properties.privateLinkServiceConnectionState && conn.properties.privateLinkServiceConnectionState.status === 'Approved' + ); + if (describeAcct.data.publicNetworkAccess && !hasActivePrivateEndpoint) { helpers.addResult(results, 2, 'Automation account does not have public network access disabled', location, account.id); } else { helpers.addResult(results, 0, 'Automation account has public network access disabled', location, account.id); diff --git a/plugins/azure/automationAccounts/automationAcctPublicAccess.spec.js b/plugins/azure/automationAccounts/automationAcctPublicAccess.spec.js index 5d4ce4e11e..2ae8b11990 100644 --- a/plugins/azure/automationAccounts/automationAcctPublicAccess.spec.js +++ b/plugins/azure/automationAccounts/automationAcctPublicAccess.spec.js @@ -36,6 +36,26 @@ const account = [ "type": "Microsoft.Automation/AutomationAccounts", "tags": {}, "publicNetworkAccess": true, + }, + { + "id": "/subscriptions/12345/resourceGroups/DefaultResourceGroup-WUS/providers/Microsoft.Automation/automationAccounts/Automate-12345-WUS", + "location": "westus", + "name": "Automate-12345-WUS", + "type": "Microsoft.Automation/AutomationAccounts", + "tags": {}, + "publicNetworkAccess": true, + "privateEndpointConnections": [ + { + "id": "/subscriptions/12345/resourceGroups/DefaultResourceGroup-WUS/providers/Microsoft.Automation/automationAccounts/Automate-12345-WUS/privateEndpointConnections/pe-conn-1", + "name": "pe-conn-1", + "properties": { + "privateLinkServiceConnectionState": { + "status": "Approved", + "description": "Auto-approved" + } + } + } + ] } ]; @@ -110,5 +130,16 @@ describe('automationAcctPublicAccess', function () { done(); }); }); + + it('should give passing result if automation account has public network access enabled but with active private endpoint', function (done) { + const cache = createCache(automationAccounts, account[2]); + automationAcctPublicAccess.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Automation account has public network access disabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); }); }); \ No newline at end of file diff --git a/plugins/azure/batchAccounts/batchAccountsPublicAccess.js b/plugins/azure/batchAccounts/batchAccountsPublicAccess.js index 505ed92048..e1beac8c4b 100644 --- a/plugins/azure/batchAccounts/batchAccountsPublicAccess.js +++ b/plugins/azure/batchAccounts/batchAccountsPublicAccess.js @@ -36,9 +36,14 @@ module.exports = { for (let batchAccount of batchAccounts.data) { if (!batchAccount.id) continue; - - if (batchAccount.publicNetworkAccess && - batchAccount.publicNetworkAccess.toLowerCase() === 'enabled') { + + const hasActivePrivateEndpoint = batchAccount.privateEndpointConnections && + batchAccount.privateEndpointConnections.length > 0 && + batchAccount.privateEndpointConnections.some(conn => + conn.properties && conn.properties.privateLinkServiceConnectionState && conn.properties.privateLinkServiceConnectionState.status === 'Approved' + ); + if ((batchAccount.publicNetworkAccess && + batchAccount.publicNetworkAccess.toLowerCase() === 'enabled') && !hasActivePrivateEndpoint) { helpers.addResult(results, 2, 'Batch account is publicly accessible', location, batchAccount.id); } else { helpers.addResult(results, 0, 'Batch account is not publicly accessible', location, batchAccount.id); diff --git a/plugins/azure/batchAccounts/batchAccountsPublicAccess.spec.js b/plugins/azure/batchAccounts/batchAccountsPublicAccess.spec.js index ca54e124fe..7274c013ed 100644 --- a/plugins/azure/batchAccounts/batchAccountsPublicAccess.spec.js +++ b/plugins/azure/batchAccounts/batchAccountsPublicAccess.spec.js @@ -21,6 +21,27 @@ const batchAccounts = [ "nodeManagementEndpoint": "123456789.eastus.service.batch.azure.com", "publicNetworkAccess": "Enabled" }, + { + "id": "/subscriptions/1234566/resourceGroups/dummy/providers/Microsoft.Batch/batchAccounts/test-private", + "name": "test-private", + "type": "Microsoft.Batch/batchAccounts", + "location": "eastus", + "accountEndpoint": "test-private.eastus.batch.azure.com", + "nodeManagementEndpoint": "123456789.eastus.service.batch.azure.com", + "publicNetworkAccess": "Enabled", + "privateEndpointConnections": [ + { + "id": "/subscriptions/1234566/resourceGroups/dummy/providers/Microsoft.Batch/batchAccounts/test-private/privateEndpointConnections/connection1", + "name": "connection1", + "type": "Microsoft.Batch/batchAccounts/privateEndpointConnections", + "properties": { + "privateLinkServiceConnectionState": { + "status": "Approved" + } + } + } + ] + }, ]; const createCache = (batchAccounts) => { @@ -91,5 +112,16 @@ describe('batchAccountsPublicAccess', function () { done(); }); }); + + it('should give passing result if Batch account is publicly accessible but has active private endpoint', function (done) { + const cache = createCache([batchAccounts[2]]); + batchAccountsPublicAccess.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Batch account is not publicly accessible'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); }); }); \ No newline at end of file diff --git a/plugins/azure/containerregistry/acrPublicAccess.js b/plugins/azure/containerregistry/acrPublicAccess.js index 61da38f39b..1ab9f3e8a0 100644 --- a/plugins/azure/containerregistry/acrPublicAccess.js +++ b/plugins/azure/containerregistry/acrPublicAccess.js @@ -38,7 +38,14 @@ module.exports = { for (let registry of registries.data){ if (!registry.id) continue; - if (registry.publicNetworkAccess && registry.publicNetworkAccess.toLowerCase() === 'enabled'){ + const hasPrivateEndpoint = registry.privateEndpointConnections && + registry.privateEndpointConnections.length > 0; + + const hasSelectedNetworks = registry.networkRuleSet && + registry.networkRuleSet.defaultAction && + registry.networkRuleSet.defaultAction.toLowerCase() === 'deny'; + + if ((!registry.publicNetworkAccess || registry.publicNetworkAccess.toLowerCase() !== 'disabled') && !hasPrivateEndpoint && !hasSelectedNetworks) { helpers.addResult(results, 2, 'Container registry is publicly accessible', location, registry.id); } else { helpers.addResult(results, 0, 'Container registry is not publicly accessible', location, registry.id); diff --git a/plugins/azure/containerregistry/acrPublicAccess.spec.js b/plugins/azure/containerregistry/acrPublicAccess.spec.js index 37e42f6954..afaa36ec48 100644 --- a/plugins/azure/containerregistry/acrPublicAccess.spec.js +++ b/plugins/azure/containerregistry/acrPublicAccess.spec.js @@ -99,6 +99,102 @@ describe('acrPublicAccess', function() { ] ); + acrPublicAccess.run(cache, {}, callback); + }); + + it('should give passing result if selected networks are configured', function(done) { + const callback = (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Container registry is not publicly accessible'); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache( + null, + [ + { + "id": "/subscriptions/ade0e01e-f9cd-49d3-bba7-d5a5362a3414/resourceGroups/devresourcegroup/providers/Microsoft.ContainerRegistry/registries/testregistry12543", + "name": "testregistry12543", + "type": "Microsoft.ContainerRegistry/registries", + "location": "eastus", + "publicNetworkAccess": "Enabled", + "networkRuleSet": { + "defaultAction": "Deny", + "ipRules": [ + { + "action": "Allow", + "ipAddressOrCidr": "103.177.240.106" + } + ] + } + } + ] + ); + + acrPublicAccess.run(cache, {}, callback); + }); + + it('should give failing result if all networks are selected', function(done) { + const callback = (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Container registry is publicly accessible'); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache( + null, + [ + { + "id": "/subscriptions/ade0e01e-f9cd-49d3-bba7-d5a5362a3414/resourceGroups/devresourcegroup/providers/Microsoft.ContainerRegistry/registries/testregistry12543", + "name": "testregistry12543", + "type": "Microsoft.ContainerRegistry/registries", + "location": "eastus", + "publicNetworkAccess": "Enabled", + "networkRuleSet": { + "defaultAction": "Allow" + } + } + ] + ); + + acrPublicAccess.run(cache, {}, callback); + }); + + it('should give passing result if private endpoint is approved', function(done) { + const callback = (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Container registry is not publicly accessible'); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache( + null, + [ + { + "id": "/subscriptions/ade0e01e-f9cd-49d3-bba7-d5a5362a3414/resourceGroups/devresourcegroup/providers/Microsoft.ContainerRegistry/registries/testregistry12543", + "name": "testregistry12543", + "type": "Microsoft.ContainerRegistry/registries", + "location": "eastus", + "publicNetworkAccess": "Enabled", + "privateEndpointConnections": [ + { + "properties": { + "privateLinkServiceConnectionState": { + "status": "Approved" + } + } + } + ] + } + ] + ); + acrPublicAccess.run(cache, {}, callback); }) }) diff --git a/plugins/azure/eventGrid/domainPublicAccess.js b/plugins/azure/eventGrid/domainPublicAccess.js index 3db87c8cc0..c0ab36ea37 100644 --- a/plugins/azure/eventGrid/domainPublicAccess.js +++ b/plugins/azure/eventGrid/domainPublicAccess.js @@ -38,10 +38,38 @@ module.exports = { for (let domain of domains.data) { if (!domain.id) continue; - if (domain.publicNetworkAccess && domain.publicNetworkAccess.toLowerCase() === 'enabled') { - helpers.addResult(results, 2, 'Event Grid domain has public network access enabled', location, domain.id); - } else { + if (domain.publicNetworkAccess && domain.publicNetworkAccess.toLowerCase() === 'disabled') { helpers.addResult(results, 0, 'Event Grid domain does not have public network access enabled', location, domain.id); + } else { + const hasPrivateEndpoint = domain.privateEndpointConnections && + domain.privateEndpointConnections.length > 0 && + domain.privateEndpointConnections.some(conn => + conn.properties && conn.properties.privateLinkServiceConnectionState && conn.properties.privateLinkServiceConnectionState.status === 'Approved' + ); + + if (hasPrivateEndpoint) { + helpers.addResult(results, 0, 'Event Grid domain does not have public network access enabled', location, domain.id); + } else { + let hasOpenCidr = false; + + if (domain.inboundIpRules && domain.inboundIpRules.length) { + for (let rule of domain.inboundIpRules) { + if (helpers.isOpenCidrRange(rule.ipMask)) { + hasOpenCidr = true; + break; + } + } + } + + const restricted = domain.inboundIpRules && + domain.inboundIpRules.length > 0 && !hasOpenCidr; + + if (restricted) { + helpers.addResult(results, 0, 'Event Grid domain does not have public network access enabled', location, domain.id); + } else { + helpers.addResult(results, 2, 'Event Grid domain has public network access enabled', location, domain.id); + } + } } } rcb(); diff --git a/plugins/azure/eventGrid/domainPublicAccess.spec.js b/plugins/azure/eventGrid/domainPublicAccess.spec.js index fcd6be09dd..e8086c6eb2 100644 --- a/plugins/azure/eventGrid/domainPublicAccess.spec.js +++ b/plugins/azure/eventGrid/domainPublicAccess.spec.js @@ -21,6 +21,40 @@ const domains = [ "location": "westus2", "name": "exampledomain1", "publicNetworkAccess": "Disabled" + }, + { + "id": "/subscriptions/8f6b6269-84f2-4d09-9e31-1127efcd1e40/resourceGroups/examplerg/providers/Microsoft.EventGrid/domains/exampledomain1", + "location": "westus2", + "name": "exampledomain1", + "publicNetworkAccess": "Enabled", + "privateEndpointConnections": [ + { + "properties": { + "privateLinkServiceConnectionState": { + "status": "Approved" + } + } + } + ] + }, + { + "id": "/subscriptions/8f6b6269-84f2-4d09-9e31-1127efcd1e40/resourceGroups/examplerg/providers/Microsoft.EventGrid/domains/exampledomain1", + "location": "westus2", + "name": "exampledomain1", + "publicNetworkAccess": "Enabled", + "inboundIpRules": [ + { "ipMask": "12.0.0.0", "action": "Allow" } + ] + }, + { + "id": "/subscriptions/8f6b6269-84f2-4d09-9e31-1127efcd1e40/resourceGroups/examplerg/providers/Microsoft.EventGrid/domains/exampledomain1", + "location": "westus2", + "name": "exampledomain1", + "publicNetworkAccess": "Enabled", + "inboundIpRules": [ + { "ipMask": "12.0.0.0", "action": "Allow" }, + { "ipMask": "0.0.0.0", "action": "Allow" } + ] } ] const createCache = (data) => { @@ -80,5 +114,38 @@ describe("domainPublicAccess", function () { done(); }); }); + + it("should give passing result if public access enabled but private endpoint is approved", function (done) { + const cache = createCache([domains[2]]); + domainPublicAccess.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include("Event Grid domain does not have public network access enabled"); + expect(results[0].region).to.equal("eastus"); + done(); + }); + }); + + it("should give passing result if public access enabled but inbound IP rules are restricted", function (done) { + const cache = createCache([domains[3]]); + domainPublicAccess.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include("Event Grid domain does not have public network access enabled"); + expect(results[0].region).to.equal("eastus"); + done(); + }); + }); + + it("should give failing result if public access enabled and inbound IP rules contain open CIDR", function (done) { + const cache = createCache([domains[4]]); + domainPublicAccess.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include("Event Grid domain has public network access enabled"); + expect(results[0].region).to.equal("eastus"); + done(); + }); + }); }); }); diff --git a/plugins/azure/machinelearning/mlRegistryPublicAccess.js b/plugins/azure/machinelearning/mlRegistryPublicAccess.js index db08920738..ad6409cfac 100644 --- a/plugins/azure/machinelearning/mlRegistryPublicAccess.js +++ b/plugins/azure/machinelearning/mlRegistryPublicAccess.js @@ -38,7 +38,17 @@ module.exports = { for (let registry of machineLearningRegistries.data) { if (!registry.id) continue; - if (registry.publicNetworkAccess && registry.publicNetworkAccess.toLowerCase()=='disabled') { + const hasPrivateEndpoint = registry.privateEndpointConnections && + registry.privateEndpointConnections.length > 0; + + const hasSelectedNetworks = registry.networkRuleSet && + registry.networkRuleSet.defaultAction && + registry.networkRuleSet.defaultAction.toLowerCase() === 'deny'; + + if (registry.publicNetworkAccess && registry.publicNetworkAccess.toLowerCase() === 'disabled') { + helpers.addResult(results, 0, + 'Machine Learning registry has public network access disabled', location, registry.id); + } else if (hasPrivateEndpoint || hasSelectedNetworks) { helpers.addResult(results, 0, 'Machine Learning registry has public network access disabled', location, registry.id); } else { diff --git a/plugins/azure/machinelearning/mlRegistryPublicAccess.spec.js b/plugins/azure/machinelearning/mlRegistryPublicAccess.spec.js index a3c21ad047..c35ce9e036 100644 --- a/plugins/azure/machinelearning/mlRegistryPublicAccess.spec.js +++ b/plugins/azure/machinelearning/mlRegistryPublicAccess.spec.js @@ -18,7 +18,30 @@ const registry = [ "type": "Microsoft.MachineLearningServices/registries", "publicNetworkAccess" : "Enabled" }, - + { + "id": "/subscriptions/12345667/resourceGroups/test/providers/Microsoft.MachineLearningServices/registries/test1", + "name": "test", + "type": "Microsoft.MachineLearningServices/registries", + "publicNetworkAccess": "Enabled", + "privateEndpointConnections": [ + { + "properties": { + "privateLinkServiceConnectionState": { + "status": "Approved" + } + } + } + ] + }, + { + "id": "/subscriptions/12345667/resourceGroups/test/providers/Microsoft.MachineLearningServices/registries/test1", + "name": "test", + "type": "Microsoft.MachineLearningServices/registries", + "publicNetworkAccess": "Enabled", + "networkRuleSet": { + "defaultAction": "Deny" + } + } ]; const createCache = (registries) => { @@ -90,5 +113,27 @@ describe('mlRegistryPublicAccess', function() { }); }); + it('should give passing result if Machine Learning registry has public access enabled but private endpoint is approved', function(done) { + const cache = createCache([registry[2]]); + mlRegistryPublicAccess.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Machine Learning registry has public network access disabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if Machine Learning registry has public access enabled but selected networks are configured', function(done) { + const cache = createCache([registry[3]]); + mlRegistryPublicAccess.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Machine Learning registry has public network access disabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); }); \ No newline at end of file diff --git a/plugins/azure/machinelearning/workspacePublicAccessDisabled.js b/plugins/azure/machinelearning/workspacePublicAccessDisabled.js index 81ddbbb611..515853a235 100644 --- a/plugins/azure/machinelearning/workspacePublicAccessDisabled.js +++ b/plugins/azure/machinelearning/workspacePublicAccessDisabled.js @@ -38,12 +38,31 @@ module.exports = { for (let workspace of machineLearningWorkspaces.data) { if (!workspace.id) continue; - if (workspace.publicNetworkAccess && workspace.publicNetworkAccess.toLowerCase()=='disabled') { + if (workspace.publicNetworkAccess && workspace.publicNetworkAccess.toLowerCase() === 'disabled') { helpers.addResult(results, 0, 'Machine Learning workspace has public network access disabled', location, workspace.id); } else { - helpers.addResult(results, 2, - 'Machine Learning workspace has public network access enabled', location, workspace.id); + const hasPrivateEndpoint = workspace.privateEndpointConnections && + workspace.privateEndpointConnections.length > 0 && + workspace.privateEndpointConnections.some(conn => + conn.properties && conn.properties.privateLinkServiceConnectionState && conn.properties.privateLinkServiceConnectionState.status === 'Approved' + ); + + const hasIpRules = workspace.networkAcls && workspace.networkAcls.ipRules && + workspace.networkAcls.ipRules.length > 0; + + const restricted = workspace.networkAcls && + workspace.networkAcls.defaultAction && + workspace.networkAcls.defaultAction.toLowerCase() === 'deny' && + hasIpRules; + + if (hasPrivateEndpoint || restricted) { + helpers.addResult(results, 0, + 'Machine Learning workspace has public network access disabled', location, workspace.id); + } else { + helpers.addResult(results, 2, + 'Machine Learning workspace has public network access enabled', location, workspace.id); + } } } diff --git a/plugins/azure/machinelearning/workspacePublicAccessDisabled.spec.js b/plugins/azure/machinelearning/workspacePublicAccessDisabled.spec.js index 1108bff0d8..8d1be83016 100644 --- a/plugins/azure/machinelearning/workspacePublicAccessDisabled.spec.js +++ b/plugins/azure/machinelearning/workspacePublicAccessDisabled.spec.js @@ -19,9 +19,37 @@ const workspaces = [ "id": "/subscriptions/12345667/resourceGroups/test/providers/Microsoft.MachineLearningServices/workspaces/test1", "name": "test", "type": "Microsoft.MachineLearningServices/workspaces", - "publicNetworkAccess" : "Enabled" + "publicNetworkAccess" : "Enabled", + "networkAcls": { + "defaultAction": "Allow", + "ipRules": [] + } + }, + { + "id": "/subscriptions/12345667/resourceGroups/test/providers/Microsoft.MachineLearningServices/workspaces/test1", + "name": "test", + "type": "Microsoft.MachineLearningServices/workspaces", + "publicNetworkAccess": "Enabled", + "privateEndpointConnections": [ + { + "properties": { + "privateLinkServiceConnectionState": { + "status": "Approved" + } + } + } + ] + }, + { + "id": "/subscriptions/12345667/resourceGroups/test/providers/Microsoft.MachineLearningServices/workspaces/test1", + "name": "test", + "type": "Microsoft.MachineLearningServices/workspaces", + "publicNetworkAccess": "Enabled", + "networkAcls": { + "defaultAction": "Deny", + "ipRules": [{"value": "123.2.21.1/32"}, {"value": "103.177.240.106/32"}] + } }, - ]; const createCache = (workspaces) => { @@ -93,5 +121,27 @@ describe('workspacePublicAccessDisabled', function() { }); }); + it('should give passing result if Machine Learning workspace has public access enabled but private endpoint is approved', function(done) { + const cache = createCache([workspaces[2]]); + workspacePublicAccessDisabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Machine Learning workspace has public network access disabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if Machine Learning workspace has selected networks with restricted IP ranges', function(done) { + const cache = createCache([workspaces[3]]); + workspacePublicAccessDisabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Machine Learning workspace has public network access disabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); }); \ No newline at end of file diff --git a/plugins/azure/openai/accountPublicAccessDisabled.js b/plugins/azure/openai/accountPublicAccessDisabled.js index e5ab13c2c5..b7f8ad7649 100644 --- a/plugins/azure/openai/accountPublicAccessDisabled.js +++ b/plugins/azure/openai/accountPublicAccessDisabled.js @@ -37,14 +37,31 @@ module.exports = { for (let account of accounts.data) { - if (account.properties && - account.properties.publicNetworkAccess && - account.properties.publicNetworkAccess.toLowerCase() == 'enabled') { - helpers.addResult(results, 2, - 'OpenAI Account is publicly accessible', location, account.id); - } else { + if (!account.id) continue; + + const publicAccess = account.publicNetworkAccess && account.publicNetworkAccess.toLowerCase(); + + if (publicAccess === 'disabled') { helpers.addResult(results, 0, 'OpenAI Account is not publicly accessible', location, account.id); + } else { + const hasPrivateEndpoint = account.privateEndpointConnections && + account.privateEndpointConnections.length > 0 && + account.privateEndpointConnections.some(conn => + conn.properties && conn.properties.privateLinkServiceConnectionState && conn.properties.privateLinkServiceConnectionState.status === 'Approved' + ); + + const restricted = account.networkAcls && + account.networkAcls.defaultAction && + account.networkAcls.defaultAction.toLowerCase() === 'deny'; + + if (hasPrivateEndpoint || restricted) { + helpers.addResult(results, 0, + 'OpenAI Account is not publicly accessible', location, account.id); + } else { + helpers.addResult(results, 2, + 'OpenAI Account is publicly accessible', location, account.id); + } } } diff --git a/plugins/azure/openai/accountPublicAccessDisabled.spec.js b/plugins/azure/openai/accountPublicAccessDisabled.spec.js index 28c74f3ea2..836869f0da 100644 --- a/plugins/azure/openai/accountPublicAccessDisabled.spec.js +++ b/plugins/azure/openai/accountPublicAccessDisabled.spec.js @@ -7,17 +7,40 @@ const accounts = [ "name": "acc1", "type": "Microsoft.CognitiveServices/accounts", "location": "eastus", - "properties": { - "publicNetworkAccess": 'Disabled' - } + "publicNetworkAccess": 'Disabled' }, { "id": "/subscriptions/12424/resourceGroups/bvttest/providers/Microsoft.CognitiveServices/accounts/acc2", "name": "acc2", "type": "Microsoft.CognitiveServices/accounts", "location": "eastus", - "properties": { - "publicNetworkAccess": 'Enabled' + "publicNetworkAccess": 'Enabled' + }, + { + "id": "/subscriptions/12424/resourceGroups/bvttest/providers/Microsoft.CognitiveServices/accounts/acc3", + "name": "acc3", + "type": "Microsoft.CognitiveServices/accounts", + "location": "eastus", + "publicNetworkAccess": 'Enabled', + "privateEndpointConnections": [ + { + "properties": { + "privateLinkServiceConnectionState": { + "status": "Approved" + } + } + } + ] + }, + { + "id": "/subscriptions/12424/resourceGroups/bvttest/providers/Microsoft.CognitiveServices/accounts/acc4", + "name": "acc4", + "type": "Microsoft.CognitiveServices/accounts", + "location": "eastus", + "publicNetworkAccess": 'Enabled', + "networkAcls": { + "defaultAction": "Deny", + "ipRules": [{"value": "192.168.1.0/24"}] } }, @@ -93,5 +116,27 @@ describe('accountPublicAccessDisabled', function() { }); }); + it('should give passing result if openai account has approved private endpoint', function(done) { + const cache = createCache([accounts[2]]); + accountPublicAccessDisabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('OpenAI Account is not publicly accessible'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if openai account has selected networks', function(done) { + const cache = createCache([accounts[3]]); + accountPublicAccessDisabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('OpenAI Account is not publicly accessible'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); }); \ No newline at end of file diff --git a/plugins/azure/servicebus/namespacePublicAccess.js b/plugins/azure/servicebus/namespacePublicAccess.js index dcf2105919..2cdb1f7714 100644 --- a/plugins/azure/servicebus/namespacePublicAccess.js +++ b/plugins/azure/servicebus/namespacePublicAccess.js @@ -1,5 +1,6 @@ var async = require('async'); var helpers = require('../../../helpers/azure'); +var cidrHelper = require('../../../helpers/azure/functions'); module.exports = { title: 'Namespace Public Access', @@ -10,7 +11,7 @@ module.exports = { more_info: 'Using private endpoints for Azure Service Bus namespace improve security by enabling private network access, encrypting communication, and enhancing performance. They seamlessly integrate with virtual networks, ensuring compliance and suitability for hybrid cloud scenarios.', recommended_action: 'Ensure that Azure Service Bus namespaces are only accessible through private endpoints.', link: 'https://learn.microsoft.com/en-us/azure/service-bus-messaging/private-link-service', - apis: ['serviceBus:listNamespacesBySubscription'], + apis: ['serviceBus:listNamespacesBySubscription', 'serviceBus:getNamespaceNetworkRuleSet'], realtime_triggers: ['microsoftservicebus:namespaces:write','microsoftservicebus:namespaces:delete'], run: function(cache, settings, callback) { @@ -36,15 +37,46 @@ module.exports = { } for (let namespace of namespaces.data) { - if (namespace.sku && namespace.sku.tier && namespace.sku.tier.toLowerCase() !== 'premium') { - helpers.addResult(results, 0, 'Service Bus Namespace is not a premium namespace', location, namespace.id); - } else if (namespace.publicNetworkAccess && namespace.publicNetworkAccess.toLowerCase() === 'enabled') { - helpers.addResult(results, 2, 'Service bus namespace is publicly accessible', location, namespace.id); + + const networkRules = helpers.addSource(cache, source, + ['serviceBus', 'getNamespaceNetworkRuleSet', location, namespace.id]); + + if (networkRules && networkRules.err) { + helpers.addResult(results, 3, 'Unable to query network rules for namespace: ' + helpers.addError(networkRules), location, namespace.id); + continue; + } + + if (namespace.publicNetworkAccess && namespace.publicNetworkAccess.toLowerCase() === 'enabled') { + + if (namespace.sku && namespace.sku.tier && namespace.sku.tier.toLowerCase() === 'premium' && + namespace.privateEndpointConnections && namespace.privateEndpointConnections.length > 0 && + namespace.privateEndpointConnections.some(conn => + conn.properties && conn.properties.privateLinkServiceConnectionState && conn.properties.privateLinkServiceConnectionState.status === 'Approved' + )) { + helpers.addResult(results, 0, 'Service bus namespace is only accessible through private endpoints', location, namespace.id); + } else { + let hasOpenCidr = false; + let hasIpRules = networkRules && networkRules.data && networkRules.data.ipRules && networkRules.data.ipRules.length > 0; + + if (hasIpRules) { + for (let rule of networkRules.data.ipRules) { + if (cidrHelper.isOpenCidrRange(rule.ipMask || rule.ipAddressOrRange)) { + hasOpenCidr = true; + break; + } + } + } + + if (hasIpRules && !hasOpenCidr) { + helpers.addResult(results, 0, 'Service bus namespace is only accessible through private endpoints', location, namespace.id); + } else { + helpers.addResult(results, 2, 'Service bus namespace is publicly accessible', location, namespace.id); + } + } } else { helpers.addResult(results, 0, 'Service bus namespace is only accessible through private endpoints', location, namespace.id); } } - rcb(); }, function() { // Global checking goes here diff --git a/plugins/azure/servicebus/namespacePublicAccess.spec.js b/plugins/azure/servicebus/namespacePublicAccess.spec.js index 4caed1c4e8..e071ea441e 100644 --- a/plugins/azure/servicebus/namespacePublicAccess.spec.js +++ b/plugins/azure/servicebus/namespacePublicAccess.spec.js @@ -22,16 +22,12 @@ const namespaces = [ publicNetworkAccess: 'Disabled', disableLocalAuth: true, provisioningState: 'Succeeded', - status: 'Active', - encryption: { - keySource: 'Microsoft.KeyVault', - requireInfrastructureEncryption: false - }, + status: 'Active' }, { sku: { name: 'Basic', tier: 'Basic' }, id: '/subscriptions/234/myrg/providers/Microsoft.ServiceBus/namespaces/test3', - name: 'test2', + name: 'test3', type: 'Microsoft.ServiceBus/Namespaces', location: 'East US', publicNetworkAccess: 'Enabled', @@ -39,21 +35,81 @@ const namespaces = [ provisioningState: 'Succeeded', status: 'Active' }, + { + sku: { name: 'Premium', tier: 'Premium', capacity: 1 }, + id: '/subscriptions/234/myrg/providers/Microsoft.ServiceBus/namespaces/test4', + name: 'test4', + type: 'Microsoft.ServiceBus/Namespaces', + location: 'East US', + publicNetworkAccess: 'Enabled', + disableLocalAuth: false, + provisioningState: 'Succeeded', + status: 'Active', + privateEndpointConnections: [ + { + id: '/subscriptions/234/myrg/providers/Microsoft.ServiceBus/namespaces/test4/privateEndpointConnections/test-pec', + name: 'test-pec', + properties: { + privateLinkServiceConnectionState: { + status: 'Approved' + } + } + } + ] + }, + { + sku: { name: 'Premium', tier: 'Premium', capacity: 1 }, + id: '/subscriptions/234/myrg/providers/Microsoft.ServiceBus/namespaces/test5', + name: 'test5', + type: 'Microsoft.ServiceBus/Namespaces', + location: 'East US', + publicNetworkAccess: 'Enabled', + disableLocalAuth: false, + provisioningState: 'Succeeded', + status: 'Active' + } ]; +const networkRules = { + restricted: { + data: { + ipRules: [{ ipMask: '10.0.0.1/32' }] + } + }, + openCidr: { + data: { + ipRules: [{ ipMask: '0.0.0.0/0' }] + } + }, + empty: { + data: { + ipRules: [] + } + } +}; -const createCache = (namespaces, err) => { - - return { +const createCache = (namespaces, err, networkRulesData = null) => { + const cache = { serviceBus: { listNamespacesBySubscription: { 'eastus': { data: namespaces, err: err } + }, + getNamespaceNetworkRuleSet: { + 'eastus': {} } } }; + + if (namespaces && namespaces.length > 0) { + namespaces.forEach((ns) => { + cache.serviceBus.getNamespaceNetworkRuleSet['eastus'][ns.id] = networkRulesData || networkRules.empty; + }); + } + + return cache; }; describe('namespacePublicAccess', function () { @@ -81,20 +137,52 @@ describe('namespacePublicAccess', function () { }); }); - - it('should give passing result if namespace is not using premium tier', function (done) { + it('should give passing result if namespace is not publicly accessible', function (done) { + const cache = createCache([namespaces[1]], null); + namespacePublicAccess.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Service bus namespace is only accessible through private endpoints'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if namespace is publicly accessible without protection', function (done) { + const cache = createCache([namespaces[0]], null); + namespacePublicAccess.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Service bus namespace is publicly accessible'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if basic tier namespace is publicly accessible', function (done) { const cache = createCache([namespaces[2]], null); + namespacePublicAccess.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Service bus namespace is publicly accessible'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if premium namespace has approved private endpoints', function (done) { + const cache = createCache([namespaces[3]], null); namespacePublicAccess.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(0); - expect(results[0].message).to.include('Service Bus Namespace is not a premium namespace'); + expect(results[0].message).to.include('Service bus namespace is only accessible through private endpoints'); expect(results[0].region).to.equal('eastus'); done(); }); }); - it('should give passing result if namespace is not publicly accessible', function (done) { - const cache = createCache([namespaces[1]], null); + it('should give passing result if namespace has public access enabled with restricted IP rules', function (done) { + const cache = createCache([namespaces[4]], null, networkRules.restricted); namespacePublicAccess.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(0); @@ -104,8 +192,8 @@ describe('namespacePublicAccess', function () { }); }); - it('should give failing result if namespace is publicly accessible', function (done) { - const cache = createCache([namespaces[0]], null); + it('should give failing result if namespace has public access enabled with open CIDR 0.0.0.0/0', function (done) { + const cache = createCache([namespaces[4]], null, networkRules.openCidr); namespacePublicAccess.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(2); @@ -114,5 +202,18 @@ describe('namespacePublicAccess', function () { done(); }); }); + + it('should give unknown result if unable to query network rules for namespace', function (done) { + const cache = createCache([namespaces[0]], null); + cache.serviceBus.getNamespaceNetworkRuleSet['eastus'][namespaces[0].id] = { err: 'error fetching network rules' }; + namespacePublicAccess.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 network rules for namespace'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); }); -}); \ No newline at end of file +}); + diff --git a/plugins/azure/storageaccounts/storageAccountPublicNetworkAccess.js b/plugins/azure/storageaccounts/storageAccountPublicNetworkAccess.js index 83dd52a9e4..c034e0d63b 100644 --- a/plugins/azure/storageaccounts/storageAccountPublicNetworkAccess.js +++ b/plugins/azure/storageaccounts/storageAccountPublicNetworkAccess.js @@ -37,12 +37,31 @@ module.exports = { for (let account of storageAccount.data) { if (!account.id) continue; - - if (account.publicNetworkAccess && (account.publicNetworkAccess.toLowerCase() == 'disabled' || account.publicNetworkAccess.toLowerCase() == 'securedbyperimeter')){ + + if (account.publicNetworkAccess && (account.publicNetworkAccess.toLowerCase() == 'disabled' || account.publicNetworkAccess.toLowerCase() == 'securedbyperimeter' )){ helpers.addResult(results, 0, 'Storage account has public network access disabled', location, account.id); } else { - helpers.addResult(results, 2, 'Storage account does not have public network access disabled', location, account.id); - } + const hasIpRules = account.networkAcls && account.networkAcls.ipRules && account.networkAcls.ipRules.length > 0; + let hasOpenCidr = false; + + if (hasIpRules) { + + for (let rule of account.networkAcls.ipRules) { + if (helpers.isOpenCidrRange(rule.value || rule.ipAddressOrRange)) { + hasOpenCidr = true; + break; + } + } + } + + const restricted = account.networkAcls && account.networkAcls.defaultAction && account.networkAcls.defaultAction.toLowerCase() === 'deny'; + + if ( restricted && !hasOpenCidr) { + helpers.addResult(results, 0, 'Storage account has public network access disabled', location, account.id); + } else { + helpers.addResult(results, 2, 'Storage account has public network access enabled for all networks', location, account.id); + } + } } rcb(); diff --git a/plugins/azure/storageaccounts/storageAccountPublicNetworkAccess.spec.js b/plugins/azure/storageaccounts/storageAccountPublicNetworkAccess.spec.js index 454522eda5..6673be93fa 100644 --- a/plugins/azure/storageaccounts/storageAccountPublicNetworkAccess.spec.js +++ b/plugins/azure/storageaccounts/storageAccountPublicNetworkAccess.spec.js @@ -22,6 +22,38 @@ const storageAccounts = [ 'name': 'acc', 'tags': {}, "publicNetworkAccess": "SecuredByPerimeter" + }, + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Storage/storageAccounts/acc', + 'location': 'eastus', + 'name': 'acc', + 'tags': {}, + "publicNetworkAccess": "Enabled", + "networkAcls": { + "defaultAction": "Deny", + "ipRules": [ + { + "value": "192.168.1.0/24", + "action": "Allow" + } + ] + } + }, + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Storage/storageAccounts/acc', + 'location': 'eastus', + 'name': 'acc', + 'tags': {}, + "publicNetworkAccess": "Enabled", + "networkAcls": { + "defaultAction": "Deny", + "ipRules": [ + { + "value": "0.0.0.0/0", + "action": "Allow" + } + ] + } } ]; @@ -87,7 +119,7 @@ describe('storageAccountPublicNetworkAccess', function() { storageAccountPublicNetworkAccess.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(2); - expect(results[0].message).to.include('Storage account does not have public network access disabled'); + expect(results[0].message).to.include('Storage account has public network access enabled for all networks'); expect(results[0].region).to.equal('eastus'); done(); }); @@ -103,5 +135,27 @@ describe('storageAccountPublicNetworkAccess', function() { done(); }); }); + + it('should give passing result if Storage account has public network access enabled but restricted by network ACLs', function(done) { + const cache = createCache([storageAccounts[3]]); + storageAccountPublicNetworkAccess.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Storage account has public network access disabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if Storage account has public network access enabled with 0.0.0.0/0', function(done) { + const cache = createCache([storageAccounts[4]]); + storageAccountPublicNetworkAccess.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Storage account has public network access enabled for all networks'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); }); }); \ No newline at end of file