diff --git a/exports.js b/exports.js index 2f37757420..323ae3214c 100644 --- a/exports.js +++ b/exports.js @@ -26,6 +26,7 @@ module.exports = { 'apigatewayDefaultEndpointDisabled' : require(__dirname + '/plugins/aws/apigateway/apigatewayDefaultEndpointDisabled.js'), 'apigatewayAuthorization' : require(__dirname + '/plugins/aws/apigateway/apigatewayAuthorization.js'), 'apigatewayV2Authorization' : require(__dirname + '/plugins/aws/apigateway/apigatewayV2Authorization.js'), + 'apigatewayRequestValidation' : require(__dirname + '/plugins/aws/apigateway/apigatewayRequestValidation.js'), 'restrictExternalTraffic' : require(__dirname + '/plugins/aws/appmesh/restrictExternalTraffic.js'), 'appmeshTLSRequired' : require(__dirname + '/plugins/aws/appmesh/appmeshTLSRequired.js'), @@ -597,6 +598,7 @@ module.exports = { 's3ProtectionEnabled' : require(__dirname + '/plugins/aws/guardduty/s3ProtectionEnabled.js'), 'rdsProtectionEnabled' : require(__dirname + '/plugins/aws/guardduty/rdsProtectionEnabled.js'), 'exportedFindingsEncrypted' : require(__dirname + '/plugins/aws/guardduty/exportedFindingsEncrypted.js'), + 'lambdaProtectionEnabled' : require(__dirname + '/plugins/aws/guardduty/lambdaProtectionEnabled.js'), 'workspacesVolumeEncryption' : require(__dirname + '/plugins/aws/workspaces/workspacesVolumeEncryption.js'), 'workSpacesHealthyInstances' : require(__dirname + '/plugins/aws/workspaces/workSpacesHealthyInstances.js'), @@ -609,6 +611,8 @@ module.exports = { 'codebuildValidSourceProviders' : require(__dirname + '/plugins/aws/codebuild/codebuildValidSourceProviders.js'), 'projectArtifactsEncrypted' : require(__dirname + '/plugins/aws/codebuild/projectArtifactsEncrypted.js'), + 'buildProjectEnvPriviligedMode' : require(__dirname + '/plugins/aws/codebuild/buildProjectEnvPriviligedMode.js'), + 'codebuildProjectLoggingEnabled': require(__dirname + '/plugins/aws/codebuild/codebuildProjectLoggingEnabled.js'), 'codestarValidRepoProviders' : require(__dirname + '/plugins/aws/codestar/codestarValidRepoProviders.js'), 'codestarHasTags' : require(__dirname + '/plugins/aws/codestar/codestarHasTags.js'), @@ -616,6 +620,7 @@ module.exports = { 'pipelineArtifactsEncrypted' : require(__dirname + '/plugins/aws/codepipeline/pipelineArtifactsEncrypted.js'), 'dataStoreEncrypted' : require(__dirname + '/plugins/aws/healthlake/dataStoreEncrypted.js'), + 'dataStoreHasTags' : require(__dirname + '/plugins/aws/healthlake/dataStoreHasTags.js'), 'codeartifactDomainEncrypted' : require(__dirname + '/plugins/aws/codeartifact/codeartifactDomainEncrypted.js'), @@ -636,6 +641,7 @@ module.exports = { 'docDbHasTags' : require(__dirname + '/plugins/aws/documentDB/docDbHasTags.js'), 'docdbDeletionProtectionEnabled': require(__dirname + '/plugins/aws/documentDB/docdbDeletionProtectionEnabled.js'), 'docdbClusterBackupRetention' : require(__dirname + '/plugins/aws/documentDB/docdbClusterBackupRetention.js'), + 'docdbClusterProfilerEnabled' : require(__dirname + '/plugins/aws/documentDB/docdbClusterProfilerEnabled.js'), 'instanceMediaStreamsEncrypted' : require(__dirname + '/plugins/aws/connect/instanceMediaStreamsEncrypted.js'), 'instanceTranscriptsEncrypted' : require(__dirname + '/plugins/aws/connect/instanceTranscriptsEncrypted.js'), @@ -855,7 +861,10 @@ module.exports = { 'enforceMySQLSSLConnection' : require(__dirname + '/plugins/azure/mysqlserver/enforceMySQLSSLConnection.js'), 'mysqlFlexibleServersMinTls' : require(__dirname + '/plugins/azure/mysqlserver/mysqlFlexibleServersMinTls.js'), + 'mysqlFlexibleServerVersion' : require(__dirname + '/plugins/azure/mysqlserver/mysqlFlexibleServerVersion.js'), 'mysqlServerHasTags' : require(__dirname + '/plugins/azure/mysqlserver/mysqlServerHasTags.js'), + 'mysqlFlexibleServerDignosticLogs': require(__dirname + '/plugins/azure/mysqlserver/mysqlFlexibleServerDignosticLogs.js'), + 'mysqlFlexibleServerIdentity' : require(__dirname + '/plugins/azure/mysqlserver/mysqlFlexibleServerIdentity.js'), 'logRetentionDays' : require(__dirname + '/plugins/azure/postgresqlserver/logRetentionDays.js'), 'connectionThrottlingEnabled' : require(__dirname + '/plugins/azure/postgresqlserver/connectionThrottlingEnabled.js'), @@ -1110,6 +1119,7 @@ module.exports = { 'eventHubMinimumTLSversion' : require(__dirname + '/plugins/azure/eventhub/eventHubMinimumTLSversion.js'), 'eventHubNamespaceHasTags' : require(__dirname + '/plugins/azure/eventhub/eventHubNamespaceHasTags.js'), + 'eventHubNamespaceAutoInflate' : require(__dirname + '/plugins/azure/eventhub/eventHubNamespaceAutoInflate.js'), 'eventHubLocalAuthDisabled' : require(__dirname + '/plugins/azure/eventhub/eventHubLocalAuthDisabled.js'), 'eventHubPublicAccess' : require(__dirname + '/plugins/azure/eventhub/eventHubPublicAccess.js'), 'eventHubNamespaceCmkEncrypted' : require(__dirname + '/plugins/azure/eventhub/eventHubNamespaceCmkEncrypted.js'), @@ -1182,6 +1192,9 @@ module.exports = { 'batchAccountCmkEncrypted' : require(__dirname + '/plugins/azure/batchAccounts/batchAccountCmkEncrypted.js'), 'batchAccountDiagnosticLogs' : require(__dirname + '/plugins/azure/batchAccounts/batchAccountDiagnosticLogs.js'), + 'batchAccountsAADEnabled' : require(__dirname + '/plugins/azure/batchAccounts/batchAccountsAADEnabled.js'), + 'batchAccountsHasTags' : require(__dirname + '/plugins/azure/batchAccounts/batchAccountsHasTags.js'), + 'batchAccountsPublicAccess' : require(__dirname + '/plugins/azure/batchAccounts/batchAccountsPublicAccess.js'), 'accountCMKEncrypted' : require(__dirname + '/plugins/azure/openai/accountCMKEncrypted.js'), 'accountManagedIdentity' : require(__dirname + '/plugins/azure/openai/accountManagedIdentity.js'), @@ -1195,6 +1208,13 @@ 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'), + + 'workspaceManagedIdentity' : require(__dirname + '/plugins/azure/synapse/workspaceManagedIdentity.js'), + 'synapseWorkspaceAdAuthEnabled' : require(__dirname + '/plugins/azure/synapse/synapseWorkspaceAdAuthEnabled.js'), + 'synapseWorkspacPrivateEndpoint': require(__dirname + '/plugins/azure/synapse/synapseWorkspacPrivateEndpoint.js'), + + 'apiInstanceManagedIdentity' : require(__dirname + '/plugins/azure/apiManagement/apiInstanceManagedIdentity.js'), + 'apiInstanceHasTags' : require(__dirname + '/plugins/azure/apiManagement/apiInstanceHasTags.js'), }, github: { diff --git a/helpers/aws/api.js b/helpers/aws/api.js index 342eb14f72..a591179819 100644 --- a/helpers/aws/api.js +++ b/helpers/aws/api.js @@ -1848,6 +1848,12 @@ var postcalls = [ reliesOnCall: 'getRestApis', filterKey: 'restApiId', filterValue: 'id' + }, + getRequestValidators: { + reliesOnService: 'apigateway', + reliesOnCall: 'getRestApis', + filterKey: 'restApiId', + filterValue: 'id' } }, ApiGatewayV2: { diff --git a/helpers/aws/api_multipart.js b/helpers/aws/api_multipart.js index 9389518a67..7ae512be79 100644 --- a/helpers/aws/api_multipart.js +++ b/helpers/aws/api_multipart.js @@ -1199,6 +1199,12 @@ var postcalls = [ reliesOnCall: 'getRestApis', filterKey: 'restApiId', filterValue: 'id' + }, + getRequestValidators: { + reliesOnService: 'apigateway', + reliesOnCall: 'getRestApis', + filterKey: 'restApiId', + filterValue: 'id' } }, AppConfig: { diff --git a/helpers/azure/api.js b/helpers/azure/api.js index 150b7491a2..000b92f186 100644 --- a/helpers/azure/api.js +++ b/helpers/azure/api.js @@ -530,6 +530,11 @@ var calls = { url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Databricks/workspaces?api-version=2023-02-01' } }, + apiManagementService: { + list: { + url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.ApiManagement/service?api-version=2022-08-01' + } + }, // For CIEM aad: { listRoleAssignments: { @@ -564,6 +569,11 @@ var calls = { } }, + synapse: { + listWorkspaces: { + url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Synapse/workspaces?api-version=2021-06-01' + } + } }; @@ -1261,7 +1271,12 @@ var tertiarycalls = { reliesOnPath: 'batchAccounts.list', properties: ['id'], url: 'https://management.azure.com/{id}/providers/microsoft.insights/diagnosticSettings?api-version=2021-05-01-preview' - } + }, + listByMysqlFlexibleServer: { + reliesOnPath: 'servers.listMysqlFlexibleServer', + properties: ['id'], + url: 'https://management.azure.com/{id}/providers/microsoft.insights/diagnosticSettings?api-version=2021-05-01-preview' + }, }, backupShortTermRetentionPolicies: { listByDatabase: { diff --git a/helpers/azure/locations.js b/helpers/azure/locations.js index 280fb294ea..c376e3ded7 100644 --- a/helpers/azure/locations.js +++ b/helpers/azure/locations.js @@ -134,5 +134,7 @@ module.exports = { databricks: locations, containerApps: locations, batchAccounts: locations, - machineLearning: locations + machineLearning: locations, + apiManagementService: locations, + synapse: locations }; diff --git a/helpers/azure/locations_gov.js b/helpers/azure/locations_gov.js index 2e55d01403..2f71aa31d7 100644 --- a/helpers/azure/locations_gov.js +++ b/helpers/azure/locations_gov.js @@ -84,5 +84,7 @@ module.exports = { publicIpAddresses: locations, computeGalleries: locations, databricks: locations, - containerApps: locations + containerApps: locations, + apiManagementService: locations, + synapse: locations }; diff --git a/helpers/azure/resources.js b/helpers/azure/resources.js index 111bd05ee4..7c05970efa 100644 --- a/helpers/azure/resources.js +++ b/helpers/azure/resources.js @@ -306,5 +306,8 @@ module.exports = { }, machineLearning: { listWorkspaces: 'id' + }, + apiManagementService: { + list: 'id' } }; diff --git a/plugins/aws/apigateway/apigatewayRequestValidation.js b/plugins/aws/apigateway/apigatewayRequestValidation.js new file mode 100644 index 0000000000..4c56b20c3a --- /dev/null +++ b/plugins/aws/apigateway/apigatewayRequestValidation.js @@ -0,0 +1,72 @@ +var async = require('async'); +var helpers = require('../../../helpers/aws'); + +module.exports = { + title: 'API Gateway Request Validation', + category: 'API Gateway', + domain: 'Availability', + severity: 'Medium', + description: 'Ensures that Amazon API Gateway method has request validation enabled.', + more_info: 'Enabling request validation for API Gateway allows to perform basic validation of an API request before proceeding with the integration request and publishes the validation results in CloudWatch Logs. When request validation fails, API Gateway immediately fails the request reducing unnecessary calls to the backend.', + recommended_action: 'Modify API Gateway configuration and ensure that appropriate request validators are set for each API.', + link: 'https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-method-request-validation.html', + apis: ['APIGateway:getRestApis', 'APIGateway:getRequestValidators'], + realtime_triggers: ['apigateway:CreateRestApi','apigateway:DeleteRestApi','apigateway:ImportRestApi','apigateway:CreateRequestValidator','apigateway:UpdateRequestValidator','apigateway:DeleteRequestValidator'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var regions = helpers.regions(settings); + var awsOrGov = helpers.defaultPartition(settings); + + async.each(regions.apigateway, function(region, rcb){ + var getRestApis = helpers.addSource(cache, source, + ['apigateway', 'getRestApis', region]); + + if (!getRestApis) return rcb(); + + if (getRestApis.err || !getRestApis.data) { + helpers.addResult(results, 3, + `Unable to query for API Gateway Rest APIs: ${helpers.addError(getRestApis)}`, region); + return rcb(); + } + + if (!getRestApis.data.length) { + helpers.addResult(results, 0, 'No API Gateway Rest APIs found', region); + return rcb(); + } + + getRestApis.data.forEach(api => { + if (!api.id) return; + + var apiArn = `arn:${awsOrGov}:apigateway:${region}::/restapis/${api.id}`; + + var getRequestValidators = helpers.addSource(cache, source, + ['apigateway', 'getRequestValidators', region, api.id]); + + if (!getRequestValidators || getRequestValidators.err || !getRequestValidators.data || !getRequestValidators.data.items) { + helpers.addResult(results, 3, + `Unable to query for API Gateway Request Validators: ${helpers.addError(getRequestValidators)}`, + region, apiArn); + return; + } + + if (!getRequestValidators.data.items.length) { + helpers.addResult(results, 2, + 'No request validators found for API Gateway Rest API', + region, apiArn); + } else { + helpers.addResult(results, 0, + 'Request validators found for API Gateway Rest API', + region, apiArn); + } + }); + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; + + \ No newline at end of file diff --git a/plugins/aws/apigateway/apigatewayRequestValidaton.spec.js b/plugins/aws/apigateway/apigatewayRequestValidaton.spec.js new file mode 100644 index 0000000000..debe9d3f9b --- /dev/null +++ b/plugins/aws/apigateway/apigatewayRequestValidaton.spec.js @@ -0,0 +1,155 @@ +var expect = require('chai').expect; +var apigatewayRequestValidation = require('./apigatewayRequestValidation'); + +const createCache = (getRestApisData, getRequestValidatorsData) => { + if (getRestApisData && getRestApisData.length && getRestApisData[0].id) var restApiId = getRestApisData[0].id; + return { + apigateway: { + getRestApis: { + 'us-east-1': { + data: getRestApisData + } + }, + getRequestValidators: { + 'us-east-1': { + [restApiId]: { + data: { + items: getRequestValidatorsData + } + } + } + } + } + }; +}; + +const createErrorCache = () => { + return { + apigateway: { + getRestApis: { + 'us-east-1': { + err: { + message: 'error fetching API Gateway Rest APIs' + }, + }, + }, + getRequestValidators: { + 'us-east-1': { + err: { + message: 'error fetching API Gateway Request Validators' + }, + }, + } + + }, + }; +}; + +const createNullCache = () => { + return { + apigateway: { + getRestApis: { + 'us-east-1': null + }, + getRequestValidators: { + 'us-east-1': null + } + } + }; +}; + +describe('apigatewayRequestValidation', function () { + describe('run', function () { + it('should return UNKNOWN if unable to query for API Gateway Rest APIs', function (done) { + const cache = createErrorCache(); + apigatewayRequestValidation.run(cache, {} , (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(3); + expect(results[0].region).to.equal('us-east-1'); + expect (results[0].message).to.include('Unable to query for API Gateway Rest APIs:'); + done(); + }); + }); + + it('should return PASS if no API Gateway Rest APIs found', function (done) { + const cache = createCache([]); + apigatewayRequestValidation.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].region).to.equal('us-east-1'); + expect (results[0].message).to.include('No API Gateway Rest APIs found'); + done(); + }); + }); + + it('should return FAIL if no request validators exist for API Gateway Rest API', function (done) { + const getRestApisData = [ + { + id: 'api-id', + name: 'TestAPI', + description: 'Test API', + createdDate: 1621916018, + apiKeySource: 'HEADER', + endpointConfiguration: { + types: ['REGIONAL'] + } + } + ]; + const getRequestValidatorsData = []; + const cache = createCache(getRestApisData, getRequestValidatorsData); + apigatewayRequestValidation.run(cache,{}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].region).to.equal('us-east-1'); + expect (results[0].message).to.include('No request validators found for API Gateway Rest API'); + done(); + }); + }); + + it('should return PASS if validators exist for API Gateway Rest API', function (done) { + const getRestApisData = [ + { + id: 'api-id', + name: 'TestAPI', + description: 'Test API', + createdDate: 1621916018, + apiKeySource: 'HEADER', + endpointConfiguration: { + types: ['REGIONAL'] + } + } + ]; + const getRequestValidatorsData = [ + [ + { + id: "70wn19", + name: "Validate body", + validateRequestBody: true, + validateRequestParameters: false, + }, + { + id: "z06eap", + name: "Validate query string parameters and headers", + validateRequestBody: false, + validateRequestParameters: true, + }, + ] + ]; + const cache = createCache(getRestApisData, getRequestValidatorsData); + apigatewayRequestValidation.run(cache,{}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].region).to.equal('us-east-1'); + expect(results[0].message).to.include('Request validators found for API Gateway Rest API') + done(); + }); + }); + it('should not return anything if get Rest APIs response is not found', function (done) { + const cache = createNullCache(); + apigatewayRequestValidation.run(cache, {}, (err, results) => { + expect(results.length).to.equal(0); + done(); + }); + }); + }); +}); diff --git a/plugins/aws/codebuild/buildProjectEnvPriviligedMode.js b/plugins/aws/codebuild/buildProjectEnvPriviligedMode.js new file mode 100644 index 0000000000..4ea182abcf --- /dev/null +++ b/plugins/aws/codebuild/buildProjectEnvPriviligedMode.js @@ -0,0 +1,72 @@ +var async = require('async'); +var helpers = require('../../../helpers/aws'); + +module.exports = { + title: 'Build Project Environment Privileged Mode', + category: 'CodeBuild', + domain: 'Application Integration', + severity: 'Medium', + description: 'Ensure that your AWS CodeBuild build project environment has privileged mode disabled.', + more_info: 'Enabling privileged mode for CodeBuild project environments allows the build container to have elevated permissions on the host machine, which can potentially lead to security vulnerabilities and unauthorized access.', + recommended_action: 'Modify CodeBuild build project and disable environment privileged mode.', + link: 'https://docs.aws.amazon.com/codebuild/latest/userguide/change-project-console.html', + apis: ['CodeBuild:listProjects', 'CodeBuild:batchGetProjects','STS:GetCallerIdentity'], + realtime_triggers: ['codebuild:CreateProject', 'codebuild:UpdateProject','codebuild:DeleteProject'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var regions = helpers.regions(settings); + + var acctRegion = helpers.defaultRegion(settings); + var awsOrGov = helpers.defaultPartition(settings); + var accountId = helpers.addSource(cache, source, ['STS', 'GetCallerIdentity', acctRegion, 'data']); + + async.each(regions.codebuild, function(region, rcb){ + var listProjects = helpers.addSource(cache, source, + ['codebuild', 'listProjects', region]); + + if (!listProjects) return rcb(); + + if (listProjects.err || !listProjects.data) { + helpers.addResult(results, 3, + `Unable to list CodeBuild projects: ${helpers.addError(listProjects)}`, region); + return rcb(); + } + + if (!listProjects.data.length) { + helpers.addResult(results, 0, + 'No CodeBuild projects found', region); + return rcb(); + } + + for (let project of listProjects.data) { + var resource = `arn:${awsOrGov}:codebuild:${region}:${accountId}:project/${project}`; + + let batchGetProjects = helpers.addSource(cache, source, + ['codebuild', 'batchGetProjects', region, project]); + + if (!batchGetProjects || batchGetProjects.err || !batchGetProjects.data || + !batchGetProjects.data.projects || !batchGetProjects.data.projects.length) { + helpers.addResult(results, 3, + `Unable to query CodeBuild project: ${helpers.addError(batchGetProjects)}`, region, resource); + } else { + if (batchGetProjects.data.projects[0] && + batchGetProjects.data.projects[0].environment && + batchGetProjects.data.projects[0].environment.privilegedMode) { + helpers.addResult(results, 2, + 'CodeBuild project environment has privileged mode enabled', region, resource); + } else { + helpers.addResult(results, 0, + 'CodeBuild project environment has privileged mode disabled', region, resource); + } + } + + } + + rcb(); + }, function(){ + callback(null, results, source); + }); + } +}; \ No newline at end of file diff --git a/plugins/aws/codebuild/buildProjectEnvPriviligedMode.spec.js b/plugins/aws/codebuild/buildProjectEnvPriviligedMode.spec.js new file mode 100644 index 0000000000..652a67efe3 --- /dev/null +++ b/plugins/aws/codebuild/buildProjectEnvPriviligedMode.spec.js @@ -0,0 +1,145 @@ +var expect = require('chai').expect; +const buildProjectEnvPriviligedMode = require('./buildProjectEnvPriviligedMode'); + +const listProjects = [ + 'test-project' +]; + + +const batchGetProjects = [ + { + "projects": [ + { + "name": "test-project", + "arn": "arn:aws:codebuild:us-east-1:111122223333:project/test-project", + "environment": { + "type": "ARM_CONTAINER", + "image": "aws/codebuild/amazonlinux2-aarch64-standard:2.0", + "computeType": "BUILD_GENERAL1_SMALL", + "environmentVariables": [], + "privilegedMode": true, + "imagePullCredentialsType": "CODEBUILD" + }, + } + ], + }, + { + "projects": [ + { + "name": "test-project", + "arn": "arn:aws:codebuild:us-east-1:111122223333:project/test-project", + "environment": { + "type": "ARM_CONTAINER", + "image": "aws/codebuild/amazonlinux2-aarch64-standard:2.0", + "computeType": "BUILD_GENERAL1_SMALL", + "environmentVariables": [], + "privilegedMode": false, + "imagePullCredentialsType": "CODEBUILD" + }, + } + ], + } +] + +const createCache = (listProjects, batchGetProjects, listProjectsErr, batchGetProjectsErr) => { + let project = (listProjects && listProjects.length) ? listProjects[0] : null; + return { + codebuild: { + listProjects: { + 'us-east-1': { + data: listProjects, + err: listProjectsErr + } + }, + batchGetProjects: { + 'us-east-1': { + [project]: { + data: batchGetProjects, + err: batchGetProjectsErr + } + } + } + } + } +}; + +const createNullCache = () => { + return { + codebuild: { + listProjects: { + 'us-east-1': { 'err': 'Error listing batchProjects' }, + }, + }, + }; +}; + +describe('buildProjectEnvPriviligedMode', function () { + describe('run', function () { + + it('should PASS if no CodeBuild projects found', function (done) { + const cache = createCache([]); + buildProjectEnvPriviligedMode.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('No CodeBuild projects found'); + expect(results[0].region).to.equal('us-east-1'); + done(); + }); + }); + + it('should UNKNOWN if unable to list codebuild project', function (done) { + const cache = createNullCache(); + buildProjectEnvPriviligedMode.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 list CodeBuild projects:'); + expect(results[0].region).to.equal('us-east-1'); + done(); + }); + }); + + it('should UNKNOWN if unable to get CodeBuild project', function (done) { + const cache = createCache(listProjects, null, null, { message: 'Unable to query CodeBuild project' }); + buildProjectEnvPriviligedMode.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 CodeBuild project:'); + expect(results[0].region).to.equal('us-east-1'); + done(); + }); + }); + + it('should UNKNOWN if unable to get CodeBuild project', function (done) { + const cache = createCache(listProjects, null, null, { message: 'Unable to query CodeBuild project' }); + buildProjectEnvPriviligedMode.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 CodeBuild project:'); + expect(results[0].region).to.equal('us-east-1'); + done(); + }); + }); + + it('should FAIL if CodeBuild project environment has privileged mode enabled', function (done) { + const cache = createCache(listProjects, batchGetProjects[0], null, null); + buildProjectEnvPriviligedMode.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('CodeBuild project environment has privileged mode enabled'); + expect(results[0].region).to.equal('us-east-1'); + done(); + }); + }); + + it('should PASS if CodeBuild project environment has privileged mode disabled', function (done) { + const cache = createCache(listProjects, batchGetProjects[1], null, null); + buildProjectEnvPriviligedMode.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('CodeBuild project environment has privileged mode disabled'); + expect(results[0].region).to.equal('us-east-1'); + done(); + }); + }); + }); +}); diff --git a/plugins/aws/codebuild/codebuildProjectLoggingEnabled.js b/plugins/aws/codebuild/codebuildProjectLoggingEnabled.js new file mode 100644 index 0000000000..7a89db2003 --- /dev/null +++ b/plugins/aws/codebuild/codebuildProjectLoggingEnabled.js @@ -0,0 +1,75 @@ +var async = require('async'); +var helpers = require('../../../helpers/aws'); + +module.exports = { + title: 'CodeBuild Project Logging Enabled', + category: 'CodeBuild', + domain: 'Application Integration', + severity: 'Medium', + description: 'Ensure that your AWS CodeBuild build project has S3 or Cloudwatch logs enabled.', + more_info: 'Monitoring AWS CodeBuild projects helps maintaining the reliability, availability, and performance of the resource. It helps to easily debug multi-point failure and potential incidents.', + recommended_action: 'Ensure that CodeBuild project has logging enabled.', + link: 'https://docs.aws.amazon.com/codebuild/latest/userguide/monitoring-builds.html', + apis: ['CodeBuild:listProjects', 'CodeBuild:batchGetProjects', 'STS:GetCallerIdentity'], + realtime_triggers: ['codebuild:CreateProject', 'codebuild:UpdateProject', 'codebuild:DeleteProject'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var regions = helpers.regions(settings); + + var acctRegion = helpers.defaultRegion(settings); + var awsOrGov = helpers.defaultPartition(settings); + var accountId = helpers.addSource(cache, source, ['STS', 'GetCallerIdentity', acctRegion, 'data']); + + async.each(regions.codebuild, function(region, rcb) { + var listProjects =helpers.addSource(cache, source, + ['codebuild', 'listProjects', region]); + + if (!listProjects) return rcb(); + + if (listProjects.err || !listProjects.data) { + helpers.addResult(results, 3, + `Unable to list CodeBuild projects: ${helpers.addError(listProjects)}`, region); + return rcb(); + } + + if (!listProjects.data.length) { + helpers.addResult(results, 0, + 'No CodeBuild projects found', region); + return rcb(); + } + + for (let project of listProjects.data) { + var resource = `arn:${awsOrGov}:codebuild:${region}:${accountId}:project/${project}`; + + let batchGetProjects = helpers.addSource(cache, source, + ['codebuild', 'batchGetProjects', region, project]); + + if (!batchGetProjects || batchGetProjects.err || !batchGetProjects.data || + !batchGetProjects.data.projects || !batchGetProjects.data.projects.length) { + helpers.addResult(results, 3, + `Unable to query CodeBuild project: ${helpers.addError(batchGetProjects)}`, region, resource); + } else { + + var found = (batchGetProjects.data.projects[0] && + batchGetProjects.data.projects[0].logsConfig && + Object.values(batchGetProjects.data.projects[0].logsConfig).some(log => log.status === 'ENABLED')) || false; + + if (found) { + helpers.addResult(results, 0, + 'CodeBuild project has logging enabled', region, resource); + } else { + helpers.addResult(results, 2, + 'CodeBuild project does not have logging enabled', region, resource); + } + } + + } + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; \ No newline at end of file diff --git a/plugins/aws/codebuild/codebuildProjectLoggingEnabled.spec.js b/plugins/aws/codebuild/codebuildProjectLoggingEnabled.spec.js new file mode 100644 index 0000000000..8a4f592701 --- /dev/null +++ b/plugins/aws/codebuild/codebuildProjectLoggingEnabled.spec.js @@ -0,0 +1,163 @@ +var expect = require('chai').expect; +const codebuildProjectLoggingEnabled = require('./codebuildProjectLoggingEnabled'); + +const listProjects = [ + 'test-project' +]; + + +const batchGetProjects = [ + { + "projects": [ + { + "name": "test-project", + "arn": "arn:aws:codebuild:us-east-1:111122223333:project/test-project", + "environment": { + "type": "ARM_CONTAINER", + "image": "aws/codebuild/amazonlinux2-aarch64-standard:2.0", + "computeType": "BUILD_GENERAL1_SMALL", + "environmentVariables": [], + "privilegedMode": true, + "imagePullCredentialsType": "CODEBUILD" + }, + "logsConfig": { + "cloudWatchLogs": { + "status": "DISABLED", + }, + "s3Logs": { + "status": "DISABLED", + "encryptionDisabled": false, + }, + } + } + ], + }, + { + "projects": [ + { + "name": "test-project", + "arn": "arn:aws:codebuild:us-east-1:111122223333:project/test-project", + "environment": { + "type": "ARM_CONTAINER", + "image": "aws/codebuild/amazonlinux2-aarch64-standard:2.0", + "computeType": "BUILD_GENERAL1_SMALL", + "environmentVariables": [], + "privilegedMode": false, + "imagePullCredentialsType": "CODEBUILD" + }, + "logsConfig": { + "cloudWatchLogs": { + "status": "ENABLED", + }, + "s3Logs": { + "status": "DISABLED", + "encryptionDisabled": false, + }, + } + } + ], + } +] + +const createCache = (listProjects, batchGetProjects, listProjectsErr, batchGetProjectsErr) => { + let project = (listProjects && listProjects.length) ? listProjects[0] : null; + return { + codebuild: { + listProjects: { + 'us-east-1': { + data: listProjects, + err: listProjectsErr + } + }, + batchGetProjects: { + 'us-east-1': { + [project]: { + data: batchGetProjects, + err: batchGetProjectsErr + } + } + } + } + } +}; + +const createNullCache = () => { + return { + codebuild: { + listProjects: { + 'us-east-1': { 'err': 'Error listing batchProjects' }, + }, + }, + }; +}; + +describe('codebuildProjectLoggingEnabled', function () { + describe('run', function () { + + it('should PASS if no CodeBuild projects found', function (done) { + const cache = createCache([]); + codebuildProjectLoggingEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('No CodeBuild projects found'); + expect(results[0].region).to.equal('us-east-1'); + done(); + }); + }); + + it('should UNKNOWN if unable to list codebuild project', function (done) { + const cache = createNullCache(); + codebuildProjectLoggingEnabled.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 list CodeBuild projects:'); + expect(results[0].region).to.equal('us-east-1'); + done(); + }); + }); + + it('should UNKNOWN if unable to get CodeBuild project', function (done) { + const cache = createCache(listProjects, null, null, { message: 'Unable to query CodeBuild project' }); + codebuildProjectLoggingEnabled.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 CodeBuild project:'); + expect(results[0].region).to.equal('us-east-1'); + done(); + }); + }); + + it('should UNKNOWN if unable to get CodeBuild project', function (done) { + const cache = createCache(listProjects, null, null, { message: 'Unable to query CodeBuild project' }); + codebuildProjectLoggingEnabled.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 CodeBuild project:'); + expect(results[0].region).to.equal('us-east-1'); + done(); + }); + }); + + it('should FAIL if CodeBuild project does not have logging enabled', function (done) { + const cache = createCache(listProjects, batchGetProjects[0], null, null); + codebuildProjectLoggingEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('CodeBuild project does not have logging enabled'); + expect(results[0].region).to.equal('us-east-1'); + done(); + }); + }); + + it('should PASS if CodeBuild project has logging enabled', function (done) { + const cache = createCache(listProjects, batchGetProjects[1], null, null); + codebuildProjectLoggingEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('CodeBuild project has logging enabled'); + expect(results[0].region).to.equal('us-east-1'); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/plugins/aws/documentDB/docdbClusterProfilerEnabled.js b/plugins/aws/documentDB/docdbClusterProfilerEnabled.js new file mode 100644 index 0000000000..35d490c472 --- /dev/null +++ b/plugins/aws/documentDB/docdbClusterProfilerEnabled.js @@ -0,0 +1,56 @@ +var async = require('async'); +var helpers = require('../../../helpers/aws'); + +module.exports = { + title: 'DocumentDB Cluster Profiler Enabled', + category: 'DocumentDB', + domain: 'Databases', + severity: 'Low', + description: 'Ensure that Amazon DocumentDB clusters have profiler feature enabled.', + more_info: 'Enabling the Profiler for your Amazon DocumentDB clusters helps to monitor and log database operations. This makes it easier to identify slowest operations on cluster and fix performance issues by analyzing detailed logs in Amazon CloudWatch.', + recommended_action: 'Modify DocumentDB cluster and enable profiler feature.', + link: 'https://docs.aws.amazon.com/documentdb/latest/developerguide/profiling.html', + apis: ['DocDB:describeDBClusters'], + realtime_triggers: ['docdb:CreateDBCluster','docdb:ModifyDBCluster','docdb:DeleteDBCluster'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var regions = helpers.regions(settings); + + async.each(regions.docdb, function(region, rcb){ + var describeDBClusters = helpers.addSource(cache, source, + ['docdb', 'describeDBClusters', region]); + + if (!describeDBClusters) return rcb(); + + if (describeDBClusters.err || !describeDBClusters.data) { + helpers.addResult(results, 3, + `Unable to list DocumentDB clusters: ${helpers.addError(describeDBClusters)}`, region); + return rcb(); + } + + if (!describeDBClusters.data.length) { + helpers.addResult(results, 0, + 'No DocumentDB clusters found', region); + return rcb(); + } + + for (let cluster of describeDBClusters.data) { + if (!cluster.DBClusterArn) continue; + + if (cluster.EnabledCloudwatchLogsExports && + cluster.EnabledCloudwatchLogsExports.length && + cluster.EnabledCloudwatchLogsExports.includes('profiler')) { + helpers.addResult(results, 0, 'DocumentDB cluster has profiler feature enabled', region, cluster.DBClusterArn); + } else { + helpers.addResult(results, 2, 'DocumentDB cluster does not have profiler feature enabled', region, cluster.DBClusterArn); + } + } + + rcb(); + }, function(){ + callback(null, results, source); + }); + } +}; diff --git a/plugins/aws/documentDB/docdbClusterProfilerEnabled.spec.js b/plugins/aws/documentDB/docdbClusterProfilerEnabled.spec.js new file mode 100644 index 0000000000..415457b032 --- /dev/null +++ b/plugins/aws/documentDB/docdbClusterProfilerEnabled.spec.js @@ -0,0 +1,88 @@ +var expect = require('chai').expect; +var docdbProfilerEnabled = require('./docdbClusterProfilerEnabled'); + +const describeDBClusters = [ + { + AvailabilityZones: [], + BackupRetentionPeriod: 1, + DBClusterArn: 'arn:aws:rds:us-east-1:000011112222:cluster:docdb-2021-11-10-10-16-10', + DBClusterIdentifier: 'docdb-2021-11-10-10-16-10', + DBClusterParameterGroup: 'default.docdb4.0', + DBSubnetGroup: 'default-vpc-99de2fe4', + Status: 'available', + DeletionProtection: true, + EnabledCloudwatchLogsExports: [ "audit", "profiler"] + }, + { + AvailabilityZones: [], + BackupRetentionPeriod: 10, + DBClusterArn: 'arn:aws:rds:us-east-1:000011112223:cluster:docdb-2021-11-10-10-16-10', + DBClusterIdentifier: 'docdb-2021-11-10-10-16-10', + DBClusterParameterGroup: 'default.docdb4.0', + DBSubnetGroup: 'default-vpc-99de2fe4', + Status: 'available', + DeletionProtection: false, + EnabledCloudwatchLogsExports: [ "audit"] + } +]; + +const createCache = (clusters, clustersErr) => { + return { + docdb: { + describeDBClusters: { + 'us-east-1': { + err: clustersErr, + data: clusters + }, + }, + } + }; +}; + +describe('docdbProfilerEnabled', function () { + describe('run', function () { + it('should PASS if DocumentDb Cluster has profiler feature enabled', function (done) { + const cache = createCache([describeDBClusters[0]]); + docdbProfilerEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('DocumentDB cluster has profiler feature enabled'); + expect(results[0].region).to.equal('us-east-1'); + done(); + }); + }); + + it('should FAIL if DocumentDB Clusters does not have profiler feature enabled', function (done) { + const cache = createCache([describeDBClusters[1]]); + docdbProfilerEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('DocumentDB cluster does not have profiler feature enabled'); + expect(results[0].region).to.equal('us-east-1'); + done(); + }); + }); + + it('should PASS if no DocumentDB Clusters found', function (done) { + const cache = createCache([]); + docdbProfilerEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('No DocumentDB clusters found'); + expect(results[0].region).to.equal('us-east-1'); + done(); + }); + }); + + it('should UNKNOWN if unable to list DocumentDB Clusters', function (done) { + const cache = createCache(null, { message: "Unable to list DocumentDB Clusters encryption" }); + docdbProfilerEnabled.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 list DocumentDB clusters:'); + expect(results[0].region).to.equal('us-east-1'); + done(); + }); + }); + }); +}); diff --git a/plugins/aws/guardduty/lambdaProtectionEnabled.js b/plugins/aws/guardduty/lambdaProtectionEnabled.js new file mode 100644 index 0000000000..7e960bc4c8 --- /dev/null +++ b/plugins/aws/guardduty/lambdaProtectionEnabled.js @@ -0,0 +1,69 @@ +var async = require('async'); +var helpers = require('../../../helpers/aws'); + +module.exports = { + title: 'GuardDuty Lambda Protection Enabled', + category: 'GuardDuty', + domain: 'Management and Governance', + severity: 'Medium', + description: 'Ensures GuardDuty protection is enabled for Lambda functions.' , + more_info: 'Enabling GuardDuty Lambda Protection helps detect potential security threats offering enhanced security by monitoring network activity logs and generating findings for suspicious activities and security issues.', + recommended_action: 'Enable GuardDuty Lambda protection for all AWS accounts.', + link: 'https://docs.aws.amazon.com/guardduty/latest/ug/lambda-protection.html', + apis: ['GuardDuty:listDetectors', 'GuardDuty:getDetector', 'STS:getCallerIdentity'], + realtime_triggers: ['guardduty:CreateDetector', 'guardduty:UpdateDetector', 'guardduty:DeleteDetector'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + + var acctRegion = helpers.defaultRegion(settings); + var awsOrGov = helpers.defaultPartition(settings); + var accountId = helpers.addSource(cache, source, ['sts', 'getCallerIdentity', acctRegion, 'data']); + + var regions = helpers.regions(settings); + + async.each(regions.guardduty, function(region, rcb) { + var listDetectors = helpers.addSource(cache, source, ['guardduty', 'listDetectors', region]); + + if (!listDetectors) return rcb(); + + if (listDetectors.err || !listDetectors.data) { + helpers.addResult(results, 3, 'Unable to list GuardDuty detectors: ' + helpers.addError(listDetectors), region); + return rcb(); + } + + if (!listDetectors.data || !listDetectors.data.length) { + helpers.addResult(results, 0, 'No GuardDuty detectors found', region); + return rcb(); + } + + listDetectors.data.forEach(function(detectorId) { + + var getDetector = helpers.addSource(cache, source, ['guardduty', 'getDetector', region, detectorId]); + + if (!getDetector) return; + + if (getDetector.err || !getDetector.data) { + helpers.addResult(results, 3, 'Unable to get GuardDuty detector: ' + helpers.addError(getDetector),region, detectorId); + return; + } + + var detector = getDetector.data; + var resource = `arn:${awsOrGov}:guardduty:${region}:${accountId}:detector/${detector.detectorId}`; + var lambdaLoginEventsFeature = (detector.Features && detector.Features.find(feature => feature.Name === 'LAMBDA_NETWORK_LOGS' && feature.Status === 'ENABLED')) ? true : false; + + if (lambdaLoginEventsFeature) { + helpers.addResult(results, 0, 'GuardDuty Lambda protection is enabled' , region, resource); + } else { + helpers.addResult(results, 2, 'GuardDuty Lambda protection is disabled ' , region, resource); + } + + }); + + rcb(); + }, function(){ + callback(null, results, source); + }); + } +}; diff --git a/plugins/aws/guardduty/lambdaProtectionEnabled.spec.js b/plugins/aws/guardduty/lambdaProtectionEnabled.spec.js new file mode 100644 index 0000000000..f35b1e651b --- /dev/null +++ b/plugins/aws/guardduty/lambdaProtectionEnabled.spec.js @@ -0,0 +1,218 @@ +var expect = require('chai').expect; +var lambdaProtectionEnabled = require('./lambdaProtectionEnabled'); + +const listDetectors = [ + "6cc45a4adb18e50f5ba51f6800db03d8" +]; + +const getDetector = [ + { + "CreatedAt": "2021-11-16T15:54:17.530Z", + "FindingPublishingFrequency": "SIX_HOURS", + "ServiceRole": "arn:aws:iam::000011112222:role/aws-service-role/guardduty.amazonaws.com/AWSServiceRoleForAmazonGuardDuty", + "Status": "ENABLED", + "UpdatedAt": "2021-12-01T14:13:59.029Z", + "Features": [ + { + "Name": "CLOUD_TRAIL", + "Status": "ENABLED", + "UpdatedAt": "2024-03-25T14:44:02+05:00" + }, + { + "Name": "DNS_LOGS", + "Status": "ENABLED", + "UpdatedAt": "2024-03-25T14:44:02+05:00" + }, + { + "Name": "FLOW_LOGS", + "Status": "ENABLED", + "UpdatedAt": "2024-03-25T14:44:02+05:00" + }, + { + "Name": "S3_DATA_EVENTS", + "Status": "DISABLED", + "UpdatedAt": "2024-03-25T14:19:28+05:00" + }, + { + "Name": "EKS_AUDIT_LOGS", + "Status": "DISABLED", + "UpdatedAt": "2024-03-25T14:19:34+05:00" + }, + { + "Name": "EBS_MALWARE_PROTECTION", + "Status": "ENABLED", + "UpdatedAt": "2024-03-25T14:14:46+05:00" + }, + { + "Name": "RDS_LOGIN_EVENTS", + "Status": "ENABLED", + "UpdatedAt": "2024-03-25T14:40:00+05:00" + }, + { + "Name": "LAMBDA_NETWORK_LOGS", + "Status": "ENABLED", + "UpdatedAt": "2024-03-25T14:40:00+05:00" + }, + ], + + "Tags": {} + }, + { + "CreatedAt": "2021-11-16T15:54:17.530Z", + "FindingPublishingFrequency": "SIX_HOURS", + "ServiceRole": "arn:aws:iam::000011112222:role/aws-service-role/guardduty.amazonaws.com/AWSServiceRoleForAmazonGuardDuty", + "Status": "ENABLED", + "UpdatedAt": "2021-12-01T14:13:59.029Z", + "Features": [ + { + "Name": "CLOUD_TRAIL", + "Status": "ENABLED", + "UpdatedAt": "2024-03-25T14:44:02+05:00" + }, + { + "Name": "DNS_LOGS", + "Status": "ENABLED", + "UpdatedAt": "2024-03-25T14:44:02+05:00" + }, + { + "Name": "FLOW_LOGS", + "Status": "ENABLED", + "UpdatedAt": "2024-03-25T14:44:02+05:00" + }, + { + "Name": "S3_DATA_EVENTS", + "Status": "DISABLED", + "UpdatedAt": "2024-03-25T14:19:28+05:00" + }, + { + "Name": "EKS_AUDIT_LOGS", + "Status": "DISABLED", + "UpdatedAt": "2024-03-25T14:19:34+05:00" + }, + { + "Name": "EBS_MALWARE_PROTECTION", + "Status": "ENABLED", + "UpdatedAt": "2024-03-25T14:14:46+05:00" + }, + { + "Name": "LAMBDA_NETWORK_LOGS", + "Status": "DISABLED", + "UpdatedAt": "2024-03-25T14:40:00+05:00" + }, + ], + "Tags": {} + } +]; + +const createCache = (listDetectors, getDetector) => { + let detectorId = (listDetectors.length) ? listDetectors[0] : null; + return { + guardduty: { + listDetectors: { + 'us-east-1': { + data: listDetectors + }, + }, + getDetector: { + 'us-east-1': { + [detectorId]: { + data: getDetector + } + } + } + } + }; +}; + +const createErrorCache = () => { + return { + guardduty: { + listDetectors: { + 'us-east-1': { + err: { + message: 'error desribing cache clusters' + }, + }, + }, + }, + }; +}; + +const createNullCache = () => { + return { + guardduty: { + listDetectors: { + 'us-east-1': null + } + } + }; +}; + + +describe('lambdaProtectionEnabled', function () { + describe('run', function () { + it('should FAIL if GuardDuty lambda protection is diabled', function (done) { + const cache = createCache(listDetectors, getDetector[1],); + lambdaProtectionEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].region).to.equal('us-east-1'); + expect(results[0].message).to.include('GuardDuty Lambda protection is disabled'); + done(); + }); + }); + + it('should PASS if GuardDuty lambda protection is enabled', function (done) { + const cache = createCache(listDetectors, getDetector[0]); + lambdaProtectionEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].region).to.equal('us-east-1'); + expect(results[0].message).to.include('GuardDuty Lambda protection is enabled'); + done(); + }); + }); + + it('should PASS if no detectors found', function (done) { + const cache = createCache([]); + lambdaProtectionEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].region).to.equal('us-east-1'); + expect(results[0].message).to.include('No GuardDuty detectors found'); + done(); + }); + }); + + it('should UNKNOWN unable to list GuardDuty detector', function (done) { + const cache = createErrorCache(); + lambdaProtectionEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(3); + expect(results[0].region).to.equal('us-east-1'); + expect(results[0].message).to.include('Unable to list GuardDuty detectors:'); + done(); + }); + }); + + it('should UNKNOWN unable to get GuardDuty detector', function (done) { + const cache = createCache([listDetectors]); + lambdaProtectionEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(3); + expect(results[0].region).to.equal('us-east-1'); + expect(results[0].message).to.include('Unable to get GuardDuty detector: '); + done(); + }); + }); + + it('should not return any result if list dectectors response not found', function (done) { + const cache = createNullCache(); + lambdaProtectionEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(0); + done(); + }); + }); + }); +}); + diff --git a/plugins/aws/healthlake/dataStoreHasTags.js b/plugins/aws/healthlake/dataStoreHasTags.js new file mode 100644 index 0000000000..80c7828fe8 --- /dev/null +++ b/plugins/aws/healthlake/dataStoreHasTags.js @@ -0,0 +1,51 @@ +var async = require('async'); +var helpers = require('../../../helpers/aws'); + +module.exports = { + title: 'HealthLake Data Store Has Tags', + category: 'AI & ML', + domain: 'Content Delivery', + severity: 'Low', + description: 'Ensure that HealthLake Data Store has tags associated.', + more_info: 'Tags help you to group resources together that are related to or associated with each other. It is a best practice to tag cloud resources to better organize and gain visibility into their usage.', + recommended_action: 'Modify HealthLake data store and add tags.', + link: 'https://docs.aws.amazon.com/healthlake/latest/devguide/add-a-tag.html', + apis: ['HealthLake:listFHIRDatastores', 'ResourceGroupsTaggingAPI:getResources'], + realtime_triggers: ['healthlake:CreateFHIRDatastore', 'healthlake:DeleteFHIRDatastore', 'healthlake:TagResource', 'healthlake:UntagResource'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var regions = helpers.regions(settings); + + async.each(regions.healthlake, function(region, rcb){ + var listFHIRDatastores = helpers.addSource(cache, source, + ['healthlake', 'listFHIRDatastores', region]); + + if (!listFHIRDatastores) return rcb(); + + if (listFHIRDatastores.err || !listFHIRDatastores.data) { + helpers.addResult(results, 3, `Unable to query HealthLake Data Store: ${helpers.addError(listFHIRDatastores)}`, region); + return rcb(); + } + + if (!listFHIRDatastores.data.length) { + helpers.addResult(results, 0, 'No HealthLake data stores found', region); + return rcb(); + } + + const arnList = []; + for (let datastore of listFHIRDatastores.data){ + if (!datastore.DatastoreArn) continue; + + arnList.push(datastore.DatastoreArn); + } + + helpers.checkTags(cache, 'Healthlake data store', arnList, region, results, settings); + return rcb(); + + }, function(){ + callback(null, results, source); + }); + } +}; \ No newline at end of file diff --git a/plugins/aws/healthlake/dataStoreHasTags.spec.js b/plugins/aws/healthlake/dataStoreHasTags.spec.js new file mode 100644 index 0000000000..a83e8f8f12 --- /dev/null +++ b/plugins/aws/healthlake/dataStoreHasTags.spec.js @@ -0,0 +1,128 @@ +var expect = require('chai').expect; +const dataStoreHasTags = require('./dataStoreHasTags'); + +const listFHIRDatastores = [ + { + "DatastoreId": "7ad17b6c9d48056865a8800b86cc2797", + "DatastoreArn": "arn:aws:healthlake:us-east-1:000111222333:datastore/fhir/7ad17b6c9d48056865a8800b86cc2797", + "DatastoreName": "sadeed-ds1", + "DatastoreStatus": "ACTIVE", + "CreatedAt": "2021-11-23T15:31:55.180000+05:00", + "DatastoreTypeVersion": "R4", + "DatastoreEndpoint": "https://healthlake.us-east-1.amazonaws.com/datastore/7ad17b6c9d48056865a8800b86cc2797/r4/", + "SseConfiguration": { + "KmsEncryptionConfig": { + "CmkType": "CUSTOMER_MANAGED_KMS_KEY", + "KmsKeyId": "arn:aws:kms:us-east-1:000111222333:key/ad013a33-b01d-4d88-ac97-127399c18b3e" + } + } + }, + { + "DatastoreId": "7ad17b6c9d48056865a8800b86cc2797", + "DatastoreArn": "arn:aws:healthlake:us-east-1:000111222333:datastore/fhir/7ad17b6c9d48056865a880", + "DatastoreName": "sadeed-ds1", + "DatastoreStatus": "ACTIVE", + "CreatedAt": "2021-11-23T15:31:55.180000+05:00", + "DatastoreTypeVersion": "R4", + "DatastoreEndpoint": "https://healthlake.us-east-1.amazonaws.com/datastore/7ad17b6c9d48056865a8800b86cc2797/r4/", + "SseConfiguration": { + "KmsEncryptionConfig": { + "CmkType": "CUSTOMER_MANAGED_KMS_KEY", + "KmsKeyId": "arn:aws:kms:us-east-1:000111222333:key/ad013a33-b01d-4d88-ac97-127399c18b3e" + } + } + }, +]; + +const getResources = [ + { + "ResourceARN": "arn:aws:healthlake:us-east-1:000111222333:datastore/fhir/7ad17b6c9d48056865a880", + "Tags": [], + }, + { + "ResourceARN": "arn:aws:healthlake:us-east-1:000111222333:datastore/fhir/7ad17b6c9d48056865a8800b86cc2797", + "Tags": [{key: 'value'}], + } +] + + +const createCache = (datastore, rgData) => { + return { + healthlake: { + listFHIRDatastores: { + 'us-east-1': { + err: null, + data: datastore + }, + }, + }, + resourcegroupstaggingapi: { + getResources: { + 'us-east-1':{ + err: null, + data: rgData + } + } + }, + }; +}; + + +describe('dataStoreHasTags', function () { + describe('run', function () { + it('should PASS if Bedrock custom model has tags', function (done) { + const cache = createCache([listFHIRDatastores[0]], [getResources[1]]); + dataStoreHasTags.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].region).to.equal('us-east-1'); + expect(results[0].message).to.include('Healthlake data store has tags') + done(); + }); + }); + + it('should FAIL if Bedrock custom model doesnot have tags', function (done) { + const cache = createCache([listFHIRDatastores[0]], [getResources[0]]); + dataStoreHasTags.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].region).to.equal('us-east-1'); + expect(results[0].message).to.include('Healthlake data store does not have any tags') + done(); + }); + }); + + it('should PASS if no Bedrock custom model found', function (done) { + const cache = createCache([]); + dataStoreHasTags.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].region).to.equal('us-east-1'); + expect(results[0].message).to.include('No HealthLake data stores found'); + done(); + }); + }); + + it('should UNKNOWN if unable to query Bedrock custom model', function (done) { + const cache = createCache(null, null); + dataStoreHasTags.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(3); + expect(results[0].region).to.equal('us-east-1'); + expect(results[0].message).to.include('Unable to query HealthLake Data Store: ') + done(); + }); + }); + + it('should give unknown result if unable to query resource group tagging api', function (done) { + const cache = createCache([listFHIRDatastores[0]],null); + dataStoreHasTags.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(3); + expect(results[0].region).to.equal('us-east-1'); + expect(results[0].message).to.include('Unable to query all resources') + done(); + }); + }); + }); +}); diff --git a/plugins/azure/apiManagement/apiInstanceHasTags.js b/plugins/azure/apiManagement/apiInstanceHasTags.js new file mode 100644 index 0000000000..f1903dcc30 --- /dev/null +++ b/plugins/azure/apiManagement/apiInstanceHasTags.js @@ -0,0 +1,52 @@ +var async = require('async'); +var helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'API Management Instance Has Tags', + category: 'API Management', + domain: 'Developer Tools', + severity: 'Medium', + description: 'Ensures that Azure API Management instance has tags associated.', + more_info: 'Tags help you to group resources together that are related to or associated with each other. It is a best practice to tag cloud resources to better organize and gain visibility into their usage.', + link: 'https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/tag-resources', + recommended_action: 'Modify API Management instance and add tags.', + apis: ['apiManagementService:list'], + realtime_triggers: ['microsoftapimanagement:service:write','microsoftapimanagement:service:delete','microsoftresources:tags:write'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var locations = helpers.locations(settings.govcloud); + + async.each(locations.apiManagementService, function(location, rcb){ + var apiManagementService = helpers.addSource(cache, source, + ['apiManagementService', 'list', location]); + + if (!apiManagementService) return rcb(); + + if (apiManagementService.err || !apiManagementService.data) { + helpers.addResult(results, 3, 'Unable to query API Management instances:' + helpers.addError(apiManagementService), location); + return rcb(); + } + + if (!apiManagementService.data.length) { + helpers.addResult(results, 0, 'No existing API Management instances found', location); + return rcb(); + } + + for (let apiInstance of apiManagementService.data) { + if (!apiInstance.id) continue; + + if (apiInstance.tags && Object.entries(apiInstance.tags).length > 0) { + helpers.addResult(results, 0, 'API Management instance has tags associated', location, apiInstance.id); + } else { + helpers.addResult(results, 2, 'API Management instance does not have tags associated', location, apiInstance.id); + } + } + + rcb(); + }, function(){ + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/apiManagement/apiInstanceHasTags.spec.js b/plugins/azure/apiManagement/apiInstanceHasTags.spec.js new file mode 100644 index 0000000000..b8575e34e9 --- /dev/null +++ b/plugins/azure/apiManagement/apiInstanceHasTags.spec.js @@ -0,0 +1,101 @@ +var expect = require('chai').expect; +var apiInstanceHasTags = require('./apiInstanceHasTags.js'); + +const apiManagementService = [ + { + "etag": "AAAAAAGIUI4=", + "publisherEmail": "dummy.@aquasec.com", + "publisherName": "dummy", + "notificationSenderEmail": "apimgmt-noreply@mail.windowsazure.com", + "provisioningState": "Succeeded", + "targetProvisioningState": "", + "identity": null, + "zones": null, + "tags": {}, + "location": "East US", + "id": "/subscriptions/123456/resourceGroups/testfunction_group/providers/Microsoft.ApiManagement/service/test", + "name": "meerab", + "type": "Microsoft.ApiManagement/service" + }, + { + "etag": "AAAAAAGIUI4=", + "publisherEmail": "dummy.@aquasec.com", + "publisherName": "dummy", + "notificationSenderEmail": "apimgmt-noreply@mail.windowsazure.com", + "provisioningState": "Succeeded", + "targetProvisioningState": "", + "identity": { + "type": "SystemAssigned", + "principalId": "fdd1f197-d0e0-4d04-a5ef-9dbb654afd14", + "tenantId": "d207c7bd-fcb1-4dd3-855a-cfd2f9b651e8" + }, + "zones": null, + "location": "East US", + "tags": {"key": "value"}, + "id": "/subscriptions/123456/resourceGroups/testfunction_group/providers/Microsoft.ApiManagement/service/test", + "name": "meerab", + "type": "Microsoft.ApiManagement/service" + } +]; + +const createCache = (apiManagementService, err) => { + return { + apiManagementService: { + list: { + 'eastus': { + data: apiManagementService, + err: err + } + } + } + } +}; + +describe('apiInstanceHasTags', function () { + describe('run', function () { + + it('should give pass result if No existing API Management service instances found', function (done) { + const cache = createCache([]); + apiInstanceHasTags.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 API Management instances found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give unknown result if Unable to query API Management service instances', function (done) { + const cache = createCache(null, 'Error'); + apiInstanceHasTags.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 API Management instances'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if API Management service instances has tags associated', function (done) { + const cache = createCache([apiManagementService[1]]); + apiInstanceHasTags.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('API Management instance has tags associated'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if API Management service instances does not have tags associated', function (done) { + const cache = createCache([apiManagementService[0]]); + apiInstanceHasTags.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('API Management instance does not have tags associated'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/plugins/azure/apiManagement/apiInstanceManagedIdentity.js b/plugins/azure/apiManagement/apiInstanceManagedIdentity.js new file mode 100644 index 0000000000..57557b0183 --- /dev/null +++ b/plugins/azure/apiManagement/apiInstanceManagedIdentity.js @@ -0,0 +1,52 @@ +var async = require('async'); +var helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'API Management Instance Managed Identity', + category: 'API Management', + domain: 'Developer Tools', + severity: 'Medium', + description: 'Ensures that Azure API Management instance has managed identity enabled.', + more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.', + link: 'https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-use-managed-service-identity', + recommended_action: 'Modify API Management instance and add managed identity.', + apis: ['apiManagementService:list'], + realtime_triggers: ['microsoftapimanagement:service:write','microsoftapimanagement:service:delete'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var locations = helpers.locations(settings.govcloud); + + async.each(locations.apiManagementService, function(location, rcb){ + var apiManagementService = helpers.addSource(cache, source, + ['apiManagementService', 'list', location]); + + if (!apiManagementService) return rcb(); + + if (apiManagementService.err || !apiManagementService.data) { + helpers.addResult(results, 3, 'Unable to query API Management instances:' + helpers.addError(apiManagementService), location); + return rcb(); + } + + if (!apiManagementService.data.length) { + helpers.addResult(results, 0, 'No existing API Management instances found', location); + return rcb(); + } + + for (let apiInstance of apiManagementService.data) { + if (!apiInstance.id) continue; + + if (apiInstance.identity) { + helpers.addResult(results, 0, 'API Management instance has managed identity enabled', location, apiInstance.id); + } else { + helpers.addResult(results, 2, 'API Management instance does not have managed identity enabled', location, apiInstance.id); + } + } + + rcb(); + }, function(){ + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/apiManagement/apiInstanceManagedIdentity.spec.js b/plugins/azure/apiManagement/apiInstanceManagedIdentity.spec.js new file mode 100644 index 0000000000..145736284f --- /dev/null +++ b/plugins/azure/apiManagement/apiInstanceManagedIdentity.spec.js @@ -0,0 +1,101 @@ +var expect = require('chai').expect; +var apiInstanceManagedIdentity = require('./apiInstanceManagedIdentity.js'); + +const apiManagementService = [ + { + "etag": "AAAAAAGIUI4=", + "publisherEmail": "dummy.@aquasec.com", + "publisherName": "dummy", + "notificationSenderEmail": "apimgmt-noreply@mail.windowsazure.com", + "provisioningState": "Succeeded", + "targetProvisioningState": "", + "identity": null, + "zones": null, + "location": "East US", + "tags": {}, + "id": "/subscriptions/123456/resourceGroups/testfunction_group/providers/Microsoft.ApiManagement/service/test", + "name": "meerab", + "type": "Microsoft.ApiManagement/service" + }, + { + "etag": "AAAAAAGIUI4=", + "publisherEmail": "dummy.@aquasec.com", + "publisherName": "dummy", + "notificationSenderEmail": "apimgmt-noreply@mail.windowsazure.com", + "provisioningState": "Succeeded", + "targetProvisioningState": "", + "identity": { + "type": "SystemAssigned", + "principalId": "fdd1f197-d0e0-4d04-a5ef-9dbb654afd14", + "tenantId": "d207c7bd-fcb1-4dd3-855a-cfd2f9b651e8" + }, + "zones": null, + "location": "East US", + "tags": {}, + "id": "/subscriptions/123456/resourceGroups/testfunction_group/providers/Microsoft.ApiManagement/service/test", + "name": "meerab", + "type": "Microsoft.ApiManagement/service" + } +]; + +const createCache = (apiManagementService, err) => { + return { + apiManagementService: { + list: { + 'eastus': { + data: apiManagementService, + err: err + } + } + } + } +}; + +describe('apiInstanceManagedIdentity', function () { + describe('run', function () { + + it('should give pass result if No existing API Management service instances found', function (done) { + const cache = createCache([]); + apiInstanceManagedIdentity.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 API Management instances found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give unknown result if Unable to query API Management service instances:', function (done) { + const cache = createCache(null, 'Error'); + apiInstanceManagedIdentity.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 API Management instances:'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if API Management service instances has managed identity enabled', function (done) { + const cache = createCache([apiManagementService[1]]); + apiInstanceManagedIdentity.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('API Management instance has managed identity enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if API Management service instances does not have managed identity enabled', function (done) { + const cache = createCache([apiManagementService[0]]); + apiInstanceManagedIdentity.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('API Management instance does not have managed identity enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/plugins/azure/batchAccounts/batchAccountsAADEnabled.js b/plugins/azure/batchAccounts/batchAccountsAADEnabled.js new file mode 100644 index 0000000000..cf9cabc3ad --- /dev/null +++ b/plugins/azure/batchAccounts/batchAccountsAADEnabled.js @@ -0,0 +1,57 @@ +var async = require('async'); +var helpers = require('../../../helpers/azure/'); + +module.exports = { + title: 'Batch Account AAD Auth Enabled', + category: 'Batch', + domain: 'Compute', + severity: 'Medium', + description: 'Ensures that Batch account has Azure Active Directory (AAD) authentication mode enabled.', + more_info: 'Enabling Azure Active Directory (AAD) authentication for Batch account ensures enhanced security by restricting the service API authentication to Microsoft Entra ID that prevents access through less secure shared key methods, thereby safeguarding batch resources from unauthorized access.', + recommended_action: 'Enable Active Directory authentication mode for all Batch accounts.', + link: 'https://learn.microsoft.com/en-us/azure/batch/batch-aad-auth', + apis: ['batchAccounts:list'], + realtime_triggers: ['microsoftbatch:batchaccounts:write', 'microsoftbatch:batchaccounts:delete'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var locations = helpers.locations(settings.govcloud); + + async.each(locations.batchAccounts, function(location, rcb) { + + var batchAccounts = helpers.addSource(cache, source, + ['batchAccounts', 'list', location]); + + if (!batchAccounts) return rcb(); + + if (batchAccounts.err || !batchAccounts.data) { + helpers.addResult(results, 3, 'Unable to query for Batch accounts: ' + helpers.addError(batchAccounts), location); + return rcb(); + } + if (!batchAccounts.data.length) { + helpers.addResult(results, 0, 'No existing Batch accounts found', location); + return rcb(); + } + + for (let batchAccount of batchAccounts.data) { + if (!batchAccount.id) continue; + + let found = batchAccount.allowedAuthenticationModes && + batchAccount.allowedAuthenticationModes.length? + batchAccount.allowedAuthenticationModes.some(mode => mode.toUpperCase() === 'AAD') : false; + + if (found) { + helpers.addResult(results, 0, 'Batch account has Active Directory authentication enabled', location, batchAccount.id); + } else { + helpers.addResult(results, 2, 'Batch account does not have Active Directory authentication enabled', location, batchAccount.id); + } + + } + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; \ No newline at end of file diff --git a/plugins/azure/batchAccounts/batchAccountsAADEnabled.spec.js b/plugins/azure/batchAccounts/batchAccountsAADEnabled.spec.js new file mode 100644 index 0000000000..cd106bc09f --- /dev/null +++ b/plugins/azure/batchAccounts/batchAccountsAADEnabled.spec.js @@ -0,0 +1,94 @@ + +var expect = require('chai').expect; +var batchAccountsAADEnabled = require('./batchAccountsAADEnabled'); + +const batchAccounts = [ + { + "id": "/subscriptions/1234566/resourceGroups/dummy/providers/Microsoft.Batch/batchAccounts/test", + "name": "test", + "type": "Microsoft.Batch/batchAccounts", + "location": "eastus", + "accountEndpoint": "test.eastus.batch.azure.com", + "nodeManagementEndpoint": "123456789.eastus.service.batch.azure.com", + "allowedAuthenticationModes": ["SharedKey", "AAD", "TaskAuthenticationToken"] + }, + { + "id": "/subscriptions/1234566/resourceGroups/dummy/providers/Microsoft.Batch/batchAccounts/test", + "name": "test", + "type": "Microsoft.Batch/batchAccounts", + "location": "eastus", + "accountEndpoint": "test.eastus.batch.azure.com", + "nodeManagementEndpoint": "123456789.eastus.service.batch.azure.com", + }, +]; + +const createCache = (batchAccounts) => { + return { + batchAccounts: { + list: { + 'eastus': { + data: batchAccounts + } + } + } + } +}; + +const createErrorCache = () => { + return { + batchAccounts: { + list: { + 'eastus': {} + } + } + }; +}; + +describe('batchAccountsAADEnabled', function () { + describe('run', function () { + + it('should give unknown result if unable to query for Batch accounts:', function (done) { + const cache = createCache(null); + batchAccountsAADEnabled.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 Batch accounts:'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if no Batch account exist', function (done) { + const cache = createCache([]); + batchAccountsAADEnabled.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 Batch accounts found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if Batch account is configured with AAD Authentication', function (done) { + const cache = createCache([batchAccounts[0]]); + batchAccountsAADEnabled.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 has Active Directory authentication enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if Batch account is not configured with AAD Authentication', function (done) { + const cache = createCache([batchAccounts[1]]); + batchAccountsAADEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Batch account does not have Active Directory authentication enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/plugins/azure/batchAccounts/batchAccountsHasTags.js b/plugins/azure/batchAccounts/batchAccountsHasTags.js new file mode 100644 index 0000000000..0ab99cb4dd --- /dev/null +++ b/plugins/azure/batchAccounts/batchAccountsHasTags.js @@ -0,0 +1,51 @@ +var async = require('async'); +var helpers = require('../../../helpers/azure/'); + +module.exports = { + title: 'Batch Account Has Tags', + category: 'Batch', + domain: 'Compute', + severity: 'Low', + description: 'Ensures that Azure Batch accounts have tags associated.', + more_info: 'Tags help you to group resources together that are related to or associated with each other. It is a best practice to tag cloud resources to better organize and gain visibility into their usage.', + recommended_action: 'Modify Batch account and add tags.', + link: 'https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/tag-resources-portal', + apis: ['batchAccounts:list'], + realtime_triggers: ['microsoftbatch:batchaccounts:write','microsoftbatch:batchaccounts:delete'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var locations = helpers.locations(settings.govcloud); + + async.each(locations.batchAccounts, function(location, rcb){ + var batchAccounts = helpers.addSource(cache, source, + ['batchAccounts', 'list', location]); + + if (!batchAccounts) return rcb(); + + if (batchAccounts.err || !batchAccounts.data) { + helpers.addResult(results, 3, 'Unable to query for Batch accounts: ' + helpers.addError(batchAccounts), location); + return rcb(); + } + if (!batchAccounts.data.length) { + helpers.addResult(results, 0, 'No existing Batch accounts found', location); + return rcb(); + } + + for (let batchAccount of batchAccounts.data) { + if (!batchAccount.id) continue; + + if (batchAccount.tags && Object.entries(batchAccount.tags).length > 0 ) { + helpers.addResult(results, 0, 'Batch account has tags associated', location, batchAccount.id); + } else { + helpers.addResult(results, 2, 'Batch account does not have tags associated', location, batchAccount.id); + } + + } + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; \ No newline at end of file diff --git a/plugins/azure/batchAccounts/batchAccountsHasTags.spec.js b/plugins/azure/batchAccounts/batchAccountsHasTags.spec.js new file mode 100644 index 0000000000..4dcd9fb6dd --- /dev/null +++ b/plugins/azure/batchAccounts/batchAccountsHasTags.spec.js @@ -0,0 +1,94 @@ + +var expect = require('chai').expect; +var batchAccountsHasTags = require('./batchAccountsHasTags'); + +const batchAccounts = [ + { + "id": "/subscriptions/1234566/resourceGroups/dummy/providers/Microsoft.Batch/batchAccounts/test", + "name": "test", + "type": "Microsoft.Batch/batchAccounts", + "location": "eastus", + "accountEndpoint": "test.eastus.batch.azure.com", + "nodeManagementEndpoint": "123456789.eastus.service.batch.azure.com", + "tags": { "key": "value" } + }, + { + "id": "/subscriptions/1234566/resourceGroups/dummy/providers/Microsoft.Batch/batchAccounts/test", + "name": "test", + "type": "Microsoft.Batch/batchAccounts", + "location": "eastus", + "accountEndpoint": "test.eastus.batch.azure.com", + "nodeManagementEndpoint": "123456789.eastus.service.batch.azure.com", + }, +]; + +const createCache = (batchAccounts) => { + return { + batchAccounts: { + list: { + 'eastus': { + data: batchAccounts + } + } + } + } +}; + +const createErrorCache = () => { + return { + batchAccounts: { + list: { + 'eastus': {} + } + } + }; +}; + +describe('batchAccountsHasTags', function () { + describe('run', function () { + + it('should give unknown result if unable to query for Batch accounts:', function (done) { + const cache = createCache(null); + batchAccountsHasTags.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 Batch accounts:'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if no Batch account exist', function (done) { + const cache = createCache([]); + batchAccountsHasTags.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 Batch accounts found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if Batch account has tags associated', function (done) { + const cache = createCache([batchAccounts[0]]); + batchAccountsHasTags.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 has tags associated'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if Batch account does not have tags associated', function (done) { + const cache = createCache([batchAccounts[1]]); + batchAccountsHasTags.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Batch account does not have tags associated'); + 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 new file mode 100644 index 0000000000..505ed92048 --- /dev/null +++ b/plugins/azure/batchAccounts/batchAccountsPublicAccess.js @@ -0,0 +1,53 @@ +var async = require('async'); +var helpers = require('../../../helpers/azure/'); + +module.exports = { + title: 'Batch Account Public Access', + category: 'Batch', + domain: 'Compute', + severity: 'Medium', + description: 'Ensures that Batch accounts are not publicly accessible.', + more_info: 'Disabling public access for your Azure Batch Account enhances security by restricting unauthorized access to resources. This setting ensures that only trusted, internal sources can interact with Batch services, protecting data from potential external threats.', + recommended_action: 'Modify Batch account and disable public access.', + link: 'https://learn.microsoft.com/en-us/azure/batch/public-network-access', + apis: ['batchAccounts:list'], + realtime_triggers: ['microsoftbatch:batchaccounts:write','microsoftbatch:batchaccounts:delete'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var locations = helpers.locations(settings.govcloud); + + async.each(locations.batchAccounts, function(location, rcb){ + + var batchAccounts = helpers.addSource(cache, source, + ['batchAccounts', 'list', location]); + + if (!batchAccounts) return rcb(); + + if (batchAccounts.err || !batchAccounts.data) { + helpers.addResult(results, 3, 'Unable to query for Batch accounts: ' + helpers.addError(batchAccounts), location); + return rcb(); + } + if (!batchAccounts.data.length) { + helpers.addResult(results, 0, 'No existing Batch accounts found', location); + return rcb(); + } + + for (let batchAccount of batchAccounts.data) { + if (!batchAccount.id) continue; + + if (batchAccount.publicNetworkAccess && + batchAccount.publicNetworkAccess.toLowerCase() === 'enabled') { + 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); + } + } + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; \ No newline at end of file diff --git a/plugins/azure/batchAccounts/batchAccountsPublicAccess.spec.js b/plugins/azure/batchAccounts/batchAccountsPublicAccess.spec.js new file mode 100644 index 0000000000..ca54e124fe --- /dev/null +++ b/plugins/azure/batchAccounts/batchAccountsPublicAccess.spec.js @@ -0,0 +1,95 @@ + +var expect = require('chai').expect; +var batchAccountsPublicAccess = require('./batchAccountsPublicAccess'); + +const batchAccounts = [ + { + "id": "/subscriptions/1234566/resourceGroups/dummy/providers/Microsoft.Batch/batchAccounts/test", + "name": "test", + "type": "Microsoft.Batch/batchAccounts", + "location": "eastus", + "accountEndpoint": "test.eastus.batch.azure.com", + "nodeManagementEndpoint": "123456789.eastus.service.batch.azure.com", + "publicNetworkAccess": "Disabled" + }, + { + "id": "/subscriptions/1234566/resourceGroups/dummy/providers/Microsoft.Batch/batchAccounts/test", + "name": "test", + "type": "Microsoft.Batch/batchAccounts", + "location": "eastus", + "accountEndpoint": "test.eastus.batch.azure.com", + "nodeManagementEndpoint": "123456789.eastus.service.batch.azure.com", + "publicNetworkAccess": "Enabled" + }, +]; + +const createCache = (batchAccounts) => { + return { + batchAccounts: { + list: { + 'eastus': { + data: batchAccounts + } + } + } + } +}; + +const createErrorCache = () => { + return { + batchAccounts: { + list: { + 'eastus': {} + } + } + }; +}; + +describe('batchAccountsPublicAccess', function () { + describe('run', function () { + + it('should give unknown result if unable to query for Batch accounts:', function (done) { + const cache = createCache(null); + batchAccountsPublicAccess.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 Batch accounts:'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if no Batch account exist', function (done) { + const cache = createCache([]); + batchAccountsPublicAccess.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 Batch accounts found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if Batch account is not publicly accessible', function (done) { + const cache = createCache([batchAccounts[0]]); + 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(); + }); + }); + + it('should give failing result if Batch account is publicly accessible', function (done) { + const cache = createCache([batchAccounts[1]]); + batchAccountsPublicAccess.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Batch account is publicly accessible'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/plugins/azure/eventhub/eventHubNamespaceAutoInflate.js b/plugins/azure/eventhub/eventHubNamespaceAutoInflate.js new file mode 100644 index 0000000000..4838725d40 --- /dev/null +++ b/plugins/azure/eventhub/eventHubNamespaceAutoInflate.js @@ -0,0 +1,63 @@ + +var async = require('async'); +const helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'Event Hubs Namespace Auto-Inflate Enabled', + category: 'Event Hubs', + domain: 'Content Delivery', + severity: 'Low', + description: 'Ensure that Event Hubs namespaces have Auto-inflate feature enabled.', + more_info: 'Enabling Auto-inflate for your Azure Event Hubs namespace ensures seamless scaling by automatically adjusting the number of throughput units (TUs) based on workload demands. This feature helps prevent throttling issues by scaling up as needed, providing efficient and reliable data handling without manual intervention.', + recommended_action: 'Modify Event Hub namespace and enable auto-inflate feature.', + link: 'https://learn.microsoft.com/en-us/azure/event-hubs/event-hubs-auto-inflate', + apis: ['eventHub:listEventHub'], + realtime_triggers: ['microsofteventhub:namespaces:write', 'microsofteventhub:namespaces:delete'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var locations = helpers.locations(settings.govcloud); + + async.each(locations.eventHub, function(location, rcb) { + var eventHubs = helpers.addSource(cache, source, + ['eventHub', 'listEventHub', location]); + + if (!eventHubs) return rcb(); + + if (eventHubs.err || !eventHubs.data) { + helpers.addResult(results, 3, + 'Unable to query for Event Hubs namespaces: ' + helpers.addError(eventHubs), location); + return rcb(); + } + + if (!eventHubs.data.length) { + helpers.addResult(results, 0, 'No Event Hubs namespaces found', location); + return rcb(); + } + + for (let eventHub of eventHubs.data) { + if (!eventHub.id) continue; + + if (eventHub.sku && + eventHub.sku.tier && + eventHub.sku.tier.toLowerCase() != 'standard') { + helpers.addResult(results, 0, + 'Event Hubs namespace is not a standard namespace', location, eventHub.id); + } else { + if (eventHub.isAutoInflateEnabled) { + helpers.addResult(results, 0, + 'Event Hubs namespace has auto inflate feature enabled', location, eventHub.id); + } else { + helpers.addResult(results, 2, + 'Event Hubs namespace does not have auto inflate feature enabled', location, eventHub.id); + } + } + } + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/eventhub/eventHubNamespaceAutoInflate.spec.js b/plugins/azure/eventhub/eventHubNamespaceAutoInflate.spec.js new file mode 100644 index 0000000000..9a4494f324 --- /dev/null +++ b/plugins/azure/eventhub/eventHubNamespaceAutoInflate.spec.js @@ -0,0 +1,151 @@ +var expect = require('chai').expect; +var eventHubNamespaceAutoInflate = require('./eventHubNamespaceAutoInflate'); +const eventHubs = [ + { + "kind": "v12.0", + "location": "eastus", + "tags": {}, + "id": "/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.EventHub/namespaces/testHub'", + "name": "testHub", + "type": 'Microsoft.EventHub/Namespaces', + "location": 'East US', + "tags": {}, + "minimumTlsVersion": '1.2', + "publicNetworkAccess": 'Enabled', + "disableLocalAuth": true, + "zoneRedundant": true, + "isAutoInflateEnabled": true, + "maximumThroughputUnits": 0, + "kafkaEnabled": false, + "sku": { + "name": "Standard", + "tier": "Standard", + "capacity": 1 + }, + }, + { + "kind": "v12.0", + "location": "eastus", + "tags": {}, + "id": "/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.EventHub/namespaces/testHub'", + "name": "testHub", + "type": 'Microsoft.EventHub/Namespaces', + "location": 'East US', + "tags": {}, + "minimumTlsVersion": '1.1', + "publicNetworkAccess": 'Enabled', + "disableLocalAuth": true, + "zoneRedundant": true, + "isAutoInflateEnabled": false, + "maximumThroughputUnits": 0, + "kafkaEnabled": false, + "sku": { + "name": "Standard", + "tier": "Standard", + "capacity": 1 + }, + }, + { + "kind": "v12.0", + "location": "eastus", + "tags": {}, + "id": "/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.EventHub/namespaces/testHub'", + "name": "testHub", + "type": 'Microsoft.EventHub/Namespaces', + "location": 'East US', + "tags": {}, + "minimumTlsVersion": '1.1', + "publicNetworkAccess": 'Enabled', + "disableLocalAuth": true, + "zoneRedundant": true, + "isAutoInflateEnabled": false, + "maximumThroughputUnits": 0, + "kafkaEnabled": false, + "sku": { + "name": "Premium", + "tier": "Premium", + "capacity": 1 + }, + } +]; + +const createCache = (hub) => { + return { + eventHub: { + listEventHub: { + 'eastus': { + data: hub + } + } + } + } +}; + +describe('eventHubNamespaceAutoInflate', function() { + describe('run', function() { + it('should give passing result if no event hub found', 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('No Event Hubs namespaces found'); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache([]); + eventHubNamespaceAutoInflate.run(cache, {}, callback); + }); + + it('should give failing result if Event Hubs namespace does not have auto-inflate feature enabled', 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('Event Hubs namespace does not have auto inflate feature enabled'); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache([eventHubs[1]]); + eventHubNamespaceAutoInflate.run(cache, {}, callback); + }); + + it('should give passing result if Event Hubs namespace has auto-inflate feature enabled', 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('Event Hubs namespace has auto inflate feature enabled'); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache([eventHubs[0]]); + eventHubNamespaceAutoInflate.run(cache, {}, callback); + }); + + it('should give unknown result if unable to query for event hubs', function(done) { + const callback = (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 Event Hubs namespaces:'); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache(null); + eventHubNamespaceAutoInflate.run(cache, {}, callback); + }); + + it('should give passing result if event hub namespace is not standard type', 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('Event Hubs namespace is not a standard namespace'); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache([eventHubs[2]]); + eventHubNamespaceAutoInflate.run(cache, {}, callback); + }); + }) +}) \ No newline at end of file diff --git a/plugins/azure/mysqlserver/mysqlFlexibleServerDignosticLogs.js b/plugins/azure/mysqlserver/mysqlFlexibleServerDignosticLogs.js new file mode 100644 index 0000000000..16f911b141 --- /dev/null +++ b/plugins/azure/mysqlserver/mysqlFlexibleServerDignosticLogs.js @@ -0,0 +1,65 @@ +const async = require('async'); +const helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'MySQL Flexible Server Diagnostic Logs', + category: 'MySQL Server', + domain: 'Databases', + severity: 'Medium', + description: 'Ensures that MySQL flexible server has diagnostic logs enabled.', + more_info: 'Enabling diagnostic logging for Azure Database for MySQL Flexible servers helps with performance monitoring, troubleshooting, and security optimization.', + recommended_action: 'Enable diagnostic logging for all MySQL flexible servers.', + link: 'https://learn.microsoft.com/en-us/azure/mysql/flexible-server/concepts-monitoring', + apis: ['servers:listMysqlFlexibleServer', 'diagnosticSettings:listByMysqlFlexibleServer'], + realtime_triggers: ['microsoftdbformysql:flexibleservers:write','microsoftdbformysql:flexibleservers:delete','microsoftinsights:diagnosticsettings:write','microsoftinsights:diagnosticsettings:delete'], + + run: function(cache, settings, callback) { + const results = []; + const source = {}; + const locations = helpers.locations(settings.govcloud); + + async.each(locations.servers, (location, rcb) => { + + const servers = helpers.addSource(cache, source, + ['servers', 'listMysqlFlexibleServer', location]); + + if (!servers) return rcb(); + + if (servers.err || !servers.data) { + helpers.addResult(results, 3, + 'Unable to query for MySQL flexible servers: ' + helpers.addError(servers), location); + return rcb(); + } + + if (!servers.data.length) { + helpers.addResult(results, 0, 'No existing MySQL flexible servers found', location); + return rcb(); + } + + for (let server of servers.data) { + if (!server.id) continue; + + var diagnosticSettings = helpers.addSource(cache, source, + ['diagnosticSettings', 'listByMysqlFlexibleServer', location, server.id]); + + if (!diagnosticSettings || diagnosticSettings.err || !diagnosticSettings.data) { + helpers.addResult(results, 3, `Unable to query for MySQL flexible server diagnostic settings: ${helpers.addError(diagnosticSettings)}`, + location, server.id); + continue; + } + + var found = diagnosticSettings.data.find(ds => ds.logs && ds.logs.length); + + if (found) { + helpers.addResult(results, 0, 'MySQL flexible server has diagnostic logs enabled', location, server.id); + } else { + helpers.addResult(results, 2, 'MySQL flexible server does not have diagnostic logs enabled', location, server.id); + } + } + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/mysqlserver/mysqlFlexibleServerDignosticLogs.spec.js b/plugins/azure/mysqlserver/mysqlFlexibleServerDignosticLogs.spec.js new file mode 100644 index 0000000000..c1e8dbc734 --- /dev/null +++ b/plugins/azure/mysqlserver/mysqlFlexibleServerDignosticLogs.spec.js @@ -0,0 +1,125 @@ +var expect = require('chai').expect; +var auth = require('./mysqlFlexibleServerDignosticLogs'); + +const servers = [ + { + "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforMySQL/flexibleServers/test-server", + }, +]; + + +const diagnosticSettings = [ + { + id: '/subscriptions/234/myrg/providers/Microsoft.DBforPostgreSQL/servers/test/providers/microsoft.insights/diagnosticSettings/test-setting', + type: 'Microsoft.Insights/diagnosticSettings', + name: 'server-setting', + location: 'eastus', + kind: null, + tags: null, + eventHubName: null, + metrics: [], + logs: [ + { + "category": null, + "categoryGroup": "allLogs", + "enabled": true, + "retentionPolicy": { + "enabled": false, + "days": 0 + } + }, + { + "category": null, + "categoryGroup": "audit", + "enabled": false, + "retentionPolicy": { + "enabled": false, + "days": 0 + } + } + ], + logAnalyticsDestinationType: null + } +]; + +const createCache = (servers, ds) => { + const id = servers && servers.length ? servers[0].id : null; + return { + servers: { + listMysqlFlexibleServer: { + 'eastus': { + data: servers + } + } + }, + diagnosticSettings: { + listByMysqlFlexibleServer: { + 'eastus': { + [id]: { + data: ds + } + } + } + + }, + }; +}; + +describe('mysqlFlexibleServerLogsEnabled', function() { + describe('run', function() { + it('should give a passing result if no existing server found', function (done) { + const cache = createCache([], null); + auth.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 MySQL flexible servers found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give unknown result if unable to query for server', function (done) { + const cache = createCache(null, ['error']); + auth.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 MySQL flexible servers: '); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + it('should give unknown result if unable to query for diagnostic settings', function(done) { + const cache = createCache([servers[0]], null); + auth.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 MySQL flexible server diagnostic settings'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if diagnostic logs enabled', function(done) { + const cache = createCache([servers[0]], [diagnosticSettings[0]]); + auth.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('MySQL flexible server has diagnostic logs enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if diagnostic logs not enabled', function(done) { + const cache = createCache([servers[0]], [[]]); + auth.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('MySQL flexible server does not have diagnostic logs enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); +}); + diff --git a/plugins/azure/mysqlserver/mysqlFlexibleServerIdentity.js b/plugins/azure/mysqlserver/mysqlFlexibleServerIdentity.js new file mode 100644 index 0000000000..a770cf0477 --- /dev/null +++ b/plugins/azure/mysqlserver/mysqlFlexibleServerIdentity.js @@ -0,0 +1,53 @@ +const async = require('async'); +const helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'MySQL Flexible Server Managed Identity', + category: 'MySQL Server', + domain: 'Databases', + severity: 'Medium', + description: 'Ensures that MySQL flexible servers have managed identity enabled.', + more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.', + recommended_action: 'Modify MySQL flexible server add managed identity.', + link: 'https://learn.microsoft.com/en-us/azure/mysql/flexible-server/how-to-azure-ad', + apis: ['servers:listMysqlFlexibleServer'], + realtime_triggers: ['microsoftdbformysql:flexibleservers:write','microsoftdbformysql:flexibleservers:delete'], + + run: function(cache, settings, callback) { + const results = []; + const source = {}; + const locations = helpers.locations(settings.govcloud); + + async.each(locations.servers, (location, rcb) => { + const servers = helpers.addSource(cache, source, + ['servers', 'listMysqlFlexibleServer', location]); + + if (!servers) return rcb(); + + if (servers.err || !servers.data) { + helpers.addResult(results, 3, + 'Unable to query for MySQL flexible servers: ' + helpers.addError(servers), location); + return rcb(); + } + + if (!servers.data.length) { + helpers.addResult(results, 0, 'No existing MySQL flexible servers found', location); + return rcb(); + } + + for (var flexibleServer of servers.data) { + if (!flexibleServer.id) return; + + if (flexibleServer.identity) { + helpers.addResult(results, 0, 'MySQL flexible server has managed identity enabled', location, flexibleServer.id); + } else { + helpers.addResult(results, 2, 'MySQL flexible server does not have managed identity enabled', location, flexibleServer.id); + } + } + rcb(); + }, function() { + // Global checking goes here + callback(null, results, source); + }); + } +}; \ No newline at end of file diff --git a/plugins/azure/mysqlserver/mysqlFlexibleServerIdentity.spec.js b/plugins/azure/mysqlserver/mysqlFlexibleServerIdentity.spec.js new file mode 100644 index 0000000000..fb6a5adf23 --- /dev/null +++ b/plugins/azure/mysqlserver/mysqlFlexibleServerIdentity.spec.js @@ -0,0 +1,103 @@ +var assert = require('assert'); +var expect = require('chai').expect; +var auth = require('./mysqlFlexibleServerIdentity'); + +const createCache = (err, list) => { + return { + servers: { + listMysqlFlexibleServer: { + 'eastus': { + err: err, + data: list + } + } + } + } +}; + +describe('mysqlFlexibleServerManagedIdentity', function() { + describe('run', function() { + it('should PASS if no existing servers found', 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('No existing MySQL flexible servers found'); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache( + null, + [], + {} + ); + + auth.run(cache, {}, callback); + }); + + it('should FAIL if MySQL server does not have managed identity', 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('MySQL flexible server does not have managed identity enabled'); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache( + null, + [ + { + "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforMySQL/flexibleServers/test-server", + "type": "Microsoft.DBforMySQL/flexibleServers", + "version": '5.8' + } + ], + ); + + auth.run(cache, {}, callback); + }); + + it('should PASS if MySQL server is using latest version', 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('MySQL flexible server has managed identity enabled'); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache( + null, + [ + { + "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforMySQL/flexibleServers/test-server", + "type": "Microsoft.DBforMySQL/flexibleServers", + "version": "8.0", + "identity": { + "type": "userassigned" + } + } + ] + ); + + auth.run(cache, {}, callback); + }); + + it('should UNKNOWN if unable to query for server', function(done) { + const callback = (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 MySQL flexible servers: '); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache( + null, null + ); + + auth.run(cache, {}, callback); + }); + }) +}) \ No newline at end of file diff --git a/plugins/azure/mysqlserver/mysqlFlexibleServerVersion.js b/plugins/azure/mysqlserver/mysqlFlexibleServerVersion.js new file mode 100644 index 0000000000..749ec2d833 --- /dev/null +++ b/plugins/azure/mysqlserver/mysqlFlexibleServerVersion.js @@ -0,0 +1,56 @@ +const async = require('async'); +const helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'MySQL Flexible Server Version', + category: 'MySQL Server', + domain: 'Databases', + severity: 'Medium', + description: 'Ensures that MySQL flexible servers are using the latest server version.', + more_info: 'Using the latest version of Upgrade the version of MySQL flexible server to the latest available version will give access to new software features, resolve reported bugs through security patches, and improve compatibility with other applications and services.', + recommended_action: 'Ensure MySQL Flexible Servers are using the latest server version.', + link: 'https://learn.microsoft.com/en-us/azure/mysql/flexible-server/how-to-upgrade', + apis: ['servers:listMysqlFlexibleServer'], + realtime_triggers: ['microsoftdbformysql:flexibleservers:write','microsoftdbformysql:flexibleservers:delete'], + + run: function(cache, settings, callback) { + const results = []; + const source = {}; + const locations = helpers.locations(settings.govcloud); + const latestServerVersion = 8.0; + + async.each(locations.servers, (location, rcb) => { + const servers = helpers.addSource(cache, source, + ['servers', 'listMysqlFlexibleServer', location]); + + if (!servers) return rcb(); + + if (servers.err || !servers.data) { + helpers.addResult(results, 3, + 'Unable to query for MySQL flexible servers: ' + helpers.addError(servers), location); + return rcb(); + } + + if (!servers.data.length) { + helpers.addResult(results, 0, 'No existing MySQL flexible servers found', location); + return rcb(); + } + + for (var flexibleServer of servers.data) { + if (!flexibleServer.id) continue; + + if (flexibleServer.version && parseFloat(flexibleServer.version) >= latestServerVersion) { + helpers.addResult(results, 0, + `MySQL flexible server has latest server version: ${flexibleServer.version}`, location, flexibleServer.id); + } else { + helpers.addResult(results, 2, + `MySQL flexible server does not have latest server version: ${latestServerVersion}`, location, flexibleServer.id); + } + } + rcb(); + }, function() { + // Global checking goes here + callback(null, results, source); + }); + } +}; \ No newline at end of file diff --git a/plugins/azure/mysqlserver/mysqlFlexibleServerVersion.spec.js b/plugins/azure/mysqlserver/mysqlFlexibleServerVersion.spec.js new file mode 100644 index 0000000000..9ac967507b --- /dev/null +++ b/plugins/azure/mysqlserver/mysqlFlexibleServerVersion.spec.js @@ -0,0 +1,100 @@ +var assert = require('assert'); +var expect = require('chai').expect; +var auth = require('./mysqlFlexibleServerVersion'); + +const createCache = (err, list) => { + return { + servers: { + listMysqlFlexibleServer: { + 'eastus': { + err: err, + data: list + } + } + } + } +}; + +describe('mysqlFlexibleServerVersion', function() { + describe('run', function() { + it('should PASS if no existing servers found', 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('No existing MySQL flexible servers found'); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache( + null, + [], + {} + ); + + auth.run(cache, {}, callback); + }); + + it('should FAIL if MySQL server is not using latest version', 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('MySQL flexible server does not have latest server version: 8'); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache( + null, + [ + { + "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforMySQL/flexibleServers/test-server", + "type": "Microsoft.DBforMySQL/flexibleServers", + "version": '5.8' + } + ], + ); + + auth.run(cache, {}, callback); + }); + + it('should PASS if MySQL server is using latest version', 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('MySQL flexible server has latest server version: 8'); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache( + null, + [ + { + "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforMySQL/flexibleServers/test-server", + "type": "Microsoft.DBforMySQL/flexibleServers", + "version": "8.0" + } + ] + ); + + auth.run(cache, {}, callback); + }); + + it('should UNKNOWN if unable to query for server', function(done) { + const callback = (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 MySQL flexible servers: '); + expect(results[0].region).to.equal('eastus'); + done() + }; + + const cache = createCache( + null, null + ); + + auth.run(cache, {}, callback); + }); + }) +}) \ No newline at end of file diff --git a/plugins/azure/synapse/synapseWorkspacPrivateEndpoint.js b/plugins/azure/synapse/synapseWorkspacPrivateEndpoint.js new file mode 100644 index 0000000000..c375d75561 --- /dev/null +++ b/plugins/azure/synapse/synapseWorkspacPrivateEndpoint.js @@ -0,0 +1,55 @@ +var async = require('async'); +var helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'Synapse Workspace Private Endpoints', + category: 'AI & ML', + domain: 'Machine Learning', + severity: 'Medium', + description: 'Ensure that Azure Synapse workspace is accessible only through managed private endpoints.', + more_info: 'Enabling managed private endpoints for Azure Synapse Analytics workspace ensure secure, private communication between your Synapse workspace and other Azure resources, traversing exclusively over the Microsoft backbone network. It enhances security by protecting against data exfiltration and allowing connectivity only to specific approved resources.', + recommended_action: 'Modify Synapse workspace and configure managed private endpoints.', + link: 'https://learn.microsoft.com/en-us/azure/synapse-analytics/security/synapse-workspace-managed-private-endpoints', + apis: ['synapse:listWorkspaces'], + realtime_triggers: ['microsoftsynapse:workspaces:write','microsoftsynapse:workspaces:delete'], + + run: function(cache, settings, callback) { + const results = []; + const source = {}; + const locations = helpers.locations(settings.govcloud); + + async.each(locations.synapse, function(location, rcb) { + const workspaces = helpers.addSource(cache, source, + ['synapse', 'listWorkspaces', location]); + + if (!workspaces) return rcb(); + + + if (workspaces.err || !workspaces.data) { + helpers.addResult(results, 3, 'Unable to query Synapse workspaces: ' + helpers.addError(workspaces), location); + return rcb(); + } + + if (!workspaces.data.length) { + helpers.addResult(results, 0, 'No existing Synapse workspaces found', location); + return rcb(); + } + + for (let workspace of workspaces.data) { + if (!workspace.id) continue; + + if (workspace.privateEndpointConnections && + workspace.privateEndpointConnections.length) { + helpers.addResult(results, 0, 'Synapse workspace has managed private endpoints configured', location, workspace.id); + } else { + helpers.addResult(results, 2, 'Synapse workspace does not have managed private endpoints configured', location, workspace.id); + } + } + + rcb(); + }, function() { + // Global checking goes here + callback(null, results, source); + }); + } +}; \ No newline at end of file diff --git a/plugins/azure/synapse/synapseWorkspacPrivateEndpoint.spec.js b/plugins/azure/synapse/synapseWorkspacPrivateEndpoint.spec.js new file mode 100644 index 0000000000..6aa7a0a3f0 --- /dev/null +++ b/plugins/azure/synapse/synapseWorkspacPrivateEndpoint.spec.js @@ -0,0 +1,94 @@ +var expect = require('chai').expect; +var synapseWorkspacPrivateEndpoint = require('./synapseWorkspacPrivateEndpoint'); + +const workspaces = [ + { + type: "Microsoft.Synapse/workspaces", + id: "/subscriptions/123/resourceGroups/rsgrp/providers/Microsoft.Synapse/workspaces/test", + location: "eastus", + name: "test", + azureADOnlyAuthentication: true, + privateEndpointConnections: [{ + id: "/subscriptions/123/resourceGroups/rsgrp/providers/Microsoft.Synapse/workspaces/test/privateEndpointConnections/test-endpoint-synapse-123", + properties: { + privateEndpoint: { + id: "/subscriptions/123/resourceGroups/rsgrp/providers/Microsoft.Network/privateEndpoints/test-endpoint-synapse", + }, + privateLinkServiceConnectionState: { + status: "Approved", + }, + }, + }], + }, + { + type: "Microsoft.Synapse/workspaces", + id: "/subscriptions/123/resourceGroups/rsgrp/providers/Microsoft.Synapse/workspaces/test", + location: "eastus", + name: "test", + privateEndpointConnections: [] + }, +]; + + +const createCache = (workspaces, err) => { + + return { + synapse: { + listWorkspaces: { + 'eastus': { + data: workspaces, + err: err + } + } + } + }; +}; + +describe('synapseWorkspacPrivateEndpoint', function () { + describe('run', function () { + + it('should give a passing result if no Synapse workspaces are found', function (done) { + const cache = createCache([], null); + synapseWorkspacPrivateEndpoint.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 Synapse workspaces found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give unknown result if unable to query for Synapse workspaces', function (done) { + const cache = createCache(null, ['error']); + synapseWorkspacPrivateEndpoint.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 Synapse workspaces'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if workspace has Private endpoints configured ', function (done) { + const cache = createCache([workspaces[0]], null); + synapseWorkspacPrivateEndpoint.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Synapse workspace has managed private endpoints configured'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if workspace does not have Private endpoints configured', function (done) { + const cache = createCache([workspaces[1]], null); + synapseWorkspacPrivateEndpoint.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Synapse workspace does not have managed private endpoints configured'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/plugins/azure/synapse/synapseWorkspaceAdAuthEnabled.js b/plugins/azure/synapse/synapseWorkspaceAdAuthEnabled.js new file mode 100644 index 0000000000..80606b792a --- /dev/null +++ b/plugins/azure/synapse/synapseWorkspaceAdAuthEnabled.js @@ -0,0 +1,54 @@ +var async = require('async'); +var helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'Synapse Workspace AD Auth Enabled', + category: 'AI & ML', + domain: 'Machine Learning', + severity: 'Medium', + description: 'Ensures that Azure Synapse workspace has Active Directory (AD) authentication enabled.', + more_info: 'Enabling Azure Active Directory authentication for Synapse workspace enhances security by ensuring that only authenticated and authorized users can access resources and eliminating the need for password storage. This integration simplifies permission management and secure access.', + recommended_action: 'Enable Active Directory (AD) authentication mode for all Synapse workspace.', + link: 'https://learn.microsoft.com/en-us/azure/synapse-analytics/sql/active-directory-authentication', + apis: ['synapse:listWorkspaces'], + realtime_triggers: ['microsoftsynapse:workspaces:write','microsoftsynapse:workspaces:delete'], + + run: function(cache, settings, callback) { + const results = []; + const source = {}; + const locations = helpers.locations(settings.govcloud); + + async.each(locations.synapse, function(location, rcb) { + const workspaces = helpers.addSource(cache, source, + ['synapse', 'listWorkspaces', location]); + + if (!workspaces) return rcb(); + + + if (workspaces.err || !workspaces.data) { + helpers.addResult(results, 3, 'Unable to query Synapse workspaces: ' + helpers.addError(workspaces), location); + return rcb(); + } + + if (!workspaces.data.length) { + helpers.addResult(results, 0, 'No existing Synapse workspaces found', location); + return rcb(); + } + + for (let workspace of workspaces.data) { + if (!workspace.id) continue; + + if (workspace.azureADOnlyAuthentication) { + helpers.addResult(results, 0, 'Synapse workspace has Active Directory authentication enabled', location, workspace.id); + } else { + helpers.addResult(results, 2, 'Synapse workspace does not have Active Directory authentication enabled', location, workspace.id); + } + } + + rcb(); + }, function() { + // Global checking goes here + callback(null, results, source); + }); + } +}; \ No newline at end of file diff --git a/plugins/azure/synapse/synapseWorkspaceAdAuthEnabled.spec.js b/plugins/azure/synapse/synapseWorkspaceAdAuthEnabled.spec.js new file mode 100644 index 0000000000..0db95cd9eb --- /dev/null +++ b/plugins/azure/synapse/synapseWorkspaceAdAuthEnabled.spec.js @@ -0,0 +1,82 @@ +var expect = require('chai').expect; +var synapseWorkspaceAdAuthEnabled = require('./synapseWorkspaceAdAuthEnabled'); + +const workspaces = [ + { + type: "Microsoft.Synapse/workspaces", + id: "/subscriptions/123/resourceGroups/rsgrp/providers/Microsoft.Synapse/workspaces/test", + location: "eastus", + name: "test", + azureADOnlyAuthentication: true + }, + { + type: "Microsoft.Synapse/workspaces", + id: "/subscriptions/123/resourceGroups/rsgrp/providers/Microsoft.Synapse/workspaces/test", + location: "eastus", + name: "test", + }, +]; + + +const createCache = (workspaces, err) => { + + return { + synapse: { + listWorkspaces: { + 'eastus': { + data: workspaces, + err: err + } + } + } + }; +}; + +describe('synapseWorkspaceAdAuthEnabled', function () { + describe('run', function () { + + it('should give a passing result if no Synapse workspaces are found', function (done) { + const cache = createCache([], null); + synapseWorkspaceAdAuthEnabled.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 Synapse workspaces found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give unknown result if unable to query for Synapse workspaces', function (done) { + const cache = createCache(null, ['error']); + synapseWorkspaceAdAuthEnabled.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 Synapse workspaces'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if workspace has AAD auth enabled', function (done) { + const cache = createCache([workspaces[0]], null); + synapseWorkspaceAdAuthEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Synapse workspace has Active Directory authentication enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if workspace does not have AAD auth', function (done) { + const cache = createCache([workspaces[1]], null); + synapseWorkspaceAdAuthEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Synapse workspace does not have Active Directory authentication enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/plugins/azure/synapse/workspaceManagedIdentity.js b/plugins/azure/synapse/workspaceManagedIdentity.js new file mode 100644 index 0000000000..51e45b6c04 --- /dev/null +++ b/plugins/azure/synapse/workspaceManagedIdentity.js @@ -0,0 +1,54 @@ +var async = require('async'); +var helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'Synapse Workspace Managed Identity', + category: 'AI & ML', + domain: 'Machine Learning', + severity: 'Medium', + description: 'Ensure that Azure Synapse workspace has managed identity enabled.', + more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.', + recommended_action: 'Modify Synapse workspace and enable managed identity.', + link: 'https://learn.microsoft.com/en-us/azure/synapse-analytics/synapse-service-identity', + apis: ['synapse:listWorkspaces'], + realtime_triggers: ['microsoftsynapse:workspaces:write','microsoftsynapse:workspaces:delete'], + + run: function(cache, settings, callback) { + const results = []; + const source = {}; + const locations = helpers.locations(settings.govcloud); + + async.each(locations.synapse, function(location, rcb) { + const workspaces = helpers.addSource(cache, source, + ['synapse', 'listWorkspaces', location]); + + if (!workspaces) return rcb(); + + + if (workspaces.err || !workspaces.data) { + helpers.addResult(results, 3, 'Unable to query Synapse workspaces: ' + helpers.addError(workspaces), location); + return rcb(); + } + + if (!workspaces.data.length) { + helpers.addResult(results, 0, 'No existing Synapse workspaces found', location); + return rcb(); + } + + for (let workspace of workspaces.data) { + if (!workspace.id) continue; + + if (workspace.identity && workspace.identity.type) { + helpers.addResult(results, 0, 'Synapse workspace has managed identity enabled', location, workspace.id); + } else { + helpers.addResult(results, 2, 'Synapse workspace does not have managed identity enabled', location, workspace.id); + } + } + + rcb(); + }, function() { + // Global checking goes here + callback(null, results, source); + }); + } +}; \ No newline at end of file diff --git a/plugins/azure/synapse/workspaceManagedIdentity.spec.js b/plugins/azure/synapse/workspaceManagedIdentity.spec.js new file mode 100644 index 0000000000..760a2fc15e --- /dev/null +++ b/plugins/azure/synapse/workspaceManagedIdentity.spec.js @@ -0,0 +1,86 @@ +var expect = require('chai').expect; +var workspaceManagedIdentity = require('./workspaceManagedIdentity'); + +const workspaces = [ + { + type: "Microsoft.Synapse/workspaces", + id: "/subscriptions/123/resourceGroups/rsgrp/providers/Microsoft.Synapse/workspaces/test", + location: "eastus", + name: "test", + identity: { + type: "SystemAssigned", + tenantId: "1234532134532134532", + principalId: "13123232223223323", + }, + }, + { + type: "Microsoft.Synapse/workspaces", + id: "/subscriptions/123/resourceGroups/rsgrp/providers/Microsoft.Synapse/workspaces/test", + location: "eastus", + name: "test", + }, +]; + + +const createCache = (workspaces, err) => { + + return { + synapse: { + listWorkspaces: { + 'eastus': { + data: workspaces, + err: err + } + } + } + }; +}; + +describe('workspaceManagedIdentity', function () { + describe('run', function () { + + it('should give a passing result if no Synapse workspaces are found', function (done) { + const cache = createCache([], null); + workspaceManagedIdentity.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 Synapse workspaces found'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give unknown result if unable to query for Synapse workspaces', function (done) { + const cache = createCache(null, ['error']); + workspaceManagedIdentity.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 Synapse workspaces'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if workspace has managed identity', function (done) { + const cache = createCache([workspaces[0]], null); + workspaceManagedIdentity.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Synapse workspace has managed identity enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if workspace does not have managed identity', function (done) { + const cache = createCache([workspaces[1]], null); + workspaceManagedIdentity.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Synapse workspace does not have managed identity enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + }); +}); \ No newline at end of file