From 82b0df6c5657e1b1fa7687589c7f4a87c401c202 Mon Sep 17 00:00:00 2001 From: Yashvardhan Nanavati Date: Mon, 2 Dec 2024 22:51:44 -0800 Subject: [PATCH] feat: add fips-operator-check task Refers to CVP-4333. This task uses the check-payload tool to verify if an operator bundle image is FIPS compliant.It utilizes Tekton stepAction because the code will be reused for checking FBC fragments in the fbc-validation check. Signed-off-by: Yashvardhan Nanavati --- CODEOWNERS | 19 +- renovate.json | 3 + .../0.1/README.md | 16 ++ .../0.1/fips-operator-check-step-action.yaml | 144 ++++++++++++++++ .../0.1/README.md | 17 ++ .../fips-operator-bundle-check-oci-ta.yaml | 162 ++++++++++++++++++ .../0.1/recipe.yaml | 9 + task/fips-operator-bundle-check/0.1/README.md | 42 +++++ .../0.1/fips-operator-bundle-check.yaml | 147 ++++++++++++++++ 9 files changed, 551 insertions(+), 8 deletions(-) create mode 100644 stepactions/fips-operator-check-step-action/0.1/README.md create mode 100644 stepactions/fips-operator-check-step-action/0.1/fips-operator-check-step-action.yaml create mode 100644 task/fips-operator-bundle-check-oci-ta/0.1/README.md create mode 100644 task/fips-operator-bundle-check-oci-ta/0.1/fips-operator-bundle-check-oci-ta.yaml create mode 100644 task/fips-operator-bundle-check-oci-ta/0.1/recipe.yaml create mode 100644 task/fips-operator-bundle-check/0.1/README.md create mode 100644 task/fips-operator-bundle-check/0.1/fips-operator-bundle-check.yaml diff --git a/CODEOWNERS b/CODEOWNERS index f02c79814a..3f8e719298 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -52,14 +52,17 @@ /.tekton/tasks/ec-checks.yaml @konflux-ci/ec # renovate groupName=integration -/task/clair-scan @konflux-ci/integration-service-maintainers -/task/clamav-scan @konflux-ci/integration-service-maintainers -/task/deprecated-image-check @konflux-ci/integration-service-maintainers -/task/fbc-related-image-check @konflux-ci/integration-service-maintainers -/task/fbc-validation @konflux-ci/integration-service-maintainers -/task/inspect-image @konflux-ci/integration-service-maintainers -/task/sbom-json-check @konflux-ci/integration-service-maintainers -/task/validate-fbc @konflux-ci/integration-service-maintainers +/task/clair-scan @konflux-ci/integration-service-maintainers +/task/clamav-scan @konflux-ci/integration-service-maintainers +/task/deprecated-image-check @konflux-ci/integration-service-maintainers +/task/fbc-related-image-check @konflux-ci/integration-service-maintainers +/task/fbc-validation @konflux-ci/integration-service-maintainers +/task/inspect-image @konflux-ci/integration-service-maintainers +/task/sbom-json-check @konflux-ci/integration-service-maintainers +/task/validate-fbc @konflux-ci/integration-service-maintainers +/task/fips-operator-bundle-check @konflux-ci/integration-service-maintainers +/task/fips-operator-bundle-check-oci-ta @konflux-ci/integration-service-maintainers +/stepactions/fips-operator-check-step-action @konflux-ci/integration-service-maintainers # renovate groupName=integration /task/coverity-availability-check @konflux-ci/integration-service-maintainers @kdudka diff --git a/renovate.json b/renovate.json index 648ef3f51b..8d2fa60d32 100644 --- a/renovate.json +++ b/renovate.json @@ -80,6 +80,7 @@ { "groupName": "integration", "matchFileNames": [ + "stepactions/fips-operator-check-step-action/**", "task/clair-scan/**", "task/clamav-scan/**", "task/coverity-availability-check-oci-ta/**", @@ -87,6 +88,8 @@ "task/deprecated-image-check/**", "task/fbc-related-image-check/**", "task/fbc-validation/**", + "task/fips-operator-bundle-check-oci-ta/**", + "task/fips-operator-bundle-check/**", "task/inspect-image/**", "task/sast-coverity-check-oci-ta/**", "task/sast-coverity-check/**", diff --git a/stepactions/fips-operator-check-step-action/0.1/README.md b/stepactions/fips-operator-check-step-action/0.1/README.md new file mode 100644 index 0000000000..276f354775 --- /dev/null +++ b/stepactions/fips-operator-check-step-action/0.1/README.md @@ -0,0 +1,16 @@ +## fips-operator-check-step-action + +This stepAction scans relatedImages of operator bundle image builds for FIPS compliance using the check-payload tool. +* The relatedImages are expected to be in a file located at `/tekton/home/unique_related_images.txt`. +* If the check-payload scan is desired to be run with the built-in exception list, the target OCP version (`v4.x`) should be in a file located at `/tekton/home/target_ocp_version.txt`. +* It also supports replacing relatedImages pullspecs with their first mirror. In order to use that, a mapping like {"source_registry_and_repo": ["mirror_registry_and_repo"]} should be stored in a file located at `/tekton/home/related-images-map.txt` + +## Results: + +| name | description | +|--------------------|--------------------------------------| +| TEST_OUTPUT | Tekton task test output. | + + +## Additional links: +https://github.com/openshift/check-payload \ No newline at end of file diff --git a/stepactions/fips-operator-check-step-action/0.1/fips-operator-check-step-action.yaml b/stepactions/fips-operator-check-step-action/0.1/fips-operator-check-step-action.yaml new file mode 100644 index 0000000000..ddd45c3d2f --- /dev/null +++ b/stepactions/fips-operator-check-step-action/0.1/fips-operator-check-step-action.yaml @@ -0,0 +1,144 @@ +--- +apiVersion: tekton.dev/v1beta1 +kind: StepAction +metadata: + labels: + app.kubernetes.io/version: "0.1" + annotations: + tekton.dev/pipelines.minVersion: "0.12.1" + tekton.dev/tags: "konflux" + name: fips-operator-check-step-action +spec: + description: >- + This stepAction scans relatedImages of operator bundle image builds for FIPS compliance using the check-payload tool. + results: + - name: TEST_OUTPUT + description: Tekton task test output. + image: quay.io/yashn/konflux-test-yashn:latest-amd64-dec17 + securityContext: + capabilities: + add: + - SETFCAP + script: | + #!/usr/bin/env bash + set -euo pipefail + # shellcheck source=/dev/null + . /utils.sh + + success_counter=0 + warnings_counter=0 + error_counter=0 + failure_counter=0 + + if [ ! -e "/tekton/home/unique_related_images.txt" ]; then + echo "No relatedImages to process" + exit 0 + fi + + related_images=$(cat /tekton/home/unique_related_images.txt) + echo "Related images are : ${related_images}" + + # If target OCP version is found, use it to apply the exception list when running check-payload + check_payload_version="" + if [ -f "/tekton/home/target_ocp_version.txt" ]; then + version=$(cat "/tekton/home/target_ocp_version.txt") + check_payload_version="-V=${version}" + echo "Target OCP version found: ${check_payload_version}" + fi + + # Check if an image to mirror map is defined for unreleased images + image_mirror_map="" + if [ -f "/tekton/home/related-images-map.txt" ]; then + image_mirror_map=$(cat "/tekton/home/related-images-map.txt") + echo "Image Mirror Map found: ${image_mirror_map}" + fi + + for related_image in ${related_images}; do + echo "Processing related image : ${related_image}" + + # Replace original pullspec with mirror, if present + if [ -n "${image_mirror_map}" ]; then + reg_and_repo=$(echo "${related_image}" | sed -E 's/^([^:@]+).*$/\1/') + first_mirror=$(echo "$image_mirror_map" | jq -r --arg image "$reg_and_repo" '.[$image][0]') + echo "${reg_and_repo} and ${first_mirror}" + if [ "$first_mirror" != "null" ]; then + replaced_image=$(replace_image_pullspec "$related_image" "$first_mirror") + echo "Replacing $related_image with $replaced_image" + related_image="$replaced_image" + fi + fi + + if ! image_labels=$(get_image_labels "${related_image}"); then + echo "Error: Could not inspect image ${related_image} for labels" + error_counter=$((error_counter + 1)) + continue + fi + component_label=$(echo "${image_labels}" | grep 'com.redhat.component=' | cut -d= -f2 || true) + echo "Component label is ${component_label}" + + if [ -z "${component_label}" ]; then + echo "Error: Could not get com.redhat.component label for ${related_image}" + error_counter=$((error_counter + 1)) + continue + fi + + # Convert image to OCI format since umoci can only handle the OCI format + if ! skopeo copy --remove-signatures "docker://${related_image}" "oci:///tekton/home/${component_label}:latest"; then + echo "Error: Could not convert image ${related_image} to OCI format" + error_counter=$((error_counter + 1)) + continue + fi + + # Unpack OCI image + if ! umoci raw unpack --rootless \ + --image "/tekton/home/${component_label}:latest" \ + "/tekton/home/unpacked-${component_label}"; then + echo "Error: Could not unpack OCI image ${related_image}" + error_counter=$((error_counter + 1)) + continue + fi + + # Run check-payload on the unpacked image + # The check-payload command fails with exit 1 when the scan for an image is unsuccessful + # or when the image is not FIPS compliant. Hence, count those as failures and not errors + if ! check-payload scan local \ + --path="/tekton/home/unpacked-${component_label}" \ + "${check_payload_version}" \ + --components="${component_label}" \ + --output-format=csv \ + --output-file="/tekton/home/report-${component_label}.csv"; then + echo "check-payload scan failed for ${related_image}" + failure_counter=$((failure_counter + 1)) + continue + fi + + if [ -f "/tekton/home/report-${component_label}.csv" ]; then + if grep -q -- "---- Successful run" "/tekton/home/report-${component_label}.csv"; then + echo "check-payload scan was successful for ${related_image}" + success_counter=$((success_counter + 1)) + elif grep -q -- "---- Successful run with warnings" "/tekton/home/report-${component_label}.csv"; then + echo "check-payload scan was successful with warnings for ${related_image}" + warnings_counter=$((warnings_counter + 1)) + fi + fi + + done + + note="Task $(context.task.name) failed: Some images could not be scanned. For details, check Tekton task log." + ERROR_OUTPUT=$(make_result_json -r ERROR -t "$note") + + note="Task $(context.task.name) completed: Check result for task result." + if [[ "$error_counter" == 0 ]]; + then + if [[ "${failure_counter}" -gt 0 ]]; then + RES="FAILURE" + elif [[ "${warnings_counter}" -gt 0 ]]; then + RES="WARNING" + else + RES="SUCCESS" + fi + TEST_OUTPUT=$(make_result_json \ + -r "${RES}" \ + -s "${success_counter}" -f "${failure_counter}" -w "${warnings_counter}" -t "$note") + fi + echo "${TEST_OUTPUT:-${ERROR_OUTPUT}}" | tee "$(step.results.TEST_OUTPUT.path)" diff --git a/task/fips-operator-bundle-check-oci-ta/0.1/README.md b/task/fips-operator-bundle-check-oci-ta/0.1/README.md new file mode 100644 index 0000000000..bd506e8c3f --- /dev/null +++ b/task/fips-operator-bundle-check-oci-ta/0.1/README.md @@ -0,0 +1,17 @@ +# fips-operator-bundle-check-oci-ta task + +Checks operator bundle image builds for FIPS compliance using the check-payload tool. + +## Parameters +|name|description|default value|required| +|---|---|---|---| +|SOURCE_ARTIFACT|The Trusted Artifact URI pointing to the artifact with the application source code.||true| +|image-digest|Image digest to scan.||true| +|image-url|Image URL.||true| + +## Results +|name|description| +|---|---| +|IMAGES_PROCESSED|Images processed in the task.| +|TEST_OUTPUT|Tekton task test output.| + diff --git a/task/fips-operator-bundle-check-oci-ta/0.1/fips-operator-bundle-check-oci-ta.yaml b/task/fips-operator-bundle-check-oci-ta/0.1/fips-operator-bundle-check-oci-ta.yaml new file mode 100644 index 0000000000..049c16b6b0 --- /dev/null +++ b/task/fips-operator-bundle-check-oci-ta/0.1/fips-operator-bundle-check-oci-ta.yaml @@ -0,0 +1,162 @@ +--- +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: fips-operator-bundle-check-oci-ta + annotations: + tekton.dev/pipelines.minVersion: 0.12.1 + tekton.dev/tags: konflux + labels: + app.kubernetes.io/version: "0.1" +spec: + description: Checks operator bundle image builds for FIPS compliance using + the check-payload tool. + params: + - name: SOURCE_ARTIFACT + description: The Trusted Artifact URI pointing to the artifact with + the application source code. + type: string + - name: image-digest + description: Image digest to scan. + - name: image-url + description: Image URL. + results: + - name: IMAGES_PROCESSED + description: Images processed in the task. + - name: TEST_OUTPUT + description: Tekton task test output. + value: $(steps.fips-operator-check-step-action.results.TEST_OUTPUT) + volumes: + - name: workdir + emptyDir: {} + stepTemplate: + volumeMounts: + - mountPath: /var/workdir + name: workdir + steps: + - name: use-trusted-artifact + image: quay.io/redhat-appstudio/build-trusted-artifacts:latest@sha256:b31dc501d5068e30621e51681a2921d4e43f5a030ab78c8991f83a5e774534a3 + args: + - use + - $(params.SOURCE_ARTIFACT)=/var/workdir/source + - name: get-unique-related-images + image: quay.io/yashn/konflux-test-yashn:latest-amd64-dec17 + env: + - name: IMAGE_URL + value: $(params.image-url) + - name: IMAGE_DIGEST + value: $(params.image-digest) + - name: SOURCE_CODE_DIR + value: /var/workdir + script: | + #!/usr/bin/env bash + set -euo pipefail + # shellcheck source=/dev/null + . /utils.sh + + image_without_tag=$(echo -n "${IMAGE_URL}" | sed "s/\(.*\):.*/\1/") + # strip new-line escape symbol from parameter and save it to variable + image_and_digest="${image_without_tag}@${IMAGE_DIGEST}" + + image_and_digest_labels=$(get_image_labels "${image_and_digest}") + if ! echo "${image_and_digest_labels}" | grep -q 'operators.operatorframework.io.bundle.mediatype.v1='; then + echo "The image $image_and_digest is not an operator bundle. Skipping FIPS static check..." + exit 0 + fi + + # Run the FIPS check only if the bundle is part of the Openshift Subscription or has the fips label set + image_and_digest_render_out=$(opm render "$image_and_digest") + subscription_label=$(echo "${image_and_digest_render_out}" | jq -r '.properties[] | select(.value.annotations["operators.openshift.io/valid-subscription"] != null) | (.value.annotations["operators.openshift.io/valid-subscription"] | fromjson)[]') + fips_label=$(echo "${image_and_digest_labels}" | grep 'features.operators.openshift.io/fips-compliant=' | cut -d= -f2 || true) + + if ! echo "${subscription_label}" | grep -e "OpenShift Kubernetes Engine" -e "OpenShift Container Platform" -e "OpenShift Platform Plus"; then + echo "OpenShift Kubernetes Engine, OpenShift Platform Plus or OpenShift Container Platform are not present in operators.openshift.io/valid-subscription." + echo "Subscription labels are : $subscription_label" + if [ -z "${fips_label}" ] || [ "${fips_label}" != "true" ]; then + echo "The label features.operators.openshift.io/fips-compliant is also not set to true. Skipping the FIPS static check..." + exit 0 + else + echo "The label features.operators.openshift.io/fips-compliant is set to true. Running the FIPS static check..." + fi + else + echo "OpenShift Kubernetes Engine, OpenShift Platform Plus or OpenShift Container Platform are present in operators.openshift.io/valid-subscription. Running the FIPS static check..." + fi + + mirror_set="${SOURCE_CODE_DIR}/source/.tekton/related-images-mirror-set.yaml" + if [[ -f "${mirror_set}" ]]; then + mirror_set_yaml=$(cat "${mirror_set}") + process_image_digest_mirror_set "${mirror_set_yaml}" >"/tekton/home/related-images-map.txt" + else + echo "Could not find Related Images mirror set at ${mirror_set}. Unreleased relatedImages will fail the scan." + fi + + unique_related_images=() + digests_processed=() + images_processed_template='{"image": {"pullspec": "'"$IMAGE_URL"'", "digests": [%s]}}' + + echo "Inspecting raw image manifest $image_and_digest." + # Get the arch and image manifests by inspecting the image. This is mainly for identifying image indexes + image_manifests=$(get_image_manifests -i "${image_and_digest}") + echo "Image manifests are $image_manifests" + + declare -A seen_related_images + # Extract relatedImages from the bundle image + while read -r _ arch_sha; do + digests_processed+=("\"$arch_sha\"") + manifest_related_images=$(extract_related_images_from_bundle "$image_without_tag@$arch_sha") + if [ -n "$manifest_related_images" ]; then + for img in $manifest_related_images; do + if [ -z "${seen_related_images["$img"]:-}" ]; then + unique_related_images+=("$img") + seen_related_images["$img"]=1 + fi + done + fi + done < <(echo "$image_manifests" | jq -r 'to_entries[] | "\(.key) \(.value)"') + + echo "Unique related images: ${unique_related_images[*]}" + echo "${unique_related_images[*]}" >/tekton/home/unique_related_images.txt + + # If the image is an Image Index, also add the Image Index digest to the list. + if [[ "${digests_processed[*]}" != *"$IMAGE_DIGEST"* ]]; then + digests_processed+=("\"$IMAGE_DIGEST\"") + fi + digests_processed_string=$( + IFS=, + echo "${digests_processed[*]}" + ) + + echo "${images_processed_template/\[%s]/[$digests_processed_string]}" >/tekton/home/images_processed.txt + computeResources: + limits: + cpu: 200m + memory: 512Mi + requests: + cpu: 100m + memory: 256Mi + securityContext: + capabilities: + add: + - SETFCAP + - name: fips-operator-check-step-action + computeResources: + limits: + cpu: 200m + memory: 512Mi + requests: + cpu: 100m + memory: 256Mi + ref: + name: fips-operator-check-step-action + - name: parse-images-processed-result + image: quay.io/redhat-appstudio/konflux-test:v1.4.9@sha256:eee855e60b437d9a55a30e63f2eb7f95d9fd6d3b111c32cac8730c9b7a071394 + script: | + #!/usr/bin/env bash + set -euo pipefail + + if [ -e "/tekton/home/images_processed.txt" ]; then + tee "$(results.IMAGES_PROCESSED.path)" - + Checks operator bundle image builds for FIPS compliance using the check-payload tool. + params: + - name: image-digest + description: Image digest to scan. + - name: image-url + description: Image URL. + results: + - name: TEST_OUTPUT + description: Tekton task test output. + value: $(steps.fips-operator-check-step-action.results.TEST_OUTPUT) + - name: IMAGES_PROCESSED + description: Images processed in the task. + steps: + - name: get-unique-related-images + image: quay.io/yashn/konflux-test-yashn:latest-amd64-dec17 + computeResources: + limits: + memory: 512Mi + cpu: 200m + requests: + memory: 256Mi + cpu: 100m + env: + - name: IMAGE_URL + value: $(params.image-url) + - name: IMAGE_DIGEST + value: $(params.image-digest) + - name: SOURCE_CODE_DIR + value: $(workspaces.workspace.path) + securityContext: + capabilities: + add: + - SETFCAP + script: | + #!/usr/bin/env bash + set -euo pipefail + # shellcheck source=/dev/null + . /utils.sh + + image_without_tag=$(echo -n "${IMAGE_URL}" | sed "s/\(.*\):.*/\1/") + # strip new-line escape symbol from parameter and save it to variable + image_and_digest="${image_without_tag}@${IMAGE_DIGEST}" + + image_and_digest_labels=$(get_image_labels "${image_and_digest}") + if ! echo "${image_and_digest_labels}" | grep -q 'operators.operatorframework.io.bundle.mediatype.v1='; then + echo "The image $image_and_digest is not an operator bundle. Skipping FIPS static check..." + exit 0 + fi + + # Run the FIPS check only if the bundle is part of the Openshift Subscription or has the fips label set + image_and_digest_render_out=$(opm render "$image_and_digest") + subscription_label=$(echo "${image_and_digest_render_out}" | jq -r '.properties[] | select(.value.annotations["operators.openshift.io/valid-subscription"] != null) | (.value.annotations["operators.openshift.io/valid-subscription"] | fromjson)[]') + fips_label=$(echo "${image_and_digest_labels}" | grep 'features.operators.openshift.io/fips-compliant=' | cut -d= -f2 || true) + + if ! echo "${subscription_label}" | grep -e "OpenShift Kubernetes Engine" -e "OpenShift Container Platform" -e "OpenShift Platform Plus"; then + echo "OpenShift Kubernetes Engine, OpenShift Platform Plus or OpenShift Container Platform are not present in operators.openshift.io/valid-subscription." + echo "Subscription labels are : $subscription_label" + if [ -z "${fips_label}" ] || [ "${fips_label}" != "true" ]; then + echo "The label features.operators.openshift.io/fips-compliant is also not set to true. Skipping the FIPS static check..." + exit 0 + else + echo "The label features.operators.openshift.io/fips-compliant is set to true. Running the FIPS static check..." + fi + else + echo "OpenShift Kubernetes Engine, OpenShift Platform Plus or OpenShift Container Platform are present in operators.openshift.io/valid-subscription. Running the FIPS static check..." + fi + + mirror_set="${SOURCE_CODE_DIR}/source/.tekton/related-images-mirror-set.yaml" + if [[ -f "${mirror_set}" ]]; then + mirror_set_yaml=$(cat "${mirror_set}") + process_image_digest_mirror_set "${mirror_set_yaml}" > "/tekton/home/related-images-map.txt" + else + echo "Could not find Related Images mirror set at ${mirror_set}. Unreleased relatedImages will fail the scan." + fi + + unique_related_images=() + digests_processed=() + images_processed_template='{"image": {"pullspec": "'"$IMAGE_URL"'", "digests": [%s]}}' + + echo "Inspecting raw image manifest $image_and_digest." + # Get the arch and image manifests by inspecting the image. This is mainly for identifying image indexes + image_manifests=$(get_image_manifests -i "${image_and_digest}") + echo "Image manifests are $image_manifests" + + declare -A seen_related_images + # Extract relatedImages from the bundle image + while read -r _ arch_sha; do + digests_processed+=("\"$arch_sha\"") + manifest_related_images=$(extract_related_images_from_bundle "$image_without_tag@$arch_sha") + if [ -n "$manifest_related_images" ]; then + for img in $manifest_related_images; do + if [ -z "${seen_related_images["$img"]:-}" ]; then + unique_related_images+=("$img") + seen_related_images["$img"]=1 + fi + done + fi + done < <(echo "$image_manifests" | jq -r 'to_entries[] | "\(.key) \(.value)"') + + echo "Unique related images: ${unique_related_images[*]}" + echo "${unique_related_images[*]}" > /tekton/home/unique_related_images.txt + + # If the image is an Image Index, also add the Image Index digest to the list. + if [[ "${digests_processed[*]}" != *"$IMAGE_DIGEST"* ]]; then + digests_processed+=("\"$IMAGE_DIGEST\"") + fi + digests_processed_string=$(IFS=,; echo "${digests_processed[*]}") + + echo "${images_processed_template/\[%s]/[$digests_processed_string]}" > /tekton/home/images_processed.txt + + - name: fips-operator-check-step-action + computeResources: + limits: + memory: 512Mi + cpu: 200m + requests: + memory: 256Mi + cpu: 100m + ref: + name: fips-operator-check-step-action + + - name: parse-images-processed-result + image: quay.io/redhat-appstudio/konflux-test:v1.4.9@sha256:eee855e60b437d9a55a30e63f2eb7f95d9fd6d3b111c32cac8730c9b7a071394 + script: | + #!/usr/bin/env bash + set -euo pipefail + + if [ -e "/tekton/home/images_processed.txt" ]; then + tee "$(results.IMAGES_PROCESSED.path)" < /tekton/home/images_processed.txt + else + echo "Task was skipped. Exiting" + exit 0 + fi + workspaces: + - name: workspace