File size: 6,394 Bytes
6ba100e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
"""
email_gatekeeper_stack.py β€” AWS CDK Stack for the Email Gatekeeper.

Resources created:
  - S3 bucket          : receives raw .eml files from SES
  - Lambda function    : classifies each email using the rule-based engine
  - DynamoDB table     : stores every triage result (email_id as partition key)
  - SNS topic          : fires an alert whenever a Security Breach is detected
  - SES receipt rule   : routes inbound email β†’ S3 bucket  (requires verified domain)
  - IAM roles/policies : least-privilege access for Lambda β†’ S3, DynamoDB, SNS

Deploy:
  cd cdk
  pip install aws-cdk-lib constructs
  cdk bootstrap          # first time only per account/region
  cdk deploy
"""

import aws_cdk as cdk
from aws_cdk import (
    Stack,
    Duration,
    RemovalPolicy,
    aws_s3 as s3,
    aws_lambda as lambda_,
    aws_dynamodb as dynamodb,
    aws_sns as sns,
    aws_sns_subscriptions as sns_subs,
    aws_s3_notifications as s3n,
    aws_ses as ses,
    aws_ses_actions as ses_actions,
    aws_iam as iam,
)
from constructs import Construct


class EmailGatekeeperStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # ── 1. S3 bucket β€” stores raw .eml files delivered by SES ─────────────
        email_bucket = s3.Bucket(
            self, "EmailBucket",
            bucket_name=f"email-gatekeeper-inbox-{self.account}",
            # Block all public access β€” emails are private
            block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
            encryption=s3.BucketEncryption.S3_MANAGED,
            # Auto-delete raw emails after 30 days to control storage costs
            lifecycle_rules=[
                s3.LifecycleRule(expiration=Duration.days(30))
            ],
            removal_policy=RemovalPolicy.RETAIN,    # keep emails if stack is deleted
        )

        # ── 2. DynamoDB table β€” persists every triage decision ─────────────────
        results_table = dynamodb.Table(
            self, "EmailResultsTable",
            table_name="EmailTriageResults",
            partition_key=dynamodb.Attribute(
                name="email_id",
                type=dynamodb.AttributeType.STRING,
            ),
            billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,  # serverless billing
            removal_policy=RemovalPolicy.RETAIN,
        )

        # ── 3. SNS topic β€” security breach alerts ─────────────────────────────
        security_topic = sns.Topic(
            self, "SecurityAlertTopic",
            topic_name="EmailGatekeeperSecurityAlerts",
            display_name="Email Gatekeeper β€” Security Breach Alerts",
        )

        # Add your alert email here β€” replace with a real address
        security_topic.add_subscription(
            sns_subs.EmailSubscription("security-team@your-domain.com")
        )

        # ── 4. Lambda function ─────────────────────────────────────────────────
        classifier_fn = lambda_.Function(
            self, "EmailClassifierFn",
            function_name="EmailGatekeeperClassifier",
            runtime=lambda_.Runtime.PYTHON_3_12,
            # Points to the ../lambda/ directory β€” CDK zips it automatically
            code=lambda_.Code.from_asset("../lambda"),
            handler="handler.lambda_handler",
            timeout=Duration.seconds(30),
            memory_size=256,                        # classifier is CPU-light
            environment={
                "EMAIL_RESULTS_TABLE":      results_table.table_name,
                "SECURITY_ALERT_TOPIC_ARN": security_topic.topic_arn,
            },
        )

        # Grant Lambda least-privilege access to each resource
        email_bucket.grant_read(classifier_fn)
        results_table.grant_write_data(classifier_fn)
        security_topic.grant_publish(classifier_fn)

        # ── 5. S3 β†’ Lambda trigger ─────────────────────────────────────────────
        # Fires whenever SES drops a new .eml into the bucket
        email_bucket.add_event_notification(
            s3.EventType.OBJECT_CREATED,
            s3n.LambdaDestination(classifier_fn),
        )

        # ── 6. SES receipt rule β€” routes inbound email to S3 ──────────────────
        # IMPORTANT: your domain must be verified in SES before this works.
        # Replace "mail.your-domain.com" with your actual verified domain.
        rule_set = ses.ReceiptRuleSet(
            self, "EmailRuleSet",
            rule_set_name="EmailGatekeeperRuleSet",
        )

        rule_set.add_rule(
            "StoreInS3Rule",
            recipients=["inbox@mail.your-domain.com"],  # ← replace with your address
            actions=[
                ses_actions.S3(
                    bucket=email_bucket,
                    object_key_prefix="incoming/",      # all emails land under incoming/
                )
            ],
            scan_enabled=True,                          # enable SES spam/virus scanning
        )

        # ── 7. Allow SES to write to the S3 bucket ────────────────────────────
        email_bucket.add_to_resource_policy(
            iam.PolicyStatement(
                sid="AllowSESPuts",
                principals=[iam.ServicePrincipal("ses.amazonaws.com")],
                actions=["s3:PutObject"],
                resources=[email_bucket.arn_for_objects("incoming/*")],
                conditions={
                    "StringEquals": {"aws:SourceAccount": self.account}
                },
            )
        )

        # ── 8. CloudFormation outputs β€” useful after deploy ────────────────────
        cdk.CfnOutput(self, "BucketName",      value=email_bucket.bucket_name)
        cdk.CfnOutput(self, "TableName",       value=results_table.table_name)
        cdk.CfnOutput(self, "LambdaArn",       value=classifier_fn.function_arn)
        cdk.CfnOutput(self, "SecurityTopicArn",value=security_topic.topic_arn)