01 ยท Building a Self-Healing Multi-Account AWS Landing Zone

Code Repository / projects / 01-self-healing-landing-zone
Project 01 ยท AWS CDK (TypeScript)

Guardrails that fix themselves: an open security group gets closed within a minute or two, with an email explaining what happened.

README

Format: AWS CDK (TypeScript) ยท Est. cost: ~$2โ€“10/month per account (AWS Config evaluations + Lambda)

Preventive + detective + corrective guardrails for an AWS Organization:

Layer What Where
Preventive SCPs: protect CloudTrail/Config/GuardDuty/Security Hub, deny root, restrict regions OrgPolicies stack (management account)
Detective AWS Config managed rules (public S3, open SSH/RDP/DB ports, unencrypted EBS, CloudTrail) SelfHealingGuardrails stack (each account)
Corrective EventBridge โ†’ Lambda remediator โ†’ SNS notice SelfHealingGuardrails stack
AWS Config rule โ”€โ”€NON_COMPLIANTโ”€โ”€โ–ถ EventBridge โ”€โ”€โ–ถ Lambda remediator โ”€โ”€โ–ถ fix resource
                                                          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ SNS (email/Slack)

Prerequisites

  • An AWS Organization (ideally with AWS Control Tower, which enables AWS Config for you)
  • Node.js 18+, AWS CDK v2 (npm i -g aws-cdk), credentials for the target account
  • cdk bootstrap run once per account/region

Deploy

npm install

# 1) Guardrails in a workload account (start in dry-run mode!)
npx cdk deploy SelfHealingGuardrails -c alertEmail=you@example.com -c dryRun=true

# If AWS Config is NOT already on in this account:
npx cdk deploy SelfHealingGuardrails -c alertEmail=you@example.com -c createConfigRecorder=true

# 2) SCPs from the management account
npx cdk deploy OrgPolicies -c targetOuIds=ou-xxxx-aaaaaaaa -c allowedRegions=us-east-1,us-west-2

Confirm the SNS email subscription, then redeploy with -c dryRun=false once you trust the notices.

Demo (the "30-second fix")

