diff --git a/github/dependency_graph.go b/github/dependency_graph.go new file mode 100644 index 0000000000..e578965cc1 --- /dev/null +++ b/github/dependency_graph.go @@ -0,0 +1,80 @@ +// Copyright 2023 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package github + +import ( + "context" + "fmt" +) + +type DependencyGraphService service + +// SBOM represents a software bill of materials, which describes the +// packages/libraries that a repository depends on. +type SBOM struct { + SBOM *SBOMInfo `json:"sbom,omitempty"` +} + +// CreationInfo represents when the SBOM was created and who created it. +type CreationInfo struct { + Created *Timestamp `json:"created,omitempty"` + Creators []string `json:"creators,omitempty"` +} + +// RepoDependencies represents the dependencies of a repo. +type RepoDependencies struct { + SPDXID *string `json:"SPDXID,omitempty"` + // Package name + Name *string `json:"name,omitempty"` + VersionInfo *string `json:"versionInfo,omitempty"` + DownloadLocation *string `json:"downloadLocation,omitempty"` + FilesAnalyzed *bool `json:"filesAnalyzed,omitempty"` + LicenseConcluded *string `json:"licenseConcluded,omitempty"` + LicenseDeclared *string `json:"licenseDeclared,omitempty"` +} + +// SBOMInfo represents a software bill of materials (SBOM) using SPDX. +// SPDX is an open standard for SBOMs that +// identifies and catalogs components, licenses, copyrights, security +// references, and other metadata relating to software. +type SBOMInfo struct { + SPDXID *string `json:"SPDXID,omitempty"` + SPDXVersion *string `json:"spdxVersion,omitempty"` + CreationInfo *CreationInfo `json:"creationInfo,omitempty"` + + // Repo name + Name *string `json:"name,omitempty"` + DataLicense *string `json:"dataLicense,omitempty"` + DocumentDescribes []string `json:"documentDescribes,omitempty"` + DocumentNamespace *string `json:"documentNamespace,omitempty"` + + // List of packages dependencies + Packages []*RepoDependencies `json:"packages,omitempty"` +} + +func (s SBOM) String() string { + return Stringify(s) +} + +// GetSBOM fetches the software bill of materials for a repository. +// +// GitHub API docs: https://docs.github.com/en/rest/dependency-graph/sboms +func (s *DependencyGraphService) GetSBOM(ctx context.Context, owner, repo string) (*SBOM, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/dependency-graph/sbom", owner, repo) + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var sbom *SBOM + resp, err := s.client.Do(ctx, req, &sbom) + if err != nil { + return nil, resp, err + } + + return sbom, resp, nil +} diff --git a/github/dependency_graph_test.go b/github/dependency_graph_test.go new file mode 100644 index 0000000000..73cf07f716 --- /dev/null +++ b/github/dependency_graph_test.go @@ -0,0 +1,79 @@ +// Copyright 2023 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package github + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestDependencyGraphService_GetSBOM(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repos/owner/repo/dependency-graph/sbom", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "sbom":{ + "creationInfo":{ + "created":"2021-09-01T00:00:00Z" + }, + "name":"owner/repo", + "packages":[ + { + "name":"rubygems:rails", + "versionInfo":"1.0.0" + } + ] + } + }`) + }) + + ctx := context.Background() + sbom, _, err := client.DependencyGraph.GetSBOM(ctx, "owner", "repo") + if err != nil { + t.Errorf("DependencyGraph.GetSBOM returned error: %v", err) + } + + testTime := time.Date(2021, 9, 1, 0, 0, 0, 0, time.UTC) + want := &SBOM{ + &SBOMInfo{ + CreationInfo: &CreationInfo{ + Created: &Timestamp{testTime}, + }, + Name: String("owner/repo"), + Packages: []*RepoDependencies{ + { + Name: String("rubygems:rails"), + VersionInfo: String("1.0.0"), + }, + }, + }, + } + + if !cmp.Equal(sbom, want) { + t.Errorf("DependencyGraph.GetSBOM returned %+v, want %+v", sbom, want) + } + + const methodName = "GetSBOM" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.DependencyGraph.GetSBOM(ctx, "\n", "\n") + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.DependencyGraph.GetSBOM(ctx, "owner", "repo") + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} diff --git a/github/github-accessors.go b/github/github-accessors.go index fc7c34a6c9..8e76ff2583 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -4726,6 +4726,14 @@ func (c *CreateUserProjectOptions) GetBody() string { return *c.Body } +// GetCreated returns the Created field if it's non-nil, zero value otherwise. +func (c *CreationInfo) GetCreated() Timestamp { + if c == nil || c.Created == nil { + return Timestamp{} + } + return *c.Created +} + // GetAuthorizedCredentialExpiresAt returns the AuthorizedCredentialExpiresAt field if it's non-nil, zero value otherwise. func (c *CredentialAuthorization) GetAuthorizedCredentialExpiresAt() Timestamp { if c == nil || c.AuthorizedCredentialExpiresAt == nil { @@ -17342,6 +17350,62 @@ func (r *RenameOrgResponse) GetURL() string { return *r.URL } +// GetDownloadLocation returns the DownloadLocation field if it's non-nil, zero value otherwise. +func (r *RepoDependencies) GetDownloadLocation() string { + if r == nil || r.DownloadLocation == nil { + return "" + } + return *r.DownloadLocation +} + +// GetFilesAnalyzed returns the FilesAnalyzed field if it's non-nil, zero value otherwise. +func (r *RepoDependencies) GetFilesAnalyzed() bool { + if r == nil || r.FilesAnalyzed == nil { + return false + } + return *r.FilesAnalyzed +} + +// GetLicenseConcluded returns the LicenseConcluded field if it's non-nil, zero value otherwise. +func (r *RepoDependencies) GetLicenseConcluded() string { + if r == nil || r.LicenseConcluded == nil { + return "" + } + return *r.LicenseConcluded +} + +// GetLicenseDeclared returns the LicenseDeclared field if it's non-nil, zero value otherwise. +func (r *RepoDependencies) GetLicenseDeclared() string { + if r == nil || r.LicenseDeclared == nil { + return "" + } + return *r.LicenseDeclared +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (r *RepoDependencies) GetName() string { + if r == nil || r.Name == nil { + return "" + } + return *r.Name +} + +// GetSPDXID returns the SPDXID field if it's non-nil, zero value otherwise. +func (r *RepoDependencies) GetSPDXID() string { + if r == nil || r.SPDXID == nil { + return "" + } + return *r.SPDXID +} + +// GetVersionInfo returns the VersionInfo field if it's non-nil, zero value otherwise. +func (r *RepoDependencies) GetVersionInfo() string { + if r == nil || r.VersionInfo == nil { + return "" + } + return *r.VersionInfo +} + // GetBranch returns the Branch field if it's non-nil, zero value otherwise. func (r *RepoMergeUpstreamRequest) GetBranch() string { if r == nil || r.Branch == nil { @@ -19974,6 +20038,62 @@ func (s *SarifID) GetURL() string { return *s.URL } +// GetSBOM returns the SBOM field. +func (s *SBOM) GetSBOM() *SBOMInfo { + if s == nil { + return nil + } + return s.SBOM +} + +// GetCreationInfo returns the CreationInfo field. +func (s *SBOMInfo) GetCreationInfo() *CreationInfo { + if s == nil { + return nil + } + return s.CreationInfo +} + +// GetDataLicense returns the DataLicense field if it's non-nil, zero value otherwise. +func (s *SBOMInfo) GetDataLicense() string { + if s == nil || s.DataLicense == nil { + return "" + } + return *s.DataLicense +} + +// GetDocumentNamespace returns the DocumentNamespace field if it's non-nil, zero value otherwise. +func (s *SBOMInfo) GetDocumentNamespace() string { + if s == nil || s.DocumentNamespace == nil { + return "" + } + return *s.DocumentNamespace +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (s *SBOMInfo) GetName() string { + if s == nil || s.Name == nil { + return "" + } + return *s.Name +} + +// GetSPDXID returns the SPDXID field if it's non-nil, zero value otherwise. +func (s *SBOMInfo) GetSPDXID() string { + if s == nil || s.SPDXID == nil { + return "" + } + return *s.SPDXID +} + +// GetSPDXVersion returns the SPDXVersion field if it's non-nil, zero value otherwise. +func (s *SBOMInfo) GetSPDXVersion() string { + if s == nil || s.SPDXVersion == nil { + return "" + } + return *s.SPDXVersion +} + // GetAnalysisKey returns the AnalysisKey field if it's non-nil, zero value otherwise. func (s *ScanningAnalysis) GetAnalysisKey() string { if s == nil || s.AnalysisKey == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index fba6b9ee95..6e863e0778 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -5575,6 +5575,16 @@ func TestCreateUserProjectOptions_GetBody(tt *testing.T) { c.GetBody() } +func TestCreationInfo_GetCreated(tt *testing.T) { + var zeroValue Timestamp + c := &CreationInfo{Created: &zeroValue} + c.GetCreated() + c = &CreationInfo{} + c.GetCreated() + c = nil + c.GetCreated() +} + func TestCredentialAuthorization_GetAuthorizedCredentialExpiresAt(tt *testing.T) { var zeroValue Timestamp c := &CredentialAuthorization{AuthorizedCredentialExpiresAt: &zeroValue} @@ -20130,6 +20140,76 @@ func TestRenameOrgResponse_GetURL(tt *testing.T) { r.GetURL() } +func TestRepoDependencies_GetDownloadLocation(tt *testing.T) { + var zeroValue string + r := &RepoDependencies{DownloadLocation: &zeroValue} + r.GetDownloadLocation() + r = &RepoDependencies{} + r.GetDownloadLocation() + r = nil + r.GetDownloadLocation() +} + +func TestRepoDependencies_GetFilesAnalyzed(tt *testing.T) { + var zeroValue bool + r := &RepoDependencies{FilesAnalyzed: &zeroValue} + r.GetFilesAnalyzed() + r = &RepoDependencies{} + r.GetFilesAnalyzed() + r = nil + r.GetFilesAnalyzed() +} + +func TestRepoDependencies_GetLicenseConcluded(tt *testing.T) { + var zeroValue string + r := &RepoDependencies{LicenseConcluded: &zeroValue} + r.GetLicenseConcluded() + r = &RepoDependencies{} + r.GetLicenseConcluded() + r = nil + r.GetLicenseConcluded() +} + +func TestRepoDependencies_GetLicenseDeclared(tt *testing.T) { + var zeroValue string + r := &RepoDependencies{LicenseDeclared: &zeroValue} + r.GetLicenseDeclared() + r = &RepoDependencies{} + r.GetLicenseDeclared() + r = nil + r.GetLicenseDeclared() +} + +func TestRepoDependencies_GetName(tt *testing.T) { + var zeroValue string + r := &RepoDependencies{Name: &zeroValue} + r.GetName() + r = &RepoDependencies{} + r.GetName() + r = nil + r.GetName() +} + +func TestRepoDependencies_GetSPDXID(tt *testing.T) { + var zeroValue string + r := &RepoDependencies{SPDXID: &zeroValue} + r.GetSPDXID() + r = &RepoDependencies{} + r.GetSPDXID() + r = nil + r.GetSPDXID() +} + +func TestRepoDependencies_GetVersionInfo(tt *testing.T) { + var zeroValue string + r := &RepoDependencies{VersionInfo: &zeroValue} + r.GetVersionInfo() + r = &RepoDependencies{} + r.GetVersionInfo() + r = nil + r.GetVersionInfo() +} + func TestRepoMergeUpstreamRequest_GetBranch(tt *testing.T) { var zeroValue string r := &RepoMergeUpstreamRequest{Branch: &zeroValue} @@ -23273,6 +23353,70 @@ func TestSarifID_GetURL(tt *testing.T) { s.GetURL() } +func TestSBOM_GetSBOM(tt *testing.T) { + s := &SBOM{} + s.GetSBOM() + s = nil + s.GetSBOM() +} + +func TestSBOMInfo_GetCreationInfo(tt *testing.T) { + s := &SBOMInfo{} + s.GetCreationInfo() + s = nil + s.GetCreationInfo() +} + +func TestSBOMInfo_GetDataLicense(tt *testing.T) { + var zeroValue string + s := &SBOMInfo{DataLicense: &zeroValue} + s.GetDataLicense() + s = &SBOMInfo{} + s.GetDataLicense() + s = nil + s.GetDataLicense() +} + +func TestSBOMInfo_GetDocumentNamespace(tt *testing.T) { + var zeroValue string + s := &SBOMInfo{DocumentNamespace: &zeroValue} + s.GetDocumentNamespace() + s = &SBOMInfo{} + s.GetDocumentNamespace() + s = nil + s.GetDocumentNamespace() +} + +func TestSBOMInfo_GetName(tt *testing.T) { + var zeroValue string + s := &SBOMInfo{Name: &zeroValue} + s.GetName() + s = &SBOMInfo{} + s.GetName() + s = nil + s.GetName() +} + +func TestSBOMInfo_GetSPDXID(tt *testing.T) { + var zeroValue string + s := &SBOMInfo{SPDXID: &zeroValue} + s.GetSPDXID() + s = &SBOMInfo{} + s.GetSPDXID() + s = nil + s.GetSPDXID() +} + +func TestSBOMInfo_GetSPDXVersion(tt *testing.T) { + var zeroValue string + s := &SBOMInfo{SPDXVersion: &zeroValue} + s.GetSPDXVersion() + s = &SBOMInfo{} + s.GetSPDXVersion() + s = nil + s.GetSPDXVersion() +} + func TestScanningAnalysis_GetAnalysisKey(tt *testing.T) { var zeroValue string s := &ScanningAnalysis{AnalysisKey: &zeroValue} diff --git a/github/github-stringify_test.go b/github/github-stringify_test.go index a8a7b6f0b1..925e4563c0 100644 --- a/github/github-stringify_test.go +++ b/github/github-stringify_test.go @@ -1763,6 +1763,16 @@ func TestRepositoryRelease_String(t *testing.T) { } } +func TestSBOM_String(t *testing.T) { + v := SBOM{ + SBOM: &SBOMInfo{}, + } + want := `github.SBOM{SBOM:github.SBOMInfo{}}` + if got := v.String(); got != want { + t.Errorf("SBOM.String = %v, want %v", got, want) + } +} + func TestSSHSigningKey_String(t *testing.T) { v := SSHSigningKey{ ID: Int64(0), diff --git a/github/github.go b/github/github.go index 39f4479d4a..2b8780b502 100644 --- a/github/github.go +++ b/github/github.go @@ -186,6 +186,7 @@ type Client struct { CodeScanning *CodeScanningService Codespaces *CodespacesService Dependabot *DependabotService + DependencyGraph *DependencyGraphService Enterprise *EnterpriseService Gists *GistsService Git *GitService @@ -326,6 +327,7 @@ func NewClient(httpClient *http.Client) *Client { c.CodeScanning = (*CodeScanningService)(&c.common) c.Codespaces = (*CodespacesService)(&c.common) c.Dependabot = (*DependabotService)(&c.common) + c.DependencyGraph = (*DependencyGraphService)(&c.common) c.Enterprise = (*EnterpriseService)(&c.common) c.Gists = (*GistsService)(&c.common) c.Git = (*GitService)(&c.common)