blastcheck 0.7 · format 0.1 draft · Apache-2.0

blastcheck

A Terraform plan tells you what will change. It does not tell you whether the change is safe.

blastcheck reads terraform show -json and emits an Impact Manifest: a machine-readable change-safety assertion. Offline by default, no hosted service, and no runtime dependencies at all.


Landed here from a manifest.json?

That file was produced by this tool. It records, for every resource change in a plan, whether the change is reversible, whether anything becomes unrecoverable, whether exposure widens, and — the part that matters most — what could not be determined.

An unknown in that document is not a bug. It means the producer could not verify something, and is saying so rather than guessing. Unproven is not safe, and a consumer must never treat it as such.

Install

pip install blastcheck
brew install prococonsulting/tap/blastcheck
# or a single binary from Releases — no Python needed
terraform plan -out tfplan
terraform show -json tfplan | blastcheck > manifest.json

blastcheck plan.json      # readable summary
blastcheck tfplan         # a saved binary plan, converted for you
blastcheck rules          # what this build actually knows

On a terminal you get a summary; redirected or piped you get the manifest. No flag needed either way.

blastcheck 0.7.0 — 20 change(s) assessed, plan-only

  BLOCKED  azurerm_network_security_group.web_tier
           security      inline rule 'allow-ssh-temp': inbound Allow from ['*']
                         to port(s) ['22'] — opens sensitive access to the internet

  UNKNOWN  azurerm_managed_disk.sql_data
           reversibility `disk_size_gb` 512 -> 1024 cannot be reduced; permanent

  6 further change(s) are undetermined only because live state was not queried.
  3 change(s) had nothing to flag.

verdict: blocked
  · state was never verified against reality — re-run with --live to certify

The baseline unknowns every plan-only change shares are summarised once rather than repeated per change, and a low-confidence finding is labelled (pattern match, not a determination) — so a guess never looks like a determination in a terminal.

In a pull request:

- run: terraform show -json tfplan > plan.json

- uses: prococonsulting/blastcheck@v0
  with:
    plan: plan.json

What it reads from a plan alone

DimensionExample the plan can actually prove
reversibilityAn Azure managed disk grow is one-way. Azure cannot shrink a disk.
security_postureAn inbound rule opening 0.0.0.0/0 to 22 or 3389; a database turning on publicly_accessible; a bucket set to public-read; encryption or deletion protection switched off.
data_durabilityA delete removes the primary copy of a data-bearing resource, and recoverability is unverified rather than assumed.
state_confidencedrift_detected when Terraform's own refresh found the resource had moved. Otherwise not_verified — see below.

Every provider, in three layers

Every change in a plan is assessed. Nothing is skipped for being an unfamiliar resource type.

LayerApplies toConfidence
0 — structuralany provider ever written: action semantics, action_reason, replace_paths, drift, unreadable fieldshigh
1 — heuristicany provider, by resource-type and attribute name and value patternslow, tagged heuristic
2 — preciseresource types with an exact rule — 110 types across AWS and Azure, declared in provider packshigh

Layer 2 is data, not code. A provider pack is a JSON file declaring which resource types are data-bearing, which are stateless, which attributes only grow in one direction, and which attribute values widen exposure. Adding a resource type is an edit to a data file with a test, not a change to the analyzer — which is the only version of this that a community can actually contribute to. blastcheck rules prints exactly what the installed build knows, so the coverage claim on this page is never the one you have to trust.

On a real 55-change AWS plan, with no AWS-specific code at all, the heuristic layer finds a publicly accessible unencrypted database with final snapshots disabled, a public-read bucket, and a security group open to the internet.

Those findings are graded caution, never blocking, and their evidence is tagged source: heuristic. They are leads, not determinations, and saying so is the difference between a tool people read and a tool people mute. For the same reason an open egress rule and a default route are not flagged: both are 0.0.0.0/0 by definition, and firing on them would trip on nearly every plan ever written.

Drift, without asking you for credentials

terraform plan refreshes by default: before computing a diff it reads live reality for every managed resource and records anything that moved in a top-level resource_drift array. That is a live-state observation already sitting inside an offline artifact. blastcheck did not perform the read. Terraform did. The fact is no less true for it.

A resource appearing in both resource_drift and resource_changes is the most dangerous shape this tool can find, and it is graded blocking:

azurerm_managed_disk.sql_data          severity: blocking
  state_confidence: drift_detected     (recorded 512 -> live 1024)