SG=$(aws ec2 create-security-group --group-name shlz-demo --description demo --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id $SG --protocol tcp --port 22 --cidr 0.0.0.0/0
# wait for Config to evaluate (usually < 1โ€“2 min), then:
aws ec2 describe-security-groups --group-ids $SG --query 'SecurityGroups[0].IpPermissions'

The ingress rule disappears, the group is tagged self-healing/remediated, and you get an email.

Rolling out to every account

Synthesize once (npx cdk synth SelfHealingGuardrails) and deploy cdk.out/SelfHealingGuardrails.template.json with CloudFormation StackSets (service-managed, auto-deploy to new accounts in the OU).

Clean up

npx cdk destroy --all

Extend it

  • Add more rules + handlers in lambda/remediator.py (e.g., rds-instance-public-access-check)
  • Send notices to Slack with AWS Chatbot subscribed to the SNS topic
  • Aggregate findings org-wide with Security Hub

Files (8)

README.md ยท 60 lines
# 01 ยท Self-Healing Multi-Account Landing Zone

**Format:** AWS CDK (TypeScript) ยท **Est. cost:** ~$2โ€“10/month per account (AWS Config evaluations + Lambda)

Preventive + detective + corrective guardrails for an AWS Organization:

| Layer | What | Where |
|---|---|---|
| Preventive | SCPs: protect CloudTrail/Config/GuardDuty/Security Hub, deny root, restrict regions | `OrgPolicies` stack (management account) |
| Detective | AWS Config managed rules (public S3, open SSH/RDP/DB ports, unencrypted EBS, CloudTrail) | `SelfHealingGuardrails` stack (each account) |
| Corrective | EventBridge โ†’ Lambda remediator โ†’ SNS notice | `SelfHealingGuardrails` stack |

```
AWS Config rule โ”€โ”€NON_COMPLIANTโ”€โ”€โ–ถ EventBridge โ”€โ”€โ–ถ Lambda remediator โ”€โ”€โ–ถ fix resource
                                                          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ SNS (email/Slack)
```

## Prerequisites
- An AWS Organization (ideally with AWS Control Tower, which enables AWS Config for you)
- Node.js 18+, AWS CDK v2 (`npm i -g aws-cdk`), credentials for the target account
- `cdk bootstrap` run once per account/region

## Deploy
```bash
npm install

# 1) Guardrails in a workload account (start in dry-run mode!)
npx cdk deploy SelfHealingGuardrails -c alertEmail=you@example.com -c dryRun=true

# If AWS Config is NOT already on in this account:
npx cdk deploy SelfHealingGuardrails -c alertEmail=you@example.com -c createConfigRecorder=true

# 2) SCPs from the management account
npx cdk deploy OrgPolicies -c targetOuIds=ou-xxxx-aaaaaaaa -c allowedRegions=us-east-1,us-west-2
```
Confirm the SNS email subscription, then redeploy with `-c dryRun=false` once you trust the notices.

## Demo (the "30-second fix")
```bash
SG=$(aws ec2 create-security-group --group-name shlz-demo --description demo --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id $SG --protocol tcp --port 22 --cidr 0.0.0.0/0
# wait for Config to evaluate (usually < 1โ€“2 min), then:
aws ec2 describe-security-groups --group-ids $SG --query 'SecurityGroups[0].IpPermissions'
```
The ingress rule disappears, the group is tagged `self-healing/remediated`, and you get an email.

## Rolling out to every account
Synthesize once (`npx cdk synth SelfHealingGuardrails`) and deploy `cdk.out/SelfHealingGuardrails.template.json`
with **CloudFormation StackSets** (service-managed, auto-deploy to new accounts in the OU).

## Clean up
```bash
npx cdk destroy --all
```

## Extend it
- Add more rules + handlers in `lambda/remediator.py` (e.g., `rds-instance-public-access-check`)
- Send notices to Slack with AWS Chatbot subscribed to the SNS topic
- Aggregate findings org-wide with Security Hub
bin/app.ts ยท 29 lines
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { GuardrailsStack } from '../lib/guardrails-stack';
import { OrgPoliciesStack } from '../lib/org-policies-stack';

const app = new cdk.App();
const env = { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION };

// Deploy into every workload account (directly, or via CDK Pipelines / StackSets).
new GuardrailsStack(app, 'SelfHealingGuardrails', {
  env,
  alertEmail: app.node.tryGetContext('alertEmail'),
  createConfigRecorder: app.node.tryGetContext('createConfigRecorder') === 'true',
  dryRun: app.node.tryGetContext('dryRun') === 'true',
});

// Deploy ONCE in the Organizations management account.
// Example: cdk deploy OrgPolicies -c targetOuIds=ou-abcd-11111111,ou-abcd-22222222 -c allowedRegions=us-east-1,us-west-2
const targetOuIds: string | undefined = app.node.tryGetContext('targetOuIds');
if (targetOuIds) {
  new OrgPoliciesStack(app, 'OrgPolicies', {
    env,
    targetIds: targetOuIds.split(',').map((s) => s.trim()).filter(Boolean),
    allowedRegions: (app.node.tryGetContext('allowedRegions') ?? 'us-east-1,us-west-2')
      .split(',')
      .map((s: string) => s.trim()),
  });
}
lib/guardrails-stack.ts ยท 149 lines
import * as path from 'path';
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as config from 'aws-cdk-lib/aws-config';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as kms from 'aws-cdk-lib/aws-kms';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as subs from 'aws-cdk-lib/aws-sns-subscriptions';

export interface GuardrailsStackProps extends cdk.StackProps {
  /** Where remediation notices go. Optional; you can subscribe Slack/Chatbot later. */
  alertEmail?: string;
  /** Set true only if AWS Config is NOT already enabled (Control Tower enables it for you). */
  createConfigRecorder?: boolean;
  /** When true, the remediator only reports what it would change. */
  dryRun?: boolean;
}

/**
 * Detect -> decide -> fix -> notify.
 * AWS Config evaluates managed rules; EventBridge forwards NON_COMPLIANT results
 * to a Lambda that remediates and publishes a human-readable notice to SNS.
 */
export class GuardrailsStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: GuardrailsStackProps) {
    super(scope, id, props);

