Skip to content

Commit ad82add

Browse files
st3pentaclaude
andcommitted
Support tag-based refs in ec.oci.blob
The ec.oci.blob builtin only accepted digest refs (repo@sha256:...) via name.NewDigest(). Tag refs returned by ec.oci.image_tag_refs (e.g. repo:sha256-abc.sbom) were rejected, causing errors during legacy cosign SBOM discovery. Add a fallback for cosign tag suffixes (.sbom, .att, .sig) that resolves the tag to an image and extracts the first layer. Preserve the original digest-based verification for digest refs and use layer-reported digest for tag refs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bf7cd76 commit ad82add

2 files changed

Lines changed: 131 additions & 16 deletions

File tree

internal/rego/oci/oci.go

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -515,22 +515,70 @@ func ociBlobInternal(bctx rego.BuiltinContext, a *ast.Term, verifyDigest bool) (
515515
}
516516
logger.Debug("Starting blob retrieval")
517517

518-
ref, err := name.NewDigest(refStr)
519-
if err != nil {
520-
logger.WithFields(log.Fields{
521-
"action": "new digest",
522-
"error": err,
523-
}).Error("failed to create new digest")
524-
return nil, nil //nolint:nilerr // intentional: return nil to signal failure without OPA error
525-
}
518+
client := oci.NewClient(bctx.Context)
526519

527-
rawLayer, err := oci.NewClient(bctx.Context).Layer(ref)
520+
var rawLayer v1.Layer
521+
var expectedDigest string
522+
ref, err := name.NewDigest(refStr)
528523
if err != nil {
529-
logger.WithFields(log.Fields{
530-
"action": "fetch layer",
531-
"error": err,
532-
}).Error("failed to fetch OCI layer")
533-
return nil, nil //nolint:nilerr
524+
// Fall back to tag-based fetch: resolve the tag to an image and
525+
// extract the first layer. This supports legacy cosign tag refs
526+
// (e.g. .sbom, .att) returned by ec.oci.image_tag_refs.
527+
if !strings.HasSuffix(refStr, ".sbom") && !strings.HasSuffix(refStr, ".att") && !strings.HasSuffix(refStr, ".sig") {
528+
logger.WithFields(log.Fields{
529+
"action": "parse digest",
530+
"error": err,
531+
// Debug, not Error: this is the expected path for non-cosign refs, not a failure.
532+
}).Debug("ref is not a digest ref and not a cosign tag artifact")
533+
return nil, nil //nolint:nilerr
534+
}
535+
tagRef, tagErr := name.ParseReference(refStr)
536+
if tagErr != nil {
537+
logger.WithFields(log.Fields{
538+
"action": "parse reference",
539+
"error": tagErr,
540+
}).Error("failed to parse reference")
541+
return nil, nil //nolint:nilerr
542+
}
543+
img, imgErr := client.Image(tagRef)
544+
if imgErr != nil {
545+
logger.WithFields(log.Fields{
546+
"action": "fetch image",
547+
"error": imgErr,
548+
}).Error("failed to fetch image for tag reference")
549+
return nil, nil //nolint:nilerr
550+
}
551+
layers, layersErr := img.Layers()
552+
if layersErr != nil || len(layers) == 0 {
553+
logger.WithFields(log.Fields{
554+
"action": "get layers",
555+
"error": layersErr,
556+
}).Error("failed to get layers from image")
557+
return nil, nil //nolint:nilerr
558+
}
559+
rawLayer = layers[0]
560+
layerDigest, digestErr := rawLayer.Digest()
561+
if digestErr != nil {
562+
logger.WithFields(log.Fields{
563+
"action": "get layer digest",
564+
"error": digestErr,
565+
}).Error("failed to get layer digest")
566+
return nil, nil //nolint:nilerr
567+
}
568+
// For tag refs, digest verification is intentionally weak: it only checks
569+
// transport integrity (computed vs registry-reported digest), not a
570+
// caller-specified expectation, since there is no digest in the ref.
571+
expectedDigest = layerDigest.String()
572+
} else {
573+
expectedDigest = ref.DigestStr()
574+
rawLayer, err = client.Layer(ref)
575+
if err != nil {
576+
logger.WithFields(log.Fields{
577+
"action": "fetch layer",
578+
"error": err,
579+
}).Error("failed to fetch OCI layer")
580+
return nil, nil //nolint:nilerr
581+
}
534582
}
535583

