Skip to content

Commit 2bc1feb

Browse files
authored
Support attachment in jf evd verify and get commands
1 parent df2610d commit 2bc1feb

28 files changed

Lines changed: 1888 additions & 173 deletions

evidence/get/get_base.go

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"github.com/jfrog/jfrog-client-go/utils/log"
1010
)
1111

12-
const SchemaVersion = "1.0"
12+
const SchemaVersion = "1.1"
1313

1414
type SubjectType string // Types in GetEvidence output
1515

@@ -33,15 +33,23 @@ type JsonlLine struct {
3333
}
3434

3535
type EvidenceEntry struct {
36-
PredicateSlug string `json:"predicateSlug"`
37-
PredicateType string `json:"predicateType,omitempty"`
38-
DownloadPath string `json:"downloadPath"`
39-
Verified bool `json:"verified"`
40-
SigningKey map[string]any `json:"signingKey,omitempty"`
41-
Subject map[string]any `json:"subject"`
42-
CreatedBy string `json:"createdBy"`
43-
CreatedAt string `json:"createdAt"`
44-
Predicate map[string]any `json:"predicate,omitempty"`
36+
PredicateSlug string `json:"predicateSlug"`
37+
PredicateType string `json:"predicateType,omitempty"`
38+
DownloadPath string `json:"downloadPath"`
39+
Verified bool `json:"verified"`
40+
SigningKey map[string]any `json:"signingKey,omitempty"`
41+
Subject map[string]any `json:"subject"`
42+
CreatedBy string `json:"createdBy"`
43+
CreatedAt string `json:"createdAt"`
44+
Predicate map[string]any `json:"predicate,omitempty"`
45+
Attachments []EvidenceAttachment `json:"attachments,omitempty"`
46+
}
47+
48+
type EvidenceAttachment struct {
49+
Name string `json:"name"`
50+
Sha256 string `json:"sha256"`
51+
Type string `json:"type,omitempty"`
52+
DownloadPath string `json:"downloadPath"`
4553
}
4654

4755
type CustomEvidenceResult struct {
@@ -276,5 +284,47 @@ func createOrderedEvidenceEntry(node map[string]any, includePredicate bool) Evid
276284
}
277285
}
278286

287+
if attachments, ok := extractEvidenceAttachments(node); ok {
288+
entry.Attachments = attachments
289+
}
290+
279291
return entry
280292
}
293+
294+
func extractEvidenceAttachments(node map[string]any) ([]EvidenceAttachment, bool) {
295+
attachmentsRaw, exists := node["attachments"]
296+
if !exists || attachmentsRaw == nil {
297+
return nil, false
298+
}
299+
300+
items, ok := attachmentsRaw.([]any)
301+
if !ok || len(items) == 0 {
302+
return nil, false
303+
}
304+
305+
attachments := make([]EvidenceAttachment, 0, len(items))
306+
for _, item := range items {
307+
attMap, ok := item.(map[string]any)
308+
if !ok {
309+
continue
310+
}
311+
312+
name, _ := attMap["name"].(string)
313+
sha256, _ := attMap["sha256"].(string)
314+
downloadPath, _ := attMap["downloadPath"].(string)
315+
316+
mimeType, _ := attMap["type"].(string)
317+
318+
attachments = append(attachments, EvidenceAttachment{
319+
Name: name,
320+
Sha256: sha256,
321+
Type: mimeType,
322+
DownloadPath: downloadPath,
323+
})
324+
}
325+
326+
if len(attachments) == 0 {
327+
return nil, false
328+
}
329+
return attachments, true
330+
}

evidence/get/get_base_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,31 @@ func TestExportEvidenceToConsole(t *testing.T) {
178178
err = exportEvidenceToJsonlFile(inputJSON, "")
179179
assert.NoError(t, err)
180180
}
181+
182+
func TestCreateOrderedEvidenceEntry_Attachments(t *testing.T) {
183+
entry := createOrderedEvidenceEntry(map[string]any{
184+
"predicateSlug": "slug",
185+
"attachments": []any{
186+
map[string]any{
187+
"name": "a.txt",
188+
"sha256": "abc",
189+
"type": "text/plain",
190+
"downloadPath": "repo/.evidence/a.txt",
191+
},
192+
},
193+
}, false)
194+
195+
if assert.Len(t, entry.Attachments, 1) {
196+
assert.Equal(t, "a.txt", entry.Attachments[0].Name)
197+
assert.Equal(t, "abc", entry.Attachments[0].Sha256)
198+
assert.Equal(t, "text/plain", entry.Attachments[0].Type)
199+
}
200+
}
201+
202+
func TestCreateOrderedEvidenceEntry_NoAttachmentsFieldWhenEmpty(t *testing.T) {
203+
entry := createOrderedEvidenceEntry(map[string]any{
204+
"predicateSlug": "slug",
205+
"attachments": []any{},
206+
}, false)
207+
assert.Nil(t, entry.Attachments)
208+
}

