Policy Authoring
This document is meant to assist policy authors in creating and maintaining the policy rules defined in this repository.
1. Rule annotations
Policy rules must contain certain annotations that describe additional information about the rule.
-
title
: (required) short description of the policy rule. -
description
: (required) descriptive information about the policy rule, including possible remediation steps. -
custom
: (required) object holding additional non-default rego annotations.custom.foo
means thefoo
annotation nested under this object. -
custom.short_name
: (required) unique name of the policy rule. This is used as the value of thecode
attribute when reporting failures and warnings. It is also the value used for skipping the policy rule via the EnterpriseContractPolicy. It must not contain spaces. Words must be joined by_
, e.g.snake_case
. -
custom.failure_msg
: (required) message indicating the exact cause a policy rule did not pass. It should be as informative as possible to guide users towards remediation. The message can be in the form of a string template, allowing dynamic values to provide a more meaningful message. -
custom.effective_on
: (optional) time stamp string in the RFC3339 format. Defaults to"2022-01-01T00:00:00Z"
. A non-passing policy rule is classified as a warning, instead of a failure, if the date represented in thecustom.effective_on
annotation is in the future. This is a helpful mechanism to allow the introduction of a new policy rule while allowing a certain period of time for compliance. -
custom.rule_data
: (optional) specify additional data for the policy rule. The value must be an object where each key maps to an array of strings. This is a convenient mechanism to specify information that is used in the policy rule evaluation that may not be obvious to users. For example, the policy ruledisallowed_task_step_image
only allows certain registries to be used. The list of registries is defined in the annotationscustom.rule_data.allowed_registry_prefixes
, allowing a single source of truth for policy rule evaluation and documentation. For best results, each key in thecustom.rule_data
object should be a noun. -
custom.collections
: A list of strings representing a list of rule collections that the policy rule is included in.
The annotations must be defined at the rule
scope.
2. Package annotations
Package annotations can be used to give a title and description to a package. Use the package name "policy.<kind>.collection.<collectionName>" in an otherwise empty package for collection annotations.
-
title
: (required) short description of the rule collection. -
description
: (required) descriptive information about the rule collection.
See Open Policy Agent’s documentation for further reference on annotations.
4. Pitfalls
Today, EC takes the conftest approach for asserting violations and warnings. The approach follows the path of proving a negative. The policy rules search for issues. If there are no issues, the policy rule passes. This has pitfalls, e.g. a policy rule could accidentally pass if not written carefully.
The main motivation for the current state is that this allows policy rules to provide precise error messages. This is an important requirement of EC; a simple pass/fail result is just not enough.
To illustrate the pitfalls, consider the following policy rule.
package main
import rego.v1
deny contains result if {
some att in input.attestations
got := att.statement.predicateType
got != "https://slsa.dev/provenance/v0.2"
result := {"msg": sprintf(
"Unexpected predicate type %s in statement %s",
[got, att.statement._type],
)}
}
The policy rule above iterates over the list of attestations in the input, extracts the predicateType for each, and verifies the value is not unexpected. Even this simple policy rule has a few pitfalls.
First, if there are no attestations, the policy rule passes. Consider adding an explicit check to
ensure input.attestations
is not empty.
Second, if an attestation does not have the statement or the statement.predicateType attribute the
rule passes for that attestation. This is problematic because a missing value is clearly not equal
to the expected value, "https://slsa.dev/provenance/v0.2"
. Use helper functions with default
values to access such attributes.
Third, if the statement for an attestation does not set the _type
attribute, the policy rule
passes. Notice how this particular attribute is used only for error reporting. Use helper functions
with default values to access such attributes.
Fourth, typos when accessing nested attributes, e.g. att.statement.predicateTypoooo
, cause the
policy rule to pass. Unlike typos in function names or variable names, these are not caught by the
linter nor the compiler. Use helper functions with default values to access such attributes and
ensure code is tested with real-world data.
Each of those pitfalls can be prevented. However, these require a conscious decision by the policy rule author. This can be even more challenging for authors new to the rego programming language.
The pitfalls above showcase an interesting property of rego. A statement within the rule that produces no value (or a false value) causes the rule to not produce a value as well. In our case, since the rule is asserting a violation, no value means no violation.
Here is a safer version of the example above:
package main
import rego.v1
deny contains result if {
some att in attestations
got := predicate_type(att)
got != "https://slsa.dev/provenance/v0.2"
result := {"msg": sprintf(
"Unexpected predicate type %s in statement %s",
[got, statement_type(att)],
)}
}
deny contains result if {
count(attestations) == 0
result := {"msg": "No attestation found"}
}
attestations := object.get(input, "attestations", [])
statement_type(att) := object.get(att, ["statement", "_type"], "N/A")
predicate_type(att) := object.get(att, ["statement", "predicateType"], "N/A")