    const key = new kms.Key(this, 'AlertsKey', {
      enableKeyRotation: true,
      description: 'Encrypts self-healing guardrail notifications',
    });
    key.grantEncryptDecrypt(new iam.ServicePrincipal('events.amazonaws.com'));

    const topic = new sns.Topic(this, 'RemediationTopic', {
      displayName: 'Self-healing guardrail remediations',
      masterKey: key,
      enforceSSL: true,
    });
    if (props.alertEmail) topic.addSubscription(new subs.EmailSubscription(props.alertEmail));

    // Optional: bootstrap AWS Config in accounts that do not have it yet.
    let recorderDependency: cdk.CfnResource | undefined;
    if (props.createConfigRecorder) {
      const bucket = new s3.Bucket(this, 'ConfigBucket', {
        encryption: s3.BucketEncryption.S3_MANAGED,
        blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
        enforceSSL: true,
        lifecycleRules: [{ expiration: cdk.Duration.days(365) }],
      });
      const role = new iam.Role(this, 'ConfigRole', {
        assumedBy: new iam.ServicePrincipal('config.amazonaws.com'),
        managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWS_ConfigRole')],
      });
      bucket.grantReadWrite(role);
      const recorder = new config.CfnConfigurationRecorder(this, 'Recorder', {
        roleArn: role.roleArn,
        recordingGroup: { allSupported: true, includeGlobalResourceTypes: true },
      });
      const channel = new config.CfnDeliveryChannel(this, 'DeliveryChannel', {
        s3BucketName: bucket.bucketName,
      });
      channel.node.addDependency(recorder);
      recorderDependency = recorder;
    }

    // Managed rules we will auto-remediate (or flag) when they go NON_COMPLIANT.
    const rules: Record<string, config.ManagedRule> = {
      s3Public: new config.ManagedRule(this, 'S3PublicAccess', {
        configRuleName: 'shlz-s3-bucket-level-public-access-prohibited',
        identifier: config.ManagedRuleIdentifiers.S3_BUCKET_LEVEL_PUBLIC_ACCESS_PROHIBITED,
      }),
      ssh: new config.ManagedRule(this, 'RestrictedSsh', {
        configRuleName: 'shlz-restricted-ssh',
        identifier: config.ManagedRuleIdentifiers.EC2_SECURITY_GROUPS_INCOMING_SSH_DISABLED,
      }),
      commonPorts: new config.ManagedRule(this, 'RestrictedCommonPorts', {
        configRuleName: 'shlz-restricted-common-ports',
        identifier: config.ManagedRuleIdentifiers.EC2_SECURITY_GROUPS_RESTRICTED_INCOMING_TRAFFIC,
        inputParameters: { blockedPort1: 3389, blockedPort2: 3306, blockedPort3: 5432, blockedPort4: 1433 },
      }),
      ebs: new config.ManagedRule(this, 'EncryptedVolumes', {
        configRuleName: 'shlz-encrypted-volumes',
        identifier: config.ManagedRuleIdentifiers.EBS_ENCRYPTED_VOLUMES,
      }),
      trail: new config.ManagedRule(this, 'CloudTrailEnabled', {
        configRuleName: 'shlz-cloudtrail-enabled',
        identifier: config.ManagedRuleIdentifiers.CLOUD_TRAIL_ENABLED,
      }),
    };
    if (recorderDependency) {
      Object.values(rules).forEach((r) => r.node.addDependency(recorderDependency!));
    }

