Rego 103: Types of values and rules
Tags: iacsecurity, cloudsecurity, rego, opa
Published: 2023-11-17 02:00:13

This blog post series offers a gentle introduction to Rego, the policy language from the creators of the [Open Policy Agent (OPA)](https://www.openpolicyagent.org) engine. If you’re a beginner and want to get started with writing Rego policy as code, you’re in the right place.


In this three-part series, we’ll go over the following:


* [Part 1](https://snyk.io/blog/introduction-to-rego/): Rego 101: Introduction to Rego
* [Part 2](https://snyk.io/blog/rego-for-beginners-part-2/): Rego 102: Combining queries with AND/OR and custom messages
* Part 3 (this part!): Types of values and rules


As a reminder, [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) is a declarative query language from the makers of the [Open Policy Agent (OPA)](https://www.openpolicyagent.org) framework. The [Cloud Native Computing Foundation (CNCF)](https://www.cncf.io/) accepted OPA as an incubation-level hosted project in April 2019, and OPA [graduated](https://www.cncf.io/announcements/2021/02/04/cloud-native-computing-foundation-announces-open-policy-agent-graduation/) from incubating status in 2021.


**Rego** is used to write [**policy as code**](https://snyk.io/learn/policy-as-code/), which applies programming practices such as version control and modular design to the evaluation of cloud and infrastructure as code (IaC) resources. **OPA** is the engine that evaluates policy as code written in Rego. And Snyk uses the Rego language for [custom rules](https://docs.snyk.io/scan-infrastructure/build-your-own-custom-rules/custom-rules/writing-rules-using-the-sdk/writing-a-rule).


Part 2 recap
------------


In [Part 2](https://snyk.io/blog/rego-for-beginners-part-2/), we showed you how to use the following:


* **AND** and **OR** rules
* The `default` keyword
* The `not` keyword
* Custom `deny` messages


In this part, we'll round out the series by focusing on set rules, object rules, functions, and iteration.


Types of values
---------------


In Rego, a **value** is a representation of some kind of data. Each value is of a specific **type**. Rego types fall into two categories: **scalar** and **composite**.


**Scalar values** represent a *single* unit of data and include the following types:


* Strings are surrounded by double quotes.
* Numbers include positive and negative integers and decimals.
* Booleans can only be `true` or `false`.
* `null` represents the *absence* of a value.


**Composite values** represent a *collection* of values and include the following types:


* Arrays
* Objects
* Sets


If you've been reading our blog post series, you've already seen several examples of scalar and composite values. Composite values are a little more complex, so we'll take some time to dig into them.


### Composite values


**Arrays** are *ordered* lists of one or more values surrounded by brackets. You can have an array of strings, an array of numbers, an array of arrays, an array of mixed types, and so on:


* `["alice", "bob", "carlotta"]`
* `[2, -5, 3.8]`
* `[[1, 2, 3], [4, 5, 6]]`
* `[true, "banana", 17]`


You can access any element inside an array by referring to its **index** or position in the array. Indexes always start at 0, so the first item in an array has the index 0, the second has the index 1, and so on, as shown in the `users` array below:



```
users := ["alice", "bob", "carlotta"]
             0       1        2 
```

If you want to retrieve the first item in the users list, you'd use this syntax:



```
users[0]  # evaluates to "alice"
```

If you want to assign the second item to the variable `admin`, you'd refer to it like so:



```
admin := users[1]  # "bob" is assigned to admin
```

If you use the syntax `users[3]` to try to retrieve a (nonexistent) fourth item in the list, OPA will not find any matches, and the output will be an empty set `{}` (undefined).


**Objects** are *unordered* lists of one or more key-value pairs surrounded by curly braces. A key and value can be of any type, and the types don't have to match. Each key and its corresponding value are joined by a colon:


* `{ "alice": "admin", "bob": "user", "carlotta": "user" }`
* `{ "ports": [80, 443] }`
* `{ 80: true }`


You can access the value of an object by specifying its key. For example, the `users` object below contains three key-value pairs:



```
users := { "alice": "admin", "bob": "user", "carlotta": "user" }
```

In a query, to retrieve the value of the key-value pair with the key `"alice"`, you'd use this syntax:



```
users["alice"]  # evaluates to "admin"
```

**Sets** are *unordered* lists of one or more *unique* values, also surrounded by curly braces. Values can be of any type:


* `{1, 2, 3}`
* `{"alice", "bob", "carlotta"}`


Because sets are unordered, two sets can be equal if they have the same elements, even if they are in a different order. For example, `{1, 2, 3}` is equal to `{2, 3, 1}`.


You can look up whether an element is in a set. Let's say you have the following set:



```
nums := {1, 2, 3}
```

In a query, to look up whether the integer `4` is in the `nums` set, you'd use this syntax:



```
nums[4]  # does not evaluate to true
```

Types of rules
--------------


There are several different types of rules in Rego:


* **Complete rules** that produce a single result.
* Rules that **generate sets**.
* Rules that **generate objects**.
* **Functions**, which are actually a little different from rules.


Each of these can have a body consisting of queries. The difference is how the queries are constructed and what information they return. In the next section, we'll explain each type of rule, starting with complete rules.


Complete rules
--------------


If you've been reading this series, you've seen a complete rule already:



```
allow := true {
  input.user == "alice"
}
```

A **complete rule** assigns a *single* value to a variable. Above, the variable `allow` would be assigned the value `true` if the condition in the query is met.


Here's another example, this time with the number `80` being assigned to the variable `allowed_port` if the condition in the query is met:



```
allowed_port := 80 {
  input.account_id != "123456789012"
}
```

A complete rule can have one query, as above, or it can have multiple queries, like we've seen in another example from Part 2:



```
allow := true {
  input.user == "alice"
  input.environment == "prod"
}
```

### Constants


What if you want a variable to hold a particular value no matter what? This is called a constant in other programming languages.


You could write it like this:



```
pi := 3.14 {
  true
}
```

Because the condition in the query is always met, it evaluates to `true` no matter what — the variable `pi` always has the value `3.14`.


Thanks to some syntactic sugar, you can then leave off the body altogether:



```
pi := 3.14
```

The above is a complete rule, and no matter where you refer to `pi` in the program, it always represents `3.14`.


Set and object comprehensions
-----------------------------


There are many cases where you might want to assign the variable a *collection* of values, and that's where **set comprehensions** and **object comprehensions** come in.


### Set comprehensions


A **set comprehension** adds elements to a variable one at a time, producing a *set*. Occasionally, you may want to iterate through the input file to generate a set of all the values that meet certain conditions. You can write a **set comprehension** to account for that.


Suppose you have this input document representing an array of all the users who are currently logged in across a system:



```
{
  "users": [
    "alice",
    "bob",
    "carlotta"
  ]
}
```

Remember the company policy that says only Alice has administrative permissions? Let's say you want to create a unique list (set) of all the non-administrative users who are logged in.


You could generate a set like this:



```
nonadmins[name] {
  name := input.users[i]
  name != "alice"
}
```

To explain this, we'll look at the rule body first, then the head.


**Rule body:** This instructs OPA to scan the `input.users` array for elements that don't match `"alice"`.


How does this work? We mentioned earlier that you refer to an item in an array by its index. We use the variable `i` here to represent the index (though you could name it something else if you like), because it's incremented on each pass through the input. When OPA runs the queries, it iterates through the `users` list by substituting `i` for the index of each item to grab each element, one at a time. If you're familiar with [imperative programming](https://en.wikipedia.org/wiki/Imperative_programming), this is similar to how an imperative loop works, though it's not quite the same.


The first time OPA passes through the `users` array in the input, this is what happens behind the scenes:



```
name := input.users[0]  # Evaluates to "alice"
name != "alice"  # This condition is not fulfilled, so OPA discards the username and moves on.
```

On the second pass, this is what happens:



```
name := input.users[1]  # Evaluates to "bob"
name != "alice"  # This condition evaluates to true, so OPA adds the list item -- the username "bob" -- to the nonadmins set.
```

On the third pass, this is what happens:



```
name := input.users[2]  # Evaluates to "carlotta"
name != "alice"  # This condition also evaluates to true, so OPA adds the list item -- the username "carlotta" -- to the nonadmins set.
```

**Rule head:** The variable `nonadmins` refers to the set itself, and the variable `name` represents each unique name in the set (which in the body is `input.users[i]`, as explained above). When OPA follows the logic in the body, if the current value of `name` matches all the conditions, that value is added to the `nonadmins` set.


**Put it all together:** To sum up, on each pass through the list of users, OPA adds the current `input.users[i]` value to the `nonadmins[name]` set if the value fulfills all the conditions listed in the queries.


The result is this `nonadmins` set:



```
{
  "nonadmins": [
    "bob",
    "carlotta"
  ]
}
```

And we can see that the non-admin users on duty are Bob and Carlotta!


#### Iteration


A note about iteration in Rego — [iteration is implicit](https://www.openpolicyagent.org/docs/latest/#iteration). There are no "while x == true" or "for y in z" loops here, in contrast to other languages such as Python. Instead, you iterate through an array, set, or object by using a variable instead of an array index, set element, or object key, as we've done with `i` below in the `input.users` array:



```
name := input.users[i]
```

Because we've put a variable inside the brackets, OPA knows that we aren't referring to a single particular value — we're looking at all of them, one at a time.


This is a bit of an adjustment if you're used to imperative loops, but it's succinct! The above is equivalent to the following Python expression:



```
for i in users:
  name = i
```

Or, if you prefer:



```
for i in range(0, len(users)):
  name = users[i]
```

#### The underscore operator


If you only need to refer to the index once, you can use the [wildcard operator](https://www.openpolicyagent.org/docs/latest/#iteration) (an underscore) instead of a named variable like `i`:



```
nonadmins[name] {
  name := input.users[_]
  name != "alice"
}
```

When used as an iterator (like the variable `i`), the wildcard operator represents *any* value in an array, *any* element in a set, or *any* key in an object. In effect, the wildcard is a variable without a name — a throwaway variable. In the rule above, OPA checks whether *any* name in the `input.users` array is **not equal** to `"alice"` (and if so, OPA adds it to the nonadmins set). The end result is exactly the same as if you'd used `name := input.users[i]`.


On the other hand, if you need to keep track of the index in a rule, you'll want to use a named variable, as below:



```
deny[msg] {
  input.users[i] != "alice"
  msg := sprintf("User %v is denied access", [input.users[i]])
}
```

When used with the following input document…



```
{
  "users": [
    "alice",
    "bob",
    "carlotta"
  ]
}
```

… you'd see the following output:



```
{
  "deny": [
    "User bob is denied access",
    "User carlotta is denied access"
  ]
}
```

In this case, we *need* to use a named variable because we want to make sure the name we're checking in the query `input.users[i] != "alice"` is the same name we're printing in the `msg` query. Therefore, it's important to keep track of the index. This is easier to understand if you look at what OPA is doing behind the scenes, where it's substituting an index for the variable `i`. Here's an example of one iteration through the `input.users` array:



```
deny[msg] {
  input.users[2] != "alice"
  msg := sprintf("User %v is denied access", [input.users[2]])
}
```

We need to use `input.users[2]` in both queries to make sure we're referring to the value at the same index (in this case, `"carlotta"`).


### Object comprehension


An **object comprehension** adds elements to a variable one at a time, producing an *object* similar to how set rules generate sets. But while the purpose of generating a set is to create a collection of unique values, an object rule's goal is to produce a *collection of key-value pairs*.


The process is very similar to writing a set rule. However, in an object rule, you specify the *value* part of the key-value pair in the rule head:



```
nonadmins[name] := "logged-in" {
  name = input.users[i]
  name != "alice"
}
```

This example is exactly the same as our first set rule example, *except* this one declares the value of each key-value pair to be `"logged-in"`.


Let's use the same input document:



```
{
  "users": [
    "alice",
    "bob",
    "carlotta"
  ]
}
```

When we evaluate the rule against the input above, the output is this:



```
{
  "nonadmins": {
    "bob": "logged-in",
    "carlotta": "logged-in"
  }
}
```

The result is a `nonadmins` object containing two key-value pairs. Each pair has a username as the key and `"logged-in"` as the value.


Functions
---------


Functions in Rego are like functions in other languages: they present a modular, reusable way to instruct the program to *do* *something*. Function syntax is similar to rule syntax, and they both declare queries in the same way, but a function includes a parameter (which serves as a placeholder for a real argument) surrounded by parentheses.


For example, the function below takes the value of `x`, doubles it, and assigns the resulting value to `y`.



```
double_function(x) := y {
  y := x + x
}
```

Elsewhere in the package, you can *call* it by providing an argument for it to operate on like so:



```
z := double_function(2)
```

And `z` would evaluate to `4`.


You could call it again by passing in argument 12 and assigning the result to `foo`, and `foo` would evaluate to 24:



```
foo := double_function(12)
```

You can use a function when there's some very specific task you need to carry out multiple times, especially within other functions. This is useful when you want cleaner, more modular code. If there's a task you'll do repeatedly with different inputs, you can write a function for it.


You can also write **"helper" functions** that are used in other functions. Below, the `allow` variable is assigned the value `true` if all elements in the `input.tags` array are valid. To determine whether an element is valid, the helper functions `is_lowercase_value` and `is_long_enough` check whether a string argument is lowercase or the right length, respectively. They're both used inside the function `is_valid`, which is called from `allow`:



```
is_lowercase_value(tag) {
  lower(tag) == tag
}

is_long_enough(tag) {
  count(tag) >= 3
}

is_valid(tag) {
  is_lowercase_value(tag)
  is_long_enough(tag)
}

allow {
  tag := input.tags[i]
  is_valid(tag)
}
```

Evaluating example rules with OPA
---------------------------------


Let's experiment with set rules, object rules, and functions in Rego. As in previous blog posts, we will focus on two ways of interacting with OPA:


* Using the OPA Playground
* Using OPA’s command line tool


For instructions on using these interfaces, see Part 1.


Once again, we're using more of a real-world example involving a Kubernetes pod. Here's the JSON manifest we will use as input:



```
{
  "apiVersion": "v1",
  "kind": "Pod",
  "metadata": {
    "name": "mypod",
    "labels": {
      "stage": "prod"
    }
  },
  "spec": {
    "shareProcessNamespace": true,
    "containers": [
      {
        "name": "myapp1",
        "image": "myapp1:latest"
      },
      {
        "name": "myapp2",
        "image": "myapp2"
      }
    ]
  }
}
```

And here is the policy we'll be evaluating it against, enforcing the corporate requirement "Containers in production-stage pods should not use the latest image":



```
is_labeled_prod(labels) {
  labels.stage == "prod"
} {
  labels.stage == "production"
}

latest_containers[container] {
  container := input.spec.containers[_]
  endswith(container.image, ":latest")
}

deny[msg] {
  input.kind == "Pod"
  is_labeled_prod(input.metadata.labels)
  container = latest_containers[_]
  msg := sprintf("Container %v is using latest image on prod", [container.name])
}
```

This set of rules demonstrates several concepts we've discussed in this blog post, such as **functions**, **set rules**, **iteration**, and the **underscore operator**. Here's how it all works:


`is_labeled_prod(labels)` — This is a **function** that returns `true` if the set of labels passed to it includes a `"stage"` label with the value `"prod"` or `"production"`.


`latest_containers[container]` — This is a **set rule**. OPA uses the **underscore operator** as an iterator to check if *any* of the containers in the input uses the latest image, and if so, adds it to the `latest_containers` set. We determine if an image is the latest by checking if the image name ends in `":latest"`.


`deny[msg]` — This is also a **set rule**! It's a more advanced version of the `deny[msg]` set rule we showed you in Part 2. It has three queries, for which OPA takes the following actions:


1. Checks if the `input.kind` is "Pod".
2. Calls the `is_labeled_prod` **function** by passing in `input.metadata.labels` to check if there are any `"stage"` labels with the value `"prod"` or `"production"`.
3. Iterates through the `latest_containers` set to check for *any* containers in the set.


***IF*** the above three queries evaluate to `true` (i.e., OPA finds a match in the input for each condition), ***THEN*** OPA adds a custom message to the `deny` set listing the name of the non-compliant container.


For your convenience, we've created a playground with this content already: <https://play.openpolicyagent.org/p/HgHE4w2b4y> 


If you evaluate the rules by selecting the **Evaluate** button in the playground or by executing a command such as `opa eval -i input.json -d check_prod_pod.rego "data.rules.check_prod_pod" --format pretty` if running OPA locally, you'll see this output:



```
{
  "deny": [
    "Container myapp1 is using latest image on prod"
  ],
  "latest_containers": [
    {
      "image": "myapp1:latest",
      "name": "myapp1"
    }
  ]
}
```

The first item in the output is the deny set, showing a message that the `myapp1` container is not compliant with our policy. You can also see the elements in the `latest_containers` set, which includes the name and image for each container — in this case, the only container is `myapp1`.


Let's see what happens if we change the image name for `myapp2` to `myapp2:latest` (line 19). If we evaluate the rules again, the `deny` set now includes a message for `myapp1` and `myapp2`, and the `latest_containers` set includes both containers:



```
{
  "deny": [
    "Container myapp1 is using latest image on prod",
    "Container myapp2 is using latest image on prod"
  ],
  "latest_containers": [
    {
      "image": "myapp1:latest",
      "name": "myapp1"
    },
    {
      "image": "myapp2:latest",
      "name": "myapp2"
    }
  ]
}
```

Finally, let's remove the `"stage": "prod"` line (line 7) so the input looks like this:



```
    "labels": {
    }
```

If we evaluate the rules now, we can see that `latest_containers` still includes both containers, but the `deny` set is empty:



```
{
  "deny": [],
  "latest_containers": [
    {
      "image": "myapp1:latest",
      "name": "myapp1"
    },
    {
      "image": "myapp2:latest",
      "name": "myapp2"
    }
  ]
}
```

Because the pod isn't labeled for production, it's OK (per corporate policy) that its containers use the latest image. Therefore, the pod is compliant!


What’s next?
------------


Congratulations! You've made it through our three-part blog series about writing Rego.


If you'd like to learn more, here are some useful resources:


* [OPA’s website](https://www.openpolicyagent.org)
* [Rego policy reference](https://www.openpolicyagent.org/docs/latest/policy-reference/)
* [OPA Playground](https://play.openpolicyagent.org/)
* [Using Rego as a generic policy language](https://snyk.io/blog/rego-as-generic-policy-language/)
* [Enabling Policy as Code with OPA and Rego](https://snyk.io/blog/opa-rego-usage-for-policy-as-code/)
* [Five tips for using the Rego language for OPA](https://snyk.io/blog/5-tips-for-using-the-rego-language-for-open-policy-agent-opa/)
* [Developing custom IaC rules with Snyk](https://snyk.io/blog/developing-custom-iac-rules-with-snyk-iac/)


If you’re interested in using Rego to write custom rules for Snyk IaC check out our [documentation](https://docs.snyk.io/scan-infrastructure/build-your-own-custom-rules/build-your-own-iac+-to-cloud-custom-rules) here. In addition to Snyk’s built-in security and compliance-mapped rulesets, IaC+ custom rules enable you to set customized security controls across your SDLC.


IaC+ gives you a single view and controls for your configuration issues from code to cloud with an issues UI, ruleset, and policy engine spanning IDE, SCM, CLI, CI/CD, Terraform Cloud, and deployed cloud environments such as AWS, Azure, and Google Cloud.