From 68d4ab0f21f7a0f3a5661826f489ebec5d714233 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 --- task/fips-operator-check/0.1/README.md | 25 ++++ .../0.1/fips-operator-check-step-action.yaml | 137 ++++++++++++++++++ .../0.1/fips-operator-check.yaml | 132 +++++++++++++++++ task/fips-operator-check/OWNERS | 6 + 4 files changed, 300 insertions(+) create mode 100644 task/fips-operator-check/0.1/README.md create mode 100644 task/fips-operator-check/0.1/fips-operator-check-step-action.yaml create mode 100644 task/fips-operator-check/0.1/fips-operator-check.yaml create mode 100644 task/fips-operator-check/OWNERS diff --git a/task/fips-operator-check/0.1/README.md b/task/fips-operator-check/0.1/README.md new file mode 100644 index 0000000000..ba9cab082b --- /dev/null +++ b/task/fips-operator-check/0.1/README.md @@ -0,0 +1,25 @@ +# fips-operator-bundle-check task + +## Description: +The fips-operator-bundle-check task uses the check-payload tool to verify if an operator bundle image is FIPS compliant. +It only scans operator bundle images which either claim to be FIPS compliant by setting the `features.operators.openshift.io/fips-compliant` +label to `"true"` on the bundle image or require one of `OpenShift Kubernetes Engine, OpenShift Platform Plus or OpenShift Container Platform` +subscriptions to run the operator on an Openshift cluster. + +## Params: + +| name | description | default | +|--------------------------|------------------------------------------------------------------------|---------------| +| image-digest | Image digest to scan. | None | +| image-url | Image URL. | None | + +## Results: + +| name | description | +|--------------------|------------------------------| +| TEST_OUTPUT | Tekton task test output. | +| IMAGES_PROCESSED | Images processed in the task.| + + +## Additional links: +https://github.com/openshift/check-payload \ No newline at end of file diff --git a/task/fips-operator-check/0.1/fips-operator-check-step-action.yaml b/task/fips-operator-check/0.1/fips-operator-check-step-action.yaml new file mode 100644 index 0000000000..236860e573 --- /dev/null +++ b/task/fips-operator-check/0.1/fips-operator-check-step-action.yaml @@ -0,0 +1,137 @@ +--- +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: >- + Scans operator bundle image builds for FIPS compliance using the check-payload tool. + params: + - name: unique_related_images + description: Images to scan using check-payload. + - name: images_processed + description: Images that were processed as a part of the scan. + results: + - name: TEST_OUTPUT + description: Tekton task test output. + - name: IMAGES_PROCESSED + description: Images processed in the task. + image: quay.io/yashn/konflux-test-yashn:latest-amd64 + env: + - name: unique_related_images + value: $(params.unique_related_images) + - name: images_processed + value: $(params.images_processed) + 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 + + unique_related_images_string="${unique_related_images}" + read -r -a related_images <<< "${unique_related_images}" + + if [[ -z "${related_images[*]}" ]]; then + echo "No relatedImages to process" + else + for related_image in "${related_images[@]}"; do + echo "Processing related image : ${related_image}" + + image_labels=$(skopeo inspect docker://"${related_image}" --config | jq -r '.config.Labels // {} | to_entries[] | "\(.key)=\(.value)"') + 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 + + echo "Now RUNNING SCAN ON THE IMAGE ${related_image}" + + # 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}" \ + --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 + + echo "Success counter is : ${success_counter}" + echo "Warnings counter is : ${warnings_counter}" + echo "Error counter is: ${error_counter}" + echo "Failure counter is ${failure_counter}" + + done + fi + + 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" + elif [[ "${success_counter}" -eq 0 ]]; then + # when all counters are 0, there are no relatedImages to run the FIPS check on + note="Task $(context.task.name) success: No relatedImages found to run the FIPS check." + RES="SUCCESS" + 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)" + + images_processed_result="${images_processed}" + echo "${images_processed_result}" | tee "$(step.results.IMAGES_PROCESSED.path)" diff --git a/task/fips-operator-check/0.1/fips-operator-check.yaml b/task/fips-operator-check/0.1/fips-operator-check.yaml new file mode 100644 index 0000000000..abc4fbfef7 --- /dev/null +++ b/task/fips-operator-check/0.1/fips-operator-check.yaml @@ -0,0 +1,132 @@ +--- +apiVersion: tekton.dev/v1 +kind: Task +metadata: + labels: + app.kubernetes.io/version: "0.1" + annotations: + tekton.dev/pipelines.minVersion: "0.12.1" + tekton.dev/tags: "konflux" + name: fips-operator-check +spec: + description: >- + 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. + value: $(steps.fips-operator-check-step-action.results.IMAGES_PROCESSED) + steps: + - name: get-unique-related-images + image: quay.io/yashn/konflux-test-yashn:latest-amd64 + 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) + results: + - name: unique_related_images + - name: images_processed + securityContext: + capabilities: + add: + - SETFCAP + script: | + #!/usr/bin/env bash + set -euo pipefail + # shellcheck source=/dev/null + . /utils.sh + + imagewithouttag=$(echo -n "${IMAGE_URL}" | sed "s/\(.*\):.*/\1/") + # strip new-line escape symbol from parameter and save it to variable + imageanddigest="${imagewithouttag}@${IMAGE_DIGEST}" + + imageanddigest_labels=$(skopeo inspect docker://"${imageanddigest}" --config | jq -r '.config.Labels // {} | to_entries[] | "\(.key)=\(.value)"') + if ! echo "${imageanddigest_labels}" | grep -q 'operators.operatorframework.io.bundle.manifests.v1='; then + echo "The image $imageanddigest 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 + imageanddigest_render_out=$(opm render "$imageanddigest") + subscription_label=$(echo "${imageanddigest_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 "${imageanddigest_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 + + unique_related_images=() + digests_processed=() + images_processed_template='{"image": {"pullspec": "'"$IMAGE_URL"'", "digests": [%s]}}' + + echo "Inspecting raw image manifest $imageanddigest." + # Get the arch and image manifests by inspecting the image. This is mainly for identifying image indexes + image_manifests=$(get_image_manifests -i "${imageanddigest}") + 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\"") + bundle_render_out=$(opm render "$imagewithouttag@$arch_sha") + manifest_related_images=$(echo "${bundle_render_out}" | jq -r '.relatedImages[]?.image') + 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[*]}" | tee "$(step.results.unique_related_images.path)" + + # 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]}" | tee "$(step.results.images_processed.path)" + + - name: fips-operator-check-step-action + computeResources: + limits: + memory: 512Mi + cpu: 200m + requests: + memory: 256Mi + cpu: 100m + ref: + name: fips-operator-check-step-action + params: + - name: unique_related_images + value: $(steps.get-unique-related-images.results.unique_related_images) + - name: images_processed + value: $(steps.get-unique-related-images.results.images_processed) diff --git a/task/fips-operator-check/OWNERS b/task/fips-operator-check/OWNERS new file mode 100644 index 0000000000..0beda903ed --- /dev/null +++ b/task/fips-operator-check/OWNERS @@ -0,0 +1,6 @@ +approvers: + - integration-team + - yashvardhannanavati +reviewers: + - integration-team + - yashvardhannanavati \ No newline at end of file