    const remediator = new lambda.Function(this, 'Remediator', {
      runtime: lambda.Runtime.PYTHON_3_12,
      handler: 'remediator.handler',
      code: lambda.Code.fromAsset(path.join(__dirname, '..', 'lambda')),
      timeout: cdk.Duration.seconds(60),
      memorySize: 256,
      logGroup: new logs.LogGroup(this, 'RemediatorLogs', {
        retention: logs.RetentionDays.ONE_MONTH,
        removalPolicy: cdk.RemovalPolicy.DESTROY,
      }),
      environment: {
        TOPIC_ARN: topic.topicArn,
        DRY_RUN: props.dryRun ? 'true' : 'false',
        BLOCKED_PORTS: '22,3389,3306,5432,1433',
      },
    });
    topic.grantPublish(remediator);
    remediator.addToRolePolicy(
      new iam.PolicyStatement({
        actions: [
          's3:PutBucketPublicAccessBlock',
          's3:GetBucketPublicAccessBlock',
          'ec2:DescribeSecurityGroups',
          'ec2:RevokeSecurityGroupIngress',
          'ec2:CreateTags',
          'ec2:DescribeVolumes',
          'cloudtrail:DescribeTrails',
          'cloudtrail:StartLogging',
        ],
        resources: ['*'],
      }),
    );

    new events.Rule(this, 'NonCompliantRule', {
      description: 'Route NON_COMPLIANT Config evaluations to the remediator',
      eventPattern: {
        source: ['aws.config'],
        detailType: ['Config Rules Compliance Change'],
        detail: {
          configRuleName: Object.values(rules).map((r) => r.configRuleName),
          newEvaluationResult: { complianceType: ['NON_COMPLIANT'] },
        },
      },
      targets: [new targets.LambdaFunction(remediator, { retryAttempts: 2 })],
    });

    new cdk.CfnOutput(this, 'TopicArn', { value: topic.topicArn });
    new cdk.CfnOutput(this, 'RemediatorName', { value: remediator.functionName });
  }
}
lib/org-policies-stack.ts ยท 105 lines
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as organizations from 'aws-cdk-lib/aws-organizations';

export interface OrgPoliciesStackProps extends cdk.StackProps {
  /** OU (or account) IDs that receive the guardrail SCPs. */
  targetIds: string[];
  /** Regions workloads may use. Global services are exempted. */
  allowedRegions: string[];
}

/**
 * Preventive guardrails (Service Control Policies). Deploy in the management account.
 * The detective + corrective half lives in GuardrailsStack.
 */
export class OrgPoliciesStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: OrgPoliciesStackProps) {
    super(scope, id, props);

    const protectSecurityServices = {
      Version: '2012-10-17',
      Statement: [
        {
          Sid: 'DenyLeavingOrganization',
          Effect: 'Deny',
          Action: 'organizations:LeaveOrganization',
          Resource: '*',
        },
        {
          Sid: 'ProtectAuditTrail',
          Effect: 'Deny',
          Action: ['cloudtrail:StopLogging', 'cloudtrail:DeleteTrail', 'cloudtrail:UpdateTrail'],
          Resource: '*',
          Condition: { ArnNotLike: { 'aws:PrincipalArn': 'arn:aws:iam::*:role/AWSControlTowerExecution' } },
        },
        {
          Sid: 'ProtectConfig',
          Effect: 'Deny',
          Action: [
            'config:DeleteConfigurationRecorder',
            'config:StopConfigurationRecorder',
            'config:DeleteDeliveryChannel',
          ],
          Resource: '*',
          Condition: { ArnNotLike: { 'aws:PrincipalArn': 'arn:aws:iam::*:role/AWSControlTowerExecution' } },
        },
        {
          Sid: 'ProtectGuardDutyAndSecurityHub',
          Effect: 'Deny',
          Action: [
            'guardduty:DeleteDetector',
            'guardduty:DisassociateFromMasterAccount',
            'guardduty:UpdateDetector',
            'securityhub:DisableSecurityHub',
          ],
          Resource: '*',
        },
        {
          Sid: 'DenyRootUser',
          Effect: 'Deny',
          Action: '*',
          Resource: '*',
          Condition: { StringLike: { 'aws:PrincipalArn': 'arn:aws:iam::*:root' } },
        },
      ],
    };

