Skip to content
Merged
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
14 changes: 14 additions & 0 deletions exports.js
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +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'),
'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'),
'geoRedundantStorage' : require(__dirname + '/plugins/azure/storageaccounts/geoRedundantStorage.js'),
'fileShareSoftDeletionEnabled' : require(__dirname + '/plugins/azure/storageaccounts/fileShareSoftDeletionEnabled.js'),
'fileShareSmbProtocolVersion' : require(__dirname + '/plugins/azure/storageaccounts/fileShareSmbProtocolVersion.js'),
'fileShareSmbChannelEncryption' : require(__dirname + '/plugins/azure/storageaccounts/fileShareSmbChannelEncryption.js'),
'storageAccountsTlsVersion' : require(__dirname + '/plugins/azure/storageaccounts/storageAccountsTlsVersion.js'),
'storageAccountHasTags' : require(__dirname + '/plugins/azure/storageaccounts/storageAccountHasTags.js'),
'storageAccountPrivateEndpoint' : require(__dirname + '/plugins/azure/storageaccounts/storageAccountPrivateEndpoint.js'),
Expand Down Expand Up @@ -1057,6 +1065,8 @@ 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'),
'userAccessAdminRestricted' : require(__dirname + '/plugins/azure/entraid/userAccessAdminRestricted.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'),

