Skip to content
Merged
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
52 changes: 50 additions & 2 deletions models/purl.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package models

import (
"errors"
"fmt"
"strings"

Expand Down Expand Up @@ -68,9 +69,56 @@ func (p *Purl) Link() string {
return ""
}

// PurlFromDockerImage parses a Docker image reference and returns a valid
// Docker PURL per https://github.com/package-url/purl-spec/blob/main/types/docker-definition.json
//
// Examples:
//
// alpine:latest -> pkg:docker/alpine@latest
// ghcr.io/org/image:tag -> pkg:docker/org/image@tag?repository_url=ghcr.io
// myimage@sha256:abcdef -> pkg:docker/myimage@sha256%3Aabcdef
func PurlFromDockerImage(image string) (Purl, error) {
purl, err := packageurl.FromString("pkg:docker/" + image)
return Purl{PackageURL: purl}, err
if image == "" {
return Purl{}, errors.New("empty docker image reference")
}

var name, version string
var qualifiers packageurl.Qualifiers

// Split off version: either @digest or :tag
if idx := strings.Index(image, "@"); idx != -1 {
version = image[idx+1:]
image = image[:idx]
} else if idx := strings.LastIndex(image, ":"); idx != -1 {
// Ensure the colon is after the last slash (i.e. it's a tag, not a port/registry part)
if slashIdx := strings.LastIndex(image, "/"); idx > slashIdx {
version = image[idx+1:]
image = image[:idx]
}
}

// Split registry from the path.
// A registry is present if the first path component contains a dot or colon,
// or is "localhost" (standard Docker reference parsing heuristic).
parts := strings.SplitN(image, "/", 2)
if len(parts) == 2 && (strings.ContainsAny(parts[0], ".:") || parts[0] == "localhost") {
registry := parts[0]
qualifiers = packageurl.QualifiersFromMap(map[string]string{
"repository_url": registry,
})
image = parts[1]
}

// Split remaining path into namespace and name
if idx := strings.LastIndex(image, "/"); idx != -1 {
namespace := image[:idx]
name = image[idx+1:]
p := packageurl.NewPackageURL("docker", namespace, name, version, qualifiers, "")
return Purl{PackageURL: *p}, nil
}

p := packageurl.NewPackageURL("docker", "", image, version, qualifiers, "")
return Purl{PackageURL: *p}, nil
}

func PurlFromGithubActions(uses string, sourceGitRepo string, sourceGitRef string) (Purl, error) {
Expand Down
6 changes: 3 additions & 3 deletions models/purl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,15 @@ func TestPurlFromGithubActions(t *testing.T) {
},
{
uses: "docker://alpine:latest",
expected: "pkg:docker/alpine:latest",
expected: "pkg:docker/alpine@latest",
},
{
uses: "docker://ghcr.io/org/owner/image:tag",
expected: "pkg:docker/ghcr.io/org/owner/image:tag",
expected: "pkg:docker/org/owner/image@tag?repository_url=ghcr.io",
},
{
uses: "docker://ghcr.io/org/owner/image@sha256:digest",
expected: "pkg:docker/ghcr.io/org/owner/image@sha256:digest",
expected: "pkg:docker/org/owner/image@sha256:digest?repository_url=ghcr.io",
},
{
uses: "",
Expand Down
2 changes: 1 addition & 1 deletion opa/opa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func TestOpaBuiltins(t *testing.T) {
{
builtin: "purl.parse_docker_image",
input: `"alpine:latest"`,
expected: "pkg:docker/alpine:latest",
expected: "pkg:docker/alpine@latest",
},
}

Expand Down
1 change: 0 additions & 1 deletion opa/rego/poutine/utils.rego
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ unpinned_github_action(purl) if {

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

Expand Down
2 changes: 1 addition & 1 deletion scanner/inventory_scanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func TestRun(t *testing.T) {

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

Expand Down
12 changes: 6 additions & 6 deletions scanner/inventory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,32 +27,32 @@ func TestPurls(t *testing.T) {
require.NoError(t, err)

purls := []string{
"pkg:docker/node:latest",
"pkg:docker/node@latest",
"pkg:githubactions/hashicorp/vault-action@v3",
"pkg:githubactions/actions/checkout@main",
"pkg:githubactions/kartverket/github-workflows@main#.github/workflows/run-terraform.yml",
"pkg:githubactions/kartverket/github-workflows@v2.2#.github/workflows/run-terraform.yml",
"pkg:githubactions/kartverket/github-workflows@v2.7.1#.github/workflows/run-terraform.yml",
"pkg:docker/alpine:latest",
"pkg:docker/alpine@latest",
"pkg:githubactions/actions/github-script@main",
"pkg:githubactions/actions/setup-node@v4",
"pkg:githubactions/hashicorp/vault-action@v2.1.0",
"pkg:githubactions/actions/checkout@v4",
"pkg:docker/ruby:3.2",
"pkg:docker/postgres:15",
"pkg:docker/ruby@3.2",
"pkg:docker/postgres@15",
"pkg:gitlabci/include/template?file_name=Auto-DevOps.gitlab-ci.yml",
"pkg:gitlabci/include/project?file_name=%2Ftemplates%2F.gitlab-ci-template.yml&project=my-group%2Fmy-project&ref=main",
"pkg:gitlabci/include/remote?download_url=https%3A%2F%2Fexample.com%2F.gitlab-ci.yml",
"pkg:gitlabci/include/component?project=my-org%2Fsecurity-components%2Fsecret-detection&ref=1.0&repository_url=gitlab.example.com",
"pkg:githubactions/org/repo@main",
"pkg:docker/debian:vuln",
"pkg:docker/debian@vuln",
"pkg:githubactions/bridgecrewio/checkov-action@main",
"pkg:githubactions/org/repo@main#.github/workflows/Reusable.yml",
"pkg:azurepipelinestask/DownloadPipelineArtifact@2",
"pkg:azurepipelinestask/Cache@2",
"pkg:githubactions/org/owner@main#.github/workflows/ci.yml",
"pkg:githubactions/actions/checkout@v5",
"pkg:docker/node:18",
"pkg:docker/node@18",
"pkg:githubactions/some/action@v1",
}
assert.ElementsMatch(t, i.Purls(*scannedPackage), purls)
Expand Down
Loading