Skip to content

Commit 3916087

Browse files
authored
Include attachments in create-evidence output (#58)
Surface attachment metadata returned by the evidence service in both JSON (full record incl. download_path) and table (name/sha256/type) formats of `jf evd create --format ...`, so callers can confirm which attachment was linked. The `CreateResponseAttachment` struct mirrors the evidence service `AttachmentView` schema 1:1, so the response body unmarshals directly without any client-side transformation.
1 parent ddc16ad commit 3916087

4 files changed

Lines changed: 230 additions & 12 deletions

File tree

evidence/cli/command/command_cli.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,43 @@ func printCreateEvidenceTable(w io.Writer, responses []*model.CreateResponse) er
598598
t.AppendRow(table.Row{field.key, val})
599599
}
600600
}
601+
for _, row := range attachmentTableRows(r.Attachments) {
602+
t.AppendRow(row)
603+
}
601604
t.Render()
602605
}
603606
return nil
604607
}
608+
609+
// attachmentTableRows builds FIELD/VALUE rows for an attachments slice.
610+
// Single attachment uses "attachment.<field>", multiple use "attachments[i].<field>".
611+
// download_path is intentionally omitted from table output (too long to render
612+
// without distorting the layout); it is still emitted in JSON output.
613+
func attachmentTableRows(attachments []model.CreateResponseAttachment) []table.Row {
614+
if len(attachments) == 0 {
615+
return nil
616+
}
617+
fields := []struct {
618+
key string
619+
value func(model.CreateResponseAttachment) string
620+
}{
621+
{"name", func(a model.CreateResponseAttachment) string { return a.Name }},
622+
{"sha256", func(a model.CreateResponseAttachment) string { return a.Sha256 }},
623+
{"type", func(a model.CreateResponseAttachment) string { return a.Type }},
624+
}
625+
var rows []table.Row
626+
for i, att := range attachments {
627+
prefix := "attachment"
628+
if len(attachments) > 1 {
629+
prefix = fmt.Sprintf("attachments[%d]", i)
630+
}
631+
for _, f := range fields {
632+
val := f.value(att)
633+
if val == "" {
634+
continue
635+
}
636+
rows = append(rows, table.Row{fmt.Sprintf("%s.%s", prefix, f.key), val})
637+
}
638+
}
639+
return rows
640+
}

