diff --git a/antora/docs/modules/ROOT/pages/packages/release_maven_repos.adoc b/antora/docs/modules/ROOT/pages/packages/release_maven_repos.adoc new file mode 100644 index 000000000..cb936c9ad --- /dev/null +++ b/antora/docs/modules/ROOT/pages/packages/release_maven_repos.adoc @@ -0,0 +1,32 @@ += All maven artifacts have known repository URLs Package + +Each Maven package listed in an SBOM must specify the repository URL that it comes from, and that URL must be present in the list of known and permitted Maven repositories. If no URL is specified, the package is assumed to come from Maven Central. + +== Package Name + +* `maven_repos` + +== Rules Included + +[#maven_repos__deny_unpermitted_urls] +=== link:#maven_repos__deny_unpermitted_urls[Known Repository URLs] + +Each Maven package listed in an SBOM must specify the repository URL that it comes from, and that URL must be present in the list of known and permitted Maven repositories. If no URL is specified, the package is assumed to come from Maven Central. + +* Rule type: [rule-type-indicator failure]#FAILURE# +* FAILURE message: `%s` +* Code: `maven_repos.deny_unpermitted_urls` +* Effective from: `2026-05-10T00:00:00Z` +* https://github.com/conforma/policy/blob/{page-origin-refhash}/policy/release/maven_repos/maven_repos.rego#L40[Source, window="_blank"] + +[#maven_repos__policy_data_missing] +=== link:#maven_repos__policy_data_missing[Policy data validation] + +Ensures the required allowed_maven_repositories list is provided. + +*Solution*: Ensure that 'allowed_maven_repositories' is defined in the rule_data provided to the policy, and that it contains a list of authorized repository URLs. + +* Rule type: [rule-type-indicator failure]#FAILURE# +* FAILURE message: `Policy data is missing the required "%s" list` +* Code: `maven_repos.policy_data_missing` +* https://github.com/conforma/policy/blob/{page-origin-refhash}/policy/release/maven_repos/maven_repos.rego#L22[Source, window="_blank"] diff --git a/antora/docs/modules/ROOT/pages/release_policy.adoc b/antora/docs/modules/ROOT/pages/release_policy.adoc index 7368c6117..0649fa27b 100644 --- a/antora/docs/modules/ROOT/pages/release_policy.adoc +++ b/antora/docs/modules/ROOT/pages/release_policy.adoc @@ -71,6 +71,7 @@ a| Include policy rules responsible for validating rule data. Rules included: +* xref:packages/release_maven_repos.adoc#maven_repos__policy_data_missing[All maven artifacts have known repository URLs: Policy data validation] * xref:packages/release_attestation_type.adoc#attestation_type__known_attestation_types_provided[Attestation type: Known attestation types provided] * xref:packages/release_base_image_registries.adoc#base_image_registries__allowed_registries_provided[Base image checks: Allowed base image registry prefixes list was provided] * xref:packages/release_buildah_build_task.adoc#buildah_build_task__disallowed_platform_patterns_pattern[Buildah build task: disallowed_platform_patterns format] @@ -103,6 +104,7 @@ a| Include the set of policy rules required for Red Hat products. Rules included: +* xref:packages/release_maven_repos.adoc#maven_repos_package[All maven artifacts have known repository URLs: All maven artifacts have known repository URLs] * xref:packages/release_attestation_type.adoc#attestation_type__deprecated_policy_attestation_format[Attestation type: Deprecated policy attestation format] * xref:packages/release_attestation_type.adoc#attestation_type__known_attestation_type[Attestation type: Known attestation type found] * xref:packages/release_attestation_type.adoc#attestation_type__known_attestation_types_provided[Attestation type: Known attestation types provided] @@ -233,6 +235,13 @@ Rules included: * xref:packages/release_rpm_ostree_task.adoc#rpm_ostree_task__builder_image_param[rpm-ostree Task: Builder image parameter] * xref:packages/release_rpm_ostree_task.adoc#rpm_ostree_task__rule_data[rpm-ostree Task: Rule data] +| [#redhat_maven]`redhat_maven` +a| Ruleset for validating artifacts built via Red Hat Maven repositories. + +Rules included: + +* xref:packages/release_maven_repos.adoc#maven_repos_package[All maven artifacts have known repository URLs: All maven artifacts have known repository URLs] + | [#redhat_rpms]`redhat_rpms` a| Include the set of policy rules required for building Red Hat RPMs. @@ -336,6 +345,9 @@ Rules included: |*Package Name* |*Description* +| xref:packages/release_maven_repos.adoc[maven_repos] +a| Each Maven package listed in an SBOM must specify the repository URL that it comes from, and that URL must be present in the list of known and permitted Maven repositories. If no URL is specified, the package is assumed to come from Maven Central. + | xref:packages/release_attestation_type.adoc[attestation_type] a| Sanity checks related to the format of the image build's attestation. diff --git a/antora/docs/modules/ROOT/partials/release_policy_nav.adoc b/antora/docs/modules/ROOT/partials/release_policy_nav.adoc index 11bc8022f..964e1dd5d 100644 --- a/antora/docs/modules/ROOT/partials/release_policy_nav.adoc +++ b/antora/docs/modules/ROOT/partials/release_policy_nav.adoc @@ -4,10 +4,14 @@ *** xref:release_policy.adoc#minimal[minimal] *** xref:release_policy.adoc#policy_data[policy_data] *** xref:release_policy.adoc#redhat[redhat] +*** xref:release_policy.adoc#redhat_maven[redhat_maven] *** xref:release_policy.adoc#redhat_rpms[redhat_rpms] *** xref:release_policy.adoc#rhtap-multi-ci[rhtap-multi-ci] *** xref:release_policy.adoc#slsa3[slsa3] ** Release Rules +*** xref:packages/release_maven_repos.adoc[All maven artifacts have known repository URLs] +**** xref:packages/release_maven_repos.adoc#maven_repos__deny_unpermitted_urls[Known Repository URLs] +**** xref:packages/release_maven_repos.adoc#maven_repos__policy_data_missing[Policy data validation] *** xref:packages/release_attestation_type.adoc[Attestation type] **** xref:packages/release_attestation_type.adoc#attestation_type__deprecated_policy_attestation_format[Deprecated policy attestation format] **** xref:packages/release_attestation_type.adoc#attestation_type__known_attestation_type[Known attestation type found] diff --git a/policy/lib/sbom/maven.rego b/policy/lib/sbom/maven.rego new file mode 100644 index 000000000..1ca3f7604 --- /dev/null +++ b/policy/lib/sbom/maven.rego @@ -0,0 +1,64 @@ +# METADATA +# title: Maven Package Extraction +# description: >- +# Extracts Maven packages and their repository URLs from both CycloneDX +# and SPDX SBOM formats. +package lib.sbom + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +packages contains pkg if { + some pkg in _cyclonedx_maven_packages +} + +packages contains pkg if { + some pkg in _spdx_maven_packages +} + +_cyclonedx_maven_packages contains pkg if { + some s in cyclonedx_sboms + some component in s.components + + startswith(component.purl, "pkg:maven/") + + repos := {ref.url | + some ref in component.externalRefs + ref.type in ["distribution", "artifact-repository"] + } + + final_repos := _empty_to_default(repos) + + some repo_url in final_repos + pkg := { + "purl": component.purl, + "name": component.name, + "repository_url": repo_url, + } +} + +_spdx_maven_packages contains pkg if { + some s in spdx_sboms + some item in s.packages + + startswith(item.purl, "pkg:maven/") + + repos := {ref.referenceLocator | + some ref in item.externalRefs + ref.referenceType in ["distribution", "repository"] + } + + final_repos := _empty_to_default(repos) + + some repo_url in final_repos + pkg := { + "purl": item.purl, + "name": item.name, + "repository_url": repo_url, + } +} + +_empty_to_default(repo_set) := repo_set if { + count(repo_set) > 0 +} else := {""} diff --git a/policy/lib/sbom/maven_test.rego b/policy/lib/sbom/maven_test.rego new file mode 100644 index 000000000..8d4f4ab2b --- /dev/null +++ b/policy/lib/sbom/maven_test.rego @@ -0,0 +1,104 @@ +package lib.sbom_test + +import data.lib.sbom +import future.keywords.if +import future.keywords.in + +test_cyclonedx_maven_extraction if { + mock_components := [{ + "name": "auth-lib", + "purl": "pkg:maven/org.example/auth@1.0", + "externalRefs": [{"type": "distribution", "url": "https://repo.maven.apache.org/maven2/"}], + }] + + res := sbom.packages with sbom.cyclonedx_sboms as [_cyclonedx_sbom(mock_components)] + + res == {{ + "name": "auth-lib", + "purl": "pkg:maven/org.example/auth@1.0", + "repository_url": "https://repo.maven.apache.org/maven2/", + }} +} + +test_cyclonedx_ignores_non_maven if { + mock_components := [{"name": "react", "purl": "pkg:npm/react@18.2.0"}] + + res := sbom.packages with sbom.cyclonedx_sboms as [_cyclonedx_sbom(mock_components)] + + count(res) == 0 +} + +test_cyclonedx_empty_repo_url if { + mock_components := [{ + "name": "no-repo", + "purl": "pkg:maven/org.example/no-repo@1.0", + "externalRefs": [], + }] + + res := sbom.packages with sbom.cyclonedx_sboms as [_cyclonedx_sbom(mock_components)] + + some pkg in res + pkg.repository_url == "" +} + +test_spdx_maven_extraction if { + mock_packages := [{ + "name": "data-service", + "purl": "pkg:maven/org.example/data@2.5", + "externalRefs": [{ + "referenceType": "repository", + "referenceLocator": "https://internal.jfrog.io/artifactory", + }], + }] + + res := sbom.packages with sbom.spdx_sboms as [_spdx_sbom(mock_packages)] + + res == {{ + "name": "data-service", + "purl": "pkg:maven/org.example/data@2.5", + "repository_url": "https://internal.jfrog.io/artifactory", + }} +} + +test_combined_sources if { + mock_cdx := [{ + "name": "cdx-pkg", + "purl": "pkg:maven/cdx/pkg@1", + "externalRefs": [{"type": "distribution", "url": "url1"}], + }] + + mock_spdx := [{ + "name": "spdx-pkg", + "purl": "pkg:maven/spdx/pkg@1", + "externalRefs": [{ + "referenceType": "repository", + "referenceLocator": "url2", + }], + }] + + res := sbom.packages with sbom.cyclonedx_sboms as [_cyclonedx_sbom(mock_cdx)] + with sbom.spdx_sboms as [_spdx_sbom(mock_spdx)] + + count(res) == 2 +} + +test_cyclonedx_multiple_repo_capture if { + mock_components := [{ + "name": "multi-repo-lib", + "purl": "pkg:maven/org.example/multi@1.0", + "externalRefs": [ + {"type": "distribution", "url": "https://repo-a.com"}, + {"type": "artifact-repository", "url": "https://repo-b.com"}, + ], + }] + + pkg_list := sbom.packages with sbom.cyclonedx_sboms as [_cyclonedx_sbom(mock_components)] + + count(pkg_list) == 2 + urls := {p.repository_url | some p in pkg_list} + urls == {"https://repo-a.com", "https://repo-b.com"} +} + +_cyclonedx_sbom(components) := {"components": components} + +_spdx_sbom(packages) := {"packages": packages} diff --git a/policy/release/collection/redhat_maven/redhat_maven.rego b/policy/release/collection/redhat_maven/redhat_maven.rego new file mode 100644 index 000000000..71ef642f0 --- /dev/null +++ b/policy/release/collection/redhat_maven/redhat_maven.rego @@ -0,0 +1,8 @@ +# +# METADATA +# title: redhat_maven +# description: >- +# Ruleset for validating artifacts built via Red Hat Maven repositories. +package collection.redhat_maven + +import rego.v1 diff --git a/policy/release/maven_repos/maven_repos.rego b/policy/release/maven_repos/maven_repos.rego new file mode 100644 index 000000000..e407b03f4 --- /dev/null +++ b/policy/release/maven_repos/maven_repos.rego @@ -0,0 +1,85 @@ +# METADATA +# title: All maven artifacts have known repository URLs +# description: >- +# Each Maven package listed in an SBOM must specify the repository URL that it +# comes from, and that URL must be present in the list of known and permitted +# Maven repositories. If no URL is specified, the package is assumed to come +# from Maven Central. +# custom: +# collections: +# - redhat +# - redhat_maven +# +package release.maven_repos + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.lib +import data.lib.sbom + +# METADATA +# title: Policy data validation +# description: Ensures the required allowed_maven_repositories list is provided. +# custom: +# short_name: policy_data_missing +# failure_msg: Policy data is missing the required "%s" list +# solution: >- +# Ensure that 'allowed_maven_repositories' is defined in the rule_data +# provided to the policy, and that it contains a list of authorized +# repository URLs. +# collections: +# - policy_data +# severity: failure +deny contains result if { + some key in _rule_data_errors + result := lib.result_helper(rego.metadata.chain(), [key]) +} + +# METADATA +# title: Known Repository URLs +# description: >- +# Each Maven package listed in an SBOM must specify the repository URL that it +# comes from, and that URL must be present in the list of known and permitted +# Maven repositories. If no URL is specified, the package is assumed to come +# from Maven Central. +# scope: rule +# custom: +# short_name: deny_unpermitted_urls +# failure_msg: '%s' +# effective_on: 2026-05-10T00:00:00Z +deny contains result if { + some purl, msg in _repo_url_errors + base := lib.result_helper(rego.metadata.chain(), [msg]) + result := object.union(base, {"term": purl}) +} + +_repo_url_errors[pkg.purl] := msg if { + some pkg in sbom.packages + source := _get_effective_url(pkg.repository_url) + not _url_is_permitted(source) + msg := sprintf("Package %q (source: %q) is not in the permitted list", [pkg.purl, source]) +} + +_get_effective_url(url) := url if { + url != "" +} else := "https://repo.maven.apache.org/maven2/" + +_url_is_permitted(url) if { + permitted := lib.rule_data("allowed_maven_repositories") + url in permitted +} + +_rule_data_errors contains key if { + key := "allowed_maven_repositories" + data_list := lib.rule_data(key) + _is_invalid_data(data_list) +} + +_is_invalid_data(val) if not is_array(val) + +_is_invalid_data(val) if { + is_array(val) + count(val) == 0 +} diff --git a/policy/release/maven_repos/maven_repos_test.rego b/policy/release/maven_repos/maven_repos_test.rego new file mode 100644 index 000000000..242f0e794 --- /dev/null +++ b/policy/release/maven_repos/maven_repos_test.rego @@ -0,0 +1,154 @@ +package release.maven_repos_test + +import data.lib +import data.lib.sbom +import data.release.maven_repos +import future.keywords.if + +mock_data := {"allowed_maven_repositories": [ + "https://repo.maven.apache.org/maven2/", + "https://repo.clojars.org/", +]} + +test_cyclonedx_permitted if { + cdx_input := {"components": [{ + "purl": "pkg:maven/org.apache/log4j@2.17.1", + "name": "log4j", + "externalRefs": [{"type": "distribution", "url": "https://repo.maven.apache.org/maven2/"}], + }]} + + lib.assert_empty(maven_repos.deny) with data.rule_data as mock_data + with sbom.cyclonedx_sboms as [cdx_input] +} + +test_spdx_permitted if { + spdx_input := {"packages": [{ + "purl": "pkg:maven/com.redhat/example@1.0", + "name": "example", + "externalRefs": [{ + "referenceType": "distribution", + "referenceLocator": "https://repo.clojars.org/", + }], + }]} + + lib.assert_empty(maven_repos.deny) with data.rule_data as mock_data + with sbom.spdx_sboms as [spdx_input] +} + +test_default_maven_central_pass if { + cdx_input := {"components": [{ + "purl": "pkg:maven/org.base/no-url@1.0", + "name": "no-url", + "externalRefs": [], + }]} + + lib.assert_empty(maven_repos.deny) with data.rule_data as mock_data + with sbom.cyclonedx_sboms as [cdx_input] +} + +test_default_cdx_fail if { + restricted_data := {"allowed_maven_repositories": ["https://internal.repo/"]} + + mock_cdx := {"components": [{ + "purl": "pkg:maven/org.base/no-url@1.0", + "name": "no-url", + "externalRefs": [], + }]} + + expected_msg := sprintf("Package %q (source: %q) is not in the permitted list", [ + "pkg:maven/org.base/no-url@1.0", + "https://repo.maven.apache.org/maven2/", + ]) + + expected := {{ + "code": "release.maven_repos.deny_unpermitted_urls", + "msg": expected_msg, + "effective_on": "2026-05-10T00:00:00Z", + "term": "pkg:maven/org.base/no-url@1.0", + }} + + lib.assert_equal(maven_repos.deny, expected) with data.rule_data as restricted_data + with sbom.cyclonedx_sboms as [mock_cdx] +} + +test_spdx_default_fail if { + mock_spdx := {"packages": [{ + "name": "no-url", + "purl": "pkg:maven/org.base/no-url@1.0", + "externalRefs": [{"referenceType": "purl", "referenceLocator": "pkg:maven/org.base/no-url@1.0"}], + "downloadLocation": "NOASSERTION", + }]} + result := maven_repos.deny with sbom.spdx_sboms as [mock_spdx] + with data.rule_data as {"allowed_maven_repositories": ["https://internal.repo/"]} + count(result) > 0 +} + +test_missing_rule_data if { + expected := {{ + "code": "release.maven_repos.policy_data_missing", + "collections": ["policy_data"], + "effective_on": "2022-01-01T00:00:00Z", + "msg": "Policy data is missing the required \"allowed_maven_repositories\" list", + }} + lib.assert_equal(maven_repos.deny, expected) with data.rule_data as {} +} + +test_get_effective_url_provided if { + url := "https://repo1.maven.org/maven2/" + maven_repos._get_effective_url(url) == url +} + +test_url_is_permitted_true if { + mock_allowed := ["https://repo.maven.apache.org/maven2/", "https://internal.repo/"] + + maven_repos._url_is_permitted("https://internal.repo/") with data.rule_data.allowed_maven_repositories as mock_allowed +} + +test_url_is_permitted_false if { + mock_allowed := ["https://internal.repo/"] + target_url := "https://repo.maven.apache.org/maven2/" + + not maven_repos._url_is_permitted(target_url) with data.rule_data.allowed_maven_repositories as mock_allowed +} + +test_rule_data_errors_when_empty_array if { + mock_data := {"allowed_maven_repositories": []} + + errors := maven_repos._rule_data_errors with data.rule_data as mock_data + + count(errors) == 1 +} + +test_cyclonedx_multiple_refs_behavior if { + mock_cdx := {"components": [{ + "name": "multi-ref-lib", + "purl": "pkg:maven/org.example/multi@1.0", + "externalRefs": [ + {"type": "distribution", "url": "https://first.repo.com"}, + {"type": "artifact-repository", "url": "https://second.repo.com"}, + ], + }]} + + pkg_list := sbom.packages with sbom.cyclonedx_sboms as [mock_cdx] + + count(pkg_list) == 2 + urls := {p.repository_url | some p in pkg_list} + urls == {"https://first.repo.com", "https://second.repo.com"} +} + +test_spdx_multiple_refs_behavior if { + mock_spdx := {"packages": [{ + "name": "multi-ref-spdx", + "purl": "pkg:maven/org.example/spdx@1.0", + "externalRefs": [ + {"referenceType": "repository", "referenceLocator": "https://primary.repo.com"}, + {"referenceType": "distribution", "referenceLocator": "https://mirror.repo.com"}, + ], + }]} + + pkg_list := sbom.packages with sbom.spdx_sboms as [mock_spdx] + + count(pkg_list) == 2 + urls := {p.repository_url | some p in pkg_list} + urls == {"https://primary.repo.com", "https://mirror.repo.com"} +}