Skip to main content

tsafe_attest/
model.rs

1//! Scanner model types — `ScanReport`, `ScanFinding`, `Severity`, etc.
2//!
3//! Ported verbatim from `algol/src/model.rs` Phase 2.1 (Patch A added
4//! `SecretPlaceholder`). Phase 4 flips the wire schema name to
5//! `tsafe.scan.v1` per the coordinated rename wave; the legacy
6//! `algol.scan.v1` schema name is still accepted on parse during the
7//! v1.x compat window.
8
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11
12/// Schema version emitted on `ScanReport.schema` and on the CloudEvents
13/// `type` for scan events.
14///
15/// Phase 4 wire-format change: new emission is `tsafe.scan.v1`. Legacy
16/// `algol.scan.v1` is accepted by parsers in the v1.x compat window
17/// (see [`LEGACY_SCAN_SCHEMA`]). Removal scheduled for v2.0.0; see
18/// `CHANGELOG.md`.
19pub const SCAN_SCHEMA: &str = "tsafe.scan.v1";
20
21/// Legacy schema name accepted on parse during the v1.x compat window.
22pub const LEGACY_SCAN_SCHEMA: &str = "algol.scan.v1";
23
24/// Test whether `schema` is one of the supported `ScanReport` schema names.
25pub fn is_supported_scan_schema(schema: &str) -> bool {
26    schema == SCAN_SCHEMA || schema == LEGACY_SCAN_SCHEMA
27}
28
29/// Scanner version string, embedded in `ScanReport.scanner_version`.
30///
31/// Sourced from `tsafe-attest` `CARGO_PKG_VERSION` (not the algol crate
32/// version). The wire-shape field is named `scanner_version` and is
33/// human-readable, so existing consumers continue to parse cleanly.
34pub const ATTEST_VERSION: &str = env!("CARGO_PKG_VERSION");
35
36/// Kind of finding emitted by the scanner.
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
38#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
39pub enum FindingKind {
40    EnvFile,
41    HardcodedSecret,
42    PrivateKey,
43    CiSecretReference,
44    RuntimeEnvRead,
45    UnsafeExport,
46    RiskyEnvPropagation,
47    /// A match that would otherwise be a secret finding but appears in a
48    /// placeholder context (`.env.example`, `*.template`, comment/docstring
49    /// example, value is a known placeholder pattern, etc.).
50    ///
51    /// Emitted (rather than silently dropped) so the audit log retains the
52    /// original match for traceability. Verdict-classifiers that count
53    /// "scanner says secret" should NOT count this kind as positive.
54    SecretPlaceholder,
55}
56
57/// Severity tier for a finding.
58#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
59#[serde(rename_all = "lowercase")]
60pub enum Severity {
61    Critical,
62    High,
63    Medium,
64    Low,
65    Info,
66}
67
68impl Severity {
69    /// Risk score weight, summed across findings and capped at 100.
70    pub fn weight(self) -> u32 {
71        match self {
72            Severity::Critical => 30,
73            Severity::High => 20,
74            Severity::Medium => 10,
75            Severity::Low => 3,
76            Severity::Info => 1,
77        }
78    }
79
80    /// Uppercase label for human-readable output.
81    pub fn label(self) -> &'static str {
82        match self {
83            Severity::Critical => "CRITICAL",
84            Severity::High => "HIGH",
85            Severity::Medium => "MEDIUM",
86            Severity::Low => "LOW",
87            Severity::Info => "INFO",
88        }
89    }
90}
91
92/// A single finding emitted by the scanner.
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct ScanFinding {
95    pub id: String,
96    pub kind: FindingKind,
97    pub severity: Severity,
98    pub confidence: f32,
99    pub file: String,
100    pub line: usize,
101    pub column: usize,
102    pub secret_type: Option<String>,
103    pub name: Option<String>,
104    pub redacted_value: Option<String>,
105    /// Content fingerprint of the matched secret value.
106    ///
107    /// **Phase 3 wire-format change**: this is now `blake3:<hex>` per ec
108    /// ADR-0003 (hash convergence). Consumers that pinned the algol-era
109    /// `sha256:<hex>` prefix as a content-address must update; see
110    /// CHANGELOG.
111    pub hash: Option<String>,
112    pub message: String,
113}
114
115/// An observed read of an environment variable in source code.
116///
117/// Distinct from a *secret detection* — this is the env-authority signal
118/// that lets `tsafe attest` build an env-injection contract.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct ObservedEnvRead {
121    pub name: String,
122    pub file: String,
123    pub line: usize,
124    pub language: String,
125    pub confidence: f32,
126}
127
128/// A reference to a CI-provided secret (e.g. GitHub Actions `secrets.X`).
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct CiSecretReference {
131    pub name: String,
132    pub provider: String,
133    pub file: String,
134    pub line: usize,
135    pub context: String,
136}
137
138/// Aggregate counters + risk score for the scan.
139#[derive(Debug, Clone, Default, Serialize, Deserialize)]
140pub struct ScanSummary {
141    pub total_findings: usize,
142    pub critical: usize,
143    pub high: usize,
144    pub medium: usize,
145    pub low: usize,
146    pub risk_score: u32,
147}
148
149/// Top-level scan artifact.
150///
151/// Serialised as JSON when written to disk; the wire-format schema is
152/// stable per the `schema` field (`tsafe.scan.v1` from Phase 4 onward;
153/// `algol.scan.v1` accepted on parse during the v1.x compat window).
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct ScanReport {
156    pub schema: String,
157    pub repo_path: String,
158    pub repo_commit: Option<String>,
159    pub scanned_at: DateTime<Utc>,
160    pub scanner_version: String,
161    pub findings: Vec<ScanFinding>,
162    pub observed_env_reads: Vec<ObservedEnvRead>,
163    pub ci_secret_references: Vec<CiSecretReference>,
164    pub summary: ScanSummary,
165}