Skip to content

Commit 08f8e9c

Browse files
committed
feat: license filter consistent for SBOM packages tab
Signed-off-by: mrizzi <[email protected]> Assisted-by: Claude Code
1 parent e710813 commit 08f8e9c

File tree

9 files changed

+462
-259
lines changed

9 files changed

+462
-259
lines changed

modules/fundamental/src/common/license_filtering.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use sea_query::{
77
extension::postgres::PgExpr,
88
};
99
use trustify_common::db::{
10-
ExpandLicenseExpression,
10+
CaseLicenseTextSbomId, ExpandLicenseExpression,
1111
query::{Columns, Filtering, IntoColumns, Query, q},
1212
};
1313
use trustify_entity::{license, sbom_package, sbom_package_license, sbom_package_purl_ref};
@@ -104,6 +104,21 @@ pub fn create_sbom_license_filtering_base_query() -> Select<sbom_package_license
104104
)
105105
}
106106

107+
/// Creates a base query for SBOM package license filtering (targeting packages within a specific SBOM)
108+
pub fn create_sbom_package_license_filtering_base_query(
109+
sbom_id: sea_orm::prelude::Uuid,
110+
) -> Select<sbom_package::Entity> {
111+
sbom_package::Entity::find()
112+
.filter(sbom_package::Column::SbomId.eq(sbom_id))
113+
.select_only()
114+
.column(sbom_package::Column::NodeId)
115+
.join(JoinType::Join, sbom_package::Relation::PackageLicense.def())
116+
.join(
117+
JoinType::Join,
118+
sbom_package_license::Relation::License.def(),
119+
)
120+
}
121+
107122
/// Applies license filtering to a query using a two-phase SPDX/CycloneDX approach
108123
///
109124
/// This function encapsulates the complete license filtering pattern used by both
@@ -158,3 +173,21 @@ where
158173
Ok(main_query)
159174
}
160175
}
176+
177+
/// Returns the case_license_text_sbom_id() PLSQL function that conditionally applies expand_license_expression() for SPDX LicenseRefs
178+
///
179+
/// This function generates a SQL CASE expression that:
180+
/// - Returns the expanded license expression when the license text contains 'LicenseRef-' (SPDX format)
181+
/// - Returns the original license text for all other cases (including CycloneDX)
182+
///
183+
/// This allows unified handling of both SPDX and CycloneDX licenses in a single query.
184+
pub fn get_case_license_text_sbom_id() -> SimpleExpr {
185+
SimpleExpr::FunctionCall(
186+
Func::cust(CaseLicenseTextSbomId)
187+
.arg(Expr::col((license::Entity, license::Column::Text)))
188+
.arg(Expr::col((
189+
sbom_package_license::Entity,
190+
sbom_package_license::Column::SbomId,
191+
))),
192+
)
193+
}

modules/fundamental/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#![recursion_limit = "256"]
2+
13
pub mod advisory;
24
pub mod common;
35
pub mod endpoints;

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

