From 03a35f2a609c986284d8fae30dd8871990858284 Mon Sep 17 00:00:00 2001 From: gioroddev Date: Mon, 2 Dec 2024 13:19:12 -0500 Subject: [PATCH 1/8] Enhance network exposure checks in AWS functions - Added checks for function URL exposure and API Gateway exposure in the `checkNetworkExposure` function. - Implemented logic to identify public function URLs and API Gateway endpoints based on their configurations. --- helpers/aws/functions.js | 37 +++++++++- plugins/aws/lambda/lambdaNetworkExposure.js | 75 +++++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 plugins/aws/lambda/lambdaNetworkExposure.js diff --git a/helpers/aws/functions.js b/helpers/aws/functions.js index 4791e488da..e6f26551d6 100644 --- a/helpers/aws/functions.js +++ b/helpers/aws/functions.js @@ -1240,14 +1240,49 @@ var getAttachedELBs = function(cache, source, region, resourceId, lbField, lbAt }; var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs, region, results, resource) { - var internetExposed = ''; var isSubnetPrivate = false; + if (resource && (resource.functionPolicy || resource.functionUrlConfig)) { + // Check Function URL exposure + if (resource.functionUrlConfig && resource.functionUrlConfig.data && + resource.functionUrlConfig.data.AuthType === 'NONE') { + internetExposed += 'public function URL'; + } + + // Check API Gateway exposure + if (resource.functionPolicy && resource.functionPolicy.data && + resource.functionPolicy.data.Policy) { + let statements = helpers.normalizePolicyDocument(resource.functionPolicy.data.Policy); + + for (let statement of statements) { + if (statement.Principal && statement.Principal.Service === 'apigateway.amazonaws.com') { + let getRestApis = helpers.addSource(cache, source, + ['apigateway', 'getRestApis', region]); + + if (getRestApis && getRestApis.data && getRestApis.data.items) { + let apiId = getApiIdFromArn(statement.SourceArn); + let api = getRestApis.data.items.find(a => a.id === apiId); + + if (api && api.endpointConfiguration && + (api.endpointConfiguration.types.includes('EDGE') || + api.endpointConfiguration.types.includes('REGIONAL'))) { + internetExposed += internetExposed.length ? + `, API Gateway ${api.name}` : `API Gateway ${api.name}`; + } + } + } + } + } + + return internetExposed; + } + // Check public endpoint access for specific resources like EKS if (resource && resource.resourcesVpcConfig && resource.resourcesVpcConfig.endpointPublicAccess) { return 'public endpoint access'; } + // Scenario 1: check if resource is in a private subnet let subnetRouteTableMap, privateSubnets; var describeSubnets = helpers.addSource(cache, source, diff --git a/plugins/aws/lambda/lambdaNetworkExposure.js b/plugins/aws/lambda/lambdaNetworkExposure.js new file mode 100644 index 0000000000..2d041133ea --- /dev/null +++ b/plugins/aws/lambda/lambdaNetworkExposure.js @@ -0,0 +1,75 @@ +var async = require('async'); +var helpers = require('../../../helpers/aws'); + +module.exports = { + title: 'Network Exposure', + category: 'Lambda', + domain: 'Serverless', + severity: 'Info', + description: 'Check if Lambda functions are exposed to the internet.', + 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'], + realtime_triggers: ['lambda:CreateFunctionUrlConfig', 'lambda:UpdateFunctionUrlConfig', 'lambda:DeleteFunctionUrlConfig', + 'lambda:AddPermission', 'lambda:RemovePermission', + 'apigateway:CreateRestApi', 'apigateway:DeleteRestApi', 'apigateway:UpdateRestApi', + 'apigateway:CreateStage', 'apigateway:DeleteStage', 'apigateway:UpdateStage', + 'apigateway:PutIntegration', 'apigateway:DeleteIntegration'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var regions = helpers.regions(settings); + + async.each(regions.lambda, function(region, rcb) { + var listFunctions = helpers.addSource(cache, source, + ['lambda', 'listFunctions', region]); + + if (!listFunctions) return rcb(); + + if (listFunctions.err || !listFunctions.data) { + helpers.addResult(results, 3, + 'Unable to query for Lambda functions: ' + helpers.addError(listFunctions), region); + return rcb(); + } + + if (!listFunctions.data.length) { + helpers.addResult(results, 0, 'No Lambda functions found', region); + return rcb(); + } + + for (var lambda of listFunctions.data) { + if (!lambda.FunctionArn) continue; + + // 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]); + + let lambda = { + functionUrlConfig: getFunctionUrlConfig, + functionPolicy: getPolicy + } + + let internetExposed = helpers.checkNetworkExposure(cache, source, [], [], [], region, results, lambda); + + if (internetExposed && internetExposed.length) { + helpers.addResult(results, 2, + `Lambda function is exposed to the internet through: ${internetExposed}`, + region, lambda.FunctionArn); + } else { + helpers.addResult(results, 0, + 'Lambda function is not exposed to the internet', + region, lambda.FunctionArn); + } + } + + rcb(); + }, function() { + callback(null, results, source); + }); + } +}; \ No newline at end of file From deb7fe4dd9f305eeaef11dc54b7bce0fbe1679a8 Mon Sep 17 00:00:00 2001 From: gioroddev Date: Mon, 2 Dec 2024 13:22:56 -0500 Subject: [PATCH 2/8] updating logic --- helpers/aws/functions.js | 5 +++++ plugins/aws/lambda/lambdaNetworkExposure.js | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/helpers/aws/functions.js b/helpers/aws/functions.js index e6f26551d6..c46b46eac4 100644 --- a/helpers/aws/functions.js +++ b/helpers/aws/functions.js @@ -1239,6 +1239,11 @@ var getAttachedELBs = function(cache, source, region, resourceId, lbField, lbAt return elbs; }; +var getApiIdFromArn = function(arn) { + if (!arn) return null; + const matches = arn.match(/arn:aws:execute-api:[^:]+:[^:]+:([^/]+)/); + return matches ? matches[1] : null; +} var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs, region, results, resource) { var internetExposed = ''; var isSubnetPrivate = false; diff --git a/plugins/aws/lambda/lambdaNetworkExposure.js b/plugins/aws/lambda/lambdaNetworkExposure.js index 2d041133ea..7d2dfe4b23 100644 --- a/plugins/aws/lambda/lambdaNetworkExposure.js +++ b/plugins/aws/lambda/lambdaNetworkExposure.js @@ -49,12 +49,12 @@ module.exports = { var getPolicy = helpers.addSource(cache, source, ['lambda', 'getPolicy', region, lambda.FunctionName]); - let lambda = { + let lambdaResource = { functionUrlConfig: getFunctionUrlConfig, functionPolicy: getPolicy } - let internetExposed = helpers.checkNetworkExposure(cache, source, [], [], [], region, results, lambda); + let internetExposed = helpers.checkNetworkExposure(cache, source, [], [], [], region, results, lambdaResource); if (internetExposed && internetExposed.length) { helpers.addResult(results, 2, From c6e59a2d5c3067895a4e5ffa642193a849aa5222 Mon Sep 17 00:00:00 2001 From: gioroddev Date: Mon, 2 Dec 2024 13:25:32 -0500 Subject: [PATCH 3/8] fixing lint --- helpers/aws/functions.js | 3 ++- plugins/aws/lambda/lambdaNetworkExposure.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/helpers/aws/functions.js b/helpers/aws/functions.js index c46b46eac4..69209ea991 100644 --- a/helpers/aws/functions.js +++ b/helpers/aws/functions.js @@ -1243,7 +1243,8 @@ var getApiIdFromArn = function(arn) { if (!arn) return null; const matches = arn.match(/arn:aws:execute-api:[^:]+:[^:]+:([^/]+)/); return matches ? matches[1] : null; -} +}; + var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs, region, results, resource) { var internetExposed = ''; var isSubnetPrivate = false; diff --git a/plugins/aws/lambda/lambdaNetworkExposure.js b/plugins/aws/lambda/lambdaNetworkExposure.js index 7d2dfe4b23..850330bffd 100644 --- a/plugins/aws/lambda/lambdaNetworkExposure.js +++ b/plugins/aws/lambda/lambdaNetworkExposure.js @@ -52,7 +52,7 @@ module.exports = { let lambdaResource = { functionUrlConfig: getFunctionUrlConfig, functionPolicy: getPolicy - } + }; let internetExposed = helpers.checkNetworkExposure(cache, source, [], [], [], region, results, lambdaResource); From d8de7582ddd4b09b23e41a781ec29a0a75014c23 Mon Sep 17 00:00:00 2001 From: gioroddev Date: Mon, 2 Dec 2024 13:46:25 -0500 Subject: [PATCH 4/8] fixing failed tests --- plugins/azure/keyvaults/keyVaultKeyExpiryNonRbac.spec.js | 2 +- plugins/azure/keyvaults/keyVaultSecretExpiryNonRbac.spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/azure/keyvaults/keyVaultKeyExpiryNonRbac.spec.js b/plugins/azure/keyvaults/keyVaultKeyExpiryNonRbac.spec.js index d209c8b1a9..1c5431cb1f 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.setMonth(keyExpiryFail.getMonth() + 1); +keyExpiryFail.setDate(keyExpiryFail.getDate() + 25); // Set to 35 days in the future var keyExpired = new Date(); keyExpired.setMonth(keyExpired.getMonth() - 1); diff --git a/plugins/azure/keyvaults/keyVaultSecretExpiryNonRbac.spec.js b/plugins/azure/keyvaults/keyVaultSecretExpiryNonRbac.spec.js index fd7b70bfd3..2a64720d2e 100644 --- a/plugins/azure/keyvaults/keyVaultSecretExpiryNonRbac.spec.js +++ b/plugins/azure/keyvaults/keyVaultSecretExpiryNonRbac.spec.js @@ -5,7 +5,7 @@ var secretExpiryPass = new Date(); secretExpiryPass.setMonth(secretExpiryPass.getMonth() + 2); var secretExpiryFail = new Date(); -secretExpiryFail.setMonth(secretExpiryFail.getMonth() + 1); +secretExpiryFail.setDate(secretExpiryFail.getDate() + 25); // Set to 35 days in the future var secretExpired = new Date(); secretExpired.setMonth(secretExpired.getMonth() - 1); From 2b5681fc990c35ec9c37d2da73475e4c3a00c0e4 Mon Sep 17 00:00:00 2001 From: gioroddev Date: Mon, 2 Dec 2024 19:34:16 -0500 Subject: [PATCH 5/8] fixing logic --- helpers/aws/functions.js | 52 +++++++++++++-------- plugins/aws/lambda/lambdaNetworkExposure.js | 6 ++- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/helpers/aws/functions.js b/helpers/aws/functions.js index 69209ea991..f1a08ca5ca 100644 --- a/helpers/aws/functions.js +++ b/helpers/aws/functions.js @@ -1257,27 +1257,39 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs } // Check API Gateway exposure - if (resource.functionPolicy && resource.functionPolicy.data && - resource.functionPolicy.data.Policy) { - let statements = helpers.normalizePolicyDocument(resource.functionPolicy.data.Policy); - - for (let statement of statements) { - if (statement.Principal && statement.Principal.Service === 'apigateway.amazonaws.com') { - let getRestApis = helpers.addSource(cache, source, - ['apigateway', 'getRestApis', region]); - - if (getRestApis && getRestApis.data && getRestApis.data.items) { - let apiId = getApiIdFromArn(statement.SourceArn); - let api = getRestApis.data.items.find(a => a.id === apiId); - - if (api && api.endpointConfiguration && - (api.endpointConfiguration.types.includes('EDGE') || - api.endpointConfiguration.types.includes('REGIONAL'))) { - internetExposed += internetExposed.length ? - `, API Gateway ${api.name}` : `API Gateway ${api.name}`; - } + let getRestApis = helpers.addSource(cache, source, + ['apigateway', 'getRestApis', region]); + + if (getRestApis && getRestApis.data) { + for (let api of getRestApis.data) { + + if (!api.id || !api.name) continue; + + // Get stages to check if API is deployed + let getStages = helpers.addSource(cache, source, + ['apigateway', 'getStages', region, api.id]); + + // Only include if API has at least one stage deployed + if (!getStages || getStages.err || !getStages.data || !getStages.data.item || !getStages.data.item.length) continue; + + // Get integrations for this API + let getIntegration = helpers.addSource(cache, source, + ['apigateway', 'getIntegration', region, api.id]); + + if (!getIntegration || getIntegration.err || !Object.keys(getIntegration).length) continue; + + for (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 && + integration.data.uri.includes(resource.functionArn); + }); + + if (lambdaIntegrations.length) { + internetExposed += internetExposed.length ? `, API Gateway ${api.name}` : `API Gateway ${api.name}`; } - } + } } } diff --git a/plugins/aws/lambda/lambdaNetworkExposure.js b/plugins/aws/lambda/lambdaNetworkExposure.js index 850330bffd..d1aad36cde 100644 --- a/plugins/aws/lambda/lambdaNetworkExposure.js +++ b/plugins/aws/lambda/lambdaNetworkExposure.js @@ -10,7 +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'], + apis: ['Lambda:listFunctions', 'Lambda:getFunctionUrlConfig', 'Lambda:getPolicy', + 'APIGateway:getRestApis','APIGateway:getResources', 'APIGateway:getStages', 'APIGateway:getIntegration'], realtime_triggers: ['lambda:CreateFunctionUrlConfig', 'lambda:UpdateFunctionUrlConfig', 'lambda:DeleteFunctionUrlConfig', 'lambda:AddPermission', 'lambda:RemovePermission', 'apigateway:CreateRestApi', 'apigateway:DeleteRestApi', 'apigateway:UpdateRestApi', @@ -51,7 +52,8 @@ module.exports = { let lambdaResource = { functionUrlConfig: getFunctionUrlConfig, - functionPolicy: getPolicy + functionPolicy: getPolicy, + functionArn: lambda.FunctionArn }; let internetExposed = helpers.checkNetworkExposure(cache, source, [], [], [], region, results, lambdaResource); From 8a4726b804d7ba8589d5f1fd4a2c4ea16c47a8cd Mon Sep 17 00:00:00 2001 From: gioroddev Date: Mon, 2 Dec 2024 19:36:40 -0500 Subject: [PATCH 6/8] lint --- helpers/aws/functions.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/helpers/aws/functions.js b/helpers/aws/functions.js index f1a08ca5ca..5373602360 100644 --- a/helpers/aws/functions.js +++ b/helpers/aws/functions.js @@ -1239,11 +1239,6 @@ var getAttachedELBs = function(cache, source, region, resourceId, lbField, lbAt return elbs; }; -var getApiIdFromArn = function(arn) { - if (!arn) return null; - const matches = arn.match(/arn:aws:execute-api:[^:]+:[^:]+:([^/]+)/); - return matches ? matches[1] : null; -}; var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs, region, results, resource) { var internetExposed = ''; @@ -1278,7 +1273,7 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs if (!getIntegration || getIntegration.err || !Object.keys(getIntegration).length) continue; - for (apiResource of Object.values(getIntegration)) { + 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') && From e0c0c333cec577c8ddfc4c6c582cc6b3f2d7e971 Mon Sep 17 00:00:00 2001 From: gioroddev Date: Mon, 2 Dec 2024 21:30:58 -0500 Subject: [PATCH 7/8] excluding function URL from vpc checks and adding load balancer and more checks for iam policy conditions --- helpers/aws/functions.js | 245 ++++++++++++++++---- plugins/aws/lambda/lambdaNetworkExposure.js | 11 +- 2 files changed, 204 insertions(+), 52 deletions(-) diff --git a/helpers/aws/functions.js b/helpers/aws/functions.js index 5373602360..e84fb4522e 100644 --- a/helpers/aws/functions.js +++ b/helpers/aws/functions.js @@ -1244,11 +1244,92 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs var internetExposed = ''; var isSubnetPrivate = false; - if (resource && (resource.functionPolicy || resource.functionUrlConfig)) { + if (resource && resource.functionArn) { // Check Function URL exposure - if (resource.functionUrlConfig && resource.functionUrlConfig.data && - resource.functionUrlConfig.data.AuthType === 'NONE') { - internetExposed += 'public function URL'; + 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) { + 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) && + globalPrincipal(statement.Principal)) { + hasDenyAll = true; + break; + } + + // Check for deny with IP restrictions + 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) ? + !statement.Action.includes('lambda:InvokeFunctionUrl') : + !statement.Action.some(action => + action === '*' || + action === 'lambda:*' || + action === 'lambda:InvokeFunctionUrl' + ))) { + continue; + } + + // Check for * principal with no conditions + if (globalPrincipal(statement.Principal)) { + if (!statement.Condition || Object.keys(statement.Condition).length === 0) { + hasPublicAllow = true; + } else { + // Check for common restrictive conditions + const restrictiveConditions = [ + 'aws:SourceIp', + 'aws:SourceVpc', + 'aws:SourceVpce', + 'aws:PrincipalOrgID', + 'aws:PrincipalArn', + 'aws:SourceAccount' + ]; + + 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') { + hasPublicAllow = true; + } + } + } + } + } + + // 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' : + 'function URL with global IAM access'; + } + } + } + } } // Check API Gateway exposure @@ -1257,7 +1338,6 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs if (getRestApis && getRestApis.data) { for (let api of getRestApis.data) { - if (!api.id || !api.name) continue; // Get stages to check if API is deployed @@ -1287,8 +1367,6 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs } } } - - return internetExposed; } // Check public endpoint access for specific resources like EKS @@ -1296,39 +1374,39 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs return 'public endpoint access'; } + if (!resource.functionArn) { // 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]); + 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 && subnets.length) { - subnetRouteTableMap = getSubnetRTMap(describeSubnets.data, describeRouteTables.data); - privateSubnets = getPrivateSubnets(subnetRouteTableMap, describeSubnets.data, describeRouteTables.data); - if (privateSubnets && privateSubnets.length) { - isSubnetPrivate = !subnets.some(subnet => !privateSubnets.includes(subnet.id)); - } + 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 && subnets.length) { + subnetRouteTableMap = getSubnetRTMap(describeSubnets.data, describeRouteTables.data); + privateSubnets = getPrivateSubnets(subnetRouteTableMap, describeSubnets.data, describeRouteTables.data); + if (privateSubnets && privateSubnets.length) { + isSubnetPrivate = !subnets.some(subnet => !privateSubnets.includes(subnet.id)); + } - // if it's in a private subnet and has no ELBs attached then its not exposed - if (isSubnetPrivate && (!elbs || !elbs.length)) { - return ''; + // if it's in a private subnet and has no ELBs attached then its not exposed + if (isSubnetPrivate && (!elbs || !elbs.length) && !resource.functionArn) { + 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 (!isSubnetPrivate) { + var describeSecurityGroups; + if (!isSubnetPrivate && !resource.functionArn) { + 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); @@ -1342,9 +1420,8 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs } } - // if security group allows all traffic we need to check NACLs - if (internetExposed.length) { + if (internetExposed.length && !resource.functionArn) { let subnetIds = subnets.map(s => s.id); // Scenario 3: check if Network ACLs associated with the resource allow all traffic var describeNetworkAcls = helpers.addSource(cache, source, @@ -1398,7 +1475,7 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs }); // exposed - if NACL has an allow all rule - if (exposed) { + if (exposed && !resource.functionArn) { internetExposed += `, nacl ${instanceACL.NetworkAclId}`; } @@ -1412,29 +1489,34 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs } // not exposed - if all NACLs have deny rules - if (naclDeny) { + if (naclDeny && !resource.functionArn) { return ''; } } - } } // if there are no explicit allow or deny rules, we look at ELBs - - if (elbs && elbs.length) { - for (const lb of elbs) { + if (!describeSecurityGroups || !describeSecurityGroups.data) { + describeSecurityGroups = helpers.addSource(cache, source, + ['ec2', 'describeSecurityGroups', region]); + } + elbs.forEach(lb => { let isLBPublic = false; if (lb.Scheme && lb.Scheme.toLowerCase() === 'internet-facing') { - if (lb.SecurityGroups && lb.SecurityGroups.length && describeSecurityGroups && - !describeSecurityGroups.err && describeSecurityGroups.data && describeSecurityGroups.data.length) { - let elbSGs = describeSecurityGroups.data.filter(sg => lb.SecurityGroups.includes(sg.GroupId)); - for (var elbSG of elbSGs) { - let exposedSG = checkSecurityGroup(elbSG, cache, region, false); - if (exposedSG) { - isLBPublic = true; + 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)); + for (var elbSG of elbSGs) { + let exposedSG = checkSecurityGroup(elbSG, cache, region, false); + if (exposedSG) { + isLBPublic = true; + } } } } @@ -1443,12 +1525,74 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs if (isLBPublic) { internetExposed += internetExposed.length ? `, elb ${lb.LoadBalancerName}`: `elb ${lb.LoadBalancerName}`; } - } + }); } return internetExposed; }; +let getLambdaTargetELBs = function(cache, source, region) { + let lambdaELBMap = {}; + + var describeLoadBalancersv2 = helpers.addSource(cache, source, + ['elbv2', 'describeLoadBalancers', region]); + + if (!describeLoadBalancersv2 || describeLoadBalancersv2.err || !describeLoadBalancersv2.data) { + return lambdaELBMap; + } + + describeLoadBalancersv2.data.forEach(lb => { + var describeTargetGroups = helpers.addSource(cache, source, + ['elbv2', 'describeTargetGroups', region, lb.DNSName]); + + 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 || + !describeTargetHealth.data.TargetHealthDescriptions) return; + + describeTargetHealth.data.TargetHealthDescriptions.forEach(target => { + if (target.Target && target.Target.Id && + target.Target.Id.startsWith('arn:aws:lambda')) { + if (!lambdaELBMap[target.Target.Id]) { + lambdaELBMap[target.Target.Id] = []; + } + lb.targetGroups = lb.targetGroups || []; + lb.targetGroups.push({ + targetGroupName: tg.TargetGroupName, + 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 && + describeListeners.data.Listeners) { + hasListener = describeListeners.data.Listeners.some(listener => + listener.DefaultActions.some(action => + action.TargetGroupArn === tg.TargetGroupArn + ) + ); + } + + if (hasListener) { + lambdaELBMap[target.Target.Id].push(lb); + } + } + }); + }); + }); + + return lambdaELBMap; +} + module.exports = { addResult: addResult, findOpenPorts: findOpenPorts, @@ -1487,6 +1631,7 @@ module.exports = { processFieldSelectors: processFieldSelectors, checkNetworkInterface: checkNetworkInterface, checkNetworkExposure: checkNetworkExposure, - getAttachedELBs: getAttachedELBs + getAttachedELBs: getAttachedELBs, + getLambdaTargetELBs }; diff --git a/plugins/aws/lambda/lambdaNetworkExposure.js b/plugins/aws/lambda/lambdaNetworkExposure.js index d1aad36cde..5760ba2e37 100644 --- a/plugins/aws/lambda/lambdaNetworkExposure.js +++ b/plugins/aws/lambda/lambdaNetworkExposure.js @@ -11,7 +11,8 @@ module.exports = { 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'], + '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', 'apigateway:CreateRestApi', 'apigateway:DeleteRestApi', 'apigateway:UpdateRestApi', @@ -40,6 +41,8 @@ module.exports = { return rcb(); } + let lambdaELBMap = helpers.getLambdaTargetELBs(cache, source, region); + for (var lambda of listFunctions.data) { if (!lambda.FunctionArn) continue; @@ -50,13 +53,17 @@ module.exports = { var getPolicy = helpers.addSource(cache, source, ['lambda', 'getPolicy', region, lambda.FunctionName]); + let elbs = helpers.getAttachedELBs(cache, source, region, lambda.FunctionArn); + let lambdaResource = { functionUrlConfig: getFunctionUrlConfig, functionPolicy: getPolicy, functionArn: lambda.FunctionArn }; - let internetExposed = helpers.checkNetworkExposure(cache, source, [], [], [], region, results, lambdaResource); + let targetingELBs = lambdaELBMap[lambda.FunctionArn] || []; + + let internetExposed = helpers.checkNetworkExposure(cache, source, [], [], targetingELBs, region, results, lambdaResource); if (internetExposed && internetExposed.length) { helpers.addResult(results, 2, From dadb9c31b40d817df34e2b0b41ef2f52dc64ef82 Mon Sep 17 00:00:00 2001 From: gioroddev Date: Mon, 2 Dec 2024 21:33:34 -0500 Subject: [PATCH 8/8] lint --- helpers/aws/functions.js | 2 +- plugins/aws/lambda/lambdaNetworkExposure.js | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/helpers/aws/functions.js b/helpers/aws/functions.js index e84fb4522e..cbbdce4f0c 100644 --- a/helpers/aws/functions.js +++ b/helpers/aws/functions.js @@ -1591,7 +1591,7 @@ let getLambdaTargetELBs = function(cache, source, region) { }); return lambdaELBMap; -} +}; module.exports = { addResult: addResult, diff --git a/plugins/aws/lambda/lambdaNetworkExposure.js b/plugins/aws/lambda/lambdaNetworkExposure.js index 5760ba2e37..d552ca1723 100644 --- a/plugins/aws/lambda/lambdaNetworkExposure.js +++ b/plugins/aws/lambda/lambdaNetworkExposure.js @@ -12,7 +12,7 @@ module.exports = { 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', - 'ELBv2:describeTargetHealth', 'ELBv2:describeListeners', 'EC2:describeSecurityGroups'], + 'ELBv2:describeTargetHealth', 'ELBv2:describeListeners', 'EC2:describeSecurityGroups'], realtime_triggers: ['lambda:CreateFunctionUrlConfig', 'lambda:UpdateFunctionUrlConfig', 'lambda:DeleteFunctionUrlConfig', 'lambda:AddPermission', 'lambda:RemovePermission', 'apigateway:CreateRestApi', 'apigateway:DeleteRestApi', 'apigateway:UpdateRestApi', @@ -53,8 +53,6 @@ module.exports = { var getPolicy = helpers.addSource(cache, source, ['lambda', 'getPolicy', region, lambda.FunctionName]); - let elbs = helpers.getAttachedELBs(cache, source, region, lambda.FunctionArn); - let lambdaResource = { functionUrlConfig: getFunctionUrlConfig, functionPolicy: getPolicy,