    const regionGuardrail = {
      Version: '2012-10-17',
      Statement: [
        {
          Sid: 'DenyUnapprovedRegions',
          Effect: 'Deny',
          NotAction: [
            'iam:*', 'organizations:*', 'sts:*', 'support:*', 'budgets:*', 'ce:*',
            'cloudfront:*', 'route53:*', 'route53domains:*', 'waf:*', 'wafv2:*', 'shield:*',
            'health:*', 'trustedadvisor:*', 'globalaccelerator:*', 'kms:*', 'sso:*', 'account:*',
          ],
          Resource: '*',
          Condition: {
            StringNotEquals: { 'aws:RequestedRegion': props.allowedRegions },
            ArnNotLike: { 'aws:PrincipalArn': 'arn:aws:iam::*:role/AWSControlTowerExecution' },
          },
        },
      ],
    };

    new organizations.CfnPolicy(this, 'ProtectSecurityServicesScp', {
      name: 'shlz-protect-security-services',
      description: 'Prevents disabling audit and detection services and blocks root user actions',
      type: 'SERVICE_CONTROL_POLICY',
      content: protectSecurityServices,
      targetIds: props.targetIds,
    });

    new organizations.CfnPolicy(this, 'RegionGuardrailScp', {
      name: 'shlz-region-guardrail',
      description: `Restricts regional services to ${props.allowedRegions.join(', ')}`,
      type: 'SERVICE_CONTROL_POLICY',
      content: regionGuardrail,
      targetIds: props.targetIds,
    });
  }
}
lambda/remediator.py ยท 133 lines
"""Self-healing remediator.

Receives "Config Rules Compliance Change" events (NON_COMPLIANT only) and fixes
the offending resource, then publishes a notice to SNS. Set DRY_RUN=true to
only report what would change.
"""
import json
import os

import boto3

TOPIC_ARN = os.environ["TOPIC_ARN"]
DRY_RUN = os.environ.get("DRY_RUN", "false").lower() == "true"
BLOCKED_PORTS = {int(p) for p in os.environ.get("BLOCKED_PORTS", "22").split(",")}
OPEN_CIDRS = {"0.0.0.0/0", "::/0"}

s3 = boto3.client("s3")
ec2 = boto3.client("ec2")
cloudtrail = boto3.client("cloudtrail")
sns = boto3.client("sns")


def handler(event, _context):
    detail = event.get("detail", {})
    rule = detail.get("configRuleName", "")
    resource_type = detail.get("resourceType", "")
    resource_id = detail.get("resourceId", "")
    account = detail.get("awsAccountId", event.get("account"))
    region = detail.get("awsRegion", event.get("region"))

    if rule.endswith("s3-bucket-level-public-access-prohibited"):
        action = fix_s3_public_access(resource_id)
    elif rule.endswith("restricted-ssh") or rule.endswith("restricted-common-ports"):
        action = fix_open_security_group(resource_id)
    elif rule.endswith("encrypted-volumes"):
        action = flag_unencrypted_volume(resource_id)
    elif rule.endswith("cloudtrail-enabled"):
        action = restart_cloudtrail()
    else:
        action = f"No remediation registered for rule {rule}"

    message = {
        "rule": rule,
        "resourceType": resource_type,
        "resourceId": resource_id,
        "account": account,
        "region": region,
        "dryRun": DRY_RUN,
        "action": action,
    }
    print(json.dumps(message))
    sns.publish(
        TopicArn=TOPIC_ARN,
        Subject=f"[self-healing] {rule} -> {resource_id}"[:100],
        Message=json.dumps(message, indent=2),
    )
    return message


def fix_s3_public_access(bucket):
    if DRY_RUN:
        return f"Would enable all four S3 Block Public Access settings on {bucket}"
    s3.put_public_access_block(
        Bucket=bucket,
        PublicAccessBlockConfiguration={
            "BlockPublicAcls": True,
            "IgnorePublicAcls": True,
            "BlockPublicPolicy": True,
            "RestrictPublicBuckets": True,
        },
    )
    return f"Enabled S3 Block Public Access on {bucket}"


