expirement(tools): trustify-scanner CLI tool for vulnerability scanning#2527
expirement(tools): trustify-scanner CLI tool for vulnerability scanning#2527rh-jfuller wants to merge 1 commit into
Conversation
Reviewer's GuideIntroduces a new Rust CLI tool Sequence diagram for trustify-scanner end-to-end scan flowsequenceDiagram
actor User
participant trustify_scanner as trustify_scanner_main
participant TargetParser as target_Target
participant Analyzer as analyze_scan
participant Catalogers as cataloger_run_catalogers
participant ContainerScanner as container_scan_registry_image
participant Client as TrustifyClient
participant TrustifyAPI as Trustify_v3_API
participant Output as output_render
User->>trustify_scanner: invoke CLI (trustify-scanner)
trustify_scanner->>TargetParser: parse(target)
TargetParser-->>trustify_scanner: Target
trustify_scanner->>Client: build_client(with_token/with_oidc/unauthenticated)
trustify_scanner->>Analyzer: scan(Target, Client, upload, follow_links)
Analyzer->>Analyzer: discover_packages(Target, follow_links)
alt Target_Sbom
Analyzer->>Catalogers: cataloger_sbom_catalog_sbom
else Target_Dir
Analyzer->>Catalogers: run_catalogers(root, follow_links)
else Target_Registry
Analyzer->>ContainerScanner: scan_registry_image(image_ref, follow_links)
else Target_OciArchive
Analyzer->>ContainerScanner: scan_oci_archive(path, follow_links)
else Target_Purl
Analyzer->>Analyzer: parse_purl_name_version
end
Analyzer->>Client: analyze(purls)
Client->>TrustifyAPI: POST /api/v3/vulnerability/analyze
TrustifyAPI-->>Client: AnalysisResponse
Client-->>Analyzer: AnalysisResponse
opt upload_SBOM
Analyzer->>Analyzer: build_minimal_sbom(packages)
Analyzer->>Client: upload_sbom(sbom_json)
Client->>TrustifyAPI: POST /api/v3/sbom
TrustifyAPI-->>Client: IngestResult
end
Analyzer-->>trustify_scanner: ScanResult
trustify_scanner->>Output: render(ScanResult, OutputFormat)
Output-->>User: formatted results (list/table/json)
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:
- PURL construction and parsing are currently done with manual string manipulation across catalogers and
parse_purl_name_version; consider using thepackageurlcrate consistently to avoid subtle format/encoding bugs (e.g., scoped npm packages, namespaces) and keep behavior aligned with the spec. - Several catalogers (Cargo, Python, Maven/Gradle) implement ad‑hoc line-based parsing for TOML / lockfiles instead of using format-aware parsers; this will be brittle against variations in file structure, so it would be safer to switch to existing crates (e.g.,
cargo-lock, TOML parsers) or reuse workspace utilities where available. - In the container tar extraction (
extract_archive), the result ofentry.unpack_in(dest)is ignored and any per-entry I/O errors are silently suppressed; consider handling or logging these errors explicitly so partial extraction failures don't go unnoticed and lead to confusing scan results.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- PURL construction and parsing are currently done with manual string manipulation across catalogers and `parse_purl_name_version`; consider using the `packageurl` crate consistently to avoid subtle format/encoding bugs (e.g., scoped npm packages, namespaces) and keep behavior aligned with the spec.
- Several catalogers (Cargo, Python, Maven/Gradle) implement ad‑hoc line-based parsing for TOML / lockfiles instead of using format-aware parsers; this will be brittle against variations in file structure, so it would be safer to switch to existing crates (e.g., `cargo-lock`, TOML parsers) or reuse workspace utilities where available.
- In the container tar extraction (`extract_archive`), the result of `entry.unpack_in(dest)` is ignored and any per-entry I/O errors are silently suppressed; consider handling or logging these errors explicitly so partial extraction failures don't go unnoticed and lead to confusing scan results.
## Individual Comments
### Comment 1
<location path="tools/scanner/src/cataloger/python.rs" line_range="170-179" />
<code_context>
+///
+/// Looks for `[project] dependencies = [...]` entries with pinned (`==`)
+/// or minimum (`>=`) versions. Also checks `[tool.poetry.dependencies]`.
+fn parse_pyproject_toml(content: &str, path: &Path, packages: &mut Vec<DiscoveredPackage>) {
+ // Track which section we're in.
+ let mut in_dependencies = false;
+ let mut in_poetry_deps = false;
+
+ for line in content.lines() {
+ let trimmed = line.trim();
+
+ // Section headers.
+ if trimmed.starts_with('[') {
+ in_dependencies = trimmed == "[project]"
+ || trimmed == "dependencies = ["
+ || in_dependencies && trimmed.starts_with('"');
</code_context>
<issue_to_address>
**issue (bug_risk):** Section tracking in `parse_pyproject_toml` is brittle and likely misclassifies lines in non-[project] sections.
The current logic mixes section header detection with dependency array parsing and toggles `in_dependencies` on any `[`-prefixed line, which can misinterpret non-`[project]` sections (e.g. `[tool.poetry]` with a `dependencies = [` array). Consider instead:
- Track the current section name whenever `trimmed.starts_with('[')`.
- Set `in_dependencies = true` only when `current_section == "project"` and you see `dependencies = [`.
- Keep `in_dependencies` scoped to the `[project]` section and use a separate flag for other sections.
This will prevent accidentally parsing dependency strings from unrelated tables and make the control flow clearer.
</issue_to_address>
### Comment 2
<location path="tools/scanner/src/cataloger/mod.rs" line_range="174-176" />
<code_context>
+ let matches = if let Some(suffix) = pattern.strip_prefix('*') {
+ // Suffix match: *.lock, *.toml, etc.
+ name.ends_with(suffix)
+ } else if pattern.contains('/') {
+ // Path-based match.
+ path_str.ends_with(pattern)
+ } else {
+ // Exact filename match.
</code_context>
<issue_to_address>
**issue:** Path-based pattern matching assumes `/` as a separator, which will not work on Windows paths.
Using `path_str.ends_with(pattern)` with `/` in the pattern depends on the `Path` display representation matching that separator. On Windows, paths use `\`, so patterns like `"var/lib/dpkg/status"` or `"lib/apk/db/installed"` will never match. To make this portable, either compare `Path` components instead of string suffixes, or normalize both the path and the pattern to a common separator before matching.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| fn parse_pyproject_toml(content: &str, path: &Path, packages: &mut Vec<DiscoveredPackage>) { | ||
| // Track which section we're in. | ||
| let mut in_dependencies = false; | ||
| let mut in_poetry_deps = false; | ||
|
|
||
| for line in content.lines() { | ||
| let trimmed = line.trim(); | ||
|
|
||
| // Section headers. | ||
| if trimmed.starts_with('[') { |
There was a problem hiding this comment.
issue (bug_risk): Section tracking in parse_pyproject_toml is brittle and likely misclassifies lines in non-[project] sections.
The current logic mixes section header detection with dependency array parsing and toggles in_dependencies on any [-prefixed line, which can misinterpret non-[project] sections (e.g. [tool.poetry] with a dependencies = [ array). Consider instead:
- Track the current section name whenever
trimmed.starts_with('['). - Set
in_dependencies = trueonly whencurrent_section == "project"and you seedependencies = [. - Keep
in_dependenciesscoped to the[project]section and use a separate flag for other sections.
This will prevent accidentally parsing dependency strings from unrelated tables and make the control flow clearer.
| } else if pattern.contains('/') { | ||
| // Path-based match. | ||
| path_str.ends_with(pattern) |
There was a problem hiding this comment.
issue: Path-based pattern matching assumes / as a separator, which will not work on Windows paths.
Using path_str.ends_with(pattern) with / in the pattern depends on the Path display representation matching that separator. On Windows, paths use \, so patterns like "var/lib/dpkg/status" or "lib/apk/db/installed" will never match. To make this portable, either compare Path components instead of string suffixes, or normalize both the path and the pattern to a common separator before matching.
24e9631 to
893a8c1
Compare
A Rust CLI tool that scans directories, SBOM files, container images, and bare PURLs for known vulnerabilities using a trustify backend. Scan targets: - dir:PATH — walk filesystem, discover packages via catalogers - sbom:PATH — parse SPDX (2.x/3.x) or CycloneDX JSON - registry:IMAGE:TAG — pull from OCI registry (SBOM-first, layer fallback) - oci-archive:PATH — unpack local OCI archive tarball - pkg:TYPE/NAME@VERSION — analyze a single PURL directly Package catalogers: - APK (Alpine), dpkg (Debian/Ubuntu), RPM (filename heuristic) - Cargo.lock, go.sum, package-lock.json (v1/v2/v3) - requirements.txt, pyproject.toml, uv.lock, poetry.lock, Pipfile.lock - gradle.lockfile Container scanning uses SBOM-first strategy: checks for cosign SBOM attachments (sha256-DIGEST.sbom tag convention) before falling back to layer extraction. This matches how Red Hat SBOMer/Konflux builds attach SBOMs to container images. Output formats: list (grype-style default), table, JSON. CI integration via --fail-on severity threshold (exit code 1). Auth: static token, OIDC client_credentials, or unauthenticated. Security: symlinks off by default (--follow-links to enable), 256 MiB file size limit, 2 GiB extraction limit, CycloneDX recursion depth cap, tar path traversal rejection, UTF-8 safe truncation. Includes 28 unit tests, Makefile, README, and architecture doc.
893a8c1 to
48cb46d
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #2527 +/- ##
==========================================
- Coverage 71.69% 68.82% -2.88%
==========================================
Files 457 477 +20
Lines 27743 29729 +1986
Branches 27743 29729 +1986
==========================================
+ Hits 19890 20460 +570
- Misses 6710 8102 +1392
- Partials 1143 1167 +24 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Illustrates a security scanner CLI that discovers packages in a target and checks them against a trustify instance for known vulnerabilities.
ed: Maybe this could be in a different repo but offering it here first.
Like grype but backed by trustify instead of a local DB (no consideration for 'air gapped' environments just yet).
This brings trustify to where engineers, developers and users are, in their local environments.
What it does
Scan standalone sbom:
Scan project in filesystem:
Scan container:
Scan an individual purl:
Discovers packages → extracts PURLs → calls POST /api/v3/vulnerability/analyze → renders results, for example:
Container scanning
Uses SBOM-first strategy: checks for cosign SBOM attachments (sha256-DIGEST.sbom tag) before falling back to filesystem layer extraction. This leverages the SBOMs that SBOMer/Konflux already attach to Red Hat container images.
Supported ecosystems
Cargo.lock, go.sum, package-lock.json, requirements.txt, pyproject.toml, uv.lock, poetry.lock, Pipfile.lock, gradle.lockfile, APK (Alpine), dpkg (Debian/Ubuntu), RPM, SPDX 2.x/3.x, CycloneDX.
Summary by Sourcery
Introduce a new trustify-scanner CLI workspace tool that discovers packages from SBOMs, directories, containers, and OCI archives, sends their PURLs to the Trustify API for vulnerability analysis, and reports results with multiple output formats and CI-friendly behavior.
New Features:
Enhancements:
Documentation:
Tests: