How to write custom policies for Trivy

How to write custom policies for Trivy

Using the Rego languange

TL;DR Code

image.png

Trivy?

Trivy (tri pronounced like trigger, vy pronounced like envy) is a simple and comprehensive vulnerability/misconfiguration/secret scanner for containers and other artifacts developed by Aqua Security.

To know more about Trivy, I highly recommend to check following videos and of course the official documentation.

I installed the Trivy cli on macOS via Homebrew with following command:

brew install aquasecurity/trivy/trivy

Read the installation docs for your operating system.

Introduction to misconfiguration policies

In this blog article, I want to explain how you can write your own custom policies for Trivy to detect any misconfiguration in your configuration files.

Currently, Trivy supports following types of configuration files:

  • Kubernetes
  • Dockerfile, Containerfile
  • Terraform
  • CloudFormation
  • Helm Chart
  • RBAC

There is already a large set of build-in policies for these configuration files provided by the good people at Aqua Security and the Trivy community.

Write your first custom policy

Most important item you need to keep in mind is that custom policies in Trivy are written in Rego. I highly suggest to familiarize yourself with the Rego language.

As mention above, Trivy supports certain configuration files and detect the right policy via the file extension the type of the configuration.

File extensionConfiguration
.yaml, .yml and *.jsonKubernetes / Helm
Dockerfile, Dockerfile., and .DockerfileDockerfile
Containerfile, Containerfile., and .ContainerfileContainerfile
.yaml, .yml and *.jsonCloudFormation
.tf and .tf.jsonTerraform / Terraform Plan

Anatomy of a custom policy

I will use a very simple use case to explain the anatomy of a custom policy.

Let's assume following scenario: You want only allow that Pods from a specific container registry are allowed to be deployed to your cluster.

package user.kubernetes.ED001

import future.keywords.in
import data.lib.kubernetes
import data.lib.result

default allowedRegistries = ["quay.io","ghcr.io","gcr.io"]

__rego_metadata__ := {
  "id": "ED001",
  "title": "Allowed container registry checks",
  "severity": "CRITICAL",
  "description": "The usage of non allowed container registries is not allowed",
}

__rego_input__ := {
  "combine": false,
  "selector": [{"type": "kubernetes"}],
}

allowedRegistry(image) {
  registry := allowedRegistries[_]
  startswith(image, registry)
}

deny[res] {
  container := kubernetes.containers[_]
  not allowedRegistry(container.image)
  msg :=  kubernetes.format(sprintf("Container '%s' of %s '%s' comes from not approved container registry", [container.name, kubernetes.kind, kubernetes.name]))
  res := result.new(msg, container)
}

package is a required field and MUST be unique per policy. It must start with namespace name, the rest is up to you as long as it is unique. Here in my example I use user.kubernetes.ED001 as package name.

  • As namespace I chose user
  • A group name for clarity (kubernetes)
  • and policy id (ED001).

The namespace we will use later as a parameter, when we call Trivy to scan the files.

import future.keywords is a special import that allows to use future keywords in your policy.

import data.lib.result is a special import that allows to use the result library to highlight the findings.

__rego_metadata__ helps enrich Trivy's scan results with useful information. All fields are optional. Please check the official documentation for more information on all available fields and their meaning.

__rego_input__ an optional field that allows to filter the input data. Here in my example I only want to scan Kubernetes resources and ignore any other configuration types.

deny is a required field. According to AquaSecurity warn, violation also work for compatibility but deny is recommended to use. You can always use severity field in the __rego_metadata__

So what does my deny do in detail?

First, we check that we only apply the rule on type Pod. Then we iterate over all containers in the Pod and check if the image of the container starts with the allowedContainerRegistry.

If not, we build a message pointing the issue and highlight the container with the help of result.new(msg, container)

deny[res] {
    input.kind == "Pod"
    some container in input.spec.containers
    not startswith(container.image, allowedContainerRegistry)
    msg := sprintf("Image '%v' comes from not approved container registry in `%v`", [container.image, allowedContainerRegistry])
    res := result.new(msg, container)
}

Calling Trivy with our custom policy

I created a basic file structure for my custom policies. Under the policies directory I created subdirectories for each configuration file type.

To scan the files, I used the following command:

trivy conf --config-policy policies --policy-namespaces user config/

The subcommand config tells Trivy to call the scanning of config files. The flag --config-policy specify paths to the custom policy files directory and flag --policy-namespaces is the namespace we defined above.

As argument to the subcommand config I used the path to the directory containing the configuration files I want to scan.

You should see following output

2022-07-25T18:10:55.035+0200    INFO    Misconfiguration scanning is enabled
2022-07-25T18:10:55.213+0200    INFO    Detected config files: 1

pod.yaml (kubernetes)

Tests: 3 (SUCCESSES: 1, FAILURES: 2, EXCEPTIONS: 0)
Failures: 2 (CRITICAL: 2)

CRITICAL: Image 'nginx' comes from not approved container registry in `docker.io`
════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
The usage of Docker Hub as container registry is not allowed.
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 pod.yaml:9-11
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   9 ┌   - image: nginx
  10 │     name: nginx
  11 └     resources: {}
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────


CRITICAL: Image 'nginx' comes from not approved container registry in `docker.io`
════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
The usage of Docker Hub as container registry is not allowed.
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 pod.yaml:12-14
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  12 ┌   - image: nginx
  13 │     name: nginx
  14 └     resources: {}
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Wrap-up

The ability to write your own policies is a very interessting feature in Trivy. Especially if you already use Trivy in your CI/CD pipelines to validate any configuration files.

That Trivy is using Rego under the hood is a big plus, as you can create very powerfull polices with it and there als a huge community behind Rego