def fix_open_security_group(group_id):
    sg = ec2.describe_security_groups(GroupIds=[group_id])["SecurityGroups"][0]
    revoked = []
    for perm in sg.get("IpPermissions", []):
        from_port = perm.get("FromPort", 0)
        to_port = perm.get("ToPort", 65535)
        protocol = perm.get("IpProtocol")
        all_traffic = protocol == "-1"
        if not all_traffic and not any(from_port <= p <= to_port for p in BLOCKED_PORTS):
            continue
        open_v4 = [r for r in perm.get("IpRanges", []) if r.get("CidrIp") in OPEN_CIDRS]
        open_v6 = [r for r in perm.get("Ipv6Ranges", []) if r.get("CidrIpv6") in OPEN_CIDRS]
        if not open_v4 and not open_v6:
            continue
        bad = {"IpProtocol": protocol}
        if not all_traffic:
            bad.update({"FromPort": from_port, "ToPort": to_port})
        if open_v4:
            bad["IpRanges"] = [{"CidrIp": r["CidrIp"]} for r in open_v4]
        if open_v6:
            bad["Ipv6Ranges"] = [{"CidrIpv6": r["CidrIpv6"]} for r in open_v6]
        revoked.append(bad)

    if not revoked:
        return f"{group_id}: no internet-open sensitive ports found (already fixed?)"
    if DRY_RUN:
        return f"Would revoke {len(revoked)} internet-open rule(s) on {group_id}: {revoked}"
    ec2.revoke_security_group_ingress(GroupId=group_id, IpPermissions=revoked)
    ec2.create_tags(
        Resources=[group_id],
        Tags=[{"Key": "self-healing/remediated", "Value": "open-ingress-revoked"}],
    )
    return f"Revoked {len(revoked)} internet-open rule(s) on {group_id}"


def flag_unencrypted_volume(volume_id):
    # EBS volumes cannot be encrypted in place; flag for a snapshot/copy/swap workflow.
    if not DRY_RUN:
        ec2.create_tags(
            Resources=[volume_id],
            Tags=[{"Key": "self-healing/action-required", "Value": "re-create-encrypted"}],
        )
    return (
        f"{volume_id} is unencrypted. Tagged for follow-up: snapshot -> copy with "
        "encryption -> create volume -> swap. Also enable EBS encryption-by-default."
    )


def restart_cloudtrail():
    restarted = []
    for trail in cloudtrail.describe_trails(includeShadowTrails=False)["trailList"]:
        if DRY_RUN:
            restarted.append(trail["Name"])
            continue
        cloudtrail.start_logging(Name=trail["TrailARN"])
        restarted.append(trail["Name"])
    verb = "Would restart" if DRY_RUN else "Restarted"
    return f"{verb} logging on trails: {restarted or 'none found - create an org trail'}"
cdk.json ยท 6 lines
{
  "app": "npx ts-node --prefer-ts-exts bin/app.ts",
  "watch": { "include": ["**"], "exclude": ["README.md", "cdk*.json", "**/*.d.ts", "**/*.js", "node_modules", "cdk.out"] },
  "context": {}
}
package.json ยท 23 lines
{
  "name": "01-self-healing-landing-zone",
  "version": "1.0.0",
  "private": true,
  "bin": { "app": "bin/app.js" },
  "scripts": {
    "build": "tsc",
    "synth": "cdk synth",
    "deploy": "cdk deploy --all",
    "destroy": "cdk destroy --all"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "aws-cdk": "^2.1000.0",
    "ts-node": "^10.9.2",
    "typescript": "~5.6.3"
  },
  "dependencies": {
    "aws-cdk-lib": "^2.200.0",
    "constructs": "^10.4.2"
  }
}
tsconfig.json ยท 14 lines
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["es2022"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "noEmit": true,
    "types": ["node"]
  },
  "exclude": ["node_modules", "cdk.out"]
}

Build it yourself

Download the folder, run npm install (CDK) or aws cloudformation deploy (CloudFormation), and follow the README.