From 2d61845fdf115082e18a40ce6d52f736bb1b1944 Mon Sep 17 00:00:00 2001 From: "bxf12315@gmail.com" Date: Sun, 15 Mar 2026 21:04:49 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20advisory=E2=80=99s=20query=20and=20?= =?UTF-8?q?pruning=20functionality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etc/trustify-cli/src/api/advisory.rs | 55 ++++++ etc/trustify-cli/src/api/mod.rs | 1 + etc/trustify-cli/src/api/sbom.rs | 226 ++-------------------- etc/trustify-cli/src/commands/advisory.rs | 186 ++++++++++++++++++ etc/trustify-cli/src/commands/mod.rs | 9 + etc/trustify-cli/src/commands/sbom.rs | 5 +- etc/trustify-cli/src/common/mod.rs | 206 ++++++++++++++++++++ etc/trustify-cli/src/main.rs | 1 + 8 files changed, 476 insertions(+), 213 deletions(-) create mode 100644 etc/trustify-cli/src/api/advisory.rs create mode 100644 etc/trustify-cli/src/commands/advisory.rs create mode 100644 etc/trustify-cli/src/common/mod.rs diff --git a/etc/trustify-cli/src/api/advisory.rs b/etc/trustify-cli/src/api/advisory.rs new file mode 100644 index 000000000..d7322f0f5 --- /dev/null +++ b/etc/trustify-cli/src/api/advisory.rs @@ -0,0 +1,55 @@ +use super::client::{ApiClient, ApiError}; +use crate::common::{ + DeleteEntry, DeleteResult, ListParams, PruneParams, build_prune_query, delete_entries, + new_delete_result, +}; + +const ADVISORY_PATH: &str = "/v2/advisory"; + +pub async fn list(client: &ApiClient, params: &ListParams) -> Result { + client.get_with_query(ADVISORY_PATH, params).await +} + +pub async fn prune(client: &ApiClient, params: &PruneParams) -> Result { + let (_query, list_params) = build_prune_query(params); + + log::info!( + "Pruning advisories with query: {}", + list_params.q.as_deref().unwrap_or("") + ); + + let response = list(client, &list_params).await?; + let parsed: serde_json::Value = serde_json::from_str(&response) + .map_err(|e| ApiError::InternalError(format!("Failed to parse response: {}", e)))?; + + let items = parsed + .get("items") + .and_then(|v| v.as_array()) + .ok_or_else(|| ApiError::InternalError("No items in response".to_string()))?; + + let total = items.len() as u32; + + let entries: Vec = items + .iter() + .filter_map(|item| { + let id = item.get("uuid").and_then(|v| v.as_str())?; + + let identifier = item + .get("identifier") + .and_then(|v| v.as_str()) + .unwrap_or("unknown") + .to_string(); + + Some(DeleteEntry { + id: id.to_string(), + identifier: identifier.to_string(), + }) + }) + .collect(); + + if params.dry_run { + return Ok(new_delete_result(total)); + } + + delete_entries(client, ADVISORY_PATH, entries, params.concurrency).await +} diff --git a/etc/trustify-cli/src/api/mod.rs b/etc/trustify-cli/src/api/mod.rs index 0cb893a05..0aa032c79 100644 --- a/etc/trustify-cli/src/api/mod.rs +++ b/etc/trustify-cli/src/api/mod.rs @@ -1,3 +1,4 @@ +pub mod advisory; pub mod auth; pub mod client; pub mod sbom; diff --git a/etc/trustify-cli/src/api/sbom.rs b/etc/trustify-cli/src/api/sbom.rs index 8ad85e84e..ccf1cf969 100644 --- a/etc/trustify-cli/src/api/sbom.rs +++ b/etc/trustify-cli/src/api/sbom.rs @@ -6,11 +6,7 @@ use std::{ sync::Arc, }; -use chrono::{DateTime, Duration, Local}; -use futures::{ - future::join_all, - stream::{self, StreamExt}, -}; +use futures::future::join_all; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use log; use serde::{Deserialize, Serialize}; @@ -18,47 +14,19 @@ use serde_json::Value; use tokio::sync::Mutex; use super::client::{ApiClient, ApiError}; +use crate::common::{ + DeleteEntry, DeleteResult, ListParams, PruneParams, build_prune_query, delete_entries, + new_delete_result, +}; const SBOM_PATH: &str = "/v2/sbom"; -/// Query parameters for listing SBOMs -#[derive(Default, Serialize)] -pub struct ListParams { - #[serde(skip_serializing_if = "Option::is_none")] - pub q: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub offset: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub sort: Option, -} - /// Parameters for find duplicates pub struct FindDuplicatesParams { pub batch_size: u32, pub concurrency: usize, } -/// Parameters for pruning SBOMs -#[derive(Default, Serialize)] -pub struct PruneParams { - #[serde(skip_serializing_if = "Option::is_none")] - pub q: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub published_before: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub older_than: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub label: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub keep_latest: Option, - pub dry_run: bool, - pub concurrency: usize, -} - /// SBOM entry for duplicate detection #[derive(Debug, Clone)] struct SbomEntry { @@ -368,7 +336,7 @@ pub async fn delete_by_query( .unwrap_or("unknown"); Some(DeleteEntry { id: id.to_string(), - document_id: document_id.to_string(), + identifier: document_id.to_string(), }) }) .collect(); @@ -377,18 +345,10 @@ pub async fn delete_by_query( for entry in &entries { eprintln!( "[DRY-RUN] Would delete: {} (document_id: {})", - entry.id, entry.document_id + entry.id, entry.identifier ); } - return Ok(DeleteResult { - deleted: vec![], - deleted_total: 0, - skipped: vec![], - skipped_total: 0, - failed: vec![], - failed_total: 0, - total, - }); + return Ok(new_delete_result(total)); } delete_list(client, entries, concurrency).await @@ -396,33 +356,7 @@ pub async fn delete_by_query( /// Prune SBOMs based on the given parameters pub async fn prune(client: &ApiClient, params: &PruneParams) -> Result { - // Build query parameters for listing SBOMs to prune - let mut query = params.q.as_deref().unwrap_or("").to_string(); - if let Some(d) = params.published_before.as_ref() { - query.push_str(&format!("&published<{}", d.to_rfc3339())); - } - - if let Some(older_than) = params.older_than { - let older_than_time = Local::now() - Duration::days(older_than); - query.push_str(&format!("&ingested<{}", older_than_time.to_rfc3339())); - } - - if let Some(labels) = ¶ms.label { - for l in labels.iter() { - query.push_str(&format!("&labels:{}", l)); - } - } - - let (offset, sort) = match params.keep_latest { - Some(v) => (Some(v), Some("ingested:desc".to_string())), - None => (None, None), - }; - let list_params = ListParams { - q: Some(query), - limit: params.limit, - offset, - sort, - }; + let (_query, list_params) = build_prune_query(params); log::info!( "Pruning SBOMs with query: {}, offset: {:?}, sort: {:?}", @@ -454,69 +388,20 @@ pub async fn prune(client: &ApiClient, params: &PruneParams) -> Result, - pub deleted_total: u32, - pub skipped: Vec, - pub skipped_total: u32, - pub failed: Vec, - pub failed_total: u32, - pub total: u32, -} - -#[derive(Debug, Clone, Serialize)] -/// Successfully deleted SBOM -pub struct DeletedResult { - pub sbom_id: String, - pub document_id: String, -} - -#[derive(Debug, Clone, Serialize)] -/// Skipped SBOM (not found) -pub struct SkippedResult { - pub sbom_id: String, - pub document_id: String, -} - -#[derive(Debug, Clone, Serialize)] -/// Failed to delete SBOM -pub struct FailedResult { - pub sbom_id: String, - pub document_id: String, - pub error: String, -} - -/// Entry to delete with its document_id for logging -#[derive(Clone)] -pub struct DeleteEntry { - id: String, - document_id: String, -} - /// Read delete entries from a file pub fn read_delete_entries_from_file(input_file: &str) -> Result, ApiError> { let path = Path::new(input_file); @@ -539,7 +424,7 @@ pub fn read_delete_entries_from_file(input_file: &str) -> Result, concurrency: usize, ) -> Result { - let total = entries.len() as u32; - - eprintln!( - "Deleting {} sboms with {} concurrent requests...\n", - total, concurrency - ); - - let progress = ProgressBar::new(total as u64); - progress.set_style( - ProgressStyle::default_bar() - .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} ({percent}%) {msg}")? - .progress_chars("█▓░"), - ); - - let deleted = Arc::new(Mutex::new(Vec::new())); - let skipped = Arc::new(Mutex::new(Vec::new())); - let failed = Arc::new(Mutex::new(Vec::new())); - - stream::iter(entries) - .for_each_concurrent(concurrency, |entry| { - let client = client.clone(); - let deleted = Arc::clone(&deleted); - let skipped = Arc::clone(&skipped); - let failed = Arc::clone(&failed); - let progress = progress.clone(); - async move { - match delete(&client, &entry.id).await { - Ok(_) => { - let mut deleted_list = deleted.lock().await; - deleted_list.push(DeletedResult { - sbom_id: entry.id.clone(), - document_id: entry.document_id.clone(), - }); - } - Err(ApiError::NotFound(_)) => { - let mut skipped_list = skipped.lock().await; - skipped_list.push(SkippedResult { - sbom_id: entry.id.clone(), - document_id: entry.document_id.clone(), - }); - } - Err(e) => { - let mut failed_list = failed.lock().await; - failed_list.push(FailedResult { - sbom_id: entry.id.clone(), - document_id: entry.document_id.clone(), - error: e.to_string(), - }); - progress.println(format!( - "Failed to delete {} (document_id: {}): {}", - entry.id, entry.document_id, e - )); - } - } - progress.inc(1); - } - }) - .await; - - progress.finish_with_message("complete"); - - let deleted_list = deleted.lock().await; - let skipped_list = skipped.lock().await; - let failed_list = failed.lock().await; - - Ok(DeleteResult { - deleted: deleted_list.clone(), - deleted_total: deleted_list.len() as u32, - skipped: skipped_list.clone(), - skipped_total: skipped_list.len() as u32, - failed: failed_list.clone(), - failed_total: failed_list.len() as u32, - total, - }) + delete_entries(client, SBOM_PATH, entries, concurrency).await } /// Delete duplicates from a file with progress bar @@ -643,18 +455,10 @@ pub async fn delete_duplicates( for entry in &entries { eprintln!( "[DRY-RUN] Would delete: {} (document_id: {})", - entry.id, entry.document_id + entry.id, entry.identifier ); } - return Ok(DeleteResult { - deleted: vec![], - deleted_total: 0, - skipped: vec![], - skipped_total: 0, - failed: vec![], - failed_total: 0, - total, - }); + return Ok(new_delete_result(total)); } delete_list(client, entries, concurrency).await diff --git a/etc/trustify-cli/src/commands/advisory.rs b/etc/trustify-cli/src/commands/advisory.rs new file mode 100644 index 000000000..5eba13003 --- /dev/null +++ b/etc/trustify-cli/src/commands/advisory.rs @@ -0,0 +1,186 @@ +use std::process::ExitCode; + +use clap::{Subcommand, ValueEnum}; +use serde_json::Value; + +use crate::Context; +use crate::api::advisory as advisory_api; +use crate::common::{ListParams, PruneParams}; +use chrono::{DateTime, Local}; + +#[derive(Clone, Default, ValueEnum)] +pub enum ListFormat { + #[default] + Full, +} + +#[derive(Clone, Default, ValueEnum)] +pub enum OutputFormat { + #[default] + Json, +} + +#[derive(Subcommand)] +pub enum AdvisoryCommands { + /// List advisories + List { + /// Query filter for advisories + #[arg(long)] + query: Option, + /// Limit the number of results + #[arg(long)] + limit: Option, + /// Offset the results + #[arg(long)] + offset: Option, + /// Sort the results + #[arg(long)] + sort: Option, + }, + /// Prune advisories + Prune { + /// Query filter for advisories to delete + #[arg(long)] + query: Option, + + /// Perform a dry run without actually deleting + #[arg(long)] + dry_run: bool, + + /// Number of concurrent delete requests (default: 10) + #[arg(long, default_value = "10")] + concurrency: usize, + + /// Limit the number of advisories to query and delete (default: 100) + #[arg(long, default_value = "100")] + limit: Option, + + /// Prune advisories published before a certain date (format: RFC 3339, e.g., 2024-01-15T10:30:45Z) + #[arg(long)] + published_before: Option, + + /// Prune advisories ingested before the given number of days + #[arg(long)] + older_than: Option, + + /// Label to filter advisories to delete (can be specified multiple times) + #[arg(long)] + label: Vec, + + /// Keep N most recent advisories per identifier + #[arg(long)] + keep_latest: Option, + + /// Output file type (default: json) + #[arg(long, default_value = "json")] + output_type: Option, + + /// Output file path (default: advisories.json) + #[arg(long, default_value = "advisories.json")] + output: Option, + + /// Quiet mode + #[arg(long)] + quiet: bool, + }, +} + +fn format_list_output(json: &str, _format: &ListFormat) -> anyhow::Result<()> { + let value: Value = serde_json::from_str(json)?; + println!("{}", serde_json::to_string_pretty(&value)?); + Ok(()) +} + +impl AdvisoryCommands { + pub async fn run(&self, ctx: &Context) -> anyhow::Result { + match self { + AdvisoryCommands::List { + query, + limit, + offset, + sort, + } => { + let params = ListParams { + q: query.clone(), + limit: *limit, + offset: *offset, + sort: sort.clone(), + }; + let json = advisory_api::list(&ctx.client, ¶ms).await?; + format_list_output(&json, &ListFormat::Full)?; + Ok(ExitCode::SUCCESS) + } + AdvisoryCommands::Prune { + query, + dry_run, + concurrency, + limit, + published_before, + older_than, + label, + keep_latest, + output_type, + output, + quiet, + } => { + let published_before = if let Some(date_str) = published_before { + Some(DateTime::parse_from_rfc3339(date_str) + .map_err(|e| anyhow::anyhow!("Invalid date format for published_before: {}. Expected RFC 3339 format (e.g., 2024-01-15T10:30:45Z)", e))? + .with_timezone(&Local)) + } else { + None + }; + + let params = PruneParams { + q: query.clone(), + limit: *limit, + published_before, + older_than: *older_than, + label: Some(label.clone()).filter(|l| !l.is_empty()), + keep_latest: *keep_latest, + dry_run: *dry_run, + concurrency: *concurrency, + }; + + let prune_result = advisory_api::prune(&ctx.client, ¶ms).await?; + + if !quiet { + if *dry_run { + println!( + "[DRY-RUN] Would delete {} advisory(s)", + prune_result.deleted_total + ); + } else { + let mut msg = format!("Deleted {} advisory(s)", prune_result.deleted_total); + if prune_result.skipped_total > 0 { + msg.push_str(&format!( + ", {} skipped (not found)", + prune_result.skipped_total + )); + } + if prune_result.failed_total > 0 { + msg.push_str(&format!(", {} failed", prune_result.failed_total)); + } + msg.push_str(&format!(" out of {} total", prune_result.total)); + println!("{}", msg); + } + } + + if let Some(output_path) = output { + match output_type.as_ref() { + Some(OutputFormat::Json) | None => { + let json = + serde_json::to_string_pretty(&prune_result).map_err(|e| { + anyhow::anyhow!("Failed to serialize result: {}", e) + })?; + std::fs::write(output_path, json) + .map_err(|e| anyhow::anyhow!("Failed to write to file: {}", e))?; + } + } + } + + Ok(ExitCode::SUCCESS) + } + } + } +} diff --git a/etc/trustify-cli/src/commands/mod.rs b/etc/trustify-cli/src/commands/mod.rs index 3790d3ad9..b4822cbaa 100644 --- a/etc/trustify-cli/src/commands/mod.rs +++ b/etc/trustify-cli/src/commands/mod.rs @@ -1,3 +1,4 @@ +pub mod advisory; pub mod auth; pub mod sbom; @@ -5,6 +6,7 @@ use clap::Subcommand; use std::process::ExitCode; use crate::Context; +pub use advisory::AdvisoryCommands; pub use auth::AuthCommands; pub use sbom::SbomCommands; @@ -16,6 +18,12 @@ pub enum Commands { command: SbomCommands, }, + /// Advisory management commands + Advisory { + #[command(subcommand)] + command: AdvisoryCommands, + }, + /// Authentication commands Auth { #[command(subcommand)] @@ -27,6 +35,7 @@ impl Commands { pub async fn run(&self, ctx: &Context) -> anyhow::Result { match self { Commands::Sbom { command } => command.run(ctx).await, + Commands::Advisory { command } => command.run(ctx).await, Commands::Auth { command } => command.run(ctx).await, } } diff --git a/etc/trustify-cli/src/commands/sbom.rs b/etc/trustify-cli/src/commands/sbom.rs index 1252e221d..419f4f7a5 100644 --- a/etc/trustify-cli/src/commands/sbom.rs +++ b/etc/trustify-cli/src/commands/sbom.rs @@ -9,6 +9,7 @@ use serde_json::Value; use crate::Context; use crate::api::sbom as sbom_api; +use crate::common::{ListParams, PruneParams}; use chrono::{DateTime, Local}; /// Output format for SBOM list @@ -181,7 +182,7 @@ impl SbomCommands { sort, format, } => { - let params = sbom_api::ListParams { + let params = ListParams { q: query.clone(), limit: *limit, offset: *offset, @@ -251,7 +252,7 @@ impl SbomCommands { None }; - let params = sbom_api::PruneParams { + let params = PruneParams { q: query.clone(), limit: *limit, keep_latest: *keep_latest, diff --git a/etc/trustify-cli/src/common/mod.rs b/etc/trustify-cli/src/common/mod.rs new file mode 100644 index 000000000..5ca0f6376 --- /dev/null +++ b/etc/trustify-cli/src/common/mod.rs @@ -0,0 +1,206 @@ +use std::sync::Arc; + +use chrono::{DateTime, Duration, Local}; +use futures::stream::{self, StreamExt}; +use indicatif::{ProgressBar, ProgressStyle}; +use serde::{Deserialize, Serialize}; +use tokio::sync::Mutex; + +use crate::api::client::{ApiClient, ApiError}; + +/// Build query string and list params from prune parameters +pub fn build_prune_query(params: &PruneParams) -> (String, ListParams) { + let mut query = params.q.as_deref().unwrap_or("").to_string(); + + if let Some(d) = params.published_before.as_ref() { + query.push_str(&format!("&published<{}", d.to_rfc3339())); + } + + if let Some(older_than) = params.older_than { + let older_than_time = Local::now() - Duration::days(older_than); + query.push_str(&format!("&ingested<{}", older_than_time.to_rfc3339())); + } + + if let Some(labels) = ¶ms.label { + for l in labels.iter() { + query.push_str(&format!("&labels:{}", l)); + } + } + + let (offset, sort) = match params.keep_latest { + Some(v) => (Some(v), Some("ingested:desc".to_string())), + None => (None, None), + }; + + let list_params = ListParams { + q: Some(query.clone()), + limit: params.limit, + offset, + sort, + }; + + (query, list_params) +} + +#[derive(Default, Serialize)] +pub struct ListParams { + #[serde(skip_serializing_if = "Option::is_none")] + pub q: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub sort: Option, +} + +#[derive(Default, Serialize)] +pub struct PruneParams { + #[serde(skip_serializing_if = "Option::is_none")] + pub q: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub published_before: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub older_than: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub label: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub keep_latest: Option, + pub dry_run: bool, + pub concurrency: usize, +} + +#[derive(Clone, Debug)] +pub struct DeleteEntry { + pub id: String, + pub identifier: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeleteResult { + pub deleted: Vec, + pub deleted_total: u32, + pub skipped: Vec, + pub skipped_total: u32, + pub failed: Vec, + pub failed_total: u32, + pub total: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeletedResult { + pub id: String, + pub identifier: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SkippedResult { + pub id: String, + pub identifier: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FailedResult { + pub id: String, + pub identifier: String, + pub error: String, +} + +pub async fn delete_entries( + client: &ApiClient, + base_path: &str, + entries: Vec, + concurrency: usize, +) -> Result { + let total_count = entries.len() as u32; + + eprintln!( + "Deleting {} entries with {} concurrent requests...\n", + total_count, concurrency + ); + + let progress = ProgressBar::new(total_count as u64); + progress.set_style( + ProgressStyle::default_bar() + .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} ({percent}%) {msg}")? + .progress_chars("█▓░"), + ); + + let deleted = Arc::new(Mutex::new(Vec::new())); + let skipped = Arc::new(Mutex::new(Vec::new())); + let failed = Arc::new(Mutex::new(Vec::new())); + + stream::iter(entries) + .for_each_concurrent(concurrency, |entry| { + let client = client.clone(); + let deleted = Arc::clone(&deleted); + let skipped = Arc::clone(&skipped); + let failed = Arc::clone(&failed); + let progress = progress.clone(); + let base_path = base_path.to_string(); + + async move { + let path = format!("{}/{}", base_path, entry.id); + match client.delete(&path).await { + Ok(_) => { + let mut deleted_list = deleted.lock().await; + deleted_list.push(DeletedResult { + id: entry.id.clone(), + identifier: entry.identifier.clone(), + }); + } + Err(ApiError::HttpError(404, _)) => { + let mut skipped_list = skipped.lock().await; + skipped_list.push(SkippedResult { + id: entry.id.clone(), + identifier: entry.identifier.clone(), + }); + } + Err(e) => { + let mut failed_list = failed.lock().await; + failed_list.push(FailedResult { + id: entry.id.clone(), + identifier: entry.identifier.clone(), + error: e.to_string(), + }); + progress.println(format!( + "Failed to delete {} (identifier: {}): {}", + entry.id, entry.identifier, e + )); + } + } + progress.inc(1); + } + }) + .await; + + progress.finish_with_message("complete"); + + let deleted_list = deleted.lock().await; + let skipped_list = skipped.lock().await; + let failed_list = failed.lock().await; + + Ok(DeleteResult { + deleted: deleted_list.clone(), + deleted_total: deleted_list.len() as u32, + skipped: skipped_list.clone(), + skipped_total: skipped_list.len() as u32, + failed: failed_list.clone(), + failed_total: failed_list.len() as u32, + total: total_count, + }) +} + +pub fn new_delete_result(total: u32) -> DeleteResult { + DeleteResult { + deleted: vec![], + deleted_total: 0, + skipped: vec![], + skipped_total: 0, + failed: vec![], + failed_total: 0, + total, + } +} diff --git a/etc/trustify-cli/src/main.rs b/etc/trustify-cli/src/main.rs index 66bf78fb1..8bcb729fb 100644 --- a/etc/trustify-cli/src/main.rs +++ b/etc/trustify-cli/src/main.rs @@ -1,6 +1,7 @@ mod api; mod cli; mod commands; +mod common; mod config; use std::process::ExitCode; From aab546b14f3ddd6514f56a325dc7dde121605bf8 Mon Sep 17 00:00:00 2001 From: "bxf12315@gmail.com" Date: Sun, 15 Mar 2026 21:12:56 +0800 Subject: [PATCH 2/2] chore: update README --- etc/trustify-cli/README.md | 61 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/etc/trustify-cli/README.md b/etc/trustify-cli/README.md index 9a2739a37..1b142d366 100644 --- a/etc/trustify-cli/README.md +++ b/etc/trustify-cli/README.md @@ -44,6 +44,8 @@ trustify sbom duplicates delete - [`sbom duplicates find`](#sbom-duplicates-find) - [`sbom duplicates delete`](#sbom-duplicates-delete) - [`sbom prune`](#sbom-prune) + - [`advisory list`](#advisory-list) + - [`advisory prune`](#advisory-prune) - [API Reference](#api-reference) - [License](#license) @@ -228,4 +230,63 @@ trustify sbom prune --output results.json --quiet # Save results to f "failed_total": 2, "total": 4 } +``` + +--- + +### `advisory list` + +List advisories with filtering, pagination, and output formatting. + +```bash +trustify advisory list # Full JSON +trustify advisory list --query "title=CVE-2024-1234" # Filter by advisory title +trustify advisory list --limit 10 --offset 20 # Pagination +``` + +--- + +### `advisory prune` + +Prune advisories based on various criteria like age or labels. Always preview with `--dry-run` first! + +```bash +trustify advisory prune --dry-run # Preview what will be pruned +trustify advisory prune --older-than 90 # Delete advisories older than 90 days +trustify advisory prune --published-before 2026-01-15T10:30:45Z # Delete advisories published before the specified date +trustify advisory prune --label type=csaf --label importer=run # Delete advisories with specific labels +trustify advisory prune --keep-latest 5 # Keep only 5 most recent per identifier +trustify advisory prune --query "title=CVE-2024-1234" # Custom query filter +trustify advisory prune --limit 1000 # Limit results and increase concurrency +trustify advisory prune --output results.json --quiet # Save results to file, suppress output +``` + +**Output file format:** + +```json +{ + "deleted": [ + { + "id": "urn:uuid:7f774d1f-bd19-425c-aa7d-1e35e6d527dc", + "identifier": "CVE-2019-7589" + } + ], + "deleted_total": 1, + "skipped": [ + { + "id": "urn:uuid:3ab23f78-4bf0-44a7-9f1e-2e2bd672643a", + "identifier": "CVE-2019-7304" + } + ], + "skipped_total": 1, + "failed": [ + { + "id": "urn:uuid:abc123", + "identifier": "CVE-2024-1234", + "error": "HTTP 408: Server timeout" + } + ], + "failed_total": 1, + "total": 3 +} ``` \ No newline at end of file