feat(casan): establish assurance kernel and harden control plane

This commit is contained in:
thanhnv
2026-08-02 23:24:51 +07:00
parent 8b477f3800
commit 5745519126
51 changed files with 4076 additions and 180 deletions
@@ -110,6 +110,10 @@ LATENCY_MS=$((END_MS - START_MS))
if [[ ! -f "$OUTPUT_FILE" ]]; then
STATUS="failed"
ERROR_MSG="${ERROR_MSG:-output file not produced}"
# A zero command exit does not make the step successful when the runtime
# contract requires an output artifact and none was produced. Telemetry can
# record this failure successfully, but must propagate a failed step outcome.
[[ "$EXIT_CODE" -eq 0 ]] && EXIT_CODE=1
: > "$OUTPUT_FILE"
fi
@@ -24,6 +24,7 @@ fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/casan-paths.sh"
PROJECT_ROOT="$CASAN_APP_ROOT"
KERNEL_CLI="$CASAN_HARNESS_ROOT/scripts/python/kernel_cli.py"
# SEC-23 (MT-01): make state (control-plane settings, telemetry, audit) tenant-scoped
# when CASAN_TENANT_ID is set, so a run for tenant A never touches tenant B's state.
# No-op when no tenant is set (baseline unchanged); invalid tenant fails closed.
@@ -56,12 +57,52 @@ write_phase_report() {
"$ACTION_NAME" "$CACHE_STATUS" "$PHASE_LOG" > "$PHASE_REPORT" 2>/dev/null || true
}
emit_failed_kernel() { # phase rc — best effort, never masks the original failure
local failed_phase="$1" failed_rc="$2"
[[ -f "$KERNEL_CLI" ]] || return 0
local run_id="${EXECUTION_ID:-native-failed-${TRACE_SUFFIX:-$$}}"
local mode="${NATIVE_MODE:-observe}"
local event bundle path
event="$(CASAN_NATIVE_RISK="${ACTION_RISK_JSON:-}" python3 - "$run_id" "$ACTION_NAME" "$mode" "$failed_phase" "$failed_rc" "${CASAN_ACTOR:-${CASAN_AGENT:-}}" <<'PY'
import json, os, sys
run_id, action, mode, phase, rc, actor = sys.argv[1:]
try: risk = json.loads(os.environ.get("CASAN_NATIVE_RISK") or "{}")
except ValueError: risk = {}
print(json.dumps({
"run_id": run_id, "correlation_id": run_id, "action": action,
"tool": action, "actor": actor, "mode": mode,
"environment": os.environ.get("CASAN_PROFILE", "development"),
"execution_status": "failed", "assurance_status": "failed",
"certification_status": "non_certified", "policy_decisions": [risk] if risk else [],
"extensions": {"failed_phase": phase, "exit_code": int(rc)},
}, separators=(",", ":")))
PY
)" || return 0
bundle="$(printf '%s' "$event" | python3 "$KERNEL_CLI" adapt-native - 2>/dev/null)" || return 0
path="$CASAN_STATE_ROOT/logs/kernel/$run_id.json"
CASAN_KERNEL_BUNDLE="$bundle" python3 - "$path" <<'PY' || return 0
import json, os, sys, tempfile
path = sys.argv[1]; payload = json.loads(os.environ["CASAN_KERNEL_BUNDLE"])
os.makedirs(os.path.dirname(path), exist_ok=True)
fd, tmp = tempfile.mkstemp(prefix=".kernel-", dir=os.path.dirname(path))
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
json.dump(payload, handle, sort_keys=True, separators=(",", ":")); handle.write("\n")
handle.flush(); os.fsync(handle.fileno())
os.replace(tmp, path)
finally:
try: os.unlink(tmp)
except OSError: pass
PY
}
run_phase() { # <phase-name> <command...> — preserves the failing rc exactly
local phase="$1"; shift
local rc=0
"$@" || rc=$?
record_phase "$phase" "$rc"
if [[ "$rc" -ne 0 ]]; then
emit_failed_kernel "$phase" "$rc"
write_phase_report
exit "$rc"
fi
@@ -76,9 +117,16 @@ hash_text() {
}
CMD_STR="${*:-no_cmd}"
NATIVE_MODE="${CASAN_ENFORCEMENT_MODE:-}"
if [[ -z "$NATIVE_MODE" ]]; then
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ]]; then NATIVE_MODE="enforce"; else NATIVE_MODE="observe"; fi
fi
case "$NATIVE_MODE" in observe|enforce) : ;; *) NATIVE_MODE="enforce"; casan_log error harness "INVALID_ENFORCEMENT_MODE fail_closed=enforce" ;; esac
INPUT_HASH="$(cat "$INPUT_FILE" | hash_text)"
CMD_HASH="$(printf '%s' "$CMD_STR" | hash_text)"
IDEMPOTENCY_KEY="$(printf '%s|%s|%s' "$INPUT_HASH" "$CMD_HASH" "$ACTION_NAME" | hash_text)"
EXECUTION_ID="${CASAN_EXECUTION_ID:-native-${IDEMPOTENCY_KEY:0:24}}"
export CASAN_EXECUTION_ID="$EXECUTION_ID"
CACHE_META="$CACHE_DIR/$IDEMPOTENCY_KEY.json"
CACHE_OUT="$CACHE_DIR/$IDEMPOTENCY_KEY.output"
@@ -92,7 +140,7 @@ casan_log debug harness "action=$ACTION_NAME input=$INPUT_FILE output=$FINAL_OUT
# C7: honor an engaged kill-switch before doing any work (incident containment).
# Opt-in (default off) so the baseline is unchanged. SEC-17 (ARCH-03): under
# CASAN_PROFILE=prod it defaults ON (secure-by-default); an explicit =0 still wins.
if [[ "${CASAN_KILLSWITCH_ENFORCE:-0}" == "1" || ( -z "${CASAN_KILLSWITCH_ENFORCE+x}" && "${CASAN_PROFILE:-}" == "prod" ) ]]; then
if [[ "${CASAN_KILLSWITCH_ENFORCE:-0}" == "1" || ( -z "${CASAN_KILLSWITCH_ENFORCE+x}" && ( "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ) ) ]]; then
KS_SCOPE="${CASAN_KILLSWITCH_SCOPE:-project}"
KS_ID="${CASAN_KILLSWITCH_ID:-${CASAN_PROJECT:-current}}"
if ! bash "$SCRIPT_DIR/kill-switch.sh" check "$KS_SCOPE" "$KS_ID" >/dev/null 2>&1; then
@@ -116,7 +164,7 @@ fi
# signed manifest and REFUSE to run on any drift — editing a gate/policy is a bypass
# that leaves no input trace. Only active when a manifest is provisioned (so dev and
# prod-without-a-manifest are unaffected); a present-but-drifted bundle fails closed.
if [[ ( "${CASAN_PROFILE:-}" == "prod" || "${CASAN_VERIFY_STRICT:-}" == "1" ) \
if [[ ( "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" || "${CASAN_VERIFY_STRICT:-}" == "1" ) \
&& -f "$SCRIPT_DIR/bundle-integrity.py" ]]; then
BUNDLE_MANIFEST="${CASAN_BUNDLE_MANIFEST:-$CASAN_GOVERNANCE_ROOT/harness-bundle-manifest.json}"
if [[ -f "$BUNDLE_MANIFEST" ]]; then
@@ -132,15 +180,62 @@ fi
run_phase "H4-in" "$SCRIPT_DIR/security-check.sh" "$INPUT_FILE" "$SAFE_INPUT" input
run_phase "H5" "$SCRIPT_DIR/governance-check.sh" "$SAFE_INPUT" "$APPROVED_INPUT" "$ACTION_NAME"
# H2 tool registry gate is in the line of fire for side-effecting actions:
# it enforces idempotency key, per-agent permission, and rollback strategy
# before the command is allowed to execute. The wrapper already derived a
# content-addressed idempotency key above.
case "$ACTION_NAME" in
write_code|migration|deploy|db_write|external_api|write_file)
run_phase "H2-gate" env CASAN_IDEMPOTENCY_KEY="$IDEMPOTENCY_KEY" "$SCRIPT_DIR/tool-registry-gate.sh" "$ACTION_NAME"
;;
esac
# Canonical action classification decides whether the H2 registry and isolated
# executor are required. A classifier failure is treated as unknown/high-risk.
ACTION_RISK_JSON=""
ACTION_RISK_RC=0
ACTION_RISK_JSON="$(python3 "$KERNEL_CLI" risk --action "$ACTION_NAME" --tool "$ACTION_NAME" \
--command "$CMD_STR" --content-file "$SAFE_INPUT" --actor "${CASAN_ACTOR:-${CASAN_AGENT:-}}" \
--environment "${CASAN_PROFILE:-development}" 2>/dev/null)" || ACTION_RISK_RC=$?
if [[ "$ACTION_RISK_RC" -eq 0 && -n "$ACTION_RISK_JSON" ]]; then
ACTION_FIELDS="$(python3 - "$ACTION_RISK_JSON" <<'PY'
import json, sys
p=json.loads(sys.argv[1])
print("%s\t%s" % ("1" if p["side_effecting"] else "0", p["action_class"]))
PY
)" || ACTION_RISK_RC=$?
fi
if [[ "$ACTION_RISK_RC" -eq 0 && -n "${ACTION_FIELDS:-}" ]]; then
IFS=$'\t' read -r SIDE_EFFECTING ACTION_CLASS <<< "$ACTION_FIELDS"
else
SIDE_EFFECTING=1
ACTION_CLASS="unknown"
casan_log error harness "ACTION_CLASSIFIER_FAILED_CLOSED action=$ACTION_NAME"
fi
if [[ "$SIDE_EFFECTING" == "1" ]]; then
REGISTRY_ARGS=(registry-config --mode "$NATIVE_MODE" --profile "${CASAN_PROFILE:-development}" \
--evidence-log "$CASAN_STATE_ROOT/logs/policy/h2-registry-config.jsonl")
[[ -n "${CASAN_H2_REGISTRY+x}" ]] && REGISTRY_ARGS+=(--explicit "$CASAN_H2_REGISTRY")
REGISTRY_RC=0
REGISTRY_JSON="$(python3 "$KERNEL_CLI" "${REGISTRY_ARGS[@]}")" || REGISTRY_RC=$?
if [[ "$REGISTRY_RC" -ne 0 ]]; then
record_phase "H2-config" "$REGISTRY_RC"
echo "H2_REGISTRY_CONFIGURATION_DENIED $REGISTRY_JSON" >&2
write_phase_report
exit "$REGISTRY_RC"
fi
REGISTRY_ENABLED="$(python3 -c 'import json,sys; print("1" if json.load(sys.stdin)["enabled"] else "0")' <<< "$REGISTRY_JSON")"
if [[ "$REGISTRY_ENABLED" == "1" ]]; then
H2_ACTION="$ACTION_NAME"
case "$ACTION_CLASS" in
write) H2_ACTION="write_file" ;;
delete) H2_ACTION="delete_file" ;;
database_mutation) H2_ACTION="db_write" ;;
deployment|release|infrastructure_modification) H2_ACTION="deploy" ;;
external_network_side_effect) H2_ACTION="external_api" ;;
unknown) H2_ACTION="unknown_tool" ;;
esac
run_phase "H2-gate" python3 "$KERNEL_CLI" h2-gate \
--gate "${CASAN_H2_GATE_PATH:-$SCRIPT_DIR/tool-registry-gate.sh}" --mode "$NATIVE_MODE" \
--actor "${CASAN_ACTOR:-${CASAN_AGENT:-}}" --action "$H2_ACTION" --tool "$ACTION_NAME" \
--execution-id "$EXECUTION_ID" --enforcement-path "native_harness.pre_execution.h2_registry" \
--idempotency-key "$IDEMPOTENCY_KEY" --timeout "${CASAN_H2_GATE_TIMEOUT_SECONDS:-8}" \
--evidence-log "$CASAN_STATE_ROOT/logs/policy/h2-decisions.jsonl"
else
casan_log warn harness "HIGH H2 registry unsafe development/test bypass active; run is non-certifiable"
fi
fi
# T4: propagate step name so any nested model calls (model-call.py) log against the same step
# name, enabling provider-cost-lookup.py to match real Ollama token counts in agent-metrics.sh.
@@ -155,13 +250,34 @@ if [[ -f "$CACHE_META" && -f "$CACHE_OUT" ]]; then
run_phase "H6-exec" "$SCRIPT_DIR/agent-metrics.sh" "$APPROVED_INPUT" "$RAW_OUTPUT" -- bash -c 'cp "$1" "$CASAN_OUTPUT"' _ "$CACHE_OUT"
elif [[ "$#" -gt 0 ]]; then
CACHE_STATUS="stored"
run_phase "H6-exec" "$SCRIPT_DIR/agent-metrics.sh" "$APPROVED_INPUT" "$RAW_OUTPUT" -- \
"$SCRIPT_DIR/tool-exec.sh" "$TOOL_TIMEOUT" -- "$@"
if [[ "$SIDE_EFFECTING" == "1" && "$NATIVE_MODE" == "enforce" ]]; then
run_phase "H6-exec" env CASAN_ENFORCEMENT_MODE="$NATIVE_MODE" "$SCRIPT_DIR/agent-metrics.sh" "$APPROVED_INPUT" "$RAW_OUTPUT" -- \
"$SCRIPT_DIR/sandbox-run.sh" --workspace "$PROJECT_ROOT" --timeout "$TOOL_TIMEOUT" -- "$@"
else
run_phase "H6-exec" "$SCRIPT_DIR/agent-metrics.sh" "$APPROVED_INPUT" "$RAW_OUTPUT" -- \
"$SCRIPT_DIR/tool-exec.sh" "$TOOL_TIMEOUT" -- "$@"
fi
else
CACHE_STATUS="stored"
run_phase "H6-exec" "$SCRIPT_DIR/agent-metrics.sh" "$APPROVED_INPUT" "$RAW_OUTPUT"
fi
TOOL_OUTPUT_MAX_BYTES="${CASAN_TOOL_OUTPUT_MAX_BYTES:-1048576}"
if [[ ! "$TOOL_OUTPUT_MAX_BYTES" =~ ^[1-9][0-9]*$ ]]; then
casan_log error harness "TOOL_OUTPUT_LIMIT_INVALID value=$TOOL_OUTPUT_MAX_BYTES"
: > "$FINAL_OUTPUT"
exit 2
fi
RAW_OUTPUT_BYTES="$(wc -c < "$RAW_OUTPUT" | tr -d ' ')"
if [[ "$RAW_OUTPUT_BYTES" -gt "$TOOL_OUTPUT_MAX_BYTES" ]]; then
record_phase "H4-output-size" 2
casan_log error harness "TOOL_OUTPUT_QUARANTINED bytes=$RAW_OUTPUT_BYTES limit=$TOOL_OUTPUT_MAX_BYTES"
: > "$FINAL_OUTPUT"
write_phase_report
echo "TOOL_OUTPUT_QUARANTINED reason=output_size_limit bytes=$RAW_OUTPUT_BYTES limit=$TOOL_OUTPUT_MAX_BYTES" >&2
exit 2
fi
# V7: tool output can carry indirect injection that would re-enter a downstream
# model's context. Scan RAW_OUTPUT for injection/secret patterns before it is
# reused. Mode: off | warn (default) | block. Strict mode upgrades to block.
@@ -170,7 +286,7 @@ fi
TOOL_OUTPUT_SCAN_MODE="${CASAN_TOOL_OUTPUT_SCAN:-}"
if [[ -z "$TOOL_OUTPUT_SCAN_MODE" ]]; then
# SEC-17/M-02: prod profile defaults tool-output scanning to block (fail-closed).
if [[ "${CASAN_SECURITY_STRICT:-0}" == "1" || ( -z "${CASAN_SECURITY_STRICT+x}" && "${CASAN_PROFILE:-}" == "prod" ) ]]; then TOOL_OUTPUT_SCAN_MODE="block"; else TOOL_OUTPUT_SCAN_MODE="warn"; fi
if [[ "${CASAN_SECURITY_STRICT:-0}" == "1" || ( -z "${CASAN_SECURITY_STRICT+x}" && ( "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ) ) ]]; then TOOL_OUTPUT_SCAN_MODE="block"; else TOOL_OUTPUT_SCAN_MODE="warn"; fi
fi
if [[ "$TOOL_OUTPUT_SCAN_MODE" != "off" ]]; then
TOS_RC=0
@@ -204,6 +320,65 @@ EOF
cp "$FINAL_OUTPUT" "$CACHE_OUT"
fi
# Dual-emit the framework-independent kernel envelope. Legacy phase reports and
# outputs remain unchanged; the canonical contract is an additive artifact.
TRUST_JSON="$(python3 "$KERNEL_CLI" trust-capabilities 2>/dev/null || true)"
RUN_CERTIFICATION="non_certified"
RUN_ASSURANCE="passed"
[[ "$NATIVE_MODE" == "observe" ]] && RUN_ASSURANCE="degraded"
if [[ "$NATIVE_MODE" == "enforce" && -n "$TRUST_JSON" ]]; then
TRUST_CERTIFIABLE="$(python3 -c 'import json,sys; print("1" if json.load(sys.stdin).get("certifiable") else "0")' <<< "$TRUST_JSON" 2>/dev/null || echo 0)"
REGISTRY_BYPASS="$(printf '%s' "${REGISTRY_JSON:-{}}" | python3 -c 'import json,sys; print("1" if json.load(sys.stdin).get("unsafe_bypass") else "0")' 2>/dev/null || echo 0)"
[[ "$REGISTRY_BYPASS" == "1" ]] && RUN_ASSURANCE="degraded"
[[ "$TRUST_CERTIFIABLE" == "1" && "$REGISTRY_BYPASS" == "0" ]] && RUN_CERTIFICATION="certified"
fi
NATIVE_EVENT="$(CASAN_NATIVE_RISK="$ACTION_RISK_JSON" python3 - "$EXECUTION_ID" "$ACTION_NAME" "$ACTION_CLASS" "$NATIVE_MODE" "$RUN_ASSURANCE" "$RUN_CERTIFICATION" "${CASAN_ACTOR:-${CASAN_AGENT:-}}" "$INPUT_HASH" "$CMD_HASH" <<'PY'
import json, os, sys
run_id, action, action_class, mode, assurance, certification, actor, input_hash, command_hash = sys.argv[1:]
try:
risk = json.loads(os.environ.get("CASAN_NATIVE_RISK") or "{}")
except ValueError:
risk = {}
print(json.dumps({
"run_id": run_id, "correlation_id": run_id, "action": action,
"tool": action, "actor": actor, "mode": mode,
"environment": os.environ.get("CASAN_PROFILE", "development"),
"execution_status": "success", "assurance_status": assurance,
"certification_status": certification,
"policy_decisions": [risk] if risk else [],
"extensions": {"input_hash": input_hash, "command_hash": command_hash, "action_class": action_class},
}, separators=(",", ":")))
PY
)"
KERNEL_RC=0
KERNEL_BUNDLE="$(printf '%s' "$NATIVE_EVENT" | python3 "$KERNEL_CLI" adapt-native -)" || KERNEL_RC=$?
if [[ "$KERNEL_RC" -ne 0 ]]; then
casan_log error harness "KERNEL_CONTRACT_EMISSION_FAILED rc=$KERNEL_RC"
if [[ "$NATIVE_MODE" == "enforce" ]]; then
: > "$FINAL_OUTPUT"
exit 2
fi
RUN_CERTIFICATION="non_certified"
else
KERNEL_PATH="$CASAN_STATE_ROOT/logs/kernel/$EXECUTION_ID.json"
python3 - "$KERNEL_PATH" "$KERNEL_BUNDLE" <<'PY'
import json, os, sys, tempfile
path, raw = sys.argv[1:]
os.makedirs(os.path.dirname(path), exist_ok=True)
payload = json.loads(raw)
fd, tmp = tempfile.mkstemp(prefix=".kernel-", dir=os.path.dirname(path))
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
json.dump(payload, handle, sort_keys=True, separators=(",", ":"))
handle.write("\n")
handle.flush(); os.fsync(handle.fileno())
os.replace(tmp, path)
finally:
try: os.unlink(tmp)
except OSError: pass
PY
fi
write_phase_report
casan_log debug harness "action=$ACTION_NAME complete cache=$CACHE_STATUS"
echo "CASAN_HARNESS_COMPLETE cache=$CACHE_STATUS key=$IDEMPOTENCY_KEY output=$FINAL_OUTPUT"
echo "CASAN_HARNESS_COMPLETE cache=$CACHE_STATUS key=$IDEMPOTENCY_KEY execution=success assurance=$RUN_ASSURANCE certification=$RUN_CERTIFICATION output=$FINAL_OUTPUT"
@@ -58,27 +58,48 @@ TRACE_ID="$(new_trace_id)"
TIMESTAMP="$(timestamp)"
INPUT="$(cat "$INPUT_FILE")"
LOWER_INPUT="$(printf '%s' "$INPUT" | tr '[:upper:]' '[:lower:]')"
ACTOR="${CASAN_ACTOR:-developer}"
ACTOR="${CASAN_ACTOR:-${CASAN_AGENT:-}}"
APPROVER="${CASAN_APPROVER:-}"
APPROVAL_DECISION="${CASAN_APPROVAL_DECISION:-auto}"
AUDIT_LOG="$AUDIT_DIR/audit.jsonl"
RISK_LEVEL="low"
REASONS=()
case "$ACTION_NAME" in
deploy|launch|write_code|write_file|migration|db_write|external_api|tool_call)
RISK_LEVEL="medium"
REASONS+=("sensitive-action:$ACTION_NAME")
;;
esac
if printf '%s' "$LOWER_INPUT" | grep -Eq "(delete|drop table|password|api[_-]?key|secret|token|credential|migration|deploy|external api|shutdown|dump database)"; then
ACTION_CLASS="unknown"
RISK_FACTORS_JSON='{"action_risk":"high","content_risk":"high","environment_risk":"low","identity_risk":"low","resource_risk":"low"}'
EVIDENCE_REQUIREMENT="required"
RISK_POLICY_DECISION="require_approval"
KERNEL_CLI="$CASAN_HARNESS_ROOT/scripts/python/kernel_cli.py"
RISK_JSON=""
RISK_RC=0
if [[ -f "$KERNEL_CLI" ]]; then
RISK_JSON="$(python3 "$KERNEL_CLI" risk --action "$ACTION_NAME" --tool "$ACTION_NAME" \
--content-file "$INPUT_FILE" --actor "$ACTOR" --environment "${CASAN_PROFILE:-development}" 2>/dev/null)" || RISK_RC=$?
else
RISK_RC=127
fi
if [[ "$RISK_RC" -eq 0 && -n "$RISK_JSON" ]]; then
RISK_FIELDS="$(python3 - "$RISK_JSON" <<'PY'
import json, sys
payload = json.loads(sys.argv[1])
print("\t".join([
str(payload["action_class"]),
str(payload["effective_risk"]),
json.dumps(payload["risk_factors"], sort_keys=True, separators=(",", ":")),
str(payload["evidence_requirement"]),
str(payload["decision"]),
]))
PY
)" || RISK_RC=$?
fi
if [[ "$RISK_RC" -eq 0 && -n "${RISK_FIELDS:-}" ]]; then
IFS=$'\t' read -r ACTION_CLASS RISK_LEVEL RISK_FACTORS_JSON EVIDENCE_REQUIREMENT RISK_POLICY_DECISION <<< "$RISK_FIELDS"
REASONS+=("action-risk-floor:$ACTION_CLASS")
else
RISK_LEVEL="high"
REASONS+=("high-risk-content")
elif printf '%s' "$LOWER_INPUT" | grep -Eq "(internal|config|system|policy|permission)"; then
[[ "$RISK_LEVEL" == "low" ]] && RISK_LEVEL="medium"
REASONS+=("medium-risk-content")
ACTION_CLASS="unknown"
RISK_POLICY_DECISION="require_approval"
REASONS+=("action-risk-classifier-failed-closed")
fi
APPROVAL_STATUS="auto_approved"
@@ -88,8 +109,14 @@ if [[ "$RISK_LEVEL" == "medium" ]]; then
APPROVAL_STATUS="policy_auto_approved_with_audit"
fi
if [[ "$RISK_LEVEL" == "high" ]]; then
if [[ "${CASAN_APPROVAL_STRICT:-0}" == "1" ]]; then
if [[ "$RISK_POLICY_DECISION" == "deny" ]]; then
APPROVAL_STATUS="actor_identity_required"
DECISION="denied"
REASONS+=("actor-identity-required")
elif [[ "$RISK_LEVEL" == "high" || "$RISK_LEVEL" == "critical" || "$RISK_POLICY_DECISION" == "require_approval" ]]; then
APPROVAL_STRICT_EFFECTIVE="${CASAN_APPROVAL_STRICT:-0}"
[[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ]] && APPROVAL_STRICT_EFFECTIVE="1"
if [[ "$APPROVAL_STRICT_EFFECTIVE" == "1" ]]; then
# Approval-identity mode (V20): an env-var approver is NOT enough — the
# reviewer must cryptographically SIGN this exact request and their role must
# be authorized for the action. SoD (actor != approver) still enforced.
@@ -147,7 +174,7 @@ fi
REASONS_JSON="$(printf '%s\n' "${REASONS[@]:-}" | python -c 'import json,sys; print(json.dumps([x for x in sys.stdin.read().splitlines() if x]))')"
# approver and output_hash are part of the hashed core so they cannot be
# silently mutated after the fact.
RECORD_CORE="$(printf '%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s' "$TIMESTAMP" "$TRACE_ID" "$ACTION_NAME" "$ACTOR" "$RISK_LEVEL" "$DECISION" "$APPROVAL_STATUS" "$APPROVER" "$INPUT_HASH" "$OUTPUT_HASH" "$PREV_HASH")"
RECORD_CORE="$(printf '%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s' "$TIMESTAMP" "$TRACE_ID" "$ACTION_NAME" "$ACTION_CLASS" "$ACTOR" "$RISK_LEVEL" "$RISK_FACTORS_JSON" "$EVIDENCE_REQUIREMENT" "$DECISION" "$APPROVAL_STATUS" "$APPROVER" "$INPUT_HASH" "$OUTPUT_HASH" "$PREV_HASH")"
RECORD_HASH="$(printf '%s' "$RECORD_CORE" | hash_text)"
TRACE_FILE="$TRACE_DIR/governance-$TRACE_ID.json"
@@ -160,18 +187,24 @@ TRACE_FILE="$TRACE_DIR/governance-$TRACE_ID.json"
# written (disk full, read-only, quota), there must be NO governed action without
# its accountability record — deny and empty the output rather than proceed.
if ! CASAN_GC_REASONS="$REASONS_JSON" python - "$TRACE_FILE" "$AUDIT_LOG" \
"$TIMESTAMP" "$TRACE_ID" "$ACTION_NAME" "$ACTOR" "$RISK_LEVEL" "$DECISION" \
"$APPROVAL_STATUS" "$APPROVER" "$INPUT_HASH" "$OUTPUT_HASH" "$PREV_HASH" "$RECORD_HASH" <<'PY'
"$TIMESTAMP" "$TRACE_ID" "$ACTION_NAME" "$ACTION_CLASS" "$ACTOR" "$RISK_LEVEL" \
"$RISK_FACTORS_JSON" "$EVIDENCE_REQUIREMENT" "$DECISION" "$APPROVAL_STATUS" \
"$APPROVER" "$INPUT_HASH" "$OUTPUT_HASH" "$PREV_HASH" "$RECORD_HASH" <<'PY'
import json, os, sys
(trace_file, audit_log, ts, trace_id, action, actor, risk, decision,
approval_status, approver, input_hash, output_hash, prev_hash, record_hash) = sys.argv[1:]
(trace_file, audit_log, ts, trace_id, action, action_class, actor, risk,
risk_factors_json, evidence_requirement, decision, approval_status, approver,
input_hash, output_hash, prev_hash, record_hash) = sys.argv[1:]
try:
reasons = json.loads(os.environ.get("CASAN_GC_REASONS") or "[]")
except ValueError:
reasons = []
rec = {
"schema_version": 2, "category": "runtime_control",
"timestamp": ts, "trace_id": trace_id, "harness": "H5-governance",
"action": action, "actor": actor, "risk_level": risk, "decision": decision,
"action": action, "action_class": action_class, "actor": actor,
"risk_level": risk, "effective_risk": risk,
"risk_factors": json.loads(risk_factors_json),
"evidence_requirement": evidence_requirement, "decision": decision,
"approval_status": approval_status, "approver": approver,
"input_hash": input_hash, "output_hash": output_hash,
"previous_record_hash": prev_hash, "record_hash": record_hash,
@@ -196,8 +229,43 @@ fi
# --- External anchor: cryptographically sign the new chain head ---
# A re-forged chain (recomputed hashes) changes the head; without the private
# key the attacker cannot produce a matching signature, so verification fails.
# Production note: the private key must live off-repo (KMS/HSM). It is local
# here only for self-contained demonstration.
# Development may use a local key for self-contained demonstration. Production
# refuses that path unless an explicit emergency override is visible in evidence.
PRODUCTION_PROFILE=0
[[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ]] && PRODUCTION_PROFILE=1
EMERGENCY_TRUST_OVERRIDE="${CASAN_TRUST_EMERGENCY_OVERRIDE:-0}"
TRUST_LOG="$CASAN_STATE_ROOT/logs/readiness/trust-capabilities.jsonl"
mkdir -p "$(dirname "$TRUST_LOG")"
if [[ "$PRODUCTION_PROFILE" == "1" && "$EMERGENCY_TRUST_OVERRIDE" != "1" ]]; then
TRUST_RC=0
TRUST_JSON="$(python3 "$KERNEL_CLI" trust-capabilities 2>/dev/null)" || TRUST_RC=$?
if [[ "$TRUST_RC" -ne 0 ]]; then
if [[ -n "$TRUST_JSON" ]]; then
printf '%s\n' "$TRUST_JSON" >> "$TRUST_LOG"
else
printf '{"ready":false,"severity":"critical","reason_codes":["production_trust_configuration_invalid"]}\n' >> "$TRUST_LOG"
fi
: > "$OUTPUT_FILE"
echo "GOVERNANCE_DENIED trace_id=$TRACE_ID reason=production_trust_root_unavailable" >&2
exit 2
fi
printf '%s\n' "$TRUST_JSON" >> "$TRUST_LOG"
if ! bash "$SCRIPT_DIR/sign-audit-head.sh" "$AUDIT_LOG" >/dev/null 2>&1; then
: > "$OUTPUT_FILE"
echo "GOVERNANCE_DENIED trace_id=$TRACE_ID reason=external_signing_failed" >&2
exit 2
fi
if ! bash "$SCRIPT_DIR/audit-ship-s3.sh" "$AUDIT_DIR/audit-head.txt" >/dev/null 2>&1; then
: > "$OUTPUT_FILE"
echo "GOVERNANCE_DENIED trace_id=$TRACE_ID reason=external_immutable_anchor_failed" >&2
exit 2
fi
else
if [[ "$PRODUCTION_PROFILE" == "1" ]]; then
printf '{"schema_version":"1.0.0","profile":"production","ready":false,"certifiable":false,"emergency_override":true,"severity":"critical","reason_codes":["emergency_local_trust_override_active"]}\n' >> "$TRUST_LOG"
echo "CRITICAL: emergency local trust override active; execution cannot be production-certified" >&2
fi
if command -v openssl >/dev/null 2>&1; then
# Private signing key lives OFF-REPO (default ~/.casan/audit-keys); only the
# public key is committed. Production: replace with KMS/HSM.
@@ -207,7 +275,7 @@ if command -v openssl >/dev/null 2>&1; then
AUDIT_PUB="$PUB_DIR/audit-public.pem"
mkdir -p "$PUB_DIR" "$PRIV_DIR"
if [[ ! -f "$AUDIT_PRIV" ]]; then
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_VERIFY_STRICT:-}" == "1" ]]; then
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" || "${CASAN_VERIFY_STRICT:-}" == "1" ]]; then
# SEC-02 (H-02): in enforced mode NEVER auto-generate a local signing key.
# A freshly-minted key next to the data lets any file-writer re-sign a forged
# head. Prod must provision the key out-of-band (KMS/HSM — see sign-audit-head.sh
@@ -229,6 +297,7 @@ if command -v openssl >/dev/null 2>&1; then
openssl dgst -sha256 -sign "$AUDIT_PRIV" -out "$AUDIT_DIR/audit-head.sig" "$AUDIT_DIR/audit-head.txt" 2>/dev/null || true
fi
fi
fi
if [[ "$DECISION" != "approved" ]]; then
: > "$OUTPUT_FILE"
@@ -21,22 +21,22 @@ while IFS= read -r raw || [[ -n "$raw" ]]; do
[[ "$line" =~ ^([A-Z0-9_]+)=(.*)$ ]] || fail "invalid_env_syntax"
key="${BASH_REMATCH[1]}"; value="${BASH_REMATCH[2]}"
case "$key" in
CASAN_PUBLIC_FQDN|CASAN_CP_HTTPS_PORT|CASAN_CP_TLS_DIR|CASAN_CP_OAUTH_ENV|CASAN_CP_RUNTIME_ENV|CASAN_CP_VAULT_ENV|CASAN_CP_STATE_DIR|CASAN_CP_OUTPUT_DIR|CASAN_CP_API_IMAGE|CASAN_CP_UI_IMAGE|CASAN_S3_BUCKET|CASAN_S3_PREFIX|CASAN_S3_REGION|CASAN_S3_RETENTION_DAYS|CASAN_S3_KMS_KEY_ID) export "$key=$value" ;;
CASAN_PUBLIC_FQDN|CASAN_CP_HTTPS_PORT|CASAN_CP_TLS_DIR|CASAN_CP_OAUTH_ENV|CASAN_CP_RUNTIME_ENV|CASAN_CP_VAULT_ENV|CASAN_CP_STATE_DIR|CASAN_CP_OUTPUT_DIR|CASAN_CP_IDP_PUBLIC_KEY|CASAN_CP_API_IMAGE|CASAN_CP_UI_IMAGE|CASAN_CP_OAUTH2_PROXY_IMAGE|CASAN_S3_BUCKET|CASAN_S3_PREFIX|CASAN_S3_REGION|CASAN_S3_RETENTION_DAYS|CASAN_S3_KMS_KEY_ID) export "$key=$value" ;;
*) fail "unexpected_env_key key=$key" ;;
esac
done < "$ENV_FILE"
required=(CASAN_PUBLIC_FQDN CASAN_CP_TLS_DIR CASAN_CP_OAUTH_ENV CASAN_CP_RUNTIME_ENV CASAN_CP_VAULT_ENV CASAN_CP_STATE_DIR CASAN_CP_OUTPUT_DIR CASAN_CP_API_IMAGE CASAN_CP_UI_IMAGE CASAN_S3_BUCKET CASAN_S3_REGION CASAN_S3_KMS_KEY_ID)
required=(CASAN_PUBLIC_FQDN CASAN_CP_TLS_DIR CASAN_CP_OAUTH_ENV CASAN_CP_RUNTIME_ENV CASAN_CP_VAULT_ENV CASAN_CP_STATE_DIR CASAN_CP_OUTPUT_DIR CASAN_CP_IDP_PUBLIC_KEY CASAN_CP_API_IMAGE CASAN_CP_UI_IMAGE CASAN_CP_OAUTH2_PROXY_IMAGE CASAN_S3_BUCKET CASAN_S3_REGION CASAN_S3_KMS_KEY_ID)
for key in "${required[@]}"; do [[ -n "${!key:-}" ]] || fail "missing_env key=$key"; done
case "$CASAN_PUBLIC_FQDN" in *localhost*|*127.0.0.1*|*example.com*|*replace-with*|*/*|[0-9]* ) fail "invalid_fqdn";; esac
[[ "$CASAN_PUBLIC_FQDN" == *.* ]] || fail "fqdn_required"
for image in "$CASAN_CP_API_IMAGE" "$CASAN_CP_UI_IMAGE"; do
for image in "$CASAN_CP_API_IMAGE" "$CASAN_CP_UI_IMAGE" "$CASAN_CP_OAUTH2_PROXY_IMAGE"; do
[[ "$image" =~ @sha256:[a-f0-9]{64}$ ]] || fail "image_must_be_digest_pinned image=$image"
done
pass "public FQDN and images are production-safe"
for file in "$CASAN_CP_TLS_DIR/tls.crt" "$CASAN_CP_TLS_DIR/tls.key" "$CASAN_CP_OAUTH_ENV" "$CASAN_CP_RUNTIME_ENV" "$CASAN_CP_VAULT_ENV"; do
for file in "$CASAN_CP_TLS_DIR/tls.crt" "$CASAN_CP_TLS_DIR/tls.key" "$CASAN_CP_IDP_PUBLIC_KEY" "$CASAN_CP_OAUTH_ENV" "$CASAN_CP_RUNTIME_ENV" "$CASAN_CP_VAULT_ENV"; do
[[ -s "$file" ]] || fail "missing_or_empty path=$file"
done
openssl x509 -in "$CASAN_CP_TLS_DIR/tls.crt" -noout >/dev/null || fail "invalid_tls_certificate"
@@ -45,6 +45,7 @@ openssl x509 -in "$CASAN_CP_TLS_DIR/tls.crt" -noout -checkhost "$CASAN_PUBLIC_FQ
cert_pub="$(openssl x509 -in "$CASAN_CP_TLS_DIR/tls.crt" -pubkey -noout | openssl pkey -pubin -outform DER | openssl dgst -sha256 | awk '{print $NF}')"
key_pub="$(openssl pkey -in "$CASAN_CP_TLS_DIR/tls.key" -pubout -outform DER | openssl dgst -sha256 | awk '{print $NF}')"
[[ "$cert_pub" == "$key_pub" ]] || fail "tls_key_does_not_match_certificate"
openssl rsa -pubin -in "$CASAN_CP_IDP_PUBLIC_KEY" -noout -modulus >/dev/null 2>&1 || fail "invalid_idp_rsa_public_key"
pass "TLS certificate is valid for at least 30 days"
value_of() { sed -n -E "s/^${1}=//p" "$CASAN_CP_OAUTH_ENV" | tail -1; }
@@ -56,8 +57,24 @@ done
[[ "$(value_of OAUTH2_PROXY_OIDC_ISSUER_URL)" == https://* ]] || fail "oidc_issuer_https_required"
[[ "$(value_of OAUTH2_PROXY_REDIRECT_URL)" == "https://$CASAN_PUBLIC_FQDN/oauth2/callback" ]] || fail "oidc_redirect_mismatch"
[[ "$(value_of OAUTH2_PROXY_COOKIE_SECURE)" == true ]] || fail "oidc_secure_cookie_required"
[[ "$(value_of OAUTH2_PROXY_SET_XAUTHREQUEST)" == true ]] || fail "oidc_xauthrequest_required"
[[ "$(value_of OAUTH2_PROXY_PASS_ACCESS_TOKEN)" == true ]] || fail "oidc_access_token_forwarding_required"
[[ "$(value_of OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER)" == true ]] || fail "oidc_authorization_header_forwarding_required"
pass "enterprise OIDC configuration"
runtime_value_of() { sed -n -E "s/^${1}=//p" "$CASAN_CP_RUNTIME_ENV" | tail -1; }
[[ "$(runtime_value_of CASAN_PROFILE)" == prod ]] || fail "runtime_profile_must_be_prod"
[[ "$(runtime_value_of CASAN_CP_AUTH_MODE)" == jwt ]] || fail "runtime_jwt_auth_required"
[[ "$(runtime_value_of CASAN_CP_JWT_ISSUER)" == "$(value_of OAUTH2_PROXY_OIDC_ISSUER_URL)" ]] || fail "runtime_oidc_issuer_mismatch"
[[ "$(runtime_value_of CASAN_CP_JWT_AUDIENCE)" == "$(value_of OAUTH2_PROXY_CLIENT_ID)" ]] || fail "runtime_oidc_audience_mismatch"
[[ "$(runtime_value_of CASAN_CP_JWT_PUBLIC_KEY_FILE)" == /run/casan-idp/idp-public.pem ]] || fail "runtime_idp_public_key_path_invalid"
[[ "$(runtime_value_of CASAN_SIGNING_PROVIDER)" == vault_kms ]] || fail "runtime_external_signing_required"
[[ "$(runtime_value_of CASAN_IMMUTABLE_ANCHOR_PROVIDER)" == s3_object_lock ]] || fail "runtime_immutable_anchor_required"
[[ -z "$(runtime_value_of CASAN_CP_TRUST_AUTH_PROXY)" ]] || fail "legacy_trusted_header_auth_forbidden"
clock_skew="$(runtime_value_of CASAN_CP_JWT_CLOCK_SKEW_SECONDS)"
[[ "$clock_skew" =~ ^[0-9]+$ && "$clock_skew" -le 300 ]] || fail "runtime_jwt_clock_skew_invalid"
pass "Control Plane verifies OIDC token identity cryptographically"
vault_addr="$(sed -n -E 's/^VAULT_ADDR=//p' "$CASAN_CP_VAULT_ENV" | tail -1)"
vault_token="$(sed -n -E 's/^VAULT_TOKEN=//p' "$CASAN_CP_VAULT_ENV" | tail -1)"
vault_cacert="$(sed -n -E 's/^VAULT_CACERT=//p' "$CASAN_CP_VAULT_ENV" | tail -1)"
@@ -88,7 +105,8 @@ COMPOSE="$ROOT/docker-compose.control-panel.yml"
CASAN_CP_TLS_DIR="$CASAN_CP_TLS_DIR" CASAN_CP_OAUTH_ENV="$CASAN_CP_OAUTH_ENV" \
CASAN_CP_RUNTIME_ENV="$CASAN_CP_RUNTIME_ENV" CASAN_CP_VAULT_ENV="$CASAN_CP_VAULT_ENV" \
CASAN_CP_STATE_DIR="$CASAN_CP_STATE_DIR" CASAN_CP_OUTPUT_DIR="$CASAN_CP_OUTPUT_DIR" \
CASAN_CP_API_IMAGE="$CASAN_CP_API_IMAGE" CASAN_CP_UI_IMAGE="$CASAN_CP_UI_IMAGE" \
CASAN_CP_IDP_PUBLIC_KEY="$CASAN_CP_IDP_PUBLIC_KEY" CASAN_CP_API_IMAGE="$CASAN_CP_API_IMAGE" \
CASAN_CP_UI_IMAGE="$CASAN_CP_UI_IMAGE" CASAN_CP_OAUTH2_PROXY_IMAGE="$CASAN_CP_OAUTH2_PROXY_IMAGE" \
docker compose -f "$COMPOSE" config >/dev/null || fail "compose_config_invalid"
pass "production compose config"
@@ -47,10 +47,15 @@ docker info >/dev/null 2>&1 || { echo "SANDBOX_CONTAINER_DOCKER_DOWN" >&2; exit
# rootful Docker daemon because a compromised daemon socket defeats container
# isolation. Local developer/test profiles may use a rootful daemon, but cannot
# claim that configuration as a hardened production runner.
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_SANDBOX_REQUIRE_ROOTLESS:-0}" == "1" ]]; then
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" \
|| "${CASAN_SANDBOX_REQUIRE_ROOTLESS:-0}" == "1" ]]; then
docker info --format '{{json .SecurityOptions}}' 2>/dev/null | grep -q 'rootless' \
|| { echo "SANDBOX_CONTAINER_ROOTLESS_REQUIRED" >&2; exit 2; }
fi
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ]]; then
[[ "$IMAGE" =~ @sha256:[a-f0-9]{64}$ ]] \
|| { echo "SANDBOX_CONTAINER_IMAGE_DIGEST_REQUIRED image=$IMAGE" >&2; exit 2; }
fi
WS_ABS="$(cd "$WORKSPACE" 2>/dev/null && pwd)" || { echo "SANDBOX_CONTAINER_BAD_WORKSPACE" >&2; exit 2; }
@@ -36,6 +36,37 @@ CPU_SECONDS="${CASAN_SANDBOX_CPU_SECONDS:-30}"
# check is the real gate; a container --pids-limit is the production backstop.
MAX_PROCS="${CASAN_SANDBOX_MAX_PROCS:-}"
TIMEOUT="${CASAN_SANDBOX_TIMEOUT:-30}"
SANDBOX_MODE="${CASAN_SANDBOX_MODE:-}"
STRICT_SANDBOX=0
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" \
|| "${CASAN_ENFORCEMENT_MODE:-}" == "enforce" || "${CASAN_SANDBOX_STRICT:-0}" == "1" ]]; then
STRICT_SANDBOX=1
fi
[[ -n "$SANDBOX_MODE" ]] || { if [[ "$STRICT_SANDBOX" == "1" ]]; then SANDBOX_MODE="container"; else SANDBOX_MODE="static"; fi; }
record_sandbox() { # decision reason backend capability-json
local decision="$1" reason="$2" backend="$3" capabilities="$4"
local log="$CASAN_STATE_ROOT/logs/sandbox/decisions.jsonl"
mkdir -p "$(dirname "$log")"
CASAN_SANDBOX_CAPABILITIES="$capabilities" python3 - "$log" "$decision" "$reason" "$backend" "${CASAN_EXECUTION_ID:-sandbox-$$}" <<'PY'
import json, os, sys
path, decision, reason, backend, execution_id = sys.argv[1:]
try:
capabilities = json.loads(os.environ.get("CASAN_SANDBOX_CAPABILITIES", "{}"))
except ValueError:
capabilities = {}
record = {
"schema_version": "1.0.0", "category": "runtime_control",
"policy_id": "casan.sandbox.backend", "decision": decision,
"reason_code": reason, "backend": backend, "execution_id": execution_id,
"capabilities": capabilities,
}
with open(path, "a", encoding="utf-8") as handle:
handle.write(json.dumps(record, sort_keys=True, separators=(",", ":")) + "\n")
handle.flush()
os.fsync(handle.fileno())
PY
}
while [[ "$#" -gt 0 ]]; do
case "$1" in
@@ -54,12 +85,35 @@ if [[ "$#" -eq 0 ]]; then
exit 64
fi
# C6 production form: CASAN_SANDBOX_MODE=container runs under TRUE kernel
# C6 production form: CASAN_SANDBOX_MODE=container runs under kernel-backed
# isolation (sandbox-container.sh: --network=none --read-only --pids-limit …).
# Default stays the static-policy + ulimit scaffold so existing behaviour is
# unchanged. Falls back to the scaffold if Docker is unavailable.
if [[ "${CASAN_SANDBOX_MODE:-static}" == "container" ]] && command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
exec "$SCRIPT_DIR/sandbox-container.sh" --workspace "$WORKSPACE" --timeout "$TIMEOUT" -- "$@"
# A requested/required container backend never silently falls back.
if [[ "$SANDBOX_MODE" == "container" ]]; then
if [[ "${CASAN_SANDBOX_TEST_FORCE_UNAVAILABLE:-0}" != "1" ]] \
&& command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
record_sandbox "allow" "sandbox_container_selected" "docker" '{"network_disabled":true,"read_only_root":true,"workspace_write_restricted":true,"environment_filtered":true,"non_root":true,"resource_limits":true}'
exec "$SCRIPT_DIR/sandbox-container.sh" --workspace "$WORKSPACE" --timeout "$TIMEOUT" -- "$@"
fi
if [[ "$STRICT_SANDBOX" == "1" ]]; then
record_sandbox "deny" "sandbox_isolation_backend_unavailable" "none" '{"timeout_only":false}'
echo "SANDBOX_ISOLATION_REQUIRED backend=container reason=unavailable" >&2
exit 2
fi
if [[ "${CASAN_SANDBOX_ALLOW_STATIC_FALLBACK:-0}" != "1" ]]; then
record_sandbox "deny" "sandbox_fallback_not_approved" "none" '{}'
echo "SANDBOX_FALLBACK_REQUIRES_EXPLICIT_DEVELOPMENT_APPROVAL" >&2
exit 2
fi
record_sandbox "observe_only" "sandbox_static_fallback_development_only" "static_rlimit" '{"network_disabled":false,"read_only_root":false,"workspace_write_restricted":false,"environment_filtered":false,"non_root":false,"resource_limits":true}'
echo "HIGH: container sandbox unavailable; explicit development static fallback is not production isolation" >&2
elif [[ "$SANDBOX_MODE" != "static" ]]; then
record_sandbox "deny" "sandbox_backend_unknown" "$SANDBOX_MODE" '{}'
echo "SANDBOX_BACKEND_UNKNOWN mode=$SANDBOX_MODE" >&2
exit 2
elif [[ "$STRICT_SANDBOX" == "1" ]]; then
record_sandbox "deny" "sandbox_static_forbidden_in_enforce_mode" "static_rlimit" '{"network_disabled":false,"read_only_root":false}'
echo "SANDBOX_STATIC_FORBIDDEN_IN_ENFORCE_MODE" >&2
exit 2
fi
CMD_STR="$*"
@@ -96,6 +150,7 @@ done < <(printf '%s\n' "$CMD_STR" | grep -oE '>>?[[:space:]]*[^[:space:];|&]+' |
# ── 2. Runtime rlimits + wall-clock timeout ─────────────────────────────────
casan_log debug sandbox "SANDBOX_RUN workspace=$WS_ABS file_kb=$MAX_FILE_KB cpu=$CPU_SECONDS procs=$MAX_PROCS timeout=$TIMEOUT"
record_sandbox "allow" "sandbox_static_policy_selected" "static_rlimit" '{"network_disabled":false,"read_only_root":false,"workspace_write_restricted":false,"environment_filtered":false,"non_root":false,"resource_limits":true}'
(
ulimit -f "$((MAX_FILE_KB * 2))" 2>/dev/null || true # ulimit -f is in 512-byte blocks
ulimit -t "$CPU_SECONDS" 2>/dev/null || true
@@ -45,14 +45,24 @@ with open(path, encoding="utf-8") as f:
if not line.strip():
continue
record = json.loads(line)
core = "|".join([
record.get("timestamp",""), record.get("trace_id",""),
record.get("action",""), record.get("actor",""),
record.get("risk_level",""), record.get("decision",""),
record.get("approval_status",""), record.get("approver",""),
record.get("input_hash",""), record.get("output_hash",""),
previous,
])
if int(record.get("schema_version", 1)) >= 2:
core = "|".join([
record.get("timestamp",""), record.get("trace_id",""),
record.get("action",""), record.get("action_class",""),
record.get("actor",""), record.get("risk_level",""),
json.dumps(record.get("risk_factors", {}), sort_keys=True, separators=(",", ":")),
record.get("evidence_requirement",""), record.get("decision",""),
record.get("approval_status",""), record.get("approver",""),
record.get("input_hash",""), record.get("output_hash",""), previous,
])
else:
core = "|".join([
record.get("timestamp",""), record.get("trace_id",""),
record.get("action",""), record.get("actor",""),
record.get("risk_level",""), record.get("decision",""),
record.get("approval_status",""), record.get("approver",""),
record.get("input_hash",""), record.get("output_hash",""), previous,
])
previous = hashlib.sha256(core.encode()).hexdigest()
print(previous)
PY
@@ -68,6 +78,10 @@ printf '%s' "$HEAD_HASH" > "$HEAD_FILE"
# ── Sign the head file ────────────────────────────────────────────────────
VAULT_KMS="$SCRIPT_DIR/vault-kms.sh"
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ]]; then
[[ "${VAULT_ADDR:-}" == https://* ]] || { echo "SIGN_AUDIT_HEAD_FAIL reason=vault_https_required_in_prod" >&2; exit 1; }
fi
if [[ -n "${VAULT_ADDR:-}" && -n "${VAULT_TOKEN:-}" ]] && \
curl -sf "$VAULT_ADDR/v1/sys/health" >/dev/null 2>&1; then
# KMS path — sign via Vault Transit, export public key
@@ -103,7 +117,7 @@ PY
fi
else
# Fallback — local key (dev environment without Vault)
if [[ "${CASAN_PROFILE:-}" == "prod" ]]; then
if [[ "${CASAN_PROFILE:-}" == "prod" || "${CASAN_PROFILE:-}" == "production" || "${CASAN_PROFILE:-}" == "strict" ]]; then
echo "SIGN_AUDIT_HEAD_FAIL reason=vault_kms_required_in_prod" >&2
exit 1
fi
@@ -35,21 +35,28 @@ with open(path, encoding="utf-8") as f:
f"AUDIT_CHAIN_BROKEN line={line_no} expected_previous={previous} actual_previous={expected_previous}"
)
core = "|".join(
[
record.get("timestamp", ""),
record.get("trace_id", ""),
record.get("action", ""),
record.get("actor", ""),
record.get("risk_level", ""),
record.get("decision", ""),
record.get("approval_status", ""),
record.get("approver", ""),
record.get("input_hash", ""),
record.get("output_hash", ""),
if int(record.get("schema_version", 1)) >= 2:
core = "|".join([
record.get("timestamp", ""), record.get("trace_id", ""),
record.get("action", ""), record.get("action_class", ""),
record.get("actor", ""), record.get("risk_level", ""),
json.dumps(record.get("risk_factors", {}), sort_keys=True, separators=(",", ":")),
record.get("evidence_requirement", ""), record.get("decision", ""),
record.get("approval_status", ""), record.get("approver", ""),
record.get("input_hash", ""), record.get("output_hash", ""),
expected_previous,
]
)
])
else:
core = "|".join(
[
record.get("timestamp", ""), record.get("trace_id", ""),
record.get("action", ""), record.get("actor", ""),
record.get("risk_level", ""), record.get("decision", ""),
record.get("approval_status", ""), record.get("approver", ""),
record.get("input_hash", ""), record.get("output_hash", ""),
expected_previous,
]
)
expected_hash = hashlib.sha256(core.encode()).hexdigest()
actual_hash = record.get("record_hash", "")
if expected_hash != actual_hash: