Repository navigation
Description
Summary
ImageReferenceParser produces different registry/repository pairs for spellings that all name the same Docker Hub image: nginx, docker.io/nginx, docker.io/library/nginx, index.docker.io/library/nginx and registry-1.docker.io/library/nginx. Because the image catalog compares registry and repository exactly, one image ends up as several catalog entries, so a shared base image is not recognized as shared, an own (discovered) image is not linked to the containers that run it, and Docker Scout refuses images whose registry is not spelled docker.io. Every Docker Hub reference must be normalized to one canonical form before it reaches the catalog.
Area
docs/areas/image-catalog.md(added in [Docs] Describe the current behavior by area #263), Image references; also affectsdocs/areas/base-images.md(shared base images),docs/areas/vulnerabilities.md(Docker Scout, Trivy target) anddocs/areas/registries.md.- Security area: Database and migrations (the fix needs a data correction of existing catalog rows; see Implementation notes).
Current behavior
src/DockerUpdateGuard/Images/ImageReferenceParser.cs:130-146:
if (segments.Length == 1)
{
registry = "docker.io";
repository = $"library/{segments[0].ToLowerInvariant()}";
}
else if (LooksLikeRegistry(segments[0]))
{
registry = segments[0].ToLowerInvariant();
repository = string.Join('/', segments.Skip(1)).ToLowerInvariant();
}| Input | Registry | Repository |
|---|---|---|
nginx |
docker.io |
library/nginx |
library/nginx |
docker.io |
library/nginx |
docker.io/nginx |
docker.io |
nginx |
index.docker.io/library/nginx |
index.docker.io |
library/nginx |
registry-1.docker.io/library/nginx |
registry-1.docker.io |
library/nginx |
INDEX.DOCKER.IO/Foo/Bar |
index.docker.io |
foo/bar |
- The library prefix is only added for a single-segment reference; the Docker Hub host aliases are kept as separate registries.
ImageCatalogRepository(src/DockerUpdateGuard.Data/Repositories/ImageCatalogRepository.cs:124-125) looks up a registry repository by exactRegistry == registry && Repository == repository, so each spelling creates its ownRegistryRepositories/ImageVersionsrows.- Registry access already treats the aliases alike (
src/DockerUpdateGuard/DockerHub/DockerHubClient.cs:358-363acceptsdocker.io,index.docker.io,registry-1.docker.io, …;SplitRepositoryat:371-381addslibraryfor a single segment), so tag lookups succeed and the duplicates go unnoticed. - Writers that parse references: runtime containers (
src/DockerUpdateGuard/Images/RuntimeContainerScanOrchestrator.cs:564-582), manual registration (src/DockerUpdateGuard/Images/ImageRegistrationService.cs:61-67), base-image labels (src/DockerUpdateGuard/Images/RegistryBaseImageResolver.cs:94, stored viaImageScanOrchestrator.cs:590), andOciRegistryClient.cs:971/DockerHubClient.cs:747for label parsing. Discovery writesdocker.io+namespace/namedirectly (DockerHubClient.cs:312-326), which is already canonical.
Consequences:
- Shared base images split: image A has the label
org.opencontainers.image.base.name=docker.io/nginx:1.27, image B hasnginx:1.27./base-imagesshows two entries (docker.io/nginx:1.27,docker.io/library/nginx:1.27), each with one observed image, instead of one entry used by two images (grouping key insrc/DockerUpdateGuard.Data/Queries/SharedBaseImageQueryService.cs:108-113is built from the stored registry/repository). - Own images not linked: discovery stores
docker.io/myuser/app; a container started fromindex.docker.io/myuser/app:1.0is stored under registryindex.docker.io.LoadOwnRepositoryKeysAsync(RuntimeContainerScanOrchestrator.cs:1155-1167) comparesregistry/repository, so the container is not recognized as running an own image (no "Own image …" derived-runtime summary, no link on/my-images). - Docker Scout unsupported:
src/DockerUpdateGuard/Vulnerabilities/DockerScoutVulnerabilityProvider.cs:389returns unsupported unless the registry equalsdocker.io, so an image version stored underindex.docker.iois never scanned by Docker Scout.TrivyVulnerabilityProvider.GetScanTarget(:183-187) likewise only strips the registry fordocker.io. - Duplicate vulnerability work: each catalog duplicate is a separate live image version and is scanned separately.
Reproduction: run two containers, nginx:1.27 and docker.io/nginx:1.27 (or index.docker.io/library/nginx:1.27), and run a runtime scan. The container list shows two different image references and two catalog entries for the same image.
Expected behavior
- Docker Hub references have exactly one canonical form: registry
docker.io, repository with thelibrary/namespace for official images (a repository without a namespace), everything lowercase except the tag (tags are case-sensitive and keep their case, as today). - The registry hosts
docker.io,index.docker.ioandregistry-1.docker.io(any case) are Docker Hub and are normalized todocker.io. (hub.docker.comanddocker.comare web hosts, not registry hosts in a reference; they are not added to the parser.) - After normalization, every spelling in the table above yields
docker.io/library/nginx(respectivelydocker.io/foo/bar), so all of them resolve to the same catalog entries. - The canonical text form stays
registry/repository:tag[@digest]; parsing the canonical form gives back the same parts (round-trip guarantee). - Mirror-wrapped references keep working:
docker.io/ghcr.io/owner/app:1→ghcr.io/owner/app:1, andindex.docker.io/ghcr.io/owner/app:1behaves the same. - References of other registries (including
localhost:5000/app,ghcr.io/owner/app) are unchanged. - Existing duplicate catalog entries are reconciled once so that tracked state (observed images, tag selections) points at the canonical entries (see Implementation notes).
Acceptance criteria
-
ImageReferenceParser.Parsereturns registrydocker.ioand repositorylibrary/nginxfornginx,library/nginx,docker.io/nginx,docker.io/library/nginx,index.docker.io/nginx,index.docker.io/library/nginx,registry-1.docker.io/library/nginxandDOCKER.IO/Nginx, with tag and digest unchanged. -
docker.io/myuser/app,index.docker.io/myuser/appandmyuser/appall yielddocker.io/myuser/app. - Mirror unwrapping works after an aliased Docker Hub host (
index.docker.io/ghcr.io/owner/app:1→ghcr.io/owner/app). - Non-Docker-Hub registries and
localhostreferences are unchanged; the tag keeps its case. -
Format(Parse(x))is stable: parsing the formatted canonical form gives the same parts. - Unit tests in
src/Tests/DockerUpdateGuard.Tests/ImageReferenceParserTests.cs, e.g.ImageReferenceParserParseDockerIoSingleSegmentAddsLibraryNamespace,ImageReferenceParserParseIndexDockerIoNormalizesRegistry,ImageReferenceParserParseRegistryOneDockerIoNormalizesRegistry,ImageReferenceParserParseWrappedRegistryBehindDockerHubAliasUnwraps,ImageReferenceParserFormatCanonicalReferenceRoundTrips(data-driven tests with[DataRow]are fine). - A test proves that a runtime container started from
index.docker.io/myuser/app:1is linked to the discovered imagedocker.io/myuser/app(RuntimeContainerScanOrchestratorTests). - A test proves that base images named
docker.io/nginx:1.27andnginx:1.27by two observed images produce one/base-imagesentry with an observed image count of 2 (SharedBaseImageQueryServiceTestsor an orchestrator test). - The one-time reconciliation of existing data is covered by tests (non-canonical observed image current version and tag selection are re-pointed to the canonical entry; running it twice changes nothing).
- The area document
docs/areas/image-catalog.mddescribes the new behavior and its Known gaps entry for this issue is removed;docs/areas/vulnerabilities.md/docs/areas/base-images.mdare updated where they describe the old behavior.
Implementation notes
- Parser: after splitting the segments, map a first segment equal (case-insensitive) to
docker.io,index.docker.ioorregistry-1.docker.iotodocker.io; when the registry isdocker.ioand the remaining repository has a single segment, prefixlibrary/. Do this beforeNormalizeWrappedRegistry(which only checksdocker.io), so unwrapping keeps working for the aliases. A smallIsDockerHubRegistryHosthelper in the parser keeps the alias list in one place;DockerHubClient.IsSupportedDockerHubHostcan stay as it is (it also accepts web hosts). ImageCatalogRepositorymust stay the single entry point for catalog writes and keeps comparing exactly (guarantee in.squad/project.md); normalization belongs in the parser, which every writer already uses. Discovery already writes canonical values.- Existing data: duplicate rows exist in installations that ran containers or registered images with a non-canonical spelling. Options:
- Full merge — re-point every foreign key (container snapshots, image relationships, update findings incl. recommended version, tag candidates, vulnerability findings, tag selections, observed images) to the canonical rows and delete the duplicates. Collides with several unique indexes (relationships per child/base/depth/type, active vulnerability finding per version/advisory/package, image version per repository/tag/digest) and rewrites history; high risk.
- Do nothing — new scans write canonical entries; but manual observed images never re-parse their reference (their current version never moves), so they would stay non-canonical forever, and tag selections keyed by the old registry repository would silently stop applying.
- Recommended: targeted reconciliation of tracked state. A one-time, idempotent step re-points only the rows that represent current tracked state:
ObservedImages.CurrentImageVersionId(get-or-create the canonical image version viaIImageCatalogRepository, copying publication time and metadata) andRuntimeContainerTagSelections.RegistryRepositoryId(drop the non-canonical selection if one for the same container already exists on the canonical repository). Historical rows (snapshots, findings, relationships, scan runs) stay on the old entries; they leave the live image version set with the next runtime and observed-image scans, the full vulnerability refresh deactivates their findings and retention removes old snapshots. Catalog entries are never deleted (unchanged rule).
Implement the step in C# (e.g. aCatalogCanonicalizationServicerun once at startup afterDatabaseMigrator.MigrateAsync, guarded so it is cheap when nothing is left to do) rather than as raw SQL in a migration, because it must use the parser's rules andIImageCatalogRepository, and it is unit-testable with SQLite/InMemory. If a schema change is needed after all, name the migration perAGENTS.md(next isUpdate8).
- Containers re-parse their reference on every runtime scan, and base-image relationships are rebuilt on the next observed-image scan, so no extra work is needed for them.
- Do not change: the tag's case, the digest normalization, the
@digesthandling, the integration clients'ExternalOperationResultcontract. - No configuration change; no README change.
Related
- F-046 (P7) —
ImageReferenceParseris practically untested with a single test case #10 (ImageReferenceParser untested) — the new tests cover part of it. - [Docs] Describe the current behavior by area #263 (area documents).
Generated by Claude Code
Activity