Skip to content
Open
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
116 changes: 87 additions & 29 deletions sca/bom/buildinfo/technologies/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,28 @@ type dockerManifestList struct {
}

var (
jfrogSubdomainPattern = regexp.MustCompile(`^([a-zA-Z0-9]+)-([a-zA-Z0-9-]+)\.jfrog\.io$`)
ipAddressPattern = regexp.MustCompile(`^\d+\.`)
hexDigestPattern = regexp.MustCompile(`[a-fA-F0-9]{64}`)
hexDigestPattern = regexp.MustCompile(`[a-fA-F0-9]{64}`)
)

func ParseDockerImage(imageName string) (*DockerImageInfo, error) {
// getArtifactoryBaseDomain extracts the base domain from the configured Artifactory URL.
// e.g., "https://myinstance.jfrog.io/artifactory" -> "myinstance.jfrog.io"
// e.g., "https://artifactory.company.com:8081/artifactory" -> "artifactory.company.com"
func getArtifactoryBaseDomain(url string) string {
url = strings.TrimPrefix(url, "https://")
url = strings.TrimPrefix(url, "http://")
if idx := strings.Index(url, "/"); idx > 0 {
url = url[:idx]
}
if idx := strings.LastIndex(url, ":"); idx > 0 {
if !strings.Contains(url[idx:], "]") {
url = url[:idx]
}
}
return url
}

// ParseDockerImageWithArtifactoryUrl parses a Docker image name and extracts repo based on Artifactory URL.
func ParseDockerImageWithArtifactoryUrl(imageName, url string) (*DockerImageInfo, error) {
imageName = strings.TrimSpace(imageName)
info := &DockerImageInfo{Tag: "latest"}
if idx := strings.LastIndex(imageName, ":"); idx > 0 {
Expand All @@ -57,51 +73,94 @@ func ParseDockerImage(imageName string) (*DockerImageInfo, error) {
}

info.Registry = parts[0]
info.Repo, info.Image = parseRegistryAndExtract(info.Registry, parts[1])
remaining := parts[1]

info.Repo, info.Image = parseWithArtifactoryUrl(info.Registry, remaining, url)

log.Debug(fmt.Sprintf("Parsed Docker image - Registry: %s, Repo: %s, Image: %s, Tag: %s",
info.Registry, info.Repo, info.Image, info.Tag))

return info, nil
}

func parseRegistryAndExtract(registry string, remaining string) (repo, image string) {
image = remaining
// parseWithArtifactoryUrl determines Docker access method by comparing registry with Artifactory URL.
func parseWithArtifactoryUrl(registry, remaining, url string) (repo, image string) {
baseDomain := getArtifactoryBaseDomain(url)

// SaaS subdomain: <INSTANCE>-<REPO>.jfrog.io/image:tag (repo in subdomain, check first)
if matches := jfrogSubdomainPattern.FindStringSubmatch(registry); len(matches) > 2 {
repo = matches[2]
return
registryHost, registryPort := splitHostPort(registry)

isSaaS := strings.HasSuffix(baseDomain, ".jfrog.io") || strings.HasSuffix(baseDomain, ".jfrogdev.org")

if repo = extractSubdomainRepo(registryHost, baseDomain, isSaaS); repo != "" {
log.Debug(fmt.Sprintf("Subdomain method detected (repo=%s)", repo))
return repo, remaining
}

// Subdomain pattern: <REPO>.<DOMAIN>/image:tag (repo in subdomain, not IP, check first)
registryParts := strings.Split(registry, ".")
if len(registryParts) >= 3 && !strings.HasSuffix(registry, ".jfrog.io") && !ipAddressPattern.MatchString(registry) {
repo = registryParts[0]
return
if registryPort != "" && registryHost == baseDomain {
log.Debug(fmt.Sprintf("Port method detected (repo=%s)", registryPort))
return registryPort, remaining
}
if registryHost == baseDomain && registryPort == "" {
log.Debug("Repository Path method detected")
return extractRepoFromPath(remaining)
}
log.Debug("Fallback: using Repository Path extraction")
return extractRepoFromPath(remaining)
}

// Repository path: <REGISTRY>/<REPO>/image:tag (repo in path if contains /)
if strings.Contains(remaining, "/") {
repo, image, _ = strings.Cut(remaining, "/")
return
func splitHostPort(registry string) (host, port string) {
if idx := strings.LastIndex(registry, ":"); idx > 0 {
return registry[:idx], registry[idx+1:]
}
return registry, ""
}

// Port method: <INSTANCE>:<PORT>/image:tag (port IS the repo, single part only)
if strings.Contains(registry, ":") {
_, repo, _ = strings.Cut(registry, ":")
return
// extractSubdomainRepo extracts repo from subdomain based on platform type.
// SaaS: <instance>-<repo>.<domain> → repo is after hyphen
// Self-hosted: <repo>.<baseDomain> → repo is prepended subdomain
func extractSubdomainRepo(registryHost, baseDomain string, isSaaS bool) string {
if isSaaS {
// SaaS pattern: <instance>-<repo>.<domain>
baseParts := strings.SplitN(baseDomain, ".", 2)
if len(baseParts) != 2 {
return ""
}
instance, domainSuffix := baseParts[0], baseParts[1]
expectedSuffix := "." + domainSuffix

if strings.HasSuffix(registryHost, expectedSuffix) {
prefix := strings.TrimSuffix(registryHost, expectedSuffix)
if strings.HasPrefix(prefix, instance+"-") && prefix != instance {
return strings.TrimPrefix(prefix, instance+"-")
}
}
} else if strings.HasSuffix(registryHost, "."+baseDomain) {
// Self-hosted pattern: <repo>.<baseDomain>
return strings.TrimSuffix(registryHost, "."+baseDomain)
}
return ""
}

return "", ""
// extractRepoFromPath extracts repo as first segment if path contains "/"
func extractRepoFromPath(path string) (repo, image string) {
if strings.Contains(path, "/") {
repo, image, _ = strings.Cut(path, "/")
return
}
return "", path
}

func BuildDependencyTree(params technologies.BuildInfoBomGeneratorParams) ([]*xrayUtils.GraphNode, []string, error) {
if params.DockerImageName == "" {
return nil, nil, fmt.Errorf("docker image name is required")
}

imageInfo, err := ParseDockerImage(params.DockerImageName)
serverDetails, err := config.GetDefaultServerConf()
if err != nil {
return nil, nil, err
}

imageInfo, err := ParseDockerImageWithArtifactoryUrl(params.DockerImageName, serverDetails.Url)
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -194,12 +253,11 @@ func extractDigestFromBlockedMessage(output string) string {
}

func GetDockerRepositoryConfig(imageName string) (*project.RepositoryConfig, error) {
imageInfo, err := ParseDockerImage(imageName)
serverDetails, err := config.GetDefaultServerConf()
if err != nil {
return nil, err
}

serverDetails, err := config.GetDefaultServerConf()
imageInfo, err := ParseDockerImageWithArtifactoryUrl(imageName, serverDetails.Url)
if err != nil {
return nil, err
}
Expand Down
Loading
Loading