Container Image Scans

March 10, 2026 · View on GitHub

Compare CVE findings across container images using trivy and grype scanners. Results are stored in Supabase and visualised in a Next.js dashboard deployed to GitHub Pages.

Table of Contents

Architecture

flowchart TD
  GHA["GitHub Actions\n(nightly cron)"]
  TRIVY["trivy"]
  GRYPE["grype"]
  DB["Supabase (PostgreSQL)"]
  WEB["Next.js Dashboard (GitHub Pages)"]

  GHA --> TRIVY
  GHA --> GRYPE
  TRIVY -- "SARIF + metadata" --> DB
  GRYPE -- "SARIF + metadata" --> DB
  DB -- "anon key (read)" --> WEB

Container Images

Images are organised into groups:

GroupImage
pythondocker.io/python:alpine
pythongcr.io/distroless/python3-debian12:latest
pythoncgr.dev/chainguard/python:latest
pythonregistry.access.redhat.com/ubi10/python-312-minimal:latest
nodedocker.io/node:24-alpine
nodegcr.io/distroless/nodejs24-debian12:latest
nodecgr.dev/chainguard/node:latest
noderegistry.access.redhat.com/ubi10/nodejs-24-minimal:latest
phpdocker.io/php:alpine
phpcgr.dev/chainguard/php:latest
phpregistry.access.redhat.com/ubi10/php-83:latest
nginxdocker.io/nginx:alpine
nginxcgr.dev/chainguard/nginx:latest
nginxregistry.access.redhat.com/ubi10/nginx-126:latest
rubydocker.io/ruby:alpine
rubycgr.dev/chainguard/ruby:latest
rubyregistry.access.redhat.com/ubi10/ruby-33:latest
basedocker.io/alpine:latest
basedocker.io/debian:stable-slim
basedocker.io/ubuntu:latest
basegcr.io/distroless/static:latest
baseregistry.access.redhat.com/ubi10-micro:latest

To add more images, insert rows into the container_images table with the appropriate group_id. The nightly scan picks up all images automatically.

Prerequisites

  • mise (curl https://mise.run | sh) -- manages Node.js, Supabase CLI, fnox, grype, trivy, and yq via .mise.toml
  • A Supabase project (free tier works)
  • Docker (for running scans locally)

After cloning the repository, run mise install to install all required tools at the versions defined in .mise.toml.

For local development the fnox mise plugin fetches secrets from AWS Parameter Store (configured in fnox.toml). This requires an AWS profile named my-aws with access to the /github/ruzickap/container-image-scans/actions-secrets/ path in the eu-central-1 region.

Supabase Setup

1. Create a Supabase project

Sign up at supabase.com and create a new project. Note the Project URL and API keys from Settings > API.

2. Apply the database migration

Run the db:push mise task, which links the CLI to your project and pushes all pending migrations in supabase/migrations/:

export SUPABASE_ACCESS_TOKEN="your-access-token"
export SUPABASE_PROJECT_REF="your-project-ref"
export SUPABASE_DB_PASSWORD="your-db-password"
mise run db:push

When the fnox-env plugin is configured the three variables above are populated automatically from AWS Parameter Store.

Alternatively, apply the schema manually via the SQL Editor in the Supabase Dashboard:

  1. Open your project in the Supabase Dashboard
  2. Go to SQL Editor
  3. Paste the contents of supabase/migrations/20250301000000_initial_schema.sql
  4. Click Run

3. Verify tables exist

In the Supabase Dashboard go to Table Editor and confirm these tables were created:

  • image_groups (seeded with python, node, php, nginx, ruby, and base)
  • container_images (seeded with all twenty-two images)
  • scans (empty, populated by nightly scans)
  • cves (empty, populated by nightly scans)

4. Adding new images

-- First add the group if it does not exist
INSERT INTO image_groups (name)
VALUES ('your-group')
ON CONFLICT (name) DO NOTHING;

-- Then add the image
INSERT INTO container_images (image, group_id)
VALUES (
  'docker.io/your-image:tag',
  (SELECT id FROM image_groups WHERE name = 'your-group')
);

GitHub Secrets

Configure these secrets in Settings > Secrets and variables > Actions:

SecretDescription
SUPABASE_URLSupabase project URL (https://…supabase.co)
SUPABASE_SERVICE_ROLE_KEYSupabase service_role key (write access)
SUPABASE_ACCESS_TOKENSupabase personal access token (CLI auth)
SUPABASE_PROJECT_REFSupabase project reference ID
SUPABASE_DB_PASSWORDDatabase password (used by supabase db push)
NEXT_PUBLIC_SUPABASE_URLSame as SUPABASE_URL (used at build time)
NEXT_PUBLIC_SUPABASE_ANON_KEYSupabase anon key (read-only, public)
MY_RENOVATE_GITHUB_APP_IDGitHub App ID for Renovate
MY_RENOVATE_GITHUB_PRIVATE_KEYGitHub App private key for Renovate
MY_SLACK_BOT_TOKENSlack bot token for PR notifications
MY_SLACK_CHANNEL_IDSlack channel ID for PR notifications

The service_role key is used by the scan script to write data. The anon key is embedded in the static web app for read-only access (safe because RLS policies restrict it to SELECT only).

Local Development

Web application

mise install # install Node.js, Supabase CLI, fnox, etc.
mise run web:dev

Or manually:

cd web || exit
cp .env.example .env.local
# Edit .env.local with your Supabase credentials
npm install
npm run dev

Open http://localhost:3000.

Running a scan locally

Scan all images and print a CVE summary (no Supabase needed):

mise run scan

To also upload results to Supabase, use the scan:upload task:

export SUPABASE_URL="https://xxx.supabase.co"
export SUPABASE_SERVICE_ROLE_KEY="your-service-role-key"
mise run scan:upload

Pushing database migrations

mise run db:push

This requires SUPABASE_ACCESS_TOKEN, SUPABASE_PROJECT_REF, and SUPABASE_DB_PASSWORD in the environment (or provided automatically by the fnox-env plugin from AWS Parameter Store).

Available mise tasks

TaskDescription
web:installInstall web app dependencies
web:devStart Next.js development server
web:buildBuild static site export to web/out/
scanScan all container images, print CVE summary
scan:uploadScan all images and upload results to Supabase
db:pushLink Supabase project and push migrations
buildBuild the web app (alias for web:build)

The image list is defined in images.yml at the repository root.

GitHub Actions

Core workflows

Nightly Scans (.github/workflows/nightly-scans.yml)

  • Schedule: every day at 00:00 UTC
  • Runner: ubuntu-24.04-arm
  • Installs trivy and grype via mise
  • Pulls each container image, scans with both scanners
  • Uploads SARIF output and extracted CVEs to Supabase
  • Each scan record stores: image digest, scanner version, grype DB status, CVE count, full SARIF, and timestamp

Deploy Web (.github/workflows/deploy-web.yml)

  • Trigger: push to main touching web/**, or manual
  • Builds the Next.js static site
  • Deploys to GitHub Pages

Apply Schema (.github/workflows/apply-schema.yml)

  • Trigger: push to main touching supabase/migrations/**, or manual
  • Links the Supabase project and pushes pending migrations via mise run db:push

CI / quality workflows

WorkflowFileTrigger
MegaLintermega-linter.ymlPush to non-main branches
CodeQLcodeql.ymlPush to main, PRs, weekly
OSSF Scorecardsscorecards.ymlPush to main, weekly
Commit Checkcommit-check.ymlPRs to main
Semantic PR Titlesemantic-pull-request.ymlPRs (opened, edited, sync)

Automation workflows

WorkflowFileTrigger
Release Pleaserelease-please.ymlPush to main
Renovaterenovate.ymlPush to main, weekly
Stale Issues/PRsstale.ymlDaily at 09:09 UTC
PR Size Labelerpr-size-labeler.ymlPull requests
PR Slack Notificationpr-slack-notification.ymlPR events (open, review, close)

Project Structure

container-image-scans/
+-- .github/
|   +-- workflows/
|       +-- apply-schema.yml          # Push DB migrations via Supabase CLI
|       +-- codeql.yml                # CodeQL security analysis
|       +-- commit-check.yml          # Commit message validation
|       +-- deploy-web.yml            # Deploy dashboard to GH Pages
|       +-- mega-linter.yml           # Comprehensive linting + security
|       +-- nightly-scans.yml         # Nightly trivy + grype scans
|       +-- pr-size-labeler.yml       # Auto-label PRs by size
|       +-- pr-slack-notification.yml # Slack notifications for PRs
|       +-- release-please.yml        # Automated releases
|       +-- renovate.yml              # Dependency updates
|       +-- scorecards.yml            # OSSF Scorecard analysis
|       +-- semantic-pull-request.yml # PR title validation
|       +-- stale.yml                 # Close stale issues/PRs
+-- scripts/
|   +-- apply-schema.sh              # Link Supabase + push migrations
|   +-- scan-and-upload.sh           # Scan images & upload to Supabase
+-- supabase/
|   +-- migrations/
|   |   +-- 20250301000000_initial_schema.sql  # Tables, RLS, seed data
|   +-- config.toml                  # Supabase CLI configuration
|   +-- schema.sql                   # Reference schema (seed + DDL)
+-- web/                             # Next.js 15 dashboard application
|   +-- src/
|   |   +-- app/
|   |   |   +-- globals.css          # Global styles + CSS variables
|   |   |   +-- layout.tsx           # Root layout
|   |   |   +-- page.tsx             # Main page
|   |   +-- components/
|   |   |   +-- CveDetailTab.tsx     # CVE detail table with filters
|   |   |   +-- CveTooltip.tsx       # Hover tooltip with markdown
|   |   |   +-- HistoryCharts.tsx    # History line charts
|   |   +-- lib/
|   |       +-- supabase.ts          # Supabase client + data fetchers
|   +-- .env.example                 # Template for local env vars
|   +-- next.config.js
|   +-- package.json
|   +-- tsconfig.json
+-- .checkov.yml                     # Checkov skip-check config
+-- .gitignore
+-- .mega-linter.yml                 # MegaLinter configuration
+-- .mise.toml                       # mise tool versions + tasks
+-- .pre-commit-config.yaml          # pre-commit hooks (symlinked)
+-- .rumdl.toml                      # rumdl markdown linter config
+-- AGENTS.md                        # AI agent coding guidelines
+-- fnox.toml                        # Secrets via AWS Parameter Store
+-- images.yml                       # Container images to scan
+-- LICENSE
+-- lychee.toml                      # Link checker configuration
+-- README.md
+-- SECURITY.md                      # Security policy