mirror of
https://github.com/ossf/scorecard.git
synced 2024-09-11 08:55:27 +03:00
✨ Gitlab support (#2265)
* updated readme to reflect gitlab usage * bugfixes after a good deal of testing * removed unnecessary files from branch * cleaning up my mess * requested changes + unit tests * style fixes * updated readme to reflect gitlab usage * bugfixes after a good deal of testing * removed unnecessary files from branch * cleaning up my mess * requested changes + unit tests * style fixes * merge main into gitlab_support * check-linter fixes Signed-off-by: Nathaniel Wert <N8.Wert.B@gmail.com> Co-authored-by: nathaniel.wert <nathaniel.wert@kudelskisecurity.com>
This commit is contained in:
parent
a6983edf6e
commit
0f87094997
196
clients/gitlabrepo/branches.go
Normal file
196
clients/gitlabrepo/branches.go
Normal file
@ -0,0 +1,196 @@
|
||||
// Copyright 2022 Security Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gitlabrepo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
)
|
||||
|
||||
type branchesHandler struct {
|
||||
glClient *gitlab.Client
|
||||
once *sync.Once
|
||||
errSetup error
|
||||
repourl *repoURL
|
||||
defaultBranchRef *clients.BranchRef
|
||||
}
|
||||
|
||||
func (handler *branchesHandler) init(repourl *repoURL) {
|
||||
handler.repourl = repourl
|
||||
handler.errSetup = nil
|
||||
handler.once = new(sync.Once)
|
||||
}
|
||||
|
||||
// nolint: nestif
|
||||
func (handler *branchesHandler) setup() error {
|
||||
handler.once.Do(func() {
|
||||
if !strings.EqualFold(handler.repourl.commitSHA, clients.HeadSHA) {
|
||||
handler.errSetup = fmt.Errorf("%w: branches only supported for HEAD queries", clients.ErrUnsupportedFeature)
|
||||
return
|
||||
}
|
||||
|
||||
proj, _, err := handler.glClient.Projects.GetProject(handler.repourl.projectID, &gitlab.GetProjectOptions{})
|
||||
if err != nil {
|
||||
handler.errSetup = fmt.Errorf("requirest for project failed with error %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
branch, _, err := handler.glClient.Branches.GetBranch(handler.repourl.projectID, proj.DefaultBranch)
|
||||
if err != nil {
|
||||
handler.errSetup = fmt.Errorf("request for default branch failed with error %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
if branch.Protected {
|
||||
protectedBranch, resp, err := handler.glClient.ProtectedBranches.GetProtectedBranch(
|
||||
handler.repourl.projectID, branch.Name)
|
||||
if err != nil && resp.StatusCode != 403 {
|
||||
handler.errSetup = fmt.Errorf("request for protected branch failed with error %w", err)
|
||||
return
|
||||
} else if resp.StatusCode == 403 {
|
||||
handler.errSetup = fmt.Errorf("incorrect permissions to fully check branch protection %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
projectStatusChecks, resp, err := handler.glClient.ExternalStatusChecks.ListProjectStatusChecks(
|
||||
handler.repourl.projectID, &gitlab.ListOptions{})
|
||||
if err != nil && resp.StatusCode != 404 {
|
||||
handler.errSetup = fmt.Errorf("request for external status checks failed with error %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
projectApprovalRule, resp, err := handler.glClient.Projects.GetApprovalConfiguration(handler.repourl.projectID)
|
||||
if err != nil && resp.StatusCode != 404 {
|
||||
handler.errSetup = fmt.Errorf("request for project approval rule failed with %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
handler.defaultBranchRef = makeBranchRefFrom(branch, protectedBranch,
|
||||
projectStatusChecks, projectApprovalRule)
|
||||
} else {
|
||||
handler.defaultBranchRef = &clients.BranchRef{
|
||||
Name: &branch.Name,
|
||||
Protected: &branch.Protected,
|
||||
}
|
||||
}
|
||||
handler.errSetup = nil
|
||||
})
|
||||
return handler.errSetup
|
||||
}
|
||||
|
||||
func (handler *branchesHandler) getDefaultBranch() (*clients.BranchRef, error) {
|
||||
err := handler.setup()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error during branchesHandler.setup: %w", err)
|
||||
}
|
||||
|
||||
return handler.defaultBranchRef, nil
|
||||
}
|
||||
|
||||
func (handler *branchesHandler) getBranch(branch string) (*clients.BranchRef, error) {
|
||||
bran, _, err := handler.glClient.Branches.GetBranch(handler.repourl.projectID, branch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting branch in branchsHandler.getBranch: %w", err)
|
||||
}
|
||||
|
||||
if bran.Protected {
|
||||
protectedBranch, _, err := handler.glClient.ProtectedBranches.GetProtectedBranch(handler.repourl.projectID, bran.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request for protected branch failed with error %w", err)
|
||||
}
|
||||
|
||||
projectStatusChecks, resp, err := handler.glClient.ExternalStatusChecks.ListProjectStatusChecks(
|
||||
handler.repourl.projectID, &gitlab.ListOptions{})
|
||||
if err != nil && resp.StatusCode != 404 {
|
||||
return nil, fmt.Errorf("request for external status checks failed with error %w", err)
|
||||
}
|
||||
|
||||
projectApprovalRule, resp, err := handler.glClient.Projects.GetApprovalConfiguration(handler.repourl.projectID)
|
||||
if err != nil && resp.StatusCode != 404 {
|
||||
return nil, fmt.Errorf("request for project approval rule failed with %w", err)
|
||||
}
|
||||
|
||||
return makeBranchRefFrom(bran, protectedBranch, projectStatusChecks, projectApprovalRule), nil
|
||||
} else {
|
||||
ret := &clients.BranchRef{
|
||||
Name: &bran.Name,
|
||||
Protected: &bran.Protected,
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
}
|
||||
|
||||
func makeContextsFromResp(checks []*gitlab.ProjectStatusCheck) []string {
|
||||
ret := make([]string, len(checks))
|
||||
for i, statusCheck := range checks {
|
||||
ret[i] = statusCheck.Name
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func makeBranchRefFrom(branch *gitlab.Branch, protectedBranch *gitlab.ProtectedBranch,
|
||||
projectStatusChecks []*gitlab.ProjectStatusCheck,
|
||||
projectApprovalRule *gitlab.ProjectApprovals,
|
||||
) *clients.BranchRef {
|
||||
requiresStatusChecks := newFalse()
|
||||
if len(projectStatusChecks) > 0 {
|
||||
requiresStatusChecks = newTrue()
|
||||
}
|
||||
|
||||
statusChecksRule := clients.StatusChecksRule{
|
||||
UpToDateBeforeMerge: newTrue(),
|
||||
RequiresStatusChecks: requiresStatusChecks,
|
||||
Contexts: makeContextsFromResp(projectStatusChecks),
|
||||
}
|
||||
|
||||
pullRequestReviewRule := clients.PullRequestReviewRule{
|
||||
DismissStaleReviews: newTrue(),
|
||||
RequireCodeOwnerReviews: &protectedBranch.CodeOwnerApprovalRequired,
|
||||
}
|
||||
|
||||
if projectApprovalRule != nil {
|
||||
requiredApprovalNum := int32(projectApprovalRule.ApprovalsBeforeMerge)
|
||||
pullRequestReviewRule.RequiredApprovingReviewCount = &requiredApprovalNum
|
||||
}
|
||||
|
||||
ret := &clients.BranchRef{
|
||||
Name: &branch.Name,
|
||||
Protected: &branch.Protected,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: pullRequestReviewRule,
|
||||
AllowDeletions: newFalse(),
|
||||
AllowForcePushes: &protectedBranch.AllowForcePush,
|
||||
EnforceAdmins: newTrue(),
|
||||
CheckRules: statusChecksRule,
|
||||
},
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func newTrue() *bool {
|
||||
b := true
|
||||
return &b
|
||||
}
|
||||
|
||||
func newFalse() *bool {
|
||||
b := false
|
||||
return &b
|
||||
}
|
59
clients/gitlabrepo/checkruns.go
Normal file
59
clients/gitlabrepo/checkruns.go
Normal file
@ -0,0 +1,59 @@
|
||||
// Copyright 2022 Security Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gitlabrepo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
)
|
||||
|
||||
type checkrunsHandler struct {
|
||||
glClient *gitlab.Client
|
||||
repourl *repoURL
|
||||
}
|
||||
|
||||
func (handler *checkrunsHandler) init(repourl *repoURL) {
|
||||
handler.repourl = repourl
|
||||
}
|
||||
|
||||
func (handler *checkrunsHandler) listCheckRunsForRef(ref string) ([]clients.CheckRun, error) {
|
||||
pipelines, _, err := handler.glClient.Pipelines.ListProjectPipelines(
|
||||
handler.repourl.projectID, &gitlab.ListProjectPipelinesOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request for pipelines returned error: %w", err)
|
||||
}
|
||||
|
||||
return checkRunsFrom(pipelines, ref), nil
|
||||
}
|
||||
|
||||
// Conclusion does not exist in the pipelines for gitlab so I have a placeholder "" as the value.
|
||||
func checkRunsFrom(data []*gitlab.PipelineInfo, ref string) []clients.CheckRun {
|
||||
var checkRuns []clients.CheckRun
|
||||
for _, pipelineInfo := range data {
|
||||
if strings.EqualFold(pipelineInfo.Ref, ref) {
|
||||
checkRuns = append(checkRuns, clients.CheckRun{
|
||||
Status: pipelineInfo.Status,
|
||||
Conclusion: "",
|
||||
URL: pipelineInfo.WebURL,
|
||||
App: clients.CheckRunApp{Slug: pipelineInfo.Source},
|
||||
})
|
||||
}
|
||||
}
|
||||
return checkRuns
|
||||
}
|
261
clients/gitlabrepo/client.go
Normal file
261
clients/gitlabrepo/client.go
Normal file
@ -0,0 +1,261 @@
|
||||
// Copyright 2022 Security Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package gitlabrepo implements clients.RepoClient for GitLab.
|
||||
package gitlabrepo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
sce "github.com/ossf/scorecard/v4/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
_ clients.RepoClient = &Client{}
|
||||
errInputRepoType = errors.New("input repo should be of type repoURL")
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
repourl *repoURL
|
||||
repo *gitlab.Project
|
||||
glClient *gitlab.Client
|
||||
contributors *contributorsHandler
|
||||
branches *branchesHandler
|
||||
releases *releasesHandler
|
||||
workflows *workflowsHandler
|
||||
checkruns *checkrunsHandler
|
||||
commits *commitsHandler
|
||||
issues *issuesHandler
|
||||
project *projectHandler
|
||||
statuses *statusesHandler
|
||||
search *searchHandler
|
||||
searchCommits *searchCommitsHandler
|
||||
webhook *webhookHandler
|
||||
languages *languagesHandler
|
||||
ctx context.Context
|
||||
// tarball tarballHandler
|
||||
}
|
||||
|
||||
// InitRepo sets up the GitLab project in local storage for improving performance and GitLab token usage efficiency.
|
||||
func (client *Client) InitRepo(inputRepo clients.Repo, commitSHA string) error {
|
||||
glRepo, ok := inputRepo.(*repoURL)
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: %v", errInputRepoType, inputRepo)
|
||||
}
|
||||
|
||||
// Sanity check.
|
||||
repo, _, err := client.glClient.Projects.GetProject(glRepo.projectID, &gitlab.GetProjectOptions{})
|
||||
if err != nil {
|
||||
return sce.WithMessage(sce.ErrRepoUnreachable, err.Error())
|
||||
}
|
||||
|
||||
client.repo = repo
|
||||
|
||||
client.repourl = &repoURL{
|
||||
hostname: inputRepo.URI(),
|
||||
projectID: fmt.Sprint(repo.ID),
|
||||
defaultBranch: repo.DefaultBranch,
|
||||
commitSHA: commitSHA,
|
||||
}
|
||||
|
||||
if repo.Owner != nil {
|
||||
client.repourl.owner = repo.Owner.Name
|
||||
}
|
||||
|
||||
// Init contributorsHandler
|
||||
client.contributors.init(client.repourl)
|
||||
|
||||
// Init commitsHandler
|
||||
client.commits.init(client.repourl)
|
||||
|
||||
// Init branchesHandler
|
||||
client.branches.init(client.repourl)
|
||||
|
||||
// Init releasesHandler
|
||||
client.releases.init(client.repourl)
|
||||
|
||||
// Init issuesHandler
|
||||
client.issues.init(client.repourl)
|
||||
|
||||
// Init projectHandler
|
||||
client.project.init(client.repourl)
|
||||
|
||||
// Init workflowsHandler
|
||||
client.workflows.init(client.repourl)
|
||||
|
||||
// Init checkrunsHandler
|
||||
client.checkruns.init(client.repourl)
|
||||
|
||||
// Init statusesHandler
|
||||
client.statuses.init(client.repourl)
|
||||
|
||||
// Init searchHandler
|
||||
client.search.init(client.repourl)
|
||||
|
||||
// Init searchCommitsHandler
|
||||
client.searchCommits.init(client.repourl)
|
||||
|
||||
// Init webhookHandler
|
||||
client.webhook.init(client.repourl)
|
||||
|
||||
// Init languagesHandler
|
||||
client.languages.init(client.repourl)
|
||||
|
||||
// Init tarballHandler.
|
||||
// client.tarball.init(client.ctx, client.repourl, client.repo, commitSHA)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *Client) URI() string {
|
||||
return fmt.Sprintf("%s/%s/%s", client.repourl.hostname, client.repourl.owner, client.repourl.projectID)
|
||||
}
|
||||
|
||||
func (client *Client) ListFiles(predicate func(string) (bool, error)) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (client *Client) GetFileContent(filename string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (client *Client) ListCommits() ([]clients.Commit, error) {
|
||||
return client.commits.listCommits()
|
||||
}
|
||||
|
||||
func (client *Client) ListIssues() ([]clients.Issue, error) {
|
||||
return client.issues.listIssues()
|
||||
}
|
||||
|
||||
func (client *Client) ListReleases() ([]clients.Release, error) {
|
||||
return client.releases.getReleases()
|
||||
}
|
||||
|
||||
func (client *Client) ListContributors() ([]clients.User, error) {
|
||||
return client.contributors.getContributors()
|
||||
}
|
||||
|
||||
func (client *Client) IsArchived() (bool, error) {
|
||||
return client.project.isArchived()
|
||||
}
|
||||
|
||||
func (client *Client) GetDefaultBranch() (*clients.BranchRef, error) {
|
||||
return client.branches.getDefaultBranch()
|
||||
}
|
||||
|
||||
func (client *Client) GetDefaultBranchName() (string, error) {
|
||||
return client.repourl.defaultBranch, nil
|
||||
}
|
||||
|
||||
func (client *Client) GetBranch(branch string) (*clients.BranchRef, error) {
|
||||
return client.branches.getBranch(branch)
|
||||
}
|
||||
|
||||
func (client *Client) GetCreatedAt() (time.Time, error) {
|
||||
return client.project.getCreatedAt()
|
||||
}
|
||||
|
||||
func (client *Client) ListWebhooks() ([]clients.Webhook, error) {
|
||||
return client.webhook.listWebhooks()
|
||||
}
|
||||
|
||||
func (client *Client) ListSuccessfulWorkflowRuns(filename string) ([]clients.WorkflowRun, error) {
|
||||
return client.workflows.listSuccessfulWorkflowRuns(filename)
|
||||
}
|
||||
|
||||
func (client *Client) ListCheckRunsForRef(ref string) ([]clients.CheckRun, error) {
|
||||
return client.checkruns.listCheckRunsForRef(ref)
|
||||
}
|
||||
|
||||
func (client *Client) ListStatuses(ref string) ([]clients.Status, error) {
|
||||
return client.statuses.listStatuses(ref)
|
||||
}
|
||||
|
||||
func (client *Client) ListProgrammingLanguages() ([]clients.Language, error) {
|
||||
return client.languages.listProgrammingLanguages()
|
||||
}
|
||||
|
||||
func (client *Client) Search(request clients.SearchRequest) (clients.SearchResponse, error) {
|
||||
return client.search.search(request)
|
||||
}
|
||||
|
||||
func (client *Client) SearchCommits(request clients.SearchCommitsOptions) ([]clients.Commit, error) {
|
||||
return client.searchCommits.search(request)
|
||||
}
|
||||
|
||||
func (client *Client) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateGitlabClientWithToken(ctx context.Context, token string, repo clients.Repo) (clients.RepoClient, error) {
|
||||
client, err := gitlab.NewClient(token, gitlab.WithBaseURL(repo.URI()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create gitlab client with error: %w", err)
|
||||
}
|
||||
|
||||
return &Client{
|
||||
ctx: ctx,
|
||||
glClient: client,
|
||||
contributors: &contributorsHandler{
|
||||
glClient: client,
|
||||
},
|
||||
branches: &branchesHandler{
|
||||
glClient: client,
|
||||
},
|
||||
releases: &releasesHandler{
|
||||
glClient: client,
|
||||
},
|
||||
workflows: &workflowsHandler{
|
||||
glClient: client,
|
||||
},
|
||||
checkruns: &checkrunsHandler{
|
||||
glClient: client,
|
||||
},
|
||||
commits: &commitsHandler{
|
||||
glClient: client,
|
||||
},
|
||||
issues: &issuesHandler{
|
||||
glClient: client,
|
||||
},
|
||||
project: &projectHandler{
|
||||
glClient: client,
|
||||
},
|
||||
statuses: &statusesHandler{
|
||||
glClient: client,
|
||||
},
|
||||
search: &searchHandler{
|
||||
glClient: client,
|
||||
},
|
||||
searchCommits: &searchCommitsHandler{
|
||||
glClient: client,
|
||||
},
|
||||
webhook: &webhookHandler{
|
||||
glClient: client,
|
||||
},
|
||||
languages: &languagesHandler{
|
||||
glClient: client,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TODO(#2266): implement CreateOssFuzzRepoClient.
|
||||
func CreateOssFuzzRepoClient(ctx context.Context, logger *log.Logger) (clients.RepoClient, error) {
|
||||
return nil, fmt.Errorf("%w, oss fuzz currently only supported for github repos", clients.ErrUnsupportedFeature)
|
||||
}
|
165
clients/gitlabrepo/commits.go
Normal file
165
clients/gitlabrepo/commits.go
Normal file
@ -0,0 +1,165 @@
|
||||
// Copyright 2022 Security Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gitlabrepo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
)
|
||||
|
||||
type commitsHandler struct {
|
||||
glClient *gitlab.Client
|
||||
once *sync.Once
|
||||
errSetup error
|
||||
repourl *repoURL
|
||||
commits []clients.Commit
|
||||
}
|
||||
|
||||
func (handler *commitsHandler) init(repourl *repoURL) {
|
||||
handler.repourl = repourl
|
||||
handler.errSetup = nil
|
||||
handler.once = new(sync.Once)
|
||||
}
|
||||
|
||||
// nolint: gocognit
|
||||
func (handler *commitsHandler) setup() error {
|
||||
handler.once.Do(func() {
|
||||
commits, _, err := handler.glClient.Commits.ListCommits(handler.repourl.projectID, &gitlab.ListCommitsOptions{})
|
||||
if err != nil {
|
||||
handler.errSetup = fmt.Errorf("request for commits failed with %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// To limit the number of user requests we are going to map every committer email
|
||||
// to a user.
|
||||
userToEmail := make(map[string]*gitlab.User)
|
||||
for _, commit := range commits {
|
||||
user, ok := userToEmail[commit.AuthorEmail]
|
||||
if !ok {
|
||||
users, _, err := handler.glClient.Search.Users(commit.CommitterName, &gitlab.SearchOptions{})
|
||||
if err != nil {
|
||||
// Possibility this shouldn't be an issue as individuals can leave organizations
|
||||
// (possibly taking their account with them)
|
||||
handler.errSetup = fmt.Errorf("unable to find user associated with commit: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// For some reason some users have unknown names, so below we are going to parse their email into pieces.
|
||||
// i.e. (firstname.lastname@domain.com) -> "firstname lastname".
|
||||
if len(users) == 0 {
|
||||
users, _, err = handler.glClient.Search.Users(parseEmailToName(commit.CommitterEmail), &gitlab.SearchOptions{})
|
||||
if err != nil {
|
||||
handler.errSetup = fmt.Errorf("unable to find user associated with commit: %w", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
userToEmail[commit.AuthorEmail] = users[0]
|
||||
user = users[0]
|
||||
}
|
||||
|
||||
// Commits are able to be a part of multiple merge requests, but the only one that will be important
|
||||
// here is the earliest one.
|
||||
mergeRequests, _, err := handler.glClient.Commits.ListMergeRequestsByCommit(handler.repourl.projectID, commit.ID)
|
||||
if err != nil {
|
||||
handler.errSetup = fmt.Errorf("unable to find merge requests associated with commit: %w", err)
|
||||
return
|
||||
}
|
||||
var mergeRequest *gitlab.MergeRequest
|
||||
if len(mergeRequests) > 0 {
|
||||
mergeRequest = mergeRequests[0]
|
||||
for i := range mergeRequests {
|
||||
if mergeRequests[i] == nil || mergeRequests[i].MergedAt == nil {
|
||||
continue
|
||||
}
|
||||
if mergeRequests[i].CreatedAt.Before(*mergeRequest.CreatedAt) {
|
||||
mergeRequest = mergeRequests[i]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handler.commits = append(handler.commits, clients.Commit{
|
||||
CommittedDate: *commit.CommittedDate,
|
||||
Message: commit.Message,
|
||||
SHA: commit.ID,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if mergeRequest == nil || mergeRequest.MergedAt == nil {
|
||||
handler.commits = append(handler.commits, clients.Commit{
|
||||
CommittedDate: *commit.CommittedDate,
|
||||
Message: commit.Message,
|
||||
SHA: commit.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// Casting the Reviewers into clients.Review.
|
||||
var reviews []clients.Review
|
||||
for _, reviewer := range mergeRequest.Reviewers {
|
||||
reviews = append(reviews, clients.Review{
|
||||
Author: &clients.User{ID: int64(reviewer.ID)},
|
||||
State: "",
|
||||
})
|
||||
}
|
||||
|
||||
// Casting the Labels into []clients.Label.
|
||||
var labels []clients.Label
|
||||
for _, label := range mergeRequest.Labels {
|
||||
labels = append(labels, clients.Label{
|
||||
Name: label,
|
||||
})
|
||||
}
|
||||
|
||||
// append the commits to the handler.
|
||||
handler.commits = append(handler.commits,
|
||||
clients.Commit{
|
||||
CommittedDate: *commit.CommittedDate,
|
||||
Message: commit.Message,
|
||||
SHA: commit.ID,
|
||||
AssociatedMergeRequest: clients.PullRequest{
|
||||
Number: mergeRequest.ID,
|
||||
MergedAt: *mergeRequest.MergedAt,
|
||||
HeadSHA: mergeRequest.SHA,
|
||||
Author: clients.User{ID: int64(mergeRequest.Author.ID)},
|
||||
Labels: labels,
|
||||
Reviews: reviews,
|
||||
},
|
||||
Committer: clients.User{ID: int64(user.ID)},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return handler.errSetup
|
||||
}
|
||||
|
||||
func (handler *commitsHandler) listCommits() ([]clients.Commit, error) {
|
||||
if err := handler.setup(); err != nil {
|
||||
return nil, fmt.Errorf("error during commitsHandler.setup: %w", err)
|
||||
}
|
||||
|
||||
return handler.commits, nil
|
||||
}
|
||||
|
||||
// Expected email form: <firstname>.<lastname>@<namespace>.com.
|
||||
func parseEmailToName(email string) string {
|
||||
s := strings.Split(email, ".")
|
||||
firstName := s[0]
|
||||
lastName := strings.Split(s[1], "@")[0]
|
||||
return firstName + " " + lastName
|
||||
}
|
92
clients/gitlabrepo/contributors.go
Normal file
92
clients/gitlabrepo/contributors.go
Normal file
@ -0,0 +1,92 @@
|
||||
// Copyright 2022 Security Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gitlabrepo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
)
|
||||
|
||||
type contributorsHandler struct {
|
||||
glClient *gitlab.Client
|
||||
once *sync.Once
|
||||
errSetup error
|
||||
repourl *repoURL
|
||||
contributors []clients.User
|
||||
}
|
||||
|
||||
func (handler *contributorsHandler) init(repourl *repoURL) {
|
||||
handler.repourl = repourl
|
||||
handler.errSetup = nil
|
||||
handler.once = new(sync.Once)
|
||||
}
|
||||
|
||||
func (handler *contributorsHandler) setup() error {
|
||||
handler.once.Do(func() {
|
||||
if !strings.EqualFold(handler.repourl.commitSHA, clients.HeadSHA) {
|
||||
handler.errSetup = fmt.Errorf("%w: ListContributors only supported for HEAD queries",
|
||||
clients.ErrUnsupportedFeature)
|
||||
return
|
||||
}
|
||||
contribs, _, err := handler.glClient.Repositories.Contributors(
|
||||
handler.repourl.projectID, &gitlab.ListContributorsOptions{})
|
||||
if err != nil {
|
||||
handler.errSetup = fmt.Errorf("error during ListContributors: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, contrib := range contribs {
|
||||
if contrib.Name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// In Gitlab users only have one registered organization which is the company they work for, this means that
|
||||
// the organizations field will not be filled in and the companies field will be a singular value.
|
||||
users, _, err := handler.glClient.Search.Users(contrib.Name, &gitlab.SearchOptions{})
|
||||
if err != nil {
|
||||
handler.errSetup = fmt.Errorf("error during Users.Get: %w", err)
|
||||
return
|
||||
} else if len(users) == 0 {
|
||||
// parseEmailToName is declared in commits.go
|
||||
users, _, err = handler.glClient.Search.Users(parseEmailToName(contrib.Email), &gitlab.SearchOptions{})
|
||||
if err != nil {
|
||||
handler.errSetup = fmt.Errorf("error during Users.Get: %w", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
contributor := clients.User{
|
||||
Login: contrib.Email,
|
||||
Companies: []string{users[0].Organization},
|
||||
NumContributions: contrib.Commits,
|
||||
ID: int64(users[0].ID),
|
||||
}
|
||||
handler.contributors = append(handler.contributors, contributor)
|
||||
}
|
||||
})
|
||||
return handler.errSetup
|
||||
}
|
||||
|
||||
func (handler *contributorsHandler) getContributors() ([]clients.User, error) {
|
||||
if err := handler.setup(); err != nil {
|
||||
return nil, fmt.Errorf("error during contributorsHandler.setup: %w", err)
|
||||
}
|
||||
return handler.contributors, nil
|
||||
}
|
118
clients/gitlabrepo/issues.go
Normal file
118
clients/gitlabrepo/issues.go
Normal file
@ -0,0 +1,118 @@
|
||||
// Copyright 2022 Security Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gitlabrepo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
)
|
||||
|
||||
type issuesHandler struct {
|
||||
glClient *gitlab.Client
|
||||
once *sync.Once
|
||||
errSetup error
|
||||
repourl *repoURL
|
||||
issues []clients.Issue
|
||||
}
|
||||
|
||||
func (handler *issuesHandler) init(repourl *repoURL) {
|
||||
handler.repourl = repourl
|
||||
handler.errSetup = nil
|
||||
handler.once = new(sync.Once)
|
||||
}
|
||||
|
||||
func (handler *issuesHandler) setup() error {
|
||||
handler.once.Do(func() {
|
||||
issues, _, err := handler.glClient.Issues.ListProjectIssues(
|
||||
handler.repourl.projectID, &gitlab.ListProjectIssuesOptions{})
|
||||
if err != nil {
|
||||
handler.errSetup = fmt.Errorf("unable to find issues associated with the project id: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// There doesn't seem to be a good way to get user access_levels in gitlab so the following way may seem incredibly
|
||||
// barberic, however I couldn't find a better way in the docs.
|
||||
projectAccessTokens, resp, err := handler.glClient.ProjectAccessTokens.ListProjectAccessTokens(
|
||||
handler.repourl.projectID, &gitlab.ListProjectAccessTokensOptions{})
|
||||
if err != nil && resp.StatusCode != 401 {
|
||||
handler.errSetup = fmt.Errorf("unable to find access tokens associated with the project id: %w", err)
|
||||
return
|
||||
} else if resp.StatusCode == 401 {
|
||||
handler.errSetup = fmt.Errorf("insufficient permissions to check issue author associations %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(issues) > 0 {
|
||||
for _, issue := range issues {
|
||||
authorAssociation := clients.RepoAssociationMember
|
||||
if resp.StatusCode != 401 {
|
||||
authorAssociation = findAuthorAssociationFromUserID(projectAccessTokens, issue.Author.ID)
|
||||
}
|
||||
issueIDString := fmt.Sprint(issue.ID)
|
||||
handler.issues = append(handler.issues,
|
||||
clients.Issue{
|
||||
URI: &issueIDString,
|
||||
CreatedAt: issue.CreatedAt,
|
||||
Author: &clients.User{
|
||||
ID: int64(issue.Author.ID),
|
||||
},
|
||||
AuthorAssociation: &authorAssociation,
|
||||
Comments: nil,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
handler.issues = nil
|
||||
}
|
||||
})
|
||||
return handler.errSetup
|
||||
}
|
||||
|
||||
func (handler *issuesHandler) listIssues() ([]clients.Issue, error) {
|
||||
if err := handler.setup(); err != nil {
|
||||
return nil, fmt.Errorf("error during issuesHandler.setup: %w", err)
|
||||
}
|
||||
|
||||
return handler.issues, nil
|
||||
}
|
||||
|
||||
func findAuthorAssociationFromUserID(accessTokens []*gitlab.ProjectAccessToken, targetID int) clients.RepoAssociation {
|
||||
for _, accessToken := range accessTokens {
|
||||
if accessToken.UserID == targetID {
|
||||
switch accessToken.AccessLevel {
|
||||
case 0:
|
||||
return clients.RepoAssociationNone
|
||||
case 5:
|
||||
return clients.RepoAssociationFirstTimeContributor
|
||||
case 10:
|
||||
return clients.RepoAssociationCollaborator
|
||||
case 20:
|
||||
return clients.RepoAssociationCollaborator
|
||||
case 30:
|
||||
return clients.RepoAssociationMember
|
||||
case 40:
|
||||
return clients.RepoAssociationMaintainer
|
||||
case 50:
|
||||
return clients.RepoAssociationOwner
|
||||
default:
|
||||
return clients.RepoAssociationNone
|
||||
}
|
||||
}
|
||||
}
|
||||
return clients.RepoAssociationNone
|
||||
}
|
71
clients/gitlabrepo/languages.go
Normal file
71
clients/gitlabrepo/languages.go
Normal file
@ -0,0 +1,71 @@
|
||||
// Copyright 2022 Security Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gitlabrepo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
)
|
||||
|
||||
type languagesHandler struct {
|
||||
glClient *gitlab.Client
|
||||
once *sync.Once
|
||||
errSetup error
|
||||
repourl *repoURL
|
||||
languages []clients.Language
|
||||
}
|
||||
|
||||
func (handler *languagesHandler) init(repourl *repoURL) {
|
||||
handler.repourl = repourl
|
||||
handler.errSetup = nil
|
||||
handler.once = new(sync.Once)
|
||||
}
|
||||
|
||||
func (handler *languagesHandler) setup() error {
|
||||
handler.once.Do(func() {
|
||||
client := handler.glClient
|
||||
languageMap, _, err := client.Projects.GetProjectLanguages(handler.repourl.projectID)
|
||||
if err != nil || languageMap == nil {
|
||||
handler.errSetup = fmt.Errorf("request for repo languages failed with %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(#2266): find number of lines of gitlab project and multiple the value of each language by that number.
|
||||
for k, v := range *languageMap {
|
||||
handler.languages = append(handler.languages,
|
||||
clients.Language{
|
||||
Name: clients.LanguageName(k),
|
||||
NumLines: int(v * 100),
|
||||
},
|
||||
)
|
||||
}
|
||||
handler.errSetup = nil
|
||||
})
|
||||
|
||||
return handler.errSetup
|
||||
}
|
||||
|
||||
// Currently listProgrammingLanguages() returns the percentages (truncated) of each language in the project.
|
||||
func (handler *languagesHandler) listProgrammingLanguages() ([]clients.Language, error) {
|
||||
if err := handler.setup(); err != nil {
|
||||
return nil, fmt.Errorf("error during languagesHandler.setup: %w", err)
|
||||
}
|
||||
|
||||
return handler.languages, nil
|
||||
}
|
69
clients/gitlabrepo/project.go
Normal file
69
clients/gitlabrepo/project.go
Normal file
@ -0,0 +1,69 @@
|
||||
// Copyright 2022 Security Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gitlabrepo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
)
|
||||
|
||||
type projectHandler struct {
|
||||
glClient *gitlab.Client
|
||||
once *sync.Once
|
||||
errSetup error
|
||||
repourl *repoURL
|
||||
createdAt time.Time
|
||||
archived bool
|
||||
}
|
||||
|
||||
func (handler *projectHandler) init(repourl *repoURL) {
|
||||
handler.repourl = repourl
|
||||
handler.errSetup = nil
|
||||
handler.once = new(sync.Once)
|
||||
}
|
||||
|
||||
func (handler *projectHandler) setup() error {
|
||||
handler.once.Do(func() {
|
||||
proj, _, err := handler.glClient.Projects.GetProject(handler.repourl.projectID, &gitlab.GetProjectOptions{})
|
||||
if err != nil {
|
||||
handler.errSetup = fmt.Errorf("request for project failed with error %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
handler.createdAt = *proj.CreatedAt
|
||||
handler.archived = proj.Archived
|
||||
})
|
||||
|
||||
return handler.errSetup
|
||||
}
|
||||
|
||||
func (handler *projectHandler) isArchived() (bool, error) {
|
||||
if err := handler.setup(); err != nil {
|
||||
return true, fmt.Errorf("error during projectHandler.setup: %w", err)
|
||||
}
|
||||
|
||||
return handler.archived, nil
|
||||
}
|
||||
|
||||
func (handler *projectHandler) getCreatedAt() (time.Time, error) {
|
||||
if err := handler.setup(); err != nil {
|
||||
return time.Now(), fmt.Errorf("error during projectHandler.setup: %w", err)
|
||||
}
|
||||
|
||||
return handler.createdAt, nil
|
||||
}
|
85
clients/gitlabrepo/releases.go
Normal file
85
clients/gitlabrepo/releases.go
Normal file
@ -0,0 +1,85 @@
|
||||
// Copyright 2022 Security Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gitlabrepo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
)
|
||||
|
||||
type releasesHandler struct {
|
||||
glClient *gitlab.Client
|
||||
once *sync.Once
|
||||
errSetup error
|
||||
repourl *repoURL
|
||||
releases []clients.Release
|
||||
}
|
||||
|
||||
func (handler *releasesHandler) init(repourl *repoURL) {
|
||||
handler.repourl = repourl
|
||||
handler.errSetup = nil
|
||||
handler.once = new(sync.Once)
|
||||
}
|
||||
|
||||
func (handler *releasesHandler) setup() error {
|
||||
handler.once.Do(func() {
|
||||
if !strings.EqualFold(handler.repourl.commitSHA, clients.HeadSHA) {
|
||||
handler.errSetup = fmt.Errorf("%w: ListReleases only supported for HEAD queries", clients.ErrUnsupportedFeature)
|
||||
return
|
||||
}
|
||||
releases, _, err := handler.glClient.Releases.ListReleases(handler.repourl.projectID, &gitlab.ListReleasesOptions{})
|
||||
if err != nil {
|
||||
handler.errSetup = fmt.Errorf("%w: ListReleases failed", err)
|
||||
return
|
||||
}
|
||||
if len(releases) > 0 {
|
||||
handler.releases = releasesFrom(releases)
|
||||
} else {
|
||||
handler.releases = nil
|
||||
}
|
||||
})
|
||||
return handler.errSetup
|
||||
}
|
||||
|
||||
func (handler *releasesHandler) getReleases() ([]clients.Release, error) {
|
||||
if err := handler.setup(); err != nil {
|
||||
return nil, fmt.Errorf("error during Releases.setup: %w", err)
|
||||
}
|
||||
return handler.releases, nil
|
||||
}
|
||||
|
||||
func releasesFrom(data []*gitlab.Release) []clients.Release {
|
||||
var releases []clients.Release
|
||||
for _, r := range data {
|
||||
release := clients.Release{
|
||||
TagName: r.TagName,
|
||||
URL: r.Assets.Links[0].DirectAssetURL,
|
||||
TargetCommitish: r.CommitPath,
|
||||
}
|
||||
for _, a := range r.Assets.Sources {
|
||||
release.Assets = append(release.Assets, clients.ReleaseAsset{
|
||||
Name: a.Format,
|
||||
URL: a.URL,
|
||||
})
|
||||
}
|
||||
releases = append(releases, release)
|
||||
}
|
||||
return releases
|
||||
}
|
128
clients/gitlabrepo/repo.go
Normal file
128
clients/gitlabrepo/repo.go
Normal file
@ -0,0 +1,128 @@
|
||||
// Copyright 2022 Security Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// NOTE: In Gitlab repositories are called projects, however to ensure compatibility,
|
||||
// this package will regard to Gitlab projects as repositories.
|
||||
package gitlabrepo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
sce "github.com/ossf/scorecard/v4/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
gitlabOrgProj = ".gitlab"
|
||||
)
|
||||
|
||||
type repoURL struct {
|
||||
hostname string
|
||||
owner string
|
||||
projectID string
|
||||
defaultBranch string
|
||||
commitSHA string
|
||||
metadata []string
|
||||
}
|
||||
|
||||
// Parses input string into repoURL struct
|
||||
/*
|
||||
* Accepted input string formats are as follows:
|
||||
* "gitlab.<companyDomain:string>.com/<owner:string>/<projectID:int>"
|
||||
* "https://gitlab.<companyDomain:string>.com/<owner:string>/<projectID:int>"
|
||||
*/
|
||||
func (r *repoURL) parse(input string) error {
|
||||
switch {
|
||||
case strings.Contains(input, "https://"):
|
||||
input = strings.TrimPrefix(input, "https://")
|
||||
case strings.Contains(input, "http://"):
|
||||
input = strings.TrimPrefix(input, "http://")
|
||||
case strings.Contains(input, "://"):
|
||||
return sce.WithMessage(sce.ErrScorecardInternal, "unknown input format")
|
||||
}
|
||||
|
||||
stringParts := strings.Split(input, "/")
|
||||
|
||||
stringParts[2] = strings.TrimSuffix(stringParts[2], "/")
|
||||
|
||||
r.hostname, r.owner, r.projectID = stringParts[0], stringParts[1], stringParts[2]
|
||||
return nil
|
||||
}
|
||||
|
||||
// URI implements Repo.URI().
|
||||
// TODO: there may be a reason the string was originally in format "%s/%s/%s", hostname, owner, projectID,
|
||||
// however I changed it to be more "userful".
|
||||
func (r *repoURL) URI() string {
|
||||
return fmt.Sprintf("https://%s", r.hostname)
|
||||
}
|
||||
|
||||
// String implements Repo.String.
|
||||
func (r *repoURL) String() string {
|
||||
return fmt.Sprintf("%s-%s_%s", r.hostname, r.owner, r.projectID)
|
||||
}
|
||||
|
||||
func (r *repoURL) Org() clients.Repo {
|
||||
return &repoURL{
|
||||
hostname: r.hostname,
|
||||
owner: r.owner,
|
||||
projectID: gitlabOrgProj,
|
||||
}
|
||||
}
|
||||
|
||||
// IsValid implements Repo.IsValid.
|
||||
func (r *repoURL) IsValid() error {
|
||||
hostMatched, err := regexp.MatchString("gitlab.*com", r.hostname)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error processing regex: %w", err)
|
||||
}
|
||||
if !hostMatched {
|
||||
return sce.WithMessage(sce.ErrorInvalidURL, "non gitlab repository found")
|
||||
}
|
||||
|
||||
isNotDigit := func(c rune) bool { return c < '0' || c > '9' }
|
||||
b := strings.IndexFunc(r.projectID, isNotDigit) == -1
|
||||
if !b {
|
||||
return sce.WithMessage(sce.ErrorInvalidURL, "incorrect format for projectID")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(r.owner) == "" || strings.TrimSpace(r.projectID) == "" {
|
||||
return sce.WithMessage(sce.ErrorInvalidURL,
|
||||
fmt.Sprintf("%v. Expected the full project url", r.URI()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repoURL) AppendMetadata(metadata ...string) {
|
||||
r.metadata = append(r.metadata, metadata...)
|
||||
}
|
||||
|
||||
// Metadata implements Repo.Metadata.
|
||||
func (r *repoURL) Metadata() []string {
|
||||
return r.metadata
|
||||
}
|
||||
|
||||
// MakeGitlabRepo takes input of forms in parse and returns and implementation
|
||||
// of clients.Repo interface.
|
||||
func MakeGitlabRepo(input string) (clients.Repo, error) {
|
||||
var repo repoURL
|
||||
if err := repo.parse(input); err != nil {
|
||||
return nil, fmt.Errorf("error during parse: %w", err)
|
||||
}
|
||||
if err := repo.IsValid(); err != nil {
|
||||
return nil, fmt.Errorf("error n IsValid: %w", err)
|
||||
}
|
||||
return &repo, nil
|
||||
}
|
136
clients/gitlabrepo/repo_test.go
Normal file
136
clients/gitlabrepo/repo_test.go
Normal file
@ -0,0 +1,136 @@
|
||||
// Copyright 2022 Security Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gitlabrepo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestRepoURL_IsValid(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
inputURL string
|
||||
expected repoURL
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid http address",
|
||||
expected: repoURL{
|
||||
hostname: "gitlab.example.com",
|
||||
owner: "foo",
|
||||
projectID: "1234",
|
||||
},
|
||||
inputURL: "http://gitlab.example.com/foo/1234",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid https address",
|
||||
expected: repoURL{
|
||||
hostname: "gitlab.example.com",
|
||||
owner: "foo",
|
||||
projectID: "1234",
|
||||
},
|
||||
inputURL: "https://gitlab.example.com/foo/1234",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid http address with trailing slash",
|
||||
expected: repoURL{
|
||||
hostname: "gitlab.example.com",
|
||||
owner: "foo",
|
||||
projectID: "1234",
|
||||
},
|
||||
inputURL: "http://gitlab.example.com/foo/1234/",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid https address with trailing slash",
|
||||
expected: repoURL{
|
||||
hostname: "gitlab.example.com",
|
||||
owner: "foo",
|
||||
projectID: "1234",
|
||||
},
|
||||
inputURL: "https://gitlab.example.com/foo/1234/",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "non gitlab repository",
|
||||
expected: repoURL{
|
||||
hostname: "github.com",
|
||||
owner: "foo",
|
||||
projectID: "1234",
|
||||
},
|
||||
inputURL: "https://github.com/foo/1234",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "GitLab project with wrong projectID",
|
||||
expected: repoURL{
|
||||
hostname: "gitlab.example.com",
|
||||
owner: "foo",
|
||||
projectID: "bar",
|
||||
},
|
||||
inputURL: "https://gitlab.example.com/foo/bar",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "GitHub project with 'gitlab.' in the title",
|
||||
expected: repoURL{
|
||||
hostname: "github.com",
|
||||
owner: "foo",
|
||||
projectID: "gitlab.test",
|
||||
},
|
||||
inputURL: "http://github.com/foo/gitlab.test",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "valid gitlab project without http or https",
|
||||
expected: repoURL{
|
||||
hostname: "gitlab.example.com",
|
||||
owner: "foo",
|
||||
projectID: "1234",
|
||||
},
|
||||
inputURL: "gitlab.example.com/foo/1234",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt // Re-initializing variable so it is not changed while executing the closure blow
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := repoURL{
|
||||
hostname: tt.expected.hostname,
|
||||
owner: tt.expected.owner,
|
||||
projectID: tt.expected.projectID,
|
||||
}
|
||||
if err := r.parse(tt.inputURL); err != nil {
|
||||
t.Errorf("repoURL.parse() error = %v", err)
|
||||
}
|
||||
if err := r.IsValid(); (err != nil) != tt.wantErr {
|
||||
t.Errorf("repoURL.IsValid() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if !tt.wantErr && !cmp.Equal(tt.expected, r, cmp.AllowUnexported(repoURL{})) {
|
||||
fmt.Println("expected: " + tt.expected.hostname + " GOT: " + r.hostname)
|
||||
fmt.Println("expected: " + tt.expected.owner + " GOT: " + r.owner)
|
||||
fmt.Println("expected: " + tt.expected.projectID + " GOT: " + r.projectID)
|
||||
t.Errorf("Got diff: %s", cmp.Diff(tt.expected, r))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
94
clients/gitlabrepo/search.go
Normal file
94
clients/gitlabrepo/search.go
Normal file
@ -0,0 +1,94 @@
|
||||
// Copyright 2022 Security Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gitlabrepo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
)
|
||||
|
||||
var errEmptyQuery = errors.New("search query is empty")
|
||||
|
||||
type searchHandler struct {
|
||||
glClient *gitlab.Client
|
||||
repourl *repoURL
|
||||
}
|
||||
|
||||
func (handler *searchHandler) init(repourl *repoURL) {
|
||||
handler.repourl = repourl
|
||||
}
|
||||
|
||||
func (handler *searchHandler) search(request clients.SearchRequest) (clients.SearchResponse, error) {
|
||||
if !strings.EqualFold(handler.repourl.commitSHA, clients.HeadSHA) {
|
||||
return clients.SearchResponse{}, fmt.Errorf(
|
||||
"%w: Search only supported for HEAD queries", clients.ErrUnsupportedFeature)
|
||||
}
|
||||
query, err := handler.buildQuery(request)
|
||||
if err != nil {
|
||||
return clients.SearchResponse{}, fmt.Errorf("handler.buildQuery: %w", err)
|
||||
}
|
||||
|
||||
blobs, _, err := handler.glClient.Search.BlobsByProject(handler.repourl.projectID, query, &gitlab.SearchOptions{})
|
||||
if err != nil {
|
||||
return clients.SearchResponse{}, fmt.Errorf("Search.BlobsByProject: %w", err)
|
||||
}
|
||||
return searchResponseFrom(blobs), nil
|
||||
}
|
||||
|
||||
func (handler *searchHandler) buildQuery(request clients.SearchRequest) (string, error) {
|
||||
if request.Query == "" {
|
||||
return "", fmt.Errorf("%w", errEmptyQuery)
|
||||
}
|
||||
var queryBuilder strings.Builder
|
||||
if _, err := queryBuilder.WriteString(
|
||||
fmt.Sprintf("%s project:%s/%s",
|
||||
strings.ReplaceAll(request.Query, "/", " "),
|
||||
handler.repourl.owner, handler.repourl.projectID)); err != nil {
|
||||
return "", fmt.Errorf("WriteString: %w", err)
|
||||
}
|
||||
if request.Filename != "" {
|
||||
if _, err := queryBuilder.WriteString(
|
||||
fmt.Sprintf(" in:file filename:%s", request.Filename)); err != nil {
|
||||
return "", fmt.Errorf("WriteString: %w", err)
|
||||
}
|
||||
}
|
||||
if request.Path != "" {
|
||||
if _, err := queryBuilder.WriteString(fmt.Sprintf(" path:%s", request.Path)); err != nil {
|
||||
return "", fmt.Errorf("WriteString: %w", err)
|
||||
}
|
||||
}
|
||||
return queryBuilder.String(), nil
|
||||
}
|
||||
|
||||
// There is a possibility that path should be Basename/Filename for blobs.
|
||||
func searchResponseFrom(blobs []*gitlab.Blob) clients.SearchResponse {
|
||||
var searchResults []clients.SearchResult
|
||||
for _, blob := range blobs {
|
||||
searchResults = append(searchResults, clients.SearchResult{
|
||||
Path: blob.Filename,
|
||||
})
|
||||
}
|
||||
ret := clients.SearchResponse{
|
||||
Results: searchResults,
|
||||
Hits: len(searchResults),
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
84
clients/gitlabrepo/searchCommits.go
Normal file
84
clients/gitlabrepo/searchCommits.go
Normal file
@ -0,0 +1,84 @@
|
||||
// Copyright 2022 Security Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gitlabrepo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
)
|
||||
|
||||
type searchCommitsHandler struct {
|
||||
glClient *gitlab.Client
|
||||
repourl *repoURL
|
||||
}
|
||||
|
||||
func (handler *searchCommitsHandler) init(repourl *repoURL) {
|
||||
handler.repourl = repourl
|
||||
}
|
||||
|
||||
func (handler *searchCommitsHandler) search(request clients.SearchCommitsOptions) ([]clients.Commit, error) {
|
||||
if !strings.EqualFold(handler.repourl.commitSHA, clients.HeadSHA) {
|
||||
return nil, fmt.Errorf("%w: Search only supported for HEAD queries", clients.ErrUnsupportedFeature)
|
||||
}
|
||||
query, err := handler.buildQuery(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("handler.buildQuiery: %w", err)
|
||||
}
|
||||
|
||||
commits, _, err := handler.glClient.Search.CommitsByProject(handler.repourl.projectID, query, &gitlab.SearchOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Search.Commits: %w", err)
|
||||
}
|
||||
|
||||
// Gitlab returns a list of commits that does not contain the committer's id, unlike in
|
||||
// githubrepo/searchCommits.go so to limit the number of requests we are mapping each unique user
|
||||
// email to thei gitlab user data.
|
||||
userMap := make(map[string]*gitlab.User)
|
||||
var ret []clients.Commit
|
||||
for _, commit := range commits {
|
||||
if _, ok := userMap[commit.CommitterEmail]; !ok {
|
||||
user, _, err := handler.glClient.Search.Users(commit.CommitterEmail, &gitlab.SearchOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gitlab-searchCommits: %w", err)
|
||||
}
|
||||
userMap[commit.CommitterEmail] = user[0]
|
||||
}
|
||||
|
||||
ret = append(ret, clients.Commit{
|
||||
Committer: clients.User{ID: int64(userMap[commit.CommitterEmail].ID)},
|
||||
})
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (handler *searchCommitsHandler) buildQuery(request clients.SearchCommitsOptions) (string, error) {
|
||||
if request.Author == "" {
|
||||
return "", fmt.Errorf("%w", errEmptyQuery)
|
||||
}
|
||||
var queryBuilder strings.Builder
|
||||
if _, err := queryBuilder.WriteString(
|
||||
fmt.Sprintf("project:%s/%s author:%s",
|
||||
handler.repourl.owner, handler.repourl.projectID,
|
||||
request.Author)); err != nil {
|
||||
return "", fmt.Errorf("writestring: %w", err)
|
||||
}
|
||||
|
||||
return queryBuilder.String(), nil
|
||||
}
|
77
clients/gitlabrepo/searchCommits_test.go
Normal file
77
clients/gitlabrepo/searchCommits_test.go
Normal file
@ -0,0 +1,77 @@
|
||||
// Copyright 2022 Security Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gitlabrepo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
)
|
||||
|
||||
func TestSearchCommitsBuildQuery(t *testing.T) {
|
||||
t.Parallel()
|
||||
testcases := []struct {
|
||||
searchReq clients.SearchCommitsOptions
|
||||
expectedErrType error
|
||||
name string
|
||||
repourl *repoURL
|
||||
expectedQuery string
|
||||
hasError bool
|
||||
}{
|
||||
{
|
||||
name: "Basic",
|
||||
repourl: &repoURL{
|
||||
owner: "testowner",
|
||||
projectID: "1234",
|
||||
},
|
||||
searchReq: clients.SearchCommitsOptions{
|
||||
Author: "testAuthor",
|
||||
},
|
||||
expectedQuery: "project:testowner/1234 author:testAuthor",
|
||||
},
|
||||
{
|
||||
name: "EmptyQuery:",
|
||||
repourl: &repoURL{
|
||||
owner: "testowner",
|
||||
projectID: "1234",
|
||||
},
|
||||
searchReq: clients.SearchCommitsOptions{},
|
||||
hasError: true,
|
||||
expectedErrType: errEmptyQuery,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
testcase := testcase
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := searchCommitsHandler{
|
||||
repourl: testcase.repourl,
|
||||
}
|
||||
|
||||
query, err := handler.buildQuery(testcase.searchReq)
|
||||
if !testcase.hasError && err != nil {
|
||||
t.Fatalf("expected - no error, get: %v", err)
|
||||
}
|
||||
if testcase.hasError && !errors.Is(err, testcase.expectedErrType) {
|
||||
t.Fatalf("expectedErrType - %v, got - %v", testcase.expectedErrType, err)
|
||||
} else if query != testcase.expectedQuery {
|
||||
t.Fatalf("expectedQuery - %s, got - %s", testcase.expectedQuery, query)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
127
clients/gitlabrepo/search_test.go
Normal file
127
clients/gitlabrepo/search_test.go
Normal file
@ -0,0 +1,127 @@
|
||||
// Copyright 2022 Security Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gitlabrepo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
)
|
||||
|
||||
func TestBuildQuery(t *testing.T) {
|
||||
t.Parallel()
|
||||
testcases := []struct {
|
||||
searchReq clients.SearchRequest
|
||||
expectedErrType error
|
||||
name string
|
||||
repourl *repoURL
|
||||
expectedQuery string
|
||||
hasError bool
|
||||
}{
|
||||
{
|
||||
name: "Basic",
|
||||
repourl: &repoURL{
|
||||
owner: "testowner",
|
||||
projectID: "1234",
|
||||
},
|
||||
searchReq: clients.SearchRequest{
|
||||
Query: "testquery",
|
||||
},
|
||||
expectedQuery: "testquery project:testowner/1234",
|
||||
},
|
||||
{
|
||||
name: "EmptyQuery",
|
||||
repourl: &repoURL{
|
||||
owner: "testowner",
|
||||
projectID: "1234",
|
||||
},
|
||||
searchReq: clients.SearchRequest{},
|
||||
hasError: true,
|
||||
expectedErrType: errEmptyQuery,
|
||||
},
|
||||
{
|
||||
name: "WithFilename",
|
||||
repourl: &repoURL{
|
||||
owner: "testowner",
|
||||
projectID: "1234",
|
||||
},
|
||||
searchReq: clients.SearchRequest{
|
||||
Query: "testquery",
|
||||
Filename: "filename1.txt",
|
||||
},
|
||||
expectedQuery: "testquery project:testowner/1234 in:file filename:filename1.txt",
|
||||
},
|
||||
{
|
||||
name: "WithPath",
|
||||
repourl: &repoURL{
|
||||
owner: "testowner",
|
||||
projectID: "1234",
|
||||
},
|
||||
searchReq: clients.SearchRequest{
|
||||
Query: "testquery",
|
||||
Path: "dir1/file1.txt",
|
||||
},
|
||||
expectedQuery: "testquery project:testowner/1234 path:dir1/file1.txt",
|
||||
},
|
||||
{
|
||||
name: "WithFilenameAndPath",
|
||||
repourl: &repoURL{
|
||||
owner: "testowner",
|
||||
projectID: "1234",
|
||||
},
|
||||
searchReq: clients.SearchRequest{
|
||||
Query: "testquery",
|
||||
Filename: "filename1.txt",
|
||||
Path: "dir1/dir2",
|
||||
},
|
||||
expectedQuery: "testquery project:testowner/1234 in:file filename:filename1.txt path:dir1/dir2",
|
||||
},
|
||||
{
|
||||
name: "WithFilenameAndPathWithSeperator",
|
||||
repourl: &repoURL{
|
||||
owner: "testowner",
|
||||
projectID: "1234",
|
||||
},
|
||||
searchReq: clients.SearchRequest{
|
||||
Query: "testquery/query",
|
||||
Filename: "filename1.txt",
|
||||
Path: "dir1/dir2",
|
||||
},
|
||||
expectedQuery: "testquery query project:testowner/1234 in:file filename:filename1.txt path:dir1/dir2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
testcase := testcase
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := searchHandler{
|
||||
repourl: testcase.repourl,
|
||||
}
|
||||
|
||||
query, err := handler.buildQuery(testcase.searchReq)
|
||||
if !testcase.hasError && err != nil {
|
||||
t.Fatalf("expected - no error, got: %v", err)
|
||||
}
|
||||
if testcase.hasError && !errors.Is(err, testcase.expectedErrType) {
|
||||
t.Fatalf("expectedErrType - %v, got -%v", testcase.expectedErrType, err)
|
||||
} else if query != testcase.expectedQuery {
|
||||
t.Fatalf("expectedQuery - %s, got - %s", testcase.expectedQuery, query)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
55
clients/gitlabrepo/statuses.go
Normal file
55
clients/gitlabrepo/statuses.go
Normal file
@ -0,0 +1,55 @@
|
||||
// Copyright 2022 Security Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gitlabrepo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
)
|
||||
|
||||
type statusesHandler struct {
|
||||
glClient *gitlab.Client
|
||||
repourl *repoURL
|
||||
}
|
||||
|
||||
func (handler *statusesHandler) init(repourl *repoURL) {
|
||||
handler.repourl = repourl
|
||||
}
|
||||
|
||||
// for gitlab this only works if ref is SHA.
|
||||
func (handler *statusesHandler) listStatuses(ref string) ([]clients.Status, error) {
|
||||
commitStatuses, _, err := handler.glClient.Commits.GetCommitStatuses(
|
||||
handler.repourl.projectID, ref, &gitlab.GetCommitStatusesOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting commit statuses: %w", err)
|
||||
}
|
||||
return statusFromData(commitStatuses), nil
|
||||
}
|
||||
|
||||
func statusFromData(commitStatuses []*gitlab.CommitStatus) []clients.Status {
|
||||
var statuses []clients.Status
|
||||
for _, commitStatus := range commitStatuses {
|
||||
statuses = append(statuses, clients.Status{
|
||||
State: commitStatus.Status,
|
||||
Context: fmt.Sprint(commitStatus.ID),
|
||||
URL: commitStatus.TargetURL,
|
||||
TargetURL: commitStatus.TargetURL,
|
||||
})
|
||||
}
|
||||
return statuses
|
||||
}
|
69
clients/gitlabrepo/webhook.go
Normal file
69
clients/gitlabrepo/webhook.go
Normal file
@ -0,0 +1,69 @@
|
||||
// Copyright 2022 Security Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gitlabrepo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
)
|
||||
|
||||
type webhookHandler struct {
|
||||
glClient *gitlab.Client
|
||||
once *sync.Once
|
||||
errSetup error
|
||||
repourl *repoURL
|
||||
webhooks []clients.Webhook
|
||||
}
|
||||
|
||||
func (handler *webhookHandler) init(repourl *repoURL) {
|
||||
handler.repourl = repourl
|
||||
handler.errSetup = nil
|
||||
handler.once = new(sync.Once)
|
||||
}
|
||||
|
||||
func (handler *webhookHandler) setup() error {
|
||||
handler.once.Do(func() {
|
||||
projectHooks, _, err := handler.glClient.Projects.ListProjectHooks(
|
||||
handler.repourl.projectID, &gitlab.ListProjectHooksOptions{})
|
||||
if err != nil {
|
||||
handler.errSetup = fmt.Errorf("request for project hooks failed with %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: make sure that enablesslverification is similarly equivalent to auth secret.
|
||||
for _, hook := range projectHooks {
|
||||
handler.webhooks = append(handler.webhooks,
|
||||
clients.Webhook{
|
||||
Path: hook.URL,
|
||||
ID: int64(hook.ID),
|
||||
UsesAuthSecret: hook.EnableSSLVerification,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return handler.errSetup
|
||||
}
|
||||
|
||||
func (handler *webhookHandler) listWebhooks() ([]clients.Webhook, error) {
|
||||
if err := handler.setup(); err != nil {
|
||||
return nil, fmt.Errorf("error during webhookHandler.setup: %w", err)
|
||||
}
|
||||
|
||||
return handler.webhooks, nil
|
||||
}
|
62
clients/gitlabrepo/workflows.go
Normal file
62
clients/gitlabrepo/workflows.go
Normal file
@ -0,0 +1,62 @@
|
||||
// Copyright 2022 Security Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gitlabrepo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
)
|
||||
|
||||
type workflowsHandler struct {
|
||||
glClient *gitlab.Client
|
||||
repourl *repoURL
|
||||
}
|
||||
|
||||
func (handler *workflowsHandler) init(repourl *repoURL) {
|
||||
handler.repourl = repourl
|
||||
}
|
||||
|
||||
func (handler *workflowsHandler) listSuccessfulWorkflowRuns(filename string) ([]clients.WorkflowRun, error) {
|
||||
var buildStates []gitlab.BuildStateValue
|
||||
buildStates = append(buildStates, gitlab.Success)
|
||||
jobs, _, err := handler.glClient.Jobs.ListProjectJobs(handler.repourl.projectID,
|
||||
&gitlab.ListJobsOptions{Scope: &buildStates})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting project jobs: %w", err)
|
||||
}
|
||||
|
||||
return workflowsRunsFrom(jobs, filename), nil
|
||||
}
|
||||
|
||||
func workflowsRunsFrom(data []*gitlab.Job, filename string) []clients.WorkflowRun {
|
||||
var workflowRuns []clients.WorkflowRun
|
||||
for _, job := range data {
|
||||
// Find a better way to do this.
|
||||
for _, artifact := range job.Artifacts {
|
||||
if strings.EqualFold(artifact.Filename, filename) {
|
||||
workflowRuns = append(workflowRuns, clients.WorkflowRun{
|
||||
HeadSHA: &job.Pipeline.Sha,
|
||||
URL: job.WebURL,
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return workflowRuns
|
||||
}
|
@ -34,18 +34,24 @@ const (
|
||||
// Mannequin: Author is a placeholder for an unclaimed user.
|
||||
RepoAssociationMannequin RepoAssociation = iota
|
||||
// None: Author has no association with the repository.
|
||||
// NoPermissions: (GitLab).
|
||||
RepoAssociationNone
|
||||
// FirstTimer: Author has not previously committed to the VCS.
|
||||
RepoAssociationFirstTimer
|
||||
// FirstTimeContributor: Author has not previously committed to the repository.
|
||||
// MinimalAccessPermissions: (Gitlab).
|
||||
RepoAssociationFirstTimeContributor
|
||||
// Contributor: Author has been a contributor to the repository.
|
||||
RepoAssociationContributor
|
||||
// Collaborator: Author has been invited to collaborate on the repository.
|
||||
RepoAssociationCollaborator
|
||||
// Member: Author is a member of the organization that owns the repository.
|
||||
// DeveloperAccessPermissions: (GitLab).
|
||||
RepoAssociationMember
|
||||
// Maintainer: Author is part of the maintenance team for the repository (GitLab).
|
||||
RepoAssociationMaintainer
|
||||
// Owner: Author is the owner of the repository.
|
||||
// (Owner): (GitLab).
|
||||
RepoAssociationOwner
|
||||
)
|
||||
|
||||
|
13
go.mod
13
go.mod
@ -50,6 +50,12 @@ require (
|
||||
sigs.k8s.io/release-utils v0.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.104.0 // indirect
|
||||
cloud.google.com/go/compute v1.7.0 // indirect
|
||||
@ -101,15 +107,16 @@ require (
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/vbatts/tar-split v0.11.2 // indirect
|
||||
github.com/xanzy/go-gitlab v0.73.1
|
||||
github.com/xanzy/ssh-agent v0.3.0 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20220718184931-c8730f7fcb92 // indirect
|
||||
golang.org/x/net v0.0.0-20220805013720-a33c5aa5df48 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c // indirect
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
|
||||
google.golang.org/api v0.93.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
|
25
go.sum
25
go.sum
@ -415,6 +415,13 @@ github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
|
||||
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
|
||||
github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok=
|
||||
github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
|
||||
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
@ -594,12 +601,14 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME=
|
||||
github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI=
|
||||
github.com/xanzy/go-gitlab v0.73.1 h1:UMagqUZLJdjss1SovIC+kJCH4k2AZWXl58gJd38Y/hI=
|
||||
github.com/xanzy/go-gitlab v0.73.1/go.mod h1:d/a0vswScO7Agg1CZNz15Ic6SSvBG9vfw8egL99t4kA=
|
||||
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
|
||||
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
|
||||
@ -751,8 +760,8 @@ golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220805013720-a33c5aa5df48 h1:N9Vc/rorQUDes6B9CNdIxAn5jODGj2wzfrei2x4wNj4=
|
||||
golang.org/x/net v0.0.0-20220805013720-a33c5aa5df48/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -776,8 +785,8 @@ golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
|
||||
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
|
||||
golang.org/x/oauth2 v0.0.0-20220718184931-c8730f7fcb92 h1:oVlhw3Oe+1reYsE2Nqu19PDJfLzwdU3QUUrG86rLK68=
|
||||
golang.org/x/oauth2 v0.0.0-20220718184931-c8730f7fcb92/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c h1:q3gFqPqH7NVofKo3c3yETAP//pPI+G5mvB7qqj1Y5kY=
|
||||
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -875,8 +884,8 @@ golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
@ -896,6 +905,8 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ=
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
|
Loading…
Reference in New Issue
Block a user