Skip to content

Commit

Permalink
feat: add dependency list export client service (#2063)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
lmphil and timofurrer authored Nov 21, 2024
1 parent 912f9bf commit 8186bd9
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ to add new and/or missing endpoints. Currently, the following services are suppo
- [x] Commits
- [x] Container Registry
- [x] Custom Attributes
- [x] Dependency List Export
- [x] Deploy Keys
- [x] Deployments
- [x] Discussions (threaded comments)
Expand Down
122 changes: 122 additions & 0 deletions dependency_list_export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package gitlab

import (
"bytes"
"fmt"
"io"
"net/http"
)

type DependencyListExportService struct {
client *Client
}

// CreateDependencyListExportOptions represents the available CreateDependencyListExport()
// options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/dependency_list_export.html#create-a-pipeline-level-dependency-list-export
type CreateDependencyListExportOptions struct {
ExportType *string `url:"export_type" json:"export_type"`
}

// DependencyListExport represents a request for a GitLab project's dependency list.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/dependency_list_export.html#create-a-pipeline-level-dependency-list-export
type DependencyListExport struct {
ID int `json:"id"`
HasFinished bool `json:"has_finished"`
Self string `json:"self"`
Download string `json:"download"`
}

const defaultExportType = "sbom"

// CreateDependencyListExport creates a new CycloneDX JSON export for all the project dependencies
// detected in a pipeline.
//
// If an authenticated user does not have permission to read_dependency, this request returns a 403
// Forbidden status code.
//
// SBOM exports can be only accessed by the export’s author.
//
// GitLab docs:
// https://docs.gitlab.com/ee/api/dependency_list_export.html#create-a-pipeline-level-dependency-list-export
func (s *DependencyListExportService) CreateDependencyListExport(pipelineID int, opt *CreateDependencyListExportOptions, options ...RequestOptionFunc) (*DependencyListExport, *Response, error) {
// POST /pipelines/:id/dependency_list_exports
createExportPath := fmt.Sprintf("pipelines/%d/dependency_list_exports", pipelineID)

if opt == nil {
opt = &CreateDependencyListExportOptions{}
}
if opt.ExportType == nil {
opt.ExportType = Ptr(defaultExportType)
}

req, err := s.client.NewRequest(http.MethodPost, createExportPath, opt, options)
if err != nil {
return nil, nil, err
}

export := new(DependencyListExport)
resp, err := s.client.Do(req, &export)
if err != nil {
return nil, resp, err
}

return export, resp, nil
}

// GetDependencyListExport gets metadata about a single dependency list export.
//
// GitLab docs:
// https://docs.gitlab.com/ee/api/dependency_list_export.html#get-single-dependency-list-export
func (s *DependencyListExportService) GetDependencyListExport(id int, options ...RequestOptionFunc) (*DependencyListExport, *Response, error) {
// GET /dependency_list_exports/:id
getExportPath := fmt.Sprintf("dependency_list_exports/%d", id)

req, err := s.client.NewRequest(http.MethodGet, getExportPath, nil, options)
if err != nil {
return nil, nil, err
}

export := new(DependencyListExport)
resp, err := s.client.Do(req, &export)
if err != nil {
return nil, resp, err
}

return export, resp, nil
}

// DownloadDependencyListExport downloads a single dependency list export.
//
// The github.com/CycloneDX/cyclonedx-go package can be used to parse the data from the returned io.Reader.
//
// sbom := new(cdx.BOM)
// decoder := cdx.NewBOMDecoder(reader, cdx.BOMFileFormatJSON)
//
// if err = decoder.Decode(sbom); err != nil {
// panic(err)
// }
//
// GitLab docs:
// https://docs.gitlab.com/ee/api/dependency_list_export.html#download-dependency-list-export
func (s *DependencyListExportService) DownloadDependencyListExport(id int, options ...RequestOptionFunc) (io.Reader, *Response, error) {
// GET /dependency_list_exports/:id/download
downloadExportPath := fmt.Sprintf("dependency_list_exports/%d/download", id)

req, err := s.client.NewRequest(http.MethodGet, downloadExportPath, nil, options)
if err != nil {
return nil, nil, err
}

var sbomBuffer bytes.Buffer
resp, err := s.client.Do(req, &sbomBuffer)
if err != nil {
return nil, resp, err
}

return &sbomBuffer, resp, nil
}
85 changes: 85 additions & 0 deletions dependency_list_export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package gitlab

import (
"bytes"
"encoding/json"
"io"
"net/http"
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCreateDependencyListExport(t *testing.T) {
mux, client := setup(t)

mux.HandleFunc("/api/v4/pipelines/1234/dependency_list_exports", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodPost)
body, err := io.ReadAll(r.Body)
require.NoError(t, err)

var content CreateDependencyListExportOptions
err = json.Unmarshal(body, &content)
require.NoError(t, err)

assert.Equal(t, "sbom", *content.ExportType)
mustWriteHTTPResponse(t, w, "testdata/create_dependency_list_export.json")
})

d := &CreateDependencyListExportOptions{
ExportType: Ptr("sbom"),
}

export, _, err := client.DependencyListExport.CreateDependencyListExport(1234, d)
require.NoError(t, err)

want := &DependencyListExport{
ID: 5678,
HasFinished: false,
Self: "http://gitlab.example.com/api/v4/dependency_list_exports/5678",
Download: "http://gitlab.example.com/api/v4/dependency_list_exports/5678/download",
}
require.Equal(t, want, export)
}

