Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions securityhub-finding-sfn-remediation-cdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# AWS Security Hub Auto-Remediation with AWS Step Functions

This pattern deploys an automated remediation pipeline for AWS Security Hub findings. When HIGH or CRITICAL findings are detected, Amazon EventBridge routes them to an AWS Step Functions workflow that classifies the finding type and executes targeted remediation via AWS Lambda — closing open security groups, blocking public S3 bucket access, and marking findings as resolved.

Important: This is the first AWS Security Hub pattern in the serverless-patterns repo. Every AWS account with Security Hub enabled accumulates findings that require manual triage. This pattern automates the response for common CIS benchmark violations.

Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/securityhub-finding-sfn-remediation-cdk

## Architecture

```
┌───────────────────┐ ┌────────────────────┐ ┌────────────────────────────────────┐
│ AWS Security Hub │────▶│ Amazon EventBridge │────▶│ AWS Step Functions │
│ (HIGH/CRITICAL) │ │ (Finding Rule) │ │ (Classify + Route) │
└───────────────────┘ └────────────────────┘ └─────────────┬──────────────────────┘
┌──────────────┼──────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌────────────┐
│ Open SG │ │ Public │ │ Unsupported│
│ → Revoke │ │ Bucket │ │ → Skip │
│ ingress │ │ → Block │ └────────────┘
└────┬─────┘ └────┬─────┘
│ │
▼ ▼
┌──────────────────────────┐
│ Amazon SNS (Alert) │
│ + Update Finding Status │
└──────────────────────────┘
```

**How it works:**

1. AWS Security Hub detects a HIGH or CRITICAL finding (e.g., open security group, public Amazon S3 bucket)
2. Amazon EventBridge matches the finding and triggers the AWS Step Functions workflow
3. AWS Step Functions classifies the finding type and routes to the appropriate remediation path
4. AWS Lambda executes the remediation:
- **Open Security Group:** Revokes 0.0.0.0/0 ingress rules and tags the resource
- **Public Amazon S3 Bucket:** Enables full public access block
5. AWS Lambda updates the finding status to RESOLVED in AWS Security Hub
6. Amazon SNS delivers a notification to the security team with remediation details

## Requirements

- [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) installed and configured
- [Node.js 20+](https://nodejs.org/) with npm
- AWS account [bootstrapped for CDK](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html)
- AWS Security Hub enabled in your account
- Python 3.12 (for AWS Lambda functions)

## Deployment

```bash
cd securityhub-finding-sfn-remediation-cdk/cdk
npm install
npx cdk deploy
```

## Testing

### Generate a sample Security Hub finding

```bash
# Create a deliberately open security group to trigger a finding
SG_ID=$(aws ec2 create-security-group \
--group-name test-open-sg \
--description "Test open SG for remediation" \
--query 'GroupId' --output text)

aws ec2 authorize-security-group-ingress \
--group-id $SG_ID \
--protocol tcp --port 22 --cidr 0.0.0.0/0

echo "Created open SG: $SG_ID — Security Hub will detect this within 15 minutes"
```

### Verify remediation after Security Hub detects the finding

```bash
# Check Step Functions executions
SFN_ARN=$(aws cloudformation describe-stacks \
--stack-name SecurityhubFindingSfnRemediationStack \
--query 'Stacks[0].Outputs[?OutputKey==`StateMachineArn`].OutputValue' \
--output text)

aws stepfunctions list-executions \
--state-machine-arn $SFN_ARN --max-results 5 \
--query 'executions[].{Status:status,Start:startDate}'

# Verify the security group was closed
aws ec2 describe-security-groups --group-ids $SG_ID \
--query 'SecurityGroups[0].IpPermissions'
```

### Subscribe to alerts

```bash
TOPIC_ARN=$(aws cloudformation describe-stacks \
--stack-name SecurityhubFindingSfnRemediationStack \
--query 'Stacks[0].Outputs[?OutputKey==`AlertTopicArn`].OutputValue' \
--output text)

aws sns subscribe --topic-arn $TOPIC_ARN --protocol email \
--notification-endpoint security-team@example.com
```

## Cleanup

> **Warning:** After destroying this stack, Security Hub findings will no longer be auto-remediated.

```bash
cd securityhub-finding-sfn-remediation-cdk/cdk
npx cdk destroy
```

## Services Used

| Service | Role |
|---------|------|
| AWS Security Hub | Detects misconfigurations and compliance violations |
| Amazon EventBridge | Routes HIGH/CRITICAL findings to the remediation workflow |
| AWS Step Functions | Classifies finding type and orchestrates remediation |
| AWS Lambda | Executes remediation actions (revoke SG rules, block S3 access) |
| Amazon SNS | Delivers remediation alerts to the security team |

----
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
6 changes: 6 additions & 0 deletions securityhub-finding-sfn-remediation-cdk/cdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
cdk.out
cdk.context.json
build
*.js
*.d.ts
11 changes: 11 additions & 0 deletions securityhub-finding-sfn-remediation-cdk/cdk/bin/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env node
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0 (2026)
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { SecurityhubFindingSfnRemediationStack } from '../lib/securityhub-finding-sfn-remediation-stack';

const app = new cdk.App();
new SecurityhubFindingSfnRemediationStack(app, 'SecurityhubFindingSfnRemediationStack', {
description: 'AWS Security Hub auto-remediation with AWS Step Functions (uksb-1tupboc57)',
});
1 change: 1 addition & 0 deletions securityhub-finding-sfn-remediation-cdk/cdk/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"app":"npx ts-node --prefer-ts-exts bin/app.ts"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0 (2026)

import * as cdk from 'aws-cdk-lib';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
import * as sfn from 'aws-cdk-lib/aws-stepfunctions';
import * as sfnTasks from 'aws-cdk-lib/aws-stepfunctions-tasks';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as logs from 'aws-cdk-lib/aws-logs';
import { Construct } from 'constructs';
import * as path from 'path';

export class SecurityhubFindingSfnRemediationStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);

