Skip to content
Draft
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
4 changes: 2 additions & 2 deletions internal/presenters/templates/ufm.human.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@

{{- define "testSummary"}}{{ "Test Summary" | bold }}
{{- "\n" }}
{{- $metadata := .GetMetadata }}
{{- $metadata := .ShallowMetadataCopy }}
{{- $targetDirectory := index $metadata "target-directory" }}
{{- if not $targetDirectory }}
{{- $targetDirectory = getValueFromConfig "targetDirectory" }}
Expand All @@ -114,7 +114,7 @@
{{- end }} {{/* end testSummary */}}

{{- define "resultHeader" }}
{{- $metadata := .GetMetadata }}
{{- $metadata := .ShallowMetadataCopy }}
{{- $testingSubject := getValueFromConfig "targetDirectory" }}
{{- $targetFile := index $metadata "display-target-file" }}
{{- if $targetFile }} {{- $testingSubject = $targetFile }} {{- end }}
Expand Down
4 changes: 2 additions & 2 deletions internal/presenters/templates/ufm.sarif.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
{{- $findingType := index $findingTypes 0 }}
{{- $issues := getIssuesFromTestResult $result }}
{{- $issuesSize := sub (len $issues) 1 }}
{{- $metadata := $result.GetMetadata }}
{{- $metadata := $result.ShallowMetadataCopy }}
{
"tool": {
"driver" : {
Expand Down Expand Up @@ -58,7 +58,7 @@
}
},
"results": [
{{- $displayTargetFile := index $result.GetMetadata "display-target-file" }}
{{- $displayTargetFile := index $metadata "display-target-file" }}
{{- $targetFile := "" }}
{{- if $displayTargetFile }} {{- $targetFile = $displayTargetFile }} {{- end }}
{{- range $index, $issue := $issues }}
Expand Down
112 changes: 14 additions & 98 deletions pkg/apiclients/mocks/testapi.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

112 changes: 112 additions & 0 deletions pkg/apiclients/testapi/test_result_metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package testapi

import "maps"

// Metadata keys for TestResult. Use with SetMetadata and GetMetadataValue for stable,
// implementation-independent access to test outcome details.
const (
// MetadataKeyProjectID is the Snyk project UUID as a string (when present).
MetadataKeyProjectID = "project-id"
// MetadataKeyDependencyCount is the total dependency count from test facts when available.
MetadataKeyDependencyCount = "dependency-count"
// MetadataKeyDisplayTargetFile is the primary manifest / target path for dep-graph tests (first path segment).
MetadataKeyDisplayTargetFile = "display-target-file"
// MetadataKeyPackageManager is the package manager / ecosystem label (often set by the CLI extension).
MetadataKeyPackageManager = "package-manager"
// MetadataKeyProjectName is the human-readable project name (often set by the CLI extension).
MetadataKeyProjectName = "project-name"
// MetadataKeyTargetDirectory is the scanned target directory (often set by the CLI extension).
MetadataKeyTargetDirectory = "target-directory"

// MetadataKeyRawSummary is the full finding summary including suppressed findings (*FindingSummary).
MetadataKeyRawSummary = "raw-summary"
// MetadataKeyBreachedPolicies is the outcome breached policy set (*PolicyRefSet), when present.
MetadataKeyBreachedPolicies = "breached-policies"
// MetadataKeyTestResources is the test resources slice from the API response (*[]TestResource).
MetadataKeyTestResources = "test-resources"
// MetadataKeyTestSubject is the API test subject (*TestSubject), when present.
MetadataKeyTestSubject = "test-subject"
// MetadataKeySubjectLocators is the API subject locators (*[]TestSubjectLocator), when present.
MetadataKeySubjectLocators = "subject-locators"
)

func cloneMetadataMap(m map[string]interface{}) map[string]interface{} {
if m == nil {
return nil
}
return maps.Clone(m)
}

func populateCanonicalMetadata(r *testResult) {
if r == nil {
return
}
if pid := projectIDStringFromLocators(r.SubjectLocators); pid != "" {
r.metadata[MetadataKeyProjectID] = pid
}
if n := dependencyCountFromFacts(r.TestFacts); n > 0 {
r.metadata[MetadataKeyDependencyCount] = n
}
if path := displayTargetFileFromSubject(r.TestSubject); path != "" {
r.metadata[MetadataKeyDisplayTargetFile] = path
}
if r.RawSummary != nil {
r.metadata[MetadataKeyRawSummary] = r.RawSummary
}
if r.BreachedPolicies != nil {
r.metadata[MetadataKeyBreachedPolicies] = r.BreachedPolicies
}
if r.TestResources != nil {
r.metadata[MetadataKeyTestResources] = r.TestResources
}
if r.TestSubject != nil {
r.metadata[MetadataKeyTestSubject] = r.TestSubject
}
if r.SubjectLocators != nil {
r.metadata[MetadataKeySubjectLocators] = r.SubjectLocators
}
}

func projectIDStringFromLocators(locators *[]TestSubjectLocator) string {
if locators == nil {
return ""
}
for _, loc := range *locators {
disc, err := loc.Discriminator()
if err != nil {
continue
}
if disc != string(ProjectEntity) {
continue
}
peLoc, err := loc.AsProjectEntityLocator()
if err != nil {
continue
}
return peLoc.ProjectId.String()
}
return ""
}

func dependencyCountFromFacts(facts *[]TestFact) int {
if facts == nil {
return 0
}
for _, fact := range *facts {
if fact.Type == DependencyCountFactTypeDependencyCountFact {
return int(fact.TotalDependencyCount)
}
}
return 0
}

func displayTargetFileFromSubject(subject *TestSubject) string {
if subject == nil {
return ""
}
dg, err := subject.AsDepGraphSubject()
if err != nil || len(dg.Locator.Paths) == 0 {
return ""
}
return dg.Locator.Paths[0]
}
43 changes: 13 additions & 30 deletions pkg/apiclients/testapi/testapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,25 +107,21 @@ type TestResult interface {
GetTestID() *uuid.UUID
GetTestConfiguration() *TestConfiguration
GetCreatedAt() *time.Time
GetTestSubject() *TestSubject
GetSubjectLocators() *[]TestSubjectLocator
GetTestResources() *[]TestResource

GetExecutionState() TestExecutionStates
GetErrors() *[]IoSnykApiCommonError
GetWarnings() *[]IoSnykApiCommonError

GetPassFail() *PassFail
GetOutcomeReason() *TestOutcomeReason
GetBreachedPolicies() *PolicyRefSet

GetEffectiveSummary() *FindingSummary
GetRawSummary() *FindingSummary
GetTestFacts() *[]TestFact

SetMetadata(key string, value interface{})
GetMetadataValue(key string) interface{}
GetMetadata() map[string]interface{}
// ShallowMetadataCopy returns a shallow copy of metadata for persistence (e.g. UFM workflow data).
// Implementations that do not store metadata may return nil.
ShallowMetadataCopy() map[string]interface{}

Findings(ctx context.Context) (resultFindings []FindingData, complete bool, err error)
}
Expand Down Expand Up @@ -255,48 +251,33 @@ func (r *testResult) GetPassFail() *PassFail { return r.PassFail }
// GetOutcomeReason returns the reason for the test outcome.
func (r *testResult) GetOutcomeReason() *TestOutcomeReason { return r.OutcomeReason }

// GetBreachedPolicies returns the policies that were breached.
func (r *testResult) GetBreachedPolicies() *PolicyRefSet { return r.BreachedPolicies }

// GetTestConfiguration returns the test configuration.
func (r *testResult) GetTestConfiguration() *TestConfiguration { return r.TestConfiguration }

// GetCreatedAt returns the creation timestamp of the test.
func (r *testResult) GetCreatedAt() *time.Time { return r.CreatedAt }

// GetTestSubject returns the test subject.
func (r *testResult) GetTestSubject() *TestSubject { return r.TestSubject }

// GetSubjectLocators returns the subject locators.
func (r *testResult) GetSubjectLocators() *[]TestSubjectLocator { return r.SubjectLocators }

// GetTestResources returns the test resources.
func (r *testResult) GetTestResources() *[]TestResource { return r.TestResources }

// GetEffectiveSummary returns the summary excluding suppressed findings.
func (r *testResult) GetEffectiveSummary() *FindingSummary { return r.EffectiveSummary }

// GetRawSummary returns the summary including suppressed findings.
func (r *testResult) GetRawSummary() *FindingSummary { return r.RawSummary }

// GetTestFacts returns the facts computed during test execution.
func (r *testResult) GetTestFacts() *[]TestFact { return r.TestFacts }

// SetMetadata sets the metadata for the given key.
func (r *testResult) SetMetadata(key string, value interface{}) {
r.metadata[key] = value
}

// GetMetadata returns the metadata for the given key.
func (r *testResult) GetMetadata() map[string]interface{} {
return r.metadata
}

// GetMetadataValue returns the metadata value for the given key.
func (r *testResult) GetMetadataValue(key string) interface{} {
return r.metadata[key]
}

// ShallowMetadataCopy returns a shallow copy of metadata for UFM serialization.
func (r *testResult) ShallowMetadataCopy() map[string]interface{} {
if r.metadata == nil {
return make(map[string]interface{})
}
return cloneMetadataMap(r.metadata)
}

// NewTestClient returns a new instance of the test client, configured with the provided options.
func NewTestClient(serverBaseUrl string, options ...ConfigOption) (TestClient, error) {
cfg := config{
Expand Down Expand Up @@ -638,6 +619,8 @@ func (h *testHandle) fetchResultStatus(ctx context.Context, testID uuid.UUID) (T
result.BreachedPolicies = attrs.Outcome.BreachedPolicies
}

populateCanonicalMetadata(result)

return result, nil
}

Expand Down
Loading