Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pkg/infrastructure/scanner/analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ func TestExtractRegistryAndRepo(t *testing.T) {
{"Docker Hub", "docker.io/library/python:3.12-slim", "docker.io", "library/python", "3.12-slim"},
{"Short MCR", "azurelinux/base/python:3.12", "mcr.microsoft.com", "azurelinux/base/python", "3.12"},
{"No tag", "mcr.microsoft.com/azurelinux/base/python", "mcr.microsoft.com", "azurelinux/base/python", ""},
{"Tag with digest", "mcr.microsoft.com/dotnet/aspnet:8.0@sha256:abcdef", "mcr.microsoft.com", "dotnet/aspnet", "8.0"},
{"Digest only", "mcr.microsoft.com/dotnet/aspnet@sha256:abcdef", "mcr.microsoft.com", "dotnet/aspnet", ""},
{"Registry with port", "localhost:5000/myrepo:1.0", "localhost:5000", "myrepo", "1.0"},
{"Localhost no port", "localhost/myrepo:1.0", "localhost", "myrepo", "1.0"},
{"Nested path with digest", "mcr.microsoft.com/azurelinux/base/python:3.12-nonroot@sha256:deadbeef", "mcr.microsoft.com", "azurelinux/base/python", "3.12-nonroot"},
}

for _, tt := range tests {
Expand Down
34 changes: 26 additions & 8 deletions pkg/infrastructure/scanner/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,24 +296,42 @@ func BuildFullImageName(defaultRegistry, repo, tag string) string {
}

// ExtractRegistryAndRepo splits a full image name into registry, repository, and tag.
// Digest suffixes (@sha256:...) are stripped before parsing. Tags are taken from the
// last ":" after the last "/", so host:port registries (e.g. localhost:5000) work.
func ExtractRegistryAndRepo(imageName string) (registry, repository, tag string) {
// Split off the tag
parts := strings.SplitN(imageName, ":", 2)
nameWithoutTag := parts[0]
if len(parts) == 2 {
tag = parts[1]
name := imageName

// Strip @digest first — digests contain ":" (e.g. sha256:abc) and must not
// be mistaken for tags.
if at := strings.Index(name, "@"); at >= 0 {
name = name[:at]
}

// Tag separator is the last ":" after the last "/".
lastSlash := strings.LastIndex(name, "/")
lastColon := strings.LastIndex(name, ":")
nameWithoutTag := name
if lastColon > lastSlash {
tag = name[lastColon+1:]
nameWithoutTag = name[:lastColon]
}

// Split into registry and repository
// Registry is the first path segment when it looks like a host.
segments := strings.SplitN(nameWithoutTag, "/", 2)
if len(segments) == 2 && strings.Contains(segments[0], ".") {
if len(segments) == 2 && looksLikeRegistryHost(segments[0]) {
registry = segments[0]
repository = segments[1]
} else {
// Default to MCR
// Default to MCR for short names like "azurelinux/base/python:3.12"
registry = "mcr.microsoft.com"
repository = nameWithoutTag
}

return registry, repository, tag
}

// looksLikeRegistryHost reports whether s is a registry host (domain, host:port,
// or localhost) rather than a repository path segment.
func looksLikeRegistryHost(s string) bool {
return strings.Contains(s, ".") || strings.Contains(s, ":") || strings.EqualFold(s, "localhost")
}