Skip to content
This repository was archived by the owner on Dec 10, 2024. It is now read-only.

Commit 8186bd9

Browse files
lmphiltimofurrer
andauthored
feat: add dependency list export client service (#2063)
* feat: add dependency list client service * add missing line endings * rename client service to match endpoint name * fix and refactor * Fix casing in comment to match struct name * fix default export type and make DownloadDependencyListExport return an io.Reader * fix inconsistent indentation in commented code example * Update dependency_list_export.go --------- Co-authored-by: Timo Furrer <[email protected]>
1 parent 912f9bf commit 8186bd9

7 files changed

+253
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ to add new and/or missing endpoints. Currently, the following services are suppo
2626
- [x] Commits
2727
- [x] Container Registry
2828
- [x] Custom Attributes
29+
- [x] Dependency List Export
2930
- [x] Deploy Keys
3031
- [x] Deployments
3132
- [x] Discussions (threaded comments)

dependency_list_export.go

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package gitlab
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
)
9+
10+
type DependencyListExportService struct {
11+
client *Client
12+
}
13+
14+
// CreateDependencyListExportOptions represents the available CreateDependencyListExport()
15+
// options.
16+
//
17+
// GitLab API docs:
18+
// https://docs.gitlab.com/ee/api/dependency_list_export.html#create-a-pipeline-level-dependency-list-export
19+
type CreateDependencyListExportOptions struct {
20+
ExportType *string `url:"export_type" json:"export_type"`
21+
}
22+
23+
// DependencyListExport represents a request for a GitLab project's dependency list.
24+
//
25+
// GitLab API docs:
26+
// https://docs.gitlab.com/ee/api/dependency_list_export.html#create-a-pipeline-level-dependency-list-export
27+
type DependencyListExport struct {
28+
ID int `json:"id"`
29+
HasFinished bool `json:"has_finished"`
30+
Self string `json:"self"`
31+
Download string `json:"download"`
32+
}
33+
34+
const defaultExportType = "sbom"
35+
36+
// CreateDependencyListExport creates a new CycloneDX JSON export for all the project dependencies
37+
// detected in a pipeline.
38+
//
39+
// If an authenticated user does not have permission to read_dependency, this request returns a 403
40+
// Forbidden status code.
41+
//
42+
// SBOM exports can be only accessed by the export’s author.
43+
//
44+
// GitLab docs:
45+
// https://docs.gitlab.com/ee/api/dependency_list_export.html#create-a-pipeline-level-dependency-list-export
46+
func (s *DependencyListExportService) CreateDependencyListExport(pipelineID int, opt *CreateDependencyListExportOptions, options ...RequestOptionFunc) (*DependencyListExport, *Response, error) {
47+
// POST /pipelines/:id/dependency_list_exports
48+
createExportPath := fmt.Sprintf("pipelines/%d/dependency_list_exports", pipelineID)
49+
50+
if opt == nil {
51+
opt = &CreateDependencyListExportOptions{}
52+
}
53+
if opt.ExportType == nil {
54+
opt.ExportType = Ptr(defaultExportType)
55+
}
56+
57+
req, err := s.client.NewRequest(http.MethodPost, createExportPath, opt, options)
58+
if err != nil {
59+
return nil, nil, err
60+
}
61+
62+
export := new(DependencyListExport)
63+
resp, err := s.client.Do(req, &export)
64+
if err != nil {
65+
return nil, resp, err
66+
}
67+
68+
return export, resp, nil
69+
}
70+
71+
// GetDependencyListExport gets metadata about a single dependency list export.
72+
//
73+
// GitLab docs:
74+
// https://docs.gitlab.com/ee/api/dependency_list_export.html#get-single-dependency-list-export
75+
func (s *DependencyListExportService) GetDependencyListExport(id int, options ...RequestOptionFunc) (*DependencyListExport, *Response, error) {
76+
// GET /dependency_list_exports/:id
77+
getExportPath := fmt.Sprintf("dependency_list_exports/%d", id)
78+
79+
req, err := s.client.NewRequest(http.MethodGet, getExportPath, nil, options)
80+
if err != nil {
81+
return nil, nil, err
82+
}
83+
84+
export := new(DependencyListExport)
85+
resp, err := s.client.Do(req, &export)
86+
if err != nil {
87+
return nil, resp, err
88+
}
89+
90+
return export, resp, nil
91+
}
92+
93+
// DownloadDependencyListExport downloads a single dependency list export.
94+
//
95+
// The github.com/CycloneDX/cyclonedx-go package can be used to parse the data from the returned io.Reader.
96+
//
97+
// sbom := new(cdx.BOM)
98+
// decoder := cdx.NewBOMDecoder(reader, cdx.BOMFileFormatJSON)
99+
//
100+
// if err = decoder.Decode(sbom); err != nil {
101+
// panic(err)
102+
// }
103+
//
104+
// GitLab docs:
105+
// https://docs.gitlab.com/ee/api/dependency_list_export.html#download-dependency-list-export
106+
func (s *DependencyListExportService) DownloadDependencyListExport(id int, options ...RequestOptionFunc) (io.Reader, *Response, error) {
107+
// GET /dependency_list_exports/:id/download
108+
downloadExportPath := fmt.Sprintf("dependency_list_exports/%d/download", id)
109+
110+
req, err := s.client.NewRequest(http.MethodGet, downloadExportPath, nil, options)
111+
if err != nil {
112+
return nil, nil, err
113+
}
114+
115+
var sbomBuffer bytes.Buffer
116+
resp, err := s.client.Do(req, &sbomBuffer)
117+
if err != nil {
118+
return nil, resp, err
119+
}
120+
121+
return &sbomBuffer, resp, nil
122+
}

