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/0to 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_driftis ambiguous. It means either "refresh found nothing" or "refresh did not run" (-refresh=false), and the plan does not record which. So absence never earnsstate_matches_reality; it staysnot_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)
| File | Size | Uploaded | |
|---|---|---|---|
| blastcheck-0.5.0.tar.gz | 70.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|