AWS Lambda and Cloudtrail Data Events

Sign Up to Build

About this Architecture

Here is some information about this architecture.

How to Build This Solution

Here are the steps you can follow to build this solution on your own.

In this lab, you'll discover how to track and respond to AWS CloudTrail data events using Lambda. You'll learn to log, monitor, and analyze important API calls within your environment. It's an essential skill for security and compliance, helping you ensure that your AWS resources are being used appropriately.

Get Your AWS Credentials

If you're using the Skillmix Labs feature, open the lab settings (the beaker icon) on the right side of the code editor. Then, click the Start Lab button to start hte lab environment.

Wait for the credentials to load. Then run this in the terminal.

Be sure to enter in your own access key and secret key and name your profile 'smx-lab'.

$ aws configure --profile smx-lab
AWS Access Key ID [None]: 
AWS Secret Access Key [None]: 
Default region name [None]: us-west-2
Default output format [None]: 

Note: If you're using your own AWS account you'll need to ensure that you've created and configured a named AWS CLI profile named smx-lab.

Create the required_providers Config

Next, we'll create the required_providers config. This config is used to specify the required provider plugins for our Terraform project. In this case, we are specifying the aws provider from HashiCorp with a version constraint of ~> 4.0.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

Create the aws Config

Next, we'll create the aws config which is used to configure the AWS provider in Terraform. This config specifies the AWS profile to use, which is 'smx-lab', and the region to operate in, which is 'us-west-2'.

provider "aws" {
  profile = "smx-lab"
  region  = "us-west-2"
}

Create the function_names Config

Next, we'll create the function_names config. This config is used to specify the function ARNs that we want to monitor. It should be entered in the form of a list.

variable "function_names" {
  type        = list(string)
  description = "Enter the function ARNs to monitor in the form of a list"
}

Create the slack Config

Next, we'll create the slack config which is used to define the Slack channel URL.

variable "slack" {
  type        = string
  description = "Enter your Slack channel URL"
}

Create the bucket_name Config

Next, we'll create the bucket_name config. This config is used to specify the name of the S3 bucket that will be created. The bucket name should be unique.

variable "bucket_name" {
  type        = string
  description = "Enter a unique S3 bucketname"
}

Create the aws_iam_policy_document Config

Next, we'll create the aws_iam_policy_document config. This config is used to define an IAM policy document that specifies the permissions for an IAM role. In this case, the policy document allows the Lambda service (lambda.amazonaws.com) to assume the role.

data "aws_iam_policy_document" "assume_role" {
  statement {
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
}

Create the aws_caller_identity Config

Next, we'll create the aws_caller_identity config. This config is used to retrieve information about the AWS caller identity, such as the AWS account ID and the ARN of the IAM user or role making the request.

data "aws_caller_identity" "current" {
  # No configuration required
}

Create the aws_iam_policy_document Config

Next, we'll create the aws_iam_policy_document config. This config is used to define an IAM policy document that specifies permissions for AWS CloudTrail to access an S3 bucket.

data "aws_iam_policy_document" "foo" {
  statement {
    sid       = "AWSCloudTrailAclCheck"
    effect    = "Allow"
    principals {
      type        = "Service"
      identifiers = ["cloudtrail.amazonaws.com"]
    }
    actions    = ["s3:GetBucketAcl"]
    resources  = ["${aws_s3_bucket.foo.arn}"]
  }

  statement {
    sid       = "AWSCloudTrailWrite"
    effect    = "Allow"
    principals {
      type        = "Service"
      identifiers = ["cloudtrail.amazonaws.com"]
    }
    actions    = ["s3:PutObject"]
    resources  = ["${aws_s3_bucket.foo.arn}/prefix/AWSLogs/${data.aws_caller_identity.current.account_id}/*"]
    condition {
      test     = "StringEquals"
      variable = "s3:x-amz-acl"
      values   = ["bucket-owner-full-control"]
    }
  }
}

This config creates an IAM policy document with two statements. The first statement allows AWS CloudTrail to perform the s3:GetBucketAcl action on the specified S3 bucket. The second statement allows AWS CloudTrail to perform the s3:PutObject action on objects within the specified S3 bucket path, with the condition that the s3:x-amz-acl variable is set to bucket-owner-full-control.

Create the aws_iam_role Config

Next, we'll create the aws_iam_role config. This config is used to define an IAM role for a Lambda function.

resource "aws_iam_role" "iam_for_lambda" {
  name               = "iam_for_lambda"
  assume_role_policy = "${data.aws_iam_policy_document.assume_role.json}"
}

Create the aws_iam_role_policy Config

Next, we'll create the aws_iam_role_policy config. This config is used to define an IAM role policy in AWS.

resource "aws_iam_role_policy" "test_policy" {
  name  = "test_policy"
  role  = "${aws_iam_role.iam_for_lambda.id}"
  policy = "${jsonencode({
    'Version': '2012-10-17',
    'Statement': [
      {
        'Action': ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'],
        'Effect': 'Allow',
        'Resource': '*'
      }
    ]
  })}"
}

