diff --git a/cli/cmd/generate_azure.go b/cli/cmd/generate_azure.go index da7d0ac4b..16571a511 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,15 +17,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?" @@ -196,8 +199,19 @@ the new cloud account. In interactive mode, this command will: azure.WithEventHubPartitionCount(GenerateAzureCommandState.EventHubPartitionCount), } + if GenerateAzureCommandState.Agentless { + 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 { + 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)) @@ -225,6 +239,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 +406,30 @@ func initGenerateAzureTfCommandFlags() { "", "specify a custom activity log integration name") + generateAzureTfCommand.PersistentFlags().BoolVar( + &GenerateAzureCommandState.Agentless, + "agentless", + 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", @@ -756,6 +795,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,27 +834,33 @@ 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 - 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 { + 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 @@ -890,3 +954,100 @@ 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 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 (default: false) + if err := SurveyMultipleQuestionWithValidation([]SurveyQuestionWithValidationArgs{ + { + Icon: IconAzureAgentless, + 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 +} 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..af7413d7b 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 --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 db4498441..9de52e4c3 100644 --- a/lwgenerate/azure/azure.go +++ b/lwgenerate/azure/azure.go @@ -2,6 +2,9 @@ package azure import ( + "fmt" + "strings" + "github.com/hashicorp/hcl/v2/hclwrite" "github.com/lacework/go-sdk/v2/lwgenerate" "github.com/pkg/errors" @@ -14,6 +17,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 @@ -88,23 +94,40 @@ 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 + + // 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 - if !args.ActivityLog && !args.Config && !args.EntraIdActivityLog { - return errors.New("audit log or config integration must be enabled") + // 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.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 @@ -117,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 } @@ -127,12 +160,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, } @@ -245,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 { @@ -307,6 +354,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 @@ -350,6 +418,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 +447,7 @@ func (args *GenerateAzureTfConfigurationArgs) Generate() (string, error) { laceworkADProvider, configModule, activityLogModule, + agentlessLogModule, entraIdActivityLogModule, outputBlocks, args.ExtraBlocks), @@ -622,6 +696,84 @@ func createActivityLog(args *GenerateAzureTfConfigurationArgs) ([]*hclwrite.Bloc return blocks, nil } +func createAgentless(args *GenerateAzureTfConfigurationArgs) ([]*hclwrite.Block, error) { + blocks := []*hclwrite.Block{} + + 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 { + moduleName = fmt.Sprintf("lacework_azure_agentless_scanning_subscription_%s", formatRegionForModuleName(region)) + } + + if isFirstRegion { + firstModuleName = moduleName + } + + // 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}) + } + + // 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, block) + } + return blocks, nil +} + func createEntraIdActivityLog(args *GenerateAzureTfConfigurationArgs) ([]*hclwrite.Block, error) { blocks := []*hclwrite.Block{} if args.EntraIdActivityLog { 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 15702d13a..c0ea24a54 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"