Skip to main content

blastcheck

blastcheck reads a terraform show -json plan and emits an Impact Manifest — a machine-readable change-safety assertion. It is the reference producer of that open format.

terraform plan -out plan.tfplan
terraform show -json plan.tfplan | blastcheck > manifest.json

terraform plan tells you what will change. blastcheck adds the layer the plan can't: for each change, is it reversible, does anything become unrecoverable, does exposure widen, and — honestly — what could it not determine.

What it does, and what it deliberately doesn't

blastcheck is offline by default: no credentials, no network, no hosted service. It reasons from the plan artifact alone. Wherever a verdict genuinely requires live cloud state — "is this disk attached to something serving traffic?", "does a backup exist?" — it emits unknown / not_verified with a stated reason, rather than guessing.

A direct consequence, and the point of the format: a plan-only run can never emit safe. It never verified live state, so it says caution, blocked, or unknown — never safe. Certifying safe requires --live (below). blastcheck is honest about the ceiling of what a plan alone can prove.

What it derives from the plan alone is still substantial:

  • Irreversibility — e.g. a managed-disk grow is one-way (Azure can't shrink), visible in the plan diff.
  • Widened exposure — an inbound NSG rule opening 0.0.0.0/0 to a sensitive port; a storage account turning on public access or lowering TLS.
  • Data-loss risk — deleting a data-bearing resource flags the primary copy as removed and recoverability as unverified, not safe.
  • Cost direction and action semantics.
  • Drift — see below. This is the one place blastcheck reaches a real state determination rather than an unknown.

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 the offline artifact. blastcheck did not perform the read — Terraform did — but the fact is no less true for it.

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

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

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 that same stale state.

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; it stays not_verified.
  • Refresh only sees resources Terraform manages. Anything created outside Terraform is not in state, so nothing refreshes it. Shadow-IT discovery needs a direct cloud query and is out of scope here.

Drift on a resource this plan does not touch is recorded under extensions.drift_outside_this_plan rather than invented into a change.

blastcheck is a producer, not a gate. It emits the manifest and exits 0; turning that into pass/fail is a separate policy layer (a CI gate). Exit codes reflect execution, not the verdict.

Coverage: every provider, in three layers

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

Layer Applies to Confidence
0 — structural any provider ever written high
1 — heuristic any provider, by name and value patterns low, tagged heuristic
2 — precise types with an exact rule high

Layer 0 reads what Terraform states regardless of provider: action semantics (a delete is a delete, a replace implies a destroy), action_reason, replace_paths, resource_drift, unreadable fields, and plan-level errored / complete.

Layer 1 matches on resource-type names (*_disk, *_bucket, *_database) and attribute names and values (publicly_accessible, storage_encrypted: false, acl: public-read, an inbound 0.0.0.0/0). On a real 55-change AWS plan, with zero AWS-specific code, this finds a publicly accessible unencrypted RDS instance, a public-read S3 bucket, and a security group open to the internet.

These findings are graded caution, never blocking, carry confidence: low, 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. In the same spirit, an open egress rule and a default route are not flagged: they are 0.0.0.0/0 by definition, and firing on them would trip on nearly every plan ever written.

Layer 2 is the precise set: Azure managed disks, virtual machines, network security groups (+ rules), storage accounts, SQL databases. Where a precise rule exists it wins, and a heuristic never overrides it.

extensions.assessment records which layer produced each verdict.

Install & use

pip install blastcheck
terraform show -json plan.tfplan | blastcheck            # stdin
blastcheck --plan plan.json > manifest.json              # from a file
blastcheck --compact                                     # single-line JSON

No hosted service, no network, no runtime dependencies — a default run works entirely against the plan file.

Live checks: earning a safe verdict

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

--live verifies recorded state against live cloud reality. It is the only mode in which a safe verdict is reachable.

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-only with --live
state_confidence not_verified, or drift_detected from Terraform's refresh state_matches_reality, or drift blastcheck found itself
availability_impact unknown for anything that already exists interrupts when something is attached to it
verdict never safe safe 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.

Azure today. The prober is an interface; adding a provider is about a hundred lines.

In CI

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

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

The manifest is uploaded as a build artifact and the verdict is posted on the pull request. The action does not fail the build — blastcheck is a producer, not a gate, and what a blocked verdict should do to a pipeline is a policy question that belongs to you. To gate on it:

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

- if: steps.bc.outputs.verdict == 'blocked'
  run: exit 1

Note what you cannot write on a plan-only run: there is no verdict == 'safe' to gate on, because a plan-only run never emits one. A pipeline that proceeds only on a positive safety claim needs --live, and the credentials that implies. That is the honest ceiling of what a plan by itself can prove.

Tests

The suite's primary job is to prove every emitted manifest validates against the vendored schema (blastcheck/schema/impact-manifest.schema.json) on realistic plan fixtures — that is how a schema shape error surfaces.

pip install -e ".[test]"
pytest

Relationship to the spec

blastcheck implements the Impact Manifest specification and vendors a pinned copy of its schema at blastcheck/schema/, which ships inside the wheel. The format is open and vendor-neutral; blastcheck is a reference implementation of it, not its owner.

Status

v0.4 — draft, and evolving alongside the spec (which does not freeze at 1.0 until this tool has run against real Terraform plans).

Contributing and contact

The bar for a change, and how to add a rule: CONTRIBUTING.md. Security reports: SECURITY.md.

If you are implementing the Impact Manifest format in another tool, open an issue on the specification repository — ambiguities in the spec are the most useful feedback it can get while it is still v0.1 draft.

Anything else: kproffitt@prococonsulting.com

License

Apache-2.0. See LICENSE.

Metadata

Release files for blastcheck 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for blastcheck 0.5.0
File Size Uploaded
blastcheck-0.5.0.tar.gz 70.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for blastcheck 0.5.0
File Interpreter ABI Platform
blastcheck-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 123.8 kB

Release files / blastcheck-0.5.0.tar.gz

Download URL blastcheck-0.5.0.tar.gz
Size 70.4 kB
Tags Source
SHA-256 checksum
How to use checksums
7a703d6a0fd7dcc038d956aa03c5b069673d181f75733c6bfb5328613af596bb
BLAKE2b-256 checksum
How to use checksums
55a29074e2f656b543fc44989fac15df6febad9372b18543a7fee57658491b4d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Release files / blastcheck-0.5.0-py3-none-any.whl

Download URL blastcheck-0.5.0-py3-none-any.whl
Size 53.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b27dd869c9fd66bfa606f749bd55c06de73e85f4db3fdc9e5c1c1109fb036918
BLAKE2b-256 checksum
How to use checksums
e59dd15da7813872cdbb25a26fa5786fb34a98919da67d5f7736a681d9e66ef4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Release history Release notifications | RSS feed

0.8.0

2 release files

0.7.0

2 release files

This release

0.5.0 This release

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page