diff --git a/.github/workflows/sbom-vulnerability-scan.yml b/.github/workflows/sbom-vulnerability-scan.yml new file mode 100644 index 0000000..687ffd1 --- /dev/null +++ b/.github/workflows/sbom-vulnerability-scan.yml @@ -0,0 +1,125 @@ +name: SBOM Vulnerability Scan + +on: + workflow_call: + inputs: + fetch_depth: + description: "Number of commits to fetch. Use 0 to fetch the full history and tags." + required: false + type: number + default: 1 + + dockerfile: + description: "Path to the Dockerfile" + required: false + type: string + + requirements: + description: "Path to the requirements.txt file" + required: false + type: string + + pyproject: + description: "Path to the pyproject.toml file" + required: false + type: string + + fail-build: + description: "Fail the workflow when vulnerabilities above the severity cutoff are found" + required: false + default: false + type: boolean + +jobs: + sbom-vulnerability-scan: + runs-on: ubuntu-latest + + steps: + - name: Validate inputs + run: | + count=0 + + [ -n "${{ inputs.dockerfile }}" ] && count=$((count + 1)) + [ -n "${{ inputs.requirements }}" ] && count=$((count + 1)) + [ -n "${{ inputs.pyproject }}" ] && count=$((count + 1)) + + if [ "$count" -ne 1 ]; then + echo "Provide exactly one of: dockerfile, requirements, or pyproject." + exit 1 + fi + + - name: Checkout the code + uses: actions/checkout@v7 + with: + fetch-depth: ${{ inputs.fetch_depth }} + + # Docker + - name: Build the Docker image + if: inputs.dockerfile != '' + run: docker build . --file "${{ inputs.dockerfile }}" --tag localbuild/testimage:latest + + - name: Generate SBOM from Docker image + if: inputs.dockerfile != '' + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 + with: + image: localbuild/testimage:latest + artifact-name: docker.cyclonedx.json + output-file: docker.cyclonedx.json + format: cyclonedx-json + + - name: Scan Docker SBOM for vulnerabilities + if: inputs.dockerfile != '' + id: docker-vulnerability-scan + uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 + with: + sbom: docker.cyclonedx.json + fail-build: ${{ inputs.fail-build }} + output-format: sarif + + - name: Upload Docker vulnerability results to GitHub Security + if: inputs.dockerfile != '' + uses: github/codeql-action/upload-sarif@24ea975727876cf496b1eb0c5b36e96e01600b51 # v4.37.0 + with: + sarif_file: ${{ steps.docker-vulnerability-scan.outputs.sarif }} + category: grype-docker + + # Python + - name: Create Python environment from requirements + if: inputs.requirements != '' + run: | + python -m venv .venv + .venv/bin/pip install -r "${{ inputs.requirements }}" + + - name: Create Python environment from pyproject + if: inputs.pyproject != '' + run: | + python -m venv .venv + .venv/bin/pip install . + + - name: Generate SBOM from Python environment + if: inputs.requirements != '' || inputs.pyproject != '' + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 + with: + path: .venv + artifact-name: python.cyclonedx.json + output-file: python.cyclonedx.json + format: cyclonedx-json + + - name: Scan Python environment for vulnerabilities + if: inputs.requirements != '' || inputs.pyproject != '' + id: python-vulnerability-scan + uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 + with: + # Scan the .venv directly instead of the generated SBOM because + # the SBOM scan produced empty SARIF artifact locations, while + # the direct .venv scan provides valid locations for GitHub Code Scanning. + path: .venv + fail-build: ${{ inputs.fail-build }} + output-format: sarif + + - name: Upload Python vulnerability results to GitHub Security + if: inputs.requirements != '' || inputs.pyproject != '' + uses: github/codeql-action/upload-sarif@24ea975727876cf496b1eb0c5b36e96e01600b51 # v4.37.0 + with: + sarif_file: ${{ steps.python-vulnerability-scan.outputs.sarif }} + category: grype-python \ No newline at end of file diff --git a/README.md b/README.md index deaa8ce..fa893ef 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,61 @@ jobs: secrets: PYPI_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} ``` +## SBOM Vulnerability Scan + +The SBOM vulnerability scan workflow generates a CycloneDX SBOM and scans +dependencies for known vulnerabilities with Grype. The workflow can be used +with a Dockerfile, a `requirements.txt` file, or a `pyproject.toml` file. + +For Docker-based projects, Grype scans the SBOM generated from the Docker image. +For Python projects, a virtual environment is created from either +`requirements.txt` or `pyproject.toml`. Grype scans the virtual environment +directly because this provides valid SARIF artifact locations for GitHub Code +Scanning. + +The vulnerability results are uploaded to GitHub Code Scanning. + +You can use it e.g. like this: + +```yaml +name: SBOM Vulnerability Scan + +on: + push: + branches: [ "main" ] + +jobs: + sbom-scan: + permissions: + contents: read + security-events: write + + uses: mundialis/github-workflows/.github/workflows/sbom-vulnerability-scan.yml@main + with: + dockerfile: docker/actinia-core-alpine/Dockerfile + # requirements: requirements.txt + # pyproject: pyproject.toml +``` + +Provide exactly one of the following inputs: + +- `dockerfile`: Path to the Dockerfile. +- `requirements`: Path to the requirements.txt file. +- `pyproject`: Path to the pyproject.toml file. + +The calling job requires the following permissions: + +- `contents: read` to check out the repository. +- `security-events: write` to upload the vulnerability results to GitHub Code Scanning. + +Optional inputs: +- `fetch_depth`: Number of commits to fetch during checkout. Use `0` to fetch the full history and tags. Default: `1`. +- `fail-build`: Set to `true` if the workflow should fail when vulnerabilities above the severity +cutoff are found. Default: `false`. + +The generated Docker or Python SBOM is uploaded as a workflow artifact. + +The vulnerability results are available under **Security and quality** → **Code scanning**. # pre-commit