Skip to content

Commit

Permalink
Create org trail template and deploy action
Browse files Browse the repository at this point in the history
  • Loading branch information
farski committed Apr 22, 2024
1 parent 13a9d7f commit 5ad7a7b
Show file tree
Hide file tree
Showing 2 changed files with 358 additions and 0 deletions.
38 changes: 38 additions & 0 deletions .github/workflows/deploy-trail-org-trail.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Deploy Organization Trail

on:
push:
branches:
- main
paths:
- trails/organization-trail/template.yml

concurrency:
group: ${{ github.workflow }}

permissions:
id-token: write
contents: read

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: us-east-2
role-to-assume: arn:aws:iam::048723829744:role/PRX-GHA-AccessRole
role-session-name: gha-deploy-org-trail

- name: Deploy to management account
working-directory: trails/organization-trail
run: |
aws cloudformation deploy \
--region us-east-2 \
--stack-name organization-trail \
--template-file template.yml \
--no-fail-on-empty-changeset \
--role-arn arn:aws:iam::048723829744:role/PRX-GHA-ServiceRoleForCloudFormation
320 changes: 320 additions & 0 deletions trails/organization-trail/template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
# See: https://docs.aws.amazon.com/awscloudtrail/latest/userguide/creating-trail-organization.html
# This stack creates an Organization Trail. Creating a single Organization Trail
# somewhere within an AWS Organization will result in CloudTrail trails being
# created in various accounts and regions throughout the organization.
#
# This stack should only be launched once within the organization. It is
# intended to be launched in the *management account* of the organization.
#
# All CloudTrail log files, for all regions and accounts, will be captured in
# the S3 bucket that is created by this stack. The objects will include
# prefixes for account, region, etc. For example:
# AWSLogs/o-aabbccdd1122/1234567890/CloudTrail/ap-northeast-1/2024/04/01/048723829744_CloudTrail_ap-northeast-1_20240401T0105Z_bOL7464b6HTXPi7x.json.gz

AWSTemplateFormatVersion: "2010-09-09"
Description: Creates a CloudTrail Organization trail

Parameters:
OrganizationId:
Type: String
Description: >-
optional. An AWS Organization ID in the format: o-a1s2d3f4f5g. If
included, the CloudTrail that is created will be a organization trail,
and log activity from all accounts in the given organization.
AllowedPattern: ^$|^(o-[a-z0-9]{4,32})$
ProjectionRegions:
Type: CommaDelimitedList
ProjectionAccounts:
Type: CommaDelimitedList

