Skip to content

Commit 4c46dc7

Browse files
Add reference attestation for multiple equivalent images (#2467)
Signed-off-by: robert-cronin <[email protected]>
1 parent afed0ef commit 4c46dc7

File tree

8 files changed

+469
-5
lines changed

8 files changed

+469
-5
lines changed

internal/testing/testdata/models.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,3 +608,81 @@ var ITE6EOLPython = []byte(`{
608608
}
609609
}
610610
}`)
611+
612+
// ITE6ReferenceSingle is a test document for the Reference ingestor with a single reference
613+
var ITE6ReferenceSingle = []byte(`{
614+
"type": "https://in-toto.io/Statement/v1",
615+
"subject": [
616+
{
617+
"uri": "pkg:npm/[email protected]"
618+
}
619+
],
620+
"predicateType": "https://in-toto.io/attestation/reference/v0.1",
621+
"predicate": {
622+
"attester": {
623+
"id": "attester-123"
624+
},
625+
"references": [
626+
{
627+
"downloadLocation": "https://example.com/downloads/pkg.tar.gz",
628+
"digest": {
629+
"sha256": "abcd1234..."
630+
},
631+
"mediaType": "application/x-tar"
632+
}
633+
]
634+
}
635+
}`)
636+
637+
// ITE6ReferenceMultiple is a test document for the Reference ingestor with multiple references
638+
var ITE6ReferenceMultiple = []byte(`{
639+
"type": "https://in-toto.io/Statement/v1",
640+
"subject": [
641+
{
642+
"uri": "pkg:pypi/[email protected]"
643+
}
644+
],
645+
"predicateType": "https://in-toto.io/attestation/reference/v0.1",
646+
"predicate": {
647+
"attester": {
648+
"id": "attester-xyz"
649+
},
650+
"references": [
651+
{
652+
"downloadLocation": "https://example.com/artifacts/python-ref1.tgz",
653+
"digest": {
654+
"sha256": "aa1111111111111111111111111111111111111111111111111111111111111111"
655+
},
656+
"mediaType": "application/octet-stream"
657+
},
658+
{
659+
"downloadLocation": "https://example.com/artifacts/python-ref2.whl",
660+
"digest": {
661+
"sha256": "bb2222222222222222222222222222222222222222222222222222222222222222"
662+
},
663+
"mediaType": "application/zip"
664+
}
665+
]
666+
}
667+
}`)
668+
669+
// ITE6ReferenceNoSubject is a test document for the Reference ingestor with no subject provided
670+
var ITE6ReferenceNoSubject = []byte(`{
671+
"type": "https://in-toto.io/Statement/v1",
672+
"subject": [],
673+
"predicateType": "https://in-toto.io/attestation/reference/v0.1",
674+
"predicate": {
675+
"attester": {
676+
"id": "attester-nobody"
677+
},
678+
"references": [
679+
{
680+
"downloadLocation": "https://example.com/artifacts/no-subject.tgz",
681+
"digest": {
682+
"sha256": "no-subject-digest"
683+
},
684+
"mediaType": "application/octet-stream"
685+
}
686+
]
687+
}
688+
}`)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//
2+
// Copyright 2025 The GUAC Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package attestation
17+
18+
import (
19+
attestationv1 "github.com/in-toto/attestation/go/v1"
20+
)
21+
22+
const (
23+
PredicateReference = "https://in-toto.io/attestation/reference/v0.1"
24+
)
25+
26+
// ReferenceStatement defines the statement header and the Reference predicate
27+
type ReferenceStatement struct {
28+
attestationv1.Statement
29+
// Predicate contains type specific metadata.
30+
Predicate ReferencePredicate `json:"predicate"`
31+
}
32+
33+
// ReferencePredicate defines predicate definition of the Reference attestation
34+
type ReferencePredicate struct {
35+
Attester ReferenceAttester `json:"attester"`
36+
References []ReferenceItem `json:"references"`
37+
}
38+
39+
// ReferenceAttester defines the attester information
40+
type ReferenceAttester struct {
41+
ID string `json:"id"`
42+
}
43+
44+
// ReferenceItem represents an individual reference in the predicate
45+
type ReferenceItem struct {
46+
DownloadLocation string `json:"downloadLocation"`
47+
Digest ReferenceDigestItem `json:"digest"`
48+
MediaType string `json:"mediaType"`
49+
}
50+
51+
// ReferenceDigestItem represents an individual digest in the predicate
52+
type ReferenceDigestItem struct {
53+
SHA256 string `json:"sha256"`
54+
}

pkg/handler/processor/ite6/ite6.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ func (e *ITE6Processor) ValidateSchema(i *processor.Document) error {
3535
i.Type != processor.DocumentITE6SLSA &&
3636
i.Type != processor.DocumentITE6Vul &&
3737
i.Type != processor.DocumentITE6ClearlyDefined &&
38-
i.Type != processor.DocumentITE6EOL {
38+
i.Type != processor.DocumentITE6EOL &&
39+
i.Type != processor.DocumentITE6Reference {
3940
return fmt.Errorf("expected ITE6 document type, actual document type: %v", i.Type)
4041
}
4142

pkg/handler/processor/process/process.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ func init() {
6060
_ = RegisterDocumentProcessor(&ite6.ITE6Processor{}, processor.DocumentITE6Vul)
6161
_ = RegisterDocumentProcessor(&ite6.ITE6Processor{}, processor.DocumentITE6ClearlyDefined)
6262
_ = RegisterDocumentProcessor(&ite6.ITE6Processor{}, processor.DocumentITE6EOL)
63+
_ = RegisterDocumentProcessor(&ite6.ITE6Processor{}, processor.DocumentITE6Reference)
6364
_ = RegisterDocumentProcessor(&dsse.DSSEProcessor{}, processor.DocumentDSSE)
6465
_ = RegisterDocumentProcessor(&spdx.SPDXProcessor{}, processor.DocumentSPDX)
6566
_ = RegisterDocumentProcessor(&csaf.CSAFProcessor{}, processor.DocumentCsaf)

pkg/handler/processor/processor.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,11 @@ type DocumentType string
5656

5757
// Document* is the enumerables of DocumentType
5858
const (
59-
DocumentITE6SLSA DocumentType = "SLSA"
60-
DocumentITE6Generic DocumentType = "ITE6"
61-
DocumentITE6Vul DocumentType = "ITE6VUL"
62-
DocumentITE6EOL DocumentType = "ITE6EOL"
59+
DocumentITE6SLSA DocumentType = "SLSA"
60+
DocumentITE6Generic DocumentType = "ITE6"
61+
DocumentITE6Vul DocumentType = "ITE6VUL"
62+
DocumentITE6EOL DocumentType = "ITE6EOL"
63+
DocumentITE6Reference DocumentType = "ITE6REF"
6364
// ClearlyDefined
6465
DocumentITE6ClearlyDefined DocumentType = "ITE6CD"
6566
DocumentDSSE DocumentType = "DSSE"

pkg/ingestor/parser/parser.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
"github.com/guacsec/guac/pkg/ingestor/parser/eol"
3333
"github.com/guacsec/guac/pkg/ingestor/parser/opaque"
3434
"github.com/guacsec/guac/pkg/ingestor/parser/open_vex"
35+
"github.com/guacsec/guac/pkg/ingestor/parser/reference"
3536
"github.com/guacsec/guac/pkg/ingestor/parser/scorecard"
3637
"github.com/guacsec/guac/pkg/ingestor/parser/slsa"
3738
"github.com/guacsec/guac/pkg/ingestor/parser/spdx"
@@ -50,6 +51,7 @@ func init() {
5051
_ = RegisterDocumentParser(csaf.NewCsafParser, processor.DocumentCsaf)
5152
_ = RegisterDocumentParser(open_vex.NewOpenVEXParser, processor.DocumentOpenVEX)
5253
_ = RegisterDocumentParser(eol.NewEOLCertificationParser, processor.DocumentITE6EOL)
54+
_ = RegisterDocumentParser(reference.NewReferenceParser, processor.DocumentITE6Reference)
5355
_ = RegisterDocumentParser(opaque.NewOpaqueParser, processor.DocumentOpaque)
5456
}
5557

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
//
2+
// Copyright 2025 The GUAC Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package reference
17+
18+
import (
19+
"context"
20+
"fmt"
21+
"time"
22+
23+
jsoniter "github.com/json-iterator/go"
24+
25+
"github.com/guacsec/guac/pkg/assembler"
26+
"github.com/guacsec/guac/pkg/assembler/clients/generated"
27+
"github.com/guacsec/guac/pkg/assembler/helpers"
28+
attestation "github.com/guacsec/guac/pkg/certifier/attestation/reference"
29+
"github.com/guacsec/guac/pkg/handler/processor"
30+
"github.com/guacsec/guac/pkg/ingestor/parser/common"
31+
"github.com/guacsec/guac/pkg/logging"
32+
)
33+
34+
var json = jsoniter.ConfigCompatibleWithStandardLibrary
35+
36+
const (
37+
justification = "Retrieved from reference predicate"
38+
)
39+
40+
type parser struct {
41+
doc *processor.Document
42+
pkg *generated.PkgInputSpec
43+
collectedReference []assembler.IsOccurrenceIngest
44+
identifierStrings *common.IdentifierStrings
45+
timeScanned time.Time
46+
}
47+
48+
// newReferenceParser initializes the parser
49+
func NewReferenceParser() common.DocumentParser {
50+
return &parser{
51+
identifierStrings: &common.IdentifierStrings{},
52+
}
53+
}
54+
55+
// initializeReferenceParser clears out all values for the next iteration
56+
func (r *parser) initializeReferenceParser() {
57+
r.doc = nil
58+
r.pkg = nil
59+
r.collectedReference = make([]assembler.IsOccurrenceIngest, 0)
60+
r.identifierStrings = &common.IdentifierStrings{}
61+
r.timeScanned = time.Now()
62+
}
63+
64+
// Parse breaks out the document into the graph components
65+
func (r *parser) Parse(ctx context.Context, doc *processor.Document) error {
66+
logger := logging.FromContext(ctx)
67+
r.initializeReferenceParser()
68+
r.doc = doc
69+
70+
statement, err := parseReferenceStatement(doc.Blob)
71+
if err != nil {
72+
return fmt.Errorf("failed to parse reference predicate: %w", err)
73+
}
74+
75+
r.timeScanned = time.Now()
76+
77+
if err := r.parseSubject(statement); err != nil {
78+
logger.Warnf("unable to parse subject of statement: %v", err)
79+
return fmt.Errorf("unable to parse subject of statement: %w", err)
80+
}
81+
82+
if err := r.parseReferences(ctx, statement); err != nil {
83+
logger.Warnf("unable to parse reference statement: %v", err)
84+
return fmt.Errorf("unable to parse reference statement: %w", err)
85+
}
86+
87+
return nil
88+
}
89+
90+
func parseReferenceStatement(p []byte) (*attestation.ReferenceStatement, error) {
91+
statement := attestation.ReferenceStatement{}
92+
if err := json.Unmarshal(p, &statement); err != nil {
93+
return nil, fmt.Errorf("failed to unmarshal reference predicate: %w", err)
94+
}
95+
return &statement, nil
96+
}
97+
98+
func (r *parser) parseSubject(s *attestation.ReferenceStatement) error {
99+
if len(s.Statement.Subject) == 0 {
100+
return fmt.Errorf("no subject found in reference statement")
101+
}
102+
103+
for _, sub := range s.Statement.Subject {
104+
p, err := helpers.PurlToPkg(sub.Uri)
105+
if err != nil {
106+
return fmt.Errorf("failed to parse uri: %s to a package with error: %w", sub.Uri, err)
107+
}
108+
r.pkg = p
109+
r.identifierStrings.PurlStrings = append(r.identifierStrings.PurlStrings, sub.Uri)
110+
}
111+
return nil
112+
}
113+
114+
// parseReferences parses the attestation to collect the reference information
115+
func (r *parser) parseReferences(_ context.Context, s *attestation.ReferenceStatement) error {
116+
if r.pkg == nil {
117+
return fmt.Errorf("package not specified for reference information")
118+
}
119+
120+
for _, ref := range s.Predicate.References {
121+
refData := assembler.IsOccurrenceIngest{
122+
Pkg: r.pkg,
123+
Artifact: &generated.ArtifactInputSpec{
124+
Algorithm: "sha256",
125+
Digest: ref.Digest.SHA256,
126+
},
127+
IsOccurrence: &generated.IsOccurrenceInputSpec{
128+
Justification: justification,
129+
Collector: "GUAC",
130+
Origin: "GUAC Reference",
131+
DocumentRef: ref.DownloadLocation,
132+
},
133+
}
134+
135+
r.collectedReference = append(r.collectedReference, refData)
136+
}
137+
138+
return nil
139+
}
140+
141+
func (r *parser) GetPredicates(ctx context.Context) *assembler.IngestPredicates {
142+
logger := logging.FromContext(ctx)
143+
preds := &assembler.IngestPredicates{}
144+
145+
if r.pkg == nil {
146+
logger.Error("error getting predicates: unable to find package element")
147+
return preds
148+
}
149+
150+
preds.IsOccurrence = r.collectedReference
151+
return preds
152+
}
153+
154+
// GetIdentities gets the identity node from the document if they exist
155+
func (r *parser) GetIdentities(ctx context.Context) []common.TrustInformation {
156+
return nil
157+
}
158+
159+
func (r *parser) GetIdentifiers(ctx context.Context) (*common.IdentifierStrings, error) {
160+
common.RemoveDuplicateIdentifiers(r.identifierStrings)
161+
return r.identifierStrings, nil
162+
}

0 commit comments

Comments
 (0)