From 0ee9cdd9261a5af7d7885650f0c15ad1b497d552 Mon Sep 17 00:00:00 2001 From: Sahil Silare Date: Sun, 22 Mar 2026 21:33:56 +0530 Subject: [PATCH 1/5] Add CloudFormation IAM activity alerts with security fixes --- README.md | 273 ++++++++++++++++++++++++++++++- deploy.sh | 258 +++++++++++++++++++++++++++++ iam-alerts-cloudformation.yaml | 290 +++++++++++++++++++++++++++++++++ parameters-example.json | 18 ++ 4 files changed, 838 insertions(+), 1 deletion(-) create mode 100755 deploy.sh create mode 100644 iam-alerts-cloudformation.yaml create mode 100644 parameters-example.json diff --git a/README.md b/README.md index 1d3c2dc..d436d50 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,273 @@ # aws-iam-activity-alerts -CloudFormation template for monitoring IAM user creation and access key activities with real-time alerts via email (SNS) and Slack. + +CloudFormation template for monitoring IAM user creation and access key activities with real-time alerts via email (SNS) and/or Slack. + +## Overview + +This template sets up automated alerts for critical IAM events: +- **CreateUser** - New IAM user creation +- **CreateAccessKey** - Access key generation +- **CreateLoginProfile** - Console access enablement + +Notifications are delivered via email (SNS) and/or Slack, enabling security teams to monitor unauthorized or unexpected IAM activity in real time. + +## Use Cases + +- **Security Monitoring** - Detect unauthorized IAM user creation in real-time +- **Compliance** - Maintain audit trail for IAM changes +- **Team Awareness** - Keep security teams informed of IAM activities +- **Multi-Account Deployment** - Standardize alerting across AWS Organization + +## Files + +| File | Description | +|------|-------------| +| `iam-alerts-cloudformation.yaml` | Main CloudFormation template with EventBridge, SNS, and Lambda | +| `deploy.sh` | Interactive deployment script with validation | +| `parameters-example.json` | Sample parameters file for easy deployment | + +## Prerequisites + +- AWS account with CloudTrail enabled (management events logging) +- AWS CLI installed and configured +- (Optional) Slack Incoming Webhook URL + +## Quick Start + +### Interactive Script + +```bash +./deploy.sh +``` + +The script prompts for notification method (email, Slack, or both), excluded usernames, and rule name, then deploys the CloudFormation stack. + +### AWS CLI + +Email notifications: +```bash +aws cloudformation create-stack \ + --stack-name iam-activity-alerts \ + --template-body file://iam-alerts-cloudformation.yaml \ + --parameters \ + ParameterKey=NotificationEmail,ParameterValue=security@example.com \ + ParameterKey=ExcludedUserNames,ParameterValue="service-account1,service-account2" \ + --capabilities CAPABILITY_IAM +``` + +Slack notifications: +```bash +aws cloudformation create-stack \ + --stack-name iam-activity-alerts \ + --template-body file://iam-alerts-cloudformation.yaml \ + --parameters \ + ParameterKey=SlackWebhookUrl,ParameterValue=https://hooks.slack.com/services/YOUR/WEBHOOK/URL \ + ParameterKey=ExcludedUserNames,ParameterValue="service-account1,service-account2" \ + --capabilities CAPABILITY_IAM +``` + +Both email and Slack: +```bash +aws cloudformation create-stack \ + --stack-name iam-activity-alerts \ + --template-body file://iam-alerts-cloudformation.yaml \ + --parameters \ + ParameterKey=NotificationEmail,ParameterValue=security@example.com \ + ParameterKey=SlackWebhookUrl,ParameterValue=https://hooks.slack.com/services/YOUR/WEBHOOK/URL \ + ParameterKey=ExcludedUserNames,ParameterValue="service-account1,service-account2" \ + --capabilities CAPABILITY_IAM +``` + +### Parameters File + +Copy `parameters-example.json`, fill in your values, then: + +```bash +aws cloudformation create-stack \ + --stack-name iam-activity-alerts \ + --template-body file://iam-alerts-cloudformation.yaml \ + --parameters file://parameters.json \ + --capabilities CAPABILITY_IAM +``` + +## Configuration Parameters + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `NotificationEmail` | Email for SNS alerts | (empty) | +| `SlackWebhookUrl` | Slack webhook URL | (empty) | +| `ExcludedUserNames` | Comma-separated usernames to exclude | (empty) | +| `AlertRuleName` | EventBridge rule name | `iam-user-creation-alert` | + +## Architecture + +1. **EventBridge Rule** - Monitors CloudTrail for IAM events +2. **SNS Topic** (optional) - Email notifications with formatted messages +3. **Lambda Function** (optional) - Slack integration with color-coded alerts +4. **IAM Roles** - Minimal permissions for Lambda execution + +## Setting Up Slack Webhooks + +1. Visit [https://api.slack.com/apps](https://api.slack.com/apps) and click "Create New App" > "From scratch" +2. Under "Incoming Webhooks", toggle "Activate Incoming Webhooks" to ON +3. Click "Add New Webhook to Workspace", select your channel (e.g., `#security-alerts`), and click "Allow" +4. Copy the webhook URL (format: `https://hooks.slack.com/services/T.../B.../xxx`) + +Test it: +```bash +curl -X POST -H 'Content-type: application/json' \ + --data '{"text":"IAM Alerts test message"}' \ + https://hooks.slack.com/services/YOUR/WEBHOOK/URL +``` + +## Email Subscription Confirmation + +After deploying with an email address, AWS sends a confirmation email from `no-reply@sns.amazonaws.com`. You must click the confirmation link within 3 days before alerts will be delivered. + +Verify subscription status: +```bash +aws sns list-subscriptions-by-topic \ + --topic-arn $(aws cloudformation describe-stacks \ + --stack-name iam-activity-alerts \ + --query 'Stacks[0].Outputs[?OutputKey==`SNSTopicArn`].OutputValue' \ + --output text) +``` + +## Alert Information + +Each alert includes: +- Event type and timestamp +- Target username +- AWS Account ID and region +- Principal who performed the action (ARN) +- Source IP address + +## Testing + +```bash +# Create a test user (will trigger alert) +aws iam create-user --user-name test-alert-user + +# Wait 1-2 minutes for the alert, then clean up +aws iam delete-user --user-name test-alert-user +``` + +Ensure `test-alert-user` is not in your excluded users list. + +## Sharing Across Teams + +**Option A: Share via S3 presigned URL** +```bash +# Upload to a private bucket +aws s3 cp iam-alerts-cloudformation.yaml s3://your-templates-bucket/ + +# Generate a time-limited presigned URL (valid 7 days) +aws s3 presign s3://your-templates-bucket/iam-alerts-cloudformation.yaml \ + --expires-in 604800 +``` + +**Option B: CloudFormation StackSets** for organization-wide deployment — see [AWS StackSets documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/what-is-cfnstacksets.html). + +**Option C: Fork or clone this repository** — all files are self-contained. + +## Security Notes + +- **Slack webhook URL**: Marked `NoEcho` to prevent display in the CloudFormation console and CLI output. However, `NoEcho` does not prevent access via `lambda:GetFunctionConfiguration` — anyone with that permission can read the environment variable. For higher-security environments, store the webhook URL in [SSM Parameter Store (SecureString)](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html) or [AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html) and retrieve it at Lambda runtime instead. +- Lambda has minimal IAM permissions (CloudWatch Logs only) +- SNS topic policy restricts publishing to EventBridge only +- No credentials stored or transmitted + +## Troubleshooting + +### No email alerts received + +1. Check subscription status (see [Email Subscription Confirmation](#email-subscription-confirmation) above) +2. Check spam/junk folder — add `no-reply@sns.amazonaws.com` to contacts +3. Verify email address in stack parameters: + ```bash + aws cloudformation describe-stacks \ + --stack-name iam-activity-alerts \ + --query 'Stacks[0].Parameters[?ParameterKey==`NotificationEmail`].ParameterValue' \ + --output text + ``` + +### No Slack alerts received + +1. Test the webhook directly: + ```bash + curl -X POST -H 'Content-type: application/json' \ + --data '{"text":"Test message"}' YOUR_WEBHOOK_URL + ``` +2. Check Lambda logs: + ```bash + aws logs tail /aws/lambda/iam-user-creation-alert-slack-notifier --follow + ``` + +### No alerts at all + +1. Verify CloudTrail is enabled and logging management events: + ```bash + aws cloudtrail get-trail-status --name YOUR_TRAIL_NAME + ``` +2. Check EventBridge rule is enabled: + ```bash + aws events describe-rule --name iam-user-creation-alert + ``` + Look for `"State": "ENABLED"`. +3. Verify targets: + ```bash + aws events list-targets-by-rule --rule iam-user-creation-alert + ``` + +### Excluded users still triggering alerts + +Check the parameter value (case-sensitive): +```bash +aws cloudformation describe-stacks \ + --stack-name iam-activity-alerts \ + --query 'Stacks[0].Parameters[?ParameterKey==`ExcludedUserNames`].ParameterValue' \ + --output text +``` + +## Customization + +### Add more excluded users + +```bash +aws cloudformation update-stack \ + --stack-name iam-activity-alerts \ + --use-previous-template \ + --parameters \ + ParameterKey=ExcludedUserNames,ParameterValue="user1,user2,user3" \ + ParameterKey=NotificationEmail,UsePreviousValue=true \ + ParameterKey=SlackWebhookUrl,UsePreviousValue=true \ + ParameterKey=AlertRuleName,UsePreviousValue=true +``` + +### Monitor additional IAM events + +Edit the `eventName` list in `iam-alerts-cloudformation.yaml`: + +```yaml +eventName: + - CreateUser + - CreateAccessKey + - CreateLoginProfile + - DeleteUser # add as needed + - DeleteAccessKey # add as needed +``` + +## Cleanup + +```bash +aws cloudformation delete-stack --stack-name iam-activity-alerts +``` + +## Cost + +Expected: **< $1/month** for typical usage + +- EventBridge: No charge for rules; minimal per-event cost +- Lambda (Slack): Free tier eligible +- SNS (email): First 1,000 emails/month free +- CloudWatch Logs: Minimal charges diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..6f41d0f --- /dev/null +++ b/deploy.sh @@ -0,0 +1,258 @@ +#!/bin/bash + +# IAM Activity Alerts - CloudFormation Deployment Script +# This script helps deploy the IAM alerts stack to your AWS account + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Default values +STACK_NAME="iam-activity-alerts" +TEMPLATE_FILE="iam-alerts-cloudformation.yaml" +REGION="" + +# Function to print colored output +print_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to check if AWS CLI is installed +check_aws_cli() { + if ! command -v aws &> /dev/null; then + print_error "AWS CLI is not installed. Please install it first." + echo "Visit: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" + exit 1 + fi + print_info "AWS CLI found: $(aws --version)" +} + +# Function to check if CloudTrail is enabled +check_cloudtrail() { + print_info "Checking if CloudTrail is enabled..." + local trails + trails=$(aws cloudtrail describe-trails --query 'trailList[?IsMultiRegionTrail==`true`]' --output json 2>/dev/null) + + if [ "$trails" == "[]" ]; then + print_warning "No multi-region CloudTrail found. The alerts require CloudTrail to be enabled." + print_warning "Please ensure CloudTrail is configured to log management events." + else + print_info "CloudTrail is enabled." + fi +} + +# Function to validate email +validate_email() { + local email=$1 + if [[ $email =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then + return 0 + else + return 1 + fi +} + +# Function to get user input +get_parameters() { + echo "" + echo "==========================================" + echo "IAM Activity Alerts - Configuration" + echo "==========================================" + echo "" + + # Stack name + read -r -p "Stack name [${STACK_NAME}]: " input + STACK_NAME=${input:-$STACK_NAME} + + # AWS Region + if [ -z "$REGION" ]; then + REGION=$(aws configure get region 2>/dev/null || echo "us-east-1") + fi + read -r -p "AWS Region [${REGION}]: " input + REGION=${input:-$REGION} + + # Notification method + echo "" + echo "Choose notification method:" + echo "1) Email only" + echo "2) Slack only" + echo "3) Both email and Slack" + read -r -p "Selection [1-3]: " notification_choice + + EMAIL="" + WEBHOOK="" + + case $notification_choice in + 1) + while true; do + read -r -p "Email address: " EMAIL + if validate_email "$EMAIL"; then + break + else + print_error "Invalid email format. Please try again." + fi + done + ;; + 2) + read -r -p "Slack Webhook URL: " WEBHOOK + ;; + 3) + while true; do + read -r -p "Email address: " EMAIL + if validate_email "$EMAIL"; then + break + else + print_error "Invalid email format. Please try again." + fi + done + read -r -p "Slack Webhook URL: " WEBHOOK + ;; + *) + print_error "Invalid selection" + exit 1 + ;; + esac + + # Excluded usernames + echo "" + read -r -p "Excluded usernames (comma-separated, leave empty for none): " EXCLUDED_USERS + + # Alert rule name + read -r -p "Alert rule name [iam-user-creation-alert]: " input + ALERT_RULE_NAME=${input:-iam-user-creation-alert} +} + +# Function to build parameters array +build_parameters() { + PARAMS=( + "ParameterKey=ExcludedUserNames,ParameterValue=${EXCLUDED_USERS}" + "ParameterKey=AlertRuleName,ParameterValue=${ALERT_RULE_NAME}" + ) + + if [ -n "$EMAIL" ]; then + PARAMS+=("ParameterKey=NotificationEmail,ParameterValue=${EMAIL}") + else + PARAMS+=("ParameterKey=NotificationEmail,ParameterValue=") + fi + + if [ -n "$WEBHOOK" ]; then + PARAMS+=("ParameterKey=SlackWebhookUrl,ParameterValue=${WEBHOOK}") + else + PARAMS+=("ParameterKey=SlackWebhookUrl,ParameterValue=") + fi +} + +# Function to deploy stack +deploy_stack() { + print_info "Deploying CloudFormation stack: ${STACK_NAME}" + print_info "Region: ${REGION}" + + # Check if stack exists + if aws cloudformation describe-stacks --stack-name "$STACK_NAME" --region "$REGION" &>/dev/null; then + print_warning "Stack already exists. Updating..." + OPERATION="update-stack" + else + print_info "Creating new stack..." + OPERATION="create-stack" + fi + + # Deploy using array to avoid eval/injection risk + aws cloudformation "$OPERATION" \ + --stack-name "$STACK_NAME" \ + --template-body file://"$TEMPLATE_FILE" \ + --parameters "${PARAMS[@]}" \ + --capabilities CAPABILITY_IAM \ + --region "$REGION" + + print_info "Stack deployment initiated successfully!" + print_info "Waiting for stack to complete..." + + if [ "$OPERATION" == "create-stack" ]; then + aws cloudformation wait stack-create-complete --stack-name "$STACK_NAME" --region "$REGION" + else + aws cloudformation wait stack-update-complete --stack-name "$STACK_NAME" --region "$REGION" 2>/dev/null || true + fi + + print_info "Stack deployment completed!" + + if [ -n "$EMAIL" ]; then + echo "" + print_warning "IMPORTANT: Check your email and confirm the SNS subscription!" + fi + + echo "" + print_info "Stack outputs:" + aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --region "$REGION" \ + --query 'Stacks[0].Outputs' \ + --output table +} + +# Function to show summary +show_summary() { + echo "" + echo "==========================================" + echo "Deployment Summary" + echo "==========================================" + echo "Stack Name: ${STACK_NAME}" + echo "Region: ${REGION}" + echo "Email: ${EMAIL:-Not configured}" + echo "Slack: ${WEBHOOK:+Configured}${WEBHOOK:-Not configured}" + echo "Excluded Users: ${EXCLUDED_USERS:-None}" + echo "Alert Rule: ${ALERT_RULE_NAME}" + echo "==========================================" + echo "" + read -r -p "Proceed with deployment? (yes/no): " confirm + + if [ "$confirm" != "yes" ] && [ "$confirm" != "y" ]; then + print_warning "Deployment cancelled." + exit 0 + fi +} + +# Main execution +main() { + echo "" + echo "╔════════════════════════════════════════╗" + echo "║ IAM Activity Alerts Deployment ║" + echo "║ CloudFormation Stack Setup ║" + echo "╚════════════════════════════════════════╝" + echo "" + + check_aws_cli + check_cloudtrail + get_parameters + build_parameters + show_summary + deploy_stack + + echo "" + print_info "Deployment complete!" + echo "" + echo "Next steps:" + echo "1. If using email, confirm the SNS subscription" + echo "2. Test the alert by creating a test IAM user" + echo "3. Monitor CloudWatch Logs for any issues" + echo "" + echo "To test:" + echo " aws iam create-user --user-name test-alert-user" + echo "" + echo "To delete the stack:" + echo " aws cloudformation delete-stack --stack-name ${STACK_NAME} --region ${REGION}" + echo "" +} + +# Run main function +main diff --git a/iam-alerts-cloudformation.yaml b/iam-alerts-cloudformation.yaml new file mode 100644 index 0000000..e10b2cc --- /dev/null +++ b/iam-alerts-cloudformation.yaml @@ -0,0 +1,290 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: 'IAM Activity Monitoring - Alerts for CreateUser, CreateAccessKey, and CreateLoginProfile events' + +Parameters: + NotificationEmail: + Type: String + Description: Email address to receive IAM activity alerts (leave empty if using Slack only) + Default: '' + + SlackWebhookUrl: + Type: String + Description: > + Slack webhook URL for notifications (leave empty if using email only). + NOTE: NoEcho prevents display in the console, but the value is still readable + via lambda:GetFunctionConfiguration. For higher-security environments, store + this in SSM Parameter Store (SecureString) or Secrets Manager and retrieve + it at Lambda runtime instead. + Default: '' + NoEcho: true + + ExcludedUserNames: + Type: CommaDelimitedList + Description: Comma-separated list of usernames to exclude from alerts (e.g. service accounts) + Default: '' + + AlertRuleName: + Type: String + Description: Name for the EventBridge rule + Default: 'iam-user-creation-alert' + +Conditions: + UseEmail: !Not [!Equals [!Ref NotificationEmail, '']] + UseSlack: !Not [!Equals [!Ref SlackWebhookUrl, '']] + HasNotifications: !Or [!Condition UseEmail, !Condition UseSlack] + +Resources: + # SNS Topic for email notifications + IAMAlertTopic: + Type: AWS::SNS::Topic + Condition: UseEmail + Properties: + TopicName: !Sub '${AlertRuleName}-topic' + DisplayName: IAM Activity Alerts + Subscription: + - Endpoint: !Ref NotificationEmail + Protocol: email + + # SNS Topic Policy + IAMAlertTopicPolicy: + Type: AWS::SNS::TopicPolicy + Condition: UseEmail + Properties: + Topics: + - !Ref IAMAlertTopic + PolicyDocument: + Version: '2012-10-17' + Statement: + - Sid: AllowEventBridgeToPublish + Effect: Allow + Principal: + Service: events.amazonaws.com + Action: 'sns:Publish' + Resource: !Ref IAMAlertTopic + + # Lambda function for Slack notifications + SlackNotificationFunction: + Type: AWS::Lambda::Function + Condition: UseSlack + Properties: + FunctionName: !Sub '${AlertRuleName}-slack-notifier' + Runtime: python3.12 + Handler: index.lambda_handler + Role: !GetAtt SlackNotificationFunctionRole.Arn + Timeout: 30 + Environment: + Variables: + SLACK_WEBHOOK_URL: !Ref SlackWebhookUrl + Code: + ZipFile: | + import json + import logging + import os + import urllib3 + from datetime import datetime + + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + http = urllib3.PoolManager() + + def lambda_handler(event, context): + webhook_url = os.environ['SLACK_WEBHOOK_URL'] + + # Parse the EventBridge event + detail = event.get('detail', {}) + event_name = detail.get('eventName', 'Unknown') + user_name = detail.get('requestParameters', {}).get('userName', 'Unknown') + event_time = detail.get('eventTime', 'Unknown') + source_ip = detail.get('sourceIPAddress', 'Unknown') + user_identity = detail.get('userIdentity', {}) + principal = user_identity.get('principalId', 'Unknown') + arn = user_identity.get('arn', 'Unknown') + account_id = event.get('account', 'Unknown') + region = event.get('region', 'Unknown') + + # Determine color and emoji based on event type + color_map = { + 'CreateUser': '#FF9900', + 'CreateAccessKey': '#FF6600', + 'CreateLoginProfile': '#FF3300' + } + + emoji_map = { + 'CreateUser': ':bust_in_silhouette:', + 'CreateAccessKey': ':key:', + 'CreateLoginProfile': ':closed_lock_with_key:' + } + + color = color_map.get(event_name, '#FF0000') + emoji = emoji_map.get(event_name, ':warning:') + + # Build Slack message + slack_message = { + 'text': f'{emoji} IAM Activity Alert: {event_name}', + 'attachments': [ + { + 'color': color, + 'title': f'{event_name} Event Detected', + 'fields': [ + { + 'title': 'Target User', + 'value': user_name, + 'short': True + }, + { + 'title': 'Event Time', + 'value': event_time, + 'short': True + }, + { + 'title': 'Account ID', + 'value': account_id, + 'short': True + }, + { + 'title': 'Region', + 'value': region, + 'short': True + }, + { + 'title': 'Source IP', + 'value': source_ip, + 'short': True + }, + { + 'title': 'Principal', + 'value': principal, + 'short': True + }, + { + 'title': 'Principal ARN', + 'value': arn, + 'short': False + } + ], + 'footer': 'AWS CloudTrail', + 'ts': int(datetime.now().timestamp()) + } + ] + } + + # Send to Slack + try: + encoded_data = json.dumps(slack_message).encode('utf-8') + response = http.request( + 'POST', + webhook_url, + body=encoded_data, + headers={'Content-Type': 'application/json'} + ) + if response.status != 200: + logger.error( + 'Slack webhook returned HTTP %s: %s', + response.status, + response.data.decode('utf-8') + ) + return { + 'statusCode': response.status, + 'body': json.dumps(f'Failed to send notification: HTTP {response.status}') + } + except Exception as e: + logger.exception('Failed to send Slack notification: %s', str(e)) + raise + + return { + 'statusCode': 200, + 'body': json.dumps('Notification sent to Slack') + } + + # IAM Role for Lambda + SlackNotificationFunctionRole: + Type: AWS::IAM::Role + Condition: UseSlack + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: 'sts:AssumeRole' + ManagedPolicyArns: + - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' + + # Permission for EventBridge to invoke Lambda + SlackNotificationFunctionPermission: + Type: AWS::Lambda::Permission + Condition: UseSlack + Properties: + FunctionName: !Ref SlackNotificationFunction + Action: 'lambda:InvokeFunction' + Principal: events.amazonaws.com + SourceArn: !GetAtt IAMEventRule.Arn + + # EventBridge Rule + IAMEventRule: + Type: AWS::Events::Rule + Properties: + Name: !Ref AlertRuleName + Description: Alert on IAM user creation and access key/login profile creation + State: ENABLED + EventPattern: + source: + - aws.iam + detail-type: + - AWS API Call via CloudTrail + detail: + eventSource: + - iam.amazonaws.com + eventName: + - CreateUser + - CreateAccessKey + - CreateLoginProfile + requestParameters: + userName: + - anything-but: !Ref ExcludedUserNames + Targets: + - !If + - UseEmail + - Arn: !Ref IAMAlertTopic + Id: email-target + InputTransformer: + InputPathsMap: + eventName: $.detail.eventName + userName: $.detail.requestParameters.userName + eventTime: $.detail.eventTime + sourceIP: $.detail.sourceIPAddress + principalId: $.detail.userIdentity.principalId + arn: $.detail.userIdentity.arn + accountId: $.account + region: $.region + InputTemplate: >- + "IAM Activity Alert\n==================\n\nEvent: \nTarget User: \nTime: \nAccount ID: \nRegion: \n\nPerformed by:\nPrincipal: \nARN: \nSource IP: \n\nThis alert was triggered because a new IAM user, access key, or login profile was created." + - !Ref AWS::NoValue + - !If + - UseSlack + - Arn: !GetAtt SlackNotificationFunction.Arn + Id: slack-target + - !Ref AWS::NoValue + +Outputs: + EventRuleName: + Description: Name of the EventBridge rule + Value: !Ref IAMEventRule + Export: + Name: !Sub '${AWS::StackName}-RuleName' + + SNSTopicArn: + Condition: UseEmail + Description: ARN of the SNS topic for email notifications + Value: !Ref IAMAlertTopic + Export: + Name: !Sub '${AWS::StackName}-TopicArn' + + LambdaFunctionArn: + Condition: UseSlack + Description: ARN of the Lambda function for Slack notifications + Value: !GetAtt SlackNotificationFunction.Arn + Export: + Name: !Sub '${AWS::StackName}-LambdaArn' diff --git a/parameters-example.json b/parameters-example.json new file mode 100644 index 0000000..6686a7d --- /dev/null +++ b/parameters-example.json @@ -0,0 +1,18 @@ +[ + { + "ParameterKey": "NotificationEmail", + "ParameterValue": "security-team@example.com" + }, + { + "ParameterKey": "SlackWebhookUrl", + "ParameterValue": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL" + }, + { + "ParameterKey": "ExcludedUserNames", + "ParameterValue": "service-account1,service-account2" + }, + { + "ParameterKey": "AlertRuleName", + "ParameterValue": "iam-user-creation-alert" + } +] From 8fbb11893a0ccfb6f8f43411a60155d19d1ca10f Mon Sep 17 00:00:00 2001 From: Sahil Silare Date: Wed, 25 Mar 2026 09:49:08 +0530 Subject: [PATCH 2/5] Add teardown script, fix region validation, improve Slack formatting and README setup docs Co-Authored-By: Claude Sonnet 4.6 --- README.md | 90 +++++++++++++++++++++-- deploy.sh | 78 +++++++++++++++++++- iam-alerts-cloudformation.yaml | 14 ++-- teardown.sh | 128 +++++++++++++++++++++++++++++++++ 4 files changed, 296 insertions(+), 14 deletions(-) create mode 100755 teardown.sh diff --git a/README.md b/README.md index d436d50..4400424 100644 --- a/README.md +++ b/README.md @@ -28,19 +28,88 @@ Notifications are delivered via email (SNS) and/or Slack, enabling security team ## Prerequisites -- AWS account with CloudTrail enabled (management events logging) -- AWS CLI installed and configured -- (Optional) Slack Incoming Webhook URL +### 1. AWS CLI + +Install the AWS CLI if you haven't already: + +- **macOS**: `brew install awscli` +- **Linux**: Follow the [official install guide](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) +- **Windows**: Use the MSI installer from the link above + +Verify it's installed: +```bash +aws --version +``` + +### 2. AWS Credentials + +You need valid AWS credentials with permission to create CloudFormation stacks, IAM roles, EventBridge rules, SNS topics, and Lambda functions. + +**Standard credentials** (long-lived access keys): +```bash +aws configure +# Enter your AWS Access Key ID, Secret Access Key, region, and output format +``` + +**Temporary credentials** (AWS SSO, MFA, or assumed roles): + +`aws configure` does not prompt for a session token. If you're using temporary credentials (e.g. copied from the AWS SSO console), paste all three lines individually: +```bash +aws configure set aws_access_key_id YOUR_ACCESS_KEY_ID +aws configure set aws_secret_access_key YOUR_SECRET_ACCESS_KEY +aws configure set aws_session_token YOUR_SESSION_TOKEN +aws configure set region us-east-1 +``` + +Alternatively, the `deploy.sh` script has a built-in credential setup step — you can paste the full credentials block (all three `key=value` lines at once) when prompted. + +Verify your credentials are working: +```bash +aws sts get-caller-identity +``` +You should see your account ID and IAM principal returned. If this fails, your credentials are invalid or expired. + +### 3. CloudTrail + +This solution relies on AWS CloudTrail to capture IAM API calls. Ensure a trail exists in your account that logs **management events**: + +```bash +aws cloudtrail describe-trails --query 'trailList[*].[name,IsMultiRegionTrail,HomeRegion]' --output table +``` + +If no trail exists, create one in the AWS Console under **CloudTrail > Create trail** before deploying. + +### 4. (Optional) Slack Incoming Webhook URL + +Required only if you want Slack notifications. See [Setting Up Slack Webhooks](#setting-up-slack-webhooks) below. + +--- ## Quick Start -### Interactive Script +### Interactive Script (Recommended) + +The easiest way to deploy is using the interactive script: ```bash ./deploy.sh ``` -The script prompts for notification method (email, Slack, or both), excluded usernames, and rule name, then deploys the CloudFormation stack. +The script will walk you through the following steps: + +1. **AWS credentials** — checks if you're already authenticated. If not, prompts you to paste your credentials block. +2. **CloudTrail check** — verifies a multi-region trail is active. +3. **Stack name** — defaults to `iam-activity-alerts`, press Enter to accept. +4. **AWS region** — defaults to your currently configured region (e.g. `us-east-1`). +5. **Notification method** — choose one: + - `1` — Email only (enter your email address) + - `2` — Slack only (enter your webhook URL) + - `3` — Both email and Slack +6. **Excluded usernames** — comma-separated list of IAM usernames that should NOT trigger alerts (e.g. automated service accounts). Leave empty to alert on all users. +7. **Alert rule name** — defaults to `iam-user-creation-alert`. +8. **Confirmation** — review the summary and type `yes` to deploy. + +The script waits for the stack to finish deploying and prints the stack outputs when complete. ### AWS CLI @@ -259,8 +328,17 @@ eventName: ## Cleanup +Use the interactive teardown script to remove all deployed resources: + +```bash +./teardown.sh +``` + +The script lists all stack resources before asking for confirmation, then waits for the deletion to complete. + +Alternatively, delete directly with the AWS CLI: ```bash -aws cloudformation delete-stack --stack-name iam-activity-alerts +aws cloudformation delete-stack --stack-name iam-activity-alerts --region us-east-1 ``` ## Cost diff --git a/deploy.sh b/deploy.sh index 6f41d0f..8086658 100755 --- a/deploy.sh +++ b/deploy.sh @@ -39,6 +39,70 @@ check_aws_cli() { print_info "AWS CLI found: $(aws --version)" } +# Function to configure AWS credentials +configure_aws_credentials() { + echo "" + print_info "Checking AWS credentials..." + + # Check if credentials are already configured + if aws sts get-caller-identity &>/dev/null; then + local identity + identity=$(aws sts get-caller-identity --query 'Arn' --output text 2>/dev/null) + print_info "Already authenticated as: ${identity}" + read -r -p "Do you want to reconfigure credentials? (yes/no) [no]: " reconfigure + if [ "${reconfigure}" != "yes" ] && [ "${reconfigure}" != "y" ]; then + return + fi + else + print_warning "No valid AWS credentials found. Please configure them now." + fi + + echo "" + echo "Paste your AWS credentials block below, then press Enter twice:" + echo "(e.g. from AWS SSO: aws_access_key_id=... / aws_secret_access_key=... / aws_session_token=...)" + echo "" + + aws_access_key_id="" + aws_secret_access_key="" + aws_session_token="" + + while IFS= read -r line; do + [[ -z "$line" ]] && break + key="${line%%=*}" + value="${line#*=}" + case "$key" in + aws_access_key_id) aws_access_key_id="$value" ;; + aws_secret_access_key) aws_secret_access_key="$value" ;; + aws_session_token) aws_session_token="$value" ;; + esac + done + + if [ -z "$REGION" ]; then + REGION=$(get_configured_region) + fi + read -r -p "AWS Region [${REGION}]: " input + REGION=${input:-$REGION} + + aws configure set aws_access_key_id "$aws_access_key_id" + aws configure set aws_secret_access_key "$aws_secret_access_key" + aws configure set region "$REGION" + + if [ -n "$aws_session_token" ]; then + aws configure set aws_session_token "$aws_session_token" + print_info "Session token configured." + fi + + # Verify credentials work + if aws sts get-caller-identity &>/dev/null; then + local identity + identity=$(aws sts get-caller-identity --query 'Arn' --output text 2>/dev/null) + print_info "Successfully authenticated as: ${identity}" + else + print_error "Credential verification failed. Please check your credentials and try again." + exit 1 + fi +} + # Function to check if CloudTrail is enabled check_cloudtrail() { print_info "Checking if CloudTrail is enabled..." @@ -53,6 +117,17 @@ check_cloudtrail() { fi } +# Function to get the configured AWS region, falling back to us-east-1 if invalid +get_configured_region() { + local region + region=$(aws configure get region 2>/dev/null) + if [[ "$region" =~ ^[a-z]{2}-[a-z]+-[0-9]+$ ]]; then + echo "$region" + else + echo "us-east-1" + fi +} + # Function to validate email validate_email() { local email=$1 @@ -77,7 +152,7 @@ get_parameters() { # AWS Region if [ -z "$REGION" ]; then - REGION=$(aws configure get region 2>/dev/null || echo "us-east-1") + REGION=$(get_configured_region) fi read -r -p "AWS Region [${REGION}]: " input REGION=${input:-$REGION} @@ -232,6 +307,7 @@ main() { echo "" check_aws_cli + configure_aws_credentials check_cloudtrail get_parameters build_parameters diff --git a/iam-alerts-cloudformation.yaml b/iam-alerts-cloudformation.yaml index e10b2cc..ecc9319 100644 --- a/iam-alerts-cloudformation.yaml +++ b/iam-alerts-cloudformation.yaml @@ -129,37 +129,37 @@ Resources: 'fields': [ { 'title': 'Target User', - 'value': user_name, + 'value': f'`{user_name}`', 'short': True }, { 'title': 'Event Time', - 'value': event_time, + 'value': f'`{event_time}`', 'short': True }, { 'title': 'Account ID', - 'value': account_id, + 'value': f'`{account_id}`', 'short': True }, { 'title': 'Region', - 'value': region, + 'value': f'`{region}`', 'short': True }, { 'title': 'Source IP', - 'value': source_ip, + 'value': f'`{source_ip}`', 'short': True }, { 'title': 'Principal', - 'value': principal, + 'value': f'`{principal}`', 'short': True }, { 'title': 'Principal ARN', - 'value': arn, + 'value': f'`{arn}`', 'short': False } ], diff --git a/teardown.sh b/teardown.sh new file mode 100755 index 0000000..1e5a5e4 --- /dev/null +++ b/teardown.sh @@ -0,0 +1,128 @@ +#!/bin/bash + +# IAM Activity Alerts - CloudFormation Teardown Script +# This script removes the IAM alerts stack and all associated AWS resources + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Default values +STACK_NAME="iam-activity-alerts" +REGION="" + +print_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to get the configured AWS region, falling back to us-east-1 if invalid +get_configured_region() { + local region + region=$(aws configure get region 2>/dev/null) + if [[ "$region" =~ ^[a-z]{2}-[a-z]+-[0-9]+$ ]]; then + echo "$region" + else + echo "us-east-1" + fi +} + +check_aws_cli() { + if ! command -v aws &> /dev/null; then + print_error "AWS CLI is not installed." + exit 1 + fi + if ! aws sts get-caller-identity &>/dev/null; then + print_error "No valid AWS credentials found. Please configure them first." + exit 1 + fi +} + +get_parameters() { + echo "" + echo "==========================================" + echo "IAM Activity Alerts - Teardown" + echo "==========================================" + echo "" + + read -r -p "Stack name [${STACK_NAME}]: " input + STACK_NAME=${input:-$STACK_NAME} + + if [ -z "$REGION" ]; then + REGION=$(get_configured_region) + fi + read -r -p "AWS Region [${REGION}]: " input + REGION=${input:-$REGION} +} + +delete_stack() { + # Check the stack exists + if ! aws cloudformation describe-stacks --stack-name "$STACK_NAME" --region "$REGION" &>/dev/null; then + print_warning "Stack '${STACK_NAME}' not found in region '${REGION}'. Nothing to delete." + exit 0 + fi + + echo "" + print_warning "This will permanently delete the following CloudFormation stack and all its resources:" + echo " Stack: ${STACK_NAME}" + echo " Region: ${REGION}" + echo "" + print_info "Resources that will be removed:" + aws cloudformation list-stack-resources \ + --stack-name "$STACK_NAME" \ + --region "$REGION" \ + --query 'StackResourceSummaries[*].[ResourceType,LogicalResourceId,ResourceStatus]' \ + --output table 2>/dev/null || true + + echo "" + read -r -p "Are you sure you want to delete this stack? (yes/no): " confirm + if [ "$confirm" != "yes" ] && [ "$confirm" != "y" ]; then + print_warning "Teardown cancelled." + exit 0 + fi + + print_info "Deleting CloudFormation stack: ${STACK_NAME}..." + aws cloudformation delete-stack \ + --stack-name "$STACK_NAME" \ + --region "$REGION" + + print_info "Waiting for stack deletion to complete..." + if aws cloudformation wait stack-delete-complete \ + --stack-name "$STACK_NAME" \ + --region "$REGION" 2>/dev/null; then + print_info "Stack '${STACK_NAME}' deleted successfully." + else + print_error "Stack deletion may have failed. Check the CloudFormation console for details." + exit 1 + fi +} + +main() { + echo "" + echo "╔════════════════════════════════════════╗" + echo "║ IAM Activity Alerts Teardown ║" + echo "║ CloudFormation Stack Removal ║" + echo "╚════════════════════════════════════════╝" + echo "" + + check_aws_cli + get_parameters + delete_stack + + echo "" + print_info "Teardown complete." + echo "" +} + +main From dc3acffb42a8c4a4e9fcdba5b90a634f05b9040e Mon Sep 17 00:00:00 2001 From: Sahil Silare Date: Wed, 25 Mar 2026 09:56:03 +0530 Subject: [PATCH 3/5] Improve post-deployment instructions with full test coverage and teardown script reference Co-Authored-By: Claude Sonnet 4.6 --- deploy.sh | 61 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/deploy.sh b/deploy.sh index 8086658..1490fef 100755 --- a/deploy.sh +++ b/deploy.sh @@ -317,16 +317,61 @@ main() { echo "" print_info "Deployment complete!" echo "" - echo "Next steps:" - echo "1. If using email, confirm the SNS subscription" - echo "2. Test the alert by creating a test IAM user" - echo "3. Monitor CloudWatch Logs for any issues" + echo "==========================================" + echo "Next Steps" + echo "==========================================" + echo "" + if [ -n "$EMAIL" ]; then + echo "1. Confirm your SNS email subscription" + echo " AWS sent a confirmation email to: ${EMAIL}" + echo " You MUST click the confirmation link before alerts will be delivered." + echo "" + fi + echo "2. Verify the alert pipeline works end-to-end" + echo " Each command below triggers a monitored IAM event, which flows through:" + echo " CloudTrail -> EventBridge rule -> Lambda -> SNS/Slack notification" + echo "" + echo " --- Test 1: CreateUser ---" + echo " Triggers an alert when a new IAM user is created." + echo " aws iam create-user --user-name test-alert-user" + echo "" + echo " --- Test 2: CreateAccessKey ---" + echo " Triggers an alert when programmatic credentials are issued for a user." + echo " aws iam create-access-key --user-name test-alert-user" + echo "" + echo " --- Test 3: CreateLoginProfile ---" + echo " Triggers an alert when console (password) access is enabled for a user." + echo " aws iam create-login-profile --user-name test-alert-user --password 'TempPass123!' --no-password-reset-required" + echo "" + echo " --- Cleanup (run after testing) ---" + echo " Delete the access key (get the key ID from the CreateAccessKey output):" + echo " aws iam delete-access-key --user-name test-alert-user --access-key-id " + echo " Delete the login profile:" + echo " aws iam delete-login-profile --user-name test-alert-user" + echo " Delete the test user:" + echo " aws iam delete-user --user-name test-alert-user" + echo "" + echo "3. Check CloudWatch Logs if you don't receive an alert" + echo " The Lambda function logs to:" + echo " /aws/lambda/${ALERT_RULE_NAME}" + echo "" + echo " View recent logs:" + echo " aws logs tail /aws/lambda/${ALERT_RULE_NAME} --follow --region ${REGION}" + echo "" + echo "==========================================" + echo "Stack Management" + echo "==========================================" + echo "" + echo "View stack status:" + echo " aws cloudformation describe-stacks --stack-name ${STACK_NAME} --region ${REGION} --query 'Stacks[0].StackStatus'" echo "" - echo "To test:" - echo " aws iam create-user --user-name test-alert-user" + echo "Tear down all resources:" + echo " Use the included teardown script for a guided removal (recommended):" + echo " ./teardown.sh" echo "" - echo "To delete the stack:" - echo " aws cloudformation delete-stack --stack-name ${STACK_NAME} --region ${REGION}" + echo " Or manually via AWS CLI:" + echo " aws cloudformation delete-stack --stack-name ${STACK_NAME} --region ${REGION}" + echo " aws cloudformation wait stack-delete-complete --stack-name ${STACK_NAME} --region ${REGION}" echo "" } From 82c2817bc53a27ee2d50f9d7b23c6486d3228aa7 Mon Sep 17 00:00:00 2001 From: Sahil Silare Date: Wed, 25 Mar 2026 10:00:31 +0530 Subject: [PATCH 4/5] Add post-deletion verification to teardown script with CloudWatch log group cleanup Co-Authored-By: Claude Sonnet 4.6 --- teardown.sh | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/teardown.sh b/teardown.sh index 1e5a5e4..7b601f9 100755 --- a/teardown.sh +++ b/teardown.sh @@ -108,6 +108,55 @@ delete_stack() { fi } +verify_deletion() { + echo "" + print_info "Verifying all resources have been removed..." + local all_clean=true + + # 1. Confirm the CloudFormation stack is gone + if aws cloudformation describe-stacks --stack-name "$STACK_NAME" --region "$REGION" &>/dev/null; then + print_error "CloudFormation stack '${STACK_NAME}' still exists. Check the console for stuck resources." + all_clean=false + else + print_info "CloudFormation stack removed." + fi + + # 2. Check for leftover CloudWatch Log Groups (Lambda creates these at runtime; they are not owned by the stack) + local log_groups + log_groups=$(aws logs describe-log-groups \ + --log-group-name-prefix "/aws/lambda/" \ + --region "$REGION" \ + --query "logGroups[?contains(logGroupName, '${STACK_NAME}') || contains(logGroupName, 'iam-')].logGroupName" \ + --output text 2>/dev/null) + + if [ -n "$log_groups" ]; then + print_warning "The following CloudWatch Log Groups were not removed (Lambda creates these outside of CloudFormation):" + for lg in $log_groups; do + echo " ${lg}" + done + echo "" + read -r -p "Delete these log groups? (yes/no) [no]: " delete_logs + if [ "$delete_logs" == "yes" ] || [ "$delete_logs" == "y" ]; then + for lg in $log_groups; do + aws logs delete-log-group --log-group-name "$lg" --region "$REGION" + print_info "Deleted log group: ${lg}" + done + else + print_warning "Log groups left in place. You can delete them manually in the CloudWatch console." + fi + else + print_info "No leftover CloudWatch Log Groups found." + fi + + # 3. Summary + echo "" + if [ "$all_clean" = true ]; then + print_info "All stack resources have been removed successfully." + else + print_warning "Some resources may still exist. Review the output above." + fi +} + main() { echo "" echo "╔════════════════════════════════════════╗" @@ -119,6 +168,7 @@ main() { check_aws_cli get_parameters delete_stack + verify_deletion echo "" print_info "Teardown complete." From 548f6166ccc48b9a8e7788d489035d6884765551 Mon Sep 17 00:00:00 2001 From: Sahil Silare Date: Fri, 17 Apr 2026 08:23:45 +0530 Subject: [PATCH 5/5] Address PR review: fix Lambda retry, add CFN validation rule, fix credential handling, fix log group scope, add webhook validation --- deploy.sh | 36 +++++++++++++++++++++++++--------- iam-alerts-cloudformation.yaml | 30 ++++++++++++++-------------- teardown.sh | 28 ++++++++++++++++++++------ 3 files changed, 64 insertions(+), 30 deletions(-) diff --git a/deploy.sh b/deploy.sh index 1490fef..30508bb 100755 --- a/deploy.sh +++ b/deploy.sh @@ -83,12 +83,10 @@ configure_aws_credentials() { read -r -p "AWS Region [${REGION}]: " input REGION=${input:-$REGION} - aws configure set aws_access_key_id "$aws_access_key_id" - aws configure set aws_secret_access_key "$aws_secret_access_key" - aws configure set region "$REGION" - + export AWS_ACCESS_KEY_ID="$aws_access_key_id" + export AWS_SECRET_ACCESS_KEY="$aws_secret_access_key" if [ -n "$aws_session_token" ]; then - aws configure set aws_session_token "$aws_session_token" + export AWS_SESSION_TOKEN="$aws_session_token" print_info "Session token configured." fi @@ -121,7 +119,7 @@ check_cloudtrail() { get_configured_region() { local region region=$(aws configure get region 2>/dev/null) - if [[ "$region" =~ ^[a-z]{2}-[a-z]+-[0-9]+$ ]]; then + if [[ "$region" =~ ^[a-z]{2}(-[a-z]+)+-[0-9]+$ ]]; then echo "$region" else echo "us-east-1" @@ -180,7 +178,14 @@ get_parameters() { done ;; 2) - read -r -p "Slack Webhook URL: " WEBHOOK + while true; do + read -r -p "Slack Webhook URL: " WEBHOOK + if [[ "$WEBHOOK" =~ ^https://hooks\.slack\.com/services/ ]]; then + break + else + print_error "Invalid Slack webhook URL. Must start with https://hooks.slack.com/services/" + fi + done ;; 3) while true; do @@ -191,7 +196,14 @@ get_parameters() { print_error "Invalid email format. Please try again." fi done - read -r -p "Slack Webhook URL: " WEBHOOK + while true; do + read -r -p "Slack Webhook URL: " WEBHOOK + if [[ "$WEBHOOK" =~ ^https://hooks\.slack\.com/services/ ]]; then + break + else + print_error "Invalid Slack webhook URL. Must start with https://hooks.slack.com/services/" + fi + done ;; *) print_error "Invalid selection" @@ -256,7 +268,13 @@ deploy_stack() { if [ "$OPERATION" == "create-stack" ]; then aws cloudformation wait stack-create-complete --stack-name "$STACK_NAME" --region "$REGION" else - aws cloudformation wait stack-update-complete --stack-name "$STACK_NAME" --region "$REGION" 2>/dev/null || true + aws cloudformation wait stack-update-complete --stack-name "$STACK_NAME" --region "$REGION" 2>/dev/null + STATUS=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" --region "$REGION" \ + --query 'Stacks[0].StackStatus' --output text) + if [[ "$STATUS" != "UPDATE_COMPLETE" ]]; then + print_error "Stack update failed. Status: ${STATUS}" + exit 1 + fi fi print_info "Stack deployment completed!" diff --git a/iam-alerts-cloudformation.yaml b/iam-alerts-cloudformation.yaml index ecc9319..464c42b 100644 --- a/iam-alerts-cloudformation.yaml +++ b/iam-alerts-cloudformation.yaml @@ -33,6 +33,14 @@ Conditions: UseSlack: !Not [!Equals [!Ref SlackWebhookUrl, '']] HasNotifications: !Or [!Condition UseEmail, !Condition UseSlack] +Rules: + AtLeastOneNotification: + Assertions: + - Assert: !Or + - !Not [!Equals [!Ref NotificationEmail, '']] + - !Not [!Equals [!Ref SlackWebhookUrl, '']] + AssertDescription: At least one notification method (email or Slack) must be configured + Resources: # SNS Topic for email notifications IAMAlertTopic: @@ -91,6 +99,10 @@ Resources: def lambda_handler(event, context): webhook_url = os.environ['SLACK_WEBHOOK_URL'] + if not webhook_url.startswith('https://hooks.slack.com/'): + logger.error('Invalid webhook URL - must start with https://hooks.slack.com/') + raise RuntimeError('Invalid webhook URL') + # Parse the EventBridge event detail = event.get('detail', {}) event_name = detail.get('eventName', 'Unknown') @@ -179,15 +191,9 @@ Resources: headers={'Content-Type': 'application/json'} ) if response.status != 200: - logger.error( - 'Slack webhook returned HTTP %s: %s', - response.status, - response.data.decode('utf-8') - ) - return { - 'statusCode': response.status, - 'body': json.dumps(f'Failed to send notification: HTTP {response.status}') - } + msg = f'Slack webhook returned HTTP {response.status}: {response.data.decode("utf-8")}' + logger.error(msg) + raise RuntimeError(msg) except Exception as e: logger.exception('Failed to send Slack notification: %s', str(e)) raise @@ -272,19 +278,13 @@ Outputs: EventRuleName: Description: Name of the EventBridge rule Value: !Ref IAMEventRule - Export: - Name: !Sub '${AWS::StackName}-RuleName' SNSTopicArn: Condition: UseEmail Description: ARN of the SNS topic for email notifications Value: !Ref IAMAlertTopic - Export: - Name: !Sub '${AWS::StackName}-TopicArn' LambdaFunctionArn: Condition: UseSlack Description: ARN of the Lambda function for Slack notifications Value: !GetAtt SlackNotificationFunction.Arn - Export: - Name: !Sub '${AWS::StackName}-LambdaArn' diff --git a/teardown.sh b/teardown.sh index 7b601f9..d496502 100755 --- a/teardown.sh +++ b/teardown.sh @@ -14,6 +14,7 @@ NC='\033[0m' # No Color # Default values STACK_NAME="iam-activity-alerts" REGION="" +LAMBDA_FUNCTION_NAME="" print_info() { echo -e "${GREEN}[INFO]${NC} $1" @@ -31,7 +32,7 @@ print_error() { get_configured_region() { local region region=$(aws configure get region 2>/dev/null) - if [[ "$region" =~ ^[a-z]{2}-[a-z]+-[0-9]+$ ]]; then + if [[ "$region" =~ ^[a-z]{2}(-[a-z]+)+-[0-9]+$ ]]; then echo "$region" else echo "us-east-1" @@ -85,6 +86,13 @@ delete_stack() { --query 'StackResourceSummaries[*].[ResourceType,LogicalResourceId,ResourceStatus]' \ --output table 2>/dev/null || true + # Capture the Lambda function name before deletion so we can clean up its log group later + LAMBDA_FUNCTION_NAME=$(aws cloudformation list-stack-resources \ + --stack-name "$STACK_NAME" \ + --region "$REGION" \ + --query "StackResourceSummaries[?ResourceType=='AWS::Lambda::Function'].PhysicalResourceId" \ + --output text 2>/dev/null || echo "") + echo "" read -r -p "Are you sure you want to delete this stack? (yes/no): " confirm if [ "$confirm" != "yes" ] && [ "$confirm" != "y" ]; then @@ -123,11 +131,19 @@ verify_deletion() { # 2. Check for leftover CloudWatch Log Groups (Lambda creates these at runtime; they are not owned by the stack) local log_groups - log_groups=$(aws logs describe-log-groups \ - --log-group-name-prefix "/aws/lambda/" \ - --region "$REGION" \ - --query "logGroups[?contains(logGroupName, '${STACK_NAME}') || contains(logGroupName, 'iam-')].logGroupName" \ - --output text 2>/dev/null) + if [ -n "$LAMBDA_FUNCTION_NAME" ]; then + log_groups=$(aws logs describe-log-groups \ + --log-group-name-prefix "/aws/lambda/${LAMBDA_FUNCTION_NAME}" \ + --region "$REGION" \ + --query "logGroups[].logGroupName" \ + --output text 2>/dev/null) + else + log_groups=$(aws logs describe-log-groups \ + --log-group-name-prefix "/aws/lambda/" \ + --region "$REGION" \ + --query "logGroups[?contains(logGroupName, '${STACK_NAME}')].logGroupName" \ + --output text 2>/dev/null) + fi if [ -n "$log_groups" ]; then print_warning "The following CloudWatch Log Groups were not removed (Lambda creates these outside of CloudFormation):"