Skip to content

Commit 0a89c65

Browse files
committed
[#316] Add Cloudtrail modules
1 parent c222713 commit 0a89c65

File tree

18 files changed

+507
-6
lines changed

18 files changed

+507
-6
lines changed

.github/wiki/Security.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@ This document provides an overview of the security modules available in the infr
44

55
## Available Security Modules
66

7+
### CloudTrail
8+
9+
The CloudTrail module provides comprehensive API activity logging and monitoring for your AWS infrastructure to enhance security auditing and compliance.
10+
11+
#### Overview
12+
13+
AWS CloudTrail records API calls and events across your AWS account. This module:
14+
15+
- **Comprehensive event logging**: Captures management events, data events, and insight events based on configuration
16+
- **Multi-region support**: Can be configured to log events across all AWS regions for complete visibility
17+
- **CloudWatch integration**: Sends logs to CloudWatch for real-time monitoring and alerting
18+
- **S3 storage**: Stores all CloudTrail logs securely in Amazon S3 with configurable key prefix organization
19+
- **SNS notifications**: Integrates with SNS topics for immediate alerting on critical events
20+
- **Insight events**: Captures unusual activity patterns like API call rate and error rate anomalies
21+
722
### VPC Flow Log
823

924
The VPC Flow Log module captures network traffic information in your VPC to help with security monitoring and network analysis.

src/commands/install/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ describe('Install add-on command', () => {
103103
it('throws an error', async () => {
104104
expect(stdoutSpy).toHaveBeenCalledWith(
105105
expect.stringContaining(
106-
'Expected invalid to be one of: vpc, securityGroup, alb, bastion, ecr, ecs, cloudwatch, rds, s3, ssm'
106+
'Expected invalid to be one of: vpc, securityGroup, alb, bastion, cloudtrail, ecr, ecs, cloudwatch, rds, s3, ssm, vpcFlowLog'
107107
)
108108
);
109109
});

src/generators/addons/aws/advanced.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { applyAdvancedTemplate } from './advanced';
55
import {
66
applyAwsAlb,
77
applyAwsBastion,
8+
applyAwsCloudtrail,
89
applyAwsEcr,
910
applyAwsEcs,
1011
applyAwsCloudwatch,
@@ -71,6 +72,10 @@ describe('AWS advanced template', () => {
7172
it('does NOT apply VPC Flow Log add-on', () => {
7273
expect(applyAwsVpcFlowLog).not.toHaveBeenCalled();
7374
});
75+
76+
it('does NOT apply CloudTrail add-on', () => {
77+
expect(applyAwsCloudtrail).not.toHaveBeenCalled();
78+
});
7479
});
7580

7681
describe('given enabledSecurityFeatures is true', () => {
@@ -97,6 +102,12 @@ describe('AWS advanced template', () => {
97102
optionsEnabledSecurityFeatures
98103
);
99104
});
105+
106+
it('applies CloudTrail add-on when flag is set', () => {
107+
expect(applyAwsCloudtrail).toHaveBeenCalledWith(
108+
optionsEnabledSecurityFeatures
109+
);
110+
});
100111
});
101112
});
102113
});

