policy-library/policies/templates/gcp_resource_value_pattern_v1.yaml (147 lines of code) (raw):

# 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. # apiVersion: templates.gatekeeper.sh/v1alpha1 kind: ConstraintTemplate metadata: name: gcp-resource-value-pattern-v1 spec: crd: spec: names: kind: GCPResourceValuePatternConstraintV1 validation: openAPIV3Schema: required: ["mode", "asset_types", "field_name"] properties: mode: type: string enum: [allowlist, denylist] description: "String identifying the operational mode, allowlist or denylist. In allowlist mode, resources with field_name values that do NOT match the pattern defined will generate a violation. In denylist mode, resources with field_name values that match the pattern defined will generate a violation. Defaults to allowlist." asset_types: type: array items: type: string description: "List of Google CAI asset types to apply this constraint to. E.g. 'cloudresourcemanager.googleapis.com/Project' E.g. 'compute.googleapis.com/Instance" field_name: type: string description: "The (dot) path separated field name from the asset data to validate. E.g. 'labels.billing_id' would indicate the value for 'data': {'labels': {'billing_id': 'x'}} E.g. 'loggingService' would indicate the value for 'data': {'loggingService': 'y'}}" pattern: type: string description: "A regular expression to validate the value of the field name against. If the field is required and exists, the regular expression must match the value otherwise the asset will fail validation. Also ensure to use anchors ^$ if you need them as by default no start and end anchors are defined. E.g. '^\\d{4}$' will look for any four digits (1234) E.g. 'logging.googleapis.com' will only look for 'logging.googleapis.com'" optional: type: boolean description: "Set to true to make the field_name appearing in the asset optional- a violation is NOT generated if the field_name does not exist. Defaults to false (field_name MUST appear)." 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.GCPResourceValuePatternConstraintV1 import data.validator.gcp.lib as lib ########################### # Matches any field, including nested fields in asset data against # a regular expression pattern for validity checking. # fields can be optional such that missing fields in data # will not trigger a violation; or by default required # such that missing fields will trigger a violation. ########################### deny[{ "msg": message, "details": metadata, }] { constraint := input.constraint params := lib.get_constraint_params(constraint) asset := input.asset asset.asset_type = params.asset_types[_] field_name := params.field_name mode := lib.get_default(params, "mode", "allowlist") rule_data := { "is_optional": lib.get_default(params, "optional", false), "has_field": has_field_by_path(asset.resource.data, field_name), "has_pattern": lib.has_field(params, "pattern"), "pattern": lib.get_default(params, "pattern", ""), "value": get_default_by_path(asset.resource.data, field_name, ""), } is_not_valid(mode, rule_data) message := sprintf("%v has %v violation for field named '%v' with a value '%v' matching pattern '%v'.", [ asset.name, mode, field_name, rule_data.value, rule_data.pattern, ]) metadata := { "resource": asset.name, "mode": mode, } } ########################### # Rule Utilities ########################### # is_not_valid evaluates to true if mode is allowlist and: # optional = false; field_name exists in resource; pattern is NOT found # optional = false; field_name does NOT exist in resource # optional = true; field_name exists in resource; pattern is NOT found is_not_valid(mode, rule_data) { mode == "allowlist" allowlist_violation(rule_data) } # is_not_valid evaluates to true if mode is denylist and: # optional = false; field_name exists in resource; pattern is found # optional = false; field_name does NOT exist in resource # optional = true; field_name exists in resource; pattern is found is_not_valid(mode, rule_data) { mode == "denylist" denylist_violation(rule_data) } denylist_violation(rule_data) { is_optional_field_valid(rule_data) is_denylist_pattern_valid(rule_data) } denylist_violation(rule_data) { not is_optional_field_valid(rule_data) } is_optional_field_valid(rule_data) { rule_data.has_field == true } is_optional_field_valid(rule_data) { rule_data.is_optional == true rule_data.has_field == false } is_denylist_pattern_valid(rule_data) { rule_data.has_pattern == true rule_data.has_field == true re_match(rule_data.pattern, rule_data.value) } allowlist_violation(rule_data) { is_optional_field_valid(rule_data) not is_allowlist_pattern_valid(rule_data) } allowlist_violation(rule_data) { not is_optional_field_valid(rule_data) } is_allowlist_pattern_valid(rule_data) { rule_data.has_pattern == false } is_allowlist_pattern_valid(rule_data) { rule_data.has_field == false } is_allowlist_pattern_valid(rule_data) { rule_data.has_pattern == true rule_data.has_field == true re_match(rule_data.pattern, rule_data.value) } ########################### # Rule Library Functions ########################### get_field_by_path(obj, path) = output { split(path, ".", path_parts) walk(obj, [path_parts, output]) } # wrapper around walk to explicitly capture the output in order to generate a # true / false output instead of undefined. # see: https://openpolicyagent.slack.com/messages/C1H19LW4F/convo/C1H19LW4F-1552948594.244300/ _has_field_by_path(obj, path) { _ := get_field_by_path(obj, path) } has_field_by_path(obj, path) { _has_field_by_path(obj, path) } has_field_by_path(obj, path) = false { not _has_field_by_path(obj, path) } get_default_by_path(obj, path, _default) = output { has_field_by_path(obj, path) output := get_field_by_path(obj, path) } get_default_by_path(obj, path, _default) = output { false == has_field_by_path(obj, path) output := _default } #ENDINLINE