|
1 | 1 | package models |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "errors" |
4 | 5 | "fmt" |
5 | 6 | "strings" |
6 | 7 |
|
@@ -68,9 +69,56 @@ func (p *Purl) Link() string { |
68 | 69 | return "" |
69 | 70 | } |
70 | 71 |
|
| 72 | +// PurlFromDockerImage parses a Docker image reference and returns a valid |
| 73 | +// Docker PURL per https://github.com/package-url/purl-spec/blob/main/types/docker-definition.json |
| 74 | +// |
| 75 | +// Examples: |
| 76 | +// |
| 77 | +// alpine:latest -> pkg:docker/alpine@latest |
| 78 | +// ghcr.io/org/image:tag -> pkg:docker/org/image@tag?repository_url=ghcr.io |
| 79 | +// myimage@sha256:abcdef -> pkg:docker/myimage@sha256%3Aabcdef |
71 | 80 | func PurlFromDockerImage(image string) (Purl, error) { |
72 | | - purl, err := packageurl.FromString("pkg:docker/" + image) |
73 | | - return Purl{PackageURL: purl}, err |
| 81 | + if image == "" { |
| 82 | + return Purl{}, errors.New("empty docker image reference") |
| 83 | + } |
| 84 | + |
| 85 | + var name, version string |
| 86 | + var qualifiers packageurl.Qualifiers |
| 87 | + |
| 88 | + // Split off version: either @digest or :tag |
| 89 | + if idx := strings.Index(image, "@"); idx != -1 { |
| 90 | + version = image[idx+1:] |
| 91 | + image = image[:idx] |
| 92 | + } else if idx := strings.LastIndex(image, ":"); idx != -1 { |
| 93 | + // Ensure the colon is after the last slash (i.e. it's a tag, not a port/registry part) |
| 94 | + if slashIdx := strings.LastIndex(image, "/"); idx > slashIdx { |
| 95 | + version = image[idx+1:] |
| 96 | + image = image[:idx] |
| 97 | + } |
| 98 | + } |
| 99 | + |
| 100 | + // Split registry from the path. |
| 101 | + // A registry is present if the first path component contains a dot or colon, |
| 102 | + // or is "localhost" (standard Docker reference parsing heuristic). |
| 103 | + parts := strings.SplitN(image, "/", 2) |
| 104 | + if len(parts) == 2 && (strings.ContainsAny(parts[0], ".:") || parts[0] == "localhost") { |
| 105 | + registry := parts[0] |
| 106 | + qualifiers = packageurl.QualifiersFromMap(map[string]string{ |
| 107 | + "repository_url": registry, |
| 108 | + }) |
| 109 | + image = parts[1] |
| 110 | + } |
| 111 | + |
| 112 | + // Split remaining path into namespace and name |
| 113 | + if idx := strings.LastIndex(image, "/"); idx != -1 { |
| 114 | + namespace := image[:idx] |
| 115 | + name = image[idx+1:] |
| 116 | + p := packageurl.NewPackageURL("docker", namespace, name, version, qualifiers, "") |
| 117 | + return Purl{PackageURL: *p}, nil |
| 118 | + } |
| 119 | + |
| 120 | + p := packageurl.NewPackageURL("docker", "", image, version, qualifiers, "") |
| 121 | + return Purl{PackageURL: *p}, nil |
74 | 122 | } |
75 | 123 |
|
76 | 124 | func PurlFromGithubActions(uses string, sourceGitRepo string, sourceGitRef string) (Purl, error) { |
|
0 commit comments