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 the foo annotation nested under this object.

  • custom.short_name: (required) unique name of the policy rule. This is used as the value of the code 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 the custom.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 rule disallowed_task_step_image only allows certain registries to be used. The list of registries is defined in the annotations custom.rule_data.allowed_registry_prefixes, allowing a single source of truth for policy rule evaluation and documentation. For best results, each key in the custom.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.

3. Input

The ec-cli is reponsible for gathering the information to be validated which is made available to policies via the input object. Its structure is defined here.

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")