1use crate::RepoPath;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use serde_json::Value as JsonValue;
5use time::OffsetDateTime;
6
7pub const SCHEMA_REPORT_V1: &str = "depguard.report.v1";
9pub const SCHEMA_REPORT_V2: &str = "depguard.report.v2";
10pub const SCHEMA_SENSOR_REPORT_V1: &str = "sensor.report.v1";
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
14#[serde(rename_all = "lowercase")]
15pub enum Severity {
16 Info,
17 Warning,
18 Error,
19}
20
21#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
22pub struct Location {
23 pub path: RepoPath,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub line: Option<u32>,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub col: Option<u32>,
28}
29
30#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
31pub struct Finding {
32 pub severity: Severity,
33 pub check_id: String,
34 pub code: String,
35 pub message: String,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub location: Option<Location>,
39
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub help: Option<String>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub url: Option<String>,
44
45 #[serde(skip_serializing_if = "Option::is_none")]
48 pub fingerprint: Option<String>,
49
50 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
52 pub data: JsonValue,
53}
54
55#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
56#[serde(rename_all = "lowercase")]
57pub enum Verdict {
58 Pass,
59 Warn,
60 Fail,
61}
62
63#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
64pub struct ToolMeta {
65 pub name: String,
66 pub version: String,
67}
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
75#[serde(rename_all = "lowercase")]
76pub enum SeverityV2 {
77 Info,
78 Warn,
79 Error,
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
83#[serde(rename_all = "lowercase")]
84pub enum VerdictStatus {
85 Pass,
86 Warn,
87 Fail,
88 Skip,
89}
90
91fn is_zero(val: &u32) -> bool {
93 *val == 0
94}
95
96#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
97pub struct VerdictCounts {
98 pub info: u32,
99 pub warn: u32,
100 pub error: u32,
101 #[serde(default, skip_serializing_if = "is_zero")]
103 pub suppressed: u32,
104}
105
106#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
107pub struct VerdictV2 {
108 pub status: VerdictStatus,
109 pub counts: VerdictCounts,
110 #[serde(default)]
111 pub reasons: Vec<String>,
112}
113
114#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
115pub struct ToolMetaV2 {
116 pub name: String,
117 pub version: String,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub commit: Option<String>,
120}
121
122#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
123pub struct RunHost {
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub os: Option<String>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub arch: Option<String>,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub hostname: Option<String>,
130}
131
132#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
133pub struct RunCi {
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub provider: Option<String>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub run_id: Option<String>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub job: Option<String>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub url: Option<String>,
142}
143
144#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
145pub struct RunGit {
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub repo: Option<String>,
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub base_ref: Option<String>,
150 #[serde(skip_serializing_if = "Option::is_none")]
151 pub head_ref: Option<String>,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub base_sha: Option<String>,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub head_sha: Option<String>,
156 #[serde(skip_serializing_if = "Option::is_none")]
157 pub merge_base: Option<String>,
158}
159
160#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
166#[serde(rename_all = "lowercase")]
167pub enum CapabilityAvailability {
168 Available,
169 Missing,
170 Degraded,
171}
172
173#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
175pub struct CapabilityStatus {
176 pub status: CapabilityAvailability,
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub reason: Option<String>,
179}
180
181#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
183pub struct Capabilities {
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub git: Option<CapabilityStatus>,
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub config: Option<CapabilityStatus>,
190}
191
192#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
193pub struct RunMeta {
194 #[schemars(with = "String")]
195 #[serde(with = "time::serde::rfc3339")]
196 pub started_at: OffsetDateTime,
197 #[schemars(with = "Option<String>")]
198 #[serde(skip_serializing_if = "Option::is_none")]
199 #[serde(with = "time::serde::rfc3339::option")]
200 pub ended_at: Option<OffsetDateTime>,
201 #[serde(skip_serializing_if = "Option::is_none")]
202 pub duration_ms: Option<u64>,
203 #[serde(skip_serializing_if = "Option::is_none")]
204 pub host: Option<RunHost>,
205 #[serde(skip_serializing_if = "Option::is_none")]
206 pub ci: Option<RunCi>,
207 #[serde(skip_serializing_if = "Option::is_none")]
208 pub git: Option<RunGit>,
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub capabilities: Option<Capabilities>,
212}
213
214#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
215pub struct FindingV2 {
216 pub severity: SeverityV2,
217 pub check_id: String,
218 pub code: String,
219 pub message: String,
220
221 #[serde(skip_serializing_if = "Option::is_none")]
222 pub location: Option<Location>,
223
224 #[serde(skip_serializing_if = "Option::is_none")]
225 pub help: Option<String>,
226 #[serde(skip_serializing_if = "Option::is_none")]
227 pub url: Option<String>,
228
229 #[serde(skip_serializing_if = "Option::is_none")]
230 pub fingerprint: Option<String>,
231
232 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
233 pub data: JsonValue,
234}
235
236#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
242#[serde(rename_all = "lowercase")]
243pub enum ArtifactType {
244 Comment,
245 Annotation,
246 Extra,
247}
248
249#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
251pub struct ArtifactPointer {
252 #[serde(rename = "type")]
254 pub artifact_type: ArtifactType,
255 pub path: String,
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub format: Option<String>,
260}
261
262#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
268pub struct DepguardData {
269 pub scope: String,
270 pub profile: String,
271
272 pub manifests_scanned: u32,
273 pub dependencies_scanned: u32,
274
275 pub findings_total: u32,
276 pub findings_emitted: u32,
277
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub truncated_reason: Option<String>,
280}
281
282#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
286pub struct ReportEnvelope<TData = DepguardData> {
287 pub schema: String,
289 pub tool: ToolMeta,
290 #[schemars(with = "String")]
291 #[serde(with = "time::serde::rfc3339")]
292 pub started_at: OffsetDateTime,
293 #[schemars(with = "String")]
294 #[serde(with = "time::serde::rfc3339")]
295 pub finished_at: OffsetDateTime,
296 pub verdict: Verdict,
297 pub findings: Vec<Finding>,
298 pub data: TData,
299}
300
301pub type DepguardReportV1 = ReportEnvelope<DepguardData>;
303
304#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
306pub struct ReportEnvelopeV2<TData = DepguardData> {
307 pub schema: String,
308 pub tool: ToolMetaV2,
309 pub run: RunMeta,
310 pub verdict: VerdictV2,
311 pub findings: Vec<FindingV2>,
312 #[serde(skip_serializing_if = "Option::is_none")]
314 pub artifacts: Option<Vec<ArtifactPointer>>,
315 pub data: TData,
316}
317
318pub type DepguardReportV2 = ReportEnvelopeV2<DepguardData>;
319
320pub type DepguardReport = DepguardReportV1;
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use time::OffsetDateTime;
327
328 #[test]
329 fn verdict_counts_skip_zero_suppressed() {
330 let counts = VerdictCounts {
331 info: 0,
332 warn: 0,
333 error: 0,
334 suppressed: 0,
335 };
336 let value = serde_json::to_value(&counts).unwrap();
337 assert!(value.get("suppressed").is_none());
338
339 let counts = VerdictCounts {
340 info: 0,
341 warn: 1,
342 error: 0,
343 suppressed: 2,
344 };
345 let value = serde_json::to_value(&counts).unwrap();
346 assert_eq!(value["suppressed"], 2);
347 }
348
349 #[test]
350 fn run_meta_omits_optional_fields() {
351 let run = RunMeta {
352 started_at: OffsetDateTime::UNIX_EPOCH,
353 ended_at: None,
354 duration_ms: None,
355 host: None,
356 ci: None,
357 git: None,
358 capabilities: None,
359 };
360 let value = serde_json::to_value(&run).unwrap();
361 assert!(value.get("ended_at").is_none());
362 assert!(value.get("duration_ms").is_none());
363 assert!(value.get("host").is_none());
364 assert!(value.get("ci").is_none());
365 assert!(value.get("git").is_none());
366 assert!(value.get("capabilities").is_none());
367 assert!(value.get("started_at").is_some());
368 }
369}