diff --git a/.github/actions/security-container-scan/README.md b/.github/actions/security-container-scan/README.md index 94b8b4a..870d947 100644 --- a/.github/actions/security-container-scan/README.md +++ b/.github/actions/security-container-scan/README.md @@ -114,6 +114,7 @@ steps: | `grype-image` | Grype container image to use for scanning. Override to pin to a specific digest for supply-chain hardening. | No | `anchore/grype:latest` | | `report-json` | Filename for the JSON report. | No | `grype-results.json` | | `report-sarif` | Filename for the SARIF report. | No | `grype-results.sarif` | +| `report-table` | Filename for the human-readable table report (always generated). | No | `grype-results.txt` | | `upload-artifact` | Upload reports as a workflow artifact. | No | `true` | | `artifact-name` | Artifact name for uploaded reports. | No | `grype-container-scan` | | `generate-sbom` | Generate and upload an SBOM via `anchore/sbom-action`. | No | `true` | @@ -131,10 +132,12 @@ steps: | `detail` | Free-form detail string corresponding to `status`. | | `report_json` | Path to the JSON report (if generated). | | `report_sarif` | Path to the SARIF report (if generated). | +| `report_table` | Path to the table report (always generated). | ## Notes -- **Step summary is count-only**: the `$GITHUB_STEP_SUMMARY` output shows total matches and Critical/High/Medium/Low counts, but does **not** list individual CVE IDs or affected packages. On public repositories, run summaries are world-readable, and publishing a list of unresolved CVEs + package versions amounts to handing attackers a roadmap. Per-CVE detail is available in the JSON/SARIF artifact (collaborators only) or, when `upload-sarif: true`, in the Security tab. +- **Step summary is count-only**: the `$GITHUB_STEP_SUMMARY` output shows total matches and Critical/High/Medium/Low counts, but does **not** list individual CVE IDs or affected packages. On public repositories, run summaries are world-readable, and publishing a list of unresolved CVEs + package versions amounts to handing attackers a roadmap. Per-CVE detail is available in the JSON/SARIF/table artifact (collaborators only) or, when `upload-sarif: true`, in the Security tab. +- **Three artifact formats**: each scan produces JSON (Grype-native, used by tooling and `jq` drill-down), SARIF (GitHub code scanning / IDE viewers), and a plain-text table (drop-in readable for reviewers who don't want to touch `jq`). All three are bundled into the same workflow artifact. - **Supply chain**: `grype-image` defaults to `anchore/grype:latest` for ease of adoption and DB freshness. For hardened pipelines, override it to a specific digest (`anchore/grype@sha256:...`) and refresh periodically. - **SBOM generation**: enabled by default; set `generate-sbom: "false"` to skip when you only need the vulnerability scan. - **Multi-arch**: Grype scans the image variant that is loaded into the local Docker daemon. When the runner is `linux/amd64` and you need to scan `linux/arm64`, pull with `--platform linux/arm64` first. diff --git a/.github/actions/security-container-scan/action.yml b/.github/actions/security-container-scan/action.yml index 6567b70..2e2fd2c 100644 --- a/.github/actions/security-container-scan/action.yml +++ b/.github/actions/security-container-scan/action.yml @@ -40,6 +40,10 @@ inputs: description: 'Filename for SARIF report' required: false default: grype-results.sarif + report-table: + description: 'Filename for the human-readable table report (always produced).' + required: false + default: grype-results.txt upload-artifact: description: 'Upload reports as a workflow artifact' required: false @@ -86,6 +90,9 @@ outputs: report_sarif: description: 'Path to SARIF report (if generated)' value: ${{ steps.final.outputs.report_sarif }} + report_table: + description: 'Path to the table report (always generated).' + value: ${{ steps.final.outputs.report_table }} runs: using: composite @@ -116,7 +123,7 @@ runs: format: ${{ inputs.sbom-format }} artifact-name: ${{ inputs.sbom-artifact-name }} - - name: Run Grype scan (JSON + SARIF) + - name: Run Grype scan (JSON + SARIF + table) id: grype if: ${{ steps.precheck.outputs.image_exists == 'true' }} shell: bash @@ -124,6 +131,7 @@ runs: IMAGE: ${{ inputs.image }} REPORT_JSON: ${{ inputs.report-json }} REPORT_SARIF: ${{ inputs.report-sarif }} + REPORT_TABLE: ${{ inputs.report-table }} FAIL_ON: ${{ inputs.fail-on }} GRYPE_IMAGE: ${{ inputs.grype-image }} run: | @@ -152,6 +160,12 @@ runs: "${GRYPE_IMAGE}" \ "${IMAGE}" -o sarif > "${REPORT_SARIF}" || true + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$HOME/.cache/grype:/root/.cache/grype" \ + "${GRYPE_IMAGE}" \ + "${IMAGE}" -o table > "${REPORT_TABLE}" || true + echo "exit_code=${scan_rc}" >> "$GITHUB_OUTPUT" if [ $scan_rc -eq 0 ]; then echo "status=ok" >> "$GITHUB_OUTPUT" @@ -191,9 +205,13 @@ runs: uses: actions/upload-artifact@v4 with: name: ${{ inputs.artifact-name }} + # if-no-files-found: ignore defensively covers the case where Grype + # fails mid-run and produces only a subset of the three formats. path: | ${{ inputs.report-json }} ${{ inputs.report-sarif }} + ${{ inputs.report-table }} + if-no-files-found: ignore - name: Write scan summary if: ${{ inputs.write-summary == 'true' }} @@ -229,6 +247,7 @@ runs: INPUT_FAIL_ON: ${{ inputs.fail-on }} INPUT_REPORT_JSON: ${{ inputs.report-json }} INPUT_REPORT_SARIF: ${{ inputs.report-sarif }} + INPUT_REPORT_TABLE: ${{ inputs.report-table }} INPUT_FAIL_BUILD: ${{ inputs.fail-build }} run: | set -euo pipefail @@ -260,6 +279,7 @@ runs: echo "detail=${detail}" >> "$GITHUB_OUTPUT" echo "report_json=${INPUT_REPORT_JSON}" >> "$GITHUB_OUTPUT" echo "report_sarif=${INPUT_REPORT_SARIF}" >> "$GITHUB_OUTPUT" + echo "report_table=${INPUT_REPORT_TABLE}" >> "$GITHUB_OUTPUT" if [ "${INPUT_FAIL_BUILD}" = "true" ] && [ "${status}" != "ok" ]; then echo "Failing build due to status=${status}: ${detail}" 1>&2