verdict: blocked

The plan is internally consistent. It reads as routine. It was computed against a description of that resource which had already stopped being true, and every other verdict for it was derived from the same stale state. That is the outage this exists for.

Two limits, stated rather than papered over. An empty resource_drift is ambiguous — it means either refresh found nothing or refresh did not run (-refresh=false), and the plan does not record which, so absence never earns state_matches_reality. And refresh only sees resources Terraform manages; anything created outside Terraform is not in state, so nothing refreshes it.

Why a plan alone never earns safe

A default run never queries live cloud state, so it is not entitled to certify that a change is safe. It reports caution, blocked, or unknown — never safe, no matter how clean the plan looks. That is not a limitation being apologised for. A tool that says safe when it means I found no problem converts an absence of information into a green light and puts someone's name on it.

A false safe is catastrophic. A false unknown is annoying. The format is built around that asymmetry, and safe is a positive claim that has to be earned with evidence rather than a default that survives because nothing tripped.

Live checks: earning it

blastcheck --plan plan.json --live > manifest.json

--live verifies recorded state against live cloud reality, and it is the only mode in which a safe verdict is reachable at all.

blastcheck never handles a credential. It shells out to the provider's own CLI and uses the session you already have — az login, SSO, managed identity, whatever your environment provides. Nothing is read from a config file, nothing is accepted as a flag, nothing is stored. A tool that asks you to hand it cloud credentials so it can tell you whether a change is safe has an obvious problem.

It also never writes. Every command is built from a fixed template with a read-only verb, and a test asserts that no mutating verb can reach a subprocess call.

plan-onlywith --live
state_confidencenot_verified, or drift_detected from Terraform's refreshstate_matches_reality, or drift blastcheck found itself
availability_impactunknown for anything that already existsinterrupts when something is attached to it
data_durabilitya delete is unverified lossunrecoverable_loss when no recovery point exists — checked, not assumed
verdictnever safesafe only when every change was verified and none is dangerous

A failed probe — no CLI, not logged in, no permission, a timeout — does not degrade to a guess. The dimension stays unknown, the reason appears in the rationale, and producer.access.live_state records unavailable rather than queried. Asking and being refused is a different fact from never asking, and a reader interpreting an unknown needs to know which one happened.

Two things --live deliberately does not do. It does not average: one unverified change denies safe to the whole plan, because a plan is only as trustworthy as its least verified change. And existence alone is not a match — a resource that is present but shares no comparable attribute with recorded state stays not_verified.

An ignore does not delete a finding

A .blastcheck.json anywhere above your working directory sets defaults, including patterns to ignore:

{ "fail_on": "blocked", "live": "azure", "ignore": ["module.sandbox.*"] }

An ignore lowers that change's severity to informational so it stops gating a pipeline. Everything else stays exactly as it was — the concerns, the rationale, the evidence — and the manifest records which pattern suppressed it under extensions.ignored, along with the severity it originally had. A config file that could make a finding vanish would be the most effective way yet invented to produce a false safe, and it would be invisible to whoever read the manifest afterwards.

Producer, not a gate

By default blastcheck emits the manifest and exits 0 whatever the verdict. --fail-on lets you state your own threshold — that is the operator declaring a policy, not the tool deciding one. Exit 1 means blastcheck could not run and exit 2 means the verdict tripped your threshold; they are deliberately distinct, because a pipeline must be able to tell "this plan is dangerous" from "the tool is broken", and those call for opposite responses.

Tamper-evident

blastcheck --plan plan.json --sign > manifest.json
blastcheck --verify manifest.json

--sign attaches a digest over the manifest canonicalized with JCS (RFC 8785), so two conformant producers of the same logical document compute the same bytes. Editing a verdict after the fact breaks verification.

The format is not owned by this tool

Impact Manifest is an open specification, Apache-2.0, with no assumption of any cloud and no coupling to any implementation. blastcheck is a reference producer of it, not its owner. Other tools are expected to implement it, and the format is only worth anything if they do.

Why this exists

The plan was right. The state was wrong. — a Terraform apply that took production down on a change whose plan was three lines long and completely correct, and why every pre-apply tool would have passed it.

Contact

Bug reports and feature requests: open an issue. Questions about the format, or about implementing it in another tool: the specification repository, or email kproffitt@prococonsulting.com.

Built and maintained by Kevin Proffitt — prococonsulting.com, LinkedIn.