This post is part 2 of a series:
- Part 1 — Automating Scout Suite Scans for AWS
- Part 3— Deploying Scout Suite Automation to AWS using Terraform
Updates
2024–02–04: Code in the repository has been updated to use SNS instead of SES.
Background
In this part we will automate the deployment of the Scout Suite scanning Lambda, and will also configure it to run daily and send email notifications with the scan results. We will use AWS CDK as the Infrastructure as Code (IaC) tool.
AWS CDK
AWS Cloud Development Kit (CDK) is a framework for defining cloud infrastructure in code and provisioning it through AWS CloudFormation. This post is not intended to be an introduction into CDK, if you’re new to CDK check out Getting started with the AWS CDK.
AWS CDK uses general-purpose programming languages and supports TypeScript, JavaScript, Python, Java, C#/.Net, and Go. All the code excerpts below use TypeScript.
Architecture
What we’ll be deploying has a simple architecture:
- Scout Suite scanning Lambda that we already looked at in Part1. Lambda is using a container image pulled from Elastic Container Registry (ECR).
- EventBridge rule will invoke the scanning Lambda daily based on a cron schedule.
- Once the scan is completed, the Scanning Lambda uploads the scan report to S3.
- On object upload events, S3 is configured to invoke a notifier Lambda. Notifier Lambda is simple Python script that sends an email notification using Amazon Simple Email Service (SES).
CDK Code
Full source code is available at: https://github.com/airman604/aws-scan-automation. Below are explanations for snippets of CDK code that deploy the automation.
The basic building blocks in CDK are constructs. A construct represents a “cloud component” and encapsulates everything AWS CloudFormation needs to create the component. The AWS CDK provides the AWS Construct Library, which contains constructs representing AWS resources. All constructs take three parameters when they are initialized:
- scope — The construct’s parent or owner, either a stack or another construct, which determines its place in the construct tree. We’ll be passing this, which represents the stack.
- id — A string identifier that must be unique within this scope. It’s used to generate unique names for the resources upon creation.
- props — A set of properties that define the construct’s initial configuration. props might provide default values and be optional, in which case it can be omitted.
S3 Bucket
S3 bucket will be used to store scan reports:
// S3 bucket where the scan results are saved
const s3Bucket = new s3.Bucket(this, "ScoutResultsBucket");
Scanning Lambda
Scout Suite Lambda is created from a container image that is built from ../lambda-scout
directory. Similar to the manual configuration in Part 1, we set memory size to 2Gb and maximum execution time to 15 min. S3 bucket name and a path in the bucket where to save reports are passed to the Lambda function as environment variables.
Scanning Lambda function needs permissions to run scans. We add SecurityAudit and ReadOnlyAccess AWS managed policies, as well as allow write access to the S3 bucket created earlier. scoutReportPrefixPattern
variable defines path within the S3 bucket where the reports are saved.
// Lambda that runs Scout Suite scans
const scoutLambda = new lambda.Function(this, "ScoutLambda", {
runtime: lambda.Runtime.FROM_IMAGE,
code: lambda.Code.fromAssetImage("../lambda-scout", {
// this is needed when running on Apple silicon
platform: ecr_assets.Platform.LINUX_AMD64
}),
handler: lambda.Handler.FROM_IMAGE,
memorySize: 2048,
timeout: cdk.Duration.minutes(15),
environment: {
"S3_BUCKET": s3Bucket.bucketName,
"S3_PREFIX": scoutReportPrefix
}
});
// give scanning Lambda appropriate permissions
// see: https://github.com/nccgroup/ScoutSuite/wiki/Amazon-Web-Services#permissions
scoutLambda.role?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("SecurityAudit"));
scoutLambda.role?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("ReadOnlyAccess"));
// and access to S3 to upload reports
s3Bucket.grantPut(scoutLambda, scoutReportPrefixPattern);
EventBridge Rule
Amazon EventBridge rule is defined to trigger daily at 10:00 UTC and invoke the scanning Lambda function.
// run the scanner Lambda daily
const eventRule = new events.Rule(this, "DailyScoutScan", {
// 10:00am - time in UTC!
schedule: events.Schedule.cron({ minute: '0', hour: '10' }),
});
eventRule.addTarget(new targets.LambdaFunction(scoutLambda));
Notification Lambda
Notification Lambda is invoked when a new report is uploaded to S3. The following code creates:
- SES email identity (i.e. verified email address) to use as the sender and the recipient for email notifications. When the code is deployed, SES will send a message to verify that you have access to the email address. To receive scan notifications you will need to confirm the address.
notificationEmail
is a parameter that is defined separately (see the code in the GitHub repository) that need to be passed at deployment time. - Notification Lambda, using Python code from
../lambda-notification
directory. The email address is passed as environment variable to the Lambda code. - IAM permissions for Lambda to be able to send emails using SES.
- S3 bucket (defined above) is configured to invoke the Lambda when objects are uploaded.
- The Lambda is given S3 read permissions. We use
scoutReportPrefix
as a constant set earlier in the code — this is S3 path where the reports are saved, so only notifications for that path are sent.
// email identity to send reports (both to and from)
const sesIdentity = new ses.EmailIdentity(this, 'ScoutIdentity', {
identity: ses.Identity.email(notificationEmail.valueAsString)
});
// Lambda that will be invoked when new object is uploaded to S3
// and will send notification emails through SES
const notificationLambda = new lambda.Function(this, "NotificationLambda", {
runtime: lambda.Runtime.PYTHON_3_11,
handler: 'index.handler',
code: lambda.Code.fromAsset("../lambda-notifications"),
environment: {
"SENDER": notificationEmail.valueAsString,
"RECIPIENT": notificationEmail.valueAsString
}
});
// add permissions for Lambda to send emails
notificationLambda.addToRolePolicy(new iam.PolicyStatement({
actions: ['ses:SendEmail', 'ses:SendRawEmail'],
resources: [`arn:aws:ses:${this.region}:${this.account}:identity/${sesIdentity.emailIdentityName}`],
effect: iam.Effect.ALLOW,
}));
// invoke the lambda on new object upload
s3Bucket.addObjectCreatedNotification(new s3n.LambdaDestination(notificationLambda), {
prefix: scoutReportPrefix
});
// add permissions for Lambda to read from S3
s3Bucket.grantRead(notificationLambda, scoutReportPrefixPattern);
Deployment
The full code is available here: https://github.com/airman604/aws-scan-automation. All of the AWS CDK code is in cdk/lib/aws-scout-automation-stack.ts
(rest of the code in cdk
directory is AWS CDK template).
Deploying:
- Install pre-requisites: AWS CLI, Node, AWS CDK, Docker.
- Configure your AWS CLI credentials and default region (
aws configure
). - Clone the repository.
- Change to the
cdk
sub-directory of the repository. - Bootstrap CDK using
cdk bootstrap
. - Deploy the stack using
cdk deploy
. Email address to send notifications to needs to be passed as a parameter to the stack.
Check out README.md in the repository for more details.