feat(casan): establish assurance kernel and harden control plane
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user