diff --git a/docs/stackit_beta.md b/docs/stackit_beta.md index 7046745ce..cf35a0536 100644 --- a/docs/stackit_beta.md +++ b/docs/stackit_beta.md @@ -42,6 +42,7 @@ stackit beta [flags] * [stackit](./stackit.md) - Manage STACKIT resources using the command line * [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers +* [stackit beta alb-waf](./stackit_beta_alb-waf.md) - Manages the Web Application Firewall (WAF) for application loadbalancers * [stackit beta cdn](./stackit_beta_cdn.md) - Manage CDN resources * [stackit beta edge-cloud](./stackit_beta_edge-cloud.md) - Provides functionality for Edge Cloud services. * [stackit beta intake](./stackit_beta_intake.md) - Provides functionality for intake diff --git a/docs/stackit_beta_alb-waf.md b/docs/stackit_beta_alb-waf.md new file mode 100644 index 000000000..1f1025506 --- /dev/null +++ b/docs/stackit_beta_alb-waf.md @@ -0,0 +1,34 @@ +## stackit beta alb-waf + +Manages the Web Application Firewall (WAF) for application loadbalancers + +### Synopsis + +Manage the lifecycle of Web Application Firewall (WAF) configurations for application loadbalancers. + +``` +stackit beta alb-waf [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta alb-waf" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands +* [stackit beta alb-waf managed-rule-set](./stackit_beta_alb-waf_managed-rule-set.md) - Provides functionality for managed rule sets of the ALB WAF + diff --git a/docs/stackit_beta_alb-waf_managed-rule-set.md b/docs/stackit_beta_alb-waf_managed-rule-set.md new file mode 100644 index 000000000..8ab911165 --- /dev/null +++ b/docs/stackit_beta_alb-waf_managed-rule-set.md @@ -0,0 +1,38 @@ +## stackit beta alb-waf managed-rule-set + +Provides functionality for managed rule sets of the ALB WAF + +### Synopsis + +Provides functionality for managed rule sets (MRS) of the Web Application Firewall (WAF) for application loadbalancers. + +``` +stackit beta alb-waf managed-rule-set [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta alb-waf managed-rule-set" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta alb-waf](./stackit_beta_alb-waf.md) - Manages the Web Application Firewall (WAF) for application loadbalancers +* [stackit beta alb-waf managed-rule-set create](./stackit_beta_alb-waf_managed-rule-set_create.md) - Creates a managed rule set for the ALB WAF +* [stackit beta alb-waf managed-rule-set delete](./stackit_beta_alb-waf_managed-rule-set_delete.md) - Deletes a managed rule set of the ALB WAF +* [stackit beta alb-waf managed-rule-set describe](./stackit_beta_alb-waf_managed-rule-set_describe.md) - Describes a managed rule set of the ALB WAF +* [stackit beta alb-waf managed-rule-set list](./stackit_beta_alb-waf_managed-rule-set_list.md) - Lists all managed rule sets of the ALB WAF +* [stackit beta alb-waf managed-rule-set update](./stackit_beta_alb-waf_managed-rule-set_update.md) - Updates a managed rule set of the ALB WAF + diff --git a/docs/stackit_beta_alb-waf_managed-rule-set_create.md b/docs/stackit_beta_alb-waf_managed-rule-set_create.md new file mode 100644 index 000000000..46747ee33 --- /dev/null +++ b/docs/stackit_beta_alb-waf_managed-rule-set_create.md @@ -0,0 +1,42 @@ +## stackit beta alb-waf managed-rule-set create + +Creates a managed rule set for the ALB WAF + +### Synopsis + +Creates a managed rule set (MRS) for the Web Application Firewall (WAF) of application loadbalancers. + +``` +stackit beta alb-waf managed-rule-set create [flags] +``` + +### Examples + +``` + Create a managed rule set with name "my-managed-rule-set" + $ stackit beta alb-waf managed-rule-set create --name my-managed-rule-set +``` + +### Options + +``` + -h, --help Help for "stackit beta alb-waf managed-rule-set create" + --name string Name of the managed rule set + --type string Managed rule set type (one of: [TYPE_OWASP_CRS]) (default "TYPE_OWASP_CRS") +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta alb-waf managed-rule-set](./stackit_beta_alb-waf_managed-rule-set.md) - Provides functionality for managed rule sets of the ALB WAF + diff --git a/docs/stackit_beta_alb-waf_managed-rule-set_delete.md b/docs/stackit_beta_alb-waf_managed-rule-set_delete.md new file mode 100644 index 000000000..13e79fa99 --- /dev/null +++ b/docs/stackit_beta_alb-waf_managed-rule-set_delete.md @@ -0,0 +1,40 @@ +## stackit beta alb-waf managed-rule-set delete + +Deletes a managed rule set of the ALB WAF + +### Synopsis + +Deletes a managed rule set (MRS) of the Web Application Firewall (WAF) for application loadbalancers. + +``` +stackit beta alb-waf managed-rule-set delete NAME [flags] +``` + +### Examples + +``` + Delete a managed rule set with name "my-managed-rule-set" + $ stackit beta alb-waf managed-rule-set delete my-managed-rule-set +``` + +### Options + +``` + -h, --help Help for "stackit beta alb-waf managed-rule-set delete" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta alb-waf managed-rule-set](./stackit_beta_alb-waf_managed-rule-set.md) - Provides functionality for managed rule sets of the ALB WAF + diff --git a/docs/stackit_beta_alb-waf_managed-rule-set_describe.md b/docs/stackit_beta_alb-waf_managed-rule-set_describe.md new file mode 100644 index 000000000..73e5d3719 --- /dev/null +++ b/docs/stackit_beta_alb-waf_managed-rule-set_describe.md @@ -0,0 +1,40 @@ +## stackit beta alb-waf managed-rule-set describe + +Describes a managed rule set of the ALB WAF + +### Synopsis + +Describes a managed rule set (MRS) of the Web Application Firewall (WAF) for application loadbalancers. + +``` +stackit beta alb-waf managed-rule-set describe NAME [flags] +``` + +### Examples + +``` + Get details about a managed rule set with name "my-managed-rule-set" + $ stackit beta alb-waf managed-rule-set describe my-managed-rule-set +``` + +### Options + +``` + -h, --help Help for "stackit beta alb-waf managed-rule-set describe" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta alb-waf managed-rule-set](./stackit_beta_alb-waf_managed-rule-set.md) - Provides functionality for managed rule sets of the ALB WAF + diff --git a/docs/stackit_beta_alb-waf_managed-rule-set_list.md b/docs/stackit_beta_alb-waf_managed-rule-set_list.md new file mode 100644 index 000000000..dbae1a486 --- /dev/null +++ b/docs/stackit_beta_alb-waf_managed-rule-set_list.md @@ -0,0 +1,47 @@ +## stackit beta alb-waf managed-rule-set list + +Lists all managed rule sets of the ALB WAF + +### Synopsis + +Lists all managed rule sets (MRS) of the Web Application Firewall (WAF) for application loadbalancers. + +``` +stackit beta alb-waf managed-rule-set list [flags] +``` + +### Examples + +``` + List all managed rule sets + $ stackit beta alb-waf managed-rule-set list + + List all managed rule sets in JSON format + $ stackit beta alb-waf managed-rule-set list --output-format json + + List up to 10 managed rule sets + $ stackit beta alb-waf managed-rule-set list --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit beta alb-waf managed-rule-set list" + --limit int Number of managed rule sets to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta alb-waf managed-rule-set](./stackit_beta_alb-waf_managed-rule-set.md) - Provides functionality for managed rule sets of the ALB WAF + diff --git a/docs/stackit_beta_alb-waf_managed-rule-set_update.md b/docs/stackit_beta_alb-waf_managed-rule-set_update.md new file mode 100644 index 000000000..82407ba26 --- /dev/null +++ b/docs/stackit_beta_alb-waf_managed-rule-set_update.md @@ -0,0 +1,41 @@ +## stackit beta alb-waf managed-rule-set update + +Updates a managed rule set of the ALB WAF + +### Synopsis + +Updates the rules of a managed rule set (MRS) of the Web Application Firewall (WAF) for application loadbalancers. Only the rules provided in the configuration file are updated, all other rules remain unchanged. + +``` +stackit beta alb-waf managed-rule-set update NAME [flags] +``` + +### Examples + +``` + Update the rules of a managed rule set with name "my-managed-rule-set" from a configuration file + $ stackit beta alb-waf managed-rule-set update my-managed-rule-set --configuration my-rules.json +``` + +### Options + +``` + -c, --configuration string Filename of the input configuration file + -h, --help Help for "stackit beta alb-waf managed-rule-set update" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta alb-waf managed-rule-set](./stackit_beta_alb-waf_managed-rule-set.md) - Provides functionality for managed rule sets of the ALB WAF + diff --git a/docs/stackit_config_set.md b/docs/stackit_config_set.md index ab3d79834..6d2d7d01b 100644 --- a/docs/stackit_config_set.md +++ b/docs/stackit_config_set.md @@ -29,6 +29,7 @@ stackit config set [flags] ### Options ``` + --alb-waf-custom-endpoint string ALB WAF API base URL, used in calls to this API --allowed-url-domain string Domain name, used for the verification of the URLs that are given in the custom identity provider endpoint and "STACKIT curl" command --authorization-custom-endpoint string Authorization API base URL, used in calls to this API --cdn-custom-endpoint string CDN API base URL, used in calls to this API diff --git a/docs/stackit_config_unset.md b/docs/stackit_config_unset.md index ea816220f..255fd741f 100644 --- a/docs/stackit_config_unset.md +++ b/docs/stackit_config_unset.md @@ -26,6 +26,7 @@ stackit config unset [flags] ### Options ``` + --alb-waf-custom-endpoint ALB WAF API base URL. If unset, uses the default base URL --allowed-url-domain Domain name, used for the verification of the URLs that are given in the IDP endpoint and curl commands. If unset, defaults to stackit.cloud --assume-yes If set, skips all confirmation prompts --async Configuration option to run commands asynchronously diff --git a/go.mod b/go.mod index e1f0dc5bb..f3a0ef181 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/spf13/viper v1.21.0 github.com/stackitcloud/stackit-sdk-go/core v0.26.0 github.com/stackitcloud/stackit-sdk-go/services/alb v0.14.2 + github.com/stackitcloud/stackit-sdk-go/services/albwaf v0.13.0 github.com/stackitcloud/stackit-sdk-go/services/authorization v0.15.2 github.com/stackitcloud/stackit-sdk-go/services/cdn v1.16.0 github.com/stackitcloud/stackit-sdk-go/services/dns v0.21.0 diff --git a/go.sum b/go.sum index a619b552d..53df865b0 100644 --- a/go.sum +++ b/go.sum @@ -594,6 +594,8 @@ github.com/stackitcloud/stackit-sdk-go/core v0.26.0 h1:jQEb9gkehfp6VCP6TcYk7BI10 github.com/stackitcloud/stackit-sdk-go/core v0.26.0/go.mod h1:WU1hhxnjXw2EV7CYa1nlEvNpMiRY6CvmIOaHuL3pOaA= github.com/stackitcloud/stackit-sdk-go/services/alb v0.14.2 h1:hGzfOJjlCRoFpri5eYIiwhE27qu02pKZLprKvbsTC/w= github.com/stackitcloud/stackit-sdk-go/services/alb v0.14.2/go.mod h1:eK6oRB5Tmpt6KbXQ4UYBGg2LgW5bPtVoncL9E8JSRww= +github.com/stackitcloud/stackit-sdk-go/services/albwaf v0.13.0 h1:uuQDV7Q3ndFJhlBbE2SX9ft+AtBJIck8rEcPr1lIqVE= +github.com/stackitcloud/stackit-sdk-go/services/albwaf v0.13.0/go.mod h1:4M9G1I64kZwlXO32ZoIpt0GAN4SpZ1SYerwCVVIBGoE= github.com/stackitcloud/stackit-sdk-go/services/authorization v0.15.2 h1:b7WJ/vwxlVmNNX91kI3obqGcuoPAyaCbDL5aCMQ/sNg= github.com/stackitcloud/stackit-sdk-go/services/authorization v0.15.2/go.mod h1:T/JF25XGJ3GqER/1L2N//DgY8x5tY7gA3N+/0nvmOWY= github.com/stackitcloud/stackit-sdk-go/services/cdn v1.16.0 h1:Wqxx0PDTL2F5gqI5jjznuJY0TdqECltjA0aa/rHY63U= diff --git a/internal/cmd/beta/alb-waf/alb-waf.go b/internal/cmd/beta/alb-waf/alb-waf.go new file mode 100644 index 000000000..0e89c719e --- /dev/null +++ b/internal/cmd/beta/alb-waf/alb-waf.go @@ -0,0 +1,29 @@ +package albwaf + +import ( + managedruleset "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb-waf/managed-rule-set" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "alb-waf", + Short: "Manages the Web Application Firewall (WAF) for application loadbalancers", + Long: "Manage the lifecycle of Web Application Firewall (WAF) configurations for application loadbalancers.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand( + managedruleset.NewCmd(params), + ) +} diff --git a/internal/cmd/beta/alb-waf/managed-rule-set/create/create.go b/internal/cmd/beta/alb-waf/managed-rule-set/create/create.go new file mode 100644 index 000000000..0e361798b --- /dev/null +++ b/internal/cmd/beta/alb-waf/managed-rule-set/create/create.go @@ -0,0 +1,130 @@ +package create + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/albwaf/client" + + "github.com/spf13/cobra" + albwaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" +) + +const ( + nameFlag = "name" +) + +var typeFlag = flags.StringEnumFlag( + "type", + albwaf.AllowedTypeEnumValues, + "Managed rule set type", + flags.StringEnumDefaultValue(albwaf.TYPE_TYPE_OWASP_CRS), +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Name string + Type albwaf.Type +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a managed rule set for the ALB WAF", + Long: "Creates a managed rule set (MRS) for the Web Application Firewall (WAF) of application loadbalancers.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a managed rule set with name "my-managed-rule-set"`, + "$ stackit beta alb-waf managed-rule-set create --name my-managed-rule-set"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to create a managed rule set for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create managed rule set: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(nameFlag, "", "Name of the managed rule set") + typeFlag.Register(cmd.Flags()) + + err := flags.MarkFlagsRequired(cmd, nameFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Name: flags.FlagToStringValue(p, cmd, nameFlag), + Type: typeFlag.Get(), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *albwaf.APIClient) albwaf.ApiCreateManagedRuleSetRequest { + req := apiClient.DefaultAPI.CreateManagedRuleSet(ctx, model.ProjectId, model.Region) + payload := albwaf.CreateManagedRuleSetPayload{ + Name: model.Name, + Type: model.Type, + } + return req.CreateManagedRuleSetPayload(payload) +} + +func outputResult(p *print.Printer, outputFormat, projectLabel string, resp *albwaf.GetManagedRuleSetResponse) error { + if resp == nil { + return fmt.Errorf("create managed rule set response is empty") + } + return p.OutputResult(outputFormat, resp, func() error { + p.Outputf("Created managed rule set %q for project %q.\n", resp.Name, projectLabel) + return nil + }) +} diff --git a/internal/cmd/beta/alb-waf/managed-rule-set/create/create_test.go b/internal/cmd/beta/alb-waf/managed-rule-set/create/create_test.go new file mode 100644 index 000000000..01b1ec44c --- /dev/null +++ b/internal/cmd/beta/alb-waf/managed-rule-set/create/create_test.go @@ -0,0 +1,226 @@ +package create + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + albwaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") + testClient = &albwaf.APIClient{DefaultAPI: &albwaf.DefaultAPIService{}} + testProjectId = uuid.NewString() + testRegion = "eu01" + testName = "my-managed-rule-set" +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + nameFlag: testName, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + Region: testRegion, + }, + Name: testName, + Type: albwaf.TYPE_TYPE_OWASP_CRS, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *albwaf.ApiCreateManagedRuleSetRequest)) albwaf.ApiCreateManagedRuleSetRequest { + request := testClient.DefaultAPI.CreateManagedRuleSet(testCtx, testProjectId, testRegion) + request = request.CreateManagedRuleSetPayload(albwaf.CreateManagedRuleSetPayload{ + Name: testName, + Type: albwaf.TYPE_TYPE_OWASP_CRS, + }) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "name missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, nameFlag) + }), + isValid: false, + }, + { + description: "invalid type", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[typeFlag.Name()] = "invalid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err = cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(params.Printer, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest albwaf.ApiCreateManagedRuleSetRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, albwaf.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + projectLabel string + resp *albwaf.GetManagedRuleSetResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "base", + args: args{ + projectLabel: "my-project", + resp: &albwaf.GetManagedRuleSetResponse{ + Name: testName, + Type: albwaf.TYPE_TYPE_OWASP_CRS, + Version: "v1.0.0", + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, "", tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/alb-waf/managed-rule-set/delete/delete.go b/internal/cmd/beta/alb-waf/managed-rule-set/delete/delete.go new file mode 100644 index 000000000..dc73f5689 --- /dev/null +++ b/internal/cmd/beta/alb-waf/managed-rule-set/delete/delete.go @@ -0,0 +1,95 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/albwaf/client" + + "github.com/spf13/cobra" + albwaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" +) + +const ( + nameArg = "NAME" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Name string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", nameArg), + Short: "Deletes a managed rule set of the ALB WAF", + Long: "Deletes a managed rule set (MRS) of the Web Application Firewall (WAF) for application loadbalancers.", + Args: args.SingleArg(nameArg, nil), + Example: examples.Build( + examples.NewExample( + `Delete a managed rule set with name "my-managed-rule-set"`, + "$ stackit beta alb-waf managed-rule-set delete my-managed-rule-set", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to delete the managed rule set %q for project %q?", model.Name, projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + _, err = req.Execute() + if err != nil { + return fmt.Errorf("delete managed rule set: %w", err) + } + + params.Printer.Outputf("Managed rule set %q deleted.\n", model.Name) + return nil + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + name := inputArgs[0] + model := inputModel{ + GlobalFlagModel: globalFlags, + Name: name, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *albwaf.APIClient) albwaf.ApiDeleteManagedRuleSetRequest { + return apiClient.DefaultAPI.DeleteManagedRuleSet(ctx, model.ProjectId, model.Region, model.Name) +} diff --git a/internal/cmd/beta/alb-waf/managed-rule-set/delete/delete_test.go b/internal/cmd/beta/alb-waf/managed-rule-set/delete/delete_test.go new file mode 100644 index 000000000..b171c5295 --- /dev/null +++ b/internal/cmd/beta/alb-waf/managed-rule-set/delete/delete_test.go @@ -0,0 +1,145 @@ +package delete + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + albwaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") + testProjectId = uuid.NewString() + testRegion = "eu01" + testClient = &albwaf.APIClient{DefaultAPI: &albwaf.DefaultAPIService{}} + testName = "my-managed-rule-set" +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testName, + } + for _, m := range mods { + m(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, m := range mods { + m(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + Region: testRegion, + }, + Name: testName, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *albwaf.ApiDeleteManagedRuleSetRequest)) albwaf.ApiDeleteManagedRuleSetRequest { + request := testClient.DefaultAPI.DeleteManagedRuleSet(testCtx, testProjectId, testRegion, testName) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argsValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argsValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argsValues: []string{}, + flagValues: map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + }, + isValid: false, + }, + { + description: "no arg values", + argsValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argsValues: fixtureArgValues(), + flagValues: map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + }, + isValid: true, + expectedModel: fixtureInputModel(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argsValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedResult albwaf.ApiDeleteManagedRuleSetRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedResult: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedResult, + cmp.AllowUnexported(tt.expectedResult, albwaf.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/alb-waf/managed-rule-set/describe/describe.go b/internal/cmd/beta/alb-waf/managed-rule-set/describe/describe.go new file mode 100644 index 000000000..030e83b80 --- /dev/null +++ b/internal/cmd/beta/alb-waf/managed-rule-set/describe/describe.go @@ -0,0 +1,135 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/albwaf/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" + albwaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" +) + +const ( + nameArg = "NAME" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Name string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", nameArg), + Short: "Describes a managed rule set of the ALB WAF", + Long: "Describes a managed rule set (MRS) of the Web Application Firewall (WAF) for application loadbalancers.", + Args: args.SingleArg(nameArg, nil), + Example: examples.Build( + examples.NewExample( + `Get details about a managed rule set with name "my-managed-rule-set"`, + "$ stackit beta alb-waf managed-rule-set describe my-managed-rule-set", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("read managed rule set: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + name := inputArgs[0] + model := inputModel{ + GlobalFlagModel: globalFlags, + Name: name, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *albwaf.APIClient) albwaf.ApiGetManagedRuleSetRequest { + return apiClient.DefaultAPI.GetManagedRuleSet(ctx, model.ProjectId, model.Region, model.Name) +} + +func outputResult(p *print.Printer, outputFormat string, resp *albwaf.GetManagedRuleSetResponse) error { + if resp == nil { + return fmt.Errorf("no managed rule set found") + } + return p.OutputResult(outputFormat, resp, func() error { + usageCount := "" + if resp.Usage != nil { + usageCount = utils.PtrString(resp.Usage.Count) + } + + content := []tables.Table{} + + table := tables.NewTable() + table.SetTitle("Managed Rule Set") + table.AddRow("NAME", resp.Name) + table.AddSeparator() + table.AddRow("TYPE", string(resp.Type)) + table.AddSeparator() + table.AddRow("VERSION", resp.Version) + table.AddSeparator() + table.AddRow("USAGE COUNT", usageCount) + content = append(content, table) + + if resp.Groups != nil && len(*resp.Groups) > 0 { + for _, groupId := range utils.SortedKeys(*resp.Groups) { + group := (*resp.Groups)[groupId] + if group.Rules == nil || len(*group.Rules) == 0 { + continue + } + groupTable := tables.NewTable() + groupTable.SetTitle(fmt.Sprintf("Rule Group: %s (%s)", groupId, group.GroupName)) + groupTable.SetHeader("DESCRIPTION", "RULE ID", "SEVERITY", "MODE") + for _, ruleId := range utils.SortedKeys(*group.Rules) { + rule := (*group.Rules)[ruleId] + groupTable.AddRow(utils.Truncate(new(rule.Description), 70), ruleId, rule.Severity, string(rule.Mode)) + groupTable.AddSeparator() + } + content = append(content, groupTable) + } + } + + err := tables.DisplayTables(p, content) + if err != nil { + return fmt.Errorf("render tables: %w", err) + } + if resp.Groups != nil && len(*resp.Groups) > 0 { + p.Outputln("\nUse --output-format json/yaml to see the untruncated rule descriptions and severity.") + } + return nil + }) +} diff --git a/internal/cmd/beta/alb-waf/managed-rule-set/describe/describe_test.go b/internal/cmd/beta/alb-waf/managed-rule-set/describe/describe_test.go new file mode 100644 index 000000000..f01d4c7d3 --- /dev/null +++ b/internal/cmd/beta/alb-waf/managed-rule-set/describe/describe_test.go @@ -0,0 +1,194 @@ +package describe + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + albwaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") + testProjectId = uuid.NewString() + testRegion = "eu01" + testClient = &albwaf.APIClient{DefaultAPI: &albwaf.DefaultAPIService{}} + testName = "my-managed-rule-set" +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testName, + } + for _, m := range mods { + m(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, m := range mods { + m(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + Region: testRegion, + }, + Name: testName, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *albwaf.ApiGetManagedRuleSetRequest)) albwaf.ApiGetManagedRuleSetRequest { + request := testClient.DefaultAPI.GetManagedRuleSet(testCtx, testProjectId, testRegion, testName) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest albwaf.ApiGetManagedRuleSetRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, albwaf.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + resp *albwaf.GetManagedRuleSetResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "base", + args: args{ + resp: &albwaf.GetManagedRuleSetResponse{ + Name: testName, + Type: albwaf.TYPE_TYPE_OWASP_CRS, + Version: "v1.0.0", + }, + }, + wantErr: false, + }, + { + name: "with groups", + args: args{ + resp: &albwaf.GetManagedRuleSetResponse{ + Name: testName, + Type: albwaf.TYPE_TYPE_OWASP_CRS, + Version: "v1.0.0", + Groups: &map[string]albwaf.MRSRuleGroup{ + "942": { + Description: "SQL Injection", + GroupName: "SQL Injection", + Rules: &map[string]albwaf.MRSRule{ + "942100": { + Description: "SQL Injection Attack Detected", + Mode: albwaf.MODE_MODE_ENABLED, + Severity: "CRITICAL", + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.resp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/alb-waf/managed-rule-set/list/list.go b/internal/cmd/beta/alb-waf/managed-rule-set/list/list.go new file mode 100644 index 000000000..3243c2851 --- /dev/null +++ b/internal/cmd/beta/alb-waf/managed-rule-set/list/list.go @@ -0,0 +1,171 @@ +package list + +import ( + "context" + "fmt" + "math" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/albwaf/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + + "github.com/spf13/cobra" + albwaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" +) + +const ( + limitFlag = "limit" + maxPageSize = int64(100) +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all managed rule sets of the ALB WAF", + Long: "Lists all managed rule sets (MRS) of the Web Application Firewall (WAF) for application loadbalancers.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all managed rule sets`, + "$ stackit beta alb-waf managed-rule-set list", + ), + examples.NewExample( + `List all managed rule sets in JSON format`, + "$ stackit beta alb-waf managed-rule-set list --output-format json", + ), + examples.NewExample( + `List up to 10 managed rule sets`, + "$ stackit beta alb-waf managed-rule-set list --limit 10", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + items, err := fetchManagedRuleSets(ctx, model, apiClient) + if err != nil { + return fmt.Errorf("list managed rule sets: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, items) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Number of managed rule sets to list") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *albwaf.APIClient, nextPageID string, pageLimit int64) albwaf.ApiListManagedRuleSetsRequest { + req := apiClient.DefaultAPI.ListManagedRuleSets(ctx, model.ProjectId, model.Region) + req = req.PageSize(fmt.Sprintf("%d", pageLimit)) + if nextPageID != "" { + req = req.PageId(nextPageID) + } + return req +} + +func fetchManagedRuleSets(ctx context.Context, model *inputModel, apiClient *albwaf.APIClient) ([]albwaf.GetLimitedManagedRuleSetResponse, error) { + var nextPageID string + var items []albwaf.GetLimitedManagedRuleSetResponse + received := int64(0) + limit := int64(math.MaxInt64) + if model.Limit != nil { + limit = min(limit, *model.Limit) + } + for { + want := min(maxPageSize, limit-received) + request := buildRequest(ctx, model, apiClient, nextPageID, want) + response, err := request.Execute() + if err != nil { + return nil, err + } + items = append(items, response.Items...) + nextPageID = "" + if response.NextPageId != nil { + nextPageID = *response.NextPageId + } + received += want + if nextPageID == "" || received >= limit { + break + } + } + return items, nil +} + +func outputResult(p *print.Printer, outputFormat string, items []albwaf.GetLimitedManagedRuleSetResponse) error { + return p.OutputResult(outputFormat, items, func() error { + if len(items) == 0 { + p.Outputln("No managed rule sets found") + return nil + } + + table := tables.NewTable() + table.SetHeader("NAME", "TYPE", "VERSION", "USAGE COUNT") + + for _, item := range items { + usageCount := "" + if item.Usage != nil { + usageCount = utils.PtrString(item.Usage.Count) + } + table.AddRow( + item.Name, + string(item.Type), + item.Version, + usageCount, + ) + table.AddSeparator() + } + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/beta/alb-waf/managed-rule-set/list/list_test.go b/internal/cmd/beta/alb-waf/managed-rule-set/list/list_test.go new file mode 100644 index 000000000..4744ad5b5 --- /dev/null +++ b/internal/cmd/beta/alb-waf/managed-rule-set/list/list_test.go @@ -0,0 +1,296 @@ +package list + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "slices" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + albwaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") + testProjectId = uuid.NewString() + testRegion = "eu01" + testClient = &albwaf.APIClient{DefaultAPI: &albwaf.DefaultAPIService{}} + testNextPageID = "next-page-id-123" +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + Region: testRegion, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request albwaf.ApiListManagedRuleSetsRequest) albwaf.ApiListManagedRuleSetsRequest) albwaf.ApiListManagedRuleSetsRequest { + request := testClient.DefaultAPI.ListManagedRuleSets(testCtx, testProjectId, testRegion) + request = request.PageSize("100") + for _, mod := range mods { + request = mod(request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "with limit", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "10" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Limit = utils.Ptr(int64(10)) + }), + }, + { + description: "invalid limit", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, nil, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + nextPageID string + pageLimit int64 + expectedRequest albwaf.ApiListManagedRuleSetsRequest + }{ + { + description: "base", + model: fixtureInputModel(), + pageLimit: 100, + expectedRequest: fixtureRequest(), + }, + { + description: "with next page id", + model: fixtureInputModel(), + nextPageID: testNextPageID, + pageLimit: 100, + expectedRequest: fixtureRequest(func(request albwaf.ApiListManagedRuleSetsRequest) albwaf.ApiListManagedRuleSetsRequest { + return request.PageId(testNextPageID) + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient, tt.nextPageID, tt.pageLimit) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, albwaf.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("data does not match: %s", diff) + } + }) + } +} + +type testResponse struct { + statusCode int + body albwaf.ListManagedRuleSetResponse +} + +func fixtureTestResponse(mods ...func(resp *testResponse)) testResponse { + resp := testResponse{ + statusCode: 200, + } + for _, mod := range mods { + mod(&resp) + } + return resp +} + +func fixtureItems(count int) []albwaf.GetLimitedManagedRuleSetResponse { + items := make([]albwaf.GetLimitedManagedRuleSetResponse, count) + for i := range count { + items[i] = albwaf.GetLimitedManagedRuleSetResponse{ + Name: fmt.Sprintf("managed-rule-set-%d", i+1), + Type: albwaf.TYPE_TYPE_OWASP_CRS, + Version: "v1.0.0", + AdditionalProperties: map[string]interface{}{}, + } + } + return items +} + +func TestFetchManagedRuleSets(t *testing.T) { + tests := []struct { + description string + limit int64 + responses []testResponse + expected []albwaf.GetLimitedManagedRuleSetResponse + fails bool + }{ + { + description: "no items", + responses: []testResponse{ + fixtureTestResponse(), + }, + expected: nil, + }, + { + description: "single item, single page", + responses: []testResponse{ + fixtureTestResponse(func(resp *testResponse) { + resp.body.Items = fixtureItems(1) + }), + }, + expected: fixtureItems(1), + }, + { + description: "multiple pages", + responses: []testResponse{ + fixtureTestResponse(func(resp *testResponse) { + resp.body.NextPageId = utils.Ptr(testNextPageID) + resp.body.Items = fixtureItems(1) + }), + fixtureTestResponse(func(resp *testResponse) { + resp.body.Items = fixtureItems(1) + }), + }, + expected: slices.Concat(fixtureItems(1), fixtureItems(1)), + }, + { + description: "API error", + responses: []testResponse{ + fixtureTestResponse(func(resp *testResponse) { + resp.statusCode = 500 + }), + }, + fails: true, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + callCount := 0 + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + resp := tt.responses[callCount] + callCount++ + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(resp.statusCode) + bs, err := json.Marshal(resp.body) + if err != nil { + t.Fatalf("marshal: %v", err) + } + _, err = w.Write(bs) + if err != nil { + t.Fatalf("write: %v", err) + } + }) + server := httptest.NewServer(handler) + defer server.Close() + client, err := albwaf.NewAPIClient( + sdkConfig.WithEndpoint(server.URL), + sdkConfig.WithoutAuthentication(), + ) + if err != nil { + t.Fatalf("failed to create test client: %v", err) + } + var mods []func(m *inputModel) + if tt.limit > 0 { + mods = append(mods, func(m *inputModel) { + m.Limit = utils.Ptr(tt.limit) + }) + } + model := fixtureInputModel(mods...) + got, err := fetchManagedRuleSets(testCtx, model, client) + if err != nil { + if !tt.fails { + t.Fatalf("fetchManagedRuleSets() unexpected error: %v", err) + } + return + } + if callCount != len(tt.responses) { + t.Errorf("fetchManagedRuleSets() expected %d calls, got %d", len(tt.responses), callCount) + } + diff := cmp.Diff(got, tt.expected) + if diff != "" { + t.Errorf("fetchManagedRuleSets() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + items []albwaf.GetLimitedManagedRuleSetResponse + }{ + { + description: "no items", + items: []albwaf.GetLimitedManagedRuleSetResponse{}, + }, + { + description: "single item", + items: fixtureItems(1), + }, + } + + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + if err := outputResult(params.Printer, "", tt.items); err != nil { + t.Fatalf("outputResult: %v", err) + } + }) + } +} diff --git a/internal/cmd/beta/alb-waf/managed-rule-set/managed-rule-set.go b/internal/cmd/beta/alb-waf/managed-rule-set/managed-rule-set.go new file mode 100644 index 000000000..0faf69e61 --- /dev/null +++ b/internal/cmd/beta/alb-waf/managed-rule-set/managed-rule-set.go @@ -0,0 +1,35 @@ +package managedruleset + +import ( + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb-waf/managed-rule-set/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb-waf/managed-rule-set/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb-waf/managed-rule-set/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb-waf/managed-rule-set/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb-waf/managed-rule-set/update" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "managed-rule-set", + Short: "Provides functionality for managed rule sets of the ALB WAF", + Long: "Provides functionality for managed rule sets (MRS) of the Web Application Firewall (WAF) for application loadbalancers.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) +} diff --git a/internal/cmd/beta/alb-waf/managed-rule-set/update/testdata/testconfig.json b/internal/cmd/beta/alb-waf/managed-rule-set/update/testdata/testconfig.json new file mode 100644 index 000000000..1576d3140 --- /dev/null +++ b/internal/cmd/beta/alb-waf/managed-rule-set/update/testdata/testconfig.json @@ -0,0 +1,14 @@ +{ + "groups": { + "942": { + "rules": { + "942100": { + "mode": "MODE_DISABLED" + }, + "942190": { + "mode": "MODE_LOG_ONLY" + } + } + } + } +} diff --git a/internal/cmd/beta/alb-waf/managed-rule-set/update/update.go b/internal/cmd/beta/alb-waf/managed-rule-set/update/update.go new file mode 100644 index 000000000..7850703aa --- /dev/null +++ b/internal/cmd/beta/alb-waf/managed-rule-set/update/update.go @@ -0,0 +1,155 @@ +package update + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/goccy/go-yaml" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/albwaf/client" + + "github.com/spf13/cobra" + albwaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" +) + +const ( + nameArg = "NAME" + configurationFlag = "configuration" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Name string + Configuration *string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("update %s", nameArg), + Short: "Updates a managed rule set of the ALB WAF", + Long: "Updates the rules of a managed rule set (MRS) of the Web Application Firewall (WAF) for application loadbalancers. Only the rules provided in the configuration file are updated, all other rules remain unchanged.", + Args: args.SingleArg(nameArg, nil), + Example: examples.Build( + examples.NewExample( + `Update the rules of a managed rule set with name "my-managed-rule-set" from a configuration file`, + "$ stackit beta alb-waf managed-rule-set update my-managed-rule-set --configuration my-rules.json"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to update the managed rule set %q for project %q?", model.Name, projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req, err := buildRequest(ctx, model, apiClient) + if err != nil { + return err + } + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update managed rule set: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().StringP(configurationFlag, "c", "", "Filename of the input configuration file") + err := flags.MarkFlagsRequired(cmd, configurationFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + name := inputArgs[0] + model := inputModel{ + GlobalFlagModel: globalFlags, + Name: name, + Configuration: flags.FlagToStringPointer(p, cmd, configurationFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *albwaf.APIClient) (req albwaf.ApiPatchManagedRuleSetRequest, err error) { + payload, err := readPayload(model) + if err != nil { + return req, err + } + req = apiClient.DefaultAPI.PatchManagedRuleSet(ctx, model.ProjectId, model.Region, model.Name) + return req.PatchManagedRuleSetPayload(payload), nil +} + +func readPayload(model *inputModel) (payload albwaf.PatchManagedRuleSetPayload, err error) { + if model.Configuration == nil { + return payload, fmt.Errorf("no configuration file defined") + } + file, err := os.Open(*model.Configuration) + if err != nil { + return payload, fmt.Errorf("cannot open configuration file %q: %w", *model.Configuration, err) + } + defer file.Close() // nolint:errcheck // at this point close errors are not relevant anymore + + if strings.HasSuffix(*model.Configuration, ".yaml") { + decoder := yaml.NewDecoder(bufio.NewReader(file), yaml.UseJSONUnmarshaler()) + if err := decoder.Decode(&payload); err != nil { + return payload, fmt.Errorf("cannot deserialize yaml configuration from %q: %w", *model.Configuration, err) + } + } else if strings.HasSuffix(*model.Configuration, ".json") { + decoder := json.NewDecoder(bufio.NewReader(file)) + if err := decoder.Decode(&payload); err != nil { + return payload, fmt.Errorf("cannot deserialize json configuration from %q: %w", *model.Configuration, err) + } + } else { + return payload, fmt.Errorf("cannot determine configuration fileformat of %q by extension. Must be '.json' or '.yaml'", *model.Configuration) + } + + return payload, nil +} + +func outputResult(p *print.Printer, outputFormat, projectLabel string, resp *albwaf.GetManagedRuleSetResponse) error { + if resp == nil { + return fmt.Errorf("update managed rule set response is empty") + } + return p.OutputResult(outputFormat, resp, func() error { + p.Outputf("Updated managed rule set %q for project %q.\n", resp.Name, projectLabel) + return nil + }) +} diff --git a/internal/cmd/beta/alb-waf/managed-rule-set/update/update_test.go b/internal/cmd/beta/alb-waf/managed-rule-set/update/update_test.go new file mode 100644 index 000000000..e6282d2cd --- /dev/null +++ b/internal/cmd/beta/alb-waf/managed-rule-set/update/update_test.go @@ -0,0 +1,221 @@ +package update + +import ( + "context" + _ "embed" + "encoding/json" + "log" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + albwaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +//go:embed testdata/testconfig.json +var testConfiguration []byte + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") + testClient = &albwaf.APIClient{DefaultAPI: &albwaf.DefaultAPIService{}} + testProjectId = uuid.NewString() + testRegion = "eu01" + testName = "my-managed-rule-set" + testConfig = "testdata/testconfig.json" +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testName, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + configurationFlag: testConfig, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + Region: testRegion, + }, + Name: testName, + Configuration: &testConfig, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixturePayload(mods ...func(payload *albwaf.PatchManagedRuleSetPayload)) (payload albwaf.PatchManagedRuleSetPayload) { + if err := json.Unmarshal(testConfiguration, &payload); err != nil { + log.Panicf("cannot deserialize test configuration: %v", err) + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func fixtureRequest(mods ...func(request *albwaf.ApiPatchManagedRuleSetRequest)) albwaf.ApiPatchManagedRuleSetRequest { + request := testClient.DefaultAPI.PatchManagedRuleSet(testCtx, testProjectId, testRegion, testName) + request = request.PatchManagedRuleSetPayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no configuration flag", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, configurationFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest albwaf.ApiPatchManagedRuleSetRequest + fails bool + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "no configuration", + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + Name: testName, + }, + fails: true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, err := buildRequest(testCtx, tt.model, testClient) + if err != nil { + if !tt.fails { + t.Fatalf("cannot build request: %v", err) + } + return + } + if tt.fails { + t.Fatalf("did not fail on invalid input") + } + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, albwaf.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + projectLabel string + resp *albwaf.GetManagedRuleSetResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "base", + args: args{ + projectLabel: "my-project", + resp: &albwaf.GetManagedRuleSetResponse{ + Name: testName, + Type: albwaf.TYPE_TYPE_OWASP_CRS, + Version: "v1.0.0", + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, "", tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/beta.go b/internal/cmd/beta/beta.go index f739b0c03..5ee25e6cb 100644 --- a/internal/cmd/beta/beta.go +++ b/internal/cmd/beta/beta.go @@ -6,6 +6,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb" + albwaf "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb-waf" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/cdn" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake" @@ -45,6 +46,7 @@ func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(sqlserverflex.NewCmd(params)) cmd.AddCommand(sfs.NewCmd(params)) cmd.AddCommand(alb.NewCmd(params)) + cmd.AddCommand(albwaf.NewCmd(params)) cmd.AddCommand(edge.NewCmd(params)) cmd.AddCommand(intake.NewCmd(params)) cmd.AddCommand(cdn.NewCmd(params)) diff --git a/internal/cmd/config/set/set.go b/internal/cmd/config/set/set.go index 2895ecbe7..1733f9db2 100644 --- a/internal/cmd/config/set/set.go +++ b/internal/cmd/config/set/set.go @@ -25,6 +25,7 @@ const ( allowedUrlDomainFlag = "allowed-url-domain" authorizationCustomEndpointFlag = "authorization-custom-endpoint" + albWafCustomEndpointFlag = "alb-waf-custom-endpoint" dnsCustomEndpointFlag = "dns-custom-endpoint" edgeCustomEndpointFlag = "edge-custom-endpoint" loadBalancerCustomEndpointFlag = "load-balancer-custom-endpoint" @@ -146,6 +147,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(allowedUrlDomainFlag, "", `Domain name, used for the verification of the URLs that are given in the custom identity provider endpoint and "STACKIT curl" command`) cmd.Flags().String(observabilityCustomEndpointFlag, "", "Observability API base URL, used in calls to this API") cmd.Flags().String(authorizationCustomEndpointFlag, "", "Authorization API base URL, used in calls to this API") + cmd.Flags().String(albWafCustomEndpointFlag, "", "ALB WAF API base URL, used in calls to this API") cmd.Flags().String(dnsCustomEndpointFlag, "", "DNS API base URL, used in calls to this API") cmd.Flags().String(edgeCustomEndpointFlag, "", "Edge API base URL, used in calls to this API") cmd.Flags().String(loadBalancerCustomEndpointFlag, "", "Load Balancer API base URL, used in calls to this API") @@ -188,6 +190,8 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) err = viper.BindPFlag(config.AuthorizationCustomEndpointKey, cmd.Flags().Lookup(authorizationCustomEndpointFlag)) cobra.CheckErr(err) + err = viper.BindPFlag(config.AlbWafCustomEndpointKey, cmd.Flags().Lookup(albWafCustomEndpointFlag)) + cobra.CheckErr(err) err = viper.BindPFlag(config.DNSCustomEndpointKey, cmd.Flags().Lookup(dnsCustomEndpointFlag)) cobra.CheckErr(err) err = viper.BindPFlag(config.EdgeCustomEndpointKey, cmd.Flags().Lookup(edgeCustomEndpointFlag)) diff --git a/internal/cmd/config/unset/unset.go b/internal/cmd/config/unset/unset.go index da43bfe9e..522d4a29b 100644 --- a/internal/cmd/config/unset/unset.go +++ b/internal/cmd/config/unset/unset.go @@ -28,6 +28,7 @@ const ( allowedUrlDomainFlag = "allowed-url-domain" authorizationCustomEndpointFlag = "authorization-custom-endpoint" + albWafCustomEndpointFlag = "alb-waf-custom-endpoint" dnsCustomEndpointFlag = "dns-custom-endpoint" edgeCustomEndpointFlag = "edge-custom-endpoint" loadBalancerCustomEndpointFlag = "load-balancer-custom-endpoint" @@ -78,6 +79,7 @@ type inputModel struct { AllowedUrlDomain bool AuthorizationCustomEndpoint bool + AlbWafCustomEndpoint bool DNSCustomEndpoint bool EdgeCustomEndpoint bool LoadBalancerCustomEndpoint bool @@ -167,6 +169,9 @@ func NewCmd(params *types.CmdParams) *cobra.Command { if model.AuthorizationCustomEndpoint { viper.Set(config.AuthorizationCustomEndpointKey, "") } + if model.AlbWafCustomEndpoint { + viper.Set(config.AlbWafCustomEndpointKey, "") + } if model.DNSCustomEndpoint { viper.Set(config.DNSCustomEndpointKey, "") } @@ -278,6 +283,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(observabilityCustomEndpointFlag, false, "Observability API base URL. If unset, uses the default base URL") cmd.Flags().Bool(authorizationCustomEndpointFlag, false, "Authorization API base URL. If unset, uses the default base URL") + cmd.Flags().Bool(albWafCustomEndpointFlag, false, "ALB WAF API base URL. If unset, uses the default base URL") cmd.Flags().Bool(dnsCustomEndpointFlag, false, "DNS API base URL. If unset, uses the default base URL") cmd.Flags().Bool(edgeCustomEndpointFlag, false, "Edge API base URL. If unset, uses the default base URL") cmd.Flags().Bool(loadBalancerCustomEndpointFlag, false, "Load Balancer API base URL. If unset, uses the default base URL") @@ -323,6 +329,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { AllowedUrlDomain: flags.FlagToBoolValue(p, cmd, allowedUrlDomainFlag), AuthorizationCustomEndpoint: flags.FlagToBoolValue(p, cmd, authorizationCustomEndpointFlag), + AlbWafCustomEndpoint: flags.FlagToBoolValue(p, cmd, albWafCustomEndpointFlag), DNSCustomEndpoint: flags.FlagToBoolValue(p, cmd, dnsCustomEndpointFlag), EdgeCustomEndpoint: flags.FlagToBoolValue(p, cmd, edgeCustomEndpointFlag), LoadBalancerCustomEndpoint: flags.FlagToBoolValue(p, cmd, loadBalancerCustomEndpointFlag), diff --git a/internal/cmd/config/unset/unset_test.go b/internal/cmd/config/unset/unset_test.go index feee86e38..75965ec31 100644 --- a/internal/cmd/config/unset/unset_test.go +++ b/internal/cmd/config/unset/unset_test.go @@ -23,6 +23,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]bool)) map[string]bool allowedUrlDomainFlag: true, authorizationCustomEndpointFlag: true, + albWafCustomEndpointFlag: true, dnsCustomEndpointFlag: true, edgeCustomEndpointFlag: true, loadBalancerCustomEndpointFlag: true, @@ -70,6 +71,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { AllowedUrlDomain: true, AuthorizationCustomEndpoint: true, + AlbWafCustomEndpoint: true, DNSCustomEndpoint: true, EdgeCustomEndpoint: true, LoadBalancerCustomEndpoint: true, @@ -133,6 +135,7 @@ func TestParseInput(t *testing.T) { model.AllowedUrlDomain = false model.AuthorizationCustomEndpoint = false + model.AlbWafCustomEndpoint = false model.DNSCustomEndpoint = false model.EdgeCustomEndpoint = false model.LoadBalancerCustomEndpoint = false @@ -221,6 +224,16 @@ func TestParseInput(t *testing.T) { model.ObservabilityCustomEndpoint = false }), }, + { + description: "alb waf custom endpoint empty", + flagValues: fixtureFlagValues(func(flagValues map[string]bool) { + flagValues[albWafCustomEndpointFlag] = false + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.AlbWafCustomEndpoint = false + }), + }, { description: "dns custom endpoint empty", flagValues: fixtureFlagValues(func(flagValues map[string]bool) { diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index effb68051..b1aebe6f5 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -25,6 +25,7 @@ const ( AuthorizationCustomEndpointKey = "authorization_custom_endpoint" AlbCustomEndpoint = "alb_custom _endpoint" + AlbWafCustomEndpointKey = "alb_waf_custom_endpoint" DNSCustomEndpointKey = "dns_custom_endpoint" EdgeCustomEndpointKey = "edge_custom_endpoint" LoadBalancerCustomEndpointKey = "load_balancer_custom_endpoint" @@ -92,6 +93,7 @@ var ConfigKeys = []string{ AllowedUrlDomainKey, AlbCustomEndpoint, + AlbWafCustomEndpointKey, AuthorizationCustomEndpointKey, CDNCustomEndpointKey, DNSCustomEndpointKey, @@ -212,6 +214,7 @@ func setConfigDefaults() { viper.SetDefault(GitCustomEndpointKey, "") viper.SetDefault(IntakeCustomEndpointKey, "") viper.SetDefault(AlbCustomEndpoint, "") + viper.SetDefault(AlbWafCustomEndpointKey, "") viper.SetDefault(LogsCustomEndpointKey, "") viper.SetDefault(CDNCustomEndpointKey, "") viper.SetDefault(VPNCustomEndpointKey, "") diff --git a/internal/pkg/services/albwaf/client/client.go b/internal/pkg/services/albwaf/client/client.go new file mode 100644 index 000000000..9f85f9942 --- /dev/null +++ b/internal/pkg/services/albwaf/client/client.go @@ -0,0 +1,14 @@ +package client + +import ( + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/spf13/viper" + albwaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" +) + +func ConfigureClient(p *print.Printer, cliVersion string) (*albwaf.APIClient, error) { + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.AlbWafCustomEndpointKey), false, genericclient.CreateApiClient[*albwaf.APIClient](albwaf.NewAPIClient)) +} diff --git a/internal/pkg/utils/strings.go b/internal/pkg/utils/strings.go index 8772e38af..9dcea6f5a 100644 --- a/internal/pkg/utils/strings.go +++ b/internal/pkg/utils/strings.go @@ -67,6 +67,16 @@ func JoinStringPtr[T ~string](vals *[]T, sep string) string { return b.String() } +// SortedKeys returns the string keys of a map, sorted alphabetically. +func SortedKeys[V any](m map[string]V) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + slices.Sort(keys) + return keys +} + // Truncate trims the passed string (if it is not nil). If the input string is // longer than the given length, it is truncated to _maxLen_ and a ellipsis (…) // is attached. Therefore the resulting string has at most length _maxLen-1_ diff --git a/internal/pkg/utils/strings_test.go b/internal/pkg/utils/strings_test.go index 6f0279045..b715b69c1 100644 --- a/internal/pkg/utils/strings_test.go +++ b/internal/pkg/utils/strings_test.go @@ -1,6 +1,7 @@ package utils import ( + "reflect" "testing" "github.com/stackitcloud/stackit-sdk-go/core/utils" @@ -31,6 +32,42 @@ func TestTruncate(t *testing.T) { } } +func TestSortedKeys(t *testing.T) { + tests := []struct { + name string + input map[string]int + want []string + }{ + { + name: "nil map", + input: nil, + want: []string{}, + }, + { + name: "empty map", + input: map[string]int{}, + want: []string{}, + }, + { + name: "single element", + input: map[string]int{"b": 1}, + want: []string{"b"}, + }, + { + name: "multiple elements unsorted", + input: map[string]int{"c": 3, "a": 1, "b": 2}, + want: []string{"a", "b", "c"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := SortedKeys(tt.input); !reflect.DeepEqual(got, tt.want) { + t.Errorf("SortedKeys() = %v, want %v", got, tt.want) + } + }) + } +} + func TestJoinStringMap(t *testing.T) { tests := []struct { name string