Skip to content
Open
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
1 change: 1 addition & 0 deletions exports.js
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,7 @@ module.exports = {

'recoveryVaultByokEncrypted' : require(__dirname + '/plugins/azure/recoveryService/recoveryVaultByokEncrypted.js'),
'recoveryVaultLoggingEnabled' : require(__dirname + '/plugins/azure/recoveryService/recoveryVaultLoggingEnabled.js'),
'recoveryVaultPrivateEndpoint' : require(__dirname + '/plugins/azure/recoveryService/recoveryVaultPrivateEndpoint.js'),

'domainPublicAccessEnabled' : require(__dirname + '/plugins/azure/eventGrid/domainPublicAccess.js'),
'domainMinimumTlsVersion' : require(__dirname + '/plugins/azure/eventGrid/domainMinimumTlsVersion.js'),
Expand Down
2 changes: 1 addition & 1 deletion helpers/azure/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ var postcalls = {
getRecoveryServiceVault: {
reliesOnPath: 'recoveryServiceVaults.listBySubscriptionId',
properties: ['id'],
url: 'https://management.azure.com/{id}?api-version=2022-04-01'
url: 'https://management.azure.com/{id}?api-version=2024-04-01'
}
},
webhooks: {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"dependencies": {
"@alicloud/pop-core": "^1.7.10",
"@azure/data-tables": "^13.2.2",
"@azure/identity": "^4.13.0",
"@azure/storage-file-share": "^12.14.0",
"@azure/storage-queue": "^12.13.0",
"@azure/storage-blob": "^12.14.0",
Expand Down Expand Up @@ -68,4 +69,4 @@
"nodemon": "^1.19.4",
"nyc": "^14.1.1"
}
}
}
123 changes: 123 additions & 0 deletions plugins/azure/recoveryService/recoveryVaultPrivateEndpoint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
const async = require("async");
const helpers = require("../../../helpers/azure");

module.exports = {
title: "Recovery Services Vault Private Endpoint",
category: "Recovery Service Vault",
domain: "Backup",
severity: "High",
description:
"Ensure that Azure Recovery Services Vaults are not publicly accessible and have private endpoints configured.",
more_info:
"Private endpoints restrict access to the vault over a private network, reducing exposure to the public internet.",
recommended_action:
"Configure a private endpoint for the Recovery Services Vault.",
link: "https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-overview",
apis: [
"recoveryServiceVaults:getRecoveryServiceVault",
"recoveryServiceVaults:listBySubscriptionId",
],
realtime_triggers: [
"microsoftrecoveryservices:vaults:write",
"microsoftrecoveryservices:vaults:delete",
],

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

async.each(
locations.recoveryServiceVaults,
(location, rcb) => {
const serviceVaults = helpers.addSource(cache, source, [
"recoveryServiceVaults",
"listBySubscriptionId",
location,
]);

if (!serviceVaults || serviceVaults.err || !serviceVaults.data)
return rcb();

for (let vault of serviceVaults.data) {
if (!vault.id) continue;

const getVault = helpers.addSource(cache, source, [
"recoveryServiceVaults",
"getRecoveryServiceVault",
location,
vault.id,
]);

if (!getVault || getVault.err || !getVault.data) {
helpers.addResult(
results,
3,
"Unable to query Recovery Service Vault details: " +
helpers.addError(getVault),
location,
vault.id
);
continue;
}

// Use new API response structure
// Check for publicNetworkAccess property
const publicAccess =
typeof getVault.data.publicNetworkAccess === "string"
? getVault.data.publicNetworkAccess
: "Enabled"; // Default to 'Enabled' if missing

// Check for private endpoint states (new API: privateEndpointStateForBackup, privateEndpointStateForSiteRecovery)
// Consider vault private if either backup or site recovery endpoint is not 'None'
const backupPrivateState =
getVault.data.privateEndpointStateForBackup || "None";
const siteRecoveryPrivateState =
getVault.data.privateEndpointStateForSiteRecovery || "None";
const hasPrivate =
backupPrivateState !== "None" ||
siteRecoveryPrivateState !== "None";
const isPublicDisabled = publicAccess.toLowerCase() === "disabled";

if (hasPrivate && isPublicDisabled) {
helpers.addResult(
results,
0,
"Recovery Service Vault is private: public network access is disabled and private endpoints are configured",
location,
vault.id
);
} else if (!hasPrivate && isPublicDisabled) {
helpers.addResult(
results,
2,
"Recovery Service Vault has public network access disabled but NO private endpoints configured (not accessible)",
location,
vault.id
);
} else if (hasPrivate && !isPublicDisabled) {
helpers.addResult(
results,
2,
"Recovery Service Vault has private endpoints but public network access is still enabled (partially public)",
location,
vault.id
);
} else {
helpers.addResult(
results,
2,
"Recovery Service Vault is PUBLIC: no private endpoints and public network access is enabled",
location,
vault.id
);
}
}
rcb();
},
function () {
callback(null, results, source);
}
);
},
};
81 changes: 81 additions & 0 deletions plugins/azure/recoveryService/recoveryVaultPrivateEndpoint.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const expect = require("chai").expect;
const plugin = require("./recoveryVaultPrivateEndpoint");

const vaultId =
"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ExampleRG/providers/Microsoft.RecoveryServices/vaults/ExampleVault";
const location = "eastus";

function createCache(vaultData) {
return {
recoveryServiceVaults: {
listBySubscriptionId: {
[location]: { data: [{ id: vaultId }] },
},
getRecoveryServiceVault: {
[location]: {
[vaultId]: { data: vaultData },
},
},
},
};
}

describe("recoveryVaultPrivateEndpoint", function () {
it("should FAIL when public access is enabled and no private endpoints", function (done) {
const cache = createCache({
publicNetworkAccess: "Enabled",
privateEndpointStateForBackup: "None",
privateEndpointStateForSiteRecovery: "None",
});
plugin.run(cache, {}, (err, results) => {
expect(results[0].status).to.equal(2);
expect(results[0].message).to.include("PUBLIC");
done();
});
});

it("should FAIL when public access is enabled and private endpoints exist", function (done) {
const cache = createCache({
publicNetworkAccess: "Enabled",
privateEndpointStateForBackup: "Approved",
privateEndpointStateForSiteRecovery: "None",
});
plugin.run(cache, {}, (err, results) => {
expect(results[0].status).to.equal(2);
expect(results[0].message).to.include(
"private endpoints but public network access is still enabled"
);
done();
});
});

it("should PASS (OK) when public access is disabled and private endpoints exist", function (done) {
const cache = createCache({
publicNetworkAccess: "Disabled",
privateEndpointStateForBackup: "Approved",
privateEndpointStateForSiteRecovery: "None",
});
plugin.run(cache, {}, (err, results) => {
expect(results[0].status).to.equal(0);
expect(results[0].message).to.include(
"is private: public network access is disabled and private endpoints are configured"
);
done();
});
});

it("should FAIL when public access is disabled and no private endpoints", function (done) {
const cache = createCache({
publicNetworkAccess: "Disabled",
privateEndpointStateForBackup: "None",
privateEndpointStateForSiteRecovery: "None",
});
plugin.run(cache, {}, (err, results) => {
expect(results[0].status).to.equal(2);
expect(results[0].message).to.include(
"public network access disabled but NO private endpoints configured"
);
done();
});
});
});
Loading