From 8049a413ae0dc71f5639eec89a23f2d852e08986 Mon Sep 17 00:00:00 2001 From: Andrew Storms Date: Fri, 28 Mar 2025 13:28:40 -0700 Subject: [PATCH 1/8] rename add validations of inputs handle case if sarif cannot be created from grype, then we create one anyway this way the reporting systems will always have a recent sarif to use --- .github/workflows/scan-container-image.yml | 87 ---------- .github/workflows/scan-image-grype.yml | 183 +++++++++++++++++++++ 2 files changed, 183 insertions(+), 87 deletions(-) delete mode 100644 .github/workflows/scan-container-image.yml create mode 100644 .github/workflows/scan-image-grype.yml diff --git a/.github/workflows/scan-container-image.yml b/.github/workflows/scan-container-image.yml deleted file mode 100644 index a742ad95e6..0000000000 --- a/.github/workflows/scan-container-image.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: Scan Container Image - -on: - workflow_call: - inputs: - image: - required: true - type: string - description: 'Container image to scan (format: image:tag)' - severity-cutoff: - required: false - type: string - default: 'low' - description: 'Minimum severity to report (critical, high, medium, low, negligible)' - fail-build: - required: false - type: boolean - default: false - description: 'Fail the workflow if vulnerabilities are found' - -permissions: {} # Remove all permissions by default - -jobs: - scan: - name: Scan Image - runs-on: ubuntu-latest - timeout-minutes: 30 # Default timeout for the job - permissions: - security-events: write # Needed to upload SARIF results - contents: read # Needed to read workflow files - - steps: - - name: Extract image details - id: image_details - run: | - IMAGE_NAME=$(echo "${{ inputs.image }}" | cut -d':' -f1) - IMAGE_TAG=$(echo "${{ inputs.image }}" | cut -d':' -f2) - [[ "$IMAGE_TAG" == "$IMAGE_NAME" ]] && IMAGE_TAG="latest" - SAFE_NAME=$(echo "${IMAGE_NAME}-${IMAGE_TAG}" | sed 's/[\/:]/-/g') - { - echo "image_name=${IMAGE_NAME}" - echo "image_tag=${IMAGE_TAG}" - echo "safe_name=${SAFE_NAME}" - } >> "$GITHUB_OUTPUT" - - - name: Scan image with Anchore - uses: anchore/scan-action@v6 - id: scan - with: - image: "${{ inputs.image }}" - fail-build: "${{ inputs.fail-build }}" - severity-cutoff: "${{ inputs.severity-cutoff }}" - output-format: sarif - - - name: Enrich SARIF with image metadata - run: | - sudo apt-get update && sudo apt-get install -y jq - - jq --arg imageRef "${{ inputs.image }}" \ - --arg repo "${{ steps.image_details.outputs.image_name }}" \ - --arg name "${{ steps.image_details.outputs.image_name }}" \ - --arg tag "${{ steps.image_details.outputs.image_tag }}" \ - --arg scanTime "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \ - '.runs[0].properties = { - "imageRef": $imageRef, - "repository": $repo, - "scanTime": $scanTime, - "imageMetadata": { - "name": $name, - "tag": $tag - } - }' results.sarif > enriched-results.sarif - - mv enriched-results.sarif results.sarif - - - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: results.sarif - category: "container-scan-${{ steps.image_details.outputs.safe_name }}" - - - name: Archive scan results - uses: actions/upload-artifact@v4 - with: - name: "sarif-${{ steps.image_details.outputs.safe_name }}" - path: results.sarif - retention-days: 90 \ No newline at end of file diff --git a/.github/workflows/scan-image-grype.yml b/.github/workflows/scan-image-grype.yml new file mode 100644 index 0000000000..364953a0b1 --- /dev/null +++ b/.github/workflows/scan-image-grype.yml @@ -0,0 +1,183 @@ +name: Scan Container Image Grype SARIF + +on: + workflow_call: + inputs: + image: + required: true + type: string + description: 'Container image to scan (format: image:tag)' + severity-cutoff: + required: false + type: string + default: 'negligible' + description: 'Minimum severity to report (critical, high, medium, low, negligible)' + fail-build: + required: false + type: boolean + default: false + description: 'Fail the workflow if vulnerabilities are found' + output-file: + required: false + type: string + default: 'results.sarif' + description: 'Output file name for SARIF results' + timeout-minutes: + required: false + type: number + default: 30 + description: 'Maximum time in minutes to wait for the scan to complete' + retention-days: + required: false + type: number + default: 90 + description: 'Number of days to retain the scan results artifact' + category-prefix: + required: false + type: string + default: 'container-scan-' + description: 'Prefix to use for the SARIF category name' + +permissions: {} # Remove all permissions by default + +jobs: + validate: + name: Validate Inputs + runs-on: ubuntu-latest + steps: + - name: Validate repository access + run: | + # Extract organization from the repository name + ORG_NAME=$(echo "${{ github.repository }}" | cut -d'/' -f1) + EXPECTED_ORG="replicatedhq" + + if [ "$ORG_NAME" != "$EXPECTED_ORG" ]; then + echo "Error: This workflow can only be called from repositories within the $EXPECTED_ORG organization" + echo "Current repository: ${{ github.repository }}" + exit 1 + fi + + - name: Validate severity-cutoff + run: | + valid_severities=("critical" "high" "medium" "low" "negligible") + if [[ ! " ${valid_severities[@]} " =~ " ${{ inputs.severity-cutoff }} " ]]; then + echo "Error: Invalid severity-cutoff value '${{ inputs.severity-cutoff }}'" + echo "Valid values are: ${valid_severities[*]}" + exit 1 + fi + + - name: Validate timeout-minutes + run: | + if [[ ! "${{ inputs.timeout-minutes }}" =~ ^[0-9]+$ ]] || [ "${{ inputs.timeout-minutes }}" -lt 1 ] || [ "${{ inputs.timeout-minutes }}" -gt 360 ]; then + echo "Error: Invalid timeout-minutes value '${{ inputs.timeout-minutes }}'" + echo "Value must be a number between 1 and 360 minutes" + exit 1 + fi + + - name: Validate retention-days + run: | + if [[ ! "${{ inputs.retention-days }}" =~ ^[0-9]+$ ]] || [ "${{ inputs.retention-days }}" -lt 1 ] || [ "${{ inputs.retention-days }}" -gt 90 ]; then + echo "Error: Invalid retention-days value '${{ inputs.retention-days }}'" + echo "Value must be a number between 1 and 90 days" + exit 1 + fi + + - name: Validate category-prefix + run: | + if [[ -z "${{ inputs.category-prefix }}" ]]; then + echo "Error: category-prefix cannot be empty" + exit 1 + fi + if [[ "${{ inputs.category-prefix }}" =~ [^a-zA-Z0-9\-_] ]]; then + echo "Error: category-prefix can only contain alphanumeric characters, hyphens, and underscores" + exit 1 + fi + + scan: + name: Scan Image Grype SARIF + needs: validate + runs-on: ubuntu-latest + timeout-minutes: ${{ inputs.timeout-minutes }} + concurrency: + group: ${{ inputs.image }} + cancel-in-progress: false + permissions: + security-events: write # Needed to upload SARIF results + contents: read # Needed to read workflow files + + steps: + - name: Extract image details + id: image_details + run: | + IMAGE_NAME=$(echo "${{ inputs.image }}" | cut -d':' -f1) + IMAGE_TAG=$(echo "${{ inputs.image }}" | cut -d':' -f2) + [[ "$IMAGE_TAG" == "$IMAGE_NAME" ]] && IMAGE_TAG="latest" + SAFE_NAME=$(echo "${IMAGE_NAME}-${IMAGE_TAG}" | sed 's/[\/:]/-/g') + { + echo "image_name=${IMAGE_NAME}" + echo "image_tag=${IMAGE_TAG}" + echo "safe_name=${SAFE_NAME}" + } >> "$GITHUB_OUTPUT" + + - name: Scan image with Grype + uses: anchore/scan-action@7c05671ae9be166aeb155bad2d7df9121823df32 + id: scan + continue-on-error: true # Allow workflow to continue even if scan fails + with: + image: "${{ inputs.image }}" + fail-build: "${{ inputs.fail-build }}" + severity-cutoff: "${{ inputs.severity-cutoff }}" + output-format: sarif + output-file: "${{ inputs.output-file }}" + by-cve: true + + - name: Check scan status + if: steps.scan.outcome == 'failure' + run: | + echo "::warning::Scan failed for image ${{ inputs.image }}" + echo "Please check the scan logs above for details" + if [ "${{ inputs.fail-build }}" = "true" ]; then + echo "::error::Build will fail due to scan failure and fail-build=true" + exit 1 + fi + + - name: Enrich or generate SARIF + if: always() + run: | + sudo apt-get update && sudo apt-get install -y jq + + if [ ! -f results.sarif ]; then + echo "No SARIF file found — creating minimal empty SARIF" + + echo '{"version":"2.1.0","runs":[{"tool":{"driver":{"name":"Anchore Grype","informationUri":"https://github.com/anchore/grype","rules":[]}},"results":[],"properties":{"isFallbackSarif":true}}]}' > results.sarif + fi + + jq --arg imageRef "${{ inputs.image }}" \ + --arg repo "${{ steps.image_details.outputs.image_name }}" \ + --arg name "${{ steps.image_details.outputs.image_name }}" \ + --arg tag "${{ steps.image_details.outputs.image_tag }}" \ + --arg scanTime "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \ + '.runs[0].properties = { + "imageRef": $imageRef, + "repository": $repo, + "scanTime": $scanTime, + "imageMetadata": { + "name": $name, + "tag": $tag + } + }' results.sarif > enriched-results.sarif + + mv enriched-results.sarif results.sarif + + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif + category: "${{ inputs.category-prefix }}${{ steps.image_details.outputs.safe_name }}" + + - name: Archive scan results + uses: actions/upload-artifact@v4 + with: + name: "sarif-${{ steps.image_details.outputs.safe_name }}" + path: results.sarif + retention-days: ${{ inputs.retention-days }} \ No newline at end of file From 09a59585890b33ac69ce9310e7d3f6e691feffa4 Mon Sep 17 00:00:00 2001 From: Andrew Storms Date: Fri, 28 Mar 2025 13:30:39 -0700 Subject: [PATCH 2/8] add inputs required by the reusable workflow --- ...tainer-scans.yml => daily-image-scans.yml} | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) rename .github/workflows/{daily-container-scans.yml => daily-image-scans.yml} (70%) diff --git a/.github/workflows/daily-container-scans.yml b/.github/workflows/daily-image-scans.yml similarity index 70% rename from .github/workflows/daily-container-scans.yml rename to .github/workflows/daily-image-scans.yml index 9f0a88355d..ce12f96574 100644 --- a/.github/workflows/daily-container-scans.yml +++ b/.github/workflows/daily-image-scans.yml @@ -34,35 +34,51 @@ jobs: scan-kotsadm: name: Scan Kotsadm needs: get-latest-tag - uses: ./.github/workflows/scan-container-image.yml + uses: ./.github/workflows/scan-image-grype.yml permissions: contents: read security-events: write + actions: read with: image: kotsadm/kotsadm:${{ needs.get-latest-tag.outputs.tag_name }} - severity-cutoff: low + severity-cutoff: negligible fail-build: false + output-file: results.sarif + timeout-minutes: 30 + retention-days: 90 + category-prefix: container-scan- + scan-kotsadm-migrations: name: Scan Kotsadm Migrations needs: get-latest-tag - uses: ./.github/workflows/scan-container-image.yml + uses: ./.github/workflows/scan-image-grype.yml permissions: contents: read security-events: write + actions: read with: image: kotsadm/kotsadm-migrations:${{ needs.get-latest-tag.outputs.tag_name }} - severity-cutoff: low + severity-cutoff: negligible fail-build: false + output-file: results.sarif + timeout-minutes: 30 + retention-days: 90 + category-prefix: container-scan- scan-kurl-proxy: name: Scan Kurl Proxy needs: get-latest-tag - uses: ./.github/workflows/scan-container-image.yml + uses: ./.github/workflows/scan-image-grype.yml permissions: contents: read security-events: write + actions: read with: image: kotsadm/kurl-proxy:${{ needs.get-latest-tag.outputs.tag_name }} - severity-cutoff: low - fail-build: false \ No newline at end of file + severity-cutoff: negligible + fail-build: false + output-file: results.sarif + timeout-minutes: 30 + retention-days: 90 + category-prefix: container-scan- \ No newline at end of file From 8fa14229b952c803bbc580c5bd68fde299a6dde4 Mon Sep 17 00:00:00 2001 From: Andrew Storms Date: Fri, 28 Mar 2025 13:46:57 -0700 Subject: [PATCH 3/8] change validation syntax to make the linter happy --- .github/workflows/scan-image-grype.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/scan-image-grype.yml b/.github/workflows/scan-image-grype.yml index 364953a0b1..bba11aaa7f 100644 --- a/.github/workflows/scan-image-grype.yml +++ b/.github/workflows/scan-image-grype.yml @@ -60,7 +60,14 @@ jobs: - name: Validate severity-cutoff run: | valid_severities=("critical" "high" "medium" "low" "negligible") - if [[ ! " ${valid_severities[@]} " =~ " ${{ inputs.severity-cutoff }} " ]]; then + found=false + for severity in "${valid_severities[@]}"; do + if [[ "$severity" == "${{ inputs.severity-cutoff }}" ]]; then + found=true + break + fi + done + if [[ "$found" == "false" ]]; then echo "Error: Invalid severity-cutoff value '${{ inputs.severity-cutoff }}'" echo "Valid values are: ${valid_severities[*]}" exit 1 @@ -68,7 +75,7 @@ jobs: - name: Validate timeout-minutes run: | - if [[ ! "${{ inputs.timeout-minutes }}" =~ ^[0-9]+$ ]] || [ "${{ inputs.timeout-minutes }}" -lt 1 ] || [ "${{ inputs.timeout-minutes }}" -gt 360 ]; then + if [[ ! "${{ inputs.timeout-minutes }}" =~ ^[0-9]+$ ]] || [[ "${{ inputs.timeout-minutes }}" \< 1 ]] || [[ "${{ inputs.timeout-minutes }}" \> 360 ]]; then echo "Error: Invalid timeout-minutes value '${{ inputs.timeout-minutes }}'" echo "Value must be a number between 1 and 360 minutes" exit 1 @@ -76,7 +83,7 @@ jobs: - name: Validate retention-days run: | - if [[ ! "${{ inputs.retention-days }}" =~ ^[0-9]+$ ]] || [ "${{ inputs.retention-days }}" -lt 1 ] || [ "${{ inputs.retention-days }}" -gt 90 ]; then + if [[ ! "${{ inputs.retention-days }}" =~ ^[0-9]+$ ]] || [[ "${{ inputs.retention-days }}" \< 1 ]] || [[ "${{ inputs.retention-days }}" \> 90 ]]; then echo "Error: Invalid retention-days value '${{ inputs.retention-days }}'" echo "Value must be a number between 1 and 90 days" exit 1 From 56bda266678ef65a5aabbf3d959322bd8ffe773c Mon Sep 17 00:00:00 2001 From: Andrew Storms Date: Fri, 28 Mar 2025 13:49:58 -0700 Subject: [PATCH 4/8] making silly linters happy --- .github/workflows/scan-image-grype.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scan-image-grype.yml b/.github/workflows/scan-image-grype.yml index bba11aaa7f..2f7da5ccbf 100644 --- a/.github/workflows/scan-image-grype.yml +++ b/.github/workflows/scan-image-grype.yml @@ -75,7 +75,7 @@ jobs: - name: Validate timeout-minutes run: | - if [[ ! "${{ inputs.timeout-minutes }}" =~ ^[0-9]+$ ]] || [[ "${{ inputs.timeout-minutes }}" \< 1 ]] || [[ "${{ inputs.timeout-minutes }}" \> 360 ]]; then + if [[ ! "${{ inputs.timeout-minutes }}" =~ ^[0-9]+$ ]] || (( "${{ inputs.timeout-minutes }}" < 1 )) || (( "${{ inputs.timeout-minutes }}" > 360 )); then echo "Error: Invalid timeout-minutes value '${{ inputs.timeout-minutes }}'" echo "Value must be a number between 1 and 360 minutes" exit 1 @@ -83,7 +83,7 @@ jobs: - name: Validate retention-days run: | - if [[ ! "${{ inputs.retention-days }}" =~ ^[0-9]+$ ]] || [[ "${{ inputs.retention-days }}" \< 1 ]] || [[ "${{ inputs.retention-days }}" \> 90 ]]; then + if [[ ! "${{ inputs.retention-days }}" =~ ^[0-9]+$ ]] || (( "${{ inputs.retention-days }}" < 1 )) || (( "${{ inputs.retention-days }}" > 90 )); then echo "Error: Invalid retention-days value '${{ inputs.retention-days }}'" echo "Value must be a number between 1 and 90 days" exit 1 From 4370dbd774686167626d98f23fd69b9396ecf2e1 Mon Sep 17 00:00:00 2001 From: Andrew Storms Date: Mon, 31 Mar 2025 07:24:56 -0700 Subject: [PATCH 5/8] Update .github/workflows/scan-image-grype.yml good catch Co-authored-by: Ethan Mosbaugh --- .github/workflows/scan-image-grype.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scan-image-grype.yml b/.github/workflows/scan-image-grype.yml index 2f7da5ccbf..fedfdfe4d3 100644 --- a/.github/workflows/scan-image-grype.yml +++ b/.github/workflows/scan-image-grype.yml @@ -149,7 +149,7 @@ jobs: fi - name: Enrich or generate SARIF - if: always() + if: !cancelled() run: | sudo apt-get update && sudo apt-get install -y jq From 26b27958a7d9a9f98ebe73b8a1a4790926bc101a Mon Sep 17 00:00:00 2001 From: Andrew Storms Date: Mon, 31 Mar 2025 09:53:12 -0700 Subject: [PATCH 6/8] Note `if: !cancelled()` is a valid per docs, but I think its a false positive with the linter https://docs.github.com/en/actions/learn-github-actions/expressions#status-check-functions --- .github/workflows/scan-image-grype.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scan-image-grype.yml b/.github/workflows/scan-image-grype.yml index fedfdfe4d3..e4d5714758 100644 --- a/.github/workflows/scan-image-grype.yml +++ b/.github/workflows/scan-image-grype.yml @@ -127,7 +127,7 @@ jobs: } >> "$GITHUB_OUTPUT" - name: Scan image with Grype - uses: anchore/scan-action@7c05671ae9be166aeb155bad2d7df9121823df32 + uses: anchore/scan-action@v6 id: scan continue-on-error: true # Allow workflow to continue even if scan fails with: From 9858104c6e731a4725adf649a1094d4e2107bed0 Mon Sep 17 00:00:00 2001 From: Andrew Storms Date: Mon, 31 Mar 2025 09:59:35 -0700 Subject: [PATCH 7/8] removed jq. Confirmed is on the runner by default --- .github/workflows/scan-image-grype.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/scan-image-grype.yml b/.github/workflows/scan-image-grype.yml index e4d5714758..cb013a78bf 100644 --- a/.github/workflows/scan-image-grype.yml +++ b/.github/workflows/scan-image-grype.yml @@ -151,8 +151,6 @@ jobs: - name: Enrich or generate SARIF if: !cancelled() run: | - sudo apt-get update && sudo apt-get install -y jq - if [ ! -f results.sarif ]; then echo "No SARIF file found — creating minimal empty SARIF" From 1d54b7c680d4d7782439fd856c4d3cf9bfa3e7bd Mon Sep 17 00:00:00 2001 From: Andrew Storms Date: Mon, 31 Mar 2025 10:37:05 -0700 Subject: [PATCH 8/8] fix syntax and remove install of jq to the runner --- .github/workflows/scan-image-grype.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scan-image-grype.yml b/.github/workflows/scan-image-grype.yml index cb013a78bf..8e70e81933 100644 --- a/.github/workflows/scan-image-grype.yml +++ b/.github/workflows/scan-image-grype.yml @@ -149,7 +149,7 @@ jobs: fi - name: Enrich or generate SARIF - if: !cancelled() + if: "!cancelled()" run: | if [ ! -f results.sarif ]; then echo "No SARIF file found — creating minimal empty SARIF"