ReviewOS

stacks/buddy

Docker registry expansion (GHCR/ECR/GCR/private) + OSV coverage for Actions

#1386
Closed chris opened this 9 days ago ·
9 days ago

Part of #1367 (Phase 3 — Dependency Engine v2).

Problem

Docker support is Docker Hub only: src/utils/dockerfile-parser.ts:189-210 hardcodes registry.hub.docker.com v2 tags API (optional DOCKERHUB_TOKEN). No GHCR/ECR/GCR/Quay/private registries; the registries config (src/types.ts — npm/npmScopes/composer) has no docker entry. Also, OSV lookups skip both Docker and GitHub Actions entirely (toOsvEcosystem returns null for them, src/services/security-advisories.ts).

CodeRabbit ships Hadolint + Trivy for containers; Renovate supports every major registry. We detect "every container build file convention" (96b6fca) but can only resolve tags from one registry.

Spec

1. Generic OCI Distribution API client

src/registry/oci-client.ts implementing the standard flow that covers all compliant registries:

  1. Parse image ref → registry host, namespace, repo, tag (default host docker.io → registry-1.docker.io, library/ namespace inference)
  2. Anonymous/token auth: 401 → parse WWW-Authenticate → fetch bearer token from realm/service/scope → retry (this alone unlocks GHCR, Quay, most registries)
  3. GET /v2/{name}/tags/list with pagination (Link headers)
  4. Credentials per host via config + env:
registries.docker?: {
  [host: string]: {
    username?: string
    passwordEnv?: string    // env var name, never the secret itself
    tokenEnv?: string
  }
}

Also honor ~/.docker/config.json auth entries when present (base64 user:pass; NOT credential helpers in v1 — document).

2. Registry specifics

  • GHCR: bearer flow with GITHUB_TOKEN as password — works out of the box in Actions
  • ECR: token via GET /token with basic auth from env (AWS_* via aws ecr get-login-password when CLI present; else documented env var) — best-effort tier
  • GCR/Artifact Registry: bearer flow with oauth2accesstoken + token env
  • Tag semantics: reuse existing version-tag filtering (fetchLatestDockerImageVersion logic in dockerfile-parser.ts) — semver-ish tag detection, suffix preservation (-alpine, -slim), digest pinning support (update image@sha256:... by resolving the new tag's digest via HEAD /v2/{name}/manifests/{tag})

3. Security coverage extension

  • GitHub Actions: OSV supports GitHub Actions ecosystem — stop returning null; query advisories for action refs
  • Docker/base images: OSV has limited container data — instead surface (a) base-image EOL awareness (endoflife.date API for debian/alpine/node/etc. tags) and (b) a gate hook so #1382's dependency gate can block EOL'd base images

Tasks

  • oci-client.ts with the auth flow + pagination + fixtures (recorded 401/token/tags exchanges for docker.io, ghcr.io, quay.io)
  • Config registries.docker + validation + docs; .docker/config.json fallback
  • Digest-pinning update path (FROM image@sha256 + comment with tag)
  • Replace hardcoded Docker Hub path in dockerfile-parser.ts with the client
  • OSV: GitHub Actions ecosystem wiring + tests
  • EOL detection for common base images + dashboard/gate surfacing
  • docs/features/docker.md update (per-registry setup table)

Acceptance criteria

  • Fixture Dockerfiles referencing ghcr.io/... and quay.io/... images resolve latest tags and produce update PRs
  • FROM node:20@sha256:... updates both tag and digest
  • Vulnerable GitHub Action fixture gets an OSV advisory annotation
  • Docker Hub behavior unchanged (regression tests)

Dependencies

  • None hard; gate surfacing pairs with #1382, EOL dashboard section with #1370
9 days ago

Landed in f0470ec.

A generic OCI distribution client replaces the Docker Hub-specific API: anonymous request → bearer challenge → token exchange → retry. That one flow unlocks GHCR, Quay, GCR, Artifact Registry and self-hosted registries, since they all answer anonymously with a 401 naming where to get a token.

Credentials are per host, referenced by env var name. ghcr.io falls back to GITHUB_TOKEN, so private GHCR images work in Actions with no config.

Reference parsing follows Docker's real rules: the first segment is a host only if it has a dot/colon or is localhost (so alpine/git is a Hub org — treating it as a host would send credentials to a machine that does not exist), and the tag separator is the last colon after the last slash (so a registry port is not a tag).

Tag selection preserves what the author chose: variant is absolute (an -alpine image is never updated to Debian, which would break every later RUN apk add), and precision is preserved (18 gets 19, not 18.20.4). Pre-release markers are parsed separately from variants, since 2.0-rc1 is an earlier build of 2.0, not a different image.

Digest resolution is available for image@sha256:….

OSV now indexes GitHub Actions. An action runs with the workflow's credentials, so a compromised one is a repository compromise — the least optional ecosystem to have been skipping. Versions normalise v4 → 4, since the written form matches nothing.

Base-image EOL awareness (endoflife.date) was not implemented; opening a follow-up if it is still wanted.

9 days ago

The last leftover — base-image EOL awareness — landed in bc1a59f.

Container advisories are thin in OSV, so the useful question for a base image is not 'does it have a known CVE' but 'is anyone still shipping patches for it'. An image on an EOL cycle stops receiving fixes entirely, which is more consequential than any single advisory.

Products are mapped explicitly rather than guessed from the image name (node and nodejs are the same product under different names, and a wrong slug reports a support window belonging to something else). The most specific matching cycle wins — reporting 3 when 3.18 exists means reporting the wrong window. A cycle with no announced end is treated as supported indefinitely rather than unknown, since flagging it as a risk would be wrong.

Wired into the dependency gate as gates.dependencyGate.blockEol, so it can block a merge, and describeEol warns 90 days ahead.

Sign in to comment on this issue.