Resources:
CloudTrailS3Bucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
LifecycleConfiguration:
Rules:
- ExpirationInDays: 14
Status: Enabled
OwnershipControls:
Rules:
# The IAM Access Analyzer requires that CloudTrail objects that it
# analyzes to generate policies be owned by the bucket owner
# https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-policy-generation.html#access-analyzer-policy-generation-cross-account
- ObjectOwnership: BucketOwnerPreferred
Tags:
- { Key: prx:meta:tagging-version, Value: "2021-04-07" }
- { Key: prx:cloudformation:stack-name, Value: !Ref AWS::StackName }
- { Key: prx:cloudformation:stack-id, Value: !Ref AWS::StackId }
- { Key: prx:ops:environment, Value: Production }
- { Key: prx:dev:application, Value: Security }
CloudTrailS3BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref CloudTrailS3Bucket
PolicyDocument:
Statement:
- Sid: AWSCloudTrailAclCheck
Effect: Allow
Principal:
Service: cloudtrail.amazonaws.com
Action: s3:GetBucketAcl
Resource: !GetAtt CloudTrailS3Bucket.Arn
- Sid: AWSCloudTrailWrite
Effect: Allow
Principal:
Service: cloudtrail.amazonaws.com
Action: s3:PutObject
Resource: !Sub ${CloudTrailS3Bucket.Arn}/AWSLogs/${AWS::AccountId}/*
Condition:
StringEquals:
s3:x-amz-acl: bucket-owner-full-control
- Sid: AWSOrgCloudTrailWrite
Effect: Allow
Principal:
Service: cloudtrail.amazonaws.com
Action: s3:PutObject
Resource: !Sub ${CloudTrailS3Bucket.Arn}/AWSLogs/${OrganizationId}/*
Condition:
StringEquals:
s3:x-amz-acl: bucket-owner-full-control
- Sid: IamAccessAnalyzerRead
Effect: Allow
Principal:
AWS: "*"
Action:
- s3:GetObject
- s3:ListBucket
Resource:
- !GetAtt CloudTrailS3Bucket.Arn
- !Sub ${CloudTrailS3Bucket.Arn}/AWSLogs/${OrganizationId}/${!aws:PrincipalAccount}/*
Condition:
StringEquals:
aws:PrincipalOrgID: !Ref OrganizationId
StringLike:
aws:PrincipalArn: arn:aws:iam::${aws:PrincipalAccount}:role/service-role/AccessAnalyzerMonitorServiceRole*
Version: "2012-10-17"

CloudTrail:
Type: AWS::CloudTrail::Trail
DependsOn:
- CloudTrailS3BucketPolicy
Properties:
IncludeGlobalServiceEvents: true
IsLogging: true
IsMultiRegionTrail: true
IsOrganizationTrail: true
S3BucketName: !Ref CloudTrailS3Bucket
Tags:
- { Key: prx:meta:tagging-version, Value: "2021-04-07" }
- { Key: prx:cloudformation:stack-name, Value: !Ref AWS::StackName }
- { Key: prx:cloudformation:stack-id, Value: !Ref AWS::StackId }
- { Key: prx:ops:environment, Value: Production }
- { Key: prx:dev:application, Value: Security }

GlueDatabase:
Type: AWS::Glue::Database
Properties:
CatalogId: !Ref AWS::AccountId
DatabaseInput:
Description: Database CloudTrail organization trail
Name: !Ref AWS::StackName

CommonTrailTable:
Type: AWS::Glue::Table
Properties:
CatalogId: !Ref AWS::AccountId
DatabaseName: !Ref GlueDatabase
TableInput:
Description: >-
Organization trail logs partitioned for our most-used accounts
and regions
Name: common-accounts-and-regions
Parameters:
projection.enabled: "true"
projection.date.type: date
projection.date.range: 2021/01/01,NOW
projection.date.format: yyyy/MM/dd
projection.date.interval: "1"
projection.date.interval.unit: DAYS
projection.region.type: enum
projection.region.values: !Join [",", !Ref ProjectionRegions]
projection.account.type: enum
projection.account.values: !Join [",", !Ref ProjectionAccounts]
storage.location.template: !Sub s3://${CloudTrailS3Bucket}/AWSLogs/${OrganizationId}/${!account}/CloudTrail/${!region}/${!date}
PartitionKeys:
- Name: date
Type: string
- Name: region
Type: string
- Name: account
Type: string
StorageDescriptor:
Columns:
- Name: eventVersion
Type: string
- 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>>>
- Name: eventTime
Type: string
- Name: eventSource
Type: string
- Name: eventName
Type: string
- Name: awsRegion
Type: string
- Name: sourceIpAddress
Type: string
- Name: userAgent
Type: string
- Name: errorCode
Type: string
- Name: errorMessage
Type: string
- Name: requestParameters
Type: string
- Name: responseElements
Type: string
- Name: additionalEventData
Type: string
- Name: requestId
Type: string
- Name: eventId
Type: string
- Name: readOnly
Type: string
- Name: resources
Type: array<struct<arn:string,accountId:string,type:string>>
- Name: eventType
Type: string
- Name: apiVersion
Type: string
- Name: recipientAccountId
Type: string
- Name: serviceEventDetails
Type: string
- Name: sharedEventID
Type: string
- Name: vpcEndpointId
Type: string
InputFormat: com.amazon.emr.cloudtrail.CloudTrailInputFormat
Location: !Sub s3://${CloudTrailS3Bucket}/AWSLogs/${OrganizationId}
OutputFormat: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat
SerdeInfo:
SerializationLibrary: com.amazon.emr.hive.serde.CloudTrailSerde
TableType: EXTERNAL_TABLE
ErrorQuery:
Type: AWS::Athena::NamedQuery
Properties:
Database: !Ref GlueDatabase
Description: >-
Searches for events with any error code
Name: Organization Trail error codes
QueryString: !Sub |
SELECT
eventTime AS "Time",
account AS "Trail Account",
recipientAccountId AS "Recipient Account",
userIdentity.accountId AS "Identity Account",
awsRegion AS "Target Region",
eventType AS "Type",
eventSource AS "Service",
eventName AS "Action",
userIdentity.type AS "Identity Type",
userIdentity.arn AS "Identity ARN",
userIdentity.invokedBy AS "Invoked By",
errorCode AS "Error Code",
errorMessage AS "Error Message"
FROM "${GlueDatabase}"."${CommonTrailTable}"
WHERE "date" >= date_format(current_date - interval '1' day, '%Y/%m/%d')
AND "eventtime" >= to_iso8601(current_timestamp - interval '1' hour)
AND "account" = '000000000000'
AND errorCode <> ''
AND errorMessage <> 'No updates are to be performed.'
AND errorMessage <> 'The specified log group already exists'
ORDER BY eventTime DESC
FullScanQuery:
Type: AWS::Athena::NamedQuery
Properties:
Database: !Ref GlueDatabase
Description: >-
Scans all recent events for a given account
Name: Organization Trail basic scan
QueryString: !Sub |
SELECT
eventTime AS "Time",
account AS "Trail Account",
recipientAccountId AS "Recipient Account",
userIdentity.accountId AS "Identity Account",
awsRegion AS "Target Region",
eventType AS "Type",
eventSource AS "Service",
eventName AS "Action",
userIdentity.type AS "Identity Type",
userIdentity.arn AS "Identity ARN",
userIdentity.invokedBy AS "Invoked By"
FROM "${GlueDatabase}"."${CommonTrailTable}"
WHERE "date" >= date_format(current_date - interval '1' day, '%Y/%m/%d')
AND "eventTime" >= to_iso8601(current_timestamp - interval '1' hour)
AND "account" = '000000000000'
ORDER BY eventTime ASC
WriteOperationScanQuery:
Type: AWS::Athena::NamedQuery
Properties:
Database: !Ref GlueDatabase
Description: >-
Scans for write operations (i.e., exclude read-only) for a given account
Name: Organization Trail write operations scan
QueryString: !Sub |
SELECT
eventTime AS "Time",
account AS "Trail Account",
recipientAccountId AS "Recipient Account",
userIdentity.accountId AS "Identity Account",
awsRegion AS "Target Region",
eventType AS "Type",
eventSource AS "Service",
eventName AS "Action",
userIdentity.type AS "Identity Type",
userIdentity.arn AS "Identity ARN",
userIdentity.invokedBy AS "Invoked By"
FROM "${GlueDatabase}"."${CommonTrailTable}"
WHERE "date" >= date_format(current_date - interval '1' day, '%Y/%m/%d')
AND "eventTime" >= to_iso8601(current_timestamp - interval '1' hour)
AND "readOnly" = 'false'
AND "account" = '000000000000'
-- Exclude some common events
-- AND eventName NOT IN ('CreateLogStream')
-- Include only event from SSO users (i.e., humans)
-- AND userIdentity.arn LIKE '%AWSReservedSSO%'
ORDER BY eventTime ASC
ErrorReportQuery:
Type: AWS::Athena::NamedQuery
Properties:
Database: !Ref GlueDatabase
Description: >-
Groups events by errorCode to find high volume errors
Name: Organization Trail error code report
QueryString: !Sub |
SELECT
count(*) AS "Event Count",
errorCode AS "Error Code",
recipientAccountId AS "Recipient Account",
eventType AS "Type",
eventSource AS "Service",
eventName AS "Action",
userIdentity.arn AS "Identity ARN"
FROM "${GlueDatabase}"."${CommonTrailTable}"
WHERE "date" >= date_format(current_date - interval '1' day, '%Y/%m/%d')
AND "eventTime" >= to_iso8601(current_timestamp - interval '1' hour)
AND "account" = '000000000000'
AND errorCode <> ''
GROUP BY
recipientAccountId,
eventType,
eventName,
eventSource,
userIdentity.arn,
errorCode
ORDER BY "Event Count" DESC

0 comments on commit 5ad7a7b

Please sign in to comment.