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    /// Dangerous trigger type (pull_request_target / pr) combined with secret/identity access.
58    TriggerContextMismatch,
59    /// Authority (secret/identity) flows into an opaque external workflow via DelegatesTo.
60    CrossWorkflowAuthorityChain,
61    /// Circular DelegatesTo chain — workflow calls itself transitively.
62    AuthorityCycle,
63    /// Privileged workflow (OIDC/broad identity) with no provenance attestation step.
64    UpliftWithoutAttestation,
65    /// Step writes to the environment gate ($GITHUB_ENV, pipeline variables) — authority can propagate.
66    SelfMutatingPipeline,
67    /// ADO variable group consumed by a PR-triggered job, crossing trust boundary.
68    VariableGroupInPrJob,
69    /// Self-hosted agent pool used in a PR-triggered job that also checks out the repository.
70    SelfHostedPoolPrHijack,
71    /// Broad-scope ADO service connection reachable from a PR-triggered job without OIDC.
72    ServiceConnectionScopeMismatch,
73    // Reserved — requires ADO/GH API enrichment beyond pipeline YAML
74    /// Requires runtime network telemetry or policy enrichment — not detectable from YAML alone.
75    #[doc(hidden)]
76    EgressBlindspot,
77    /// Requires external audit-sink configuration data — not detectable from YAML alone.
78    #[doc(hidden)]
79    MissingAuditTrail,
80}
81
82/// Routing: scope findings -> TsafeRemediation; isolation findings -> CellosRemediation.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(tag = "type", rename_all = "snake_case")]
85pub enum Recommendation {
86    TsafeRemediation {
87        command: String,
88        explanation: String,
89    },
90    CellosRemediation {
91        reason: String,
92        spec_hint: String,
93    },
94    PinAction {
95        current: String,
96        pinned: String,
97    },
98    ReducePermissions {
99        current: String,
100        minimum: String,
101    },
102    FederateIdentity {
103        static_secret: String,
104        oidc_provider: String,
105    },
106    Manual {
107        action: String,
108    },
109}
110
111/// A finding is a concrete, actionable authority issue.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct Finding {
114    pub severity: Severity,
115    pub category: FindingCategory,
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub path: Option<PropagationPath>,
118    pub nodes_involved: Vec<NodeId>,
119    pub message: String,
120    pub recommendation: Recommendation,
121}