Skip to content

Commit f600b45

Browse files
committed
[#316] Add Cloudtrail modules
1 parent b55003b commit f600b45

File tree

29 files changed

+1067
-2
lines changed

29 files changed

+1067
-2
lines changed

.github/wiki/Architecture-overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The project has the following main files and folders:
1717
│ │ └── versionControl # the templates files for version control
1818
│ └── terraform # the templates folders
1919
│ ├── core # the templates files for the core folder
20+
│ ├── core-v5 # the templates files for the v5 resource core folder
2021
│ └── shared # the templates files for the shared folder
2122
├── src # the source code of the CLI
2223
│ ├── commands # the commands of the CLI

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: 3 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,8 @@ const applyAdvancedTemplate = async (options: AwsOptions) => {
2324

2425
if (options.enabledSecurityFeatures) {
2526
await applyAwsVpcFlowLog(options);
27+
// For resource require hashicorp/aws >= 5.0.0 ex:aws_chatbot_slack_channel_configuration
28+
await applyAwsCloudtrail(options);
2629
}
2730
};
2831

src/generators/addons/aws/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const AWS_DEFAULT_REGION = 'ap-southeast-1';
22
const AWS_TEMPLATE_PATH = `addons/aws`;
3+
const AWS_PROVIDER_V5_PATH = `${AWS_TEMPLATE_PATH}/modules-v5/provider.tf`;
34
const AWS_SECURITY_GROUP_PATH = `modules/security_group`;
45
const AWS_SECURITY_GROUP_MAIN_PATH = `${AWS_SECURITY_GROUP_PATH}/main.tf`;
56
const AWS_SECURITY_GROUP_OUTPUTS_PATH = `${AWS_SECURITY_GROUP_PATH}/outputs.tf`;
@@ -8,6 +9,7 @@ const AWS_SECURITY_GROUP_VARIABLES_PATH = `${AWS_SECURITY_GROUP_PATH}/variables.
89
export {
910
AWS_DEFAULT_REGION,
1011
AWS_TEMPLATE_PATH,
12+
AWS_PROVIDER_V5_PATH,
1113
AWS_SECURITY_GROUP_PATH,
1214
AWS_SECURITY_GROUP_MAIN_PATH,
1315
AWS_SECURITY_GROUP_OUTPUTS_PATH,

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-v5/cloudtrail',
57+
mainContent: 'module "cloudtrail"',
58+
applyModuleFunction: (options: AwsOptions) => applyAwsCloudtrail(options),
59+
},
5360
ecr: {
5461
name: 'ecr',
5562
path: 'modules/ecr',
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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-v5/main.tf',
42+
'core-v5/providers.tf',
43+
'core-v5/outputs.tf',
44+
'core-v5/variables.tf',
45+
'modules-v5/cloudtrail/main.tf',
46+
'modules-v5/cloudtrail/variables.tf',
47+
'modules-v5/cloudtrail/outputs.tf',
48+
'modules-v5/cloudtrail/s3-bucket-cloudtrail/main.tf',
49+
'modules-v5/cloudtrail/s3-bucket-cloudtrail/variables.tf',
50+
'modules-v5/cloudtrail/s3-bucket-cloudtrail/outputs.tf',
51+
'modules-v5/cloudtrail/sns/main.tf',
52+
'modules-v5/cloudtrail/sns/variables.tf',
53+
'modules-v5/cloudtrail/sns/outputs.tf',
54+
'modules-v5/cloudtrail/alerts/main.tf',
55+
'modules-v5/cloudtrail/alerts/variables.tf',
56+
'modules-v5/cloudtrail/alerts/outputs.tf',
57+
];
58+
59+
expect(projectDir).toHaveFiles(expectedFiles);
60+
});
61+
62+
it('adds cloudtrail module to main.tf', () => {
63+
expect(projectDir).toHaveContentInFile(
64+
'core-v5/main.tf',
65+
cloudtrailModuleContent
66+
);
67+
});
68+
69+
it('adds cloudtrail variables to variables.tf', () => {
70+
expect(projectDir).toHaveContentInFile(
71+
'core-v5/variables.tf',
72+
cloudtrailVariablesContent
73+
);
74+
});
75+
76+
it('adds cloudtrail outputs to outputs.tf', () => {
77+
expect(projectDir).toHaveContentInFile(
78+
'core-v5/outputs.tf',
79+
cloudtrailOutputsContent
80+
);
81+
});
82+
});
83+
});
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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_V5_MAIN_PATH,
7+
INFRA_CORE_V5_VARIABLES_PATH,
8+
INFRA_CORE_V5_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_trail_name" {
16+
description = "Name of the CloudTrail trail"
17+
type = string
18+
}
19+
20+
variable "cloudtrail_s3_bucket_name" {
21+
description = "Name for the S3 bucket to store CloudTrail logs"
22+
type = string
23+
}
24+
25+
variable "cloudtrail_s3_key_prefix" {
26+
description = "S3 key prefix for the CloudTrail logs"
27+
type = string
28+
default = "cloudtrail"
29+
}
30+
31+
variable "cloudtrail_event_type" {
32+
description = "Type of events that the trail will log. Valid values are 'All', 'Management', 'Data' and 'Insight'"
33+
type = string
34+
default = "All"
35+
}
36+
37+
variable "cloudtrail_log_retention_days" {
38+
description = "The number of days to retain CloudTrail logs in CloudWatch"
39+
type = number
40+
default = 365
41+
}
42+
43+
variable "cloudtrail_sns_topic_name" {
44+
description = "Name for the SNS topic to send CloudTrail notifications"
45+
type = string
46+
}
47+
48+
variable "cloudtrail_enable_alerts" {
49+
description = "Enable CloudTrail alerts and monitoring"
50+
type = bool
51+
default = false
52+
}
53+
54+
variable "cloudtrail_slack_channel_id" {
55+
description = "Slack channel ID (starts with C)"
56+
type = string
57+
}
58+
59+
variable "cloudtrail_slack_team_id" {
60+
description = "Slack workspace/team ID (starts with T)"
61+
type = string
62+
}`;
63+
64+
const cloudtrailModuleContent = dedent`
65+
module "cloudtrail_s3_bucket" {
66+
source = "../modules-v5/cloudtrail/s3-bucket-cloudtrail"
67+
68+
env_namespace = local.env_namespace
69+
s3_bucket_name = var.cloudtrail_s3_bucket_name
70+
log_retention_days = var.cloudtrail_log_retention_days
71+
trail_names = [var.cloudtrail_trail_name]
72+
s3_key_prefixes = [var.cloudtrail_s3_key_prefix]
73+
}
74+
75+
module "cloudtrail_sns" {
76+
source = "../modules-v5/cloudtrail/sns"
77+
78+
env_namespace = local.env_namespace
79+
sns_name = var.cloudtrail_sns_topic_name
80+
trail_names = [var.cloudtrail_trail_name]
81+
}
82+
83+
module "cloudtrail" {
84+
depends_on = [module.cloudtrail_s3_bucket, module.cloudtrail_sns]
85+
source = "../modules-v5/cloudtrail"
86+
87+
env_namespace = local.env_namespace
88+
trail_name = var.cloudtrail_trail_name
89+
cloudtrail_event_type = var.cloudtrail_event_type
90+
s3_bucket_name = var.cloudtrail_s3_bucket_name
91+
s3_bucket_id = module.cloudtrail_s3_bucket.s3_bucket_id
92+
s3_key_prefix = var.cloudtrail_s3_key_prefix
93+
sns_topic_arn = module.cloudtrail_sns.sns_topic_arn
94+
log_retention_days = var.cloudtrail_log_retention_days
95+
s3_ignore_data_bucket_arns = [module.cloudtrail_s3_bucket.s3_bucket_arn]
96+
}
97+
98+
# CloudTrail alerts and monitoring (optional)
99+
module "cloudtrail_alerts" {
100+
count = var.cloudtrail_enable_alerts ? 1 : 0
101+
depends_on = [module.cloudtrail_sns]
102+
source = "../modules-v5/cloudtrail/alerts"
103+
104+
env_namespace = local.env_namespace
105+
cloudtrail_log_group_name = "/aws/cloudtrail/\${var.cloudtrail_trail_name}"
106+
sns_topic_arn = module.cloudtrail_sns.sns_topic_arn
107+
slack_channel_id = var.cloudtrail_slack_channel_id
108+
slack_team_id = var.cloudtrail_slack_team_id
109+
chatbot_role_name = "AWSChatbotRole"
110+
chatbot_configuration_name = "cloudtrail-alerts-channel"
111+
metric_filter_name = "FailedLoginAttempts"
112+
metric_namespace = "CloudTrailMetrics"
113+
alarm_name = "FailedLoginAttemptsAlarm"
114+
alarm_threshold = 4
115+
alarm_period = 300
116+
}`;
117+
118+
const cloudtrailOutputsContent = dedent`
119+
output "cloudtrail_s3_bucket_id" {
120+
description = "The ID of the S3 bucket for CloudTrail logs"
121+
value = module.cloudtrail_s3_bucket.s3_bucket_id
122+
}
123+
124+
output "cloudtrail_s3_bucket_arn" {
125+
description = "The ARN of the S3 bucket for CloudTrail logs"
126+
value = module.cloudtrail_s3_bucket.s3_bucket_arn
127+
}
128+
129+
output "cloudtrail_arn" {
130+
description = "The ARN of the CloudTrail"
131+
value = module.cloudtrail.cloudtrail_arn
132+
}
133+
134+
output "cloudtrail_sns_topic_arn" {
135+
description = "The ARN of the SNS topic for CloudTrail notifications"
136+
value = module.cloudtrail_sns.sns_topic_arn
137+
}
138+
139+
output "cloudtrail_arm" {
140+
description = "The ARN of CloudTrail"
141+
value = module.cloudtrail.cloudtrail_arn
142+
}`;
143+
144+
const applyAwsCloudtrail = async (options: AwsOptions) => {
145+
if (isAwsModuleAdded('cloudtrail', options.projectName)) {
146+
return;
147+
}
148+
149+
copy(
150+
`${AWS_TEMPLATE_PATH}/modules-v5/cloudtrail`,
151+
'modules-v5/cloudtrail',
152+
options.projectName
153+
);
154+
appendToFile(
155+
INFRA_CORE_V5_VARIABLES_PATH,
156+
cloudtrailVariablesContent,
157+
options.projectName
158+
);
159+
appendToFile(
160+
INFRA_CORE_V5_MAIN_PATH,
161+
cloudtrailModuleContent,
162+
options.projectName
163+
);
164+
appendToFile(
165+
INFRA_CORE_V5_OUTPUTS_PATH,
166+
cloudtrailOutputsContent,
167+
options.projectName
168+
);
169+
};
170+
171+
export default applyAwsCloudtrail;
172+
export {
173+
cloudtrailVariablesContent,
174+
cloudtrailModuleContent,
175+
cloudtrailOutputsContent,
176+
};

src/generators/addons/aws/modules/core/provider.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { AwsOptions } from '@/generators/addons/aws';
2-
import { AWS_TEMPLATE_PATH } from '@/generators/addons/aws/constants';
2+
import {
3+
AWS_TEMPLATE_PATH,
4+
AWS_PROVIDER_V5_PATH,
5+
} from '@/generators/addons/aws/constants';
36
import {
47
INFRA_CORE_PATH,
8+
INFRA_CORE_V5_PATH,
59
INFRA_SHARED_PATH,
610
} from '@/generators/terraform/constants';
711
import { copy } from '@/helpers/file';
@@ -17,6 +21,15 @@ const applyTerraformAwsProvider = async (options: AwsOptions) => {
1721
`${INFRA_SHARED_PATH}/providers.tf`,
1822
options.projectName
1923
);
24+
25+
// For advanced infrastructure, also copy to core-v5
26+
if (options.infrastructureType === 'advanced') {
27+
copy(
28+
AWS_PROVIDER_V5_PATH,
29+
`${INFRA_CORE_V5_PATH}/providers.tf`,
30+
options.projectName
31+
);
32+
}
2033
};
2134

2235
export default applyTerraformAwsProvider;

0 commit comments

Comments
 (0)