evidence/get/get_custom.go

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import (
1010
"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils"
1111
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
1212
"github.com/jfrog/jfrog-cli-evidence/evidence"
13+
evidenceutils "github.com/jfrog/jfrog-cli-evidence/evidence/utils"
1314
"github.com/jfrog/jfrog-client-go/onemodel"
1415
)
1516

16-
const getCustomEvidenceWithoutPredicateGraphqlQuery = `{"query":"{ evidence { searchEvidence( where: { hasSubjectWith: { repositoryKey: \"%s\", path: \"%s\", name: \"%s\"}} ) { totalCount edges { node { predicateSlug predicateType downloadPath verified signingKey { alias } createdBy createdAt subject { sha256 } } } } } }"}`
17-
const getCustomEvidenceWithPredicateGraphqlQuery = `{"query":"{ evidence { searchEvidence( where: { hasSubjectWith: { repositoryKey: \"%s\", path: \"%s\", name: \"%s\"}} ) { totalCount edges { node { predicateSlug predicateType downloadPath verified signingKey { alias } createdBy createdAt subject { sha256 } predicate } } } } }"}`
17+
const getCustomEvidenceQueryTemplate = `{"query":"{ evidence { searchEvidence( where: { hasSubjectWith: { repositoryKey: \"%s\", path: \"%s\", name: \"%s\"}} ) { totalCount edges { node { ` + evidenceutils.NodeFieldsPlaceholder + ` } } } } }"}`
1818

