diff --git a/README.md b/README.md index f6ddddc6..3c31e395 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,7 @@ OVH_CLOUD_PROJECT_SERVICE= | | login | Partially | | **Infra Meta** | location | Partially | | **Network** | ip | Partially | +| | ip firewall | Yes | | | overthebox | Partially | | | vrack | Partially | | | vrackservices | Partially | diff --git a/doc/ovhcloud_ip.md b/doc/ovhcloud_ip.md index 5c2b99a9..0ef2d6ef 100644 --- a/doc/ovhcloud_ip.md +++ b/doc/ovhcloud_ip.md @@ -31,6 +31,7 @@ Retrieve information and manage your IP services * [ovhcloud](ovhcloud.md) - CLI to manage your OVHcloud services * [ovhcloud ip edit](ovhcloud_ip_edit.md) - Edit the given IP +* [ovhcloud ip firewall](ovhcloud_ip_firewall.md) - Manage firewall (Edge Firewall) on the given IP * [ovhcloud ip get](ovhcloud_ip_get.md) - Retrieve information of a specific Ip * [ovhcloud ip list](ovhcloud_ip_list.md) - List your Ip services * [ovhcloud ip reverse](ovhcloud_ip_reverse.md) - Manage reverses on the given IP diff --git a/doc/ovhcloud_ip_firewall.md b/doc/ovhcloud_ip_firewall.md new file mode 100644 index 00000000..d9b86483 --- /dev/null +++ b/doc/ovhcloud_ip_firewall.md @@ -0,0 +1,40 @@ +## ovhcloud ip firewall + +Manage firewall (Edge Firewall) on the given IP + +### Options + +``` + -h, --help help for firewall +``` + +### Options inherited from parent commands + +``` + -d, --debug Activate debug mode (will log all HTTP requests details) + -e, --ignore-errors Ignore errors in API calls when it is not fatal to the execution + -o, --output string Output format: json, yaml, interactive, or a custom format expression (using https://github.com/PaesslerAG/gval syntax) + Examples: + --output json + --output yaml + --output interactive + --output 'id' (to extract a single field) + --output 'nested.field.subfield' (to extract a nested field) + --output '[id, "name"]' (to extract multiple fields as an array) + --output '{"newKey": oldKey, "otherKey": nested.field}' (to extract and rename fields in an object) + --output 'name+","+type' (to extract and concatenate fields in a string) + --output '(nbFieldA + nbFieldB) * 10' (to compute values from numeric fields) + --profile string Use a specific profile from the configuration file +``` + +### SEE ALSO + +* [ovhcloud ip](ovhcloud_ip.md) - Retrieve information and manage your IP services +* [ovhcloud ip firewall add](ovhcloud_ip_firewall_add.md) - Add an IP to the firewall +* [ovhcloud ip firewall delete](ovhcloud_ip_firewall_delete.md) - Remove IP and all rules from firewall +* [ovhcloud ip firewall disable](ovhcloud_ip_firewall_disable.md) - Disable the firewall on the given IP +* [ovhcloud ip firewall enable](ovhcloud_ip_firewall_enable.md) - Enable the firewall on the given IP +* [ovhcloud ip firewall get](ovhcloud_ip_firewall_get.md) - Get firewall status for a specific IP +* [ovhcloud ip firewall list](ovhcloud_ip_firewall_list.md) - List IPs registered on the firewall +* [ovhcloud ip firewall rule](ovhcloud_ip_firewall_rule.md) - Manage firewall rules + diff --git a/doc/ovhcloud_ip_firewall_add.md b/doc/ovhcloud_ip_firewall_add.md new file mode 100644 index 00000000..2dc9bc9b --- /dev/null +++ b/doc/ovhcloud_ip_firewall_add.md @@ -0,0 +1,37 @@ +## ovhcloud ip firewall add + +Add an IP to the firewall + +``` +ovhcloud ip firewall add [flags] +``` + +### Options + +``` + -h, --help help for add +``` + +### Options inherited from parent commands + +``` + -d, --debug Activate debug mode (will log all HTTP requests details) + -e, --ignore-errors Ignore errors in API calls when it is not fatal to the execution + -o, --output string Output format: json, yaml, interactive, or a custom format expression (using https://github.com/PaesslerAG/gval syntax) + Examples: + --output json + --output yaml + --output interactive + --output 'id' (to extract a single field) + --output 'nested.field.subfield' (to extract a nested field) + --output '[id, "name"]' (to extract multiple fields as an array) + --output '{"newKey": oldKey, "otherKey": nested.field}' (to extract and rename fields in an object) + --output 'name+","+type' (to extract and concatenate fields in a string) + --output '(nbFieldA + nbFieldB) * 10' (to compute values from numeric fields) + --profile string Use a specific profile from the configuration file +``` + +### SEE ALSO + +* [ovhcloud ip firewall](ovhcloud_ip_firewall.md) - Manage firewall (Edge Firewall) on the given IP + diff --git a/doc/ovhcloud_ip_firewall_delete.md b/doc/ovhcloud_ip_firewall_delete.md new file mode 100644 index 00000000..1c3c7e09 --- /dev/null +++ b/doc/ovhcloud_ip_firewall_delete.md @@ -0,0 +1,37 @@ +## ovhcloud ip firewall delete + +Remove IP and all rules from firewall + +``` +ovhcloud ip firewall delete [flags] +``` + +### Options + +``` + -h, --help help for delete +``` + +### Options inherited from parent commands + +``` + -d, --debug Activate debug mode (will log all HTTP requests details) + -e, --ignore-errors Ignore errors in API calls when it is not fatal to the execution + -o, --output string Output format: json, yaml, interactive, or a custom format expression (using https://github.com/PaesslerAG/gval syntax) + Examples: + --output json + --output yaml + --output interactive + --output 'id' (to extract a single field) + --output 'nested.field.subfield' (to extract a nested field) + --output '[id, "name"]' (to extract multiple fields as an array) + --output '{"newKey": oldKey, "otherKey": nested.field}' (to extract and rename fields in an object) + --output 'name+","+type' (to extract and concatenate fields in a string) + --output '(nbFieldA + nbFieldB) * 10' (to compute values from numeric fields) + --profile string Use a specific profile from the configuration file +``` + +### SEE ALSO + +* [ovhcloud ip firewall](ovhcloud_ip_firewall.md) - Manage firewall (Edge Firewall) on the given IP + diff --git a/doc/ovhcloud_ip_firewall_disable.md b/doc/ovhcloud_ip_firewall_disable.md new file mode 100644 index 00000000..b0fb3bfd --- /dev/null +++ b/doc/ovhcloud_ip_firewall_disable.md @@ -0,0 +1,37 @@ +## ovhcloud ip firewall disable + +Disable the firewall on the given IP + +``` +ovhcloud ip firewall disable [flags] +``` + +### Options + +``` + -h, --help help for disable +``` + +### Options inherited from parent commands + +``` + -d, --debug Activate debug mode (will log all HTTP requests details) + -e, --ignore-errors Ignore errors in API calls when it is not fatal to the execution + -o, --output string Output format: json, yaml, interactive, or a custom format expression (using https://github.com/PaesslerAG/gval syntax) + Examples: + --output json + --output yaml + --output interactive + --output 'id' (to extract a single field) + --output 'nested.field.subfield' (to extract a nested field) + --output '[id, "name"]' (to extract multiple fields as an array) + --output '{"newKey": oldKey, "otherKey": nested.field}' (to extract and rename fields in an object) + --output 'name+","+type' (to extract and concatenate fields in a string) + --output '(nbFieldA + nbFieldB) * 10' (to compute values from numeric fields) + --profile string Use a specific profile from the configuration file +``` + +### SEE ALSO + +* [ovhcloud ip firewall](ovhcloud_ip_firewall.md) - Manage firewall (Edge Firewall) on the given IP + diff --git a/doc/ovhcloud_ip_firewall_enable.md b/doc/ovhcloud_ip_firewall_enable.md new file mode 100644 index 00000000..0f4dc9ec --- /dev/null +++ b/doc/ovhcloud_ip_firewall_enable.md @@ -0,0 +1,37 @@ +## ovhcloud ip firewall enable + +Enable the firewall on the given IP + +``` +ovhcloud ip firewall enable [flags] +``` + +### Options + +``` + -h, --help help for enable +``` + +### Options inherited from parent commands + +``` + -d, --debug Activate debug mode (will log all HTTP requests details) + -e, --ignore-errors Ignore errors in API calls when it is not fatal to the execution + -o, --output string Output format: json, yaml, interactive, or a custom format expression (using https://github.com/PaesslerAG/gval syntax) + Examples: + --output json + --output yaml + --output interactive + --output 'id' (to extract a single field) + --output 'nested.field.subfield' (to extract a nested field) + --output '[id, "name"]' (to extract multiple fields as an array) + --output '{"newKey": oldKey, "otherKey": nested.field}' (to extract and rename fields in an object) + --output 'name+","+type' (to extract and concatenate fields in a string) + --output '(nbFieldA + nbFieldB) * 10' (to compute values from numeric fields) + --profile string Use a specific profile from the configuration file +``` + +### SEE ALSO + +* [ovhcloud ip firewall](ovhcloud_ip_firewall.md) - Manage firewall (Edge Firewall) on the given IP + diff --git a/doc/ovhcloud_ip_firewall_get.md b/doc/ovhcloud_ip_firewall_get.md new file mode 100644 index 00000000..7a0e1fb6 --- /dev/null +++ b/doc/ovhcloud_ip_firewall_get.md @@ -0,0 +1,37 @@ +## ovhcloud ip firewall get + +Get firewall status for a specific IP + +``` +ovhcloud ip firewall get [flags] +``` + +### Options + +``` + -h, --help help for get +``` + +### Options inherited from parent commands + +``` + -d, --debug Activate debug mode (will log all HTTP requests details) + -e, --ignore-errors Ignore errors in API calls when it is not fatal to the execution + -o, --output string Output format: json, yaml, interactive, or a custom format expression (using https://github.com/PaesslerAG/gval syntax) + Examples: + --output json + --output yaml + --output interactive + --output 'id' (to extract a single field) + --output 'nested.field.subfield' (to extract a nested field) + --output '[id, "name"]' (to extract multiple fields as an array) + --output '{"newKey": oldKey, "otherKey": nested.field}' (to extract and rename fields in an object) + --output 'name+","+type' (to extract and concatenate fields in a string) + --output '(nbFieldA + nbFieldB) * 10' (to compute values from numeric fields) + --profile string Use a specific profile from the configuration file +``` + +### SEE ALSO + +* [ovhcloud ip firewall](ovhcloud_ip_firewall.md) - Manage firewall (Edge Firewall) on the given IP + diff --git a/doc/ovhcloud_ip_firewall_list.md b/doc/ovhcloud_ip_firewall_list.md new file mode 100644 index 00000000..8b0433c5 --- /dev/null +++ b/doc/ovhcloud_ip_firewall_list.md @@ -0,0 +1,44 @@ +## ovhcloud ip firewall list + +List IPs registered on the firewall + +``` +ovhcloud ip firewall list [flags] +``` + +### Options + +``` + --filter stringArray Filter results by any property using https://github.com/PaesslerAG/gval syntax + Examples: + --filter 'state="running"' + --filter 'name=~"^my.*"' + --filter 'nested.property.subproperty>10' + --filter 'startDate>="2023-12-01"' + --filter 'name=~"something" && nbField>10' + -h, --help help for list +``` + +### Options inherited from parent commands + +``` + -d, --debug Activate debug mode (will log all HTTP requests details) + -e, --ignore-errors Ignore errors in API calls when it is not fatal to the execution + -o, --output string Output format: json, yaml, interactive, or a custom format expression (using https://github.com/PaesslerAG/gval syntax) + Examples: + --output json + --output yaml + --output interactive + --output 'id' (to extract a single field) + --output 'nested.field.subfield' (to extract a nested field) + --output '[id, "name"]' (to extract multiple fields as an array) + --output '{"newKey": oldKey, "otherKey": nested.field}' (to extract and rename fields in an object) + --output 'name+","+type' (to extract and concatenate fields in a string) + --output '(nbFieldA + nbFieldB) * 10' (to compute values from numeric fields) + --profile string Use a specific profile from the configuration file +``` + +### SEE ALSO + +* [ovhcloud ip firewall](ovhcloud_ip_firewall.md) - Manage firewall (Edge Firewall) on the given IP + diff --git a/doc/ovhcloud_ip_firewall_rule.md b/doc/ovhcloud_ip_firewall_rule.md new file mode 100644 index 00000000..84cf35f4 --- /dev/null +++ b/doc/ovhcloud_ip_firewall_rule.md @@ -0,0 +1,37 @@ +## ovhcloud ip firewall rule + +Manage firewall rules + +### Options + +``` + -h, --help help for rule +``` + +### Options inherited from parent commands + +``` + -d, --debug Activate debug mode (will log all HTTP requests details) + -e, --ignore-errors Ignore errors in API calls when it is not fatal to the execution + -o, --output string Output format: json, yaml, interactive, or a custom format expression (using https://github.com/PaesslerAG/gval syntax) + Examples: + --output json + --output yaml + --output interactive + --output 'id' (to extract a single field) + --output 'nested.field.subfield' (to extract a nested field) + --output '[id, "name"]' (to extract multiple fields as an array) + --output '{"newKey": oldKey, "otherKey": nested.field}' (to extract and rename fields in an object) + --output 'name+","+type' (to extract and concatenate fields in a string) + --output '(nbFieldA + nbFieldB) * 10' (to compute values from numeric fields) + --profile string Use a specific profile from the configuration file +``` + +### SEE ALSO + +* [ovhcloud ip firewall](ovhcloud_ip_firewall.md) - Manage firewall (Edge Firewall) on the given IP +* [ovhcloud ip firewall rule create](ovhcloud_ip_firewall_rule_create.md) - Create a new firewall rule +* [ovhcloud ip firewall rule delete](ovhcloud_ip_firewall_rule_delete.md) - Delete a firewall rule +* [ovhcloud ip firewall rule get](ovhcloud_ip_firewall_rule_get.md) - Get a specific firewall rule +* [ovhcloud ip firewall rule list](ovhcloud_ip_firewall_rule_list.md) - List firewall rules for the given IP + diff --git a/doc/ovhcloud_ip_firewall_rule_create.md b/doc/ovhcloud_ip_firewall_rule_create.md new file mode 100644 index 00000000..62923a02 --- /dev/null +++ b/doc/ovhcloud_ip_firewall_rule_create.md @@ -0,0 +1,81 @@ +## ovhcloud ip firewall rule create + +Create a new firewall rule + +### Synopsis + +Use this command to create a new firewall rule. +There are three ways to define the creation parameters: + +1. Using only CLI flags: + + ovhcloud ip firewall rule create --action permit --protocol tcp --sequence 0 --destination-port 443 + +2. Using a configuration file: + + First generate an example parameters file: + + ovhcloud ip firewall rule create --init-file ./rule.json + + After editing the file, run: + + ovhcloud ip firewall rule create --from-file ./rule.json + + You can also pipe the content: + + cat ./rule.json | ovhcloud ip firewall rule create + +3. Using your default text editor: + + ovhcloud ip firewall rule create --editor + + +``` +ovhcloud ip firewall rule create [flags] +``` + +### Options + +``` + --action string Action: deny or permit (required) + --destination-port int Destination port (TCP/UDP only) + --destination-port-from int Destination port range start (mutually exclusive with --destination-port) + --destination-port-to int Destination port range end + --editor Use a text editor to define parameters + --from-file string File containing parameters + -h, --help help for create + --init-file string Create a file with example parameters + --protocol string Protocol: ah, esp, gre, icmp, ipv4, tcp, udp (required) + --replace Replace parameters file if it already exists + --sequence int Rule priority 0-19 (required) (default -1) + --source string Source IP/CIDR (defaults to any) + --source-port int Source port (TCP/UDP only) + --source-port-from int Source port range start (mutually exclusive with --source-port) + --source-port-to int Source port range end + --tcp-fragments TCP fragments option + --tcp-option string TCP option: established or syn (TCP only) +``` + +### Options inherited from parent commands + +``` + -d, --debug Activate debug mode (will log all HTTP requests details) + -e, --ignore-errors Ignore errors in API calls when it is not fatal to the execution + -o, --output string Output format: json, yaml, interactive, or a custom format expression (using https://github.com/PaesslerAG/gval syntax) + Examples: + --output json + --output yaml + --output interactive + --output 'id' (to extract a single field) + --output 'nested.field.subfield' (to extract a nested field) + --output '[id, "name"]' (to extract multiple fields as an array) + --output '{"newKey": oldKey, "otherKey": nested.field}' (to extract and rename fields in an object) + --output 'name+","+type' (to extract and concatenate fields in a string) + --output '(nbFieldA + nbFieldB) * 10' (to compute values from numeric fields) + --profile string Use a specific profile from the configuration file +``` + +### SEE ALSO + +* [ovhcloud ip firewall rule](ovhcloud_ip_firewall_rule.md) - Manage firewall rules + diff --git a/doc/ovhcloud_ip_firewall_rule_delete.md b/doc/ovhcloud_ip_firewall_rule_delete.md new file mode 100644 index 00000000..fb1edf83 --- /dev/null +++ b/doc/ovhcloud_ip_firewall_rule_delete.md @@ -0,0 +1,37 @@ +## ovhcloud ip firewall rule delete + +Delete a firewall rule + +``` +ovhcloud ip firewall rule delete [flags] +``` + +### Options + +``` + -h, --help help for delete +``` + +### Options inherited from parent commands + +``` + -d, --debug Activate debug mode (will log all HTTP requests details) + -e, --ignore-errors Ignore errors in API calls when it is not fatal to the execution + -o, --output string Output format: json, yaml, interactive, or a custom format expression (using https://github.com/PaesslerAG/gval syntax) + Examples: + --output json + --output yaml + --output interactive + --output 'id' (to extract a single field) + --output 'nested.field.subfield' (to extract a nested field) + --output '[id, "name"]' (to extract multiple fields as an array) + --output '{"newKey": oldKey, "otherKey": nested.field}' (to extract and rename fields in an object) + --output 'name+","+type' (to extract and concatenate fields in a string) + --output '(nbFieldA + nbFieldB) * 10' (to compute values from numeric fields) + --profile string Use a specific profile from the configuration file +``` + +### SEE ALSO + +* [ovhcloud ip firewall rule](ovhcloud_ip_firewall_rule.md) - Manage firewall rules + diff --git a/doc/ovhcloud_ip_firewall_rule_get.md b/doc/ovhcloud_ip_firewall_rule_get.md new file mode 100644 index 00000000..5e4205d5 --- /dev/null +++ b/doc/ovhcloud_ip_firewall_rule_get.md @@ -0,0 +1,37 @@ +## ovhcloud ip firewall rule get + +Get a specific firewall rule + +``` +ovhcloud ip firewall rule get [flags] +``` + +### Options + +``` + -h, --help help for get +``` + +### Options inherited from parent commands + +``` + -d, --debug Activate debug mode (will log all HTTP requests details) + -e, --ignore-errors Ignore errors in API calls when it is not fatal to the execution + -o, --output string Output format: json, yaml, interactive, or a custom format expression (using https://github.com/PaesslerAG/gval syntax) + Examples: + --output json + --output yaml + --output interactive + --output 'id' (to extract a single field) + --output 'nested.field.subfield' (to extract a nested field) + --output '[id, "name"]' (to extract multiple fields as an array) + --output '{"newKey": oldKey, "otherKey": nested.field}' (to extract and rename fields in an object) + --output 'name+","+type' (to extract and concatenate fields in a string) + --output '(nbFieldA + nbFieldB) * 10' (to compute values from numeric fields) + --profile string Use a specific profile from the configuration file +``` + +### SEE ALSO + +* [ovhcloud ip firewall rule](ovhcloud_ip_firewall_rule.md) - Manage firewall rules + diff --git a/doc/ovhcloud_ip_firewall_rule_list.md b/doc/ovhcloud_ip_firewall_rule_list.md new file mode 100644 index 00000000..d834cdb7 --- /dev/null +++ b/doc/ovhcloud_ip_firewall_rule_list.md @@ -0,0 +1,44 @@ +## ovhcloud ip firewall rule list + +List firewall rules for the given IP + +``` +ovhcloud ip firewall rule list [flags] +``` + +### Options + +``` + --filter stringArray Filter results by any property using https://github.com/PaesslerAG/gval syntax + Examples: + --filter 'state="running"' + --filter 'name=~"^my.*"' + --filter 'nested.property.subproperty>10' + --filter 'startDate>="2023-12-01"' + --filter 'name=~"something" && nbField>10' + -h, --help help for list +``` + +### Options inherited from parent commands + +``` + -d, --debug Activate debug mode (will log all HTTP requests details) + -e, --ignore-errors Ignore errors in API calls when it is not fatal to the execution + -o, --output string Output format: json, yaml, interactive, or a custom format expression (using https://github.com/PaesslerAG/gval syntax) + Examples: + --output json + --output yaml + --output interactive + --output 'id' (to extract a single field) + --output 'nested.field.subfield' (to extract a nested field) + --output '[id, "name"]' (to extract multiple fields as an array) + --output '{"newKey": oldKey, "otherKey": nested.field}' (to extract and rename fields in an object) + --output 'name+","+type' (to extract and concatenate fields in a string) + --output '(nbFieldA + nbFieldB) * 10' (to compute values from numeric fields) + --profile string Use a specific profile from the configuration file +``` + +### SEE ALSO + +* [ovhcloud ip firewall rule](ovhcloud_ip_firewall_rule.md) - Manage firewall rules + diff --git a/internal/cmd/ip.go b/internal/cmd/ip.go index a8bc0cae..b018d0f8 100644 --- a/internal/cmd/ip.go +++ b/internal/cmd/ip.go @@ -5,6 +5,7 @@ package cmd import ( + "github.com/ovh/ovhcloud-cli/internal/assets" "github.com/ovh/ovhcloud-cli/internal/services/ip" "github.com/spf13/cobra" ) @@ -73,5 +74,134 @@ func init() { } ipReverseCmd.AddCommand(ipReverseDeleteCmd) + // Firewall commands + ipFirewallCmd := &cobra.Command{ + Use: "firewall", + Short: "Manage firewall (Edge Firewall) on the given IP", + } + ipCmd.AddCommand(ipFirewallCmd) + + ipFirewallCmd.AddCommand(withFilterFlag(&cobra.Command{ + Use: "list ", + Aliases: []string{"ls"}, + Short: "List IPs registered on the firewall", + Args: cobra.ExactArgs(1), + Run: ip.ListFirewall, + })) + + ipFirewallCmd.AddCommand(&cobra.Command{ + Use: "add ", + Short: "Add an IP to the firewall", + Args: cobra.ExactArgs(2), + Run: ip.AddFirewall, + }) + + ipFirewallCmd.AddCommand(&cobra.Command{ + Use: "get ", + Short: "Get firewall status for a specific IP", + Args: cobra.ExactArgs(2), + Run: ip.GetFirewall, + }) + + ipFirewallCmd.AddCommand(&cobra.Command{ + Use: "enable ", + Short: "Enable the firewall on the given IP", + Args: cobra.ExactArgs(2), + Run: ip.EnableFirewall, + }) + + ipFirewallCmd.AddCommand(&cobra.Command{ + Use: "disable ", + Short: "Disable the firewall on the given IP", + Args: cobra.ExactArgs(2), + Run: ip.DisableFirewall, + }) + + ipFirewallCmd.AddCommand(&cobra.Command{ + Use: "delete ", + Short: "Remove IP and all rules from firewall", + Args: cobra.ExactArgs(2), + Run: ip.DeleteFirewall, + }) + + // Firewall rule sub-commands + ipFirewallRuleCmd := &cobra.Command{ + Use: "rule", + Short: "Manage firewall rules", + } + ipFirewallCmd.AddCommand(ipFirewallRuleCmd) + + ipFirewallRuleCmd.AddCommand(withFilterFlag(&cobra.Command{ + Use: "list ", + Aliases: []string{"ls"}, + Short: "List firewall rules for the given IP", + Args: cobra.ExactArgs(2), + Run: ip.ListFirewallRules, + })) + + ipFirewallRuleCmd.AddCommand(&cobra.Command{ + Use: "get ", + Short: "Get a specific firewall rule", + Args: cobra.ExactArgs(3), + Run: ip.GetFirewallRule, + }) + + ipFirewallRuleCreateCmd := &cobra.Command{ + Use: "create ", + Short: "Create a new firewall rule", + Long: `Use this command to create a new firewall rule. +There are three ways to define the creation parameters: + +1. Using only CLI flags: + + ovhcloud ip firewall rule create --action permit --protocol tcp --sequence 0 --destination-port 443 + +2. Using a configuration file: + + First generate an example parameters file: + + ovhcloud ip firewall rule create --init-file ./rule.json + + After editing the file, run: + + ovhcloud ip firewall rule create --from-file ./rule.json + + You can also pipe the content: + + cat ./rule.json | ovhcloud ip firewall rule create + +3. Using your default text editor: + + ovhcloud ip firewall rule create --editor +`, + Args: cobra.ExactArgs(2), + RunE: ip.CreateFirewallRule, + } + ipFirewallRuleCreateCmd.Flags().StringVar(&ip.FirewallRuleSpec.Action, "action", "", "Action: deny or permit (required)") + ipFirewallRuleCreateCmd.Flags().StringVar(&ip.FirewallRuleSpec.Protocol, "protocol", "", "Protocol: ah, esp, gre, icmp, ipv4, tcp, udp (required)") + ipFirewallRuleCreateCmd.Flags().IntVar(&ip.FirewallRuleSpec.Sequence, "sequence", -1, "Rule priority 0-19 (required)") + ipFirewallRuleCreateCmd.Flags().StringVar(&ip.FirewallRuleSpec.Source, "source", "", "Source IP/CIDR (defaults to any)") + ipFirewallRuleCreateCmd.Flags().IntVar(&ip.FirewallRuleSpec.DestinationPort, "destination-port", 0, "Destination port (TCP/UDP only)") + ipFirewallRuleCreateCmd.Flags().IntVar(&ip.FirewallRuleSpec.DestinationPortFrom, "destination-port-from", 0, "Destination port range start (mutually exclusive with --destination-port)") + ipFirewallRuleCreateCmd.Flags().IntVar(&ip.FirewallRuleSpec.DestinationPortTo, "destination-port-to", 0, "Destination port range end") + ipFirewallRuleCreateCmd.Flags().IntVar(&ip.FirewallRuleSpec.SourcePort, "source-port", 0, "Source port (TCP/UDP only)") + ipFirewallRuleCreateCmd.Flags().IntVar(&ip.FirewallRuleSpec.SourcePortFrom, "source-port-from", 0, "Source port range start (mutually exclusive with --source-port)") + ipFirewallRuleCreateCmd.Flags().IntVar(&ip.FirewallRuleSpec.SourcePortTo, "source-port-to", 0, "Source port range end") + ipFirewallRuleCreateCmd.Flags().BoolVar(&ip.FirewallRuleSpec.TCPFragments, "tcp-fragments", false, "TCP fragments option") + ipFirewallRuleCreateCmd.Flags().StringVar(&ip.FirewallRuleSpec.TCPOption, "tcp-option", "", "TCP option: established or syn (TCP only)") + ipFirewallRuleCreateCmd.MarkFlagsMutuallyExclusive("destination-port", "destination-port-from") + ipFirewallRuleCreateCmd.MarkFlagsMutuallyExclusive("source-port", "source-port-from") + addParameterFileFlags(ipFirewallRuleCreateCmd, false, assets.IpOpenapiSchema, "/ip/{ip}/firewall/{ipOnFirewall}/rule", "post", ip.FirewallRuleCreateExample, nil) + addInteractiveEditorFlag(ipFirewallRuleCreateCmd) + ipFirewallRuleCreateCmd.MarkFlagsMutuallyExclusive("from-file", "editor") + ipFirewallRuleCmd.AddCommand(ipFirewallRuleCreateCmd) + + ipFirewallRuleCmd.AddCommand(&cobra.Command{ + Use: "delete ", + Short: "Delete a firewall rule", + Args: cobra.ExactArgs(3), + Run: ip.DeleteFirewallRule, + }) + rootCmd.AddCommand(ipCmd) } diff --git a/internal/cmd/ip_firewall_test.go b/internal/cmd/ip_firewall_test.go new file mode 100644 index 00000000..5cb2cb9c --- /dev/null +++ b/internal/cmd/ip_firewall_test.go @@ -0,0 +1,359 @@ +// SPDX-FileCopyrightText: 2025 OVH SAS +// +// SPDX-License-Identifier: Apache-2.0 + +package cmd_test + +import ( + "encoding/json" + "os" + + "github.com/jarcoal/httpmock" + "github.com/maxatome/go-testdeep/td" + "github.com/maxatome/tdhttpmock" + "github.com/ovh/ovhcloud-cli/internal/cmd" +) + +const ( + testFirewallIPBlock = "198.51.100.42/32" + testFirewallIP = "198.51.100.42" +) + +func (ms *MockSuite) TestIpFirewallListCmd(assert, require *td.T) { + httpmock.RegisterResponder("GET", + "https://eu.api.ovh.com/v1/ip/198.51.100.42%2F32/firewall", + httpmock.NewStringResponder(200, `["`+testFirewallIP+`"]`).Once()) + + httpmock.RegisterResponder("GET", + "https://eu.api.ovh.com/v1/ip/198.51.100.42%2F32/firewall/198.51.100.42", + httpmock.NewStringResponder(200, `{ + "ipOnFirewall": "`+testFirewallIP+`", + "enabled": true, + "state": "ok" + }`).Once()) + + out, err := cmd.Execute("ip", "firewall", "list", testFirewallIPBlock, "-o", "json") + + require.CmpNoError(err) + assert.Cmp(json.RawMessage(out), td.JSON(`[ + { + "ipOnFirewall": "`+testFirewallIP+`", + "enabled": true, + "state": "ok" + } + ]`)) +} + +func (ms *MockSuite) TestIpFirewallAddCmd(assert, require *td.T) { + httpmock.RegisterMatcherResponder("POST", + "https://eu.api.ovh.com/v1/ip/198.51.100.42%2F32/firewall", + tdhttpmock.JSONBody(td.JSON(`{"ipOnFirewall": "`+testFirewallIP+`"}`)), + httpmock.NewStringResponder(200, `{ + "ipOnFirewall": "`+testFirewallIP+`", + "enabled": false, + "state": "ok" + }`), + ) + + out, err := cmd.Execute("ip", "firewall", "add", testFirewallIPBlock, testFirewallIP) + + require.CmpNoError(err) + assert.String(out, "✅ IP "+testFirewallIP+" successfully added to firewall") +} + +func (ms *MockSuite) TestIpFirewallGetCmd(assert, require *td.T) { + httpmock.RegisterResponder("GET", + "https://eu.api.ovh.com/v1/ip/198.51.100.42%2F32/firewall/198.51.100.42", + httpmock.NewStringResponder(200, `{ + "ipOnFirewall": "`+testFirewallIP+`", + "enabled": true, + "state": "ok" + }`).Once()) + + out, err := cmd.Execute("ip", "firewall", "get", testFirewallIPBlock, testFirewallIP, "-o", "json") + + require.CmpNoError(err) + assert.Cmp(json.RawMessage(out), td.JSON(`{ + "ipOnFirewall": "`+testFirewallIP+`", + "enabled": true, + "state": "ok" + }`)) +} + +func (ms *MockSuite) TestIpFirewallEnableCmd(assert, require *td.T) { + httpmock.RegisterMatcherResponder("PUT", + "https://eu.api.ovh.com/v1/ip/198.51.100.42%2F32/firewall/198.51.100.42", + tdhttpmock.JSONBody(td.JSON(`{"enabled": true}`)), + httpmock.NewStringResponder(200, ``), + ) + + out, err := cmd.Execute("ip", "firewall", "enable", testFirewallIPBlock, testFirewallIP) + + require.CmpNoError(err) + assert.String(out, "✅ Firewall successfully enabled for "+testFirewallIP) +} + +func (ms *MockSuite) TestIpFirewallDisableCmd(assert, require *td.T) { + httpmock.RegisterMatcherResponder("PUT", + "https://eu.api.ovh.com/v1/ip/198.51.100.42%2F32/firewall/198.51.100.42", + tdhttpmock.JSONBody(td.JSON(`{"enabled": false}`)), + httpmock.NewStringResponder(200, ``), + ) + + out, err := cmd.Execute("ip", "firewall", "disable", testFirewallIPBlock, testFirewallIP) + + require.CmpNoError(err) + assert.String(out, "✅ Firewall successfully disabled for "+testFirewallIP) +} + +func (ms *MockSuite) TestIpFirewallDeleteCmd(assert, require *td.T) { + httpmock.RegisterResponder("DELETE", + "https://eu.api.ovh.com/v1/ip/198.51.100.42%2F32/firewall/198.51.100.42", + httpmock.NewStringResponder(200, ``), + ) + + out, err := cmd.Execute("ip", "firewall", "delete", testFirewallIPBlock, testFirewallIP) + + require.CmpNoError(err) + assert.String(out, "✅ Firewall and all rules successfully removed for "+testFirewallIP) +} + +func (ms *MockSuite) TestIpFirewallRuleListCmd(assert, require *td.T) { + httpmock.RegisterResponder("GET", + "https://eu.api.ovh.com/v1/ip/198.51.100.42%2F32/firewall/198.51.100.42/rule", + httpmock.NewStringResponder(200, `[5, 19]`).Once()) + + httpmock.RegisterResponder("GET", + "https://eu.api.ovh.com/v1/ip/198.51.100.42%2F32/firewall/198.51.100.42/rule/5", + httpmock.NewStringResponder(200, `{ + "sequence": 5, + "action": "permit", + "protocol": "tcp", + "source": "192.0.2.1/32", + "destination": "`+testFirewallIPBlock+`", + "destinationPort": "eq 22", + "sourcePort": null, + "rule": "permit tcp 192.0.2.1/32 `+testFirewallIPBlock+` eq 22", + "state": "ok", + "creationDate": "2026-03-03T19:43:08+01:00", + "tcpOption": null, + "fragments": false, + "l3PacketLength": null + }`).Once()) + + httpmock.RegisterResponder("GET", + "https://eu.api.ovh.com/v1/ip/198.51.100.42%2F32/firewall/198.51.100.42/rule/19", + httpmock.NewStringResponder(200, `{ + "sequence": 19, + "action": "deny", + "protocol": "tcp", + "source": "any", + "destination": "`+testFirewallIPBlock+`", + "destinationPort": "eq 22", + "sourcePort": null, + "rule": "deny tcp any `+testFirewallIPBlock+` eq 22", + "state": "ok", + "creationDate": "2025-08-11T13:16:53+02:00", + "tcpOption": null, + "fragments": false, + "l3PacketLength": null + }`).Once()) + + out, err := cmd.Execute("ip", "firewall", "rule", "list", + testFirewallIPBlock, testFirewallIP, "-o", "json") + + require.CmpNoError(err) + assert.Cmp(json.RawMessage(out), td.JSON(`[ + { + "sequence": 5, + "action": "permit", + "protocol": "tcp", + "source": "192.0.2.1/32", + "destination": "`+testFirewallIPBlock+`", + "destinationPort": "eq 22", + "sourcePort": null, + "rule": "permit tcp 192.0.2.1/32 `+testFirewallIPBlock+` eq 22", + "state": "ok", + "creationDate": "2026-03-03T19:43:08+01:00", + "tcpOption": null, + "fragments": false, + "l3PacketLength": null + }, + { + "sequence": 19, + "action": "deny", + "protocol": "tcp", + "source": "any", + "destination": "`+testFirewallIPBlock+`", + "destinationPort": "eq 22", + "sourcePort": null, + "rule": "deny tcp any `+testFirewallIPBlock+` eq 22", + "state": "ok", + "creationDate": "2025-08-11T13:16:53+02:00", + "tcpOption": null, + "fragments": false, + "l3PacketLength": null + } + ]`)) +} + +func (ms *MockSuite) TestIpFirewallRuleGetCmd(assert, require *td.T) { + httpmock.RegisterResponder("GET", + "https://eu.api.ovh.com/v1/ip/198.51.100.42%2F32/firewall/198.51.100.42/rule/5", + httpmock.NewStringResponder(200, `{ + "sequence": 5, + "action": "permit", + "protocol": "tcp", + "source": "192.0.2.1/32", + "destination": "`+testFirewallIPBlock+`", + "destinationPort": "eq 22", + "sourcePort": null, + "rule": "permit tcp 192.0.2.1/32 `+testFirewallIPBlock+` eq 22", + "state": "ok", + "creationDate": "2026-03-03T19:43:08+01:00", + "tcpOption": null, + "fragments": false, + "l3PacketLength": null + }`).Once()) + + out, err := cmd.Execute("ip", "firewall", "rule", "get", + testFirewallIPBlock, testFirewallIP, "5", "-o", "json") + + require.CmpNoError(err) + assert.Cmp(json.RawMessage(out), td.JSON(`{ + "sequence": 5, + "action": "permit", + "protocol": "tcp", + "source": "192.0.2.1/32", + "destination": "`+testFirewallIPBlock+`", + "destinationPort": "eq 22", + "sourcePort": null, + "rule": "permit tcp 192.0.2.1/32 `+testFirewallIPBlock+` eq 22", + "state": "ok", + "creationDate": "2026-03-03T19:43:08+01:00", + "tcpOption": null, + "fragments": false, + "l3PacketLength": null + }`)) +} + +func (ms *MockSuite) TestIpFirewallRuleCreateCmd(assert, require *td.T) { + httpmock.RegisterMatcherResponder("POST", + "https://eu.api.ovh.com/v1/ip/198.51.100.42%2F32/firewall/198.51.100.42/rule", + tdhttpmock.JSONBody(td.JSON(`{ + "action": "permit", + "protocol": "tcp", + "sequence": 0, + "source": "10.0.0.1/32", + "destinationPort": 443 + }`)), + httpmock.NewStringResponder(200, `{ + "sequence": 0, + "action": "permit", + "protocol": "tcp", + "source": "10.0.0.1/32", + "destination": "`+testFirewallIPBlock+`", + "destinationPort": "eq 443", + "rule": "permit tcp 10.0.0.1/32 `+testFirewallIPBlock+` eq 443", + "state": "creationPending" + }`), + ) + + out, err := cmd.Execute("ip", "firewall", "rule", "create", + testFirewallIPBlock, testFirewallIP, + "--action", "permit", + "--protocol", "tcp", + "--sequence", "0", + "--source", "10.0.0.1/32", + "--destination-port", "443", + ) + + require.CmpNoError(err) + assert.String(out, "✅ Rule #0 successfully created") +} + +func (ms *MockSuite) TestIpFirewallRuleCreateFromFile(assert, require *td.T) { + // Create a temp JSON file with rule parameters + tmpFile, err := os.CreateTemp("", "firewall-rule-*.json") + require.CmpNoError(err) + defer os.Remove(tmpFile.Name()) + + _, err = tmpFile.WriteString(`{ + "action": "permit", + "protocol": "tcp", + "sequence": 0, + "source": "10.0.0.1/32", + "destinationPort": 443 + }`) + require.CmpNoError(err) + tmpFile.Close() + + httpmock.RegisterMatcherResponder("POST", + "https://eu.api.ovh.com/v1/ip/198.51.100.42%2F32/firewall/198.51.100.42/rule", + tdhttpmock.JSONBody(td.JSON(`{ + "action": "permit", + "protocol": "tcp", + "sequence": 0, + "source": "10.0.0.1/32", + "destinationPort": 443 + }`)), + httpmock.NewStringResponder(200, `{ + "sequence": 0, + "action": "permit", + "protocol": "tcp", + "source": "10.0.0.1/32", + "destination": "`+testFirewallIPBlock+`", + "destinationPort": "eq 443", + "rule": "permit tcp 10.0.0.1/32 `+testFirewallIPBlock+` eq 443", + "state": "creationPending" + }`), + ) + + out, err := cmd.Execute("ip", "firewall", "rule", "create", + testFirewallIPBlock, testFirewallIP, + "--from-file", tmpFile.Name(), + ) + + require.CmpNoError(err) + assert.String(out, "✅ Rule #0 successfully created") +} + +func (ms *MockSuite) TestIpFirewallRuleDeleteCmd(assert, require *td.T) { + httpmock.RegisterResponder("DELETE", + "https://eu.api.ovh.com/v1/ip/198.51.100.42%2F32/firewall/198.51.100.42/rule/5", + httpmock.NewStringResponder(200, ``), + ) + + out, err := cmd.Execute("ip", "firewall", "rule", "delete", + testFirewallIPBlock, testFirewallIP, "5") + + require.CmpNoError(err) + assert.String(out, "✅ Rule #5 successfully deleted") +} + +func (ms *MockSuite) TestIpFirewallRuleCreatePortExcl(assert, require *td.T) { + _, err := cmd.Execute("ip", "firewall", "rule", "create", + testFirewallIPBlock, testFirewallIP, + "--action", "permit", + "--protocol", "tcp", + "--sequence", "0", + "--destination-port", "443", + "--destination-port-from", "80", + ) + + require.CmpError(err) + assert.Contains(err.Error(), "destination-port") +} + +func (ms *MockSuite) TestIpFirewallRuleCreateTcpOnly(assert, require *td.T) { + _, err := cmd.Execute("ip", "firewall", "rule", "create", + testFirewallIPBlock, testFirewallIP, + "--action", "deny", + "--protocol", "udp", + "--sequence", "1", + "--tcp-option", "established", + ) + + require.CmpError(err) + assert.Contains(err.Error(), "tcp-option") +} diff --git a/internal/services/ip/firewall.go b/internal/services/ip/firewall.go new file mode 100644 index 00000000..cf72a777 --- /dev/null +++ b/internal/services/ip/firewall.go @@ -0,0 +1,412 @@ +// SPDX-FileCopyrightText: 2025 OVH SAS +// +// SPDX-License-Identifier: Apache-2.0 + +package ip + +import ( + "bufio" + _ "embed" + "encoding/json" + "fmt" + "net/url" + "os" + "strings" + + "github.com/ovh/ovhcloud-cli/internal/display" + filtersLib "github.com/ovh/ovhcloud-cli/internal/filters" + "github.com/ovh/ovhcloud-cli/internal/flags" + httpLib "github.com/ovh/ovhcloud-cli/internal/http" + "github.com/ovh/ovhcloud-cli/internal/utils" + "github.com/spf13/cobra" +) + +const FirewallRuleCreateExample = `{ + "action": "permit", + "protocol": "tcp", + "sequence": 0, + "source": "10.0.0.0/8", + "destinationPort": 443 +}` + +var ( + firewallColumnsToDisplay = []string{"ipOnFirewall", "enabled", "state"} + + firewallRuleColumnsToDisplay = []string{ + "sequence", "action", "protocol", "source", + "destinationPort", "rule", "state", + } + + //go:embed templates/firewall.tmpl + firewallTemplate string + + //go:embed templates/firewall_rule.tmpl + firewallRuleTemplate string + + FirewallRuleSpec struct { + Action string `json:"action"` + Protocol string `json:"protocol"` + Sequence int `json:"sequence"` + Source string `json:"source,omitempty"` + DestinationPort int `json:"destinationPort,omitempty"` + DestinationPortFrom int `json:"-"` + DestinationPortTo int `json:"-"` + SourcePort int `json:"sourcePort,omitempty"` + SourcePortFrom int `json:"-"` + SourcePortTo int `json:"-"` + TCPFragments bool `json:"-"` + TCPOption string `json:"-"` + } +) + +// ListFirewall lists IPs registered on the firewall for the given IP block. +// API: GET /v1/ip/{ip}/firewall +func ListFirewall(_ *cobra.Command, args []string) { + baseURL := fmt.Sprintf("/v1/ip/%s/firewall", url.PathEscape(args[0])) + + ids, err := httpLib.FetchArray(baseURL, "") + if err != nil { + display.OutputError(&flags.OutputFormatConfig, "failed to fetch firewall list: %s", err) + return + } + + // FetchObjectsParallel uses fmt.Sprintf(fmtPath, id), so we must escape any + // literal '%' characters in the base URL (e.g. %2F from url.PathEscape). + fmtPath := strings.ReplaceAll(baseURL, "%", "%%") + "/%s" + objects, err := httpLib.FetchObjectsParallel[map[string]any](fmtPath, ids, flags.IgnoreErrors) + if err != nil { + display.OutputError(&flags.OutputFormatConfig, "failed to fetch firewall details: %s", err) + return + } + + var body []map[string]any + for _, obj := range objects { + if obj != nil { + body = append(body, obj) + } + } + + body, err = filtersLib.FilterLines(body, flags.GenericFilters) + if err != nil { + display.OutputError(&flags.OutputFormatConfig, "failed to filter results: %s", err) + return + } + + display.RenderTable(body, firewallColumnsToDisplay, &flags.OutputFormatConfig) +} + +// AddFirewall adds an IP to the firewall. +// API: POST /v1/ip/{ip}/firewall +func AddFirewall(_ *cobra.Command, args []string) { + apiURL := fmt.Sprintf("/v1/ip/%s/firewall", url.PathEscape(args[0])) + if err := httpLib.Client.Post(apiURL, map[string]string{"ipOnFirewall": args[1]}, nil); err != nil { + display.OutputError(&flags.OutputFormatConfig, "%s", err) + return + } + + display.OutputInfo(&flags.OutputFormatConfig, nil, "✅ IP %s successfully added to firewall", args[1]) +} + +// GetFirewall shows firewall status for a specific IP. +// API: GET /v1/ip/{ip}/firewall/{ipOnFirewall} +func GetFirewall(_ *cobra.Command, args []string) { + apiURL := fmt.Sprintf("/v1/ip/%s/firewall/%s", url.PathEscape(args[0]), url.PathEscape(args[1])) + + var object map[string]any + if err := httpLib.Client.Get(apiURL, &object); err != nil { + display.OutputError(&flags.OutputFormatConfig, "error fetching firewall for %s: %s", args[1], err) + return + } + + display.OutputObject(object, args[1], firewallTemplate, &flags.OutputFormatConfig) +} + +// EnableFirewall enables the firewall on an IP. +// API: PUT /v1/ip/{ip}/firewall/{ipOnFirewall} +func EnableFirewall(_ *cobra.Command, args []string) { + apiURL := fmt.Sprintf("/v1/ip/%s/firewall/%s", url.PathEscape(args[0]), url.PathEscape(args[1])) + if err := httpLib.Client.Put(apiURL, map[string]bool{"enabled": true}, nil); err != nil { + display.OutputError(&flags.OutputFormatConfig, "%s", err) + return + } + + display.OutputInfo(&flags.OutputFormatConfig, nil, "✅ Firewall successfully enabled for %s", args[1]) +} + +// DisableFirewall disables the firewall on an IP. +// API: PUT /v1/ip/{ip}/firewall/{ipOnFirewall} +func DisableFirewall(_ *cobra.Command, args []string) { + apiURL := fmt.Sprintf("/v1/ip/%s/firewall/%s", url.PathEscape(args[0]), url.PathEscape(args[1])) + if err := httpLib.Client.Put(apiURL, map[string]bool{"enabled": false}, nil); err != nil { + display.OutputError(&flags.OutputFormatConfig, "%s", err) + return + } + + display.OutputInfo(&flags.OutputFormatConfig, nil, "✅ Firewall successfully disabled for %s", args[1]) +} + +// DeleteFirewall removes an IP and all its rules from the firewall. +// API: DELETE /v1/ip/{ip}/firewall/{ipOnFirewall} +func DeleteFirewall(_ *cobra.Command, args []string) { + apiURL := fmt.Sprintf("/v1/ip/%s/firewall/%s", url.PathEscape(args[0]), url.PathEscape(args[1])) + if err := httpLib.Client.Delete(apiURL, nil); err != nil { + display.OutputError(&flags.OutputFormatConfig, "%s", err) + return + } + + display.OutputInfo(&flags.OutputFormatConfig, nil, "✅ Firewall and all rules successfully removed for %s", args[1]) +} + +// ListFirewallRules lists all firewall rules for the given IP. +// API: GET /v1/ip/{ip}/firewall/{ipOnFirewall}/rule +func ListFirewallRules(_ *cobra.Command, args []string) { + baseURL := fmt.Sprintf("/v1/ip/%s/firewall/%s/rule", + url.PathEscape(args[0]), url.PathEscape(args[1])) + + ids, err := httpLib.FetchArray(baseURL, "") + if err != nil { + display.OutputError(&flags.OutputFormatConfig, "failed to fetch firewall rules: %s", err) + return + } + + // FetchObjectsParallel uses fmt.Sprintf(fmtPath, id), so we must escape any + // literal '%' characters in the base URL (e.g. %2F from url.PathEscape). + fmtPath := strings.ReplaceAll(baseURL, "%", "%%") + "/%s" + objects, err := httpLib.FetchObjectsParallel[map[string]any](fmtPath, ids, flags.IgnoreErrors) + if err != nil { + display.OutputError(&flags.OutputFormatConfig, "failed to fetch rule details: %s", err) + return + } + + var body []map[string]any + for _, obj := range objects { + if obj != nil { + body = append(body, obj) + } + } + + body, err = filtersLib.FilterLines(body, flags.GenericFilters) + if err != nil { + display.OutputError(&flags.OutputFormatConfig, "failed to filter results: %s", err) + return + } + + display.RenderTable(body, firewallRuleColumnsToDisplay, &flags.OutputFormatConfig) +} + +// GetFirewallRule gets details of a specific firewall rule. +// API: GET /v1/ip/{ip}/firewall/{ipOnFirewall}/rule/{sequence} +func GetFirewallRule(_ *cobra.Command, args []string) { + apiURL := fmt.Sprintf("/v1/ip/%s/firewall/%s/rule/%s", + url.PathEscape(args[0]), url.PathEscape(args[1]), args[2]) + + var object map[string]any + if err := httpLib.Client.Get(apiURL, &object); err != nil { + display.OutputError(&flags.OutputFormatConfig, "error fetching rule #%s: %s", args[2], err) + return + } + + display.OutputObject(object, args[2], firewallRuleTemplate, &flags.OutputFormatConfig) +} + +// CreateFirewallRule creates a new firewall rule. +// API: POST /v1/ip/{ip}/firewall/{ipOnFirewall}/rule +// Uses RunE signature so client-side validation errors are returned to cobra +// (which prints them and exits cleanly without calling os.Exit). +func CreateFirewallRule(cmd *cobra.Command, args []string) error { + apiURL := fmt.Sprintf("/v1/ip/%s/firewall/%s/rule", url.PathEscape(args[0]), url.PathEscape(args[1])) + + // If --from-file or pipe: load base params, merge explicit CLI flags on top, POST directly + usingFile := flags.ParametersFile != "" || utils.IsInputFromPipe() + if usingFile { + var fileData []byte + var err error + if utils.IsInputFromPipe() { + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + fileData = append(fileData, scanner.Bytes()...) + } + if err = scanner.Err(); err != nil { + return fmt.Errorf("failed to read from pipe: %w", err) + } + } else { + fileData, err = os.ReadFile(flags.ParametersFile) + if err != nil { + return fmt.Errorf("failed to read file %s: %w", flags.ParametersFile, err) + } + } + + var body map[string]any + if err := json.Unmarshal(fileData, &body); err != nil { + return fmt.Errorf("failed to parse parameters: %w", err) + } + + // Override with any explicitly set CLI flags + if cmd.Flags().Changed("action") { + body["action"] = FirewallRuleSpec.Action + } + if cmd.Flags().Changed("protocol") { + body["protocol"] = FirewallRuleSpec.Protocol + } + if cmd.Flags().Changed("sequence") { + body["sequence"] = FirewallRuleSpec.Sequence + } + if cmd.Flags().Changed("source") { + body["source"] = FirewallRuleSpec.Source + } + if cmd.Flags().Changed("destination-port") { + body["destinationPort"] = FirewallRuleSpec.DestinationPort + } + if cmd.Flags().Changed("destination-port-from") || cmd.Flags().Changed("destination-port-to") { + body["destinationPortRange"] = map[string]int{ + "from": FirewallRuleSpec.DestinationPortFrom, + "to": FirewallRuleSpec.DestinationPortTo, + } + } + if cmd.Flags().Changed("source-port") { + body["sourcePort"] = FirewallRuleSpec.SourcePort + } + if cmd.Flags().Changed("source-port-from") || cmd.Flags().Changed("source-port-to") { + body["sourcePortRange"] = map[string]int{ + "from": FirewallRuleSpec.SourcePortFrom, + "to": FirewallRuleSpec.SourcePortTo, + } + } + if cmd.Flags().Changed("tcp-fragments") || cmd.Flags().Changed("tcp-option") { + tcpOpt := map[string]any{} + if cmd.Flags().Changed("tcp-fragments") { + tcpOpt["fragments"] = FirewallRuleSpec.TCPFragments + } + if cmd.Flags().Changed("tcp-option") { + tcpOpt["option"] = FirewallRuleSpec.TCPOption + } + body["tcpOption"] = tcpOpt + } + + // Validate required fields are present (from file or CLI) + for _, field := range []string{"action", "protocol", "sequence"} { + if _, ok := body[field]; !ok { + return fmt.Errorf("required field %q is missing (must be provided via file or --%s flag)", field, field) + } + } + + var createdRule map[string]any + if err := httpLib.Client.Post(apiURL, body, &createdRule); err != nil { + display.OutputError(&flags.OutputFormatConfig, "error creating rule: %s", err) + return nil + } + display.OutputInfo(&flags.OutputFormatConfig, nil, "✅ Rule #%v successfully created", createdRule["sequence"]) + return nil + } + + // --- CLI-flags-only path: validate and build body manually --- + + // Validate required flags were provided + for _, flagName := range []string{"action", "protocol", "sequence"} { + if !cmd.Flags().Changed(flagName) { + return fmt.Errorf("required flag %q not set", flagName) + } + } + + // Client-side validation + validActions := map[string]bool{"deny": true, "permit": true} + if !validActions[FirewallRuleSpec.Action] { + return fmt.Errorf("invalid action %q: must be 'deny' or 'permit'", FirewallRuleSpec.Action) + } + + validProtocols := map[string]bool{ + "ah": true, "esp": true, "gre": true, "icmp": true, "ipv4": true, "tcp": true, "udp": true, + } + if !validProtocols[FirewallRuleSpec.Protocol] { + return fmt.Errorf("invalid protocol %q: must be one of ah, esp, gre, icmp, ipv4, tcp, udp", FirewallRuleSpec.Protocol) + } + + if FirewallRuleSpec.Sequence < 0 || FirewallRuleSpec.Sequence > 19 { + return fmt.Errorf("invalid sequence %d: must be between 0 and 19", FirewallRuleSpec.Sequence) + } + + tcpUDP := FirewallRuleSpec.Protocol == "tcp" || FirewallRuleSpec.Protocol == "udp" + + if !tcpUDP { + for _, portFlag := range []string{"destination-port", "destination-port-from", "destination-port-to", "source-port", "source-port-from", "source-port-to"} { + if cmd.Flags().Changed(portFlag) { + return fmt.Errorf("port options are only valid for TCP and UDP protocols") + } + } + } + + if FirewallRuleSpec.Protocol != "tcp" { + for _, tcpFlag := range []string{"tcp-option", "tcp-fragments"} { + if cmd.Flags().Changed(tcpFlag) { + return fmt.Errorf("--%s is only valid for TCP protocol", tcpFlag) + } + } + } + + // Build POST body + body := map[string]any{ + "action": FirewallRuleSpec.Action, + "protocol": FirewallRuleSpec.Protocol, + "sequence": FirewallRuleSpec.Sequence, + } + + if cmd.Flags().Changed("source") { + body["source"] = FirewallRuleSpec.Source + } + + if cmd.Flags().Changed("destination-port") { + body["destinationPort"] = FirewallRuleSpec.DestinationPort + } else if cmd.Flags().Changed("destination-port-from") || cmd.Flags().Changed("destination-port-to") { + body["destinationPortRange"] = map[string]int{ + "from": FirewallRuleSpec.DestinationPortFrom, + "to": FirewallRuleSpec.DestinationPortTo, + } + } + + if cmd.Flags().Changed("source-port") { + body["sourcePort"] = FirewallRuleSpec.SourcePort + } else if cmd.Flags().Changed("source-port-from") || cmd.Flags().Changed("source-port-to") { + body["sourcePortRange"] = map[string]int{ + "from": FirewallRuleSpec.SourcePortFrom, + "to": FirewallRuleSpec.SourcePortTo, + } + } + + if FirewallRuleSpec.Protocol == "tcp" { + tcpFragmentsChanged := cmd.Flags().Changed("tcp-fragments") + tcpOptionChanged := cmd.Flags().Changed("tcp-option") + + if tcpFragmentsChanged || tcpOptionChanged { + tcpOption := map[string]any{} + if tcpFragmentsChanged { + tcpOption["fragments"] = FirewallRuleSpec.TCPFragments + } + if tcpOptionChanged { + tcpOption["option"] = FirewallRuleSpec.TCPOption + } + body["tcpOption"] = tcpOption + } + } + + var createdRule map[string]any + if err := httpLib.Client.Post(apiURL, body, &createdRule); err != nil { + display.OutputError(&flags.OutputFormatConfig, "error creating rule: %s", err) + return nil + } + + display.OutputInfo(&flags.OutputFormatConfig, nil, "✅ Rule #%v successfully created", createdRule["sequence"]) + return nil +} + +// DeleteFirewallRule deletes a firewall rule. +// API: DELETE /v1/ip/{ip}/firewall/{ipOnFirewall}/rule/{sequence} +func DeleteFirewallRule(_ *cobra.Command, args []string) { + apiURL := fmt.Sprintf("/v1/ip/%s/firewall/%s/rule/%s", + url.PathEscape(args[0]), url.PathEscape(args[1]), args[2]) + if err := httpLib.Client.Delete(apiURL, nil); err != nil { + display.OutputError(&flags.OutputFormatConfig, "%s", err) + return + } + + display.OutputInfo(&flags.OutputFormatConfig, nil, "✅ Rule #%s successfully deleted", args[2]) +} diff --git a/internal/services/ip/templates/firewall.tmpl b/internal/services/ip/templates/firewall.tmpl new file mode 100644 index 00000000..23456f14 --- /dev/null +++ b/internal/services/ip/templates/firewall.tmpl @@ -0,0 +1,10 @@ +Firewall {{.ServiceName}} +======= + +| Key | Value | +| -------------- | ------------------------------------- | +| IP on Firewall | {{index .Result "ipOnFirewall"}} | +| Enabled | {{index .Result "enabled"}} | +| State | {{index .Result "state"}} | + +💡 Use option -o json or -o yaml to get the raw output with all information diff --git a/internal/services/ip/templates/firewall_rule.tmpl b/internal/services/ip/templates/firewall_rule.tmpl new file mode 100644 index 00000000..9b4ccfd8 --- /dev/null +++ b/internal/services/ip/templates/firewall_rule.tmpl @@ -0,0 +1,20 @@ +Rule #{{index .Result "sequence"}} +======= + +| Key | Value | +| ---------------- | ------------------------------------------ | +| Sequence | {{index .Result "sequence"}} | +| Action | {{index .Result "action"}} | +| Protocol | {{index .Result "protocol"}} | +| Source | {{index .Result "source"}} | +| Destination | {{index .Result "destination"}} | +| Destination Port | {{index .Result "destinationPort"}} | +| Source Port | {{index .Result "sourcePort"}} | +| Rule | {{index .Result "rule"}} | +| State | {{index .Result "state"}} | +| Creation Date | {{index .Result "creationDate"}} | +| TCP Option | {{index .Result "tcpOption"}} | +| Fragments | {{index .Result "fragments"}} | +| L3 Packet Length | {{index .Result "l3PacketLength"}} | + +💡 Use option -o json or -o yaml to get the raw output with all information