diff --git a/collectors/aws/ecs/listContainerInstances.js b/collectors/aws/ecs/listContainerInstances.js index 987ca65d2b..c7b9eb175e 100644 --- a/collectors/aws/ecs/listContainerInstances.js +++ b/collectors/aws/ecs/listContainerInstances.js @@ -16,13 +16,13 @@ module.exports = function(AWSConfig, collection, retries, callback) { helpers.makeCustomCollectorCall(ecs, 'listContainerInstances', params, retries, null, null, null, function(err, data) { if (err) { collection.ecs.listContainerInstances[AWSConfig.region][cluster].err = err; + } else if (data && data.containerInstanceArns) { + collection.ecs.listContainerInstances[AWSConfig.region][cluster].data = data.containerInstanceArns; } - collection.ecs.listContainerInstances[AWSConfig.region][cluster].data = data.containerInstanceArns; - cb(); }); }, function(){ callback(); }); -}; \ No newline at end of file +}; diff --git a/collectors/aws/ecs/listServices.js b/collectors/aws/ecs/listServices.js index 916ab9002d..4898290ad1 100644 --- a/collectors/aws/ecs/listServices.js +++ b/collectors/aws/ecs/listServices.js @@ -19,12 +19,11 @@ module.exports = function(AWSConfig, collection, retries, callback) { if (err) { collection.ecs.listServices[AWSConfig.region][cluster].err = err; } - - collection.ecs.listServices[AWSConfig.region][cluster].data = data.serviceArns; + if (data && data.serviceArns) collection.ecs.listServices[AWSConfig.region][cluster].data = data.serviceArns; cb(); }); }, function(){ callback(); }); -}; \ No newline at end of file +}; diff --git a/collectors/aws/iam/getRole.js b/collectors/aws/iam/getRole.js new file mode 100644 index 0000000000..5ec172fe45 --- /dev/null +++ b/collectors/aws/iam/getRole.js @@ -0,0 +1,40 @@ +var AWS = require('aws-sdk'); +var async = require('async'); +var helpers = require(__dirname + '/../../../helpers/aws'); + +module.exports = function(AWSConfig, collection, retries, callback) { + var iam = new AWS.IAM(AWSConfig); + + if (!collection.iam || + !collection.iam.listRoles || + !collection.iam.listRoles[AWSConfig.region] || + !collection.iam.listRoles[AWSConfig.region].data) return callback(); + + async.eachLimit(collection.iam.listRoles[AWSConfig.region].data, 10, function(role, cb){ + if (!role.Arn || + !collection.iam.listRoles || + !collection.iam.listRoles[AWSConfig.region] || + !collection.iam.listRoles[AWSConfig.region].data) { + + return cb(); + } + + collection.iam.getRole[AWSConfig.region][role.RoleName] = {}; + + helpers.makeCustomCollectorCall(iam, 'getRole', {RoleName: role.RoleName}, retries, null, null, null, function(err, data) { + if (err) { + collection.iam.getRole[AWSConfig.region][role.RoleName].err = err; + } + if (data) { + delete data['ResponseMetadata']; + + data.Role.AssumeRolePolicyDocument = helpers.normalizePolicyDocument(data.Role.AssumeRolePolicyDocument); + collection.iam.getRole[AWSConfig.region][role.RoleName].data = data; + } + + cb(); + }); + }, function(){ + callback(); + }); +}; diff --git a/exports.js b/exports.js index dcbf2a3dbb..9364256d79 100644 --- a/exports.js +++ b/exports.js @@ -32,7 +32,7 @@ module.exports = { 'restrictExternalTraffic' : require(__dirname + '/plugins/aws/appmesh/restrictExternalTraffic.js'), 'appmeshTLSRequired' : require(__dirname + '/plugins/aws/appmesh/appmeshTLSRequired.js'), 'appmeshVGHealthChecks' : require(__dirname + '/plugins/aws/appmesh/appmeshVGHealthChecks.js'), - + 'asgMultiAz' : require(__dirname + '/plugins/aws/autoscaling/asgMultiAz.js'), 'asgActiveNotifications' : require(__dirname + '/plugins/aws/autoscaling/asgActiveNotifications.js'), @@ -242,6 +242,7 @@ module.exports = { 'ebsVolumeHasTags' : require(__dirname + '/plugins/aws/ec2/ebsVolumeHasTags.js'), 'openAllPortsProtocolsEgress' : require(__dirname + '/plugins/aws/ec2/openAllPortsProtocolsEgress.js'), 'defaultSecurityGroupInUse' : require(__dirname + '/plugins/aws/ec2/defaultSecurityGroupInUse.js'), + 'ec2NetworkExposure' : require(__dirname + '/plugins/aws/ec2/ec2NetworkExposure.js'), 'efsCmkEncrypted' : require(__dirname + '/plugins/aws/efs/efsCmkEncrypted.js'), 'efsEncryptionEnabled' : require(__dirname + '/plugins/aws/efs/efsEncryptionEnabled.js'), @@ -323,7 +324,7 @@ module.exports = { 'opensearchPublicEndpoint' : require(__dirname + '/plugins/aws/opensearch/opensearchPublicEndpoint.js'), 'opensearchRequireIAMAuth' : require(__dirname + '/plugins/aws/opensearch/opensearchRequireIAMAuth.js'), 'opensearchTlsVersion' : require(__dirname + '/plugins/aws/opensearch/opensearchTlsVersion.js'), - 'opensearchUpgradeAvailable' : require(__dirname + '/plugins/aws/opensearch/opensearchUpgradeAvailable.js'), + 'opensearchUpgradeAvailable' : require(__dirname + '/plugins/aws/opensearch/opensearchUpgradeAvailable.js'), 'opensearchVersion' : require(__dirname + '/plugins/aws/opensearch/opensearchVersion.js'), 'opensearchZoneAwarenessEnabled': require(__dirname + '/plugins/aws/opensearch/opensearchZoneAwarenessEnabled.js'), @@ -715,7 +716,7 @@ module.exports = { 'storageAccountHasTags' : require(__dirname + '/plugins/azure/storageaccounts/storageAccountHasTags.js'), 'storageAccountPrivateEndpoint' : require(__dirname + '/plugins/azure/storageaccounts/storageAccountPrivateEndpoint.js'), 'infrastructureEncryption' : require(__dirname + '/plugins/azure/storageaccounts/infrastructureEncryption.js'), - 'queueServiceLoggingEnabled' : require(__dirname + '/plugins/azure/storageaccounts/queueServiceLoggingEnabled.js'), + 'queueServiceLoggingEnabled' : require(__dirname + '/plugins/azure/storageaccounts/queueServiceLoggingEnabled.js'), 'tableServiceLoggingEnabled' : require(__dirname + '/plugins/azure/storageaccounts/tableServiceLoggingEnabled.js'), 'blobServiceLoggingEnabled' : require(__dirname + '/plugins/azure/storageaccounts/blobServiceLoggingEnabled.js'), @@ -725,8 +726,8 @@ module.exports = { 'fileServiceAllAccessAcl' : require(__dirname + '/plugins/azure/fileservice/fileServiceAllAccessAcl.js'), 'tableServiceAllAccessAcl' : require(__dirname + '/plugins/azure/tableservice/tableServiceAllAccessAcl.js'), - 'queueServiceAllAccessAcl' : require(__dirname + '/plugins/azure/queueservice/queueServiceAllAccessAcl.js'), - + 'queueServiceAllAccessAcl' : require(__dirname + '/plugins/azure/queueservice/queueServiceAllAccessAcl.js'), + 'externalNetworkAccess' : require(__dirname + '/plugins/azure/containerapps/externalNetworkAccess.js'), 'containerAppManagedIdentity' : require(__dirname + '/plugins/azure/containerapps/containerAppManagedIdentity.js'), 'containerAppAuthEnabled' : require(__dirname + '/plugins/azure/containerapps/containerAppAuthEnabled.js'), @@ -805,6 +806,7 @@ module.exports = { 'vmDiskCMKRotation' : require(__dirname + '/plugins/azure/virtualmachines/vmDiskCMKRotation.js'), 'vmDiskPublicAccess' : require(__dirname + '/plugins/azure/virtualmachines/vmDiskPublicAccess.js'), 'computeGalleryRbacSharing' : require(__dirname + '/plugins/azure/virtualmachines/computeGalleryRbacSharing.js'), + 'vmNetworkExposure' : require(__dirname + '/plugins/azure/virtualmachines/vmNetworkExposure.js'), 'bastionHostExists' : require(__dirname + '/plugins/azure/bastion/bastionHostExists.js'), 'bastionHostDiagnosticLogs' : require(__dirname + '/plugins/azure/bastion/bastionHostDiagnosticLogs.js'), @@ -815,7 +817,7 @@ module.exports = { 'monitorLogsEnabled' : require(__dirname + '/plugins/azure/monitor/monitorLogsEnabled.js'), 'diagnosticsCapturedCategories' : require(__dirname + '/plugins/azure/monitor/diagnosticsCapturedCategories.js'), 'diagnosticsSettingsEnabled' : require(__dirname + '/plugins/azure/monitor/diagnosticsSettingsEnabled.js'), - 'resourceAppropriateSKU' : require(__dirname + '/plugins/azure/monitor/monitorResourceSku.js'), + 'resourceAppropriateSKU' : require(__dirname + '/plugins/azure/monitor/monitorResourceSku.js'), 'securityPolicyAlertsEnabled' : require(__dirname + '/plugins/azure/logalerts/securityPolicyAlertsEnabled.js'), 'nsgLoggingEnabled' : require(__dirname + '/plugins/azure/logalerts/nsgLoggingEnabled.js'), @@ -966,7 +968,7 @@ module.exports = { 'sqlServerRecurringScans' : require(__dirname + '/plugins/azure/sqlserver/sqlServerRecurringScans.js'), 'sqlServerSendScanReports' : require(__dirname + '/plugins/azure/sqlserver/sqlServerSendScanReports.js'), 'sqlServerHasTags' : require(__dirname + '/plugins/azure/sqlserver/sqlServerHasTags.js'), - 'restrictOutboundNetworking' : require(__dirname + '/plugins/azure/sqlserver/restrictOutboundNetworking.js'), + 'restrictOutboundNetworking' : require(__dirname + '/plugins/azure/sqlserver/restrictOutboundNetworking.js'), 'auditOperationsEnabled' : require(__dirname + '/plugins/azure/sqlserver/auditOperationsEnabled.js'), 'serverConnectionPolicy' : require(__dirname + '/plugins/azure/sqlserver/serverConnectionPolicy.js'), 'auditStorageAuthType' : require(__dirname + '/plugins/azure/sqlserver/auditStorageAuthType.js'), @@ -1048,7 +1050,7 @@ module.exports = { 'dbLedgerEnabled' : require(__dirname + '/plugins/azure/sqldatabases/dbLedgerEnabled.js'), 'dbEnableSecureEnclaves' : require(__dirname + '/plugins/azure/sqldatabases/dbEnableSecureEnclaves.js'), 'dbDataDiscoveryClassification' : require(__dirname + '/plugins/azure/sqldatabases/dbDataDiscoveryClassification.js'), - + 'lbHttpsOnly' : require(__dirname + '/plugins/azure/loadbalancer/lbHttpsOnly.js'), 'lbNoInstances' : require(__dirname + '/plugins/azure/loadbalancer/lbNoInstances.js'), 'lbHasTags' : require(__dirname + '/plugins/azure/loadbalancer/lbHasTags.js'), @@ -1080,7 +1082,7 @@ module.exports = { 'cosmosdbHasTags' : require(__dirname + '/plugins/azure/cosmosdb/cosmosdbHasTags.js'), 'cosmosdbManagedIdentity' : require(__dirname + '/plugins/azure/cosmosdb/cosmosdbManagedIdentity.js'), 'cosmosdbLocalAuth' : require(__dirname + '/plugins/azure/cosmosdb/cosmosdbLocalAuth.js'), - + 'checkAdvisorRecommendations' : require(__dirname + '/plugins/azure/advisor/checkAdvisorRecommendations.js'), 'enableDefenderForStorage' : require(__dirname + '/plugins/azure/defender/enableDefenderForStorage.js'), @@ -1102,7 +1104,7 @@ module.exports = { 'applicationGatewayHasTags' : require(__dirname + '/plugins/azure/applicationGateway/applicationGatewayHasTags.js'), 'agSecurityLoggingEnabled' : require(__dirname + '/plugins/azure/applicationGateway/agSecurityLoggingEnabled.js'), 'agSslPolicy' : require(__dirname + '/plugins/azure/applicationGateway/agSslPolicy'), - 'agPreventionModeEnabled' : require(__dirname + '/plugins/azure/applicationGateway/agPreventionModeEnabled.js'), + 'agPreventionModeEnabled' : require(__dirname + '/plugins/azure/applicationGateway/agPreventionModeEnabled.js'), 'agRequestBodyInspection' : require(__dirname + '/plugins/azure/applicationGateway/agRequestBodyInspection'), 'agRequestBodySize' : require(__dirname + '/plugins/azure/applicationGateway/agRequestBodySize.js'), 'agHttpsListenerOnly' : require(__dirname + '/plugins/azure/applicationGateway/agHttpsListenerOnly.js'), @@ -1152,7 +1154,7 @@ module.exports = { 'namespaceLoggingEnabled' : require(__dirname + '/plugins/azure/servicebus/namespaceLoggingEnabled.js'), 'namespacePublicAccess' : require(__dirname + '/plugins/azure/servicebus/namespacePublicAccess.js'), 'namespaceInfraEncryption' : require(__dirname + '/plugins/azure/servicebus/namespaceInfraEncryption.js'), - + 'amsStorageAccountIdentity' : require(__dirname + '/plugins/azure/mediaServices/amsStorageAccountIdentity.js'), 'amsDiagnosticLogsEnabled' : require(__dirname + '/plugins/azure/mediaServices/amsDiagnosticLogsEnabled.js'), 'amsPublicAccessDisabled' : require(__dirname + '/plugins/azure/mediaServices/amsPublicAccessDisabled.js'), @@ -1177,7 +1179,7 @@ module.exports = { 'healthMonitoringExtensionHttps': require(__dirname + '/plugins/azure/virtualmachinescaleset/healthMonitoringExtensionHttps.js'), 'vmssBootDiagnosticsEnabled' : require(__dirname + '/plugins/azure/virtualmachinescaleset/vmssBootDiagnosticsEnabled'), 'vmssWindowsAntiMalwareExt' : require(__dirname + '/plugins/azure/virtualmachinescaleset/vmssWindowsAntiMalwareExt'), - + 'appConfigManagedIdentity' : require(__dirname + '/plugins/azure/appConfigurations/appConfigManagedIdentity.js'), 'appConfigurationDiagnosticLogs': require(__dirname + '/plugins/azure/appConfigurations/appConfigurationDiagnosticLogs.js'), 'appConfigurationPublicAccess' : require(__dirname + '/plugins/azure/appConfigurations/appConfigurationPublicAccess.js'), @@ -1187,7 +1189,7 @@ module.exports = { 'automationAcctDiagnosticLogs' : require(__dirname + '/plugins/azure/automationAccounts/automationAcctDiagnosticLogs.js'), 'automationAcctManagedIdentity' : require(__dirname + '/plugins/azure/automationAccounts/automationAcctManagedIdentity.js'), - 'automationAcctApprovedCerts' : require(__dirname + '/plugins/azure/automationAccounts/automationAcctApprovedCerts.js'), + 'automationAcctApprovedCerts' : require(__dirname + '/plugins/azure/automationAccounts/automationAcctApprovedCerts.js'), 'automationAcctEncryptedVars' : require(__dirname + '/plugins/azure/automationAccounts/automationAcctEncryptedVars.js'), 'automationAcctPublicAccess' : require(__dirname + '/plugins/azure/automationAccounts/automationAcctPublicAccess.js'), 'automationAcctExpiredWebhooks' : require(__dirname + '/plugins/azure/automationAccounts/automationAcctExpiredWebhooks.js'), @@ -1213,7 +1215,7 @@ 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'), @@ -1436,6 +1438,7 @@ module.exports = { 'confidentialComputingEnabled' : require(__dirname + '/plugins/google/compute/confidentialComputingEnabled.js'), 'imagesCMKEncrypted' : require(__dirname + '/plugins/google/compute/imagesCMKEncrypted.js'), 'snapshotEncryption' : require(__dirname + '/plugins/google/compute/snapshotEncryption.js'), + 'instanceNetworkExposure' : require(__dirname + '/plugins/google/compute/instanceNetworkExposure.js'), 'keyRotation' : require(__dirname + '/plugins/google/cryptographickeys/keyRotation.js'), 'keyProtectionLevel' : require(__dirname + '/plugins/google/cryptographickeys/keyProtectionLevel.js'), diff --git a/helpers/asl/asl-1.js b/helpers/asl/asl-1.js index 55a68c39cc..06bfed0541 100644 --- a/helpers/asl/asl-1.js +++ b/helpers/asl/asl-1.js @@ -191,6 +191,30 @@ var validate = function(condition, conditionResult, inputResultsArr, message, pr message.push(`${condition.parsed} is not the right property type for this operation`); return 2; } + } else if (condition.op == 'NOTCONTAINS') { + var conditionStringified = JSON.stringify(condition.parsed); + if (condition.value && condition.value.includes(':')) { + + var conditionKey = condition.value.split(/:(?!.*:)/)[0]; + var conditionValue = condition.value.split(/:(?!.*:)/)[1]; + + if (conditionStringified.includes(conditionKey) && !conditionStringified.includes(conditionValue)){ + message.push(`${property}: ${condition.value} not found in ${conditionStringified}`); + return 0; + } else { + message.push(`${condition.value} found in ${conditionStringified}`); + return 2; + } + } else if (conditionStringified && !conditionStringified.includes(condition.value)) { + message.push(`${property}: ${condition.value} not found in ${conditionStringified}`); + return 0; + } else if (conditionStringified && conditionStringified.length){ + message.push(`${condition.value} found in ${conditionStringified}`); + return 2; + } else { + message.push(`${condition.parsed} is not the right property type for this operation`); + return 2; + } } else { // Recurse into the same function var subProcessed = []; @@ -280,6 +304,17 @@ var validate = function(condition, conditionResult, inputResultsArr, message, pr message.push(`${condition.parsed} is not the right property type for this operation`); return 2; } + } else if (condition.op == 'NOTCONTAINS') { + if (condition.parsed && condition.parsed.length && !condition.parsed.includes(condition.value)) { + message.push(`${property}: ${condition.value} not found in ${condition.parsed}`); + return 0; + } else if (condition.parsed && condition.parsed.length){ + message.push(`${condition.value} found in ${condition.parsed}`); + return 2; + } else { + message.push(`${condition.parsed} is not the right property type for this operation`); + return 2; + } } return conditionResult; } @@ -331,7 +366,7 @@ var runValidation = function(obj, condition, inputResultsArr, nestedResultArr) { propertyArr.shift(); property = propertyArr.join('.'); condition.property = property; - if (condition.op !== 'CONTAINS') { + if (condition.op !== 'CONTAINS' || condition.op !== 'NOTCONTAINS') { condition.parsed.forEach(parsed => { if (property.includes('[*]')) { runValidation(parsed, condition, inputResultsArr, nestedResultArr); @@ -567,4 +602,4 @@ var asl = function(source, input, resourceMap, cloud, accountId, callback) { callback(null, results, data); }; -module.exports = asl; \ No newline at end of file +module.exports = asl; diff --git a/helpers/aws/api.js b/helpers/aws/api.js index ee79979aaa..a0227ae405 100644 --- a/helpers/aws/api.js +++ b/helpers/aws/api.js @@ -1561,7 +1561,25 @@ var calls = { paginate: 'NextToken' }, getFindings: { - paginate: 'NextToken' + property: 'Findings', + paginate: 'NextToken', + params: { + MaxResults: 100, + Filters: { + RecordState: [ + { + Comparison: 'EQUALS', + Value: 'ACTIVE' + } + ], + WorkflowStatus: [ + { + Comparison: 'EQUALS', + Value: 'NEW' + } + ] + } + } } }, SageMaker: { @@ -2109,7 +2127,6 @@ var postcalls = [ filterKey: 'id', filterValue: 'projectId' }, - sendIntegration: serviceMap['CodeStar'] }, CustomerProfiles: { getDomain: { @@ -2175,7 +2192,7 @@ var postcalls = [ reliesOnService: 'docdb', reliesOnCall: 'describeDBClusters', filterKey: 'ResourceName', - filterValue: 'DBClusterArn' + filterValue: 'DBClusterArn' }, sendIntegration: serviceMap['DocumentDB'] }, @@ -3015,8 +3032,7 @@ var postcalls = [ getRole: { reliesOnService: 'iam', reliesOnCall: 'listRoles', - filterKey: 'RoleName', - filterValue: 'RoleName' + override: true }, getUser: { reliesOnService: 'iam', diff --git a/helpers/aws/api_multipart.js b/helpers/aws/api_multipart.js index 13d19bdb93..4590b8ffb0 100644 --- a/helpers/aws/api_multipart.js +++ b/helpers/aws/api_multipart.js @@ -1046,7 +1046,25 @@ var calls = [ paginate: 'NextToken' }, getFindings: { - paginate: 'NextToken' + property: 'Findings', + paginate: 'NextToken', + params: { + MaxResults: 100, + Filters: { + RecordState: [ + { + Comparison: 'EQUALS', + Value: 'ACTIVE' + } + ], + WorkflowStatus: [ + { + Comparison: 'EQUALS', + Value: 'NEW' + } + ] + } + } } }, Transfer: { @@ -1424,7 +1442,7 @@ var postcalls = [ reliesOnService: 'docdb', reliesOnCall: 'describeDBClusters', filterKey: 'ResourceName', - filterValue: 'DBClusterArn' + filterValue: 'DBClusterArn' }, }, DynamoDB: { @@ -2323,8 +2341,7 @@ var postcalls = [ getRole: { reliesOnService: 'iam', reliesOnCall: 'listRoles', - filterKey: 'RoleName', - filterValue: 'RoleName', + override: true, rateLimit: 500 }, listRolePolicies: { diff --git a/helpers/aws/functions.js b/helpers/aws/functions.js index b681ed5743..3b66ac6acf 100644 --- a/helpers/aws/functions.js +++ b/helpers/aws/functions.js @@ -174,15 +174,18 @@ function findOpenPorts(groups, ports, service, region, results, cache, config, c } } } - + return; } -function checkNetworkInterface(groupId, groupName, resultsString, region, results, resource, cache) { +function checkNetworkInterface(groupId, groupName, resultsString, region, results, resource, cache, bool = false) { const describeNetworkInterfaces = helpers.addSource(cache, {}, ['ec2', 'describeNetworkInterfaces', region]); if (!describeNetworkInterfaces || describeNetworkInterfaces.err || !describeNetworkInterfaces.data) { + if (bool) { + return false; + } helpers.addResult(results, 3, 'Unable to query for network interfaces: ' + helpers.addError(describeNetworkInterfaces), region); return; @@ -198,20 +201,28 @@ function checkNetworkInterface(groupId, groupName, resultsString, region, result } } } + if (bool && !networksWithSecurityGroup.length) { + return groupId; + } + let exposedENI; if (hasOpenSecurityGroup) { let hasPublicIp = false; for (var eni of networksWithSecurityGroup) { if (eni.Association && eni.Association.PublicIp) { hasPublicIp = true; + exposedENI = `sg ${groupId} > eni ${eni.NetworkInterfaceId}`; break; } } if (hasPublicIp) { + if (bool) return exposedENI; addResult(results, 2, `Security Group ${groupId}(${groupName}) is associated with an ENI that is publicly exposed`, region, resource); } else { + if (bool) return false; addResult(results, 0, `Security Group ${groupId} (${groupName}) is only exposed internally`, region, resource); } } else { + if (bool) return false; addResult(results, 2, resultsString, region, resource); } } @@ -291,7 +302,7 @@ function userGlobalAccess(statement, restrictedPermissions) { statement.Action && restrictedPermissions.some(permission=> statement.Action.includes(permission))) { return true; } - + return false; } @@ -324,7 +335,7 @@ function crossAccountPrincipal(principal, accountId, fetchPrincipals, settings={ } function hasFederatedUserRole(policyDocument) { - // true iff every statement refers to federated user access + // true iff every statement refers to federated user access for (let statement of policyDocument) { if (statement.Action && !statement.Action.includes('sts:AssumeRoleWithSAML') && @@ -339,13 +350,13 @@ function extractStatementPrincipals(statement) { let response = []; if (statement.Principal) { let principal = statement.Principal; - + if (typeof principal === 'string') { return [principal]; } if (!principal.AWS) return response; - + var awsPrincipals = principal.AWS; if (!Array.isArray(awsPrincipals)) { awsPrincipals = [awsPrincipals]; @@ -516,7 +527,7 @@ function getS3BucketLocation(cache, region, bucketName) { if (getBucketLocation.data.LocationConstraint && regions.all.includes(getBucketLocation.data.LocationConstraint)) return getBucketLocation.data.LocationConstraint; else if (getBucketLocation.data.LocationConstraint && - !regions.all.includes(getBucketLocation.data.LocationConstraint)) return 'global'; + !regions.all.includes(getBucketLocation.data.LocationConstraint)) return 'global'; else return 'us-east-1'; } @@ -893,7 +904,7 @@ function getOrganizationAccounts(listAccounts, accountId) { if (listAccounts.data && listAccounts.data.length){ listAccounts.data.forEach(account => { if (account.Id && account.Id !== accountId) orgAccountIds.push(account.Id); - }); + }); } return orgAccountIds; @@ -903,7 +914,7 @@ function getUsedSecurityGroups(cache, results, region) { let result = []; const describeNetworkInterfaces = helpers.addSource(cache, {}, ['ec2', 'describeNetworkInterfaces', region]); - + if (!describeNetworkInterfaces || describeNetworkInterfaces.err || !describeNetworkInterfaces.data) { helpers.addResult(results, 3, 'Unable to query for network interfaces: ' + helpers.addError(describeNetworkInterfaces), region); @@ -912,7 +923,7 @@ function getUsedSecurityGroups(cache, results, region) { const listFunctions = helpers.addSource(cache, {}, ['lambda', 'listFunctions', region]); - + if (!listFunctions || listFunctions.err || !listFunctions.data) { helpers.addResult(results, 3, 'Unable to list lambda functions: ' + helpers.addError(listFunctions), region); @@ -967,7 +978,7 @@ function getSubnetRTMap(subnets, routeTables) { }); } if (routeTable.VpcId && routeTable.RouteTableId && routeTable.Associations && - routeTable.Associations.find(association => association.Main) && !vpcRTMap[routeTable.VpcId]) vpcRTMap[routeTable.VpcId] = routeTable.RouteTableId; + routeTable.Associations.find(association => association.Main) && !vpcRTMap[routeTable.VpcId]) vpcRTMap[routeTable.VpcId] = routeTable.RouteTableId; }); subnets.forEach(subnet => { @@ -1023,6 +1034,7 @@ var debugApiCalls = function(call, service, debugMode, finished) { }; var logError = function(service, call, region, err, errorsLocal, apiCallErrorsLocal, apiCallTypeErrorsLocal, totalApiCallErrorsLocal, errorSummaryLocal, errorTypeSummaryLocal, debugMode) { + if (debugMode) console.log(`[INFO] ${service}:${call} returned error: ${err.message}`); totalApiCallErrorsLocal++; if (!errorSummaryLocal[service]) errorSummaryLocal[service] = {}; @@ -1112,7 +1124,7 @@ function processFieldSelectors(fieldSelectors,buckets ,startsWithBuckets,notEnds var checkTags = function(cache, resourceName, resourceList, region, results, settings={}) { const allResources = helpers.addSource(cache, {}, ['resourcegroupstaggingapi', 'getResources', region]); - + if (!allResources || allResources.err || !allResources.data) { helpers.addResult(results, 3, 'Unable to query all resources from group tagging api:' + helpers.addError(allResources), region); @@ -1128,7 +1140,7 @@ var checkTags = function(cache, resourceName, resourceList, region, results, set }); resourceList.map(arn => { - if (filteredResourceARN.includes(arn)) { + if (filteredResourceARN.includes(arn)) { helpers.addResult(results, 0, `${resourceName} has tags`, region, arn); } else { helpers.addResult(results, 2, `${resourceName} does not have any tags`, region, arn); @@ -1136,6 +1148,134 @@ var checkTags = function(cache, resourceName, resourceList, region, results, set }); }; +function checkSecurityGroup(securityGroup, cache, region) { + let allowsAllTraffic; + for (var p in securityGroup.IpPermissions) { + var permission = securityGroup.IpPermissions[p]; + + for (var k in permission.IpRanges) { + var range = permission.IpRanges[k]; + + if (range.CidrIp === '0.0.0.0/0') { + allowsAllTraffic = true; + } + } + + for (var l in permission.Ipv6Ranges) { + var rangeV6 = permission.Ipv6Ranges[l]; + + if (rangeV6.CidrIpv6 === '::/0') { + allowsAllTraffic = true; + } + } + } + + if (allowsAllTraffic) { + return checkNetworkInterface(securityGroup.GroupId, securityGroup.GroupName, '', region, null, securityGroup, cache, true); + } + return false; +} + +var checkNetworkExposure = function(cache, source, subnetId, securityGroups, region, results) { + + var internetExposed = ''; + + // Scenario 1: check if resource is in a private subnet + let subnetRouteTableMap, privateSubnets; + var describeSubnets = helpers.addSource(cache, source, + ['ec2', 'describeSubnets', region]); + var describeRouteTables = helpers.addSource(cache, {}, + ['ec2', 'describeRouteTables', region]); + + if (!describeRouteTables || describeRouteTables.err || !describeRouteTables.data) { + helpers.addResult(results, 3, + 'Unable to query for route tables: ' + helpers.addError(describeRouteTables), region); + } else if (!describeSubnets || describeSubnets.err || !describeSubnets.data) { + helpers.addResult(results, 3, + 'Unable to query for subnets: ' + helpers.addError(describeSubnets), region); + } else if (describeSubnets.data.length && subnetId) { + subnetRouteTableMap = getSubnetRTMap(describeSubnets.data, describeRouteTables.data); + privateSubnets = getPrivateSubnets(subnetRouteTableMap, describeSubnets.data, describeRouteTables.data); + if (privateSubnets && privateSubnets.length && privateSubnets.find(subnet => subnet === subnetId)) { + return ''; + } + // If the subnet is not private we will check if security groups and Network ACLs allow internal traffic + } + + // Scenario 2: check if security group allows all traffic + var describeSecurityGroups = helpers.addSource(cache, source, + ['ec2', 'describeSecurityGroups', region]); + + + if (!describeSecurityGroups || describeSecurityGroups.err || !describeSecurityGroups.data) { + helpers.addResult(results, 3, + 'Unable to query for security groups: ' + helpers.addError(describeSecurityGroups), region); + } else if (describeSecurityGroups.data.length && securityGroups && securityGroups.length) { + let instanceSGs = describeSecurityGroups.data.filter(sg => securityGroups.find(isg => isg.GroupId === sg.GroupId)); + for (var group of instanceSGs) { + let exposedSG = checkSecurityGroup(group, cache, region); + if (!exposedSG) { + return ''; + } else { + internetExposed += exposedSG; + } + } + } + + + + // Scenario 3: check if Network ACLs associated with the resource allow all traffic + var describeNetworkAcls = helpers.addSource(cache, source, + ['ec2', 'describeNetworkAcls', region]); + + if (!describeNetworkAcls || describeNetworkAcls.err || !describeNetworkAcls.data) { + helpers.addResult(results, 3, + `Unable to query for Network ACLs: ${helpers.addError(describeNetworkAcls)}`, region); + } else if (describeNetworkAcls.data.length && subnetId) { + let instanceACL = describeNetworkAcls.data.find(acl => acl.Associations.find(assoc => assoc.SubnetId === subnetId)); + if (instanceACL && instanceACL.Entries && instanceACL.Entries.length) { + + const allowRules = instanceACL.Entries.filter(entry => + entry.Egress === false && + entry.RuleAction === 'allow' && + (entry.CidrBlock === '0.0.0.0/0' || entry.Ipv6CidrBlock === '::/0') + ); + + + // Checking if there's a deny rule with lower rule number + let exposed = allowRules.some(allowRule => { + // Check if there's a deny with a lower rule number + return !instanceACL.Entries.some(denyRule => { + return ( + denyRule.Egress === false && + denyRule.RuleAction === 'deny' && + ( + (allowRule.CidrBlock && denyRule.CidrBlock === allowRule.CidrBlock) || + (allowRule.Ipv6CidrBlock && denyRule.Ipv6CidrBlock === allowRule.Ipv6CidrBlock) + ) && + denyRule.Protocol === allowRule.Protocol && + ( + denyRule.PortRange ? + (allowRule.PortRange && + denyRule.PortRange.From === allowRule.PortRange.From && + denyRule.PortRange.To === allowRule.PortRange.To) : true + ) && + denyRule.RuleNumber < allowRule.RuleNumber + ); + }); + }); + if (exposed) { + internetExposed += `> nacl ${instanceACL.NetworkAclId}`; + } else { + internetExposed = ''; + } + } + + } + + return internetExposed; +}; + module.exports = { addResult: addResult, findOpenPorts: findOpenPorts, @@ -1173,4 +1313,6 @@ module.exports = { checkConditions: checkConditions, processFieldSelectors: processFieldSelectors, checkNetworkInterface: checkNetworkInterface, -}; \ No newline at end of file + checkNetworkExposure: checkNetworkExposure, +}; + diff --git a/helpers/azure/functions.js b/helpers/azure/functions.js index 3b8261074f..3fdc0a5652 100644 --- a/helpers/azure/functions.js +++ b/helpers/azure/functions.js @@ -392,16 +392,16 @@ function checkFlexibleServerConfigs(servers, cache, source, location, results, s } function checkMicrosoftDefender(pricings, serviceName, serviceDisplayName, results, location ) { - + let pricingData = pricings.data.find((pricing) => pricing.name.toLowerCase() === serviceName); if (pricingData) { if (pricingData.pricingTier.toLowerCase() === 'standard') { - addResult(results, 0, `Azure Defender is enabled for ${serviceDisplayName}`, location, pricingData.id); + addResult(results, 0, `Azure Defender is enabled for ${serviceDisplayName}`, location, pricingData.id); } else { - addResult(results, 2, `Azure Defender is not enabled for ${serviceDisplayName}`, location, pricingData.id); + addResult(results, 2, `Azure Defender is not enabled for ${serviceDisplayName}`, location, pricingData.id); } } else { - addResult(results, 2, `Azure Defender is not enabled for ${serviceDisplayName}`, location); + addResult(results, 2, `Azure Defender is not enabled for ${serviceDisplayName}`, location); } } @@ -612,12 +612,12 @@ function remediateOpenPorts(putCall, pluginName, protocol, port, config, cache, sourceAddressArr.push(settings.input[inputKey]); sourceAddressArr.splice(sourceAddressArr.indexOf(publicString), 1); - // this if the input specified already exists + // this if the input specified already exists } else if (settings.input && settings.input[inputKey] && sourceAddressArr.indexOf(settings.input[inputKey]) > -1) { ipType === 'ipv4' ? localIpExists = true : localIpV6Exists = true; sourceAddressArr.splice(sourceAddressArr.indexOf(publicString), 1); - // this is if there is no input and the failing port is in an array (destinationPortRanges). Will remove the port from the array + // this is if there is no input and the failing port is in an array (destinationPortRanges). Will remove the port from the array } else if ((!settings.input || !settings.input[inputKey]) && (failingRulePortIndex[failingPermission.name]) && !spliced) { spliced = true; failingPermission.properties['destinationPortRanges'].splice([failingRulePortIndex[failingPermission.name]], 1); @@ -741,6 +741,56 @@ function remediateOpenPorts(putCall, pluginName, protocol, port, config, cache, }); } +function checkSecurityGroup(securityGroups) { + var openPrefix = ['*', '0.0.0.0', '0.0.0.0/0', '', '/0', '::/0', 'internet']; + + const allRules = securityGroups.flatMap(nsg => + [ + ...(nsg.securityRules ? nsg.securityRules.map(rule => ({ + ...rule, + nsgName: nsg.name + })) : []), + ...(nsg.defaultSecurityRules ? nsg.defaultSecurityRules.map(rule => ({ + ...rule, + nsgName: nsg.name + })) : []) + ] + ); + + // sorting by priority + const sortedRules = allRules.sort((a, b) => a.properties.priority - b.properties.priority); + + // The most restrictive rule takes precedence + for (const rule of sortedRules) { + if (rule.properties.direction === "Inbound" && openPrefix.includes(rule.properties.sourceAddressPrefix)) { + if (rule.properties.access === "Deny") { + return {exposed: false}; + } + if (rule.properties.access === "Allow") { + return {exposed: true, nsg: rule.nsgName}; + } + } + } + + return {exposed: true}; +} + +function checkNetworkExposure(cache, source, networkInterfaces, securityGroups, region, results) { + let exposedPath = ''; + + if (securityGroups && securityGroups.length) { + // Scenario 1: check if security group allow all inbound traffic + let exposedSG = checkSecurityGroup(securityGroups); + if (exposedSG && exposedSG.exposed) { + if (exposedSG.nsg) { + exposedPath += `nsg ${exposedSG.nsg}` + } + } + + return exposedPath + } +} + module.exports = { addResult: addResult, findOpenPorts: findOpenPorts, @@ -753,6 +803,7 @@ module.exports = { remediateOpenPorts: remediateOpenPorts, remediateOpenPortsHelper: remediateOpenPortsHelper, checkMicrosoftDefender: checkMicrosoftDefender, - checkFlexibleServerConfigs:checkFlexibleServerConfigs + checkFlexibleServerConfigs: checkFlexibleServerConfigs, + checkNetworkExposure: checkNetworkExposure, }; diff --git a/helpers/google/functions.js b/helpers/google/functions.js index ba1c667897..f8b9367bef 100644 --- a/helpers/google/functions.js +++ b/helpers/google/functions.js @@ -365,7 +365,7 @@ function checkOrgPolicy(orgPolicies, constraintName, constraintType, shouldBeEna } else { isEnabled = ifNotFound; } - } + } let successMessage = `"${displayName}" constraint is enforced at the organization level.`; let failureMessage = `"${displayName}" constraint is not enforced at the organization level.`; let status, message; @@ -402,6 +402,54 @@ function checkIAMRole(iamPolicy, roles, region, results, project, notFoundMessag } } +function checkFirewallRules(firewallRules) { + firewallRules.sort((a, b) => (a.priority || 1000) - (b.priority || 1000)); + + for (const firewallRule of firewallRules) { + if (firewallRule.direction !== 'INGRESS' || firewallRule.disabled) { + continue; + } + + const networkName = firewallRule.network ? firewallRule.network.split('/').pop() : ''; + + let allSources = firewallRule.sourceRanges && firewallRule.sourceRanges.some(sourceAddressPrefix => + sourceAddressPrefix === '*' || + sourceAddressPrefix === '0.0.0.0/0' || + sourceAddressPrefix === '::/0' || + sourceAddressPrefix.includes('/0') || + sourceAddressPrefix.toLowerCase() === 'internet' || + sourceAddressPrefix.includes('/0') + ); + + if (allSources && firewallRule.allowed && firewallRule.allowed.some(allow => !!allow.IPProtocol)) { + return { exposed: true, networkName: `vpc ${networkName}` }; + } + + if (allSources && firewallRule.denied && firewallRule.denied.some(deny => deny.IPProtocol === 'all')) { + return { exposed: false }; + } + + } + + return {exposed: true}; + + +} + +function checkNetworkExposure(cache, source, networks, firewallRules, region, results) { + let exposedPath = ''; + + if (firewallRules && firewallRules.length) { + // Scenario 1: check if any firewall rule allows all inbound traffic + let isExposed = checkFirewallRules(firewallRules); + if (isExposed.exposed) { + if (isExposed.networkName) { + return isExposed.networkName; + } + } + } + return exposedPath +} module.exports = { addResult: addResult, findOpenPorts: findOpenPorts, @@ -413,5 +461,6 @@ module.exports = { createResourceName: createResourceName, checkOrgPolicy: checkOrgPolicy, checkIAMRole: checkIAMRole, - findOpenAllPortsEgress: findOpenAllPortsEgress + findOpenAllPortsEgress: findOpenAllPortsEgress, + checkNetworkExposure: checkNetworkExposure }; diff --git a/helpers/shared.js b/helpers/shared.js index 6c2a4ca572..f230eb3c4f 100644 --- a/helpers/shared.js +++ b/helpers/shared.js @@ -92,14 +92,14 @@ var processIntegration = function(serviceName, settings, collection, calls, post }; var processIntegrationAdditionalData = function(serviceName, localSettings, localCollection, calls, postcalls, localEventCollection, callback){ - if (localCollection == undefined || - (localCollection && - (JSON.stringify(localCollection)==='{}' || - localCollection[serviceName.toLowerCase()] == undefined || - JSON.stringify(localCollection[serviceName.toLowerCase()])==='{}'))) { + if (!localCollection || + !Object.keys(localCollection).length || + !localCollection[serviceName.toLowerCase()] || + !Object.keys(localCollection[serviceName.toLowerCase()]).length) { return callback(null); } + let callsMap = calls[serviceName] ? Object.keys(calls[serviceName]) : null; let foundData=[]; diff --git a/plugins/alibaba/securitycenter/securityCenterEdition.js b/plugins/alibaba/securitycenter/securityCenterEdition.js index eaf4804d72..254d116da1 100644 --- a/plugins/alibaba/securitycenter/securityCenterEdition.js +++ b/plugins/alibaba/securitycenter/securityCenterEdition.js @@ -1,4 +1,5 @@ var helpers = require('../../../helpers/alibaba'); +const async = require('async'); module.exports = { title: 'Security Center Edition', @@ -6,7 +7,7 @@ module.exports = { domain: 'Management and Governance', severity: 'Medium', description: 'Ensure that your cloud Security Center edition is Advanced or plus.', - more_info: 'Premium Security Center editions like Advanced or Enterprise Edition provides crucial features liekthreat detection for network and endpoints, ' + + more_info: 'Premium Security Center editions like Advanced or Enterprise Edition provides crucial features liekthreat detection for network and endpoints, ' + 'providing malware detection, webshell detection and anomaly detection in Security Center.', link: 'https://www.alibabacloud.com/help/product/28498.htm', recommended_action: 'Upgrade your Security Center edition to at least Advanced.', @@ -15,7 +16,7 @@ module.exports = { run: function(cache, settings, callback) { var results = []; var source = {}; - var region = helpers.defaultRegion(settings); + var regions = helpers.regions(settings); // Below map might not be accurate as I checked with Anti-virus and Advanced editions and API is returning // 6 and 5 respectively against the version key. As it will be costly to try all editions to get the acrual @@ -29,29 +30,32 @@ module.exports = { 5: 'Advanced', 6: 'Anti-virus' }; - - var describeVersionConfig = helpers.addSource(cache, source, - ['tds', 'DescribeVersionConfig', region]); - - if (!describeVersionConfig) { - return callback(null, results, source); - } - - if (describeVersionConfig.err || !describeVersionConfig.data) { - helpers.addResult(results, 3, - `Unable to query Security Center version config: ${helpers.addError(describeVersionConfig)}`, - region); - return callback(null, results, source); - } - - let securityVersion = describeVersionConfig.data.Version ? describeVersionConfig.data.Version : 1; - - if (securityVersion == 1 || securityVersion == 6) { - helpers.addResult(results, 2, `Security Center edition is ${versionIdNameMap[securityVersion]}`, region); - } else { - helpers.addResult(results, 0, `Security Center edition is ${versionIdNameMap[securityVersion]}`, region); - } - - callback(null, results, source); + async.each(regions.tds, function(region, rcb) { + var describeVersionConfig = helpers.addSource(cache, source, + ['tds', 'DescribeVersionConfig', region]); + + if (!describeVersionConfig) { + return rcb(); + } + + if (describeVersionConfig.err || !describeVersionConfig.data) { + helpers.addResult(results, 3, + `Unable to query Security Center version config: ${helpers.addError(describeVersionConfig)}`, + region); + return rcb(); + } + + let securityVersion = describeVersionConfig.data.Version ? describeVersionConfig.data.Version : 1; + + if (securityVersion == 1 || securityVersion == 6) { + helpers.addResult(results, 2, `Security Center edition is ${versionIdNameMap[securityVersion]}`, region); + } else { + helpers.addResult(results, 0, `Security Center edition is ${versionIdNameMap[securityVersion]}`, region); + } + + rcb(); + }, function(){ + callback(null, results, source); + }); } -}; \ No newline at end of file +}; diff --git a/plugins/aws/ec2/ec2NetworkExposure.js b/plugins/aws/ec2/ec2NetworkExposure.js new file mode 100644 index 0000000000..c86c247c24 --- /dev/null +++ b/plugins/aws/ec2/ec2NetworkExposure.js @@ -0,0 +1,61 @@ +var async = require('async'); +var helpers = require('../../../helpers/aws'); + +module.exports = { + title: 'Network Exposure', + category: 'EC2', + domain: 'Compute', + severity: 'Info', + description: 'Check if EC2 instances are exposed to the internet.', + more_info: 'EC2 instances exposed to the internet are at a higher risk of unauthorized access, data breaches, and cyberattacks. It’s crucial to limit exposure by securing access through proper configuration of security groups, NACLs, and route tables.', + link: 'https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Security.html', + recommended_action: 'Secure EC2 instances by restricting access with properly configured security groups and NACLs.', + apis: ['EC2:describeInstances', 'EC2:describeNetworkAcls', 'EC2:describeSecurityGroups', 'EC2:describeNetworkInterfaces', 'EC2:describeSubnets', 'EC2:describeRouteTables'], + realtime_triggers: ['ec2:RunInstances','ec2:TerminateInstances', 'ec2:CreateNetworkAcl', 'ec2:ReplaceNetworkAclEntry', 'ec2:ReplaceNetworkAclAssociation', + 'ec2:DeleteNetworkAcl', 'ec2:CreateSecurityGroup', 'ec2:AuthorizeSecurityGroupIngress','ec2:ModifySecurityGroupRules','ec2:RevokeSecurityGroupIngress', + 'ec2:DeleteSecurityGroup', 'ec2:ModifyInstanceAttribute', 'ec2:ModifySubnetAttribute'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var regions = helpers.regions(settings); + var awsOrGov = helpers.defaultPartition(settings); + + async.each(regions.ec2, function(region, rcb){ + var describeInstances = helpers.addSource(cache, source, + ['ec2', 'describeInstances', region]); + + if (!describeInstances) return rcb(); + + if (describeInstances.err || !describeInstances.data) { + helpers.addResult(results, 3, + 'Unable to query for instances: ' + helpers.addError(describeInstances), region); + return rcb(); + } + + if (!describeInstances.data.length) { + helpers.addResult(results, 0, 'No instances found', region); + return rcb(); + } + + for (var instances of describeInstances.data){ + const { OwnerId } = instances; + for (var instance of instances.Instances) { + const { InstanceId } = instance; + const arn = `arn:${awsOrGov}:ec2:${region}:${OwnerId}:instance/${InstanceId}`; + + let internetExposed = helpers.checkNetworkExposure(cache, source, instance.SubnetId, instance.SecurityGroups, region, results); + if (internetExposed && internetExposed.length) { + helpers.addResult(results, 2, `EC2 instance is exposed to the internet through ${internetExposed}`, region, arn); + } else { + helpers.addResult(results, 0, 'EC2 instance is not exposed to the internet', region, arn); + } + } + } + + rcb(); + }, function(){ + callback(null, results, source); + }); + } +}; diff --git a/plugins/aws/eks/eksKubernetesVersion.spec.js b/plugins/aws/eks/eksKubernetesVersion.spec.js index b53206f8d2..0997f85358 100644 --- a/plugins/aws/eks/eksKubernetesVersion.spec.js +++ b/plugins/aws/eks/eksKubernetesVersion.spec.js @@ -82,7 +82,7 @@ describe('eksKubernetesVersion', function () { "cluster": { "name": "mycluster", "arn": "arn:aws:eks:us-east-1:012345678911:cluster/mycluster", - "version": "1.27", + "version": "1.29", } } ); diff --git a/plugins/aws/elbv2/elbv2UnhealthyInstance.js b/plugins/aws/elbv2/elbv2UnhealthyInstance.js index 3d7ff74573..e7b1acf66a 100644 --- a/plugins/aws/elbv2/elbv2UnhealthyInstance.js +++ b/plugins/aws/elbv2/elbv2UnhealthyInstance.js @@ -37,27 +37,27 @@ module.exports = { } describeLoadBalancers.data.forEach(function(lb) { - var resource = describeLoadBalancers.LoadBalancerArn; + var resource = lb.LoadBalancerArn; var unhealthyInstances = 0; var describeTargetGroups = helpers.addSource(cache, source, ['elbv2', 'describeTargetGroups', region, lb.DNSName]); - + if (!describeTargetGroups || describeTargetGroups.err || !describeTargetGroups.data) { helpers.addResult(results, 3, `Unable to query for Application/Network load balancer target groups: ${helpers.addError(describeTargetGroups)}`, region, resource); return; } - + if (!describeTargetGroups.data.TargetGroups || !describeTargetGroups.data.TargetGroups.length) { helpers.addResult(results, 2, 'No Application/Network load balancer target groups found', region, resource); return; } - + describeTargetGroups.data.TargetGroups.forEach(function(tg) { var describeTargetHealth = helpers.addSource(cache, source, ['elbv2', 'describeTargetHealth', region, tg.TargetGroupArn]); - + if (!describeTargetHealth || describeTargetHealth.err || !describeTargetHealth.data || !describeTargetHealth.data.TargetHealthDescriptions || !describeTargetHealth.data.TargetHealthDescriptions.length) { return; @@ -81,10 +81,10 @@ module.exports = { region, resource); } }); - + rcb(); }, function(){ callback(null, results, source); }); } -}; \ No newline at end of file +}; diff --git a/plugins/aws/elbv2/elbv2WafEnabled.js b/plugins/aws/elbv2/elbv2WafEnabled.js index d9f67c5ad1..f238e2805d 100644 --- a/plugins/aws/elbv2/elbv2WafEnabled.js +++ b/plugins/aws/elbv2/elbv2WafEnabled.js @@ -12,7 +12,7 @@ module.exports = { recommended_action: '1. Enter the WAF service. 2. Enter Web ACLs and filter by the region the Application Load Balancer is in. 3. If no Web ACL is found, Create a new Web ACL in the region the ALB resides and in Resource type to associate with web ACL, select the Load Balancer. ', apis: ['ELBv2:describeLoadBalancers', 'WAFV2:listWebACLs', 'WAFRegional:listWebACLs', 'WAFV2:listResourcesForWebACL', 'WAFRegional:listResourcesForWebACL'], realtime_triggers: ['elasticloadbalancing:CreateLoadBalancer', 'wafv2:CreateWebAcl', 'wafv2:UpdateWebAcl', 'wafregional:CreateWebAcl', 'wafregional:UpdateWebAcl', 'wafv2:DeleteWebAcl', 'wafregional:DeleteWebAcl'], - + run: function(cache, settings, callback) { var results = []; var source = {}; @@ -83,15 +83,25 @@ module.exports = { return lcb(); } + var appElbFound = false; + loadBalancers.data.forEach(loadBalancer => { - if (loadBalancer.LoadBalancerArn && (resourcesToCheck.indexOf(loadBalancer.LoadBalancerArn) > -1)) { - resourcesToCheck.splice(resourcesToCheck.indexOf(loadBalancer.LoadBalancerArn), 1); - helpers.addResult(results, 0, 'The Application Load Balancer has WAF enabled', loc, loadBalancer.LoadBalancerArn); - } else { - helpers.addResult(results, 2, 'The Application Load Balancer does not have WAF enabled', loc, loadBalancer.LoadBalancerArn); + if (loadBalancer.Type && + loadBalancer.Type.toLowerCase() === 'application') { + appElbFound = true; + if (loadBalancer.LoadBalancerArn && (resourcesToCheck.indexOf(loadBalancer.LoadBalancerArn) > -1)) { + resourcesToCheck.splice(resourcesToCheck.indexOf(loadBalancer.LoadBalancerArn), 1); + helpers.addResult(results, 0, 'The Application Load Balancer has WAF enabled', loc, loadBalancer.LoadBalancerArn); + } else { + helpers.addResult(results, 2, 'The Application Load Balancer does not have WAF enabled', loc, loadBalancer.LoadBalancerArn); + } } }); + if (!appElbFound) { + helpers.addResult(results, 0, 'No Application Load Balancers found', loc); + } + lcb(); }, function() { callback(null, results, source); diff --git a/plugins/aws/iam/iamMasterManagerRoles.js b/plugins/aws/iam/iamMasterManagerRoles.js index 153461e679..1bf9f4f532 100644 --- a/plugins/aws/iam/iamMasterManagerRoles.js +++ b/plugins/aws/iam/iamMasterManagerRoles.js @@ -202,7 +202,7 @@ module.exports = { var getRolePolicy = helpers.addSource(cache, source, ['iam', 'getRolePolicy', region, role.RoleName]); - if (listRolePolicies.err || !listRolePolicies.data || !listRolePolicies.data.PolicyNames) { + if (!listRolePolicies || listRolePolicies.err || !listRolePolicies.data || !listRolePolicies.data.PolicyNames) { helpers.addResult(results, 3, 'Unable to query for IAM role policy for role: ' + role.RoleName + ': ' + helpers.addError(listRolePolicies), 'global', role.Arn); return cb(); diff --git a/plugins/aws/securityhub/securityHubActiveFindings.js b/plugins/aws/securityhub/securityHubActiveFindings.js index 6c1cea3b12..30f9d165e4 100644 --- a/plugins/aws/securityhub/securityHubActiveFindings.js +++ b/plugins/aws/securityhub/securityHubActiveFindings.js @@ -46,13 +46,13 @@ module.exports = { if (!getFindings) { helpers.addResult(results, 0, 'No active findings available', region, resource); return rcb(); - } else if (getFindings.err || !getFindings.data || !getFindings.data.Findings) { + } else if (getFindings.err || !getFindings.data ) { helpers.addResult(results, 3, `Unable to get SecurityHub findings: ${helpers.addError(getFindings)}`, region, resource); - } else if (!getFindings.data.Findings.length) { + } else if (!getFindings.data.length) { helpers.addResult(results, 0, 'No active findings available', region, resource); return rcb(); } else { - let activeFindings = getFindings.data.Findings.filter(finding => finding.CreatedAt && + let activeFindings = getFindings.data.filter(finding => finding.CreatedAt && helpers.hoursBetween(new Date, finding.CreatedAt) > config.securityhub_findings_fail); let status = (activeFindings && activeFindings.length) ? 2 : 0; diff --git a/plugins/aws/securityhub/securityHubActiveFindings.spec.js b/plugins/aws/securityhub/securityHubActiveFindings.spec.js index bbbe5c55d2..7ddb72010c 100644 --- a/plugins/aws/securityhub/securityHubActiveFindings.spec.js +++ b/plugins/aws/securityhub/securityHubActiveFindings.spec.js @@ -12,51 +12,22 @@ const describeHub = { const getFindings = [ { - "Findings": [ - { - 'AwsAccountId':'123456', - 'CompanyName':'AWS', - 'CreatedAt': new Date(), - 'Description': 'Real-time monitoring of API calls can be achieved by directing CloudTrail Logs to CloudWatch Logs and establishing corresponding metric filters and alarms. Routing tables are used to route network traffic between subnets and to network gateways. It is recommended that a metric filter and alarm be established for changes to route tables.', - }, - { - 'AwsAccountId':'123456', - 'CompanyName':'AWS', - 'CreatedAt': failDate, - 'Description': 'Real-time monitoring of API calls can be achieved by directing CloudTrail Logs to CloudWatch Logs and establishing corresponding metric filters and alarms. Routing tables are used to route network traffic between subnets and to network gateways. It is recommended that a metric filter and alarm be established for changes to route tables.', - }, - { - 'AwsAccountId':'123456', - 'CompanyName':'AWS', - 'CreatedAt': new Date(), - 'Description': 'Real-time monitoring of API calls can be achieved by directing CloudTrail Logs to CloudWatch Logs and establishing corresponding metric filters and alarms. Routing tables are used to route network traffic between subnets and to network gateways. It is recommended that a metric filter and alarm be established for changes to route tables.', - } - ] + 'AwsAccountId':'123456', + 'CompanyName':'AWS', + 'CreatedAt': new Date(), + 'Description': 'Real-time monitoring of API calls can be achieved by directing CloudTrail Logs to CloudWatch Logs and establishing corresponding metric filters and alarms. Routing tables are used to route network traffic between subnets and to network gateways. It is recommended that a metric filter and alarm be established for changes to route tables.', }, { - "Findings": [ - { - 'AwsAccountId':'123456', - 'CompanyName':'AWS', - 'CreatedAt': new Date(), - 'Description': 'Real-time monitoring of API calls can be achieved by directing CloudTrail Logs to CloudWatch Logs and establishing corresponding metric filters and alarms. Routing tables are used to route network traffic between subnets and to network gateways. It is recommended that a metric filter and alarm be established for changes to route tables.', - }, - { - 'AwsAccountId':'123456', - 'CompanyName':'AWS', - 'CreatedAt': new Date(), - 'Description': 'Real-time monitoring of API calls can be achieved by directing CloudTrail Logs to CloudWatch Logs and establishing corresponding metric filters and alarms. Routing tables are used to route network traffic between subnets and to network gateways. It is recommended that a metric filter and alarm be established for changes to route tables.', - }, - { - 'AwsAccountId':'123456', - 'CompanyName':'AWS', - 'CreatedAt': new Date(), - 'Description': 'Real-time monitoring of API calls can be achieved by directing CloudTrail Logs to CloudWatch Logs and establishing corresponding metric filters and alarms. Routing tables are used to route network traffic between subnets and to network gateways. It is recommended that a metric filter and alarm be established for changes to route tables.', - } - ] + 'AwsAccountId':'123456', + 'CompanyName':'AWS', + 'CreatedAt': failDate, + 'Description': 'Real-time monitoring of API calls can be achieved by directing CloudTrail Logs to CloudWatch Logs and establishing corresponding metric filters and alarms. Routing tables are used to route network traffic between subnets and to network gateways. It is recommended that a metric filter and alarm be established for changes to route tables.', }, { - "Findings": [] + 'AwsAccountId':'123456', + 'CompanyName':'AWS', + 'CreatedAt': new Date(), + 'Description': 'Real-time monitoring of API calls can be achieved by directing CloudTrail Logs to CloudWatch Logs and establishing corresponding metric filters and alarms. Routing tables are used to route network traffic between subnets and to network gateways. It is recommended that a metric filter and alarm be established for changes to route tables.', } ] @@ -109,7 +80,7 @@ describe('securityHubActiveFindings', function () { }); it('should PASS if Security Hub has no active findings', function (done) { - const cache = createCache(describeHub, null, getFindings[2]); + const cache = createCache(describeHub, null, []); securityHubActiveFindings.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(0); @@ -120,7 +91,7 @@ describe('securityHubActiveFindings', function () { }); it('should PASS if Security Hub has zero active findings', function (done) { - const cache = createCache(describeHub, null, getFindings[1]); + const cache = createCache(describeHub, null, [getFindings[0]]); securityHubActiveFindings.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(0); @@ -131,7 +102,7 @@ describe('securityHubActiveFindings', function () { }); it('should FAIL if Security Hub has active findings', function (done) { - const cache = createCache(describeHub, null, getFindings[0]); + const cache = createCache(describeHub, null, [getFindings[1]]); securityHubActiveFindings.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(2); diff --git a/plugins/azure/appservice/authEnabled.js b/plugins/azure/appservice/authEnabled.js index e022e5713d..a50f69d139 100644 --- a/plugins/azure/appservice/authEnabled.js +++ b/plugins/azure/appservice/authEnabled.js @@ -22,12 +22,24 @@ module.exports = { 'for auditing and security controls.', pci: 'Access to system components must be restricted to known users.' }, + settings: { + whitelist_functions_for_auth_enabled: { + name: 'Whitelist Functions For Authentication Enabled', + description: 'List of comma separated functions which should be whitelisted to check', + regex: '^.*$', + default: 'aqua-agentless-scanner-continuous-onboarding', + } + }, run: function(cache, settings, callback) { const results = []; const source = {}; const locations = helpers.locations(settings.govcloud); + let config = { + whitelist_functions_for_auth_enabled: settings.whitelist_functions_for_auth_enabled || this.settings.whitelist_functions_for_auth_enabled.default + }; + async.each(locations.webApps, function(location, rcb) { const webApps = helpers.addSource( @@ -51,19 +63,24 @@ module.exports = { webApps.data.forEach(function(webApp) { if (webApp.kind && webApp.kind.includes('workflowapp')) return; - const authSettings = helpers.addSource( - cache, source, ['webApps', 'getAuthSettings', location, webApp.id] - ); - - if (!authSettings || authSettings.err || !authSettings.data) { - helpers.addResult(results, 3, - 'Unable to query App Service: ' + helpers.addError(authSettings), - location, webApp.id); + if (webApp.name.includes(config.whitelist_functions_for_auth_enabled)) { + helpers.addResult(results, 0, 'The App Service is whitelisted', location, webApp.id); } else { - if (authSettings.data.enabled) { - helpers.addResult(results, 0, 'App Service has App Service Authentication enabled', location, webApp.id); + + const authSettings = helpers.addSource( + cache, source, ['webApps', 'getAuthSettings', location, webApp.id] + ); + + if (!authSettings || authSettings.err || !authSettings.data) { + helpers.addResult(results, 3, + 'Unable to query App Service: ' + helpers.addError(authSettings), + location, webApp.id); } else { - helpers.addResult(results, 2, 'App Service does not have App Service Authentication enabled', location, webApp.id); + if (authSettings.data.enabled) { + helpers.addResult(results, 0, 'App Service has App Service Authentication enabled', location, webApp.id); + } else { + helpers.addResult(results, 2, 'App Service does not have App Service Authentication enabled', location, webApp.id); + } } } }); diff --git a/plugins/azure/appservice/authEnabled.spec.js b/plugins/azure/appservice/authEnabled.spec.js index f2b88ffc38..eeafd97039 100644 --- a/plugins/azure/appservice/authEnabled.spec.js +++ b/plugins/azure/appservice/authEnabled.spec.js @@ -71,6 +71,41 @@ describe('authEnabled', function() { auth.run(cache, {}, callback); }); + it('should give passing result if App Service is whitelisted', 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('App Service is whitelisted') + expect(results[0].region).to.equal('eastus') + done() + }; + + const cache = createCache( + null, + [ + { + "id": "/subscriptions/abcdef-ebf6-437f-a3b0-28fc0d22117e/resourceGroups/devresourcegroup/providers/Microsoft.Web/sites/aqua-agentless-scanner-continuous-onboarding-i2hxc3is", + "name": "aqua-agentless-scanner-continuous-onboarding-i2hxc3is", + "type": "Microsoft.Web/sites", + "kind": "app,linux,container", + "location": "East US", + "state": "Running" + } + ], + { + "/subscriptions/abcdef-ebf6-437f-a3b0-28fc0d22117e/resourceGroups/devresourcegroup/providers/Microsoft.Web/sites/test-webapp": { + "data": { + "name": "authsettings", + "type": "Microsoft.Web/sites/config", + "enabled": false + } + } + } + ); + + auth.run(cache, {whitelist_functions_for_auth_enabled: 'aqua-agentless-scanner-continuous-onboarding'}, callback); + }); + it('should give passing result if enabled App Service', function(done) { const callback = (err, results) => { expect(results.length).to.equal(1) @@ -138,4 +173,4 @@ describe('authEnabled', function() { auth.run(cache, {}, callback); }); }) -}) \ No newline at end of file +}) diff --git a/plugins/azure/appservice/httpsOnlyEnabled.js b/plugins/azure/appservice/httpsOnlyEnabled.js index 2f15c12d17..eb9400604f 100644 --- a/plugins/azure/appservice/httpsOnlyEnabled.js +++ b/plugins/azure/appservice/httpsOnlyEnabled.js @@ -26,12 +26,24 @@ module.exports = { 'App Service HTTPS redirection should be used to ensure site visitors ' + 'are always connecting over a secure channel.' }, + settings: { + whitelist_functions_for_https_only: { + name: 'Whitelist Functions For HTTPS Only', + description: 'List of comma separated functions which should be whitelisted to check', + regex: '^.*$', + default: 'aqua-agentless-scanner-continuous-onboarding', + } + }, run: function(cache, settings, callback) { const results = []; const source = {}; const locations = helpers.locations(settings.govcloud); + let config = { + whitelist_functions_for_https_only: settings.whitelist_functions_for_https_only || this.settings.whitelist_functions_for_https_only.default + }; + async.each(locations.webApps, function(location, rcb) { const webApps = helpers.addSource( @@ -52,10 +64,16 @@ module.exports = { } webApps.data.forEach(function(webApp) { - if (webApp.httpsOnly) { - helpers.addResult(results, 0, 'The App Service has HTTPS Only enabled', location, webApp.id); + + if (webApp.name.includes(config.whitelist_functions_for_https_only)) { + helpers.addResult(results, 0, 'The App Service is whitelisted', location, webApp.id); } else { - helpers.addResult(results, 2, 'The App Service does not have HTTPS Only enabled', location, webApp.id); + + if (webApp.httpsOnly) { + helpers.addResult(results, 0, 'The App Service has HTTPS Only enabled', location, webApp.id); + } else { + helpers.addResult(results, 2, 'The App Service does not have HTTPS Only enabled', location, webApp.id); + } } }); diff --git a/plugins/azure/appservice/httpsOnlyEnabled.spec.js b/plugins/azure/appservice/httpsOnlyEnabled.spec.js index 79feef203f..3aed7bb557 100644 --- a/plugins/azure/appservice/httpsOnlyEnabled.spec.js +++ b/plugins/azure/appservice/httpsOnlyEnabled.spec.js @@ -11,6 +11,11 @@ const webApps = [ 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Web/sites/app1', 'name': 'app1', 'httpsOnly': false + }, + { + 'id': '/subscriptions/123/resourceGroups/aqua-resource-group/providers/Microsoft.Web/sites/aqua-agentless-scanner-continuous-onboarding-i2hxc3is', + 'name': 'aqua-agentless-scanner-continuous-onboarding-i2hxc3is', + 'httpsOnly': false } ]; @@ -81,5 +86,16 @@ describe('httpsOnlyEnabled', function() { done(); }); }); + + it('should PASS if app service gets whitelisted', function (done) { + const cache = createCache([webApps[2]]); + httpsOnlyEnabled.run(cache, { whitelist_functions_for_https_only:'aqua-agentless-scanner-continuous-onboarding' }, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('The App Service is whitelisted'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); }); -}); \ No newline at end of file +}); diff --git a/plugins/azure/storageaccounts/blobServiceLoggingEnabled.js b/plugins/azure/storageaccounts/blobServiceLoggingEnabled.js index a1f6853f72..fc9278de8c 100644 --- a/plugins/azure/storageaccounts/blobServiceLoggingEnabled.js +++ b/plugins/azure/storageaccounts/blobServiceLoggingEnabled.js @@ -43,6 +43,9 @@ module.exports = { storageAccount.sku.tier && storageAccount.sku.tier.toLowerCase() == 'premium') { helpers.addResult(results, 0, 'Storage Account tier is premium', location, storageAccount.id); + } else if (storageAccount.kind && + storageAccount.kind.toLowerCase() != 'storagev2') { + helpers.addResult(results, 0, 'Storage Account kind is not StorageV2', location, storageAccount.id); } else { const diagnosticSettings = helpers.addSource(cache, source, diff --git a/plugins/azure/storageaccounts/blobServiceLoggingEnabled.spec.js b/plugins/azure/storageaccounts/blobServiceLoggingEnabled.spec.js index 4f4ae422c4..812ea23c0e 100644 --- a/plugins/azure/storageaccounts/blobServiceLoggingEnabled.spec.js +++ b/plugins/azure/storageaccounts/blobServiceLoggingEnabled.spec.js @@ -18,6 +18,16 @@ const storageAccounts = [ sku: { tier: 'Premium' } + }, + { + kind: 'BlobStorage', + id: '/subscriptions/1234/resourceGroups/cloud-shell-storage-eastus/providers/Microsoft.Storage/storageAccounts/csb100320011e293683', + name: 'csb100320011e293683', + type: 'Microsoft.Storage/storageAccounts', + location: 'eastus', + sku: { + tier: 'Standard' + } } ]; @@ -193,6 +203,18 @@ describe('blobServiceLoggingEnabled', function () { }); }); + it('should PASS if storage account kind in not StorageV2', function (done) { + const cache = createCache([storageAccounts[2]], []); + blobServiceLoggingEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].region).to.equal('eastus'); + expect(results[0].message).to.equal('Storage Account kind is not StorageV2'); + + done(); + }); + }); + it('should UNKNOWN if Unable to query for for storage accounts', function (done) { const cache = createErrorCache('diagnostic'); blobServiceLoggingEnabled.run(cache, {}, (err, results) => { diff --git a/plugins/azure/storageaccounts/queueServiceLoggingEnabled.js b/plugins/azure/storageaccounts/queueServiceLoggingEnabled.js index a5eb3e0ca4..720a20f9b7 100644 --- a/plugins/azure/storageaccounts/queueServiceLoggingEnabled.js +++ b/plugins/azure/storageaccounts/queueServiceLoggingEnabled.js @@ -43,6 +43,9 @@ module.exports = { storageAccount.sku.tier && storageAccount.sku.tier.toLowerCase() == 'premium') { helpers.addResult(results, 0, 'Storage Account tier is premium', location, storageAccount.id); + } else if (storageAccount.kind && + storageAccount.kind.toLowerCase() != 'storagev2') { + helpers.addResult(results, 0, 'Storage Account kind is not StorageV2', location, storageAccount.id); } else { const diagnosticSettings = helpers.addSource(cache, source, @@ -75,4 +78,4 @@ module.exports = { callback(null, results, source); }); } -}; \ No newline at end of file +}; diff --git a/plugins/azure/storageaccounts/queueServiceLoggingEnabled.spec.js b/plugins/azure/storageaccounts/queueServiceLoggingEnabled.spec.js index 12c0b26261..d83e2cd773 100644 --- a/plugins/azure/storageaccounts/queueServiceLoggingEnabled.spec.js +++ b/plugins/azure/storageaccounts/queueServiceLoggingEnabled.spec.js @@ -28,6 +28,16 @@ const storageAccounts = [ sku: { tier: 'Premium' } + }, + { + kind: 'BlobStorage', + id: '/subscriptions/1234/resourceGroups/cloud-shell-storage-eastus/providers/Microsoft.Storage/storageAccounts/csb100320011e293683', + name: 'csb100320011e293683', + type: 'Microsoft.Storage/storageAccounts', + location: 'eastus', + sku: { + tier: 'Standard' + } } ]; @@ -205,6 +215,18 @@ describe('queueServiceLoggingEnabled', function () { }); }); + it('should PASS if storage account kind in not StorageV2', function (done) { + const cache = createCache([storageAccounts[2]], []); + queueServiceLoggingEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].region).to.equal('eastus'); + expect(results[0].message).to.equal('Storage Account kind is not StorageV2'); + + done(); + }); + }); + it('should UNKNOWN if Unable to query for for storage accounts', function (done) { const cache = createErrorCache('storageAccounts'); queueServiceLoggingEnabled.run(cache, {}, (err, results) => { @@ -227,4 +249,3 @@ describe('queueServiceLoggingEnabled', function () { }); }); }); - diff --git a/plugins/azure/storageaccounts/storageAccountsHttps.js b/plugins/azure/storageaccounts/storageAccountsHttps.js index 9edaaff5dc..2e64c6db93 100644 --- a/plugins/azure/storageaccounts/storageAccountsHttps.js +++ b/plugins/azure/storageaccounts/storageAccountsHttps.js @@ -80,7 +80,6 @@ module.exports = { 'properties': { 'supportsHttpsTrafficOnly': true } - }; // logging diff --git a/plugins/azure/storageaccounts/tableServiceLoggingEnabled.js b/plugins/azure/storageaccounts/tableServiceLoggingEnabled.js index 074000d1a5..df0e56456d 100644 --- a/plugins/azure/storageaccounts/tableServiceLoggingEnabled.js +++ b/plugins/azure/storageaccounts/tableServiceLoggingEnabled.js @@ -42,6 +42,9 @@ module.exports = { storageAccount.sku.tier && storageAccount.sku.tier.toLowerCase() == 'premium') { helpers.addResult(results, 0, 'Storage Account tier is premium', location, storageAccount.id); + } else if (storageAccount.kind && + storageAccount.kind.toLowerCase() != 'storagev2') { + helpers.addResult(results, 0, 'Storage Account kind is not StorageV2', location, storageAccount.id); } else { const diagnosticSettings = helpers.addSource(cache, source, diff --git a/plugins/azure/storageaccounts/tableServiceLoggingEnabled.spec.js b/plugins/azure/storageaccounts/tableServiceLoggingEnabled.spec.js index c6da89bd60..a73c8c9127 100644 --- a/plugins/azure/storageaccounts/tableServiceLoggingEnabled.spec.js +++ b/plugins/azure/storageaccounts/tableServiceLoggingEnabled.spec.js @@ -28,6 +28,16 @@ const storageAccounts = [ sku: { tier: 'Premium' } + }, + { + kind: 'BlobStorage', + id: '/subscriptions/1234/resourceGroups/cloud-shell-storage-eastus/providers/Microsoft.Storage/storageAccounts/csb100320011e293683', + name: 'csb100320011e293683', + type: 'Microsoft.Storage/storageAccounts', + location: 'eastus', + sku: { + tier: 'Standard' + } } ]; @@ -205,6 +215,18 @@ describe('tableServiceLoggingEnabled', function () { }); }); + it('should PASS if storage account kind in not StorageV2', function (done) { + const cache = createCache([storageAccounts[2]], []); + tableServiceLoggingEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].region).to.equal('eastus'); + expect(results[0].message).to.equal('Storage Account kind is not StorageV2'); + + done(); + }); + }); + it('should UNKNOWN if Unable to query for for storage accounts', function (done) { const cache = createErrorCache('storageAccounts'); tableServiceLoggingEnabled.run(cache, {}, (err, results) => { @@ -227,4 +249,3 @@ describe('tableServiceLoggingEnabled', function () { }); }); }); - diff --git a/plugins/azure/virtualmachines/vmNetworkExposure.js b/plugins/azure/virtualmachines/vmNetworkExposure.js new file mode 100644 index 0000000000..4258c9954f --- /dev/null +++ b/plugins/azure/virtualmachines/vmNetworkExposure.js @@ -0,0 +1,99 @@ +var async = require('async'); +var helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'Network Exposure', + category: 'Virtual Machines', + domain: 'Compute', + severity: 'Info', + description: 'Check if Azure virtual machines are exposed to the internet.', + more_info: 'Virtual machines exposed to the internet are at a higher risk of unauthorized access, data breaches, and cyberattacks. It’s crucial to limit exposure by securing access through proper configuration of security group and firewall rules.', + link: 'https://learn.microsoft.com/en-us/azure/security/fundamentals/virtual-machines-overview', + recommended_action: 'Secure VM instances by restricting access with properly configured security group and firewall rules.', + apis: ['virtualMachines:listAll', 'networkInterfaces:listAll', 'networkSecurityGroups:listAll', 'virtualNetworks:listAll'], + realtime_triggers: ['microsoftcompute:virtualmachines:write', 'microsoftnetwork:networkinterfaces:write', 'microsoftcompute:virtualmachines:delete', 'microsoftnetwork:networkinterfaces:delete', 'microsoftnetwork:networksecuritygroups:write','microsoftnetwork:networksecuritygroups:delete', 'microsoftnetwork:virtualnetworks:write','microsoftnetwork:virtualnetworks:delete'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var locations = helpers.locations(settings.govcloud); + + async.each(locations.virtualMachines, function(location, rcb) { + var virtualMachines = helpers.addSource(cache, source, + ['virtualMachines', 'listAll', location]); + + if (!virtualMachines) return rcb(); + + if (virtualMachines.err || !virtualMachines.data) { + helpers.addResult(results, 3, 'Unable to query for virtualMachines: ' + helpers.addError(virtualMachines), location); + return rcb(); + } + + if (!virtualMachines.data.length) { + helpers.addResult(results, 0, 'No existing Virtual Machines found', location); + return rcb(); + } + + var networkInterfaces = helpers.addSource(cache, source, + ['networkInterfaces', 'listAll', location]); + + if (!networkInterfaces || networkInterfaces.err || !networkInterfaces.data || !networkInterfaces.data.length) { + helpers.addResult(results, 3, 'Unable to query for network interfaces: ' + helpers.addError(networkInterfaces), location); + return rcb(); + } + + let networkSecurityGroups = helpers.addSource(cache, source, + ['networkSecurityGroups', 'listAll', location]); + + + if (!networkSecurityGroups || networkSecurityGroups.err || !networkSecurityGroups.data) { + helpers.addResult(results, 3, 'Unable to query for Network Security Groups: ' + helpers.addError(networkSecurityGroups), location); + return rcb(); + } + + var virtualNetworks = helpers.addSource(cache, source, + ['virtualNetworks', 'listAll', location]); + + + virtualMachines.data.forEach(virtualMachine => { + let vm_interfaces = []; + let securityGroups = []; + if (virtualMachine.networkProfile && virtualMachine.networkProfile.networkInterfaces && + virtualMachine.networkProfile.networkInterfaces.length > 0) { + let interfaceIDs = virtualMachine.networkProfile.networkInterfaces.map(nic => nic.id); + vm_interfaces = networkInterfaces.data.filter(nic => interfaceIDs.includes(nic.id)); + if (networkSecurityGroups && networkSecurityGroups.data && networkSecurityGroups.data.length) { + let securityGroupIDs = vm_interfaces.filter(interface => interface.networkSecurityGroup && interface.networkSecurityGroup.id).map(nic => nic.networkSecurityGroup.id); + let allSubnetIDs = vm_interfaces.reduce((acc, nic) => { + let subnetIds = nic.ipConfigurations.map(ipConfig => ipConfig.properties.subnet.id); + return acc.concat(subnetIds); + }, []); + + if (virtualNetworks && !virtualNetworks.err && virtualNetworks.data && virtualNetworks.data.length) { + virtualNetworks.data.forEach(vnet => { + if (vnet.subnets && vnet.subnets.length) { + vnet.subnets.forEach(subnet => { + if (allSubnetIDs.includes(subnet.id) && subnet.properties && subnet.properties.networkSecurityGroup && subnet.properties.networkSecurityGroup.id) { + securityGroupIDs.push(subnet.properties.networkSecurityGroup.id); + } + }); + } + }); + + } + securityGroups = networkSecurityGroups.data.filter(nsg => securityGroupIDs.includes(nsg.id)); + } + } + let internetExposed = helpers.checkNetworkExposure(cache, source, vm_interfaces, securityGroups, location, results); + if (internetExposed && internetExposed.length) { + helpers.addResult(results, 2, `VM is exposed to the internet through ${internetExposed}`, location, virtualMachine.id); + } else { + helpers.addResult(results, 0, 'VM is not exposed to the internet', location, virtualMachine.id); + } + }); + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; diff --git a/plugins/google/compute/instanceNetworkExposure.js b/plugins/google/compute/instanceNetworkExposure.js new file mode 100644 index 0000000000..2b896162b2 --- /dev/null +++ b/plugins/google/compute/instanceNetworkExposure.js @@ -0,0 +1,108 @@ +var async = require('async'); +var helpers = require('../../../helpers/google'); + +module.exports = { + title: 'Network Exposure', + category: 'Compute', + domain: 'Compute', + severity: 'Info', + description: 'Check if GCP virtual machines are exposed to the internet.', + more_info: 'Virtual machines exposed to the internet are at a higher risk of unauthorized access, data breaches, and cyberattacks. It’s crucial to limit exposure by securing access through proper configuration of network and firewall rules.', + link: 'https://cloud.google.com/firewall/docs/firewalls', + recommended_action: 'Secure VM instances by restricting access with properly configured security group and firewall rules.', + apis: ['compute:list', 'firewalls:list'], + realtime_triggers: ['compute.instances.insert', 'compute.instances.delete','compute.firewalls.insert', 'compute.firewalls.delete', 'compute.firewalls.patch'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var regions = helpers.regions(); + + let projects = helpers.addSource(cache, source, + ['projects','get', 'global']); + + if (!projects || projects.err || !projects.data || !projects.data.length) { + helpers.addResult(results, 3, + 'Unable to query for projects: ' + helpers.addError(projects), 'global', null, null, (projects) ? projects.err : null); + return callback(null, results, source); + } + + var project = projects.data[0].name; + + + async.each(regions.compute, (region, rcb) => { + var zones = regions.zones; + var noInstances = []; + + let firewalls = helpers.addSource( + cache, source, ['firewalls', 'list', 'global']); + + if (!firewalls) return rcb(); + + if (!firewalls || firewalls.err || !firewalls.data) { + helpers.addResult(results, 3, 'Unable to query firewall rules', region, null, null, firewalls.err); + return rcb(); + } + + if (!firewalls.data.length) { + helpers.addResult(results, 0, 'No firewall rules found', region); + return rcb(); + } + + async.each(zones[region], function(zone, zcb) { + var instances = helpers.addSource(cache, source, + ['compute','list', zone]); + + if (!instances) return zcb(); + + if (instances.err || !instances.data) { + helpers.addResult(results, 3, 'Unable to query compute instances', region, null, null, instances.err); + return zcb(); + } + + if (!instances.data.length) { + noInstances.push(zone); + return zcb(); + } + + instances.data.forEach(instance => { + let networks = instance.networkInterfaces.map(nic => nic.network); + let tags = instance.tags && instance.tags.items ? instance.tags.items : []; + let serviceAccount = instance.serviceAccounts && instance.serviceAccounts[0] && instance.serviceAccounts[0].email ? instance.serviceAccounts[0].email : ''; + + let firewallRules = firewalls.data.filter(rule => { + let isNetworkMatch = networks.some(network => rule.network.endsWith(network)); + + let isTagMatch = rule.targetTags ? rule.targetTags.some(tag => tags.includes(tag)) : true; + + let isServiceAccountMatch = rule.targetServiceAccounts ? + rule.targetServiceAccounts.includes(serviceAccount) : true; + + return isNetworkMatch && isTagMatch && isServiceAccountMatch; + }); + + + networks = networks.map(network => network.split('/').pop()); + let internetExposed = helpers.checkNetworkExposure(cache, source, networks, firewallRules, region, results); + + let resource = helpers.createResourceName('instances', instance.name, project, 'zone', zone); + + if (internetExposed && internetExposed.length) { + helpers.addResult(results, 2, `VM is exposed to the internet through ${internetExposed}`, region, resource); + } else { + helpers.addResult(results, 0, 'VM is not exposed to the internet', region, resource); + } + + }); + zcb(); + }, function() { + if (noInstances.length) { + helpers.addResult(results, 0, `No instances found in following zones: ${noInstances.join(', ')}`, region); + } + rcb(); + }); + }, function() { + callback(null, results, source); + }); + } +};