Skip to content

Commit fc9321e

Browse files
committed
feat: convert SBOM into scan results
1 parent 5f6f9f9 commit fc9321e

File tree

6 files changed

+204
-2
lines changed

6 files changed

+204
-2
lines changed

internal/snykclient/errors.go

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package snykclient
2+
3+
import "fmt"
4+
5+
func newStatusCodeError(statusCode int) error {
6+
return fmt.Errorf("unexpected status code: %d", statusCode)
7+
}

internal/snykclient/monitordeps.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func (t *SnykClient) MonitorDeps(
5252
defer resp.Body.Close()
5353

5454
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
55-
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
55+
return nil, newStatusCodeError(resp.StatusCode)
5656
}
5757

5858
bodyBytes, err := io.ReadAll(resp.Body)

internal/snykclient/sbomconvert.go

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package snykclient
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"net/url"
10+
"path"
11+
12+
"github.com/snyk/cli-extension-sbom/internal/errors"
13+
)
14+
15+
const (
16+
sbomConvertAPIVersion = "2025-03-06~beta"
17+
MIMETypeOctetStream = "application/octet-stream"
18+
)
19+
20+
func (t *SnykClient) SBOMConvert(
21+
ctx context.Context,
22+
errFactory *errors.ErrorFactory,
23+
sbom io.Reader,
24+
) ([]ScanResult, error) {
25+
u, err := buildAPIURL(t.apiBaseURL, sbomConvertAPIVersion, t.orgID)
26+
if err != nil {
27+
return nil, fmt.Errorf("sbom convert api url invalid: %w", err)
28+
}
29+
30+
req, err := http.NewRequestWithContext(
31+
ctx,
32+
http.MethodPost,
33+
u,
34+
sbom,
35+
)
36+
if err != nil {
37+
return nil, fmt.Errorf("failed to create request: %w", err)
38+
}
39+
40+
req.Header.Set(ContentTypeHeader, MIMETypeOctetStream)
41+
42+
resp, err := t.client.Do(req)
43+
if err != nil {
44+
return nil, fmt.Errorf("failed to send request: %w", err)
45+
}
46+
defer resp.Body.Close()
47+
48+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
49+
return nil, newStatusCodeError(resp.StatusCode)
50+
}
51+
52+
bodyBytes, err := io.ReadAll(resp.Body)
53+
if err != nil {
54+
return nil, fmt.Errorf("failed to read response: %w", err)
55+
}
56+
57+
var convertResp SBOMConvertResponse
58+
err = json.Unmarshal(bodyBytes, &convertResp)
59+
if err != nil {
60+
return nil, fmt.Errorf("failed to unmarshal JSON response: %w", err)
61+
}
62+
63+
return convertResp.ScanResults, nil
64+
}
65+
66+
func buildAPIURL(apiBaseURL, apiVersion, orgID string) (string, error) {
67+
base, err := url.Parse(apiBaseURL)
68+
if err != nil {
69+
return "", err
70+
}
71+
72+
base.Path = path.Join(base.Path, "hidden/orgs", orgID, "sboms/convert")
73+
74+
query := url.Values{}
75+
query.Set("version", apiVersion)
76+
base.RawQuery = query.Encode()
77+
78+
return base.String(), nil
79+
}
+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package snykclient_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"net/http"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
12+
"github.com/snyk/cli-extension-sbom/internal/mocks"
13+
"github.com/snyk/cli-extension-sbom/internal/snykclient"
14+
)
15+
16+
func Test_SBOMConvert(t *testing.T) {
17+
response := mocks.NewMockResponse(
18+
"application/json; charset=utf-8",
19+
[]byte(`{"scanResults":[{"name":"Scan 1"},{"name":"Scan 2"}]}`),
20+
http.StatusOK,
21+
)
22+
23+
sbomContent := `{"foo":"bar"}`
24+
25+
mockHTTPClient := mocks.NewMockSBOMService(response, func(r *http.Request) {
26+
assert.Equal(t, http.MethodPost, r.Method)
27+
assert.Equal(t, "/hidden/orgs/org1/sboms/convert?version=2025-03-06~beta", r.RequestURI)
28+
assert.Equal(t, "application/octet-stream", r.Header.Get("Content-Type"))
29+
})
30+
31+
client := snykclient.NewSnykClient(mockHTTPClient.Client(), mockHTTPClient.URL, "org1")
32+
depsResp, err := client.SBOMConvert(
33+
context.Background(),
34+
errFactory,
35+
bytes.NewBuffer([]byte(sbomContent)),
36+
)
37+
38+
assert.NoError(t, err)
39+
assert.Equal(t, 2, len(depsResp))
40+
assert.Equal(t, "Scan 1", depsResp[0].Name)
41+
assert.Equal(t, "Scan 2", depsResp[1].Name)
42+
}
43+
44+
func Test_SBOMConvert_InvalidJSONReturned(t *testing.T) {
45+
response := mocks.NewMockResponse(
46+
"application/json; charset=utf-8",
47+
[]byte(`{"scanResults":[{"name":"Scan 1"`),
48+
http.StatusOK,
49+
)
50+
51+
sbomContent := `{"foo":"bar"}`
52+
53+
mockHTTPClient := mocks.NewMockSBOMService(response)
54+
55+
client := snykclient.NewSnykClient(mockHTTPClient.Client(), mockHTTPClient.URL, "org1")
56+
_, err := client.SBOMConvert(
57+
context.Background(),
58+
errFactory,
59+
bytes.NewBuffer([]byte(sbomContent)),
60+
)
61+
62+
assert.ErrorContains(t, err, "failed to unmarshal JSON response")
63+
}
64+
65+
func Test_SBOMConvert_ServerErrors(t *testing.T) {
66+
testCases := []struct {
67+
name string
68+
statusCode int
69+
responseBody string
70+
}{
71+
{
72+
name: "Forbidden (403) - Feature not available",
73+
statusCode: http.StatusForbidden,
74+
responseBody: `{"message":"This functionality is not available on your plan."}`,
75+
},
76+
{
77+
name: "Bad Request (400) - Malformed Request",
78+
statusCode: http.StatusBadRequest,
79+
responseBody: "",
80+
},
81+
{
82+
name: "Internal Server Error (500) - Server Issue",
83+
statusCode: http.StatusInternalServerError,
84+
responseBody: `{"message":"Internal server error."}`,
85+
},
86+
}
87+
88+
sbomContent := `{"foo":"bar"}`
89+
90+
for _, tc := range testCases {
91+
t.Run(tc.name, func(t *testing.T) {
92+
response := mocks.NewMockResponse(
93+
"application/json; charset=utf-8",
94+
[]byte(tc.responseBody),
95+
tc.statusCode,
96+
)
97+
98+
mockHTTPClient := mocks.NewMockSBOMService(response)
99+
100+
client := snykclient.NewSnykClient(mockHTTPClient.Client(), mockHTTPClient.URL, "org1")
101+
_, err := client.SBOMConvert(context.Background(), errFactory, bytes.NewBufferString(sbomContent))
102+
103+
assert.ErrorContainsf(
104+
t,
105+
err,
106+
fmt.Sprintf("%d", tc.statusCode),
107+
"Expected error to contain status code %d",
108+
tc.statusCode,
109+
)
110+
})
111+
}
112+
}

internal/snykclient/sbomtest.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func (t *SBOMTest) GetStatus(ctx context.Context, errFactory *errors.ErrorFactor
120120
}
121121

122122
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
123-
return SBOMTestStatusIndeterminate, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
123+
return SBOMTestStatusIndeterminate, newStatusCodeError(resp.StatusCode)
124124
}
125125

126126
var statusDoc SBOMTestStatusResourceDocument

internal/snykclient/types.go

+4
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,10 @@ type ScanResultRequest struct {
376376
ScanResult ScanResult `json:"scanResult"`
377377
}
378378

379+
type SBOMConvertResponse struct {
380+
ScanResults []ScanResult `json:"scanResults"`
381+
}
382+
379383
type MonitorDepsResponse struct {
380384
OK bool `json:"ok"`
381385
Org string `json:"org"`

0 commit comments

Comments
 (0)