safety

package
v1.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 7, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Overview

Package safety implements bumper's enforcement layer: verifying a saved Terraform plan (binding a passing scan to the exact plan file by sha256) and guarding `terraform apply`/`destroy` as a Claude Code PreToolUse hook, so an agent cannot apply a plan nobody verified.

The deterministic core (engine/rules/plan) never imports this package; this is the outermost shell.

Index

Constants

View Source
const DefaultMaxAge = 24 * time.Hour

DefaultMaxAge is how long a verdict stays valid; past this, guard asks for a re-verify (the plan may predate drift). 0 disables expiry.

View Source
const DefaultMinSeverity = "high"

DefaultMinSeverity is the threshold at or above which a finding blocks an apply (and fails `verify` unless --accept is given).

Variables

This section is empty.

Functions

func ResolvePlanData

func ResolvePlanData(planJSON, path string) (data []byte, source string, err error)

ResolvePlanData turns a plan reference into `terraform show -json` bytes. Inline JSON is used as-is; a path that already holds JSON is read directly; any other path is treated as a binary plan and run through `terraform show -json`. source is a label describing which path was taken.

func Sha256File

func Sha256File(path string) (string, error)

Sha256File returns the lowercase hex sha256 of a file's contents. This is the binding key: the verdict for a plan is stored under the hash of the exact bytes that `terraform apply <plan>` will consume.

func ShowJSON

func ShowJSON(planPath string) ([]byte, error)

ShowJSON runs `terraform show -json <planPath>` and returns its stdout. It degrades with a clear message when terraform is not installed.

Types

type Decision

type Decision struct {
	Deny   bool
	Reason string
}

Decision is guard's verdict on a tool call. A zero Decision means "no opinion" — guard stays silent and defers to Claude Code's normal permission flow.

func Decide

func Decide(in HookInput, shellTool string, now time.Time, maxAge time.Duration) Decision

Decide inspects a Bash tool call and blocks unverified terraform apply/destroy. Any other tool or command yields a zero (silent) Decision, which is what makes guard safe to install as a global, always-on hook.

func Guard

func Guard(r io.Reader, w io.Writer, shellTool string, now time.Time, maxAge time.Duration) (Decision, error)

Guard reads a PreToolUse payload, decides, and writes a deny decision (if any) as the hook's JSON output. It is fail-open on malformed input: a bumper bug must never wedge the user's shell. The decision to block is conveyed purely shellTool is the host agent's shell-execution tool name (e.g. "Bash" for Claude Code, "launch-process" for Augment); any other tool yields a silent allow.

It writes the JSON deny decision to w and RETURNS the Decision, so the caller can also surface a block via stderr + exit 2 — the universal backstop every agent honors (and the only mechanism Gemini accepts, since it ignores the JSON).

type HookInput

type HookInput struct {
	HookEventName  string   `json:"hook_event_name"`
	ToolName       string   `json:"tool_name"`
	CWD            string   `json:"cwd"`
	WorkspaceRoots []string `json:"workspace_roots"`
	ToolInput      struct {
		Command string `json:"command"`
	} `json:"tool_input"`
}

HookInput is the subset of a PreToolUse stdin payload we read. Claude Code provides `cwd`; Augment instead provides `workspace_roots` (an array, no cwd), so we read both and resolve via WorkDir.

func (HookInput) WorkDir added in v1.2.0

func (in HookInput) WorkDir() string

WorkDir resolves the directory plan paths are relative to: the agent's cwd if given (Claude), else the first workspace root (Augment), else "".

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is a directory of verdict files keyed by plan sha256.

func StoreForPlan

func StoreForPlan(planPath string) (*Store, error)

StoreForPlan returns the verdict store rooted at the plan file's directory (<plan-dir>/.bumper/verified). Both verify and guard derive the store this way from the same plan path, so the lookup is location-independent.

func (*Store) Load

func (s *Store) Load(sha string) (Verdict, bool, error)

Load returns the verdict for a plan sha, or ok=false if none is recorded.

func (*Store) Save

func (s *Store) Save(v Verdict) error

Save writes a verdict, creating the store directory if needed.

type Verdict

type Verdict struct {
	PlanSHA       string    `json:"plan_sha"`
	PlanFile      string    `json:"plan_file"`
	Verdict       string    `json:"verdict"` // "pass" | "accepted"
	MinSeverity   string    `json:"min_severity"`
	FindingsTotal int       `json:"findings_total"`
	Blocking      int       `json:"blocking"` // count of findings at/above the threshold
	Accepted      bool      `json:"accepted"` // true when recorded via --accept despite blocking findings
	BumperVersion string    `json:"bumper_version"`
	VerifiedAt    time.Time `json:"verified_at"`
}

Verdict is the record written when a plan passes (or is explicitly accepted). It binds a scan result to the exact plan bytes via PlanSHA.

type VerifyResult

type VerifyResult struct {
	Verdict  Verdict
	Findings []engine.Finding // all findings (not just blocking ones)
	Blocking []engine.Finding // findings at/above the threshold
	Passed   bool             // true if no blocking findings, or accepted
}

VerifyResult is the outcome of verifying a plan.

func Verify

func Verify(set *rules.Set, planPath, minSeverity string, accept bool, now time.Time) (VerifyResult, error)

Verify scans the plan at planPath and decides whether it may be applied. On a pass (no blocking findings) — or when accept is true despite blocking findings — it writes a verdict bound to the plan's sha256 and returns Passed=true. It does not write a verdict on a hard fail.

now is injected so callers/tests control the timestamp.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL