-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add rootfs image scanning notifications (#403)
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 <[email protected]>
- Loading branch information
Showing
4 changed files
with
171 additions
and
0 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 only in Prod account. | ||
// 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)) | ||
} | ||
} |
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,64 @@ | ||
''' | ||
lambda function to read ECR Image Inpsection events from Amazon EventBridge | ||
and send notifications to Finch team regarding security notifications. | ||
''' | ||
import boto3 | ||
import os | ||
from aws_lambda_powertools.utilities.data_classes import EventBridgeEvent | ||
from aws_lambda_powertools.utilities.typing import LambdaContext | ||
|
||
def build_message(event: EventBridgeEvent): | ||
'''build_message reads an {event} from Inspector image scanning and builds | ||
the body of reporting email with vulnerability findings. | ||
:param 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: str, message: str): | ||
'''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", region="us-west-2") | ||
topic_arn = os.environ["SNS_ARN"] | ||
client.publish(TopicArn=topic_arn, Message=message, Subject=subject) | ||
|
||
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> dict: | ||
detailType = event["detail-type"] | ||
|
||
if (detailType == "Inspector2 Finding"): | ||
subject = "Rootfs Image Security Finding" | ||
message = build_message(event) | ||
send_sns(subject, message) | ||
else: | ||
print("No findings found, skipping sending email") | ||
|
||
return {'statusCode': 200} | ||
|
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,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" | ||
] | ||
} | ||
} | ||
], | ||
} | ||
}); | ||
}); | ||
}) |