diff --git a/crates/proofpath-verifier/src/bin/proofpath-scig.rs b/crates/proofpath-verifier/src/bin/proofpath-scig.rs new file mode 100644 index 0000000..ebc60da --- /dev/null +++ b/crates/proofpath-verifier/src/bin/proofpath-scig.rs @@ -0,0 +1,476 @@ +use serde::Deserialize; +use std::{env, fs, process}; + +const INVARIANT_RESULTS: &[&str] = &["held", "violated", "unknown"]; +const LIFECYCLE_RESULTS: &[&str] = &["passed", "failed", "unknown"]; +const CAUSAL_TYPES: &[&str] = &[ + "enabled_by", + "required", + "triggered", + "bypassed", + "failed_to_prevent", + "amplified", + "masked", + "recovered_by", + "verified_by", +]; + +#[derive(Debug, Deserialize)] +struct ScigDocument { + schema_version: String, + incident_id: String, + actor: Entity, + action: Entity, + pre_state: Entity, + control: Control, + transition: Transition, + post_state: Entity, + invariants: Vec, + cause: Vec, + containment: Lifecycle, + recovery: Lifecycle, + verification: Verification, + evidence: Vec, +} + +#[derive(Debug, Deserialize)] +struct Entity { + id: String, + #[serde(rename = "type")] + kind: Option, +} + +#[derive(Debug, Deserialize)] +struct Control { + id: String, + #[serde(rename = "type")] + kind: Option, + expected_outcome: String, +} + +#[derive(Debug, Deserialize)] +struct Transition { + id: String, + from: String, + action: String, + to: String, + phase: String, + observed_at: String, +} + +#[derive(Debug, Deserialize)] +struct Invariant { + id: String, + description: String, + result: String, + evidence_reference: Option, +} + +#[derive(Debug, Deserialize)] +struct CausalEdge { + #[serde(rename = "type")] + kind: String, + source: String, + target: String, + evidence_reference: Option, +} + +#[derive(Debug, Deserialize)] +struct Lifecycle { + action: String, + result: String, + target_state: Option, + evidence_reference: Option, +} + +#[derive(Debug, Deserialize)] +struct Verification { + test_id: String, + expected: String, + observed: String, + result: String, + evidence_reference: Option, +} + +#[derive(Debug, Deserialize)] +struct Evidence { + id: String, + #[serde(rename = "type")] + kind: String, +} + +#[derive(Debug)] +struct ValidationReport { + errors: Vec, +} + +impl ValidationReport { + fn new() -> Self { + Self { errors: Vec::new() } + } + + fn require(&mut self, condition: bool, message: impl Into) { + if !condition { + self.errors.push(message.into()); + } + } + + fn is_valid(&self) -> bool { + self.errors.is_empty() + } +} + +fn non_empty(value: &str) -> bool { + !value.trim().is_empty() +} + +fn evidence_exists(doc: &ScigDocument, id: &str) -> bool { + doc.evidence.iter().any(|evidence| evidence.id == id) +} + +fn validate_identity_and_states(doc: &ScigDocument, report: &mut ValidationReport) { + report.require(doc.schema_version == "0.1", "schema_version must equal 0.1"); + report.require(non_empty(&doc.incident_id), "incident_id must not be empty"); + report.require(non_empty(&doc.actor.id), "actor.id must not be empty"); + report.require(non_empty(&doc.action.id), "action.id must not be empty"); + report.require( + non_empty(&doc.pre_state.id), + "pre_state.id must not be empty", + ); + report.require( + non_empty(&doc.post_state.id), + "post_state.id must not be empty", + ); + report.require(non_empty(&doc.control.id), "control.id must not be empty"); + report.require( + non_empty(&doc.control.expected_outcome), + "control.expected_outcome must not be empty", + ); +} + +fn validate_transition(doc: &ScigDocument, report: &mut ValidationReport) { + report.require( + non_empty(&doc.transition.id), + "transition.id must not be empty", + ); + report.require( + doc.transition.from == doc.pre_state.id, + "transition.from must reference pre_state.id", + ); + report.require( + doc.transition.to == doc.post_state.id, + "transition.to must reference post_state.id", + ); + report.require( + doc.transition.action == doc.action.id, + "transition.action must reference action.id", + ); + report.require( + non_empty(&doc.transition.phase), + "transition.phase must not be empty", + ); + report.require( + doc.transition.observed_at.contains('T') && doc.transition.observed_at.ends_with('Z'), + "transition.observed_at must be an RFC3339-like UTC timestamp", + ); +} + +fn validate_invariants(doc: &ScigDocument, report: &mut ValidationReport) { + report.require( + !doc.invariants.is_empty(), + "at least one invariant is required", + ); + for invariant in &doc.invariants { + report.require(non_empty(&invariant.id), "invariant.id must not be empty"); + report.require( + non_empty(&invariant.description), + format!("{} description must not be empty", invariant.id), + ); + report.require( + INVARIANT_RESULTS.contains(&invariant.result.as_str()), + format!("{} has invalid invariant result", invariant.id), + ); + if let Some(reference) = &invariant.evidence_reference { + report.require( + evidence_exists(doc, reference), + format!("{} references missing evidence {reference}", invariant.id), + ); + } + } +} + +fn validate_causal_edges(doc: &ScigDocument, report: &mut ValidationReport) { + for edge in &doc.cause { + report.require( + CAUSAL_TYPES.contains(&edge.kind.as_str()), + format!( + "causal edge {} -> {} has invalid type {}", + edge.source, edge.target, edge.kind + ), + ); + report.require( + non_empty(&edge.source), + "causal edge source must not be empty", + ); + report.require( + non_empty(&edge.target), + "causal edge target must not be empty", + ); + if let Some(reference) = &edge.evidence_reference { + report.require( + evidence_exists(doc, reference), + format!("causal edge references missing evidence {reference}"), + ); + } + } +} + +fn validate_lifecycle( + doc: &ScigDocument, + lifecycle: &Lifecycle, + label: &str, + report: &mut ValidationReport, +) { + report.require( + non_empty(&lifecycle.action), + format!("{label}.action must not be empty"), + ); + report.require( + LIFECYCLE_RESULTS.contains(&lifecycle.result.as_str()), + format!("{label}.result must be passed, failed, or unknown"), + ); + if let Some(reference) = &lifecycle.evidence_reference { + report.require( + evidence_exists(doc, reference), + format!("{label} references missing evidence {reference}"), + ); + } +} + +fn validate_verification(doc: &ScigDocument, report: &mut ValidationReport) { + report.require( + LIFECYCLE_RESULTS.contains(&doc.verification.result.as_str()), + "verification.result must be passed, failed, or unknown", + ); + report.require( + non_empty(&doc.verification.test_id), + "verification.test_id must not be empty", + ); + report.require( + non_empty(&doc.verification.expected), + "verification.expected must not be empty", + ); + report.require( + non_empty(&doc.verification.observed), + "verification.observed must not be empty", + ); + if let Some(reference) = &doc.verification.evidence_reference { + report.require( + evidence_exists(doc, reference), + format!("verification references missing evidence {reference}"), + ); + } + if doc.verification.result == "passed" { + report.require( + doc.recovery.result == "passed", + "verification cannot pass unless recovery passed", + ); + report.require( + doc.verification.expected == doc.verification.observed, + "verification passed but expected and observed differ", + ); + } +} + +fn validate_evidence(doc: &ScigDocument, report: &mut ValidationReport) { + report.require( + !doc.evidence.is_empty(), + "at least one evidence object is required", + ); + for evidence in &doc.evidence { + report.require(non_empty(&evidence.id), "evidence.id must not be empty"); + report.require( + non_empty(&evidence.kind), + format!("evidence {} type must not be empty", evidence.id), + ); + } +} + +fn validate(doc: &ScigDocument) -> ValidationReport { + let mut report = ValidationReport::new(); + validate_identity_and_states(doc, &mut report); + validate_transition(doc, &mut report); + validate_invariants(doc, &mut report); + validate_causal_edges(doc, &mut report); + validate_lifecycle(doc, &doc.containment, "containment", &mut report); + validate_lifecycle(doc, &doc.recovery, "recovery", &mut report); + validate_verification(doc, &mut report); + validate_evidence(doc, &mut report); + + let _ = ( + &doc.actor.kind, + &doc.action.kind, + &doc.pre_state.kind, + &doc.post_state.kind, + &doc.control.kind, + &doc.containment.target_state, + &doc.recovery.target_state, + ); + + report +} + +fn lifecycle_label(result: &str) -> &'static str { + match result { + "passed" => "PASSED", + "failed" => "FAILED", + _ => "UNKNOWN", + } +} + +fn invariant_label(result: &str) -> &'static str { + match result { + "held" => "HELD", + "violated" => "VIOLATED", + _ => "UNKNOWN", + } +} + +fn print_report(doc: &ScigDocument, report: &ValidationReport) { + println!("SCIG {}", doc.incident_id); + for invariant in &doc.invariants { + println!( + "{:<18} {}", + invariant.id, + invariant_label(&invariant.result) + ); + } + println!( + "{:<18} {}", + "CONTAINMENT", + lifecycle_label(&doc.containment.result) + ); + println!( + "{:<18} {}", + "RECOVERY", + lifecycle_label(&doc.recovery.result) + ); + println!( + "{:<18} {}", + "VERIFICATION", + lifecycle_label(&doc.verification.result) + ); + println!( + "{:<18} {}", + "RESULT", + if report.is_valid() { + "VALID" + } else { + "INVALID" + } + ); + + if !report.is_valid() { + eprintln!("\nValidation errors:"); + for error in &report.errors { + eprintln!("- {error}"); + } + } +} + +fn run(path: &str) -> Result { + let raw = fs::read_to_string(path).map_err(|error| format!("cannot read {path}: {error}"))?; + let document: ScigDocument = + serde_json::from_str(&raw).map_err(|error| format!("invalid SCIG JSON: {error}"))?; + let report = validate(&document); + print_report(&document, &report); + Ok(report.is_valid()) +} + +fn main() { + let mut args = env::args(); + let program = args.next().unwrap_or_else(|| "proofpath-scig".to_string()); + let Some(path) = args.next() else { + eprintln!("usage: {program} "); + process::exit(2); + }; + + match run(&path) { + Ok(true) => {} + Ok(false) => process::exit(1), + Err(error) => { + eprintln!("{error}"); + process::exit(2); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn valid_document() -> ScigDocument { + serde_json::from_str( + r#"{ + "schema_version":"0.1", + "incident_id":"TEST-1", + "actor":{"id":"agent","type":"ai_agent"}, + "action":{"id":"action","type":"tool_call"}, + "pre_state":{"id":"before"}, + "control":{"id":"policy","type":"capability_policy","expected_outcome":"deny"}, + "transition":{"id":"t1","from":"before","action":"action","to":"after","phase":"execution","observed_at":"2026-08-10T11:42:31Z"}, + "post_state":{"id":"after"}, + "invariants":[{"id":"INV-1","description":"must hold","result":"held","evidence_reference":"ev1"}], + "cause":[{"type":"verified_by","source":"action","target":"VERIFY-1","evidence_reference":"ev1"}], + "containment":{"action":"block","result":"passed","evidence_reference":"ev1"}, + "recovery":{"action":"restore","result":"passed","evidence_reference":"ev1"}, + "verification":{"test_id":"VERIFY-1","expected":"denied","observed":"denied","result":"passed","evidence_reference":"ev1"}, + "evidence":[{"id":"ev1","type":"test_result"}] + }"#, + ) + .expect("fixture should deserialize") + } + + #[test] + fn accepts_valid_document() { + let doc = valid_document(); + let report = validate(&doc); + assert!(report.is_valid(), "{:?}", report.errors); + } + + #[test] + fn rejects_broken_transition_reference() { + let mut doc = valid_document(); + doc.transition.to = "wrong-state".to_string(); + let report = validate(&doc); + assert!(!report.is_valid()); + assert!(report + .errors + .iter() + .any(|error| error.contains("transition.to"))); + } + + #[test] + fn rejects_passing_verification_without_recovery() { + let mut doc = valid_document(); + doc.recovery.result = "failed".to_string(); + let report = validate(&doc); + assert!(!report.is_valid()); + assert!(report + .errors + .iter() + .any(|error| error.contains("recovery passed"))); + } + + #[test] + fn rejects_missing_evidence_reference() { + let mut doc = valid_document(); + doc.invariants[0].evidence_reference = Some("missing".to_string()); + let report = validate(&doc); + assert!(!report.is_valid()); + assert!(report + .errors + .iter() + .any(|error| error.contains("missing evidence"))); + } +} diff --git a/docs/safe-causal-incident-graph/README.md b/docs/safe-causal-incident-graph/README.md new file mode 100644 index 0000000..761b5d5 --- /dev/null +++ b/docs/safe-causal-incident-graph/README.md @@ -0,0 +1,44 @@ +# SCIG v0.1 Quickstart + +The SAFE Causal Incident Graph (SCIG) is ProofPath's causal, temporal, recovery, verification, and evidence model for AI incidents and near misses. + +## Artifacts + +- [`SAFE_CAUSAL_INCIDENT_GRAPH.md`](./SAFE_CAUSAL_INCIDENT_GRAPH.md) — normative v0.1 proposal. +- [`../../schemas/safe-causal-incident-graph-v0.1.schema.json`](../../schemas/safe-causal-incident-graph-v0.1.schema.json) — Draft 2020-12 JSON Schema. +- [`../../examples/safe-near-miss.json`](../../examples/safe-near-miss.json) — reference near-miss. +- `proofpath-scig` — Rust verifier bundled with `proofpath-verifier`. + +## Run + +```bash +cargo run -p proofpath-verifier --bin proofpath-scig -- examples/safe-near-miss.json +``` + +Expected summary: + +```text +SCIG SAFE-SCIG-2026-0001 +INV-CRED-001 VIOLATED +INV-NET-001 HELD +CONTAINMENT PASSED +RECOVERY PASSED +VERIFICATION PASSED +RESULT VALID +``` + +## Test + +```bash +cargo test -p proofpath-verifier --bin proofpath-scig +``` + +The verifier includes negative tests for broken state-transition references, missing evidence references, and verification that claims success without successful recovery. + +## Design boundary + +SCIG does not replace OpenTelemetry or SAFE-style exchange. It provides a causal interpretation layer over preserved evidence: + +```text +telemetry/evidence → causal state graph → failed control → recovery → verification proof +``` diff --git a/docs/safe-causal-incident-graph/SAFE_CAUSAL_INCIDENT_GRAPH.md b/docs/safe-causal-incident-graph/SAFE_CAUSAL_INCIDENT_GRAPH.md new file mode 100644 index 0000000..4b70401 --- /dev/null +++ b/docs/safe-causal-incident-graph/SAFE_CAUSAL_INCIDENT_GRAPH.md @@ -0,0 +1,329 @@ +# SAFE Causal Incident Graph (SCIG) + +**Version:** 0.1 +**Status:** Experimental proposal +**Project:** ProofPath + +SCIG is a machine-readable causal state-transition model for AI incidents and near misses. It complements incident-sharing formats and telemetry systems by connecting observed evidence to the question: **why was this action possible, which control failed, what contained the outcome, how was recovery performed, and how was the fix verified?** + +## Canonical model + +```text +actor +→ action +→ pre_state +→ control +→ transition +→ post_state +→ invariant +→ violation +→ cause +→ containment +→ recovery +→ verification +→ evidence +``` + +Every relevant object SHOULD carry or reference the following dimensions when available: + +```text +phase + time + provenance + trace_reference + evidence_reference +``` + +This is the ProofPath canonical safety chain: + +```text +state + causality + phase + transition + time + recovery + verification + evidence +``` + +## 1. Goals + +SCIG makes an incident or near miss: + +- reconstructable; +- causally explainable; +- queryable; +- reproducible; +- mechanically verifiable; +- traceable back to preserved evidence. + +SCIG does not replace OpenTelemetry or an incident-sharing format. Telemetry remains the observation transport; SCIG provides the causal and verification semantics over those observations. + +## 2. Core objects + +### Actor + +Entity capable of initiating or influencing an action, for example an AI agent, human, service, scheduler, tool, workflow, or model. + +### Action + +Attempted or completed operation such as a tool call, network request, credential use, filesystem write, command execution, delegation, or policy change. + +### Pre-state and post-state + +Observable system conditions before and after a transition. + +### Control + +A mechanism intended to constrain behaviour, such as authorization, sandboxing, capability policy, network filtering, approval gates, tool permissions, identity scope, or rate limits. + +A control SHOULD declare its expected defensive outcome. + +### Transition + +A first-class state change: + +```text +(pre_state, action, conditions) → post_state +``` + +Transitions SHOULD record `phase`, `observed_at`, and ordering information. + +### Invariant + +A safety property expected to remain true across a specified scope. + +Example: + +```text +A restricted agent MUST NOT establish an authenticated external network session. +``` + +Invariant result values in v0.1 are: + +- `held` +- `violated` +- `unknown` + +### Violation + +A violation exists when an observed state or transition contradicts an invariant. + +### Cause + +SCIG distinguishes temporal correlation from causal contribution. v0.1 causal edge vocabulary: + +- `enabled_by` +- `required` +- `triggered` +- `bypassed` +- `failed_to_prevent` +- `amplified` +- `masked` +- `recovered_by` +- `verified_by` + +### Containment + +Action or control that prevents an unsafe condition from expanding. Containment success does **not** imply remediation success. + +### Recovery + +Movement from unsafe/degraded state into an explicitly defined acceptable target state. + +### Verification + +Evidence-backed determination that remediation restored the intended safety property. + +Preferred form: + +```text +previous failing path +→ replay or equivalent test +→ unsafe transition no longer reachable +→ invariant holds +→ evidence preserved +``` + +### Evidence + +A reference to an authoritative artifact such as a trace/span, log, policy decision, configuration snapshot, file hash, network event, human approval, test result, or attestation. + +SCIG references source evidence rather than duplicating it. + +## 3. Phase model + +Suggested phase vocabulary: + +- `planning` +- `reasoning` +- `authorization` +- `execution` +- `observation` +- `verification` +- `containment` +- `recovery` + +Phase is important because an unsafe plan, unsafe authorization, and unsafe execution represent materially different control failures. + +## 4. Temporal and causal ordering + +Wall-clock time alone is insufficient for distributed systems. Implementations SHOULD preserve `observed_at` and MAY include: + +- sequence numbers; +- trace/span parentage; +- logical clocks; +- causal parent IDs. + +SCIG treats chronological order and causal order as related but distinct. + +## 5. OpenTelemetry mapping + +SCIG evidence and transitions MAY reference OpenTelemetry identifiers: + +```json +{ + "trace_reference": { + "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", + "span_id": "00f067aa0ba902b7" + } +} +``` + +This allows existing observability systems to retain evidence while SCIG adds causal interpretation. + +## 6. SAFE mapping + +SCIG is designed to complement SAFE-style incident and near-miss exchange: + +```text +preserved incident evidence + ↓ +SCIG causal reconstruction + ↓ +root cause / failed control + ↓ +reproducible test + ↓ +recovery + verification + ↓ +evidence-backed safety claim +``` + +SCIG focuses on: + +- how observations relate causally; +- which transition created the unsafe state; +- which control was expected to prevent it; +- which invariant held or failed; +- what contained the outcome; +- how recovery was performed; +- how remediation was independently verified. + +## 7. Query: Why was this action possible? + +A conforming implementation SHOULD be able to return an evidence-backed causal path for an action or invariant violation. + +Example: + +```text +Agent requested external access +↓ enabled_by +Credential appeared in tool output +↓ failed_to_prevent +Capability policy evaluated incomplete context +↓ +External request attempted +↓ +INV-CRED-001 violated +↓ recovered_by +Credential revoked +↓ verified_by +Replay test passed +``` + +A useful machine-oriented abstraction is: + +```text +PATH(invariant_violation ← caused_by* ← action) +``` + +## 8. Near-miss semantics + +A near miss SHOULD preserve failed primary controls and successful secondary controls separately. + +Example: + +```text +INV-CRED-001 = violated +INV-NET-001 = held +``` + +The correct conclusion is not merely "no breach occurred". The useful reusable finding is: + +```text +primary control failed + secondary containment succeeded +``` + +## 9. Verification rules + +SCIG v0.1 verifier checks: + +1. required top-level structure is present; +2. schema version is `0.1`; +3. at least one invariant exists; +4. at least one evidence object exists; +5. invariant results use the canonical vocabulary; +6. transition references pre/post states; +7. containment/recovery/verification results use explicit outcomes; +8. causal edges contain source, target, and causal type; +9. verification cannot be considered passed when recovery is absent or failed; +10. a report is emitted with invariant and lifecycle status. + +## 10. Minimal output + +```text +SCIG SAFE-SCIG-2026-0001 +INV-CRED-001 VIOLATED +INV-NET-001 HELD +CONTAINMENT PASSED +RECOVERY PASSED +VERIFICATION PASSED +RESULT VALID +``` + +## 11. Non-goals + +SCIG v0.1 does not define: + +- a replacement for OpenTelemetry; +- a universal telemetry transport; +- confidential disclosure governance; +- vulnerability severity scoring; +- a complete policy language; +- model chain-of-thought representation. + +Only externally observable and auditable system behaviour belongs in the graph. + +## 12. Security principle + +A safety claim without evidence is an assertion. +A control without verification is an assumption. +A recovered system without a reproducible test is an unverified state. + +Therefore: + +```text +STATE ++ CAUSE ++ TRANSITION ++ TIME ++ CONTROL ++ RECOVERY ++ VERIFICATION ++ EVIDENCE += VERIFIABLE SAFETY CLAIM +``` + +## 13. Reference artifacts + +- Schema: `schemas/safe-causal-incident-graph-v0.1.schema.json` +- Example: `examples/safe-near-miss.json` +- Verifier CLI: `proofpath-scig` + +Run from repository root: + +```bash +cargo run -p proofpath-verifier --bin proofpath-scig -- examples/safe-near-miss.json +cargo test -p proofpath-verifier --bin proofpath-scig +``` diff --git a/examples/safe-near-miss.json b/examples/safe-near-miss.json new file mode 100644 index 0000000..d801340 --- /dev/null +++ b/examples/safe-near-miss.json @@ -0,0 +1,139 @@ +{ + "schema_version": "0.1", + "incident_id": "SAFE-SCIG-2026-0001", + "actor": { + "id": "agent-17", + "type": "ai_agent", + "phase": "execution", + "provenance": "evaluation-run-2026-08-10" + }, + "action": { + "id": "action-42", + "type": "external_authenticated_request", + "phase": "execution" + }, + "pre_state": { + "id": "state-41", + "type": "restricted_execution", + "network_access": false, + "credential_scope": ["sandbox"] + }, + "control": { + "id": "control-capability-1", + "type": "capability_policy", + "expected_outcome": "Credentials outside the declared capability set cannot be used by the agent." + }, + "transition": { + "id": "transition-42", + "from": "state-41", + "action": "action-42", + "to": "state-42", + "phase": "execution", + "observed_at": "2026-08-10T11:42:31.219Z", + "sequence": 184, + "provenance": "gateway-observer", + "trace_reference": { + "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", + "span_id": "00f067aa0ba902b7" + } + }, + "post_state": { + "id": "state-42", + "type": "blocked_external_attempt", + "external_request_attempted": true, + "external_connection_established": false + }, + "invariants": [ + { + "id": "INV-CRED-001", + "description": "Credentials outside the agent declared capability set MUST NOT become usable by that agent.", + "result": "violated", + "evidence_reference": "evidence-tool-output" + }, + { + "id": "INV-NET-001", + "description": "Restricted agents MUST NOT establish authenticated external network sessions.", + "result": "held", + "evidence_reference": "evidence-network-block" + } + ], + "cause": [ + { + "type": "enabled_by", + "source": "credential-exposure", + "target": "action-42", + "evidence_reference": "evidence-tool-output" + }, + { + "type": "failed_to_prevent", + "source": "control-capability-1", + "target": "action-42", + "evidence_reference": "evidence-policy-decision" + }, + { + "type": "recovered_by", + "source": "action-42", + "target": "credential-revocation", + "evidence_reference": "evidence-recovery" + }, + { + "type": "verified_by", + "source": "credential-revocation", + "target": "VERIFY-CRED-001", + "evidence_reference": "evidence-verification" + } + ], + "containment": { + "action": "outbound-network-policy-block", + "result": "passed", + "target_state": "no-external-session", + "evidence_reference": "evidence-network-block" + }, + "recovery": { + "action": "credential-revocation-and-cache-purge", + "result": "passed", + "target_state": "credential-unusable", + "evidence_reference": "evidence-recovery" + }, + "verification": { + "test_id": "VERIFY-CRED-001", + "expected": "credential_use_denied_and_external_request_blocked", + "observed": "credential_use_denied_and_external_request_blocked", + "result": "passed", + "evidence_reference": "evidence-verification" + }, + "evidence": [ + { + "id": "evidence-tool-output", + "type": "tool_result", + "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "provenance": "tool-proxy" + }, + { + "id": "evidence-policy-decision", + "type": "policy_decision", + "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "provenance": "authorization-layer" + }, + { + "id": "evidence-network-block", + "type": "otel_span", + "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", + "span_id": "00f067aa0ba902b7", + "sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "provenance": "network-policy" + }, + { + "id": "evidence-recovery", + "type": "attestation", + "sha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "provenance": "recovery-runbook" + }, + { + "id": "evidence-verification", + "type": "test_result", + "sha256": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "provenance": "proofpath-verifier" + } + ] +} diff --git a/schemas/safe-causal-incident-graph-v0.1.schema.json b/schemas/safe-causal-incident-graph-v0.1.schema.json new file mode 100644 index 0000000..2179d69 --- /dev/null +++ b/schemas/safe-causal-incident-graph-v0.1.schema.json @@ -0,0 +1,172 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/safal207/ProofPath/schemas/safe-causal-incident-graph-v0.1.schema.json", + "title": "SAFE Causal Incident Graph v0.1", + "type": "object", + "additionalProperties": true, + "required": [ + "schema_version", + "incident_id", + "actor", + "action", + "pre_state", + "control", + "transition", + "post_state", + "invariants", + "cause", + "containment", + "recovery", + "verification", + "evidence" + ], + "properties": { + "schema_version": { "const": "0.1" }, + "incident_id": { "type": "string", "minLength": 1 }, + "actor": { "$ref": "#/$defs/entity" }, + "action": { "$ref": "#/$defs/entity" }, + "pre_state": { "$ref": "#/$defs/entity" }, + "control": { "$ref": "#/$defs/control" }, + "transition": { "$ref": "#/$defs/transition" }, + "post_state": { "$ref": "#/$defs/entity" }, + "invariants": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/invariant" } + }, + "cause": { + "type": "array", + "items": { "$ref": "#/$defs/causalEdge" } + }, + "containment": { "$ref": "#/$defs/lifecycleResult" }, + "recovery": { "$ref": "#/$defs/lifecycleResult" }, + "verification": { "$ref": "#/$defs/verification" }, + "evidence": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/evidence" } + } + }, + "$defs": { + "entity": { + "type": "object", + "required": ["id"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "type": { "type": "string" }, + "phase": { "type": "string" }, + "observed_at": { "type": "string", "format": "date-time" }, + "provenance": { "type": "string" } + }, + "additionalProperties": true + }, + "control": { + "allOf": [ + { "$ref": "#/$defs/entity" }, + { + "type": "object", + "required": ["expected_outcome"], + "properties": { + "expected_outcome": { "type": "string", "minLength": 1 } + } + } + ] + }, + "transition": { + "type": "object", + "required": ["id", "from", "action", "to", "phase", "observed_at"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "from": { "type": "string", "minLength": 1 }, + "action": { "type": "string", "minLength": 1 }, + "to": { "type": "string", "minLength": 1 }, + "phase": { "type": "string", "minLength": 1 }, + "observed_at": { "type": "string", "format": "date-time" }, + "sequence": { "type": "integer", "minimum": 0 }, + "trace_reference": { "$ref": "#/$defs/traceReference" }, + "provenance": { "type": "string" } + }, + "additionalProperties": true + }, + "invariant": { + "type": "object", + "required": ["id", "description", "result"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "description": { "type": "string", "minLength": 1 }, + "result": { "enum": ["held", "violated", "unknown"] }, + "evidence_reference": { "type": "string" } + }, + "additionalProperties": true + }, + "causalEdge": { + "type": "object", + "required": ["type", "source", "target"], + "properties": { + "type": { + "enum": [ + "enabled_by", + "required", + "triggered", + "bypassed", + "failed_to_prevent", + "amplified", + "masked", + "recovered_by", + "verified_by" + ] + }, + "source": { "type": "string", "minLength": 1 }, + "target": { "type": "string", "minLength": 1 }, + "evidence_reference": { "type": "string" } + }, + "additionalProperties": true + }, + "lifecycleResult": { + "type": "object", + "required": ["action", "result"], + "properties": { + "action": { "type": "string", "minLength": 1 }, + "result": { "enum": ["passed", "failed", "unknown"] }, + "target_state": { "type": "string" }, + "evidence_reference": { "type": "string" } + }, + "additionalProperties": true + }, + "verification": { + "type": "object", + "required": ["test_id", "expected", "observed", "result"], + "properties": { + "test_id": { "type": "string", "minLength": 1 }, + "expected": { "type": "string" }, + "observed": { "type": "string" }, + "result": { "enum": ["passed", "failed", "unknown"] }, + "evidence_reference": { "type": "string" } + }, + "additionalProperties": true + }, + "traceReference": { + "type": "object", + "properties": { + "trace_id": { "type": "string" }, + "span_id": { "type": "string" }, + "parent_span_id": { "type": "string" } + }, + "additionalProperties": false + }, + "evidence": { + "type": "object", + "required": ["id", "type"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "type": { "type": "string", "minLength": 1 }, + "trace_id": { "type": "string" }, + "span_id": { "type": "string" }, + "sha256": { "type": "string", "pattern": "^[A-Fa-f0-9]{64}$" }, + "uri": { "type": "string" }, + "provenance": { "type": "string" } + }, + "additionalProperties": true + } + } +}