evidence/cli/command/command_cli_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,3 +1014,123 @@ func TestPrintCreateEvidenceResponse_UnsupportedFormat(t *testing.T) {
10141014
assert.Error(t, err)
10151015
assert.Contains(t, err.Error(), "unsupported format")
10161016
}
1017+
1018+
func TestPrintCreateEvidenceResponse_JSON_WithAttachment(t *testing.T) {
1019+
var buf bytes.Buffer
1020+
r := sampleResponse()
1021+
r.Attachments = []model.CreateResponseAttachment{{
1022+
Name: "scan.txt",
1023+
Sha256: "att-sha-1",
1024+
Type: "text/plain",
1025+
DownloadPath: "my-repo/.evidence/abc/scan.txt",
1026+
}}
1027+
1028+
err := printCreateEvidenceResponse(&buf, format.Json, []*model.CreateResponse{r})
1029+
assert.NoError(t, err)
1030+
1031+
expected := `{
1032+
"repository": "my-repo",
1033+
"path": "com/example",
1034+
"name": "artifact-1.0.jar",
1035+
"uri": "https://example.jfrog.io/my-repo/com/example/artifact-1.0.jar",
1036+
"sha256": "abc123def456",
1037+
"predicate_category": "SPDX",
1038+
"predicate_type": "https://in-toto.io/attestation/vulns",
1039+
"predicate_slug": "vulns",
1040+
"created_at": "2024-01-15T10:30:00Z",
1041+
"created_by": "ci-user",
1042+
"verified": true,
1043+
"provider_id": "jfrog",
1044+
"attachments": [
1045+
{
1046+
"name": "scan.txt",
1047+
"sha256": "att-sha-1",
1048+
"type": "text/plain",
1049+
"download_path": "my-repo/.evidence/abc/scan.txt"
1050+
}
1051+
]
1052+
}`
1053+
assert.JSONEq(t, expected, buf.String())
1054+
}
1055+
1056+
func TestPrintCreateEvidenceResponse_JSON_AttachmentsOmittedWhenEmpty(t *testing.T) {
1057+
var buf bytes.Buffer
1058+
r := sampleResponse()
1059+
1060+
err := printCreateEvidenceResponse(&buf, format.Json, []*model.CreateResponse{r})
1061+
assert.NoError(t, err)
1062+
assert.NotContains(t, buf.String(), "attachments")
1063+
}
1064+
1065+
func TestPrintCreateEvidenceResponse_Table_SingleAttachment(t *testing.T) {
1066+
var buf bytes.Buffer
1067+
r := sampleResponse()
1068+
r.Attachments = []model.CreateResponseAttachment{{
1069+
Name: "scan.txt",
1070+
Sha256: "att-sha-1",
1071+
Type: "text/plain",
1072+
DownloadPath: "my-repo/.evidence/abc/scan.txt",
1073+
}}
1074+
1075+
err := printCreateEvidenceResponse(&buf, format.Table, []*model.CreateResponse{r})
1076+
assert.NoError(t, err)
1077+
1078+
out := buf.String()
1079+
assert.Contains(t, out, "attachment.name")
1080+
assert.Contains(t, out, "scan.txt")
1081+
assert.Contains(t, out, "attachment.sha256")
1082+
assert.Contains(t, out, "att-sha-1")
1083+
assert.Contains(t, out, "attachment.type")
1084+
assert.Contains(t, out, "text/plain")
1085+
// download_path is JSON-only; it must not appear in the table output.
1086+
assert.NotContains(t, out, "download_path")
1087+
assert.NotContains(t, out, "my-repo/.evidence/abc/scan.txt")
1088+
}
1089+
1090+
func TestPrintCreateEvidenceResponse_Table_SingleAttachment_OmitsEmptyFields(t *testing.T) {
1091+
var buf bytes.Buffer
1092+
r := sampleResponse()
1093+
r.Attachments = []model.CreateResponseAttachment{{
1094+
Name: "scan.txt",
1095+
Sha256: "att-sha-1",
1096+
}}
1097+
1098+
err := printCreateEvidenceResponse(&buf, format.Table, []*model.CreateResponse{r})
1099+
assert.NoError(t, err)
1100+
1101+
out := buf.String()
1102+
assert.Contains(t, out, "attachment.name")
1103+
assert.Contains(t, out, "attachment.sha256")
1104+
assert.NotContains(t, out, "attachment.type")
1105+
}
1106+
1107+
func TestPrintCreateEvidenceResponse_Table_MultipleAttachments(t *testing.T) {
1108+
var buf bytes.Buffer
1109+
r := sampleResponse()
1110+
r.Attachments = []model.CreateResponseAttachment{
1111+
{Name: "a.txt", Sha256: "sha-a"},
1112+
{Name: "b.txt", Sha256: "sha-b"},
1113+
}
1114+
1115+
err := printCreateEvidenceResponse(&buf, format.Table, []*model.CreateResponse{r})
1116+
assert.NoError(t, err)
1117+
1118+
out := buf.String()
1119+
assert.Contains(t, out, "attachments[0].name")
1120+
assert.Contains(t, out, "sha-a")
1121+
assert.Contains(t, out, "attachments[1].name")
1122+
assert.Contains(t, out, "sha-b")
1123+
assert.NotContains(t, out, "attachment.name")
1124+
}
1125+
1126+
func TestPrintCreateEvidenceResponse_Table_NoAttachmentRowsWhenAbsent(t *testing.T) {
1127+
var buf bytes.Buffer
1128+
r := sampleResponse()
1129+
1130+
err := printCreateEvidenceResponse(&buf, format.Table, []*model.CreateResponse{r})
1131+
assert.NoError(t, err)
1132+
1133+
out := buf.String()
1134+
assert.NotContains(t, out, "attachment.")
1135+
assert.NotContains(t, out, "attachments[")
1136+
}

evidence/create/create_base_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,59 @@ func TestUploadEvidence_Error(t *testing.T) {
477477
assert.Error(t, err)
478478
}
479479

