1use camino::Utf8PathBuf;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ReceiptEnvelope {
15 pub schema: String,
17
18 pub tool: ToolInfo,
19
20 #[serde(default)]
21 pub run: RunInfo,
22
23 #[serde(default)]
24 pub verdict: Verdict,
25
26 #[serde(default)]
27 pub findings: Vec<Finding>,
28
29 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub capabilities: Option<ReceiptCapabilities>,
32
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub data: Option<serde_json::Value>,
36}
37
38#[derive(Debug, Clone, Default, Serialize, Deserialize)]
40pub struct ReceiptCapabilities {
41 #[serde(default, skip_serializing_if = "Vec::is_empty")]
43 pub check_ids: Vec<String>,
44
45 #[serde(default, skip_serializing_if = "Vec::is_empty")]
47 pub scopes: Vec<String>,
48
49 #[serde(default)]
51 pub partial: bool,
52
53 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub reason: Option<String>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct ToolInfo {
60 pub name: String,
61
62 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub version: Option<String>,
64
65 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub repo: Option<String>,
67
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub commit: Option<String>,
70}
71
72#[derive(Debug, Clone, Default, Serialize, Deserialize)]
73pub struct RunInfo {
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub started_at: Option<DateTime<Utc>>,
76
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub ended_at: Option<DateTime<Utc>>,
79
80 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub git_head_sha: Option<String>,
84}
85
86#[derive(Debug, Clone, Default, Serialize, Deserialize)]
87pub struct Verdict {
88 #[serde(default)]
89 pub status: VerdictStatus,
90
91 #[serde(default)]
92 pub counts: Counts,
93
94 #[serde(default, skip_serializing_if = "Vec::is_empty")]
95 pub reasons: Vec<String>,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
99#[serde(rename_all = "snake_case")]
100pub enum VerdictStatus {
101 Pass,
102 Warn,
103 Fail,
104 #[default]
105 Unknown,
106}
107
108#[derive(Debug, Clone, Default, Serialize, Deserialize)]
109pub struct Counts {
110 #[serde(default)]
111 pub findings: u64,
112
113 #[serde(default)]
114 pub errors: u64,
115
116 #[serde(default)]
117 pub warnings: u64,
118}
119
120#[derive(Debug, Clone, Default, Serialize, Deserialize)]
121pub struct Finding {
122 #[serde(default)]
123 pub severity: Severity,
124
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub check_id: Option<String>,
127
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub code: Option<String>,
130
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub message: Option<String>,
133
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub location: Option<Location>,
136
137 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub fingerprint: Option<String>,
140
141 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub data: Option<serde_json::Value>,
144
145 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub confidence: Option<f64>,
149
150 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub provenance: Option<Provenance>,
153
154 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub context: Option<FindingContext>,
157}
158
159impl Finding {
160 pub fn is_high_confidence(&self) -> bool {
162 self.confidence.is_some_and(|c| c >= 0.9)
163 }
164
165 pub fn has_full_consensus(&self) -> bool {
167 self.context
168 .as_ref()
169 .is_some_and(|ctx| ctx.workspace.as_ref().is_some_and(|ws| ws.all_crates_agree))
170 }
171
172 pub fn has_tool_agreement(&self) -> bool {
174 self.provenance.as_ref().is_some_and(|p| p.agreement)
175 }
176}
177
178#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
180pub struct Provenance {
181 pub method: String,
183
184 #[serde(default, skip_serializing_if = "Vec::is_empty")]
186 pub tools: Vec<String>,
187
188 #[serde(default)]
190 pub agreement: bool,
191
192 #[serde(default, skip_serializing_if = "Vec::is_empty")]
194 pub evidence_chain: Vec<Evidence>,
195}
196
197#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
199pub struct Evidence {
200 pub source: String,
202
203 pub value: serde_json::Value,
205
206 #[serde(default)]
208 pub validated: bool,
209}
210
211impl Default for Evidence {
212 fn default() -> Self {
213 Self {
214 source: String::new(),
215 value: serde_json::Value::Null,
216 validated: false,
217 }
218 }
219}
220
221#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
223pub struct FindingContext {
224 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub workspace: Option<WorkspaceContext>,
227
228 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub analysis_depth: Option<AnalysisDepth>,
231}
232
233#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
235pub struct WorkspaceContext {
236 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub consensus_value: Option<serde_json::Value>,
239
240 #[serde(default)]
242 pub consensus_count: u64,
243
244 #[serde(default)]
246 pub total_crates: u64,
247
248 #[serde(default, skip_serializing_if = "Vec::is_empty")]
250 pub outliers: Vec<serde_json::Value>,
251
252 #[serde(default, skip_serializing_if = "Vec::is_empty")]
254 pub outlier_crates: Vec<String>,
255
256 #[serde(default)]
258 pub all_crates_agree: bool,
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
263#[serde(rename_all = "snake_case")]
264pub enum AnalysisDepth {
265 Shallow,
267 #[default]
269 Full,
270 Deep,
272}
273
274#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
275#[serde(rename_all = "snake_case")]
276pub enum Severity {
277 #[default]
278 Info,
279 Warn,
280 Error,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct Location {
285 pub path: Utf8PathBuf,
286
287 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub line: Option<u64>,
289
290 #[serde(default, skip_serializing_if = "Option::is_none")]
291 pub column: Option<u64>,
292}