diff --git a/exports.js b/exports.js index 910c707cd1..24c6faa62f 100644 --- a/exports.js +++ b/exports.js @@ -460,6 +460,7 @@ module.exports = { 'advancedDataSecurityEnabled' : require(__dirname + '/plugins/azure/sqlserver/advancedDataSecurityEnabled.js'), 'tdeProtectorEncrypted' : require(__dirname + '/plugins/azure/sqlserver/tdeProtectorEncrypted.js'), 'noPublicAccess' : require(__dirname + '/plugins/azure/sqlserver/noPublicAccess.js'), + 'serverPrivateEndpoints' : require(__dirname + '/plugins/azure/sqlserver/serverPrivateEndpoints.js'), 'auditRetentionPolicy' : require(__dirname + '/plugins/azure/sqlserver/auditRetentionPolicy.js'), 'auditActionGroupsEnabled' : require(__dirname + '/plugins/azure/sqlserver/auditActionGroupsEnabled.js'), 'serverAuditingEnabled' : require(__dirname + '/plugins/azure/sqlserver/serverAuditingEnabled.js'), diff --git a/plugins/azure/sqlserver/serverPrivateEndpoints.js b/plugins/azure/sqlserver/serverPrivateEndpoints.js new file mode 100644 index 0000000000..bbfbb52837 --- /dev/null +++ b/plugins/azure/sqlserver/serverPrivateEndpoints.js @@ -0,0 +1,50 @@ +var async = require('async'); +const helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'SQL Server Private Endpoints Configured', + category: 'SQL Server', + description: 'Ensures that SQL Servers are accessible only through private endpoints', + more_info: 'Azure Private Endpoint is a network interface that connects you privately and securely to a service powered by Azure Private Link. Private Endpoint uses a private IP address from your VNet, effectively bringing the service such as Azure SQL Server into your VNet.', + recommended_action: 'Ensure that Private Endpoints are configured properly and Public Network Access is disabled for SQL Server', + link: 'https://docs.microsoft.com/en-us/azure/private-link/private-link-overview', + apis: ['servers:listSql'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var locations = helpers.locations(settings.govcloud); + + async.each(locations.servers, function(location, rcb) { + + var servers = helpers.addSource(cache, source, + ['servers', 'listSql', location]); + + if (!servers) return rcb(); + + if (servers.err || !servers.data) { + helpers.addResult(results, 3, + 'Unable to query for SQL servers: ' + helpers.addError(servers), location); + return rcb(); + } + + if (!servers.data.length) { + helpers.addResult(results, 0, 'No SQL servers found', location); + return rcb(); + } + + for (const server of servers.data) { + if (server.privateEndpointConnections && server.privateEndpointConnections.length) { + helpers.addResult(results, 0, 'Private Endpoints are configured for the SQL Server', location, server.id); + } else { + helpers.addResult(results, 2, 'Private Endpoints are not configured for the SQL Server', location, server.id); + } + } + + rcb(); + }, function() { + // Global checking goes here + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/sqlserver/serverPrivateEndpoints.spec.js b/plugins/azure/sqlserver/serverPrivateEndpoints.spec.js new file mode 100644 index 0000000000..7be65402d6 --- /dev/null +++ b/plugins/azure/sqlserver/serverPrivateEndpoints.spec.js @@ -0,0 +1,85 @@ +var expect = require('chai').expect; +var serverPrivateEndpoints = require('./serverPrivateEndpoints'); + +const servers = [ + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Sql/servers/test-server', + 'name': 'test-server', + 'privateEndpointConnections': [ + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Sql/servers/test-server/privateEndpointConnections/test-endpoint', + 'provisioningState': 'Ready' + } + ], + 'location': 'eastus' + }, + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Sql/servers/test-server', + 'name': 'test-server', + 'privateEndpointConnections': [], + 'location': 'eastus' + } +]; + + +const createCache = (servers) => { + let server = {}; + if (servers) { + server['data'] = servers; + } + return { + servers: { + listSql: { + 'eastus': server + } + } + }; +}; + +describe('serverPrivateEndpoints', function() { + describe('run', function() { + it('should give passing result if no SQL servers', function(done) { + const cache = createCache([]); + serverPrivateEndpoints.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('No SQL servers found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give unknown result if unable to query for SQL servers', function(done) { + const cache = createCache(null); + serverPrivateEndpoints.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 SQL servers:'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if private endpoints are configured', function(done) { + const cache = createCache([servers[0]]); + serverPrivateEndpoints.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Private Endpoints are configured for the SQL Server'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if private endpoints are not configured', function(done) { + const cache = createCache([servers[1]]); + serverPrivateEndpoints.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Private Endpoints are not configured for the SQL Server'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); +}); \ No newline at end of file