Skip to content

Commit

Permalink
✨ Gitlab support (ossf#2265)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: nathaniel.wert <[email protected]>
  • Loading branch information
2 people authored and raghavkaul committed Sep 28, 2022
1 parent 2a5182b commit 43c271f
Show file tree
Hide file tree
Showing 21 changed files with 1,982 additions and 10 deletions.
196 changes: 196 additions & 0 deletions clients/gitlabrepo/branches.go
Original file line number Diff line number Diff line change
@@ -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 changes: 59 additions & 0 deletions clients/gitlabrepo/checkruns.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 43c271f

Please sign in to comment.