Skip to content

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.

  1. Write one Policy Pack

    Create the Pack manifest and one Policy under policies/:

    This tutorial Pack 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 RF
    policy_pack "tutorial" {
    version = "0.1.0"
    }
    policies/network-context.rf.hcl RF
    policy "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.

  2. Prepare the three plans

    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 HCL
    terraform {
    required_providers {
    aws = {
    source = "hashicorp/aws"
    version = "= 6.62.0"
    }
    }
    }
    provider "aws" {
    region = "us-east-1"
    access_key = "example"
    secret_key = "example"
    max_retries = 1
    skip_credentials_validation = true
    skip_metadata_api_check = true
    skip_region_validation = true
    skip_requesting_account_id = true
    }

    For the passing case, save these three connected resources:

    pass/main.tf HCL
    resource "aws_vpc" "main" {
    cidr_block = "10.20.0.0/16"
    }
    resource "aws_subnet" "application" {
    vpc_id = aws_vpc.main.id
    cidr_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 HCL
    resource "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 HCL
    resource "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 terraform with tofu in these commands:

    for 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)
    done
    Shell

    Each directory now contains matching plan.tfplan and plan.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.

  3. Observe a pass

    Run from network-review/. First analyze the plan and save its Form. Pairing the saved plan lets Rootform follow direct vpc_id and subnet_id traversals even though their values are unknown until apply. Then check the saved Form: check reads it without compiling the plan again. --policy-pack selects this Pack for this command only; selecting a Dialect alone would not select it.

    rootform run pass/plan.json --plan-file pass/plan.tfplan \
    --no-serve -o pass/analysis.json
    rootform check pass/analysis.json --policy-pack ./policies \
    -o pass/results.json -o pass/results.sarif --color always
    Shell
    Passing check, excerpt OUTPUT
    Policy check completed
    Input pass/analysis.json
    Origin Plan (saved Form)
    Stage Planned
    Policies 1 selected
    Evaluations 2
    Passed 2
    Violated 0
    Indeterminate 0
    Verdict PASSED
    All selected evaluations passed.

    Status 0 means every selected Policy was evaluated and passed, here on both targets. The VPC itself is not a target. pass/analysis.json is the Form preserving the interpreted architecture. pass/results.json is the Policy result: it identifies that Form by digest and records the evaluated stage, the selected Policies, and every evaluation. pass/results.sarif presents the same result to review tools. None of these files contains sensitive plan values. If either target stays indeterminate, confirm that --plan-file names the saved plan used for the JSON export.

  4. Inspect the proof

    Ask why the instance has a network Context. The saved Form avoids recompiling the plan:

    rootform explain instance aws_instance.application \
    --input pass/analysis.json --color always
    Shell
    Instance evidence, excerpt OUTPUT
    Instance explained
    Input pass/analysis.json
    Stage Planned
    aws_instance.application
    Instance managed instance of aws_instance; planned (create)
    Provider registry.terraform.io/hashicorp/aws
    Interpretation applied aws.rule.instance as compute-instance
    Conclusion Interpreted as compute-instance by aws.rule.instance: network
    context to aws_subnet.application.
    Facts
    -> context network aws_subnet.application evidence: traversal
    Closures
    context network -> subnet
    via source.subnet_id, match exact by id
    resolved, 1 fact

    The traversal label identifies saved-plan evidence, not a network probe. Explain the Policy outcome that check recorded in pass/results.json:

    rootform explain policy tutorial.policy.network-context \
    --result pass/results.json --input pass/analysis.json --color always
    Shell
    Policy explanation, excerpt OUTPUT
    Policy explained
    Policy tutorial.policy.network-context
    Result pass/results.json
    Input pass/analysis.json
    Origin Plan (saved Form)
    Stage Planned
    Evaluations 2
    Passed 2
    Violated 0
    Indeterminate 0
    Coverage Complete
    Outcome PASSED
    Requirement
    Network resources must have an established network context.
    Assertion exists(contexts(rf.context.network))
    Target Rules aws.rule.instance, aws.rule.subnet
    All 2 evaluations passed.

    explain policy reads the saved result and evaluates nothing again. The Requirement block quotes the Policy message and the assertion and target the result records. The optional --input must be the Form that result was computed from; Rootform refuses any other Form. Add --details to list each evaluation with its recorded evidence and conclusion; the evidence itself is described only when --input supplies that Form. Explain a Policy defines its accepted inputs and options.

  5. Distinguish a violation

    Check the same Policy against the explicit empty VPC ID. check also accepts a plan JSON directly and compiles it first:

    rootform check violation/plan.json --plan-file violation/plan.tfplan \
    --policy-pack ./policies --color always
    Shell
    Violation, excerpt OUTPUT
    Policy check completed
    Input violation/plan.json
    Origin Plan
    Stage Planned
    Policies 1 selected
    Evaluations 1
    Passed 0
    Violated 1
    Indeterminate 0
    Verdict VIOLATED
    VIOLATED
    Policy tutorial.policy.network-context
    Resource aws_subnet.application
    Requirement Network resources must have an established network context.
    Evidence context network -> virtual-network via source.vpc_id: absent

    Requirement quotes what the Policy declares; Evidence is 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 through source.vpc_id is absent and the Policy is violated: check returns status 1. Reports requested with -o are written whatever the verdict. This says nothing about a deployed subnet; the scenario is an unapplied plan.

  6. Keep unresolved evidence indeterminate

    Check the passing plan again, this time without its saved plan:

    rootform check pass/plan.json --policy-pack ./policies --color always
    Shell
    Indeterminate result, excerpt OUTPUT
    Policy check completed
    Input pass/plan.json
    Origin Plan
    Stage Planned
    Policies 1 selected
    Evaluations 2
    Passed 0
    Violated 0
    Indeterminate 2
    Verdict INDETERMINATE
    INDETERMINATE
    Policy tutorial.policy.network-context
    Resource aws_instance.application
    Requirement Network resources must have an established network context.
    Evidence context network -> subnet via source.subnet_id:
    indeterminate (unknown until apply)
    Policy tutorial.policy.network-context
    Resource aws_subnet.application
    Requirement 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 3 prevents an uncertain result from becoming approval. The saved plan is optional for analysis, but matters to this Policy verdict.

  7. Separate no target from no selection

    The same selected Policy finds no subnet or instance in the VPC-only plan:

    rootform check no-target/plan.json --plan-file no-target/plan.tfplan \
    --policy-pack ./policies --color always
    Shell
    No target, excerpt OUTPUT
    Policy check completed
    Input no-target/plan.json
    Origin Plan
    Stage Planned
    Policies 1 selected
    Evaluations 0
    Passed 0
    Violated 0
    Indeterminate 0
    Verdict NO DECISION
    WITHOUT TARGET
    Policy tutorial.policy.network-context
    Target Rules aws.rule.instance, aws.rule.subnet

    Status 3 means the selected Policy made no decision: a Policy without target never counts as passed, and a check that selects no Policy also returns 3. rootform run never evaluates Policies, so its status 0 is analysis success, not compliance. Policies and Policy Packs explains aggregation.

  8. Write a review document

    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.sh and run it with sh review.sh, or use it as one CI shell step. Its exit statements end the shell, so do not paste it into an interactive terminal.

    rootform run violation/plan.json --plan-file violation/plan.tfplan \
    --no-serve -o violation/analysis.json -o violation/architecture.md || exit
    check_status=0
    rootform check violation/analysis.json --policy-pack ./policies \
    -o violation/policies.md || check_status=$?
    case $check_status in
    0 | 1 | 3) ;;
    *) exit "$check_status" ;;
    esac
    { cat violation/architecture.md && printf '\n' && cat violation/policies.md; } \
    > violation/review.md || exit 4
    exit "$check_status"
    Shell

    The block works with or without set -e.

    • If run fails, the script stops with the status of run and joins nothing.
    • check writes its report whatever the verdict: 0 passed, 1 violated, 3 no verdict. The script keeps that status and returns it last.
    • Status 2 (incorrect use) or 4 (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. If violation/review.md cannot be written, the script exits 4, so a write failure never reads as a Policy verdict.

    Here check returns 1: the script writes violation/review.md, then exits 1.

    The script creates violation/review.md by joining the architecture and Policy reports. Rootform owns their current Markdown layout; see Review with Markdown for headings, excerpts, and --details behavior.

  9. Use the same gate in CI

    The local Pack is an invocation override. To record it as project selection, run these commands from network-review/:

    rootform add policy-packs ./policies
    rootform check pass/analysis.json --locked --policy 'tutorial/*' \
    -o pass/locked.sarif
    Shell

    The first command updates rootform.lock; the second evaluates the selected Policy from that lock against the saved Form and returns status 0. Commit the lock with the project once reviewed. --locked refuses --policy-pack as a usage error, so CI cannot silently override the recorded selection. Let the status of check decide 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.