diff --git a/RULES.md b/RULES.md index 5d28cd202d..07149f1d9c 100644 --- a/RULES.md +++ b/RULES.md @@ -702,6 +702,7 @@ A collection of community rules that are not currently included in any of the pr | 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. | +| LambdaLogging | The Lambda Function does not define an explicit Log Level | For cost optimization purposes, you should explicitly define the required log level for cost effective storage of Lambda Logs. | ## Footnotes diff --git a/src/rules/lambda/LambdaLogging.ts b/src/rules/lambda/LambdaLogging.ts new file mode 100644 index 0000000000..54c4c7b85c --- /dev/null +++ b/src/rules/lambda/LambdaLogging.ts @@ -0,0 +1,26 @@ +/* +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 { CfnFunction } from 'aws-cdk-lib/aws-lambda'; +import { NagRuleCompliance } from '../../nag-rules'; + +/** + * Lambda functions explicitly define their CloudWatch Log Groups + * @param node the CfnResource to check + */ +export default Object.defineProperty( + (node: CfnResource): NagRuleCompliance => { + if (node instanceof CfnFunction) { + const loggingConfig = Stack.of(node).resolve(node.loggingConfig); + if (loggingConfig && loggingConfig.logGroup) + return NagRuleCompliance.COMPLIANT; + return NagRuleCompliance.NON_COMPLIANT; + } + return NagRuleCompliance.NOT_APPLICABLE; + }, + 'name', + { value: parse(__filename).name } +); diff --git a/src/rules/lambda/index.ts b/src/rules/lambda/index.ts index 8d96afe93e..92ce59d3e4 100644 --- a/src/rules/lambda/index.ts +++ b/src/rules/lambda/index.ts @@ -9,3 +9,4 @@ export { default as LambdaFunctionPublicAccessProhibited } from './LambdaFunctio export { default as LambdaFunctionUrlAuth } from './LambdaFunctionUrlAuth'; export { default as LambdaInsideVPC } from './LambdaInsideVPC'; export { default as LambdaLatestVersion } from './LambdaLatestVersion'; +export { default as LambdaLogging } from './LambdaLogging' diff --git a/test/rules/Lambda.test.ts b/test/rules/Lambda.test.ts index 1ef1b8c79b..9764e7fd1e 100644 --- a/test/rules/Lambda.test.ts +++ b/test/rules/Lambda.test.ts @@ -14,7 +14,11 @@ import { Function, FunctionUrlAuthType, Runtime, + LogFormat, + SystemLogLevel, + ApplicationLogLevel, } from 'aws-cdk-lib/aws-lambda'; +import { LogGroup } from 'aws-cdk-lib/aws-logs'; import { TestPack, TestType, validateStack } from './utils'; import { LambdaConcurrency, @@ -23,6 +27,7 @@ import { LambdaFunctionUrlAuth, LambdaInsideVPC, LambdaLatestVersion, + LambdaLogging, } from '../../src/rules/lambda'; const testPack = new TestPack([ @@ -32,6 +37,7 @@ const testPack = new TestPack([ LambdaFunctionUrlAuth, LambdaInsideVPC, LambdaLatestVersion, + LambdaLogging, ]); let stack: Stack; @@ -351,4 +357,63 @@ describe('AWS Lambda', () => { validateStack(stack, ruleId, TestType.VALIDATION_FAILURE); }); }); + + describe('LambdaLogging: Lambda functions have a explicit log level', () => { + const ruleId = 'LambdaLogging'; + test('Noncompliance 1 - L1 Construct', () => { + new CfnFunction(stack, 'Function', { + code: {}, + role: 'somerole', + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Noncompliance 2 - L2 Constructs missing ApplicationLogLevel', () => { + new Function(stack, 'Function', { + handler: '', + runtime: Runtime.NODEJS_LATEST, + code: Code.fromInline('console.log("hello world'), + logFormat: LogFormat.JSON, + systemLogLevel: SystemLogLevel.WARN + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Noncompliance 2 - L2 Constructs missing systemLogLevel', () => { + new Function(stack, 'Function', { + handler: '', + runtime: Runtime.NODEJS_LATEST, + code: Code.fromInline('console.log("hello world'), + logFormat: LogFormat.JSON, + applicationLogLevel: ApplicationLogLevel.TRACE, + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Compliance - L1 Construct', () => { + new CfnFunction(stack, 'Function', { + code: {}, + role: 'somerole', + loggingConfig: { + applicationLogLevel: 'WARN', + logFormat: 'logFormat', + logGroup: 'logGroup', + systemLogLevel: 'DEBUG', + }, + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + + test('Compliance 1 - L2 Constructs', () => { + new Function(stack, 'Function', { + handler: '', + runtime: Runtime.NODEJS_LATEST, + code: Code.fromInline('console.log("hello world'), + logFormat: LogFormat.JSON, + applicationLogLevel: ApplicationLogLevel.TRACE, + systemLogLevel: SystemLogLevel.WARN, + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + }); });