480+
type uploaderReturningServerAttachments struct {
481+
last evdservices.EvidenceDetails
482+
}
483+
484+
func (u *uploaderReturningServerAttachments) UploadEvidence(d evdservices.EvidenceDetails) ([]byte, error) {
485+
u.last = d
486+
resp := model.CreateResponse{
487+
Verified: true,
488+
Attachments: []model.CreateResponseAttachment{{
489+
Name: "scan.txt",
490+
Sha256: "server-sha",
491+
Type: "text/plain",
492+
DownloadPath: "subject-repo/.evidence/xyz/scan.txt",
493+
}},
494+
}
495+
return json.Marshal(resp)
496+
}
497+
498+
func TestUploadEvidence_UsesServerAttachmentsWhenPresent(t *testing.T) {
499+
upl := &uploaderReturningServerAttachments{}
500+
c := &createEvidenceBase{uploader: upl}
501+
502+
att := &statementAttachment{
503+
Repository: "attachments-repo",
504+
Path: "reports/scan.txt",
505+
Sha256: "local-sha",
506+
Name: "scan.txt",
507+
Type: "text/plain",
508+
}
509+
510+
wrappedPayload, err := c.wrapCreatePayloadWithAttachments([]byte(`{"payloadType":"x","payload":"y","signatures":[]}`), att)
511+
assert.NoError(t, err)
512+
resp, err := c.uploadEvidence(wrappedPayload, "r/p", att)
513+
assert.NoError(t, err)
514+
assert.Len(t, resp.Attachments, 1)
515+
assert.Equal(t, "scan.txt", resp.Attachments[0].Name)
516+
assert.Equal(t, "server-sha", resp.Attachments[0].Sha256)
517+
assert.Equal(t, "text/plain", resp.Attachments[0].Type)
518+
assert.Equal(t, "subject-repo/.evidence/xyz/scan.txt", resp.Attachments[0].DownloadPath)
519+
assert.Len(t, upl.last.Attachments, 1)
520+
assert.Equal(t, "attachments-repo", upl.last.Attachments[0].Repository)
521+
assert.Equal(t, "reports/scan.txt", upl.last.Attachments[0].Path)
522+
}
523+
524+
func TestUploadEvidence_NoAttachmentLeavesAttachmentsNil(t *testing.T) {
525+
upl := &captureUploader{}
526+
c := &createEvidenceBase{uploader: upl}
527+
resp, err := c.uploadEvidence([]byte(`{"payloadType":"x","payload":"y","signatures":[]}`), "r/p", nil)
528+
assert.NoError(t, err)
529+
assert.Nil(t, resp.Attachments)
530+
assert.Empty(t, upl.last.Attachments)
531+
}
532+
480533
func TestUploadEvidence_InvalidJSON(t *testing.T) {
481534
u := &invalidJSONUploader{}
482535
c := &createEvidenceBase{uploader: u}

evidence/model/create.go

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
package model
22

33
type CreateResponse struct {
4-
Repository string `json:"repository"`
5-
Path string `json:"path"`
6-
Name string `json:"name"`
7-
Uri string `json:"uri"`
8-
Sha256 string `json:"sha256"`
9-
PredicateCategory string `json:"predicate_category"`
10-
PredicateType string `json:"predicate_type"`
11-
PredicateSlug string `json:"predicate_slug"`
12-
CreatedAt string `json:"created_at"`
13-
CreatedBy string `json:"created_by"`
14-
Verified bool `json:"verified"`
15-
ProviderId string `json:"provider_id"`
4+
Repository string `json:"repository"`
5+
Path string `json:"path"`
6+
Name string `json:"name"`
7+
Uri string `json:"uri"`
8+
Sha256 string `json:"sha256"`
9+
PredicateCategory string `json:"predicate_category"`
10+
PredicateType string `json:"predicate_type"`
11+
PredicateSlug string `json:"predicate_slug"`
12+
CreatedAt string `json:"created_at"`
13+
CreatedBy string `json:"created_by"`
14+
Verified bool `json:"verified"`
15+
ProviderId string `json:"provider_id"`
16+
Attachments []CreateResponseAttachment `json:"attachments,omitempty"`
17+
}
18+
19+
// CreateResponseAttachment mirrors the evidence service AttachmentView schema.
20+
type CreateResponseAttachment struct {
21+
Name string `json:"name,omitempty"`
22+
Sha256 string `json:"sha256,omitempty"`
23+
Type string `json:"type,omitempty"`
24+
DownloadPath string `json:"download_path,omitempty"`
1625
}

0 commit comments

Comments
 (0)