Skip to content

Commit 3cf0d1a

Browse files
committed
feat: refactored license type management in CSV license export (#10)
Signed-off-by: mrizzi <[email protected]>
1 parent 2e915d7 commit 3cf0d1a

File tree

5 files changed

+59
-144
lines changed

5 files changed

+59
-144
lines changed

entity/src/sbom_package_license.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub enum Relation {
2727
License,
2828
}
2929

30-
#[derive(Copy, Clone, Debug, Eq, PartialEq, EnumIter, DeriveActiveEnum)]
30+
#[derive(Copy, Clone, Debug, strum::Display, Eq, PartialEq, EnumIter, DeriveActiveEnum)]
3131
#[sea_orm(rs_type = "i32", db_type = "Integer")]
3232
pub enum LicenseCategory {
3333
Declared = 0,
Lines changed: 10 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,33 @@
11
use sea_orm::FromQueryResult;
2-
use trustify_entity::{
3-
labels::Labels, qualified_purl::CanonicalPurl, sbom_package_license::LicenseCategory,
4-
};
2+
use trustify_entity::{qualified_purl::CanonicalPurl, sbom_package_license::LicenseCategory};
53
use uuid::Uuid;
64

7-
#[derive(Debug, Clone, PartialEq, Default)]
5+
#[derive(Debug, Clone, Default)]
86
pub struct SbomPackageLicense {
97
pub name: String,
108
pub group: Option<String>,
119
pub version: Option<String>,
1210
/// package package URL
1311
pub purl: Vec<Purl>,
1412
pub cpe: Vec<trustify_entity::cpe::Model>,
15-
/// List of all package license
16-
pub license_declared_text: Option<String>,
17-
pub license_concluded_text: Option<String>,
13+
/// List of all package licenses with their types
14+
pub license_text: Option<String>,
15+
pub license_type: Option<LicenseCategory>,
1816
}
1917

20-
#[derive(Debug, Clone, PartialEq, FromQueryResult)]
18+
#[derive(Debug, Clone, FromQueryResult)]
2119
pub struct Sbom {
2220
pub sbom_id: Uuid,
2321
pub node_id: String,
2422
pub sbom_namespace: String,
2523
}
2624

27-
#[derive(Debug, Clone, PartialEq, FromQueryResult)]
25+
#[derive(Debug, Clone, FromQueryResult)]
2826
pub struct Purl {
2927
pub purl: CanonicalPurl,
3028
}
3129

32-
#[derive(Debug, Clone, PartialEq, FromQueryResult)]
30+
#[derive(Debug, Clone, FromQueryResult)]
3331
pub struct SbomPackageLicenseBase {
3432
pub node_id: String,
3533
pub sbom_id: Uuid,
@@ -40,43 +38,16 @@ pub struct SbomPackageLicenseBase {
4038
pub license_type: Option<LicenseCategory>,
4139
}
4240

43-
#[derive(Debug, Clone, Default, PartialEq, FromQueryResult)]
41+
#[derive(Debug, Clone, Default, FromQueryResult)]
4442
pub struct SbomNameId {
4543
pub sbom_name: String,
4644
pub sbom_id: String,
47-
pub labels: Labels,
4845
}
4946

50-
#[derive(Debug, Clone, PartialEq, FromQueryResult)]
47+
#[derive(Debug, Clone, FromQueryResult)]
5148
pub struct ExtractedLicensingInfos {
5249
pub license_id: String,
5350
pub name: String,
5451
pub extracted_text: String,
5552
pub comment: String,
5653
}
57-
58-
#[derive(Debug, Clone, PartialEq)]
59-
pub struct MergedSbomPackageLicense {
60-
pub node_id: String,
61-
pub sbom_id: Uuid,
62-
pub name: String,
63-
pub group: Option<String>,
64-
pub version: Option<String>,
65-
pub license_declared_text: Option<String>,
66-
pub license_concluded_text: Option<String>,
67-
}
68-
69-
impl MergedSbomPackageLicense {
70-
pub fn apply_license(&mut self, license: &SbomPackageLicenseBase) {
71-
if let Some(license_type) = &license.license_type {
72-
match license_type {
73-
LicenseCategory::Declared => {
74-
self.license_declared_text = license.license_text.clone();
75-
}
76-
LicenseCategory::Concluded => {
77-
self.license_concluded_text = license.license_text.clone();
78-
}
79-
}
80-
}
81-
}
82-
}

modules/fundamental/src/license/service/license_export.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use csv::{Writer, WriterBuilder};
1010
use flate2::{Compression, write::GzEncoder};
1111
use tar::Builder;
1212
use trustify_common::purl::Purl;
13+
use trustify_entity::sbom_package_license::LicenseCategory;
1314

1415
type CSVs = (Writer<Vec<u8>>, Writer<Vec<u8>>);
1516

@@ -111,8 +112,8 @@ impl LicenseExporter {
111112
"package version",
112113
"package purl",
113114
"package cpe",
114-
"declared license",
115-
"concluded license",
115+
"license",
116+
"license type",
116117
])?;
117118

118119
for extracted_licensing_info in self.extracted_licensing_infos {
@@ -135,7 +136,7 @@ impl LicenseExporter {
135136
let purl_list = package
136137
.purl
137138
.into_iter()
138-
.map(|purl| format!("{}", Purl::from(purl.purl.clone())))
139+
.map(|purl| format!("{}", Purl::from(purl.purl)))
139140
.collect::<Vec<_>>()
140141
.join("\n");
141142

@@ -147,12 +148,11 @@ impl LicenseExporter {
147148
&package.version.unwrap_or_default(),
148149
&purl_list,
149150
&alternate_package_reference,
151+
&package.license_text.unwrap_or_else(String::default),
150152
&package
151-
.license_declared_text
152-
.unwrap_or_else(String::default),
153-
&package
154-
.license_concluded_text
155-
.unwrap_or_else(String::default),
153+
.license_type
154+
.unwrap_or(LicenseCategory::Declared)
155+
.to_string(),
156156
])?;
157157
}
158158

modules/fundamental/src/license/service/mod.rs

Lines changed: 4 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@ use crate::{
33
license::model::{
44
SpdxLicenseDetails, SpdxLicenseSummary,
55
sbom_license::{
6-
ExtractedLicensingInfos, MergedSbomPackageLicense, Purl, SbomNameId,
7-
SbomPackageLicense, SbomPackageLicenseBase,
6+
ExtractedLicensingInfos, Purl, SbomNameId, SbomPackageLicense, SbomPackageLicenseBase,
87
},
98
},
109
};
1110
use sea_orm::{ColumnTrait, ConnectionTrait, EntityTrait, QueryFilter, QuerySelect, RelationTrait};
1211
use sea_query::{Condition, JoinType};
13-
use std::collections::HashMap;
1412
use trustify_common::{
1513
db::query::Query,
1614
id::{Id, TrySelectForId},
@@ -20,28 +18,17 @@ use trustify_entity::{
2018
license, licensing_infos, qualified_purl, sbom, sbom_node, sbom_package, sbom_package_cpe_ref,
2119
sbom_package_license, sbom_package_purl_ref,
2220
};
23-
use uuid::Uuid;
2421

2522
pub mod license_export;
2623

2724
pub struct LicenseService {}
2825

29-
#[derive(Debug, Clone)]
3026
pub struct LicenseExportResult {
3127
pub sbom_package_license: Vec<SbomPackageLicense>,
3228
pub extracted_licensing_infos: Vec<ExtractedLicensingInfos>,
3329
pub sbom_name_group_version: Option<SbomNameId>,
3430
}
3531

36-
#[derive(Eq, Hash, PartialEq)]
37-
pub struct SbomPackageLicenseBasekey {
38-
pub node_id: String,
39-
pub sbom_id: Uuid,
40-
pub name: String,
41-
pub group: Option<String>,
42-
pub version: Option<String>,
43-
}
44-
4532
impl Default for LicenseService {
4633
fn default() -> Self {
4734
Self::new()
@@ -64,7 +51,6 @@ impl LicenseService {
6451
.select_only()
6552
.column_as(sbom::Column::DocumentId, "sbom_id")
6653
.column_as(sbom_node::Column::Name, "sbom_name")
67-
.column_as(sbom::Column::Labels, "labels")
6854
.into_model::<SbomNameId>()
6955
.one(connection)
7056
.await?;
@@ -93,66 +79,8 @@ impl LicenseService {
9379
.all(connection)
9480
.await?;
9581

96-
fn merge_package_licenses_for_spdx(
97-
package_licenses: Vec<SbomPackageLicenseBase>,
98-
) -> Vec<MergedSbomPackageLicense> {
99-
let mut grouped: HashMap<SbomPackageLicenseBasekey, MergedSbomPackageLicense> =
100-
HashMap::new();
101-
102-
for license in package_licenses {
103-
let key = SbomPackageLicenseBasekey {
104-
node_id: license.node_id.clone(),
105-
sbom_id: license.sbom_id,
106-
name: license.name.clone(),
107-
group: license.group.clone(),
108-
version: license.version.clone(),
109-
};
110-
111-
grouped
112-
.entry(key)
113-
.or_insert_with(|| MergedSbomPackageLicense {
114-
node_id: license.node_id.clone(),
115-
sbom_id: license.sbom_id,
116-
name: license.name.clone(),
117-
group: license.group.clone(),
118-
version: license.version.clone(),
119-
license_declared_text: None,
120-
license_concluded_text: None,
121-
})
122-
.apply_license(&license);
123-
}
124-
grouped.into_values().collect()
125-
}
126-
127-
fn merge_package_licenses_for_cydx(
128-
package_licenses: Vec<SbomPackageLicenseBase>,
129-
) -> Vec<MergedSbomPackageLicense> {
130-
package_licenses
131-
.into_iter()
132-
.map(|cydx| MergedSbomPackageLicense {
133-
node_id: cydx.node_id,
134-
sbom_id: cydx.sbom_id,
135-
name: cydx.name,
136-
group: cydx.group,
137-
version: cydx.version,
138-
license_declared_text: cydx.license_text,
139-
license_concluded_text: None,
140-
})
141-
.collect()
142-
}
143-
144-
let package_license_list = if let Some(nvg) = name_version_group.clone() {
145-
if nvg.labels.0.get("type").map(String::as_str) == Some("spdx") {
146-
merge_package_licenses_for_spdx(package_license)
147-
} else {
148-
merge_package_licenses_for_cydx(package_license)
149-
}
150-
} else {
151-
merge_package_licenses_for_cydx(package_license)
152-
};
153-
15482
let mut sbom_package_list = Vec::new();
155-
for spl in package_license_list {
83+
for spl in package_license {
15684
let result_purl: Vec<Purl> = sbom_package_purl_ref::Entity::find()
15785
.join(JoinType::Join, sbom_package_purl_ref::Relation::Purl.def())
15886
.filter(
@@ -191,8 +119,8 @@ impl LicenseService {
191119
version: spl.version,
192120
purl: result_purl,
193121
cpe: result_cpe,
194-
license_declared_text: spl.license_declared_text,
195-
license_concluded_text: spl.license_concluded_text,
122+
license_text: spl.license_text,
123+
license_type: spl.license_type,
196124
});
197125
}
198126
let license_info_list: Vec<ExtractedLicensingInfos> = licensing_infos::Entity::find()

modules/fundamental/tests/sbom/license.rs

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::io::Read;
44
use tar::Archive;
55
use test_context::test_context;
66
use test_log::test;
7+
use trustify_entity::sbom_package_license::LicenseCategory;
78
use trustify_entity::{sbom_package, sbom_package_license};
89
use trustify_module_fundamental::license::{
910
model::sbom_license::SbomNameId,
@@ -57,20 +58,35 @@ async fn test_spdx(ctx: &TrustifyContext) -> Result<(), anyhow::Error> {
5758
let spl: Vec<sbom_package_license::Model> =
5859
sbom_package_license::Entity::find().all(&ctx.db).await?;
5960

60-
assert_eq!(2084, license_result.sbom_package_license.len());
61+
assert_eq!(4168, license_result.sbom_package_license.len());
6162

6263
let package_license_result = license_result
6364
.sbom_package_license
64-
.iter()
65-
.find(|sl| sl.name == "rubygem-bundler_ext");
66-
assert_ne!(Option::None, package_license_result);
67-
if let Some(plr) = package_license_result {
68-
assert_eq!(Some("LicenseRef-0"), plr.license_declared_text.as_deref());
69-
assert_eq!(
70-
Some("Apache-2.0 AND MIT"),
71-
plr.license_concluded_text.as_deref()
72-
);
73-
}
65+
.into_iter()
66+
.filter(|sl| sl.name == "rubygem-bundler_ext")
67+
.collect::<Vec<_>>();
68+
// there are 4 with the same name but different qualifiers: two has 'arch=src' and
69+
// the other two have 'arch=noarch'.
70+
// Each qualifier appears twice: once for the declared license and once for the concluded one.
71+
assert_eq!(4, package_license_result.len());
72+
assert_eq!(
73+
2,
74+
package_license_result
75+
.iter()
76+
.filter(|plr| plr.license_text == Some("LicenseRef-0".to_string())
77+
&& plr.license_type == Some(LicenseCategory::Declared))
78+
.count()
79+
);
80+
assert_eq!(
81+
2,
82+
package_license_result
83+
.iter()
84+
.filter(
85+
|plr| plr.license_text == Some("Apache-2.0 AND MIT".to_string())
86+
&& plr.license_type == Some(LicenseCategory::Concluded)
87+
)
88+
.count()
89+
);
7490
assert_eq!(2084, sp.len());
7591
assert_eq!(4168, spl.len());
7692
assert_eq!(49, license_result.extracted_licensing_infos.len());
@@ -100,7 +116,7 @@ async fn test_license_export_spdx(ctx: &TrustifyContext) -> Result<(), anyhow::E
100116
license_result.extracted_licensing_infos.clone(),
101117
);
102118
assert_eq!(45, license_result.extracted_licensing_infos.len());
103-
assert_eq!(5388, license_result.sbom_package_license.len());
119+
assert_eq!(10776, license_result.sbom_package_license.len());
104120

105121
let compressed_data = exporter
106122
.generate()
@@ -117,20 +133,20 @@ async fn test_license_export_spdx(ctx: &TrustifyContext) -> Result<(), anyhow::E
117133
licenses_csv_found = true;
118134
let mut sbom_licenses = String::new();
119135
entry.read_to_string(&mut sbom_licenses)?;
120-
assert_eq!(10777, sbom_licenses.matches("MTV-2.6").count());
136+
assert_eq!(21554, sbom_licenses.matches("MTV-2.6").count());
121137
assert_eq!(
122-
5388,
138+
10776,
123139
sbom_licenses
124140
.matches("https://access.redhat.com/security/data/sbom/spdx/MTV-2.6")
125141
.count()
126142
);
127-
assert_eq!(28, sbom_licenses.matches("pkg:oci/").count());
128-
assert_eq!(1976, sbom_licenses.matches("pkg:npm/").count());
129-
assert_eq!(2185, sbom_licenses.matches("pkg:golang/").count());
130-
assert_eq!(1191, sbom_licenses.matches("pkg:rpm/").count());
143+
assert_eq!(56, sbom_licenses.matches("pkg:oci/").count());
144+
assert_eq!(3952, sbom_licenses.matches("pkg:npm/").count());
145+
assert_eq!(4370, sbom_licenses.matches("pkg:golang/").count());
146+
assert_eq!(2382, sbom_licenses.matches("pkg:rpm/").count());
131147
assert_eq!(8972, sbom_licenses.matches("NOASSERTION").count());
132-
assert_eq!(1, sbom_licenses.matches("declared license").count());
133-
assert_eq!(1, sbom_licenses.matches("concluded license").count());
148+
assert_eq!(5388, sbom_licenses.matches("Declared").count());
149+
assert_eq!(5388, sbom_licenses.matches("Concluded").count());
134150
}
135151
Ok(path) if path.file_name().unwrap_or_default() == "MTV-2.6_license_ref.csv" => {
136152
licenses_ref_csv_found = true;

0 commit comments

Comments
 (0)