Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,23 @@ Other times there might be no results because the purl is not linked to any prod
$ trust-products -d pkg:oci/quay-builder-qemu-rhcos-rhel8
```

#### Including RPM packages in containers

By default, `trust-products` filters out RPM packages that are found within container images to avoid false positives. However, many modern products (like MTV, OpenShift components) have RPM packages bundled in OCI containers. To include these results, use the `--include-rpm-containers` flag:

```console
$ trust-products --include-rpm-containers pkg:rpm/redhat/harfbuzz
pkg:rpm/redhat/harfbuzz
└── pkg:oci/mtv-must-gather-rhel8
└── cpe:/a:redhat:migration_toolkit_virtualization:2.10:*:el8:*
```

Or use the short form `-i`:

```console
$ trust-products -i pkg:rpm/redhat/harfbuzz
```

### Prime the Trustify graph:
If components are found with the trust-purl command, but they are not being linked to products with
trust-products, it could be because the Trustify graph cache is not yet primed. To prime the graph
Expand All @@ -151,5 +168,48 @@ It can also be run with `--check` to see the graph and sbom counts without actua
It's possible to map CPEs to products using product metadata as demonstrated in the `docs/product-definitions.json`
file. This allows integration with a bug tracking system like Jira.

The way this mapping works is to match against a ps_update_steam if such a map exists. If not, we try to match
The way this mapping works is to match against a ps_update_stream if such a map exists. If not, we try to match
against ps_modules.

#### RHEL Release Graph Mapping

For RHEL product streams (RHEL 9 and earlier), TrustShell uses an enhanced mapping mechanism that leverages the
RHEL release graph data from the `rhel-release-graph` repository. This feature addresses a specific issue with
how packages are distributed in RHEL minor releases.

**Why this feature is needed:**

SBOM data returned by Trustify already contain CPEs with minor versions (e.g., `cpe:/a:redhat:enterprise_linux:9.6::appstream`).
However, for RHEL 9 and earlier versions, packages are not re-released when a new minor version is created. Instead,
all packages are pushed into a DNF repository. This means that if a package was only released at the beginning of a RHEL stream (e.g., when RHEL 9 was at version 9.0), and subsequent releases occurred for 9.1, 9.2, etc., that package would not be visible to those later minor release streams when using direct CPE matching.

The RHEL release graph mapping solves this by:
1. Using the release hierarchy from the `rhel-release-graph` repository to understand parent-child relationships
between RHEL releases
2. When a CPE matches a parent release node, automatically associating it with all descendant leaf nodes whose
CPEs are in active product streams
3. This ensures that packages released in earlier minor versions are correctly associated with later minor version
streams

**EUS (Extended Update Support) Streams:**

Another important justification for the RHEL release graph mapping is the handling of EUS streams. When an EUS stream
is created, it includes only packages that were previously released in the 'main' RHEL stream up to the EUS start date.
Any packages that are updated or newly released in the 'main' RHEL stream after the EUS start date should **not** be
included in the EUS stream.

The rhel-release-graph mapping, along with special handling and minor version CPEs, ensures that:
- EUS streams correctly inherit only packages from their parent release that existed before the EUS start date
- Packages released after the EUS start date in the main stream are properly excluded from EUS streams
- The release hierarchy accurately reflects the temporal relationships between main RHEL releases and their EUS variants

**RHEL 10 and later:**

This mapping feature is **not necessary** for RHEL 10 and later versions, as a separate CPE is created for each
minor release. The direct CPE matching works correctly for these versions.

**Configuration:**

The RHEL release graph data is automatically loaded from the GitLab repository specified by the `RHEL_RELEASE_GRAPH_URL`
environment variable (see the [Configuration](#configuration) section above). The system caches this data locally and
only refreshes when the repository is updated.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "trustshell"
version = "0.2.1"
version = "0.2.2"
description = "Command Line tool for Trustify"
readme = "README.md"
authors = [
Expand Down
30 changes: 25 additions & 5 deletions src/trustshell/products.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ def prime_cache(check: bool, debug: bool) -> None:
)
@click.option("--latest", "-l", is_flag=True, default=False)
@click.option("--cpes", "-c", is_flag=True, default=False)
@click.option(
"--include-rpm-containers",
"-i",
is_flag=True,
default=False,
help="Include RPM packages found in container images.",
)
@click.option("--flaw", "-f", help="OSIDB flaw uuid or CVE")
@click.option(
"--replace",
Expand All @@ -119,6 +126,7 @@ def search(
latest: bool,
cpes: bool,
versions: bool,
include_rpm_containers: bool,
) -> None:
"""Relate a purl to products in Trustify"""
if not debug:
Expand All @@ -132,7 +140,12 @@ def search(
console.print(f"{purl} is not a valid Package URL", style="error")
sys.exit(1)

ancestor_trees = _get_roots(purl, latest, show_versions=versions)
ancestor_trees = _get_roots(
purl,
latest,
show_versions=versions,
include_rpm_containers=include_rpm_containers,
)
if not ancestor_trees or len(ancestor_trees) == 0:
console.print("No results")
return
Expand Down Expand Up @@ -229,7 +242,10 @@ def extract_affects(ancestor_trees: list[Node]) -> set[tuple[str, str]]:


def _get_roots(
base_purl: str, latest: bool = True, show_versions: bool = False
base_purl: str,
latest: bool = True,
show_versions: bool = False,
include_rpm_containers: bool = False,
) -> list[Node]:
"""Look up base_purl ancestors in Trustify

Expand All @@ -254,7 +270,7 @@ def _get_roots(
endpoint, base_params, auth_header, component_name=base_purl
)
logger.debug(f"Number of matches for {base_purl}: {ancestors['total']}")
return _trees_with_cpes(ancestors, show_versions)
return _trees_with_cpes(ancestors, show_versions, include_rpm_containers)


def build_ancestor_tree(
Expand Down Expand Up @@ -414,7 +430,11 @@ def _remove_duplicate_branches(root: Node) -> Node:
return root


def _trees_with_cpes(ancestor_data: dict[str, Any], show_versions: bool) -> list[Node]:
def _trees_with_cpes(
ancestor_data: dict[str, Any],
show_versions: bool,
include_rpm_containers: bool = False,
) -> list[Node]:
"""Builds a tree of ancestors with a target component root"""
if "items" not in ancestor_data or not ancestor_data["items"]:
return []
Expand All @@ -428,7 +448,7 @@ def _trees_with_cpes(ancestor_data: dict[str, Any], show_versions: bool) -> list
trees_with_cpes: list[Node] = []
for tree in first_children:
# Remove this once https://issues.redhat.com/browse/TC-2659 is implemented
if tree.name.startswith("pkg:rpm/"):
if tree.name.startswith("pkg:rpm/") and not include_rpm_containers:
if container_in_tree(tree):
continue
_remove_non_cpe_branches(tree)
Expand Down
Loading