dependency_list_export_test.go

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package gitlab
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"io"
7+
"net/http"
8+
"os"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestCreateDependencyListExport(t *testing.T) {
16+
mux, client := setup(t)
17+
18+
mux.HandleFunc("/api/v4/pipelines/1234/dependency_list_exports", func(w http.ResponseWriter, r *http.Request) {
19+
testMethod(t, r, http.MethodPost)
20+
body, err := io.ReadAll(r.Body)
21+
require.NoError(t, err)
22+
23+
var content CreateDependencyListExportOptions
24+
err = json.Unmarshal(body, &content)
25+
require.NoError(t, err)
26+
27+
assert.Equal(t, "sbom", *content.ExportType)
28+
mustWriteHTTPResponse(t, w, "testdata/create_dependency_list_export.json")
29+
})
30+
31+
d := &CreateDependencyListExportOptions{
32+
ExportType: Ptr("sbom"),
33+
}
34+
35+
export, _, err := client.DependencyListExport.CreateDependencyListExport(1234, d)
36+
require.NoError(t, err)
37+
38+
want := &DependencyListExport{
39+
ID: 5678,
40+
HasFinished: false,
41+
Self: "http://gitlab.example.com/api/v4/dependency_list_exports/5678",
42+
Download: "http://gitlab.example.com/api/v4/dependency_list_exports/5678/download",
43+
}
44+
require.Equal(t, want, export)
45+
}
46+
47+
func TestGetDependencyListExport(t *testing.T) {
48+
mux, client := setup(t)
49+
50+
mux.HandleFunc("/api/v4/dependency_list_exports/5678", func(w http.ResponseWriter, r *http.Request) {
51+
testMethod(t, r, http.MethodGet)
52+
mustWriteHTTPResponse(t, w, "testdata/get_dependency_list_export.json")
53+
})
54+
55+
export, _, err := client.DependencyListExport.GetDependencyListExport(5678)
56+
require.NoError(t, err)
57+
58+
want := &DependencyListExport{
59+
ID: 5678,
60+
HasFinished: true,
61+
Self: "http://gitlab.example.com/api/v4/dependency_list_exports/5678",
62+
Download: "http://gitlab.example.com/api/v4/dependency_list_exports/5678/download",
63+
}
64+
require.Equal(t, want, export)
65+
}
66+
67+
func TestDownloadDependencyListExport(t *testing.T) {
68+
mux, client := setup(t)
69+
70+
mux.HandleFunc("/api/v4/dependency_list_exports/5678/download", func(w http.ResponseWriter, r *http.Request) {
71+
testMethod(t, r, http.MethodGet)
72+
mustWriteHTTPResponse(t, w, "testdata/download_dependency_list_export.json")
73+
})
74+
75+
sbomReader, _, err := client.DependencyListExport.DownloadDependencyListExport(5678)
76+
require.NoError(t, err)
77+
78+
expectedSbom, err := os.ReadFile("testdata/download_dependency_list_export.json")
79+
require.NoError(t, err)
80+
81+
var want bytes.Buffer
82+
want.Write(expectedSbom)
83+
84+
require.Equal(t, &want, sbomReader)
85+
}

