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
105 changes: 105 additions & 0 deletions macie-sfn-s3-quarantine-cdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Amazon Macie to AWS Step Functions to Amazon S3 Quarantine

This pattern deploys an automated sensitive data response pipeline that uses Amazon Macie to detect sensitive data in Amazon S3, routes findings through Amazon EventBridge to AWS Step Functions for severity-based classification, and automatically quarantines high-severity objects to a separate Amazon S3 bucket while notifying security teams via Amazon SNS.

Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/macie-sfn-s3-quarantine-cdk

Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example.

## Requirements

* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources.
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured
* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [Node.js 18+](https://nodejs.org/en/download/) installed
* [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) installed and bootstrapped

## Deployment Instructions

1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository:
```bash
git clone https://github.com/aws-samples/serverless-patterns
```
2. Change directory to the pattern directory:
```bash
cd serverless-patterns/macie-sfn-s3-quarantine-cdk/cdk
```
3. Install dependencies:
```bash
npm install
```
4. Deploy the stack:
```bash
npx cdk deploy --parameters NotificationEmail=your-email@example.com
```
5. Confirm the SNS subscription email you receive.

## How it works

This pattern creates an automated security response pipeline for sensitive data detected by Amazon Macie:

1. **Amazon Macie** continuously scans objects in the monitored Amazon S3 bucket for sensitive data (PII, credentials, financial data).
2. When sensitive data is found, Macie publishes a finding to **Amazon EventBridge**.
3. The Amazon EventBridge rule matches `SensitiveData` finding types and triggers the **AWS Step Functions** state machine.
4. The state machine classifies the finding by severity score:
- **HIGH (score ≥ 7)**: Quarantine the object (copy to quarantine bucket, delete from source) + notify
- **MEDIUM (score 4-6)**: Tag the object with finding metadata + notify
- **LOW (score < 4)**: Notify only (no remediation action)
5. **Amazon SNS** delivers notifications to the security team with finding details.

## Architecture

```
Amazon Macie (scan) --> Amazon EventBridge (finding) --> AWS Step Functions (classify + quarantine)
|
+--> AWS Lambda (move/tag object) --> Amazon S3 (quarantine)
|
+--> Amazon SNS (alert)
```

## Testing

1. Upload a file containing sensitive data (e.g., credit card numbers, SSNs) to the monitored bucket:
```bash
# Create a test file with sample sensitive data
echo "Name: John Doe, SSN: 123-45-6789, Card: 4111-1111-1111-1111" > /tmp/sensitive-test.txt

# Upload to the monitored bucket
aws s3 cp /tmp/sensitive-test.txt s3://macie-monitored-<ACCOUNT_ID>-<REGION>/test/sensitive-test.txt
```

2. Wait for Amazon Macie to scan the object (findings are published every 15 minutes by default, or trigger a one-time classification job):
```bash
# Create a one-time classification job for faster testing
aws macie2 create-classification-job \
--job-type ONE_TIME \
--name "test-scan" \
--s3-job-definition '{"bucketDefinitions": [{"accountId": "<ACCOUNT_ID>", "buckets": ["macie-monitored-<ACCOUNT_ID>-<REGION>"]}]}'
```

3. Monitor the Step Functions execution:
```bash
aws stepfunctions list-executions \
--state-machine-arn <StateMachineArn from stack outputs> \
--status-filter SUCCEEDED
```

4. Verify quarantine (for high-severity findings):
```bash
aws s3 ls s3://macie-quarantine-<ACCOUNT_ID>-<REGION>/quarantined/ --recursive
```

## Cleanup

> **Warning**: Destroying this stack will delete all objects in both the monitored and quarantine buckets. Ensure you have backed up any data you need before proceeding.

```bash
npx cdk destroy
```

## Resources

- [Amazon Macie documentation](https://docs.aws.amazon.com/macie/latest/user/)
- [Amazon Macie finding types](https://docs.aws.amazon.com/macie/latest/user/findings-types.html)
- [AWS Step Functions documentation](https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html)
- [Amazon EventBridge event patterns](https://docs.aws.amazon.com/eventbridge/latest/userguide/filtering-examples-structure.html)
7 changes: 7 additions & 0 deletions macie-sfn-s3-quarantine-cdk/cdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
cdk.out
cdk.context.json
build
*.js
*.d.ts
!jest.config.js
15 changes: 15 additions & 0 deletions macie-sfn-s3-quarantine-cdk/cdk/bin/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env node
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0
// 2026

import * as cdk from 'aws-cdk-lib';
import { MacieSfnS3QuarantineStack } from '../lib/macie-sfn-s3-quarantine-stack';

const app = new cdk.App();
new MacieSfnS3QuarantineStack(app, 'MacieSfnS3QuarantineStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
});
6 changes: 6 additions & 0 deletions macie-sfn-s3-quarantine-cdk/cdk/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"app": "npx ts-node --prefer-ts-exts bin/app.ts",
"context": {
"@aws-cdk/customresources:installLatestAwsSdkDefault": false
}
}
219 changes: 219 additions & 0 deletions macie-sfn-s3-quarantine-cdk/cdk/lib/macie-sfn-s3-quarantine-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0
// 2026

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

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

// Email parameter for SNS notifications
const notificationEmail = new cdk.CfnParameter(this, 'NotificationEmail', {
type: 'String',
description: 'Email address to receive sensitive data finding notifications',
default: 'security-team@example.com',
});

// Source bucket (monitored by Amazon Macie)
const sourceBucket = new s3.Bucket(this, 'MonitoredBucket', {
bucketName: `macie-monitored-${cdk.Aws.ACCOUNT_ID}-${cdk.Aws.REGION}`,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
encryption: s3.BucketEncryption.S3_MANAGED,
versioned: true,
});

// Quarantine bucket (where sensitive objects are moved)
const quarantineBucket = new s3.Bucket(this, 'QuarantineBucket', {
bucketName: `macie-quarantine-${cdk.Aws.ACCOUNT_ID}-${cdk.Aws.REGION}`,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
encryption: s3.BucketEncryption.S3_MANAGED,
versioned: true,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});

// SNS topic for notifications
const notificationTopic = new sns.Topic(this, 'FindingNotificationTopic', {
topicName: 'MacieSensitiveDataFindings',
displayName: 'Amazon Macie Sensitive Data Findings',
});

notificationTopic.addSubscription(
new sns_subscriptions.EmailSubscription(notificationEmail.valueAsString)
);

// Lambda function to quarantine S3 objects
const quarantineFunction = new lambda.Function(this, 'QuarantineFunction', {
runtime: lambda.Runtime.PYTHON_3_12,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, '../src/quarantine-handler')),
timeout: cdk.Duration.seconds(60),
environment: {
QUARANTINE_BUCKET: quarantineBucket.bucketName,
},
});