const region = cdk.Stack.of(this).region;
const account = cdk.Stack.of(this).account;

// =========================================================
// 1. Amazon SNS Topic (security team notifications)
// =========================================================
const alertTopic = new sns.Topic(this, 'SecurityAlertTopic', {
topicName: 'securityhub-remediation-alerts',
displayName: 'AWS Security Hub - Auto-Remediation Alerts',
});

// =========================================================
// 2. AWS Lambda: Remediation Handler
// =========================================================
const remediateFn = new lambda.Function(this, 'RemediateFunction', {
runtime: lambda.Runtime.PYTHON_3_12,
handler: 'handler.lambda_handler',
code: lambda.Code.fromAsset(path.join(__dirname, '../../lambdas/remediate')),
timeout: cdk.Duration.seconds(120),
memorySize: 256,
description: 'Remediates AWS Security Hub findings: closes open SGs, enables encryption, enforces MFA',
});

// EC2/S3/IAM permissions for remediation actions
remediateFn.addToRolePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'ec2:RevokeSecurityGroupIngress',
'ec2:DescribeSecurityGroups',
'ec2:CreateTags',
],
resources: [`arn:aws:ec2:${region}:${account}:security-group/*`],
}));

remediateFn.addToRolePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
's3:PutBucketPublicAccessBlock',
's3:PutBucketEncryption',
],
resources: [`arn:aws:s3:::*`],
conditions: {
StringEquals: { 'aws:ResourceAccount': account },
},
}));

remediateFn.addToRolePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['securityhub:BatchUpdateFindings'],
resources: [`arn:aws:securityhub:${region}:${account}:hub/default`],
}));

// =========================================================
// 3. AWS Step Functions: Remediation Workflow
// =========================================================

// Remediate via Lambda
const remediateTask = new sfnTasks.LambdaInvoke(this, 'RemediateFinding', {
lambdaFunction: remediateFn,
outputPath: '$.Payload',
comment: 'Execute auto-remediation for the finding type',
});

// Notify security team
const notifyTask = new sfnTasks.SnsPublish(this, 'NotifySecurityTeam', {
topic: alertTopic,
subject: sfn.JsonPath.format(
'SecurityHub: {} remediated ({})',
sfn.JsonPath.stringAt('$.remediationType'),
sfn.JsonPath.stringAt('$.status'),
),
message: sfn.TaskInput.fromJsonPathAt('$'),
resultPath: '$.notification',
});

// Skip state for unsupported findings
const skipUnsupported = new sfn.Pass(this, 'UnsupportedFinding', {
result: sfn.Result.fromObject({ status: 'SKIPPED', reason: 'Finding type not supported for auto-remediation' }),
});

