From 94cf2c8a3517d3d17879df15c1bb6a9c38db962e Mon Sep 17 00:00:00 2001 From: Gavin Inglis Date: Thu, 7 Sep 2023 14:57:42 -0700 Subject: [PATCH] feat: add rootfs image scanning notifications Amazon ECR offers Enhanced Image Scanning, which we have enabled for the rootfs image used for Finch on Windows. This change implements a stack to automatically notify Finch developers of scan results when the finding is HIGH or CRITICAL in severity (aka CVE score >= 7.0). An SNS topic is created for the notification, and a Lambda function is wired to EventBridge to listen for events from AWS Inspector of those severities. Signed-off-by: Gavin Inglis --- lib/event-bridge-scan-notifs-stack.ts | 52 +++++++++++++++ lib/finch-pipeline-app-stage.ts | 6 ++ .../main.py | 65 +++++++++++++++++++ test/event-bridge-scan-notifs-stack.test.ts | 49 ++++++++++++++ 4 files changed, 172 insertions(+) create mode 100644 lib/event-bridge-scan-notifs-stack.ts create mode 100644 lib/image-scanning-notifications-lambda-handler/main.py create mode 100644 test/event-bridge-scan-notifs-stack.test.ts diff --git a/lib/event-bridge-scan-notifs-stack.ts b/lib/event-bridge-scan-notifs-stack.ts new file mode 100644 index 0000000..869be22 --- /dev/null +++ b/lib/event-bridge-scan-notifs-stack.ts @@ -0,0 +1,52 @@ +import * as cdk from 'aws-cdk-lib'; +import * as events from 'aws-cdk-lib/aws-events'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as sns from 'aws-cdk-lib/aws-sns'; +import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions'; +import * as targets from 'aws-cdk-lib/aws-events-targets'; +import { Construct } from 'constructs'; +import path from 'path'; + +export class EventBridgeScanNotifsStack extends cdk.Stack { + constructor(scope: Construct, id: string, stage: string, props?: cdk.StackProps) { + super(scope, id, props); + + const topic = new sns.Topic(this, 'ECR Image Inspector Findings'); + + // Let's not expose this on GitHub, will only be visible in AWS logs Finch team owns, which is low risk. + // Secret has to be created in Beta and Prod accounts. + // unsafeUnwrap is used because SNS does not have any construct that accepts a SecretValue property. + const securityEmail = cdk.SecretValue.secretsManager('security-notifications-email').unsafeUnwrap() + topic.addSubscription(new subscriptions.EmailSubscription(securityEmail.toString())); + + const notificationFn = new lambda.Function(this, 'SendECRImageInspectorFindings', { + runtime: lambda.Runtime.PYTHON_3_11, + handler: 'main.lambda_handler', + code: lambda.Code.fromAsset(path.join(__dirname, 'image-scanning-notifications-lambda-handler')), + environment: {'SNS_ARN': topic.topicArn,}, + }); + + const snsTopicPolicy = new iam.PolicyStatement({ + actions: ['sns:publish'], + resources: ['*'], + }); + + notificationFn.addToRolePolicy(snsTopicPolicy); + + // Only publish CRITICAL and HIGH findings (more than 7.0 CVE score) that are ACTIVE + // https://docs.aws.amazon.com/inspector/latest/user/findings-understanding-severity.html + const rule = new events.Rule(this, 'rule', { + eventPattern: { + source: ['aws.inspector2'], + detail: { + severity: ['HIGH', 'CRITICAL'], + status: events.Match.exactString('ACTIVE') + }, + detailType: events.Match.exactString('Inspector2 Finding'), + }, + }); + + rule.addTarget(new targets.LambdaFunction(notificationFn)) + } +} \ No newline at end of file diff --git a/lib/finch-pipeline-app-stage.ts b/lib/finch-pipeline-app-stage.ts index 196d012..9848df8 100644 --- a/lib/finch-pipeline-app-stage.ts +++ b/lib/finch-pipeline-app-stage.ts @@ -9,6 +9,7 @@ import { ContinuousIntegrationStack } from './continuous-integration-stack'; import { ECRRepositoryStack } from './ecr-repo-stack'; import { PVREReportingStack } from './pvre-reporting-stack'; import { RunnerProps } from '../config/runner-config'; +import { EventBridgeScanNotifsStack } from './event-bridge-scan-notifs-stack'; export enum ENVIRONMENT_STAGE { Beta, @@ -57,6 +58,11 @@ export class FinchPipelineAppStage extends cdk.Stage { this.ecrRepositoryOutput = ecrRepositoryStack.repositoryOutput; this.ecrRepository = ecrRepositoryStack.repository; + // Only report rootfs image scans in prod to avoid duplicate notifications. + if (props.environmentStage == ENVIRONMENT_STAGE.Prod) { + new EventBridgeScanNotifsStack(this, 'EventBridgeScanNotifsStack', this.stageName) + } + new ContinuousIntegrationStack( this, 'FinchContinuousIntegrationStack', diff --git a/lib/image-scanning-notifications-lambda-handler/main.py b/lib/image-scanning-notifications-lambda-handler/main.py new file mode 100644 index 0000000..ca92390 --- /dev/null +++ b/lib/image-scanning-notifications-lambda-handler/main.py @@ -0,0 +1,65 @@ +''' +lambda function to read ECR Image Inpsection events from Amazon EventBridge +and send notifications to Finch team regarding security notifications. +''' +import os +import boto3 +import json + +def build_message(event): + '''build_message reads an {event} from Inspector image scanning and builds + the body of reporting email with vulnerability findings. + + :param events.EventBridgeEvent event: The EventBridgeEvent containing an Inspector scan finding. + Schema: https://docs.aws.amazon.com/inspector/latest/user/eventbridge-integration.html#event-finding + ''' + detail = event['detail'] + title = detail['title'] + description = detail['description'] + severity = detail['severity'] + source_url = detail['packageVulnerabilityDetails']['sourceUrl'] + status = detail['status'] + type = detail['type'] + finding_arn = detail['findingArn'] + first_observed_at = detail['firstObservedAt'] + + message = f'''{title} - Severity {severity} + + Severity: {severity} + Type: {type} + Description: {description} + Source URL: {source_url} + + Status: {status} + Observed: {first_observed_at} + + For more info, view the finding via ARN in the AWS Console: {finding_arn} + ''' + + return message + +def send_sns(subject, message): + '''send_sns sends an email with subject and body + + :param str subject: The subject of the email + :param str message: The body of the email + ''' + client = boto3.client("sns") + topic_arn = os.environ["SNS_ARN"] + client.publish(TopicArn=topic_arn, Message=message, Subject=subject) + +def lambda_handler(event, context): + detailType= event["detail-type"] + + if (detailType == "Inspector2 Finding"): + message = build_message(event) + else: + print("No findings found, skipping sending email") + return {'statusCode': 200} + + subject = "Rootfs Image Security Finding" + message = build_message(event) + send_sns(subject, message) + + return {'statusCode': 200} + \ No newline at end of file diff --git a/test/event-bridge-scan-notifs-stack.test.ts b/test/event-bridge-scan-notifs-stack.test.ts new file mode 100644 index 0000000..a17e734 --- /dev/null +++ b/test/event-bridge-scan-notifs-stack.test.ts @@ -0,0 +1,49 @@ +import * as cdk from 'aws-cdk-lib'; +import { Template, Match } from 'aws-cdk-lib/assertions'; +import { EventBridgeScanNotifsStack } from '../lib/event-bridge-scan-notifs-stack'; + +describe('EventBridgeScanNotifsStack', () => { + test('synthesizes the way we expect', () => { + const app = new cdk.App(); + const eventBridgeStack = new EventBridgeScanNotifsStack(app, 'EventBridgeScanNotifsStack', 'test'); + + const template = Template.fromStack(eventBridgeStack); + + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResource('AWS::Lambda::Function', { + Properties: { + Environment:{ + Variables: { + "SNS_ARN": Match.anyValue() + } + }, + Runtime: "python3.11", + }, + }); + + const lambda = template.findResources('AWS::Lambda::Function') + const lambdaLogicalID = Object.keys(lambda)[0] + + template.resourceCountIs('AWS::SNS::Topic', 1); + + template.resourceCountIs('AWS::Events::Rule', 1); + template.hasResource('AWS::Events::Rule', { + Properties: { + EventPattern: { + source: ["aws.inspector2"] + }, + State: "ENABLED", + Targets: [ + { + "Arn":{ + "Fn::GetAtt": [ + lambdaLogicalID, + "Arn" + ] + } + } + ], + } + }); + }); +})