// Grant the Lambda function read from source and write to quarantine
sourceBucket.grantRead(quarantineFunction);
sourceBucket.grantDelete(quarantineFunction);
quarantineBucket.grantWrite(quarantineFunction);

// Also allow tagging the source object
quarantineFunction.addToRolePolicy(new iam.PolicyStatement({
actions: ['s3:PutObjectTagging'],
resources: [sourceBucket.arnForObjects('*')],
}));

// Step Functions state machine
// Step 1: Classify severity
const classifySeverity = new sfn.Choice(this, 'ClassifySeverity')
.when(
sfn.Condition.numberGreaterThanEquals('$.detail.severity.score', 7),
new sfn.Pass(this, 'HighSeverity', {
result: sfn.Result.fromObject({ action: 'quarantine', severity: 'HIGH' }),
resultPath: '$.classification',
})
)
.when(
sfn.Condition.numberGreaterThanEquals('$.detail.severity.score', 4),
new sfn.Pass(this, 'MediumSeverity', {
result: sfn.Result.fromObject({ action: 'tag_and_notify', severity: 'MEDIUM' }),
resultPath: '$.classification',
})
)
.otherwise(
new sfn.Pass(this, 'LowSeverity', {
result: sfn.Result.fromObject({ action: 'notify_only', severity: 'LOW' }),
resultPath: '$.classification',
})
);

// Step 2: Quarantine the object (move to quarantine bucket)
const quarantineObject = new tasks.LambdaInvoke(this, 'QuarantineObject', {
lambdaFunction: quarantineFunction,
payload: sfn.TaskInput.fromObject({
'bucketName.$': '$.detail.resourcesAffected.s3Bucket.name',
'objectKey.$': '$.detail.resourcesAffected.s3Object.key',
'findingId.$': '$.detail.id',
'severity.$': '$.detail.severity.description',
}),
resultPath: '$.quarantineResult',
});

