diff --git a/security-actions/sca/action.yml b/security-actions/sca/action.yml index 4ceca51e..b5e9caf5 100644 --- a/security-actions/sca/action.yml +++ b/security-actions/sca/action.yml @@ -14,20 +14,6 @@ inputs: description: 'Specify a file to be scanned. This is mutually exclusive to dir and image' required: false default: '' - image: - description: 'specify an image to be scanned. Specify registry credentials if the image is remote. Takes priority over dir and file' - required: false - default: '' - tag: - description: 'specify a docker image tag / release tag / ref to be scanned' - required: false - default: '' - registry_username: - description: 'docker username to login against private docker registry' - required: false - registry_password: - description: 'docker password to login against private docker registry' - required: false config: description: 'file path to syft custom configuration' required: false @@ -45,9 +31,6 @@ outputs: global-enforce-build-failure: description: 'Globally fail the build on failure. Overrides fail_build when set' value: ${{ steps.meta.outputs.global_enforce_build_failure }} - analyzed-image: - description: 'sanitized docker image / tar only when image input is specified' - value: ${{ steps.meta.outputs.scan_image }} cis-json-report: description: 'docker-cis json report' value: ${{ steps.meta.outputs.cis_json_file }} @@ -72,8 +55,6 @@ runs: shell: bash id: meta env: - IMAGE: ${{ inputs.image }} - TAG: ${{ inputs.tag }} DIR: ${{ inputs.dir }} FILE: ${{ inputs.file }} ASSET_PREFIX: ${{ inputs.asset_prefix }} @@ -85,9 +66,6 @@ runs: id: sbom_spdx with: config: ${{ inputs.config }} - image: ${{ steps.meta.outputs.scan_image }} - registry-username: ${{ inputs.registry_username }} - registry-password: ${{ inputs.registry_password }} path: ${{ steps.meta.outputs.scan_dir }} file: ${{ steps.meta.outputs.scan_file }} format: spdx-json @@ -102,9 +80,6 @@ runs: id: sbom_cyclonedx with: config: ${{ inputs.config }} - image: ${{ steps.meta.outputs.scan_image }} - registry-username: ${{ inputs.registry_username }} - registry-password: ${{ inputs.registry_password }} path: ${{ steps.meta.outputs.scan_dir }} file: ${{ steps.meta.outputs.scan_file }} format: cyclonedx-json diff --git a/security-actions/sca/scripts/scan-metadata.sh b/security-actions/sca/scripts/scan-metadata.sh index 7de1b891..7c43e607 100755 --- a/security-actions/sca/scripts/scan-metadata.sh +++ b/security-actions/sca/scripts/scan-metadata.sh @@ -11,25 +11,16 @@ readonly cis_json_ext="cis-report.json" global_severity_cutoff='critical' global_enforce_build_failure='false' -if [[ -n ${IMAGE} && -n ${DIR} ]] || [[ -n ${IMAGE} && -n ${FILE} ]] || [[ -n ${DIR} && -n ${FILE} ]]; then - echo '::error ::Input fields "image", "dir" and "file" are mutually exlcusive' +if [[ -n ${DIR} && -n ${FILE} ]]; then + echo '::error ::Input fields "dir" and "file" are mutually exlcusive' exit 1 fi -if [[ -z ${IMAGE} && -z ${DIR} && -z ${FILE} ]]; then - echo '::error ::Specify one of "image", "dir" and "file" inputs fields' +if [[ -z ${DIR} && -z ${FILE} ]]; then + echo '::error ::Specify one of "dir" and "file" inputs fields' exit 1 fi -# OCI archive should be passed as image instead of file -if [[ -n ${IMAGE} ]]; then - if [[ -n ${TAG} ]]; then - echo "scan_image=${IMAGE}:${TAG}" >> $GITHUB_OUTPUT - else - echo "scan_image=${IMAGE}" >> $GITHUB_OUTPUT - fi -fi - if [[ -n ${DIR} ]]; then echo "scan_dir=${DIR}" >> $GITHUB_OUTPUT fi diff --git a/security-actions/scan-docker-image/action.yml b/security-actions/scan-docker-image/action.yml index 37d4f720..3474b582 100644 --- a/security-actions/scan-docker-image/action.yml +++ b/security-actions/scan-docker-image/action.yml @@ -20,6 +20,9 @@ inputs: registry_password: description: 'docker password to login against private docker registry' required: false + config: + description: 'file path to syft custom configuration' + required: false fail_build: description: 'fail the build if the vulnerability is above the severity cutoff' required: false @@ -32,84 +35,179 @@ inputs: outputs: cis-json-report: description: 'docker-cis json report' - value: ${{ steps.sca.outputs.cis-json-report }} + value: ${{ steps.meta.outputs.cis_json_file }} grype-json-report: description: 'vulnerability json report' - value: ${{ steps.sca.outputs.grype_json_report }} + value: ${{ steps.meta.outputs.grype_json_report }} grype-sarif-report: description: 'vulnerability sarif report' - value: ${{ steps.sca.outputs.grype_sarif_report }} + value: ${{ steps.meta.outputs.grype_sarif_report }} sbom-spdx-report: description: 'SBOM spdx report' - value: ${{ steps.sca.outputs.sbom_spdx_file }} + value: ${{ steps.meta.outputs.sbom_spdx_file }} sbom-cyclonedx-report: description: 'SBOM cyclonedx report' - value: ${{ steps.sca.outputs.sbom_cyclonedx_file }} + value: ${{ steps.meta.outputs.sbom_cyclonedx_file }} runs: using: composite steps: - # Due to https://github.com/orgs/community/discussions/41927 - - name: Symlink current Actions repo - working-directory: ${{ github.action_path }} + - name: Set Scan Job Metadata shell: bash - run: ln -fs $(realpath ../../) /home/runner/work/_actions/current + id: meta + env: + IMAGE: ${{ inputs.image }} + TAG: ${{ inputs.tag }} + ASSET_PREFIX: ${{ inputs.asset_prefix }} + run: $GITHUB_ACTION_PATH/scripts/scan-metadata.sh + + # Must upload artifact for output file parameter to have effect + - name: Generate SPDX SBOM Using Syft + uses: anchore/sbom-action@v0.15.8 + id: sbom_spdx + with: + config: ${{ inputs.config }} + image: ${{ steps.meta.outputs.scan_image }} + registry-username: ${{ inputs.registry_username }} + registry-password: ${{ inputs.registry_password }} + format: spdx-json + artifact-name: ${{ steps.meta.outputs.sbom_spdx_file }} + output-file: ${{ steps.meta.outputs.sbom_spdx_file }} + upload-artifact: true + upload-release-assets: false + dependency-snapshot: false + + - name: Generate CycloneDX SBOM Using Syft + uses: anchore/sbom-action@v0.15.8 + id: sbom_cyclonedx + with: + config: ${{ inputs.config }} + image: ${{ steps.meta.outputs.scan_image }} + registry-username: ${{ inputs.registry_username }} + registry-password: ${{ inputs.registry_password }} + format: cyclonedx-json + artifact-name: ${{ steps.meta.outputs.sbom_cyclonedx_file }} + output-file: ${{ steps.meta.outputs.sbom_cyclonedx_file }} + upload-artifact: true + upload-release-assets: false + dependency-snapshot: false + + - name: Check SBOM files existence + uses: andstor/file-existence-action@v3 + id: sbom_report + with: + files: "${{ steps.meta.outputs.sbom_spdx_file }}, ${{ steps.meta.outputs.sbom_cyclonedx_file }}" + fail: true + + # Don't fail during report generation + - name: Vulnerability analysis of SBOM + uses: anchore/scan-action@v3.6.4 + id: grype_analysis_sarif + if: ${{ steps.sbom_report.outputs.files_exists == 'true' }} + with: + sbom: ${{ steps.meta.outputs.sbom_spdx_file }} + output-format: sarif + fail-build: 'false' + add-cpes-if-none: true + severity-cutoff: ${{ steps.meta.outputs.global_severity_cutoff }} + + # Don't fail during report generation + # JSON format will report any ignored rules + - name: Vulnerability analysis of SBOM + uses: anchore/scan-action@v3.6.4 + id: grype_analysis_json + if: ${{ steps.sbom_report.outputs.files_exists == 'true' }} + with: + sbom: ${{ steps.meta.outputs.sbom_spdx_file }} + output-format: json + fail-build: 'false' + add-cpes-if-none: true + severity-cutoff: ${{ steps.meta.outputs.global_severity_cutoff }} - - run: | - ls -al /home/runner/work/_actions/current + - name: Check vulnerability analysis report existence + uses: andstor/file-existence-action@v3 + id: grype_report + with: + files: "${{ steps.grype_analysis_sarif.outputs.sarif }}, ${{ steps.grype_analysis_json.outputs.json }}" + fail: true + + # Grype CVE Action generates an ./results.sarif or ./results.report and no way to customize output file name + # Hack to increase readability of grype artifacts attached to workflows and releases + - name: Rename grype analysis report shell: bash + run: | + mv ${{ steps.grype_analysis_sarif.outputs.sarif }} ${{ steps.meta.outputs.grype_sarif_file }} + mv ${{ steps.grype_analysis_json.outputs.json }} ${{ steps.meta.outputs.grype_json_file }} + + - name: Upload grype analysis report + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.meta.outputs.grype_sarif_file }} + path: | + ${{ steps.meta.outputs.grype_sarif_file }} + if-no-files-found: warn + + # Upload grype cve reports + - name: Upload grype analysis report + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.meta.outputs.grype_json_file }} + path: | + ${{ steps.meta.outputs.grype_json_file }} + if-no-files-found: warn - # Reuse SCA action for docker-image - - name: Perform Docker SCA - id: sca - uses: ./../../_actions/current/security-actions/sca + # Fail based on severity and input parameters + # Notify grype quick scan results in table format + # Table format will supress any specified ignore rules + - name: Inspect Vulnerability analysis of SBOM + uses: anchore/scan-action@v3.6.4 + if: ${{ steps.sbom_report.outputs.files_exists == 'true' }} with: - asset_prefix: ${{ inputs.asset_prefix }} - image: ${{ inputs.image }} - tag: ${{ inputs.tag }} - registry_username: ${{ inputs.registry_username }} - registry_password: ${{ inputs.registry_password }} - fail_build: ${{ inputs.fail_build }} + sbom: ${{ steps.meta.outputs.sbom_spdx_file }} + output-format: table + fail-build: ${{ steps.meta.outputs.global_enforce_build_failure == 'true' && steps.meta.outputs.global_enforce_build_failure || inputs.fail_build }} + add-cpes-if-none: true + severity-cutoff: ${{ steps.meta.outputs.global_severity_cutoff }} - name: Check docker OCI tar existence - if: ${{ steps.sca.outputs.analyzed-image != '' }} + if: ${{ steps.meta.outputs.scan_image != '' }} uses: andstor/file-existence-action@v3 id: docker_tar with: - files: "${{ steps.sca.outputs.analyzed-image }}" + files: "${{ steps.meta.outputs.scan_image }}" - name: Generate docker-cis JSON report uses: docker://ghcr.io/aquasecurity/trivy:0.37.2 - if: ${{ steps.sca.outputs.analyzed-image != '' }} + if: ${{ steps.meta.outputs.scan_image != '' }} id: cis_json with: entrypoint: trivy - args: "image ${{ env.input }} ${{ steps.sca.outputs.analyzed-image }} --compliance ${{ env.compliance }} -f json --severity ${{ env.severity }} --ignore-unfixed -o ${{ steps.sca.outputs.cis-json-report }}" + args: "image ${{ env.input }} ${{ steps.meta.outputs.scan_image }} --compliance ${{ env.compliance }} -f json --severity ${{ env.severity }} --ignore-unfixed -o ${{ steps.meta.outputs.cis_json_file }}" env: compliance: docker-cis - severity: ${{ steps.sca.outputs.global_severity_cutoff }} + severity: ${{ steps.meta.outputs.global_enforce_build_failure }} input: ${{ steps.docker_tar.outputs.files_exists == 'true' && '--input' || '' }} - name: upload docker-cis JSON report - if: ${{ steps.sca.outputs.analyzed-image != '' }} + if: ${{ steps.meta.outputs.scan_image != '' }} uses: actions/upload-artifact@v4 with: - name: ${{ steps.sca.outputs.cis-json-report }} + name: ${{ steps.meta.outputs.cis_json_file }} path: | - ${{ steps.sca.outputs.cis-json-report }} + ${{ steps.meta.outputs.cis_json_file }} if-no-files-found: warn - name: Inspect docker-cis report - if: ${{ steps.sca.outputs.analyzed-image != '' }} + if: ${{ steps.meta.outputs.scan_image != '' }} uses: docker://ghcr.io/aquasecurity/trivy:0.37.2 with: entrypoint: trivy - args: "image ${{ env.input }} ${{ steps.sca.outputs.analyzed-image }} --compliance ${{ env.compliance }} -f table --severity ${{ env.severity }} --ignore-unfixed --exit-code ${{ env.exit-code }}" + args: "image ${{ env.input }} ${{ steps.meta.outputs.scan_image }} --compliance ${{ env.compliance }} -f table --severity ${{ env.severity }} --ignore-unfixed --exit-code ${{ env.exit-code }}" env: - exit-code: ${{ (steps.sca.outputs.global_enforce_build_failure == 'true' || inputs.fail_build == 'true') && '1' || '0' }} + exit-code: ${{ (steps.meta.outputs.global_enforce_build_failure == 'true' || inputs.fail_build == 'true') && '1' || '0' }} compliance: docker-cis - severity: ${{ steps.sca.outputs.global_severity_cutoff }} + severity: ${{ steps.meta.outputs.global_enforce_build_failure }} input: ${{ steps.docker_tar.outputs.files_exists == 'true' && '--input' || '' }} \ No newline at end of file diff --git a/security-actions/scan-docker-image/scripts/scan-metadata.sh b/security-actions/scan-docker-image/scripts/scan-metadata.sh new file mode 100755 index 00000000..0bb3198f --- /dev/null +++ b/security-actions/scan-docker-image/scripts/scan-metadata.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +set -euo pipefail + +readonly spdx_ext="sbom.spdx.json" +readonly cyclonedx_ext="sbom.cyclonedx.json" +readonly cve_json_ext="cve-report.json" +readonly cve_sarif_ext="cve-report.sarif" +readonly cis_json_ext="cis-report.json" + +global_severity_cutoff='critical' +global_enforce_build_failure='false' + +if [[ -z ${IMAGE} ]]; then + echo '::error ::Specify "image" inputs fields' + exit 1 +fi + +# OCI archive should be passed as image instead of file +if [[ -n ${IMAGE} ]]; then + if [[ -n ${TAG} ]]; then + echo "scan_image=${IMAGE}:${TAG}" >> $GITHUB_OUTPUT + else + echo "scan_image=${IMAGE}" >> $GITHUB_OUTPUT + fi +fi + +if [[ -n ${ASSET_PREFIX} ]]; then + echo "sbom_spdx_file=${ASSET_PREFIX##*/}-${spdx_ext}" >> $GITHUB_OUTPUT + echo "sbom_cyclonedx_file=${ASSET_PREFIX##*/}-${cyclonedx_ext}" >> $GITHUB_OUTPUT + echo "grype_json_file=${ASSET_PREFIX##*/}-${cve_json_ext}" >> $GITHUB_OUTPUT + echo "grype_sarif_file=${ASSET_PREFIX##*/}-${cve_sarif_ext}" >> $GITHUB_OUTPUT + echo "cis_json_file=${ASSET_PREFIX##*/}-${cis_json_ext}" >> $GITHUB_OUTPUT +else + echo "sbom_spdx_file=${spdx_ext}" >> $GITHUB_OUTPUT + echo "sbom_cyclonedx_file=${cyclonedx_ext}" >> $GITHUB_OUTPUT + echo "grype_json_file=${cve_json_ext}" >> $GITHUB_OUTPUT + echo "grype_sarif_file=${cve_sarif_ext}" >> $GITHUB_OUTPUT + echo "cis_json_file=${cis_json_ext}" >> $GITHUB_OUTPUT +fi + +if [[ -n ${global_severity_cutoff} ]]; then + echo "global_severity_cutoff=${global_severity_cutoff}" >> $GITHUB_OUTPUT +else + echo '::error ::set global_severity_cutoff in $0' + exit 1 +fi + +if [[ -n ${global_enforce_build_failure} ]]; then + echo "global_enforce_build_failure=${global_enforce_build_failure}" >> $GITHUB_OUTPUT +else + echo '::error ::set global_enforce_build_failure in $0' + exit 1 +fi