Add trustify-cli as a subfolder with preserved history#2220
Add trustify-cli as a subfolder with preserved history#2220ruromero wants to merge 14 commits intoguacsec:mainfrom
Conversation
Signed-off-by: Ruben Romero Montes <[email protected]> Assisted by: Cursor
Signed-off-by: Ruben Romero Montes <[email protected]> Assisted-by: Cursor
Signed-off-by: Ruben Romero Montes <[email protected]> Assisted-by: Cursor
Signed-off-by: Ruben Romero Montes <[email protected]>
Signed-off-by: Ruben Romero Montes <[email protected]> Assisted-by: Cursor
Signed-off-by: Ruben Romero Montes <[email protected]> Assisted-by: Cursor
Reviewer's GuideImports the standalone trustify-cli Rust project into the monorepo as a new etc/trustify-cli workspace member, wiring it into the Cargo workspace and exposing a Sequence diagram for CLI startup and authenticated API client initializationsequenceDiagram
actor User
participant Shell
participant trustify_bin as trustify
participant Main as main_rs
participant Cli as Cli_parser
participant Config as Config
participant AuthCreds as AuthCredentials
participant AuthApi as auth_get_token
participant ApiClient as ApiClient
participant Command as Commands
User->>Shell: run trustify sbom list ...
Shell->>trustify_bin: start process
trustify_bin->>Main: main()
Main->>Main: dotenvy::dotenv()
Main->>Cli: Cli::parse()
Cli-->>Main: Cli { config, command }
Main->>Config: config.auth_credentials()
alt auth configured
Config-->>Main: Some(sso_url, client_id, client_secret)
Main->>AuthCreds: AuthCredentials::new(sso_url, client_id, client_secret)
AuthCreds-->>Main: AuthCredentials
Main->>AuthApi: get_token(token_url, client_id, client_secret)
AuthApi-->>Main: access_token
Main->>ApiClient: ApiClient::new(url, Some(access_token), Some(AuthCredentials))
else no auth configured
Config-->>Main: None
Main->>ApiClient: ApiClient::new(url, None, None)
end
ApiClient-->>Main: ApiClient
Main->>Command: command.run(Context)
Command-->>User: command output
Command-->>trustify_bin: exit code
trustify_bin-->>Shell: process exit
Sequence diagram for SBOM duplicate detection and deletion workflowsequenceDiagram
actor User
participant trustify as trustify
participant Cli as SbomCommands
participant DupCmd as DuplicatesCommands
participant SbomApi as sbom_api
participant ApiClient as ApiClient
participant TrustifyAPI as Trustify_HTTP_API
participant FS as FileSystem
rect rgb(230,230,255)
User->>trustify: sbom duplicates find
trustify->>Cli: SbomCommands::run
Cli->>DupCmd: DuplicatesCommands::run(Find)
DupCmd->>FS: check_output_file(output)
FS-->>DupCmd: final_output_path
DupCmd->>SbomApi: find_duplicates(client, params, Some(final_output))
SbomApi->>SbomApi: list first page to get total
SbomApi->>ApiClient: get_with_query(/v2/sbom, ListParams(limit=1))
ApiClient->>TrustifyAPI: HTTP GET /api/v2/sbom?limit=1
TrustifyAPI-->>ApiClient: 200 items,total
ApiClient-->>SbomApi: JSON body
loop for each worker and page
SbomApi->>ApiClient: get_with_query(/v2/sbom, ListParams(limit=batch,offset))
ApiClient->>TrustifyAPI: HTTP GET /api/v2/sbom
TrustifyAPI-->>ApiClient: 200 items
ApiClient-->>SbomApi: JSON body
SbomApi->>SbomApi: parse items into SbomEntry
end
SbomApi->>SbomApi: group by document_id and build DuplicateGroup list
SbomApi->>FS: write duplicates.json
FS-->>SbomApi: ok
SbomApi-->>DupCmd: Vec DuplicateGroup
DupCmd-->>User: summary of duplicates and output file
end
rect rgb(230,255,230)
User->>trustify: sbom duplicates delete [--dry-run]
trustify->>Cli: SbomCommands::run
Cli->>DupCmd: DuplicatesCommands::run(Delete)
DupCmd->>SbomApi: delete_duplicates(client, input_path, concurrency, dry_run)
SbomApi->>FS: open duplicates.json
FS-->>SbomApi: Vec DuplicateGroup
alt dry_run
SbomApi-->>DupCmd: DeleteDuplicatesResult(total, deleted=0,...)
DupCmd-->>User: print planned deletions
else real deletion
loop for each duplicate id (concurrent)
SbomApi->>ApiClient: delete(/v2/sbom/{id})
ApiClient->>TrustifyAPI: HTTP DELETE /api/v2/sbom/{id}
TrustifyAPI-->>ApiClient: 200 or 404 or error
ApiClient-->>SbomApi: result
SbomApi->>SbomApi: update deleted/skipped/failed counters
end
SbomApi-->>DupCmd: DeleteDuplicatesResult
DupCmd-->>User: print deletion summary
end
end
Sequence diagram for automatic token refresh on expired access tokensequenceDiagram
participant Command as Command_run
participant ApiClient as ApiClient
participant Exec as execute_with_retry
participant HTTP as Trustify_HTTP_API
participant AuthCreds as AuthCredentials
participant AuthApi as get_token
Command->>ApiClient: get(/v2/sbom)
ApiClient->>Exec: execute_with_retry(closure)
loop first attempt
Exec->>ApiClient: closure()
ApiClient->>HTTP: HTTP GET /api/v2/sbom with Authorization: Bearer old_token
HTTP-->>ApiClient: 401 Unauthorized
ApiClient-->>Exec: Err(TokenExpired)
Exec->>ApiClient: refresh_token()
ApiClient->>AuthCreds: get_token()
AuthCreds->>AuthApi: get_token(token_url, client_id, client_secret)
AuthApi-->>AuthCreds: new_access_token
AuthCreds-->>ApiClient: new_access_token
ApiClient->>ApiClient: store new token in RwLock
ApiClient-->>Exec: Ok
end
loop retry with new token
Exec->>ApiClient: closure()
ApiClient->>HTTP: HTTP GET /api/v2/sbom with Authorization: Bearer new_access_token
HTTP-->>ApiClient: 200 OK
ApiClient-->>Exec: Ok(body)
end
Exec-->>Command: Ok(body)
Command->>Command: process response
Class diagram for the new trustify CLI structureclassDiagram
class Cli {
+Config config
+Commands command
}
class Config {
+String url
+Option_String sso_url
+Option_String client_id
+Option_String client_secret
+bool has_auth()
+Option_tuple_auth_credentials auth_credentials()
}
class Context {
+Config config
+ApiClient client
}
class Commands {
<<enum>>
+Sbom(SbomCommands)
+Auth(AuthCommands)
+run(ctx: Context)
}
class AuthCommands {
<<enum>>
+Token
+run(ctx: Context)
}
class SbomCommands {
<<enum>>
+Get(id: String)
+List(query: Option_String, limit: Option_u32, offset: Option_u32, sort: Option_String, format: ListFormat)
+Delete(id: Option_String, query: Option_String, dry_run: bool)
+Duplicates(command: DuplicatesCommands)
+run(ctx: Context)
}
class DuplicatesCommands {
<<enum>>
+Find(batch_size: u32, concurrency: usize, output: Option_String)
+Delete(input: Option_String, concurrency: usize, dry_run: bool)
+run(ctx: Context)
}
class ListFormat {
<<enum>>
+Id
+Name
+Short
+Full
}
class ApiClient {
+Client client
+String base_url
+RwLock_Option_String token
+Option_AuthCredentials auth_credentials
+new(base_url: String, token: Option_String, auth_credentials: Option_AuthCredentials) ApiClient
+url(path: String) String
+get(path: String) String
+get_with_query(path: String, query: Serializable) String
+delete(path: String) String
+authorize(request: RequestBuilder) RequestBuilder
+refresh_token() Result_void_ApiError
+execute_with_retry(f: FutureFn) Result_String_ApiError
+handle_response(response: Response) Result_String_ApiError
}
class ApiError {
<<enum>>
+NetworkError(message: String)
+HttpError(status: u16, body: String)
+NotFound(message: String)
+Unauthorized
+TokenExpired
+Timeout(status: u16)
+ServerError(status: u16, body: String)
+InternalError(message: String)
+TemplateError(message: String)
}
class AuthCredentials {
+String token_url
+String client_id
+String client_secret
+new(sso_url: String, client_id: String, client_secret: String) AuthCredentials
+get_token() Result_String_AuthError
}
class AuthError {
<<enum>>
+ConnectionError(error: ReqwestError)
+AuthenticationFailed
+ServerError(message: String)
}
class ListParams {
+Option_String q
+Option_u32 limit
+Option_u32 offset
+Option_String sort
}
class FindDuplicatesParams {
+u32 batch_size
+usize concurrency
}
class DuplicateGroup {
+String document_id
+Option_String published
+String id
+Vec_String duplicates
}
class DeleteDuplicatesResult {
+u32 deleted
+u32 skipped
+u32 failed
+u32 total
}
class SbomEntry {
+String id
+String document_id
+Option_String published
}
class DeleteEntry {
+String id
+String document_id
}
%% Relationships
Cli --> Config
Cli --> Commands
Context --> Config
Context --> ApiClient
Commands --> SbomCommands
Commands --> AuthCommands
AuthCommands --> Context
SbomCommands --> Context
DuplicatesCommands --> Context
ApiClient --> AuthCredentials
ApiClient --> ApiError
AuthCredentials --> AuthError
SbomCommands --> ListFormat
SbomCommands --> ListParams
SbomCommands --> DuplicatesCommands
DuplicatesCommands --> FindDuplicatesParams
DuplicatesCommands --> DuplicateGroup
DuplicatesCommands --> DeleteDuplicatesResult
ListParams --> ApiClient
FindDuplicatesParams --> ApiClient
DuplicateGroup --> SbomEntry
DeleteDuplicatesResult --> DeleteEntry
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- The
sbom deleteCLI subcommand currently only prints a success message and does not invoke the underlying API delete logic, which is likely surprising to users; consider wiring this tosbom_api::delete(and/or a query-based bulk delete) or clearly marking it as unimplemented. - In
api::client::ApiError, theNotFound(String)variant always gets constructed with the constant "Resource not found" message, so theStringpayload is redundant; either drop the payload or pass through the response body for more context. - The
reqwestdependency is pinned to0.13.1, which is quite old compared to the current ecosystem and may lack bug/security fixes; consider updating to a more recent 0.11+ version that aligns with your other services' HTTP stack.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `sbom delete` CLI subcommand currently only prints a success message and does not invoke the underlying API delete logic, which is likely surprising to users; consider wiring this to `sbom_api::delete` (and/or a query-based bulk delete) or clearly marking it as unimplemented.
- In `api::client::ApiError`, the `NotFound(String)` variant always gets constructed with the constant "Resource not found" message, so the `String` payload is redundant; either drop the payload or pass through the response body for more context.
- The `reqwest` dependency is pinned to `0.13.1`, which is quite old compared to the current ecosystem and may lack bug/security fixes; consider updating to a more recent 0.11+ version that aligns with your other services' HTTP stack.
## Individual Comments
### Comment 1
<location> `trustify-cli/Dockerfile:2` </location>
<code_context>
+# Build stage
+FROM rust:1.83-alpine AS builder
+
+# Install musl-dev for static linking
</code_context>
<issue_to_address>
**issue (bug_risk):** Container build/runtime are likely to fail due to TLS/openssl and CA certificate assumptions with reqwest.
With `reqwest` using the default `native-tls` stack, this Alpine-based build and a distroless runtime are likely to miss required TLS libs and CA certs:
- Builder (Alpine): `native-tls` typically needs `openssl-dev` (and related libs) to compile.
- Runtime (distroless): you need CA certificates available, or HTTPS calls from `reqwest` will fail.
You can either (1) keep `native-tls` and add the needed OpenSSL libs + CA bundle to the images, or (2) switch `reqwest` to `rustls-tls` to avoid system OpenSSL and better support static/distroless images.
</issue_to_address>
### Comment 2
<location> `trustify-cli/src/api/sbom.rs:237-238` </location>
<code_context>
+ )));
+ }
+
+ // Wait for all workers to complete
+ join_all(handles).await;
+
+ let all_entries = Arc::try_unwrap(results)
</code_context>
<issue_to_address>
**issue (bug_risk):** Worker join errors are ignored, which can hide panics and lead to silently incomplete results.
Because the `join_all(handles).await` result is ignored, any worker panic or cancellation is silently dropped and `find_duplicates` continues as if all workers succeeded, potentially yielding incomplete `duplicate_groups`.
Consider checking each `JoinHandle` and returning an `ApiError` on failure, e.g.:
```rust
let join_results = join_all(handles).await;
for res in join_results {
if let Err(e) = res {
return Err(ApiError::InternalError(format!(
"Worker task failed: {}",
e
)));
}
}
```
This ensures worker failures are surfaced instead of producing corrupted output.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| // Wait for all workers to complete | ||
| join_all(handles).await; |
There was a problem hiding this comment.
issue (bug_risk): Worker join errors are ignored, which can hide panics and lead to silently incomplete results.
Because the join_all(handles).await result is ignored, any worker panic or cancellation is silently dropped and find_duplicates continues as if all workers succeeded, potentially yielding incomplete duplicate_groups.
Consider checking each JoinHandle and returning an ApiError on failure, e.g.:
let join_results = join_all(handles).await;
for res in join_results {
if let Err(e) = res {
return Err(ApiError::InternalError(format!(
"Worker task failed: {}",
e
)));
}
}This ensures worker failures are surfaced instead of producing corrupted output.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2220 +/- ##
==========================================
- Coverage 68.74% 68.73% -0.01%
==========================================
Files 397 397
Lines 22322 22322
Branches 22322 22322
==========================================
- Hits 15345 15343 -2
+ Misses 6072 6068 -4
- Partials 905 911 +6 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
trustify-cli/Dockerfile
Outdated
| @@ -0,0 +1,31 @@ | |||
| # Build stage | |||
| FROM rust:1.83-alpine AS builder | |||
There was a problem hiding this comment.
Should this be consistent with the root repo’s version, 1.91?
There was a problem hiding this comment.
👍 we are using 1.92.0 https://github.com/guacsec/trustify/blob/main/Cargo.toml#L32
trustify-cli/Dockerfile
Outdated
| RUN touch src/main.rs && cargo build --release | ||
|
|
||
| # Runtime stage - use minimal distroless image | ||
| FROM gcr.io/distroless/static-debian12:nonroot |
There was a problem hiding this comment.
Should this use the company’s ubi9 here?
There was a problem hiding this comment.
I would maybe remove docker stuff for now. We can add it later if needed
|
There are a few other minor issues. |
trustify-cli/Cargo.toml
Outdated
| [package] | ||
| name = "trustify-cli" | ||
| version = "0.1.0" | ||
| edition = "2021" |
There was a problem hiding this comment.
Some minor adjustments.
… the code, and replace unwrap to make clippy happy.
|
I'm curious what's the motivation for this? I thought we liked consuming the CLI as a crate, no? |
Downgrade reqwest to 0.12, remove the form and query features, format…
Let me rephrase, as I'm obviously confusing the CLI with the UX... 😄 Should we consume the CLI as a crate, like we do for the UX? |
|
@jcrossley3 We discussed this offline and the majority decision was not to create an official CLI that we will release and maintain (at least not at this point), but to provide an example script/tool that people can use to do these kind of tasks. The thought process is that most of the organization will have somewhat different requirements and would like to modify the logic, but having a good, well-documented starting point should be helpful. So we should come up with a good documentation of how to use this tool, what it can and can not do and how to adapt it. Perhaps in the future it can graduate to be an official CLI that we will release as a separate binary. |
|
Thanks for the clarification @dejanb. How about we keep it beneath |
|
@jcrossley3 Yeah, that sounds reasonable to me. I was unaware of gensbom tool. |
OK |
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- Since this crate is part of the workspace, consider removing the nested
etc/trustify-cli/Cargo.lockfile and relying solely on the top-levelCargo.lockto avoid divergence and dependency resolution confusion. - Many of the command handlers (
SbomCommands,DuplicatesCommands,AuthCommands) callprocess::exitdirectly; if you anticipate reusing this logic (e.g. from tests or other binaries), it may be cleaner to returnResultvalues and centralize exit handling inmain.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Since this crate is part of the workspace, consider removing the nested `etc/trustify-cli/Cargo.lock` file and relying solely on the top-level `Cargo.lock` to avoid divergence and dependency resolution confusion.
- Many of the command handlers (`SbomCommands`, `DuplicatesCommands`, `AuthCommands`) call `process::exit` directly; if you anticipate reusing this logic (e.g. from tests or other binaries), it may be cleaner to return `Result` values and centralize exit handling in `main`.
## Individual Comments
### Comment 1
<location> `etc/trustify-cli/src/api/client.rs:24-25` </location>
<code_context>
+ #[error("HTTP 404: Resource not found")]
+ NotFound(String),
+
+ #[error("HTTP 401: Please check your authentication credentials")]
+ Unauthorized,
+
+ #[error("HTTP 401: Token expired")]
</code_context>
<issue_to_address>
**issue (bug_risk):** Using `Unauthorized` for 403 responses makes the error message misleading.
In `handle_response`, 403 (`StatusCode::FORBIDDEN`) is mapped to `ApiError::Unauthorized`, whose message is hard‑coded as `HTTP 401`. This will mislead users receiving a 403 (e.g. insufficient permissions) but seeing a 401 error text. Please either add a separate `Forbidden` variant or adjust the messages so they don’t embed `401` and remain consistent with the actual status code.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
dejanb
left a comment
There was a problem hiding this comment.
The PR needs rebasing. Hopefully that would resolve build problem, but if not that test failure in examples should be examined.
| } | ||
| } | ||
| } | ||
| SbomCommands::Delete { id, query, dry_run } => { |
There was a problem hiding this comment.
The delete command is just a stub at the moment. Let's implement it before merging
Summary
Changes
trustify-cli/folder with complete source codeContext
This is a one-time import to merge the trustify-cli project into the main trustify repository. The git subtree approach was used to preserve the full commit history while placing all files in a dedicated subdirectory to avoid conflicts with existing files.
After merging, the separate trustify-cli repository can be archived or removed.
🤖 Generated with Claude Code
Summary by Sourcery
Import the trustify CLI as a new workspace member providing command-line interaction with the Trustify API for SBOM and auth management.
New Features:
Enhancements:
Build:
Documentation: