From e64cec5adda1276ab3e37359778ed352dae7fd7a Mon Sep 17 00:00:00 2001 From: yucheng-berri Date: Thu, 25 Jun 2026 16:35:50 -0700 Subject: [PATCH] ci(image-scan): add Grype image scan for OS + library CVEs (#31151) * ci(image-scan): add Grype image scan for OS + library CVEs Builds each of the 6 Dockerfiles via a matrix and scans the resulting image with Grype (pinned v0.114.0, sha256 verified), failing on fixable HIGH or CRITICAL across both OS/apk and language packages. This catches the layer osv-scan is structurally blind to (Wolfi/apk OS packages and vendored deps like prisma's node engine), which is the structural reason the openssl CVE slipped past CI and a customer's image scanner flagged it. Skipped on fork PRs so an outside contributor cannot run arbitrary code on our hosted runner via a malicious Dockerfile RUN line. The same pattern is used by guard-fork-dependencies.yml. Grype runs as a pinned binary with a verified checksum, so there is no mutable-tag GitHub Action in the dependency chain and no vendor credentials in the scan job. The job uses read-only contents permissions and an empty top-level permissions block. * ci(image-scan): scan only Dockerfile.non_root (rootless target) All Dockerfile variants share the same wolfi base and apk set today, so a single scan of Dockerfile.non_root gives the same OS-layer coverage at one-sixth the build cost. Dockerfile.non_root is the rootless variant we ship (USER 65534), so the scan tracks the image customers actually run. Matrix-scan if the variants ever diverge. * ci: retrigger checks (proxy_pass_through_endpoint_tests flaked on prior run) --- .github/workflows/image-scan.yml | 65 ++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 .github/workflows/image-scan.yml diff --git a/.github/workflows/image-scan.yml b/.github/workflows/image-scan.yml new file mode 100644 index 00000000000..90ede5a653f --- /dev/null +++ b/.github/workflows/image-scan.yml @@ -0,0 +1,65 @@ +name: Image Scan + +on: + pull_request: + branches: + - main + - litellm_internal_staging + - litellm_oss_branch + - "litellm_**" + paths: + - docker/Dockerfile.non_root + - uv.lock + - ui/litellm-dashboard/package-lock.json + - .github/workflows/image-scan.yml + schedule: + - cron: "41 6 * * *" + workflow_dispatch: + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + image-scan: + name: image-scan + runs-on: ubuntu-latest + if: >- + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository + timeout-minutes: 30 + permissions: + contents: read + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false + + - name: Download Grype v0.114.0 + run: | + curl -fsSL --retry 3 -o "$RUNNER_TEMP/grype.tar.gz" \ + https://github.com/anchore/grype/releases/download/v0.114.0/grype_0.114.0_linux_amd64.tar.gz + echo "edda0968d8827daab01d32b3cd7de192ae0915005e7bbfcfef9e68e79bc43343 $RUNNER_TEMP/grype.tar.gz" | sha256sum -c - + tar xzf "$RUNNER_TEMP/grype.tar.gz" -C "$RUNNER_TEMP" grype + chmod +x "$RUNNER_TEMP/grype" + + # Dockerfile.non_root is the rootless variant we ship. The other + # Dockerfiles share the same wolfi base and apk set, so OS-layer coverage + # is the same; matrix-scan if those variants ever diverge. + - name: Build runtime image + run: docker build -f docker/Dockerfile.non_root -t litellm-image-scan:${{ github.sha }} . + + # Scans the whole shipped artifact: OS/apk plus every language package + # baked into the image, including ones no lockfile declares (e.g. prisma's + # vendored node engine) that osv-scan cannot see. osv-scan stays the fast + # source-level gate; this is the customer's-eye-view backstop. Credential- + # free OSS, run as a pinned, checksum-verified binary; no GitHub Action + # dependency and no vendor SaaS callout. + - name: Scan image for fixable HIGH/CRITICAL CVEs + run: | + "$RUNNER_TEMP/grype" litellm-image-scan:${{ github.sha }} \ + --only-fixed \ + --fail-on high \ + --output table