Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion helpers/azure/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:{
Expand Down Expand Up @@ -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'
}
}
};

Expand Down
14 changes: 12 additions & 2 deletions helpers/azure/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -868,7 +878,7 @@ module.exports = {
remediateOpenPortsHelper: remediateOpenPortsHelper,
checkMicrosoftDefender: checkMicrosoftDefender,
checkFlexibleServerConfigs:checkFlexibleServerConfigs,
checkNetworkExposure: checkNetworkExposure

checkNetworkExposure: checkNetworkExposure,
isOpenCidrRange: isOpenCidrRange
};

Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
}
];

Expand Down Expand Up @@ -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();
});
});
});
});
32 changes: 28 additions & 4 deletions plugins/azure/appservice/appServicePublicAccess.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
});

Expand Down
48 changes: 48 additions & 0 deletions plugins/azure/appservice/appServicePublicAccess.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
]
}
];

Expand Down Expand Up @@ -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();
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
]
}
];

Expand Down Expand Up @@ -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();
});
});
});
});
11 changes: 8 additions & 3 deletions plugins/azure/batchAccounts/batchAccountsPublicAccess.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
32 changes: 32 additions & 0 deletions plugins/azure/batchAccounts/batchAccountsPublicAccess.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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();
});
});
});
});
9 changes: 8 additions & 1 deletion plugins/azure/containerregistry/acrPublicAccess.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading