Skip to main content

taudit_core/
finding.rs

1use crate::graph::NodeId;
2use crate::propagation::PropagationPath;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum Severity {
8    Critical,
9    High,
10    Medium,
11    Low,
12    Info,
13}
14
15impl Severity {
16    fn rank(self) -> u8 {
17        match self {
18            Severity::Critical => 0,
19            Severity::High => 1,
20            Severity::Medium => 2,
21            Severity::Low => 3,
22            Severity::Info => 4,
23        }
24    }
25}
26
27impl Ord for Severity {
28    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
29        self.rank().cmp(&other.rank())
30    }
31}
32
33impl PartialOrd for Severity {
34    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
35        Some(self.cmp(other))
36    }
37}
38
39/// MVP categories (1-5) are derivable from pipeline YAML alone.
40/// Stretch categories (6-9) need heuristics or metadata enrichment.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum FindingCategory {
44    // MVP
45    AuthorityPropagation,
46    OverPrivilegedIdentity,
47    UnpinnedAction,
48    UntrustedWithAuthority,
49    ArtifactBoundaryCrossing,
50    // Stretch — implemented
51    FloatingImage,
52    LongLivedCredential,
53    /// Credential written to disk by a step (e.g. `persistCredentials: true` on a checkout).
54    /// Disk-persisted credentials are accessible to all subsequent steps and any process
55    /// with filesystem access, unlike runtime-only `HasAccessTo` authority.
56    PersistedCredential,
57    // Reserved — requires ADO/GH API enrichment beyond pipeline YAML
58    /// Requires runtime network telemetry or policy enrichment — not detectable from YAML alone.
59    #[doc(hidden)]
60    EgressBlindspot,
61    /// Requires external audit-sink configuration data — not detectable from YAML alone.
62    #[doc(hidden)]
63    MissingAuditTrail,
64}
65
66/// Routing: scope findings -> TsafeRemediation; isolation findings -> CellosRemediation.
67#[derive(Debug, Clone, Serialize, Deserialize)]
68#[serde(tag = "type", rename_all = "snake_case")]
69pub enum Recommendation {
70    TsafeRemediation {
71        command: String,
72        explanation: String,
73    },
74    CellosRemediation {
75        reason: String,
76        spec_hint: String,
77    },
78    PinAction {
79        current: String,
80        pinned: String,
81    },
82    ReducePermissions {
83        current: String,
84        minimum: String,
85    },
86    FederateIdentity {
87        static_secret: String,
88        oidc_provider: String,
89    },
90    Manual {
91        action: String,
92    },
93}
94
95/// A finding is a concrete, actionable authority issue.
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct Finding {
98    pub severity: Severity,
99    pub category: FindingCategory,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub path: Option<PropagationPath>,
102    pub nodes_involved: Vec<NodeId>,
103    pub message: String,
104    pub recommendation: Recommendation,
105}