Files
git.stella-ops.org/devops/tools/replay/verify-policy-sim-lock.sh
2025-12-26 18:11:06 +02:00

89 lines
2.9 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
set -euo pipefail
# Offline verifier for policy-sim inputs lock (PS1PS10 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 <file> --graph <file> --sbom <file> --time-anchor <file> --dataset <file> [--max-age-hours <n>]" >&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)."