diff --git a/collectors/aws/accessanalyzer/listFindingsV2.js b/collectors/aws/accessanalyzer/listFindingsV2.js new file mode 100644 index 0000000000..acf74ddd83 --- /dev/null +++ b/collectors/aws/accessanalyzer/listFindingsV2.js @@ -0,0 +1,49 @@ +var AWS = require('aws-sdk'); +var async = require('async'); +var helpers = require(__dirname + '/../../../helpers/aws'); + +module.exports = function(AWSConfig, collection, retries, callback) { + var accessanalyzer = new AWS.AccessAnalyzer(AWSConfig); + async.eachLimit(collection.accessanalyzer.listAnalyzers[AWSConfig.region].data, 15, function(analyzer, cb) { + collection.accessanalyzer.listFindingsV2[AWSConfig.region][analyzer.arn] = {}; + var params = { + analyzerArn: analyzer.arn + }; + + var paginating = false; + var paginateCb = function(err, data) { + if (err) collection.accessanalyzer.listFindingsV2[AWSConfig.region][analyzer.arn].err = err; + + if (!data) return cb(); + + if (paginating && data.findings && data.findings.length && + collection.accessanalyzer.listFindingsV2[AWSConfig.region][analyzer.arn].data.findings && + collection.accessanalyzer.listFindingsV2[AWSConfig.region][analyzer.arn].data.findings.length) { + collection.accessanalyzer.listFindingsV2[AWSConfig.region][analyzer.arn].data.findings = collection.accessanalyzer.listFindings[AWSConfig.region][analyzer.arn].data.findings.concat(data.findings); + } else { + collection.accessanalyzer.listFindingsV2[AWSConfig.region][analyzer.arn].data = data; + } + + if (data.nextToken && data.nextToken.length) { + paginating = true; + return execute(data.nextToken); + } + + cb(); + }; + + function execute(nextToken) { // eslint-disable-line no-inner-declarations + var localParams = JSON.parse(JSON.stringify(params || {})); + if (nextToken) localParams['nextToken'] = nextToken; + if (nextToken) { + helpers.makeCustomCollectorCall(accessanalyzer, 'listFindingsV2', localParams, retries, null, null, null, paginateCb); + } else { + helpers.makeCustomCollectorCall(accessanalyzer, 'listFindingsV2', params, retries, null, null, null, paginateCb); + } + } + + execute(); + }, function(){ + callback(); + }); +}; diff --git a/exports.js b/exports.js index 39820a8731..24876c757d 100644 --- a/exports.js +++ b/exports.js @@ -514,6 +514,7 @@ module.exports = { 'lambdaDeadLetterQueue' : require(__dirname + '/plugins/aws/lambda/lambdaDeadLetterQueue.js'), 'lambdaEnhancedMonitoring' : require(__dirname + '/plugins/aws/lambda/lambdaEnhancedMonitoring.js'), 'lambdaUniqueExecutionRole' : require(__dirname + '/plugins/aws/lambda/lambdaUniqueExecutionRole.js'), + 'lambdaNetworkExposure' : require(__dirname + '/plugins/aws/lambda/lambdaNetworkExposure.js'), 'webServerPublicAccess' : require(__dirname + '/plugins/aws/mwaa/webServerPublicAccess.js'), 'environmentAdminPrivileges' : require(__dirname + '/plugins/aws/mwaa/environmentAdminPrivileges.js'), @@ -1000,6 +1001,7 @@ module.exports = { 'disableFTPDeployments' : require(__dirname + '/plugins/azure/appservice/disableFTPDeployments.js'), 'accessControlAllowCredential' : require(__dirname + '/plugins/azure/appservice/accessControlAllowCredential.js'), 'appServiceDiagnosticLogs' : require(__dirname + '/plugins/azure/appservice/appServiceDiagnosticLogs.js'), + 'functionAppNetworkExposure' : require(__dirname + '/plugins/azure/appservice/functionAppNetworkExposure.js'), 'rbacEnabled' : require(__dirname + '/plugins/azure/kubernetesservice/rbacEnabled.js'), 'aksManagedIdentity' : require(__dirname + '/plugins/azure/kubernetesservice/aksManagedIdentity.js'), @@ -1605,8 +1607,8 @@ module.exports = { 'cloudFunctionLabelsAdded' : require(__dirname + '/plugins/google/cloudfunctions/cloudFunctionLabelsAdded.js'), 'cloudFunctionOldRuntime' : require(__dirname + '/plugins/google/cloudfunctions/cloudFunctionOldRuntime.js'), 'functionAllUsersPolicy' : require(__dirname + '/plugins/google/cloudfunctions/functionAllUsersPolicy.js'), - 'serverlessVPCAccess' : require(__dirname + '/plugins/google/cloudfunctions/serverlessVPCAccess.js'), + 'cloudFunctionNetworkExposure' : require(__dirname + '/plugins/google/cloudfunctions/cloudFunctionNetworkExposure.js'), 'computeAllowedExternalIPs' : require(__dirname + '/plugins/google/cloudresourcemanager/computeAllowedExternalIPs.js'), 'disableAutomaticIAMGrants' : require(__dirname + '/plugins/google/cloudresourcemanager/disableAutomaticIAMGrants.js'), diff --git a/helpers/asl/asl-1.js b/helpers/asl/asl-1.js index ac8420dca8..928ce2785d 100644 --- a/helpers/asl/asl-1.js +++ b/helpers/asl/asl-1.js @@ -24,7 +24,7 @@ var parse = function(obj, path, region, cloud, accountId, resourceId) { return parse(obj[localPath], path); } else return ['not set']; } else if (!Array.isArray(obj) && path && path.length) { - if (obj[path]) return [obj[path]]; + if (obj[path] || typeof obj[path] === 'boolean') return [obj[path]]; else { if (cloud==='aws' && path.startsWith('arn:aws')) { const template_string = path; diff --git a/helpers/aws/api.js b/helpers/aws/api.js index da1bb49d2a..d6ecd57c61 100644 --- a/helpers/aws/api.js +++ b/helpers/aws/api.js @@ -1833,8 +1833,14 @@ var postcalls = [ reliesOnCall: 'listAnalyzers', override: true }, + listFindingsV2: { + reliesOnService: 'accessanalyzer', + reliesOnCall: 'listAnalyzers', + override: true + }, sendIntegration: serviceMap['IAM'][0] }, + APIGateway: { getStages: { reliesOnService: 'apigateway', diff --git a/helpers/aws/functions.js b/helpers/aws/functions.js index cbbdce4f0c..1f63906077 100644 --- a/helpers/aws/functions.js +++ b/helpers/aws/functions.js @@ -1239,7 +1239,6 @@ var getAttachedELBs = function(cache, source, region, resourceId, lbField, lbAt return elbs; }; - var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs, region, results, resource) { var internetExposed = ''; var isSubnetPrivate = false; @@ -1249,41 +1248,41 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs if (resource.functionUrlConfig && resource.functionUrlConfig.data) { if (resource.functionUrlConfig.data.AuthType === 'NONE') { internetExposed += 'public function URL'; - } else if (resource.functionUrlConfig.data.AuthType === 'AWS_IAM' && - resource.functionPolicy && resource.functionPolicy.data) { + } else if (resource.functionUrlConfig.data.AuthType === 'AWS_IAM' && + resource.functionPolicy && resource.functionPolicy.data) { let authConfig = resource.functionPolicy.data; if (authConfig.Policy) { let statements = normalizePolicyDocument(authConfig.Policy); - + if (statements) { let hasDenyAll = false; let hasPublicAllow = false; let hasRestrictiveConditions = false; - + for (let statement of statements) { // Check for explicit deny statements first if (statement.Effect === 'Deny') { // Check if there's a deny for all principals - if ((!statement.Condition || Object.keys(statement.Condition).length === 0) && + if ((!statement.Condition || Object.keys(statement.Condition).length === 0) && globalPrincipal(statement.Principal)) { hasDenyAll = true; break; } - + // Check for deny with IP restrictions - if (statement.Condition && - (statement.Condition['NotIpAddress'] || - statement.Condition['IpAddress'])) { + if (statement.Condition && + (statement.Condition['NotIpAddress'] || + statement.Condition['IpAddress'])) { hasRestrictiveConditions = true; } } else if (statement.Effect === 'Allow') { // Skip if the statement doesn't include relevant Lambda actions - if (!statement.Action || - (!Array.isArray(statement.Action) ? + if (!statement.Action || + (!Array.isArray(statement.Action) ? !statement.Action.includes('lambda:InvokeFunctionUrl') : - !statement.Action.some(action => - action === '*' || - action === 'lambda:*' || + !statement.Action.some(action => + action === '*' || + action === 'lambda:*' || action === 'lambda:InvokeFunctionUrl' ))) { continue; @@ -1303,17 +1302,17 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs 'aws:PrincipalArn', 'aws:SourceAccount' ]; - - const hasRestriction = restrictiveConditions.some(condition => - Object.keys(statement.Condition).some(key => + + const hasRestriction = restrictiveConditions.some(condition => + Object.keys(statement.Condition).some(key => key.toLowerCase().includes(condition.toLowerCase()) ) ); - + if (hasRestriction) { hasRestrictiveConditions = true; } else if (statement.Condition['StringEquals'] && - statement.Condition['StringEquals']['lambda:FunctionUrlAuthType'] === 'NONE') { + statement.Condition['StringEquals']['lambda:FunctionUrlAuthType'] === 'NONE') { hasPublicAllow = true; } } @@ -1323,8 +1322,8 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs // Only mark as exposed if we have a public allow and no restrictions if (hasPublicAllow && !hasDenyAll && !hasRestrictiveConditions) { - internetExposed += internetExposed.length ? - ', function URL with global IAM access' : + internetExposed += internetExposed.length ? + ', function URL with global IAM access' : 'function URL with global IAM access'; } } @@ -1352,19 +1351,19 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs ['apigateway', 'getIntegration', region, api.id]); if (!getIntegration || getIntegration.err || !Object.keys(getIntegration).length) continue; - + for (let apiResource of Object.values(getIntegration)) { // Check if any integration points to this Lambda function let lambdaIntegrations = Object.values(apiResource).filter(integration => { - return integration && integration.data && (integration.data.type === 'AWS' || integration.data.type === 'AWS_PROXY') && - integration.data.uri && + return integration && integration.data && (integration.data.type === 'AWS' || integration.data.type === 'AWS_PROXY') && + integration.data.uri && integration.data.uri.includes(resource.functionArn); }); if (lambdaIntegrations.length) { internetExposed += internetExposed.length ? `, API Gateway ${api.name}` : `API Gateway ${api.name}`; } - } + } } } } @@ -1375,7 +1374,7 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs } if (!resource.functionArn) { - // Scenario 1: check if resource is in a private subnet + // Scenario 1: check if resource is in a private subnet let subnetRouteTableMap, privateSubnets; var describeSubnets = helpers.addSource(cache, source, ['ec2', 'describeSubnets', region]); @@ -1500,15 +1499,13 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs if (elbs && elbs.length) { if (!describeSecurityGroups || !describeSecurityGroups.data) { describeSecurityGroups = helpers.addSource(cache, source, - ['ec2', 'describeSecurityGroups', region]); + ['ec2', 'describeSecurityGroups', region]); } elbs.forEach(lb => { let isLBPublic = false; if (lb.Scheme && lb.Scheme.toLowerCase() === 'internet-facing') { if (lb.SecurityGroups && lb.SecurityGroups.length) { - var describeSecurityGroups = helpers.addSource(cache, source, - ['ec2', 'describeSecurityGroups', region]); if (describeSecurityGroups && !describeSecurityGroups.err && describeSecurityGroups.data && describeSecurityGroups.data.length) { let elbSGs = describeSecurityGroups.data.filter(sg => lb.SecurityGroups.includes(sg.GroupId)); @@ -1533,7 +1530,7 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs let getLambdaTargetELBs = function(cache, source, region) { let lambdaELBMap = {}; - + var describeLoadBalancersv2 = helpers.addSource(cache, source, ['elbv2', 'describeLoadBalancers', region]); @@ -1545,18 +1542,18 @@ let getLambdaTargetELBs = function(cache, source, region) { var describeTargetGroups = helpers.addSource(cache, source, ['elbv2', 'describeTargetGroups', region, lb.DNSName]); - if (!describeTargetGroups || describeTargetGroups.err || !describeTargetGroups.data || + if (!describeTargetGroups || describeTargetGroups.err || !describeTargetGroups.data || !describeTargetGroups.data.TargetGroups) return; describeTargetGroups.data.TargetGroups.forEach(tg => { var describeTargetHealth = helpers.addSource(cache, source, ['elbv2', 'describeTargetHealth', region, tg.TargetGroupArn]); - if (!describeTargetHealth || describeTargetHealth.err || !describeTargetHealth.data || + if (!describeTargetHealth || describeTargetHealth.err || !describeTargetHealth.data || !describeTargetHealth.data.TargetHealthDescriptions) return; describeTargetHealth.data.TargetHealthDescriptions.forEach(target => { - if (target.Target && target.Target.Id && + if (target.Target && target.Target.Id && target.Target.Id.startsWith('arn:aws:lambda')) { if (!lambdaELBMap[target.Target.Id]) { lambdaELBMap[target.Target.Id] = []; @@ -1567,13 +1564,13 @@ let getLambdaTargetELBs = function(cache, source, region) { targetGroupArn: tg.TargetGroupArn, targets: [target.Target] }); - + // Check if there's an active listener for this target group let hasListener = false; var describeListeners = helpers.addSource(cache, source, ['elbv2', 'describeListeners', region, lb.DNSName]); - - if (describeListeners && describeListeners.data && + + if (describeListeners && describeListeners.data && describeListeners.data.Listeners) { hasListener = describeListeners.data.Listeners.some(listener => listener.DefaultActions.some(action => @@ -1581,7 +1578,7 @@ let getLambdaTargetELBs = function(cache, source, region) { ) ); } - + if (hasListener) { lambdaELBMap[target.Target.Id].push(lb); } diff --git a/helpers/azure/functions.js b/helpers/azure/functions.js index 4906bc5143..5e6d6e75f0 100644 --- a/helpers/azure/functions.js +++ b/helpers/azure/functions.js @@ -775,21 +775,29 @@ function checkSecurityGroup(securityGroups) { return {exposed: true}; } -function checkNetworkExposure(cache, source, networkInterfaces, securityGroups, location, results, lbNames) { +function checkNetworkExposure(cache, source, networkInterfaces, securityGroups, location, results, attachedResources, resource) { 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) { - return `nsg ${exposedSG.nsg}` - } else { - return ''; + const isFunctionApp = resource && resource.kind && + resource.kind.toLowerCase().includes('functionapp'); + + if (!isFunctionApp) { + 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) { + return `nsg ${exposedSG.nsg}` + } else { + return ''; + } } } } + + const { applicationGateways, lbNames, frontDoors } = attachedResources; + if (lbNames && lbNames.length) { const loadBalancers = shared.addSource(cache, source, ['loadBalancers', 'listAll', location]); @@ -802,18 +810,51 @@ function checkNetworkExposure(cache, source, networkInterfaces, securityGroups, if (lb.frontendIPConfigurations && lb.frontendIPConfigurations.length) { isPublic = lb.frontendIPConfigurations.some(ipConfig => ipConfig.properties && ipConfig.properties.publicIPAddress && ipConfig.properties.publicIPAddress.id); - if (isPublic && ((lb.nboundNatRules && nboundNatRules.length) || (lb.loadBalancingRules && lb.loadBalancingRules.length))) { - exposedPath += `lb ${lb.name}`; - break; + if (isPublic && ((lb.inboundNatRules && inboundNatRules.length) || (lb.loadBalancingRules && lb.loadBalancingRules.length))) { + exposedPath += exposedPath.length ? `, lb ${lb.name}` : `lb ${lb.name}`; } } } } } } + + + if (applicationGateways && applicationGateways.length) { + for (const ag of applicationGateways) { + if (ag.frontendIPConfigurations && ag.frontendIPConfigurations.some(config => config.publicIPAddress && config.publicIPAddress.id)) { + exposedPath += exposedPath.length ? `, ag ${ag.name}` : `ag ${ag.name}`; + } + } + } + + if (frontDoors && frontDoors.length) { + for (const fd of frontDoors) { + if (!fd.associatedWafPolicies || !fd.associatedWafPolicies.length) { + exposedPath += exposedPath.length ? `, fd ${fd.name}` : `fd ${fd.name}`; + continue; + } + + // Check WAF policies + let hasSecureWaf = false; + for (const policy of fd.associatedWafPolicies) { + if (policy.policySettings && + policy.policySettings.enabledState === 'Enabled' && + policy.policySettings.mode === 'Prevention') { + hasSecureWaf = true; + break; + } + } + + if (!hasSecureWaf) { + exposedPath += exposedPath.length ? `, fd ${fd.name}` : `fd ${fd.name}`; + } + } + } + + return exposedPath; } - module.exports = { addResult: addResult, findOpenPorts: findOpenPorts, @@ -830,3 +871,4 @@ module.exports = { checkNetworkExposure: checkNetworkExposure }; + diff --git a/helpers/google/api.js b/helpers/google/api.js index 1d4f998702..2357b1aa65 100644 --- a/helpers/google/api.js +++ b/helpers/google/api.js @@ -224,6 +224,22 @@ var calls = { enabled: true } }, + apiGateways: { + list: { + url: 'https://apigateway.googleapis.com/v1/projects/{projectId}/locations/{locationId}/gateways', + location: 'region', + dataKey: 'gateways', + isDataArray: true + } + }, + api: { + list: { + url: 'https://apigateway.googleapis.com/v1/projects/{projectId}/locations/{locationId}/apis', + location: 'region', + dataKey: 'apis', + isDataArray: true + } + }, images: { list: { url: 'https://compute.googleapis.com/compute/v1/projects/{projectId}/global/images', @@ -620,7 +636,8 @@ var calls = { listDatasets: { url: 'https://{locationId}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{locationId}/datasets', location: 'region', - dataKey: 'datasets' + dataKey: 'datasets', + isDataArray: true }, listModels: { url: 'https://{locationId}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{locationId}/models', @@ -752,6 +769,24 @@ var postcalls = { } }, }, + apiConfigs: { + list: { + url: 'https://apigateway.googleapis.com/v1/{name}/configs', + location: 'region', + reliesOnService: ['api'], + reliesOnCall: ['list'], + properties: ['name'], + } + }, + apiGateways: { + getIamPolicy: { + url: 'https://apigateway.googleapis.com/v1/{name}:getIamPolicy', + location: 'region', + reliesOnService: ['apiGateways'], + reliesOnCall: ['list'], + properties: ['name'], + } + }, datasets: { get: { url: 'https://bigquery.googleapis.com/bigquery/v2/projects/{projectId}/datasets/{datasetId}', diff --git a/helpers/google/functions.js b/helpers/google/functions.js index 18fd8ac7dc..54413ff620 100644 --- a/helpers/google/functions.js +++ b/helpers/google/functions.js @@ -419,10 +419,10 @@ function checkFirewallRules(firewallRules) { sourceAddressPrefix.includes('/0') || sourceAddressPrefix.toLowerCase() === 'internet' || sourceAddressPrefix.includes('/0') - ): null; + ): false; - var allowed = firewallRule.allowed? firewallRule.allowed.some(allow => !!allow.IPProtocol): null; - var denied = firewallRule.denied? firewallRule.denied.some(deny => deny.IPProtocol === 'all'): null; + var allowed = firewallRule.allowed? firewallRule.allowed.some(allow => !!allow.IPProtocol): false; + var denied = firewallRule.denied? firewallRule.denied.some(deny => deny.IPProtocol === 'all'): false; if (allSources && allowed) { return {exposed: true, networkName: `vpc ${networkName}`}; } @@ -450,14 +450,22 @@ function getForwardingRules(cache, source, region, resource) { return []; } - backendServices = backendServices.filter(service => { - if (service.backends && service.backends.length) { - return service.backends.some(backend => { - let group = backend.group.replace(/^.*?(\/projects\/.*)$/, '$1'); - return resource.selfLink.includes(group); - }); - } - }); + if (resource.httpsTrigger && resource.httpsTrigger.url) { + backendServices = backendServices.filter(service => { + if (service.backends && service.backends.length) { + return service.backends.some(backend => backend.target && backend.target.includes(resource.httpsTrigger.url)); + } + }); + } else { + backendServices = backendServices.filter(service => { + if (service.backends && service.backends.length) { + return service.backends.some(backend => { + let group = backend.group.replace(/^.*?(\/projects\/.*)$/, '$1'); + return resource.selfLink.includes(group); + }); + } + }); + } if (backendServices && backendServices.length) { forwardingRules.forEach(rule => { diff --git a/helpers/google/index.js b/helpers/google/index.js index 47b1d601ba..7654dec0f8 100644 --- a/helpers/google/index.js +++ b/helpers/google/index.js @@ -306,7 +306,7 @@ var execute = async function(LocalGoogleConfig, collection, service, callObj, ca resultItems = setData(collectionItems, data.data[callObj.dataKey], postCall, parent, {'service': service, 'callKey': callKey, maxLimit: callObj.maxLimit}); } else if (data.data.clusters && ['kubernetes', 'dataproc'].includes(service)) { resultItems = setData(collectionItems, data.data['clusters'], postCall, parent, {'service': service, 'callKey': callKey, maxLimit: callObj.maxLimit}); - } else if (callObj.dataKey && data.data && data.data.length && service == 'vertexAI') { + } else if (callObj.dataKey && data.data && data.data.length && callObj.isDataArray) { resultItems = setData(collectionItems, data.data[0][callObj.dataKey], postCall, parent, {'service': service, 'callKey': callKey, maxLimit: callObj.maxLimit}); } else if (callObj.dataFilterKey && data.data[callObj.dataFilterKey]) { resultItems = setData(collectionItems, data.data[callObj.dataFilterKey], postCall, parent, {'service': service, 'callKey': callKey, maxLimit: callObj.maxLimit}); diff --git a/helpers/google/regions.js b/helpers/google/regions.js index 69d2d233f4..58ee29bad3 100644 --- a/helpers/google/regions.js +++ b/helpers/google/regions.js @@ -157,6 +157,9 @@ module.exports = { accessApproval: ['global'], networkRoutes: ['global'], roles: ['global'], + apiGateways: ['global','asia-northeast1', 'australia-southeast1', 'europe-west1', 'europe-west2', 'us-central1', 'us-east1', 'us-east4', 'us-west2', 'us-west3', 'us-west4'], + api: ['global','asia-northeast1', 'australia-southeast1', 'europe-west1', 'europe-west2', 'us-central1', 'us-east1', 'us-east4', 'us-west2', 'us-west3', 'us-west4'], + apiConfigs: ['global','asia-northeast1', 'australia-southeast1', 'europe-west1', 'europe-west2', 'us-central1', 'us-east1', 'us-east4', 'us-west2', 'us-west3', 'us-west4'], vertexAI: ['us-west1', 'us-west2', 'us-west3', 'us-west4', 'us-central1', 'us-east1', 'us-east4', 'us-south1', 'northamerica-northeast1', 'northamerica-northeast2', 'southamerica-east1', 'southamerica-west1', 'europe-west1', 'europe-west2', 'europe-west3', 'europe-west4', 'europe-west6', 'europe-west8', 'europe-west9', 'europe-north1', 'europe-central2', diff --git a/plugins/aws/accessanalyzer/accessAnalyzerActiveFindings.js b/plugins/aws/accessanalyzer/accessAnalyzerActiveFindings.js index 93d9f22b35..28e47c4c08 100644 --- a/plugins/aws/accessanalyzer/accessAnalyzerActiveFindings.js +++ b/plugins/aws/accessanalyzer/accessAnalyzerActiveFindings.js @@ -11,7 +11,7 @@ module.exports = { 'You can view IAM Access Analyzer findings at any time. Work through all of the findings in your account until you have zero active findings.', link: 'https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-work-with-findings.html', recommended_action: 'Investigate into active findings in your account and do the needful until you have zero active findings.', - apis: ['AccessAnalyzer:listAnalyzers', 'AccessAnalyzer:listFindings'], + apis: ['AccessAnalyzer:listAnalyzers', 'AccessAnalyzer:listFindings', 'AccessAnalyzer:listFindingsV2'], realtime_triggers: ['accessanalyzer:CreateAnalyzer','accessanalyzer:DeleteAnalyzer','accessanalyzer:CreateArchiveRule','accessanalyzer:StartResourceScan'], run: function(cache, settings, callback) { @@ -40,19 +40,32 @@ module.exports = { if (!analyzer.arn) continue; let resource = analyzer.arn; + let totalFiltered = []; var listFindings = helpers.addSource(cache, source, ['accessanalyzer', 'listFindings', region, analyzer.arn]); - if (!listFindings || listFindings.err || !listFindings.data) { + if (listFindings && !listFindings.err && listFindings.data) { + let filtered = listFindings.data.findings.filter(finding => finding.status === 'ACTIVE'); + totalFiltered = totalFiltered.concat(filtered); + } + + var listFindingsV2 = helpers.addSource(cache, source, + ['accessanalyzer', 'listFindingsV2', region, analyzer.arn]); + + if (listFindingsV2 && !listFindingsV2.err && listFindingsV2.data) { + let filteredv2 = listFindingsV2.data.findings.filter(finding => finding.status === 'ACTIVE'); + totalFiltered = totalFiltered.concat(filteredv2); + } + + if ((!listFindings || listFindings.err || !listFindings.data) && (!listFindingsV2 || listFindingsV2.err || !listFindingsV2.data)) { helpers.addResult(results, 3, - `Unable to IAM Access Analyzer findings: ${helpers.addError(listFindings)}`, + `Unable to IAM Access Analyzer findings: ${helpers.addError(listFindings)} ${helpers.addError(listFindingsV2)}`, region, resource); continue; } - let filtered = listFindings.data.findings.filter(finding => finding.status === 'ACTIVE'); - if (!filtered.length) { + if (!totalFiltered.length) { helpers.addResult(results, 0, 'Amazon IAM Access Analyzer has no active findings', region, resource); diff --git a/plugins/aws/accessanalyzer/accessAnalyzerActiveFindings.spec.js b/plugins/aws/accessanalyzer/accessAnalyzerActiveFindings.spec.js index ce47e5ea8d..fa03fd44a8 100644 --- a/plugins/aws/accessanalyzer/accessAnalyzerActiveFindings.spec.js +++ b/plugins/aws/accessanalyzer/accessAnalyzerActiveFindings.spec.js @@ -138,6 +138,83 @@ const listFindings = [ ]; +const listFindingsV2 = [ +{ + "findings": [ + { + "analyzedAt": "2025-01-23T13:06:24+00:00", + "createdAt": "2025-01-23T13:06:56+00:00", + "id": "1a234567-bc6d-7yui-h5j7-4f5f9j8987y0", + "resource": "arn:aws:iam::123456789123:role/abcd-abcd-adfitoui-abcdefg-p1-AsdfghTfjdudnjkDkjg-Z9JgMyMzcxOZ", + "resourceType": "AWS::IAM::Role", + "resourceOwnerAccount": "123456789123", + "status": "ACTIVE", + "updatedAt": "2025-01-23T13:06:56+00:00", + "findingType": "UnusedIAMRole" + }, + { + "analyzedAt": "2025-01-23T13:06:24+00:00", + "createdAt": "2025-01-23T13:06:56+00:00", + "id": "938r4848-4h4j-8449-76d8-8768dh5dhh4u", + "resource": "arn:aws:iam::123456789123:role/abcd-abcd-adfitoui-abcdefg-AsdfghTfjdudnjkDkjg-6vzrTVSqTaNe", + "resourceType": "AWS::IAM::Role", + "resourceOwnerAccount": "123456789123", + "status": "ACTIVE", + "updatedAt": "2025-01-23T13:06:56+00:00", + "findingType": "UnusedIAMRole" + }, + { + "analyzedAt": "2025-01-23T13:06:55+00:00", + "createdAt": "2025-01-23T13:06:56+00:00", + "id": "7484f848-984j-498l-784s-yryh74748f45", + "resource": "arn:aws:iam::123456789123:role/service-role/sdfghyFj-FGH-njkkjg-plgd-6uhjn9ok", + "resourceType": "AWS::IAM::Role", + "resourceOwnerAccount": "123456789123", + "status": "ACTIVE", + "updatedAt": "2025-01-23T13:06:56+00:00", + "findingType": "UnusedPermission" + }, + ] +}, +{ + "findings": [ + { + "analyzedAt": "2025-01-23T13:06:24+00:00", + "createdAt": "2025-01-23T13:06:56+00:00", + "id": "1a234567-bc6d-7yui-h5j7-4f5f9j8987y0", + "resource": "arn:aws:iam::123456789123:role/abcd-abcd-adfitoui-abcdefg-p1-AsdfghTfjdudnjkDkjg-Z9JgMyMzcxOZ", + "resourceType": "AWS::IAM::Role", + "resourceOwnerAccount": "123456789123", + "status": "ARCHIVED", + "updatedAt": "2025-01-23T13:06:56+00:00", + "findingType": "UnusedIAMRole" + }, + { + "analyzedAt": "2025-01-23T13:06:24+00:00", + "createdAt": "2025-01-23T13:06:56+00:00", + "id": "938r4848-4h4j-8449-76d8-8768dh5dhh4u", + "resource": "arn:aws:iam::123456789123:role/abcd-abcd-adfitoui-abcdefg-AsdfghTfjdudnjkDkjg-6vzrTVSqTaNe", + "resourceType": "AWS::IAM::Role", + "resourceOwnerAccount": "123456789123", + "status": "ARCHIVED", + "updatedAt": "2025-01-23T13:06:56+00:00", + "findingType": "UnusedIAMRole" + }, + { + "analyzedAt": "2025-01-23T13:06:55+00:00", + "createdAt": "2025-01-23T13:06:56+00:00", + "id": "7484f848-984j-498l-784s-yryh74748f45", + "resource": "arn:aws:iam::123456789123:role/service-role/sdfghyFj-FGH-njkkjg-plgd-6uhjn9ok", + "resourceType": "AWS::IAM::Role", + "resourceOwnerAccount": "123456789123", + "status": "RESOLVED", + "updatedAt": "2025-01-23T13:06:56+00:00", + "findingType": "UnusedPermission" + }, + ] +} + +] const createCache = (analyzer, listFindings, analyzerErr, listFindingsErr) => { var analyzerArn = (analyzer && analyzer.length) ? analyzer[0].arn: null; @@ -163,7 +240,7 @@ const createCache = (analyzer, listFindings, analyzerErr, listFindingsErr) => { describe('accessAnalyzerActiveFindings', function () { describe('run', function () { - it('should FAIL if Amazon IAM access analyzer has active findings.', function (done) { + it('should FAIL if Amazon IAM access analyzer V1 has active findings.', function (done) { const cache = createCache(listAnalyzers, listFindings[0]); accessAnalyzerActiveFindings.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); @@ -174,7 +251,18 @@ describe('accessAnalyzerActiveFindings', function () { }); }); - it('should PASS if Amazon IAM access analyzer have no active findings.', function (done) { + it('should FAIL if Amazon IAM access analyzer v2 has active findings.', function (done) { + const cache = createCache(listAnalyzers, listFindingsV2[0]); + accessAnalyzerActiveFindings.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('Amazon IAM Access Analyzer has active findings'); + done(); + }); + }); + + it('should PASS if Amazon IAM access analyzer V1 have no active findings.', function (done) { const cache = createCache(listAnalyzers, listFindings[1]); accessAnalyzerActiveFindings.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); @@ -186,6 +274,19 @@ describe('accessAnalyzerActiveFindings', function () { }); }); + + it('should PASS if Amazon IAM access analyzer V2 have no active findings.', function (done) { + const cache = createCache(listAnalyzers, listFindingsV2[1]); + accessAnalyzerActiveFindings.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('Amazon IAM Access Analyzer has no active findings'); + + done(); + }); + }); + it('should PASS if no analyzers found', function (done) { const cache = createCache([]); accessAnalyzerActiveFindings.run(cache, {}, (err, results) => { diff --git a/plugins/aws/ec2/securityGroupRfc1918.js b/plugins/aws/ec2/securityGroupRfc1918.js index 12b6eff30a..39c8e9778d 100644 --- a/plugins/aws/ec2/securityGroupRfc1918.js +++ b/plugins/aws/ec2/securityGroupRfc1918.js @@ -70,16 +70,15 @@ module.exports = { } } } - - if (!privateCidrsFound.length) { - helpers.addResult(results, 0, - 'Security group "' + group.GroupName + '" is not configured to allow inbound access from any source IP address within any reserved private addresses', - region, resource); - } else { - helpers.addResult(results, 2, - 'Security group "' + group.GroupName + '" is configured to allow inbound access from any source IP address within these reserved private addresses: ' + privateCidrsFound.join(', '), - region, resource); - } + } + if (!privateCidrsFound.length) { + helpers.addResult(results, 0, + 'Security group "' + group.GroupName + '" is not configured to allow inbound access from any source IP address within any reserved private addresses', + region, resource); + } else { + helpers.addResult(results, 2, + 'Security group "' + group.GroupName + '" is configured to allow inbound access from any source IP address within these reserved private addresses: ' + privateCidrsFound.join(', '), + region, resource); } } rcb(); diff --git a/plugins/aws/iam/iamRolePolicies.js b/plugins/aws/iam/iamRolePolicies.js index 6d3dd14d23..86c0fc1847 100644 --- a/plugins/aws/iam/iamRolePolicies.js +++ b/plugins/aws/iam/iamRolePolicies.js @@ -81,6 +81,12 @@ module.exports = { description: 'Enable this setting to ignore resource wildcards i.e. \'"Resource": "*"\' in the IAM policy, which by default, are being flagged.', regex: '^(true|false)$', default: 'false' + }, + iam_policy_message_format: { + name: 'IAM Policy Message Format', + description: 'Enable this setting to include policy names in the failure messages', + regex: '^(true|false)$', + default: 'false' } }, realtime_triggers: ['iam:CreateRole','iam:DeleteRole','iam:AttachRolePolicy','iam:DetachRolePolicy','iam:PutRolePolicy','iam:DeleteRolePolicy'], @@ -94,7 +100,8 @@ module.exports = { ignore_customer_managed_iam_policies: settings.ignore_customer_managed_iam_policies || this.settings.ignore_customer_managed_iam_policies.default, iam_role_policies_ignore_tag: settings.iam_role_policies_ignore_tag || this.settings.iam_role_policies_ignore_tag.default, iam_policy_resource_specific_wildcards: settings.iam_policy_resource_specific_wildcards || this.settings.iam_policy_resource_specific_wildcards.default, - ignore_iam_policy_resource_wildcards: settings.ignore_iam_policy_resource_wildcards || this.settings.ignore_iam_policy_resource_wildcards.default + ignore_iam_policy_resource_wildcards: settings.ignore_iam_policy_resource_wildcards || this.settings.ignore_iam_policy_resource_wildcards.default, + iam_policy_message_format: settings.iam_policy_message_format || this.settings.iam_policy_message_format.default }; config.ignore_service_specific_wildcards = (config.ignore_service_specific_wildcards === 'true'); @@ -102,7 +109,7 @@ module.exports = { config.ignore_aws_managed_iam_policies = (config.ignore_aws_managed_iam_policies === 'true'); config.ignore_customer_managed_iam_policies = (config.ignore_customer_managed_iam_policies === 'true'); config.ignore_iam_policy_resource_wildcards = (config.ignore_iam_policy_resource_wildcards === 'true'); - + config.iam_policy_message_format = (config.iam_policy_message_format === 'true'); var allowedRegex = RegExp(config.iam_policy_resource_specific_wildcards); @@ -196,7 +203,8 @@ module.exports = { return cb(); } - var roleFailures = []; + var roleFailures = config.iam_policy_message_format ? {} : []; + // See if role has admin managed policy if (listAttachedRolePolicies.data && @@ -204,7 +212,11 @@ module.exports = { for (var policy of listAttachedRolePolicies.data.AttachedPolicies) { if (policy.PolicyArn === managedAdminPolicy) { - roleFailures.push('Role has managed AdministratorAccess policy'); + if (config.iam_policy_message_format) { + roleFailures.admin = 'managedAdminPolicy'; + } else { + roleFailures.push('Role has managed AdministratorAccess policy'); + } break; } @@ -230,7 +242,11 @@ module.exports = { getPolicyVersion.data.PolicyVersion.Document); if (!statements) break; - addRoleFailures(roleFailures, statements, 'managed', config.ignore_service_specific_wildcards, allowedRegex, config.ignore_iam_policy_resource_wildcards); + if (config.iam_policy_message_format) { + addRoleFailuresPolicyName(roleFailures, statements, 'managed', policy.PolicyName, config.ignore_service_specific_wildcards, allowedRegex, config.ignore_iam_policy_resource_wildcards); + } else { + addRoleFailures(roleFailures, statements, 'managed', config.ignore_service_specific_wildcards, allowedRegex, config.ignore_iam_policy_resource_wildcards); + } } } } @@ -249,21 +265,22 @@ module.exports = { var statements = getRolePolicy[policyName].data.PolicyDocument; if (!statements) break; - addRoleFailures(roleFailures, statements, 'inline', config.ignore_service_specific_wildcards, allowedRegex, config.ignore_iam_policy_resource_wildcards); + if (config.iam_policy_message_format) { + addRoleFailuresPolicyName(roleFailures, statements, 'inline', policyName, config.ignore_service_specific_wildcards, allowedRegex, config.ignore_iam_policy_resource_wildcards); + } else { + addRoleFailures(roleFailures, statements, 'inline', config.ignore_service_specific_wildcards, allowedRegex, config.ignore_iam_policy_resource_wildcards); + } } } } - if (roleFailures.length) { - helpers.addResult(results, 2, - roleFailures.join(', '), - 'global', role.Arn, custom); + if (config.iam_policy_message_format) { + compileFormattedResults(roleFailures, role, results, custom); } else { - helpers.addResult(results, 0, - 'Role does not have overly-permissive policy', - 'global', role.Arn, custom); + compileSimpleResults(roleFailures, role, results, custom); } + cb(); }, function(){ callback(null, results, source); @@ -308,4 +325,173 @@ function addRoleFailures(roleFailures, statements, policyType, ignoreServiceSpec if (failMsg && roleFailures.indexOf(failMsg) === -1) roleFailures.push(failMsg); } } +} + +function addRoleFailuresPolicyName(roleFailures, statements, policyType, policyName, ignoreServiceSpecific, regResource, ignoreResourceSpecific) { + // Initialize roleFailures as an object for the first time + if (!roleFailures.managed) { + roleFailures.managed = { + allActionsAllResources: [], + allActionsSelectedResources: [], + actionsAllResources: [], + wildcardActions: {}, + regexMismatch: {} + }; + } + if (!roleFailures.inline) { + roleFailures.inline = { + allActionsAllResources: [], + allActionsSelectedResources: [], + actionsAllResources: [], + wildcardActions: {}, + regexMismatch: {} + }; + } + if (!roleFailures.admin) roleFailures.admin = false; + + for (var statement of statements) { + if (statement.Effect === 'Allow' && !statement.Condition) { + let targetObj = roleFailures[policyType]; + + if (statement.Action && + statement.Action.indexOf('*') > -1 && + statement.Resource && + statement.Resource.indexOf('*') > -1) { + targetObj.allActionsAllResources.push(policyName); + } else if (statement.Action.indexOf('*') > -1) { + targetObj.allActionsSelectedResources.push(policyName); + } else if (!ignoreResourceSpecific && statement.Resource && statement.Resource == '*') { + targetObj.actionsAllResources.push(policyName); + } else if (!ignoreServiceSpecific && statement.Action && statement.Action.length) { + // Check each action for wildcards + let wildcards = []; + for (var a in statement.Action) { + if (/^.+:[a-zA-Z]?\*.?$/.test(statement.Action[a])) { + wildcards.push(statement.Action[a]); + } + } + if (wildcards.length) { + if (!targetObj.wildcardActions[wildcards.join(', ')]) { + targetObj.wildcardActions[wildcards.join(', ')] = []; + } + if (!targetObj.wildcardActions[wildcards.join(', ')].includes(policyName)) { + targetObj.wildcardActions[wildcards.join(', ')].push(policyName); + } + } + } else if (statement.Resource && statement.Resource.length) { + // Check each resource for wildcard + let wildcards = []; + for (var resource of statement.Resource) { + if (!regResource.test(resource)) { + wildcards.push(resource); + } + } + if (wildcards.length) { + if (!targetObj.regexMismatch[wildcards.join(', ')]) { + targetObj.regexMismatch[wildcards.join(', ')] = []; + } + if (!targetObj.regexMismatch[wildcards.join(', ')].includes(policyName)) { + targetObj.regexMismatch[wildcards.join(', ')].push(policyName); + } + } + } + } + } +} + +function hasFailures(roleFailures) { + if (roleFailures.admin) return true; + + if (roleFailures.managed) { + if (roleFailures.managed.allActionsAllResources.length) return true; + if (roleFailures.managed.allActionsSelectedResources.length) return true; + if (roleFailures.managed.actionsAllResources.length) return true; + if (Object.keys(roleFailures.managed.wildcardActions).length) return true; + if (roleFailures.managed.regexMismatch.length) return true; + } + + if (roleFailures.inline) { + if (roleFailures.inline.allActionsAllResources.length) return true; + if (roleFailures.inline.allActionsSelectedResources.length) return true; + if (roleFailures.inline.actionsAllResources.length) return true; + if (Object.keys(roleFailures.inline.wildcardActions).length) return true; + if (roleFailures.inline.regexMismatch.length) return true; + } + + return false; +} + +function formatPolicyNames(policyArray) { + if (policyArray.length <= 5) { + return [...new Set(policyArray)].join('", "'); + } + return [...new Set(policyArray)].slice(0, 5).join('", "') + '" and so on...'; +} + +function compileSimpleResults(roleFailures, role, results, custom) { + if (roleFailures.length) { + helpers.addResult(results, 2, + roleFailures.join(', '), + 'global', role.Arn, custom); + } else { + helpers.addResult(results, 0, + 'Role does not have overly-permissive policy', + 'global', role.Arn, custom); + } +} + +function compileFormattedResults(roleFailures, role, results, custom) { + if (hasFailures(roleFailures)) { + let messages = []; + + if (roleFailures.admin == 'managedAdminPolicy') { + messages.push('Role has managed AdministratorAccess policy'); + } + + // Format managed policies + if (roleFailures.managed) { + if (roleFailures.managed.allActionsAllResources.length) { + messages.push(`Role managed policy "${formatPolicyNames(roleFailures.managed.allActionsAllResources)}" allows all actions on all resources`); + } + if (roleFailures.managed.allActionsSelectedResources.length) { + messages.push(`Role managed policy "${formatPolicyNames(roleFailures.managed.allActionsSelectedResources)}" allows all actions on selected resources`); + } + if (roleFailures.managed.actionsAllResources.length) { + messages.push(`Role managed policy "${formatPolicyNames(roleFailures.managed.actionsAllResources)}" allows actions on all resources`); + } + for (let action in roleFailures.managed.wildcardActions) { + messages.push(`Role managed policy "${roleFailures.managed.wildcardActions[action].join('", "')}" allows wildcard actions: ${action}`); + } + for (let resource in roleFailures.managed.regexMismatch) { + messages.push(`Role managed policy "${roleFailures.managed.regexMismatch[resource].join('", "')}" does not match provided regex: ${resource}`); + } + } + + // Format inline policies + if (roleFailures.inline) { + if (roleFailures.inline.allActionsAllResources.length) { + messages.push(`Role inline policy "${formatPolicyNames(roleFailures.inline.allActionsAllResources)}" allows all actions on all resources`); + } + if (roleFailures.inline.allActionsSelectedResources.length) { + messages.push(`Role inline policy "${formatPolicyNames(roleFailures.inline.allActionsSelectedResources)}" allows all actions on selected resources`); + } + if (roleFailures.inline.actionsAllResources.length) { + messages.push(`Role inline policy "${formatPolicyNames(roleFailures.inline.actionsAllResources)}" allows actions on all resources`); + } + for (let action in roleFailures.inline.wildcardActions) { + messages.push(`Role inline policy "${roleFailures.inline.wildcardActions[action].join('", "')}" allows wildcard actions: ${action}`); + } + for (let resource in roleFailures.inline.regexMismatch) { + messages.push(`Role inline policy "${roleFailures.inline.regexMismatch[resource].join('", "')}" does not match provided regex: ${resource}`); + } + } + + helpers.addResult(results, 2, + messages.join('\n'), + 'global', role.Arn, custom); + } else { + helpers.addResult(results, 0, + 'Role does not have overly-permissive policy', + 'global', role.Arn, custom); + } } \ No newline at end of file diff --git a/plugins/aws/iam/iamRolePolicies.spec.js b/plugins/aws/iam/iamRolePolicies.spec.js index 0e336d1f6c..25fdcc3b0e 100644 --- a/plugins/aws/iam/iamRolePolicies.spec.js +++ b/plugins/aws/iam/iamRolePolicies.spec.js @@ -393,7 +393,7 @@ describe('iamRolePolicies', function () { const cache = createCache([listRoles[0]],getRole[0], {}, listRolePolicies[1], getRolePolicy[4]); iamRolePolicies.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); - expect(results[0].message).to.include('policy allows all actions on selected resources'); + expect(results[0].message).to.include('allows all actions on selected resources'); expect(results[0].status).to.equal(2); done(); }); @@ -403,7 +403,7 @@ describe('iamRolePolicies', function () { const cache = createCache([listRoles[1]],getRole[0], {}, listRolePolicies[1], getRolePolicy[3]); iamRolePolicies.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); - expect(results[0].message).to.include('policy allows all actions on all resources'); + expect(results[0].message).to.include('allows all actions on all resources'); expect(results[0].status).to.equal(2); done(); }); diff --git a/plugins/aws/kinesisvideo/videostreamDataEncrypted.js b/plugins/aws/kinesisvideo/videostreamDataEncrypted.js index a1146e4225..940cefd8ca 100644 --- a/plugins/aws/kinesisvideo/videostreamDataEncrypted.js +++ b/plugins/aws/kinesisvideo/videostreamDataEncrypted.js @@ -11,7 +11,7 @@ module.exports = { 'It is recommended to use customer-managed keys (CMKs) for encryption in order to gain more granular control over encryption/decryption process.', recommended_action: 'Encrypt Kinesis Video Streams data with customer-manager keys (CMKs).', link: 'https://docs.aws.amazon.com/kinesisvideostreams/latest/dg/how-kms.html', - apis: ['KinesisVideo:listStreams', 'KMS:describeKey', 'KMS:listKeys'], + apis: ['KinesisVideo:listStreams', 'KMS:describeKey', 'KMS:listKeys', 'KMS:listAliases'], settings: { video_stream_data_desired_encryption_level: { name: 'Kinesis Video Streams Data Target Encryption Level', @@ -59,16 +59,39 @@ module.exports = { return rcb(); } + var listAliases = helpers.addSource(cache, source, + ['kms', 'listAliases', region]); + + if (!listAliases || listAliases.err || !listAliases.data) { + helpers.addResult(results, 3, + 'Unable to query for KMS aliases: ' + helpers.addError(listAliases), + region); + return rcb(); + } + + var keyArn; + var kmsAliasArnMap = {}; + listAliases.data.forEach(function(alias) { + keyArn = alias.AliasArn.replace(/:alias\/.*/, ':key/' + alias.TargetKeyId); + kmsAliasArnMap[alias.AliasName] = keyArn; + }); + for (let streamData of listStreams.data) { if (!streamData.StreamARN) continue; let resource = streamData.StreamARN; if (streamData.KmsKeyId) { - var kmsKeyId = streamData.KmsKeyId.split('/')[1] ? streamData.KmsKeyId.split('/')[1] : streamData.KmsKeyId; + + let aliasKey = streamData.KmsKeyId.includes('alias/') ? streamData.KmsKeyId.split(':').pop() : streamData.KmsKeyId; + let kmsKeyArn = (aliasKey.startsWith('alias/')) + ? (kmsAliasArnMap[aliasKey] ? kmsAliasArnMap[aliasKey] : streamData.KmsKeyId) + : streamData.KmsKeyId; + var kmsKeyId = kmsKeyArn.split('/')[1] ? kmsKeyArn.split('/')[1] : kmsKeyArn; + var describeKey = helpers.addSource(cache, source, - ['kms', 'describeKey', region, kmsKeyId]); + ['kms', 'describeKey', region, kmsKeyId]); if (!describeKey || describeKey.err || !describeKey.data || !describeKey.data.KeyMetadata) { helpers.addResult(results, 3, diff --git a/plugins/aws/kinesisvideo/videostreamDataEncrypted.spec.js b/plugins/aws/kinesisvideo/videostreamDataEncrypted.spec.js index 3dc7073d33..0c7177eecc 100644 --- a/plugins/aws/kinesisvideo/videostreamDataEncrypted.spec.js +++ b/plugins/aws/kinesisvideo/videostreamDataEncrypted.spec.js @@ -21,6 +21,14 @@ const listKeys = [ } ]; +const listAliases = [ + { + "AliasName": "alias/my-kinesis-key", + "AliasArn": "arn:aws:kms:us-east-1:000011112222:alias/my-kinesis-key", + "TargetKeyId": "ad013a33-b01d-4d88-ac97-127399c18b3e" + } +]; + const describeKey = [ { "KeyMetadata": { @@ -60,7 +68,7 @@ const describeKey = [ } ]; -const createCache = (streamData, keys, describeKey, streamDataErr, keysErr, describeKeyErr) => { +const createCache = (streamData, keys, aliases, describeKey, streamDataErr, keysErr, aliasesErr, describeKeyErr) => { var keyId = (keys && keys.length ) ? keys[0].KeyId : null; return { kinesisvideo: { @@ -78,6 +86,12 @@ const createCache = (streamData, keys, describeKey, streamDataErr, keysErr, desc err: keysErr } }, + listAliases: { + 'us-east-1': { + data: aliases, + err: aliasesErr + } + }, describeKey: { 'us-east-1': { [keyId]: { @@ -95,8 +109,8 @@ const createCache = (streamData, keys, describeKey, streamDataErr, keysErr, desc describe('videostreamDataEncrypted', function () { describe('run', function () { - it('should PASS if Kinesis Video Streams data is using desired encryption level', function (done) { - const cache = createCache(listStreams, listKeys, describeKey[0]); + it('should PASS if Kinesis Video Streams data is using customer-managed encryption (awscmk)', function (done) { + const cache = createCache(listStreams, listKeys, listAliases, describeKey[0]); videostreamDataEncrypted.run(cache, { video_stream_data_desired_encryption_level: 'awscmk' }, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(0); @@ -106,9 +120,9 @@ describe('videostreamDataEncrypted', function () { }); - it('should FAIL if Kinesis Video Streams data is using desired encyption level', function (done) { - const cache = createCache(listStreams, listKeys, describeKey[1]); - videostreamDataEncrypted.run(cache, { video_stream_data_desired_encryption_level:'awscmk' }, (err, results) => { + it('should FAIL if Kinesis Video Streams data is using AWS managed encryption (awskms)', function (done) { + const cache = createCache(listStreams, listKeys, listAliases, describeKey[1]); + videostreamDataEncrypted.run(cache, { video_stream_data_desired_encryption_level: 'awscmk' }, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(2); expect(results[0].message).to.include('Kinesis Video Streams data is using awskms'); @@ -117,7 +131,7 @@ describe('videostreamDataEncrypted', function () { }); - it('should PASS if no Kinesis Video Streams found', function (done) { + it('should PASS if no Kinesis Video Streams are found', function (done) { const cache = createCache([]); videostreamDataEncrypted.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); @@ -128,7 +142,7 @@ describe('videostreamDataEncrypted', function () { }); it('should UNKNOWN if unable to list Kinesis Video Streams', function (done) { - const cache = createCache(null, null, null, { message: "Unable to list Kinesis Video Streams encryption" }); + const cache = createCache(null, null, null, null, { message: "Unable to list Kinesis Video Streams" }); videostreamDataEncrypted.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(3); @@ -138,7 +152,16 @@ describe('videostreamDataEncrypted', function () { }); it('should UNKNOWN if unable to list KMS keys', function (done) { - const cache = createCache(null, null, null, null, { message: "Unable to list KMS keys" }); + const cache = createCache(null, null, null, null, null, { message: "Unable to list KMS keys" }); + videostreamDataEncrypted.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(3); + done(); + }); + }); + + it('should UNKNOWN if unable to retrieve KMS alias data', function (done) { + const cache = createCache(listStreams, listKeys, null, describeKey[0], null, null, { message: "Unable to list KMS aliases" }); videostreamDataEncrypted.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(3); diff --git a/plugins/aws/lambda/lambdaNetworkExposure.js b/plugins/aws/lambda/lambdaNetworkExposure.js index d552ca1723..e440cd23e0 100644 --- a/plugins/aws/lambda/lambdaNetworkExposure.js +++ b/plugins/aws/lambda/lambdaNetworkExposure.js @@ -10,8 +10,8 @@ module.exports = { more_info: 'Lambda functions can be exposed to the internet through Function URLs with public access policies or through API Gateway integrations. It\'s important to ensure these endpoints are properly secured.', link: 'https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html', recommended_action: 'Ensure Lambda Function URLs have proper authorization configured and API Gateway integrations use appropriate security measures.', - apis: ['Lambda:listFunctions', 'Lambda:getFunctionUrlConfig', 'Lambda:getPolicy', - 'APIGateway:getRestApis','APIGateway:getResources', 'APIGateway:getStages', 'APIGateway:getIntegration', 'ELBv2:describeLoadBalancers', 'ELBv2:describeTargetGroups', + apis: ['Lambda:listFunctions', 'Lambda:getFunctionUrlConfig', 'Lambda:getPolicy', + 'APIGateway:getRestApis','APIGateway:getResources', 'APIGateway:getStages', 'APIGateway:getIntegration', 'ELBv2:describeLoadBalancers', 'ELBv2:describeTargetGroups', 'ELBv2:describeTargetHealth', 'ELBv2:describeListeners', 'EC2:describeSecurityGroups'], realtime_triggers: ['lambda:CreateFunctionUrlConfig', 'lambda:UpdateFunctionUrlConfig', 'lambda:DeleteFunctionUrlConfig', 'lambda:AddPermission', 'lambda:RemovePermission', @@ -49,7 +49,7 @@ module.exports = { // Get function URL config and policy for Lambda-specific checks var getFunctionUrlConfig = helpers.addSource(cache, source, ['lambda', 'getFunctionUrlConfig', region, lambda.FunctionName]); - + var getPolicy = helpers.addSource(cache, source, ['lambda', 'getPolicy', region, lambda.FunctionName]); @@ -79,4 +79,4 @@ module.exports = { callback(null, results, source); }); } -}; \ No newline at end of file +}; diff --git a/plugins/aws/route53/domainExpiry.js b/plugins/aws/route53/domainExpiry.js index cf2a9869ed..0fd5ba80e3 100644 --- a/plugins/aws/route53/domainExpiry.js +++ b/plugins/aws/route53/domainExpiry.js @@ -42,7 +42,7 @@ module.exports = { if (difference > 35) { helpers.addResult(results, 0, returnMsg, 'global', domain.DomainName); - } else if (domain.DomainName.endsWith(('.com.ar, .com.br, .jp')) && difference > 30) { + } else if (['.com.ar', '.com.br', '.jp'].some(suffix => domain.DomainName.endsWith(suffix)) && difference > 30){ helpers.addResult(results, 0, returnMsg, 'global', domain.DomainName); } else if (difference > 0) { helpers.addResult(results, 2, returnMsg, 'global', domain.DomainName); diff --git a/plugins/azure/appservice/functionAppNetworkExposure.js b/plugins/azure/appservice/functionAppNetworkExposure.js new file mode 100644 index 0000000000..91f0ffa4e1 --- /dev/null +++ b/plugins/azure/appservice/functionAppNetworkExposure.js @@ -0,0 +1,131 @@ +var async = require('async'); +var helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'Network Exposure', + category: 'App Service', + domain: 'Application Integration', + severity: 'Info', + description: 'Ensures that Azure function apps are not exposed to the internet.', + more_info: 'Azure Functions exposed to the internet are at higher risk of unauthorized access and exploitation. Securing access through proper configuration of authorization levels, IP restrictions, private endpoints, or service-specific security settings is critical to minimize vulnerabilities.', + recommended_action: 'Restrict Azure Function exposure by implementing secure access controls, such as authorization levels, IP restrictions, private endpoints, or integrating with VNETs.', + link: 'https://learn.microsoft.com/en-us/azure/azure-functions/functions-networking-options', + apis: ['webApps:list', 'applicationGateways:list', 'loadBalancers:list', 'classicFrontDoors:list', 'afdWafPolicies:listAll'], + realtime_triggers: ['microsoftweb:sites:write','microsoftweb:sites:delete', 'microsoftnetwork:applicationgateways:write', 'microsoftnetwork:applicationgateways:delete', 'microsoftnetwork:loadbalancers:write', 'microsoftnetwork:loadbalancers:delete', + 'microsoftnetwork:frontdoors:write', 'microsoftnetwork:frontdoors:delete', 'microsoftnetwork:frontdoorwebapplicationfirewallpolicies:write', 'microsoftnetwork:frontdoorwebapplicationfirewallpolicies:delete'], + + run: function(cache, settings, callback) { + const results = []; + const source = {}; + const locations = helpers.locations(settings.govcloud); + + async.each(locations.webApps, function(location, rcb) { + const webApps = helpers.addSource(cache, source, + ['webApps', 'list', location]); + + if (!webApps) return rcb(); + + if (webApps.err || !webApps.data) { + helpers.addResult(results, 3, + 'Unable to query for Function Apps: ' + helpers.addError(webApps), location); + return rcb(); + } + + if (webApps.data && webApps.data.length) { + webApps.data = webApps.data.filter(app => app.id && app.kind && app.kind.toLowerCase().includes('functionapp')); + } + + if (!webApps.data.length) { + helpers.addResult(results, 0, 'No existing Function Apps found', location); + return rcb(); + } + + const appGateways = helpers.addSource(cache, source, + ['applicationGateways', 'list', location]); + + const loadBalancers = helpers.addSource(cache, source, + ['loadBalancers', 'list', location]); + + + const frontDoors = helpers.addSource(cache, source, + ['classicFrontDoors', 'list', 'global']); + + + const wafPolicies = helpers.addSource(cache, source, + ['afdWafPolicies', 'listAll', 'global']); + + + for (let functionApp of webApps.data) { + let internetExposed = ''; + if (functionApp.publicNetworkAccess && functionApp.publicNetworkAccess === 'Enabled') { + internetExposed = 'public network access'; + } else { + let attachedResources = { + appGateways: [], + lbNames: [], + frontDoors: [] + }; + + // list attached app gateways + if (appGateways && !appGateways.err && appGateways.data && appGateways.data.length) { + attachedResources.appGateways = appGateways.data.filter(ag => + ag.backendAddressPools && ag.backendAddressPools.some(pool => + pool.backendAddresses && pool.backendAddresses.some(addr => + addr.fqdn === functionApp.properties.defaultHostName))); + } + + //list attached load balancers + if (loadBalancers && !loadBalancers.err && loadBalancers.data && loadBalancers.data.length) { + attachedResources.lbNames = loadBalancers.data.filter(lb => + lb.backendAddressPools && lb.backendAddressPools.some(pool => + pool.properties.backendIPConfigurations && + pool.properties.backendIPConfigurations.some(config => + config.id.toLowerCase().includes(functionApp.id.toLowerCase())))); + + attachedResources.lbNames = attachedResources.lbNames.map(lb => lb.name); + } + + // list attached front doors + if (frontDoors && !frontDoors.err && frontDoors.data && frontDoors.data.length) { + frontDoors.data.forEach(fd => { + const isFunctionAppBackend = fd.backendPools && fd.backendPools.some(pool => + pool.backends && pool.backends.some(backend => + backend.address === functionApp.properties.defaultHostName)); + + if (isFunctionAppBackend) { + fd.associatedWafPolicies = []; + + if (fd.frontendEndpoints && wafPolicies && !wafPolicies.err && wafPolicies.data && wafPolicies.data.length) { + fd.frontendEndpoints.forEach(endpoint => { + if (endpoint.webApplicationFirewallPolicyLink) { + const policyId = endpoint.webApplicationFirewallPolicyLink.id.toLowerCase(); + const matchingPolicy = wafPolicies.data.find(policy => + policy.id && policy.id.toLowerCase() === policyId); + if (matchingPolicy) { + fd.associatedWafPolicies.push(matchingPolicy); + } + } + }); + } + + attachedResources.frontDoors.push(fd); + } + }); + } + + internetExposed = helpers.checkNetworkExposure(cache, source, [], [], location, results, attachedResources, functionApp); + } + + if (internetExposed && internetExposed.length) { + helpers.addResult(results, 2, `Function App is exposed to the internet through ${internetExposed}`, location, functionApp.id); + } else { + helpers.addResult(results, 0, 'Function App is not exposed to the internet', location, functionApp.id); + } + } + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; diff --git a/plugins/azure/automationAccounts/automationAcctExpiredWebhooks.spec.js b/plugins/azure/automationAccounts/automationAcctExpiredWebhooks.spec.js index 341fcb9109..6f96b2ab95 100644 --- a/plugins/azure/automationAccounts/automationAcctExpiredWebhooks.spec.js +++ b/plugins/azure/automationAccounts/automationAcctExpiredWebhooks.spec.js @@ -1,5 +1,7 @@ var expect = require('chai').expect; var automationAcctExpiredWebhooks = require('./automationAcctExpiredWebhooks'); +var nextMonthExpiry = new Date(); +nextMonthExpiry.setMonth(nextMonthExpiry.getMonth() + 1); const automationAccounts = [ { @@ -31,7 +33,7 @@ const webhooks = [ "id": "/subscriptions/12345/resourceGroups/test-rg/providers/Microsoft.Automation/automationAccounts/test-automationacct/webhooks/test1", "name": "test1", "creationTime": "2024-01-22T13:33:52.1066667+00:00", - "expiryTime": "2025-01-22T13:33:52.1066667+00:00", + "expiryTime": nextMonthExpiry, }, { "id": "/subscriptions/12345/resourceGroups/test-rg/providers/Microsoft.Automation/automationAccounts/test-automationacct/webhooks/test2", diff --git a/plugins/azure/keyvaults/keyVaultKeyExpiry.js b/plugins/azure/keyvaults/keyVaultKeyExpiry.js index 4d1cfeda43..6ba32db018 100644 --- a/plugins/azure/keyvaults/keyVaultKeyExpiry.js +++ b/plugins/azure/keyvaults/keyVaultKeyExpiry.js @@ -30,7 +30,7 @@ module.exports = { }; async.each(locations.vaults, function(location, rcb) { - var vaults = helpers.addSource(cache, source, + var vaults = helpers.addSource(cache, source, ['vaults', 'list', location]); if (!vaults) return rcb(); @@ -46,11 +46,7 @@ module.exports = { } vaults.data.forEach(function(vault) { - if (!vault || !vault.properties) { - helpers.addResult(results, 3, 'Unable to read vault properties', location, vault.id); - return; - } - if (!vault.properties.enableRbacAuthorization) { + if (!vault.enableRbacAuthorization) { return; } diff --git a/plugins/azure/keyvaults/keyVaultKeyExpiry.spec.js b/plugins/azure/keyvaults/keyVaultKeyExpiry.spec.js index 2227db5dc7..7aa4a804b9 100644 --- a/plugins/azure/keyvaults/keyVaultKeyExpiry.spec.js +++ b/plugins/azure/keyvaults/keyVaultKeyExpiry.spec.js @@ -16,8 +16,8 @@ const listKeyVaults = [ name: 'test-vault', type: 'Microsoft.KeyVault/vaults', location: 'eastus', + enableRbacAuthorization: true, properties: { - enableRbacAuthorization: true, vaultUri: 'https://test-vault.vault.azure.net/' } }, @@ -26,8 +26,8 @@ const listKeyVaults = [ name: 'test-vault-2', type: 'Microsoft.KeyVault/vaults', location: 'eastus', + enableRbacAuthorization: false, properties: { - enableRbacAuthorization: false, vaultUri: 'https://test-vault-2.vault.azure.net/' } } @@ -163,4 +163,4 @@ describe('keyVaultKeyExpiryRbac', function() { auth.run(createCache(null, [listKeyVaults[0]], [getKeys[2]]), { key_vault_key_expiry_fail: '40' }, callback); }); }); -}); +}); diff --git a/plugins/azure/keyvaults/keyVaultKeyExpiryNonRbac.js b/plugins/azure/keyvaults/keyVaultKeyExpiryNonRbac.js index 4e9dae2d56..df9665a291 100644 --- a/plugins/azure/keyvaults/keyVaultKeyExpiryNonRbac.js +++ b/plugins/azure/keyvaults/keyVaultKeyExpiryNonRbac.js @@ -30,7 +30,7 @@ module.exports = { }; async.each(locations.vaults, function(location, rcb) { - var vaults = helpers.addSource(cache, source, + var vaults = helpers.addSource(cache, source, ['vaults', 'list', location]); if (!vaults) return rcb(); @@ -46,12 +46,7 @@ module.exports = { } vaults.data.forEach(function(vault) { - if (!vault || !vault.properties) { - helpers.addResult(results, 3, 'Unable to read vault properties', location, vault.id); - return; - } - - if (vault.properties.enableRbacAuthorization) { + if (vault.enableRbacAuthorization) { return; } @@ -59,10 +54,10 @@ module.exports = { ['vaults', 'getKeys', location, vault.id]); if (!keys || keys.err || !keys.data) { - helpers.addResult(results, 3, + helpers.addResult(results, 3, 'Unable to query for Key Vault keys: ' + helpers.addError(keys), location, vault.id); } else if (!keys.data.length) { - helpers.addResult(results, 0, + helpers.addResult(results, 0, 'No Key Vault keys found in non RBAC vault', location, vault.id); } else { keys.data.forEach(function(key) { diff --git a/plugins/azure/keyvaults/keyVaultKeyExpiryNonRbac.spec.js b/plugins/azure/keyvaults/keyVaultKeyExpiryNonRbac.spec.js index 1c5431cb1f..43ed0ab134 100644 --- a/plugins/azure/keyvaults/keyVaultKeyExpiryNonRbac.spec.js +++ b/plugins/azure/keyvaults/keyVaultKeyExpiryNonRbac.spec.js @@ -5,7 +5,7 @@ var keyExpiryPass = new Date(); keyExpiryPass.setMonth(keyExpiryPass.getMonth() + 2); var keyExpiryFail = new Date(); -keyExpiryFail.setDate(keyExpiryFail.getDate() + 25); // Set to 35 days in the future +keyExpiryFail.setDate(keyExpiryFail.getDate() + 25); var keyExpired = new Date(); keyExpired.setMonth(keyExpired.getMonth() - 1); @@ -16,8 +16,8 @@ const listKeyVaults = [ name: 'test-vault', type: 'Microsoft.KeyVault/vaults', location: 'eastus', + enableRbacAuthorization: false, properties: { - enableRbacAuthorization: false, vaultUri: 'https://test-vault.vault.azure.net/' } }, @@ -26,8 +26,8 @@ const listKeyVaults = [ name: 'test-vault-2', type: 'Microsoft.KeyVault/vaults', location: 'eastus', + enableRbacAuthorization: true, properties: { - enableRbacAuthorization: true, vaultUri: 'https://test-vault-2.vault.azure.net/' } } @@ -172,4 +172,4 @@ describe('keyVaultKeyExpiryNonRbac', function() { auth.run(createCache(null, [listKeyVaults[0]], [getKeys[2]]), { key_vault_key_expiry_fail: '40' }, callback); }); }); -}); \ No newline at end of file +}); diff --git a/plugins/azure/keyvaults/keyVaultSecretExpiry.js b/plugins/azure/keyvaults/keyVaultSecretExpiry.js index 063f3125d4..2dd31788d5 100644 --- a/plugins/azure/keyvaults/keyVaultSecretExpiry.js +++ b/plugins/azure/keyvaults/keyVaultSecretExpiry.js @@ -47,7 +47,7 @@ module.exports = { vaults.data.forEach(function(vault) { // Check if vault is RBAC-enabled - if (!vault.properties || !vault.properties.enableRbacAuthorization) { + if (!vault.enableRbacAuthorization) { return; } diff --git a/plugins/azure/keyvaults/keyVaultSecretExpiry.spec.js b/plugins/azure/keyvaults/keyVaultSecretExpiry.spec.js index 72c5c5baea..4bf693b9e5 100644 --- a/plugins/azure/keyvaults/keyVaultSecretExpiry.spec.js +++ b/plugins/azure/keyvaults/keyVaultSecretExpiry.spec.js @@ -21,9 +21,8 @@ const listKeyVaults = [ "family": "A", "name": "Standard" }, - "properties": { - "enableRbacAuthorization": true - } + "enableRbacAuthorization": true, + }, { "id": "/subscriptions/abcdef123-ebf6-437f-a3b0-28fc0d22117e/resourceGroups/Default-ActivityLogAlerts/providers/Microsoft.KeyVault/vaults/testvault", @@ -35,9 +34,7 @@ const listKeyVaults = [ "family": "A", "name": "Standard" }, - "properties": { - "enableRbacAuthorization": false - } + "enableRbacAuthorization": false, } ]; diff --git a/plugins/azure/keyvaults/keyVaultSecretExpiryNonRbac.js b/plugins/azure/keyvaults/keyVaultSecretExpiryNonRbac.js index 258fc67ed9..e2f8a15fd1 100644 --- a/plugins/azure/keyvaults/keyVaultSecretExpiryNonRbac.js +++ b/plugins/azure/keyvaults/keyVaultSecretExpiryNonRbac.js @@ -18,7 +18,7 @@ module.exports = { regex: '^[1-9]{1}[0-9]{0,3}$', default: '30' } - }, + }, realtime_triggers: ['microsoftkeyvault:vaults:write', 'microsoftkeyvault:vaults:delete'], run: function(cache, settings, callback) { @@ -47,7 +47,7 @@ module.exports = { vaults.data.forEach(function(vault) { // Check if vault is non-RBAC - if (vault.properties && vault.properties.enableRbacAuthorization) { + if (vault.enableRbacAuthorization) { return; } diff --git a/plugins/azure/kubernetesservice/aksNetworkExposure.js b/plugins/azure/kubernetesservice/aksNetworkExposure.js index 012fcdb677..0e3a58a472 100644 --- a/plugins/azure/kubernetesservice/aksNetworkExposure.js +++ b/plugins/azure/kubernetesservice/aksNetworkExposure.js @@ -83,7 +83,7 @@ module.exports = { } }); securityGroups = networkSecurityGroups.data.filter(nsg => securityGroupIDs.includes(nsg.id)); - internetExposed = helpers.checkNetworkExposure(cache, source, [], securityGroups, location, results, []); + internetExposed = helpers.checkNetworkExposure(cache, source, [], securityGroups, location, results, {}, cluster); } } } diff --git a/plugins/azure/virtualmachines/vmNetworkExposure.js b/plugins/azure/virtualmachines/vmNetworkExposure.js index ccb42e09e1..c48767d4f0 100644 --- a/plugins/azure/virtualmachines/vmNetworkExposure.js +++ b/plugins/azure/virtualmachines/vmNetworkExposure.js @@ -118,7 +118,7 @@ module.exports = { } } } - let internetExposed = helpers.checkNetworkExposure(cache, source, vm_interfaces, securityGroups, location, results, loadBalancers); + let internetExposed = helpers.checkNetworkExposure(cache, source, vm_interfaces, securityGroups, location, results, {lbNames: loadBalancers}, virtualMachine); if (internetExposed && internetExposed.length) { helpers.addResult(results, 2, `VM is exposed to the internet through ${internetExposed}`, location, virtualMachine.id); } else { diff --git a/plugins/google/cloudfunctions/cloudFunctionNetworkExposure.js b/plugins/google/cloudfunctions/cloudFunctionNetworkExposure.js new file mode 100644 index 0000000000..2745d9cc91 --- /dev/null +++ b/plugins/google/cloudfunctions/cloudFunctionNetworkExposure.js @@ -0,0 +1,159 @@ +var async = require('async'); +var helpers = require('../../../helpers/google'); + +module.exports = { + title: 'Network Exposure', + category: 'Cloud Functions', + domain: 'Serverless', + severity: 'Info', + description: 'Ensures Cloud Functions are not publicly exposed to all inbound traffic.', + more_info: 'Cloud Functions should be properly secured using ingress settings and load balancer configurations to control which sources can invoke the function.', + link: 'https://cloud.google.com/functions/docs/networking/network-settings', + recommended_action: 'Modify the Cloud Function to restrict ingress settings and ensure load balancer and api gateway configurations are properly secured.', + apis: ['functions:list', 'urlMaps:list', 'targetHttpProxies:list', 'targetHttpsProxies:list', + 'forwardingRules:list', 'backendServices:list', 'apiGateways:list', 'api:list', 'apiConfigs:list', 'apiGateways:getIamPolicy'], + realtime_triggers: ['functions.CloudFunctionsService.UpdateFunction', 'functions.CloudFunctionsService.CreateFunction', 'functions.CloudFunctionsService.DeleteFunction', + 'compute.backendServices.insert', 'compute.backendServices.delete', 'compute.backendServices.patch', 'compute.instanceGroups.removeInstances', 'compute.urlMaps.insert', 'compute.urlMaps.delete', 'compute.urlMaps.update', 'compute.urlMaps.patch', + 'compute.targetHttpProxies.insert', 'compute.targetHttpProxies.delete', 'compute.targetHttpProxies.patch', 'compute.targetHttpsProxies.insert', 'compute.targetHttpsProxies.delete', 'compute.targetHttpsProxies.patch', + 'compute.forwardingRules.insert', 'compute.forwardingRules.delete', 'compute.forwardingRules.patch', 'apigateway.gateways.create', 'apigateway.gateways.update', 'apigateway.gateways.delete' + ], + + 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); + } + + let apiGateways = [], apis = [], apiConfigs = []; + for (let region of regions.apiGateways) { + var gateways = helpers.addSource(cache, source, + ['apiGateways', 'list', region]); + + if (gateways && !gateways.err && gateways.data && gateways.data.length) { + apiGateways = apiGateways.concat(gateways.data); + } + + + var apiList = helpers.addSource(cache, source, + ['api', 'list', region]); + + if (apiList && !apiList.err && apiList.data && apiList.data.length) { + apis = apis.concat(apiList.data); + } + + var configs = helpers.addSource(cache, source, + ['apiConfigs', 'list', region]); + + if (configs && !configs.err && configs.data && configs.data.length) { + apiConfigs = apiConfigs.concat(configs.data); + } + } + + async.each(regions.functions, (region, rcb) => { + var functions = helpers.addSource(cache, source, + ['functions', 'list', region]); + + if (!functions) return rcb(); + + if (functions.err || !functions.data) { + helpers.addResult(results, 3, + 'Unable to query for Google Cloud Functions: ' + helpers.addError(functions), region, null, null, functions.err); + return rcb(); + } + + if (!functions.data.length) { + helpers.addResult(results, 0, 'No Google Cloud functions found', region); + return rcb(); + } + + functions.data.forEach(func => { + if (!func.name) return; + let internetExposed = ''; + if (func.ingressSettings && func.ingressSettings.toUpperCase() == 'ALLOW_ALL') { + internetExposed = 'public access'; + } else if (func.ingressSettings && func.ingressSettings.toUpperCase() == 'ALLOW_INTERNAL_AND_GCLB') { + // only check load balancer flow if it allows traffic from LBs + let forwardingRules = []; + forwardingRules = helpers.getForwardingRules(cache, source, region, func); + let firewallRules = []; + let networks = []; + internetExposed = helpers.checkNetworkExposure(cache, source, networks, firewallRules, region, results, forwardingRules); + + if (!internetExposed || !internetExposed.length) { + const gatewayPolicies = helpers.addSource(cache, source, + ['apiGateways', 'getIamPolicy', region]); + + if (apiGateways && apiGateways.length && apiConfigs && apiConfigs.length) { + apiGateways.forEach(gateway => { + let isGatewayExposed = false; + if (!gateway.apiConfig || !gateway.defaultHostname) return; + + const apiConfig = apiConfigs.find(config => + gateway.apiConfig.includes(config.name)); + + if (!apiConfig) return; + + if (apiConfig.openapiDocuments) { + const specs = apiConfig.openapiDocuments.map(doc => + typeof doc === 'string' ? JSON.parse(doc) : doc); + + const hasFunctionReference = specs.some(spec => + JSON.stringify(spec).includes(func.httpsTrigger.url) || + JSON.stringify(spec).includes(func.name) + ); + + if (!hasFunctionReference) return; + + const gatewayPolicy = gatewayPolicies.data.find(policy => + policy.parent && policy.parent.name === gateway.name); + + if (gatewayPolicy && gatewayPolicy.bindings) { + const publicAccess = gatewayPolicy.bindings.some(binding => + binding.members.includes('allUsers') || + binding.members.includes('allAuthenticatedUsers')); + if (publicAccess) { + isGatewayExposed = true; + } + } + + if (!apiConfig.securityDefinitions || !Object.keys(apiConfig.securityDefinitions).length || + !apiConfig.security || !apiConfig.security.length) { + isGatewayExposed = true; + } + + + if (isGatewayExposed) { + internetExposed += internetExposed.length ? `, ag ${gateway.displayName}` : `ag ${gateway.displayName}`; + } + } + }); + } + } + + } + + if (internetExposed && internetExposed.length) { + helpers.addResult(results, 2, `Cloud function is exposed to the internet through ${internetExposed}`, region, func.name); + } else { + helpers.addResult(results, 0, 'Cloud function is not exposed to the internet', region, func.name); + } + + + }); + + rcb(); + }, function() { + callback(null, results, source); + }); + + } +}; + diff --git a/plugins/google/kubernetes/clusterNetworkExposure.js b/plugins/google/kubernetes/clusterNetworkExposure.js index 44540c8890..b805618039 100644 --- a/plugins/google/kubernetes/clusterNetworkExposure.js +++ b/plugins/google/kubernetes/clusterNetworkExposure.js @@ -30,19 +30,18 @@ module.exports = { var project = projects.data[0].name; - async.each(regions.kubernetes, function(region, rcb){ - - let firewalls = helpers.addSource( - cache, source, ['firewalls', 'list', 'global']); - - if (!firewalls || firewalls.err || !firewalls.data) { - helpers.addResult(results, 3, 'Unable to query firewall rules', region, null, null, firewalls.err); - } - - if (!firewalls.data.length) { - helpers.addResult(results, 0, 'No firewall rules found', region); - } + let firewalls = helpers.addSource( + cache, source, ['firewalls', 'list', 'global']); + + if (!firewalls || firewalls.err || !firewalls.data) { + helpers.addResult(results, 3, 'Unable to query firewall rules', 'global', null, null, firewalls.err); + } + + if (!firewalls.data.length) { + helpers.addResult(results, 0, 'No firewall rules found', 'global'); + } + async.each(regions.kubernetes, function(region, rcb){ let clusters = helpers.addSource(cache, source, ['kubernetes', 'list', region]); diff --git a/postprocess/suppress.js b/postprocess/suppress.js index bccea55237..4b02d2e402 100644 --- a/postprocess/suppress.js +++ b/postprocess/suppress.js @@ -7,11 +7,52 @@ module.exports = { // of the items can be * to indicate match all. if (!suppressions) suppressions = []; + // Validate suppression format + const validateSuppression = (expr) => { + // Check basic format (three parts separated by colons) + const parts = expr.split(':'); + if (parts.length !== 3) { + throw new Error(`Invalid suppression format: ${expr}. Expected format: pluginId:region:resourceId`); + } + + const pluginPattern = /^[A-Za-z0-9]{1,255}$/; // eslint-disable-line + const regionPattern = /^[A-Za-z0-9\-_]{1,255}$/; // eslint-disable-line + const resourcePattern = /^[ A-Za-z0-9._~()'!*:@,;+?#$%^&={}\\[\]\\|\"/-]{1,255}$/; // eslint-disable-line + const [pluginId, region, resourceId] = parts; + + // Validate pluginId + if (!pluginPattern.test(pluginId)) { + throw new Error(`Invalid pluginId in suppression: ${pluginId}. Must only contain letters and numbers and be between 1-255 characters.`); + } + + // Validate region + if (!regionPattern.test(region)) { + throw new Error(`Invalid region in suppression: ${region}. Must only contain letters, numbers, hyphen (-), and underscore (_) and be between 1-255 characters.`); + } + + // Validate resourceId with specific pattern + if (!resourcePattern.test(resourceId)) { + throw new Error(`Invalid resourceId in suppression: ${resourceId}. Must match allowed pattern and be between 1-255 characters.`); + } + + return true; + }; + + // Validate and create expressions var expressions = suppressions .map(function(expr) { + // Validate the expression format + validateSuppression(expr); + + // Escape special regex characters except * which we handle specially + const escapedExpr = expr + .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape special regex chars + .split('*') + .join('.*'); // Replace * with .* + return [ expr, - new RegExp('^' + expr.split('*').join('.*') + '$') + new RegExp('^' + escapedExpr + '$') ]; }); diff --git a/postprocess/suppress.spec.js b/postprocess/suppress.spec.js index fad473faea..9d57b86355 100644 --- a/postprocess/suppress.spec.js +++ b/postprocess/suppress.spec.js @@ -4,26 +4,24 @@ var suppress = require('./suppress'); describe('create', function () { it('should return undefined when no filter specified', function () { var filter = suppress.create([]); - expect(filter('any')).to.be.undefined; }); it('should return the filter if matches', function () { - var filter = suppress.create(['*n*']); - - expect(filter('any')).to.equal('*n*'); + var filter = suppress.create(['plugin123:us-east-1:n*']); + expect(filter('plugin123:us-east-1:name')).to.equal('plugin123:us-east-1:n*'); }); it('should return the filter if matches whole word', function () { - var filter = suppress.create(['*longer*']); - - expect(filter('longer')).to.equal('*longer*'); + var filter = suppress.create(['plugin123:us-east-1:longer']); + expect(filter('plugin123:us-east-1:longer')).to.equal('plugin123:us-east-1:longer'); }); it('should return the filter if multiple and second matches', function () { - var filter = suppress.create(['*first*', - 'second']); - - expect(filter('second')).to.equal('second'); + var filter = suppress.create([ + 'plugin123:us-east-1:first*', + 'plugin456:us-west-2:second' + ]); + expect(filter('plugin456:us-west-2:second')).to.equal('plugin456:us-west-2:second'); }); });