Lines changed: 32 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
22
Error,
3-
common::LicenseRefMapping,
3+
common::{LicenseRefMapping, license_filtering, license_filtering::LICENSE},
44
license::model::{
55
SpdxLicenseDetails, SpdxLicenseSummary,
66
sbom_license::{
@@ -10,11 +10,12 @@ use crate::{
1010
};
1111
use sea_orm::{
1212
ColumnTrait, ConnectionTrait, EntityTrait, FromQueryResult, QueryFilter, QuerySelect,
13-
RelationTrait, Statement,
13+
QueryTrait, RelationTrait, Statement,
14+
};
15+
use sea_query::{
16+
Alias, ColumnType, Condition, Expr, JoinType, Order::Asc, PostgresQueryBuilder, query,
1417
};
15-
use sea_query::{ColumnType, Condition, Expr, Func, JoinType, SimpleExpr};
1618
use serde::{Deserialize, Serialize};
17-
use trustify_common::db::CaseLicenseTextSbomId;
1819
use trustify_common::{
1920
db::{
2021
limiter::LimiterAsModelTrait,
@@ -232,52 +233,35 @@ impl LicenseService {
232233
.one(connection)
233234
.await?;
234235

236+
const EXPANDED_LICENSE: &str = "expanded_license";
237+
const LICENSE_NAME: &str = "license_name";
235238
match sbom {
236239
Some(sbom) => {
237-
let result: Vec<LicenseRefMapping> = LicenseRefMapping::find_by_statement(Statement::from_sql_and_values(
238-
connection.get_database_backend(),
239-
r#"
240-
(
241-
-- Successfully parsed (during SBOM ingestion) license ID values can be
242-
-- retrieved from the spdx_licenses column. The DISTINCT must be on lower values
243-
-- because the license identifiers have to be managed in case-insensitive way
244-
-- ref. https://spdx.github.io/spdx-spec/v3.0.1/annexes/spdx-license-expressions/#case-sensitivity
245-
SELECT DISTINCT on (lower(l.spdx_licenses)) l.spdx_licenses as license_name, l.spdx_licenses as license_id
246-
FROM sbom_package_license spl
247-
-- 'spdx_licenses' must be unnested and sorted before joining in order to ensure consistent results
248-
JOIN (
249-
SELECT id, unnest(spdx_licenses) as spdx_licenses
250-
FROM license
251-
ORDER BY id, spdx_licenses
252-
) AS l ON spl.license_id = l.id
253-
WHERE spl.sbom_id = $1
254-
AND l.spdx_licenses IS NOT NULL
255-
UNION
256-
-- CycloneDX SBOMs has NO "LicenseRef" by specifications (hence
257-
-- the above condition 'licensing_infos.license_id IS NULL') so
258-
-- all the values in the license.text whose spdx_licenses is null
259-
-- must be added to the result set. The need for the DISTINCT on lower is
260-
-- clearly explained above.
261-
SELECT DISTINCT ON (LOWER(l.text)) l.text as license_name, l.text as license_id
262-
FROM sbom_package_license spl
263-
JOIN license l ON spl.license_id = l.id
264-
LEFT JOIN licensing_infos ON licensing_infos.sbom_id = spl.sbom_id
265-
WHERE spl.sbom_id = $1
266-
AND l.spdx_licenses IS NULL
267-
AND licensing_infos.license_id IS NULL
268-
UNION
269-
-- SPDX SBOMs has "LicenseRef" by specifications and they're stored in
270-
-- licensing_infos and so their names have to be added as well
271-
SELECT DISTINCT name as license_name, license_id
272-
FROM licensing_infos
273-
WHERE sbom_id = $1
274-
ORDER BY license_name
240+
let expand_license_expression = sbom_package_license::Entity::find()
241+
.select_only()
242+
.distinct()
243+
.column_as(
244+
license_filtering::get_case_license_text_sbom_id(),
245+
EXPANDED_LICENSE,
246+
)
247+
.join(
248+
JoinType::Join,
249+
sbom_package_license::Relation::License.def(),
275250
)
276-
"#,
277-
[sbom.sbom_id.into()],
278-
))
279-
.all(connection)
280-
.await?;
251+
.filter(sbom_package_license::Column::SbomId.eq(sbom.sbom_id));
252+
let (sql, values) = query::Query::select()
253+
// reported twice to keep compatibility with LicenseRefMapping currently
254+
// exposed in the involved endpoint.
255+
.expr_as(Expr::col(Alias::new(EXPANDED_LICENSE)), LICENSE_NAME)
256+
.expr_as(Expr::col(Alias::new(EXPANDED_LICENSE)), "license_id")
257+
.from_subquery(expand_license_expression.into_query(), "expanded_licenses")
258+
.order_by(LICENSE_NAME, Asc)
259+
.build(PostgresQueryBuilder);
260+
let result: Vec<LicenseRefMapping> = LicenseRefMapping::find_by_statement(
261+
Statement::from_sql_and_values(connection.get_database_backend(), sql, values),
262+
)
263+
.all(connection)
264+
.await?;
281265
Ok(Some(result))
282266
}
283267
None => Ok(None),
@@ -290,15 +274,7 @@ impl LicenseService {
290274
paginated: Paginated,
291275
connection: &C,
292276
) -> Result<PaginatedResults<LicenseText>, Error> {
293-
let case_license_text_sbom_id = SimpleExpr::FunctionCall(
294-
Func::cust(CaseLicenseTextSbomId)
295-
.arg(Expr::col(license::Column::Text))
296-
.arg(Expr::col((
297-
sbom_package_license::Entity,
298-
sbom_package_license::Column::SbomId,
299-
))),
300-
);
301-
const LICENSE: &str = "license";
277+
let case_license_text_sbom_id = license_filtering::get_case_license_text_sbom_id();
302278
let limiter = license::Entity::find()
303279
.distinct()
304280
.select_only()

modules/fundamental/src/purl/endpoints/test.rs

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -281,32 +281,15 @@ async fn test_purl_license_details(ctx: &TrustifyContext) -> Result<(), anyhow::
281281
"advisories": [],
282282
"licenses": [
283283
{
284-
"license_name": "(LicenseRef-8 OR LicenseRef-0 OR LicenseRef-MPL) AND (LicenseRef-Netscape OR LicenseRef-0 OR LicenseRef-8)",
284+
"license_name": "(LGPLv2+ OR GPLv2+ OR MPL) AND (Netscape OR GPLv2+ OR LGPLv2+)",
285285
"license_type": "declared"
286286
},
287287
{
288288
"license_name": "NOASSERTION",
289289
"license_type": "concluded"
290290
}
291291
],
292-
"licenses_ref_mapping": [
293-
{
294-
"license_id": "LicenseRef-Netscape",
295-
"license_name": "Netscape"
296-
},
297-
{
298-
"license_id": "LicenseRef-MPL",
299-
"license_name": "MPL"
300-
},
301-
{
302-
"license_id": "LicenseRef-8",
303-
"license_name": "LGPLv2+"
304-
},
305-
{
306-
"license_id": "LicenseRef-0",
307-
"license_name": "GPLv2+"
308-
}
309-
]
292+
"licenses_ref_mapping": []
310293
});
311294
assert!(expected_result.contains_subset(response.clone()));
312295
Ok(())

modules/fundamental/src/purl/model/details/purl.rs

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
use crate::common::LicenseInfo;
2-
use crate::sbom::service::sbom::LicenseBasicInfo;
31
use crate::{
42
Error,
53
advisory::model::AdvisoryHead,
6-
common::{LicenseRefMapping, service::extract_license_ref_mappings},
4+
common::{LicenseInfo, LicenseRefMapping, license_filtering},
75
purl::model::{BasePurlHead, PurlHead, VersionedPurlHead},
8-
sbom::{model::SbomHead, service::SbomService},
6+
sbom::{model::SbomHead, service::sbom::LicenseBasicInfo},
97
vulnerability::model::VulnerabilityHead,
108
};
119
use sea_orm::{
@@ -41,17 +39,18 @@ pub struct PurlDetails {
4139
pub base: BasePurlHead,
4240
pub advisories: Vec<PurlAdvisory>,
4341
pub licenses: Vec<LicenseInfo>,
42+
#[deprecated]
4443
pub licenses_ref_mapping: Vec<LicenseRefMapping>,
4544
}
4645

4746
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema, FromQueryResult)]
4847
pub struct PurlLicenseResult {
49-
pub sbom_id: Uuid,
5048
pub license_name: String,
5149
pub license_type: i32,
5250
}
5351

5452
impl PurlDetails {
53+
#[allow(deprecated)]
5554
pub async fn from_entity<C: ConnectionTrait>(
5655
package: Option<base_purl::Model>,
5756
package_version: Option<versioned_purl::Model>,
@@ -107,11 +106,13 @@ impl PurlDetails {
107106
)
108107
.await?;
109108

110-
let purl_license_results: Vec<PurlLicenseResult> = sbom_package_purl_ref::Entity::find()
109+
let licenses: Vec<LicenseInfo> = sbom_package_purl_ref::Entity::find()
111110
.distinct()
112111
.select_only()
113-
.select_column(sbom_package::Column::SbomId)
114-
.select_column_as(license::Column::Text, "license_name")
112+
.column_as(
113+
license_filtering::get_case_license_text_sbom_id(),
114+
"license_name",
115+
)
115116
.select_column(sbom_package_license::Column::LicenseType)
116117
.filter(sbom_package_purl_ref::Column::QualifiedPurlId.eq(qualified_package.id))
117118
.join(
@@ -123,33 +124,25 @@ impl PurlDetails {
123124
JoinType::Join,
124125
sbom_package_license::Relation::License.def(),
125126
)
126-
.into_model()
127+
.into_model::<PurlLicenseResult>()
127128
.all(tx)
128-
.await?;
129-
130-
let mut purl_license_info = Vec::new();
131-
let mut license_ref_mapping = Vec::new();
132-
133-
for plr in purl_license_results {
134-
let licensing_infos = SbomService::get_licensing_infos(tx, plr.sbom_id).await?;
135-
extract_license_ref_mappings(
136-
plr.license_name.as_str(),
137-
&licensing_infos,
138-
&mut license_ref_mapping,
139-
);
140-
purl_license_info.push(LicenseInfo::from(LicenseBasicInfo {
141-
license_name: plr.license_name,
142-
license_type: plr.license_type,
143-
}));
144-
}
129+
.await?
130+
.iter()
131+
.map(|purl_license_result| {
132+
LicenseInfo::from(LicenseBasicInfo {
133+
license_name: purl_license_result.license_name.clone(),
134+
license_type: purl_license_result.license_type,
135+
})
136+
})
137+
.collect();
145138

146139
Ok(PurlDetails {
147140
head: PurlHead::from_entity(&package, &package_version, qualified_package),
148141
version: VersionedPurlHead::from_entity(&package, &package_version),
149142
base: BasePurlHead::from_entity(&package),
150143
advisories: PurlAdvisory::from_entities(purl_statuses, product_statuses, tx).await?,
151-
licenses: purl_license_info,
152-
licenses_ref_mapping: license_ref_mapping,
144+
licenses,
145+
licenses_ref_mapping: vec![],
153146
})
154147
}
155148
}

0 commit comments

Comments
 (0)