1+ # Copyright 2019 Google LLC
2+ #
3+ # Licensed under the Apache License, Version 2.0 (the "License");
4+ # you may not use this file except in compliance with the License.
5+ # You may obtain a copy of the License at
6+ #
7+ # http://www.apache.org/licenses/LICENSE-2.0
8+ #
9+ # Unless required by applicable law or agreed to in writing, software
10+ # distributed under the License is distributed on an "AS IS" BASIS,
11+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ # See the License for the specific language governing permissions and
13+ # limitations under the License.
14+ #
15+
16+
17+ # This template enables you to create a list of "whitelist" rules that are
18+ # compliant with your regulations.
19+ # Each firewall rule in your GCP projects is checked against these whitelist rules.
20+ # If there is a match, then no alerts are triggered. If there is no match, then
21+ # that firewall is alerted.
22+
23+ # It is possible to use regex, port ranges and IP CIDR ranges to define whitelists.
24+ # For instance:
25+ # - port: "1-100" covers "80" but not "443"
26+ # - 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
27+ # - sourceTags, targetTags, sourceServiceAccounts, targetServiceAccounts can be defined via regular expression statements
28+ # - IPProtocol can be a list of protocols.
29+
30+ # The overall logic is as follows:
31+ # Raise an alert if a firewall rule is not listed by any of the whitelist rules defined in this constraint file:
32+ # 1. Do the direction (ingress/egress) match?
33+ # 2. Do both firewall rule and whitelist rule have the same fields defined? No more no less.
34+ # 3. Do the IP and their ports match? IPs are checked by equality while ports are checked via ranges. See above as an example.
35+ # 4. Check that whitelist sourceRange/destinationRange CIDR overlaps the whole firewall rule's source range if a source range exist.
36+ # 5. Check regex match for sourceServiceAccounts, sourceTags, targetTags, and targetServiceAccounts.
37+ # All the values in a firewall rule should be whitelisted. PARTIAL matches are NOT enough.
38+
39+
40+ # WARNINGS:
41+ # - partial matches are NOT good enough. A firewall rule should be fully covered by the whitelist rules.
42+ # - some fields like sourceTags and sourceServiceAccounts
43+ # can NOT exist at the same time in a GCP firewall rule. Therefore, please create separate rules for each.
44+ # - As hinted above, to have a match every defined field should exist in both firewall rule and whitelist rule.
45+ # If you try to create a rule for ingress, tcp, 22, from 0.0.0.0/0,
46+ # it does NOT cover ingress, tcp, 22, from 0.0.0.0/0, targetTags = ["https"] since targetTags is not defined in
47+ # whitelisting.
48+
49+ apiVersion : templates.gatekeeper.sh/v1alpha1
50+ kind : ConstraintTemplate
51+ metadata :
52+ name : gcp-network-firewall-whitelist-v1
53+ annotations :
54+ # Example of tying a template to a CIS benchmark
55+ benchmark : CIS11_5.03
56+ spec :
57+ crd :
58+ spec :
59+ names :
60+ kind : GCPNetworkFirewallWhitelistConstraintV1
61+ plural : gcpnetworkfirewallwhitelistconstraintsv1
62+ validation :
63+ openAPIV3Schema :
64+ properties :
65+ rules :
66+ type : array
67+ items :
68+ type : object
69+ properties :
70+ direction :
71+ type : string
72+ enum : [ingress, egress]
73+ sourceServiceAccounts :
74+ type : array
75+ items : string
76+ sourceTags :
77+ type : array
78+ items : string
79+ sourceRanges :
80+ type : array
81+ items : string
82+ destinationRanges :
83+ type : array
84+ items : string
85+ targetServiceAccounts :
86+ type : array
87+ items : string
88+ targetTags :
89+ type : array
90+ items : string
91+ allowed :
92+ type : array
93+ items : object
94+ properties :
95+ IPProtocol :
96+ type : string
97+ ports :
98+ type : array
99+ items : string
100+ denied :
101+ type : array
102+ items : object
103+ properties :
104+ IPProtocol :
105+ type : string
106+ ports :
107+ type : array
108+ items : string
109+ targets :
110+ validation.gcp.forsetisecurity.org :
111+ rego : | # INLINE("validator/gcp_network_firewall_whitelist.rego")
112+ #
113+ # Copyright 2019 Google LLC
114+ #
115+ # Licensed under the Apache License, Version 2.0 (the "License");
116+ # you may not use this file except in compliance with the License.
117+ # You may obtain a copy of the License at
118+ #
119+ # http://www.apache.org/licenses/LICENSE-2.0
120+ #
121+ # Unless required by applicable law or agreed to in writing, software
122+ # distributed under the License is distributed on an "AS IS" BASIS,
123+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
124+ # See the License for the specific language governing permissions and
125+ # limitations under the License.
126+ #
127+
128+ package templates.gcp.GCPNetworkFirewallWhitelistConstraintV1
129+
130+ import data.validator.gcp.lib as lib
131+
132+ ###########################
133+ # Find Whitelist Violations
134+ ###########################
135+
136+ deny[{
137+ "msg": message,
138+ "details": metadata,
139+ }] {
140+ constraint := input.constraint
141+ lib.get_constraint_params(constraint, params)
142+
143+ asset := input.asset
144+ asset.asset_type == "compute.googleapis.com/Firewall"
145+
146+ fw_rule := asset.resource.data
147+
148+ not is_there_any_match(fw_rule, params.rules)
149+ message := sprintf("%s Firewall rule is prohibited.", [asset.name])
150+ metadata := {
151+ "resource": asset.name,
152+ "allowed_rules": params.rules,
153+ }
154+ }
155+
156+ is_there_any_match(fw_rule, param_rules) {
157+ check_any_match(fw_rule, param_rules[_])
158+ }
159+
160+ check_any_match(fw_rule, param_rule) {
161+ lower(fw_rule.direction) == lower(param_rule.direction)
162+
163+ do_all_fields_exist_in_both(fw_rule, param_rule)
164+
165+ # check sourceRanges or destinationRanges overlap if they are defined
166+ do_source_destination_cidr_ranges_overlap( fw_rule, param_rule)
167+
168+ regex_array_fields := [
169+ "sourceTags",
170+ "sourceServiceAccounts",
171+ "targetTags",
172+ "targetServiceAccounts",
173+ ]
174+
175+ do_all_regex_fields_match(fw_rule, param_rule, regex_array_fields)
176+
177+ do_all_protocols_and_ports_match(fw_rule, param_rule)
178+ }
179+
180+ ######### CHECKING IP RANGES ############
181+
182+ # We check either source or destination ranges match
183+ do_source_destination_cidr_ranges_overlap(fw_rule, param_rule) {
184+ param_rule.direction == "ingress"
185+ do_cidr_ranges_overlap(fw_rule, param_rule, "sourceRanges")
186+ }
187+
188+ do_source_destination_cidr_ranges_overlap(fw_rule, param_rule) {
189+ param_rule.direction == "egress"
190+ do_cidr_ranges_overlap(fw_rule, param_rule, "destinationRanges")
191+ }
192+
193+ do_cidr_ranges_overlap(fw_rule, param_rule, field_name) {
194+ # if there are no ranges defined, simply skip
195+ # be aware that we have already checked the existence
196+ # or non-existence of sourceRanges in both
197+ # parameter and firewall rules
198+ not lib.has_field(param_rule, field_name)
199+ }
200+
201+ do_cidr_ranges_overlap(fw_rule, param_rule, field_name) {
202+ overlapped_ranges := [t | t := fw_rule[field_name][i]; any_cidr_overlap(t, param_rule[field_name])]
203+ count(overlapped_ranges) == count(fw_rule[field_name])
204+ }
205+
206+ any_cidr_overlap(fw_cidr_range, param_cidr_ranges) {
207+ net.cidr_contains(param_cidr_ranges[_], fw_cidr_range)
208+ }
209+
210+ ######### CHECKING TYPE ############
211+
212+ check_type(fw_rule) = "allowed" {
213+ lib.has_field(fw_rule, "allowed")
214+ }
215+
216+ check_type(fw_rule) = "denied" {
217+ lib.has_field(fw_rule, "denied")
218+ }
219+
220+ ######### CHECKING PROTOCOL PORT MATCH ############
221+
222+ do_all_protocols_and_ports_match(fw_rule, param_rule) {
223+ allowed_or_denied := check_type(fw_rule)
224+
225+ # we collect all the protocol-port pairs that are covered by the parameters
226+ matched_ones := [t | t := fw_rule[allowed_or_denied][i]; does_a_protocol_and_port_match(t, param_rule[allowed_or_denied])]
227+
228+ count(matched_ones) == count(fw_rule[allowed_or_denied])
229+ }
230+
231+ does_a_protocol_and_port_match(fw_protocol_port, param_protocols_ports) {
232+ any_protocol_port_match(fw_protocol_port, param_protocols_ports[_])
233+ }
234+
235+ any_protocol_port_match(fw_proto_port, param_proto_port) {
236+ lower(fw_proto_port.IPProtocol) == lower(param_proto_port.IPProtocol)
237+ lib.has_field(fw_proto_port, "ports")
238+ lib.has_field(param_proto_port, "ports")
239+ all_ports_match(fw_proto_port.ports, param_proto_port.ports)
240+ }
241+
242+ any_protocol_port_match(fw_proto_port, param_proto_port) {
243+ lower(fw_proto_port.IPProtocol) == lower(param_proto_port.IPProtocol)
244+ not lib.has_field(fw_proto_port, "ports")
245+ not lib.has_field(param_proto_port, "ports")
246+ }
247+
248+ all_ports_match(fw_ports, param_ports) {
249+ trace(sprintf("matching fw_ports %v to param_ports %v", [fw_ports, param_ports]))
250+ matched_ports := [t | t := fw_ports[i]; do_ports_match(t, param_ports[_])]
251+ trace(sprintf("matched fw_ports %v ", [matched_ports]))
252+ count(matched_ports) == count(fw_ports)
253+ }
254+
255+ do_ports_match(fw_port, param_port) {
256+ fw_port == param_port
257+ }
258+
259+ do_ports_match(fw_port, param_port) {
260+ # If both are ranges
261+ contains(fw_port, "-")
262+ contains(param_port, "-")
263+ range_match(fw_port, param_port)
264+ }
265+
266+ do_ports_match(fw_port, param_port) {
267+ # If fw_port is a range but not parameter
268+ contains(fw_port, "-")
269+ not contains(param_port, "-")
270+ param_port_range := sprintf("%s-%s", [param_port, param_port])
271+ range_match(fw_port, param_port_range)
272+ }
273+
274+ do_ports_match(fw_port, param_port) {
275+ # If param_port is a range but not the actual firewall port
276+ not contains(fw_port, "-")
277+ contains(param_port, "-")
278+ fw_port_range := sprintf("%s-%s", [fw_port, fw_port])
279+ range_match(fw_port_range, param_port)
280+ }
281+
282+ # range_match tests if test_range is included in target_range
283+ # returns true if test_range is equal to, or included in target_range
284+ range_match(test_range, target_range) {
285+ # check if target_range is a range
286+ re_match("-", target_range)
287+
288+ # check if test_range is a range
289+ re_match( "-", test_range)
290+
291+ # getting the target range bounds
292+ target_range_bounds := split(target_range, "-")
293+ target_low_bound := to_number(target_range_bounds[0])
294+ target_high_bound := to_number(target_range_bounds[1])
295+
296+ # getting the test range bounds
297+ test_range_bounds := split(test_range, "-")
298+ test_low_bound := to_number(test_range_bounds[0])
299+ test_high_bound := to_number(test_range_bounds[1])
300+
301+ # check if test low bound is >= target low bound and target high bound >= test high bound
302+ test_low_bound >= target_low_bound
303+
304+ test_high_bound <= target_high_bound
305+ }
306+
307+ ######### CHECKING REGEX MATCH FOR TAGS and SERVICE ACCOUNTS ############
308+
309+ do_all_regex_fields_match(fw_rule, param_rule, field_names) {
310+ matched_fields := [t | t := field_names[i]; do_fields_match(fw_rule, param_rule, t)]
311+ count(matched_fields) == count(field_names)
312+ }
313+
314+ do_fields_match(fw_rule, param_rule, field_name) {
315+ # if the field does not exist in both, that's fine.
316+ not lib.has_field(fw_rule, field_name)
317+ not lib.has_field(param_rule, field_name)
318+ }
319+
320+ # all the fields inside a firewall rules should be matched
321+ # to achive this, we filter out matched ones. If any unmatched is left
322+ # return false.
323+ do_fields_match(fw_rule, param_rule, field_name) {
324+ matched_firewall_tags_sas := [t | t := fw_rule[field_name][i]; any_fw_tag_sa_match(t, param_rule[field_name])]
325+ 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]]))
326+
327+ # if all the network tags of the firewall rule matches,
328+ # then this rule is covered
329+ count(fw_rule[field_name]) == count(matched_firewall_tags_sas)
330+ }
331+
332+ any_fw_tag_sa_match(fw_rule_items, tags_sas_param) {
333+ re_match(tags_sas_param[_], fw_rule_items)
334+ }
335+
336+ ######### CHECKING EXISTENCE OF FIELDS ############
337+
338+ # make sure that if a field is defined in fw_rule it should exist in parameter as well.
339+ # and vice-versa. Any deviation means not match.
340+ # We don't check protocol and port since they are displayed differently
341+ do_all_fields_exist_in_both(fw_rule, param_rule) {
342+ fields := [
343+ "sourceTags",
344+ "sourceRanges",
345+ "sourceServiceAccounts",
346+ "targetTags",
347+ "targetServiceAccounts",
348+ "destinationRanges",
349+ "allowed",
350+ "denied",
351+ "direction",
352+ ]
353+
354+ field_in_both(fw_rule, param_rule, fields[_])
355+ }
356+
357+ field_in_both(fw_rule, param_rule, field) {
358+ lib.has_field(fw_rule, field)
359+ lib.has_field(param_rule, field)
360+ }
361+
362+ field_in_both(fw_rule, param_rule, field) {
363+ not lib.has_field(fw_rule, field)
364+ not lib.has_field(param_rule, field)
365+ }
366+ #ENDINLINE
0 commit comments