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

feat: add dependency list export client service #2063

Merged
merged 8 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
lmphil marked this conversation as resolved.
Show resolved Hide resolved
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"
}