From abbd03b304613fa7b3ad5e04706b327ecfc780fe Mon Sep 17 00:00:00 2001 From: lvadlamudi Date: Thu, 26 Jun 2025 13:37:13 -0700 Subject: [PATCH 1/4] feat: add azure Agentless support in generate command (cli) --- cli/cmd/generate_azure.go | 56 ++++++++++++++++++++++++++---- lwgenerate/azure/azure.go | 73 +++++++++++++++++++++++++++++++++++++-- lwgenerate/constants.go | 2 ++ 3 files changed, 122 insertions(+), 9 deletions(-) diff --git a/cli/cmd/generate_azure.go b/cli/cmd/generate_azure.go index da7d0ac4b..9a36d585e 100644 --- a/cli/cmd/generate_azure.go +++ b/cli/cmd/generate_azure.go @@ -16,15 +16,17 @@ import ( // Question labels const ( - IconAzureConfig = "[Configuration]" - IconActivityLog = "[Activity Log]" - IconEntraID = "[Entra ID Activity Log]" - IconAD = "[Active Directory Application]" + IconAzureConfig = "[Configuration]" + IconActivityLog = "[Activity Log]" + IconEntraID = "[Entra ID Activity Log]" + IconAD = "[Active Directory Application]" + IconAzureAgentless = "[Agentless]" ) var ( // Define question text here so they can be reused in testing // Core questions + QuestionAzureEnableAgentless = "Enable Agentless integration?" QuestionAzureEnableConfig = "Enable Configuration integration?" QuestionAzureConfigName = "Custom Configuration integration name: (optional)" QuestionEnableActivityLog = "Enable Activity Log Integration?" @@ -225,6 +227,7 @@ the new cloud account. In interactive mode, this command will: data := azure.NewTerraform( GenerateAzureCommandState.Config, GenerateAzureCommandState.ActivityLog, + GenerateAzureCommandState.Agentless, GenerateAzureCommandState.EntraIdActivityLog, GenerateAzureCommandState.CreateAdIntegration, mods...) @@ -391,6 +394,12 @@ func initGenerateAzureTfCommandFlags() { "", "specify a custom activity log integration name") + generateAzureTfCommand.PersistentFlags().BoolVar( + &GenerateAzureCommandState.Agentless, + "agentless", + false, + "enable agentless integration") + generateAzureTfCommand.PersistentFlags().BoolVar( &GenerateAzureCommandState.EntraIdActivityLog, "entra_id_activity_log", @@ -756,6 +765,25 @@ func promptAzureGenerate( } } + // Ask Agentless integration + if err := SurveyMultipleQuestionWithValidation( + []SurveyQuestionWithValidationArgs{ + { + Icon: IconAzureAgentless, + Prompt: &survey.Confirm{Message: QuestionAzureEnableAgentless, Default: config.Agentless}, + Response: &config.Agentless, + }, + }); err != nil { + return err + } + + // Ask Activity Log questions immediately if enabled + if config.Agentless { + if err := promptAzureAgentlessQuestions(config); err != nil { + return err + } + } + // Ask Entra ID integration if err := SurveyMultipleQuestionWithValidation( []SurveyQuestionWithValidationArgs{ @@ -776,8 +804,8 @@ func promptAzureGenerate( } // Validate one of config or activity log was enabled; otherwise error out - if !config.Config && !config.ActivityLog && !config.EntraIdActivityLog { - return errors.New("must enable at least one of: Configuration or Activity Log integration") + if !config.Config && !config.ActivityLog && !config.Agentless && !config.EntraIdActivityLog { + return errors.New("must enable at least one of: Configuration, Agentless or Activity Log integrations") } // Ask AD integration @@ -890,3 +918,19 @@ func promptAzureActivityLogQuestions(config *azure.GenerateAzureTfConfigurationA return nil } + + +func promptAzureAgentlessQuestions(config *azure.GenerateAzureTfConfigurationArgs) error { + // Ask for Agentless integration + // if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{ + // { + // Icon: IconEntraID, + // Prompt: &survey.Input{Message: QuestionEntraIdActivityLogName, Default: config.EntraIdIntegrationName}, + // Response: &config.EntraIdIntegrationName, + // }, + // }); err != nil { + // return err + // } + + return nil +} \ No newline at end of file diff --git a/lwgenerate/azure/azure.go b/lwgenerate/azure/azure.go index db4498441..06303e222 100644 --- a/lwgenerate/azure/azure.go +++ b/lwgenerate/azure/azure.go @@ -14,6 +14,9 @@ type GenerateAzureTfConfigurationArgs struct { // Should we add Config integration in LW? Config bool + // Should we add Agentless integration in LW? + Agentless bool + // Should we create an Entra ID integration in LW? EntraIdActivityLog bool @@ -93,8 +96,8 @@ type GenerateAzureTfConfigurationArgs struct { // Ensure all combinations of inputs are valid for supported spec func (args *GenerateAzureTfConfigurationArgs) validate() error { // Validate one of config or activity log was enabled; otherwise error out - if !args.ActivityLog && !args.Config && !args.EntraIdActivityLog { - return errors.New("audit log or config integration must be enabled") + if !args.ActivityLog && !args.Agentless && !args.Config && !args.EntraIdActivityLog { + return errors.New("audit log, agentless or config integration must be enabled") } if (args.ActivityLog || args.Config || args.EntraIdActivityLog) && args.SubscriptionID == "" { @@ -127,12 +130,13 @@ type AzureTerraformModifier func(c *GenerateAzureTfConfigurationArgs) // // Note: Additional configuration details may be set using modifiers of the AzureTerraformModifier type func NewTerraform( - enableConfig bool, enableActivityLog bool, enableEntraIdActivityLog, createAdIntegration bool, + enableConfig bool, enableActivityLog bool, enableAgentless bool, enableEntraIdActivityLog, createAdIntegration bool, mods ...AzureTerraformModifier, ) *GenerateAzureTfConfigurationArgs { config := &GenerateAzureTfConfigurationArgs{ ActivityLog: enableActivityLog, Config: enableConfig, + Agentless: enableAgentless, EntraIdActivityLog: enableEntraIdActivityLog, CreateAdIntegration: createAdIntegration, } @@ -350,6 +354,11 @@ func (args *GenerateAzureTfConfigurationArgs) Generate() (string, error) { return "", errors.Wrap(err, "failed to generate azure activity log module") } + agentlessLogModule, err := createAgentless(args) + if err != nil { + return "", errors.Wrap(err, "failed to generate azure agentless module") + } + entraIdActivityLogModule, err := createEntraIdActivityLog(args) if err != nil { return "", errors.Wrap(err, "failed to generate azure Entra ID activity log module") @@ -374,6 +383,7 @@ func (args *GenerateAzureTfConfigurationArgs) Generate() (string, error) { laceworkADProvider, configModule, activityLogModule, + agentlessLogModule, entraIdActivityLogModule, outputBlocks, args.ExtraBlocks), @@ -622,6 +632,63 @@ func createActivityLog(args *GenerateAzureTfConfigurationArgs) ([]*hclwrite.Bloc return blocks, nil } +func createAgentless(args *GenerateAzureTfConfigurationArgs) ([]*hclwrite.Block, error) { + blocks := []*hclwrite.Block{} + if args.ActivityLog { + attributes := map[string]interface{}{} + moduleDetails := []lwgenerate.HclModuleModifier{} + + // Check if we have created an Active Directory integration + if args.CreateAdIntegration { + attributes["use_existing_ad_application"] = true + attributes["application_id"] = lwgenerate.CreateSimpleTraversal( + []string{"module", "az_ad_application", "application_id"}) + attributes["application_password"] = lwgenerate.CreateSimpleTraversal( + []string{"module", "az_ad_application", "application_password"}) + attributes["service_principal_id"] = lwgenerate.CreateSimpleTraversal( + []string{"module", "az_ad_application", "service_principal_id"}) + } else { + attributes["use_existing_ad_application"] = true + attributes["application_id"] = args.AdApplicationId + attributes["application_password"] = args.AdApplicationPassword + attributes["service_principal_id"] = args.AdServicePrincipalId + } + + // // Only set subscription ids if all subscriptions flag is not set + // if !args.AllSubscriptions { + // if len(args.SubscriptionIds) > 0 { + // attributes["subscription_ids"] = args.SubscriptionIds + // } + // } else { + // // Set Subscription information + // attributes["all_subscriptions"] = args.AllSubscriptions + // } + + + // // Set the location if needed + // if args.StorageLocation != "" { + // attributes["location"] = args.StorageLocation + // } + + moduleDetails = append(moduleDetails, + lwgenerate.HclModuleWithAttributes(attributes), + ) + + moduleBlock, err := lwgenerate.NewModule( + "az_agentless", + lwgenerate.LWAzureActivityLogSource, + append(moduleDetails, lwgenerate.HclModuleWithVersion(lwgenerate.LWAzureActivityLogVersion))..., + ).ToBlock() + + if err != nil { + return nil, err + } + blocks = append(blocks, moduleBlock) + + } + return blocks, nil +} + func createEntraIdActivityLog(args *GenerateAzureTfConfigurationArgs) ([]*hclwrite.Block, error) { blocks := []*hclwrite.Block{} if args.EntraIdActivityLog { diff --git a/lwgenerate/constants.go b/lwgenerate/constants.go index 15702d13a..1eaf3212a 100644 --- a/lwgenerate/constants.go +++ b/lwgenerate/constants.go @@ -20,6 +20,8 @@ const ( LWAzureConfigSource = "lacework/config/azure" LWAzureConfigVersion = "~> 3.0" + LWAzureAgentlessSource = "lacework/agentless-scanning/azure" + LWAzureAgentlessVersion = "~> 1.5" LWAzureActivityLogSource = "lacework/activity-log/azure" LWAzureActivityLogVersion = "~> 3.0" LWAzureEntraIdActivityLogSource = "lacework/microsoft-entra-id-activity-log/azure" From 11f61291667a43f4d9ec3285c6d938ce0c32a668 Mon Sep 17 00:00:00 2001 From: lvadlamudi Date: Mon, 30 Jun 2025 12:31:24 -0700 Subject: [PATCH 2/4] chore: fix agentless az generation code --- cli/cmd/generate_azure.go | 56 ++++++++++++++++++----- lwgenerate/azure/azure.go | 94 ++++++++++++++++++++++++--------------- 2 files changed, 102 insertions(+), 48 deletions(-) diff --git a/cli/cmd/generate_azure.go b/cli/cmd/generate_azure.go index 9a36d585e..b277d76f2 100644 --- a/cli/cmd/generate_azure.go +++ b/cli/cmd/generate_azure.go @@ -198,8 +198,20 @@ the new cloud account. In interactive mode, this command will: azure.WithEventHubPartitionCount(GenerateAzureCommandState.EventHubPartitionCount), } + if GenerateAzureCommandState.Global != nil { + mods = append(mods, azure.WithGlobal(*GenerateAzureCommandState.Global)) + } + if GenerateAzureCommandState.CreateLogAnalyticsWorkspace != nil { + mods = append(mods, azure.WithCreateLogAnalyticsWorkspace(*GenerateAzureCommandState.CreateLogAnalyticsWorkspace)) + } + + // Always set the integration level to subscription + if GenerateAzureCommandState.Agentless { + mods = append(mods, azure.WithIntegrationLevel("SUBSCRIPTION")) + } + // Check if AD Creation is required, need to set values for current integration - if !GenerateAzureCommandState.CreateAdIntegration { + if !GenerateAzureCommandState.CreateAdIntegration && (GenerateAzureCommandState.Config || GenerateAzureCommandState.ActivityLog || GenerateAzureCommandState.EntraIdActivityLog) { mods = append(mods, azure.WithAdApplicationId(GenerateAzureCommandState.AdApplicationId)) mods = append(mods, azure.WithAdApplicationPassword(GenerateAzureCommandState.AdApplicationPassword)) mods = append(mods, azure.WithAdServicePrincipalId(GenerateAzureCommandState.AdServicePrincipalId)) @@ -821,7 +833,7 @@ func promptAzureGenerate( } // If AD integration is not being created, ask for existing AD details immediately - if !config.CreateAdIntegration { + if !config.CreateAdIntegration && (config.Config || config.ActivityLog || config.EntraIdActivityLog) { if err := promptAzureAdIntegrationQuestions(config); err != nil { return err } @@ -921,16 +933,36 @@ func promptAzureActivityLogQuestions(config *azure.GenerateAzureTfConfigurationA func promptAzureAgentlessQuestions(config *azure.GenerateAzureTfConfigurationArgs) error { - // Ask for Agentless integration - // if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{ - // { - // Icon: IconEntraID, - // Prompt: &survey.Input{Message: QuestionEntraIdActivityLogName, Default: config.EntraIdIntegrationName}, - // Response: &config.EntraIdIntegrationName, - // }, - // }); err != nil { - // return err - // } + // prompt for global setting + if config.Global == nil { + defaultGlobal := true + config.Global = &defaultGlobal + } + + if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{ + { + Icon: IconAzureAgentless, + Prompt: &survey.Confirm{Message: "Enable global agentless scanning?", Default: *config.Global}, + Response: config.Global, + }, + }); err != nil { + return err + } + + // prompt for log analytics workspace creation + if config.CreateLogAnalyticsWorkspace == nil { + defaultCreateLogAnalyticsWorkspace := true + config.CreateLogAnalyticsWorkspace = &defaultCreateLogAnalyticsWorkspace + } + if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{ + { + Icon: IconAzureAgentless, + Prompt: &survey.Confirm{Message: "Create Log Analytics Workspace?", Default: *config.CreateLogAnalyticsWorkspace}, + Response: config.CreateLogAnalyticsWorkspace, + }, + }); err != nil { + return err + } return nil } \ No newline at end of file diff --git a/lwgenerate/azure/azure.go b/lwgenerate/azure/azure.go index 06303e222..28589b97e 100644 --- a/lwgenerate/azure/azure.go +++ b/lwgenerate/azure/azure.go @@ -2,6 +2,8 @@ package azure import ( + "fmt" + "github.com/hashicorp/hcl/v2/hclwrite" "github.com/lacework/go-sdk/v2/lwgenerate" "github.com/pkg/errors" @@ -91,6 +93,15 @@ type GenerateAzureTfConfigurationArgs struct { // Custom outputs CustomOutputs []lwgenerate.HclOutput + + // Should agentless scanning be global? + Global *bool + + // Should we create a Log Analytics Workspace for agentless scanning? + CreateLogAnalyticsWorkspace *bool + + // Integration level for agentless scanning (e.g., "SUBSCRIPTION", "TENANT") + IntegrationLevel string } // Ensure all combinations of inputs are valid for supported spec @@ -105,9 +116,11 @@ func (args *GenerateAzureTfConfigurationArgs) validate() error { } // Validate that active directory settings are correct - if !args.CreateAdIntegration && (args.AdApplicationId == "" || - args.AdServicePrincipalId == "" || args.AdApplicationPassword == "") { - return errors.New("Active directory details must be set") + if !args.CreateAdIntegration && (args.Config || args.ActivityLog || args.EntraIdActivityLog) { + if (args.AdApplicationId == "" || + args.AdServicePrincipalId == "" || args.AdApplicationPassword == "") { + return errors.New("Active directory details must be set") + } } // Validate the Mangement Group @@ -136,9 +149,10 @@ func NewTerraform( config := &GenerateAzureTfConfigurationArgs{ ActivityLog: enableActivityLog, Config: enableConfig, - Agentless: enableAgentless, + Agentless:enableAgentless, EntraIdActivityLog: enableEntraIdActivityLog, CreateAdIntegration: createAdIntegration, + IntegrationLevel: "SUBSCRIPTION", // Default integration level } for _, m := range mods { m(config) @@ -311,6 +325,27 @@ func WithSubscriptionID(subcriptionID string) AzureTerraformModifier { } } +// WithGlobal sets the Global field for agentless scanning +func WithGlobal(global bool) AzureTerraformModifier { + return func(c *GenerateAzureTfConfigurationArgs) { + c.Global = &global + } +} + +// WithCreateLogAnalyticsWorkspace sets the CreateLogAnalyticsWorkspace field for agentless scanning +func WithCreateLogAnalyticsWorkspace(create bool) AzureTerraformModifier { + return func(c *GenerateAzureTfConfigurationArgs) { + c.CreateLogAnalyticsWorkspace = &create + } +} + +// WithIntegrationLevel sets the IntegrationLevel field for agentless scanning +func WithIntegrationLevel(level string) AzureTerraformModifier { + return func(c *GenerateAzureTfConfigurationArgs) { + c.IntegrationLevel = level + } +} + // Generate new Terraform code based on the supplied args. func (args *GenerateAzureTfConfigurationArgs) Generate() (string, error) { // Validate inputs @@ -634,41 +669,29 @@ func createActivityLog(args *GenerateAzureTfConfigurationArgs) ([]*hclwrite.Bloc func createAgentless(args *GenerateAzureTfConfigurationArgs) ([]*hclwrite.Block, error) { blocks := []*hclwrite.Block{} - if args.ActivityLog { + if args.Agentless { attributes := map[string]interface{}{} moduleDetails := []lwgenerate.HclModuleModifier{} - // Check if we have created an Active Directory integration - if args.CreateAdIntegration { - attributes["use_existing_ad_application"] = true - attributes["application_id"] = lwgenerate.CreateSimpleTraversal( - []string{"module", "az_ad_application", "application_id"}) - attributes["application_password"] = lwgenerate.CreateSimpleTraversal( - []string{"module", "az_ad_application", "application_password"}) - attributes["service_principal_id"] = lwgenerate.CreateSimpleTraversal( - []string{"module", "az_ad_application", "service_principal_id"}) + // only include supported arguments if they are set + if args.CreateLogAnalyticsWorkspace != nil { + attributes["create_log_analytics_workspace"] = *args.CreateLogAnalyticsWorkspace + } + if args.Global != nil { + attributes["global"] = *args.Global + } + if args.IntegrationLevel == "" { + // Default integration level is "SUBSCRIPTION" + attributes["integration_level"] = "SUBSCRIPTION" } else { - attributes["use_existing_ad_application"] = true - attributes["application_id"] = args.AdApplicationId - attributes["application_password"] = args.AdApplicationPassword - attributes["service_principal_id"] = args.AdServicePrincipalId + // If IntegrationLevel is set, use it + attributes["integration_level"] = args.IntegrationLevel } - // // Only set subscription ids if all subscriptions flag is not set - // if !args.AllSubscriptions { - // if len(args.SubscriptionIds) > 0 { - // attributes["subscription_ids"] = args.SubscriptionIds - // } - // } else { - // // Set Subscription information - // attributes["all_subscriptions"] = args.AllSubscriptions - // } - - - // // Set the location if needed - // if args.StorageLocation != "" { - // attributes["location"] = args.StorageLocation - // } + // for SUBSCRIPTION integration level, we need to set the subscription id + if args.IntegrationLevel == "SUBSCRIPTION" && args.SubscriptionID != "" { + attributes["included_subscriptions"] = []string{fmt.Sprintf("/subscriptions/%s", args.SubscriptionID)} + } moduleDetails = append(moduleDetails, lwgenerate.HclModuleWithAttributes(attributes), @@ -676,15 +699,14 @@ func createAgentless(args *GenerateAzureTfConfigurationArgs) ([]*hclwrite.Block, moduleBlock, err := lwgenerate.NewModule( "az_agentless", - lwgenerate.LWAzureActivityLogSource, - append(moduleDetails, lwgenerate.HclModuleWithVersion(lwgenerate.LWAzureActivityLogVersion))..., + lwgenerate.LWAzureAgentlessSource, + append(moduleDetails, lwgenerate.HclModuleWithVersion(lwgenerate.LWAzureAgentlessVersion))..., ).ToBlock() if err != nil { return nil, err } blocks = append(blocks, moduleBlock) - } return blocks, nil } From a8c6a855ae4bc6e5ce5ae2ff55407a244a807fca Mon Sep 17 00:00:00 2001 From: lvadlamudi Date: Tue, 8 Jul 2025 17:57:42 -0700 Subject: [PATCH 3/4] chore: support tenant integration level --- cli/cmd/generate_azure.go | 141 ++++++++++++++---- cli/cmd/generate_azure_test.go | 3 +- integration/azure_generation_test.go | 55 ++++--- .../help/generate_cloud-account_azure | 1 + lwgenerate/azure/azure.go | 131 ++++++++++++---- lwgenerate/azure/azure_test.go | 52 +++---- lwgenerate/constants.go | 2 +- 7 files changed, 275 insertions(+), 110 deletions(-) diff --git a/cli/cmd/generate_azure.go b/cli/cmd/generate_azure.go index b277d76f2..a7baba9a9 100644 --- a/cli/cmd/generate_azure.go +++ b/cli/cmd/generate_azure.go @@ -1,6 +1,7 @@ package cmd import ( + "fmt" "regexp" "strconv" "strings" @@ -16,11 +17,11 @@ import ( // Question labels const ( - IconAzureConfig = "[Configuration]" - IconActivityLog = "[Activity Log]" - IconEntraID = "[Entra ID Activity Log]" - IconAD = "[Active Directory Application]" - IconAzureAgentless = "[Agentless]" + IconAzureConfig = "[Configuration]" + IconActivityLog = "[Activity Log]" + IconEntraID = "[Entra ID Activity Log]" + IconAD = "[Active Directory Application]" + IconAzureAgentless = "[Agentless]" ) var ( @@ -198,20 +199,20 @@ the new cloud account. In interactive mode, this command will: azure.WithEventHubPartitionCount(GenerateAzureCommandState.EventHubPartitionCount), } - if GenerateAzureCommandState.Global != nil { - mods = append(mods, azure.WithGlobal(*GenerateAzureCommandState.Global)) - } - if GenerateAzureCommandState.CreateLogAnalyticsWorkspace != nil { - mods = append(mods, azure.WithCreateLogAnalyticsWorkspace(*GenerateAzureCommandState.CreateLogAnalyticsWorkspace)) - } - // Always set the integration level to subscription if GenerateAzureCommandState.Agentless { - mods = append(mods, azure.WithIntegrationLevel("SUBSCRIPTION")) + mods = append(mods, azure.WithIntegrationLevel(GenerateAzureCommandState.IntegrationLevel)) + mods = append(mods, azure.WithAgentlessSubscriptionIds(GenerateAzureCommandState.AgentlessSubscriptionIds)) + mods = append(mods, azure.WithRegions(GenerateAzureCommandState.Regions)) + mods = append(mods, azure.WithCreateLogAnalyticsWorkspace(*GenerateAzureCommandState.CreateLogAnalyticsWorkspace)) + mods = append(mods, azure.WithGlobal(*GenerateAzureCommandState.Global)) } // Check if AD Creation is required, need to set values for current integration - if !GenerateAzureCommandState.CreateAdIntegration && (GenerateAzureCommandState.Config || GenerateAzureCommandState.ActivityLog || GenerateAzureCommandState.EntraIdActivityLog) { + if !GenerateAzureCommandState.CreateAdIntegration && + (GenerateAzureCommandState.Config || + GenerateAzureCommandState.ActivityLog || + GenerateAzureCommandState.EntraIdActivityLog) { mods = append(mods, azure.WithAdApplicationId(GenerateAzureCommandState.AdApplicationId)) mods = append(mods, azure.WithAdApplicationPassword(GenerateAzureCommandState.AdApplicationPassword)) mods = append(mods, azure.WithAdServicePrincipalId(GenerateAzureCommandState.AdServicePrincipalId)) @@ -821,22 +822,28 @@ func promptAzureGenerate( } // Ask AD integration - if err := SurveyMultipleQuestionWithValidation( - []SurveyQuestionWithValidationArgs{ - { - Icon: IconAD, - Prompt: &survey.Confirm{Message: QuestionEnableAdIntegration, Default: config.CreateAdIntegration}, - Response: &config.CreateAdIntegration, - }, - }); err != nil { - return err - } - - // If AD integration is not being created, ask for existing AD details immediately - if !config.CreateAdIntegration && (config.Config || config.ActivityLog || config.EntraIdActivityLog) { - if err := promptAzureAdIntegrationQuestions(config); err != nil { + if config.Config || config.ActivityLog || config.EntraIdActivityLog { + if err := SurveyMultipleQuestionWithValidation( + []SurveyQuestionWithValidationArgs{ + { + Icon: IconAD, + Prompt: &survey.Confirm{Message: QuestionEnableAdIntegration, Default: config.CreateAdIntegration}, + Response: &config.CreateAdIntegration, + }, + }); err != nil { return err } + + // If AD integration is not being created, ask for existing AD details immediately + if !config.CreateAdIntegration { + if err := promptAzureAdIntegrationQuestions(config); err != nil { + return err + } + } + } + // for agentless only scenario, we set CreateAdIntegration to false + if config.Agentless && !config.Config && !config.ActivityLog && !config.EntraIdActivityLog { + config.CreateAdIntegration = false } // Ask about output location @@ -931,8 +938,25 @@ func promptAzureActivityLogQuestions(config *azure.GenerateAzureTfConfigurationA return nil } - func promptAzureAgentlessQuestions(config *azure.GenerateAzureTfConfigurationArgs) error { + // Prompt for integration level(SUBSCRIPTION or TENANT) + if config.IntegrationLevel == "" { + config.IntegrationLevel = "SUBSCRIPTION" + } + if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{ + { + Icon: IconAzureAgentless, + Prompt: &survey.Select{ + Message: "Select integration level:", + Options: []string{"SUBSCRIPTION", "TENANT"}, + Default: config.IntegrationLevel, + }, + Response: &config.IntegrationLevel, + }, + }); err != nil { + return err + } + // prompt for global setting if config.Global == nil { defaultGlobal := true @@ -949,9 +973,9 @@ func promptAzureAgentlessQuestions(config *azure.GenerateAzureTfConfigurationArg return err } - // prompt for log analytics workspace creation + // prompt for log analytics workspace creation (default: false) if config.CreateLogAnalyticsWorkspace == nil { - defaultCreateLogAnalyticsWorkspace := true + defaultCreateLogAnalyticsWorkspace := false config.CreateLogAnalyticsWorkspace = &defaultCreateLogAnalyticsWorkspace } if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{ @@ -964,5 +988,58 @@ func promptAzureAgentlessQuestions(config *azure.GenerateAzureTfConfigurationArg return err } + // Ask for regions + var regionsInput string + if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{ + { + Icon: IconAzureAgentless, + Prompt: &survey.Input{ + Message: "Comma-separated list of regions for Agentless scanning (e.g., 'East US, West US')", + Default: "West US", + }, + Response: ®ionsInput, + }, + }); err != nil { + return err + } + // parse regions from comma-separated string + if regionsInput != "" { + regions := strings.Split(regionsInput, ",") + for i, region := range regions { + regions[i] = strings.TrimSpace(region) + } + config.Regions = regions + } + + // Only ask for subscription IDs if SUBSCRIPTION integration level is selected + if config.IntegrationLevel == "SUBSCRIPTION" { + var subscriptionIdsInput string + for { + if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{ + { + Icon: IconAzureAgentless, + Prompt: &survey.Input{ + Message: "Comma-separated list of subscription IDs for Agentless scanning (e.g., 'sub1, sub2')", + Default: "", + }, + Response: &subscriptionIdsInput, + }, + }); err != nil { + return err + } + subscriptionIdsInput = strings.TrimSpace(subscriptionIdsInput) + if subscriptionIdsInput != "" { + subscriptionIds := strings.Split(subscriptionIdsInput, ",") + for i, subId := range subscriptionIds { + subscriptionIds[i] = strings.TrimSpace(subId) + } + config.AgentlessSubscriptionIds = subscriptionIds + break + } else { + fmt.Println("Subscription IDs cannot be empty. Please provide at least one subscription ID.") + } + + } + } return nil -} \ No newline at end of file +} diff --git a/cli/cmd/generate_azure_test.go b/cli/cmd/generate_azure_test.go index da49820c8..c79478b77 100644 --- a/cli/cmd/generate_azure_test.go +++ b/cli/cmd/generate_azure_test.go @@ -39,10 +39,11 @@ func TestMissingValidEntity(t *testing.T) { data := azure.GenerateAzureTfConfigurationArgs{} data.Config = false data.ActivityLog = false + data.Agentless = false err := promptAzureGenerate(&data, &AzureGenerateCommandExtraState{Output: "/tmp"}) assert.Error(t, err) - assert.Equal(t, "must enable at least one of: Configuration or Activity Log integration", err.Error()) + assert.Equal(t, "must enable at least one of: Configuration, Agentless or Activity Log integrations", err.Error()) } func TestValidStorageLocations(t *testing.T) { diff --git a/integration/azure_generation_test.go b/integration/azure_generation_test.go index 4b8eb6148..81db27940 100644 --- a/integration/azure_generation_test.go +++ b/integration/azure_generation_test.go @@ -43,8 +43,9 @@ func TestGenerationAzureErrorOnNoSelection(t *testing.T) { MsgRsp{cmd.AzureSubscriptions, "n"}, MsgRsp{cmd.QuestionAzureEnableConfig, "n"}, MsgRsp{cmd.QuestionEnableActivityLog, "n"}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, - MsgOnly{"ERROR collecting/confirming parameters: must enable at least one of: Configuration or Activity Log integration"}, + MsgOnly{"ERROR collecting/confirming parameters: must enable at least one of: Configuration, Agentless or Activity Log integration"}, }) }, "generate", @@ -76,6 +77,7 @@ func TestGenerationAzureSimple(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -93,7 +95,7 @@ func TestGenerationAzureSimple(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(true, true, false, true, azure.WithSubscriptionID(mockSubscriptionID)).Generate() + buildTf, _ := azure.NewTerraform(true, true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID)).Generate() assert.Equal(t, buildTf, tfResult) } @@ -124,6 +126,7 @@ func TestGenerationAzureCustomizedOutputLocation(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, dir}, @@ -144,7 +147,7 @@ func TestGenerationAzureCustomizedOutputLocation(t *testing.T) { result, _ := os.ReadFile(filepath.FromSlash(fmt.Sprintf("%s/main.tf", dir))) // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(true, true, false, true, azure.WithSubscriptionID(mockSubscriptionID)).Generate() + buildTf, _ := azure.NewTerraform(true, true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID)).Generate() assert.Equal(t, buildTf, string(result)) } @@ -165,6 +168,7 @@ func TestGenerationAzureConfigOnly(t *testing.T) { MsgRsp{cmd.QuestionAzureConfigName, ""}, MsgRsp{cmd.QuestionEnableManagementGroup, "n"}, MsgRsp{cmd.QuestionEnableActivityLog, "n"}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -182,7 +186,7 @@ func TestGenerationAzureConfigOnly(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID)).Generate() + buildTf, _ := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID(mockSubscriptionID)).Generate() assert.Equal(t, buildTf, tfResult) } @@ -204,6 +208,7 @@ func TestGenerationAzureActivityLogOnly(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -221,7 +226,7 @@ func TestGenerationAzureActivityLogOnly(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(false, true, false, true, azure.WithSubscriptionID(mockSubscriptionID)).Generate() + buildTf, _ := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID)).Generate() assert.Equal(t, buildTf, tfResult) } @@ -248,6 +253,7 @@ func TestGenerationAzureNoADEnabled(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "n"}, MsgRsp{cmd.QuestionADApplicationPass, pass}, @@ -268,7 +274,7 @@ func TestGenerationAzureNoADEnabled(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(true, true, false, false, + buildTf, _ := azure.NewTerraform(true, true, false, false, false, azure.WithSubscriptionID(mockSubscriptionID), azure.WithAdApplicationPassword(pass), azure.WithAdServicePrincipalId(principalId), @@ -296,6 +302,7 @@ func _TestGenerationAzureNamedConfig(t *testing.T) { MsgRsp{cmd.QuestionAzureConfigName, configName}, MsgRsp{cmd.QuestionEnableManagementGroup, "n"}, MsgRsp{cmd.QuestionEnableActivityLog, "n"}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -313,7 +320,7 @@ func _TestGenerationAzureNamedConfig(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(true, false, false, true, + buildTf, _ := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID(mockSubscriptionID), azure.WithConfigIntegrationName(configName), ).Generate() @@ -354,7 +361,7 @@ func _TestGenerationAzureNamedActivityLog(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(false, true, false, true, + buildTf, _ := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID), azure.WithActivityLogIntegrationName(activityName), ).Generate() @@ -393,6 +400,7 @@ func TestGenerationAzureWithExistingTerraform(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, dir}, @@ -435,6 +443,7 @@ func TestGenerationAzureConfigAllSubs(t *testing.T) { MsgRsp{cmd.QuestionAzureConfigName, ""}, MsgRsp{cmd.QuestionEnableManagementGroup, "n"}, MsgRsp{cmd.QuestionEnableActivityLog, "n"}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -452,7 +461,7 @@ func TestGenerationAzureConfigAllSubs(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(true, false, false, true, + buildTf, _ := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID(mockSubscriptionID), azure.WithAllSubscriptions(true), ).Generate() @@ -478,6 +487,7 @@ func TestGenerationAzureConfigMgmntGroup(t *testing.T) { MsgRsp{cmd.QuestionEnableManagementGroup, "y"}, MsgRsp{cmd.QuestionManagementGroupId, mgmtGrpId}, MsgRsp{cmd.QuestionEnableActivityLog, "n"}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -495,7 +505,7 @@ func TestGenerationAzureConfigMgmntGroup(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(true, false, false, true, + buildTf, _ := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID(mockSubscriptionID), azure.WithManagementGroup(true), azure.WithManagementGroupId(mgmtGrpId), @@ -523,6 +533,7 @@ func TestGenerationAzureConfigSubs(t *testing.T) { MsgRsp{cmd.QuestionAzureConfigName, ""}, MsgRsp{cmd.QuestionEnableManagementGroup, "n"}, MsgRsp{cmd.QuestionEnableActivityLog, "n"}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -540,7 +551,7 @@ func TestGenerationAzureConfigSubs(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(true, false, false, true, + buildTf, _ := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID(mockSubscriptionID), azure.WithSubscriptionIds(testIds), ).Generate() @@ -568,6 +579,7 @@ func TestGenerationAzureActivityLogSubs(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -585,7 +597,7 @@ func TestGenerationAzureActivityLogSubs(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(false, true, false, true, + buildTf, _ := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID), azure.WithSubscriptionIds(testIds), ).Generate() @@ -614,6 +626,7 @@ func TestGenerationAzureActivityLogStorageAccount(t *testing.T) { MsgRsp{cmd.QuestionStorageAccountName, storageAccountName}, MsgRsp{cmd.QuestionStorageAccountResourceGroup, storageResourceGrp}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -631,7 +644,7 @@ func TestGenerationAzureActivityLogStorageAccount(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(false, true, false, true, + buildTf, _ := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID), azure.WithExistingStorageAccount(true), azure.WithStorageAccountName(storageAccountName), @@ -659,6 +672,7 @@ func TestGenerationAzureActivityLogAllSubs(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -676,7 +690,7 @@ func TestGenerationAzureActivityLogAllSubs(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(false, true, false, true, + buildTf, _ := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID), azure.WithAllSubscriptions(true), ).Generate() @@ -702,6 +716,7 @@ func TestGenerationAzureActivityLogLocation(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, region}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -719,7 +734,7 @@ func TestGenerationAzureActivityLogLocation(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(false, true, false, true, + buildTf, _ := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID), azure.WithStorageLocation(region), ).Generate() @@ -750,6 +765,7 @@ func TestGenerationAzureOverwrite(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, dir}, @@ -776,6 +792,7 @@ func TestGenerationAzureOverwrite(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, dir}, @@ -820,6 +837,7 @@ func TestGenerationAzureOverwriteOutput(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, output_dir}, @@ -847,6 +865,7 @@ func TestGenerationAzureOverwriteOutput(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, output_dir}, @@ -882,6 +901,7 @@ func TestGenerationAzureLaceworkProfile(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -899,7 +919,7 @@ func TestGenerationAzureLaceworkProfile(t *testing.T) { assert.Nil(t, runError) assert.Contains(t, final, "Terraform code saved in") - buildTf, _ := azure.NewTerraform(true, true, false, true, + buildTf, _ := azure.NewTerraform(true, true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID), azure.WithLaceworkProfile(azProfile), ).Generate() @@ -925,6 +945,7 @@ func TestGenerationAzureWithSubscriptionID(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -942,7 +963,7 @@ func TestGenerationAzureWithSubscriptionID(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(true, true, false, true, azure.WithSubscriptionID(mockSubscriptionID)).Generate() + buildTf, _ := azure.NewTerraform(true, true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID)).Generate() assert.Equal(t, buildTf, tfResult) } diff --git a/integration/test_resources/help/generate_cloud-account_azure b/integration/test_resources/help/generate_cloud-account_azure index e032e2145..214b6301c 100644 --- a/integration/test_resources/help/generate_cloud-account_azure +++ b/integration/test_resources/help/generate_cloud-account_azure @@ -27,6 +27,7 @@ Flags: --ad_id string existing active directory application id --ad_pass string existing active directory application password --ad_pid string existing active directory application service principle id + --agentless enable agentless integration --all_subscriptions subscription ids grant read access to ALL subscriptions within Tenant (overrides subscription ids) --apply run terraform apply for the generated hcl --configuration enable configuration integration diff --git a/lwgenerate/azure/azure.go b/lwgenerate/azure/azure.go index 28589b97e..e01ce0562 100644 --- a/lwgenerate/azure/azure.go +++ b/lwgenerate/azure/azure.go @@ -3,6 +3,7 @@ package azure import ( "fmt" + "strings" "github.com/hashicorp/hcl/v2/hclwrite" "github.com/lacework/go-sdk/v2/lwgenerate" @@ -94,31 +95,37 @@ type GenerateAzureTfConfigurationArgs struct { // Custom outputs CustomOutputs []lwgenerate.HclOutput + // Integration level for agentless scanning (e.g., "SUBSCRIPTION", "TENANT") + IntegrationLevel string + // Should agentless scanning be global? Global *bool // Should we create a Log Analytics Workspace for agentless scanning? CreateLogAnalyticsWorkspace *bool - // Integration level for agentless scanning (e.g., "SUBSCRIPTION", "TENANT") - IntegrationLevel string + // List of regions to deploy for agentless scanning + Regions []string + + // List of subscription IDs for agentless scanning + AgentlessSubscriptionIds []string } // Ensure all combinations of inputs are valid for supported spec func (args *GenerateAzureTfConfigurationArgs) validate() error { - // Validate one of config or activity log was enabled; otherwise error out + // Validate one of config ,agentless or activity log was enabled; otherwise error out if !args.ActivityLog && !args.Agentless && !args.Config && !args.EntraIdActivityLog { return errors.New("audit log, agentless or config integration must be enabled") } - if (args.ActivityLog || args.Config || args.EntraIdActivityLog) && args.SubscriptionID == "" { + if (args.ActivityLog || args.Agentless || args.Config || args.EntraIdActivityLog) && args.SubscriptionID == "" { return errors.New("subscription_id must be provided") } // Validate that active directory settings are correct if !args.CreateAdIntegration && (args.Config || args.ActivityLog || args.EntraIdActivityLog) { - if (args.AdApplicationId == "" || - args.AdServicePrincipalId == "" || args.AdApplicationPassword == "") { + if args.AdApplicationId == "" || + args.AdServicePrincipalId == "" || args.AdApplicationPassword == "" { return errors.New("Active directory details must be set") } } @@ -133,6 +140,16 @@ func (args *GenerateAzureTfConfigurationArgs) validate() error { return errors.New("When using existing storage account, storage account details must be configured") } + // Validate Agentless Scanning + if args.Agentless { + if args.IntegrationLevel == "" { + return errors.New("integration_level must be set for Agentless Integration") + } + if args.IntegrationLevel == "SUBSCRIPTION" && len(args.AgentlessSubscriptionIds) == 0 { + return errors.New("subscription_ids must be provided for Agentless Integration with SUBSCRIPTION integration level") + } + } + return nil } @@ -149,10 +166,9 @@ func NewTerraform( config := &GenerateAzureTfConfigurationArgs{ ActivityLog: enableActivityLog, Config: enableConfig, - Agentless:enableAgentless, + Agentless: enableAgentless, EntraIdActivityLog: enableEntraIdActivityLog, CreateAdIntegration: createAdIntegration, - IntegrationLevel: "SUBSCRIPTION", // Default integration level } for _, m := range mods { m(config) @@ -263,6 +279,19 @@ func WithSubscriptionIds(subscriptionIds []string) AzureTerraformModifier { } } +// WithAgentlessSubscriptionIds List of subscriptions for agentless scanning. +func WithAgentlessSubscriptionIds(agentlessSubscriptionIds []string) AzureTerraformModifier { + return func(c *GenerateAzureTfConfigurationArgs) { + c.AgentlessSubscriptionIds = agentlessSubscriptionIds + } +} + +func WithRegions(regions []string) AzureTerraformModifier { + return func(c *GenerateAzureTfConfigurationArgs) { + c.Regions = regions + } +} + // WithAllSubscriptions Grant read access to ALL subscriptions within // the selected Tenant (overrides 'subscription_ids') func WithAllSubscriptions(allSubscriptions bool) AzureTerraformModifier { @@ -669,44 +698,80 @@ func createActivityLog(args *GenerateAzureTfConfigurationArgs) ([]*hclwrite.Bloc func createAgentless(args *GenerateAzureTfConfigurationArgs) ([]*hclwrite.Block, error) { blocks := []*hclwrite.Block{} - if args.Agentless { - attributes := map[string]interface{}{} - moduleDetails := []lwgenerate.HclModuleModifier{} - // only include supported arguments if they are set - if args.CreateLogAnalyticsWorkspace != nil { - attributes["create_log_analytics_workspace"] = *args.CreateLogAnalyticsWorkspace - } - if args.Global != nil { - attributes["global"] = *args.Global - } - if args.IntegrationLevel == "" { - // Default integration level is "SUBSCRIPTION" - attributes["integration_level"] = "SUBSCRIPTION" + if !args.Agentless { + return blocks, nil + } + + // Helper function to format region names for module naming + formatRegionForModuleName := func(region string) string { + return strings.ToLower(strings.ReplaceAll(region, " ", "_")) + } + + // Determine regions to process + regions := args.Regions + if len(regions) == 0 { + regions = []string{"West US"} // Default to West US if no regions specified + } + + isTenant := strings.EqualFold(args.IntegrationLevel, "TENANT") + isSubscription := strings.EqualFold(args.IntegrationLevel, "SUBSCRIPTION") + + var firstModuleName string + for i, region := range regions { + isFirstRegion := (i == 0) + + // Build module name + var moduleName string + if isTenant { + moduleName = fmt.Sprintf("lacework_azure_agentless_scanning_tenant_%s", formatRegionForModuleName(region)) } else { - // If IntegrationLevel is set, use it - attributes["integration_level"] = args.IntegrationLevel + moduleName = fmt.Sprintf("lacework_azure_agentless_scanning_subscription_%s", formatRegionForModuleName(region)) } - // for SUBSCRIPTION integration level, we need to set the subscription id - if args.IntegrationLevel == "SUBSCRIPTION" && args.SubscriptionID != "" { - attributes["included_subscriptions"] = []string{fmt.Sprintf("/subscriptions/%s", args.SubscriptionID)} + if isFirstRegion { + firstModuleName = moduleName } - moduleDetails = append(moduleDetails, - lwgenerate.HclModuleWithAttributes(attributes), - ) + // Build attributes + attrs := map[string]interface{}{ + "integration_level": args.IntegrationLevel, + "region": region, + "global": isFirstRegion, + } + if args.Global != nil { + attrs["global"] = *args.Global && isFirstRegion + } + if args.CreateLogAnalyticsWorkspace != nil { + attrs["create_log_analytics_workspace"] = *args.CreateLogAnalyticsWorkspace + } + if args.SubscriptionID != "" { + attrs["scanning_subscription_id"] = args.SubscriptionID + } + if isSubscription && isFirstRegion && len(args.AgentlessSubscriptionIds) > 0 { + subs := make([]string, len(args.AgentlessSubscriptionIds)) + for j, id := range args.AgentlessSubscriptionIds { + subs[j] = fmt.Sprintf("/subscriptions/%s", id) + } + attrs["included_subscriptions"] = subs + } + if !isFirstRegion { + attrs["global_module_reference"] = lwgenerate.CreateSimpleTraversal([]string{"module", firstModuleName}) + } - moduleBlock, err := lwgenerate.NewModule( - "az_agentless", + // Create module details + moduleDetails := []lwgenerate.HclModuleModifier{ + lwgenerate.HclModuleWithAttributes(attrs), + } + block, err := lwgenerate.NewModule( + moduleName, lwgenerate.LWAzureAgentlessSource, append(moduleDetails, lwgenerate.HclModuleWithVersion(lwgenerate.LWAzureAgentlessVersion))..., ).ToBlock() - if err != nil { return nil, err } - blocks = append(blocks, moduleBlock) + blocks = append(blocks, block) } return blocks, nil } diff --git a/lwgenerate/azure/azure_test.go b/lwgenerate/azure/azure_test.go index fbe4c492c..8fa079485 100644 --- a/lwgenerate/azure/azure_test.go +++ b/lwgenerate/azure/azure_test.go @@ -22,7 +22,7 @@ func getFileContent(filename string) (string, error) { func TestGenerationActivityLogWithoutConfig(t *testing.T) { ActivityLogWithoutConfig, fileErr := getFileContent("test-data/activity_log_without_config.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(false, true, false, true, azure.WithSubscriptionID("test-subscription")).Generate() + hcl, err := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID("test-subscription")).Generate() assert.Nil(t, err) assert.NotNil(t, hcl) assert.Equal(t, ActivityLogWithoutConfig, hcl) @@ -31,7 +31,7 @@ func TestGenerationActivityLogWithoutConfig(t *testing.T) { func TestGenerationActivityLogWithConfig(t *testing.T) { var ActivityLogWithConfig, fileErr = getFileContent("test-data/activity_log_with_config.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, true, false, true, azure.WithSubscriptionID("test-subscription")).Generate() + hcl, err := azure.NewTerraform(true, true, false, false, true, azure.WithSubscriptionID("test-subscription")).Generate() assert.Nil(t, err) assert.NotNil(t, hcl) assert.Equal(t, ActivityLogWithConfig, hcl) @@ -43,7 +43,7 @@ func TestGenerationActivityLogWithConfigAndExtraBlocks(t *testing.T) { assert.Nil(t, fileErr) extraBlock, err := lwgenerate.HclCreateGenericBlock("variable", []string{"var_name"}, nil) assert.NoError(t, err) - hcl, err := azure.NewTerraform(true, true, false, true, + hcl, err := azure.NewTerraform(true, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithExtraBlocks([]*hclwrite.Block{extraBlock}), ).Generate() @@ -55,7 +55,7 @@ func TestGenerationActivityLogWithConfigAndExtraBlocks(t *testing.T) { func TestGenerationActivityLogWithConfigAndExtraAzureRMProviderBlocks(t *testing.T) { var ActivityLogWithConfig, fileErr = getFileContent("test-data/activity_log_with_config_provider_args.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, true, false, true, + hcl, err := azure.NewTerraform(true, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithExtraAZRMArguments(map[string]interface{}{"foo": "bar"}), ).Generate() @@ -67,7 +67,7 @@ func TestGenerationActivityLogWithConfigAndExtraAzureRMProviderBlocks(t *testing func TestGenerationActivityLogWithConfigAndExtraAZUReadProviderBlocks(t *testing.T) { var ActivityLogWithConfig, fileErr = getFileContent("test-data/activity_log_with_config_azureadprovider_args.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, true, false, true, + hcl, err := azure.NewTerraform(true, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithExtraAZReadArguments(map[string]interface{}{"foo": "bar"}), ).Generate() @@ -81,7 +81,7 @@ func TestGenerationActivityLogWithConfigAndCustomBackendBlock(t *testing.T) { assert.NoError(t, err) var ActivityLogWithConfig, fileErr = getFileContent("test-data/activity_log_with_config_root_blocks.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, true, false, true, + hcl, err := azure.NewTerraform(true, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithExtraRootBlocks([]*hclwrite.Block{customBlock}), ).Generate() @@ -93,14 +93,14 @@ func TestGenerationActivityLogWithConfigAndCustomBackendBlock(t *testing.T) { func TestGenerationConfigWithoutActivityLog(t *testing.T) { ConfigWithoutActivityLog, fileErr := getFileContent("test-data/config_without_activity_log.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, false, false, true, azure.WithSubscriptionID("test-subscription")).Generate() + hcl, err := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID("test-subscription")).Generate() assert.Nil(t, err) assert.NotNil(t, hcl) assert.Equal(t, ConfigWithoutActivityLog, hcl) } func TestGenerationWithoutActivityLogOrConfig(t *testing.T) { - hcl, err := azure.NewTerraform(false, false, false, true).Generate() + hcl, err := azure.NewTerraform(false, false, false, false, true).Generate() assert.NotNil(t, err) assert.True(t, strings.Contains(errors.Unwrap(err).Error(), "invalid inputs")) assert.Empty(t, hcl) @@ -108,7 +108,7 @@ func TestGenerationWithoutActivityLogOrConfig(t *testing.T) { func TestGenerationRenamedConfig(t *testing.T) { RenamedConfig, fileErr := getFileContent("test-data/renamed_config.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, false, false, true, + hcl, err := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithConfigIntegrationName("Test Config Rename"), ).Generate() @@ -120,7 +120,7 @@ func TestGenerationRenamedConfig(t *testing.T) { func TestGenerationRenamedActivityLog(t *testing.T) { RenamedActivityLog, fileErr := getFileContent("test-data/renamed_activity_log.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(false, true, false, true, + hcl, err := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithActivityLogIntegrationName("Test Activity Log Rename"), ).Generate() @@ -132,7 +132,7 @@ func TestGenerationRenamedActivityLog(t *testing.T) { func TestGenerationRenamedConfigAndActivityLog(t *testing.T) { RenamedConfigAndActivityLog, fileErr := getFileContent("test-data/renamed_config_and_activity_log.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, true, false, true, + hcl, err := azure.NewTerraform(true, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithConfigIntegrationName("Test Config Rename"), azure.WithActivityLogIntegrationName("Test Activity Log Rename"), @@ -143,7 +143,7 @@ func TestGenerationRenamedConfigAndActivityLog(t *testing.T) { } func TestGenerationNoActiveDirectorySettings(t *testing.T) { - hcl, err := azure.NewTerraform(true, true, false, false, + hcl, err := azure.NewTerraform(true, true, false, false, false, azure.WithSubscriptionID("test-subscription"), azure.WithConfigIntegrationName("Test Config Rename"), azure.WithActivityLogIntegrationName("Test Activity Log Rename"), @@ -155,7 +155,7 @@ func TestGenerationNoActiveDirectorySettings(t *testing.T) { func TestGenerationCustomActiveDirectory(t *testing.T) { CustomADDetails, fileErr := getFileContent("test-data/customer-ad-details.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, true, false, false, + hcl, err := azure.NewTerraform(true, true, false, false, false, azure.WithSubscriptionID("test-subscription"), azure.WithConfigIntegrationName("Test Config Rename"), azure.WithActivityLogIntegrationName("Test Activity Log Rename"), @@ -171,7 +171,7 @@ func TestGenerationCustomActiveDirectory(t *testing.T) { func TestGenerationActivityLogWithExistingStorageAccount(t *testing.T) { ActivityLogWithStorage, fileErr := getFileContent("test-data/activity-log-with-existing-storage.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(false, true, false, true, + hcl, err := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithExistingStorageAccount(true), azure.WithStorageAccountName("Test-Storage-Account-Name"), @@ -185,7 +185,7 @@ func TestGenerationActivityLogWithExistingStorageAccount(t *testing.T) { func TestGenerationActivityLogWithAllSubscriptions(t *testing.T) { ActivityLogAllSubs, fileErr := getFileContent("test-data/activity-log-with-all-subscriptions.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(false, true, false, true, + hcl, err := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithAllSubscriptions(true), ).Generate() @@ -197,7 +197,7 @@ func TestGenerationActivityLogWithAllSubscriptions(t *testing.T) { func TestGenerationConfigWithAllSubscriptions(t *testing.T) { ConfigAllSubs, fileErr := getFileContent("test-data/config-with-all-subscriptions.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, false, false, true, + hcl, err := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithAllSubscriptions(true), ).Generate() @@ -209,7 +209,7 @@ func TestGenerationConfigWithAllSubscriptions(t *testing.T) { func TestGenerationConfigWithManagementGroup(t *testing.T) { ConfigWithMgmtGroup, fileErr := getFileContent("test-data/config-with-management-group.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, false, false, true, + hcl, err := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithManagementGroup(true), azure.WithManagementGroupId("test-management-group-1"), @@ -220,7 +220,7 @@ func TestGenerationConfigWithManagementGroup(t *testing.T) { } func TestGenerationConfigWithManagementGroupError(t *testing.T) { - hcl, err := azure.NewTerraform(true, false, false, true, + hcl, err := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithManagementGroup(true), ).Generate() @@ -233,7 +233,7 @@ func TestGenerationActivityLogWithSubscriptionsList(t *testing.T) { ActivityLogWithSubscriptions, fileErr := getFileContent("test-data/activity-log-with-list-subscriptions.tf") assert.Nil(t, fileErr) testIds := []string{"test-id-1", "test-id-2", "test-id-3"} - hcl, err := azure.NewTerraform(false, true, false, true, + hcl, err := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithSubscriptionIds(testIds), ).Generate() @@ -246,7 +246,7 @@ func TestGenerationConfigWithSubscriptionsList(t *testing.T) { ConfigWithSubscriptions, fileErr := getFileContent("test-data/config-log-with-list-subscriptions.tf") assert.Nil(t, fileErr) testIds := []string{"test-id-1", "test-id-2", "test-id-3"} - hcl, err := azure.NewTerraform(true, false, false, true, + hcl, err := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithSubscriptionIds(testIds), ).Generate() @@ -258,7 +258,7 @@ func TestGenerationConfigWithSubscriptionsList(t *testing.T) { func TestGenerationLocation(t *testing.T) { ActivityLogLocation, fileErr := getFileContent("test-data/activity-log-with-location.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(false, true, false, true, + hcl, err := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithStorageLocation("West US 2"), ).Generate() @@ -271,7 +271,7 @@ func TestGenerationWithLaceworkProvider(t *testing.T) { laceworkProfile, fileErr := getFileContent("test-data/activity-log-with-lacework-profile.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(false, true, false, true, + hcl, err := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithLaceworkProfile("test-profile"), ).Generate() @@ -284,7 +284,7 @@ func TestGenerationAzureRmProviderWithSubscriptionID(t *testing.T) { configWithSubscription, fileErr := getFileContent("test-data/config-with-azurerm-subscription.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, false, false, true, azure.WithSubscriptionID("test-subscription")).Generate() + hcl, err := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID("test-subscription")).Generate() assert.Nil(t, err) assert.NotNil(t, hcl) assert.Equal(t, configWithSubscription, hcl) @@ -293,7 +293,7 @@ func TestGenerationAzureRmProviderWithSubscriptionID(t *testing.T) { func TestGenerationEntraIDActivityLog(t *testing.T) { ActivityLogEntraID, fileErr := getFileContent("test-data/entra-id-activity-log-no-custom-input.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(false, false, true, true, azure.WithSubscriptionID("test-subscription")).Generate() + hcl, err := azure.NewTerraform(false, false, false, true, true, azure.WithSubscriptionID("test-subscription")).Generate() assert.Nil(t, err) assert.NotNil(t, hcl) assert.Equal(t, ActivityLogEntraID, hcl) @@ -302,7 +302,7 @@ func TestGenerationEntraIDActivityLog(t *testing.T) { func TestGenerationEntraIDActivityLogExistingActiveDirectoryApp(t *testing.T) { ActivityLogEntraID, fileErr := getFileContent("test-data/entra-id-activity-log-existing-ad-app.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(false, false, true, false, + hcl, err := azure.NewTerraform(false, false, false, true, false, azure.WithSubscriptionID("test-subscription"), azure.WithAdApplicationId("testID"), azure.WithAdApplicationPassword("pass"), @@ -316,7 +316,7 @@ func TestGenerationEntraIDActivityLogExistingActiveDirectoryApp(t *testing.T) { func TestGenerationEntraIDActivityLogEventHubLocationAndPartition(t *testing.T) { ActivityLogEntraID, fileErr := getFileContent("test-data/entra-id-activity-log-event-hub-location-and-partition.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(false, false, true, true, + hcl, err := azure.NewTerraform(false, false, false, true, true, azure.WithSubscriptionID("test-subscription"), azure.WithEventHubLocation("West US 2"), azure.WithEventHubPartitionCount(2), diff --git a/lwgenerate/constants.go b/lwgenerate/constants.go index 1eaf3212a..c0ea24a54 100644 --- a/lwgenerate/constants.go +++ b/lwgenerate/constants.go @@ -20,7 +20,7 @@ const ( LWAzureConfigSource = "lacework/config/azure" LWAzureConfigVersion = "~> 3.0" - LWAzureAgentlessSource = "lacework/agentless-scanning/azure" + LWAzureAgentlessSource = "lacework/agentless-scanning/azure" LWAzureAgentlessVersion = "~> 1.5" LWAzureActivityLogSource = "lacework/activity-log/azure" LWAzureActivityLogVersion = "~> 3.0" From 84b5203da0c3cb1b57e4dea5d33a1104c069bcb8 Mon Sep 17 00:00:00 2001 From: lvadlamudi Date: Tue, 8 Jul 2025 17:57:42 -0700 Subject: [PATCH 4/4] chore: support tenant integration level --- cli/cmd/generate_azure.go | 173 +++++++++++++----- cli/cmd/generate_azure_test.go | 3 +- integration/azure_generation_test.go | 55 ++++-- .../help/generate_cloud-account_azure | 4 + lwgenerate/azure/azure.go | 137 ++++++++++---- lwgenerate/azure/azure_test.go | 68 ++++--- .../test-data/agentless-scanning-only.tf | 28 +++ lwgenerate/constants.go | 2 +- 8 files changed, 344 insertions(+), 126 deletions(-) create mode 100644 lwgenerate/azure/test-data/agentless-scanning-only.tf diff --git a/cli/cmd/generate_azure.go b/cli/cmd/generate_azure.go index b277d76f2..1e8a054ee 100644 --- a/cli/cmd/generate_azure.go +++ b/cli/cmd/generate_azure.go @@ -1,6 +1,7 @@ package cmd import ( + "fmt" "regexp" "strconv" "strings" @@ -16,11 +17,11 @@ import ( // Question labels const ( - IconAzureConfig = "[Configuration]" - IconActivityLog = "[Activity Log]" - IconEntraID = "[Entra ID Activity Log]" - IconAD = "[Active Directory Application]" - IconAzureAgentless = "[Agentless]" + IconAzureConfig = "[Configuration]" + IconActivityLog = "[Activity Log]" + IconEntraID = "[Entra ID Activity Log]" + IconAD = "[Active Directory Application]" + IconAzureAgentless = "[Agentless]" ) var ( @@ -198,20 +199,19 @@ the new cloud account. In interactive mode, this command will: azure.WithEventHubPartitionCount(GenerateAzureCommandState.EventHubPartitionCount), } - if GenerateAzureCommandState.Global != nil { - mods = append(mods, azure.WithGlobal(*GenerateAzureCommandState.Global)) - } - if GenerateAzureCommandState.CreateLogAnalyticsWorkspace != nil { - mods = append(mods, azure.WithCreateLogAnalyticsWorkspace(*GenerateAzureCommandState.CreateLogAnalyticsWorkspace)) - } - - // Always set the integration level to subscription if GenerateAzureCommandState.Agentless { - mods = append(mods, azure.WithIntegrationLevel("SUBSCRIPTION")) + mods = append(mods, azure.WithIntegrationLevel(GenerateAzureCommandState.IntegrationLevel)) + mods = append(mods, azure.WithAgentlessSubscriptionIds(GenerateAzureCommandState.AgentlessSubscriptionIds)) + mods = append(mods, azure.WithRegions(GenerateAzureCommandState.Regions)) + mods = append(mods, azure.WithCreateLogAnalyticsWorkspace(GenerateAzureCommandState.CreateLogAnalyticsWorkspace)) + mods = append(mods, azure.WithGlobal(GenerateAzureCommandState.Global)) } // Check if AD Creation is required, need to set values for current integration - if !GenerateAzureCommandState.CreateAdIntegration && (GenerateAzureCommandState.Config || GenerateAzureCommandState.ActivityLog || GenerateAzureCommandState.EntraIdActivityLog) { + if !GenerateAzureCommandState.CreateAdIntegration && + (GenerateAzureCommandState.Config || + GenerateAzureCommandState.ActivityLog || + GenerateAzureCommandState.EntraIdActivityLog) { mods = append(mods, azure.WithAdApplicationId(GenerateAzureCommandState.AdApplicationId)) mods = append(mods, azure.WithAdApplicationPassword(GenerateAzureCommandState.AdApplicationPassword)) mods = append(mods, azure.WithAdServicePrincipalId(GenerateAzureCommandState.AdServicePrincipalId)) @@ -412,6 +412,24 @@ func initGenerateAzureTfCommandFlags() { false, "enable agentless integration") + generateAzureTfCommand.PersistentFlags().StringVar( + &GenerateAzureCommandState.IntegrationLevel, + "integration_level", + "", + "specify the agentless integration level (e.g., 'SUBSCRIPTION', 'TENANT')") + + generateAzureTfCommand.PersistentFlags().BoolVar( + &GenerateAzureCommandState.Global, + "global", + true, + "enable global agentless scanning") + + generateAzureTfCommand.PersistentFlags().BoolVar( + &GenerateAzureCommandState.CreateLogAnalyticsWorkspace, + "create_log_analytics_workspace", + false, + "enable creation of Log Analytics Workspace for agentless scanning") + generateAzureTfCommand.PersistentFlags().BoolVar( &GenerateAzureCommandState.EntraIdActivityLog, "entra_id_activity_log", @@ -821,22 +839,28 @@ func promptAzureGenerate( } // Ask AD integration - if err := SurveyMultipleQuestionWithValidation( - []SurveyQuestionWithValidationArgs{ - { - Icon: IconAD, - Prompt: &survey.Confirm{Message: QuestionEnableAdIntegration, Default: config.CreateAdIntegration}, - Response: &config.CreateAdIntegration, - }, - }); err != nil { - return err - } - - // If AD integration is not being created, ask for existing AD details immediately - if !config.CreateAdIntegration && (config.Config || config.ActivityLog || config.EntraIdActivityLog) { - if err := promptAzureAdIntegrationQuestions(config); err != nil { + if config.Config || config.ActivityLog || config.EntraIdActivityLog { + if err := SurveyMultipleQuestionWithValidation( + []SurveyQuestionWithValidationArgs{ + { + Icon: IconAD, + Prompt: &survey.Confirm{Message: QuestionEnableAdIntegration, Default: config.CreateAdIntegration}, + Response: &config.CreateAdIntegration, + }, + }); err != nil { return err } + + // If AD integration is not being created, ask for existing AD details immediately + if !config.CreateAdIntegration { + if err := promptAzureAdIntegrationQuestions(config); err != nil { + return err + } + } + } + // for agentless only scenario, we set CreateAdIntegration to false + if config.Agentless && !config.Config && !config.ActivityLog && !config.EntraIdActivityLog { + config.CreateAdIntegration = false } // Ask about output location @@ -931,38 +955,99 @@ func promptAzureActivityLogQuestions(config *azure.GenerateAzureTfConfigurationA return nil } - func promptAzureAgentlessQuestions(config *azure.GenerateAzureTfConfigurationArgs) error { - // prompt for global setting - if config.Global == nil { - defaultGlobal := true - config.Global = &defaultGlobal + // Prompt for integration level(SUBSCRIPTION or TENANT) + if config.IntegrationLevel == "" { + config.IntegrationLevel = "SUBSCRIPTION" + } + if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{ + { + Icon: IconAzureAgentless, + Prompt: &survey.Select{ + Message: "Select integration level:", + Options: []string{"SUBSCRIPTION", "TENANT"}, + Default: config.IntegrationLevel, + }, + Response: &config.IntegrationLevel, + }, + }); err != nil { + return err } + // prompt for global setting if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{ { Icon: IconAzureAgentless, - Prompt: &survey.Confirm{Message: "Enable global agentless scanning?", Default: *config.Global}, - Response: config.Global, + Prompt: &survey.Confirm{Message: "Enable global agentless scanning?", Default: config.Global}, + Response: &config.Global, }, }); err != nil { return err } - // prompt for log analytics workspace creation - if config.CreateLogAnalyticsWorkspace == nil { - defaultCreateLogAnalyticsWorkspace := true - config.CreateLogAnalyticsWorkspace = &defaultCreateLogAnalyticsWorkspace - } + // prompt for log analytics workspace creation (default: false) if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{ { Icon: IconAzureAgentless, - Prompt: &survey.Confirm{Message: "Create Log Analytics Workspace?", Default: *config.CreateLogAnalyticsWorkspace}, - Response: config.CreateLogAnalyticsWorkspace, + Prompt: &survey.Confirm{Message: "Create Log Analytics Workspace?", Default: config.CreateLogAnalyticsWorkspace}, + Response: &config.CreateLogAnalyticsWorkspace, + }, + }); err != nil { + return err + } + + // Ask for regions + var regionsInput string + if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{ + { + Icon: IconAzureAgentless, + Prompt: &survey.Input{ + Message: "Comma-separated list of regions for Agentless scanning (e.g., 'East US, West US')", + Default: "West US", + }, + Response: ®ionsInput, }, }); err != nil { return err } + // parse regions from comma-separated string + if regionsInput != "" { + regions := strings.Split(regionsInput, ",") + for i, region := range regions { + regions[i] = strings.TrimSpace(region) + } + config.Regions = regions + } + // Only ask for subscription IDs if SUBSCRIPTION integration level is selected + if config.IntegrationLevel == "SUBSCRIPTION" { + var subscriptionIdsInput string + for { + if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{ + { + Icon: IconAzureAgentless, + Prompt: &survey.Input{ + Message: "Comma-separated list of subscription IDs for Agentless scanning (e.g., 'sub1, sub2')", + Default: "", + }, + Response: &subscriptionIdsInput, + }, + }); err != nil { + return err + } + subscriptionIdsInput = strings.TrimSpace(subscriptionIdsInput) + if subscriptionIdsInput != "" { + subscriptionIds := strings.Split(subscriptionIdsInput, ",") + for i, subId := range subscriptionIds { + subscriptionIds[i] = strings.TrimSpace(subId) + } + config.AgentlessSubscriptionIds = subscriptionIds + break + } else { + fmt.Println("Subscription IDs cannot be empty. Please provide at least one subscription ID.") + } + + } + } return nil -} \ No newline at end of file +} diff --git a/cli/cmd/generate_azure_test.go b/cli/cmd/generate_azure_test.go index da49820c8..c79478b77 100644 --- a/cli/cmd/generate_azure_test.go +++ b/cli/cmd/generate_azure_test.go @@ -39,10 +39,11 @@ func TestMissingValidEntity(t *testing.T) { data := azure.GenerateAzureTfConfigurationArgs{} data.Config = false data.ActivityLog = false + data.Agentless = false err := promptAzureGenerate(&data, &AzureGenerateCommandExtraState{Output: "/tmp"}) assert.Error(t, err) - assert.Equal(t, "must enable at least one of: Configuration or Activity Log integration", err.Error()) + assert.Equal(t, "must enable at least one of: Configuration, Agentless or Activity Log integrations", err.Error()) } func TestValidStorageLocations(t *testing.T) { diff --git a/integration/azure_generation_test.go b/integration/azure_generation_test.go index 4b8eb6148..81db27940 100644 --- a/integration/azure_generation_test.go +++ b/integration/azure_generation_test.go @@ -43,8 +43,9 @@ func TestGenerationAzureErrorOnNoSelection(t *testing.T) { MsgRsp{cmd.AzureSubscriptions, "n"}, MsgRsp{cmd.QuestionAzureEnableConfig, "n"}, MsgRsp{cmd.QuestionEnableActivityLog, "n"}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, - MsgOnly{"ERROR collecting/confirming parameters: must enable at least one of: Configuration or Activity Log integration"}, + MsgOnly{"ERROR collecting/confirming parameters: must enable at least one of: Configuration, Agentless or Activity Log integration"}, }) }, "generate", @@ -76,6 +77,7 @@ func TestGenerationAzureSimple(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -93,7 +95,7 @@ func TestGenerationAzureSimple(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(true, true, false, true, azure.WithSubscriptionID(mockSubscriptionID)).Generate() + buildTf, _ := azure.NewTerraform(true, true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID)).Generate() assert.Equal(t, buildTf, tfResult) } @@ -124,6 +126,7 @@ func TestGenerationAzureCustomizedOutputLocation(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, dir}, @@ -144,7 +147,7 @@ func TestGenerationAzureCustomizedOutputLocation(t *testing.T) { result, _ := os.ReadFile(filepath.FromSlash(fmt.Sprintf("%s/main.tf", dir))) // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(true, true, false, true, azure.WithSubscriptionID(mockSubscriptionID)).Generate() + buildTf, _ := azure.NewTerraform(true, true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID)).Generate() assert.Equal(t, buildTf, string(result)) } @@ -165,6 +168,7 @@ func TestGenerationAzureConfigOnly(t *testing.T) { MsgRsp{cmd.QuestionAzureConfigName, ""}, MsgRsp{cmd.QuestionEnableManagementGroup, "n"}, MsgRsp{cmd.QuestionEnableActivityLog, "n"}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -182,7 +186,7 @@ func TestGenerationAzureConfigOnly(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID)).Generate() + buildTf, _ := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID(mockSubscriptionID)).Generate() assert.Equal(t, buildTf, tfResult) } @@ -204,6 +208,7 @@ func TestGenerationAzureActivityLogOnly(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -221,7 +226,7 @@ func TestGenerationAzureActivityLogOnly(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(false, true, false, true, azure.WithSubscriptionID(mockSubscriptionID)).Generate() + buildTf, _ := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID)).Generate() assert.Equal(t, buildTf, tfResult) } @@ -248,6 +253,7 @@ func TestGenerationAzureNoADEnabled(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "n"}, MsgRsp{cmd.QuestionADApplicationPass, pass}, @@ -268,7 +274,7 @@ func TestGenerationAzureNoADEnabled(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(true, true, false, false, + buildTf, _ := azure.NewTerraform(true, true, false, false, false, azure.WithSubscriptionID(mockSubscriptionID), azure.WithAdApplicationPassword(pass), azure.WithAdServicePrincipalId(principalId), @@ -296,6 +302,7 @@ func _TestGenerationAzureNamedConfig(t *testing.T) { MsgRsp{cmd.QuestionAzureConfigName, configName}, MsgRsp{cmd.QuestionEnableManagementGroup, "n"}, MsgRsp{cmd.QuestionEnableActivityLog, "n"}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -313,7 +320,7 @@ func _TestGenerationAzureNamedConfig(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(true, false, false, true, + buildTf, _ := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID(mockSubscriptionID), azure.WithConfigIntegrationName(configName), ).Generate() @@ -354,7 +361,7 @@ func _TestGenerationAzureNamedActivityLog(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(false, true, false, true, + buildTf, _ := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID), azure.WithActivityLogIntegrationName(activityName), ).Generate() @@ -393,6 +400,7 @@ func TestGenerationAzureWithExistingTerraform(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, dir}, @@ -435,6 +443,7 @@ func TestGenerationAzureConfigAllSubs(t *testing.T) { MsgRsp{cmd.QuestionAzureConfigName, ""}, MsgRsp{cmd.QuestionEnableManagementGroup, "n"}, MsgRsp{cmd.QuestionEnableActivityLog, "n"}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -452,7 +461,7 @@ func TestGenerationAzureConfigAllSubs(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(true, false, false, true, + buildTf, _ := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID(mockSubscriptionID), azure.WithAllSubscriptions(true), ).Generate() @@ -478,6 +487,7 @@ func TestGenerationAzureConfigMgmntGroup(t *testing.T) { MsgRsp{cmd.QuestionEnableManagementGroup, "y"}, MsgRsp{cmd.QuestionManagementGroupId, mgmtGrpId}, MsgRsp{cmd.QuestionEnableActivityLog, "n"}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -495,7 +505,7 @@ func TestGenerationAzureConfigMgmntGroup(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(true, false, false, true, + buildTf, _ := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID(mockSubscriptionID), azure.WithManagementGroup(true), azure.WithManagementGroupId(mgmtGrpId), @@ -523,6 +533,7 @@ func TestGenerationAzureConfigSubs(t *testing.T) { MsgRsp{cmd.QuestionAzureConfigName, ""}, MsgRsp{cmd.QuestionEnableManagementGroup, "n"}, MsgRsp{cmd.QuestionEnableActivityLog, "n"}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -540,7 +551,7 @@ func TestGenerationAzureConfigSubs(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(true, false, false, true, + buildTf, _ := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID(mockSubscriptionID), azure.WithSubscriptionIds(testIds), ).Generate() @@ -568,6 +579,7 @@ func TestGenerationAzureActivityLogSubs(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -585,7 +597,7 @@ func TestGenerationAzureActivityLogSubs(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(false, true, false, true, + buildTf, _ := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID), azure.WithSubscriptionIds(testIds), ).Generate() @@ -614,6 +626,7 @@ func TestGenerationAzureActivityLogStorageAccount(t *testing.T) { MsgRsp{cmd.QuestionStorageAccountName, storageAccountName}, MsgRsp{cmd.QuestionStorageAccountResourceGroup, storageResourceGrp}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -631,7 +644,7 @@ func TestGenerationAzureActivityLogStorageAccount(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(false, true, false, true, + buildTf, _ := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID), azure.WithExistingStorageAccount(true), azure.WithStorageAccountName(storageAccountName), @@ -659,6 +672,7 @@ func TestGenerationAzureActivityLogAllSubs(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -676,7 +690,7 @@ func TestGenerationAzureActivityLogAllSubs(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(false, true, false, true, + buildTf, _ := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID), azure.WithAllSubscriptions(true), ).Generate() @@ -702,6 +716,7 @@ func TestGenerationAzureActivityLogLocation(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, region}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -719,7 +734,7 @@ func TestGenerationAzureActivityLogLocation(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(false, true, false, true, + buildTf, _ := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID), azure.WithStorageLocation(region), ).Generate() @@ -750,6 +765,7 @@ func TestGenerationAzureOverwrite(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, dir}, @@ -776,6 +792,7 @@ func TestGenerationAzureOverwrite(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, dir}, @@ -820,6 +837,7 @@ func TestGenerationAzureOverwriteOutput(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, output_dir}, @@ -847,6 +865,7 @@ func TestGenerationAzureOverwriteOutput(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, output_dir}, @@ -882,6 +901,7 @@ func TestGenerationAzureLaceworkProfile(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -899,7 +919,7 @@ func TestGenerationAzureLaceworkProfile(t *testing.T) { assert.Nil(t, runError) assert.Contains(t, final, "Terraform code saved in") - buildTf, _ := azure.NewTerraform(true, true, false, true, + buildTf, _ := azure.NewTerraform(true, true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID), azure.WithLaceworkProfile(azProfile), ).Generate() @@ -925,6 +945,7 @@ func TestGenerationAzureWithSubscriptionID(t *testing.T) { MsgRsp{cmd.QuestionActivityLogName, ""}, MsgRsp{cmd.QuestionUseExistingStorageAccount, "n"}, MsgRsp{cmd.QuestionStorageLocation, ""}, + MsgRsp{cmd.QuestionAzureEnableAgentless, "n"}, MsgRsp{cmd.QuestionEnableEntraIdActivityLog, "n"}, MsgRsp{cmd.QuestionEnableAdIntegration, "y"}, MsgRsp{cmd.QuestionAzureCustomizeOutputLocation, ""}, @@ -942,7 +963,7 @@ func TestGenerationAzureWithSubscriptionID(t *testing.T) { assert.Contains(t, final, "Terraform code saved in") // Create the TF directly with lwgenerate and validate same result via CLI - buildTf, _ := azure.NewTerraform(true, true, false, true, azure.WithSubscriptionID(mockSubscriptionID)).Generate() + buildTf, _ := azure.NewTerraform(true, true, false, false, true, azure.WithSubscriptionID(mockSubscriptionID)).Generate() assert.Equal(t, buildTf, tfResult) } diff --git a/integration/test_resources/help/generate_cloud-account_azure b/integration/test_resources/help/generate_cloud-account_azure index e032e2145..2e008c528 100644 --- a/integration/test_resources/help/generate_cloud-account_azure +++ b/integration/test_resources/help/generate_cloud-account_azure @@ -27,16 +27,20 @@ Flags: --ad_id string existing active directory application id --ad_pass string existing active directory application password --ad_pid string existing active directory application service principle id + --agentless enable agentless integration --all_subscriptions subscription ids grant read access to ALL subscriptions within Tenant (overrides subscription ids) --apply run terraform apply for the generated hcl --configuration enable configuration integration --configuration_name string specify a custom configuration integration name + --create_log_analytics_workspace enable creation of Log Analytics Workspace for agentless scanning (default false) --entra_id_activity_log enable Entra ID activity log integration --entra_id_activity_log_integration_name string specify a custom Entra ID activity log integration name --event_hub_location string specify the location where the Event Hub for logging will reside --event_hub_partition_count int specify the number of partitions for the Event Hub (default 1) --existing_storage use existing storage account + --global enable global agentless scanning (default true) -h, --help help for azure + --integration_level string specify the agentless integration level (e.g., 'SUBSCRIPTION', 'TENANT') --location string specify azure region where storage account logging resides --management_group management group level integration --management_group_id string specify management group id. Required if mgmt_group provided diff --git a/lwgenerate/azure/azure.go b/lwgenerate/azure/azure.go index 28589b97e..6d6aaab3c 100644 --- a/lwgenerate/azure/azure.go +++ b/lwgenerate/azure/azure.go @@ -3,6 +3,7 @@ package azure import ( "fmt" + "strings" "github.com/hashicorp/hcl/v2/hclwrite" "github.com/lacework/go-sdk/v2/lwgenerate" @@ -94,31 +95,37 @@ type GenerateAzureTfConfigurationArgs struct { // Custom outputs CustomOutputs []lwgenerate.HclOutput + // Integration level for agentless scanning (e.g., "SUBSCRIPTION", "TENANT") + IntegrationLevel string + // Should agentless scanning be global? - Global *bool + Global bool // Should we create a Log Analytics Workspace for agentless scanning? - CreateLogAnalyticsWorkspace *bool + CreateLogAnalyticsWorkspace bool - // Integration level for agentless scanning (e.g., "SUBSCRIPTION", "TENANT") - IntegrationLevel string + // List of regions to deploy for agentless scanning + Regions []string + + // List of subscription IDs for agentless scanning + AgentlessSubscriptionIds []string } // Ensure all combinations of inputs are valid for supported spec func (args *GenerateAzureTfConfigurationArgs) validate() error { - // Validate one of config or activity log was enabled; otherwise error out + // Validate one of config ,agentless or activity log was enabled; otherwise error out if !args.ActivityLog && !args.Agentless && !args.Config && !args.EntraIdActivityLog { return errors.New("audit log, agentless or config integration must be enabled") } - if (args.ActivityLog || args.Config || args.EntraIdActivityLog) && args.SubscriptionID == "" { + if (args.ActivityLog || args.Agentless || args.Config || args.EntraIdActivityLog) && args.SubscriptionID == "" { return errors.New("subscription_id must be provided") } // Validate that active directory settings are correct if !args.CreateAdIntegration && (args.Config || args.ActivityLog || args.EntraIdActivityLog) { - if (args.AdApplicationId == "" || - args.AdServicePrincipalId == "" || args.AdApplicationPassword == "") { + if args.AdApplicationId == "" || + args.AdServicePrincipalId == "" || args.AdApplicationPassword == "" { return errors.New("Active directory details must be set") } } @@ -133,6 +140,16 @@ func (args *GenerateAzureTfConfigurationArgs) validate() error { return errors.New("When using existing storage account, storage account details must be configured") } + // Validate Agentless Scanning + if args.Agentless { + if args.IntegrationLevel == "" { + return errors.New("integration_level must be set for Agentless Integration") + } + if args.IntegrationLevel == "SUBSCRIPTION" && len(args.AgentlessSubscriptionIds) == 0 { + return errors.New("subscription_ids must be provided for Agentless Integration with SUBSCRIPTION integration level") + } + } + return nil } @@ -149,10 +166,9 @@ func NewTerraform( config := &GenerateAzureTfConfigurationArgs{ ActivityLog: enableActivityLog, Config: enableConfig, - Agentless:enableAgentless, + Agentless: enableAgentless, EntraIdActivityLog: enableEntraIdActivityLog, CreateAdIntegration: createAdIntegration, - IntegrationLevel: "SUBSCRIPTION", // Default integration level } for _, m := range mods { m(config) @@ -263,6 +279,19 @@ func WithSubscriptionIds(subscriptionIds []string) AzureTerraformModifier { } } +// WithAgentlessSubscriptionIds List of subscriptions for agentless scanning. +func WithAgentlessSubscriptionIds(agentlessSubscriptionIds []string) AzureTerraformModifier { + return func(c *GenerateAzureTfConfigurationArgs) { + c.AgentlessSubscriptionIds = agentlessSubscriptionIds + } +} + +func WithRegions(regions []string) AzureTerraformModifier { + return func(c *GenerateAzureTfConfigurationArgs) { + c.Regions = regions + } +} + // WithAllSubscriptions Grant read access to ALL subscriptions within // the selected Tenant (overrides 'subscription_ids') func WithAllSubscriptions(allSubscriptions bool) AzureTerraformModifier { @@ -328,14 +357,14 @@ func WithSubscriptionID(subcriptionID string) AzureTerraformModifier { // WithGlobal sets the Global field for agentless scanning func WithGlobal(global bool) AzureTerraformModifier { return func(c *GenerateAzureTfConfigurationArgs) { - c.Global = &global + c.Global = global } } // WithCreateLogAnalyticsWorkspace sets the CreateLogAnalyticsWorkspace field for agentless scanning func WithCreateLogAnalyticsWorkspace(create bool) AzureTerraformModifier { return func(c *GenerateAzureTfConfigurationArgs) { - c.CreateLogAnalyticsWorkspace = &create + c.CreateLogAnalyticsWorkspace = create } } @@ -669,44 +698,78 @@ func createActivityLog(args *GenerateAzureTfConfigurationArgs) ([]*hclwrite.Bloc func createAgentless(args *GenerateAzureTfConfigurationArgs) ([]*hclwrite.Block, error) { blocks := []*hclwrite.Block{} - if args.Agentless { - attributes := map[string]interface{}{} - moduleDetails := []lwgenerate.HclModuleModifier{} - // only include supported arguments if they are set - if args.CreateLogAnalyticsWorkspace != nil { - attributes["create_log_analytics_workspace"] = *args.CreateLogAnalyticsWorkspace - } - if args.Global != nil { - attributes["global"] = *args.Global - } - if args.IntegrationLevel == "" { - // Default integration level is "SUBSCRIPTION" - attributes["integration_level"] = "SUBSCRIPTION" + if !args.Agentless { + return blocks, nil + } + + // Helper function to format region names for module naming + formatRegionForModuleName := func(region string) string { + return strings.ToLower(strings.ReplaceAll(region, " ", "_")) + } + + // Determine regions to process + regions := args.Regions + if len(regions) == 0 { + regions = []string{"West US"} // Default to West US if no regions specified + } + + isTenant := strings.EqualFold(args.IntegrationLevel, "TENANT") + isSubscription := strings.EqualFold(args.IntegrationLevel, "SUBSCRIPTION") + + var firstModuleName string + for i, region := range regions { + isFirstRegion := (i == 0) + + // Build module name + var moduleName string + if isTenant { + moduleName = fmt.Sprintf("lacework_azure_agentless_scanning_tenant_%s", formatRegionForModuleName(region)) } else { - // If IntegrationLevel is set, use it - attributes["integration_level"] = args.IntegrationLevel + moduleName = fmt.Sprintf("lacework_azure_agentless_scanning_subscription_%s", formatRegionForModuleName(region)) } - // for SUBSCRIPTION integration level, we need to set the subscription id - if args.IntegrationLevel == "SUBSCRIPTION" && args.SubscriptionID != "" { - attributes["included_subscriptions"] = []string{fmt.Sprintf("/subscriptions/%s", args.SubscriptionID)} + if isFirstRegion { + firstModuleName = moduleName } - moduleDetails = append(moduleDetails, - lwgenerate.HclModuleWithAttributes(attributes), - ) + // Build attributes + attrs := map[string]interface{}{ + "integration_level": args.IntegrationLevel, + "region": region, + "global": isFirstRegion, + } + + attrs["global"] = args.Global && isFirstRegion + attrs["create_log_analytics_workspace"] = args.CreateLogAnalyticsWorkspace + + if args.SubscriptionID != "" { + attrs["scanning_subscription_id"] = args.SubscriptionID + } + if isSubscription && isFirstRegion && len(args.AgentlessSubscriptionIds) > 0 { + subs := make([]string, len(args.AgentlessSubscriptionIds)) + for j, id := range args.AgentlessSubscriptionIds { + subs[j] = fmt.Sprintf("/subscriptions/%s", id) + } + attrs["included_subscriptions"] = subs + } + if !isFirstRegion { + attrs["global_module_reference"] = lwgenerate.CreateSimpleTraversal([]string{"module", firstModuleName}) + } - moduleBlock, err := lwgenerate.NewModule( - "az_agentless", + // Create module details + moduleDetails := []lwgenerate.HclModuleModifier{ + lwgenerate.HclModuleWithAttributes(attrs), + } + block, err := lwgenerate.NewModule( + moduleName, lwgenerate.LWAzureAgentlessSource, append(moduleDetails, lwgenerate.HclModuleWithVersion(lwgenerate.LWAzureAgentlessVersion))..., ).ToBlock() - if err != nil { return nil, err } - blocks = append(blocks, moduleBlock) + blocks = append(blocks, block) } return blocks, nil } diff --git a/lwgenerate/azure/azure_test.go b/lwgenerate/azure/azure_test.go index fbe4c492c..a8bc5e3c6 100644 --- a/lwgenerate/azure/azure_test.go +++ b/lwgenerate/azure/azure_test.go @@ -22,7 +22,7 @@ func getFileContent(filename string) (string, error) { func TestGenerationActivityLogWithoutConfig(t *testing.T) { ActivityLogWithoutConfig, fileErr := getFileContent("test-data/activity_log_without_config.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(false, true, false, true, azure.WithSubscriptionID("test-subscription")).Generate() + hcl, err := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID("test-subscription")).Generate() assert.Nil(t, err) assert.NotNil(t, hcl) assert.Equal(t, ActivityLogWithoutConfig, hcl) @@ -31,7 +31,7 @@ func TestGenerationActivityLogWithoutConfig(t *testing.T) { func TestGenerationActivityLogWithConfig(t *testing.T) { var ActivityLogWithConfig, fileErr = getFileContent("test-data/activity_log_with_config.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, true, false, true, azure.WithSubscriptionID("test-subscription")).Generate() + hcl, err := azure.NewTerraform(true, true, false, false, true, azure.WithSubscriptionID("test-subscription")).Generate() assert.Nil(t, err) assert.NotNil(t, hcl) assert.Equal(t, ActivityLogWithConfig, hcl) @@ -43,7 +43,7 @@ func TestGenerationActivityLogWithConfigAndExtraBlocks(t *testing.T) { assert.Nil(t, fileErr) extraBlock, err := lwgenerate.HclCreateGenericBlock("variable", []string{"var_name"}, nil) assert.NoError(t, err) - hcl, err := azure.NewTerraform(true, true, false, true, + hcl, err := azure.NewTerraform(true, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithExtraBlocks([]*hclwrite.Block{extraBlock}), ).Generate() @@ -55,7 +55,7 @@ func TestGenerationActivityLogWithConfigAndExtraBlocks(t *testing.T) { func TestGenerationActivityLogWithConfigAndExtraAzureRMProviderBlocks(t *testing.T) { var ActivityLogWithConfig, fileErr = getFileContent("test-data/activity_log_with_config_provider_args.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, true, false, true, + hcl, err := azure.NewTerraform(true, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithExtraAZRMArguments(map[string]interface{}{"foo": "bar"}), ).Generate() @@ -67,7 +67,7 @@ func TestGenerationActivityLogWithConfigAndExtraAzureRMProviderBlocks(t *testing func TestGenerationActivityLogWithConfigAndExtraAZUReadProviderBlocks(t *testing.T) { var ActivityLogWithConfig, fileErr = getFileContent("test-data/activity_log_with_config_azureadprovider_args.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, true, false, true, + hcl, err := azure.NewTerraform(true, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithExtraAZReadArguments(map[string]interface{}{"foo": "bar"}), ).Generate() @@ -81,7 +81,7 @@ func TestGenerationActivityLogWithConfigAndCustomBackendBlock(t *testing.T) { assert.NoError(t, err) var ActivityLogWithConfig, fileErr = getFileContent("test-data/activity_log_with_config_root_blocks.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, true, false, true, + hcl, err := azure.NewTerraform(true, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithExtraRootBlocks([]*hclwrite.Block{customBlock}), ).Generate() @@ -93,14 +93,14 @@ func TestGenerationActivityLogWithConfigAndCustomBackendBlock(t *testing.T) { func TestGenerationConfigWithoutActivityLog(t *testing.T) { ConfigWithoutActivityLog, fileErr := getFileContent("test-data/config_without_activity_log.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, false, false, true, azure.WithSubscriptionID("test-subscription")).Generate() + hcl, err := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID("test-subscription")).Generate() assert.Nil(t, err) assert.NotNil(t, hcl) assert.Equal(t, ConfigWithoutActivityLog, hcl) } func TestGenerationWithoutActivityLogOrConfig(t *testing.T) { - hcl, err := azure.NewTerraform(false, false, false, true).Generate() + hcl, err := azure.NewTerraform(false, false, false, false, true).Generate() assert.NotNil(t, err) assert.True(t, strings.Contains(errors.Unwrap(err).Error(), "invalid inputs")) assert.Empty(t, hcl) @@ -108,7 +108,7 @@ func TestGenerationWithoutActivityLogOrConfig(t *testing.T) { func TestGenerationRenamedConfig(t *testing.T) { RenamedConfig, fileErr := getFileContent("test-data/renamed_config.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, false, false, true, + hcl, err := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithConfigIntegrationName("Test Config Rename"), ).Generate() @@ -120,7 +120,7 @@ func TestGenerationRenamedConfig(t *testing.T) { func TestGenerationRenamedActivityLog(t *testing.T) { RenamedActivityLog, fileErr := getFileContent("test-data/renamed_activity_log.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(false, true, false, true, + hcl, err := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithActivityLogIntegrationName("Test Activity Log Rename"), ).Generate() @@ -132,7 +132,7 @@ func TestGenerationRenamedActivityLog(t *testing.T) { func TestGenerationRenamedConfigAndActivityLog(t *testing.T) { RenamedConfigAndActivityLog, fileErr := getFileContent("test-data/renamed_config_and_activity_log.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, true, false, true, + hcl, err := azure.NewTerraform(true, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithConfigIntegrationName("Test Config Rename"), azure.WithActivityLogIntegrationName("Test Activity Log Rename"), @@ -143,7 +143,7 @@ func TestGenerationRenamedConfigAndActivityLog(t *testing.T) { } func TestGenerationNoActiveDirectorySettings(t *testing.T) { - hcl, err := azure.NewTerraform(true, true, false, false, + hcl, err := azure.NewTerraform(true, true, false, false, false, azure.WithSubscriptionID("test-subscription"), azure.WithConfigIntegrationName("Test Config Rename"), azure.WithActivityLogIntegrationName("Test Activity Log Rename"), @@ -155,7 +155,7 @@ func TestGenerationNoActiveDirectorySettings(t *testing.T) { func TestGenerationCustomActiveDirectory(t *testing.T) { CustomADDetails, fileErr := getFileContent("test-data/customer-ad-details.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, true, false, false, + hcl, err := azure.NewTerraform(true, true, false, false, false, azure.WithSubscriptionID("test-subscription"), azure.WithConfigIntegrationName("Test Config Rename"), azure.WithActivityLogIntegrationName("Test Activity Log Rename"), @@ -171,7 +171,7 @@ func TestGenerationCustomActiveDirectory(t *testing.T) { func TestGenerationActivityLogWithExistingStorageAccount(t *testing.T) { ActivityLogWithStorage, fileErr := getFileContent("test-data/activity-log-with-existing-storage.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(false, true, false, true, + hcl, err := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithExistingStorageAccount(true), azure.WithStorageAccountName("Test-Storage-Account-Name"), @@ -185,7 +185,7 @@ func TestGenerationActivityLogWithExistingStorageAccount(t *testing.T) { func TestGenerationActivityLogWithAllSubscriptions(t *testing.T) { ActivityLogAllSubs, fileErr := getFileContent("test-data/activity-log-with-all-subscriptions.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(false, true, false, true, + hcl, err := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithAllSubscriptions(true), ).Generate() @@ -197,7 +197,7 @@ func TestGenerationActivityLogWithAllSubscriptions(t *testing.T) { func TestGenerationConfigWithAllSubscriptions(t *testing.T) { ConfigAllSubs, fileErr := getFileContent("test-data/config-with-all-subscriptions.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, false, false, true, + hcl, err := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithAllSubscriptions(true), ).Generate() @@ -209,7 +209,7 @@ func TestGenerationConfigWithAllSubscriptions(t *testing.T) { func TestGenerationConfigWithManagementGroup(t *testing.T) { ConfigWithMgmtGroup, fileErr := getFileContent("test-data/config-with-management-group.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, false, false, true, + hcl, err := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithManagementGroup(true), azure.WithManagementGroupId("test-management-group-1"), @@ -220,7 +220,7 @@ func TestGenerationConfigWithManagementGroup(t *testing.T) { } func TestGenerationConfigWithManagementGroupError(t *testing.T) { - hcl, err := azure.NewTerraform(true, false, false, true, + hcl, err := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithManagementGroup(true), ).Generate() @@ -233,7 +233,7 @@ func TestGenerationActivityLogWithSubscriptionsList(t *testing.T) { ActivityLogWithSubscriptions, fileErr := getFileContent("test-data/activity-log-with-list-subscriptions.tf") assert.Nil(t, fileErr) testIds := []string{"test-id-1", "test-id-2", "test-id-3"} - hcl, err := azure.NewTerraform(false, true, false, true, + hcl, err := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithSubscriptionIds(testIds), ).Generate() @@ -246,7 +246,7 @@ func TestGenerationConfigWithSubscriptionsList(t *testing.T) { ConfigWithSubscriptions, fileErr := getFileContent("test-data/config-log-with-list-subscriptions.tf") assert.Nil(t, fileErr) testIds := []string{"test-id-1", "test-id-2", "test-id-3"} - hcl, err := azure.NewTerraform(true, false, false, true, + hcl, err := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithSubscriptionIds(testIds), ).Generate() @@ -258,7 +258,7 @@ func TestGenerationConfigWithSubscriptionsList(t *testing.T) { func TestGenerationLocation(t *testing.T) { ActivityLogLocation, fileErr := getFileContent("test-data/activity-log-with-location.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(false, true, false, true, + hcl, err := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithStorageLocation("West US 2"), ).Generate() @@ -271,7 +271,7 @@ func TestGenerationWithLaceworkProvider(t *testing.T) { laceworkProfile, fileErr := getFileContent("test-data/activity-log-with-lacework-profile.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(false, true, false, true, + hcl, err := azure.NewTerraform(false, true, false, false, true, azure.WithSubscriptionID("test-subscription"), azure.WithLaceworkProfile("test-profile"), ).Generate() @@ -284,7 +284,7 @@ func TestGenerationAzureRmProviderWithSubscriptionID(t *testing.T) { configWithSubscription, fileErr := getFileContent("test-data/config-with-azurerm-subscription.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(true, false, false, true, azure.WithSubscriptionID("test-subscription")).Generate() + hcl, err := azure.NewTerraform(true, false, false, false, true, azure.WithSubscriptionID("test-subscription")).Generate() assert.Nil(t, err) assert.NotNil(t, hcl) assert.Equal(t, configWithSubscription, hcl) @@ -293,7 +293,7 @@ func TestGenerationAzureRmProviderWithSubscriptionID(t *testing.T) { func TestGenerationEntraIDActivityLog(t *testing.T) { ActivityLogEntraID, fileErr := getFileContent("test-data/entra-id-activity-log-no-custom-input.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(false, false, true, true, azure.WithSubscriptionID("test-subscription")).Generate() + hcl, err := azure.NewTerraform(false, false, false, true, true, azure.WithSubscriptionID("test-subscription")).Generate() assert.Nil(t, err) assert.NotNil(t, hcl) assert.Equal(t, ActivityLogEntraID, hcl) @@ -302,7 +302,7 @@ func TestGenerationEntraIDActivityLog(t *testing.T) { func TestGenerationEntraIDActivityLogExistingActiveDirectoryApp(t *testing.T) { ActivityLogEntraID, fileErr := getFileContent("test-data/entra-id-activity-log-existing-ad-app.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(false, false, true, false, + hcl, err := azure.NewTerraform(false, false, false, true, false, azure.WithSubscriptionID("test-subscription"), azure.WithAdApplicationId("testID"), azure.WithAdApplicationPassword("pass"), @@ -316,7 +316,7 @@ func TestGenerationEntraIDActivityLogExistingActiveDirectoryApp(t *testing.T) { func TestGenerationEntraIDActivityLogEventHubLocationAndPartition(t *testing.T) { ActivityLogEntraID, fileErr := getFileContent("test-data/entra-id-activity-log-event-hub-location-and-partition.tf") assert.Nil(t, fileErr) - hcl, err := azure.NewTerraform(false, false, true, true, + hcl, err := azure.NewTerraform(false, false, false, true, true, azure.WithSubscriptionID("test-subscription"), azure.WithEventHubLocation("West US 2"), azure.WithEventHubPartitionCount(2), @@ -325,3 +325,19 @@ func TestGenerationEntraIDActivityLogEventHubLocationAndPartition(t *testing.T) assert.NotNil(t, hcl) assert.Equal(t, ActivityLogEntraID, hcl) } + +// Agentless Scanning Tests +func TestGenerationAgentlessScanning(t *testing.T) { + AgentlessScanning, fileErr := getFileContent("test-data/agentless-scanning-only.tf") + assert.Nil(t, fileErr) + hcl, err := azure.NewTerraform(false, false, true, false, false, + azure.WithSubscriptionID("11111111-2222-3333-4444-111111111111"), + azure.WithIntegrationLevel("SUBSCRIPTION"), + azure.WithRegions([]string{"West US"}), + azure.WithGlobal(true), + azure.WithAgentlessSubscriptionIds([]string{"11111111-2222-3333-4444-111111111111"}), + ).Generate() + assert.Nil(t, err) + assert.NotNil(t, hcl) + assert.Equal(t, AgentlessScanning, hcl) +} diff --git a/lwgenerate/azure/test-data/agentless-scanning-only.tf b/lwgenerate/azure/test-data/agentless-scanning-only.tf new file mode 100644 index 000000000..c37f85431 --- /dev/null +++ b/lwgenerate/azure/test-data/agentless-scanning-only.tf @@ -0,0 +1,28 @@ +terraform { + required_providers { + lacework = { + source = "lacework/lacework" + version = "~> 2.0" + } + } +} + +provider "azuread" { +} + +provider "azurerm" { + subscription_id = "11111111-2222-3333-4444-111111111111" + features { + } +} + +module "lacework_azure_agentless_scanning_subscription_west_us" { + source = "lacework/agentless-scanning/azure" + version = "~> 1.5" + create_log_analytics_workspace = false + global = true + included_subscriptions = ["/subscriptions/11111111-2222-3333-4444-111111111111"] + integration_level = "SUBSCRIPTION" + region = "West US" + scanning_subscription_id = "11111111-2222-3333-4444-111111111111" +} diff --git a/lwgenerate/constants.go b/lwgenerate/constants.go index 1eaf3212a..c0ea24a54 100644 --- a/lwgenerate/constants.go +++ b/lwgenerate/constants.go @@ -20,7 +20,7 @@ const ( LWAzureConfigSource = "lacework/config/azure" LWAzureConfigVersion = "~> 3.0" - LWAzureAgentlessSource = "lacework/agentless-scanning/azure" + LWAzureAgentlessSource = "lacework/agentless-scanning/azure" LWAzureAgentlessVersion = "~> 1.5" LWAzureActivityLogSource = "lacework/activity-log/azure" LWAzureActivityLogVersion = "~> 3.0"