// Done
const done = new sfn.Pass(this, 'RemediationComplete', {
result: sfn.Result.fromObject({ status: 'COMPLETE' }),
});

// Route by finding type
const definition = new sfn.Choice(this, 'ClassifyFinding')
.when(
sfn.Condition.or(
sfn.Condition.stringMatches('$.detail.findings[0].Type', '*EC2*SecurityGroup*'),
sfn.Condition.stringMatches('$.detail.findings[0].Type', '*Software and Configuration*'),
),
remediateTask.next(notifyTask).next(done),
)
.when(
sfn.Condition.stringMatches('$.detail.findings[0].Type', '*S3*PublicAccess*'),
new sfnTasks.LambdaInvoke(this, 'RemediateS3Finding', {
lambdaFunction: remediateFn,
outputPath: '$.Payload',
}).next(new sfnTasks.SnsPublish(this, 'NotifyS3Remediation', {
topic: alertTopic,
subject: sfn.JsonPath.format(
'SecurityHub: {} remediated ({})',
sfn.JsonPath.stringAt('$.remediationType'),
sfn.JsonPath.stringAt('$.status'),
),
message: sfn.TaskInput.fromJsonPathAt('$'),
resultPath: '$.notification',
})).next(new sfn.Pass(this, 'S3Done', {
result: sfn.Result.fromObject({ status: 'COMPLETE' }),
})),
)
.otherwise(skipUnsupported);

const stateMachine = new sfn.StateMachine(this, 'RemediationWorkflow', {
definitionBody: sfn.DefinitionBody.fromChainable(definition),
timeout: cdk.Duration.minutes(10),
tracingEnabled: true,
logs: {
destination: new logs.LogGroup(this, 'SfnLogGroup', {
logGroupName: '/aws/stepfunctions/securityhub-remediation',
retention: logs.RetentionDays.TWO_WEEKS,
removalPolicy: cdk.RemovalPolicy.DESTROY,
}),
level: sfn.LogLevel.ALL,
},
});

// =========================================================
// 4. Amazon EventBridge Rule: Security Hub findings
// =========================================================
new events.Rule(this, 'SecurityHubFindingRule', {
ruleName: 'securityhub-high-findings-to-sfn',
description: 'Routes HIGH/CRITICAL AWS Security Hub findings to AWS Step Functions for auto-remediation',
eventPattern: {
source: ['aws.securityhub'],
detailType: ['Security Hub Findings - Imported'],
detail: {
findings: {
Severity: { Label: ['HIGH', 'CRITICAL'] },
Workflow: { Status: ['NEW'] },
RecordState: ['ACTIVE'],
},
},
},
targets: [new targets.SfnStateMachine(stateMachine)],
});

// =========================================================
// Outputs
// =========================================================
new cdk.CfnOutput(this, 'StateMachineArn', {
value: stateMachine.stateMachineArn,
description: 'AWS Step Functions remediation workflow ARN',
});

new cdk.CfnOutput(this, 'AlertTopicArn', {
value: alertTopic.topicArn,
description: 'Amazon SNS security alert topic ARN',
});

new cdk.CfnOutput(this, 'RemediateFunctionArn', {
value: remediateFn.functionArn,
description: 'AWS Lambda remediation function ARN',
});
}
}
12 changes: 12 additions & 0 deletions securityhub-finding-sfn-remediation-cdk/cdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "securityhub-finding-sfn-remediation-cdk",
"version": "1.0.0",
"bin": { "app": "bin/app.ts" },
"scripts": { "build": "tsc", "synth": "cdk synth" },
"dependencies": {
"aws-cdk-lib": "2.185.0",
"constructs": "^10.0.0",
"source-map-support": "^0.5.21"
},
"devDependencies": { "typescript": "~5.4.0", "ts-node": "^10.9.0" }
}
1 change: 1 addition & 0 deletions securityhub-finding-sfn-remediation-cdk/cdk/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"compilerOptions":{"target":"ES2020","module":"commonjs","lib":["es2020"],"declaration":true,"strict":true,"noImplicitAny":true,"strictNullChecks":true,"noImplicitReturns":true,"inlineSourceMap":true,"inlineSources":true,"experimentalDecorators":true,"strictPropertyInitialization":false,"outDir":"./build","rootDir":"."},"exclude":["node_modules","build"]}
Loading