# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
# This template is for policies restricting which firewall rules
# should exist in a specific location in GCP.
# All firewall rules matching the passed parameters will be flagged as violations.
# All parameters are optional.
#
# A firewall rule will trigger a violation ONLY IF it matches ALL the passed parameters.

apiVersion: templates.gatekeeper.sh/v1alpha1
kind: ConstraintTemplate
metadata:
  name: gcp-restricted-firewall-rules-v1
spec:
  crd:
    spec:
      names:
        kind: GCPRestrictedFirewallRulesConstraintV1
      validation:
        openAPIV3Schema:
          properties:
            mode:
              description: |
                "String identifying the operational mode, allowlist or denylist.
                  allowlist mode: generate violations if any firewall rules do NOT match the passed parameters.
                  denylist mode: generate violations if any firewall rules DO match the passed parameters.
                Default is denylist."
              type: string
              enum: [allowlist, denylist]
            rules:
              description: |
                "List of rules to match (denylist) / not match (allowlist)
                in order to raise violations. Rule are object describing rules (see below)"
              type: array
              items:
                type: object
                properties:
                  direction:
                    description: "Direction of the rules to restrict."
                    type: string
                    enum: ["INGRESS", "EGRESS", "any"]
                  rule_type:
                    description: "Type of the rules to restrict."
                    type: string
                    enum: ["allowed", "denied", "any"]
                  port:
                    description: |
                      "Port of the rules to restrict. This parameter is tighly coupled with the
                      protocol paramter: the rule will be marked as in violation if the parameter
                      protocol:port pair matches (denylist) / does not match (allowlist) one of its
                      protocol:port listed.
                      Valid values are:
                          * single number
                          * ranges (e.g 22-25) - Note: only FW rules with a range greater or equal
                            to this passed range would match
                          * 'any' - when set to any, the ports of the rule will NOT be a factor to
                                    determine if the FW rule is restricted
                          * 'all' (if the rule matches all ports for a given protocol)"
                    type: string
                  protocol:
                    description: |
                      "Protocol of the rules to restrict. This parameter is tighly coupled with
                      the port paramter: the rule will be marked as in violation if the parameter
                      protocol:port pair matches (denylist) / does not match (allowlist)
                      one of its protocol:port listed.
                      Valid values are:
                          * single protocol (e.g 'tcp', 'icmp' etc.)
                          * 'any' - when set to any, the protocol of the rule will NOT be a factor
                                    to determine if the FW rule is restricted
                          * 'all' (if the rule matches all protocols)"
                    type: string
                  source_ranges:
                    description: |
                      "Source ranges of the rules to restrict. A fw rule only matches if ONE of its
                      source CIDR ranges is an exact match of ANY of the CIDR blocks passed in this
                      parameter (denylist) / does not match (allowlist).
                      Valid values are:
                          * 'X.X.X.X/XX' - i.e valid CIDR blocks
                          * 'any' - when set to any, the source ranges of the rule will NOT be a factor
                                    to determine if the FW rule is restricted
                          * '*' - all CIDR blocks match: this only checks that a source range exists
                                  in the restricted FW rule"
                    type: array
                    items:
                      type: string
                  source_tags:
                    description: |
                      "Source tags of the rules to restrict. A fw rule only matches if ONE of its source
                      tags is a match of ANY of the tag patterns (regex) passed in this parameter (denylist)
                      / does not match (allowlist).
                      Valid values are:
                          * 'mytag-*' - valid regex patterns for source tags
                          * 'any' - when set to any, the source tags of the rule will NOT be a factor
                                    to determine if the FW rule is restricted
                          * '*' - all source tags match: this only checks that a source tag exists
                                  in the restricted FW rule"
                    type: array
                    items:
                      type: string
                  source_service_accounts:
                    description: |
                      "Source service accounts of the rules to restrict. A fw rule only matches if ONE of
                      its source service accounts is a match of ANY of the service account
                      patterns (regex) passed in this parameter (denylist) / does not match (allowlist).
                      Valid values are:
                          * 'mytag-*' - valid regex patterns for source tags
                          * 'any' - when set to any, the source tags of the rule will NOT be a factor
                                    to determine if the FW rule is restricted
                          * '*' - all source service accounts match: this only checks that a source
                                  service account existsin the restricted FW rule"
                    type: array
                    items:
                      type: string
                  target_ranges:
                    description: |
                      "Target (or destination) ranges of the rules to restrict. A fw rule only matches
                      if ONE of its destination CIDR ranges is an exact match of ANY of the CIDR blocks
                      passed in this parameter(denylist) / does not match (allowlist).
                      Valid values are:
                          * 'X.X.X.X/XX' - i.e valid CIDR blocks
                          * 'any' - when set to any, the target ranges of the rule will NOT be a factor
                                    to determine if the FW rule is restricted
                          * '*' - all CIDR blocks match: this only checks that a target / destination
                                  range exists in the restricted FW rule"
                    type: array
                    items:
                      type: string
                  target_tags:
                    description: |
                      "Target tags of the rules to restrict. A fw rule only matches if ONE of its target
                      tags is a match of ANY of the tag patterns (regex) passed in this
                      parameter (denylist) / does not match (allowlist).
                      Valid values are:
                          * 'mytag-*' - valid regex patterns for target tags
                          * 'any' - when set to any, the target tags of the rule will NOT be a factor
                                    to determine if the FW rule is restricted
                          * '*' - all target tags match: this only checks that a target tag range exists
                                  in the restricted FW rule"
                    type: array
                    items:
                      type: string
                  target_service_accounts:
                    description: |
                      "Target service accounts of the rules to restrict. A fw rule only matches if ONE of
                        its target service accounts is a match of ANY of the service account
                        patterns (regex) passed in this parameter (denylist) / does not match (allowlist).
                      Valid values are:
                          * 'mytag-*' - valid regex patterns for target tags
                          * 'any' - when set to any, the target tags of the rule will NOT be a factor
                                    to determine if the FW rule is restricted
                          * '*' - all target service account match: this only checks that a target
                                  service account exists in the restricted FW rule"
                    type: array
                    items:
                      type: string
                  enabled:
                    description: "Status of the rules to restrict (enabled vs disabled)."
                    type: string
                    enum: ["true", "false", "any"]
            exemptions_mode:
              type: string
              enum: ["exact", "regex"]
              description: "Match mode for exemptions, exact or regular expression."
            exemptions:
              type: array
              items:
                type: string
              description: "Array of firewall rules to exempt from the rules. String
                values in the array should correspond to the full name values of exempted
                firewall rules if exact exemptions_mode is used."
          required: ["rules"]
  targets:
    validation.gcp.forsetisecurity.org:
      rego: |
        #
        # Copyright 2021 Google LLC
        #
        # Licensed under the Apache License, Version 2.0 (the "License");
        # you may not use this file except in compliance with the License.
        # You may obtain a copy of the License at
        #
        #     https://www.apache.org/licenses/LICENSE-2.0
        #
        # Unless required by applicable law or agreed to in writing, software
        # distributed under the License is distributed on an "AS IS" BASIS,
        # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
        # See the License for the specific language governing permissions and
        # limitations under the License.
        #

        package templates.gcp.GCPRestrictedFirewallRulesConstraintV1

        import data.validator.gcp.lib as lib

        deny[{
        	"msg": message,
        	"details": metadata,
        }] {
        	constraint := input.constraint
        	lib.get_constraint_params(constraint, params)

        	# Update params (set all missing params to their default value)
        	rules_params := update_params(params.rules[_])
        	mode := lib.get_default(params, "mode", "denylist")

        	asset := input.asset
        	asset.asset_type == "compute.googleapis.com/Firewall"

        	match_mode := lib.get_default(params, "exemptions_mode", "exact")
        	check_for_exemptions(asset.name, constraint, match_mode)

        	fw_rule = asset.resource.data

        	is_valid(mode, fw_rule, rules_params)

        	message := sprintf("%s Firewall rule is prohibited.", [asset.name])
        	metadata := {
        		"resource": asset.name,
        		"restricted_rules": rules_params,
        	}
        }

        ###########################
        # Rule Utilities
        ###########################

        # update_params - set all the default to input parameters
        # All parameters are optional. update_params set all missing parameters to "any"
        update_params(params) = updated_params {
        	updated_params := {
        		"direction": lib.get_default(params, "direction", "any"),
        		"rule_type": lib.get_default(params, "rule_type", "any"),
        		"port": lib.get_default(params, "port", "any"),
        		"protocol": lib.get_default(params, "protocol", "any"),
        		"source_ranges": lib.get_default(params, "source_ranges", ["any"]),
        		"target_ranges": lib.get_default(params, "target_ranges", ["any"]),
        		"source_tags": lib.get_default(params, "source_tags", ["any"]),
        		"target_tags": lib.get_default(params, "target_tags", ["any"]),
        		"source_service_accounts": lib.get_default(params, "source_service_accounts", ["any"]),
        		"target_service_accounts": lib.get_default(params, "target_service_accounts", ["any"]),
        		"enabled": lib.get_default(params, "enabled", "any"),
        	}
        }

        is_valid(mode, fw_rule, params) {
        	mode == "denylist"
        	fw_rule_is_restricted(fw_rule, params)
        }

        is_valid(mode, fw_rule, params) {
        	mode == "allowlist"
        	not fw_rule_is_restricted(fw_rule, params)
        }

        # fw_rule_is_restricted for Ingress rules
        fw_rule_is_restricted(fw_rule, params) {
        	# check direction
        	fw_rule_check_direction(fw_rule, params.direction)

        	# check rule type
        	fw_rule_check_rule_type(fw_rule, params.rule_type)

        	ip_configs := fw_rule_get_ip_configs(fw_rule, params.rule_type)

        	# check protocol and port
        	fw_rule_check_protocol_and_port(ip_configs, params.protocol, params.port)

        	# Check sources (ip ranges and/or tags and/or service accounts)
        	fw_rule_check_all_sources(fw_rule, params)

        	# Check targets
        	fw_rule_check_all_targets(fw_rule, params)

        	fw_rule_check_enabled(fw_rule, params.enabled)
        }

        #### Check direction functions

        # fw_rule_check_direction when direction is set to any
        fw_rule_check_direction(fw_rule, direction) {
        	direction == "any"
        }

        # fw_rule_check_direction when direction is not set to any
        fw_rule_check_direction(fw_rule, direction) {
        	direction != "any"
        	lower(direction) == lower(fw_rule.direction)
        }

        #### Check Type functions

        # fw_rule_check_type when rule_type is set to any
        fw_rule_check_rule_type(fw_rule, rule_type) {
        	rule_type == "any"
        }

        # fw_rule_check_direction when direction is not set to any
        fw_rule_check_rule_type(fw_rule, rule_type) {
        	rule_type != "any"

        	# test that the key exists in the fw_rule (allowed or denied)
        	fw_rule[lower(rule_type)]
        }

        ##### Get IP Config from rule

        ### fw_rule_get_ip_configs when rule_type is set to any and rule is allowed type
        fw_rule_get_ip_configs(fw_rule, rule_type) = ip_configs {
        	rule_type == "any"
        	ip_configs = fw_rule.allowed
        }

        ### fw_rule_get_ip_configs when rule_type is set to any and rule is allowed type
        fw_rule_get_ip_configs(fw_rule, rule_type) = ip_configs {
        	rule_type == "any"
        	ip_configs = fw_rule.denied
        }

        ### fw_rule_get_ip_configs when rule_type is not set to any
        fw_rule_get_ip_configs(fw_rule, rule_type) = ip_configs {
        	rule_type != "any"
        	ip_configs = fw_rule[rule_type]
        }

        #### Check protocol + port functions

        # fw_rule_check_protocol when one ip_configs is set to "all"
        fw_rule_check_protocol_and_port(ip_configs, protocol, port) {
        	ip_configs[_].IPProtocol == "all"
        }

        # fw_rule_check_protocol when protocol is set to any
        fw_rule_check_protocol_and_port(ip_configs, protocol, port) {
        	protocol == "any"
        	fw_rule_check_port(ip_configs[_], port)
        }

        # fw_rule_check_protocol when protocol is not set to any
        fw_rule_check_protocol_and_port(ip_configs, protocol, port) {
        	protocol != "any"

        	# Check if the protocol is in the rule
        	ip_configs[i].IPProtocol == protocol

        	# Check if the associated port is also a match
        	fw_rule_check_port(ip_configs[i], port)
        }

        # fw_rule_check_port when protocol is set to any
        fw_rule_check_port(ip_configs, port) {
        	port == "any"
        }

        # fw_rule_check_port when protocol is tcp, udp or all AND port is not set (i.e all ports match)
        fw_rule_check_port(ip_config, port) {
        	protocol_with_ports := {"tcp", "udp", "all"}

        	# only for protocol with ports or "all" - since it includes tcp and udp
        	ip_config.IPProtocol == protocol_with_ports[_]

        	# if port is not set in ip_config, any port passed as a param matches
        	not ip_config.ports
        }

        # fw_rule_check_port when port is all and there are no ports
        fw_rule_check_port(ip_config, port) {
        	port == "all"

        	# check if the port matches
        	not ip_config.ports
        }

        # fw_rule_check_port when port is all and the fw rule exposes ports 0-65535
        fw_rule_check_port(ip_config, port) {
        	port == "all"

        	# check if the port matches
        	range_match("0-65535", ip_config.ports[_])
        }

        # fw_rule_check_port when port is all and the fw rule exposes ports 1-65535
        fw_rule_check_port(ip_config, port) {
        	port == "all"

        	# check if the port matches
        	range_match("1-65535", ip_config.ports[_])
        }

        # fw_rule_check_port when port is a single number
        fw_rule_check_port(ip_config, port) {
        	port != "all"
        	port != "any"
        	not re_match("-", port)

        	# check if the port matches
        	rule_ports := ip_config.ports

        	# check if port is in one of rule_ports values
        	port_is_in_values(port, rule_ports[_])
        }

        # fw_rule_check_port when port is a range (e.g 100-200)
        fw_rule_check_port(ip_config, port) {
        	port != "all"
        	port != "any"
        	re_match("-", port)

        	# check if the port range is included in the fw_rule port
        	rule_ports := ip_config.ports

        	rule_port := rule_ports[_]

        	# check if port range is included in one of rule_ports values
        	# Note: if rule_port is not a range, range_match will return False
        	range_match(port, rule_port)
        }

        # port_is_in_values if rule_port is not a range
        # Note: only called when port is not a range
        port_is_in_values(port, rule_port) {
        	# check if rule_port is not a range
        	not re_match("-", rule_port)

        	# test if rule port matches
        	rule_port == port
        }

        # port_is_in_values if rule_port is a range
        # Note: only called when port is not a range
        port_is_in_values(port, rule_port) {
        	# check if rule_port is a range
        	re_match("-", rule_port)

        	# build a simple port-port range to test if it belongs to rule_port range
        	port_range := sprintf("%s-%s", [port, port])

        	# Check if port is included in rule port
        	range_match(port_range, rule_port)
        }

        # range_match tests if test_range is included in target_range
        # returns true if test_range is equal to, or included in target_range
        range_match(test_range, target_range) {
        	# check if target_range is a range
        	re_match("-", target_range)

        	# check if test_range is a range
        	re_match("-", test_range)

        	# getting the target range bounds
        	target_range_bounds := split(target_range, "-")
        	target_low_bound := to_number(target_range_bounds[0])
        	target_high_bound := to_number(target_range_bounds[1])

        	# getting the test range bounds
        	test_range_bounds := split(test_range, "-")
        	test_low_bound := to_number(test_range_bounds[0])
        	test_high_bound := to_number(test_range_bounds[1])

        	# check if test low bound is >= target low bound and target high bound >= test high bound
        	test_low_bound >= target_low_bound

        	test_high_bound <= target_high_bound
        }

        #### Check sources functions

        # fw_rule_check_sources returns true if all sources matches based on parameters
        # checks for source ranges, tags and service accounts
        fw_rule_check_all_sources(fw_rule, params) {
        	# Check that source ranges AND source tags AND source service accounts match
        	fw_rule_check_source_range(fw_rule, params.source_ranges[_])
        	fw_rule_check_source_tag(fw_rule, params.source_tags[_])
        	fw_rule_check_source_sas(fw_rule, params.source_service_accounts[_])
        }

        # fw_rule_check_source when source range is passed
        fw_rule_check_source_range(fw_rule, source_range) {
        	# test if sourceRanges exists in the rule
        	fw_rule.sourceRanges

        	# check that source ranges are set
        	fw_rule_ranges = fw_rule.sourceRanges

        	# check if any range matches
        	# no CIDR matching logic at this time
        	source_range == fw_rule_ranges[_]
        }

        # fw_rule_check_source_range if source_ranges is set to "*"
        fw_rule_check_source_range(fw_rule, source_range) {
        	source_range == "*"

        	# Check that at least a source range is set
        	fw_rule.sourceRanges
        }

        # fw_rule_check_source_range if source_ranges is set to any (default)
        fw_rule_check_source_range(fw_rule, source_range) {
        	source_range == "any"
        }

        # fw_rule_check_source when source tag is passed and is not "*"
        fw_rule_check_source_tag(fw_rule, source_tag) {
        	source_tag != "*"

        	# check that the rule source tags are set
        	fw_rule_source_tags := fw_rule.sourceTags

        	# check if the input tag matches any tag in the rule
        	re_match(source_tag, fw_rule_source_tags[_])
        }

        # fw_rule_check_source_tag if source tag is set to "*"
        fw_rule_check_source_tag(fw_rule, source_tag) {
        	source_tag == "*"

        	# Verify that we have a source tag set, regardless of its value
        	fw_rule.sourceTags
        }

        # fw_rule_check_source_tag if source tag is set to any (default)
        fw_rule_check_source_tag(fw_rule, source_tag) {
        	source_tag == "any"
        }

        # fw_rule_check_source when source service account is passed and is not "*"
        fw_rule_check_source_sas(fw_rule, source_service_account) {
        	source_service_account != "*"

        	# check that source service accounts are set
        	fw_rule_source_sas = fw_rule.sourceServiceAccounts

        	# check if the rule service account matches
        	re_match(source_service_account, fw_rule_source_sas[_])
        }

        # fw_rule_check_source_sas if source service account is set to "*"
        fw_rule_check_source_sas(fw_rule, source_service_account) {
        	source_service_account == "*"

        	# Verify that we have a source service account set, regardless of its value
        	fw_rule.sourceServiceAccounts
        }

        # fw_rule_check_source_sas if source service account is set to any
        fw_rule_check_source_sas(fw_rule, source_service_account) {
        	source_service_account == "any"
        }

        #### Check targets functions

        # fw_rule_check_targets returns true if all targets match based on parameters
        # checks for target ranges, tags and service accounts
        fw_rule_check_all_targets(fw_rule, params) {
        	# Check that target ranges AND target tags AND target service accounts match
        	fw_rule_check_target_range(fw_rule, params.target_ranges[_])
        	fw_rule_check_target_tag(fw_rule, params.target_tags[_])
        	fw_rule_check_target_sas(fw_rule, params.target_service_accounts[_])
        }

        # fw_rule_check_target when target range is passed
        fw_rule_check_target_range(fw_rule, target_range) {
        	# test if destinationRanges exists in the rule
        	fw_rule.destinationRanges

        	# check that target ranges are set
        	fw_rule_ranges = fw_rule.destinationRanges

        	# check if any range matches
        	# no CIDR matching logic at this time
        	target_range == fw_rule_ranges[_]
        }

        # fw_rule_check_target_range if target_ranges is set to "*"
        fw_rule_check_target_range(fw_rule, target_range) {
        	target_range == "*"

        	# Check that at least a target range is set
        	fw_rule.destinationRanges
        }

        # fw_rule_check_target_range if target_ranges is set to any (default)
        fw_rule_check_target_range(fw_rule, target_range) {
        	target_range == "any"
        }

        # fw_rule_check_target when target tag is passed and is not "*"
        fw_rule_check_target_tag(fw_rule, target_tag) {
        	target_tag != "*"

        	# check that the rule target tags are set
        	fw_rule_target_tags := fw_rule.targetTags

        	# check if the input tag matches any tag in the rule
        	re_match(target_tag, fw_rule_target_tags[_])
        }

        # fw_rule_check_target_tag if target tag is set to "*"
        fw_rule_check_target_tag(fw_rule, target_tag) {
        	target_tag == "*"

        	# Verify that we have a target tag set, regardless of its value
        	fw_rule.targetTags
        }

        # fw_rule_check_target_tag if target tag is set to any (default)
        fw_rule_check_target_tag(fw_rule, target_tag) {
        	target_tag == "any"
        }

        # fw_rule_check_target when target service account is passed and is not "*"
        fw_rule_check_target_sas(fw_rule, target_service_account) {
        	target_service_account != "*"

        	# check that target service accounts are set
        	fw_rule_target_sas = fw_rule.targetServiceAccounts

        	# check if the rule service account matches
        	re_match(target_service_account, fw_rule_target_sas[_])
        }

        # fw_rule_check_target_sas if target service account is set to "*"
        fw_rule_check_target_sas(fw_rule, target_service_account) {
        	target_service_account == "*"

        	# Verify that we have a target service account set, regardless of its value
        	fw_rule.targetServiceAccounts
        }

        # fw_rule_check_target_sas if target service account is set to any
        fw_rule_check_target_sas(fw_rule, target_service_account) {
        	target_service_account == "any"
        }

        #### Check enabled functions

        # fw_rule_check_enabled when enabled is set to any
        fw_rule_check_enabled(fw_rule, enabled) {
        	enabled == "any"
        }

        # fw_rule_check_enabled when enabled is not set to any
        fw_rule_check_enabled(fw_rule, enabled) {
        	enabled != "any"

        	# the following test only works when enabled is a boolean too
        	is_boolean(enabled)
        	enabled != fw_rule.disabled
        }

        # fw_rule_check_enabled when enabled is not set to "true" (string)
        # This function is just for convenience when the enabled parameter is a string instead of a boolean
        fw_rule_check_enabled(fw_rule, enabled) {
        	enabled != "any"
        	is_string(enabled)

        	# this is necessary as cast_boolean does not work on strings...
        	lower(enabled) == "true"
        	not fw_rule.disabled
        }

        # fw_rule_check_enabled when enabled is not set to "false" (string)
        # This function is just for convenience when the enabled parameter is a string instead of a boolean
        fw_rule_check_enabled(fw_rule, enabled) {
        	enabled != "any"
        	is_string(enabled)

        	# this is necessary as cast_boolean does not work on strings...
        	lower(enabled) == "false"
        	fw_rule.disabled
        }

        # Check for exempted resources in regex mode
        check_for_exemptions(asset_name, constraint, exemptions_mode) {
        	exemptions_mode == "regex"
        	lib.get_constraint_params(constraint, params)
        	exempt_list := params.exemptions
        	not re_match_name(asset_name, exempt_list)
        }

        # Check for exempted resources in exact mode (default)
        check_for_exemptions(asset_name, constraint, exemptions_mode) {
        	exemptions_mode == "exact"
        	lib.get_constraint_params(constraint, params)
        	exempt_list := params.exemptions
        	matches := {asset_name} & cast_set(exempt_list)
        	count(matches) == 0
        }

        # Check for empty exemption mode
        check_for_exemptions(asset_name, constraint, exemptions_mode) {
        	lib.has_field(constraint.spec.parameters, "exemptions") == false
        }

        re_match_name(name, filters) {
        	re_match(filters[_], name)
        }
        #ENDINLINE
