tsafe-attest 1.1.0

Attestation pipeline for tsafe — secret scanner + env-injection contract + run-evidence harness (algol-merged)
Documentation
//! Scanner model types — `ScanReport`, `ScanFinding`, `Severity`, etc.
//!
//! Ported verbatim from `algol/src/model.rs` Phase 2.1 (Patch A added
//! `SecretPlaceholder`). Phase 4 flips the wire schema name to
//! `tsafe.scan.v1` per the coordinated rename wave; the legacy
//! `algol.scan.v1` schema name is still accepted on parse during the
//! v1.x compat window.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

/// Schema version emitted on `ScanReport.schema` and on the CloudEvents
/// `type` for scan events.
///
/// Phase 4 wire-format change: new emission is `tsafe.scan.v1`. Legacy
/// `algol.scan.v1` is accepted by parsers in the v1.x compat window
/// (see [`LEGACY_SCAN_SCHEMA`]). Removal scheduled for v2.0.0; see
/// `CHANGELOG.md`.
pub const SCAN_SCHEMA: &str = "tsafe.scan.v1";

/// Legacy schema name accepted on parse during the v1.x compat window.
pub const LEGACY_SCAN_SCHEMA: &str = "algol.scan.v1";

/// Test whether `schema` is one of the supported `ScanReport` schema names.
pub fn is_supported_scan_schema(schema: &str) -> bool {
    schema == SCAN_SCHEMA || schema == LEGACY_SCAN_SCHEMA
}

/// Scanner version string, embedded in `ScanReport.scanner_version`.
///
/// Sourced from `tsafe-attest` `CARGO_PKG_VERSION` (not the algol crate
/// version). The wire-shape field is named `scanner_version` and is
/// human-readable, so existing consumers continue to parse cleanly.
pub const ATTEST_VERSION: &str = env!("CARGO_PKG_VERSION");

/// Kind of finding emitted by the scanner.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum FindingKind {
    EnvFile,
    HardcodedSecret,
    PrivateKey,
    CiSecretReference,
    RuntimeEnvRead,
    UnsafeExport,
    RiskyEnvPropagation,
    /// A match that would otherwise be a secret finding but appears in a
    /// placeholder context (`.env.example`, `*.template`, comment/docstring
    /// example, value is a known placeholder pattern, etc.).
    ///
    /// Emitted (rather than silently dropped) so the audit log retains the
    /// original match for traceability. Verdict-classifiers that count
    /// "scanner says secret" should NOT count this kind as positive.
    SecretPlaceholder,
}

/// Severity tier for a finding.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
    Critical,
    High,
    Medium,
    Low,
    Info,
}

impl Severity {
    /// Risk score weight, summed across findings and capped at 100.
    pub fn weight(self) -> u32 {
        match self {
            Severity::Critical => 30,
            Severity::High => 20,
            Severity::Medium => 10,
            Severity::Low => 3,
            Severity::Info => 1,
        }
    }

    /// Uppercase label for human-readable output.
    pub fn label(self) -> &'static str {
        match self {
            Severity::Critical => "CRITICAL",
            Severity::High => "HIGH",
            Severity::Medium => "MEDIUM",
            Severity::Low => "LOW",
            Severity::Info => "INFO",
        }
    }
}

/// A single finding emitted by the scanner.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanFinding {
    pub id: String,
    pub kind: FindingKind,
    pub severity: Severity,
    pub confidence: f32,
    pub file: String,
    pub line: usize,
    pub column: usize,
    pub secret_type: Option<String>,
    pub name: Option<String>,
    pub redacted_value: Option<String>,
    /// Content fingerprint of the matched secret value.
    ///
    /// **Phase 3 wire-format change**: this is now `blake3:<hex>` per ec
    /// ADR-0003 (hash convergence). Consumers that pinned the algol-era
    /// `sha256:<hex>` prefix as a content-address must update; see
    /// CHANGELOG.
    pub hash: Option<String>,
    pub message: String,
}

/// An observed read of an environment variable in source code.
///
/// Distinct from a *secret detection* — this is the env-authority signal
/// that lets `tsafe attest` build an env-injection contract.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObservedEnvRead {
    pub name: String,
    pub file: String,
    pub line: usize,
    pub language: String,
    pub confidence: f32,
}

/// A reference to a CI-provided secret (e.g. GitHub Actions `secrets.X`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CiSecretReference {
    pub name: String,
    pub provider: String,
    pub file: String,
    pub line: usize,
    pub context: String,
}

/// Aggregate counters + risk score for the scan.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ScanSummary {
    pub total_findings: usize,
    pub critical: usize,
    pub high: usize,
    pub medium: usize,
    pub low: usize,
    pub risk_score: u32,
}

/// Top-level scan artifact.
///
/// Serialised as JSON when written to disk; the wire-format schema is
/// stable per the `schema` field (`tsafe.scan.v1` from Phase 4 onward;
/// `algol.scan.v1` accepted on parse during the v1.x compat window).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanReport {
    pub schema: String,
    pub repo_path: String,
    pub repo_commit: Option<String>,
    pub scanned_at: DateTime<Utc>,
    pub scanner_version: String,
    pub findings: Vec<ScanFinding>,
    pub observed_env_reads: Vec<ObservedEnvRead>,
    pub ci_secret_references: Vec<CiSecretReference>,
    pub summary: ScanSummary,
}