@@ -17,64 +17,107 @@ import data.lib.tekton
1717# METADATA
1818# title: Unique Version
1919# description: >-
20- # Check if there is more than one version of the same RPM installed across different
21- # architectures . This check only applies for Image Indexes, aka multi-platform images.
20+ # Check if a multi-arch build has the same RPM versions installed across each different
21+ # architecture . This check only applies for Image Indexes, aka multi-platform images.
2222# Use the `non_unique_rpm_names` rule data key to ignore certain RPMs.
2323# custom:
2424# short_name: unique_version
25- # failure_msg: 'Multiple versions of the %q RPM were found: %s'
25+ # failure_msg: 'Mismatched versions of the %q RPM were found across different arches. %s'
2626# collections:
2727# - redhat
28- # # Pushed back again due to https://issues.redhat.com/browse/EC-1354
29- # effective_on: 2025-10-01T00:00:00Z
3028#
3129deny contains result if {
3230 image.is_image_index (input.image.ref)
3331
34- # Arguably this is a weird edge case that should be dealt with by changing
35- # the build to not be multi-arch. But this avoids producing a confusing and
36- # not useful violation in some cases.
37- not _is_single_image_index (input.image.ref )
32+ some rpm_name in rpm_names_with_mismatched_nvr_sets
33+ not rpm_name in lib. rule_data ( " non_unique_rpm_names " )
34+
35+ detail_text := concat ( " " , sort ( rpm_mismatch_details (rpm_name)) )
3836
39- some name, versions in grouped_rpm_purls
40- count (versions) > 1
41- not name in lib.rule_data (" non_unique_rpm_names" )
4237 result := lib.result_helper_with_term (
4338 rego.metadata.chain (),
44- [name, concat ( " , " , versions) ],
45- name ,
39+ [rpm_name, detail_text ],
40+ rpm_name ,
4641 )
4742}
4843
49- # grouped_rpm_purls groups the found RPMs by name to facilitate detecting different versions. It
50- # has the following structure:
51- # {
52- # "spam-maps": {"1.2.3-0", "1.2.3-9"},
53- # "bacon": {"7.8.8-8"},
54- # }
55- grouped_rpm_purls[name] contains version if {
56- some rpm_purl in all_rpm_purls
57- rpm := ec.purl.parse (rpm_purl)
58- name := rpm.name
44+ # Do some extra work here to make a nice tidy violation message
45+ rpm_mismatch_details (rpm_name) := [detail |
46+ # Get all unique NVR sets for this RPM
47+ some nvr_set in {nvrs |
48+ some platform, nvrs in all_rpms_by_name_and_platform[rpm_name]
49+ }
50+
51+ # Find all platforms that have this NVR set
52+ platforms_with_nvr_set := [platform |
53+ some platform, nvrs in all_rpms_by_name_and_platform[rpm_name]
54+ nvrs == nvr_set
55+ ]
5956
60- # NOTE: This includes both version and release.
61- version := rpm.version
57+ detail := sprintf (" %s %s %s %s." , [
58+ lib.pluralize_maybe (platforms_with_nvr_set, " Platform" , " " ),
59+ concat (" , " , sort (platforms_with_nvr_set)),
60+ lib.pluralize_maybe (platforms_with_nvr_set, " has" , " have" ),
61+ concat (" , " , sort (nvr_set)),
62+ ])
63+ ]
64+
65+ # Detect RPMs where the set of nvrs differs across platforms.
66+ # Generally the sets of versions are of size one, but in some cases we have more
67+ # than one version of a particular rpm due to multi-stage builds.
68+ rpm_names_with_mismatched_nvr_sets contains rpm_name if {
69+ some rpm_name, platform_sets in all_rpms_by_name_and_platform
70+ nvr_sets := {nvrs | some _platform, nvrs in platform_sets}
71+
72+ # If there are more than one unique set of nvrs, then we have some
73+ # kind of mismatch between the platforms
74+ count (nvr_sets) > 1
6275}
6376
64- all_rpm_purls contains rpm.purl if {
77+ # A list of rpms grouped by rpm name and by platform
78+ # Something like this:
79+ # {
80+ # "acl": {
81+ # "linux/arm64": ["acl-2.3.1-4.el9"],
82+ # "linux/ppc64le": ["acl-2.3.1-4.el9"],
83+ # ...
84+ # },
85+ # ...
86+ # }
87+ all_rpms_by_name_and_platform[rpm_name][platform] contains nvr if {
6588 some attestation in lib.pipelinerun_attestations
89+
90+ # We're expecting multiple matrixed build tasks, one
91+ # for each platform
6692 some build_task in tekton.build_tasks (attestation)
93+
94+ # Determine which os/arch was built by this build task.
95+ # Note: We expect this to be present for the Konflux multi-arch builds. If it
96+ # isn't then this check doesn't work and mismatched rpm versions will not be
97+ # detected. If there was somehow a different way to build a multi-arch image,
98+ # we would need to find another way to figure out which platform each SBOM is
99+ # related to.
100+ platform := tekton.task_param (build_task, " PLATFORM" )
101+
102+ # Find the SBOM location
67103 some result in tekton.task_results (build_task)
68104 result.name == " SBOM_BLOB_URL"
69- url := result.value
70- blob := ec.oci.blob (url)
71- s := json.unmarshal (blob)
72- some rpm in sbom.rpms_from_sbom (s)
73- }
105+ sbom_blob_ref := result.value
74106
75- # For detecting image indexes with just a single image in them.
76- # (I don't think there are any valid reasons for these to exist)
77- _is_single_image_index (ref) if {
78- index := ec.oci.image_index (ref)
79- count (index.manifests) == 1
80- } else := false
107+ # Fetch the SBOM data
108+ sbom_blob := ec.oci.blob (sbom_blob_ref)
109+ s := json.unmarshal (sbom_blob)
110+
111+ # Extract the list of rpm purls from the SBOM and parse out
112+ # the rpm version details
113+ some rpm_info in sbom.rpms_from_sbom (s)
114+ rpm := ec.purl.parse (rpm_info.purl)
115+ rpm_name := rpm.name
116+ rpm_version := rpm.version
117+
118+ # We really only need the version, but it's convenient for
119+ # creating violation messages if we use the full nvr here.
120+ # Note that rpm.version is actually the version and the release in
121+ # RPM terms, hence this is the name-version-release, aka the nvr
122+ nvr := sprintf (" %s-%s" , [rpm_name, rpm_version])
123+ }
0 commit comments