Skip to content

Commit 29935df

Browse files
feat: add org-cloudtrail to lw_generate (#1433)
* feat(GROW-2540): add agentless to generate command * chore: pr suggestions * feat: add org-cloudtrail to lw_generate Signed-off-by: Darren Murray <[email protected]> * feat: cloudtrail org account mapping Signed-off-by: Darren Murray <[email protected]> feat: cloudtrail org account mapping Signed-off-by: Darren Murray <[email protected]> --------- Signed-off-by: Darren Murray <[email protected]>
1 parent 4185b65 commit 29935df

File tree

5 files changed

+381
-4
lines changed

5 files changed

+381
-4
lines changed

cli/cmd/generate_aws.go

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"strings"
67
"time"
@@ -48,6 +49,14 @@ var (
4849
QuestionAwsAnotherAdvancedOpt = "Configure another advanced integration option"
4950
QuestionAwsCustomizeOutputLocation = "Provide the location for the output to be written:"
5051

52+
// Cloudtrail Org Questions
53+
QuestionEnableCloudtrailOrganization = "Enable CloudTrail organizational integration?"
54+
QuestionConfigureCloudtrailOrganizationMappings = "Configure CloudTrail organization account mappings?"
55+
QuestionCloudtrailAccountMappingsLWDefaultAccount = "Specify org account mappings default Lacework account:"
56+
QuestionCloudtrailOrgAccountMappingAnotherAdvancedOpt = "Configure another org account mapping?"
57+
QuestionCloudtrailOrgAccountMappingsLWAccount = "Specify lacework account: "
58+
QuestionCloudtrailOrgAccountMappingsAwsAccounts = "Specify aws accounts:"
59+
5160
// S3 Bucket Questions
5261
QuestionBucketEnableEncryption = "Enable S3 bucket encryption when creating bucket"
5362
QuestionBucketSseKeyArn = "Specify existing KMS encryption key arn for S3 bucket (optional)"
@@ -123,6 +132,13 @@ See help output for more details on the parameter value(s) required for Terrafor
123132
GenerateAwsCommandState.LaceworkProfile = cli.Profile
124133
}
125134

135+
// Parse org_account_mapping json, if passed
136+
if cmd.Flags().Changed("cloudtrail_org_account_mapping") {
137+
if err := parseCloudtrailOrgAccountMappingsFlag(GenerateAwsCommandState); err != nil {
138+
return err
139+
}
140+
}
141+
126142
// Setup modifiers for NewTerraform constructor
127143
mods := []aws.AwsTerraformModifier{
128144
aws.WithAwsProfile(GenerateAwsCommandState.AwsProfile),
@@ -138,6 +154,7 @@ See help output for more details on the parameter value(s) required for Terrafor
138154
aws.UseExistingIamRole(GenerateAwsCommandState.ExistingIamRole),
139155
aws.WithConfigName(GenerateAwsCommandState.ConfigName),
140156
aws.WithCloudtrailName(GenerateAwsCommandState.CloudtrailName),
157+
aws.WithOrgAccountMappings(GenerateAwsCommandState.OrgAccountMappings),
141158
aws.WithBucketName(GenerateAwsCommandState.BucketName),
142159
aws.WithBucketEncryptionEnabled(GenerateAwsCommandState.BucketEncryptionEnabled),
143160
aws.WithBucketSSEKeyArn(GenerateAwsCommandState.BucketSseKeyArn),
@@ -446,6 +463,11 @@ func initGenerateAwsTfCommandFlags() {
446463
"consolidated_cloudtrail",
447464
false,
448465
"use consolidated trail")
466+
generateAwsTfCommand.PersistentFlags().StringVar(
467+
&GenerateAwsCommandState.OrgAccountMappingsJson,
468+
"cloudtrail_org_account_mapping", "", "Org account mapping json string. Example: "+
469+
"'{\"default_lacework_account\":\"main\", \"mapping\": [{ \"aws_accounts\": [\"123456789011\"], "+
470+
"\"lacework_account\": \"sub-account-1\"}]}'")
449471

450472
// DEPRECATED
451473
generateAwsTfCommand.PersistentFlags().BoolVar(
@@ -707,6 +729,13 @@ func promptAwsCtQuestions(config *aws.GenerateAwsTfConfigurationArgs, extraState
707729
return err
708730
}
709731
if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{
732+
{
733+
Prompt: &survey.Confirm{
734+
Message: QuestionEnableCloudtrailOrganization,
735+
Default: config.AwsOrganization,
736+
},
737+
Response: &config.AwsOrganization,
738+
},
710739
{
711740
Prompt: &survey.Input{Message: QuestionCloudtrailName, Default: config.CloudtrailName},
712741
Checks: []*bool{existingCloudTrailDisabled(extraState)},
@@ -726,6 +755,25 @@ func promptAwsCtQuestions(config *aws.GenerateAwsTfConfigurationArgs, extraState
726755
return err
727756
}
728757

758+
if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{
759+
{Prompt: &survey.Confirm{
760+
Message: QuestionConfigureCloudtrailOrganizationMappings,
761+
Default: config.AwsOrganizationMappings,
762+
},
763+
Response: &config.AwsOrganizationMappings,
764+
Checks: []*bool{&config.AwsOrganization},
765+
},
766+
}, config.Cloudtrail); err != nil {
767+
return err
768+
}
769+
770+
if config.Cloudtrail && config.AwsOrganizationMappings {
771+
err := promptCloudtrailOrgAccountMappings(config)
772+
if err != nil {
773+
return err
774+
}
775+
}
776+
729777
newBucket := config.ExistingCloudtrailBucketArn == ""
730778
if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{
731779
// If new bucket created, allow user to optionally name the bucket
@@ -1124,7 +1172,7 @@ func promptAwsGenerate(
11241172
return errors.New("must enable agentless, cloudtrail or config")
11251173
}
11261174

1127-
if !cli.InteractiveMode() && config.AwsOrganization {
1175+
if !cli.InteractiveMode() && config.AwsOrganization && config.Agentless {
11281176
if config.AgentlessManagementAccountID == "" {
11291177
return errors.New("must specify a management account ID for Agentless organization integration")
11301178
}
@@ -1158,3 +1206,67 @@ func promptAwsGenerate(
11581206

11591207
return nil
11601208
}
1209+
1210+
func promptCloudtrailAddOrgAccountMappings(input *aws.GenerateAwsTfConfigurationArgs) error {
1211+
mapping := aws.OrgAccountMap{}
1212+
var accountsAnswer string
1213+
if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{
1214+
{
1215+
Prompt: &survey.Input{Message: QuestionCloudtrailOrgAccountMappingsLWAccount},
1216+
Response: &mapping.LaceworkAccount,
1217+
},
1218+
{
1219+
Prompt: &survey.Multiline{Message: QuestionCloudtrailOrgAccountMappingsAwsAccounts},
1220+
Response: &accountsAnswer,
1221+
},
1222+
}); err != nil {
1223+
return err
1224+
}
1225+
mapping.AwsAccounts = strings.Split(accountsAnswer, "\n")
1226+
input.OrgAccountMappings.Mapping = append(input.OrgAccountMappings.Mapping, mapping)
1227+
return nil
1228+
}
1229+
1230+
func promptCloudtrailOrgAccountMappings(input *aws.GenerateAwsTfConfigurationArgs) error {
1231+
if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{
1232+
{
1233+
Prompt: &survey.Input{
1234+
Message: QuestionCloudtrailAccountMappingsLWDefaultAccount,
1235+
Default: input.OrgAccountMappings.DefaultLaceworkAccount},
1236+
Response: &input.OrgAccountMappings.DefaultLaceworkAccount,
1237+
},
1238+
}); err != nil {
1239+
return err
1240+
}
1241+
1242+
if err := promptCloudtrailAddOrgAccountMappings(input); err != nil {
1243+
return err
1244+
}
1245+
1246+
var askAgain bool
1247+
for {
1248+
if err := SurveyQuestionInteractiveOnly(SurveyQuestionWithValidationArgs{
1249+
Prompt: &survey.Confirm{Message: QuestionControlTowerOrgAccountMappingAnotherAdvancedOpt},
1250+
Response: &askAgain}); err != nil {
1251+
return err
1252+
}
1253+
1254+
if !askAgain {
1255+
break
1256+
}
1257+
1258+
if err := promptCloudtrailAddOrgAccountMappings(input); err != nil {
1259+
return err
1260+
}
1261+
}
1262+
1263+
return nil
1264+
}
1265+
1266+
func parseCloudtrailOrgAccountMappingsFlag(args *aws.GenerateAwsTfConfigurationArgs) error {
1267+
if err := json.Unmarshal([]byte(args.OrgAccountMappingsJson), &args.OrgAccountMappings); err != nil {
1268+
return errors.Wrap(err, "failed to parse 'cloudtrail_org_account_mapping'")
1269+
}
1270+
1271+
return nil
1272+
}

integration/aws_generation_test.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ func TestGenerationAwsAdvancedOptsConsolidated(t *testing.T) {
220220
MsgMenu{cmd.AwsAdvancedOptDone, 2},
221221
MsgRsp{cmd.QuestionConsolidatedCloudtrail, "y"},
222222
MsgRsp{cmd.QuestionUseExistingCloudtrail, "n"},
223+
MsgRsp{cmd.QuestionEnableCloudtrailOrganization, "n"},
223224
MsgRsp{cmd.QuestionCloudtrailName, ""},
224225
// S3 Bucket Questions
225226
MsgRsp{cmd.QuestionBucketName, ""},
@@ -277,6 +278,7 @@ func TestGenerationAwsAdvancedOptsUseExistingCloudtrail(t *testing.T) {
277278
MsgMenu{cmd.AwsAdvancedOptDone, 2},
278279
MsgRsp{cmd.QuestionConsolidatedCloudtrail, "n"},
279280
MsgRsp{cmd.QuestionUseExistingCloudtrail, "y"},
281+
MsgRsp{cmd.QuestionEnableCloudtrailOrganization, "n"},
280282
MsgRsp{cmd.QuestionCloudtrailExistingBucketArn, "notright"},
281283
MsgRsp{"invalid arn supplied", "arn:aws:s3:::bucket_name"},
282284
// SNS Topic Questions
@@ -330,6 +332,7 @@ func TestGenerationAwsAdvancedOptsConsolidatedWithSubAccounts(t *testing.T) {
330332
MsgMenu{cmd.AwsAdvancedOptDone, 2},
331333
MsgRsp{cmd.QuestionConsolidatedCloudtrail, "y"},
332334
MsgRsp{cmd.QuestionUseExistingCloudtrail, "n"},
335+
MsgRsp{cmd.QuestionEnableCloudtrailOrganization, "n"},
333336
MsgRsp{cmd.QuestionCloudtrailName, ""},
334337
MsgRsp{cmd.QuestionBucketName, ""},
335338
MsgRsp{cmd.QuestionBucketEnableEncryption, "y"},
@@ -546,6 +549,7 @@ func TestGenerationAwsAdvancedOptsUseExistingElements(t *testing.T) {
546549
MsgMenu{cmd.AwsAdvancedOptDone, 2},
547550
MsgRsp{cmd.QuestionConsolidatedCloudtrail, "n"},
548551
MsgRsp{cmd.QuestionUseExistingCloudtrail, "y"},
552+
MsgRsp{cmd.QuestionEnableCloudtrailOrganization, "n"},
549553
MsgRsp{cmd.QuestionCloudtrailExistingBucketArn, bucketArn},
550554
MsgRsp{cmd.QuestionsUseExistingSNSTopic, "y"},
551555
MsgRsp{cmd.QuestionSnsTopicArn, topicArn},
@@ -599,6 +603,7 @@ func TestGenerationAwsAdvancedOptsCreateNewElements(t *testing.T) {
599603
MsgMenu{cmd.AwsAdvancedOptDone, 2},
600604
MsgRsp{cmd.QuestionConsolidatedCloudtrail, "n"},
601605
MsgRsp{cmd.QuestionUseExistingCloudtrail, "n"},
606+
MsgRsp{cmd.QuestionEnableCloudtrailOrganization, "n"},
602607
MsgRsp{cmd.QuestionCloudtrailName, trailName},
603608
// S3 Questions
604609
MsgRsp{cmd.QuestionBucketName, bucketName},
@@ -894,6 +899,7 @@ func TestGenerationAwsS3BucketNotificationInteractive(t *testing.T) {
894899

895900
MsgRsp{cmd.QuestionConsolidatedCloudtrail, ""},
896901
MsgRsp{cmd.QuestionUseExistingCloudtrail, ""},
902+
MsgRsp{cmd.QuestionEnableCloudtrailOrganization, "n"},
897903
MsgRsp{cmd.QuestionCloudtrailName, ""},
898904
// S3 Questions
899905
MsgRsp{cmd.QuestionBucketName, ""},
@@ -930,6 +936,176 @@ func TestGenerationAwsS3BucketNotificationInteractive(t *testing.T) {
930936
assert.Equal(t, buildTf, tfResult)
931937
}
932938

939+
func TestGenerationAwsCloudtrailOrganization(t *testing.T) {
940+
os.Setenv("LW_NOCACHE", "true")
941+
defer os.Setenv("LW_NOCACHE", "")
942+
var final string
943+
var runError error
944+
region := "us-west-2"
945+
946+
tfResult := runGenerateTest(t,
947+
func(c *expect.Console) {
948+
expectsCliOutput(t, c, []MsgRspHandler{
949+
MsgRsp{cmd.QuestionEnableAgentless, "n"},
950+
MsgRsp{cmd.QuestionAwsEnableConfig, "n"},
951+
MsgRsp{cmd.QuestionEnableCloudtrail, "y"},
952+
MsgRsp{cmd.QuestionAwsRegion, region},
953+
954+
MsgRsp{cmd.QuestionAwsConfigAdvanced, "y"},
955+
MsgMenu{cmd.AwsAdvancedOptDone, 0},
956+
957+
MsgRsp{cmd.QuestionConsolidatedCloudtrail, ""},
958+
MsgRsp{cmd.QuestionUseExistingCloudtrail, ""},
959+
MsgRsp{cmd.QuestionEnableCloudtrailOrganization, "y"},
960+
MsgRsp{cmd.QuestionCloudtrailName, ""},
961+
MsgRsp{cmd.QuestionConfigureCloudtrailOrganizationMappings, "n"},
962+
// S3 Questions
963+
MsgRsp{cmd.QuestionBucketName, ""},
964+
MsgRsp{cmd.QuestionBucketEnableEncryption, ""},
965+
MsgRsp{cmd.QuestionBucketSseKeyArn, ""},
966+
MsgRsp{cmd.QuestionS3BucketNotification, "n"},
967+
// SNS Topic Questions
968+
MsgRsp{cmd.QuestionsUseExistingSNSTopic, ""},
969+
MsgRsp{cmd.QuestionSnsTopicName, ""},
970+
MsgRsp{cmd.QuestionSnsEnableEncryption, ""},
971+
MsgRsp{cmd.QuestionSnsEncryptionKeyArn, ""},
972+
// SQS Questions
973+
MsgRsp{cmd.QuestionSqsQueueName, ""},
974+
MsgRsp{cmd.QuestionSqsEnableEncryption, ""},
975+
MsgRsp{cmd.QuestionSqsEncryptionKeyArn, ""},
976+
977+
MsgRsp{cmd.QuestionAwsAnotherAdvancedOpt, "n"},
978+
MsgRsp{cmd.QuestionRunTfPlan, "n"},
979+
})
980+
981+
final, _ = c.ExpectEOF()
982+
},
983+
"generate",
984+
"cloud-account",
985+
"aws",
986+
)
987+
988+
assert.Nil(t, runError)
989+
assert.Contains(t, final, "Terraform code saved in")
990+
991+
buildTf, _ := aws.NewTerraform(region, true, false, false,
992+
true).Generate()
993+
assert.Equal(t, buildTf, tfResult)
994+
}
995+
996+
func TestGenerationAwsCloudtrailOrganizationAccountMappings(t *testing.T) {
997+
os.Setenv("LW_NOCACHE", "true")
998+
defer os.Setenv("LW_NOCACHE", "")
999+
var final string
1000+
var runError error
1001+
region := "us-west-2"
1002+
1003+
tfResult := runGenerateTest(t,
1004+
func(c *expect.Console) {
1005+
expectsCliOutput(t, c, []MsgRspHandler{
1006+
MsgRsp{cmd.QuestionEnableAgentless, "n"},
1007+
MsgRsp{cmd.QuestionAwsEnableConfig, "n"},
1008+
MsgRsp{cmd.QuestionEnableCloudtrail, "y"},
1009+
MsgRsp{cmd.QuestionAwsRegion, region},
1010+
1011+
MsgRsp{cmd.QuestionAwsConfigAdvanced, "y"},
1012+
MsgMenu{cmd.AwsAdvancedOptDone, 0},
1013+
1014+
MsgRsp{cmd.QuestionConsolidatedCloudtrail, ""},
1015+
MsgRsp{cmd.QuestionUseExistingCloudtrail, ""},
1016+
MsgRsp{cmd.QuestionEnableCloudtrailOrganization, "y"},
1017+
MsgRsp{cmd.QuestionCloudtrailName, ""},
1018+
MsgRsp{cmd.QuestionConfigureCloudtrailOrganizationMappings, "y"},
1019+
MsgRsp{cmd.QuestionCloudtrailAccountMappingsLWDefaultAccount, "main"},
1020+
MsgRsp{cmd.QuestionCloudtrailOrgAccountMappingsLWAccount, "sub-account-1"},
1021+
MsgMultilineRsp{cmd.QuestionCloudtrailOrgAccountMappingsAwsAccounts, []string{"123456789011"}},
1022+
MsgRsp{cmd.QuestionCloudtrailOrgAccountMappingAnotherAdvancedOpt, "n"},
1023+
// S3 Questions
1024+
MsgRsp{cmd.QuestionBucketName, ""},
1025+
MsgRsp{cmd.QuestionBucketEnableEncryption, ""},
1026+
MsgRsp{cmd.QuestionBucketSseKeyArn, ""},
1027+
MsgRsp{cmd.QuestionS3BucketNotification, "n"},
1028+
// SNS Topic Questions
1029+
MsgRsp{cmd.QuestionsUseExistingSNSTopic, ""},
1030+
MsgRsp{cmd.QuestionSnsTopicName, ""},
1031+
MsgRsp{cmd.QuestionSnsEnableEncryption, ""},
1032+
MsgRsp{cmd.QuestionSnsEncryptionKeyArn, ""},
1033+
// SQS Questions
1034+
MsgRsp{cmd.QuestionSqsQueueName, ""},
1035+
MsgRsp{cmd.QuestionSqsEnableEncryption, ""},
1036+
MsgRsp{cmd.QuestionSqsEncryptionKeyArn, ""},
1037+
1038+
MsgRsp{cmd.QuestionAwsAnotherAdvancedOpt, "n"},
1039+
MsgRsp{cmd.QuestionRunTfPlan, "n"},
1040+
})
1041+
1042+
final, _ = c.ExpectEOF()
1043+
},
1044+
"generate",
1045+
"cloud-account",
1046+
"aws",
1047+
)
1048+
1049+
assert.Nil(t, runError)
1050+
assert.Contains(t, final, "Terraform code saved in")
1051+
1052+
orgAccountMappings := aws.OrgAccountMapping{
1053+
DefaultLaceworkAccount: "main",
1054+
Mapping: []aws.OrgAccountMap{
1055+
{
1056+
LaceworkAccount: "sub-account-1",
1057+
AwsAccounts: []string{"123456789011"},
1058+
},
1059+
},
1060+
}
1061+
1062+
buildTf, _ := aws.NewTerraform(region, true, false, false,
1063+
true, aws.WithOrgAccountMappings(orgAccountMappings)).Generate()
1064+
assert.Equal(t, buildTf, tfResult)
1065+
}
1066+
1067+
func TestGenerationCloudtrailOrgMappingsNonInteractive(t *testing.T) {
1068+
os.Setenv("LW_NOCACHE", "true")
1069+
defer os.Setenv("LW_NOCACHE", "")
1070+
var final string
1071+
var runError error
1072+
region := "us-east-2"
1073+
1074+
tfResult := runGenerateTest(t,
1075+
func(c *expect.Console) {
1076+
final, _ = c.ExpectEOF()
1077+
},
1078+
"generate",
1079+
"ca",
1080+
"aws",
1081+
"--cloudtrail",
1082+
"--aws_region",
1083+
"us-east-2",
1084+
"--aws_organization",
1085+
"--cloudtrail_org_account_mapping",
1086+
"{\"default_lacework_account\":\"main\", \"mapping\": [{ \"aws_accounts\": [\"123456789011\"], \"lacework_account\": \"sub-account-1\"}]}",
1087+
"--noninteractive",
1088+
)
1089+
1090+
assert.Nil(t, runError)
1091+
assert.Contains(t, final, "Terraform code saved in")
1092+
1093+
orgAccountMappings := aws.OrgAccountMapping{
1094+
DefaultLaceworkAccount: "main",
1095+
Mapping: []aws.OrgAccountMap{
1096+
{
1097+
LaceworkAccount: "sub-account-1",
1098+
AwsAccounts: []string{"123456789011"},
1099+
},
1100+
},
1101+
}
1102+
1103+
buildTf, _ := aws.NewTerraform(region, true, false, false,
1104+
true, aws.WithOrgAccountMappings(orgAccountMappings)).Generate()
1105+
1106+
assert.Equal(t, buildTf, tfResult)
1107+
}
1108+
9331109
// Test Agentless organization integration
9341110
func TestGenerationAgentlessOrganization(t *testing.T) {
9351111
os.Setenv("LW_NOCACHE", "true")

0 commit comments

Comments
 (0)