diff --git a/docs/auth0_protection.md b/docs/auth0_protection.md index 33d3008dd..86ae6b799 100644 --- a/docs/auth0_protection.md +++ b/docs/auth0_protection.md @@ -8,6 +8,7 @@ Auth0 can detect attacks and stop malicious attempts to access your application ## Commands +- [auth0 protection bot-detection](auth0_protection_bot-detection.md) - Manage bot detection settings - [auth0 protection breached-password-detection](auth0_protection_breached-password-detection.md) - Manage breached password detection settings - [auth0 protection brute-force-protection](auth0_protection_brute-force-protection.md) - Manage brute force protection settings - [auth0 protection suspicious-ip-throttling](auth0_protection_suspicious-ip-throttling.md) - Manage suspicious ip throttling settings diff --git a/docs/auth0_protection_bot-detection.md b/docs/auth0_protection_bot-detection.md new file mode 100644 index 000000000..92b93cd9b --- /dev/null +++ b/docs/auth0_protection_bot-detection.md @@ -0,0 +1,14 @@ +--- +layout: default +has_toc: false +has_children: true +--- +# auth0 protection bot-detection + +Bot detection protects your applications from automated attacks by detecting and blocking bot traffic. Auth0 can challenge suspicious requests with CAPTCHA or block them entirely. Configure detection sensitivity, CAPTCHA policies for different authentication flows, and allowlists for trusted IP addresses. + +## Commands + +- [auth0 protection bot-detection show](auth0_protection_bot-detection_show.md) - Show bot detection settings +- [auth0 protection bot-detection update](auth0_protection_bot-detection_update.md) - Update bot detection settings + diff --git a/docs/auth0_protection_bot-detection_show.md b/docs/auth0_protection_bot-detection_show.md new file mode 100644 index 000000000..36e358479 --- /dev/null +++ b/docs/auth0_protection_bot-detection_show.md @@ -0,0 +1,47 @@ +--- +layout: default +parent: auth0 protection bot-detection +has_toc: false +--- +# auth0 protection bot-detection show + +Display the current bot detection settings. + +## Usage +``` +auth0 protection bot-detection show [flags] +``` + +## Examples + +``` + auth0 protection bot-detection show + auth0 ap bd show --json + auth0 ap bd show --json-compact +``` + + +## Flags + +``` + --json Output in json format. + --json-compact Output in compact json format. +``` + + +## Inherited Flags + +``` + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 protection bot-detection show](auth0_protection_bot-detection_show.md) - Show bot detection settings +- [auth0 protection bot-detection update](auth0_protection_bot-detection_update.md) - Update bot detection settings + + diff --git a/docs/auth0_protection_bot-detection_update.md b/docs/auth0_protection_bot-detection_update.md new file mode 100644 index 000000000..69e1eea76 --- /dev/null +++ b/docs/auth0_protection_bot-detection_update.md @@ -0,0 +1,55 @@ +--- +layout: default +parent: auth0 protection bot-detection +has_toc: false +--- +# auth0 protection bot-detection update + +Update the bot detection settings. + +## Usage +``` +auth0 protection bot-detection update [flags] +``` + +## Examples + +``` + auth0 protection bot-detection update + auth0 ap bd update --bot-detection-level medium --json-compact + auth0 ap bd update --bot-detection-level low --challenge-password-policy never + auth0 ap bd update --monitoring-mode=true --allowlist "198.51.100.42,10.0.0.0/24" + auth0 ap bd update -l high -a "198.51.100.42" -m=false --json +``` + + +## Flags + +``` + -a, --allowlist strings List of comma-separated trusted IP addresses that will not have bot detection enforced against them. Supports IPv4, IPv6 and CIDR notations. + -l, --bot-detection-level string The level of bot detection sensitivity. Possible values: low, medium, high. + --challenge-password-policy string Determines how often to challenge users with a CAPTCHA for password-based login. Possible values: never, when_risky, always. + --challenge-password-reset-policy string Determines how often to challenge users with a CAPTCHA for password reset. Possible values: never, when_risky, always. + --challenge-passwordless-policy string Determines how often to challenge users with a CAPTCHA for passwordless login. Possible values: never, when_risky, always. + --json Output in json format. + --json-compact Output in compact json format. + -m, --monitoring-mode Enable (or disable) monitoring mode. When enabled, logs but does not block. +``` + + +## Inherited Flags + +``` + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 protection bot-detection show](auth0_protection_bot-detection_show.md) - Show bot detection settings +- [auth0 protection bot-detection update](auth0_protection_bot-detection_update.md) - Update bot detection settings + + diff --git a/go.mod b/go.mod index 46de237e1..107a60481 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/PuerkitoBio/rehttp v1.4.0 github.com/atotto/clipboard v0.1.4 github.com/auth0/go-auth0 v1.33.0 + github.com/auth0/go-auth0/v2 v2.5.0 github.com/briandowns/spinner v1.23.2 github.com/charmbracelet/glamour v0.10.0 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e diff --git a/go.sum b/go.sum index ec3d81e44..276f70ee2 100644 --- a/go.sum +++ b/go.sum @@ -22,10 +22,12 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/auth0/go-auth0 v1.32.1 h1:AAXQqaNaFZWkRm2bg5mVVXpqDLmusv7v238uIaxuFpo= -github.com/auth0/go-auth0 v1.32.1/go.mod h1:32sQB1uAn+99fJo6N819EniKq8h785p0ag0lMWhiTaE= github.com/auth0/go-auth0 v1.33.0 h1:7qx0UCA6Tn2udnEVA35xzKsseh/R9559f+nnGcUI0Ss= github.com/auth0/go-auth0 v1.33.0/go.mod h1:32sQB1uAn+99fJo6N819EniKq8h785p0ag0lMWhiTaE= +github.com/auth0/go-auth0 v1.33.1-0.20260211120643-ac1cfcb90495 h1:RMJY2JenrbX8RiEIihcEJFnsQVwjs1njavZAgKTwIzg= +github.com/auth0/go-auth0 v1.33.1-0.20260211120643-ac1cfcb90495/go.mod h1:32sQB1uAn+99fJo6N819EniKq8h785p0ag0lMWhiTaE= +github.com/auth0/go-auth0/v2 v2.5.0 h1:IBfiYGsqFwOu4hsxV1JDtB6+ayRinybUIUCU/fRBE8Y= +github.com/auth0/go-auth0/v2 v2.5.0/go.mod h1:XVRck9fw1EIw1z4guYcbKFGmElnexb+xOvQ/0U1hHd0= github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0 h1:0NmehRCgyk5rljDQLKUO+cRJCnduDyn11+zGZIc9Z48= github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0/go.mod h1:6L7zgvqo0idzI7IO8de6ZC051AfXb5ipkIJ7bIA2tGA= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= diff --git a/internal/auth0/attack_protection.go b/internal/auth0/attack_protection.go index c3d6afc11..abbe0c559 100644 --- a/internal/auth0/attack_protection.go +++ b/internal/auth0/attack_protection.go @@ -4,6 +4,8 @@ import ( "context" "github.com/auth0/go-auth0/management" + managementv2 "github.com/auth0/go-auth0/v2/management" + "github.com/auth0/go-auth0/v2/management/option" ) type AttackProtectionAPI interface { @@ -64,3 +66,19 @@ type AttackProtectionAPI interface { opts ...management.RequestOption, ) (err error) } + +type AttackProtectionBotDetectionAPIV2 interface { + // Get the Bot Detection configuration of tenant. + // + // Required scope: `read:attack_protection` + // + // See: https://auth0.com/docs/api/management/v2#!/attack-protection/get-bot-detection + Get(ctx context.Context, opts ...option.RequestOption) (*managementv2.GetBotDetectionSettingsResponseContent, error) + + // Update the Bot Detection configuration of tenant. + // + // Required scope: `update:attack_protection` + // + // See: https://auth0.com/docs/api/management/v2#!/attack-protection/patch-bot-detection + Update(ctx context.Context, request *managementv2.UpdateBotDetectionSettingsRequestContent, opts ...option.RequestOption) (*managementv2.UpdateBotDetectionSettingsResponseContent, error) +} diff --git a/internal/auth0/auth0.go b/internal/auth0/auth0.go index 514aea343..4f1f7eba8 100644 --- a/internal/auth0/auth0.go +++ b/internal/auth0/auth0.go @@ -3,6 +3,7 @@ package auth0 import ( "github.com/auth0/go-auth0" "github.com/auth0/go-auth0/management" + managementv2 "github.com/auth0/go-auth0/v2/management/client" ) // API mimics `management.Management`s general interface, except it refers to @@ -76,6 +77,16 @@ func NewAPI(m *management.Management) *API { } } +type APIV2 struct { + AttackProtectionBotDetection AttackProtectionBotDetectionAPIV2 +} + +func NewAPIV2(m *managementv2.Management) *APIV2 { + return &APIV2{ + AttackProtectionBotDetection: m.AttackProtection.BotDetection, + } +} + // Alias all the helper methods so we can keep just typing `auth0.Bool` and the // compiler can autocomplete our internal package. var ( diff --git a/internal/cli/attack_protection.go b/internal/cli/attack_protection.go index ecf0b5dd1..d5b0a9d3b 100644 --- a/internal/cli/attack_protection.go +++ b/internal/cli/attack_protection.go @@ -19,6 +19,7 @@ func attackProtectionCmd(cli *cli) *cobra.Command { cmd.AddCommand(breachedPasswordDetectionCmd(cli)) cmd.AddCommand(bruteForceProtectionCmd(cli)) cmd.AddCommand(suspiciousIPThrottlingCmd(cli)) + cmd.AddCommand(botDetectionCmd(cli)) return cmd } diff --git a/internal/cli/attack_protection_bot_detection.go b/internal/cli/attack_protection_bot_detection.go new file mode 100644 index 000000000..e2d90168f --- /dev/null +++ b/internal/cli/attack_protection_bot_detection.go @@ -0,0 +1,265 @@ +package cli + +import ( + "strings" + + managementv2 "github.com/auth0/go-auth0/v2/management" + "github.com/spf13/cobra" + + "github.com/auth0/auth0-cli/internal/ansi" +) + +var ( + botDetectionLevelPossibleValues = []string{"low", "medium", "high"} + passwordPolicyPossibleValues = []string{"never", "when_risky", "always"} +) + +var bdFlags = botDetectionFlags{ + BotDetectionLevel: Flag{ + Name: "Bot Detection Level", + LongForm: "bot-detection-level", + ShortForm: "l", + Help: "The level of bot detection sensitivity. Possible values: " + strings.Join(botDetectionLevelPossibleValues, ", ") + ".", + AlwaysPrompt: true, + }, + ChallengePasswordPolicy: Flag{ + Name: "Challenge Password Policy", + LongForm: "challenge-password-policy", + Help: "Determines how often to challenge users with a CAPTCHA for password-based login. Possible values: " + + strings.Join(passwordPolicyPossibleValues, ", ") + ".", + AlwaysPrompt: true, + }, + ChallengePasswordlessPolicy: Flag{ + Name: "Challenge Passwordless Policy", + LongForm: "challenge-passwordless-policy", + Help: "Determines how often to challenge users with a CAPTCHA for passwordless login. Possible values: " + + strings.Join(passwordPolicyPossibleValues, ", ") + ".", + AlwaysPrompt: true, + }, + ChallengePasswordResetPolicy: Flag{ + Name: "Challenge Password Reset Policy", + LongForm: "challenge-password-reset-policy", + Help: "Determines how often to challenge users with a CAPTCHA for password reset. Possible values: " + + strings.Join(passwordPolicyPossibleValues, ", ") + ".", + AlwaysPrompt: true, + }, + AllowList: Flag{ + Name: "Allow List", + LongForm: "allowlist", + ShortForm: "a", + Help: "List of comma-separated trusted IP addresses that will not have bot detection enforced against them. " + + "Supports IPv4, IPv6 and CIDR notations.", + AlwaysPrompt: true, + }, + MonitoringModeEnabled: Flag{ + Name: "Monitoring Mode Enabled", + LongForm: "monitoring-mode", + ShortForm: "m", + Help: "Enable (or disable) monitoring mode. When enabled, logs but does not block.", + AlwaysPrompt: true, + }, +} + +type ( + botDetectionFlags struct { + BotDetectionLevel Flag + ChallengePasswordPolicy Flag + ChallengePasswordlessPolicy Flag + ChallengePasswordResetPolicy Flag + AllowList Flag + MonitoringModeEnabled Flag + } + + botDetectionInputs struct { + BotDetectionLevel string + ChallengePasswordPolicy string + ChallengePasswordlessPolicy string + ChallengePasswordResetPolicy string + AllowList []string + MonitoringModeEnabled bool + } +) + +func botDetectionCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "bot-detection", + Args: cobra.MaximumNArgs(1), + Aliases: []string{"bd"}, + Short: "Manage bot detection settings", + Long: "Bot detection protects your applications from automated attacks by detecting and blocking bot traffic. " + + "Auth0 can challenge suspicious requests with CAPTCHA or block them entirely. " + + "Configure detection sensitivity, CAPTCHA policies for different authentication flows, and allowlists for trusted IP addresses.", + } + + cmd.SetUsageTemplate(resourceUsageTemplate()) + + cmd.AddCommand(showBotDetectionCmd(cli)) + cmd.AddCommand(updateBotDetectionCmd(cli)) + + return cmd +} + +func showBotDetectionCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "show", + Args: cobra.NoArgs, + Short: "Show bot detection settings", + Long: "Display the current bot detection settings.", + Example: ` auth0 protection bot-detection show + auth0 ap bd show --json + auth0 ap bd show --json-compact`, + RunE: showBotDetectionCmdRun(cli), + } + + cmd.Flags().BoolVar(&cli.json, "json", false, "Output in json format.") + cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output in compact json format.") + cmd.MarkFlagsMutuallyExclusive("json", "json-compact") + + return cmd +} + +func updateBotDetectionCmd(cli *cli) *cobra.Command { + var inputs botDetectionInputs + + cmd := &cobra.Command{ + Use: "update", + Args: cobra.NoArgs, + Short: "Update bot detection settings", + Long: "Update the bot detection settings.", + Example: ` auth0 protection bot-detection update + auth0 ap bd update --bot-detection-level medium --json-compact + auth0 ap bd update --bot-detection-level low --challenge-password-policy never + auth0 ap bd update --monitoring-mode=true --allowlist "198.51.100.42,10.0.0.0/24" + auth0 ap bd update -l high -a "198.51.100.42" -m=false --json`, + RunE: updateBotDetectionCmdRun(cli, &inputs), + } + + bdFlags.BotDetectionLevel.RegisterStringU(cmd, &inputs.BotDetectionLevel, "") + bdFlags.ChallengePasswordPolicy.RegisterStringU(cmd, &inputs.ChallengePasswordPolicy, "") + bdFlags.ChallengePasswordlessPolicy.RegisterStringU(cmd, &inputs.ChallengePasswordlessPolicy, "") + bdFlags.ChallengePasswordResetPolicy.RegisterStringU(cmd, &inputs.ChallengePasswordResetPolicy, "") + bdFlags.AllowList.RegisterStringSliceU(cmd, &inputs.AllowList, []string{}) + bdFlags.MonitoringModeEnabled.RegisterBoolU(cmd, &inputs.MonitoringModeEnabled, false) + + cmd.Flags().BoolVar(&cli.json, "json", false, "Output in json format.") + cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output in compact json format.") + cmd.MarkFlagsMutuallyExclusive("json", "json-compact") + + return cmd +} + +func showBotDetectionCmdRun(cli *cli) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + var bd *managementv2.GetBotDetectionSettingsResponseContent + err := ansi.Waiting(func() (err error) { + bd, err = cli.apiv2.AttackProtectionBotDetection.Get(cmd.Context()) + return err + }) + if err != nil { + return err + } + + cli.renderer.BotDetectionShow(bd) + + return nil + } +} + +func updateBotDetectionCmdRun(cli *cli, inputs *botDetectionInputs) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + var current *managementv2.GetBotDetectionSettingsResponseContent + err := ansi.Waiting(func() (err error) { + current, err = cli.apiv2.AttackProtectionBotDetection.Get(cmd.Context()) + return err + }) + if err != nil { + return err + } + + bdUpdate := &managementv2.UpdateBotDetectionSettingsRequestContent{} + + // Set bot detection level. + if err := bdFlags.BotDetectionLevel.AskU(cmd, &inputs.BotDetectionLevel, stringPtr(current.BotDetectionLevel.Ptr())); err != nil { + return err + } + if inputs.BotDetectionLevel == "" { + inputs.BotDetectionLevel = string(current.GetBotDetectionLevel()) + } + botDetectionLevel, err := managementv2.NewBotDetectionLevelEnumFromString(inputs.BotDetectionLevel) + if err != nil { + return err + } + bdUpdate.SetBotDetectionLevel(&botDetectionLevel) + + // Set challenge password policy. + if err := bdFlags.ChallengePasswordPolicy.AskU(cmd, &inputs.ChallengePasswordPolicy, stringPtr(current.ChallengePasswordPolicy.Ptr())); err != nil { + return err + } + if inputs.ChallengePasswordPolicy == "" { + inputs.ChallengePasswordPolicy = string(current.GetChallengePasswordPolicy()) + } + challengePasswordPolicy, err := managementv2.NewBotDetectionChallengePolicyPasswordFlowEnumFromString(inputs.ChallengePasswordPolicy) + if err != nil { + return err + } + bdUpdate.SetChallengePasswordPolicy(&challengePasswordPolicy) + + // Set challenge passwordless policy. + if err := bdFlags.ChallengePasswordlessPolicy.AskU(cmd, &inputs.ChallengePasswordlessPolicy, stringPtr(current.ChallengePasswordlessPolicy.Ptr())); err != nil { + return err + } + if inputs.ChallengePasswordlessPolicy == "" { + inputs.ChallengePasswordlessPolicy = string(current.GetChallengePasswordlessPolicy()) + } + challengePasswordlessPolicy, err := managementv2.NewBotDetectionChallengePolicyPasswordlessFlowEnumFromString(inputs.ChallengePasswordlessPolicy) + if err != nil { + return err + } + bdUpdate.SetChallengePasswordlessPolicy(&challengePasswordlessPolicy) + + // Set challenge password reset policy. + if err := bdFlags.ChallengePasswordResetPolicy.AskU(cmd, &inputs.ChallengePasswordResetPolicy, stringPtr(current.ChallengePasswordResetPolicy.Ptr())); err != nil { + return err + } + if inputs.ChallengePasswordResetPolicy == "" { + inputs.ChallengePasswordResetPolicy = string(current.GetChallengePasswordResetPolicy()) + } + challengePasswordResetPolicy, err := managementv2.NewBotDetectionChallengePolicyPasswordResetFlowEnumFromString(inputs.ChallengePasswordResetPolicy) + if err != nil { + return err + } + bdUpdate.SetChallengePasswordResetPolicy(&challengePasswordResetPolicy) + + // Set allowlist. + allowListString := strings.Join(current.GetAllowlist(), ",") + if err := bdFlags.AllowList.AskManyU(cmd, &inputs.AllowList, &allowListString); err != nil { + return err + } + if len(inputs.AllowList) == 0 { + inputs.AllowList = current.GetAllowlist() + } + bdUpdate.SetAllowlist(&inputs.AllowList) + + // Set monitoring mode enabled. + if !bdFlags.MonitoringModeEnabled.IsSet(cmd) { + inputs.MonitoringModeEnabled = current.GetMonitoringModeEnabled() + } + if err := bdFlags.MonitoringModeEnabled.AskBoolU(cmd, &inputs.MonitoringModeEnabled, ¤t.MonitoringModeEnabled); err != nil { + return err + } + bdUpdate.SetMonitoringModeEnabled(&inputs.MonitoringModeEnabled) + + var updatedBD *managementv2.UpdateBotDetectionSettingsResponseContent + if err := ansi.Waiting(func() error { + var err error + updatedBD, err = cli.apiv2.AttackProtectionBotDetection.Update(cmd.Context(), bdUpdate) + return err + }); err != nil { + return err + } + + cli.renderer.BotDetectionUpdate(updatedBD) + + return nil + } +} diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 61d1fb5d7..9c4f56106 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -35,6 +35,7 @@ const userAgent = "Auth0 CLI" type cli struct { // Core primitives exposed to command builders. api *auth0.API + apiv2 *auth0.APIV2 renderer *display.Renderer tracker *analytics.Tracker @@ -132,7 +133,13 @@ func (c *cli) setupWithAuthentication(ctx context.Context) error { return err } + apiv2, err := initializeManagementClientV2(tenant.Domain, tenant.GetAccessToken()) + if err != nil { + return err + } + c.api = auth0.NewAPI(api) + c.apiv2 = auth0.NewAPIV2(apiv2) return nil } diff --git a/internal/cli/management.go b/internal/cli/management.go index 5f0286c8f..fc0a65bf6 100644 --- a/internal/cli/management.go +++ b/internal/cli/management.go @@ -13,6 +13,8 @@ import ( "github.com/PuerkitoBio/rehttp" "github.com/auth0/go-auth0/management" + managementv2 "github.com/auth0/go-auth0/v2/management/client" + "github.com/auth0/go-auth0/v2/management/option" "github.com/auth0/auth0-cli/internal/buildinfo" ) @@ -30,6 +32,21 @@ func initializeManagementClient(tenantDomain string, accessToken string) (*manag return client, err } +func initializeManagementClientV2(tenantDomain string, accessToken string) (*managementv2.Management, error) { + client, err := managementv2.New( + tenantDomain, + option.WithToken(accessToken), + option.WithUserAgent(fmt.Sprintf("%v/%v", userAgent, strings.TrimPrefix(buildinfo.Version, "v"))), + option.WithAuth0ClientEnvEntry("Auth0-CLI", strings.TrimPrefix(buildinfo.Version, "v")), + // If not set, it defaults to 2 as per v2@v2.5.0/management/internal/retrier.go:43. + // Setting it to 1 to avoid retries from `go-auth0` since we have our own retry logic in the custom HTTP client. + // TODO: confirm this assumption, or check if this needs to be excluded like terraform provider. + option.WithMaxAttempts(1), + option.WithHTTPClient(customClientWithRetries()), + ) + return client, err +} + func customClientWithRetries() *http.Client { client := &http.Client{ Transport: rateLimitTransport( diff --git a/internal/cli/utils_shared.go b/internal/cli/utils_shared.go index 2b7b9901b..2d3f710c5 100644 --- a/internal/cli/utils_shared.go +++ b/internal/cli/utils_shared.go @@ -322,6 +322,16 @@ func formatManageTenantURL(tenant string, cfg *config.Config) string { ) } +// stringPtr converts a pointer of string-derived type to pointer of string. +// Returns nil if the input pointer is nil. +func stringPtr[strPtrType ~string](ptr *strPtrType) *string { + if ptr == nil { + return nil + } + s := string(*ptr) + return &s +} + func parseFlexibleDate(input string) (string, error) { now := time.Now().UTC() input = strings.TrimSpace(input) diff --git a/internal/cli/utils_shared_test.go b/internal/cli/utils_shared_test.go index 67a4d3cd5..fb2ff9c8c 100644 --- a/internal/cli/utils_shared_test.go +++ b/internal/cli/utils_shared_test.go @@ -14,6 +14,30 @@ import ( "github.com/auth0/auth0-cli/internal/config" ) +func TestStringPtr(t *testing.T) { + t.Run("returns nil when input is nil", func(t *testing.T) { + type CustomString string + var nilPtr *CustomString + result := stringPtr(nilPtr) + assert.Nil(t, result) + }) + + t.Run("converts custom string pointer to string pointer", func(t *testing.T) { + type CustomString string + value := CustomString("test-value") + result := stringPtr(&value) + assert.NotNil(t, result) + assert.Equal(t, "test-value", *result) + }) + + t.Run("converts regular string pointer to string pointer", func(t *testing.T) { + value := "regular-string" + result := stringPtr(&value) + assert.NotNil(t, result) + assert.Equal(t, "regular-string", *result) + }) +} + func TestBuildOauthTokenURL(t *testing.T) { url := BuildOauthTokenURL("cli-demo.us.auth0.com") assert.Equal(t, "https://cli-demo.us.auth0.com/oauth/token", url) diff --git a/internal/display/attack_protection.go b/internal/display/attack_protection.go index 6f8a422b5..4d20a73af 100644 --- a/internal/display/attack_protection.go +++ b/internal/display/attack_protection.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/auth0/go-auth0/management" + managementv2 "github.com/auth0/go-auth0/v2/management" "github.com/auth0/auth0-cli/internal/ansi" ) @@ -182,3 +183,77 @@ func makeSuspiciousIPThrottlingView(sit *management.SuspiciousIPThrottling) *sus return view } + +type botDetectionView struct { + BotDetectionLevel string + ChallengePasswordPolicy string + ChallengePasswordlessPolicy string + ChallengePasswordResetPolicy string + AllowList []string + MonitoringModeEnabled string + + raw interface{} +} + +func (bd *botDetectionView) AsTableHeader() []string { + // There is no list command for this resource, hence this func never gets called. + // Dummy implementation to satisfy View interface. + return []string{} +} + +func (bd *botDetectionView) AsTableRow() []string { + // There is no list command for this resource, hence this func never gets called. + // Dummy implementation to satisfy View interface. + return []string{} +} + +func (bd *botDetectionView) KeyValues() [][]string { + return [][]string{ + {ansi.Bold("BOT_DETECTION_LEVEL"), bd.BotDetectionLevel}, + {ansi.Bold("CHALLENGE_PASSWORD_POLICY"), bd.ChallengePasswordPolicy}, + {ansi.Bold("CHALLENGE_PASSWORDLESS_POLICY"), bd.ChallengePasswordlessPolicy}, + {ansi.Bold("CHALLENGE_PASSWORD_RESET_POLICY"), bd.ChallengePasswordResetPolicy}, + {ansi.Bold("ALLOW_LIST"), strings.Join(bd.AllowList, ", ")}, + {ansi.Bold("MONITORING_MODE_ENABLED"), bd.MonitoringModeEnabled}, + } +} + +func (bd *botDetectionView) Object() interface{} { + return bd.raw +} + +func (r *Renderer) BotDetectionShow(bd *managementv2.GetBotDetectionSettingsResponseContent) { + r.Heading("bot detection") + r.Result(makeBotDetectionShowView(bd)) +} + +func (r *Renderer) BotDetectionUpdate(bd *managementv2.UpdateBotDetectionSettingsResponseContent) { + r.Heading("bot detection updated") + r.Result(makeBotDetectionUpdateView(bd)) +} + +func makeBotDetectionShowView(bd *managementv2.GetBotDetectionSettingsResponseContent) *botDetectionView { + return &botDetectionView{ + BotDetectionLevel: string(bd.GetBotDetectionLevel()), + ChallengePasswordPolicy: string(bd.GetChallengePasswordPolicy()), + ChallengePasswordlessPolicy: string(bd.GetChallengePasswordlessPolicy()), + ChallengePasswordResetPolicy: string(bd.GetChallengePasswordResetPolicy()), + AllowList: bd.GetAllowlist(), + MonitoringModeEnabled: boolean(bd.GetMonitoringModeEnabled()), + + raw: bd, + } +} + +func makeBotDetectionUpdateView(bd *managementv2.UpdateBotDetectionSettingsResponseContent) *botDetectionView { + return &botDetectionView{ + BotDetectionLevel: string(bd.GetBotDetectionLevel()), + ChallengePasswordPolicy: string(bd.GetChallengePasswordPolicy()), + ChallengePasswordlessPolicy: string(bd.GetChallengePasswordlessPolicy()), + ChallengePasswordResetPolicy: string(bd.GetChallengePasswordResetPolicy()), + AllowList: bd.GetAllowlist(), + MonitoringModeEnabled: boolean(bd.GetMonitoringModeEnabled()), + + raw: bd, + } +} diff --git a/test/integration/attack-protection-test-cases.yaml b/test/integration/attack-protection-test-cases.yaml new file mode 100644 index 000000000..711307ed0 --- /dev/null +++ b/test/integration/attack-protection-test-cases.yaml @@ -0,0 +1,110 @@ +config: + inherit-env: true + retries: 1 + +tests: + 001 - attack protection breached password detection show: + command: auth0 attack-protection breached-password-detection show + stdout: + contains: + - ENABLED + - SHIELDS + - ADMIN_NOTIFICATION_FREQUENCY + - METHOD + exit-code: 0 + + 002 - attack protection brute force protection show: + command: auth0 attack-protection brute-force-protection show + stdout: + contains: + - ENABLED + - SHIELDS + - ALLOW_LIST + - MODE + - MAX_ATTEMPTS + exit-code: 0 + + 003 - attack protection suspicious ip throttling show: + command: auth0 attack-protection suspicious-ip-throttling show + stdout: + contains: + - ENABLED + - SHIELDS + - ALLOW_LIST + - STAGE_PRE_LOGIN_MAX_ATTEMPTS + - STAGE_PRE_LOGIN_RATE + - STAGE_PRE_USER_REGISTRATION_MAX_ATTEMPTS + - STAGE_PRE_USER_REGISTRATION_RATE + exit-code: 0 + + 004 - attack protection bot detection show: + command: auth0 attack-protection bot-detection show + stdout: + contains: + - BOT_DETECTION_LEVEL + - CHALLENGE_PASSWORD_POLICY + - CHALLENGE_PASSWORDLESS_POLICY + - CHALLENGE_PASSWORD_RESET_POLICY + - ALLOW_LIST + - MONITORING_MODE_ENABLED + exit-code: 0 + + 005 - attack protection suspicious ip throttling ips check: + command: auth0 attack-protection suspicious-ip-throttling ips check 127.0.0.1 + stderr: + contains: + - "The IP 127.0.0.1 is not blocked." + exit-code: 0 + + 006 - attack protection suspicious ip throttling ips unblock: + command: auth0 attack-protection suspicious-ip-throttling ips unblock 127.0.0.1 + stderr: + contains: + - "The IP 127.0.0.1 was unblocked." + exit-code: 0 + + 007 - attack protection update breached password detection: + command: auth0 attack-protection breached-password-detection update --enabled + stdout: + contains: + - ENABLED + - SHIELDS + - ADMIN_NOTIFICATION_FREQUENCY + - METHOD + exit-code: 0 + + 008 - attack protection update brute force protection: + command: auth0 attack-protection brute-force-protection update --enabled + stdout: + contains: + - ENABLED + - SHIELDS + - ALLOW_LIST + - MODE + - MAX_ATTEMPTS + exit-code: 0 + + 009 - attack protection update suspicious ip throttling: + command: auth0 attack-protection suspicious-ip-throttling update --enabled + stdout: + contains: + - ENABLED + - SHIELDS + - ALLOW_LIST + - STAGE_PRE_LOGIN_MAX_ATTEMPTS + - STAGE_PRE_LOGIN_RATE + - STAGE_PRE_USER_REGISTRATION_MAX_ATTEMPTS + - STAGE_PRE_USER_REGISTRATION_RATE + exit-code: 0 + + 010 - attack protection update bot detection: + command: auth0 attack-protection bot-detection update --bot-detection-level medium + stdout: + contains: + - BOT_DETECTION_LEVEL + - CHALLENGE_PASSWORD_POLICY + - CHALLENGE_PASSWORDLESS_POLICY + - CHALLENGE_PASSWORD_RESET_POLICY + - ALLOW_LIST + - MONITORING_MODE_ENABLED + exit-code: 0 diff --git a/test/integration/test-cases.yaml b/test/integration/test-cases.yaml index 4461a0320..f3ef6da43 100644 --- a/test/integration/test-cases.yaml +++ b/test/integration/test-cases.yaml @@ -15,88 +15,6 @@ tests: auth0 completion powershell: exit-code: 0 - attack protection breached password detection show: - command: auth0 attack-protection breached-password-detection show - stdout: - contains: - - ENABLED - - SHIELDS - - ADMIN_NOTIFICATION_FREQUENCY - - METHOD - exit-code: 0 - - attack protection brute force protection show: - command: auth0 attack-protection brute-force-protection show - stdout: - contains: - - ENABLED - - SHIELDS - - ALLOW_LIST - - MODE - - MAX_ATTEMPTS - exit-code: 0 - - attack protection suspicious ip throttling show: - command: auth0 attack-protection suspicious-ip-throttling show - stdout: - contains: - - ENABLED - - SHIELDS - - ALLOW_LIST - - STAGE_PRE_LOGIN_MAX_ATTEMPTS - - STAGE_PRE_LOGIN_RATE - - STAGE_PRE_USER_REGISTRATION_MAX_ATTEMPTS - - STAGE_PRE_USER_REGISTRATION_RATE - exit-code: 0 - - attack protection suspicious ip throttling ips check: - command: auth0 attack-protection suspicious-ip-throttling ips check 127.0.0.1 - stderr: - contains: - - "The IP 127.0.0.1 is not blocked." - exit-code: 0 - - attack protection suspicious ip throttling ips unblock: - command: auth0 attack-protection suspicious-ip-throttling ips unblock 127.0.0.1 - stderr: - contains: - - "The IP 127.0.0.1 was unblocked." - exit-code: 0 - - attack protection update breached password detection: - command: auth0 attack-protection breached-password-detection update --enabled - stdout: - contains: - - ENABLED - - SHIELDS - - ADMIN_NOTIFICATION_FREQUENCY - - METHOD - exit-code: 0 - - attack protection update brute force protection: - command: auth0 attack-protection brute-force-protection update --enabled - stdout: - contains: - - ENABLED - - SHIELDS - - ALLOW_LIST - - MODE - - MAX_ATTEMPTS - exit-code: 0 - - attack protection update suspicious ip throttling: - command: auth0 attack-protection suspicious-ip-throttling update --enabled - stdout: - contains: - - ENABLED - - SHIELDS - - ALLOW_LIST - - STAGE_PRE_LOGIN_MAX_ATTEMPTS - - STAGE_PRE_LOGIN_RATE - - STAGE_PRE_USER_REGISTRATION_MAX_ATTEMPTS - - STAGE_PRE_USER_REGISTRATION_RATE - exit-code: 0 - tenants list: command: auth0 tenants list exit-code: 0