Expand Down Expand Up @@ -1150,6 +1160,7 @@ module.exports = {
'agHttpsListenerOnly' : require(__dirname + '/plugins/azure/applicationGateway/agHttpsListenerOnly.js'),

'subscriptionHasTags' : require(__dirname + '/plugins/azure/subscription/subscriptionHasTags.js'),
'subscriptionTenantPolicy' : require(__dirname + '/plugins/azure/subscription/subscriptionTenantPolicy.js'),

'rgHasTags' : require(__dirname + '/plugins/azure/resourceGroup/rgHasTags.js'),

Expand Down Expand Up @@ -1256,6 +1267,9 @@ module.exports = {
'workspaceManagedServicesCmk' : require(__dirname + '/plugins/azure/databricks/workspaceManagedServicesCmk.js'),
'workspaceManagedDiskCmk' : require(__dirname + '/plugins/azure/databricks/workspaceManagedDiskCmk.js'),
'workspaceHasTags' : require(__dirname + '/plugins/azure/databricks/workspaceHasTags.js'),
'workspaceVnetInjection' : require(__dirname + '/plugins/azure/databricks/workspaceVnetInjection.js'),
'workspacePublicAccess' : require(__dirname + '/plugins/azure/databricks/workspacePublicAccess.js'),
'workspacePrivateEndpoints' : require(__dirname + '/plugins/azure/databricks/workspacePrivateEndpoints.js'),

'workspaceManagedIdentity' : require(__dirname + '/plugins/azure/synapse/workspaceManagedIdentity.js'),
'synapseWorkspaceAdAuthEnabled' : require(__dirname + '/plugins/azure/synapse/synapseWorkspaceAdAuthEnabled.js'),
Expand Down
48 changes: 47 additions & 1 deletion helpers/azure/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,13 @@ var calls = {
hasListResponse: true
}
},
securityContactv3: {
listAll: {
url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Security/securityContacts?api-version=2023-12-01-preview',
ignoreLocation: true,
hasListResponse: true
}
},
subscriptions: {
listLocations: {
url: 'https://management.azure.com/subscriptions/{subscriptionId}/locations?api-version=2020-01-01'
Expand Down Expand Up @@ -510,10 +517,29 @@ var calls = {
},
users: {
list: {
url: 'https://graph.microsoft.com/v1.0/users',
url: 'https://graph.microsoft.com/v1.0/users?$select=id,displayName,userPrincipalName,mail,userType,accountEnabled',
Comment thread
Hamza-Aziz61 marked this conversation as resolved.
graph: true
}
},
securityDefaultsPolicy: {
get: {
url: 'https://graph.microsoft.com/v1.0/policies/identitySecurityDefaultsEnforcementPolicy',
graph: true,
getCompleteResponse: true
}
},
subscriptionPolicies: {
get: {
url: 'https://management.azure.com/providers/Microsoft.Subscription/policies/default?api-version=2021-10-01',
getCompleteResponse: true
}
},
appInsights: {
list: {
url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/microsoft.insights/components?api-version=2020-02-02',
ignoreLocation: true
}
},
applications: {
list: {
url: 'https://graph.microsoft.com/v1.0/applications/',
Expand Down Expand Up @@ -958,6 +984,14 @@ var postcalls = {
rateLimit: 3000
}
},
fileServices: {
getServiceProperties: {
reliesOnPath: 'storageAccounts.list',
properties: ['id'],
url: 'https://management.azure.com/{id}/fileServices/default?api-version=2023-01-01',
rateLimit: 500
}
},
storageAccounts: {
listKeys: {
reliesOnPath: 'storageAccounts.list',
Expand Down Expand Up @@ -1049,6 +1083,11 @@ var postcalls = {
url: '{vaultUri}keys?api-version=7.0',
vault: true
},
listKeys: {
reliesOnPath: 'vaults.list',
properties: ['id'],
url: 'https://management.azure.com/{id}/keys?api-version=2023-07-01'
},
getSecrets: {
reliesOnPath: 'vaults.list',
properties: ['vaultUri'],
Expand Down Expand Up @@ -1468,6 +1507,13 @@ var tertiarycalls = {
vault: true
}
},
getKey: {
get: {
reliesOnPath: 'vaults.listKeys',
properties: ['id'],
url: 'https://management.azure.com/{id}?api-version=2023-07-01'
}
},
syncGroups: {
list: {
reliesOnPath: 'databases.listByServer',
Expand Down
4 changes: 4 additions & 0 deletions helpers/azure/locations.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ module.exports = {
networkGatewayConnections: locations,
natGateways: locations,
users: ['global'],
securityDefaultsPolicy: ['global'],
subscriptionPolicies: ['global'],
appInsights: ['global'],
registries: locations,
redisCaches: locations,
pricings: ['global'],
Expand All @@ -112,6 +115,7 @@ module.exports = {
privateDnsZones: ['global'],
privateEndpoints: locations,
securityContactv2: ['global'],
securityContactv3: ['global'],
images: locations,
vmScaleSet: locations,
applicationGateway: locations,
Expand Down
59 changes: 59 additions & 0 deletions plugins/azure/databricks/workspacePrivateEndpoints.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
var async = require('async');
var helpers = require('../../../helpers/azure');

module.exports = {
title: 'Databricks Workspace Private Endpoints',
category: 'AI & ML',
domain: 'Machine Learning',
severity: 'Medium',
description: 'Ensures that Azure Databricks Workspace has an approved private endpoint connection.',
more_info: 'Private endpoints allow clients and services to access the Databricks workspace over an encrypted Private Link using a private IP address from the virtual network. This keeps traffic off the public internet and reduces the attack surface. A private endpoint connection only carries traffic once its connection state is approved.',
recommended_action: 'Create a private endpoint for the Databricks workspace and approve the private endpoint connection.',
link: 'https://learn.microsoft.com/en-us/azure/databricks/security/network/classic/private-link',
apis: ['databricks:listWorkspaces'],
realtime_triggers: ['microsoftdatabricks:workspaces:write','microsoftdatabricks:workspaces:delete'],

run: function(cache, settings, callback) {
const results = [];
const source = {};
const locations = helpers.locations(settings.govcloud);

async.each(locations.databricks, function(location, rcb) {
const databricks = helpers.addSource(cache, source,
['databricks', 'listWorkspaces', location]);

if (!databricks) return rcb();

if (databricks.err || !databricks.data) {
helpers.addResult(results, 3, 'Unable to query for Databricks Workspaces: ' + helpers.addError(databricks), location);
return rcb();
}

if (!databricks.data.length) {
helpers.addResult(results, 0, 'No existing Databricks Workspaces found', location);
return rcb();
}

for (let workspace of databricks.data) {
if (!workspace.id) continue;

var approved = workspace.privateEndpointConnections && workspace.privateEndpointConnections.length ?
workspace.privateEndpointConnections.some(connection => connection.properties &&
connection.properties.privateLinkServiceConnectionState &&
connection.properties.privateLinkServiceConnectionState.status &&
connection.properties.privateLinkServiceConnectionState.status.toLowerCase() === 'approved') : false;

if (approved) {
helpers.addResult(results, 0, 'Databricks workspace has an approved private endpoint connection', location, workspace.id);
} else {
helpers.addResult(results, 2, 'Databricks workspace does not have an approved private endpoint connection', location, workspace.id);
}
}

rcb();
}, function() {
// Global checking goes here
callback(null, results, source);
});
}
};
140 changes: 140 additions & 0 deletions plugins/azure/databricks/workspacePrivateEndpoints.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
var expect = require('chai').expect;
var workspacePrivateEndpoints = require('./workspacePrivateEndpoints.js');

const workspaces = [
{
"managedResourceGroupId": "/subscriptions/1234/resourceGroups/test",
"privateEndpointConnections": [],
"id": "/subscriptions/1234/resourceGroups/test/providers/Microsoft.Databricks/workspaces/test-workspace",
"name": "test-workspace",
"type": "Microsoft.Databricks/workspaces",
"sku": {
"name": "premium"
},
"location": "eastus",
"tags": {}
},
{
"managedResourceGroupId": "/subscriptions/1234/resourceGroups/test",
"privateEndpointConnections": [
{
"id": "/subscriptions/1234/resourceGroups/test/providers/Microsoft.Databricks/workspaces/test-workspace/privateEndpointConnections/test-connection",
"name": "test-connection",
"type": "Microsoft.Databricks/workspaces/privateEndpointConnections",
"properties": {
"privateLinkServiceConnectionState": {
"status": "Pending",
"actionsRequired": "None"
}
}
}
],
"id": "/subscriptions/1234/resourceGroups/test/providers/Microsoft.Databricks/workspaces/test-workspace",
"name": "test-workspace",
"type": "Microsoft.Databricks/workspaces",
"sku": {
"name": "premium"
},
"location": "eastus",
"tags": {}
},
{
"managedResourceGroupId": "/subscriptions/1234/resourceGroups/test",
"privateEndpointConnections": [
{
"id": "/subscriptions/1234/resourceGroups/test/providers/Microsoft.Databricks/workspaces/test-workspace/privateEndpointConnections/test-connection",
"name": "test-connection",
"type": "Microsoft.Databricks/workspaces/privateEndpointConnections",
"properties": {
"privateLinkServiceConnectionState": {
"status": "Approved",
"actionsRequired": "None"
}
}
}
],
"id": "/subscriptions/1234/resourceGroups/test/providers/Microsoft.Databricks/workspaces/test-workspace",
"name": "test-workspace",
"type": "Microsoft.Databricks/workspaces",
"sku": {
"name": "premium"
},
"location": "eastus",
"tags": {}
},
];


const createCache = (workspaces, err) => {

return {
databricks: {
listWorkspaces: {
'eastus': {
data: workspaces,
err: err
}
}
}
};
};

describe('workspacePrivateEndpoints', function () {
describe('run', function () {

it('should give a passing result if no Databricks workspaces are found', function (done) {
const cache = createCache([], null);
workspacePrivateEndpoints.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 Databricks Workspaces found');
expect(results[0].region).to.equal('eastus');
done();
});
});

it('should give unknown result if unable to query for Databricks workspaces', function (done) {
const cache = createCache(null, ['error']);
workspacePrivateEndpoints.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 Databricks Workspaces');
expect(results[0].region).to.equal('eastus');
done();
});
});

it('should give passing result if Databricks workspace has an approved private endpoint connection', function (done) {
const cache = createCache([workspaces[2]], null);
workspacePrivateEndpoints.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
expect(results[0].message).to.include('Databricks workspace has an approved private endpoint connection');
expect(results[0].region).to.equal('eastus');
done();
});
});

it('should give failing result if Databricks workspace does not have private endpoint connections', function (done) {
const cache = createCache([workspaces[0]], null);
workspacePrivateEndpoints.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
expect(results[0].message).to.include('Databricks workspace does not have an approved private endpoint connection');
expect(results[0].region).to.equal('eastus');
done();
});
});

it('should give failing result if Databricks workspace private endpoint connection is not approved', function (done) {
const cache = createCache([workspaces[1]], null);
workspacePrivateEndpoints.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
expect(results[0].message).to.include('Databricks workspace does not have an approved private endpoint connection');
expect(results[0].region).to.equal('eastus');
done();
});
});
});
});
Loading
Loading