-
Notifications
You must be signed in to change notification settings - Fork 67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Serverless Nag Pack #1793
Open
Kevinwochan
wants to merge
46
commits into
cdklabs:main
Choose a base branch
from
Kevinwochan:feat/serverless-nag-pack-complete
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
46 commits
Select commit
Hold shift + click to select a range
6b112a8
feat: implemented rules and 1 test
Kevinwochan 3cd5e1c
feat: lambda logging rules
Kevinwochan a757e4b
fix: typo in function id
Kevinwochan 38c23bc
fix: typo in function id
Kevinwochan 66181a6
styling: rule description grammar
Kevinwochan 71793d0
docs: added docs for lambda logging
Kevinwochan bc1d61c
feat: testing L2 constructs
Kevinwochan 515f898
implemeted all rules, untested and undocumented
Kevinwochan 9211f03
feat: LambdaLogLevels rules
Kevinwochan 1e0d6ce
Merge branch 'feat/serverless-nag-pack' into feat/serverless-nag-pack…
Kevinwochan 4fb7e60
feat: lambda logging rules
Kevinwochan e61f13c
Merge branch 'feat/serverless-nag-pack' into feat/serverless-nag-pack…
Kevinwochan c9b8332
docs(serverless): added example of warning and error
Kevinwochan 21ab1a2
docs(serverless): added example warning and error rules
Kevinwochan 55144a9
feat(serverless): remove lambda logging rule
Kevinwochan f8a47b9
feat(serverless): remove lambda logging rule
Kevinwochan 0ff3d12
feat(serverless): unit tests for lambda xray tracing
Kevinwochan 9b4cd49
feat(serverless): updated LambdaTracing to use tracingConfig object
Kevinwochan d783d82
docs(serverless): updated docs for LambdaTracing rule
Kevinwochan 872bdf7
feat(serverless): LambdaEventSourceMappingDestination unit tested
Kevinwochan 9620a94
feat(serverless): added docs and checks to the serverless nag pack
Kevinwochan 5d2cb8b
feat(serverless): added iam wildcard checks
Kevinwochan ce169fc
feat(serverless): added cw logs retention checks
Kevinwochan 554fced
feat(serverless): unit tested and documented LambdaDefaultMemorySize …
Kevinwochan 73821c8
feat(serverless): unit tests for Lambda Default Timeout
Kevinwochan 3fd72f7
feat(serverless): documented LambdaDefaultTimeout in the serverless n…
Kevinwochan 11eb99a
feat(serverless): implemented LambdaAsyncFailureDestination rule
Kevinwochan 901b423
feat(serverless): added latestlambdaversion check to serverless nag pack
Kevinwochan 47091d2
feat(serverless): added APIGW access logging check
Kevinwochan eac6ce2
feat(serverless): unit tests for apigw structured logging
Kevinwochan 58dde65
feat(serverless): implemented and tested APIGWStructuredLogging
Kevinwochan 2debbe9
feat(serverless): Stepfunctions rules
Kevinwochan 37beb39
feat(serverless): implemented and unit tested redrive policy
Kevinwochan 156a3e3
feat(serverless): added sqs redrive policy to serverless nag pack
Kevinwochan c22ee05
feat(serverless): removing redundant implementation
Kevinwochan d083e17
feat(serverless): sns redrive policy rule
Kevinwochan 9a8af8c
feat(serverless): eventbridge dlq rules
Kevinwochan 256ebfa
feat(serverless): appsync tracing rule
Kevinwochan f8cb04a
feat(serverless): http and rest api throttling rule
Kevinwochan 760975e
feat(serverless): added apigw throttling rule
Kevinwochan e21a7d4
feat(serverless): remove unused file
Kevinwochan 1069abb
Merge branch 'cdklabs:main' into main
Kevinwochan 3fd6a50
Merge branch 'main' into feat/serverless-nag-pack-complete
Kevinwochan 0452413
feat(serverless): declerative grammar
Kevinwochan 8308a3d
feat(serverless): declarative grammar
Kevinwochan 84f7b7a
feat(serverless): update to use declarative grammar
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,279 @@ | ||||||
import { CfnResource } from 'aws-cdk-lib'; | ||||||
import { IConstruct } from 'constructs'; | ||||||
import { NagPack, NagPackProps } from '../nag-pack'; | ||||||
import { NagMessageLevel } from '../nag-rules'; | ||||||
import { | ||||||
apigw, | ||||||
appsync, | ||||||
cloudwatch, | ||||||
eventbridge, | ||||||
iam, | ||||||
lambda, | ||||||
sns, | ||||||
sqs, | ||||||
stepfunctions, | ||||||
} from '../rules'; | ||||||
|
||||||
/** | ||||||
* Serverless Checks are a compilation of rules to validate infrastructure-as-code template against recommended practices. | ||||||
* | ||||||
*/ | ||||||
export class ServerlessChecks extends NagPack { | ||||||
constructor(props?: NagPackProps) { | ||||||
super(props); | ||||||
this.packName = 'Serverless'; | ||||||
} | ||||||
public visit(node: IConstruct): void { | ||||||
if (node instanceof CfnResource) { | ||||||
this.checkLambda(node); | ||||||
this.checkCloudwatch(node); | ||||||
this.checkIAM(node); | ||||||
this.checkApiGw(node); | ||||||
this.checkAppSync(node); | ||||||
this.checkEventBridge(node); | ||||||
this.checkSNS(node); | ||||||
this.checkSQS(node); | ||||||
this.checkStepFunctions(node); | ||||||
} | ||||||
} | ||||||
|
||||||
/** | ||||||
* Check Lambda Resources | ||||||
* @param node the CfnResource to check | ||||||
* @param ignores list of ignores for the resource | ||||||
*/ | ||||||
private checkLambda(node: CfnResource) { | ||||||
this.applyRule({ | ||||||
info: 'The Lambda function should have tracing set to Tracing.ACTIVE.', | ||||||
explanation: | ||||||
'When a Lambda function has ACTIVE tracing, Lambda automatically samples invocation requests, based on the sampling algorithm specified by X-Ray.', | ||||||
level: NagMessageLevel.WARN, | ||||||
rule: lambda.LambdaTracing, | ||||||
node: node, | ||||||
}); | ||||||
|
||||||
this.applyRule({ | ||||||
info: 'Lambda Event Source Mappings have a destination configured for failed invocations.', | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The info on
Suggested change
|
||||||
explanation: | ||||||
'Configuring a destination for failed invocations in Lambda Event Source Mappings allows you to capture and process events that fail to be processed by your Lambda function. This helps in monitoring, debugging, and implementing retry mechanisms for failed events, improving the reliability and observability of your serverless applications.', | ||||||
level: NagMessageLevel.ERROR, | ||||||
rule: lambda.LambdaEventSourceMappingDestination, | ||||||
node: node, | ||||||
}); | ||||||
|
||||||
this.applyRule({ | ||||||
info: 'Lambda functions utilize the latest available runtime version.', | ||||||
explanation: | ||||||
"Using the latest runtime version ensures that your Lambda function has access to the most recent features, performance improvements, and security updates. It's important to regularly update your Lambda functions to use the latest runtime versions to maintain optimal performance and security.", | ||||||
level: NagMessageLevel.WARN, | ||||||
rule: lambda.LambdaLatestVersion, | ||||||
node: node, | ||||||
}); | ||||||
|
||||||
this.applyRule({ | ||||||
info: 'Lambda functions have a configured failure destination for asynchronous invocations.', | ||||||
explanation: | ||||||
"When a Lambda function is invoked asynchronously (e.g., by S3, SNS, or EventBridge), it's important to configure a failure destination. This allows you to capture and handle events that fail processing, improving the reliability and observability of your serverless applications.", | ||||||
level: NagMessageLevel.ERROR, | ||||||
rule: lambda.LambdaAsyncFailureDestination, | ||||||
node: node, | ||||||
}); | ||||||
|
||||||
this.applyRule({ | ||||||
info: 'Lambda functions have an explicit memory value configured', | ||||||
explanation: | ||||||
"Lambda allocates CPU power in proportion to the amount of memory configured. By default, your functions have 128 MB of memory allocated. You can increase that value up to 10 GB. With more CPU resources, your Lambda function's duration might decrease. You can use tools such as AWS Lambda Power Tuning to test your function at different memory settings to find the one that matches your cost and performance requirements the best.", | ||||||
level: NagMessageLevel.ERROR, | ||||||
rule: lambda.LambdaDefaultMemorySize, | ||||||
node: node, | ||||||
}); | ||||||
|
||||||
this.applyRule({ | ||||||
info: 'Lambda functions must have an explicitly defined timeout value.', | ||||||
explanation: | ||||||
'Lambda functions have a default timeout of 3 seconds. If your timeout value is too short, Lambda might terminate invocations prematurely. On the other side, setting the timeout much higher than the average execution may cause functions to execute for longer upon code malfunction, resulting in higher costs and possibly reaching concurrency limits depending on how such functions are invoked. You can also use AWS Lambda Power Tuning to test your function at different timeout settings to find the one that matches your cost and performance requirements the best.', | ||||||
level: NagMessageLevel.ERROR, | ||||||
rule: lambda.LambdaDefaultTimeout, | ||||||
node: node, | ||||||
}); | ||||||
|
||||||
this.applyRule({ | ||||||
info: 'Lambda functions have a dead letter target configured.', | ||||||
explanation: | ||||||
'When a lambda function has the DeadLetterConfig property set, failed messages can be temporarily stored for review in an SQS queue or an SNS topic.', | ||||||
level: NagMessageLevel.ERROR, | ||||||
rule: lambda.LambdaDLQ, | ||||||
node: node, | ||||||
}); | ||||||
|
||||||
this.applyRule({ | ||||||
info: 'Lambda functions are required to use the latest runtime version.', | ||||||
explanation: | ||||||
"Using the latest runtime version ensures that your Lambda function has access to the most recent features, performance improvements, and security updates. It's important to regularly update your Lambda functions to use the latest runtime versions to maintain optimal performance and security.", | ||||||
level: NagMessageLevel.ERROR, | ||||||
rule: lambda.LambdaLatestVersion, | ||||||
node: node, | ||||||
}); | ||||||
|
||||||
this.applyRule({ | ||||||
info: 'Lambda Event Source Mappings must have a destination configured for failed invocations.', | ||||||
explanation: | ||||||
'Configuring a destination for failed invocations in Lambda Event Source Mappings allows you to capture and process events that fail to be processed by your Lambda function. This helps in monitoring, debugging, and implementing retry mechanisms for failed events, improving the reliability and observability of your serverless applications.', | ||||||
level: NagMessageLevel.ERROR, | ||||||
rule: lambda.LambdaEventSourceMappingDestination, | ||||||
node: node, | ||||||
}); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Check Lambda Resources | ||||||
* @param node the CfnResource to check | ||||||
* @param ignores list of ignores for the resource | ||||||
*/ | ||||||
private checkIAM(node: CfnResource) { | ||||||
this.applyRule({ | ||||||
info: 'Lambda functions do not have overly permissive IAM roles.', | ||||||
explanation: | ||||||
'Lambda functions should follow the principle of least privilege. Avoid using wildcard (*) permissions in IAM roles attached to Lambda functions. Instead, specify only the permissions required for the function to operate.', | ||||||
level: NagMessageLevel.WARN, | ||||||
rule: iam.IAMNoWildcardPermissions, | ||||||
node: node, | ||||||
}); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Check Cloudwatch Resources | ||||||
* @param node the CfnResource to check | ||||||
* @param ignores list of ignores for the resource | ||||||
*/ | ||||||
private checkCloudwatch(node: CfnResource) { | ||||||
this.applyRule({ | ||||||
info: 'CloudWatch Log Groups must have an explicit retention policy defined.', | ||||||
explanation: | ||||||
'By default, logs are kept indefinitely and never expire. You can adjust the retention policy for each log group, keeping the indefinite retention, or choosing a retention period between one day and 10 years. For Lambda functions, this applies to their automatically created CloudWatch Log Groups.', | ||||||
level: NagMessageLevel.WARN, | ||||||
rule: cloudwatch.CloudWatchLogGroupRetentionPeriod, | ||||||
node: node, | ||||||
}); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Check API Gateway Resources | ||||||
* @param node the CfnResource to check | ||||||
* @param ignores list of ignores for the resource | ||||||
*/ | ||||||
private checkApiGw(node: CfnResource) { | ||||||
this.applyRule({ | ||||||
info: 'API Gateway stages have access logging enabled.', | ||||||
explanation: | ||||||
'API Gateway provides access logging for API stages. Enable access logging on your API stages to monitor API requests and responses.', | ||||||
level: NagMessageLevel.ERROR, | ||||||
rule: apigw.APIGWAccessLogging, | ||||||
node: node, | ||||||
}); | ||||||
this.applyRule({ | ||||||
info: 'API Gateways have Tracing enabled.', | ||||||
explanation: | ||||||
'Amazon API Gateway provides active tracing support for AWS X-Ray. Enable active tracing on your API stages to sample incoming requests and send traces to X-Ray.', | ||||||
level: NagMessageLevel.ERROR, | ||||||
rule: apigw.APIGWXrayEnabled, | ||||||
node: node, | ||||||
}); | ||||||
this.applyRule({ | ||||||
info: 'API Gateway logs are configured for the JSON format.', | ||||||
explanation: | ||||||
'You can customize the log format that Amazon API Gateway uses to send logs. JSON Structured logging makes it easier to derive queries to answer arbitrary questions about the health of your application.', | ||||||
level: NagMessageLevel.ERROR, | ||||||
rule: apigw.APIGWStructuredLogging, | ||||||
node: node, | ||||||
}); | ||||||
this.applyRule({ | ||||||
info: 'API Gateway stages are not using default throttling setting.s', | ||||||
explanation: | ||||||
'API Gateway default throttling settings may not be suitable for all applications. Custom throttling limits help protect your backend systems from being overwhelmed with requests, ensure consistent performance, and can be tailored to your specific use case.', | ||||||
level: NagMessageLevel.ERROR, | ||||||
rule: apigw.APIGWDefaultThrottling, | ||||||
node: node, | ||||||
}); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Check AppSync Resources | ||||||
* @param node the CfnResource to check | ||||||
* @param ignores list of ignores for the resource | ||||||
*/ | ||||||
private checkAppSync(node: CfnResource) { | ||||||
this.applyRule({ | ||||||
info: 'AppSync APIs have tracing enabled', | ||||||
explanation: | ||||||
'AWS AppSync can emit traces to AWS X-Ray, which enables visualizing service maps for faster troubleshooting.', | ||||||
level: NagMessageLevel.WARN, | ||||||
rule: appsync.AppSyncTracing, | ||||||
node: node, | ||||||
}); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Check EventBridge Resources | ||||||
* @param node the CfnResource to check | ||||||
* @param ignores list of ignores for the resource | ||||||
*/ | ||||||
private checkEventBridge(node: CfnResource) { | ||||||
this.applyRule({ | ||||||
info: 'EventBridge targets have a DLQ configured.', | ||||||
explanation: | ||||||
"Configuring a Dead-Letter Queue (DLQ) for EventBridge rules helps manage failed event deliveries. When a rule's target fails to process an event, the DLQ captures these failed events, allowing for analysis, troubleshooting, and potential reprocessing. This improves the reliability and observability of your event-driven architectures by providing a safety net for handling delivery failures.", | ||||||
level: NagMessageLevel.ERROR, | ||||||
rule: eventbridge.EventBusDLQ, | ||||||
node: node, | ||||||
}); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Check SNS Resources | ||||||
* @param node the CfnResource to check | ||||||
* @param ignores list of ignores for the resource | ||||||
*/ | ||||||
private checkSNS(node: CfnResource) { | ||||||
this.applyRule({ | ||||||
info: 'SNS subscriptions have a redrive policy configured.', | ||||||
explanation: | ||||||
'Configuring a redrive policy helps manage message delivery failures by sending undeliverable messages to a dead-letter queue.', | ||||||
level: NagMessageLevel.ERROR, | ||||||
rule: sns.SNSRedrivePolicy, | ||||||
node: node, | ||||||
}); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Check SQS Resources | ||||||
* @param node the CfnResource to check | ||||||
* @param ignores list of ignores for the resource | ||||||
*/ | ||||||
private checkSQS(node: CfnResource) { | ||||||
this.applyRule({ | ||||||
info: 'SQS queues have a redrive policy configured.', | ||||||
explanation: | ||||||
'Configuring a redrive policy on an SQS queue allows you to define how many times SQS will make messages available for consumers before sending them to a dead-letter queue. This helps in managing message processing failures and provides a mechanism for handling problematic messages.', | ||||||
level: NagMessageLevel.ERROR, | ||||||
rule: sqs.SQSRedrivePolicy, | ||||||
node: node, | ||||||
}); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Check StepFunctions Resources | ||||||
* @param node the CfnResource to check | ||||||
* @param ignores list of ignores for the resource | ||||||
*/ | ||||||
private checkStepFunctions(node: CfnResource) { | ||||||
this.applyRule({ | ||||||
info: 'StepFunctions have X-Ray tracing configured.', | ||||||
explanation: | ||||||
'AWS StepFunctions provides active tracing support for AWS X-Ray. Enable active tracing on your API stages to sample incoming requests and send traces to X-Ray.', | ||||||
level: NagMessageLevel.ERROR, | ||||||
rule: stepfunctions.StepFunctionStateMachineXray, | ||||||
node: node, | ||||||
}); | ||||||
} | ||||||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { CfnResource, Stack } from 'aws-cdk-lib'; | ||
import { CfnStage } from 'aws-cdk-lib/aws-apigateway'; | ||
import { CfnStage as CfnHttpStage } from 'aws-cdk-lib/aws-apigatewayv2'; | ||
import { NagRuleCompliance } from '../../nag-rules'; | ||
|
||
/** | ||
* API Gateway stages are not using default throttling settings | ||
* @param node The CfnStage or CfnHttpStage to check | ||
*/ | ||
export default Object.defineProperty( | ||
(node: CfnResource): NagRuleCompliance => { | ||
if (node instanceof CfnStage) { | ||
// Check REST API | ||
const methodSettings = Stack.of(node).resolve(node.methodSettings); | ||
if ( | ||
Array.isArray(methodSettings) && | ||
methodSettings.some( | ||
(setting) => | ||
setting.throttlingBurstLimit !== undefined && | ||
setting.throttlingRateLimit !== undefined && | ||
setting.httpMethod === '*' && | ||
setting.resourcePath === '/*' | ||
) | ||
) { | ||
return NagRuleCompliance.COMPLIANT; | ||
} | ||
return NagRuleCompliance.NON_COMPLIANT; | ||
} else if (node instanceof CfnHttpStage) { | ||
// Check HTTP API | ||
const defaultRouteSettings = Stack.of(node).resolve( | ||
node.defaultRouteSettings | ||
); | ||
if ( | ||
defaultRouteSettings && | ||
defaultRouteSettings.throttlingBurstLimit !== undefined && | ||
defaultRouteSettings.throttlingRateLimit !== undefined | ||
) { | ||
return NagRuleCompliance.COMPLIANT; | ||
} | ||
return NagRuleCompliance.NON_COMPLIANT; | ||
} | ||
return NagRuleCompliance.NOT_APPLICABLE; | ||
}, | ||
'name', | ||
{ value: 'APIGWDefaultThrottling' } | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/* | ||
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 { CfnDeployment, CfnStage } from 'aws-cdk-lib/aws-apigateway'; | ||
import { CfnStage as CfnStageV2 } from 'aws-cdk-lib/aws-apigatewayv2'; | ||
import { CfnApi, CfnHttpApi } from 'aws-cdk-lib/aws-sam'; | ||
import { NagRuleCompliance } from '../../nag-rules'; | ||
|
||
const isJSON = (str: string) => { | ||
try { | ||
JSON.parse(str); | ||
} catch (e) { | ||
return false; | ||
} | ||
return true; | ||
}; | ||
|
||
/** | ||
* API Gateway logs are configured in JSON format. | ||
* @param node the CfnResource to check | ||
*/ | ||
export default Object.defineProperty( | ||
(node: CfnResource): NagRuleCompliance => { | ||
if ( | ||
node instanceof CfnApi || | ||
node instanceof CfnHttpApi || | ||
node instanceof CfnStage | ||
) { | ||
const accessLogSetting = Stack.of(node).resolve(node.accessLogSetting); | ||
if (!accessLogSetting) return NagRuleCompliance.NOT_APPLICABLE; | ||
if (isJSON(accessLogSetting.format)) return NagRuleCompliance.COMPLIANT; | ||
return NagRuleCompliance.NON_COMPLIANT; | ||
} else if (node instanceof CfnDeployment) { | ||
const stageDescription = Stack.of(node).resolve(node.stageDescription); | ||
const accessLogSetting = stageDescription.accessLogSetting; | ||
if (!accessLogSetting) return NagRuleCompliance.NOT_APPLICABLE; | ||
if (isJSON(accessLogSetting.format)) return NagRuleCompliance.COMPLIANT; | ||
return NagRuleCompliance.NON_COMPLIANT; | ||
} else if (node instanceof CfnStageV2) { | ||
const accessLogSetting = Stack.of(node).resolve(node.accessLogSettings); | ||
if (!accessLogSetting) return NagRuleCompliance.NOT_APPLICABLE; | ||
if (isJSON(accessLogSetting.format)) return NagRuleCompliance.COMPLIANT; | ||
return NagRuleCompliance.NON_COMPLIANT; | ||
} | ||
return NagRuleCompliance.NOT_APPLICABLE; | ||
}, | ||
'name', | ||
{ value: parse(__filename).name } | ||
); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does not
was appropriate wording