AWS CloudTrail Audit Logging: Setup, Athena Queries, and Alerting on Sensitive API Calls
Complete guide to AWS CloudTrail for security auditing. Set up multi-region trails, query logs with Athena, detect sensitive API calls like root login and IAM changes, and alert via SNS.
Every AWS API call in your account is an audit event. Who created that IAM role? When did someone delete the S3 bucket? Which Lambda function assumed that cross-account role at 2 AM? Without CloudTrail, you have no answers. With it properly configured, you have a searchable, tamper-resistant log of everything.
CloudTrail is mandatory for any production AWS workload. It's required for SOC 2, PCI-DSS, HIPAA, and ISO 27001 compliance. This guide covers everything from initial setup to running Athena queries against your trails and alerting on suspicious API calls.
What CloudTrail Records
CloudTrail records three types of events:
-
Management events (control plane): API calls that change AWS resources —
CreateBucket,PutRolePolicy,RunInstances. Enabled by default when you create a trail. -
Data events (data plane): Object-level operations within services — S3
GetObject/PutObject, Lambda invocations, DynamoDB reads/writes. Not enabled by default (high volume). -
Insights events: Unusual activity detection — abnormal API call rates, error rates. Optional, billed separately.
Terraform Setup: Production-Grade Trail
# cloudtrail/main.tf
locals {
trail_name = "${var.project}-cloudtrail"
bucket_name = "${var.project}-cloudtrail-logs-${data.aws_caller_identity.current.account_id}"
}
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
# S3 bucket for CloudTrail logs
resource "aws_s3_bucket" "cloudtrail" {
bucket = local.bucket_name
force_destroy = false # NEVER set true in prod — prevents accidental deletion
}
resource "aws_s3_bucket_versioning" "cloudtrail" {
bucket = aws_s3_bucket.cloudtrail.id
versioning_configuration { status = "Enabled" }
}
resource "aws_s3_bucket_server_side_encryption_configuration" "cloudtrail" {
bucket = aws_s3_bucket.cloudtrail.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.cloudtrail.arn
}
bucket_key_enabled = true
}
}
resource "aws_s3_bucket_public_access_block" "cloudtrail" {
bucket = aws_s3_bucket.cloudtrail.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_lifecycle_configuration" "cloudtrail" {
bucket = aws_s3_bucket.cloudtrail.id
rule {
id = "archive-and-expire"
status = "Enabled"
transition {
days = 90
storage_class = "GLACIER_IR"
}
expiration {
days = 2557 # 7 years — common compliance requirement
}
}
}
# Bucket policy required by CloudTrail
resource "aws_s3_bucket_policy" "cloudtrail" {
bucket = aws_s3_bucket.cloudtrail.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AWSCloudTrailAclCheck"
Effect = "Allow"
Principal = { Service = "cloudtrail.amazonaws.com" }
Action = "s3:GetBucketAcl"
Resource = "arn:aws:s3:::${local.bucket_name}"
Condition = {
StringEquals = {
"AWS:SourceArn" = "arn:aws:cloudtrail:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:trail/${local.trail_name}"
}
}
},
{
Sid = "AWSCloudTrailWrite"
Effect = "Allow"
Principal = { Service = "cloudtrail.amazonaws.com" }
Action = "s3:PutObject"
Resource = "arn:aws:s3:::${local.bucket_name}/AWSLogs/${data.aws_caller_identity.current.account_id}/*"
Condition = {
StringEquals = {
"s3:x-amz-acl" = "bucket-owner-full-control"
"AWS:SourceArn" = "arn:aws:cloudtrail:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:trail/${local.trail_name}"
}
}
}
]
})
}
# KMS key for CloudTrail encryption
resource "aws_kms_key" "cloudtrail" {
description = "CloudTrail log encryption key"
deletion_window_in_days = 30
enable_key_rotation = true
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "Enable IAM User Permissions"
Effect = "Allow"
Principal = { AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" }
Action = "kms:*"
Resource = "*"
},
{
Sid = "Allow CloudTrail to encrypt logs"
Effect = "Allow"
Principal = { Service = "cloudtrail.amazonaws.com" }
Action = ["kms:GenerateDataKey*", "kms:DescribeKey"]
Resource = "*"
Condition = {
StringEquals = {
"AWS:SourceArn" = "arn:aws:cloudtrail:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:trail/${local.trail_name}"
}
StringLike = {
"kms:EncryptionContext:aws:cloudtrail:arn" = "arn:aws:cloudtrail:*:${data.aws_caller_identity.current.account_id}:trail/*"
}
}
},
{
Sid = "Allow Athena to decrypt"
Effect = "Allow"
Principal = { AWS = aws_iam_role.athena_cloudtrail.arn }
Action = ["kms:Decrypt", "kms:GenerateDataKey"]
Resource = "*"
}
]
})
}
resource "aws_kms_alias" "cloudtrail" {
name = "alias/${var.project}-cloudtrail"
target_key_id = aws_kms_key.cloudtrail.key_id
}
# CloudWatch Log Group for real-time analysis
resource "aws_cloudwatch_log_group" "cloudtrail" {
name = "/aws/cloudtrail/${var.project}"
retention_in_days = 90
kms_key_id = aws_kms_key.cloudtrail.arn
}
resource "aws_iam_role" "cloudtrail_cloudwatch" {
name = "${var.project}-cloudtrail-cw-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "cloudtrail.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "cloudtrail_cloudwatch" {
role = aws_iam_role.cloudtrail_cloudwatch.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["logs:CreateLogStream", "logs:PutLogEvents"]
Resource = "${aws_cloudwatch_log_group.cloudtrail.arn}:*"
}]
})
}
# The CloudTrail itself
resource "aws_cloudtrail" "main" {
name = local.trail_name
s3_bucket_name = aws_s3_bucket.cloudtrail.id
include_global_service_events = true # IAM, STS, CloudFront
is_multi_region_trail = true # catches all regions
enable_log_file_validation = true # tamper detection
kms_key_id = aws_kms_key.cloudtrail.arn
cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.cloudtrail.arn}:*"
cloud_watch_logs_role_arn = aws_iam_role.cloudtrail_cloudwatch.arn
# Record S3 data events for sensitive buckets
event_selector {
read_write_type = "All"
include_management_events = true
data_resource {
type = "AWS::S3::Object"
values = ["arn:aws:s3:::${var.sensitive_bucket_name}/"]
}
}
# Record Lambda invocations
event_selector {
read_write_type = "WriteOnly"
include_management_events = false
data_resource {
type = "AWS::Lambda::Function"
values = ["arn:aws:lambda"] # All functions
}
}
# CloudTrail Insights for anomaly detection
insight_selector {
insight_type = "ApiCallRateInsight"
}
insight_selector {
insight_type = "ApiErrorRateInsight"
}
tags = var.common_tags
depends_on = [
aws_s3_bucket_policy.cloudtrail,
aws_cloudwatch_log_group.cloudtrail,
]
}
☁️ Is Your Cloud Costing Too Much?
Most teams overspend 30–40% on cloud — wrong instance types, no reserved pricing, bloated storage. We audit, right-size, and automate your infrastructure.
- AWS, GCP, Azure certified engineers
- Infrastructure as Code (Terraform, CDK)
- Docker, Kubernetes, GitHub Actions CI/CD
- Typical audit recovers $500–$3,000/month in savings
Athena Setup for Log Queries
CloudTrail logs land in S3 as gzipped JSON. Athena lets you query them with SQL:
# athena.tf
resource "aws_athena_database" "cloudtrail" {
name = "cloudtrail_logs"
bucket = aws_s3_bucket.athena_results.bucket
}
resource "aws_glue_catalog_table" "cloudtrail" {
name = "cloudtrail_events"
database_name = aws_athena_database.cloudtrail.name
table_type = "EXTERNAL_TABLE"
parameters = {
EXTERNAL = "TRUE"
"projection.enabled" = "true"
"projection.dt.type" = "date"
"projection.dt.range" = "2024/01/01,NOW"
"projection.dt.format" = "yyyy/MM/dd"
"projection.dt.interval" = "1"
"projection.dt.interval.unit" = "DAYS"
"storage.location.template" = "s3://${local.bucket_name}/AWSLogs/${data.aws_caller_identity.current.account_id}/CloudTrail/$${region}/$${dt}/"
"projection.region.type" = "enum"
"projection.region.values" = "us-east-1,us-west-2,eu-west-1,ap-southeast-1"
}
storage_descriptor {
location = "s3://${local.bucket_name}/AWSLogs/${data.aws_caller_identity.current.account_id}/CloudTrail/"
input_format = "com.amazon.emr.cloudtrail.CloudTrailInputFormat"
output_format = "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat"
ser_de_info {
serialization_library = "com.amazon.emr.hive.serde.CloudTrailSerde"
}
columns {
name = "eventversion" type = "string"
}
columns { name = "useridentity" type = "struct<type:string,principalid:string,arn:string,accountid:string,invokedby:string,accesskeyid:string,userName:string,sessioncontext:struct<attributes:struct<mfaauthenticated:string,creationdate:string>,sessionissuer:struct<type:string,principalid:string,arn:string,accountid:string,username:string>>>" }
columns { name = "eventtime" type = "string" }
columns { name = "eventsource" type = "string" }
columns { name = "eventname" type = "string" }
columns { name = "awsregion" type = "string" }
columns { name = "sourceipaddress" type = "string" }
columns { name = "useragent" type = "string" }
columns { name = "errorcode" type = "string" }
columns { name = "errormessage" type = "string" }
columns { name = "requestparameters" type = "string" }
columns { name = "responseelements" type = "string" }
columns { name = "requestid" type = "string" }
columns { name = "eventid" type = "string" }
columns { name = "resources" type = "array<struct<ARN:string,accountId:string,type:string>>" }
columns { name = "eventtype" type = "string" }
columns { name = "apiversion" type = "string" }
columns { name = "readonly" type = "string" }
columns { name = "recipientaccountid" type = "string" }
columns { name = "serviceeventdetails" type = "string" }
columns { name = "sharedeventid" type = "string" }
columns { name = "vpcendpointid" type = "string" }
partition_keys {
name = "region" type = "string"
}
partition_keys {
name = "dt" type = "string"
}
}
}
Essential Athena Security Queries
-- 1. Root account activity (always high severity)
SELECT
eventtime,
eventname,
eventsource,
sourceipaddress,
useragent,
useridentity.type AS identity_type
FROM cloudtrail_logs.cloudtrail_events
WHERE dt BETWEEN '2027/01/01' AND '2027/03/04'
AND region = 'us-east-1'
AND useridentity.type = 'Root'
AND eventtype = 'AwsApiCall'
ORDER BY eventtime DESC
LIMIT 100;
-- 2. Failed authentication attempts (brute force detection)
SELECT
sourceipaddress,
errorcode,
COUNT(*) AS attempt_count,
MIN(eventtime) AS first_seen,
MAX(eventtime) AS last_seen,
ARRAY_AGG(DISTINCT eventname) AS events
FROM cloudtrail_logs.cloudtrail_events
WHERE dt >= '2027/03/01'
AND errorcode IN ('AccessDenied', 'UnauthorizedOperation', 'AuthFailure')
GROUP BY sourceipaddress, errorcode
HAVING COUNT(*) > 10
ORDER BY attempt_count DESC;
-- 3. IAM privilege escalation attempts
SELECT
eventtime,
eventname,
eventsource,
sourceipaddress,
useridentity.arn AS actor_arn,
useridentity.username AS username,
requestparameters
FROM cloudtrail_logs.cloudtrail_events
WHERE dt >= '2027/01/01'
AND eventsource = 'iam.amazonaws.com'
AND eventname IN (
'AttachUserPolicy',
'AttachRolePolicy',
'CreatePolicy',
'CreatePolicyVersion',
'PutUserPolicy',
'PutRolePolicy',
'AddUserToGroup',
'CreateUser',
'CreateRole',
'UpdateAssumeRolePolicy'
)
ORDER BY eventtime DESC
LIMIT 500;
-- 4. Security group and network changes
SELECT
eventtime,
eventname,
sourceipaddress,
useridentity.arn AS actor_arn,
requestparameters
FROM cloudtrail_logs.cloudtrail_events
WHERE dt >= '2027/01/01'
AND eventsource = 'ec2.amazonaws.com'
AND eventname IN (
'AuthorizeSecurityGroupIngress',
'AuthorizeSecurityGroupEgress',
'RevokeSecurityGroupIngress',
'CreateSecurityGroup',
'DeleteSecurityGroup',
'ModifyInstanceAttribute',
'CreateNetworkAclEntry',
'DeleteNetworkAcl'
)
ORDER BY eventtime DESC
LIMIT 200;
-- 5. S3 bucket policy changes (data exfiltration risk)
SELECT
eventtime,
eventname,
sourceipaddress,
useridentity.arn AS actor_arn,
requestparameters
FROM cloudtrail_logs.cloudtrail_events
WHERE dt >= '2027/01/01'
AND eventsource = 's3.amazonaws.com'
AND eventname IN (
'PutBucketPolicy',
'DeleteBucketPolicy',
'PutBucketAcl',
'PutBucketCors',
'PutBucketPublicAccessBlock',
'DeletePublicAccessBlock'
)
ORDER BY eventtime DESC;
-- 6. Console logins with MFA status
SELECT
eventtime,
sourceipaddress,
useridentity.username AS username,
useridentity.type AS identity_type,
JSON_EXTRACT_SCALAR(
responseelements,
'$.ConsoleLogin'
) AS login_result,
JSON_EXTRACT_SCALAR(
requestparameters,
'$.additionalEventData.MFAUsed'
) AS mfa_used
FROM cloudtrail_logs.cloudtrail_events
WHERE dt >= '2027/02/01'
AND eventname = 'ConsoleLogin'
ORDER BY eventtime DESC
LIMIT 200;
-- 7. Cross-account role assumptions
SELECT
eventtime,
sourceipaddress,
useridentity.arn AS assumed_by,
JSON_EXTRACT_SCALAR(requestparameters, '$.roleArn') AS role_arn,
JSON_EXTRACT_SCALAR(requestparameters, '$.roleSessionName') AS session_name
FROM cloudtrail_logs.cloudtrail_events
WHERE dt >= '2027/01/01'
AND eventname = 'AssumeRole'
AND eventsource = 'sts.amazonaws.com'
AND useridentity.accountid != recipientaccountid -- cross-account
ORDER BY eventtime DESC;
-- 8. Secrets Manager and SSM Parameter Store access
SELECT
eventtime,
eventname,
sourceipaddress,
useridentity.arn AS accessor,
JSON_EXTRACT_SCALAR(requestparameters, '$.secretId') AS secret_id,
errorcode
FROM cloudtrail_logs.cloudtrail_events
WHERE dt >= '2027/01/01'
AND eventsource = 'secretsmanager.amazonaws.com'
AND eventname IN ('GetSecretValue', 'DescribeSecret', 'DeleteSecret')
ORDER BY eventtime DESC;
⚙️ DevOps Done Right — Zero Downtime, Full Automation
Ship faster without breaking things. We build CI/CD pipelines, monitoring stacks, and auto-scaling infrastructure that your team can actually maintain.
- Staging + production environments with feature flags
- Automated security scanning in the pipeline
- Uptime monitoring + alerting + runbook automation
- On-call support handover docs included
CloudWatch Alarms for Real-Time Alerting
# alerts.tf
resource "aws_sns_topic" "security_alerts" {
name = "${var.project}-security-alerts"
kms_master_key_id = aws_kms_key.cloudtrail.id
}
resource "aws_sns_topic_subscription" "security_email" {
topic_arn = aws_sns_topic.security_alerts.arn
protocol = "email"
endpoint = var.security_alert_email
}
locals {
alarm_defaults = {
period = 300
evaluation_periods = 1
statistic = "Sum"
namespace = "CloudTrailMetrics"
alarm_actions = [aws_sns_topic.security_alerts.arn]
ok_actions = [aws_sns_topic.security_alerts.arn]
treat_missing_data = "notBreaching"
}
}
# Metric filters + alarms
resource "aws_cloudwatch_log_metric_filter" "root_login" {
name = "RootAccountUsage"
pattern = "{ $.userIdentity.type = \"Root\" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != \"AwsServiceEvent\" }"
log_group_name = aws_cloudwatch_log_group.cloudtrail.name
metric_transformation {
name = "RootAccountUsageCount"
namespace = "CloudTrailMetrics"
value = "1"
default_value = "0"
}
}
resource "aws_cloudwatch_metric_alarm" "root_login" {
alarm_name = "${var.project}-root-account-usage"
alarm_description = "Root account was used — investigate immediately"
metric_name = "RootAccountUsageCount"
comparison_operator = "GreaterThanOrEqualToThreshold"
threshold = 1
namespace = local.alarm_defaults.namespace
period = local.alarm_defaults.period
evaluation_periods = local.alarm_defaults.evaluation_periods
statistic = local.alarm_defaults.statistic
alarm_actions = local.alarm_defaults.alarm_actions
treat_missing_data = local.alarm_defaults.treat_missing_data
}
resource "aws_cloudwatch_log_metric_filter" "iam_changes" {
name = "IAMPolicyChanges"
pattern = "{($.eventName=DeleteGroupPolicy)||($.eventName=DeleteRolePolicy)||($.eventName=DeleteUserPolicy)||($.eventName=PutGroupPolicy)||($.eventName=PutRolePolicy)||($.eventName=PutUserPolicy)||($.eventName=CreatePolicy)||($.eventName=DeletePolicy)||($.eventName=CreatePolicyVersion)||($.eventName=DeletePolicyVersion)||($.eventName=SetDefaultPolicyVersion)||($.eventName=AttachRolePolicy)||($.eventName=DetachRolePolicy)||($.eventName=AttachUserPolicy)||($.eventName=DetachUserPolicy)||($.eventName=AttachGroupPolicy)||($.eventName=DetachGroupPolicy)}"
log_group_name = aws_cloudwatch_log_group.cloudtrail.name
metric_transformation {
name = "IAMPolicyChangesCount"
namespace = "CloudTrailMetrics"
value = "1"
default_value = "0"
}
}
resource "aws_cloudwatch_metric_alarm" "iam_changes" {
alarm_name = "${var.project}-iam-policy-changes"
alarm_description = "IAM policy changes detected"
metric_name = "IAMPolicyChangesCount"
comparison_operator = "GreaterThanOrEqualToThreshold"
threshold = 1
namespace = local.alarm_defaults.namespace
period = local.alarm_defaults.period
evaluation_periods = local.alarm_defaults.evaluation_periods
statistic = local.alarm_defaults.statistic
alarm_actions = local.alarm_defaults.alarm_actions
treat_missing_data = local.alarm_defaults.treat_missing_data
}
resource "aws_cloudwatch_log_metric_filter" "cloudtrail_disabled" {
name = "CloudTrailChanges"
pattern = "{($.eventName=CreateTrail)||($.eventName=UpdateTrail)||($.eventName=DeleteTrail)||($.eventName=StartLogging)||($.eventName=StopLogging)}"
log_group_name = aws_cloudwatch_log_group.cloudtrail.name
metric_transformation {
name = "CloudTrailChangesCount"
namespace = "CloudTrailMetrics"
value = "1"
default_value = "0"
}
}
resource "aws_cloudwatch_metric_alarm" "cloudtrail_disabled" {
alarm_name = "${var.project}-cloudtrail-configuration-change"
alarm_description = "CloudTrail was modified or stopped — possible attempt to cover tracks"
metric_name = "CloudTrailChangesCount"
comparison_operator = "GreaterThanOrEqualToThreshold"
threshold = 1
namespace = local.alarm_defaults.namespace
period = 60 # More aggressive — 1 minute
evaluation_periods = 1
statistic = local.alarm_defaults.statistic
alarm_actions = local.alarm_defaults.alarm_actions
treat_missing_data = local.alarm_defaults.treat_missing_data
}
resource "aws_cloudwatch_log_metric_filter" "no_mfa_console" {
name = "ConsoleSignInWithoutMFA"
pattern = "{ ($.eventName = \"ConsoleLogin\") && ($.additionalEventData.MFAUsed != \"Yes\") && ($.userIdentity.type = \"IAMUser\") && ($.responseElements.ConsoleLogin = \"Success\") }"
log_group_name = aws_cloudwatch_log_group.cloudtrail.name
metric_transformation {
name = "ConsoleSignInWithoutMFACount"
namespace = "CloudTrailMetrics"
value = "1"
default_value = "0"
}
}
resource "aws_cloudwatch_metric_alarm" "no_mfa_console" {
alarm_name = "${var.project}-console-signin-without-mfa"
alarm_description = "Console sign-in without MFA detected"
metric_name = "ConsoleSignInWithoutMFACount"
comparison_operator = "GreaterThanOrEqualToThreshold"
threshold = 1
namespace = local.alarm_defaults.namespace
period = local.alarm_defaults.period
evaluation_periods = local.alarm_defaults.evaluation_periods
statistic = local.alarm_defaults.statistic
alarm_actions = local.alarm_defaults.alarm_actions
treat_missing_data = local.alarm_defaults.treat_missing_data
}
resource "aws_cloudwatch_log_metric_filter" "s3_bucket_policy_change" {
name = "S3BucketPolicyChanges"
pattern = "{($.eventSource=s3.amazonaws.com)&&(($.eventName=PutBucketAcl)||($.eventName=PutBucketPolicy)||($.eventName=PutBucketCors)||($.eventName=PutBucketLifecycle)||($.eventName=PutBucketReplication)||($.eventName=DeleteBucketPolicy)||($.eventName=DeleteBucketCors)||($.eventName=DeleteBucketLifecycle)||($.eventName=DeleteBucketReplication))}"
log_group_name = aws_cloudwatch_log_group.cloudtrail.name
metric_transformation {
name = "S3BucketPolicyChangesCount"
namespace = "CloudTrailMetrics"
value = "1"
default_value = "0"
}
}
resource "aws_cloudwatch_metric_alarm" "s3_bucket_policy_change" {
alarm_name = "${var.project}-s3-bucket-policy-change"
alarm_description = "S3 bucket policy or ACL was modified"
metric_name = "S3BucketPolicyChangesCount"
comparison_operator = "GreaterThanOrEqualToThreshold"
threshold = 1
namespace = local.alarm_defaults.namespace
period = local.alarm_defaults.period
evaluation_periods = local.alarm_defaults.evaluation_periods
statistic = local.alarm_defaults.statistic
alarm_actions = local.alarm_defaults.alarm_actions
treat_missing_data = local.alarm_defaults.treat_missing_data
}
Lambda-Based Alert Enrichment
Raw CloudWatch alarms are terse. A Lambda enricher pulls context and formats a readable Slack message:
// functions/cloudtrail-alert-enricher/handler.ts
import { SNSEvent } from "aws-lambda";
import {
CloudTrailClient,
LookupEventsCommand,
} from "@aws-sdk/client-cloudtrail";
const cloudtrail = new CloudTrailClient({});
interface AlarmMessage {
AlarmName: string;
NewStateValue: string;
NewStateReason: string;
AlarmDescription: string;
StateChangeTime: string;
}
export async function handler(event: SNSEvent): Promise<void> {
for (const record of event.Records) {
const alarm = JSON.parse(record.Sns.Message) as AlarmMessage;
if (alarm.NewStateValue !== "ALARM") continue;
// Look up the triggering CloudTrail event
const lookupResponse = await cloudtrail.send(
new LookupEventsCommand({
StartTime: new Date(Date.now() - 15 * 60 * 1000), // last 15 min
EndTime: new Date(),
MaxResults: 5,
LookupAttributes: getAlarmAttributes(alarm.AlarmName),
})
);
const events = lookupResponse.Events ?? [];
const eventDetails = events
.map((e) => {
const detail = e.CloudTrailEvent
? JSON.parse(e.CloudTrailEvent)
: {};
return {
time: e.EventTime?.toISOString(),
name: e.EventName,
user: detail.userIdentity?.arn ?? detail.userIdentity?.userName,
ip: detail.sourceIPAddress,
region: detail.awsRegion,
};
})
.slice(0, 3);
await sendSlackAlert({
alarm: alarm.AlarmName,
description: alarm.AlarmDescription,
stateChangeTime: alarm.StateChangeTime,
events: eventDetails,
});
}
}
function getAlarmAttributes(alarmName: string) {
const attributeMap: Record<string, { AttributeKey: string; AttributeValue: string }> = {
"root-account-usage": { AttributeKey: "Username", AttributeValue: "root" },
"iam-policy-changes": { AttributeKey: "EventSource", AttributeValue: "iam.amazonaws.com" },
"cloudtrail-configuration-change": {
AttributeKey: "EventSource",
AttributeValue: "cloudtrail.amazonaws.com",
},
};
const key = Object.keys(attributeMap).find((k) => alarmName.includes(k));
return key ? [attributeMap[key]] : [];
}
async function sendSlackAlert(payload: {
alarm: string;
description: string;
stateChangeTime: string;
events: Array<{ time?: string; name?: string; user?: string; ip?: string; region?: string }>;
}) {
const webhookUrl = process.env.SLACK_SECURITY_WEBHOOK!;
const eventBlocks = payload.events.map((e) => ({
type: "section",
fields: [
{ type: "mrkdwn", text: `*Event:* ${e.name}` },
{ type: "mrkdwn", text: `*Actor:* ${e.user ?? "Unknown"}` },
{ type: "mrkdwn", text: `*IP:* ${e.ip ?? "Unknown"}` },
{ type: "mrkdwn", text: `*Region:* ${e.region ?? "Unknown"}` },
{ type: "mrkdwn", text: `*Time:* ${e.time ?? "Unknown"}` },
],
}));
await fetch(webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
blocks: [
{
type: "header",
text: { type: "plain_text", text: `🚨 Security Alert: ${payload.alarm}` },
},
{
type: "section",
text: { type: "mrkdwn", text: `*${payload.description}*\n${payload.stateChangeTime}` },
},
{ type: "divider" },
...eventBlocks,
{
type: "actions",
elements: [
{
type: "button",
text: { type: "plain_text", text: "View in CloudTrail" },
url: `https://console.aws.amazon.com/cloudtrail/home#/events`,
style: "danger",
},
],
},
],
}),
});
}
Cost Estimates
| Component | Cost |
|---|---|
| CloudTrail management events (1 trail) | Free for first trail per region |
| CloudTrail data events | $0.10 per 100,000 events |
| CloudTrail Insights | $0.35 per 100,000 events analyzed |
| S3 storage (90-day active, 7-year Glacier archive) | ~$5–20/month depending on volume |
| Athena queries | $5 per TB scanned (partition pruning critical) |
| CloudWatch Logs (90-day retention) | ~$3–10/month |
| Lambda enricher invocations | Negligible at alert volumes |
| Total typical setup | $10–50/month |
Cost and Timeline Estimates
| Scope | Team Size | Timeline | Cost Range |
|---|---|---|---|
| Basic CloudTrail + S3 setup | 1 dev | 1 day | $200–400 |
| Full Terraform trail + Athena + 5 alarms | 1 dev | 2–3 days | $600–1,200 |
| Full security audit setup + Slack enricher | 1–2 devs | 1 week | $2,000–4,000 |
| Multi-account org trail + SIEM integration | 2–3 devs | 2–3 weeks | $6,000–14,000 |
See Also
- AWS Secrets Manager vs Parameter Store
- Terraform State Management and Remote Backends
- AWS ECS Fargate Production Deployment
- SaaS Audit Logging Implementation
- PostgreSQL Triggers and Audit Tables
Working With Viprasol
CloudTrail is table stakes for production AWS security — but the setup that actually catches threats requires proper multi-region configuration, Athena partition projection, and alarms tuned to avoid alert fatigue. Our cloud team configures security observability for AWS workloads from startup to enterprise scale.
What we deliver:
- Terraform-managed CloudTrail with KMS encryption, S3 lifecycle, and tamper validation
- Athena table with partition projection for cost-efficient querying
- CloudWatch metric filters and alarms for all CIS AWS Foundations Benchmark controls
- Slack/PagerDuty alert enrichment via Lambda
- Runbook documentation for incident response
Talk to our team about AWS security architecture →
Or explore our cloud infrastructure services.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need DevOps & Cloud Expertise?
Scale your infrastructure with confidence. AWS, GCP, Azure certified team.
Free consultation • No commitment • Response within 24 hours
Making sense of your data at scale?
Viprasol builds end-to-end big data analytics solutions — ETL pipelines, data warehouses on Snowflake or BigQuery, and self-service BI dashboards. One reliable source of truth for your entire organisation.