Skip to content

Commit 97ce8a3

Browse files
feat(reachability): human readable output
1 parent eb9d583 commit 97ce8a3

File tree

5 files changed

+134
-46
lines changed

5 files changed

+134
-46
lines changed

internal/commands/ostest/__snapshots__/sbom_reachability_flow_test.snap

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11

2-
[Test_RunSbomReachabilityFlow_Success - 1]
2+
[Test_RunSbomReachabilityFlow_JSON - 1]
33
{
44
"dependencyCount": 0,
5-
"displayTargetFile": "",
5+
"displayTargetFile": "./testdata/bom.json",
66
"filesystemPolicy": false,
77
"filtered": {
88
"ignore": [],
@@ -57,7 +57,28 @@
5757
}
5858
---
5959

60-
[Test_RunSbomReachabilityFlow_Success - 2]
60+
[Test_RunSbomReachabilityFlow_JSON - 2]
61+
{
62+
"artifacts": 0,
63+
"results": [
64+
{
65+
"ignored": 0,
66+
"open": 1,
67+
"severity": "high",
68+
"total": 1
69+
}
70+
],
71+
"severity_order_asc": [
72+
"low",
73+
"medium",
74+
"high",
75+
"critical"
76+
],
77+
"type": "open-source"
78+
}
79+
---
80+
81+
[Test_RunSbomReachabilityFlow_HumanReadable - 1]
6182
{
6283
"artifacts": 0,
6384
"results": [

internal/commands/ostest/sbom_reachability_flow.go

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package ostest
22

33
import (
4-
"bytes"
54
"context"
6-
"encoding/json"
75
"fmt"
86
"os"
97

@@ -13,6 +11,7 @@ import (
1311

1412
"github.com/snyk/cli-extension-os-flows/internal/bundlestore"
1513
"github.com/snyk/cli-extension-os-flows/internal/errors"
14+
"github.com/snyk/cli-extension-os-flows/internal/legacy/definitions"
1615
)
1716

1817
// RunSbomReachabilityFlow runs the SBOM reachability flow.
@@ -69,27 +68,18 @@ func RunSbomReachabilityFlow(
6968
return nil, fmt.Errorf("failed to create sbom test reachability subject: %w", err)
7069
}
7170

72-
findings, summary, err := RunTest(ctx, ictx, testClient, subject, "", "", int(0), "", orgID, orgSlugOrID, errFactory, logger, nil)
71+
findings, summary, err := RunTest(ctx, ictx, testClient, subject, "", "", int(0), sbomPath, orgID, orgSlugOrID, errFactory, logger, nil)
7372
if err != nil {
7473
return nil, err
7574
}
7675

77-
var finalOutput []workflow.Data
78-
var buffer bytes.Buffer
79-
encoder := json.NewEncoder(&buffer)
80-
encoder.SetEscapeHTML(false)
81-
encoder.SetIndent("", " ")
82-
err = encoder.Encode(findings)
83-
if err != nil {
84-
return nil, errFactory.NewLegacyJSONTransformerError(fmt.Errorf("marshaling to json: %w", err))
76+
var allLegacyFindings []definitions.LegacyVulnerabilityResponse
77+
if findings != nil {
78+
allLegacyFindings = append(allLegacyFindings, *findings)
8579
}
86-
// encoder.Encode adds a newline, which we trim to match Marshal's behavior.
87-
findingsBytes := bytes.TrimRight(buffer.Bytes(), "\n")
88-
89-
finalOutput = append(finalOutput, NewWorkflowData(ApplicationJSONContentType, findingsBytes))
90-
finalOutput = append(finalOutput, summary...)
9180

92-
return finalOutput, nil
81+
//nolint:contextcheck // The handleOutput call chain is not context-aware
82+
return handleOutput(ictx, allLegacyFindings, summary, errFactory)
9383
}
9484

9585
// validateDirectory checks if the given path exists and contains files.

internal/commands/ostest/sbom_reachability_flow_test.go

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ import (
1616
"github.com/snyk/go-application-framework/pkg/apiclients/testapi"
1717
"github.com/snyk/go-application-framework/pkg/configuration"
1818
gafmocks "github.com/snyk/go-application-framework/pkg/mocks"
19+
"github.com/snyk/go-application-framework/pkg/runtimeinfo"
20+
"github.com/snyk/go-application-framework/pkg/workflow"
1921

22+
"github.com/snyk/cli-extension-os-flows/internal/bundlestore"
2023
"github.com/snyk/cli-extension-os-flows/internal/commands/ostest"
2124
"github.com/snyk/cli-extension-os-flows/internal/errors"
2225
"github.com/snyk/cli-extension-os-flows/internal/mocks"
@@ -28,12 +31,65 @@ import (
2831
// pathRgxp is used for replacing the "path" in the output for snapshot consistency.
2932
var pathRgxp = regexp.MustCompile(`\s*,?"path"\s*:\s*"[^"]*",?`)
3033

31-
func Test_RunSbomReachabilityFlow_Success(t *testing.T) {
32-
logger := zerolog.Nop()
33-
ef := errors.NewErrorFactory(&logger)
34+
var nopLogger = zerolog.Nop()
35+
36+
func Test_RunSbomReachabilityFlow_JSON(t *testing.T) {
37+
ctrl := gomock.NewController(t)
38+
defer ctrl.Finish()
39+
40+
ctx := context.Background()
41+
ef := errors.NewErrorFactory(&nopLogger)
42+
mockIctx, mockTestClient, mockBsClient, orgID, orgSlug, sbomPath, sourceCodePath := setupTest(ctx, t, ctrl, true)
43+
44+
// This should now succeed with proper finding data
45+
result, err := ostest.RunSbomReachabilityFlow(ctx, mockIctx, mockTestClient, ef, &nopLogger, sbomPath, sourceCodePath, mockBsClient, orgID, orgSlug)
46+
47+
require.NoError(t, err)
48+
require.NotNil(t, result)
49+
require.Len(t, result, 2) // Should return legacy data + summary data
50+
require.Contains(t, result[0].GetContentType(), "application/json") // legacy data
51+
require.Contains(t, result[1].GetContentType(), "application/json; schema=test-summary") // summary data
52+
53+
legacyJSON, ok := result[0].GetPayload().([]byte)
54+
require.True(t, ok)
55+
legacySummary, ok := result[1].GetPayload().([]byte)
56+
require.True(t, ok)
57+
snaps.MatchJSON(t, pathRgxp.ReplaceAll(legacyJSON, nil))
58+
snaps.MatchJSON(t, pathRgxp.ReplaceAll(legacySummary, nil))
59+
}
60+
61+
func Test_RunSbomReachabilityFlow_HumanReadable(t *testing.T) {
3462
ctrl := gomock.NewController(t)
3563
defer ctrl.Finish()
64+
3665
ctx := context.Background()
66+
ef := errors.NewErrorFactory(&nopLogger)
67+
mockIctx, mockTestClient, mockBsClient, orgID, orgSlug, sbomPath, sourceCodePath := setupTest(ctx, t, ctrl, false)
68+
69+
// This should now succeed with proper finding data
70+
result, err := ostest.RunSbomReachabilityFlow(ctx, mockIctx, mockTestClient, ef, &nopLogger, sbomPath, sourceCodePath, mockBsClient, orgID, orgSlug)
71+
72+
require.NoError(t, err)
73+
require.NotNil(t, result)
74+
require.Len(t, result, 1)
75+
require.Contains(t, result[0].GetContentType(), "application/json; schema=test-summary")
76+
77+
legacySummary, ok := result[0].GetPayload().([]byte)
78+
require.True(t, ok)
79+
snaps.MatchJSON(t, pathRgxp.ReplaceAll(legacySummary, nil))
80+
}
81+
82+
//nolint:gocritic // Not important for tests.
83+
func setupTest(ctx context.Context, t *testing.T, ctrl *gomock.Controller, jsonOutput bool) (
84+
workflow.InvocationContext,
85+
testapi.TestClient,
86+
bundlestore.Client,
87+
string,
88+
string,
89+
string,
90+
string,
91+
) {
92+
t.Helper()
3793
sbomPath := "./testdata/bom.json"
3894
sourceCodePath := "./testdata/test_dir"
3995
orgID := "test-org-id"
@@ -159,24 +215,12 @@ func Test_RunSbomReachabilityFlow_Success(t *testing.T) {
159215

160216
// Mock Invocation Context
161217
mockConfig := configuration.New()
162-
mockConfig.Set(outputworkflow.OutputConfigKeyJSON, true)
218+
mockConfig.Set(outputworkflow.OutputConfigKeyJSON, jsonOutput)
163219
mockConfig.Set(configuration.ORGANIZATION_SLUG, orgSlug)
164220
mockIctx := gafmocks.NewMockInvocationContext(ctrl)
165221
mockIctx.EXPECT().GetConfiguration().Return(mockConfig).AnyTimes()
222+
mockIctx.EXPECT().GetEnhancedLogger().Return(&nopLogger).AnyTimes()
223+
mockIctx.EXPECT().GetRuntimeInfo().Return(runtimeinfo.New()).AnyTimes()
166224

167-
// This should now succeed with proper finding data
168-
result, err := ostest.RunSbomReachabilityFlow(ctx, mockIctx, mockTestClient, ef, &logger, sbomPath, sourceCodePath, mockBsClient, orgID, orgSlug)
169-
170-
require.NoError(t, err)
171-
require.NotNil(t, result)
172-
require.Len(t, result, 2) // Should return legacy data + summary data
173-
require.Contains(t, result[0].GetContentType(), "application/json") // legacy data
174-
require.Contains(t, result[1].GetContentType(), "application/json; schema=test-summary") // summary data
175-
176-
legacyJSON, ok := result[0].GetPayload().([]byte)
177-
require.True(t, ok)
178-
legacySummary, ok := result[1].GetPayload().([]byte)
179-
require.True(t, ok)
180-
snaps.MatchJSON(t, pathRgxp.ReplaceAll(legacyJSON, nil))
181-
snaps.MatchJSON(t, pathRgxp.ReplaceAll(legacySummary, nil))
225+
return mockIctx, mockTestClient, mockBsClient, orgID, orgSlug, sbomPath, sourceCodePath
182226
}

internal/presenters/funcs.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414
"github.com/snyk/go-application-framework/pkg/runtimeinfo"
1515
)
1616

17+
const notApplicable = "N/A"
18+
1719
// add returns the sum of two integers.
1820
func add(a, b int) int {
1921
return a + b
@@ -150,6 +152,36 @@ func getIntroducedBy(finding testapi.FindingData) string {
150152
return ""
151153
}
152154

155+
// getReachability returns the reachability status for a finding.
156+
func getReachability(finding testapi.FindingData) string {
157+
if finding.Attributes == nil || len(finding.Attributes.Evidence) == 0 {
158+
return notApplicable
159+
}
160+
for _, evidence := range finding.Attributes.Evidence {
161+
evDisc, err := evidence.Discriminator()
162+
if err != nil {
163+
continue
164+
}
165+
166+
if evDisc == string(testapi.Reachability) {
167+
reachEvidence, err := evidence.AsReachabilityEvidence()
168+
if err != nil {
169+
continue
170+
}
171+
172+
switch reachEvidence.Reachability {
173+
case testapi.ReachabilityTypeFunction:
174+
return "Reachable"
175+
case testapi.ReachabilityTypeNoInfo:
176+
return "No reachable path found"
177+
case testapi.ReachabilityTypeNotApplicable, testapi.ReachabilityTypeNone:
178+
return notApplicable
179+
}
180+
}
181+
}
182+
return notApplicable
183+
}
184+
153185
// getFromConfig returns a function that retrieves configuration values.
154186
func getFromConfig(config configuration.Configuration) func(key string) string {
155187
return func(key string) string {
@@ -300,7 +332,7 @@ func getDefaultTemplateFuncMap(config configuration.Configuration, ri runtimeinf
300332
if finding.Id != nil {
301333
return finding.Id.String()
302334
}
303-
return "N/A"
335+
return notApplicable
304336
}
305337

306338
defaultMap := template.FuncMap{}
@@ -311,6 +343,7 @@ func getDefaultTemplateFuncMap(config configuration.Configuration, ri runtimeinf
311343
defaultMap["getVulnInfoURL"] = getVulnInfoURL
312344
defaultMap["getIntroducedThrough"] = getIntroducedThrough
313345
defaultMap["getIntroducedBy"] = getIntroducedBy
346+
defaultMap["getReachability"] = getReachability
314347
defaultMap["filterFinding"] = filterFinding
315348
defaultMap["hasField"] = hasField
316349
defaultMap["notHasField"] = func(path string) func(obj any) bool {

internal/presenters/templates/unified_finding.tmpl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@
1717
Risk Score: N/A
1818
{{- end }}
1919
{{- end }}
20-
{{- $isReachable := getFieldValueFrom . "Attributes.IsReachable" -}}
21-
{{- if eq $isReachable "true" }}
22-
Reachable: YES
20+
{{- $reachability := getReachability . -}}
21+
{{- if ne $reachability "N/A" }}
22+
Reachability: {{ $reachability }}
2323
{{- end }}
24-
24+
2525
{{end}}
2626

2727
{{- define "details" -}}
@@ -74,14 +74,14 @@
7474
Organization: {{ getValueFromConfig "internal_org_slug" }}
7575
Test type: {{ if eq .Summary.Type "sast" }}Static code analysis{{else}}{{ .Summary.Type }}{{ end}}
7676
Project path: {{ .Summary.Path }}
77-
77+
7878
{{- $total := 0 }}{{- $open := 0 }}{{- $ignored := 0 }}
7979
{{- range $res := .Summary.Results }}
8080
{{- $total = add $total $res.Total }}
8181
{{- $open = add $open $res.Open }}
8282
{{- $ignored = add $ignored $res.Ignored }}
8383
{{- end }}
84-
84+
8585
Total issues: {{ $total }}
8686
{{- if gt $total 0}}
8787
Ignored issues: {{ print $ignored | bold }} [

0 commit comments

Comments
 (0)