1919
type getEvidenceCustom struct {
2020
getEvidenceBase
@@ -66,13 +66,25 @@ func (g *getEvidenceCustom) Run() error {
6666
}
6767

6868
func (g *getEvidenceCustom) getEvidence(onemodelClient onemodel.Manager) ([]byte, error) {
69-
query, err := g.buildGraphqlQuery(g.subjectRepoPath)
69+
query, err := g.buildGraphqlQuery(g.subjectRepoPath, true)
7070
if err != nil {
7171
return nil, err
7272
}
7373
evidence, err := onemodelClient.GraphqlQuery(query)
7474
if err != nil {
75-
return nil, err
75+
if evidenceutils.IsAttachmentsFieldNotFound(err) {
76+
log.Debug("GraphQL schema does not support attachments field. Falling back to query without attachments.")
77+
queryWithoutAttachments, qErr := g.buildGraphqlQuery(g.subjectRepoPath, false)
78+
if qErr != nil {
79+
return nil, qErr
80+
}
81+
evidence, err = onemodelClient.GraphqlQuery(queryWithoutAttachments)
82+
if err != nil {
83+
return nil, err
84+
}
85+
} else {
86+
return nil, err
87+
}
7688
}
7789

7890
transformedEvidence, err := g.transformGraphQLOutput(evidence)
@@ -136,23 +148,30 @@ func (g *getEvidenceCustom) transformGraphQLOutput(rawEvidence []byte) ([]byte,
136148
return transformed, nil
137149
}
138150

139-
func (g *getEvidenceCustom) buildGraphqlQuery(subjectRepoPath string) ([]byte, error) {
140-
repoKey, path, name, err := g.getRepoKeyAndPath(subjectRepoPath)
151+
func (g *getEvidenceCustom) buildGraphqlQuery(subjectRepoPath string, includeAttachments bool) ([]byte, error) {
152+
repoKey, pathVal, name, err := g.getRepoKeyAndPath(subjectRepoPath)
141153
if err != nil {
142154
return nil, err
143155
}
144-
graphqlQuery := fmt.Sprintf(g.getGraphqlQuery(g.includePredicate), repoKey, path, name)
156+
nodeFields := evidenceutils.NewNodeFieldsBuilder(
157+
evidenceutils.FieldPredicateSlug,
158+
evidenceutils.FieldPredicateType,
159+
evidenceutils.FieldDownloadPath,
160+
evidenceutils.FieldVerified,
161+
evidenceutils.FieldSigningKeyAlias,
162+
evidenceutils.FieldCreatedBy,
163+
evidenceutils.FieldCreatedAt,
164+
evidenceutils.FieldSubjectSha256,
165+
).
166+
WithIf(includeAttachments, evidenceutils.AttachmentsFragment).
167+
WithIf(g.includePredicate, evidenceutils.FieldPredicate).
168+
Build()
169+
queryTemplate := evidenceutils.BuildQuery(getCustomEvidenceQueryTemplate, nodeFields)
170+
graphqlQuery := fmt.Sprintf(queryTemplate, repoKey, pathVal, name)
145171
log.Debug("GraphQL query: ", graphqlQuery)
146172
return []byte(graphqlQuery), nil
147173
}
148174

149-
func (g *getEvidenceCustom) getGraphqlQuery(includePredicate bool) string {
150-
if includePredicate {
151-
return getCustomEvidenceWithPredicateGraphqlQuery
152-
}
153-
return getCustomEvidenceWithoutPredicateGraphqlQuery
154-
}
155-
156175
func (g *getEvidenceCustom) getRepoKeyAndPath(subjectRepoPath string) (string, string, string, error) {
157176
firstSlashIndex := strings.Index(subjectRepoPath, "/")
158177
if firstSlashIndex <= 0 || firstSlashIndex == len(subjectRepoPath)-1 {

evidence/get/get_custom_test.go

Lines changed: 124 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package get
33
import (
44
"encoding/json"
55
"fmt"
6-
"log"
76
"net/http"
87
"testing"
98

@@ -27,6 +26,19 @@ func (m *mockOnemodelManagerCustomError) GraphqlQuery(_ []byte) ([]byte, error)
2726
return nil, fmt.Errorf("HTTP %d: Not Found", http.StatusNotFound)
2827
}
2928

29+
type mockOnemodelManagerCustomFallback struct {
30+
calls int
31+
}
32+
33+
func (m *mockOnemodelManagerCustomFallback) GraphqlQuery(_ []byte) ([]byte, error) {
34+
m.calls++
35+
if m.calls == 1 {
36+
return nil, fmt.Errorf(`{"errors":[{"message":"Cannot query field \"attachments\" on type \"Evidence\"."}]}`)
37+
}
38+
response := `{"data":{"evidence":{"searchEvidence":{"totalCount":1,"edges":[{"cursor":"1","node":{"predicateSlug":"test-slug","downloadPath":"test/path","verified":true,"signingKey":{"alias":"test-alias"},"subject":{"sha256":"test-digest"},"createdBy":"test-user","createdAt":"2024-01-01T00:00:00Z"}}]}}}}`
39+
return []byte(response), nil
40+
}
41+
3042
func validatePredicateEvidence(t *testing.T, result []byte) {
3143
var output CustomEvidenceOutput
3244
err := json.Unmarshal(result, &output)
@@ -74,22 +86,19 @@ func TestNewGetEvidenceCustom(t *testing.T) {
7486
// Test getEvidence method
7587
func TestGetCustomEvidence(t *testing.T) {
7688
tests := []struct {
77-
name string
78-
onemodelClient onemodel.Manager
79-
expectedError bool
80-
expectedEvidenceLen int
89+
name string
90+
onemodelClient onemodel.Manager
91+
expectedError bool
8192
}{
8293
{
83-
name: "Successful evidence retrieval",
84-
onemodelClient: &mockOnemodelManagerCustomSuccess{},
85-
expectedError: false,
86-
expectedEvidenceLen: 1,
94+
name: "Successful evidence retrieval",
95+
onemodelClient: &mockOnemodelManagerCustomSuccess{},
96+
expectedError: false,
8797
},
8898
{
89-
name: "Error retrieving evidence",
90-
onemodelClient: &mockOnemodelManagerCustomError{},
91-
expectedError: true,
92-
expectedEvidenceLen: 0,
99+
name: "Error retrieving evidence",
100+
onemodelClient: &mockOnemodelManagerCustomError{},
101+
expectedError: true,
93102
},
94103
}
95104

@@ -112,23 +121,20 @@ func TestGetCustomEvidence(t *testing.T) {
112121
assert.Empty(t, evidence)
113122
} else {
114123
assert.NoError(t, err)
115-
assert.NotEmpty(t, evidence)
116-
117-
// Additional check on the number of edges in the result
118-
var data map[string]any
119-
if err := json.Unmarshal(evidence, &data); err == nil {
120-
if evidenceData, ok := data["data"].(map[string]any); ok {
121-
if evidenceNode, ok := evidenceData["evidence"].(map[string]any); ok {
122-
if searchEvidence, ok := evidenceNode["searchEvidence"].(map[string]any); ok {
123-
edgesInterface, ok := searchEvidence["edges"].([]any)
124-
if !ok {
125-
log.Fatalf("Type assertion failed: expected []any")
126-
}
127-
edges := edgesInterface
128-
assert.Equal(t, tt.expectedEvidenceLen, len(edges))
129-
}
130-
}
131-
}
124+
125+
var output CustomEvidenceOutput
126+
assert.NoError(t, json.Unmarshal(evidence, &output))
127+
assert.Equal(t, SchemaVersion, output.SchemaVersion)
128+
assert.Equal(t, ArtifactType, output.Type)
129+
assert.Equal(t, "myRepo/my/path", output.Result.RepoPath)
130+
131+
if assert.Len(t, output.Result.Evidence, 1) {
132+
entry := output.Result.Evidence[0]
133+
assert.Equal(t, "test-slug", entry.PredicateSlug)
134+
assert.Equal(t, "test/path", entry.DownloadPath)
135+
assert.Equal(t, true, entry.Verified)
136+
assert.Equal(t, "test-user", entry.CreatedBy)
137+
assert.Equal(t, "2024-01-01T00:00:00Z", entry.CreatedAt)
132138
}
133139
}
134140
})
@@ -311,3 +317,91 @@ func TestTransformGraphQLOutput(t *testing.T) {
311317
})
312318
}
313319
}
320+
321+
func TestGetCustomEvidence_FallbackToLegacyQueryWhenAttachmentsUnsupported(t *testing.T) {
322+
manager := &mockOnemodelManagerCustomFallback{}
323+
g := &getEvidenceCustom{
324+
subjectRepoPath: "myRepo/my/path",
325+
getEvidenceBase: getEvidenceBase{
326+
includePredicate: true,
327+
},
328+
}
329+
330+
evidence, err := g.getEvidence(manager)
331+
assert.NoError(t, err)
332+
assert.Equal(t, 2, manager.calls, "should have made 2 GraphQL calls (initial + fallback)")
333+
334+
var output CustomEvidenceOutput
335+
assert.NoError(t, json.Unmarshal(evidence, &output))
336+
assert.Equal(t, SchemaVersion, output.SchemaVersion)
337+
assert.Equal(t, ArtifactType, output.Type)
338+
339+
if assert.Len(t, output.Result.Evidence, 1) {
340+
entry := output.Result.Evidence[0]
341+
assert.Equal(t, "test-slug", entry.PredicateSlug)
342+
assert.Equal(t, "test/path", entry.DownloadPath)
343+
assert.Equal(t, true, entry.Verified)
344+
assert.Equal(t, "test-user", entry.CreatedBy)
345+
}
346+
}
347+
348+
func TestTransformGraphQLOutput_WithAttachments(t *testing.T) {
349+
g := &getEvidenceCustom{
350+
subjectRepoPath: "test-repo/path/file.txt",
351+
getEvidenceBase: getEvidenceBase{
352+
includePredicate: false,
353+
},
354+
}
355+
356+
input := []byte(`{
357+
"data": {
358+
"evidence": {
359+
"searchEvidence": {
360+
"edges": [{
361+
"node": {
362+
"predicateSlug": "slug",
363+
"downloadPath": "evd/path",
364+
"verified": true,
365+
"subject": {"sha256": "sub-sha"},
366+
"createdBy": "me",
367+
"createdAt": "2026-01-01T00:00:00Z",
368+
"attachments": [{
369+
"name": "a.txt",
370+
"sha256": "abc",
371+
"type": "text/plain",
372+
"downloadPath": "repo/.evidence/att/a.txt"
373+
}]
374+
}
375+
}]
376+
}
377+
}
378+
}
379+
}`)
380+
381+
result, err := g.transformGraphQLOutput(input)
382+
assert.NoError(t, err)
383+
384+
var output CustomEvidenceOutput
385+
assert.NoError(t, json.Unmarshal(result, &output))
386+
387+
assert.Equal(t, SchemaVersion, output.SchemaVersion)
388+
assert.Equal(t, ArtifactType, output.Type)
389+
assert.Equal(t, "test-repo/path/file.txt", output.Result.RepoPath)
390+
391+
if assert.Len(t, output.Result.Evidence, 1) {
392+
entry := output.Result.Evidence[0]
393+
assert.Equal(t, "slug", entry.PredicateSlug)
394+
assert.Equal(t, "evd/path", entry.DownloadPath)
395+
assert.Equal(t, true, entry.Verified)
396+
assert.Equal(t, "me", entry.CreatedBy)
397+
assert.Equal(t, "2026-01-01T00:00:00Z", entry.CreatedAt)
398+
399+
if assert.Len(t, entry.Attachments, 1) {
400+
att := entry.Attachments[0]
401+
assert.Equal(t, "a.txt", att.Name)
402+
assert.Equal(t, "abc", att.Sha256)
403+
assert.Equal(t, "text/plain", att.Type)
404+
assert.Equal(t, "repo/.evidence/att/a.txt", att.DownloadPath)
405+
}
406+
}
407+
}

0 commit comments

Comments
 (0)