diff --git a/.github/actions/security-container-scan-aggregate/README.md b/.github/actions/security-container-scan-aggregate/README.md new file mode 100644 index 0000000..84fdf81 --- /dev/null +++ b/.github/actions/security-container-scan-aggregate/README.md @@ -0,0 +1,85 @@ +# Security Container Scan Aggregate Action + +A companion action to [`security-container-scan`](../security-container-scan/) that consolidates per-image Grype reports from an entire workflow run into a single summary table. + +Follows the same pattern as `publish-test-results`: the individual scan jobs do not post their own summaries, and this aggregator runs once after all scans complete to produce: + +- A single consolidated table in `$GITHUB_STEP_SUMMARY` on the aggregator job page +- (Optional) a sticky PR comment on `pull-request/NNN` branches created by copy-pr-bot + +## When to use + +- Your workflow scans **multiple images in one run** (typically via matrix or a fan-out of reusable workflow calls) and you want one summary per PR instead of one per image. +- You are OK with the per-image scan jobs *also* writing their own summary (the default of `security-container-scan`). If you do not want that, pass `write-summary: 'false'` to each `security-container-scan` invocation. + +## Prerequisites + +- Each upstream `security-container-scan` job must upload a per-image artifact whose name matches the pattern passed via `artifact-pattern` (default `grype-*`). The built-in convention is `grype---`; the aggregator strips those trailing numeric segments to display a clean service name. +- Each artifact must contain `grype-results.json` at its root (this is what `security-container-scan` uploads by default). +- If `post-pr-comment: true` is used, the **caller job** must grant `pull-requests: write` permission. A default `GITHUB_TOKEN` is used unless an override is supplied via `github-token`. + +## PR comment scope + +This action only posts PR comments on push events that target the `pull-request/NNN` branch pattern produced by copy-pr-bot. The native `pull_request` event is intentionally **not** supported, matching the policy that all PR CI on these repos must be driven by copy-pr-bot. + +On any other ref (`main`, tags, `feat/**`, `fix/**`, etc.) the PR-comment step is a no-op. + +## Usage + +```yaml +jobs: + build-and-scan: + # ... your per-service build job with security-container-scan ... + + container-scan-summary: + name: Container Scan Summary + needs: build-and-scan + if: always() # still summarise if some services failed + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write # required for post-pr-comment: true + steps: + - uses: NVIDIA/dsx-github-actions/.github/actions/security-container-scan-aggregate@main + with: + post-pr-comment: 'true' +``` + +### Customising the artifact pattern + +```yaml +- uses: NVIDIA/dsx-github-actions/.github/actions/security-container-scan-aggregate@main + with: + artifact-pattern: 'scan-report-*' # if your callers name artifacts differently + post-pr-comment: 'true' +``` + +## Inputs + +| Input | Description | Required | Default | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ---------------- | +| `artifact-pattern` | Glob pattern forwarded to `actions/download-artifact` to pick which artifacts to aggregate. | No | `grype-*` | +| `download-path` | Directory the artifacts are downloaded into. | No | `.grype-aggregate` | +| `post-pr-comment` | Post/update a sticky PR comment on copy-pr-bot `pull-request/NNN` branches. No-op on other refs. | No | `false` | +| `github-token` | Token for listing, creating, and patching PR comments. | No | `${{ github.token }}` | + +## Outputs + +| Output | Description | +| -------------- | ------------------------------------------------------------------------------- | +| `summary-path` | Absolute path to the rendered markdown summary file on the runner. | +| `pr-number` | PR number extracted from a `pull-request/NNN` ref; empty string on other refs. | + +## Output format + +Rendered table columns: + +| Service | Total | Critical | High | Medium | Low | Other | + +`Other` merges the `Negligible` and `Unknown` severity buckets so the main table stays narrow; detail lives in the per-service artifacts. + +By design, the summary contains **only severity counts** — no CVE IDs, packages, or versions — to avoid turning a public workflow run into an attacker roadmap. Drill-down into the per-service `grype-*` artifact (JSON + SARIF) is collaborator-only. + +## Sticky comment marker + +The PR comment is marked with the HTML comment `` so successive runs on the same PR update the same comment in place rather than appending new ones. Do not include this marker in other bot comments on the same PR. diff --git a/.github/actions/security-container-scan-aggregate/action.yml b/.github/actions/security-container-scan-aggregate/action.yml new file mode 100644 index 0000000..fc7c2f4 --- /dev/null +++ b/.github/actions/security-container-scan-aggregate/action.yml @@ -0,0 +1,117 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: security-container-scan-aggregate +description: >- + Aggregate Grype JSON reports produced by multiple `security-container-scan` + jobs in the same workflow run into a single consolidated summary, written + to `$GITHUB_STEP_SUMMARY` and (optionally) posted as a sticky PR comment + on `pull-request/NNN` branches created by copy-pr-bot. + +inputs: + artifact-pattern: + description: 'Glob pattern for Grype artifact names to aggregate (passed to actions/download-artifact).' + required: false + default: 'grype-*' + download-path: + description: 'Directory to download Grype artifacts into.' + required: false + default: '.grype-aggregate' + post-pr-comment: + description: 'Post/update a sticky PR comment with the consolidated table. Only acts on copy-pr-bot `pull-request/NNN` branches; a no-op on `main`, tags, and other branches. Requires `pull-requests: write` on the caller job.' + required: false + default: 'false' + github-token: + description: 'GitHub token used to read/create/patch PR comments.' + required: false + default: ${{ github.token }} + +outputs: + summary-path: + description: 'Absolute path to the rendered markdown summary file.' + value: ${{ steps.render.outputs.summary-path }} + pr-number: + description: 'PR number detected from a `pull-request/NNN` ref; empty otherwise.' + value: ${{ steps.pr-ctx.outputs.pr }} + +runs: + using: composite + steps: + - name: Download Grype artifacts + uses: actions/download-artifact@v4 + with: + pattern: ${{ inputs.artifact-pattern }} + path: ${{ inputs.download-path }} + + - name: Render consolidated summary + id: render + shell: bash + env: + DOWNLOAD_PATH: ${{ inputs.download-path }} + run: | + set -euo pipefail + out="${RUNNER_TEMP}/grype-aggregate-summary.md" + python3 "$GITHUB_ACTION_PATH/aggregate.py" --dir "$DOWNLOAD_PATH" > "$out" + cat "$out" >> "$GITHUB_STEP_SUMMARY" + echo "summary-path=$out" >> "$GITHUB_OUTPUT" + + - name: Detect copy-pr-bot PR context + id: pr-ctx + shell: bash + env: + REF_NAME: ${{ github.ref_name }} + run: | + set -euo pipefail + pr="" + # Only copy-pr-bot's `pull-request/` push pattern is supported here — + # native `pull_request` events are deliberately out of scope, matching + # the policy that all PR CI on these repos must be driven by copy-pr-bot. + if [[ "$REF_NAME" =~ ^pull-request/([0-9]+)$ ]]; then + pr="${BASH_REMATCH[1]}" + fi + echo "pr=${pr}" >> "$GITHUB_OUTPUT" + + - name: Post or update sticky PR comment + if: inputs.post-pr-comment == 'true' && steps.pr-ctx.outputs.pr != '' + shell: bash + env: + GH_TOKEN: ${{ inputs.github-token }} + PR_NUMBER: ${{ steps.pr-ctx.outputs.pr }} + SUMMARY_PATH: ${{ steps.render.outputs.summary-path }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + MARKER="" + + body_file="${RUNNER_TEMP}/grype-comment-body.md" + { + echo "${MARKER}" + echo + cat "$SUMMARY_PATH" + } > "$body_file" + + existing_id=$(gh api "/repos/${REPO}/issues/${PR_NUMBER}/comments" \ + --jq ".[] | select(.body | contains(\"${MARKER}\")) | .id" \ + 2>/dev/null | head -1 || true) + + if [ -n "$existing_id" ]; then + echo "Updating existing grype-scan-summary comment ${existing_id} on PR #${PR_NUMBER}" + gh api "/repos/${REPO}/issues/comments/${existing_id}" \ + --method PATCH \ + --field body=@"$body_file" >/dev/null + else + echo "Creating new grype-scan-summary comment on PR #${PR_NUMBER}" + gh pr comment "$PR_NUMBER" --repo "$REPO" --body-file "$body_file" + fi diff --git a/.github/actions/security-container-scan-aggregate/aggregate.py b/.github/actions/security-container-scan-aggregate/aggregate.py new file mode 100644 index 0000000..b843310 --- /dev/null +++ b/.github/actions/security-container-scan-aggregate/aggregate.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Aggregate multiple Grype JSON reports into a consolidated markdown table. + +Expects the input directory to contain one subdirectory per downloaded +artifact (as produced by `actions/download-artifact@v4` with a pattern), +each holding a `grype-results.json` file. Subdirectory name is assumed +to follow `grype-[--]`; that convention +is what `security-container-scan` produces and what callers should use +for artifact names. +""" + +import argparse +import glob +import json +import os +import re +import sys +from collections import Counter + + +def _load_matches(path): + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + except Exception as exc: + print(f"WARN: failed to parse {path}: {exc}", file=sys.stderr) + return None + matches = data.get("matches", []) + return matches if isinstance(matches, list) else [] + + +def _service_from_dirname(dirname): + name = os.path.basename(dirname) + name = re.sub(r"^grype-", "", name, count=1) + # Strip trailing `--` that security-container-scan + # callers use to keep artifact names unique across reruns. + name = re.sub(r"-\d+-\d+$", "", name) + return name or os.path.basename(dirname) + + +def _counts(matches): + counter = Counter() + for m in matches: + vuln = m.get("vulnerability") or {} + sev = vuln.get("severity") or "Unknown" + if not isinstance(sev, str): + sev = "Unknown" + counter[sev] += 1 + return counter + + +def _render(rows): + lines = ["## 🔍 Container Scan Summary", ""] + if not rows: + lines.append("_No Grype artifacts were found to aggregate._") + lines.append("") + return "\n".join(lines) + + lines.append("| Service | Total | Critical | High | Medium | Low | Other |") + lines.append("|---|---:|---:|---:|---:|---:|---:|") + totals = [0] * 6 + for name, total, crit, high, med, low, other in rows: + lines.append( + f"| {name} | {total} | {crit} | {high} | {med} | {low} | {other} |" + ) + for i, v in enumerate((total, crit, high, med, low, other)): + totals[i] += v + + lines.append( + "| **TOTAL** | **{0}** | **{1}** | **{2}** | **{3}** | **{4}** | **{5}** |".format( + *totals + ) + ) + lines.append("") + lines.append( + "_Per-CVE detail lives in the per-service `grype-*` artifacts " + "(JSON + SARIF). Severity counts only — no CVE IDs published here._" + ) + lines.append("") + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--dir", + required=True, + help="Root directory containing `grype-*/grype-results.json` subtrees.", + ) + args = parser.parse_args() + + pattern = os.path.join(args.dir, "*", "grype-results.json") + report_paths = sorted(glob.glob(pattern)) + + rows = [] + for path in report_paths: + matches = _load_matches(path) + if matches is None: + continue + name = _service_from_dirname(os.path.dirname(path)) + counter = _counts(matches) + rows.append( + ( + name, + len(matches), + counter.get("Critical", 0), + counter.get("High", 0), + counter.get("Medium", 0), + counter.get("Low", 0), + counter.get("Negligible", 0) + counter.get("Unknown", 0), + ) + ) + + rows.sort(key=lambda r: r[0]) + sys.stdout.write(_render(rows)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/actions/security-container-scan/README.md b/.github/actions/security-container-scan/README.md new file mode 100644 index 0000000..94b8b4a --- /dev/null +++ b/.github/actions/security-container-scan/README.md @@ -0,0 +1,141 @@ +# Security Container Scan Action + +A composite GitHub Action that generates an SBOM (via Syft) and scans a locally-built container image for known vulnerabilities (via Anchore Grype). It produces JSON and SARIF reports, writes a human-friendly summary, and can optionally upload the SARIF to the GitHub code scanning Security tab. + +## Features + +- ✅ SBOM generation with `anchore/sbom-action` (Syft, SPDX-JSON by default) +- ✅ Vulnerability scan with Anchore Grype (JSON + SARIF outputs) +- ✅ Top-N CVE summary written to `$GITHUB_STEP_SUMMARY` +- ✅ Reports uploaded as a workflow artifact +- ✅ Optional SARIF upload to GitHub code scanning with per-image categories +- ✅ Non-fatal by default — findings surface without blocking the build unless opted in + +## Prerequisites + +### Docker daemon on the runner + +The action scans a local image by shelling out to `docker image inspect` and running Grype in a container with the host Docker socket mounted. The image you want to scan must already exist in the runner's Docker daemon (build with `load: true` or `docker pull` before calling this action). + +### GitHub Advanced Security (only when `upload-sarif: true`) + +Uploading SARIF to the Security tab uses the standard GitHub code scanning pipeline, which requires **GitHub Advanced Security (GHAS)** on private repositories: + +- ✅ **Public repositories**: GHAS is free and available +- ⚠️ **Private repositories**: GHAS requires a paid license + +Upload failures (including `422 Advanced Security must be enabled for this repository`) are wrapped in `continue-on-error: true`, so they do not fail the job. If GHAS is not available, leave `upload-sarif` at its default of `false` and rely on the artifact + job summary for findings. + +The caller job must also grant `security-events: write` permission when `upload-sarif: true`. + +## Usage + +### Basic — scan a locally built image + +```yaml +jobs: + build-and-scan: + runs-on: linux-amd64-cpu4 + steps: + - uses: actions/checkout@v4 + + - name: Build image locally + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64 + push: false + load: true + tags: localbuild/myapp:${{ github.run_id }} + + - name: Grype scan + uses: NVIDIA/dsx-github-actions/.github/actions/security-container-scan@main + with: + image: localbuild/myapp:${{ github.run_id }} + fail-on: high + fail-build: "false" +``` + +### With SARIF upload to GitHub Security tab + +```yaml +jobs: + scan: + runs-on: linux-amd64-cpu4 + permissions: + contents: read + security-events: write # required for SARIF upload + steps: + - uses: actions/checkout@v4 + + - name: Build image locally + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64 + push: false + load: true + tags: localbuild/myapp:${{ github.run_id }} + + - name: Grype scan + SARIF upload + uses: NVIDIA/dsx-github-actions/.github/actions/security-container-scan@main + with: + image: localbuild/myapp:${{ github.run_id }} + fail-on: critical + fail-build: "false" + upload-sarif: "true" + sarif-category: grype-myapp +``` + +### Scanning multiple images in the same run (matrix) + +When scanning several images in one workflow run, always pass a unique `sarif-category` per image — otherwise later uploads replace earlier ones in the Security tab. + +```yaml +strategy: + fail-fast: false + matrix: + service: [api, worker, gateway] +steps: + - uses: NVIDIA/dsx-github-actions/.github/actions/security-container-scan@main + with: + image: localbuild/${{ matrix.service }}:${{ github.run_id }} + upload-sarif: "true" + sarif-category: grype-${{ matrix.service }} +``` + +## Inputs + +| Input | Description | Required | Default | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------- | +| `image` | Local container image reference to scan (must exist on the runner). | Yes | — | +| `fail-on` | Minimum Grype severity that counts as a failure. One of `negligible`, `low`, `medium`, `high`, `critical`. | No | `high` | +| `fail-build` | If `true`, fail the step when Grype finds vulnerabilities at/above `fail-on` or when SBOM/scan prerequisites fail. | No | `false` | +| `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` | +| `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` | +| `sbom-format` | SBOM format for `sbom-action` (e.g. `spdx-json`, `cyclonedx-json`). | No | `spdx-json` | +| `sbom-artifact-name` | Artifact name for the SBOM uploaded by `sbom-action`. | No | `container.spdx.json` | +| `write-summary` | Write a human-friendly summary into `$GITHUB_STEP_SUMMARY`. | No | `true` | +| `upload-sarif` | Upload SARIF to GitHub code scanning. Requires `security-events: write` and, for private repos, GHAS. Failures are non-fatal. | No | `false` | +| `sarif-category` | Category for SARIF upload. Must be unique per image in multi-image runs. Defaults to `grype-`. | No | `""` (auto-derived) | + +## Outputs + +| Output | Description | +| -------------- | ----------------------------------------------------------------------------------------------------------- | +| `status` | One of `ok`, `high_or_error`, `image_not_found`, `pull_failed`, `sbom_failed`, `grype_unknown`. | +| `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). | + +## 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. +- **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. +- **GHAS-free fallback**: leave `upload-sarif` at `false`. Findings still appear in the workflow artifact and job summary; the job does not fail. diff --git a/.github/actions/security-container-scan/action.yml b/.github/actions/security-container-scan/action.yml index 5265f11..6567b70 100644 --- a/.github/actions/security-container-scan/action.yml +++ b/.github/actions/security-container-scan/action.yml @@ -64,6 +64,14 @@ inputs: description: 'Write a human-friendly summary into $GITHUB_STEP_SUMMARY' required: false default: 'true' + upload-sarif: + description: 'Upload the SARIF report to GitHub code scanning (Security tab). Requires `security-events: write` permission on the caller job and, for private repositories, GitHub Advanced Security. Upload failures are non-fatal.' + required: false + default: 'false' + sarif-category: + description: 'Category for the SARIF upload. Use a unique value per image when multiple images are scanned in the same run so entries do not overwrite each other in the Security tab. When empty, defaults to `grype-`.' + required: false + default: '' outputs: status: @@ -152,6 +160,32 @@ runs: fi exit 0 + - name: Compute SARIF category + id: sarif-cat + if: ${{ inputs.upload-sarif == 'true' && steps.precheck.outputs.image_exists == 'true' }} + shell: bash + env: + INPUT_CATEGORY: ${{ inputs.sarif-category }} + INPUT_IMAGE: ${{ inputs.image }} + run: | + set -euo pipefail + if [ -n "${INPUT_CATEGORY}" ]; then + category="${INPUT_CATEGORY}" + else + # Sanitize image ref for use as a code-scanning category (keep alnum, dot, underscore, dash) + sanitized=$(echo "${INPUT_IMAGE}" | tr -c 'A-Za-z0-9._-' '-' | tr -s '-' | sed 's/^-//;s/-$//') + category="grype-${sanitized}" + fi + echo "category=${category}" >> "$GITHUB_OUTPUT" + + - name: Upload SARIF to GitHub code scanning + if: ${{ inputs.upload-sarif == 'true' && steps.precheck.outputs.image_exists == 'true' }} + continue-on-error: true + uses: github/codeql-action/upload-sarif@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5, use sha to align security guidelines + with: + sarif_file: ${{ inputs.report-sarif }} + category: ${{ steps.sarif-cat.outputs.category }} + - name: Upload Grype reports if: ${{ inputs.upload-artifact == 'true' && steps.precheck.outputs.image_exists == 'true' }} uses: actions/upload-artifact@v4 @@ -176,7 +210,10 @@ runs: echo "- Reports artifact: \`${INPUT_ARTIFACT_NAME}\` (sarif + json)" >> "$GITHUB_STEP_SUMMARY" if [ -f "${INPUT_REPORT_JSON}" ]; then - python3 "$GITHUB_ACTION_PATH/grype_summary.py" --json "${INPUT_REPORT_JSON}" --max-top 10 >> "$GITHUB_STEP_SUMMARY" || true + # --max-top 0 suppresses the top CVE table; run summaries can be public + # on open-source repos, and listing specific vulns + package versions + # is a roadmap for attackers. Per-CVE detail stays in the artifact. + python3 "$GITHUB_ACTION_PATH/grype_summary.py" --json "${INPUT_REPORT_JSON}" --max-top 0 >> "$GITHUB_STEP_SUMMARY" || true fi - name: Finalize status/outputs diff --git a/README.md b/README.md index 96a4417..0333ba6 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ A collection of reusable GitHub Actions for standardizing CI/CD workflows across | ------------------------------------------------------- | --------------------------------- | --------------------------------- | | [codeql-scan](.github/actions/codeql-scan/) | Static code analysis with CodeQL | Security vulnerability detection | | [trufflehog-scan](.github/actions/trufflehog-scan/) | Secret scanning with TruffleHog | Leaked credentials detection | +| [security-container-scan](.github/actions/security-container-scan/) | Container vuln scan (SBOM + Grype) | Container image CVE detection | +| [security-container-scan-aggregate](.github/actions/security-container-scan-aggregate/) | Aggregate multi-image Grype reports into one summary | Per-PR consolidated scan summary + sticky comment | | [semantic-release](.github/actions/semantic-release/) | Automated versioning and releases | Semantic versioning and changelog | | [resource-push-ngc](.github/actions/resource-push-ngc/) | Push resources to NGC | Artifact publishing | | [docker-build](.github/actions/docker-build/) | Docker Buildx build/push wrapper | Build/push multi-arch OCI images | @@ -27,7 +29,7 @@ A collection of reusable GitHub Actions for standardizing CI/CD workflows across ## ⚠️ Important: GitHub Advanced Security Required -The security scanning action (`codeql-scan`) uploads results to GitHub's Code Scanning feature, which **requires GitHub Advanced Security (GHAS)** to be enabled: +The security scanning actions (`codeql-scan`, `security-container-scan` with `upload-sarif: true`) upload results to GitHub's Code Scanning feature, which **requires GitHub Advanced Security (GHAS)** to be enabled: - ✅ **Public repositories**: Free and automatically available - ⚠️ **Private repositories**: Requires GHAS license @@ -35,9 +37,10 @@ The security scanning action (`codeql-scan`) uploads results to GitHub's Code Sc Without GHAS enabled, scans will run successfully but uploads will fail. See individual action documentation for workarounds and details: - [CodeQL Prerequisites](.github/actions/codeql-scan/README.md#️-prerequisites) +- [Security Container Scan Prerequisites](.github/actions/security-container-scan/README.md#prerequisites) > **Note**: `trivy-scan` has been removed due to a supply chain compromise (March 2026). -> See: https://github.com/aquasecurity/trivy/discussions/10425 +> See: https://github.com/aquasecurity/trivy/discussions/10425 — use `security-container-scan` (Anchore Grype) as the replacement. ## 📖 Quick Start @@ -107,6 +110,8 @@ This reusable workflow wraps `skopeo copy`, so it copies the entire manifest lis - [CodeQL Scan Action](.github/actions/codeql-scan/README.md) - [TruffleHog Secret Scan Action](.github/actions/trufflehog-scan/README.md) +- [Security Container Scan Action](.github/actions/security-container-scan/README.md) +- [Security Container Scan Aggregate Action](.github/actions/security-container-scan-aggregate/README.md) - [Semantic Release Action](.github/actions/semantic-release/README.md) - [Resource Push NGC Action](.github/actions/resource-push-ngc/README.md) - [Docker Build Action](.github/actions/docker-build/README.md) @@ -311,17 +316,19 @@ If CI still fails, execute `pre-commit run actionlint --all-files` or `pre-commi ```text .github/ ├── actions/ -│ ├── codeql-scan/ # Static code analysis (CodeQL) -│ ├── trufflehog-scan/ # Secret scanning (TruffleHog) -│ ├── docker-build/ # Docker build/push wrapper -│ ├── semantic-release/ # Automated versioning and releases -│ ├── resource-push-ngc/ # NGC resources publishing -│ ├── git-tag/ # Create and push git tag -│ ├── slack-notify/ # Send Slack notifications -│ ├── go-lint/ # Go linting (golangci-lint, fmt, vet) -│ ├── go-test/ # Go tests with coverage and JUnit -│ ├── license-headers/ # SPDX license header checks -│ └── commitlint/ # Conventional commit validation +│ ├── codeql-scan/ # Static code analysis (CodeQL) +│ ├── trufflehog-scan/ # Secret scanning (TruffleHog) +│ ├── security-container-scan/ # Container vuln scan (SBOM + Grype) +│ ├── security-container-scan-aggregate/ # Multi-image Grype summary + PR comment +│ ├── docker-build/ # Docker build/push wrapper +│ ├── semantic-release/ # Automated versioning and releases +│ ├── resource-push-ngc/ # NGC resources publishing +│ ├── git-tag/ # Create and push git tag +│ ├── slack-notify/ # Send Slack notifications +│ ├── go-lint/ # Go linting (golangci-lint, fmt, vet) +│ ├── go-test/ # Go tests with coverage and JUnit +│ ├── license-headers/ # SPDX license header checks +│ └── commitlint/ # Conventional commit validation └── workflows/ ├── release.yml # Automatic semantic versioning ├── promote-image.yml # Promote image across registries