diff --git a/Cargo.lock b/Cargo.lock index 0e81a9e..f433bbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1353,6 +1353,7 @@ dependencies = [ "log", "openid", "packageurl", + "rand 0.8.5", "reqwest", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index e48a30f..7e8a700 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ sqlx = { version = "0.8", features = ["runtime-tokio", "tls-native-tls", "uuid", tokio = { version = "1.43.1", features = ["sync"] } urlencoding = "2" packageurl = "0.4.2" +rand = "0.8" [features] default = ["postgres"] diff --git a/src/main.rs b/src/main.rs index b11c30d..90e5fef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -180,7 +180,9 @@ async fn main() -> Result<(), anyhow::Error> { .register_transaction(tx!(list_sboms_paginated)) .register_transaction(tx!(get_analysis_status)) .register_transaction(tx!(get_analysis_latest_cpe)) - .register_transaction(tx!(list_advisory_labels)); + .register_transaction(tx!(list_advisory_labels)) + .register_transaction(tx!(patch_advisory_labels)) + .register_transaction(tx!(put_advisory_labels)); tx!(s.get_sbom?(scenario.get_sbom.clone())); tx!(s.get_sbom_advisories?(scenario.get_sbom_advisories.clone())); @@ -224,7 +226,8 @@ async fn main() -> Result<(), anyhow::Error> { tx!(s.delete_sbom_from_pool_sequential?( scenario.delete_sbom_pool.clone(), delete_counter.clone() - ), name: format!("delete_sbom_from_pool_sequential[{} SBOMs]", pool.len())) + ), + name: format!("delete_sbom_from_pool_sequential[{} SBOMs]", pool.len())) } s }) diff --git a/src/restapi.rs b/src/restapi.rs index a99de09..186b885 100644 --- a/src/restapi.rs +++ b/src/restapi.rs @@ -1,12 +1,19 @@ use crate::utils::DisplayVec; -use goose::goose::{GooseUser, TransactionResult}; +use goose::goose::{GooseMethod, GooseRequest, GooseUser, TransactionError, TransactionResult}; +use reqwest::{Client, RequestBuilder}; + +use rand::Rng; use serde_json::json; use std::sync::{ Arc, atomic::{AtomicUsize, Ordering}, }; +use tokio::sync::OnceCell; use urlencoding::encode; +// Static variable to store advisory total count +static ADVISORY_TOTAL: OnceCell = OnceCell::const_new(); + pub async fn get_advisory(id: String, user: &mut GooseUser) -> TransactionResult { let uri = format!("/api/v2/advisory/{}", encode(&format!("urn:uuid:{}", id))); @@ -44,6 +51,46 @@ pub async fn list_advisory(user: &mut GooseUser) -> TransactionResult { Ok(()) } +/// Get advisory total count and store it in static OnceCell +async fn get_advisory_total(user: &mut GooseUser) -> Result> { + let response = user.get("/api/v2/advisory").await?; + let json_data = response.response?.json::().await?; + + // Extract total from the response + if let Some(total) = json_data.get("total").and_then(|t| t.as_u64()) { + log::info!("Advisory total count: {}", total); + return Ok(total); + } + + Err(Box::new(TransactionError::Custom( + "Failed to get advisory total count".to_string(), + ))) +} + +/// Get cached advisory total count, fetch if not available using get_or_init +async fn get_cached_advisory_total(user: &mut GooseUser) -> Result> { + // Try to get from cache first + if let Some(&total) = ADVISORY_TOTAL.get() { + return Ok(total); + } + + // If not cached, fetch it and handle errors properly + match get_advisory_total(user).await { + Ok(total) => { + // Store in cache for future use + let _ = ADVISORY_TOTAL.set(total); + Ok(total) + } + Err(e) => { + // Propagate the error with context instead of silently returning 0 + Err(Box::new(TransactionError::Custom(format!( + "Failed to get advisory total count: {}", + e + )))) + } + } +} + pub async fn list_advisory_paginated(user: &mut GooseUser) -> TransactionResult { let _response = user.get("/api/v2/advisory?offset=100&limit=10").await?; @@ -67,6 +114,92 @@ pub async fn search_advisory(user: &mut GooseUser) -> TransactionResult { Ok(()) } +/// List advisory with random offset and limit=1, return advisory ID +async fn list_advisory_random_single( + user: &mut GooseUser, +) -> Result> { + let total = get_cached_advisory_total(user).await?; + // Generate random offset + let offset = rand::thread_rng().gen_range(0..=total); + let url = format!("/api/v2/advisory?offset={}&limit=1", offset); + + let response = user.get(&url).await?; + let json_data = response.response?.json::().await?; + + // Extract advisory ID from the response + if let Some(items) = json_data.get("items").and_then(|i| i.as_array()) { + if let Some(first_item) = items.first() { + if let Some(id) = first_item.get("uuid").and_then(|u| u.as_str()) { + log::info!("Listing advisory with offset {}: {}", offset, id); + return Ok(id.to_string()); + } + } + } + + // Return error if no advisory found + Err(Box::new(TransactionError::Custom(format!( + "No advisory found at offset: {}", + offset + )))) +} + +// + +/// Send Advisory labels request +async fn send_advisory_label_request( + advisory_id: String, + user: &mut GooseUser, + method: GooseMethod, + source: &str, + client_method: fn(&Client, String) -> RequestBuilder, +) -> TransactionResult { + let path = format!("/api/v2/advisory/{}/label", advisory_id); + let json = json!({ + "source": source, + "foo": "bar", + "space": "with space", + "empty": "", + }); + + let url = user.build_url(&path)?; + + let reqwest_request_builder = client_method(&user.client, url); + let goose_request = GooseRequest::builder() + .method(method) + .path(path.as_str()) + .set_request_builder(reqwest_request_builder.json(&json)) + .build(); + let _response = user.request(goose_request).await?; + + Ok(()) +} + +/// Send Advisory labels request using PUT method +pub async fn put_advisory_labels(user: &mut GooseUser) -> TransactionResult { + let advisory_id = list_advisory_random_single(user).await?; + send_advisory_label_request( + advisory_id, + user, + GooseMethod::Put, + "It's a put request", + Client::put, + ) + .await +} + +/// Send Advisory labels request using PATCH method +pub async fn patch_advisory_labels(user: &mut GooseUser) -> TransactionResult { + let advisory_id = list_advisory_random_single(user).await?; + send_advisory_label_request( + advisory_id, + user, + GooseMethod::Patch, + "It's a patch request", + Client::patch, + ) + .await +} + pub async fn list_importer(user: &mut GooseUser) -> TransactionResult { let _response = user.get("/api/v2/importer").await?;