gitlab.go

+2
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ type Client struct {
122122
Commits *CommitsService
123123
ContainerRegistry *ContainerRegistryService
124124
CustomAttribute *CustomAttributesService
125+
DependencyListExport *DependencyListExportService
125126
DeployKeys *DeployKeysService
126127
DeployTokens *DeployTokensService
127128
DeploymentMergeRequests *DeploymentMergeRequestsService
@@ -360,6 +361,7 @@ func newClient(options ...ClientOptionFunc) (*Client, error) {
360361
c.Commits = &CommitsService{client: c}
361362
c.ContainerRegistry = &ContainerRegistryService{client: c}
362363
c.CustomAttribute = &CustomAttributesService{client: c}
364+
c.DependencyListExport = &DependencyListExportService{client: c}
363365
c.DeployKeys = &DeployKeysService{client: c}
364366
c.DeployTokens = &DeployTokensService{client: c}
365367
c.DeploymentMergeRequests = &DeploymentMergeRequestsService{client: c}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"id": 5678,
3+
"has_finished": false,
4+
"self": "http://gitlab.example.com/api/v4/dependency_list_exports/5678",
5+
"download": "http://gitlab.example.com/api/v4/dependency_list_exports/5678/download"
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"bomFormat": "CycloneDX",
3+
"specVersion": "1.4",
4+
"serialNumber": "urn:uuid:3fa3b1c2-7e21-4dae-917b-b320f6d25ae1",
5+
"version": 1,
6+
"metadata": {
7+
"timestamp": "2024-11-14T23:39:16.117Z",
8+
"authors": [{ "name": "GitLab", "email": "[email protected]" }],
9+
"properties": [
10+
{
11+
"name": "gitlab:dependency_scanning:input_file:path",
12+
"value": "my_package_manager.lock"
13+
},
14+
{
15+
"name": "gitlab:dependency_scanning:package_manager:name",
16+
"value": "my_package_manager"
17+
},
18+
{ "name": "gitlab:meta:schema_version", "value": "1" }
19+
],
20+
"tools": [{ "vendor": "GitLab", "name": "Gemnasium", "version": "5.8.0" }]
21+
},
22+
"components": [
23+
{
24+
"name": "dummy",
25+
"version": "1.0.0",
26+
"purl": "pkg:testing/[email protected]",
27+
"type": "library",
28+
"licenses": [{ "license": { "name": "unknown" } }]
29+
}
30+
]
31+
}
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"id": 5678,
3+
"has_finished": true,
4+
"self": "http://gitlab.example.com/api/v4/dependency_list_exports/5678",
5+
"download": "http://gitlab.example.com/api/v4/dependency_list_exports/5678/download"
6+
}

0 commit comments

Comments
 (0)