osv/
schema.rs

1use chrono::{DateTime, Utc};
2use serde::de::{self, Visitor};
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4/// Package identifies the code library or command that
5/// is potentially affected by a particular vulnerability.
6#[derive(Debug, Serialize, Deserialize)]
7
8pub struct Package {
9    /// The name of the package or dependency.
10    pub name: String,
11
12    /// The ecosystem identifies the overall library ecosystem that this
13    /// package can be obtained from.
14    pub ecosystem: Ecosystem,
15
16    /// The purl field is a string following the [Package URL
17    /// specification](https://github.com/package-url/purl-spec) that identifies the
18    /// package. This field is optional but recommended.
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub purl: Option<String>,
21}
22
23/// A commit is a full SHA1 Git hash in hex format.
24pub type Commit = String;
25
26/// Version is arbitrary string representing the version of a package.
27pub type Version = String;
28
29/// The package ecosystem that the vulnerabilities in the OSV database
30/// are associated with.
31#[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/// Type of the affected range supplied. This can be an ecosystem
317/// specific value, semver, or a git commit hash.
318#[derive(Debug, Serialize, Deserialize)]
319#[serde(rename_all = "UPPERCASE")]
320#[non_exhaustive]
321pub enum RangeType {
322    /// The versions introduced and fixed are arbitrary, uninterpreted strings specific to the
323    /// package ecosystem
324    Ecosystem,
325
326    /// The versions introduced and fixed are full-length Git commit hashes.
327    Git,
328
329    /// The versions introduced and fixed are semantic versions as defined by SemVer 2.0.0.
330    Semver,
331
332    /// Default for the case where a range type is omitted.
333    Unspecified,
334}
335
336/// The event captures information about the how and when
337/// the package was affected by the vulnerability.
338#[derive(Debug, Serialize, Deserialize)]
339#[serde(rename_all = "lowercase")]
340#[non_exhaustive]
341pub enum Event {
342    /// The version or commit in which the vulnerability was
343    /// introduced.
344    Introduced(String),
345
346    /// The version which the vulnerability was fixed.
347    Fixed(String),
348
349    /// Describes the last known affected version
350    #[serde(rename = "last_affected")]
351    LastAffected(String),
352
353    /// The upper limit on the range being described.
354    Limit(String),
355}
356
357/// The range of versions of a package for which
358/// it is affected by the vulnerability.
359#[derive(Debug, Serialize, Deserialize)]
360pub struct Range {
361    /// The format that the range events are specified in, for
362    /// example SEMVER or GIT.
363    #[serde(rename = "type")]
364    pub range_type: RangeType,
365
366    /// The ranges object’s repo field is the URL of the package’s code repository. The value
367    /// should be in a format that’s directly usable as an argument for the version control
368    /// system’s clone command
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub repo: Option<String>,
371
372    /// Represent a status timeline for how the vulnerability affected the package. For
373    /// example when the vulnerability was first introduced into the codebase.
374    pub events: Vec<Event>,
375}
376
377/// The versions of the package that are affected
378/// by a particular vulnerability. The affected ranges can include
379/// when the vulnerability was first introduced and also when it
380/// was fixed.
381#[derive(Debug, Serialize, Deserialize)]
382pub struct Affected {
383    /// The package that is affected by the vulnerability
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub package: Option<Package>,
386
387    /// This `severity` field applies to a specific package, in cases where affected
388    /// packages have differing severities for the same vulnerability. If any package
389    /// level `severity` fields are set, the top level [`severity`](#severity-field)
390    /// must not be set.
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub severity: Option<Vec<Severity>>,
393
394    /// The range of versions or git commits that this vulnerability
395    /// was first introduced and/or version that it was fixed in.
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub ranges: Option<Vec<Range>>,
398
399    /// Each string is a single affected version in whatever version syntax is
400    /// used by the given package ecosystem.
401    #[serde(skip_serializing_if = "Option::is_none")]
402    pub versions: Option<Vec<String>>,
403
404    /// A JSON object that holds any additional information about the
405    /// vulnerability as defined by the ecosystem for which the record applies.
406    ///
407    #[serde(skip_serializing_if = "Option::is_none")]
408    pub ecosystem_specific: Option<serde_json::Value>,
409
410    /// A JSON object to hold any additional information about the range
411    /// from which this record was obtained. The meaning of the values within
412    /// the object is entirely defined by the database.
413    ///
414    #[serde(skip_serializing_if = "Option::is_none")]
415    pub database_specific: Option<serde_json::Value>,
416}
417
418/// The type of reference information that has been provided. Examples include
419/// links to the original report, external advisories, or information about the
420/// fix.
421#[derive(Debug, Serialize, Deserialize)]
422#[serde(rename_all = "UPPERCASE")]
423#[non_exhaustive]
424pub enum ReferenceType {
425    /// A published security advisory for the vulnerability.
426    Advisory,
427
428    /// An article or blog post describing the vulnerability.
429    Article,
430
431    /// A tool, script, scanner, or other mechanism that allows for detection
432    /// of the vulnerability in production environments
433    Detection,
434
435    /// A social media discussion regarding the vulnerability.
436    Discussion,
437
438    /// A demonstration of the validity of a vulnerability claim
439    Evidence,
440
441    /// A source code browser link to the fix.
442    Fix,
443
444    /// Git commit hash or range where the issue occurred
445    Git,
446
447    /// A source code browser link to the introduction of the vulnerability.
448    Introduced,
449
450    /// A home web page for the package.
451    Package,
452
453    /// A report, typically on a bug or issue tracker, of the vulnerability.
454    Report,
455
456    #[serde(rename = "NONE")]
457    Undefined,
458
459    /// A web page of some unspecified kind.
460    Web,
461}
462
463/// Reference to additional information about the vulnerability.
464#[derive(Debug, Serialize, Deserialize)]
465pub struct Reference {
466    /// The type of reference this URL points to.
467    #[serde(rename = "type")]
468    pub reference_type: ReferenceType,
469
470    /// The url where more information can be obtained about
471    /// the vulnerability or associated the fix.
472    pub url: String,
473}
474
475/// The [`SeverityType`](SeverityType) describes the quantitative scoring method used to rate the
476/// severity of the vulnerability.
477#[derive(Debug, Serialize, Deserialize)]
478#[non_exhaustive]
479pub enum SeverityType {
480    /// A CVSS vector string representing the unique characteristics and severity of the vulnerability
481    /// using a version of the [Common Vulnerability Scoring System notation](https://www.first.org/cvss/v2/)
482    /// that is == 2.0 (e.g.`"AV:L/AC:M/Au:N/C:N/I:P/A:C"`).
483    #[serde(rename = "CVSS_V2")]
484    CVSSv2,
485
486    /// A CVSS vector string representing the unique characteristics and severity of the
487    /// vulnerability using a version of the Common Vulnerability Scoring System notation that is
488    /// >= 3.0 and < 4.0 (e.g.`"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:N/A:N"`).
489    #[serde(rename = "CVSS_V3")]
490    CVSSv3,
491
492    /// A CVSS vector string representing the unique characterictics and severity of the vulnerability
493    /// using a version on the [Common Vulnerability Scoring System notation](https://www.first.org/cvss/)
494    /// that is >= 4.0 and < 5.0 (e.g. `"CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N"`).
495    #[serde(rename = "CVSS_V4")]
496    CVSSv4,
497
498    /// The severity score was arrived at by using an unspecified
499    /// scoring method.
500    #[serde(rename = "UNSPECIFIED")]
501    Unspecified,
502}
503
504/// The type and score used to describe the severity of a vulnerability using one
505/// or more quantitative scoring methods.
506#[derive(Debug, Serialize, Deserialize)]
507pub struct Severity {
508    /// The severity type property must be a [`SeverityType`](SeverityType), which describes the
509    /// quantitative method used to calculate the associated score.
510    #[serde(rename = "type")]
511    pub severity_type: SeverityType,
512
513    /// The score property is a string representing the severity score based on the
514    /// selected severity type.
515    pub score: String,
516}
517
518/// The [`CreditType`](CreditType) this optional field should specify
519/// the type or role of the individual or entity being credited.
520///
521/// These values and their definitions correspond directly to the [MITRE CVE specification](https://cveproject.github.io/cve-schema/schema/v5.0/docs/#collapseDescription_oneOf_i0_containers_cna_credits_items_type).
522#[derive(Debug, Serialize, Deserialize)]
523#[serde(rename_all = "UPPERCASE")]
524#[non_exhaustive]
525pub enum CreditType {
526    /// Validated the vulnerability to ensure accruacy or severity.
527    Analyst,
528
529    /// Facilitated the corredinated response process.
530    Coordinator,
531
532    /// Identified the vulnerability
533    Finder,
534
535    /// Any other type or role that does not fall under the categories
536    /// described above.
537    Other,
538
539    /// Prepared a code change or other remediation plans.
540    RemediationDeveloper,
541
542    /// Reviewed vulnerability remediation plans or code changes
543    /// for effectiveness and completeness.
544    RemediationReviewer,
545
546    /// Tested and verified the vulnerability or its remediation.
547    RemediationVerifier,
548
549    /// Notified the vendor of the vulnerability to a CNA.
550    Reporter,
551
552    /// Supported the vulnerability identification or remediation activities.
553    Sponsor,
554
555    /// Names of tools used in vulnerability discovery or identification.
556    Tool,
557}
558
559/// Provides a way to give credit for the discovery, confirmation, patch or other events in the
560/// life cycle of a vulnerability.
561#[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/// A vulnerability is the standard exchange format that is
571/// defined by the OSV schema <https://ossf.github.io/osv-schema/>.
572///
573/// This is the entity that is returned when vulnerable data exists for
574/// a given package or when requesting information about a specific vulnerability
575/// by unique identifier.
576#[derive(Debug, Serialize, Deserialize)]
577pub struct Vulnerability {
578    /// The schema_version field is used to indicate which version of the OSV schema a particular
579    /// vulnerability was exported with.
580    pub schema_version: Option<String>,
581    /// The id field is a unique identifier for the vulnerability entry. It is a string of the
582    /// format <DB>-<ENTRYID>, where DB names the database and ENTRYID is in the format used by the
583    /// database. For example: “OSV-2020-111”, “CVE-2021-3114”, or “GHSA-vp9c-fpxx-744v”.
584    pub id: String,
585
586    /// The published field gives the time the entry should be considered to have been published,
587    /// as an RFC3339-formatted time stamp in UTC (ending in “Z”).
588    pub published: Option<DateTime<Utc>>,
589
590    /// The modified field gives the time the entry was last modified, as an RFC3339-formatted
591    /// timestamptime stamp in UTC (ending in “Z”).
592    pub modified: DateTime<Utc>,
593
594    /// The withdrawn field gives the time the entry should be considered to have been withdrawn,
595    /// as an RFC3339-formatted timestamp in UTC (ending in “Z”). If the field is missing, then the
596    /// entry has not been withdrawn. Any rationale for why the vulnerability has been withdrawn
597    /// should go into the summary text.
598    #[serde(skip_serializing_if = "Option::is_none")]
599    pub withdrawn: Option<DateTime<Utc>>,
600
601    /// The aliases field gives a list of IDs of the same vulnerability in other databases, in the
602    /// form of the id field. This allows one database to claim that its own entry describes the
603    /// same vulnerability as one or more entries in other databases. Or if one database entry has
604    /// been deduplicated into another in the same database, the duplicate entry could be written
605    /// using only the id, modified, and aliases field, to point to the canonical one.
606    #[serde(skip_serializing_if = "Option::is_none")]
607    pub aliases: Option<Vec<String>>,
608
609    /// The related field gives a list of IDs of closely related vulnerabilities, such as the same
610    /// problem in alternate ecosystems.
611    #[serde(skip_serializing_if = "Option::is_none")]
612    pub related: Option<Vec<String>>,
613
614    /// The summary field gives a one-line, English textual summary of the vulnerability. It is
615    /// recommended that this field be kept short, on the order of no more than 120 characters.
616    #[serde(skip_serializing_if = "Option::is_none")]
617    pub summary: Option<String>,
618
619    /// The details field gives additional English textual details about the vulnerability. The
620    /// details field is CommonMark markdown (a subset of GitHub-Flavored Markdown). Display code
621    /// may at its discretion sanitize the input further, such as stripping raw HTML and links that
622    /// do not start with http:// or https://. Databases are encouraged not to include those in the
623    /// first place. (The goal is to balance flexibility of presentation with not exposing
624    /// vulnerability database display sites to unnecessary vulnerabilities.)
625    #[serde(skip_serializing_if = "Option::is_none")]
626    pub details: Option<String>,
627
628    /// Indicates the specific package ranges that are affected by this vulnerability.
629    pub affected: Vec<Affected>,
630
631    /// An optional list of external reference's that provide more context about this
632    /// vulnerability.
633    #[serde(skip_serializing_if = "Option::is_none")]
634    pub references: Option<Vec<Reference>>,
635
636    /// The severity field is a JSON array that allows generating systems to describe the severity
637    /// of a vulnerability using one or more quantitative scoring methods. Each severity item is a
638    /// object specifying a type and score property.
639    #[serde(skip_serializing_if = "Option::is_none")]
640    pub severity: Option<Vec<Severity>>,
641
642    /// Provides a way to give credit for the discovery, confirmation, patch or other events in the
643    /// life cycle of a vulnerability.
644    #[serde(skip_serializing_if = "Option::is_none")]
645    pub credits: Option<Vec<Credit>>,
646
647    /// Top level field to hold any additional information about the vulnerability as defined
648    /// by the database from which the record was obtained.
649    #[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}