Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ chrono = { version = "0.4", features = ["serde"] }
cvss = { version = "2", features = ["serde"] }
serde_json = "1"
serde_with = "3"
packageurl = "0.3"
packageurl = "0.6.0-rc.1"
cpe = "0.1.2"
tempfile = "3"

Expand All @@ -35,3 +35,6 @@ rustsec-interop = ["rustsec", "crates-index"]

[dev-dependencies]
serde_json = "1"

[patch.crates-io]
packageurl = { git = "https://github.com/jcrossley3/packageurl.rs", branch = "issue-28" }
39 changes: 36 additions & 3 deletions src/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ pub struct Acknowledgment {
pub struct BranchesT(pub Vec<Branch>);

impl BranchesT {
pub(crate) fn product_ids(&self) -> Option<Vec<ProductIdT>> {
pub fn product_ids(&self) -> Option<Vec<ProductIdT>> {
if self.0.is_empty() {
None
} else {
Some(self.0.iter().map(|x| x.try_into().unwrap()).collect())
Some(self.0.iter().flat_map(|x| x.try_into().ok()).collect())
}
}
}
Expand Down Expand Up @@ -86,7 +86,7 @@ pub struct FullProductName {
#[serde_with::skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct ProductIdentificationHelper {
#[serde_as(as = "Option<DisplayFromStr>")]
#[serde(default, with = "custom_cpe_format")]
pub cpe: Option<cpe::uri::OwnedUri>,
pub hashes: Option<Vec<HashCollection>>,
pub model_numbers: Option<Vec<String>>, // TODO: No empty strings, enforce unique
Expand All @@ -98,6 +98,39 @@ pub struct ProductIdentificationHelper {
pub x_generic_uris: Option<Vec<Url>>,
}

mod custom_cpe_format {
use cpe::uri::OwnedUri;
use serde::{self, Deserialize, Deserializer, Serializer};
use std::str::FromStr;

pub fn serialize<S>(value: &Option<OwnedUri>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match value {
Some(value) => {
let x = format!("{:0}", value);
serializer.serialize_str(&x)
}
None => serializer.serialize_none(),
}
}

pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<OwnedUri>, D::Error>
where
D: Deserializer<'de>,
{
let result = Option::<String>::deserialize(deserializer);
match result? {
None => Ok(None),
Some(s) => {
let x = OwnedUri::from_str(s.as_str()).map_err(serde::de::Error::custom)?;
Ok(Some(x))
}
}
}
}

/// [Hashes](https://github.com/oasis-tcs/csaf/blob/master/csaf_2.0/prose/csaf-v2-editor-draft.md#31332-full-product-name-type---product-identification-helper---hashes)
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct HashCollection {
Expand Down
4 changes: 4 additions & 0 deletions src/interop.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#![allow(clippy::unwrap_used)]
#![allow(clippy::expect_used)]

#[cfg(feature = "rustsec-interop")]
pub mod rustsec {
use std::convert::TryInto;
Expand Down Expand Up @@ -318,6 +321,7 @@ pub mod rustsec {
PackageUrl::new("cargo", package.to_string())
.unwrap()
.with_version(version.to_string())
.unwrap()
.to_owned(),
),
sbom_urls: None,
Expand Down
31 changes: 21 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![deny(clippy::unwrap_used)]
#![deny(clippy::expect_used)]
//! Common Security Advisory Framework (CSAF)
//!
//! A lovingly hand-crafted implementation of [CSAF](https://www.oasis-open.org/committees/tc_home.php?wg_abbrev=csaf) for Rust. Currently,
Expand All @@ -11,16 +13,14 @@

use serde::{Deserialize, Serialize};

pub mod document;
use document::Document;

pub mod product_tree;
use product_tree::ProductTree;

pub mod vulnerability;
use vulnerability::Vulnerability;

pub mod definitions;
pub mod document;
pub mod product_tree;
pub mod vulnerability;

pub mod interop;

Expand All @@ -38,7 +38,6 @@ pub struct Csaf {
#[cfg(test)]
mod tests {
use super::*;
use serde_json;

#[test]
fn generic_template_deserializes() {
Expand Down Expand Up @@ -69,26 +68,38 @@ mod tests {
}
}"#;

let document: Csaf = serde_json::from_str(generic).unwrap();
let document: Csaf = serde_json_round_trip(generic);
println!("{:#?}", document);
}

fn serde_json_round_trip(doc_str: &str) -> Csaf {
let document: Csaf = serde_json::from_str(doc_str).unwrap();
let bytes = serde_json::to_vec_pretty(&document).unwrap();
let round_trip_document: Csaf = serde_json::from_slice(bytes.clone().as_slice())
.unwrap_or_else(|err| panic!("re-serialized document:\n{}\ndeserialization of re-serialized document failed: {}", String::from_utf8(bytes.clone()).unwrap(), err));
assert_eq!(
document, round_trip_document,
"re-serialized document should be equal to original"
);
document
}

#[test]
fn first_example_deserializes() {
let example = include_str!("../tests/CVE-2018-0171-modified.json");
let document: Csaf = serde_json::from_str(example).unwrap();
let document: Csaf = serde_json_round_trip(example);
println!("{:#?}", document);
}
#[test]
fn second_example_deserializes() {
let example = include_str!("../tests/cvrf-rhba-2018-0489-modified.json");
let document: Csaf = serde_json::from_str(example).unwrap();
let document: Csaf = serde_json_round_trip(example);
println!("{:#?}", document);
}
#[test]
fn third_example_deserializes() {
let example = include_str!("../tests/rhba-2023_0564.json");
let document: Csaf = serde_json::from_str(example).unwrap();
let document: Csaf = serde_json_round_trip(example);
println!("{:#?}", document);
}
}
11 changes: 6 additions & 5 deletions src/vulnerability.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, FromInto};
use serde_with::{serde_as, TryFromInto};
use url::Url;

use crate::definitions::{AcknowledgmentsT, NotesT, ProductGroupsT, ProductsT, ReferencesT};
Expand Down Expand Up @@ -167,7 +167,7 @@ pub struct Score {
// TODO: A CVSS v2 representation, or just document it as out of scope
// TODO: Should have at least one of:
pub cvss_v2: Option<serde_json::Value>,
#[serde_as(as = "Option<FromInto<cvss_json::Cvss3>>")]
#[serde_as(as = "Option<TryFromInto<cvss_json::Cvss3>>")]
pub cvss_v3: Option<cvss::v3::Base>,
}

Expand Down Expand Up @@ -213,10 +213,11 @@ mod cvss_json {
}
}

impl From<Cvss3> for cvss::v3::Base {
fn from(b: Cvss3) -> Self {
impl TryFrom<Cvss3> for cvss::v3::Base {
type Error = <cvss::v3::Base as FromStr>::Err;

fn try_from(b: Cvss3) -> Result<Self, Self::Error> {
cvss::v3::Base::from_str(&b.vector_string)
.expect("You should not have been able to construct a cvss_json::Cvss3 except from a cvss::v3::Base which should always have a valid vector string")
}
}

Expand Down
11 changes: 11 additions & 0 deletions tests/rhba-2023_0564.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@
"branches": [
{
"branches": [
{
"category": "product_version",
"name": "eap7-resteasy-0:3.15.8-1.Final_redhat_00001.1.el8eap.src",
"product": {
"name": "eap7-resteasy-0:3.15.8-1.Final_redhat_00001.1.el8eap.src",
"product_id": "eap7-resteasy-0:3.15.8-1.Final_redhat_00001.1.el8eap.src",
"product_identification_helper": {
"purl": "pkg:rpm/redhat/eap7-resteasy@3.15.8-1.Final_redhat_00001.1.el8eap?arch=src"
}
}
},
{
"category": "product_name",
"name": "Red Hat OpenShift Container Platform 4.11",
Expand Down