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.
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.
Prerequisites
- Clone the repo:
git clone https://github.com/fugue/fregot.git
- Move to the demo directory:
cd fregot/examples/demo
- Install fregot
Optional Steps
If you'd like to generate the Terraform plan JSON yourself:
- Install Terraform v0.12 or later
- 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
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.