func TestGetDependencyListExport(t *testing.T) {
mux, client := setup(t)

mux.HandleFunc("/api/v4/dependency_list_exports/5678", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
mustWriteHTTPResponse(t, w, "testdata/get_dependency_list_export.json")
})

export, _, err := client.DependencyListExport.GetDependencyListExport(5678)
require.NoError(t, err)

want := &DependencyListExport{
ID: 5678,
HasFinished: true,
Self: "http://gitlab.example.com/api/v4/dependency_list_exports/5678",
Download: "http://gitlab.example.com/api/v4/dependency_list_exports/5678/download",
}
require.Equal(t, want, export)
}

func TestDownloadDependencyListExport(t *testing.T) {
mux, client := setup(t)

mux.HandleFunc("/api/v4/dependency_list_exports/5678/download", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
mustWriteHTTPResponse(t, w, "testdata/download_dependency_list_export.json")
})

sbomReader, _, err := client.DependencyListExport.DownloadDependencyListExport(5678)
require.NoError(t, err)

expectedSbom, err := os.ReadFile("testdata/download_dependency_list_export.json")
require.NoError(t, err)

var want bytes.Buffer
want.Write(expectedSbom)

require.Equal(t, &want, sbomReader)
}
2 changes: 2 additions & 0 deletions gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ type Client struct {
Commits *CommitsService
ContainerRegistry *ContainerRegistryService
CustomAttribute *CustomAttributesService
DependencyListExport *DependencyListExportService
DeployKeys *DeployKeysService
DeployTokens *DeployTokensService
DeploymentMergeRequests *DeploymentMergeRequestsService
Expand Down Expand Up @@ -360,6 +361,7 @@ func newClient(options ...ClientOptionFunc) (*Client, error) {
c.Commits = &CommitsService{client: c}
c.ContainerRegistry = &ContainerRegistryService{client: c}
c.CustomAttribute = &CustomAttributesService{client: c}
c.DependencyListExport = &DependencyListExportService{client: c}
c.DeployKeys = &DeployKeysService{client: c}
c.DeployTokens = &DeployTokensService{client: c}
c.DeploymentMergeRequests = &DeploymentMergeRequestsService{client: c}
Expand Down
6 changes: 6 additions & 0 deletions testdata/create_dependency_list_export.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"id": 5678,
"has_finished": false,
"self": "http://gitlab.example.com/api/v4/dependency_list_exports/5678",
"download": "http://gitlab.example.com/api/v4/dependency_list_exports/5678/download"
}
31 changes: 31 additions & 0 deletions testdata/download_dependency_list_export.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:3fa3b1c2-7e21-4dae-917b-b320f6d25ae1",
"version": 1,
"metadata": {
"timestamp": "2024-11-14T23:39:16.117Z",
"authors": [{ "name": "GitLab", "email": "[email protected]" }],
"properties": [
{
"name": "gitlab:dependency_scanning:input_file:path",
"value": "my_package_manager.lock"
},
{
"name": "gitlab:dependency_scanning:package_manager:name",
"value": "my_package_manager"
},
{ "name": "gitlab:meta:schema_version", "value": "1" }
],
"tools": [{ "vendor": "GitLab", "name": "Gemnasium", "version": "5.8.0" }]
},
"components": [
{
"name": "dummy",
"version": "1.0.0",
"purl": "pkg:testing/[email protected]",
"type": "library",
"licenses": [{ "license": { "name": "unknown" } }]
}
]
}
6 changes: 6 additions & 0 deletions testdata/get_dependency_list_export.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"id": 5678,
"has_finished": true,
"self": "http://gitlab.example.com/api/v4/dependency_list_exports/5678",
"download": "http://gitlab.example.com/api/v4/dependency_list_exports/5678/download"
}

0 comments on commit 8186bd9

Please sign in to comment.