///|
/// The kind of evidence attached to a release review.
pub(all) enum EvidenceKind {
Source
Dependency
Build
Test
Review
} derive(Debug, Eq)
///|
/// A single, content-addressed release evidence record.
pub(all) struct EvidenceRecord {
id : String
kind : EvidenceKind
subject : String
digest : String
observed_at : String
note : String
}
///|
/// A deterministic evidence bundle bound to one manifest root.
pub(all) struct EvidenceBundle {
name : String
version : String
manifest_root : String
records : Array[EvidenceRecord]
mut bundle_digest : String
}
///|
/// Construct one evidence record. `digest` should be a lowercase SHA-256 hex
/// digest of the referenced artifact, log, or review material.
pub fn EvidenceRecord::new(
id : String,
kind : EvidenceKind,
subject : String,
digest : String,
observed_at : String,
note : String,
) -> EvidenceRecord {
{ id, kind, subject, digest, observed_at, note }
}
///|
/// Return the stable wire name used for an evidence kind.
pub fn EvidenceKind::to_text(self : EvidenceKind) -> String {
match self {
EvidenceKind::Source => "source"
EvidenceKind::Dependency => "dependency"
EvidenceKind::Build => "build"
EvidenceKind::Test => "test"
EvidenceKind::Review => "review"
}
}
///|
/// Validate fields before an evidence record is included in a bundle.
pub fn EvidenceRecord::is_valid(self : EvidenceRecord) -> Bool {
self.id.length() > 0 &&
self.subject.length() > 0 &&
self.observed_at.length() > 0 &&
valid_evidence_text(self.id) &&
valid_evidence_text(self.subject) &&
valid_evidence_text(self.observed_at) &&
valid_evidence_text(self.note) &&
valid_evidence_digest(self.digest)
}
///|
/// Serialize a record without a trailing newline for deterministic hashing.
pub fn EvidenceRecord::canonical_text(self : EvidenceRecord) -> String {
self.id +
"|" +
self.kind.to_text() +
"|" +
self.subject +
"|" +
self.digest +
"|" +
self.observed_at +
"|" +
self.note
}
///|
/// Construct a bundle and calculate its digest immediately.
pub fn EvidenceBundle::new(
name : String,
version : String,
manifest_root : String,
records : Array[EvidenceRecord],
) -> EvidenceBundle {
let bundle = { name, version, manifest_root, records, bundle_digest: "" }
bundle.refresh_digest()
bundle
}
///|
/// Construct evidence metadata directly from the manifest under review.
pub fn Manifest::evidence_bundle(
self : Manifest,
records : Array[EvidenceRecord],
) -> EvidenceBundle {
EvidenceBundle::new(self.name, self.version, self.merkle_root, records)
}
///|
/// Return the records in insertion order for audit integrations.
pub fn EvidenceBundle::records(self : EvidenceBundle) -> Array[EvidenceRecord] {
self.records
}
///|
/// Return the number of evidence records in the bundle.
pub fn EvidenceBundle::record_count(self : EvidenceBundle) -> Int {
self.records.length()
}
///|
/// Append one record and refresh the bundle digest.
pub fn EvidenceBundle::add(
self : EvidenceBundle,
record : EvidenceRecord,
) -> Unit {
self.records.push(record)
self.refresh_digest()
}
///|
/// Find an evidence record by its stable identifier.
pub fn EvidenceBundle::find(
self : EvidenceBundle,
id : String,
) -> EvidenceRecord? {
for record in self.records {
if record.id == id {
return Some(record)
}
}
None
}
///|
/// Return true when at least one record of the requested kind is present.
pub fn EvidenceBundle::has_kind(
self : EvidenceBundle,
kind : EvidenceKind,
) -> Bool {
for record in self.records {
if record.kind == kind {
return true
}
}
false
}
///|
/// Count records belonging to one evidence kind.
pub fn EvidenceBundle::count_kind(
self : EvidenceBundle,
kind : EvidenceKind,
) -> Int {
let mut count = 0
for record in self.records {
if record.kind == kind {
count = count + 1
}
}
count
}
///|
/// Return which required evidence kinds are absent.
pub fn EvidenceBundle::missing_kinds(
self : EvidenceBundle,
required : Array[EvidenceKind],
) -> Array[EvidenceKind] {
let missing : Array[EvidenceKind] = []
for kind in required {
if !self.has_kind(kind) {
missing.push(kind)
}
}
missing
}
///|
/// The conservative evidence set expected for a public release.
pub fn release_evidence_kinds() -> Array[EvidenceKind] {
[
EvidenceKind::Source,
EvidenceKind::Dependency,
EvidenceKind::Build,
EvidenceKind::Test,
EvidenceKind::Review,
]
}
///|
/// Return true only when all standard release evidence is present.
pub fn EvidenceBundle::is_complete(self : EvidenceBundle) -> Bool {
self.missing_kinds(release_evidence_kinds()).length() == 0
}
///|
/// Return true when the bundle names the exact manifest being reviewed.
pub fn EvidenceBundle::binds_to(
self : EvidenceBundle,
manifest : Manifest,
) -> Bool {
self.name == manifest.name &&
self.version == manifest.version &&
self.manifest_root == manifest.merkle_root
}
///|
/// Return the canonical header and record lines used for hashing.
pub fn EvidenceBundle::canonical_text(self : EvidenceBundle) -> String {
let out = StringBuilder::new()
out.write_string("name=" + self.name + "\n")
out.write_string("version=" + self.version + "\n")
out.write_string("manifest_root=" + self.manifest_root + "\n")
for record in self.records {
out.write_string("record=" + record.canonical_text() + "\n")
}
out.to_string()
}
///|
/// Recompute and store the SHA-256 digest of the canonical evidence text.
pub fn EvidenceBundle::refresh_digest(self : EvidenceBundle) -> Unit {
self.bundle_digest = @digest.sha256_hex(
evidence_string_bytes(self.canonical_text()),
)
}
///|
/// Return the stored digest of the evidence bundle.
pub fn EvidenceBundle::digest(self : EvidenceBundle) -> String {
self.bundle_digest
}
///|
/// Verify digest, record validity, duplicate IDs, and manifest binding data.
pub fn EvidenceBundle::verify(self : EvidenceBundle) -> Bool {
let duplicate_ids = self.duplicate_ids()
if duplicate_ids.length() > 0 {
return false
}
for record in self.records {
if !record.is_valid() {
return false
}
}
valid_evidence_digest(self.manifest_root) &&
self.bundle_digest ==
@digest.sha256_hex(evidence_string_bytes(self.canonical_text()))
}
///|
/// Return duplicate record identifiers in first-seen order.
pub fn EvidenceBundle::duplicate_ids(self : EvidenceBundle) -> Array[String] {
let seen : Array[String] = []
let duplicates : Array[String] = []
for record in self.records {
if array_contains_string(seen, record.id) {
if !array_contains_string(duplicates, record.id) {
duplicates.push(record.id)
}
} else {
seen.push(record.id)
}
}
duplicates
}
///|
/// Format a compact audit summary suitable for a CI artifact.
pub fn EvidenceBundle::to_text(self : EvidenceBundle) -> String {
let out = StringBuilder::new()
out.write_string("name=" + self.name + "\n")
out.write_string("version=" + self.version + "\n")
out.write_string("manifest_root=" + self.manifest_root + "\n")
out.write_string("records=" + self.records.length().to_string() + "\n")
out.write_string("complete=" + self.is_complete().to_string() + "\n")
out.write_string("valid=" + self.verify().to_string() + "\n")
out.write_string("bundle_digest=" + self.bundle_digest + "\n")
out.to_string()
}
///|
fn evidence_string_bytes(value : String) -> Bytes {
let bytes : Array[Byte] = []
for i = 0; i < value.length(); i = i + 1 {
bytes.push(value[i].to_int().to_byte())
}
Bytes::from_array(bytes)
}
///|
fn valid_evidence_text(value : String) -> Bool {
for i = 0; i < value.length(); i = i + 1 {
if value[i] == '|' || value[i] == '\n' || value[i] == '\r' {
return false
}
}
true
}
///|
fn valid_evidence_digest(value : String) -> Bool {
if value.length() != 64 {
return false
}
for i = 0; i < value.length(); i = i + 1 {
let c = value[i]
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
return false
}
}
true
}
///|
fn array_contains_string(values : Array[String], target : String) -> Bool {
for value in values {
if value == target {
return true
}
}
false
}