Skip to content

Commit 40d8336

Browse files
authored
Merge pull request #110 from snyk/feat/UNIFY-530-convert-sbom
feat: convert SBOM into scan results
2 parents 5f6f9f9 + 0543a69 commit 40d8336

File tree

4 files changed

+190
-8
lines changed

4 files changed

+190
-8
lines changed

internal/snykclient/monitordeps.go

+2-8
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"context"
66
"encoding/json"
77
"fmt"
8-
"io"
98
"net/http"
109
"net/url"
1110

@@ -52,16 +51,11 @@ func (t *SnykClient) MonitorDeps(
5251
defer resp.Body.Close()
5352

5453
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
55-
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
56-
}
57-
58-
bodyBytes, err := io.ReadAll(resp.Body)
59-
if err != nil {
60-
return nil, fmt.Errorf("failed to read response: %w", err)
54+
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) //nolint:goconst // ok to repeat error message.
6155
}
6256

6357
var depsResp MonitorDepsResponse
64-
err = json.Unmarshal(bodyBytes, &depsResp)
58+
err = json.NewDecoder(resp.Body).Decode(&depsResp)
6559
if err != nil {
6660
return nil, fmt.Errorf("failed to unmarshal JSON response: %w", err)
6761
}

internal/snykclient/sbomconvert.go

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package snykclient
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"net/url"
10+
11+
"github.com/snyk/cli-extension-sbom/internal/errors"
12+
)
13+
14+
const (
15+
sbomConvertAPIVersion = "2025-03-06~beta"
16+
MIMETypeOctetStream = "application/octet-stream"
17+
)
18+
19+
func (t *SnykClient) SBOMConvert(
20+
ctx context.Context,
21+
errFactory *errors.ErrorFactory,
22+
sbom io.Reader,
23+
) ([]ScanResult, error) {
24+
u, err := buildSBOMConvertAPIURL(t.apiBaseURL, sbomConvertAPIVersion, t.orgID)
25+
if err != nil {
26+
return nil, fmt.Errorf("sbom convert api url invalid: %w", err)
27+
}
28+
29+
req, err := http.NewRequestWithContext(
30+
ctx,
31+
http.MethodPost,
32+
u.String(),
33+
sbom,
34+
)
35+
if err != nil {
36+
return nil, fmt.Errorf("failed to create request: %w", err)
37+
}
38+
39+
req.Header.Set(ContentTypeHeader, MIMETypeOctetStream)
40+
41+
resp, err := t.client.Do(req)
42+
if err != nil {
43+
return nil, fmt.Errorf("failed to send request: %w", err)
44+
}
45+
defer resp.Body.Close()
46+
47+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
48+
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
49+
}
50+
51+
var convertResp SBOMConvertResponse
52+
err = json.NewDecoder(resp.Body).Decode(&convertResp)
53+
if err != nil {
54+
return nil, fmt.Errorf("failed to unmarshal JSON response: %w", err)
55+
}
56+
57+
return convertResp.ScanResults, nil
58+
}
59+
60+
func buildSBOMConvertAPIURL(apiBaseURL, apiVersion, orgID string) (*url.URL, error) {
61+
u, err := url.Parse(apiBaseURL)
62+
if err != nil {
63+
return nil, err
64+
}
65+
66+
u = u.JoinPath("hidden", "orgs", orgID, "sboms", "convert")
67+
68+
query := url.Values{"version": {apiVersion}}
69+
u.RawQuery = query.Encode()
70+
71+
return u, nil
72+
}
+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/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)