From 20bfd1c8451383c23c6a63a439b7230b2e656f8e Mon Sep 17 00:00:00 2001 From: Yunus Durmus Date: Sat, 2 Nov 2019 13:11:36 +0100 Subject: [PATCH] adds firewall whitelisting feature adds more comments and examples --- .../templates/gcp_network_firewall_v1.yaml | 382 ++++++++++++++++++ samples/gcp_network_firewall_whitelist.yaml | 183 +++++++++ validator/gcp_network_firewall_whitelist.rego | 270 +++++++++++++ .../gcp_network_firewall_whitelist_test.rego | 131 ++++++ .../assets/egress/data.json | 160 ++++++++ .../ingress_ssh/with_target_tag/data.json | 113 ++++++ .../constraints/egress/data.yaml | 35 ++ .../ingress_ssh/with_target_tag/data.yaml | 38 ++ .../ingress_ssh/without_target_tag/data.yaml | 34 ++ 9 files changed, 1346 insertions(+) create mode 100644 policies/templates/gcp_network_firewall_v1.yaml create mode 100644 samples/gcp_network_firewall_whitelist.yaml create mode 100644 validator/gcp_network_firewall_whitelist.rego create mode 100644 validator/gcp_network_firewall_whitelist_test.rego create mode 100644 validator/test/fixtures/gcp_network_firewall_whitelist/assets/egress/data.json create mode 100644 validator/test/fixtures/gcp_network_firewall_whitelist/assets/ingress_ssh/with_target_tag/data.json create mode 100644 validator/test/fixtures/gcp_network_firewall_whitelist/constraints/egress/data.yaml create mode 100644 validator/test/fixtures/gcp_network_firewall_whitelist/constraints/ingress_ssh/with_target_tag/data.yaml create mode 100644 validator/test/fixtures/gcp_network_firewall_whitelist/constraints/ingress_ssh/without_target_tag/data.yaml diff --git a/policies/templates/gcp_network_firewall_v1.yaml b/policies/templates/gcp_network_firewall_v1.yaml new file mode 100644 index 00000000..e9393686 --- /dev/null +++ b/policies/templates/gcp_network_firewall_v1.yaml @@ -0,0 +1,382 @@ +# Copyright 2019 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 +# +# http://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 enables you to create a list of "whitelist" rules that are +# compliant with your regulations. +# Each firewall rule in your GCP projects is checked against these whitelist rules. +# If there is a match, then no alerts are triggered. If there is no match, then +# that firewall is alerted. + +# It is possible to use regex, port ranges and IP CIDR ranges to define whitelists. +# For instance: +# - port: "1-100" covers "80" but not "443" +# - sourceRange: "10.128.0.0/16" covers "10.128.1.0/24" but not "10.0.0.0/24". 0.0.0.0/0 covers all the ranges +# - sourceTags, targetTags, sourceServiceAccounts, targetServiceAccounts can be defined via regular expression statements +# - IPProtocol can be a list of protocols. + +# The overall logic is as follows: +# Raise an alert if a firewall rule is not listed by any of the whitelist rules defined in this constraint file: +# 1. Do the direction (ingress/egress) match? +# 2. Do both firewall rule and whitelist rule have the same fields defined? No more no less. +# 3. Do the IP and their ports match? IPs are checked by equality while ports are checked via ranges. See above as an example. +# 4. Check that whitelist sourceRange/destinationRange CIDR overlaps the whole firewall rule's source range if a source range exist. +# 5. Check regex match for sourceServiceAccounts, sourceTags, targetTags, and targetServiceAccounts. +# All the values in a firewall rule should be whitelisted. PARTIAL matches are NOT enough. + + +# WARNINGS: +# - partial matches are NOT good enough. A firewall rule should be fully covered by the whitelist rules. +# - some fields like sourceTags and sourceServiceAccounts +# can NOT exist at the same time in a GCP firewall rule. Therefore, please create separate rules for each. +# - As hinted above, to have a match every defined field should exist in both firewall rule and whitelist rule. +# If you try to create a rule for ingress, tcp, 22, from 0.0.0.0/0, +# it does NOT cover ingress, tcp, 22, from 0.0.0.0/0, targetTags = ["https"] since targetTags is not defined in +# whitelisting. + +apiVersion: templates.gatekeeper.sh/v1alpha1 +kind: ConstraintTemplate +metadata: + name: gcp-network-firewall-whitelist-v1 + annotations: + # Example of tying a template to a CIS benchmark + benchmark: CIS11_5.03 +spec: + crd: + spec: + names: + kind: GCPNetworkFirewallWhitelistConstraintV1 + plural: gcpnetworkfirewallwhitelistconstraintsv1 + validation: + openAPIV3Schema: + properties: + rules: + type: array + items: + type: object + properties: + direction: + type: string + enum: [ingress, egress] + sourceServiceAccounts: + type: array + items: string + sourceTags: + type: array + items: string + sourceRanges: + type: array + items: string + destinationRanges: + type: array + items: string + targetServiceAccounts: + type: array + items: string + targetTags: + type: array + items: string + allowed: + type: array + items: object + properties: + IPProtocol: + type: string + ports: + type: array + items: string + denied: + type: array + items: object + properties: + IPProtocol: + type: string + ports: + type: array + items: string + targets: + validation.gcp.forsetisecurity.org: + rego: | #INLINE("validator/gcp_network_firewall_whitelist.rego") + # + # Copyright 2019 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 + # + # http://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.GCPNetworkFirewallWhitelistConstraintV1 + + import data.validator.gcp.lib as lib + + ########################### + # Find Whitelist Violations + ########################### + + deny[{ + "msg": message, + "details": metadata, + }] { + constraint := input.constraint + lib.get_constraint_params(constraint, params) + + asset := input.asset + asset.asset_type == "compute.googleapis.com/Firewall" + + fw_rule := asset.resource.data + + not is_there_any_match(fw_rule, params.rules) + message := sprintf("%s Firewall rule is prohibited.", [asset.name]) + metadata := { + "resource": asset.name, + "allowed_rules": params.rules, + } + } + + is_there_any_match(fw_rule, param_rules) { + check_any_match(fw_rule, param_rules[_]) + } + + check_any_match(fw_rule, param_rule) { + # Direction match? ingress/egress + lower(fw_rule.direction) == lower(param_rule.direction) + + # AND + + # The whitelist rule and actual firewall rule should have the same fields defined + # any difference means that there is no match + do_all_fields_exist_in_both( fw_rule, param_rule) + + # AND + + # check sourceRanges or destinationRanges overlap if they are defined + # the actual firewall rule should be a subset of this whitelist + do_source_destination_cidr_ranges_overlap( fw_rule, param_rule) + + # AND + + regex_array_fields := [ + "sourceTags", + "sourceServiceAccounts", + "targetTags", + "targetServiceAccounts", + ] + + # each field of firewall rule should be a subset of the whitelist rule + # basically, the regex match should happen. + do_all_regex_fields_match( fw_rule, param_rule, regex_array_fields) + + # AND + + # the IPProtocol, and ports (or port ranges) defined by the actual firewall rule + # should be a subset of the whitelist rule + do_all_protocols_and_ports_match( fw_rule, param_rule) + } + + ######### CHECKING IP RANGES ############ + + # We check either source or destination ranges match + do_source_destination_cidr_ranges_overlap(fw_rule, param_rule) { + param_rule.direction == "ingress" + do_cidr_ranges_overlap(fw_rule, param_rule, "sourceRanges") + } + + do_source_destination_cidr_ranges_overlap(fw_rule, param_rule) { + param_rule.direction == "egress" + do_cidr_ranges_overlap(fw_rule, param_rule, "destinationRanges") + } + + do_cidr_ranges_overlap(fw_rule, param_rule, field_name) { + # if there are no ranges defined, simply skip + # be aware that we have already checked the existence + # or non-existence of sourceRanges in both + # parameter and firewall rules + not lib.has_field(param_rule, field_name) + } + + do_cidr_ranges_overlap(fw_rule, param_rule, field_name) { + overlapped_ranges := [t | t := fw_rule[field_name][i]; any_cidr_overlap(t, param_rule[field_name])] + count(overlapped_ranges) == count(fw_rule[field_name]) + } + + any_cidr_overlap(fw_cidr_range, param_cidr_ranges) { + net.cidr_contains(param_cidr_ranges[_], fw_cidr_range) + } + + ######### CHECKING TYPE ############ + + check_type(fw_rule) = "allowed" { + lib.has_field(fw_rule, "allowed") + } + + check_type(fw_rule) = "denied" { + lib.has_field(fw_rule, "denied") + } + + ######### CHECKING PROTOCOL PORT MATCH ############ + + do_all_protocols_and_ports_match(fw_rule, param_rule) { + allowed_or_denied := check_type(fw_rule) + + # we collect all the protocol-port pairs that are covered by the parameters + matched_ones := [t | t := fw_rule[allowed_or_denied][i]; does_a_protocol_and_port_match(t, param_rule[allowed_or_denied])] + + count(matched_ones) == count(fw_rule[allowed_or_denied]) + } + + does_a_protocol_and_port_match(fw_protocol_port, param_protocols_ports) { + any_protocol_port_match(fw_protocol_port, param_protocols_ports[_]) + } + + any_protocol_port_match(fw_proto_port, param_proto_port) { + lower(fw_proto_port.IPProtocol) == lower(param_proto_port.IPProtocol) + lib.has_field(fw_proto_port, "ports") + lib.has_field(param_proto_port, "ports") + all_ports_match(fw_proto_port.ports, param_proto_port.ports) + } + + any_protocol_port_match(fw_proto_port, param_proto_port) { + lower(fw_proto_port.IPProtocol) == lower(param_proto_port.IPProtocol) + not lib.has_field(fw_proto_port, "ports") + not lib.has_field(param_proto_port, "ports") + } + + all_ports_match(fw_ports, param_ports) { + trace(sprintf("matching fw_ports %v to param_ports %v", [fw_ports, param_ports])) + matched_ports := [t | t := fw_ports[i]; do_ports_match(t, param_ports[_])] + trace(sprintf("matched fw_ports %v ", [matched_ports])) + count(matched_ports) == count(fw_ports) + } + + do_ports_match(fw_port, param_port) { + fw_port == param_port + } + + do_ports_match(fw_port, param_port) { + # If both are ranges + contains(fw_port, "-") + contains(param_port, "-") + range_match(fw_port, param_port) + } + + do_ports_match(fw_port, param_port) { + # If fw_port is a range but not parameter + contains(fw_port, "-") + not contains(param_port, "-") + param_port_range := sprintf("%s-%s", [param_port, param_port]) + range_match(fw_port, param_port_range) + } + + do_ports_match(fw_port, param_port) { + # If param_port is a range but not the actual firewall port + not contains(fw_port, "-") + contains(param_port, "-") + fw_port_range := sprintf("%s-%s", [fw_port, fw_port]) + range_match(fw_port_range, param_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 + } + + ######### CHECKING REGEX MATCH FOR TAGS and SERVICE ACCOUNTS ############ + + do_all_regex_fields_match(fw_rule, param_rule, field_names) { + matched_fields := [t | t := field_names[i]; do_fields_match(fw_rule, param_rule, t)] + count(matched_fields) == count(field_names) + } + + do_fields_match(fw_rule, param_rule, field_name) { + # if the field does not exist in both, that's fine. + not lib.has_field(fw_rule, field_name) + not lib.has_field(param_rule, field_name) + } + + # all the fields inside a firewall rules should be matched + # to achive this, we filter out matched ones. If any unmatched is left + # return false. + do_fields_match(fw_rule, param_rule, field_name) { + matched_firewall_tags_sas := [t | t := fw_rule[field_name][i]; any_fw_tag_sa_match(t, param_rule[field_name])] + trace(sprintf("field_name %v, matched_firewall_tags_sas %v, firewall items %v, tags_sas_param %v", [field_name, matched_firewall_tags_sas, fw_rule[field_name], param_rule[field_name]])) + + # if all the network tags of the firewall rule matches, + # then this rule is covered + count(fw_rule[field_name]) == count(matched_firewall_tags_sas) + } + + any_fw_tag_sa_match(fw_rule_items, tags_sas_param) { + re_match(tags_sas_param[_], fw_rule_items) + } + + ######### CHECKING EXISTENCE OF FIELDS ############ + + # make sure that if a field is defined in fw_rule it should exist in parameter as well. + # and vice-versa. Any deviation means not match. + # We don't check protocol and port since they are displayed differently + do_all_fields_exist_in_both(fw_rule, param_rule) { + fields := [ + "sourceTags", + "sourceRanges", + "sourceServiceAccounts", + "targetTags", + "targetServiceAccounts", + "destinationRanges", + "allowed", + "denied", + "direction", + ] + + field_in_both(fw_rule, param_rule, fields[_]) + } + + field_in_both(fw_rule, param_rule, field) { + lib.has_field(fw_rule, field) + lib.has_field(param_rule, field) + } + + field_in_both(fw_rule, param_rule, field) { + not lib.has_field(fw_rule, field) + not lib.has_field(param_rule, field) + } + #ENDINLINE \ No newline at end of file diff --git a/samples/gcp_network_firewall_whitelist.yaml b/samples/gcp_network_firewall_whitelist.yaml new file mode 100644 index 00000000..c32a9802 --- /dev/null +++ b/samples/gcp_network_firewall_whitelist.yaml @@ -0,0 +1,183 @@ +# Copyright 2019 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 +# +# http://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 enables you to create a list of "whitelist" rules that are +# compliant with your regulations. +# Each firewall rule in your GCP projects is checked against these whitelist rules. +# If there is a match, then no alerts are triggered. If there is no match, then +# that firewall is alerted. +# Match is basically defined as: whitelist rule should be a superset of the actual allowed rules. + +# It is possible to use regex, port ranges and IP CIDR ranges to define whitelists. +# For instance: +# - port: "1-100" covers "80" but not "443" +# - sourceRange: "10.128.0.0/16" covers "10.128.1.0/24" but not "10.0.0.0/24". 0.0.0.0/0 covers all the ranges +# - sourceTags, targetTags, sourceServiceAccounts, targetServiceAccounts can be defined via regular expression statements +# - IPProtocol can be a list of protocols. + +# The overall logic is as follows: +# Raise an alert if a firewall rule is not a subset by any of the whitelist rules defined in this constraint file: +# 1. Does the direction (ingress/egress) match? +# 2. Do both firewall rule and whitelist rule have the same fields defined? No more no less. +# 3. Do the IPProtocol and its ports match? IPProtocols are checked by equality while ports are checked via ranges. See above. +# 4. Check whether whitelist sourceRange/destinationRange CIDR overlap the whole firewall rule's source range if a source range/destination range exist. +# 5. Check regex match for sourceServiceAccounts, sourceTags, targetTags, and targetServiceAccounts. +# All the SAs,Tags in a firewall rule should be whitelisted. PARTIAL overlaps are NOT enough. For instance, if 2 out of 3 targetTags are matched, it is a NO. + + +# WARNINGS: +# - partial matches are NOT good enough. A firewall rule should be fully covered by the whitelist rules. +# - some fields like sourceTags and sourceServiceAccounts +# can NOT exist at the same time in a GCP firewall rule. Therefore, please create separate rules for each. +# - As hinted above, to have a match every defined field should exist in both firewall rule and whitelist rule. +# If you try to create a rule for ingress, tcp, 22, from 0.0.0.0/0, +# it does NOT cover ingress, tcp, 22, from 0.0.0.0/0, targetTags = ["https"] since targetTags is not defined in +# whitelisting. + +apiVersion: constraints.gatekeeper.sh/v1alpha1 +kind: GCPNetworkFirewallWhitelistConstraintV1 +metadata: + name: forbid-firewalls-that-are-not-listed +spec: + severity: high + parameters: + #### HINT: Asset inventory output, which is used by this policy library as input, + # shows firewalls in JSON format. + # You may refer them to see the naming and fields. + # The goal is to create a whitelist rule that is superset of the actual allowed firewall rules. + + rules: + # Allow SSH (22) to the bastion VMs only + # the bastion VM is defined by a service account + - direction: ingress + allowed: + - IPProtocol: "tcp" + ports: + - "22" + targetServiceAccounts: + - "bastion-sa@PROJECT.iam.gserviceaccount.com" + sourceRanges: + - "0.0.0.0/0" + + # Allow SSH (22) to the bastion VMs only + # the bastion VM is defined by a target tag + - direction: ingress + allowed: + - IPProtocol: "tcp" + ports: + - "22" + targetTags: + - "^bastion$" + sourceRanges: + - "0.0.0.0/0" + + # allow SSH over IAP (35.235.240.0/20) + - direction: ingress + allowed: + - IPProtocol: "tcp" + ports: + - "22" + sourceRanges: + - "35.235.240.0/20" + + # allow all traffic + # from public internet and private network, 0.0.0.0/0 + # to VMs with taged as "tags.*" or "test.*" + - direction: ingress + allowed: + - IPProtocol: "tcp" + ports: + - "1-65535" + - IPProtocol: "udp" + ports: + - "1-65535" + - IPProtocol: "icmp" + - IPProtocol: "esp" + - IPProtocol: "ah" + - IPProtocol: "sctp" + targetTags: + - "tags.*" + - "test.*" + sourceRanges: + - "0.0.0.0/0" + + # allow only 22 (SSH) and 80 (HTTP) traffic + # from public internet and private network, 0.0.0.0/0 + # to VMs with taged as "tags.*" or "test.*" + - direction: ingress + allowed: + - IPProtocol: "tcp" + ports: + - "22" + - "80" + targetTags: + - "tags.*" + - "test.*" + sourceRanges: + - "0.0.0.0/0" + + # allow only source service account based ingress rules to ALL instances. + - direction: ingress + allowed: + - IPProtocol: "tcp" + ports: + - "1-65535" + - IPProtocol: "tcp" # we provide this line since when it is ALL port, we may not see ports + - IPProtocol: "udp" + ports: + - "1-65535" + - IPProtocol: "udp" + - IPProtocol: "icmp" + sourceServiceAccounts: + - ".*@.*gserviceaccount.com" + # As a complementary to the above rule, you may use this one, so that you allow + # SA -> SA traffic firewall rules. + - direction: egress + allowed: + - IPProtocol: "tcp" + ports: + - "1-65535" + - IPProtocol: "tcp" # we provide this line since when it is ALL port, we may not see ports + - IPProtocol: "udp" + ports: + - "1-65535" + - IPProtocol: "udp" + - IPProtocol: "icmp" + sourceServiceAccounts: + - ".*@.*gserviceaccount.com" + targetServiceAccounts: + - ".*@.*gserviceaccount.com" + # allow all protocols, ports from internet + # to VMs tagged with ".*public_vm" or "public_service.*" + - direction: ingress + allowed: + - IPProtocol: "ALL" + sourceRanges: + - "0.0.0.0/0" + targetTags: + - ".*public_vm" + - "public_service.*" + # allow all all protocols/ports from Internet + # this rule does not cover the previous rule with tag + # since targetTag is not mentioned. + - direction: ingress + allowed: + - IPProtocol: "ALL" + sourceRanges: + - "0.0.0.0/0" + + \ No newline at end of file diff --git a/validator/gcp_network_firewall_whitelist.rego b/validator/gcp_network_firewall_whitelist.rego new file mode 100644 index 00000000..8f966642 --- /dev/null +++ b/validator/gcp_network_firewall_whitelist.rego @@ -0,0 +1,270 @@ +# +# Copyright 2019 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 +# +# http://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.GCPNetworkFirewallWhitelistConstraintV1 + +import data.validator.gcp.lib as lib + +########################### +# Find Whitelist Violations +########################### + +deny[{ + "msg": message, + "details": metadata, +}] { + constraint := input.constraint + lib.get_constraint_params(constraint, params) + + asset := input.asset + asset.asset_type == "compute.googleapis.com/Firewall" + + fw_rule := asset.resource.data + + not is_there_any_match(fw_rule, params.rules) + message := sprintf("%s Firewall rule is prohibited.", [asset.name]) + metadata := { + "resource": asset.name, + "allowed_rules": params.rules, + } +} + +is_there_any_match(fw_rule, param_rules) { + check_any_match(fw_rule, param_rules[_]) +} + +check_any_match(fw_rule, param_rule) { + # Direction match? ingress/egress + lower(fw_rule.direction) == lower(param_rule.direction) + + # AND + + # The whitelist rule and actual firewall rule should have the same fields defined + # any difference means that there is no match +do_all_fields_exist_in_both( fw_rule, param_rule) + + # AND + + # check sourceRanges or destinationRanges overlap if they are defined + # the actual firewall rule should be a subset of this whitelist +do_source_destination_cidr_ranges_overlap( fw_rule, param_rule) + + # AND + + regex_array_fields := [ + "sourceTags", + "sourceServiceAccounts", + "targetTags", + "targetServiceAccounts", + ] + + # each field of firewall rule should be a subset of the whitelist rule + # basically, the regex match should happen. +do_all_regex_fields_match( fw_rule, param_rule, regex_array_fields) + + # AND + + # the IPProtocol, and ports (or port ranges) defined by the actual firewall rule + # should be a subset of the whitelist rule +do_all_protocols_and_ports_match( fw_rule, param_rule) +} + +######### CHECKING IP RANGES ############ + +# We check either source or destination ranges match +do_source_destination_cidr_ranges_overlap(fw_rule, param_rule) { + param_rule.direction == "ingress" + do_cidr_ranges_overlap(fw_rule, param_rule, "sourceRanges") +} + +do_source_destination_cidr_ranges_overlap(fw_rule, param_rule) { + param_rule.direction == "egress" + do_cidr_ranges_overlap(fw_rule, param_rule, "destinationRanges") +} + +do_cidr_ranges_overlap(fw_rule, param_rule, field_name) { + # if there are no ranges defined, simply skip + # be aware that we have already checked the existence + # or non-existence of sourceRanges in both + # parameter and firewall rules + not lib.has_field(param_rule, field_name) +} + +do_cidr_ranges_overlap(fw_rule, param_rule, field_name) { + overlapped_ranges := [t | t := fw_rule[field_name][i]; any_cidr_overlap(t, param_rule[field_name])] + count(overlapped_ranges) == count(fw_rule[field_name]) +} + +any_cidr_overlap(fw_cidr_range, param_cidr_ranges) { + net.cidr_contains(param_cidr_ranges[_], fw_cidr_range) +} + +######### CHECKING TYPE ############ + +check_type(fw_rule) = "allowed" { + lib.has_field(fw_rule, "allowed") +} + +check_type(fw_rule) = "denied" { + lib.has_field(fw_rule, "denied") +} + +######### CHECKING PROTOCOL PORT MATCH ############ + +do_all_protocols_and_ports_match(fw_rule, param_rule) { + allowed_or_denied := check_type(fw_rule) + + # we collect all the protocol-port pairs that are covered by the parameters + matched_ones := [t | t := fw_rule[allowed_or_denied][i]; does_a_protocol_and_port_match(t, param_rule[allowed_or_denied])] + + count(matched_ones) == count(fw_rule[allowed_or_denied]) +} + +does_a_protocol_and_port_match(fw_protocol_port, param_protocols_ports) { + any_protocol_port_match(fw_protocol_port, param_protocols_ports[_]) +} + +any_protocol_port_match(fw_proto_port, param_proto_port) { + lower(fw_proto_port.IPProtocol) == lower(param_proto_port.IPProtocol) + lib.has_field(fw_proto_port, "ports") + lib.has_field(param_proto_port, "ports") + all_ports_match(fw_proto_port.ports, param_proto_port.ports) +} + +any_protocol_port_match(fw_proto_port, param_proto_port) { + lower(fw_proto_port.IPProtocol) == lower(param_proto_port.IPProtocol) + not lib.has_field(fw_proto_port, "ports") + not lib.has_field(param_proto_port, "ports") +} + +all_ports_match(fw_ports, param_ports) { + trace(sprintf("matching fw_ports %v to param_ports %v", [fw_ports, param_ports])) + matched_ports := [t | t := fw_ports[i]; do_ports_match(t, param_ports[_])] + trace(sprintf("matched fw_ports %v ", [matched_ports])) + count(matched_ports) == count(fw_ports) +} + +do_ports_match(fw_port, param_port) { + fw_port == param_port +} + +do_ports_match(fw_port, param_port) { + # If both are ranges + contains(fw_port, "-") + contains(param_port, "-") + range_match(fw_port, param_port) +} + +do_ports_match(fw_port, param_port) { + # If fw_port is a range but not parameter + contains(fw_port, "-") + not contains(param_port, "-") + param_port_range := sprintf("%s-%s", [param_port, param_port]) + range_match(fw_port, param_port_range) +} + +do_ports_match(fw_port, param_port) { + # If param_port is a range but not the actual firewall port + not contains(fw_port, "-") + contains(param_port, "-") + fw_port_range := sprintf("%s-%s", [fw_port, fw_port]) + range_match(fw_port_range, param_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 +} + +######### CHECKING REGEX MATCH FOR TAGS and SERVICE ACCOUNTS ############ + +do_all_regex_fields_match(fw_rule, param_rule, field_names) { + matched_fields := [t | t := field_names[i]; do_fields_match(fw_rule, param_rule, t)] + count(matched_fields) == count(field_names) +} + +do_fields_match(fw_rule, param_rule, field_name) { + # if the field does not exist in both, that's fine. + not lib.has_field(fw_rule, field_name) + not lib.has_field(param_rule, field_name) +} + +# all the fields inside a firewall rules should be matched +# to achive this, we filter out matched ones. If any unmatched is left +# return false. +do_fields_match(fw_rule, param_rule, field_name) { + matched_firewall_tags_sas := [t | t := fw_rule[field_name][i]; any_fw_tag_sa_match(t, param_rule[field_name])] + trace(sprintf("field_name %v, matched_firewall_tags_sas %v, firewall items %v, tags_sas_param %v", [field_name, matched_firewall_tags_sas, fw_rule[field_name], param_rule[field_name]])) + + # if all the network tags of the firewall rule matches, + # then this rule is covered + count(fw_rule[field_name]) == count(matched_firewall_tags_sas) +} + +any_fw_tag_sa_match(fw_rule_items, tags_sas_param) { + re_match(tags_sas_param[_], fw_rule_items) +} + +######### CHECKING EXISTENCE OF FIELDS ############ + +# make sure that if a field is defined in fw_rule it should exist in parameter as well. +# and vice-versa. Any deviation means not match. +# We don't check protocol and port since they are displayed differently +do_all_fields_exist_in_both(fw_rule, param_rule) { + fields := [ + "sourceTags", + "sourceRanges", + "sourceServiceAccounts", + "targetTags", + "targetServiceAccounts", + "destinationRanges", + "allowed", + "denied", + "direction", + ] + + field_in_both(fw_rule, param_rule, fields[_]) +} + +field_in_both(fw_rule, param_rule, field) { + lib.has_field(fw_rule, field) + lib.has_field(param_rule, field) +} + +field_in_both(fw_rule, param_rule, field) { + not lib.has_field(fw_rule, field) + not lib.has_field(param_rule, field) +} diff --git a/validator/gcp_network_firewall_whitelist_test.rego b/validator/gcp_network_firewall_whitelist_test.rego new file mode 100644 index 00000000..13afac42 --- /dev/null +++ b/validator/gcp_network_firewall_whitelist_test.rego @@ -0,0 +1,131 @@ +# +# Copyright 2019 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 +# +# http://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.GCPNetworkFirewallWhitelistConstraintV1 + +import data.validator.gcp.lib as lib + +import data.test.fixtures.gcp_network_firewall_whitelist.assets.ingress_ssh.with_target_tag as ingress_ssh_assets +import data.test.fixtures.gcp_network_firewall_whitelist.constraints.ingress_ssh.with_target_tag as ingress_ssh_constraint_with_target_tag +import data.test.fixtures.gcp_network_firewall_whitelist.constraints.ingress_ssh.without_target_tag as ingress_ssh_constraint_without_target_tag + +import data.test.fixtures.gcp_network_firewall_whitelist.assets.egress as egress_assets +import data.test.fixtures.gcp_network_firewall_whitelist.constraints.egress as egress_constraint + +find_violations[violation] { + firewall := data.firewalls[_] + + constraint := data.constraint + issues := deny with input.asset as firewall + with input.constraint as constraint + + trace(sprintf("ISSUES: %v", [issues[_].details.resource])) + violation := issues[_] +} + +test_ingress_ssh_with_target_tag { + violation := find_violations with data.firewalls as ingress_ssh_assets + with data.constraint as ingress_ssh_constraint_with_target_tag + + count(violation) == 1 + + violation[_].details.resource != "//compute.googleapis.com/projects/self-perimeter-yunusd/global/firewalls/forseti-test" +} + +test_ingress_ssh_without_target_tag { + violation := find_violations with data.firewalls as ingress_ssh_assets + with data.constraint as ingress_ssh_constraint_without_target_tag + + count(violation) == 1 + + violation[_].details.resource == "//compute.googleapis.com/projects/self-perimeter-yunusd/global/firewalls/forseti-test" +} + +test_egress { + violation := find_violations with data.firewalls as egress_assets + with data.constraint as egress_constraint + + count(violation) == 2 + + violation[_].details.resource != "//compute.googleapis.com/projects/self-perimeter-yunusd/global/firewalls/forseti-test-egress" +} + +test_do_all_regex_fields_match { + do_all_regex_fields_match({"targetServiceAccounts": ["sa1", "sa2"], "targetTags": ["tag1"]}, {"targetServiceAccounts": ["sa.*"], "targetTags": ["tag.*"]}, ["not_exist", "targetTags", "targetServiceAccounts"]) +} + +test_not_all_regex_fields_match { + not do_all_regex_fields_match({"targetServiceAccounts": ["sa1", "sa2"], "targetTags": ["tag1"]}, {"targetServiceAccounts": ["sa.*"], "targetTags": ["nomatch"]}, ["not_exist", "targetTags", "targetServiceAccounts"]) +} + +fields_match_violations { + do_fields_match(data.rule, data.param, data.field) +} + +test_do_fields_match_not_exist { + do_fields_match({"targetServiceAccounts": ["sa1", "sa2"], "targetTags": ["tag1"]}, {"targetServiceAccounts": ["sa.*"], "targetTags": ["tag.*"]}, "not_exist") +} + +test_do_fields_match { + do_fields_match({"targetServiceAccounts": ["sa1", "sa2"], "targetTags": ["tag1"]}, {"targetServiceAccounts": ["sa.*"], "targetTags": ["tag.*"]}, "targetServiceAccounts") +} + +test_do_fields_no_match { + not do_fields_match({"targetServiceAccounts": ["sa1", "sa2"]}, {"targetServiceAccounts": ["nomatch"]}, "targetServiceAccounts") +} + +test_do_all_protocols_and_ports_match { + do_all_protocols_and_ports_match({"allowed": [{"IPProtocol": "tcp", "ports": ["0-65535"]}, {"IPProtocol": "udp", "ports": ["0-65535"]}, {"IPProtocol": "icmp"}]}, {"allowed": [{"IPProtocol": "tcp", "ports": ["0-65535"]}, {"IPProtocol": "udp", "ports": ["0-65535"]}, {"IPProtocol": "icmp"}]}) +} + +test_not_all_protocols_and_ports_match { + not do_all_protocols_and_ports_match({"allowed": [{"IPProtocol": "tcp", "ports": ["0-65535"]}, {"IPProtocol": "udp", "ports": ["0-65535"]}, {"IPProtocol": "icmp"}]}, {"allowed": [{"IPProtocol": "udp", "ports": ["0-65535"]}, {"IPProtocol": "icmp"}]}) +} + +test_exact_protocol_port_match { + does_a_protocol_and_port_match({"IPProtocol": "tcp", "ports": ["80", "443"]}, [{"IPProtocol": "tcp", "ports": ["80", "443"]}]) +} + +test_one_port_missin_no_match { + not does_a_protocol_and_port_match({"IPProtocol": "tcp", "ports": ["80", "443"]}, [{"IPProtocol": "tcp", "ports": ["80"]}]) +} + +test_one_extra_port_match { + does_a_protocol_and_port_match({"IPProtocol": "tcp", "ports": ["80"]}, [{"IPProtocol": "tcp", "ports": ["80", "443"]}, {"IPProtocol": "udp", "ports": ["80", "443"]}]) +} + +test_range_port_match { + does_a_protocol_and_port_match({"IPProtocol": "tcp", "ports": ["80"]}, [{"IPProtocol": "tcp", "ports": ["79-90", "443"]}]) +} + +test_no_fw_port_no_match { + not does_a_protocol_and_port_match({"IPProtocol": "tcp"}, [{"IPProtocol": "tcp", "ports": ["79-90", "443"]}]) +} + +test_no_param_port_no_match { + not does_a_protocol_and_port_match({"IPProtocol": "tcp", "ports": ["80"]}, [{"IPProtocol": "tcp"}]) +} + +test_no_port_match { + does_a_protocol_and_port_match({"IPProtocol": "icmp"}, [{"IPProtocol": "icmp"}]) +} + +test_cidr_ranges { + do_source_destination_cidr_ranges_overlap({"direction": "ingress", "sourceRanges": ["0.0.0.0/0"]}, {"direction": "ingress", "sourceRanges": ["0.0.0.0/0"]}) + do_source_destination_cidr_ranges_overlap({"direction": "ingress", "sourceRanges": ["0.0.0.0/0"]}, {"direction": "ingress", "sourceRanges": ["0.0.0.0/0", "10.0.0.0/8"]}) + not do_source_destination_cidr_ranges_overlap({"direction": "ingress", "sourceRanges": ["0.0.0.0/0", "10.0.0.0/8"]}, {"direction": "ingress", "sourceRanges": ["10.0.0.0/8"]}) + do_source_destination_cidr_ranges_overlap({"direction": "egress", "sourceRanges": ["10.0.0.0/8"]}, {"direction": "egress", "sourceRanges": ["0.0.0.0/0"]}) +} diff --git a/validator/test/fixtures/gcp_network_firewall_whitelist/assets/egress/data.json b/validator/test/fixtures/gcp_network_firewall_whitelist/assets/egress/data.json new file mode 100644 index 00000000..52f9014f --- /dev/null +++ b/validator/test/fixtures/gcp_network_firewall_whitelist/assets/egress/data.json @@ -0,0 +1,160 @@ +[ + { + "name": "//compute.googleapis.com/projects/self-perimeter-yunusd/global/firewalls/forseti-test", + "asset_type": "compute.googleapis.com/Firewall", + "resource": { + "version": "v1", + "discovery_document_uri": "https://www.googleapis.com/discovery/v1/apis/compute/v1/rest", + "discovery_name": "Firewall", + "parent": "//cloudresourcemanager.googleapis.com/projects/873162022610", + "data": { + "allowed": [ + { + "IPProtocol": "tcp", + "ipProtocol": "tcp", + "port": [ + "22" + ], + "ports": [ + "22" + ] + }, + { + "IPProtocol": "tcp", + "ipProtocol": "tcp", + "port": [ + "22" + ], + "ports": [ + "22" + ] + } + ], + "creationTimestamp": "2019-10-29T15:10:31.890-07:00", + "description": "", + "direction": "INGRESS", + "disabled": false, + "id": "3344227225818170936", + "logConfig": { + "enable": false + }, + "name": "forseti-test", + "network": "https://www.googleapis.com/compute/v1/projects/self-perimeter-yunusd/global/networks/default", + "priority": 1000, + "selfLink": "https://www.googleapis.com/compute/v1/projects/self-perimeter-yunusd/global/firewalls/forseti-test", + "sourceRange": [ + "0.0.0.0/0" + ], + "sourceRanges": [ + "0.0.0.0/0" + ], + "targetTag": [ + "tag1", + "test2" + ], + "targetTags": [ + "tag1", + "test2" + ] + } + }, + "ancestors": [ + "projects/873162022610", + "folders/1009515865153", + "organizations/416234700201" + ] + }, + { + "name": "//compute.googleapis.com/projects/spark-dicle/global/firewalls/default-allow-ssh", + "asset_type": "compute.googleapis.com/Firewall", + "resource": { + "version": "v1", + "discovery_document_uri": "https://www.googleapis.com/discovery/v1/apis/compute/v1/rest", + "discovery_name": "Firewall", + "parent": "//cloudresourcemanager.googleapis.com/projects/101190804177", + "data": { + "allowed": [ + { + "IPProtocol": "tcp", + "ipProtocol": "tcp", + "port": [ + "22" + ], + "ports": [ + "22" + ] + } + ], + "creationTimestamp": "2019-07-31T11:50:18.875-07:00", + "description": "Allow SSH from anywhere", + "direction": "INGRESS", + "disabled": false, + "id": "5458400829766392837", + "logConfig": { + "enable": false + }, + "name": "default-allow-ssh", + "network": "https://www.googleapis.com/compute/v1/projects/spark-dicle/global/networks/default", + "priority": 65534, + "selfLink": "https://www.googleapis.com/compute/v1/projects/spark-dicle/global/firewalls/default-allow-ssh", + "sourceRange": [ + "0.0.0.0/0" + ], + "sourceRanges": [ + "0.0.0.0/0" + ] + } + }, + "ancestors": [ + "projects/101190804177", + "organizations/416234700201" + ] + }, + { + "name": "//compute.googleapis.com/projects/self-perimeter-yunusd/global/firewalls/forseti-test-egress", + "asset_type": "compute.googleapis.com/Firewall", + "resource": { + "version": "v1", + "discovery_document_uri": "https://www.googleapis.com/discovery/v1/apis/compute/v1/rest", + "discovery_name": "Firewall", + "parent": "//cloudresourcemanager.googleapis.com/projects/873162022610", + "data": { + "allowed": [ + { + "IPProtocol": "all", + "ipProtocol": "all" + } + ], + "creationTimestamp": "2019-11-02T01:51:11.570-07:00", + "description": "", + "destinationRange": [ + "192.168.0.0/16" + ], + "destinationRanges": [ + "192.168.0.0/16" + ], + "direction": "EGRESS", + "disabled": false, + "id": "6131219834766163344", + "logConfig": { + "enable": false + }, + "name": "forseti-test-egress", + "network": "https://www.googleapis.com/compute/v1/projects/self-perimeter-yunusd/global/networks/default", + "priority": 1000, + "selfLink": "https://www.googleapis.com/compute/v1/projects/self-perimeter-yunusd/global/firewalls/forseti-test-egress", + "targetServiceAccount": [ + "873162022610-compute@developer.gserviceaccount.com" + ], + "targetServiceAccounts": [ + "873162022610-compute@developer.gserviceaccount.com" + ] + } + }, + "ancestors": [ + "projects/873162022610", + "folders/1009515865153", + "organizations/416234700201" + ] + } +] \ No newline at end of file diff --git a/validator/test/fixtures/gcp_network_firewall_whitelist/assets/ingress_ssh/with_target_tag/data.json b/validator/test/fixtures/gcp_network_firewall_whitelist/assets/ingress_ssh/with_target_tag/data.json new file mode 100644 index 00000000..cd406526 --- /dev/null +++ b/validator/test/fixtures/gcp_network_firewall_whitelist/assets/ingress_ssh/with_target_tag/data.json @@ -0,0 +1,113 @@ +[ + { + "name": "//compute.googleapis.com/projects/self-perimeter-yunusd/global/firewalls/forseti-test", + "asset_type": "compute.googleapis.com/Firewall", + "resource": { + "version": "v1", + "discovery_document_uri": "https://www.googleapis.com/discovery/v1/apis/compute/v1/rest", + "discovery_name": "Firewall", + "parent": "//cloudresourcemanager.googleapis.com/projects/873162022610", + "data": { + "allowed": [ + { + "IPProtocol": "tcp", + "ipProtocol": "tcp", + "port": [ + "22" + ], + "ports": [ + "22" + ] + }, + { + "IPProtocol": "tcp", + "ipProtocol": "tcp", + "port": [ + "22" + ], + "ports": [ + "22" + ] + } + ], + "creationTimestamp": "2019-10-29T15:10:31.890-07:00", + "description": "", + "direction": "INGRESS", + "disabled": false, + "id": "3344227225818170936", + "logConfig": { + "enable": false + }, + "name": "forseti-test", + "network": "https://www.googleapis.com/compute/v1/projects/self-perimeter-yunusd/global/networks/default", + "priority": 1000, + "selfLink": "https://www.googleapis.com/compute/v1/projects/self-perimeter-yunusd/global/firewalls/forseti-test", + "sourceRange": [ + "0.0.0.0/0" + ], + "sourceRanges": [ + "0.0.0.0/0" + ], + "targetTag": [ + "tag1", + "test2" + ], + "targetTags": [ + "tag1", + "test2" + ] + } + }, + "ancestors": [ + "projects/873162022610", + "folders/1009515865153", + "organizations/416234700201" + ] + }, + { + "name": "//compute.googleapis.com/projects/spark-dicle/global/firewalls/default-allow-ssh", + "asset_type": "compute.googleapis.com/Firewall", + "resource": { + "version": "v1", + "discovery_document_uri": "https://www.googleapis.com/discovery/v1/apis/compute/v1/rest", + "discovery_name": "Firewall", + "parent": "//cloudresourcemanager.googleapis.com/projects/101190804177", + "data": { + "allowed": [ + { + "IPProtocol": "tcp", + "ipProtocol": "tcp", + "port": [ + "22" + ], + "ports": [ + "22" + ] + } + ], + "creationTimestamp": "2019-07-31T11:50:18.875-07:00", + "description": "Allow SSH from anywhere", + "direction": "INGRESS", + "disabled": false, + "id": "5458400829766392837", + "logConfig": { + "enable": false + }, + "name": "default-allow-ssh", + "network": "https://www.googleapis.com/compute/v1/projects/spark-dicle/global/networks/default", + "priority": 65534, + "selfLink": "https://www.googleapis.com/compute/v1/projects/spark-dicle/global/firewalls/default-allow-ssh", + "sourceRange": [ + "0.0.0.0/0" + ], + "sourceRanges": [ + "0.0.0.0/0" + ] + } + }, + "ancestors": [ + "projects/101190804177", + "organizations/416234700201" + ] + } +] \ No newline at end of file diff --git a/validator/test/fixtures/gcp_network_firewall_whitelist/constraints/egress/data.yaml b/validator/test/fixtures/gcp_network_firewall_whitelist/constraints/egress/data.yaml new file mode 100644 index 00000000..4f3f8b75 --- /dev/null +++ b/validator/test/fixtures/gcp_network_firewall_whitelist/constraints/egress/data.yaml @@ -0,0 +1,35 @@ +# Copyright 2019 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 +# +# http://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. +# +apiVersion: constraints.gatekeeper.sh/v1alpha1 +kind: GCPNetworkFirewallConstraintV1 +metadata: + name: forbid-firewalls-that-are-not-listed + bundles.validator.forsetisecurity.org/scorecard-v1: security +spec: + severity: high + parameters: + rules: + - direction: egress + allowed: + - IPProtocol: "all" + destinationRanges: + - "0.0.0.0/0" + targetServiceAccounts: + - ".*@.*gserviceaccount.com" + + + + + \ No newline at end of file diff --git a/validator/test/fixtures/gcp_network_firewall_whitelist/constraints/ingress_ssh/with_target_tag/data.yaml b/validator/test/fixtures/gcp_network_firewall_whitelist/constraints/ingress_ssh/with_target_tag/data.yaml new file mode 100644 index 00000000..fc252aba --- /dev/null +++ b/validator/test/fixtures/gcp_network_firewall_whitelist/constraints/ingress_ssh/with_target_tag/data.yaml @@ -0,0 +1,38 @@ +# Copyright 2019 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 +# +# http://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. +# +apiVersion: constraints.gatekeeper.sh/v1alpha1 +kind: GCPNetworkFirewallConstraintV1 +metadata: + name: forbid-firewalls-that-are-not-listed + bundles.validator.forsetisecurity.org/scorecard-v1: security +spec: + severity: high + parameters: + rules: + - direction: ingress + allowed: + - IPProtocol: "tcp" + ports: + - "22" + targetTags: + - "tag1.*" + - "test.*" + sourceRanges: + - "0.0.0.0/0" + + + + + \ No newline at end of file diff --git a/validator/test/fixtures/gcp_network_firewall_whitelist/constraints/ingress_ssh/without_target_tag/data.yaml b/validator/test/fixtures/gcp_network_firewall_whitelist/constraints/ingress_ssh/without_target_tag/data.yaml new file mode 100644 index 00000000..1ad45a0e --- /dev/null +++ b/validator/test/fixtures/gcp_network_firewall_whitelist/constraints/ingress_ssh/without_target_tag/data.yaml @@ -0,0 +1,34 @@ +# Copyright 2019 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 +# +# http://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. +# +apiVersion: constraints.gatekeeper.sh/v1alpha1 +kind: GCPNetworkFirewallConstraintV1 +metadata: + name: forbid-firewalls-that-are-not-listed +spec: + severity: high + parameters: + rules: + - direction: ingress + allowed: + - IPProtocol: "tcp" + ports: + - "22" + sourceRanges: + - "0.0.0.0/0" + + + + + \ No newline at end of file