A Policy verdict is only as trustworthy as the evidence behind it. This tutorial follows one Policy through a pass, a violation, indeterminate evidence, and no target, so that each exit status means something concrete before you put it in a gate. To check an existing Form with a Pack in a few commands, Check a Form with Policies is the short route; come back here to understand what each verdict proves.
rootform run analyzes an input and saves its Form; rootform check evaluates Policies against the Form's selected architecture and exits with the verdict. You need Rootform, Terraform or OpenTofu, and the AWS provider download for planning. Work from a new network-review/ directory. The pass/, violation/, and no-target/ directories hold separate scenarios; policies/ holds one local Policy Pack.
Plans, their JSON exports, and state files can contain secrets in clear text. Keep them out of Git and public artifacts. Rootform reads them locally and does not contact AWS. Its reports omit sensitive values but still describe topology and names.
-
Create the Pack manifest and one Policy under
policies/:This
tutorialPack targets AWS resources in this walkthrough. Keep it as a separate example; it does not change the baseline Pack used by the commerce Playground.policies/pack.rf.hcl RFpolicy_pack "tutorial" {version = "0.1.0"}policies/network-context.rf.hcl RFpolicy "network-context" {target {rules = [aws.rule.subnet, aws.rule.instance]}assert = exists(contexts(rf.context.network))message = "Network resources must have an established network context."}The target selects instances interpreted by either named AWS Rule. The assertion asks whether each selected instance has a proven network Context. A source reference alone cannot satisfy it. Write a Policy Pack covers the syntax beyond this example.
-
Use the same AWS provider configuration in each scenario. As in Trace a placement, placeholder credentials grant no account access and skipped validation lets these examples plan without an AWS account. Never copy these placeholder settings into a real project. In each directory, save this provider block as
provider.tf:provider.tf HCLterraform {required_providers {aws = {source = "hashicorp/aws"version = "= 6.62.0"}}}provider "aws" {region = "us-east-1"access_key = "example"secret_key = "example"max_retries = 1skip_credentials_validation = trueskip_metadata_api_check = trueskip_region_validation = trueskip_requesting_account_id = true}For the passing case, save these three connected resources:
pass/main.tf HCLresource "aws_vpc" "main" {cidr_block = "10.20.0.0/16"}resource "aws_subnet" "application" {vpc_id = aws_vpc.main.idcidr_block = "10.20.1.0/24"}resource "aws_instance" "application" {ami = "ami-0123456789abcdef0"instance_type = "t3.micro"subnet_id = aws_subnet.application.id}The violation has a subnet with an explicitly empty VPC ID. Terraform can export this plan, but the declaration cannot establish the required network Context. Do not apply this synthetic plan:
violation/main.tf HCLresource "aws_subnet" "application" {vpc_id = ""cidr_block = "10.20.1.0/24"}The no-target case has only a VPC, so neither Policy target Rule applies:
no-target/main.tf HCLresource "aws_vpc" "main" {cidr_block = "10.20.0.0/16"}Initialize and save a plan in each scenario, then export JSON from that exact saved plan. OpenTofu users replace
terraformwithtofuin these commands:Shellfor scenario in pass violation no-target; do(cd "$scenario" && terraform init -input=false && \terraform plan -out=plan.tfplan && \terraform show -json plan.tfplan > plan.json)doneEach directory now contains matching
plan.tfplanandplan.json. If Terraform or OpenTofu rejects a scenario before writing the saved plan, inspect its diagnostic; Rootform cannot analyze a plan that was never exported. Plan inputs explains the input boundary. -
Run from
network-review/. First analyze the plan and save its Form. Pairing the saved plan lets Rootform follow directvpc_idandsubnet_idtraversals even though their values are unknown until apply. Then check the saved Form:checkreads it without compiling the plan again.--policy-packselects this Pack for this command only; selecting a Dialect alone would not select it.Shellrootform run pass/plan.json --plan-file pass/plan.tfplan \--no-serve -o pass/analysis.jsonrootform check pass/analysis.json --policy-pack ./policies \-o pass/results.json -o pass/results.sarif --color alwaysPassing check, excerpt OUTPUTPolicy check completedInput pass/analysis.jsonOrigin Plan (saved Form)Stage PlannedPolicies 1 selectedEvaluations 2Passed 2Violated 0Indeterminate 0Verdict PASSEDAll selected evaluations passed.Status
0means every selected Policy was evaluated and passed, here on both targets. The VPC itself is not a target.pass/analysis.jsonis the Form preserving the interpreted architecture.pass/results.jsonis the Policy result: it identifies that Form by digest and records the evaluated stage, the selected Policies, and every evaluation.pass/results.sarifpresents the same result to review tools. None of these files contains sensitive plan values. If either target stays indeterminate, confirm that--plan-filenames the saved plan used for the JSON export. -
Ask why the instance has a network Context. The saved Form avoids recompiling the plan:
Shellrootform explain instance aws_instance.application \--input pass/analysis.json --color alwaysInstance evidence, excerpt OUTPUTInstance explainedInput pass/analysis.jsonStage Plannedaws_instance.applicationInstance managed instance of aws_instance; planned (create)Provider registry.terraform.io/hashicorp/awsInterpretation applied aws.rule.instance as compute-instanceConclusion Interpreted as compute-instance by aws.rule.instance: networkcontext to aws_subnet.application.Facts-> context network aws_subnet.application evidence: traversalClosurescontext network -> subnetvia source.subnet_id, match exact by idresolved, 1 factThe
traversallabel identifies saved-plan evidence, not a network probe. Explain the Policy outcome thatcheckrecorded inpass/results.json:Shellrootform explain policy tutorial.policy.network-context \--result pass/results.json --input pass/analysis.json --color alwaysPolicy explanation, excerpt OUTPUTPolicy explainedPolicy tutorial.policy.network-contextResult pass/results.jsonInput pass/analysis.jsonOrigin Plan (saved Form)Stage PlannedEvaluations 2Passed 2Violated 0Indeterminate 0Coverage CompleteOutcome PASSEDRequirementNetwork resources must have an established network context.Assertion exists(contexts(rf.context.network))Target Rules aws.rule.instance, aws.rule.subnetAll 2 evaluations passed.explain policyreads the saved result and evaluates nothing again. The Requirement block quotes the Policy message and the assertion and target the result records. The optional--inputmust be the Form that result was computed from; Rootform refuses any other Form. Add--detailsto list each evaluation with its recorded evidence and conclusion; the evidence itself is described only when--inputsupplies that Form. Explain a Policy defines its accepted inputs and options. -
Check the same Policy against the explicit empty VPC ID.
checkalso accepts a plan JSON directly and compiles it first:Shellrootform check violation/plan.json --plan-file violation/plan.tfplan \--policy-pack ./policies --color alwaysViolation, excerpt OUTPUTPolicy check completedInput violation/plan.jsonOrigin PlanStage PlannedPolicies 1 selectedEvaluations 1Passed 0Violated 1Indeterminate 0Verdict VIOLATEDVIOLATEDPolicy tutorial.policy.network-contextResource aws_subnet.applicationRequirement Network resources must have an established network context.Evidence context network -> virtual-network via source.vpc_id: absentRequirementquotes what the Policy declares;Evidenceis the recorded observation that decided the verdict. A known empty value proves that this subnet has no declared target for the Rule's network emission, so its network closure throughsource.vpc_idis absent and the Policy is violated:checkreturns status1. Reports requested with-oare written whatever the verdict. This says nothing about a deployed subnet; the scenario is an unapplied plan. -
Check the passing plan again, this time without its saved plan:
Shellrootform check pass/plan.json --policy-pack ./policies --color alwaysIndeterminate result, excerpt OUTPUTPolicy check completedInput pass/plan.jsonOrigin PlanStage PlannedPolicies 1 selectedEvaluations 2Passed 0Violated 0Indeterminate 2Verdict INDETERMINATEINDETERMINATEPolicy tutorial.policy.network-contextResource aws_instance.applicationRequirement Network resources must have an established network context.Evidence context network -> subnet via source.subnet_id:indeterminate (unknown until apply)Policy tutorial.policy.network-contextResource aws_subnet.applicationRequirement Network resources must have an established network context.Evidence context network -> virtual-network via source.vpc_id:indeterminate (unknown until apply)The plan values for the new VPC and subnet IDs are unknown until apply. Without verified traversals, Rootform cannot prove either connection or its absence. Status
3prevents an uncertain result from becoming approval. The saved plan is optional for analysis, but matters to this Policy verdict. -
The same selected Policy finds no subnet or instance in the VPC-only plan:
Shellrootform check no-target/plan.json --plan-file no-target/plan.tfplan \--policy-pack ./policies --color alwaysNo target, excerpt OUTPUTPolicy check completedInput no-target/plan.jsonOrigin PlanStage PlannedPolicies 1 selectedEvaluations 0Passed 0Violated 0Indeterminate 0Verdict NO DECISIONWITHOUT TARGETPolicy tutorial.policy.network-contextTarget Rules aws.rule.instance, aws.rule.subnetStatus
3means the selected Policy made no decision: a Policy without target never counts as passed, and a check that selects no Policy also returns3.rootform runnever evaluates Policies, so its status0is analysis success, not compliance. Policies and Policy Packs explains aggregation. -
A pull request or CI job summary reads Markdown. This script saves the violating plan's Form with its architecture review, writes the Policy review from that Form, joins both reviews, and ends with the status of
check:Save the following block as
review.shand run it withsh review.sh, or use it as one CI shell step. Itsexitstatements end the shell, so do not paste it into an interactive terminal.Shellrootform run violation/plan.json --plan-file violation/plan.tfplan \--no-serve -o violation/analysis.json -o violation/architecture.md || exitcheck_status=0rootform check violation/analysis.json --policy-pack ./policies \-o violation/policies.md || check_status=$?case $check_status in0 | 1 | 3) ;;*) exit "$check_status" ;;esac{ cat violation/architecture.md && printf '\n' && cat violation/policies.md; } \> violation/review.md || exit 4exit "$check_status"The block works with or without
set -e.- If
runfails, the script stops with the status ofrunand joins nothing. checkwrites its report whatever the verdict:0passed,1violated,3no verdict. The script keeps that status and returns it last.- Status
2(incorrect use) or4(a file could not be read or written) stops the script with that status before anything is joined. printf '\n'leaves a blank line between the two reports. Ifviolation/review.mdcannot be written, the script exits4, so a write failure never reads as a Policy verdict.
Here
checkreturns1: the script writesviolation/review.md, then exits1.The script creates
violation/review.mdby joining the architecture and Policy reports. Rootform owns their current Markdown layout; see Review with Markdown for headings, excerpts, and--detailsbehavior. - If
-
The local Pack is an invocation override. To record it as project selection, run these commands from
network-review/:Shellrootform add policy-packs ./policiesrootform check pass/analysis.json --locked --policy 'tutorial/*' \-o pass/locked.sarifThe first command updates
rootform.lock; the second evaluates the selected Policy from that lock against the saved Form and returns status0. Commit the lock with the project once reviewed.--lockedrefuses--policy-packas a usage error, so CI cannot silently override the recorded selection. Let the status ofcheckdecide the job: CI integration shows the gate and artifact handling, rootform check is the command reference, and Outputs and exit status is the status reference.
Continue with Review a pull request to apply these Policies to the head of a pull request, or with GitHub to run the same gate in a workflow.