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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum FindingCategory {
44 AuthorityPropagation,
46 OverPrivilegedIdentity,
47 UnpinnedAction,
48 UntrustedWithAuthority,
49 ArtifactBoundaryCrossing,
50 FloatingImage,
52 LongLivedCredential,
53 PersistedCredential,
57 TriggerContextMismatch,
59 CrossWorkflowAuthorityChain,
61 AuthorityCycle,
63 UpliftWithoutAttestation,
65 SelfMutatingPipeline,
67 VariableGroupInPrJob,
69 SelfHostedPoolPrHijack,
71 ServiceConnectionScopeMismatch,
73 #[doc(hidden)]
76 EgressBlindspot,
77 #[doc(hidden)]
79 MissingAuditTrail,
80}
81
82#[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#[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}