Skip to content

Commit

Permalink
feat: add rule LambdaEventSourceSQSVisibilityTimeout (#1813)
Browse files Browse the repository at this point in the history
Fixes #1798

New rule for "SQS queue visibility timeout of Lambda Event Source Mapping is at least 6 times timeout of Lambda function". Helps prevent configurations resulting in duplicate processing of queue items due to visibility timeout being too low

Recommendation in the [SQS docs](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-configure-lambda-function-trigger.html):

> To allow your function time to process each batch of records, set the source queue's visibility timeout to at least six times the [timeout that you configure](https://docs.aws.amazon.com/lambda/latest/dg/configuration-function-common.html#configuration-common-summary) on your function. The extra time allows for Lambda to retry if your function is throttled while processing a previous batch.
  • Loading branch information
robert-hanuschke authored Nov 1, 2024
1 parent 3a20507 commit bfa2820
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 4 deletions.
7 changes: 4 additions & 3 deletions RULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -695,9 +695,10 @@ Unimplemented rules from the AWS PCI DSS 3.2.1 Conformance Pack.

A collection of community rules that are not currently included in any of the pre-built NagPacks, but are still available for inclusion in [custom NagPacks](https://github.com/cdklabs/cdk-nag/blob/main/docs/NagPack.md).

| Rule ID | Cause | Explanation |
| --------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| LambdaFunctionUrlAuth | The Lambda Function URL allows for public, unauthenticated access. | AWS Lambda Function URLs allow you to invoke your function via a HTTPS end-point, setting the authentication to NONE allows anyone on the internet to invoke your function. |
| Rule ID | Cause | Explanation |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| LambdaFunctionUrlAuth | The Lambda Function URL allows for public, unauthenticated access. | AWS Lambda Function URLs allow you to invoke your function via a HTTPS end-point, setting the authentication to NONE allows anyone on the internet to invoke your function. |
| LambdaEventSourceSQSVisibilityTimeout | The SQS queue visibility timeout of Lambda Event Source Mapping is less than 6 times timeout of Lambda function. | Setting the visibility timeout to [at least 6 times the Lambda function timeout](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-configure-lambda-function-trigger.html) helps prevent configurations resulting in duplicate processing of queue items when the Lambda function execution is retried. |

## Footnotes

Expand Down
100 changes: 100 additions & 0 deletions src/rules/lambda/LambdaEventSourceSQSVisibilityTimeout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

import { parse } from 'path';
import { CfnResource, Stack } from 'aws-cdk-lib';
import { CfnEventSourceMapping, CfnFunction } from 'aws-cdk-lib/aws-lambda';
import { CfnQueue } from 'aws-cdk-lib/aws-sqs';
import { NagRuleCompliance } from '../../nag-rules';
import { flattenCfnReference } from '../../utils/flatten-cfn-reference';

/**
* SQS queue visibility timeout of Lambda Event Source Mapping is at least 6 times timeout of Lambda function
* @param node the CfnResource to check
*/
export default Object.defineProperty(
(node: CfnResource): NagRuleCompliance => {
if (node instanceof CfnEventSourceMapping) {
const sourceArn = flattenCfnReference(
Stack.of(node).resolve(node.eventSourceArn) ?? ''
);
if (!sourceArn) {
return NagRuleCompliance.NOT_APPLICABLE;
}
const sourceSqsQueue = getSourceSqsQueue(node, sourceArn);
if (!sourceSqsQueue) {
return NagRuleCompliance.NOT_APPLICABLE;
}
const queueVisibilityTimeoutSetting = Stack.of(node).resolve(
sourceSqsQueue.visibilityTimeout
);
const queueVisibilityTimeout =
typeof queueVisibilityTimeoutSetting === 'number' // can be 0, just testing for value truthiness would be wrong
? queueVisibilityTimeoutSetting
: 30; // default SQS Queue visibility timeout
const lambdaFunctionTimeout = getLambdaFunctionTimeout(node);
if (!lambdaFunctionTimeout) {
return NagRuleCompliance.NOT_APPLICABLE;
}
if (lambdaFunctionTimeout > queueVisibilityTimeout / 6) {
return NagRuleCompliance.NON_COMPLIANT;
}
return NagRuleCompliance.COMPLIANT;
} else {
return NagRuleCompliance.NOT_APPLICABLE;
}
},
'name',
{ value: parse(__filename).name }
);

/**
* Helper function to get the SQS queue of the Event Source Mapping
* @param node the CfnEventSourceMapping
* @param sourceArn the already flattened reference to the source Arn
* returns the source CfnQueue or undefined if not found
*/
function getSourceSqsQueue(
node: CfnEventSourceMapping,
sourceArn: string
): CfnQueue | undefined {
for (const child of Stack.of(node).node.findAll()) {
if (
child instanceof CfnQueue &&
flattenCfnReference(Stack.of(node).resolve(child.attrArn)) === sourceArn
) {
return child;
}
}
return undefined;
}

/**
* Helper function to get timeout setting of the CfnEventSourceMapping's Lambda function
* @param node the CfnEventSourceMapping
* returns the timeout value of the Lambda function or undefined if not found
*/
function getLambdaFunctionTimeout(
node: CfnEventSourceMapping
): number | undefined {
const functionRef = flattenCfnReference(
Stack.of(node).resolve(node.functionName)
);
for (const child of Stack.of(node).node.findAll()) {
if (
child instanceof CfnFunction &&
flattenCfnReference(functionRef) ===
flattenCfnReference(Stack.of(node).resolve(child.ref))
) {
const timeoutSetting = Stack.of(node).resolve(child.timeout);
if (typeof timeoutSetting === 'number') {
return timeoutSetting;
} else {
return 3; // default Lambda function timeout
}
}
}
return undefined;
}
1 change: 1 addition & 0 deletions src/rules/lambda/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ SPDX-License-Identifier: Apache-2.0

export { default as LambdaConcurrency } from './LambdaConcurrency';
export { default as LambdaDLQ } from './LambdaDLQ';
export { default as LambdaEventSourceSQSVisibilityTimeout } from './LambdaEventSourceSQSVisibilityTimeout';
export { default as LambdaFunctionPublicAccessProhibited } from './LambdaFunctionPublicAccessProhibited';
export { default as LambdaFunctionUrlAuth } from './LambdaFunctionUrlAuth';
export { default as LambdaInsideVPC } from './LambdaInsideVPC';
Expand Down
163 changes: 162 additions & 1 deletion test/rules/Lambda.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,32 @@
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
import { Aspects, Stack } from 'aws-cdk-lib';
import { Aspects, Duration, Stack } from 'aws-cdk-lib';
import {
AttributeType,
StreamViewType,
TableV2,
} from 'aws-cdk-lib/aws-dynamodb';
import { Repository } from 'aws-cdk-lib/aws-ecr';
import {
CfnEventSourceMapping,
CfnFunction,
CfnPermission,
CfnUrl,
Code,
DockerImageCode,
DockerImageFunction,
EventSourceMapping,
Function,
FunctionUrlAuthType,
Runtime,
} from 'aws-cdk-lib/aws-lambda';
import { Queue } from 'aws-cdk-lib/aws-sqs';
import { TestPack, TestType, validateStack } from './utils';
import {
LambdaConcurrency,
LambdaDLQ,
LambdaEventSourceSQSVisibilityTimeout,
LambdaFunctionPublicAccessProhibited,
LambdaFunctionUrlAuth,
LambdaInsideVPC,
Expand All @@ -28,6 +37,7 @@ import {
const testPack = new TestPack([
LambdaConcurrency,
LambdaDLQ,
LambdaEventSourceSQSVisibilityTimeout,
LambdaFunctionPublicAccessProhibited,
LambdaFunctionUrlAuth,
LambdaInsideVPC,
Expand Down Expand Up @@ -117,6 +127,157 @@ describe('AWS Lambda', () => {
});
});

describe('LambdaEventSourceSQSVisibilityTimeout: SQS queue visibility timeout of Lambda Event Source Mapping is at least 6 times timeout of Lambda function', () => {
const ruleId = 'LambdaEventSourceSQSVisibilityTimeout';
const defaultLambdaFunctionTimeoutSeconds = 3;
const defaultSQSVisibilityTimeoutSeconds = 30;
const minValidMultiplier = 6;
test('Noncompliance 1 - all values defined', () => {
const testVisibilityTimeoutSeconds = 20;
const lambdaFunction = new Function(stack, 'Function', {
code: Code.fromInline('hi'),
timeout: Duration.seconds(
Math.ceil(testVisibilityTimeoutSeconds / minValidMultiplier + 1)
),
handler: 'index.handler',
runtime: Runtime.NODEJS_20_X,
});
const sqsQueue = new Queue(stack, 'SqsQueue', {
visibilityTimeout: Duration.seconds(testVisibilityTimeoutSeconds),
});
new EventSourceMapping(stack, 'EventSourceMapping', {
target: lambdaFunction,
eventSourceArn: sqsQueue.queueArn,
});
validateStack(stack, ruleId, TestType.NON_COMPLIANCE);
});

test('Noncompliance 2 - Lambda timeout defined, SQS visibility timeout default', () => {
const lambdaFunction = new Function(stack, 'Function', {
code: Code.fromInline('hi'),
timeout: Duration.seconds(
Math.ceil(defaultSQSVisibilityTimeoutSeconds / minValidMultiplier + 1)
),
handler: 'index.handler',
runtime: Runtime.NODEJS_20_X,
});
const sqsQueue = new Queue(stack, 'SqsQueue', {});
new EventSourceMapping(stack, 'EventSourceMapping', {
target: lambdaFunction,
eventSourceArn: sqsQueue.queueArn,
});
validateStack(stack, ruleId, TestType.NON_COMPLIANCE);
});

test('Noncompliance 3 - Lambda timeout default, SQS visibility timeout defined', () => {
const lambdaFunction = new Function(stack, 'Function', {
code: Code.fromInline('hi'),
handler: 'index.handler',
runtime: Runtime.NODEJS_20_X,
});
const sqsQueue = new Queue(stack, 'SqsQueue', {
visibilityTimeout: Duration.seconds(
defaultLambdaFunctionTimeoutSeconds * minValidMultiplier - 1
),
});
new EventSourceMapping(stack, 'EventSourceMapping', {
target: lambdaFunction,
eventSourceArn: sqsQueue.queueArn,
});
validateStack(stack, ruleId, TestType.NON_COMPLIANCE);
});

test('Compliance 1 - all values default', () => {
const lambdaFunction = new Function(stack, 'Function', {
code: Code.fromInline('hi'),
handler: 'index.handler',
runtime: Runtime.NODEJS_20_X,
});
const sqsQueue = new Queue(stack, 'SqsQueue', {});
new EventSourceMapping(stack, 'EventSourceMapping', {
target: lambdaFunction,
eventSourceArn: sqsQueue.queueArn,
});
validateStack(stack, ruleId, TestType.COMPLIANCE);
});

test('Compliance 2 - Lambda timeout defined, SQS visibility timeout default', () => {
const lambdaFunction = new Function(stack, 'Function', {
code: Code.fromInline('hi'),
timeout: Duration.seconds(
Math.floor(defaultSQSVisibilityTimeoutSeconds / minValidMultiplier)
),
handler: 'index.handler',
runtime: Runtime.NODEJS_20_X,
});
const sqsQueue = new Queue(stack, 'SqsQueue', {});
new EventSourceMapping(stack, 'EventSourceMapping', {
target: lambdaFunction,
eventSourceArn: sqsQueue.queueArn,
});
validateStack(stack, ruleId, TestType.COMPLIANCE);
});

test('Compliance 3 - Lambda timeout default, SQS visibility timeout defined', () => {
const lambdaFunction = new Function(stack, 'Function', {
code: Code.fromInline('hi'),
handler: 'index.handler',
runtime: Runtime.NODEJS_20_X,
});
const sqsQueue = new Queue(stack, 'SqsQueue', {
visibilityTimeout: Duration.seconds(
defaultLambdaFunctionTimeoutSeconds * minValidMultiplier
),
});
new EventSourceMapping(stack, 'EventSourceMapping', {
target: lambdaFunction,
eventSourceArn: sqsQueue.queueArn,
});
validateStack(stack, ruleId, TestType.COMPLIANCE);
});

test('Compliance 4 - eventSourceArn is not of an SQS queue', () => {
const lambdaFunction = new Function(stack, 'Function', {
code: Code.fromInline('hi'),
handler: 'index.handler',
runtime: Runtime.NODEJS_20_X,
});
const ddbTable = new TableV2(stack, 'DdbTable', {
partitionKey: { name: 'id', type: AttributeType.STRING },
dynamoStream: StreamViewType.KEYS_ONLY,
});
new EventSourceMapping(stack, 'EventSourceMapping', {
target: lambdaFunction,
eventSourceArn: ddbTable.tableStreamArn,
});
validateStack(stack, ruleId, TestType.COMPLIANCE);
});

test('Compliance 5 - Kafka source, no eventSourceArn set', () => {
const lambdaFunction = new Function(stack, 'Function', {
code: Code.fromInline('hi'),
handler: 'index.handler',
runtime: Runtime.NODEJS_20_X,
});
new EventSourceMapping(stack, 'EventSourceMapping', {
target: lambdaFunction,
kafkaBootstrapServers: ['abc.example.com:9096'],
});
validateStack(stack, ruleId, TestType.COMPLIANCE);
});

test('Compliance 6 - Lambda function not found in stack', () => {
const sqsQueue = new Queue(stack, 'SqsQueue', {
visibilityTimeout: Duration.seconds(20),
});
new CfnEventSourceMapping(stack, 'EventSourceMapping', {
functionName: 'myFunction',
eventSourceArn: sqsQueue.queueArn,
});
validateStack(stack, ruleId, TestType.COMPLIANCE);
});
});

describe('LambdaFunctionPublicAccessProhibited: Lambda function permissions do not grant public access', () => {
const ruleId = 'LambdaFunctionPublicAccessProhibited';
test('Noncompliance 1', () => {
Expand Down

0 comments on commit bfa2820

Please sign in to comment.