Skip to content

Note: This blog post was updated on December 10, 2021, to reflect fregot v0.13.4.

Fugue performs more than 100 million policy validations a day in order to identify compliance violations for cloud infrastructure environments at scale. These policy-as-code validations are written in Rego, the policy language for the Open Policy Agent (OPA) engine. To enhance the process of writing and debugging Rego policies, we recently open-sourced fregot, the Fugue Rego Toolkit.

 

fregot-1

 

 

You can think of fregot as an alternative to OPA's built-in interpreter -- the REPL allows you interactively debug Rego code with easy-to-understand error messages, and you can evaluate expressions and test policies. Read more about it in our blog post here.

 

This tutorial is an abbreviated version of the full walkthrough in fregot's GitHub repo. It shows how to use fregot to debug a Rego policy that checks whether AWS EC2 instances in a Terraform plan use AMIs from an approved list.

 

New call-to-action

 

Prerequisites

  1. Clone the repo: git clone https://github.com/fugue/fregot.git
  2. Move to the demo directory: cd fregot/examples/demo
  3. Install fregot

 

Optional Steps

If you'd like to generate the Terraform plan JSON yourself:

  1. Install Terraform v0.12 or later
  2. Optional: Install jq

 

Steps

Generate Terraform Plan as JSON

Let's say your organization requires Amazon Web Services EC2 instances to only use hardened Linux Amazon Machine Images (AMIs) that are on a whitelist. Your boss wants to prevent any Terraform with a non-blessed AMI ID from being deployed, so you've installed fregot and have written a Rego policy to validate the Terraform plan before it is applied. You'll be working with these files:

 

demo.rego is a Rego policy that checks AWS AMI IDs in a Terraform plan against a whitelist. The policy contains an error that we'll debug in this tutorial.

 

demo.tf is a Terraform file that will deploy two EC2 instances. If you take a look, you'll see that ami-0b69ea66ff7391e80 is listed in approved_amis in the policy demo.rego, and ami-atotallyfakeamiid is (unsurprisingly) not.

 

repl_demo_input.json is the Terraform plan formatted as JSON so fregot can evaluate it. We've done this for you, but if you've installed Terraform v0.12 or later and you'd like to generate the output yourself, you can do so with the following commands:

 

Initialize Terraform directory:

terraform init

Create Terraform plan:

terraform plan -out=tfplan

Generate JSON representation of plan and pretty-print it with jq:

terraform show -json tfplan | jq . > repl_demo_input.json

 

Evaluate Terraform Plan with Fregot

Let's start by validating the Terraform plan JSON against the Rego policy. We'll use fregot eval to specify the input file (repl_demo_input.json), the function we want to evaluate (data.fregot.examples.demo.deny), and the Rego file it is in (demo.rego):

fregot eval --input \
repl_demo_input.json \
'data.fregot.examples.demo.deny' demo.rego

 

But wait, what's this...

 

Oh No, an Error!

Uh oh! There's an error in the Rego file. Evaluating deny produces this message:

fregot (eval error):
"demo.rego" (line 11, column 5):
builtin error:

11| startswith(ami, "ami-")
^^^^^^^^^^^^^^^^^^^^^^^

Expected string but got object

Stack trace:
rule fregot.examples.demo.amis at demo.rego:22:11
rule fregot.examples.demo.deny at cli:1:1

 

Well, it's a good thing we just installed fregot! Let's use the REPL to interactively debug the code.

 

Launch REPL

We'll start by launching the REPL:

fregot repl demo.rego --watch

 

The --watch flag tells fregot to automatically reload the loaded files (including input) when it detects a change.

 

(Handy, right? When used in conjunction with the :watch command, fregot monitors an expression and prints an updated evaluation whenever the policy and/or input files change! See the full walkthrough on GitHub for details.)

 

Load Policy and Input

First, load the policy:

:load demo.rego

 

Next, set the input:

:input repl_demo_input.json

 

We're going to take a closer look at data.fregot.examples.demo.deny. Let's set a breakpoint so we can investigate.

 

Set and Activate Breakpoint

To set the breakpoint at deny, you can use the  :break command with the rule name. Since the policy has already been loaded in the REPL, rather than using the full name data.fregot.examples.demo.deny, you can simplify it like so:

:break deny

 

We set the breakpoint with the rule name here, but we could also have used the line number: :break demo.rego:21

Now, evaluate the rule to activate the breakpoint:

deny

 

You'll see this output:

22|     ami = amis[ami]
^^^^^^^^^^^^^^^

 

 

Great! We've entered debugging mode and fregot is showing us the first line of the rule body. Notice how the prompt shows the word debug now:

fregot.examples.demo(debug)%

 

Step Forward

Nothing seems out of order yet, so step forward into the next query with the :step command:

:step

 

You'll see this output:

10|     ami = input.resource_changes.change.after
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

 

 

So far, so good. Step forward again:

:step

You'll see this output:

11|     startswith(ami, "ami-")
^^^^^^^^^^^^^^^^^^^^^^^

Again, still looking good. Step forward one more time:

:step

Look, there's the error message we saw earlier!

(debug) error
fregot (eval error):
"demo.rego" (line 11, column 5):
builtin error:

11| startswith(ami, "ami-")
^^^^^^^^^^^^^^^^^^^^^^^

Expected string but got object

Stack trace:
rule fregot.examples.demo.amis at demo.rego:22:11
rule fregot.examples.demo.deny at cli:1:1

 

 

Error Mode

fregot automatically puts you into error mode, indicated by the REPL prompt:

fregot.examples.demo(error)%

 

Let's look at the error message closely. Something is wrong with the value of the ami variable, since line 11 is where we check whether it starts with the string "ami-" :

11|     startswith(ami, "ami-")
^^^^^^^^^^^^^^^^^^^^^^^

 

 

Consider the error reason:

Expected string but got object

 

In this case, that means the value of ami isn't a string like we expect it to be. There's something wrong with this syntax on line 10:

ami = input.resource_changes[_].change.after

Let's see for ourselves what the value of ami looks like in the REPL, so we can figure out how to fix our syntax.

Check Type

We can use the :type command in the REPL to return the type of a term in the loaded package. We expect the ami variable to represent a string -- the AMI ID -- so let's find out what it actually is:

:type ami

We see this output:

ami : object{
"instance_initiated_shutdown_behavior": null,
"ami": string,
"ebs_optimized": null,
"instance_type": string,
"user_data": null,
"monitoring": null,
"tags": null,
"get_password_data": boolean,
"credit_specification": array{},
"disable_api_termination": null,
"timeouts": null,
"source_dest_check": boolean,
"user_data_base64": null,
"iam_instance_profile": null
}

 

Aha! This is definitely not just the AMI ID. The ami variable currently represents an object, which includes a host of other information.

We need to extract just the "ami" string inside the object. This means that the way we've assigned the ami variable is incorrect. As a refresher, this is the Rego code on line 10:

ami = input.resource_changes[_].change.after

 

Looks like we need to go one level deeper in the input document. If we add .ami at the end, it should narrow down the input to just the "ami" string.

But before we make any code changes, let's examine the input document and make sure that's the correct syntax.

Examine Input

First, quit debug mode:

:quit

Now we can view the entire input document by evaluating input:

input

 

That's a lot of JSON! Let's narrow it down all the way to input.resource_changes[_].change.after.ami:

input.resource_changes[_].change.after.ami

We see this output:

(debug) = "ami-0b69ea66ff7391e80"
(debug) = "ami-atotallyfakeamiid"
(debug) finished

 

Yes! That is the syntax we want to use to declare the ami variable on line 10. As you can see, it returns just the AMI ID strings we need.

If you look at repl_demo_input.json, you'll see the JSON follows this basic structure:

{
"resource_changes": [
{
"change": {
"after": {
"ami": "ami-0b69ea66ff7391e80"
},
}
},
{
"change": {
"after": {
"ami": "ami-atotallyfakeamiid"
}
}
}
]
}

 

 

As you can see, we simply forgot to narrow down the input to the ami field.

 

Fix Error in Policy

Now that we know how to fix the policy, let's add .ami to line 10 of demo.rego. It should look like this now:

ami = input.resource_changes[_].change.after.ami

 

Save your changes and go back to fregot -- you'll see that the updated policy has automatically been reloaded:

Reloaded demo.rego

 

(Note: If you didn't launch the REPL with --watch, you can manually reload all modified files with :reload.)

Go ahead and quit the REPL so we can test out the policy:

:quit

 

Now we can evaluate the policy as we did before -- this time, it should work properly:

fregot eval --input \
repl_demo_input.json \
'data.fregot.examples.demo.deny' demo.rego

 

 

And it does!

[true]

 

deny returns true, which means we've successfully tested the Terraform plan against the Rego policy and determined that the plan fails validation.

 

Now that you've solved the code error, you may use the same policy to evaluate your own JSON Terraform plans. Output the plan as JSON and evaluate it with fregot and the demo.rego policy, then execute the same fregot eval command from earlier, substituting your_input_file_here for your own input:

fregot eval --input \
your_input_file_here \
'data.fregot.examples.demo.deny' demo.rego

 

 

If the AMIs in the Terraform plan are whitelisted and the plan passes validation, you'll see output like this because deny returns no results -- concretely meaning that the request will not be denied:

 

[]

 

If the AMI IDs in the plan aren't whitelisted, the output looks like this because deny returns true, as you saw earlier:

 

[true]

What's Next?

This is an abbreviated version of the full walkthrough in fregot's GitHub repo. To learn how to use the :watch command to automatically print the updated value of deny whenever the policy or input changes, and to see a demo of the :next command, check out the walkthrough.

 

And if you want to try your hand at debugging on your own, you'll find an alternative version of this policy at fregot/examples/ami_id/, where you can introduce an error and debug with fregot to your heart's content.

 

Fregot is still in active development, so if you encounter any issues, please file a report. For more information about fregot, see the README on GitHub.

 

New call-to-action

Categorized Under