536584
layer, err := rawLayer.Uncompressed()
@@ -572,7 +620,6 @@ func ociBlobInternal(bctx rego.BuiltinContext, a *ast.Term, verifyDigest bool) (
572620
// that's not as easy as it sounds, since it may require another
573621
// io.Copy which could be inefficient. For now let's just skip it.
574622
//
575-
expectedDigest := ref.DigestStr()
576623
if verifyDigest {
577624
computedDigest := fmt.Sprintf("sha256:%x", hasher.Sum(nil))
578625
if computedDigest != expectedDigest {

internal/rego/oci/oci_test.go

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"github.com/google/go-containerregistry/pkg/name"
3636
"github.com/google/go-containerregistry/pkg/registry"
3737
v1 "github.com/google/go-containerregistry/pkg/v1"
38+
"github.com/google/go-containerregistry/pkg/v1/empty"
3839
v1fake "github.com/google/go-containerregistry/pkg/v1/fake"
3940
"github.com/google/go-containerregistry/pkg/v1/mutate"
4041
"github.com/google/go-containerregistry/pkg/v1/partial"
@@ -62,6 +63,10 @@ func TestOCIBlob(t *testing.T) {
6263
uri *ast.Term
6364
err bool
6465
remoteErr error
66+
tagRef bool // when true, mock Image() instead of Layer() for tag-based fetch
67+
imageErr error
68+
noLayers bool // when true with tagRef, mock Image() to return an image with no layers
69+
layersErr error // when set with tagRef, mock Image() to return a fake image that errors on Layers()
6570
}{
6671
{
6772
name: "success",
@@ -99,14 +104,77 @@ func TestOCIBlob(t *testing.T) {
99104
uri: ast.StringTerm("registry.local/spam@sha256:4bbf56a3a9231f752d3b9c174637975f0f83ed2b15e65799837c571e4ef3374b"),
100105
err: true,
101106
},
107+
{
108+
name: "tag reference fetches first layer from image",
109+
data: `{"bomFormat": "CycloneDX", "specVersion": "1.6"}`,
110+
uri: ast.StringTerm("registry.local/spam:sha256-abc123def456.sbom"),
111+
tagRef: true,
112+
},
113+
{
114+
name: "tag reference with image fetch error",
115+
data: `{"spam": "maps"}`,
116+
uri: ast.StringTerm("registry.local/spam:sha256-abc123def456.sbom"),
117+
tagRef: true,
118+
imageErr: errors.New("image not found"),
119+
err: true,
120+
},
121+
{
122+
name: "tag reference with no layers",
123+
data: `{"spam": "maps"}`,
124+
uri: ast.StringTerm("registry.local/spam:sha256-abc123def456.sbom"),
125+
tagRef: true,
126+
noLayers: true,
127+
err: true,
128+
},
129+
{
130+
name: "tag reference layers error",
131+
data: `{"spam": "maps"}`,
132+
uri: ast.StringTerm("registry.local/spam:sha256-abc123def456.sbom"),
133+
tagRef: true,
134+
layersErr: errors.New("layers failed"),
135+
err: true,
136+
},
137+
{
138+
name: "non-cosign tag reference returns nil without image fetch",
139+
data: `{"spam": "maps"}`,
140+
uri: ast.StringTerm("registry.local/spam:latest"),
141+
err: true,
142+
},
143+
{
144+
name: "tag reference fetches first layer from image with .att suffix",
145+
data: `{"bomFormat": "CycloneDX", "specVersion": "1.6"}`,
146+
uri: ast.StringTerm("registry.local/spam:sha256-abc123def456.att"),
147+
tagRef: true,
148+
},
149+
{
150+
name: "tag reference fetches first layer from image with .sig suffix",
151+
data: `{"bomFormat": "CycloneDX", "specVersion": "1.6"}`,
152+
uri: ast.StringTerm("registry.local/spam:sha256-abc123def456.sig"),
153+
tagRef: true,
154+
},
102155
}
103156

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

108161
client := fake.FakeClient{}
109-
if c.remoteErr != nil {
162+
if c.tagRef {
163+
if c.imageErr != nil {
164+
client.On("Image", mock.Anything).Return(nil, c.imageErr)
165+
} else if c.noLayers {
166+
client.On("Image", mock.Anything).Return(empty.Image, nil)
167+
} else if c.layersErr != nil {
168+
fakeImg := &v1fake.FakeImage{}
169+
fakeImg.LayersReturns(nil, c.layersErr)
170+
client.On("Image", mock.Anything).Return(fakeImg, nil)
171+
} else {
172+
layer := static.NewLayer([]byte(c.data), types.OCIUncompressedLayer)
173+
img, imgErr := mutate.AppendLayers(empty.Image, layer)
174+
require.NoError(t, imgErr)
175+
client.On("Image", mock.Anything).Return(img, nil)
176+
}
177+
} else if c.remoteErr != nil {
110178
client.On("Layer", mock.Anything, mock.Anything).Return(nil, c.remoteErr)
111179
} else {
112180
layer := static.NewLayer([]byte(c.data), types.OCIUncompressedLayer)

0 commit comments

Comments
 (0)