Skip to main content

cyclonedx_bom/models/
composition.rs

1/*
2 * This file is part of CycloneDX Rust Cargo.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16 * SPDX-License-Identifier: Apache-2.0
17 */
18
19use crate::validation::{Validate, ValidationContext, ValidationError, ValidationResult};
20
21use super::{
22    bom::{BomReference, SpecVersion},
23    signature::Signature,
24};
25
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct Composition {
28    pub bom_ref: Option<BomReference>,
29    pub aggregate: AggregateType,
30    pub assemblies: Option<Vec<BomReference>>,
31    pub dependencies: Option<Vec<BomReference>>,
32    pub vulnerabilities: Option<Vec<BomReference>>,
33    pub signature: Option<Signature>,
34}
35
36impl Validate for Composition {
37    fn validate_version(&self, version: SpecVersion) -> ValidationResult {
38        ValidationContext::new()
39            .add_field("aggregate", &self.aggregate, |at| {
40                validate_aggregate_type(at, version)
41            })
42            .add_struct_option("signature", self.signature.as_ref(), version)
43            .into()
44    }
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
48pub struct Compositions(pub Vec<Composition>);
49
50impl Validate for Compositions {
51    fn validate_version(&self, version: SpecVersion) -> ValidationResult {
52        ValidationContext::new()
53            .add_list("composition", &self.0, |composition| {
54                composition.validate_version(version)
55            })
56            .into()
57    }
58}
59
60/// Validates the given [`AggregateType`].
61pub fn validate_aggregate_type(
62    aggregate_type: &AggregateType,
63    version: SpecVersion,
64) -> Result<(), ValidationError> {
65    if version <= SpecVersion::V1_4 {
66        if AggregateType::IncompleteFirstPartyProprietaryOnly < *aggregate_type {
67            return Err("Unknown aggregate type".into());
68        }
69    } else if version <= SpecVersion::V1_5
70        && matches!(aggregate_type, AggregateType::UnknownAggregateType(_))
71    {
72        return Err(ValidationError::new("Unknown aggregate type"));
73    }
74    Ok(())
75}
76
77#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, strum::Display)]
78#[strum(serialize_all = "snake_case")]
79#[repr(u16)]
80pub enum AggregateType {
81    Complete = 1,
82    Incomplete = 2,
83    IncompleteFirstPartyOnly = 3,
84    IncompleteThirdPartyOnly = 4,
85    Unknown = 5,
86    NotSpecified = 6,
87    /// Added in 1.5
88    IncompleteFirstPartyProprietaryOnly = 7,
89    /// Added in 1.5
90    IncompleteFirstPartyOpensourceOnly = 8,
91    /// Added in 1.5
92    IncompleteThirdPartyProprietaryOnly = 9,
93    /// Added in 1.5
94    IncompleteThirdPartyOpensourceOnly = 10,
95    #[doc(hidden)]
96    #[strum(default)]
97    UnknownAggregateType(String),
98}
99
100impl AggregateType {
101    pub fn new_unchecked<A: AsRef<str>>(value: A) -> Self {
102        match value.as_ref() {
103            "complete" => Self::Complete,
104            "incomplete" => Self::Incomplete,
105            "incomplete_first_party_only" => Self::IncompleteFirstPartyOnly,
106            "incomplete_first_party_propprietary_only" => Self::IncompleteFirstPartyProprietaryOnly,
107            "incomplete_first_party_opensource_only" => Self::IncompleteFirstPartyOpensourceOnly,
108            "incomplete_third_party_only" => Self::IncompleteThirdPartyOnly,
109            "incomplete_third_party_proprietary_only" => Self::IncompleteThirdPartyProprietaryOnly,
110            "incomplete_third_party_opensource_only" => Self::IncompleteThirdPartyOpensourceOnly,
111            "unknown" => Self::Unknown,
112            "not_specified" => Self::NotSpecified,
113            unknown => Self::UnknownAggregateType(unknown.to_string()),
114        }
115    }
116}
117
118#[cfg(test)]
119mod test {
120    use crate::{models::signature::Algorithm, validation};
121
122    use super::*;
123    use pretty_assertions::assert_eq;
124
125    #[test]
126    fn it_should_pass_validation() {
127        let validation_result = Compositions(vec![Composition {
128            bom_ref: Some(BomReference::new("composition-1")),
129            aggregate: AggregateType::Complete,
130            assemblies: Some(vec![BomReference::new("assembly-ref")]),
131            dependencies: Some(vec![BomReference::new("dependency-ref")]),
132            vulnerabilities: Some(vec![BomReference::new("vulnerability-ref")]),
133            signature: Some(Signature::single(Algorithm::HS512, "abcdefgh")),
134        }])
135        .validate();
136
137        assert!(validation_result.passed());
138    }
139
140    #[test]
141    fn it_should_fail_validation() {
142        let validation_result = Compositions(vec![Composition {
143            bom_ref: Some(BomReference::new("composition-1")),
144            aggregate: AggregateType::UnknownAggregateType("unknown aggregate type".to_string()),
145            assemblies: Some(vec![BomReference::new("assembly-ref")]),
146            dependencies: Some(vec![BomReference::new("dependency-ref")]),
147            vulnerabilities: Some(vec![BomReference::new("vulnerability-ref")]),
148            signature: Some(Signature::single(Algorithm::HS512, "abcdefgh")),
149        }])
150        .validate();
151
152        assert_eq!(
153            validation_result,
154            validation::list(
155                "composition",
156                [(
157                    0,
158                    validation::r#field("aggregate", "Unknown aggregate type")
159                )]
160            )
161        );
162    }
163}