Skip to content

Commit 3e2f4ff

Browse files
authored
chore: add CloudTrail Control Tower to generate AWS (#1464)
1 parent 2e98444 commit 3e2f4ff

File tree

6 files changed

+445
-75
lines changed

6 files changed

+445
-75
lines changed

cli/cmd/generate_aws.go

Lines changed: 219 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,26 @@ var (
6868
QuestionCloudtrailName = "Name of cloudtrail integration (optional):"
6969
QuestionCloudtrailAdvanced = "Configure advanced options?"
7070

71+
// CloudTrail Control Tower questions
72+
QuestionControlTower = "Is your AWS organzation using Control Tower?"
73+
QuestionControlTowerS3BucketArn = "AWS Control Tower S3 bucket ARN:"
74+
QuestionControlTowerSnsTopicArn = "AWS Control Tower SNS topic ARN:"
75+
QuestionControlTowerAuditAccountProfile = "AWS Control Tower audit account profile:"
76+
QuestionControlTowerAuditAccountRegion = "AWS Control Tower audit account region:"
77+
QuestionControlTowerLogArchiveAccountProfile = "AWS Control Tower log archive account profile:"
78+
QuestionControlTowerLogArchiveAccountRegion = "AWS Control Tower log archive account region:"
79+
QuestionControlTowerKmsKeyArn = "AWS Control Tower custom KMS Key ARN (optional):"
80+
7181
// CloudTrail advanced options
7282
OptCloudtrailMessage = "Which options would you like to configure?"
7383

74-
OptCloudtrailOrg = "Configure org account mappings"
75-
OptCloudtrailS3 = "Configure S3 bucket"
76-
OptCloudtrailSNS = "Configure SNS topic"
77-
OptCloudtrailSQS = "Configure SQS queue"
78-
OptCloudtrailIAM = "Configure an existing IAM role"
79-
OptCloudtrailDone = "Done"
84+
OptCloudtrailOrg = "Configure org account mappings"
85+
OptCloudtrailKmsKeyArn = "Configure custom KMS key"
86+
OptCloudtrailS3 = "Configure S3 bucket"
87+
OptCloudtrailSNS = "Configure SNS topic"
88+
OptCloudtrailSQS = "Configure SQS queue"
89+
OptCloudtrailIAM = "Configure an existing IAM role"
90+
OptCloudtrailDone = "Done"
8091

8192
// CloudTrail Org questions
8293
QuestionCloudtrailOrgAccountMappingsDefaultLWAccount = "Org account mappings default Lacework account:"
@@ -188,6 +199,10 @@ See help output for more details on the parameter value(s) required for Terrafor
188199
aws.WithConfigOrgId(GenerateAwsCommandState.ConfigOrgId),
189200
aws.WithConfigOrgUnits(GenerateAwsCommandState.ConfigOrgUnits),
190201
aws.WithConfigOrgCfResourcePrefix(GenerateAwsCommandState.ConfigOrgCfResourcePrefix),
202+
aws.WithControlTower(GenerateAwsCommandState.ControlTower),
203+
aws.WithControlTowerAuditAccount(GenerateAwsCommandState.ControlTowerAuditAccount),
204+
aws.WithControlTowerLogArchiveAccount(GenerateAwsCommandState.ControlTowerLogArchiveAccount),
205+
aws.WithControlTowerKmsKeyArn(GenerateAwsCommandState.ControlTowerKmsKeyArn),
191206
aws.WithConsolidatedCloudtrail(GenerateAwsCommandState.ConsolidatedCloudtrail),
192207
aws.WithCloudtrailUseExistingS3(GenerateAwsCommandState.CloudtrailUseExistingS3),
193208
aws.WithCloudtrailUseExistingSNSTopic(GenerateAwsCommandState.CloudtrailUseExistingSNSTopic),
@@ -399,6 +414,28 @@ See help output for more details on the parameter value(s) required for Terrafor
399414
GenerateAwsCommandState.AgentlessScanningAccounts = accounts
400415
}
401416

417+
// Parse passed in Control Tower Audit account
418+
if GenerateAwsCommandExtraState.ControlTowerAuditAccount != "" {
419+
accounts, err := parseAwsAccountsFromCommandFlag(
420+
[]string{GenerateAwsCommandExtraState.ControlTowerAuditAccount},
421+
)
422+
if err != nil {
423+
return err
424+
}
425+
GenerateAwsCommandState.ControlTowerAuditAccount = &accounts[0]
426+
}
427+
428+
// Parse passed in Control Tower Log Archive account
429+
if GenerateAwsCommandExtraState.ControlTowerLogArchiveAccount != "" {
430+
accounts, err := parseAwsAccountsFromCommandFlag(
431+
[]string{GenerateAwsCommandExtraState.ControlTowerLogArchiveAccount},
432+
)
433+
if err != nil {
434+
return err
435+
}
436+
GenerateAwsCommandState.ControlTowerLogArchiveAccount = &accounts[0]
437+
}
438+
402439
return nil
403440
},
404441
}
@@ -461,6 +498,26 @@ func initGenerateAwsTfCommandFlags() {
461498
"agentless_scanning_accounts",
462499
[]string{},
463500
"AWS scanning accounts for Agentless integrations; value format must be <aws profile>:<region>")
501+
generateAwsTfCommand.PersistentFlags().BoolVar(
502+
&GenerateAwsCommandState.ControlTower,
503+
"controltower",
504+
false,
505+
"enable Control Tower integration")
506+
generateAwsTfCommand.PersistentFlags().StringVar(
507+
&GenerateAwsCommandExtraState.ControlTowerAuditAccount,
508+
"controltower_audit_account",
509+
"",
510+
"specify AWS Control Tower Audit account; value format must be <aws profile>:<region>")
511+
generateAwsTfCommand.PersistentFlags().StringVar(
512+
&GenerateAwsCommandExtraState.ControlTowerLogArchiveAccount,
513+
"controltower_log_archive_account",
514+
"",
515+
"specify AWS Control Tower Log Archive account; value format must be <aws profile>:<region>")
516+
generateAwsTfCommand.PersistentFlags().StringVar(
517+
&GenerateAwsCommandState.ControlTowerKmsKeyArn,
518+
"controltower_kms_key_arn",
519+
"",
520+
"specify AWS Control Tower custom kMS key ARN")
464521
generateAwsTfCommand.PersistentFlags().BoolVar(
465522
&GenerateAwsCommandState.Cloudtrail,
466523
"cloudtrail",
@@ -955,10 +1012,17 @@ func promptCloudtrailQuestions(
9551012
return nil
9561013
}
9571014

1015+
if err := promptCloudtrailControlTowerQuestions(config); err != nil {
1016+
return err
1017+
}
1018+
1019+
noControlTower := !config.ControlTower
1020+
9581021
if err := SurveyQuestionInteractiveOnly(SurveyQuestionWithValidationArgs{
9591022
Icon: IconCloudTrail,
9601023
Prompt: &survey.Confirm{Message: QuestionCloudtrailUseConsolidated, Default: config.ConsolidatedCloudtrail},
9611024
Response: &config.ConsolidatedCloudtrail,
1025+
Checks: []*bool{&noControlTower},
9621026
}); err != nil {
9631027
return err
9641028
}
@@ -983,6 +1047,13 @@ func promptCloudtrailQuestions(
9831047
OptCloudtrailDone,
9841048
}
9851049
if config.AwsOrganization {
1050+
if config.ControlTower {
1051+
options = []string{
1052+
OptCloudtrailKmsKeyArn,
1053+
OptCloudtrailIAM,
1054+
OptCloudtrailDone,
1055+
}
1056+
}
9861057
options = append([]string{OptCloudtrailOrg}, options...)
9871058
}
9881059
for answer != OptCloudtrailDone {
@@ -1001,6 +1072,10 @@ func promptCloudtrailQuestions(
10011072
if err := promptCloudtrailOrgQuestions(config); err != nil {
10021073
return err
10031074
}
1075+
case OptCloudtrailKmsKeyArn:
1076+
if err := promptCloudtrailKmsKeyQuestions(config); err != nil {
1077+
return err
1078+
}
10041079
case OptCloudtrailS3:
10051080
if err := promptCloudtrailS3Questions(config); err != nil {
10061081
return err
@@ -1024,6 +1099,128 @@ func promptCloudtrailQuestions(
10241099
return nil
10251100
}
10261101

1102+
func promptCloudtrailControlTowerQuestions(config *aws.GenerateAwsTfConfigurationArgs) error {
1103+
if err := SurveyQuestionInteractiveOnly(SurveyQuestionWithValidationArgs{
1104+
Icon: IconCloudTrail,
1105+
Prompt: &survey.Confirm{Message: QuestionControlTower, Default: config.ControlTower},
1106+
Response: &config.ControlTower,
1107+
Checks: []*bool{&config.AwsOrganization},
1108+
}); err != nil {
1109+
return err
1110+
}
1111+
1112+
if !config.ControlTower {
1113+
return nil
1114+
}
1115+
1116+
if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{
1117+
{
1118+
Icon: IconCloudTrail,
1119+
Prompt: &survey.Input{
1120+
Message: QuestionControlTowerS3BucketArn,
1121+
Default: config.ExistingCloudtrailBucketArn,
1122+
},
1123+
Required: true,
1124+
Opts: []survey.AskOpt{survey.WithValidator(validateAwsArnFormat)},
1125+
Response: &config.ExistingCloudtrailBucketArn,
1126+
},
1127+
{
1128+
Icon: IconCloudTrail,
1129+
Prompt: &survey.Input{
1130+
Message: QuestionControlTowerSnsTopicArn,
1131+
Default: config.ExistingSnsTopicArn,
1132+
},
1133+
Required: true,
1134+
Opts: []survey.AskOpt{survey.WithValidator(validateAwsArnFormat)},
1135+
Response: &config.ExistingSnsTopicArn,
1136+
},
1137+
}); err != nil {
1138+
return err
1139+
}
1140+
1141+
profile := ""
1142+
region := ""
1143+
defaultProfile := ""
1144+
defaultRegion := ""
1145+
if config.ControlTowerAuditAccount != nil {
1146+
defaultProfile = config.ControlTowerAuditAccount.AwsProfile
1147+
}
1148+
if config.ControlTowerAuditAccount != nil {
1149+
defaultRegion = config.ControlTowerAuditAccount.AwsRegion
1150+
}
1151+
1152+
if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{
1153+
{
1154+
Icon: IconCloudTrail,
1155+
Prompt: &survey.Input{
1156+
Message: QuestionControlTowerAuditAccountProfile,
1157+
Default: defaultProfile,
1158+
},
1159+
Required: true,
1160+
Response: &profile,
1161+
},
1162+
{
1163+
Icon: IconCloudTrail,
1164+
Prompt: &survey.Input{
1165+
Message: QuestionControlTowerAuditAccountRegion,
1166+
Default: defaultRegion,
1167+
},
1168+
Required: true,
1169+
Opts: []survey.AskOpt{survey.WithValidator(validateAwsRegion)},
1170+
Response: &region,
1171+
},
1172+
}); err != nil {
1173+
return err
1174+
}
1175+
1176+
config.ControlTowerAuditAccount = &aws.AwsSubAccount{
1177+
AwsProfile: profile,
1178+
AwsRegion: region,
1179+
Alias: fmt.Sprintf("%s-%s", profile, region),
1180+
}
1181+
1182+
defaultProfile = ""
1183+
defaultRegion = ""
1184+
if config.ControlTowerLogArchiveAccount != nil {
1185+
defaultProfile = config.ControlTowerLogArchiveAccount.AwsProfile
1186+
}
1187+
if config.ControlTowerLogArchiveAccount != nil {
1188+
defaultRegion = config.ControlTowerLogArchiveAccount.AwsRegion
1189+
}
1190+
1191+
if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{
1192+
{
1193+
Icon: IconCloudTrail,
1194+
Prompt: &survey.Input{
1195+
Message: QuestionControlTowerLogArchiveAccountProfile,
1196+
Default: defaultProfile,
1197+
},
1198+
Required: true,
1199+
Response: &profile,
1200+
},
1201+
{
1202+
Icon: IconCloudTrail,
1203+
Prompt: &survey.Input{
1204+
Message: QuestionControlTowerLogArchiveAccountRegion,
1205+
Default: defaultRegion,
1206+
},
1207+
Required: true,
1208+
Opts: []survey.AskOpt{survey.WithValidator(validateAwsRegion)},
1209+
Response: &region,
1210+
},
1211+
}); err != nil {
1212+
return err
1213+
}
1214+
1215+
config.ControlTowerLogArchiveAccount = &aws.AwsSubAccount{
1216+
AwsProfile: profile,
1217+
AwsRegion: region,
1218+
Alias: fmt.Sprintf("%s-%s", profile, region),
1219+
}
1220+
1221+
return nil
1222+
}
1223+
10271224
func promptCloudtrailOrgAccountMappingQuestions(config *aws.GenerateAwsTfConfigurationArgs) error {
10281225
mapping := aws.OrgAccountMap{}
10291226
var accountsAnswer string
@@ -1075,6 +1272,22 @@ func promptCloudtrailOrgQuestions(config *aws.GenerateAwsTfConfigurationArgs) er
10751272
return nil
10761273
}
10771274

1275+
func promptCloudtrailKmsKeyQuestions(config *aws.GenerateAwsTfConfigurationArgs) error {
1276+
if err := SurveyQuestionInteractiveOnly(SurveyQuestionWithValidationArgs{
1277+
Icon: IconCloudTrail,
1278+
Prompt: &survey.Input{
1279+
Message: QuestionControlTowerKmsKeyArn,
1280+
Default: config.ControlTowerKmsKeyArn,
1281+
},
1282+
Opts: []survey.AskOpt{survey.WithValidator(validateAwsArnFormat)},
1283+
Response: &config.ControlTowerKmsKeyArn,
1284+
}); err != nil {
1285+
return err
1286+
}
1287+
1288+
return nil
1289+
}
1290+
10781291
func promptCloudtrailS3Questions(config *aws.GenerateAwsTfConfigurationArgs) error {
10791292
if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{
10801293
{

cli/docs/lacework_generate_cloud-account_aws.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ lacework generate cloud-account aws [flags]
6363
--config_organization_id string specify AWS organization ID for Config organization integration
6464
--config_organization_units strings specify AWS organization units for Config organization integration
6565
--consolidated_cloudtrail use consolidated trail
66+
--controltower enable Control Tower integration
67+
--controltower_audit_account string specify AWS Control Tower Audit account; value format must be <aws profile>:<region>
68+
--controltower_kms_key_arn string specify AWS Control Tower custom kMS key ARN
69+
--controltower_log_archive_account string specify AWS Control Tower Log Archive account; value format must be <aws profile>:<region>
6670
--existing_bucket_arn string specify existing cloudtrail S3 bucket ARN
6771
--existing_iam_role_arn string specify existing iam role arn to use
6872
--existing_iam_role_externalid string specify existing iam role external_id to use

integration/aws_generation_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ func TestGenerationAwsCloudtrailOrganization(t *testing.T) {
436436
MsgRsp{cmd.QuestionEnableAgentless, "n"},
437437
MsgRsp{cmd.QuestionEnableConfig, "n"},
438438
MsgRsp{cmd.QuestionEnableCloudtrail, "y"},
439+
MsgRsp{cmd.QuestionControlTower, "n"},
439440
MsgRsp{cmd.QuestionCloudtrailUseConsolidated, "y"},
440441
MsgRsp{cmd.QuestionCloudtrailAdvanced, "y"},
441442
MsgMenu{cmd.OptCloudtrailOrg, 0},
@@ -484,6 +485,72 @@ func TestGenerationAwsCloudtrailOrganization(t *testing.T) {
484485
assert.Equal(t, buildTf, tfResult)
485486
}
486487

488+
// Test CloudTrail Control Tower integration
489+
func TestGenerationAwsCloudtrailControlTower(t *testing.T) {
490+
os.Setenv("LW_NOCACHE", "true")
491+
defer os.Setenv("LW_NOCACHE", "")
492+
var final string
493+
494+
s3BucketArn := "arn:aws:s3:::bucket-name"
495+
snsTopicArn := "arn:aws:sns:us-east-2:249446771485:topic-name"
496+
497+
// Tempdir for test
498+
dir, err := os.MkdirTemp("", "t")
499+
if err != nil {
500+
panic(err)
501+
}
502+
defer os.RemoveAll(dir)
503+
504+
// Run CLI
505+
runGenerateTest(t,
506+
func(c *expect.Console) {
507+
expectsCliOutput(t, c, []MsgRspHandler{
508+
MsgRsp{cmd.QuestionEnableAwsOrganization, "y"},
509+
MsgRsp{cmd.QuestionMainAwsProfile, "main"},
510+
MsgRsp{cmd.QuestionMainAwsRegion, "us-east-2"},
511+
MsgRsp{cmd.QuestionEnableAgentless, "n"},
512+
MsgRsp{cmd.QuestionEnableConfig, "n"},
513+
MsgRsp{cmd.QuestionEnableCloudtrail, "y"},
514+
MsgRsp{cmd.QuestionControlTower, "y"},
515+
MsgRsp{cmd.QuestionControlTowerS3BucketArn, s3BucketArn},
516+
MsgRsp{cmd.QuestionControlTowerSnsTopicArn, snsTopicArn},
517+
MsgRsp{cmd.QuestionControlTowerAuditAccountProfile, "audit"},
518+
MsgRsp{cmd.QuestionControlTowerAuditAccountRegion, "us-west-1"},
519+
MsgRsp{cmd.QuestionControlTowerLogArchiveAccountProfile, "log-archive"},
520+
MsgRsp{cmd.QuestionControlTowerLogArchiveAccountRegion, "us-west-2"},
521+
MsgRsp{cmd.QuestionCloudtrailAdvanced, "n"},
522+
MsgRsp{cmd.QuestionAwsOutputLocation, dir},
523+
MsgRsp{cmd.QuestionRunTfPlan, "n"},
524+
})
525+
final, _ = c.ExpectEOF()
526+
},
527+
"generate",
528+
"cloud-account",
529+
"aws",
530+
)
531+
532+
// Get result
533+
tfResult, _ := os.ReadFile(filepath.FromSlash(fmt.Sprintf("%s/main.tf", dir)))
534+
535+
auditAccount := aws.NewAwsSubAccount("audit", "us-west-1", "audit-us-west-1")
536+
logArchiveAccount := aws.NewAwsSubAccount("log-archive", "us-west-2", "log-archive-us-west-2")
537+
538+
// Ensure CLI ran correctly
539+
assert.Contains(t, final, "Terraform code saved in")
540+
541+
// Create the TF directly with lwgenerate and validate same result via CLI
542+
buildTf, _ := aws.NewTerraform(true, false, false, true,
543+
aws.WithAwsProfile("main"),
544+
aws.WithAwsRegion("us-east-2"),
545+
aws.WithControlTower(true),
546+
aws.WithControlTowerAuditAccount(&auditAccount),
547+
aws.WithControlTowerLogArchiveAccount(&logArchiveAccount),
548+
aws.WithExistingCloudtrailBucketArn(s3BucketArn),
549+
aws.WithExistingSnsTopicArn(snsTopicArn),
550+
).Generate()
551+
assert.Equal(t, buildTf, string(tfResult))
552+
}
553+
487554
// Test CloudTrail integration with existing S3 bucket
488555
func TestGenerationAwsCloudtrailWithExistingS3Bucket(t *testing.T) {
489556
os.Setenv("LW_NOCACHE", "true")

integration/test_resources/help/generate_cloud-account_aws

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ Flags:
5151
--config_organization_id string specify AWS organization ID for Config organization integration
5252
--config_organization_units strings specify AWS organization units for Config organization integration
5353
--consolidated_cloudtrail use consolidated trail
54+
--controltower enable Control Tower integration
55+
--controltower_audit_account string specify AWS Control Tower Audit account; value format must be <aws profile>:<region>
56+
--controltower_kms_key_arn string specify AWS Control Tower custom kMS key ARN
57+
--controltower_log_archive_account string specify AWS Control Tower Log Archive account; value format must be <aws profile>:<region>
5458
--existing_bucket_arn string specify existing cloudtrail S3 bucket ARN
5559
--existing_iam_role_arn string specify existing iam role arn to use
5660
--existing_iam_role_externalid string specify existing iam role external_id to use

0 commit comments

Comments
 (0)