Policies / detail

attestation.rego

Edit

Type rego · State inactive · Current version v2 · Updated 2026-05-08T14:35:37.969592648+00:00

Current body

sha256: ff5884cf1814db51d283dae51647aa386759790a80039595a333a6408d7faf84
# Attestation enforcement (warden-specs/TECH_SPEC.md#identity-service §6).
#
# Two deny rules gate sensitive tools on a verified attestation claim:
#
#   1. tool_requires_attestation true + attestation absent or expired
#      → deny with `attestation_stale` reason.
#   2. tool_requires_attestation true + attestation fresh, but the
#      claimed measurement is not in the per-tool allowlist
#      → deny with `measurement not in allowlist` reason.
#
# The allowlist itself lives in `attestation_allowlist.json` (loaded as
# a regorus data document by `build_engine_from_dir`). v1 covers
# `wire_transfer` and the `delete_*` family, with `dev-binary-hash` as
# the only approved measurement — the chaos-monkey scenario in 
# fires `wire_transfer` with a non-allowlisted measurement and asserts
# the deny path. Per warden-specs/TECH_SPEC.md#identity-service §6 the file format will move to a
# Sigstore-style signed transparency log; v1 is "the code that calls
# wire_transfer is the code we checked into git."
#
# Why `input.tool_type` and not `input.method` (as the spec sketch
# uses): existing rules in `governance.rego` key on `tool_type` (the
# `params.name` extracted by the proxy), while `method` is the
# JSON-RPC method (`call_tool`, `tools/list`, ...). For attestation
# enforcement the question is "what does the tool DO," so `tool_type`
# is the right key.

package warden.authz

import rego.v1

# Tools that require a fresh, allowlisted attestation. The v1 set is
# the warden-specs/TECH_SPEC.md#identity-service §9-phase-4 launch slice: `wire_transfer` and the
# `delete_*` family. Pattern matching with `startswith` keeps the
# allowlist data-driven rather than baking each delete tool here.
#
# **Migration-phase gating.** Both rules also require
# `input.agent_spiffe` to be present — i.e. attestation is only
# enforced for agents that have switched to warden-identity-issued
# SVIDs (post ). Legacy CA-minted agents (CN-only certs in
# `warden-e2e/run.sh` and `warden-chaos-monkey`) continue to flow
# through the existing review path until SVID-bound mTLS becomes
# mandatory. The chaos-monkey `unattested_binary` scenario in 
# bypasses this clause by minting a SPIFFE-SAN cert, which is what
# makes that scenario a real attestation-gate test.
tool_requires_attestation if {
	# `is_string` handles both shapes a non-SVID caller can produce:
	# the JSON key absent (Option::None with `skip_serializing_if`)
	# AND the JSON key set to `null` (Option::None without the skip).
	# A bare `input.agent_spiffe` would fire on the second shape since
	# rego treats null-valued bindings as defined.
	is_string(input.agent_spiffe)
	input.tool_type == "wire_transfer"
}

tool_requires_attestation if {
	is_string(input.agent_spiffe)
	startswith(input.tool_type, "delete_")
}

# Fresh = attestation present AND `expires_at` in the future per the
# proxy's clock (`ns_now`, defined in governance.rego). Undefined when
# `input.attestation` is absent — `not fresh_attestation` then fires
# the stale-deny rule below. RFC 3339 parse failures also leave the
# rule undefined → safe (we err toward "attestation_stale" rather
# than letting a malformed expires_at sneak through).
fresh_attestation if {
	input.attestation
	expires_ns := time.parse_rfc3339_ns(input.attestation.expires_at)
	expires_ns > ns_now
}

# Lookup against the allowlist data document. Undefined when the tool
# has no allowlist entry OR when the agent's measurement isn't listed.
# The deny rule below pairs this with `tool_requires_attestation` so
# tools we never registered (i.e. typos) hit the stale-deny path
# rather than this one — attempting to run an unregistered sensitive
# tool should still 403, just with a clearer reason than "your binary
# isn't in the allowlist for $tool that doesn't exist."
measurement_allowed if {
	input.attestation
	some m in data.warden.attestation.allowed_measurements[input.tool_type]
	m == input.attestation.measurement
}

# Stale / missing attestation for an attestation-required tool. The
# `attestation_stale` substring is the operator-facing grep target —
# chaos-monkey's `unattested_binary` scenario asserts on it, and
# auditors filter ledger rows by it.
deny contains msg if {
	tool_requires_attestation
	not fresh_attestation
	msg := sprintf(
		"Violation: attestation_stale — required attestation is missing or expired for %s.",
		[input.tool_type],
	)
}

# Fresh attestation but the binary is not approved for this tool.
# Format the measurement into the message so a triager pasting the
# row into the SIEM can see exactly which build was rejected.
deny contains msg if {
	tool_requires_attestation
	fresh_attestation
	not measurement_allowed
	# regorus's `sprintf` doesn't support Go's `%q` verb (quote-and-
	# escape) so we splice the measurement with explicit quotes.
	msg := sprintf(
		"Violation: agent measurement \"%s\" not in allowlist for %s.",
		[input.attestation.measurement, input.tool_type],
	)
}