diff --git a/cli/cmd/generate_aws.go b/cli/cmd/generate_aws.go index 013b7e9c0..19177ea9e 100644 --- a/cli/cmd/generate_aws.go +++ b/cli/cmd/generate_aws.go @@ -22,11 +22,6 @@ var ( "(e.g. 123456789000,ou-abcd-12345678,r-abcd):" QuestionAgentlessMonitoredAccountIDsHelp = "Please provide a comma seprated list that may " + " contain account IDs, OUs, or the organization root." - QuestionAgentlessMonitoredAccountProfile = "Specify the AWS account profile name to be used for " + - "the monitored account:" - QuestionAgentlessMonitoredAccountRegion = "Specify the AWS region to be used for the monitored account:" - QuestionAgentlessMonitoredAccountAddMore = "Add another monitored AWS account?" - QuestionAgentlessMonitoredAccountsReplace = "Currently configured Agentless monitored accounts: %s, replace?" QuestionAwsEnableConfig = "Enable configuration integration?" QuestionCustomizeConfigName = "Customize Config integration name?" @@ -40,15 +35,14 @@ var ( QuestionExistingIamRoleName = "Specify an existing IAM role name for CloudTrail access:" QuestionExistingIamRoleArn = "Specify an existing IAM role ARN for CloudTrail access:" QuestionExistingIamRoleExtID = "Specify the external ID to be used with the existing IAM role:" - QuestionPrimaryAwsAccountProfile = "Before adding sub-accounts, your primary AWS account profile name " + - "must be set; which profile should the main account use?" - QuestionSubAccountProfileName = "Supply the profile name for this additional AWS account:" - QuestionSubAccountRegion = "What region should be used for this account?" - QuestionSubAccountAddMore = "Add another AWS account?" - QuestionSubAccountReplace = "Currently configured AWS sub-accounts: %s, replace?" - QuestionAwsConfigAdvanced = "Configure advanced integration options?" - QuestionAwsAnotherAdvancedOpt = "Configure another advanced integration option" - QuestionAwsCustomizeOutputLocation = "Provide the location for the output to be written:" + QuestionPrimaryAwsAccountProfile = "Specify the primary AWS account profile name:" + QuestionSubAccountProfileName = "Supply the profile name for this additional AWS account:" + QuestionSubAccountRegion = "What region should be used for this account?" + QuestionSubAccountAddMore = "Add another AWS account?" + QuestionSubAccountReplace = "Currently configured AWS sub-accounts: %s, replace?" + QuestionAwsConfigAdvanced = "Configure advanced integration options?" + QuestionAwsAnotherAdvancedOpt = "Configure another advanced integration option" + QuestionAwsCustomizeOutputLocation = "Provide the location for the output to be written:" // S3 Bucket Questions QuestionBucketEnableEncryption = "Enable S3 bucket encryption when creating bucket" @@ -131,7 +125,6 @@ See help output for more details on the parameter value(s) required for Terrafor aws.WithLaceworkAccountID(GenerateAwsCommandState.LaceworkAccountID), aws.WithAgentlessManagementAccountID(GenerateAwsCommandState.AgentlessManagementAccountID), aws.WithAgentlessMonitoredAccountIDs(GenerateAwsCommandState.AgentlessMonitoredAccountIDs), - aws.WithAgentlessMonitoredAccounts(GenerateAwsCommandState.AgentlessMonitoredAccounts...), aws.ExistingCloudtrailBucketArn(GenerateAwsCommandState.ExistingCloudtrailBucketArn), aws.ExistingSnsTopicArn(GenerateAwsCommandState.ExistingSnsTopicArn), aws.WithSubaccounts(GenerateAwsCommandState.SubAccounts...), @@ -516,7 +509,7 @@ func validateAgentlessMonitoredAccountIDs(val interface{}) error { if err := validateStringWithRegex( id, regex, - fmt.Sprintf("invalid account ID, OU ID or root ID supplied: $s", id), + fmt.Sprintf("invalid account ID, OU ID or root ID supplied: %s", id), ); err != nil { return err } @@ -542,10 +535,6 @@ func promptAgentlessQuestions( config *aws.GenerateAwsTfConfigurationArgs, extraState *AwsGenerateCommandExtraState, ) error { - askAgain := true - monitoredAccountIDsInput := "" - monitoredAccounts := []aws.AwsSubAccount{} - if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{ { Prompt: &survey.Confirm{ @@ -558,7 +547,18 @@ func promptAgentlessQuestions( return err } + monitoredAccountIDsInput := "" + if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{ + { + Prompt: &survey.Input{ + Message: QuestionPrimaryAwsAccountProfile, + Default: config.AwsProfile, + }, + Opts: []survey.AskOpt{survey.WithValidator(validateAwsProfile)}, + Response: &config.AwsProfile, + Required: true, + }, { Prompt: &survey.Input{ Message: QuestionAgentlessManagementAccountID, @@ -588,67 +588,6 @@ func promptAgentlessQuestions( config.AgentlessMonitoredAccountIDs = strings.Split(monitoredAccountIDsInput, ",") } - // If there are existing monitored accounts configured (i.e., from the CLI), - // display them and ask if they want to add more - if len(config.AgentlessMonitoredAccounts) > 0 { - accountListing := []string{} - for _, account := range config.AgentlessMonitoredAccounts { - accountListing = append( - accountListing, - fmt.Sprintf("%s:%s", account.AwsProfile, account.AwsRegion), - ) - } - - if err := SurveyQuestionInteractiveOnly(SurveyQuestionWithValidationArgs{ - Prompt: &survey.Confirm{ - Message: fmt.Sprintf( - QuestionAgentlessMonitoredAccountsReplace, - strings.Trim(strings.Join(strings.Fields(fmt.Sprint(accountListing)), ", "), "[]"), - ), - }, - Response: &askAgain}); err != nil { - return err - } - } - - for askAgain && extraState.EnableAgentlessOrganization { - var accountProfileName string - var accountProfileRegion string - - if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{ - { - Prompt: &survey.Input{Message: QuestionAgentlessMonitoredAccountProfile}, - Opts: []survey.AskOpt{survey.WithValidator(validateAwsProfile)}, - Required: true, - Response: &accountProfileName, - }, - { - Prompt: &survey.Input{Message: QuestionAgentlessMonitoredAccountRegion}, - Opts: []survey.AskOpt{survey.WithValidator(validateAwsRegion)}, - Required: true, - Response: &accountProfileRegion, - }, - }); err != nil { - return err - } - - monitoredAccounts = append( - monitoredAccounts, - aws.AwsSubAccount{AwsProfile: accountProfileName, AwsRegion: accountProfileRegion}) - - if err := SurveyQuestionInteractiveOnly(SurveyQuestionWithValidationArgs{ - Prompt: &survey.Confirm{Message: QuestionAgentlessMonitoredAccountAddMore}, - Response: &askAgain, - }); err != nil { - return err - } - } - - // If we created new accounts, re-write config - if len(monitoredAccounts) > 0 { - config.AgentlessMonitoredAccounts = monitoredAccounts - } - return nil } diff --git a/integration/aws_generation_test.go b/integration/aws_generation_test.go index a81206e8b..c8ce55a14 100644 --- a/integration/aws_generation_test.go +++ b/integration/aws_generation_test.go @@ -930,8 +930,8 @@ func TestGenerationAwsS3BucketNotificationInteractive(t *testing.T) { assert.Equal(t, buildTf, tfResult) } -// Test using agentless with monitored accounts -func TestGenerationAgentlessWithMonitoredAccounts(t *testing.T) { +// Test Agentless organization integration +func TestGenerationAgentlessOrganization(t *testing.T) { os.Setenv("LW_NOCACHE", "true") defer os.Setenv("LW_NOCACHE", "") var final string @@ -947,17 +947,10 @@ func TestGenerationAgentlessWithMonitoredAccounts(t *testing.T) { MsgRsp{cmd.QuestionAwsRegion, region}, MsgRsp{cmd.QuestionAwsConfigAdvanced, "y"}, MsgMenu{cmd.AwsAdvancedOptDone, 0}, - MsgRsp{cmd.QuestionAgentlessName, "custom_agentless_name"}, - MsgRsp{cmd.QuestionEnableAgentlessMultiAccount, "y"}, + MsgRsp{cmd.QuestionEnableAgentlessOrganization, "y"}, + MsgRsp{cmd.QuestionPrimaryAwsAccountProfile, "default-profile"}, MsgRsp{cmd.QuestionAgentlessManagementAccountID, "123456789000"}, - MsgRsp{cmd.QuestionAgentlessMonitoredAccountID, "123456789001"}, - MsgRsp{cmd.QuestionAgentlessMonitoredAccountProfile, "monitored-account-1"}, - MsgRsp{cmd.QuestionAgentlessMonitoredAccountRegion, "us-east-1"}, - MsgRsp{cmd.QuestionAgentlessMonitoredAccountAddMore, "y"}, - MsgRsp{cmd.QuestionAgentlessMonitoredAccountID, "123456789002"}, - MsgRsp{cmd.QuestionAgentlessMonitoredAccountProfile, "monitored-account-2"}, - MsgRsp{cmd.QuestionAgentlessMonitoredAccountRegion, "us-east-2"}, - MsgRsp{cmd.QuestionAgentlessMonitoredAccountAddMore, "n"}, + MsgRsp{cmd.QuestionAgentlessMonitoredAccountIDs, "123456789000,ou-abcd-12345678,r-abcd"}, MsgRsp{cmd.QuestionAwsAnotherAdvancedOpt, "n"}, MsgRsp{cmd.QuestionRunTfPlan, "n"}, }) @@ -972,14 +965,11 @@ func TestGenerationAgentlessWithMonitoredAccounts(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := aws.NewTerraform(region, true, true, true, + buildTf, _ := aws.NewTerraform(region, true, false, false, aws.UseConsolidatedCloudtrail(), - aws.WithAwsProfile("default"), - aws.WithAgentlessName("custom_agentless_name"), - aws.WithAgentlessMonitoredAccounts( - aws.AwsSubAccount{AwsProfile: "monitored-account-1", AwsRegion: "us-east-1", AccountID: "123456789001"}, - aws.AwsSubAccount{AwsProfile: "monitored-account-2", AwsRegion: "us-east-2", AccountID: "123456789002"}, - ), + aws.WithAwsProfile("default-profile"), + aws.WithAgentlessManagementAccountID("123456789000"), + aws.WithAgentlessMonitoredAccountIDs([]string{"123456789000", "ou-abcd-12345678", "r-abcd"}), ).Generate() assert.Equal(t, buildTf, tfResult) } diff --git a/lwgenerate/aws/aws.go b/lwgenerate/aws/aws.go index f379ac846..ef496f488 100644 --- a/lwgenerate/aws/aws.go +++ b/lwgenerate/aws/aws.go @@ -83,9 +83,6 @@ type GenerateAwsTfConfigurationArgs struct { // Agentless Monitored AWS account IDs, OUs, or the organization root. AgentlessMonitoredAccountIDs []string - // Monitored AWS accounts for multi-account agentless integration - AgentlessMonitoredAccounts []AwsSubAccount - // Should we configure Cloudtrail integration in LW? Cloudtrail bool @@ -263,13 +260,6 @@ func WithAgentlessMonitoredAccountIDs(accountIDs []string) AwsTerraformModifier } } -// WithAgentlessMonitoredAccounts Set Agentless monitored accounts -func WithAgentlessMonitoredAccounts(accounts ...AwsSubAccount) AwsTerraformModifier { - return func(c *GenerateAwsTfConfigurationArgs) { - c.AgentlessMonitoredAccounts = accounts - } -} - // ExistingCloudtrailBucketArn Set the bucket ARN of an existing Cloudtrail setup func ExistingCloudtrailBucketArn(arn string) AwsTerraformModifier { return func(c *GenerateAwsTfConfigurationArgs) { @@ -458,11 +448,8 @@ func createAwsProvider(args *GenerateAwsTfConfigurationArgs) ([]*hclwrite.Block, } if args.AwsProfile != "" { - attrs["profile"] = args.AwsProfile - } - - if len(args.SubAccounts) > 0 { attrs["alias"] = "main" + attrs["profile"] = args.AwsProfile } provider, err := lwgenerate.NewProvider( @@ -476,10 +463,8 @@ func createAwsProvider(args *GenerateAwsTfConfigurationArgs) ([]*hclwrite.Block, blocks = append(blocks, provider) } - accounts := append(args.SubAccounts, args.AgentlessMonitoredAccounts...) - - if len(accounts) > 0 { - for _, subaccount := range accounts { + if len(args.SubAccounts) > 0 { + for _, subaccount := range args.SubAccounts { attrs := map[string]interface{}{ "alias": subaccount.AwsProfile, "profile": subaccount.AwsProfile, @@ -667,11 +652,12 @@ func createAgentless(args *GenerateAwsTfConfigurationArgs) ([]*hclwrite.Block, e blocks := []*hclwrite.Block{} + enableMultiAccount := args.AgentlessManagementAccountID != "" && len(args.AgentlessMonitoredAccountIDs) > 0 + globalModuleAttributes := map[string]interface{}{ "global": true, "regional": true, } - enableMultiAccount := args.AgentlessManagementAccountID != "" && len(args.AgentlessMonitoredAccounts) > 0 if enableMultiAccount { ids := []string{} @@ -684,12 +670,21 @@ func createAgentless(args *GenerateAwsTfConfigurationArgs) ([]*hclwrite.Block, e }) } + globalModuleModifiers := []lwgenerate.HclModuleModifier{ + lwgenerate.HclModuleWithVersion(lwgenerate.AwsAgentlessVersion), + lwgenerate.HclModuleWithAttributes(globalModuleAttributes), + } + + if args.AwsProfile != "" { + globalModuleModifiers = append(globalModuleModifiers, + lwgenerate.HclModuleWithProviderDetails(map[string]string{"aws": "aws.main"})) + } + // Add global module globalModule, err := lwgenerate.NewModule( "lacework_aws_agentless_scanning_global", lwgenerate.AwsAgentlessSource, - lwgenerate.HclModuleWithVersion(lwgenerate.AwsAgentlessVersion), - lwgenerate.HclModuleWithAttributes(globalModuleAttributes), + globalModuleModifiers..., ).ToBlock() if err != nil { @@ -737,9 +732,7 @@ func createAgentless(args *GenerateAwsTfConfigurationArgs) ([]*hclwrite.Block, e "lacework_aws_agentless_management_scanning_role", lwgenerate.AwsAgentlessSource, lwgenerate.HclModuleWithVersion(lwgenerate.AwsAgentlessVersion), - lwgenerate.HclModuleWithProviderDetails(map[string]string{ - "aws": fmt.Sprintf("aws.%s", args.AwsProfile), - }), + lwgenerate.HclModuleWithProviderDetails(map[string]string{"aws": "aws.main"}), lwgenerate.HclModuleWithAttributes(attributes), ).ToBlock() @@ -749,24 +742,84 @@ func createAgentless(args *GenerateAwsTfConfigurationArgs) ([]*hclwrite.Block, e blocks = append(blocks, managementModule) - // Add management modules - for _, monitoredAccount := range args.AgentlessMonitoredAccounts { - monitoredModule, err := lwgenerate.NewModule( - fmt.Sprintf("lacework_aws_agentless_monitored_scanning_role_%s", monitoredAccount.AwsProfile), - lwgenerate.AwsAgentlessSource, - lwgenerate.HclModuleWithVersion(lwgenerate.AwsAgentlessVersion), - lwgenerate.HclModuleWithProviderDetails(map[string]string{ - "aws": fmt.Sprintf("aws.%s", monitoredAccount.AwsProfile), - }), - lwgenerate.HclModuleWithAttributes(attributes), - ).ToBlock() + autoDeploymentBlock, err := lwgenerate.HclCreateGenericBlock( + "auto_deployment", + nil, + map[string]interface{}{"enabled": true, "retain_stacks_on_account_removal": false}, + ) - if err != nil { - return nil, err + if err != nil { + return nil, err + } + + stacksetResource, err := lwgenerate.NewResource( + "aws_cloudformation_stack_set", + "snapshot_role", + lwgenerate.HclResourceWithAttributesAndProviderDetails( + map[string]interface{}{ + "capabilities": lwgenerate.CreateSimpleTraversal([]string{"[\"CAPABILITY_NAMED_IAM\"]"}), + "description": "Lacework AWS Agentless Workload Scanning Organization Roles", + "name": "lacework-agentless-scanning-stackset", + "permission_model": "SERVICE_MANAGED", + "template_url": "https://agentless-workload-scanner.s3.amazonaws.com" + + "/cloudformation-lacework/latest/snapshot-role.json", + "parameters": lwgenerate.CreateMapTraversalTokens(map[string]string{ + "ExternalId": "module.lacework_aws_agentless_scanning_global.external_id", + "ECSTaskRoleArn": "module.lacework_aws_agentless_scanning_global.agentless_scan_ecs_task_role_arn", + "ResourceNamePrefix": "module.lacework_aws_agentless_scanning_global.prefix", + "ResourceNameSuffix": "module.lacework_aws_agentless_scanning_global.suffix", + }), + }, + []string{"aws.main"}, + ), + lwgenerate.HclResourceWithGenericBlocks(autoDeploymentBlock), + ).ToResourceBlock() + + if err != nil { + return nil, err + } + + blocks = append(blocks, stacksetResource) + + // Get OU IDs for the organizational_unit_ids attribute + OUIDs := []string{} + for _, accountID := range args.AgentlessMonitoredAccountIDs { + if strings.HasPrefix(accountID, "ou-") { + OUIDs = append(OUIDs, fmt.Sprintf("\"%s\"", accountID)) } + } - blocks = append(blocks, monitoredModule) + deploymentTargetsBlock, err := lwgenerate.HclCreateGenericBlock( + "deployment_targets", + nil, + map[string]interface{}{"organizational_unit_ids": lwgenerate.CreateSimpleTraversal( + []string{fmt.Sprintf("[%s]", strings.Join(OUIDs, ","))}, + )}, + ) + + if err != nil { + return nil, err + } + + stacksetInstanceResource, err := lwgenerate.NewResource( + "aws_cloudformation_stack_set_instance", + "snapshot_role", + lwgenerate.HclResourceWithAttributesAndProviderDetails( + map[string]interface{}{ + "stack_set_name": lwgenerate.CreateSimpleTraversal( + []string{"aws_cloudformation_stack_set", "snapshot_role"}, + ), + }, + []string{"aws.main"}, + ), + lwgenerate.HclResourceWithGenericBlocks(deploymentTargetsBlock), + ).ToResourceBlock() + + if err != nil { + return nil, err } + + blocks = append(blocks, stacksetInstanceResource) } return blocks, nil diff --git a/lwgenerate/aws/aws_test.go b/lwgenerate/aws/aws_test.go index 42c2e66cd..91f3fa173 100644 --- a/lwgenerate/aws/aws_test.go +++ b/lwgenerate/aws/aws_test.go @@ -341,6 +341,7 @@ var awsProvider = `provider "aws" { ` var awsProviderWithProfile = `provider "aws" { + alias = "main" profile = "myprofile" region = "us-east-2" } diff --git a/lwgenerate/hcl.go b/lwgenerate/hcl.go index ed1f5cc4d..193f88e26 100644 --- a/lwgenerate/hcl.go +++ b/lwgenerate/hcl.go @@ -212,6 +212,13 @@ func (m *HclResource) ToResourceBlock() (*hclwrite.Block, error) { block.Body().SetAttributeTraversal("provider", CreateSimpleTraversal(m.providerDetails)) } + if m.blocks != nil { + for _, b := range m.blocks { + block.Body().AppendNewline() + block.Body().AppendBlock(b) + } + } + return block, nil } @@ -229,6 +236,9 @@ type HclResource struct { // accepted. Unfortunately map[string]hcl.Traversal is not a format that is supported by hclwrite.SetAttributeValue // today so we must work around it (https://github.com/hashicorp/hcl/issues/347). providerDetails []string + + // optional. Generic blocks + blocks []*hclwrite.Block } type HclResourceModifier func(p *HclResource) @@ -251,6 +261,13 @@ func HclResourceWithAttributesAndProviderDetails(attrs map[string]interface{}, } } +// HclResourceWithGenericBlocks sets the generic blocks within the resource +func HclResourceWithGenericBlocks(blocks ...*hclwrite.Block) HclResourceModifier { + return func(p *HclResource) { + p.blocks = blocks + } +} + // Convert standard value types to cty.Value // // All values used in hclwrite.Block(s) must be cty.Value or a cty.Traversal. This function performs that conversion