#!/usr/bin/env bash set -euo pipefail # Offline verifier for policy-sim inputs lock (PS1–PS10 remediation). # Usage: verify-policy-sim-lock.sh lock.json --policy path --graph path --sbom path --time-anchor path --dataset path [--max-age-hours 24] usage() { echo "Usage: $0 lock.json --policy --graph --sbom --time-anchor --dataset [--max-age-hours ]" >&2 exit 2 } [[ $# -lt 11 ]] && usage lock="" policy="" graph="" sbom="" time_anchor="" dataset="" max_age_hours=0 while [[ $# -gt 0 ]]; do case "$1" in --policy) policy=${2:-}; shift ;; --graph) graph=${2:-}; shift ;; --sbom) sbom=${2:-}; shift ;; --time-anchor) time_anchor=${2:-}; shift ;; --dataset) dataset=${2:-}; shift ;; --max-age-hours) max_age_hours=${2:-0}; shift ;; *) if [[ -z "$lock" ]]; then lock=$1; else usage; fi ;; esac shift done [[ -z "$lock" || -z "$policy" || -z "$graph" || -z "$sbom" || -z "$time_anchor" || -z "$dataset" ]] && usage require() { command -v "$1" >/dev/null || { echo "$1 is required" >&2; exit 2; }; } require jq require sha256sum calc_sha() { sha256sum "$1" | awk '{print $1}'; } lock_policy=$(jq -r '.policyBundleSha256' "$lock") lock_graph=$(jq -r '.graphSha256' "$lock") lock_sbom=$(jq -r '.sbomSha256' "$lock") lock_anchor=$(jq -r '.timeAnchorSha256' "$lock") lock_dataset=$(jq -r '.datasetSha256' "$lock") lock_shadow=$(jq -r '.shadowIsolation' "$lock") lock_scopes=$(jq -r '.requiredScopes[]?' "$lock" | tr '\n' ' ') lock_generated=$(jq -r '.generatedAt' "$lock") sha_ok() { [[ $1 =~ ^[A-Fa-f0-9]{64}$ ]] } for h in "$lock_policy" "$lock_graph" "$lock_sbom" "$lock_anchor" "$lock_dataset"; do sha_ok "$h" || { echo "invalid digest format: $h" >&2; exit 3; } done [[ "$lock_shadow" == "true" ]] || { echo "shadowIsolation must be true" >&2; exit 5; } if ! grep -qi "policy:simulate:shadow" <<< "$lock_scopes"; then echo "requiredScopes missing policy:simulate:shadow" >&2; exit 5; fi [[ "$lock_policy" == "$(calc_sha "$policy")" ]] || { echo "policy digest mismatch" >&2; exit 3; } [[ "$lock_graph" == "$(calc_sha "$graph")" ]] || { echo "graph digest mismatch" >&2; exit 3; } [[ "$lock_sbom" == "$(calc_sha "$sbom")" ]] || { echo "sbom digest mismatch" >&2; exit 3; } [[ "$lock_anchor" == "$(calc_sha "$time_anchor")" ]] || { echo "time anchor digest mismatch" >&2; exit 3; } [[ "$lock_dataset" == "$(calc_sha "$dataset")" ]] || { echo "dataset digest mismatch" >&2; exit 3; } if [[ $max_age_hours -gt 0 ]]; then now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") age_hours=$(python3 - <<'PY' import sys,datetime lock=sys.argv[1].replace('Z','+00:00') now=sys.argv[2].replace('Z','+00:00') l=datetime.datetime.fromisoformat(lock) n=datetime.datetime.fromisoformat(now) print((n-l).total_seconds()/3600) PY "$lock_generated" "$now") if (( $(printf '%.0f' "$age_hours") > max_age_hours )); then echo "lock stale: ${age_hours}h > ${max_age_hours}h" >&2 exit 4 fi fi echo "policy-sim lock verified (shadow mode enforced)."