# 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-storage-bucket-retention-v1
spec:
  crd:
    spec:
      names:
        kind: GCPStorageBucketRetentionConstraintV1
      validation:
        openAPIV3Schema:
          properties:
            minimum_retention_days:
              type: integer
              description: "Minimum number of days that a storage bucket life should
                be. Generate a violation if any of the following are true: * lifecycle
                Delete 'age' condition (i.e. retention days) is less than minimum_retention_days
                * lifecycle Delete 'createdBefore' condition is less than minimum_retention_days
                * lifecycle Delete 'numNewerVersions' is 0 or does NOT exist E.g.
                minimum_retention_days: 100 will generate violations for --> * Any
                storage buckets that have a retention less than 100 days * Any storage
                buckets whose difference between now and its 'createdBefore' condition
                is less than 100 days * Any storage buckets where 'numNewerVersions'
                is 0 or does NOT exist"
            maximum_retention_days:
              type: integer
              description: "Maximum number of days that a storage bucket life should
                be limited to. Generate a violation if any of the following are true:
                * lifecycle Delete 'age' condition (i.e. retention days) is greater
                than maximum_retention_days * lifecycle Delete 'age' condition does
                NOT exist and maximum_retention_days is defined * lifecycle Delete
                action itself does NOT exist when maximum_retention_days is defined
                E.g. maximum_retention_days: 200 will generate violations for -->
                * Any storage buckets that have a retention greater than 200 days
                * Any storage buckets that have a Delete action but no 'age' specified
                * Any storage buckets that do NOT have a Delete action NOTE: Add values
                for both minimum_retention_days and maximum_retention_days to define
                a range of days that the resource retention should be within. E.g.
                minimum_retention_days: 100 and maximum_retention_days: 200 will generate
                violations for --> * Any storage buckets that have a retention less
                than 100 days * Any storage buckets that have a retention greater
                than 200 days * Any storage buckets that have a Delete action but
                no 'age' specified * Any storage buckets that do NOT have a Delete
                action * Any storage buckets whose difference between now and its
                'createdBefore' condition is less than 100 days * Any storage buckets
                where 'numNewerVersions' is 0 or does NOT exist"
            exemptions:
              type: array
              items:
                type: string
              description: "Array of storage buckets to exempt from retention restriction.
                String values in the array should correspond to the full name values
                of exempted storage buckets, e.g. //storage.googleapis.com/my-storage-bucket"
  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.GCPStorageBucketRetentionConstraintV1

        import data.validator.gcp.lib as lib

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

        	asset := input.asset
        	asset.asset_type == "storage.googleapis.com/Bucket"

        	# Check if resource is in exempt list
        	exempt_list := params.exemptions
        	matches := {asset.name} & cast_set(exempt_list)
        	count(matches) == 0

        	violations := get_diff
        	count(violations) > 0
        	violation_msg := concat(" & ", violations)
        	message := sprintf("Storage bucket %v has a retention policy violation: %v", [asset.name, violation_msg])

        	metadata := {
        		"resource": asset.name,
        		"violation_type": violations,
        	}
        }

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

        # Generate a violation if there is no bucket lifecycle Delete condition and maximum_retention_days is defined.
        get_diff[output] {
        	input.constraint.spec.parameters.maximum_retention_days != ""
        	lifecycle_delete_exists := [is_delete | is_delete = input.asset.resource.data.lifecycle.rule[_].action.type == "Delete"; is_delete = true]
        	count(lifecycle_delete_exists) == 0
        	output := "Lifecycle delete action does not exist when maximum_retention_days is defined"
        }

        # Generate a violation if the bucket lifecycle Delete 'age' condition is greater than the maximum_retention_days defined.
        get_diff[output] {
        	some i
        	input.constraint.spec.parameters.maximum_retention_days != ""
        	input.asset.resource.data.lifecycle.rule[i].action.type == "Delete"
        	lifecycle_age := input.asset.resource.data.lifecycle.rule[i].condition.age
        	lifecycle_age > input.constraint.spec.parameters.maximum_retention_days
        	output := "Lifecycle age is greater than maximum_retention_days"
        }

        # Generate a violation if the bucket lifecycle Delete 'age' condition does NOT exist and maximum_retention_days is defined.
        get_diff[output] {
        	some i
        	input.constraint.spec.parameters.maximum_retention_days != ""
        	input.asset.resource.data.lifecycle.rule[i].action.type == "Delete"
        	lifecycle_age := lib.get_default(input.asset.resource.data.lifecycle.rule[i].condition, "age", "")
        	lifecycle_age == ""
        	output := "Lifecycle age is not set when maximum_retention_days is defined"
        }

        # Generate a violation if lifecycle Delete 'age' condition (i.e. retention days) is less than minimum_retention_days
        get_diff[output] {
        	some i
        	input.constraint.spec.parameters.minimum_retention_days != ""
        	input.asset.resource.data.lifecycle.rule[i].action.type == "Delete"
        	rule_condition := input.asset.resource.data.lifecycle.rule[i].condition
        	output := get_min_retention_age_violation(rule_condition, input.constraint.spec.parameters.minimum_retention_days)
        }

        # lifecycle Delete 'createdBefore' condition is less than minimum_retention_days
        get_diff[output] {
        	some i
        	input.constraint.spec.parameters.minimum_retention_days != ""
        	input.asset.resource.data.lifecycle.rule[i].action.type == "Delete"
        	rule_condition := input.asset.resource.data.lifecycle.rule[i].condition
        	output := get_min_retention_created_before_violation(rule_condition, input.constraint.spec.parameters.minimum_retention_days)
        }

        # lifecycle Delete 'numNewerVersions' is 0 or does NOT exist when minimum_retention_days is defined
        get_diff[output] {
        	some i
        	input.constraint.spec.parameters.minimum_retention_days != ""
        	input.asset.resource.data.lifecycle.rule[i].action.type == "Delete"
        	rule_condition := input.asset.resource.data.lifecycle.rule[i].condition
        	output := get_min_retention_num_newer_versions_violation(rule_condition, input.constraint.spec.parameters.minimum_retention_days)
        }

        # Generate a violation if the bucket lifecycle Delete 'age' condition is less than the minimum_retention_days defined.
        get_min_retention_age_violation(rule_condition, minimum_retention_days) = output {
        	lifecycle_age := rule_condition.age
        	lifecycle_age != ""

        	lifecycle_age < minimum_retention_days
        	output := "Lifecycle age is less than minimum_retention_days"
        }

        # Generate a violation if the difference between now and the bucket lifecycle Delete 'createdBefore' condition is less than the minimum_retention_days defined.
        get_min_retention_created_before_violation(rule_condition, minimum_retention_days) = output {
        	created_before := rule_condition.createdBefore
        	created_before != ""
        	created_before_date_ns := convert_rfc3339_compatible(created_before)
        	diff_days := get_diff_in_days_from_now(created_before_date_ns)

        	diff_days < minimum_retention_days
        	output := "createdBefore condition is less than minimum_retention_days"
        }

        # Generate a violation if the bucket lifecycle Delete 'numNewerVersions' condition is 0 or does NOT exist when minimum_retention_days is defined.
        get_min_retention_num_newer_versions_violation(rule_condition, minimum_retention_days) = output {
        	num_newer_versions := lib.get_default(rule_condition, "numNewerVersions", 0)

        	num_newer_versions == 0
        	output := "numNewerVersions condition is 0 or non-existent"
        }

        # Convert 'createdBefore' time to ns
        convert_rfc3339_compatible(input_time) = converted_ns {
        	t_format := "T00:00:00Z"
        	input_time_array := [input_time, t_format]
        	concat_array := concat("", input_time_array)
        	converted_ns := time.parse_rfc3339_ns(concat_array)
        }

        # Get the difference in days between now and 'createdBefore' time
        get_diff_in_days_from_now(time_ns) = diff_in_days {
        	ns_in_days := ((((24 * 60) * 60) * 1000) * 1000) * 1000
        	time_now_ms := time.now_ns()
        	diff_ns := time_now_ms - time_ns
        	diff_in_days := round(diff_ns / ns_in_days)
        }
        #ENDINLINE
