Skip to content
Merged
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
157 changes: 136 additions & 21 deletions evidence/cli/command/command_cli.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
package command

import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"slices"
"strings"

"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/application"
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/artifacts"
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/build"
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/flags"
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/github"
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/interface"
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/package"
_interface "github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/interface"
_package "github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/package"
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/releasebundle"
commandUtils "github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/utils"
evdConfig "github.com/jfrog/jfrog-cli-evidence/evidence/config"
"github.com/jfrog/jfrog-cli-evidence/evidence/model"

commonCliUtils "github.com/jfrog/jfrog-cli-core/v2/common/cliutils"
"github.com/jfrog/jfrog-cli-core/v2/common/commands"
"github.com/jfrog/jfrog-cli-core/v2/common/format"
pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common"
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
Expand All @@ -37,15 +43,23 @@ import (
generateCmd "github.com/jfrog/jfrog-cli-evidence/evidence/generate"
)

// responseCollector is implemented by evidence create commands that accumulate
// CreateResponse values during Run(). The interface is checked via type assertion
// after command execution so that printCreateEvidenceResponse can format output.
type responseCollector interface {
CollectedResponses() []*model.CreateResponse
}

func GetCommands() []components.Command {
return []components.Command{
{
Name: "create-evidence",
Aliases: []string{"create"},
Flags: flags.GetCommandFlags(flags.CreateEvidence),
Description: create.GetDescription(),
Arguments: create.GetArguments(),
Action: createEvidence,
Name: "create-evidence",
Aliases: []string{"create"},
Flags: flags.GetCommandFlags(flags.CreateEvidence),
Description: create.GetDescription(),
Arguments: create.GetArguments(),
Action: createEvidence,
SupportedFormats: []format.OutputFormat{format.Json, format.Table},
Comment thread
ehl-jf marked this conversation as resolved.
},
{
Name: "get-evidence",
Expand Down Expand Up @@ -74,8 +88,10 @@ func GetCommands() []components.Command {
}
}

var execFunc = commands.Exec
var ErrUnsupportedSubject = errors.New("unsupported subject")
var (
execFunc = commands.Exec
ErrUnsupportedSubject = errors.New("unsupported subject")
)

func createEvidence(ctx *components.Context) error {
if err := validateCreateEvidenceCommonContext(ctx); err != nil {
Expand All @@ -90,23 +106,50 @@ func createEvidence(ctx *components.Context) error {
return err
}

if slices.Contains(evidenceType, flags.TypeFlag) || (slices.Contains(evidenceType, flags.BuildName) && slices.Contains(evidenceType, flags.TypeFlag)) {
return github.NewEvidenceGitHubCommand(ctx, execFunc).CreateEvidence(ctx, serverDetails)
outputFormat, err := ctx.GetOutputFormat()
if err != nil {
return err
}

evidenceCommands := map[string]func(*components.Context, commandUtils.ExecCommandFunc) _interface.EvidenceCommands{
flags.SubjectRepoPath: artifacts.NewEvidenceCustomCommand,
flags.ReleaseBundle: releasebundle.NewEvidenceReleaseBundleCommand,
flags.BuildName: build.NewEvidenceBuildCommand,
flags.PackageName: _package.NewEvidencePackageCommand,
flags.ApplicationKey: application.NewEvidenceApplicationCommand,
// Wrap execFunc to capture collected responses after each command runs.
var collectedResponses []*model.CreateResponse
collectingExec := func(cmd commands.Command) error {
if runErr := execFunc(cmd); runErr != nil {
return runErr
}
if rc, ok := cmd.(responseCollector); ok {
collectedResponses = append(collectedResponses, rc.CollectedResponses()...)
}
return nil
}

if commandFunc, exists := evidenceCommands[evidenceType[0]]; exists {
return commandFunc(ctx, execFunc).CreateEvidence(ctx, serverDetails)
var createErr error
if slices.Contains(evidenceType, flags.TypeFlag) || (slices.Contains(evidenceType, flags.BuildName) && slices.Contains(evidenceType, flags.TypeFlag)) {
createErr = github.NewEvidenceGitHubCommand(ctx, collectingExec).CreateEvidence(ctx, serverDetails)
} else {
evidenceCommands := map[string]func(*components.Context, commandUtils.ExecCommandFunc) _interface.EvidenceCommands{
flags.SubjectRepoPath: artifacts.NewEvidenceCustomCommand,
flags.ReleaseBundle: releasebundle.NewEvidenceReleaseBundleCommand,
flags.BuildName: build.NewEvidenceBuildCommand,
flags.PackageName: _package.NewEvidencePackageCommand,
flags.ApplicationKey: application.NewEvidenceApplicationCommand,
}

if commandFunc, exists := evidenceCommands[evidenceType[0]]; exists {
createErr = commandFunc(ctx, collectingExec).CreateEvidence(ctx, serverDetails)
} else {
return ErrUnsupportedSubject
}
}

return ErrUnsupportedSubject
if createErr != nil {
return createErr
}

if outputFormat != "" {
return printCreateEvidenceResponse(os.Stdout, outputFormat, collectedResponses)
}
return nil
}

func getEvidence(ctx *components.Context) error {
Expand Down Expand Up @@ -487,3 +530,75 @@ func generateKeyPair(ctx *components.Context) error {
cmd := generateCmd.NewGenerateKeyPairCommand(serverDetails, uploadKey, alias, keyFilePath, fileName)
return cmd.Run()
}

// tableFieldOrder defines the display order for create-evidence table output.
var tableFieldOrder = []struct {
key string
value func(*model.CreateResponse) string
}{
{"repository", func(r *model.CreateResponse) string { return r.Repository }},
{"path", func(r *model.CreateResponse) string { return r.Path }},
{"name", func(r *model.CreateResponse) string { return r.Name }},
{"uri", func(r *model.CreateResponse) string { return r.Uri }},
{"sha256", func(r *model.CreateResponse) string { return r.Sha256 }},
{"predicate_type", func(r *model.CreateResponse) string { return r.PredicateType }},
{"predicate_category", func(r *model.CreateResponse) string { return r.PredicateCategory }},
{"predicate_slug", func(r *model.CreateResponse) string { return r.PredicateSlug }},
{"created_at", func(r *model.CreateResponse) string { return r.CreatedAt }},
{"created_by", func(r *model.CreateResponse) string { return r.CreatedBy }},
{"verified", func(r *model.CreateResponse) string {
if r.Verified {
return "true"
}
return "false"
}},
{"provider_id", func(r *model.CreateResponse) string { return r.ProviderId }},
}

// printCreateEvidenceResponse writes formatted output for the given responses.
func printCreateEvidenceResponse(w io.Writer, outputFormat format.OutputFormat, responses []*model.CreateResponse) error {
switch outputFormat {
case format.Json:
return printCreateEvidenceJSON(w, responses)
case format.Table:
return printCreateEvidenceTable(w, responses)
default:
return fmt.Errorf("unsupported format %q: accepted values are json, table", outputFormat)
}
}

func printCreateEvidenceJSON(w io.Writer, responses []*model.CreateResponse) error {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
if len(responses) == 1 {
return enc.Encode(responses[0])
}
return enc.Encode(responses)
}

func printCreateEvidenceTable(w io.Writer, responses []*model.CreateResponse) error {
for i, r := range responses {
if i > 0 {
if _, err := fmt.Fprintln(w); err != nil {
return err
}
}
t := table.NewWriter()
t.SetOutputMirror(w)
t.SetStyle(table.StyleLight)
t.Style().Options.SeparateRows = false
t.AppendHeader(table.Row{"FIELD", "VALUE"})
t.SetColumnConfigs([]table.ColumnConfig{
{Number: 1, Align: text.AlignLeft, AlignHeader: text.AlignLeft},
{Number: 2, Align: text.AlignLeft, AlignHeader: text.AlignLeft},
})
for _, field := range tableFieldOrder {
val := field.value(r)
if val != "" {
t.AppendRow(table.Row{field.key, val})
}
}
t.Render()
}
return nil
}
144 changes: 140 additions & 4 deletions evidence/cli/command/command_cli_test.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package command

import (
"bytes"
"flag"
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/flags"
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/utils"
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/test"
evdConfig "github.com/jfrog/jfrog-cli-evidence/evidence/config"
"os"
"testing"

"github.com/jfrog/jfrog-cli-core/v2/common/commands"
"github.com/jfrog/jfrog-cli-core/v2/common/format"
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
coreUtils "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/flags"
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/utils"
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/test"
evdConfig "github.com/jfrog/jfrog-cli-evidence/evidence/config"
"github.com/jfrog/jfrog-cli-evidence/evidence/model"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli"
"go.uber.org/mock/gomock"
Expand Down Expand Up @@ -878,3 +881,136 @@ func TestValidateCreateEvidenceCommonContext_Attachments(t *testing.T) {
assert.Error(t, validateCreateEvidenceCommonContext(c))
})
}

// ── printCreateEvidenceResponse tests ──────────────────────────────────────────

func sampleResponse() *model.CreateResponse {
return &model.CreateResponse{
Repository: "my-repo",
Path: "com/example",
Name: "artifact-1.0.jar",
Uri: "https://example.jfrog.io/my-repo/com/example/artifact-1.0.jar",
Sha256: "abc123def456",
PredicateType: "https://in-toto.io/attestation/vulns",
PredicateCategory: "SPDX",
PredicateSlug: "vulns",
CreatedAt: "2024-01-15T10:30:00Z",
CreatedBy: "ci-user",
Verified: true,
ProviderId: "jfrog",
}
}

func TestPrintCreateEvidenceResponse_JSON(t *testing.T) {
var buf bytes.Buffer
r := sampleResponse()

err := printCreateEvidenceResponse(&buf, format.Json, []*model.CreateResponse{r})
assert.NoError(t, err)

expected := `{
"repository": "my-repo",
"path": "com/example",
"name": "artifact-1.0.jar",
"uri": "https://example.jfrog.io/my-repo/com/example/artifact-1.0.jar",
"sha256": "abc123def456",
"predicate_category": "SPDX",
"predicate_type": "https://in-toto.io/attestation/vulns",
"predicate_slug": "vulns",
"created_at": "2024-01-15T10:30:00Z",
"created_by": "ci-user",
"verified": true,
"provider_id": "jfrog"
}`
assert.JSONEq(t, expected, buf.String())
}

func TestPrintCreateEvidenceResponse_JSON_MultipleResponses(t *testing.T) {
var buf bytes.Buffer
r1 := sampleResponse()
r2 := sampleResponse()
r2.Name = "artifact-2.0.jar"

err := printCreateEvidenceResponse(&buf, format.Json, []*model.CreateResponse{r1, r2})
assert.NoError(t, err)

expected := `[
{
"repository": "my-repo",
"path": "com/example",
"name": "artifact-1.0.jar",
"uri": "https://example.jfrog.io/my-repo/com/example/artifact-1.0.jar",
"sha256": "abc123def456",
"predicate_category": "SPDX",
"predicate_type": "https://in-toto.io/attestation/vulns",
"predicate_slug": "vulns",
"created_at": "2024-01-15T10:30:00Z",
"created_by": "ci-user",
"verified": true,
"provider_id": "jfrog"
},
{
"repository": "my-repo",
"path": "com/example",
"name": "artifact-2.0.jar",
"uri": "https://example.jfrog.io/my-repo/com/example/artifact-1.0.jar",
"sha256": "abc123def456",
"predicate_category": "SPDX",
"predicate_type": "https://in-toto.io/attestation/vulns",
"predicate_slug": "vulns",
"created_at": "2024-01-15T10:30:00Z",
"created_by": "ci-user",
"verified": true,
"provider_id": "jfrog"
}
]`
assert.JSONEq(t, expected, buf.String())
}

func TestPrintCreateEvidenceResponse_Table(t *testing.T) {
var buf bytes.Buffer
r := sampleResponse()

err := printCreateEvidenceResponse(&buf, format.Table, []*model.CreateResponse{r})
assert.NoError(t, err)

out := buf.String()
// Table must have a FIELD / VALUE header.
assert.Contains(t, out, "FIELD")
assert.Contains(t, out, "VALUE")
// Known field names and values should appear.
assert.Contains(t, out, "repository")
assert.Contains(t, out, "my-repo")
assert.Contains(t, out, "sha256")
assert.Contains(t, out, "abc123def456")
assert.Contains(t, out, "verified")
assert.Contains(t, out, "true")
}

func TestPrintCreateEvidenceResponse_Table_AbsentFieldsOmitted(t *testing.T) {
var buf bytes.Buffer
// Sparse response – only repository is set.
r := &model.CreateResponse{
Repository: "sparse-repo",
}

err := printCreateEvidenceResponse(&buf, format.Table, []*model.CreateResponse{r})
assert.NoError(t, err)

out := buf.String()
assert.Contains(t, out, "repository")
assert.Contains(t, out, "sparse-repo")
// Fields that are empty must not appear in the table.
assert.NotContains(t, out, "sha256")
assert.NotContains(t, out, "predicate_type")
assert.NotContains(t, out, "provider_id")
}

func TestPrintCreateEvidenceResponse_UnsupportedFormat(t *testing.T) {
var buf bytes.Buffer
r := sampleResponse()

err := printCreateEvidenceResponse(&buf, format.OutputFormat("xml"), []*model.CreateResponse{r})
assert.Error(t, err)
assert.Contains(t, err.Error(), "unsupported format")
}
7 changes: 7 additions & 0 deletions evidence/create/create_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ type createEvidenceBase struct {
artifactoryClient artifactory.ArtifactoryServicesManager
uploader evidenceUploader
stmtResolver sonar.StatementResolver
collectedResponses []*model.CreateResponse
}

// CollectedResponses returns the list of CreateResponse objects gathered during evidence upload.
func (c *createEvidenceBase) CollectedResponses() []*model.CreateResponse {
return c.collectedResponses
}

const EvdDefaultUser = "JFrog CLI"
Expand Down Expand Up @@ -275,6 +281,7 @@ func (c *createEvidenceBase) uploadEvidence(evidencePayload []byte, repoPath str
} else {
log.Info("Evidence successfully created but not verified due to missing/invalid public key")
}
c.collectedResponses = append(c.collectedResponses, createResponse)
return createResponse, nil
}

Expand Down
Loading
Loading