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