diff --git a/modules/analysis/src/endpoints.rs b/modules/analysis/src/endpoints.rs index 9402ee3cc..203cbf41c 100644 --- a/modules/analysis/src/endpoints.rs +++ b/modules/analysis/src/endpoints.rs @@ -407,4 +407,57 @@ mod test { ); Ok(assert_eq!(&response["total"], 2)) } + + #[test_context(TrustifyContext)] + #[test(actix_web::test)] + async fn test_retrieve_query_params_endpoint( + ctx: &TrustifyContext, + ) -> Result<(), anyhow::Error> { + let app = caller(ctx).await?; + ctx.ingest_documents(["spdx/simple.json"]).await?; + + // filter on node_id + let uri = "/api/v1/analysis/dep?q=node_id%3DSPDXRef-A"; + let request: Request = TestRequest::get().uri(uri).to_request(); + let response: Value = app.call_and_read_body_json(request).await; + assert_eq!(response["items"][0]["name"], "A"); + assert_eq!(&response["total"], 1); + + // filter on node_id + let uri = "/api/v1/analysis/root-component?q=node_id%3DSPDXRef-B"; + let request: Request = TestRequest::get().uri(uri).to_request(); + let response: Value = app.call_and_read_body_json(request).await; + assert_eq!(response["items"][0]["name"], "B"); + assert_eq!(&response["total"], 1); + + // filter on node_id & name + let uri = "/api/v1/analysis/root-component?q=node_id%3DSPDXRef-B%26name%3DB"; + let request: Request = TestRequest::get().uri(uri).to_request(); + let response: Value = app.call_and_read_body_json(request).await; + assert_eq!(response["items"][0]["name"], "B"); + assert_eq!(&response["total"], 1); + + // filter on sbom_id (which has urn:uuid: prefix) + let sbom_id = response["items"][0]["sbom_id"].as_str().unwrap(); + let uri = format!( + "/api/v1/analysis/root-component?q=sbom_id=urn:uuid:{}", + sbom_id + ); + let request: Request = TestRequest::get().uri(uri.clone().as_str()).to_request(); + let response: Value = app.call_and_read_body_json(request).await; + assert_eq!(&response["total"], 7); + + // negative test + let uri = "/api/v1/analysis/root-component?q=sbom_id=urn:uuid:99999999-9999-9999-9999-999999999999"; + let request: Request = TestRequest::get().uri(uri).to_request(); + let response: Value = app.call_and_read_body_json(request).await; + assert_eq!(&response["total"], 0); + + // negative test + let uri = "/api/v1/analysis/root-component?q=node_id%3DSPDXRef-B%26name%3DA"; + let request: Request = TestRequest::get().uri(uri).to_request(); + let response: Value = app.call_and_read_body_json(request).await; + + Ok(assert_eq!(&response["total"], 0)) + } } diff --git a/modules/analysis/src/service.rs b/modules/analysis/src/service.rs index 5150a0af5..97ab2952f 100644 --- a/modules/analysis/src/service.rs +++ b/modules/analysis/src/service.rs @@ -19,6 +19,7 @@ use petgraph::visit::{NodeIndexable, VisitMap, Visitable}; use petgraph::Direction; use sea_query::Order; use std::str::FromStr; +use trustify_common::db::query::Filtering; use trustify_common::db::ConnectionOrTransaction; use trustify_common::purl::Purl; use trustify_entity::relationship::Relationship; @@ -273,6 +274,23 @@ pub async fn load_graphs( } } +fn convert_query_to_hashmap(query: &Query) -> HashMap { + let mut query_map = HashMap::new(); + if query.q.contains('=') { + for pair in query.q.split('&') { + if let Some((key, mut value)) = pair.split_once('=') { + if value.starts_with("urn:uuid:") { + value = value.strip_prefix("urn:uuid:").unwrap_or(value); + } + query_map.insert(key.to_owned(), value.to_owned()); + } + } + } else { + query_map.insert("re_name".to_owned(), query.q.clone()); + } + query_map +} + impl AnalysisService { pub fn new(db: Database) -> Self { GraphMap::get_instance(); @@ -340,8 +358,9 @@ impl AnalysisService { ) -> Result, Error> { let connection = self.db.connection(&tx); + let graph_query_map = convert_query_to_hashmap(&query); let search_sbom_node_name_subquery = sbom_node::Entity::find() - .filter(sbom_node::Column::Name.like(format!("%{}%", query.q.as_str()))) + .filtering(query)? .select_only() .column(sbom_node::Column::SbomId) .distinct() @@ -371,7 +390,23 @@ impl AnalysisService { .node_indices() .filter(|&i| { if let Some(node) = graph.node_weight(i) { - node.name.contains(&query.q.to_string()) + if let Some(re_name) = graph_query_map.get("re_name") { + // if no specific url params supplied then use contains search + node.name.contains(re_name) + } else { + // if any specific url params supplied then match equals + let matches_sbom_id = graph_query_map + .get("sbom_id") + .map_or(true, |sbom_id| node.sbom_id.eq(sbom_id)); + let matches_node_id = graph_query_map + .get("node_id") + .map_or(true, |node_id| node.node_id.eq(node_id)); + let matches_name = + graph_query_map.get("name").map_or(true, |name| { + !name.is_empty() && node.name.eq(name) + }); + matches_sbom_id && matches_node_id && matches_name + } } else { false // Return false if the node does not exist } @@ -564,8 +599,9 @@ impl AnalysisService { ) -> Result, Error> { let connection = self.db.connection(&tx); + let graph_query_map = convert_query_to_hashmap(&query); let search_sbom_node_name_subquery = sbom_node::Entity::find() - .filter(sbom_node::Column::Name.like(format!("%{}%", query.q.as_str()))) + .filtering(query)? .select_only() .column(sbom_node::Column::SbomId) .distinct() @@ -595,7 +631,23 @@ impl AnalysisService { .node_indices() .filter(|&i| { if let Some(node) = graph.node_weight(i) { - node.name.contains(&query.q.to_string()) + if let Some(re_name) = graph_query_map.get("re_name") { + // if no specific url params supplied then use contains search + node.name.contains(re_name) + } else { + // if any specific url params supplied then match equals + let matches_sbom_id = graph_query_map + .get("sbom_id") + .map_or(true, |sbom_id| node.sbom_id.eq(sbom_id)); + let matches_node_id = graph_query_map + .get("node_id") + .map_or(true, |node_id| node.node_id.eq(node_id)); + let matches_name = + graph_query_map.get("name").map_or(true, |name| { + !name.is_empty() && node.name.eq(name) + }); + matches_sbom_id && matches_node_id && matches_name + } } else { false // Return false if the node does not exist }