1use chrono::{DateTime, Utc};
2use serde::de::{self, Visitor};
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4#[derive(Debug, Serialize, Deserialize)]
7
8pub struct Package {
9 pub name: String,
11
12 pub ecosystem: Ecosystem,
15
16 #[serde(skip_serializing_if = "Option::is_none")]
20 pub purl: Option<String>,
21}
22
23pub type Commit = String;
25
26pub type Version = String;
28
29#[derive(Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Clone)]
32#[non_exhaustive]
33pub enum Ecosystem {
34 AlmaLinux(Option<String>),
35 Alpine(Option<String>),
36 Android,
37 Bioconductor,
38 Bitnami,
39 Chainguard,
40 ConanCenter,
41 CRAN,
42 CratesIO,
43 Debian(Option<String>),
44 DWF,
45 GHC,
46 GSD,
47 GitHubActions,
48 Go,
49 Hackage,
50 Hex,
51 JavaScript,
52 Linux,
53 Mageia(String),
54 Maven(String),
55 Npm,
56 NuGet,
57 OpenSUSE(Option<String>),
58 OssFuzz,
59 Packagist,
60 PhotonOS(Option<String>),
61 Pub,
62 PyPI,
63 Python,
64 RedHat(Option<String>),
65 RockyLinux(Option<String>),
66 RubyGems,
67 SUSE(Option<String>),
68 SwiftURL,
69 Ubuntu {
70 version: String,
71 pro: bool,
72 lts: bool,
73 },
74 UVI,
75}
76
77impl Serialize for Ecosystem {
78 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
79 where
80 S: Serializer,
81 {
82 match self {
83 Ecosystem::AlmaLinux(None) => serializer.serialize_str("AlmaLinux"),
84 Ecosystem::AlmaLinux(Some(release)) => {
85 serializer.serialize_str(&format!("AlmaLinux:{}", release))
86 }
87 Ecosystem::Alpine(None) => serializer.serialize_str("Alpine"),
88 Ecosystem::Alpine(Some(version)) => {
89 serializer.serialize_str(&format!("Alpine:{}", version))
90 }
91 Ecosystem::Android => serializer.serialize_str("Android"),
92 Ecosystem::Bioconductor => serializer.serialize_str("Bioconductor"),
93 Ecosystem::Bitnami => serializer.serialize_str("Bitnami"),
94 Ecosystem::Chainguard => serializer.serialize_str("Chainguard"),
95 Ecosystem::ConanCenter => serializer.serialize_str("ConanCenter"),
96 Ecosystem::CRAN => serializer.serialize_str("CRAN"),
97 Ecosystem::CratesIO => serializer.serialize_str("crates.io"),
98 Ecosystem::Debian(None) => serializer.serialize_str("Debian"),
99 Ecosystem::Debian(Some(version)) => {
100 serializer.serialize_str(&format!("Debian:{}", version))
101 }
102 Ecosystem::DWF => serializer.serialize_str("DWF"),
103 Ecosystem::GHC => serializer.serialize_str("GHC"),
104 Ecosystem::GSD => serializer.serialize_str("GSD"),
105 Ecosystem::GitHubActions => serializer.serialize_str("GitHub Actions"),
106 Ecosystem::Go => serializer.serialize_str("Go"),
107 Ecosystem::Hackage => serializer.serialize_str("Hackage"),
108 Ecosystem::Hex => serializer.serialize_str("Hex"),
109 Ecosystem::JavaScript => serializer.serialize_str("JavaScript"),
110 Ecosystem::Linux => serializer.serialize_str("Linux"),
111 Ecosystem::Mageia(release) => serializer.serialize_str(&format!("Mageia:{}", release)),
112 Ecosystem::Maven(repository) => {
113 let mvn: String = match repository.as_str() {
114 "https://repo.maven.apache.org/maven2" => "Maven".to_string(),
115 _ => format!("Maven:{}", repository),
116 };
117 serializer.serialize_str(&mvn)
118 }
119 Ecosystem::Npm => serializer.serialize_str("npm"),
120 Ecosystem::NuGet => serializer.serialize_str("NuGet"),
121 Ecosystem::OpenSUSE(None) => serializer.serialize_str("OpenSUSE"),
122 Ecosystem::OpenSUSE(Some(release)) => {
123 serializer.serialize_str(&format!("OpenSUSE:{}", release))
124 }
125 Ecosystem::OssFuzz => serializer.serialize_str("OSS-Fuzz"),
126 Ecosystem::Packagist => serializer.serialize_str("Packagist"),
127 Ecosystem::PhotonOS(None) => serializer.serialize_str("Photon OS"),
128 Ecosystem::PhotonOS(Some(release)) => {
129 serializer.serialize_str(&format!("Photon OS:{}", release))
130 }
131 Ecosystem::Pub => serializer.serialize_str("Pub"),
132 Ecosystem::PyPI => serializer.serialize_str("PyPI"),
133 Ecosystem::Python => serializer.serialize_str("Python"),
134 Ecosystem::RedHat(None) => serializer.serialize_str("Red Hat"),
135 Ecosystem::RedHat(Some(release)) => {
136 serializer.serialize_str(&format!("Red Hat:{}", release))
137 }
138 Ecosystem::RockyLinux(None) => serializer.serialize_str("Rocky Linux"),
139 Ecosystem::RockyLinux(Some(release)) => {
140 serializer.serialize_str(&format!("Rocky Linux:{}", release))
141 }
142 Ecosystem::RubyGems => serializer.serialize_str("RubyGems"),
143 Ecosystem::SUSE(None) => serializer.serialize_str("SUSE"),
144 Ecosystem::SUSE(Some(release)) => {
145 serializer.serialize_str(&format!("SUSE:{}", release))
146 }
147 Ecosystem::SwiftURL => serializer.serialize_str("SwiftURL"),
148 Ecosystem::Ubuntu {
149 version: v,
150 pro: true,
151 lts: true,
152 } => serializer.serialize_str(&format!("Ubuntu:Pro:{}:LTS", v)),
153 Ecosystem::Ubuntu {
154 version: v,
155 pro: true,
156 lts: false,
157 } => serializer.serialize_str(&format!("Ubuntu:Pro:{}", v)),
158 Ecosystem::Ubuntu {
159 version: v,
160 pro: false,
161 lts: true,
162 } => serializer.serialize_str(&format!("Ubuntu:{}:LTS", v)),
163 Ecosystem::Ubuntu {
164 version: v,
165 pro: false,
166 lts: false,
167 } => serializer.serialize_str(&format!("Ubuntu:{}", v)),
168 Ecosystem::UVI => serializer.serialize_str("UVI"),
169 }
170 }
171}
172
173impl<'de> Deserialize<'de> for Ecosystem {
174 fn deserialize<D>(deserializer: D) -> Result<Ecosystem, D::Error>
175 where
176 D: Deserializer<'de>,
177 {
178 struct EcosystemVisitor;
179
180 impl<'de> Visitor<'de> for EcosystemVisitor {
181 type Value = Ecosystem;
182
183 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
184 formatter.write_str("a valid string representing an ecosystem")
185 }
186
187 fn visit_str<E>(self, value: &str) -> Result<Ecosystem, E>
188 where
189 E: de::Error,
190 {
191 match value {
192 "AlmaLinux" | "AlmaLinux:" => Ok(Ecosystem::AlmaLinux(None)),
193 _ if value.starts_with("AlmaLinux:") => Ok(Ecosystem::AlmaLinux(
194 value.strip_prefix("AlmaLinux:").map(|v| v.to_string()),
195 )),
196 "Alpine" => Ok(Ecosystem::Alpine(None)),
197 _ if value.starts_with("Alpine:") => Ok(Ecosystem::Alpine(
198 value.strip_prefix("Alpine:").map(|v| v.to_string()),
199 )),
200 "Android" => Ok(Ecosystem::Android),
201 "Bioconductor" => Ok(Ecosystem::Bioconductor),
202 "Bitnami" => Ok(Ecosystem::Bitnami),
203 "Chainguard" => Ok(Ecosystem::Chainguard),
204 "ConanCenter" => Ok(Ecosystem::ConanCenter),
205 "CRAN" => Ok(Ecosystem::CRAN),
206 "crates.io" => Ok(Ecosystem::CratesIO),
207 "Debian" => Ok(Ecosystem::Debian(None)),
208 _ if value.starts_with("Debian:") => Ok(Ecosystem::Debian(
209 value.strip_prefix("Debian:").map(|v| v.to_string()),
210 )),
211 "DWF" => Ok(Ecosystem::DWF),
212 "GHC" => Ok(Ecosystem::GHC),
213 "GitHub Actions" => Ok(Ecosystem::GitHubActions),
214 "Go" => Ok(Ecosystem::Go),
215 "GSD" => Ok(Ecosystem::GSD),
216 "Hackage" => Ok(Ecosystem::Hackage),
217 "Hex" => Ok(Ecosystem::Hex),
218 "JavaScript" => Ok(Ecosystem::JavaScript),
219 "Linux" => Ok(Ecosystem::Linux),
220 _ if value.starts_with("Mageia:") => Ok(Ecosystem::Mageia(
221 value
222 .strip_prefix("Mageia:")
223 .map(|v| v.to_string())
224 .unwrap(),
225 )),
226 "Maven" | "Maven:" => Ok(Ecosystem::Maven(
227 "https://repo.maven.apache.org/maven2".to_string(),
228 )),
229 _ if value.starts_with("Maven:") => Ok(Ecosystem::Maven(
230 value.strip_prefix("Maven:").map(|v| v.to_string()).unwrap(),
231 )),
232 "npm" => Ok(Ecosystem::Npm),
233 "NuGet" => Ok(Ecosystem::NuGet),
234 "OpenSUSE" => Ok(Ecosystem::OpenSUSE(None)),
235 _ if value.starts_with("OpenSUSE:") => Ok(Ecosystem::OpenSUSE(
236 value.strip_prefix("OpenSUSE:").map(|v| v.to_string()),
237 )),
238 "OSS-Fuzz" => Ok(Ecosystem::OssFuzz),
239 "Packagist" => Ok(Ecosystem::Packagist),
240 "Photon OS" | "Photon OS:" => Ok(Ecosystem::PhotonOS(None)),
241 _ if value.starts_with("Photon OS:") => Ok(Ecosystem::PhotonOS(
242 value.strip_prefix("Photon OS:").map(|v| v.to_string()),
243 )),
244 "Pub" => Ok(Ecosystem::Pub),
245 "PyPI" => Ok(Ecosystem::PyPI),
246 "Python" => Ok(Ecosystem::Python),
247 "Red Hat" => Ok(Ecosystem::RedHat(None)),
248 _ if value.starts_with("Red Hat:") => Ok(Ecosystem::RedHat(
249 value.strip_prefix("Red Hat:").map(|v| v.to_string()),
250 )),
251 "Rocky Linux" | "Rocky Linux:" => Ok(Ecosystem::RockyLinux(None)),
252 _ if value.starts_with("Rocky Linux:") => Ok(Ecosystem::RockyLinux(
253 value.strip_prefix("Rocky Linux:").map(|v| v.to_string()),
254 )),
255 "RubyGems" => Ok(Ecosystem::RubyGems),
256 "SUSE" => Ok(Ecosystem::SUSE(None)),
257 _ if value.starts_with("SUSE:") => Ok(Ecosystem::SUSE(
258 value.strip_prefix("SUSE:").map(|v| v.to_string()),
259 )),
260 "SwiftURL" => Ok(Ecosystem::SwiftURL),
261 _ if value.starts_with("Ubuntu:Pro:") => {
262 value.strip_prefix("Ubuntu:Pro:").map_or(
263 Err(de::Error::unknown_variant(value, &["Ecosystem"])),
264 |v| {
265 let parts: Vec<&str> = v.split(':').collect();
266 match parts.as_slice() {
267 [ver, "LTS"] => Ok(Ecosystem::Ubuntu {
268 version: ver.to_string(),
269 pro: true,
270 lts: true,
271 }),
272 [ver] => Ok(Ecosystem::Ubuntu {
273 version: ver.to_string(),
274 pro: true,
275 lts: false,
276 }),
277 _ => Err(de::Error::unknown_variant(
278 value,
279 &["Ecosystem", "Ubuntu:Pro:YY.MM:(LTS?)"],
280 )),
281 }
282 },
283 )
284 }
285 _ if value.starts_with("Ubuntu:") => value.strip_prefix("Ubuntu:").map_or(
286 Err(de::Error::unknown_variant(value, &["Ecosystem"])),
287 |v| {
288 let parts: Vec<&str> = v.split(':').collect();
289 match parts.as_slice() {
290 [ver, "LTS"] => Ok(Ecosystem::Ubuntu {
291 version: ver.to_string(),
292 pro: false,
293 lts: true,
294 }),
295 [ver] => Ok(Ecosystem::Ubuntu {
296 version: ver.to_string(),
297 pro: false,
298 lts: false,
299 }),
300 _ => Err(de::Error::unknown_variant(
301 value,
302 &["Ecosystem", "Ubuntu:YY.MM:(?LTS)"],
303 )),
304 }
305 },
306 ),
307 "UVI" => Ok(Ecosystem::UVI),
308 _ => Err(de::Error::unknown_variant(value, &["Ecosystem"])),
309 }
310 }
311 }
312 deserializer.deserialize_str(EcosystemVisitor)
313 }
314}
315
316#[derive(Debug, Serialize, Deserialize)]
319#[serde(rename_all = "UPPERCASE")]
320#[non_exhaustive]
321pub enum RangeType {
322 Ecosystem,
325
326 Git,
328
329 Semver,
331
332 Unspecified,
334}
335
336#[derive(Debug, Serialize, Deserialize)]
339#[serde(rename_all = "lowercase")]
340#[non_exhaustive]
341pub enum Event {
342 Introduced(String),
345
346 Fixed(String),
348
349 #[serde(rename = "last_affected")]
351 LastAffected(String),
352
353 Limit(String),
355}
356
357#[derive(Debug, Serialize, Deserialize)]
360pub struct Range {
361 #[serde(rename = "type")]
364 pub range_type: RangeType,
365
366 #[serde(skip_serializing_if = "Option::is_none")]
370 pub repo: Option<String>,
371
372 pub events: Vec<Event>,
375}
376
377#[derive(Debug, Serialize, Deserialize)]
382pub struct Affected {
383 #[serde(skip_serializing_if = "Option::is_none")]
385 pub package: Option<Package>,
386
387 #[serde(skip_serializing_if = "Option::is_none")]
392 pub severity: Option<Vec<Severity>>,
393
394 #[serde(skip_serializing_if = "Option::is_none")]
397 pub ranges: Option<Vec<Range>>,
398
399 #[serde(skip_serializing_if = "Option::is_none")]
402 pub versions: Option<Vec<String>>,
403
404 #[serde(skip_serializing_if = "Option::is_none")]
408 pub ecosystem_specific: Option<serde_json::Value>,
409
410 #[serde(skip_serializing_if = "Option::is_none")]
415 pub database_specific: Option<serde_json::Value>,
416}
417
418#[derive(Debug, Serialize, Deserialize)]
422#[serde(rename_all = "UPPERCASE")]
423#[non_exhaustive]
424pub enum ReferenceType {
425 Advisory,
427
428 Article,
430
431 Detection,
434
435 Discussion,
437
438 Evidence,
440
441 Fix,
443
444 Git,
446
447 Introduced,
449
450 Package,
452
453 Report,
455
456 #[serde(rename = "NONE")]
457 Undefined,
458
459 Web,
461}
462
463#[derive(Debug, Serialize, Deserialize)]
465pub struct Reference {
466 #[serde(rename = "type")]
468 pub reference_type: ReferenceType,
469
470 pub url: String,
473}
474
475#[derive(Debug, Serialize, Deserialize)]
478#[non_exhaustive]
479pub enum SeverityType {
480 #[serde(rename = "CVSS_V2")]
484 CVSSv2,
485
486 #[serde(rename = "CVSS_V3")]
490 CVSSv3,
491
492 #[serde(rename = "CVSS_V4")]
496 CVSSv4,
497
498 #[serde(rename = "UNSPECIFIED")]
501 Unspecified,
502}
503
504#[derive(Debug, Serialize, Deserialize)]
507pub struct Severity {
508 #[serde(rename = "type")]
511 pub severity_type: SeverityType,
512
513 pub score: String,
516}
517
518#[derive(Debug, Serialize, Deserialize)]
523#[serde(rename_all = "UPPERCASE")]
524#[non_exhaustive]
525pub enum CreditType {
526 Analyst,
528
529 Coordinator,
531
532 Finder,
534
535 Other,
538
539 RemediationDeveloper,
541
542 RemediationReviewer,
545
546 RemediationVerifier,
548
549 Reporter,
551
552 Sponsor,
554
555 Tool,
557}
558
559#[derive(Debug, Serialize, Deserialize)]
562pub struct Credit {
563 pub name: String,
564 #[serde(skip_serializing_if = "Option::is_none")]
565 pub contact: Option<Vec<String>>,
566 #[serde(skip_serializing_if = "Option::is_none")]
567 pub credit_type: Option<CreditType>,
568}
569
570#[derive(Debug, Serialize, Deserialize)]
577pub struct Vulnerability {
578 pub schema_version: Option<String>,
581 pub id: String,
585
586 pub published: Option<DateTime<Utc>>,
589
590 pub modified: DateTime<Utc>,
593
594 #[serde(skip_serializing_if = "Option::is_none")]
599 pub withdrawn: Option<DateTime<Utc>>,
600
601 #[serde(skip_serializing_if = "Option::is_none")]
607 pub aliases: Option<Vec<String>>,
608
609 #[serde(skip_serializing_if = "Option::is_none")]
612 pub related: Option<Vec<String>>,
613
614 #[serde(skip_serializing_if = "Option::is_none")]
617 pub summary: Option<String>,
618
619 #[serde(skip_serializing_if = "Option::is_none")]
626 pub details: Option<String>,
627
628 pub affected: Vec<Affected>,
630
631 #[serde(skip_serializing_if = "Option::is_none")]
634 pub references: Option<Vec<Reference>>,
635
636 #[serde(skip_serializing_if = "Option::is_none")]
640 pub severity: Option<Vec<Severity>>,
641
642 #[serde(skip_serializing_if = "Option::is_none")]
645 pub credits: Option<Vec<Credit>>,
646
647 #[serde(skip_serializing_if = "Option::is_none")]
650 pub database_specific: Option<serde_json::Value>,
651}
652
653#[cfg(test)]
654mod tests {
655 use super::*;
656
657 #[test]
658 fn test_no_serialize_null_fields() {
659 let vuln = Vulnerability {
660 schema_version: Some("1.3.0".to_string()),
661 id: "OSV-2020-484".to_string(),
662 published: Some(chrono::Utc::now()),
663 modified: chrono::Utc::now(),
664 withdrawn: None,
665 aliases: None,
666 related: None,
667 summary: None,
668 details: None,
669 affected: vec![],
670 references: None,
671 severity: None,
672 credits: None,
673 database_specific: None,
674 };
675
676 let as_json = serde_json::json!(vuln);
677 let str_json = as_json.to_string();
678 assert!(!str_json.contains("withdrawn"));
679 assert!(!str_json.contains("aliases"));
680 assert!(!str_json.contains("related"));
681 assert!(!str_json.contains("summary"));
682 assert!(!str_json.contains("details"));
683 assert!(!str_json.contains("references"));
684 assert!(!str_json.contains("severity"));
685 assert!(!str_json.contains("credits"));
686 assert!(!str_json.contains("database_specific"));
687 }
688
689 #[test]
690 fn test_maven_ecosystem() {
691 let maven = Ecosystem::Maven("https://repo.maven.apache.org/maven2".to_string());
692 let as_json = serde_json::json!(maven);
693 assert_eq!(as_json, serde_json::json!("Maven"));
694
695 let maven = Ecosystem::Maven("https://repo1.example.com/maven2".to_string());
696 let as_json = serde_json::json!(maven);
697 assert_eq!(
698 as_json,
699 serde_json::json!("Maven:https://repo1.example.com/maven2")
700 );
701
702 let json_str = r#""Maven""#;
703 let maven: Ecosystem = serde_json::from_str(json_str).unwrap();
704 assert_eq!(
705 maven,
706 Ecosystem::Maven("https://repo.maven.apache.org/maven2".to_string())
707 );
708
709 let json_str = r#""Maven:""#;
710 let maven: Ecosystem = serde_json::from_str(json_str).unwrap();
711 assert_eq!(
712 maven,
713 Ecosystem::Maven("https://repo.maven.apache.org/maven2".to_string())
714 );
715 }
716
717 #[test]
718 fn test_ubuntu_ecosystem() {
719 let ubuntu = Ecosystem::Ubuntu {
720 version: "20.04".to_string(),
721 pro: true,
722 lts: true,
723 };
724 let as_json = serde_json::json!(ubuntu);
725 assert_eq!(as_json, serde_json::json!("Ubuntu:Pro:20.04:LTS"));
726
727 let ubuntu = Ecosystem::Ubuntu {
728 version: "20.04".to_string(),
729 pro: true,
730 lts: false,
731 };
732 let as_json = serde_json::json!(ubuntu);
733 assert_eq!(as_json, serde_json::json!("Ubuntu:Pro:20.04"));
734
735 let ubuntu = Ecosystem::Ubuntu {
736 version: "20.04".to_string(),
737 pro: false,
738 lts: true,
739 };
740 let as_json = serde_json::json!(ubuntu);
741 assert_eq!(as_json, serde_json::json!("Ubuntu:20.04:LTS"));
742
743 let ubuntu = Ecosystem::Ubuntu {
744 version: "20.04".to_string(),
745 pro: false,
746 lts: false,
747 };
748 let as_json = serde_json::json!(ubuntu);
749 assert_eq!(as_json, serde_json::json!("Ubuntu:20.04"));
750
751 let json_str = r#""Ubuntu:Pro:20.04:LTS""#;
752 let ubuntu: Ecosystem = serde_json::from_str(json_str).unwrap();
753 assert_eq!(
754 ubuntu,
755 Ecosystem::Ubuntu {
756 version: "20.04".to_string(),
757 pro: true,
758 lts: true
759 }
760 );
761
762 let json_str = r#""Ubuntu:Pro:20.04""#;
763 let ubuntu: Ecosystem = serde_json::from_str(json_str).unwrap();
764 assert_eq!(
765 ubuntu,
766 Ecosystem::Ubuntu {
767 version: "20.04".to_string(),
768 pro: true,
769 lts: false
770 }
771 );
772
773 let json_str = r#""Ubuntu:20.04:LTS""#;
774 let ubuntu: Ecosystem = serde_json::from_str(json_str).unwrap();
775 assert_eq!(
776 ubuntu,
777 Ecosystem::Ubuntu {
778 version: "20.04".to_string(),
779 pro: false,
780 lts: true
781 }
782 );
783
784 let json_str = r#""Ubuntu:20.04""#;
785 let ubuntu: Ecosystem = serde_json::from_str(json_str).unwrap();
786 assert_eq!(
787 ubuntu,
788 Ecosystem::Ubuntu {
789 version: "20.04".to_string(),
790 pro: false,
791 lts: false
792 }
793 );
794 }
795}