// Step 3: Send SNS notification
const sendNotification = new tasks.SnsPublish(this, 'SendNotification', {
topic: notificationTopic,
subject: sfn.JsonPath.format(
'Macie Finding: Sensitive data detected [{}]',
sfn.JsonPath.stringAt('$.detail.severity.description')
),
message: sfn.TaskInput.fromObject({
'findingId.$': '$.detail.id',
'findingType.$': '$.detail.type',
'severity.$': '$.detail.severity.description',
'bucket.$': '$.detail.resourcesAffected.s3Bucket.name',
'objectKey.$': '$.detail.resourcesAffected.s3Object.key',
'detectedDataTypes.$': '$.detail.classificationDetails.result.sensitiveData[*].category',
'action.$': '$.classification.action',
}),
resultPath: '$.notificationResult',
});

// Step 4: Tag source object (for medium severity - don't quarantine but mark it)
const tagObject = new tasks.LambdaInvoke(this, 'TagObject', {
lambdaFunction: quarantineFunction,
payload: sfn.TaskInput.fromObject({
'bucketName.$': '$.detail.resourcesAffected.s3Bucket.name',
'objectKey.$': '$.detail.resourcesAffected.s3Object.key',
'findingId.$': '$.detail.id',
'severity.$': '$.detail.severity.description',
'tagOnly': true,
}),
resultPath: '$.tagResult',
});

// Wire the state machine
const highSeverityChain = quarantineObject.next(sendNotification);
const mediumSeverityChain = tagObject.next(sendNotification);
const lowSeverityChain = sendNotification;

// After classification, route to appropriate action
const routeAction = new sfn.Choice(this, 'RouteAction')
.when(
sfn.Condition.stringEquals('$.classification.action', 'quarantine'),
highSeverityChain
)
.when(
sfn.Condition.stringEquals('$.classification.action', 'tag_and_notify'),
mediumSeverityChain
)
.otherwise(lowSeverityChain);

// Build the full definition
const definition = classifySeverity.afterwards().next(routeAction);

const stateMachine = new sfn.StateMachine(this, 'MacieResponseStateMachine', {
stateMachineName: 'MacieSensitiveDataResponse',
definitionBody: sfn.DefinitionBody.fromChainable(definition),
timeout: cdk.Duration.minutes(5),
});

// EventBridge rule: Macie finding published
const macieRule = new events.Rule(this, 'MacieFindingRule', {
ruleName: 'MacieSensitiveDataFinding',
description: 'Routes Amazon Macie sensitive data findings to Step Functions for automated response',
eventPattern: {
source: ['aws.macie'],
detailType: ['Macie Finding'],
detail: {
type: [{ prefix: 'SensitiveData' }],
},
},
});

macieRule.addTarget(new events_targets.SfnStateMachine(stateMachine));

// Enable Macie (creates a session if not already enabled)
const macieSession = new cdk.CfnResource(this, 'MacieSession', {
type: 'AWS::Macie::Session',
properties: {
FindingPublishingFrequency: 'FIFTEEN_MINUTES',
Status: 'ENABLED',
},
});

// Outputs
new cdk.CfnOutput(this, 'MonitoredBucketName', {
value: sourceBucket.bucketName,
description: 'Upload files here to be scanned by Amazon Macie',
});

new cdk.CfnOutput(this, 'QuarantineBucketName', {
value: quarantineBucket.bucketName,
description: 'High-severity sensitive data objects are moved here',
});

new cdk.CfnOutput(this, 'StateMachineArn', {
value: stateMachine.stateMachineArn,
description: 'Step Functions state machine ARN for Macie response automation',
});

new cdk.CfnOutput(this, 'NotificationTopicArn', {
value: notificationTopic.topicArn,
description: 'SNS topic for finding notifications',
});
}
}
24 changes: 24 additions & 0 deletions macie-sfn-s3-quarantine-cdk/cdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "macie-sfn-s3-quarantine-cdk",
"version": "1.0.0",
"description": "Amazon Macie sensitive data finding triggers AWS Step Functions to quarantine objects in Amazon S3",
"bin": {
"app": "bin/app.ts"
},
"scripts": {
"build": "tsc",
"synth": "cdk synth",
"deploy": "cdk deploy",
"destroy": "cdk destroy"
},
"dependencies": {
"aws-cdk-lib": "^2.185.0",
"constructs": "^10.0.0"
},
"devDependencies": {
"typescript": "~5.4.0",
"@types/node": "^20.0.0",
"ts-node": "^10.9.0",
"aws-cdk": "^2.185.0"
}
}
Loading