Skip to content

Commit

Permalink
excluding function URL from vpc checks and adding load balancer and m…
Browse files Browse the repository at this point in the history
…ore checks for iam policy conditions
  • Loading branch information
giorod3 committed Dec 3, 2024
1 parent 8a4726b commit e0c0c33
Show file tree
Hide file tree
Showing 2 changed files with 204 additions and 52 deletions.
245 changes: 195 additions & 50 deletions helpers/aws/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -1287,48 +1367,46 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs
}
}
}

return internetExposed;
}

// Check public endpoint access for specific resources like EKS
if (resource && resource.resourcesVpcConfig && resource.resourcesVpcConfig.endpointPublicAccess) {
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);
Expand All @@ -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,
Expand Down Expand Up @@ -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}`;
}

Expand All @@ -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;
}
}
}
}
Expand All @@ -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;
}

Check failure on line 1594 in helpers/aws/functions.js

View workflow job for this annotation

GitHub Actions / build

Missing semicolon

Check failure on line 1594 in helpers/aws/functions.js

View workflow job for this annotation

GitHub Actions / build

Missing semicolon

module.exports = {
addResult: addResult,
findOpenPorts: findOpenPorts,
Expand Down Expand Up @@ -1487,6 +1631,7 @@ module.exports = {
processFieldSelectors: processFieldSelectors,
checkNetworkInterface: checkNetworkInterface,
checkNetworkExposure: checkNetworkExposure,
getAttachedELBs: getAttachedELBs
getAttachedELBs: getAttachedELBs,
getLambdaTargetELBs
};

11 changes: 9 additions & 2 deletions plugins/aws/lambda/lambdaNetworkExposure.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'],

Check failure on line 15 in plugins/aws/lambda/lambdaNetworkExposure.js

View workflow job for this annotation

GitHub Actions / build

Expected indentation of 8 spaces but found 4

Check failure on line 15 in plugins/aws/lambda/lambdaNetworkExposure.js

View workflow job for this annotation

GitHub Actions / build

Expected indentation of 8 spaces but found 4
realtime_triggers: ['lambda:CreateFunctionUrlConfig', 'lambda:UpdateFunctionUrlConfig', 'lambda:DeleteFunctionUrlConfig',
'lambda:AddPermission', 'lambda:RemovePermission',
'apigateway:CreateRestApi', 'apigateway:DeleteRestApi', 'apigateway:UpdateRestApi',
Expand Down Expand Up @@ -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;

Expand All @@ -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);

Check failure on line 56 in plugins/aws/lambda/lambdaNetworkExposure.js

View workflow job for this annotation

GitHub Actions / build

'elbs' is assigned a value but never used

Check failure on line 56 in plugins/aws/lambda/lambdaNetworkExposure.js

View workflow job for this annotation

GitHub Actions / build

'elbs' is assigned a value but never used

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,
Expand Down

0 comments on commit e0c0c33

Please sign in to comment.