Create the aws_lambda_function Config

Next, we'll create the aws_lambda_function config. This config is used to define an AWS Lambda function.

resource "aws_lambda_function" "test_lambda" {
  filename      = "slacklambda.py.zip"
  function_name = "slacklambda"
  role          = "${aws_iam_role.iam_for_lambda.arn}"
  handler       = "slacklambda.lambda_handler"
  runtime       = "python3.9"
  environment   = [
    {
      variables = {
        url = "${var.slack}"
      }
    }
  ]
}

Create the aws_lambda_permission Config

Next, we'll create the aws_lambda_permission config. This config is used to grant permission to the AWS Lambda function test_lambda to be invoked by the AWS CloudWatch Logs service in the us-east-1 region. The action specified is lambda:InvokeFunction, and the principal is set to logs.us-east-1.amazonaws.com. The source_arn is set to ${aws_cloudwatch_log_group.LambdaInvokeEvents.arn}:*, which allows any log group in the specified region to invoke the Lambda function.

resource "aws_lambda_permission" "logging" {
  action        = "lambda:InvokeFunction"
  function_name = "${aws_lambda_function.test_lambda.function_name}"
  principal     = "logs.us-east-1.amazonaws.com"
  source_arn    = "${aws_cloudwatch_log_group.LambdaInvokeEvents.arn}:*"
}

Create the aws_cloudwatch_log_group Config

Next, we'll create the aws_cloudwatch_log_group config. This config is used to create a CloudWatch log group named LambdaInvokeEvents.

resource "aws_cloudwatch_log_group" "LambdaInvokeEvents" {
  name = "LambdaInvokeEvents"
}

Create the aws_cloudwatch_log_subscription_filter Config

Next, we'll create the aws_cloudwatch_log_subscription_filter config. This config is used to create a subscription filter for CloudWatch Logs.

resource "aws_cloudwatch_log_subscription_filter" "logfilter" {
  name                = "slack_lambda_logfilter"
  log_group_name      = "${aws_cloudwatch_log_group.LambdaInvokeEvents.name}"
  filter_pattern      = "{$.userIdentity.type=\\"IAMUser\\"}"
  destination_arn     = "${aws_lambda_function.test_lambda.arn}"
}

Create the aws_iam_role Config

Next, we'll create the aws_iam_role config. This config is used to create an AWS IAM role named 'cloudTrail-cloudWatch-role' with an assume role policy that allows the 'cloudtrail.amazonaws.com' service to assume this role.

resource "aws_iam_role" "cloud_trail" {
  name = "cloudTrail-cloudWatch-role"
  assume_role_policy = <<EOF
  {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Principal": {
          "Service": "cloudtrail.amazonaws.com"
        },
        "Action": "sts:AssumeRole"
      }
    ]
  }
  EOF
}

Create the aws_iam_role_policy_cloudTrail_cloudWatch Config

Next, we'll create the aws_iam_role_policy_cloudTrail_cloudWatch config. This config is used to define an IAM role policy for CloudTrail and CloudWatch integration.

resource "aws_iam_role_policy" "aws_iam_role_policy_cloudTrail_cloudWatch" {
  name = "cloudTrail-cloudWatch-policy"
  role = "${aws_iam_role.cloud_trail.id}"
  policy = <<EOF
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "AWSCloudTrailCreateLogStream2014110",
          "Effect": "Allow",
          "Action": [
            "logs:CreateLogStream"
          ],
          "Resource": [
            "${aws_cloudwatch_log_group.LambdaInvokeEvents.arn}:*"
          ]
        },
        {
          "Sid": "AWSCloudTrailPutLogEvents20141101",
          "Effect": "Allow",
          "Action": [
            "logs:PutLogEvents"
          ],
          "Resource": [
            "${aws_cloudwatch_log_group.LambdaInvokeEvents.arn}:*"
          ]
        }
      ]
    }
  EOF
}

