Skip to content

Commit ea261b1

Browse files
authored
fix docker image parsing and purls (#413)
1 parent 2066e9b commit ea261b1

6 files changed

Lines changed: 61 additions & 14 deletions

File tree

models/purl.go

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package models
22

33
import (
4+
"errors"
45
"fmt"
56
"strings"
67

@@ -68,9 +69,56 @@ func (p *Purl) Link() string {
6869
return ""
6970
}
7071

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
7180
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
74122
}
75123

76124
func PurlFromGithubActions(uses string, sourceGitRepo string, sourceGitRef string) (Purl, error) {

models/purl_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,15 @@ func TestPurlFromGithubActions(t *testing.T) {
5353
},
5454
{
5555
uses: "docker://alpine:latest",
56-
expected: "pkg:docker/alpine:latest",
56+
expected: "pkg:docker/alpine@latest",
5757
},
5858
{
5959
uses: "docker://ghcr.io/org/owner/image:tag",
60-
expected: "pkg:docker/ghcr.io/org/owner/image:tag",
60+
expected: "pkg:docker/org/owner/image@tag?repository_url=ghcr.io",
6161
},
6262
{
6363
uses: "docker://ghcr.io/org/owner/image@sha256:digest",
64-
expected: "pkg:docker/ghcr.io/org/owner/image@sha256:digest",
64+
expected: "pkg:docker/org/owner/image@sha256:digest?repository_url=ghcr.io",
6565
},
6666
{
6767
uses: "",

opa/opa_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func TestOpaBuiltins(t *testing.T) {
4343
{
4444
builtin: "purl.parse_docker_image",
4545
input: `"alpine:latest"`,
46-
expected: "pkg:docker/alpine:latest",
46+
expected: "pkg:docker/alpine@latest",
4747
},
4848
}
4949

opa/rego/poutine/utils.rego

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ unpinned_github_action(purl) if {
1010

1111
unpinned_docker(purl) if {
1212
startswith(purl, "pkg:docker/")
13-
not contains(purl, "@")
1413
not regex.match("@sha256:[a-f0-9]{64}", purl)
1514
}
1615

scanner/inventory_scanner_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func TestRun(t *testing.T) {
8181

8282
assert.Contains(t, scannedPackage.BuildDependencies, "pkg:githubactions/actions/checkout@v4")
8383
assert.Contains(t, scannedPackage.PackageDependencies, "pkg:githubactions/actions/github-script@main")
84-
assert.Contains(t, scannedPackage.PackageDependencies, "pkg:docker/alpine:latest")
84+
assert.Contains(t, scannedPackage.PackageDependencies, "pkg:docker/alpine@latest")
8585
assert.Equal(t, 3, len(scannedPackage.GitlabciConfigs))
8686
}
8787

scanner/inventory_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,32 +27,32 @@ func TestPurls(t *testing.T) {
2727
require.NoError(t, err)
2828

2929
purls := []string{
30-
"pkg:docker/node:latest",
30+
"pkg:docker/node@latest",
3131
"pkg:githubactions/hashicorp/vault-action@v3",
3232
"pkg:githubactions/actions/checkout@main",
3333
"pkg:githubactions/kartverket/github-workflows@main#.github/workflows/run-terraform.yml",
3434
"pkg:githubactions/kartverket/github-workflows@v2.2#.github/workflows/run-terraform.yml",
3535
"pkg:githubactions/kartverket/github-workflows@v2.7.1#.github/workflows/run-terraform.yml",
36-
"pkg:docker/alpine:latest",
36+
"pkg:docker/alpine@latest",
3737
"pkg:githubactions/actions/github-script@main",
3838
"pkg:githubactions/actions/setup-node@v4",
3939
"pkg:githubactions/hashicorp/vault-action@v2.1.0",
4040
"pkg:githubactions/actions/checkout@v4",
41-
"pkg:docker/ruby:3.2",
42-
"pkg:docker/postgres:15",
41+
"pkg:docker/ruby@3.2",
42+
"pkg:docker/postgres@15",
4343
"pkg:gitlabci/include/template?file_name=Auto-DevOps.gitlab-ci.yml",
4444
"pkg:gitlabci/include/project?file_name=%2Ftemplates%2F.gitlab-ci-template.yml&project=my-group%2Fmy-project&ref=main",
4545
"pkg:gitlabci/include/remote?download_url=https%3A%2F%2Fexample.com%2F.gitlab-ci.yml",
4646
"pkg:gitlabci/include/component?project=my-org%2Fsecurity-components%2Fsecret-detection&ref=1.0&repository_url=gitlab.example.com",
4747
"pkg:githubactions/org/repo@main",
48-
"pkg:docker/debian:vuln",
48+
"pkg:docker/debian@vuln",
4949
"pkg:githubactions/bridgecrewio/checkov-action@main",
5050
"pkg:githubactions/org/repo@main#.github/workflows/Reusable.yml",
5151
"pkg:azurepipelinestask/DownloadPipelineArtifact@2",
5252
"pkg:azurepipelinestask/Cache@2",
5353
"pkg:githubactions/org/owner@main#.github/workflows/ci.yml",
5454
"pkg:githubactions/actions/checkout@v5",
55-
"pkg:docker/node:18",
55+
"pkg:docker/node@18",
5656
"pkg:githubactions/some/action@v1",
5757
}
5858
assert.ElementsMatch(t, i.Purls(*scannedPackage), purls)

0 commit comments

Comments
 (0)