use std::collections::{BTreeMap, BTreeSet};
use chrono::{DateTime, Utc};
use serde::Serialize;
use sha2::{Digest, Sha256};
use super::query::{ComplianceFramework, ComplianceQueryError, EvidencePack, EvidenceRow};
use crate::domains::org::audit::AuthorityAtSigning;
pub const INTOTO_STATEMENT_TYPE: &str = "https://in-toto.io/Statement/v1";
pub const SLSA_PROVENANCE_PREDICATE_TYPE: &str = "https://slsa.dev/provenance/v1";
pub const SLSA_VSA_PREDICATE_TYPE: &str = "https://slsa.dev/verification_summary/v1";
pub const SPDX_VERSION: &str = "SPDX-2.3";
pub const AUTHS_BUILDER_ID: &str = "https://auths.dev/builder/compliance-query";
pub const SLSA_REPORT_PREDICATE_TYPE: &str = "https://auths.dev/compliance/slsa/v1";
pub const SBOM_REPORT_PREDICATE_TYPE: &str = "https://auths.dev/compliance/sbom/v1";
pub const CRA_REPORT_PREDICATE_TYPE: &str = "https://auths.dev/compliance/cra/v1";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum RowTimeliness {
Timeless,
TimeSensitive,
}
#[derive(Debug, Clone, Default)]
pub struct SignerVerifierAllowList {
pairs: BTreeMap<String, BTreeSet<String>>,
}
impl SignerVerifierAllowList {
pub fn new() -> Self {
Self::default()
}
pub fn allow(mut self, signer: impl Into<String>, verifier: impl Into<String>) -> Self {
self.pairs
.entry(signer.into())
.or_default()
.insert(verifier.into());
self
}
pub fn is_allowed(&self, signer: &str, verifier: &str) -> bool {
match self.pairs.get(signer) {
Some(verifiers) => verifiers.contains(verifier),
None => self.pairs.is_empty(),
}
}
}
#[derive(Debug, Clone)]
pub struct VsaParams {
pub verifier_id: String,
pub time_verified: DateTime<Utc>,
pub allow_list: SignerVerifierAllowList,
}
#[derive(Debug, Clone)]
pub struct FrameworkReport {
pub framework: ComplianceFramework,
pub predicate_type: String,
pub statement: serde_json::Value,
pub sbom_sha256: Option<String>,
}
impl FrameworkReport {
pub fn to_intoto_statement(&self) -> Result<String, ComplianceQueryError> {
json_canon::to_string(&self.statement)
.map_err(|e| ComplianceQueryError::Canonicalize(e.to_string()))
}
}
pub fn build_framework_report(
pack: &EvidencePack,
vsa: &VsaParams,
) -> Result<FrameworkReport, ComplianceQueryError> {
match pack.framework {
ComplianceFramework::Slsa => Ok(build_slsa(pack, vsa)),
ComplianceFramework::Sbom => Ok(build_sbom(pack)),
ComplianceFramework::Cra => Ok(build_cra(pack)),
}
}
pub fn sbom_document_sha256(document: &serde_json::Value) -> Result<String, ComplianceQueryError> {
let bytes = json_canon::to_vec(document)
.map_err(|e| ComplianceQueryError::Canonicalize(e.to_string()))?;
Ok(format!("sha256:{}", hex::encode(Sha256::digest(&bytes))))
}
fn subjects(rows: &[EvidenceRow]) -> Vec<serde_json::Value> {
rows.iter()
.map(|r| {
let digest = r
.artifact_digest
.strip_prefix("sha256:")
.unwrap_or(&r.artifact_digest);
serde_json::json!({ "name": r.artifact_digest, "digest": { "sha256": digest } })
})
.collect()
}
fn statement(
rows: &[EvidenceRow],
predicate_type: &str,
predicate: serde_json::Value,
) -> serde_json::Value {
serde_json::json!({
"_type": INTOTO_STATEMENT_TYPE,
"subject": subjects(rows),
"predicateType": predicate_type,
"predicate": predicate,
})
}
fn authority_authorized(authority: &AuthorityAtSigning) -> bool {
matches!(authority, AuthorityAtSigning::AuthorizedBeforeRevocation)
}
fn slsa_provenance(row: &EvidenceRow) -> serde_json::Value {
let digest = row
.artifact_digest
.strip_prefix("sha256:")
.unwrap_or(&row.artifact_digest);
serde_json::json!({
"predicateType": SLSA_PROVENANCE_PREDICATE_TYPE,
"subject": [{ "name": row.artifact_digest, "digest": { "sha256": digest } }],
"timeliness": RowTimeliness::Timeless,
"predicate": {
"buildDefinition": {
"buildType": "https://auths.dev/buildtypes/signed-release/v1",
"externalParameters": { "artifact": row.artifact_digest },
"internalParameters": { "signedAtSeq": row.signed_at.map(|s| s.to_string()) },
"resolvedDependencies": []
},
"runDetails": {
"builder": { "id": AUTHS_BUILDER_ID },
"metadata": { "invocationId": row.signer.as_str() }
}
}
})
}
fn slsa_vsa(row: &EvidenceRow, vsa: &VsaParams) -> serde_json::Value {
let allowed = vsa
.allow_list
.is_allowed(row.signer.as_str(), &vsa.verifier_id);
let passed = authority_authorized(&row.authority_at_release) && allowed;
serde_json::json!({
"predicateType": SLSA_VSA_PREDICATE_TYPE,
"verifier": { "id": vsa.verifier_id },
"timeVerified": vsa.time_verified,
"resourceUri": row.artifact_digest,
"signer": row.signer.as_str(),
"policy": { "uri": "https://auths.dev/compliance/policy/authority-at-release/v1" },
"verificationResult": if passed { "PASSED" } else { "FAILED" },
"verifiedLevels": ["AUTHS_AUTHORITY_AT_RELEASE"],
"signerVerifierAllowed": allowed,
"authorityAtRelease": row.authority_at_release,
"timeliness": RowTimeliness::TimeSensitive,
})
}
fn build_slsa(pack: &EvidencePack, vsa: &VsaParams) -> FrameworkReport {
let provenance: Vec<serde_json::Value> = pack.rows.iter().map(slsa_provenance).collect();
let verification_summaries: Vec<serde_json::Value> =
pack.rows.iter().map(|r| slsa_vsa(r, vsa)).collect();
let predicate = serde_json::json!({
"provenance": provenance,
"verificationSummaries": verification_summaries,
"equivocationVisibility": pack.equivocation_visibility,
});
FrameworkReport {
framework: ComplianceFramework::Slsa,
predicate_type: SLSA_REPORT_PREDICATE_TYPE.to_string(),
statement: statement(&pack.rows, SLSA_REPORT_PREDICATE_TYPE, predicate),
sbom_sha256: None,
}
}
fn spdx_document(pack: &EvidencePack) -> serde_json::Value {
let packages: Vec<serde_json::Value> = pack
.rows
.iter()
.enumerate()
.map(|(i, r)| {
let digest = r
.artifact_digest
.strip_prefix("sha256:")
.unwrap_or(&r.artifact_digest);
serde_json::json!({
"SPDXID": format!("SPDXRef-Package-{i}"),
"name": r.artifact_digest,
"downloadLocation": "NOASSERTION",
"checksums": [{ "algorithm": "SHA256", "checksumValue": digest }],
"supplier": format!("Organization: {}", r.signer.as_str()),
})
})
.collect();
serde_json::json!({
"spdxVersion": SPDX_VERSION,
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": format!("auths-compliance-{}", pack.period),
"documentNamespace": format!("https://auths.dev/spdx/{}", pack.period),
"creationInfo": { "creators": [format!("Tool: {AUTHS_BUILDER_ID}")] },
"packages": packages,
})
}
fn build_sbom(pack: &EvidencePack) -> FrameworkReport {
let document = spdx_document(pack);
let sha = sbom_document_sha256(&document).ok();
let predicate = serde_json::json!({
"spdxVersion": SPDX_VERSION,
"document": document,
"documentSha256": sha,
"equivocationVisibility": pack.equivocation_visibility,
});
FrameworkReport {
framework: ComplianceFramework::Sbom,
predicate_type: SBOM_REPORT_PREDICATE_TYPE.to_string(),
statement: statement(&pack.rows, SBOM_REPORT_PREDICATE_TYPE, predicate),
sbom_sha256: sha,
}
}
#[derive(Debug, Clone, Serialize)]
struct CraObligation {
id: &'static str,
description: &'static str,
ssdf_practices: &'static [&'static str],
satisfied_by: &'static str,
status: &'static str,
}
const CRA_OBLIGATIONS: &[CraObligation] = &[
CraObligation {
id: "CRA-Annex-I-2(1)",
description: "Identify and document components (SBOM).",
ssdf_practices: &["PS.3.2", "PW.4.1"],
satisfied_by: "sbom",
status: "covered",
},
CraObligation {
id: "CRA-Annex-I-2(3)",
description: "Apply security updates / off-board compromised signers.",
ssdf_practices: &["RV.1.1", "PO.3.2"],
satisfied_by: "authority_at_release",
status: "covered",
},
CraObligation {
id: "CRA-Annex-I-1(2)(j)",
description: "Record and verify the provenance of releases.",
ssdf_practices: &["PS.3.1", "PS.2.1"],
satisfied_by: "slsa_provenance",
status: "covered",
},
];
fn build_cra(pack: &EvidencePack) -> FrameworkReport {
let predicate = serde_json::json!({
"obligations": CRA_OBLIGATIONS,
"releasesAssessed": pack.rows.len(),
"equivocationVisibility": pack.equivocation_visibility,
"note": "Maps the evidence pack to CRA obligations and NIST SSDF practices; \
it is a reporting mapping, not a new trust primitive.",
});
FrameworkReport {
framework: ComplianceFramework::Cra,
predicate_type: CRA_REPORT_PREDICATE_TYPE.to_string(),
statement: statement(&pack.rows, CRA_REPORT_PREDICATE_TYPE, predicate),
sbom_sha256: None,
}
}