sbom_walker/model/sbom/
mod.rs

1//! SBOM Model
2
3#[cfg(feature = "serde-cyclonedx")]
4pub mod serde_cyclonedx;
5
6mod json;
7
8pub use json::JsonPayload;
9
10use anyhow::{anyhow, bail};
11use serde_json::Value;
12use std::fmt::{Debug, Display, Formatter};
13
14pub enum Parser {
15    CycloneDxJson,
16    CycloneDxXml,
17}
18
19/// A tool to work with multiple SBOM formats and versions
20#[allow(clippy::large_enum_variant)]
21#[derive(Clone, Debug, PartialEq)]
22pub enum Sbom {
23    #[cfg(feature = "spdx-rs")]
24    Spdx(spdx_rs::models::SPDX),
25    #[cfg(feature = "cyclonedx-bom")]
26    #[deprecated(
27        since = "0.12.0",
28        note = "Replaced with serde_cyclondex and the SerdeCycloneDx variant"
29    )]
30    CycloneDx(cyclonedx_bom::prelude::Bom),
31    #[cfg(feature = "serde-cyclonedx")]
32    SerdeCycloneDx(serde_cyclonedx::Sbom<'static>),
33}
34
35#[derive(Default, Debug)]
36pub struct ParseAnyError(pub Vec<(ParserKind, anyhow::Error)>);
37
38impl std::error::Error for ParseAnyError {}
39
40impl From<(ParserKind, anyhow::Error)> for ParseAnyError {
41    fn from(value: (ParserKind, anyhow::Error)) -> Self {
42        Self(vec![value])
43    }
44}
45
46impl ParseAnyError {
47    pub fn new() -> Self {
48        Self(vec![])
49    }
50
51    pub fn add(mut self, kind: ParserKind, error: anyhow::Error) -> Self {
52        self.0.push((kind, error));
53        self
54    }
55}
56
57impl Display for ParseAnyError {
58    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
59        write!(f, "failed to parse SBOM:")?;
60        if self.0.is_empty() {
61            write!(f, "failed to parse SBOM: no parser configured")?;
62        } else if self.0.len() == 1 {
63            write!(f, "{}: {}", self.0[0].0, self.0[0].1)?;
64        } else {
65            writeln!(f)?;
66            for (kind, err) in &self.0 {
67                writeln!(f, "  {kind}: {err}")?;
68            }
69        }
70
71        Ok(())
72    }
73}
74
75#[derive(Copy, Clone, Debug, PartialEq, Eq)]
76pub enum ParserKind {
77    Cyclone13DxJson,
78    Cyclone13DxXml,
79    Spdx23Json,
80    Spdx23Tag,
81}
82
83impl Display for ParserKind {
84    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
85        match self {
86            Self::Cyclone13DxJson => write!(f, "CycloneDX 1.3 JSON"),
87            Self::Cyclone13DxXml => write!(f, "CycloneDX 1.3 XML"),
88            Self::Spdx23Json => write!(f, "SPDX 2.3 JSON"),
89            Self::Spdx23Tag => write!(f, "SPDX 2.3 tagged"),
90        }
91    }
92}
93
94impl Sbom {
95    /// test if the file is a CycloneDX document, returning the file version
96    pub fn is_cyclondx_json(json: &Value) -> anyhow::Result<&str> {
97        let format = json["bomFormat"]
98            .as_str()
99            .ok_or_else(|| anyhow!("Missing field 'bomFormat'"))?;
100
101        if format != "CycloneDX" {
102            bail!("Unknown CycloneDX 'bomFormat' value: {format}");
103        }
104
105        let spec_version = json["specVersion"]
106            .as_str()
107            .ok_or_else(|| anyhow!("Missing field 'specVersion'"))?;
108
109        Ok(spec_version)
110    }
111
112    /// test if the file is a SPDX document, returning the file version
113    pub fn is_spdx_json(json: &Value) -> anyhow::Result<&str> {
114        let version = json["spdxVersion"]
115            .as_str()
116            .ok_or_else(|| anyhow!("Missing field 'spdxVersion'"))?;
117
118        Ok(version)
119    }
120
121    pub fn try_parse_any_json(json: Value) -> Result<Self, ParseAnyError> {
122        let err = ParseAnyError::new();
123
124        #[cfg(feature = "serde-cyclonedx")]
125        let err = match Self::is_cyclondx_json(&json) {
126            Ok("1.4" | "1.5" | "1.6") => {
127                return Self::try_serde_cyclonedx_json(JsonPayload::Value(json)).map_err(|e| {
128                    // drop any previous error, as we know what format and version it is
129                    ParseAnyError::from((ParserKind::Cyclone13DxJson, e.into()))
130                });
131            }
132            Ok(version) => {
133                // We can stop here, and drop any previous error, as we know what the format is.
134                // But we disagree with the version.
135                return Err(ParseAnyError::from((
136                    ParserKind::Cyclone13DxJson,
137                    anyhow!("Unsupported CycloneDX version: {version}"),
138                )));
139            }
140            // failed to detect as CycloneDX, record error and move on
141            Err(e) => err.add(ParserKind::Cyclone13DxJson, e),
142        };
143
144        #[cfg(feature = "cyclonedx-bom")]
145        let err = match Self::is_cyclondx_json(&json) {
146            Ok("1.2" | "1.3" | "1.4") => {
147                return Self::try_cyclonedx_json(JsonPayload::Value(json)).map_err(|e| {
148                    // drop any previous error, as we know what format and version it is
149                    ParseAnyError::from((ParserKind::Cyclone13DxJson, e.into()))
150                });
151            }
152            Ok(version) => {
153                // We can stop here, and drop any previous error, as we know what the format is.
154                // But we disagree with the version.
155                return Err(ParseAnyError::from((
156                    ParserKind::Cyclone13DxJson,
157                    anyhow!("Unsupported CycloneDX version: {version}"),
158                )));
159            }
160            // failed to detect as CycloneDX, record error and move on
161            Err(e) => err.add(ParserKind::Cyclone13DxJson, e),
162        };
163
164        #[cfg(feature = "spdx-rs")]
165        let err = match Self::is_spdx_json(&json) {
166            Ok("SPDX-2.2" | "SPDX-2.3") => {
167                return Self::try_spdx_json(JsonPayload::Value(json)).map_err(|e| {
168                    // drop any previous error, as we know what format and version it is
169                    ParseAnyError::from((ParserKind::Spdx23Json, e.into()))
170                });
171            }
172            Ok(version) => {
173                // We can stop here, and drop any previous error, as we know what the format is.
174                // But we disagree with the version.
175                return Err(ParseAnyError::from((
176                    ParserKind::Spdx23Json,
177                    anyhow!("Unsupported SPDX version: {version}"),
178                )));
179            }
180            Err(e) => err.add(ParserKind::Spdx23Json, e),
181        };
182
183        // mark as unused for clippy
184        let _json = json;
185        Err(err)
186    }
187
188    /// try parsing with all possible kinds that make sense.
189    pub fn try_parse_any(data: &[u8]) -> Result<Self, ParseAnyError> {
190        #[allow(unused)]
191        if let Ok(json) = serde_json::from_slice(data) {
192            // try to parse this as JSON, which eliminates e.g. the "tag" format, which seems to just parse anything
193
194            Self::try_parse_any_json(json)
195        } else {
196            // it is not JSON, it could be XML or "tagged"
197            let err = ParseAnyError::new();
198
199            #[cfg(feature = "cyclonedx-bom")]
200            let err = match Self::try_cyclonedx_xml(data) {
201                Ok(doc) => return Ok(doc),
202                Err(e) => err.add(ParserKind::Cyclone13DxXml, e.into()),
203            };
204
205            #[cfg(feature = "spdx-rs")]
206            use anyhow::Context;
207
208            #[cfg(feature = "spdx-rs")]
209            let err = match std::str::from_utf8(data)
210                .context("unable to interpret bytes as string")
211                .and_then(|data| Self::try_spdx_tag(data).map_err(|err| err.into()))
212            {
213                Ok(doc) => return Ok(doc),
214                Err(e) => err.add(ParserKind::Spdx23Tag, e),
215            };
216
217            Err(err)
218        }
219    }
220
221    #[cfg(feature = "spdx-rs")]
222    pub fn try_spdx_json(data: JsonPayload) -> Result<Self, serde_json::Error> {
223        Ok(Self::Spdx(data.parse()?))
224    }
225
226    #[cfg(feature = "spdx-rs")]
227    pub fn try_spdx_tag(data: &str) -> Result<Self, spdx_rs::error::SpdxError> {
228        Ok(Self::Spdx(spdx_rs::parsers::spdx_from_tag_value(data)?))
229    }
230
231    #[cfg(feature = "cyclonedx-bom")]
232    #[allow(deprecated)]
233    pub fn try_cyclonedx_json<'a>(
234        data: impl Into<JsonPayload<'a>>,
235    ) -> Result<Self, cyclonedx_bom::errors::JsonReadError> {
236        use cyclonedx_bom::prelude::Bom;
237
238        match data.into() {
239            JsonPayload::Value(json) => Ok(Self::CycloneDx(Bom::parse_json_value(json)?)),
240            JsonPayload::Bytes(data) => Ok(Self::CycloneDx(Bom::parse_from_json(data)?)),
241        }
242    }
243
244    #[cfg(feature = "cyclonedx-bom")]
245    #[allow(deprecated)]
246    pub fn try_cyclonedx_xml(data: &[u8]) -> Result<Self, cyclonedx_bom::errors::XmlReadError> {
247        Ok(Self::CycloneDx(
248            cyclonedx_bom::prelude::Bom::parse_from_xml_v1_3(data)?,
249        ))
250    }
251
252    #[cfg(feature = "serde-cyclonedx")]
253    pub fn try_serde_cyclonedx_json<'a>(
254        data: impl Into<JsonPayload<'a>>,
255    ) -> Result<Self, serde_json::Error> {
256        match data.into() {
257            JsonPayload::Value(json) => Ok(Self::SerdeCycloneDx(serde_json::from_value(json)?)),
258            JsonPayload::Bytes(data) => Ok(Self::SerdeCycloneDx(serde_json::from_slice(data)?)),
259        }
260    }
261}