Skip to content

Commit 7375d7b

Browse files
feat: add Azure Agentless support in generate command (#1741)
* feat: add azure Agentless support in generate command (cli)
1 parent 6ed2d45 commit 7375d7b

13 files changed

+782
-108
lines changed

cli/cmd/generate_azure.go

Lines changed: 258 additions & 55 deletions
Large diffs are not rendered by default.

cli/cmd/generate_azure_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@ func TestMissingValidEntity(t *testing.T) {
3939
data := azure.GenerateAzureTfConfigurationArgs{}
4040
data.Config = false
4141
data.ActivityLog = false
42+
data.Agentless = false
4243

4344
err := promptAzureGenerate(&data, &AzureGenerateCommandExtraState{Output: "/tmp"})
4445
assert.Error(t, err)
45-
assert.Equal(t, "must enable at least one of: Configuration or Activity Log integration", err.Error())
46+
assert.Equal(t, "must enable at least one of: Configuration, Agentless or Activity Log integrations", err.Error())
4647
}
4748

4849
func TestValidStorageLocations(t *testing.T) {

cli/docs/lacework_generate_cloud-account_azure.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,26 @@ lacework generate cloud-account azure [flags]
4040
--ad_id string existing active directory application id
4141
--ad_pass string existing active directory application password
4242
--ad_pid string existing active directory application service principle id
43+
--agentless enable agentless integration
44+
--agentless_subscription_ids strings comma-separated list of subscription IDs for Agentless scanning (e.g., 'sub1,sub2,sub3')
4345
--all_subscriptions subscription ids grant read access to ALL subscriptions within Tenant (overrides subscription ids)
4446
--apply run terraform apply for the generated hcl
4547
--configuration enable configuration integration
4648
--configuration_name string specify a custom configuration integration name
49+
--create_log_analytics_workspace enable creation of Log Analytics Workspace for agentless scanning
4750
--entra_id_activity_log enable Entra ID activity log integration
4851
--entra_id_activity_log_integration_name string specify a custom Entra ID activity log integration name
4952
--event_hub_location string specify the location where the Event Hub for logging will reside
5053
--event_hub_partition_count int specify the number of partitions for the Event Hub (default 1)
5154
--existing_storage use existing storage account
55+
--global enable global agentless scanning
5256
-h, --help help for azure
57+
--integration_level string specify the agentless integration level (e.g., 'SUBSCRIPTION', 'TENANT')
5358
--location string specify azure region where storage account logging resides
5459
--management_group management group level integration
5560
--management_group_id string specify management group id. Required if mgmt_group provided
5661
--output string location to write generated content (default is ~/lacework/azure)
62+
--regions strings comma-separated list of Azure regions for agentless scanning (e.g., 'East US,West US')
5763
--storage_account_name string specify storage account name
5864
--storage_resource_group string specify storage resource group
5965
--subscription_id string specify the Azure Subscription ID to be used to provision Lacework resources

integration/azure_generation_test.go

Lines changed: 132 additions & 17 deletions
Large diffs are not rendered by default.

integration/framework_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ func TestMain(m *testing.M) {
491491
func terraformInstall() {
492492
installer := &releases.ExactVersion{
493493
Product: product.Terraform,
494-
Version: version.Must(version.NewVersion("1.5.0")),
494+
Version: version.Must(version.NewVersion("1.9.0")),
495495
}
496496

497497
_execPath, err := installer.Install(context.Background())

integration/test_resources/help/generate_cloud-account_azure

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,26 @@ Flags:
2727
--ad_id string existing active directory application id
2828
--ad_pass string existing active directory application password
2929
--ad_pid string existing active directory application service principle id
30+
--agentless enable agentless integration
31+
--agentless_subscription_ids strings comma-separated list of subscription IDs for Agentless scanning (e.g., 'sub1,sub2,sub3')
3032
--all_subscriptions subscription ids grant read access to ALL subscriptions within Tenant (overrides subscription ids)
3133
--apply run terraform apply for the generated hcl
3234
--configuration enable configuration integration
3335
--configuration_name string specify a custom configuration integration name
36+
--create_log_analytics_workspace enable creation of Log Analytics Workspace for agentless scanning
3437
--entra_id_activity_log enable Entra ID activity log integration
3538
--entra_id_activity_log_integration_name string specify a custom Entra ID activity log integration name
3639
--event_hub_location string specify the location where the Event Hub for logging will reside
3740
--event_hub_partition_count int specify the number of partitions for the Event Hub (default 1)
3841
--existing_storage use existing storage account
42+
--global enable global agentless scanning
3943
-h, --help help for azure
44+
--integration_level string specify the agentless integration level (e.g., 'SUBSCRIPTION', 'TENANT')
4045
--location string specify azure region where storage account logging resides
4146
--management_group management group level integration
4247
--management_group_id string specify management group id. Required if mgmt_group provided
4348
--output string location to write generated content (default is ~/lacework/azure)
49+
--regions strings comma-separated list of Azure regions for agentless scanning (e.g., 'East US,West US')
4450
--storage_account_name string specify storage account name
4551
--storage_resource_group string specify storage resource group
4652
--subscription_id string specify the Azure Subscription ID to be used to provision Lacework resources

lwgenerate/azure/azure.go

Lines changed: 158 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
package azure
33

44
import (
5+
"fmt"
6+
"strings"
7+
58
"github.com/hashicorp/hcl/v2/hclwrite"
69
"github.com/lacework/go-sdk/v2/lwgenerate"
710
"github.com/pkg/errors"
@@ -14,6 +17,9 @@ type GenerateAzureTfConfigurationArgs struct {
1417
// Should we add Config integration in LW?
1518
Config bool
1619

20+
// Should we add Agentless integration in LW?
21+
Agentless bool
22+
1723
// Should we create an Entra ID integration in LW?
1824
EntraIdActivityLog bool
1925

@@ -88,23 +94,40 @@ type GenerateAzureTfConfigurationArgs struct {
8894

8995
// Custom outputs
9096
CustomOutputs []lwgenerate.HclOutput
97+
98+
// Integration level for agentless scanning (e.g., "SUBSCRIPTION", "TENANT")
99+
IntegrationLevel string
100+
101+
// Should agentless scanning be global?
102+
Global bool
103+
104+
// Should we create a Log Analytics Workspace for agentless scanning?
105+
CreateLogAnalyticsWorkspace bool
106+
107+
// List of regions to deploy for agentless scanning
108+
Regions []string
109+
110+
// List of subscription IDs for agentless scanning
111+
AgentlessSubscriptionIds []string
91112
}
92113

93114
// Ensure all combinations of inputs are valid for supported spec
94115
func (args *GenerateAzureTfConfigurationArgs) validate() error {
95-
// Validate one of config or activity log was enabled; otherwise error out
96-
if !args.ActivityLog && !args.Config && !args.EntraIdActivityLog {
97-
return errors.New("audit log or config integration must be enabled")
116+
// Validate one of config, agentless or activity log was enabled; otherwise error out
117+
if !args.ActivityLog && !args.Agentless && !args.Config && !args.EntraIdActivityLog {
118+
return errors.New("audit log, agentless or config integration must be enabled")
98119
}
99120

100-
if (args.ActivityLog || args.Config || args.EntraIdActivityLog) && args.SubscriptionID == "" {
121+
if (args.ActivityLog || args.Agentless || args.Config || args.EntraIdActivityLog) && args.SubscriptionID == "" {
101122
return errors.New("subscription_id must be provided")
102123
}
103124

104125
// Validate that active directory settings are correct
105-
if !args.CreateAdIntegration && (args.AdApplicationId == "" ||
106-
args.AdServicePrincipalId == "" || args.AdApplicationPassword == "") {
107-
return errors.New("Active directory details must be set")
126+
if !args.CreateAdIntegration && (args.Config || args.ActivityLog || args.EntraIdActivityLog) {
127+
if args.AdApplicationId == "" ||
128+
args.AdServicePrincipalId == "" || args.AdApplicationPassword == "" {
129+
return errors.New("Active directory details must be set")
130+
}
108131
}
109132

110133
// Validate the Mangement Group
@@ -117,6 +140,16 @@ func (args *GenerateAzureTfConfigurationArgs) validate() error {
117140
return errors.New("When using existing storage account, storage account details must be configured")
118141
}
119142

143+
// Validate Agentless Scanning
144+
if args.Agentless {
145+
if args.IntegrationLevel == "" {
146+
return errors.New("integration_level must be set for Agentless Integration")
147+
}
148+
if args.IntegrationLevel == "SUBSCRIPTION" && len(args.AgentlessSubscriptionIds) == 0 {
149+
return errors.New("subscription_ids must be provided for Agentless Integration with SUBSCRIPTION integration level")
150+
}
151+
}
152+
120153
return nil
121154
}
122155

@@ -127,12 +160,13 @@ type AzureTerraformModifier func(c *GenerateAzureTfConfigurationArgs)
127160
//
128161
// Note: Additional configuration details may be set using modifiers of the AzureTerraformModifier type
129162
func NewTerraform(
130-
enableConfig bool, enableActivityLog bool, enableEntraIdActivityLog, createAdIntegration bool,
163+
enableConfig bool, enableActivityLog bool, enableAgentless bool, enableEntraIdActivityLog, createAdIntegration bool,
131164
mods ...AzureTerraformModifier,
132165
) *GenerateAzureTfConfigurationArgs {
133166
config := &GenerateAzureTfConfigurationArgs{
134167
ActivityLog: enableActivityLog,
135168
Config: enableConfig,
169+
Agentless: enableAgentless,
136170
EntraIdActivityLog: enableEntraIdActivityLog,
137171
CreateAdIntegration: createAdIntegration,
138172
}
@@ -245,6 +279,19 @@ func WithSubscriptionIds(subscriptionIds []string) AzureTerraformModifier {
245279
}
246280
}
247281

282+
// WithAgentlessSubscriptionIds List of subscriptions for agentless scanning.
283+
func WithAgentlessSubscriptionIds(agentlessSubscriptionIds []string) AzureTerraformModifier {
284+
return func(c *GenerateAzureTfConfigurationArgs) {
285+
c.AgentlessSubscriptionIds = agentlessSubscriptionIds
286+
}
287+
}
288+
289+
func WithRegions(regions []string) AzureTerraformModifier {
290+
return func(c *GenerateAzureTfConfigurationArgs) {
291+
c.Regions = regions
292+
}
293+
}
294+
248295
// WithAllSubscriptions Grant read access to ALL subscriptions within
249296
// the selected Tenant (overrides 'subscription_ids')
250297
func WithAllSubscriptions(allSubscriptions bool) AzureTerraformModifier {
@@ -307,6 +354,27 @@ func WithSubscriptionID(subcriptionID string) AzureTerraformModifier {
307354
}
308355
}
309356

357+
// WithGlobal sets the Global field for agentless scanning
358+
func WithGlobal(global bool) AzureTerraformModifier {
359+
return func(c *GenerateAzureTfConfigurationArgs) {
360+
c.Global = global
361+
}
362+
}
363+
364+
// WithCreateLogAnalyticsWorkspace sets the CreateLogAnalyticsWorkspace field for agentless scanning
365+
func WithCreateLogAnalyticsWorkspace(create bool) AzureTerraformModifier {
366+
return func(c *GenerateAzureTfConfigurationArgs) {
367+
c.CreateLogAnalyticsWorkspace = create
368+
}
369+
}
370+
371+
// WithIntegrationLevel sets the IntegrationLevel field for agentless scanning
372+
func WithIntegrationLevel(level string) AzureTerraformModifier {
373+
return func(c *GenerateAzureTfConfigurationArgs) {
374+
c.IntegrationLevel = level
375+
}
376+
}
377+
310378
// Generate new Terraform code based on the supplied args.
311379
func (args *GenerateAzureTfConfigurationArgs) Generate() (string, error) {
312380
// Validate inputs
@@ -350,6 +418,11 @@ func (args *GenerateAzureTfConfigurationArgs) Generate() (string, error) {
350418
return "", errors.Wrap(err, "failed to generate azure activity log module")
351419
}
352420

421+
agentlessLogModule, err := createAgentless(args)
422+
if err != nil {
423+
return "", errors.Wrap(err, "failed to generate azure agentless module")
424+
}
425+
353426
entraIdActivityLogModule, err := createEntraIdActivityLog(args)
354427
if err != nil {
355428
return "", errors.Wrap(err, "failed to generate azure Entra ID activity log module")
@@ -374,6 +447,7 @@ func (args *GenerateAzureTfConfigurationArgs) Generate() (string, error) {
374447
laceworkADProvider,
375448
configModule,
376449
activityLogModule,
450+
agentlessLogModule,
377451
entraIdActivityLogModule,
378452
outputBlocks,
379453
args.ExtraBlocks),
@@ -622,6 +696,82 @@ func createActivityLog(args *GenerateAzureTfConfigurationArgs) ([]*hclwrite.Bloc
622696
return blocks, nil
623697
}
624698

699+
func createAgentless(args *GenerateAzureTfConfigurationArgs) ([]*hclwrite.Block, error) {
700+
blocks := []*hclwrite.Block{}
701+
702+
if !args.Agentless {
703+
return blocks, nil
704+
}
705+
706+
// Helper function to format region names for module naming
707+
formatRegionForModuleName := func(region string) string {
708+
return strings.ToLower(strings.ReplaceAll(region, " ", "_"))
709+
}
710+
711+
// Determine regions to process
712+
regions := args.Regions
713+
if len(regions) == 0 {
714+
regions = []string{"West US"} // Default to West US if no regions specified
715+
}
716+
717+
isTenant := strings.EqualFold(args.IntegrationLevel, "TENANT")
718+
isSubscription := strings.EqualFold(args.IntegrationLevel, "SUBSCRIPTION")
719+
720+
var firstModuleName string
721+
for i, region := range regions {
722+
isFirstRegion := (i == 0)
723+
724+
// Build module name
725+
var moduleName string
726+
if isTenant {
727+
moduleName = fmt.Sprintf("lacework_azure_agentless_scanning_tenant_%s", formatRegionForModuleName(region))
728+
} else {
729+
moduleName = fmt.Sprintf("lacework_azure_agentless_scanning_subscription_%s", formatRegionForModuleName(region))
730+
}
731+
732+
// Build attributes
733+
attrs := map[string]interface{}{
734+
"integration_level": args.IntegrationLevel,
735+
"region": region,
736+
"global": args.Global && isFirstRegion,
737+
"create_log_analytics_workspace": args.CreateLogAnalyticsWorkspace,
738+
}
739+
740+
// set the first module name for global reference
741+
if isFirstRegion {
742+
firstModuleName = moduleName
743+
} else {
744+
attrs["global_module_reference"] = lwgenerate.CreateSimpleTraversal([]string{"module", firstModuleName})
745+
}
746+
747+
if args.SubscriptionID != "" {
748+
attrs["scanning_subscription_id"] = args.SubscriptionID
749+
}
750+
if isSubscription && isFirstRegion && len(args.AgentlessSubscriptionIds) > 0 {
751+
subs := make([]string, len(args.AgentlessSubscriptionIds))
752+
for j, id := range args.AgentlessSubscriptionIds {
753+
subs[j] = fmt.Sprintf("/subscriptions/%s", id)
754+
}
755+
attrs["included_subscriptions"] = subs
756+
}
757+
758+
// Create module details
759+
moduleDetails := []lwgenerate.HclModuleModifier{
760+
lwgenerate.HclModuleWithAttributes(attrs),
761+
}
762+
block, err := lwgenerate.NewModule(
763+
moduleName,
764+
lwgenerate.LWAzureAgentlessSource,
765+
append(moduleDetails, lwgenerate.HclModuleWithVersion(lwgenerate.LWAzureAgentlessVersion))...,
766+
).ToBlock()
767+
if err != nil {
768+
return nil, err
769+
}
770+
blocks = append(blocks, block)
771+
}
772+
return blocks, nil
773+
}
774+
625775
func createEntraIdActivityLog(args *GenerateAzureTfConfigurationArgs) ([]*hclwrite.Block, error) {
626776
blocks := []*hclwrite.Block{}
627777
if args.EntraIdActivityLog {

0 commit comments

Comments
 (0)