This config creates an IAM role policy named cloudTrail-cloudWatch-policy and associates it with the IAM role specified by ${aws_iam_role.cloud_trail.id}. The policy allows the role to perform actions related to creating log streams and putting log events in the specified CloudWatch log group ${aws_cloudwatch_log_group.LambdaInvokeEvents.arn}.

Create the aws_cloudtrail Config

Next, we'll create the aws_cloudtrail config. This config is used to create an AWS CloudTrail resource named slack_cloudtrail. It specifies the following properties:

  • name: The name of the CloudTrail resource.

  • s3_bucket_name: The ID of the S3 bucket where CloudTrail logs will be stored. It references the id property of an existing aws_s3_bucket resource named foo.

  • s3_key_prefix: The prefix to be added to the S3 object key for CloudTrail logs.

  • include_global_service_events: Specifies whether to include global service events in the CloudTrail logs.

  • event_selector: Specifies the event selectors for the CloudTrail resource. In this case, it includes all read and write events for AWS Lambda functions, excluding management events. The values property references a variable named function_names which should be defined elsewhere.

  • cloud_watch_logs_group_arn: The ARN of the CloudWatch Logs group where CloudTrail logs will be delivered. It references the arn property of an existing aws_cloudwatch_log_group resource named LambdaInvokeEvents.

  • cloud_watch_logs_role_arn: The ARN of the IAM role that CloudTrail will assume to deliver logs to CloudWatch Logs. It references the arn property of an existing aws_iam_role resource named cloud_trail.

resource "aws_cloudtrail" "foobar" {
  name                            = "slack_cloudtrail"
  s3_bucket_name                  = "${aws_s3_bucket.foo.id}"
  s3_key_prefix                   = "prefix"
  include_global_service_events   = false

  event_selector {
    read_write_type = "All"
    include_management_events = false

    data_resource {
      type   = "AWS::Lambda::Function"
      values = "${var.function_names}"
    }
  }

  cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.LambdaInvokeEvents.arn}:*"
  cloud_watch_logs_role_arn  = "${aws_iam_role.cloud_trail.arn}"
}

Create the aws_s3_bucket Config

Next, we'll create the aws_s3_bucket config. This config is used to create an S3 bucket in AWS.

resource "aws_s3_bucket" "foo" {
  bucket        = "${var.bucket_name}"
  force_destroy = true
}

Create the aws_s3_bucket_policy Config

Next, we'll create the aws_s3_bucket_policy config. This config is used to define the policy for an S3 bucket in AWS.

resource "aws_s3_bucket_policy" "foo" {
  bucket = "${aws_s3_bucket.foo.id}"
  policy = "${data.aws_iam_policy_document.foo.json}"
}

Deploy the Solution

Let's deploy this thing! If you haven't done so, start the Skillmix lab session and get the account credentials. Configure your Terraform environment to use those credentials.

Then, open a terminal or command prompt, navigate to the folder with your Terraform file, and execute these commands:

# initiatlize the project 
$ terraform init 

# show the plan 
$ terraform plan 

# apply the changes 
$ terraform apply

# during prompts, Enter the function ARNS to monitor in the form of a list
["Lambda1","Lambda2"]
Enter your slack channel url
Enter a unique S3 bucketname (for Cloudtrail)

Wait for the changes to be applied before proceeding.

How it works

This pattern creates an AWS Cloudtrail for capturing data events of your lambda function to be monitored and the events will be pushed to Cloudwatch logs. It will also create a Lambda function which will have the code to push Slack notifications for the events fetched from the Cloudwatch using subscription filter to fetch only IAM user events.

Testing

Open the lambda function which you added to monitor and manually test invoke it. After 5 minutes you should see the message in your slack channel showing the user invocation.

#sample message:

User 'arn:aws:iam::<account-id>:user/RogueUser' performed Invoke on function 'arn:aws:lambda:us-east-1:<account-id>:function:testfunction' at 2023-03-27T13:04:09Z

Source: https://github.com/aws-samples/serverless-patterns/tree/main/terraform-cloudtrail-lambda-slack