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
82 changes: 67 additions & 15 deletions internal/rego/oci/oci.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"fmt"
"io"
"path"
"regexp"
"runtime"
"strings"
"sync"
Expand Down Expand Up @@ -68,6 +69,10 @@ const (

var maxTarEntrySize int64 = maxTarEntrySizeConst // Use var to allow override in tests

// cosignTagPattern matches legacy cosign tag refs produced by ec.oci.image_tag_refs.
// Format: sha256-<hex>.(sbom|att|sig) — e.g. "sha256-d34db33f...abcd.sbom"
var cosignTagPattern = regexp.MustCompile(`:sha256-[0-9a-f]+\.(sbom|att|sig)$`)

func registerOCIBlob() {
decl := rego.Function{
Name: ociBlobName,
Expand Down Expand Up @@ -515,22 +520,70 @@ func ociBlobInternal(bctx rego.BuiltinContext, a *ast.Term, verifyDigest bool) (
}
logger.Debug("Starting blob retrieval")

ref, err := name.NewDigest(refStr)
if err != nil {
logger.WithFields(log.Fields{
"action": "new digest",
"error": err,
}).Error("failed to create new digest")
return nil, nil //nolint:nilerr // intentional: return nil to signal failure without OPA error
}
client := oci.NewClient(bctx.Context)

rawLayer, err := oci.NewClient(bctx.Context).Layer(ref)
var rawLayer v1.Layer
var expectedDigest string
ref, err := name.NewDigest(refStr)
if err != nil {
logger.WithFields(log.Fields{
"action": "fetch layer",
"error": err,
}).Error("failed to fetch OCI layer")
return nil, nil //nolint:nilerr
// Fall back to tag-based fetch: resolve the tag to an image and
// extract the first layer. This supports legacy cosign tag refs
// (e.g. .sbom, .att) returned by ec.oci.image_tag_refs.
if !cosignTagPattern.MatchString(refStr) {
logger.WithFields(log.Fields{
"action": "parse digest",
"error": err,
// Debug, not Error: this is the expected path for non-cosign refs, not a failure.
}).Debug("ref is not a digest ref and not a cosign tag artifact")
return nil, nil //nolint:nilerr
}
tagRef, tagErr := name.ParseReference(refStr)
if tagErr != nil {
logger.WithFields(log.Fields{
"action": "parse reference",
"error": tagErr,
}).Error("failed to parse reference")
return nil, nil //nolint:nilerr
}
img, imgErr := client.Image(tagRef)
if imgErr != nil {
logger.WithFields(log.Fields{
"action": "fetch image",
"error": imgErr,
}).Error("failed to fetch image for tag reference")
return nil, nil //nolint:nilerr
}
layers, layersErr := img.Layers()
if layersErr != nil || len(layers) == 0 {
logger.WithFields(log.Fields{
"action": "get layers",
"error": layersErr,
}).Error("failed to get layers from image")
return nil, nil //nolint:nilerr
}
rawLayer = layers[0]
layerDigest, digestErr := rawLayer.Digest()
if digestErr != nil {
logger.WithFields(log.Fields{
"action": "get layer digest",
"error": digestErr,
}).Error("failed to get layer digest")
return nil, nil //nolint:nilerr
}
// For tag refs, digest verification is intentionally weak: it only checks
// transport integrity (computed vs registry-reported digest), not a
// caller-specified expectation, since there is no digest in the ref.
expectedDigest = layerDigest.String()
} else {
expectedDigest = ref.DigestStr()
rawLayer, err = client.Layer(ref)
if err != nil {
logger.WithFields(log.Fields{
"action": "fetch layer",
"error": err,
}).Error("failed to fetch OCI layer")
return nil, nil //nolint:nilerr
}
}

layer, err := rawLayer.Uncompressed()
Expand Down Expand Up @@ -572,7 +625,6 @@ func ociBlobInternal(bctx rego.BuiltinContext, a *ast.Term, verifyDigest bool) (
// that's not as easy as it sounds, since it may require another
// io.Copy which could be inefficient. For now let's just skip it.
//
expectedDigest := ref.DigestStr()
if verifyDigest {
computedDigest := fmt.Sprintf("sha256:%x", hasher.Sum(nil))
if computedDigest != expectedDigest {
Expand Down
76 changes: 75 additions & 1 deletion internal/rego/oci/oci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/registry"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
v1fake "github.com/google/go-containerregistry/pkg/v1/fake"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/partial"
Expand Down Expand Up @@ -62,6 +63,10 @@ func TestOCIBlob(t *testing.T) {
uri *ast.Term
err bool
remoteErr error
tagRef bool // when true, mock Image() instead of Layer() for tag-based fetch
imageErr error
noLayers bool // when true with tagRef, mock Image() to return an image with no layers
layersErr error // when set with tagRef, mock Image() to return a fake image that errors on Layers()
}{
{
name: "success",
Expand Down Expand Up @@ -99,14 +104,83 @@ func TestOCIBlob(t *testing.T) {
uri: ast.StringTerm("registry.local/spam@sha256:4bbf56a3a9231f752d3b9c174637975f0f83ed2b15e65799837c571e4ef3374b"),
err: true,
},
{
name: "tag reference fetches first layer from image",
data: `{"bomFormat": "CycloneDX", "specVersion": "1.6"}`,
uri: ast.StringTerm("registry.local/spam:sha256-abc123def456.sbom"),
tagRef: true,
},
{
name: "tag reference with image fetch error",
data: `{"spam": "maps"}`,
uri: ast.StringTerm("registry.local/spam:sha256-abc123def456.sbom"),
tagRef: true,
imageErr: errors.New("image not found"),
err: true,
},
{
name: "tag reference with no layers",
data: `{"spam": "maps"}`,
uri: ast.StringTerm("registry.local/spam:sha256-abc123def456.sbom"),
tagRef: true,
noLayers: true,
err: true,
},
{
name: "tag reference layers error",
data: `{"spam": "maps"}`,
uri: ast.StringTerm("registry.local/spam:sha256-abc123def456.sbom"),
tagRef: true,
layersErr: errors.New("layers failed"),
err: true,
},
{
name: "non-cosign tag reference returns nil without image fetch",
data: `{"spam": "maps"}`,
uri: ast.StringTerm("registry.local/spam:latest"),
err: true,
},
{
name: "non-cosign tag with cosign suffix but wrong format",
data: `{"spam": "maps"}`,
uri: ast.StringTerm("registry.local/spam:release.sbom"),
err: true,
},
{
name: "tag reference fetches first layer from image with .att suffix",
data: `{"bomFormat": "CycloneDX", "specVersion": "1.6"}`,
uri: ast.StringTerm("registry.local/spam:sha256-abc123def456.att"),
tagRef: true,
},
{
name: "tag reference fetches first layer from image with .sig suffix",
data: `{"bomFormat": "CycloneDX", "specVersion": "1.6"}`,
uri: ast.StringTerm("registry.local/spam:sha256-abc123def456.sig"),
tagRef: true,
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
ClearCaches() // Clear cache before each subtest

client := fake.FakeClient{}
if c.remoteErr != nil {
if c.tagRef {
if c.imageErr != nil {
client.On("Image", mock.Anything).Return(nil, c.imageErr)
} else if c.noLayers {
client.On("Image", mock.Anything).Return(empty.Image, nil)
} else if c.layersErr != nil {
fakeImg := &v1fake.FakeImage{}
fakeImg.LayersReturns(nil, c.layersErr)
client.On("Image", mock.Anything).Return(fakeImg, nil)
} else {
layer := static.NewLayer([]byte(c.data), types.OCIUncompressedLayer)
img, imgErr := mutate.AppendLayers(empty.Image, layer)
require.NoError(t, imgErr)
client.On("Image", mock.Anything).Return(img, nil)
}
} else if c.remoteErr != nil {
client.On("Layer", mock.Anything, mock.Anything).Return(nil, c.remoteErr)
} else {
layer := static.NewLayer([]byte(c.data), types.OCIUncompressedLayer)
Expand Down
Loading