src/generators/addons/aws/advanced.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AwsOptions } from '.';
22
import {
33
applyAwsAlb,
44
applyAwsBastion,
5+
applyAwsCloudtrail,
56
applyAwsEcr,
67
applyAwsEcs,
78
applyAwsCloudwatch,
@@ -23,6 +24,7 @@ const applyAdvancedTemplate = async (options: AwsOptions) => {
2324

2425
if (options.enabledSecurityFeatures) {
2526
await applyAwsVpcFlowLog(options);
27+
await applyAwsCloudtrail(options);
2628
}
2729
};
2830

src/generators/addons/aws/dependencies.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { AwsOptions } from '@/generators/addons/aws';
55
import {
66
applyAwsAlb,
77
applyAwsBastion,
8+
applyAwsCloudtrail,
89
applyAwsEcr,
910
applyAwsEcs,
1011
applyAwsCloudwatch,
@@ -50,6 +51,12 @@ const AWS_MODULES: Record<AwsModuleName | string, AwsModule> = {
5051
mainContent: 'module "bastion"',
5152
applyModuleFunction: (options: AwsOptions) => applyAwsBastion(options),
5253
},
54+
cloudtrail: {
55+
name: 'cloudtrail',
56+
path: 'modules/cloudtrail',
57+
mainContent: 'module "cloudtrail"',
58+
applyModuleFunction: (options: AwsOptions) => applyAwsCloudtrail(options),
59+
},
5360
ecr: {
5461
name: 'ecr',
5562
path: 'modules/ecr',
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { AwsOptions } from '@/generators/addons/aws';
2+
import { applyTerraformCore } from '@/generators/terraform';
3+
import { remove } from '@/helpers/file';
4+
5+
import applyAwsCloudtrail, {
6+
cloudtrailModuleContent,
7+
cloudtrailOutputsContent,
8+
cloudtrailVariablesContent,
9+
} from './cloudtrail';
10+
import applyTerraformAwsProvider from './core/provider';
11+
12+
jest.mock('inquirer', () => {
13+
return {
14+
prompt: jest.fn().mockResolvedValue({ apply: true }),
15+
};
16+
});
17+
18+
describe('CloudTrail add-on', () => {
19+
describe('given valid AWS options', () => {
20+
const projectDir = 'cloudtrail-addon-test';
21+
22+
beforeAll(async () => {
23+
const awsOptions: AwsOptions = {
24+
projectName: projectDir,
25+
provider: 'aws',
26+
infrastructureType: 'advanced',
27+
};
28+
29+
await applyTerraformCore(awsOptions);
30+
await applyTerraformAwsProvider(awsOptions);
31+
await applyAwsCloudtrail(awsOptions);
32+
});
33+
34+
afterAll(() => {
35+
jest.clearAllMocks();
36+
remove('/', projectDir);
37+
});
38+
39+
it('creates expected files', () => {
40+
const expectedFiles = [
41+
'core/main.tf',
42+
'core/providers.tf',
43+
'core/outputs.tf',
44+
'core/variables.tf',
45+
'modules/cloudtrail/main.tf',
46+
'modules/cloudtrail/variables.tf',
47+
'modules/cloudtrail/outputs.tf',
48+
];
49+
50+
expect(projectDir).toHaveFiles(expectedFiles);
51+
});
52+
53+
it('adds cloudtrail module to main.tf', () => {
54+
expect(projectDir).toHaveContentInFile(
55+
'core/main.tf',
56+
cloudtrailModuleContent
57+
);
58+
});
59+
60+
it('adds cloudtrail variables to variables.tf', () => {
61+
expect(projectDir).toHaveContentInFile(
62+
'core/variables.tf',
63+
cloudtrailVariablesContent
64+
);
65+
});
66+
67+
it('adds cloudtrail outputs to outputs.tf', () => {
68+
expect(projectDir).toHaveContentInFile(
69+
'core/outputs.tf',
70+
cloudtrailOutputsContent
71+
);
72+
});
73+
});
74+
});
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { dedent } from 'ts-dedent';
2+
3+
import { AwsOptions } from '@/generators/addons/aws';
4+
import { isAwsModuleAdded } from '@/generators/addons/aws/dependencies';
5+
import {
6+
INFRA_CORE_MAIN_PATH,
7+
INFRA_CORE_VARIABLES_PATH,
8+
INFRA_CORE_OUTPUTS_PATH,
9+
} from '@/generators/terraform/constants';
10+
import { appendToFile, copy } from '@/helpers/file';
11+
12+
import { AWS_TEMPLATE_PATH } from '../constants';
13+
14+
const cloudtrailVariablesContent = dedent`
15+
variable "cloudtrail_log_retention_days" {
16+
description = "The number of days to retain CloudTrail logs in CloudWatch"
17+
type = number
18+
default = 365
19+
}`;
20+
21+
const cloudtrailModuleContent = dedent`
22+
module "cloudtrail_s3_bucket" {
23+
source = "../modules/s3"
24+
25+
env_namespace = local.env_namespace
26+
bucket_name = "\${local.env_namespace}-cloudtrail-logs-\${data.aws_caller_identity.current.account_id}"
27+
force_destroy = true
28+
object_ownership = "BucketOwnerPreferred"
29+
versioning_enabled = true
30+
lifecycle_configuration = {
31+
id = "log-expiration"
32+
status = "Enabled"
33+
filter = {
34+
prefix = ""
35+
}
36+
expiration = {
37+
days = var.cloudtrail_log_retention_days
38+
}
39+
}
40+
}
41+
42+
module "cloudtrail_s3_bucket_policy" {
43+
source = "../modules/s3BucketPolicy"
44+
45+
s3_bucket_name = module.cloudtrail_s3_bucket.aws_s3_bucket_name
46+
s3_bucket_policy = {
47+
Version = "2012-10-17"
48+
Statement = [
49+
{
50+
Sid = "AWSCloudTrailAclCheck"
51+
Effect = "Allow"
52+
Principal = {
53+
Service = "cloudtrail.amazonaws.com"
54+
}
55+
Action = [
56+
"s3:GetBucketAcl",
57+
"s3:ListBucket"
58+
]
59+
Resource = "arn:aws:s3:::\${module.cloudtrail_s3_bucket.aws_s3_bucket_name}"
60+
Condition = {
61+
StringEquals = {
62+
"aws:SourceAccount" = data.aws_caller_identity.current.account_id
63+
}
64+
ArnLike = {
65+
"aws:SourceArn" = "arn:aws:cloudtrail:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:*"
66+
}
67+
}
68+
},
69+
{
70+
Sid = "AWSCloudTrailWrite"
71+
Effect = "Allow"
72+
Principal = {
73+
Service = "cloudtrail.amazonaws.com"
74+
}
75+
Action = "s3:PutObject"
76+
Resource = "arn:aws:s3:::\${module.cloudtrail_s3_bucket.aws_s3_bucket_name}/cloudtrail/*"
77+
Condition = {
78+
StringEquals = {
79+
"aws:SourceAccount" = data.aws_caller_identity.current.account_id
80+
"s3:x-amz-acl" = "bucket-owner-full-control"
81+
}
82+
ArnLike = {
83+
"aws:SourceArn" = "arn:aws:cloudtrail:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:*"
84+
}
85+
}
86+
}
87+
]
88+
}
89+
}
90+
91+
module "cloudtrail_cloudwatch" {
92+
source = "../modules/cloudwatch"
93+
94+
cloud_watch_name = "/aws/cloudtrail/\${local.env_namespace}-cloudtrail"
95+
log_retention_in_days = var.cloudtrail_log_retention_days
96+
}
97+
98+
module "cloudtrail" {
99+
source = "../modules/cloudtrail"
100+
101+
env_namespace = local.env_namespace
102+
trail_name = "\${local.env_namespace}-cloudtrail"
103+
s3_bucket_name = module.cloudtrail_s3_bucket.aws_s3_bucket_name
104+
s3_key_prefix = "cloudtrail"
105+
log_retention_days = var.cloudtrail_log_retention_days
106+
s3_ignore_data_bucket_arns = ["arn:aws:s3:::\${module.cloudtrail_s3_bucket.aws_s3_bucket_name}"]
107+
cloud_watch_arn = module.cloudtrail_cloudwatch.aws_cloudwatch_log_group_arn
108+
}`;
109+
110+
const cloudtrailOutputsContent = dedent`
111+
output "cloudtrail_arn" {
112+
description = "The ARN of the CloudTrail"
113+
value = module.cloudtrail.cloudtrail_arn
114+
}`;
115+
116+
const applyAwsCloudtrail = async (options: AwsOptions) => {
117+
if (isAwsModuleAdded('cloudtrail', options.projectName)) {
118+
return;
119+
}
120+
121+
copy(
122+
`${AWS_TEMPLATE_PATH}/modules/cloudtrail`,
123+
'modules/cloudtrail',
124+
options.projectName
125+
);
126+
appendToFile(
127+
INFRA_CORE_VARIABLES_PATH,
128+
cloudtrailVariablesContent,
129+
options.projectName
130+
);
131+
appendToFile(
132+
INFRA_CORE_MAIN_PATH,
133+
cloudtrailModuleContent,
134+
options.projectName
135+
);
136+
appendToFile(
137+
INFRA_CORE_OUTPUTS_PATH,
138+
cloudtrailOutputsContent,
139+
options.projectName
140+
);
141+
};
142+
143+
export default applyAwsCloudtrail;
144+
export {
145+
cloudtrailVariablesContent,
146+
cloudtrailModuleContent,
147+
cloudtrailOutputsContent,
148+
};

src/generators/addons/aws/modules/cloudwatch.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ const cloudwatchModuleContent = dedent`
2020
module "cloudwatch" {
2121
source = "../modules/cloudwatch"
2222
23-
env_namespace = local.env_namespace
24-
23+
cloud_watch_name = "\${local.env_namespace}-cloudwatch-log-group"
2524
log_retention_in_days = var.cloudwatch_log_retention_in_days
2625
}`;
2726

src/generators/addons/aws/modules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import applyAwsAlb from './alb';
22
import applyAwsBastion from './bastion';
3+
import applyAwsCloudtrail from './cloudtrail';
34
import applyAwsCloudwatch from './cloudwatch';
45
import applyTerraformAwsData from './core/data';
56
import applyAwsIamUserAndGroup from './core/iamUserAndGroup';
@@ -17,6 +18,7 @@ import applyAwsVpcFlowLog from './vpcFlowLog';
1718
export {
1819
applyAwsAlb,
1920
applyAwsBastion,
21+
applyAwsCloudtrail,
2022
applyTerraformAwsProvider,
2123
applyTerraformAwsData,
2224
applyAwsCloudwatch,

src/generators/terraform/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const awsModules = [
55
'securityGroup',
66
'alb',
77
'bastion',
8+
'cloudtrail',
89
'ecr',
910
'ecs',
1011
'cloudwatch',

0 commit comments

Comments
 (0)