Skip to content

Commit

Permalink
feat: add org-cloudtrail to lw_generate (#1433)
Browse files Browse the repository at this point in the history
* feat(GROW-2540): add agentless to generate command

* chore: pr suggestions

* feat: add org-cloudtrail to lw_generate

Signed-off-by: Darren Murray <darren.murray@lacework.net>

* feat: cloudtrail org account mapping

Signed-off-by: Darren Murray <darren.murray@lacework.net>

feat: cloudtrail org account mapping

Signed-off-by: Darren Murray <darren.murray@lacework.net>

---------

Signed-off-by: Darren Murray <darren.murray@lacework.net>
  • Loading branch information
dmurray-lacework authored Nov 13, 2023
1 parent 4185b65 commit 29935df
Show file tree
Hide file tree
Showing 5 changed files with 381 additions and 4 deletions.
114 changes: 113 additions & 1 deletion cli/cmd/generate_aws.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"encoding/json"
"fmt"
"strings"
"time"
Expand Down Expand Up @@ -48,6 +49,14 @@ var (
QuestionAwsAnotherAdvancedOpt = "Configure another advanced integration option"
QuestionAwsCustomizeOutputLocation = "Provide the location for the output to be written:"

// Cloudtrail Org Questions
QuestionEnableCloudtrailOrganization = "Enable CloudTrail organizational integration?"
QuestionConfigureCloudtrailOrganizationMappings = "Configure CloudTrail organization account mappings?"
QuestionCloudtrailAccountMappingsLWDefaultAccount = "Specify org account mappings default Lacework account:"
QuestionCloudtrailOrgAccountMappingAnotherAdvancedOpt = "Configure another org account mapping?"
QuestionCloudtrailOrgAccountMappingsLWAccount = "Specify lacework account: "
QuestionCloudtrailOrgAccountMappingsAwsAccounts = "Specify aws accounts:"

// S3 Bucket Questions
QuestionBucketEnableEncryption = "Enable S3 bucket encryption when creating bucket"
QuestionBucketSseKeyArn = "Specify existing KMS encryption key arn for S3 bucket (optional)"
Expand Down Expand Up @@ -123,6 +132,13 @@ See help output for more details on the parameter value(s) required for Terrafor
GenerateAwsCommandState.LaceworkProfile = cli.Profile
}

// Parse org_account_mapping json, if passed
if cmd.Flags().Changed("cloudtrail_org_account_mapping") {
if err := parseCloudtrailOrgAccountMappingsFlag(GenerateAwsCommandState); err != nil {
return err
}
}

// Setup modifiers for NewTerraform constructor
mods := []aws.AwsTerraformModifier{
aws.WithAwsProfile(GenerateAwsCommandState.AwsProfile),
Expand All @@ -138,6 +154,7 @@ See help output for more details on the parameter value(s) required for Terrafor
aws.UseExistingIamRole(GenerateAwsCommandState.ExistingIamRole),
aws.WithConfigName(GenerateAwsCommandState.ConfigName),
aws.WithCloudtrailName(GenerateAwsCommandState.CloudtrailName),
aws.WithOrgAccountMappings(GenerateAwsCommandState.OrgAccountMappings),
aws.WithBucketName(GenerateAwsCommandState.BucketName),
aws.WithBucketEncryptionEnabled(GenerateAwsCommandState.BucketEncryptionEnabled),
aws.WithBucketSSEKeyArn(GenerateAwsCommandState.BucketSseKeyArn),
Expand Down Expand Up @@ -446,6 +463,11 @@ func initGenerateAwsTfCommandFlags() {
"consolidated_cloudtrail",
false,
"use consolidated trail")
generateAwsTfCommand.PersistentFlags().StringVar(
&GenerateAwsCommandState.OrgAccountMappingsJson,
"cloudtrail_org_account_mapping", "", "Org account mapping json string. Example: "+
"'{\"default_lacework_account\":\"main\", \"mapping\": [{ \"aws_accounts\": [\"123456789011\"], "+
"\"lacework_account\": \"sub-account-1\"}]}'")

// DEPRECATED
generateAwsTfCommand.PersistentFlags().BoolVar(
Expand Down Expand Up @@ -707,6 +729,13 @@ func promptAwsCtQuestions(config *aws.GenerateAwsTfConfigurationArgs, extraState
return err
}
if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{
{
Prompt: &survey.Confirm{
Message: QuestionEnableCloudtrailOrganization,
Default: config.AwsOrganization,
},
Response: &config.AwsOrganization,
},
{
Prompt: &survey.Input{Message: QuestionCloudtrailName, Default: config.CloudtrailName},
Checks: []*bool{existingCloudTrailDisabled(extraState)},
Expand All @@ -726,6 +755,25 @@ func promptAwsCtQuestions(config *aws.GenerateAwsTfConfigurationArgs, extraState
return err
}

if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{
{Prompt: &survey.Confirm{
Message: QuestionConfigureCloudtrailOrganizationMappings,
Default: config.AwsOrganizationMappings,
},
Response: &config.AwsOrganizationMappings,
Checks: []*bool{&config.AwsOrganization},
},
}, config.Cloudtrail); err != nil {
return err
}

if config.Cloudtrail && config.AwsOrganizationMappings {
err := promptCloudtrailOrgAccountMappings(config)
if err != nil {
return err
}
}

newBucket := config.ExistingCloudtrailBucketArn == ""
if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{
// If new bucket created, allow user to optionally name the bucket
Expand Down Expand Up @@ -1124,7 +1172,7 @@ func promptAwsGenerate(
return errors.New("must enable agentless, cloudtrail or config")
}

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

return nil
}

func promptCloudtrailAddOrgAccountMappings(input *aws.GenerateAwsTfConfigurationArgs) error {
mapping := aws.OrgAccountMap{}
var accountsAnswer string
if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{
{
Prompt: &survey.Input{Message: QuestionCloudtrailOrgAccountMappingsLWAccount},
Response: &mapping.LaceworkAccount,
},
{
Prompt: &survey.Multiline{Message: QuestionCloudtrailOrgAccountMappingsAwsAccounts},
Response: &accountsAnswer,
},
}); err != nil {
return err
}
mapping.AwsAccounts = strings.Split(accountsAnswer, "\n")
input.OrgAccountMappings.Mapping = append(input.OrgAccountMappings.Mapping, mapping)
return nil
}

func promptCloudtrailOrgAccountMappings(input *aws.GenerateAwsTfConfigurationArgs) error {
if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{
{
Prompt: &survey.Input{
Message: QuestionCloudtrailAccountMappingsLWDefaultAccount,
Default: input.OrgAccountMappings.DefaultLaceworkAccount},
Response: &input.OrgAccountMappings.DefaultLaceworkAccount,
},
}); err != nil {
return err
}

if err := promptCloudtrailAddOrgAccountMappings(input); err != nil {
return err
}

var askAgain bool
for {
if err := SurveyQuestionInteractiveOnly(SurveyQuestionWithValidationArgs{
Prompt: &survey.Confirm{Message: QuestionControlTowerOrgAccountMappingAnotherAdvancedOpt},
Response: &askAgain}); err != nil {
return err
}

if !askAgain {
break
}

if err := promptCloudtrailAddOrgAccountMappings(input); err != nil {
return err
}
}

return nil
}

func parseCloudtrailOrgAccountMappingsFlag(args *aws.GenerateAwsTfConfigurationArgs) error {
if err := json.Unmarshal([]byte(args.OrgAccountMappingsJson), &args.OrgAccountMappings); err != nil {
return errors.Wrap(err, "failed to parse 'cloudtrail_org_account_mapping'")
}

return nil
}
176 changes: 176 additions & 0 deletions integration/aws_generation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ func TestGenerationAwsAdvancedOptsConsolidated(t *testing.T) {
MsgMenu{cmd.AwsAdvancedOptDone, 2},
MsgRsp{cmd.QuestionConsolidatedCloudtrail, "y"},
MsgRsp{cmd.QuestionUseExistingCloudtrail, "n"},
MsgRsp{cmd.QuestionEnableCloudtrailOrganization, "n"},
MsgRsp{cmd.QuestionCloudtrailName, ""},
// S3 Bucket Questions
MsgRsp{cmd.QuestionBucketName, ""},
Expand Down Expand Up @@ -277,6 +278,7 @@ func TestGenerationAwsAdvancedOptsUseExistingCloudtrail(t *testing.T) {
MsgMenu{cmd.AwsAdvancedOptDone, 2},
MsgRsp{cmd.QuestionConsolidatedCloudtrail, "n"},
MsgRsp{cmd.QuestionUseExistingCloudtrail, "y"},
MsgRsp{cmd.QuestionEnableCloudtrailOrganization, "n"},
MsgRsp{cmd.QuestionCloudtrailExistingBucketArn, "notright"},
MsgRsp{"invalid arn supplied", "arn:aws:s3:::bucket_name"},
// SNS Topic Questions
Expand Down Expand Up @@ -330,6 +332,7 @@ func TestGenerationAwsAdvancedOptsConsolidatedWithSubAccounts(t *testing.T) {
MsgMenu{cmd.AwsAdvancedOptDone, 2},
MsgRsp{cmd.QuestionConsolidatedCloudtrail, "y"},
MsgRsp{cmd.QuestionUseExistingCloudtrail, "n"},
MsgRsp{cmd.QuestionEnableCloudtrailOrganization, "n"},
MsgRsp{cmd.QuestionCloudtrailName, ""},
MsgRsp{cmd.QuestionBucketName, ""},
MsgRsp{cmd.QuestionBucketEnableEncryption, "y"},
Expand Down Expand Up @@ -546,6 +549,7 @@ func TestGenerationAwsAdvancedOptsUseExistingElements(t *testing.T) {
MsgMenu{cmd.AwsAdvancedOptDone, 2},
MsgRsp{cmd.QuestionConsolidatedCloudtrail, "n"},
MsgRsp{cmd.QuestionUseExistingCloudtrail, "y"},
MsgRsp{cmd.QuestionEnableCloudtrailOrganization, "n"},
MsgRsp{cmd.QuestionCloudtrailExistingBucketArn, bucketArn},
MsgRsp{cmd.QuestionsUseExistingSNSTopic, "y"},
MsgRsp{cmd.QuestionSnsTopicArn, topicArn},
Expand Down Expand Up @@ -599,6 +603,7 @@ func TestGenerationAwsAdvancedOptsCreateNewElements(t *testing.T) {
MsgMenu{cmd.AwsAdvancedOptDone, 2},
MsgRsp{cmd.QuestionConsolidatedCloudtrail, "n"},
MsgRsp{cmd.QuestionUseExistingCloudtrail, "n"},
MsgRsp{cmd.QuestionEnableCloudtrailOrganization, "n"},
MsgRsp{cmd.QuestionCloudtrailName, trailName},
// S3 Questions
MsgRsp{cmd.QuestionBucketName, bucketName},
Expand Down Expand Up @@ -894,6 +899,7 @@ func TestGenerationAwsS3BucketNotificationInteractive(t *testing.T) {

MsgRsp{cmd.QuestionConsolidatedCloudtrail, ""},
MsgRsp{cmd.QuestionUseExistingCloudtrail, ""},
MsgRsp{cmd.QuestionEnableCloudtrailOrganization, "n"},
MsgRsp{cmd.QuestionCloudtrailName, ""},
// S3 Questions
MsgRsp{cmd.QuestionBucketName, ""},
Expand Down Expand Up @@ -930,6 +936,176 @@ func TestGenerationAwsS3BucketNotificationInteractive(t *testing.T) {
assert.Equal(t, buildTf, tfResult)
}

func TestGenerationAwsCloudtrailOrganization(t *testing.T) {
os.Setenv("LW_NOCACHE", "true")
defer os.Setenv("LW_NOCACHE", "")
var final string
var runError error
region := "us-west-2"

tfResult := runGenerateTest(t,
func(c *expect.Console) {
expectsCliOutput(t, c, []MsgRspHandler{
MsgRsp{cmd.QuestionEnableAgentless, "n"},
MsgRsp{cmd.QuestionAwsEnableConfig, "n"},
MsgRsp{cmd.QuestionEnableCloudtrail, "y"},
MsgRsp{cmd.QuestionAwsRegion, region},

MsgRsp{cmd.QuestionAwsConfigAdvanced, "y"},
MsgMenu{cmd.AwsAdvancedOptDone, 0},

MsgRsp{cmd.QuestionConsolidatedCloudtrail, ""},
MsgRsp{cmd.QuestionUseExistingCloudtrail, ""},
MsgRsp{cmd.QuestionEnableCloudtrailOrganization, "y"},
MsgRsp{cmd.QuestionCloudtrailName, ""},
MsgRsp{cmd.QuestionConfigureCloudtrailOrganizationMappings, "n"},
// S3 Questions
MsgRsp{cmd.QuestionBucketName, ""},
MsgRsp{cmd.QuestionBucketEnableEncryption, ""},
MsgRsp{cmd.QuestionBucketSseKeyArn, ""},
MsgRsp{cmd.QuestionS3BucketNotification, "n"},
// SNS Topic Questions
MsgRsp{cmd.QuestionsUseExistingSNSTopic, ""},
MsgRsp{cmd.QuestionSnsTopicName, ""},
MsgRsp{cmd.QuestionSnsEnableEncryption, ""},
MsgRsp{cmd.QuestionSnsEncryptionKeyArn, ""},
// SQS Questions
MsgRsp{cmd.QuestionSqsQueueName, ""},
MsgRsp{cmd.QuestionSqsEnableEncryption, ""},
MsgRsp{cmd.QuestionSqsEncryptionKeyArn, ""},

MsgRsp{cmd.QuestionAwsAnotherAdvancedOpt, "n"},
MsgRsp{cmd.QuestionRunTfPlan, "n"},
})

final, _ = c.ExpectEOF()
},
"generate",
"cloud-account",
"aws",
)

assert.Nil(t, runError)
assert.Contains(t, final, "Terraform code saved in")

buildTf, _ := aws.NewTerraform(region, true, false, false,
true).Generate()
assert.Equal(t, buildTf, tfResult)
}

func TestGenerationAwsCloudtrailOrganizationAccountMappings(t *testing.T) {
os.Setenv("LW_NOCACHE", "true")
defer os.Setenv("LW_NOCACHE", "")
var final string
var runError error
region := "us-west-2"

tfResult := runGenerateTest(t,
func(c *expect.Console) {
expectsCliOutput(t, c, []MsgRspHandler{
MsgRsp{cmd.QuestionEnableAgentless, "n"},
MsgRsp{cmd.QuestionAwsEnableConfig, "n"},
MsgRsp{cmd.QuestionEnableCloudtrail, "y"},
MsgRsp{cmd.QuestionAwsRegion, region},

MsgRsp{cmd.QuestionAwsConfigAdvanced, "y"},
MsgMenu{cmd.AwsAdvancedOptDone, 0},

MsgRsp{cmd.QuestionConsolidatedCloudtrail, ""},
MsgRsp{cmd.QuestionUseExistingCloudtrail, ""},
MsgRsp{cmd.QuestionEnableCloudtrailOrganization, "y"},
MsgRsp{cmd.QuestionCloudtrailName, ""},
MsgRsp{cmd.QuestionConfigureCloudtrailOrganizationMappings, "y"},
MsgRsp{cmd.QuestionCloudtrailAccountMappingsLWDefaultAccount, "main"},
MsgRsp{cmd.QuestionCloudtrailOrgAccountMappingsLWAccount, "sub-account-1"},
MsgMultilineRsp{cmd.QuestionCloudtrailOrgAccountMappingsAwsAccounts, []string{"123456789011"}},
MsgRsp{cmd.QuestionCloudtrailOrgAccountMappingAnotherAdvancedOpt, "n"},
// S3 Questions
MsgRsp{cmd.QuestionBucketName, ""},
MsgRsp{cmd.QuestionBucketEnableEncryption, ""},
MsgRsp{cmd.QuestionBucketSseKeyArn, ""},
MsgRsp{cmd.QuestionS3BucketNotification, "n"},
// SNS Topic Questions
MsgRsp{cmd.QuestionsUseExistingSNSTopic, ""},
MsgRsp{cmd.QuestionSnsTopicName, ""},
MsgRsp{cmd.QuestionSnsEnableEncryption, ""},
MsgRsp{cmd.QuestionSnsEncryptionKeyArn, ""},
// SQS Questions
MsgRsp{cmd.QuestionSqsQueueName, ""},
MsgRsp{cmd.QuestionSqsEnableEncryption, ""},
MsgRsp{cmd.QuestionSqsEncryptionKeyArn, ""},

MsgRsp{cmd.QuestionAwsAnotherAdvancedOpt, "n"},
MsgRsp{cmd.QuestionRunTfPlan, "n"},
})

final, _ = c.ExpectEOF()
},
"generate",
"cloud-account",
"aws",
)

assert.Nil(t, runError)
assert.Contains(t, final, "Terraform code saved in")

orgAccountMappings := aws.OrgAccountMapping{
DefaultLaceworkAccount: "main",
Mapping: []aws.OrgAccountMap{
{
LaceworkAccount: "sub-account-1",
AwsAccounts: []string{"123456789011"},
},
},
}

buildTf, _ := aws.NewTerraform(region, true, false, false,
true, aws.WithOrgAccountMappings(orgAccountMappings)).Generate()
assert.Equal(t, buildTf, tfResult)
}

func TestGenerationCloudtrailOrgMappingsNonInteractive(t *testing.T) {
os.Setenv("LW_NOCACHE", "true")
defer os.Setenv("LW_NOCACHE", "")
var final string
var runError error
region := "us-east-2"

tfResult := runGenerateTest(t,
func(c *expect.Console) {
final, _ = c.ExpectEOF()
},
"generate",
"ca",
"aws",
"--cloudtrail",
"--aws_region",
"us-east-2",
"--aws_organization",
"--cloudtrail_org_account_mapping",
"{\"default_lacework_account\":\"main\", \"mapping\": [{ \"aws_accounts\": [\"123456789011\"], \"lacework_account\": \"sub-account-1\"}]}",
"--noninteractive",
)

assert.Nil(t, runError)
assert.Contains(t, final, "Terraform code saved in")

orgAccountMappings := aws.OrgAccountMapping{
DefaultLaceworkAccount: "main",
Mapping: []aws.OrgAccountMap{
{
LaceworkAccount: "sub-account-1",
AwsAccounts: []string{"123456789011"},
},
},
}

buildTf, _ := aws.NewTerraform(region, true, false, false,
true, aws.WithOrgAccountMappings(orgAccountMappings)).Generate()

assert.Equal(t, buildTf, tfResult)
}

// Test Agentless organization integration
func TestGenerationAgentlessOrganization(t *testing.T) {
os.Setenv("LW_NOCACHE", "true")
Expand Down
Loading

0 comments on commit 29935df

Please sign in to comment.