mirror of
https://github.com/ossf/scorecard.git
synced 2024-08-16 11:50:37 +03:00
✨ GitLab: Security Policy check (#2754)
* Add tarballHandler for GitLab, enabling repo download Signed-off-by: Raghav Kaul <raghavkaul@google.com> * Abstract OrgSecurityPolicy details to RepoClient instead of checker Signed-off-by: Raghav Kaul <raghavkaul@google.com> * Remove Org() from RepoClient Signed-off-by: Raghav Kaul <raghavkaul@google.com> * Rename Signed-off-by: Raghav Kaul <raghavkaul@google.com> * Don't run as part of CI tests that depend on external sites Signed-off-by: Raghav Kaul <raghavkaul@google.com> --------- Signed-off-by: Raghav Kaul <raghavkaul@google.com>
This commit is contained in:
parent
9831629874
commit
e2715fd7f0
2
Makefile
2
Makefile
@ -335,7 +335,7 @@ e2e-gh-token: build-scorecard check-env | $(GINKGO)
|
||||
TOKEN_TYPE="GITHUB_TOKEN" $(GINKGO) --race -p -v -cover -coverprofile=e2e-coverage.out --keep-separate-coverprofiles ./...
|
||||
|
||||
e2e-gitlab-token: ## Runs e2e tests that require a GITLAB_TOKEN
|
||||
TOKEN_TYPE="GITLAB_PAT" $(GINKGO) --race -p -vv --focus '.*GitLab Token' ./...
|
||||
TEST_GITLAB_EXTERNAL=1 TOKEN_TYPE="GITLAB_PAT" $(GINKGO) --race -p -vv --focus '.*GitLab Token' ./...
|
||||
|
||||
e2e-gitlab: ## Runs e2e tests for GitLab only. TOKEN_TYPE is not used (since these are public APIs), but must be set to something
|
||||
TOKEN_TYPE="GITLAB_PAT" $(GINKGO) --race -p -vv --focus '.*GitLab' ./...
|
||||
|
@ -25,10 +25,8 @@ import (
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/checks/fileparser"
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
"github.com/ossf/scorecard/v4/clients/githubrepo"
|
||||
sce "github.com/ossf/scorecard/v4/errors"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
"github.com/ossf/scorecard/v4/log"
|
||||
)
|
||||
|
||||
type securityPolicyFilesWithURI struct {
|
||||
@ -62,21 +60,18 @@ func SecurityPolicy(c *checker.CheckRequest) (checker.SecurityPolicyData, error)
|
||||
|
||||
// Check if present in parent org.
|
||||
// https#://docs.github.com/en/github/building-a-strong-community/creating-a-default-community-health-file.
|
||||
// TODO(1491): Make this non-GitHub specific.
|
||||
logger := log.NewLogger(log.InfoLevel)
|
||||
// HAD TO HARD CODE TO 30
|
||||
dotGitHubClient := githubrepo.CreateGithubRepoClient(c.Ctx, logger)
|
||||
err = dotGitHubClient.InitRepo(c.Repo.Org(), clients.HeadSHA, 0)
|
||||
client, err := c.RepoClient.GetOrgRepoClient(c.Ctx)
|
||||
|
||||
switch {
|
||||
case err == nil:
|
||||
defer dotGitHubClient.Close()
|
||||
data.uri = dotGitHubClient.URI()
|
||||
err = fileparser.OnAllFilesDo(dotGitHubClient, isSecurityPolicyFile, &data)
|
||||
defer client.Close()
|
||||
data.uri = client.URI()
|
||||
err = fileparser.OnAllFilesDo(client, isSecurityPolicyFile, &data)
|
||||
if err != nil {
|
||||
return checker.SecurityPolicyData{}, err
|
||||
return checker.SecurityPolicyData{}, fmt.Errorf("unable to create github client: %w", err)
|
||||
}
|
||||
|
||||
case errors.Is(err, sce.ErrRepoUnreachable):
|
||||
case errors.Is(err, sce.ErrRepoUnreachable), errors.Is(err, clients.ErrUnsupportedFeature):
|
||||
break
|
||||
default:
|
||||
return checker.SecurityPolicyData{}, err
|
||||
@ -91,7 +86,7 @@ func SecurityPolicy(c *checker.CheckRequest) (checker.SecurityPolicyData, error)
|
||||
if data.files[idx].File.Type == finding.FileTypeURL {
|
||||
filePattern = strings.Replace(filePattern, data.uri+"/", "", 1)
|
||||
}
|
||||
err := fileparser.OnMatchingFileContentDo(dotGitHubClient, fileparser.PathMatcher{
|
||||
err := fileparser.OnMatchingFileContentDo(client, fileparser.PathMatcher{
|
||||
Pattern: filePattern,
|
||||
CaseSensitive: false,
|
||||
}, checkSecurityPolicyFileContent, &data.files[idx].File, &data.files[idx].Information)
|
||||
|
@ -138,8 +138,6 @@ func TestSecurityPolicy(t *testing.T) {
|
||||
mockRepo := mockrepo.NewMockRepo(ctrl)
|
||||
|
||||
mockRepoClient.EXPECT().ListFiles(gomock.Any()).Return(tt.files, nil).AnyTimes()
|
||||
mockRepo.EXPECT().Org().Return(nil).AnyTimes()
|
||||
//
|
||||
// the revised Security Policy will immediate go for the
|
||||
// file contents once found. This test will return that
|
||||
// mock file, but this specific unit test is not testing
|
||||
|
@ -195,6 +195,21 @@ func (client *Client) GetCreatedAt() (time.Time, error) {
|
||||
return client.repo.CreatedAt.Time, nil
|
||||
}
|
||||
|
||||
func (client *Client) GetOrgRepoClient(ctx context.Context) (clients.RepoClient, error) {
|
||||
dotGithubRepo, err := MakeGithubRepo(fmt.Sprintf("%s/.github", client.repourl.owner))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error during MakeGithubRepo: %w", err)
|
||||
}
|
||||
|
||||
logger := log.NewLogger(log.InfoLevel)
|
||||
c := CreateGithubRepoClient(ctx, logger)
|
||||
if err := c.InitRepo(dotGithubRepo, clients.HeadSHA, 0); err != nil {
|
||||
return nil, fmt.Errorf("error during InitRepo: %w", err)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// ListWebhooks implements RepoClient.ListWebhooks.
|
||||
func (client *Client) ListWebhooks() ([]clients.Webhook, error) {
|
||||
return client.webhook.listWebhooks()
|
||||
|
@ -23,10 +23,6 @@ import (
|
||||
sce "github.com/ossf/scorecard/v4/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
githubOrgRepo = ".github"
|
||||
)
|
||||
|
||||
type repoURL struct {
|
||||
host, owner, repo, defaultBranch, commitSHA string
|
||||
metadata []string
|
||||
@ -85,15 +81,6 @@ func (r *repoURL) String() string {
|
||||
return fmt.Sprintf("%s-%s-%s", r.host, r.owner, r.repo)
|
||||
}
|
||||
|
||||
// Org implements Repo.Org.
|
||||
func (r *repoURL) Org() clients.Repo {
|
||||
return &repoURL{
|
||||
host: r.host,
|
||||
owner: r.owner,
|
||||
repo: githubOrgRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// IsValid implements Repo.IsValid.
|
||||
func (r *repoURL) IsValid() error {
|
||||
switch r.host {
|
||||
|
@ -51,6 +51,7 @@ type Client struct {
|
||||
webhook *webhookHandler
|
||||
languages *languagesHandler
|
||||
licenses *licensesHandler
|
||||
tarball *tarballHandler
|
||||
ctx context.Context
|
||||
commitDepth int
|
||||
}
|
||||
@ -128,6 +129,9 @@ func (client *Client) InitRepo(inputRepo clients.Repo, commitSHA string, commitD
|
||||
// Init languagesHandler
|
||||
client.licenses.init(client.repourl)
|
||||
|
||||
// Init tarballHandler
|
||||
client.tarball.init(client.ctx, client.repourl, repo, commitSHA)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -140,11 +144,11 @@ func (client *Client) LocalPath() (string, error) {
|
||||
}
|
||||
|
||||
func (client *Client) ListFiles(predicate func(string) (bool, error)) ([]string, error) {
|
||||
return nil, nil
|
||||
return client.tarball.listFiles(predicate)
|
||||
}
|
||||
|
||||
func (client *Client) GetFileContent(filename string) ([]byte, error) {
|
||||
return nil, nil
|
||||
return client.tarball.getFileContent(filename)
|
||||
}
|
||||
|
||||
func (client *Client) ListCommits() ([]clients.Commit, error) {
|
||||
@ -183,6 +187,10 @@ func (client *Client) GetCreatedAt() (time.Time, error) {
|
||||
return client.project.getCreatedAt()
|
||||
}
|
||||
|
||||
func (client *Client) GetOrgRepoClient(ctx context.Context) (clients.RepoClient, error) {
|
||||
return nil, fmt.Errorf("GetOrgRepoClient (GitLab): %w", clients.ErrUnsupportedFeature)
|
||||
}
|
||||
|
||||
func (client *Client) ListWebhooks() ([]clients.Webhook, error) {
|
||||
return client.webhook.listWebhooks()
|
||||
}
|
||||
@ -269,6 +277,7 @@ func CreateGitlabClientWithToken(ctx context.Context, token string, repo clients
|
||||
glClient: client,
|
||||
},
|
||||
licenses: &licensesHandler{},
|
||||
tarball: &tarballHandler{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -27,10 +27,6 @@ import (
|
||||
sce "github.com/ossf/scorecard/v4/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
gitlabOrgProj = ".gitlab"
|
||||
)
|
||||
|
||||
type repoURL struct {
|
||||
scheme string
|
||||
host string
|
||||
@ -95,14 +91,6 @@ func (r *repoURL) String() string {
|
||||
return fmt.Sprintf("%s-%s_%s", r.host, r.owner, r.project)
|
||||
}
|
||||
|
||||
func (r *repoURL) Org() clients.Repo {
|
||||
return &repoURL{
|
||||
host: r.host,
|
||||
owner: r.owner,
|
||||
project: gitlabOrgProj,
|
||||
}
|
||||
}
|
||||
|
||||
// IsValid implements Repo.IsValid.
|
||||
func (r *repoURL) IsValid() error {
|
||||
if strings.Contains(r.host, "gitlab.") {
|
||||
|
@ -16,6 +16,7 @@ package gitlabrepo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@ -25,10 +26,11 @@ import (
|
||||
func TestRepoURL_IsValid(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
inputURL string
|
||||
expected repoURL
|
||||
wantErr bool
|
||||
name string
|
||||
inputURL string
|
||||
expected repoURL
|
||||
wantErr bool
|
||||
flagRequired bool
|
||||
}{
|
||||
{
|
||||
name: "github repository",
|
||||
@ -73,7 +75,6 @@ func TestRepoURL_IsValid(t *testing.T) {
|
||||
inputURL: "https://gitlab.com/ossf-test/scorecard-check-binary-artifacts-e2e/",
|
||||
wantErr: false,
|
||||
},
|
||||
|
||||
{
|
||||
name: "valid hosted gitlab project",
|
||||
expected: repoURL{
|
||||
@ -82,12 +83,16 @@ func TestRepoURL_IsValid(t *testing.T) {
|
||||
owner: "webmaster-team",
|
||||
project: "webml",
|
||||
},
|
||||
inputURL: "https://salsa.debian.org/webmaster-team/webwml",
|
||||
wantErr: false,
|
||||
inputURL: "https://salsa.debian.org/webmaster-team/webwml",
|
||||
wantErr: false,
|
||||
flagRequired: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt // Re-initializing variable so it is not changed while executing the closure blow
|
||||
if tt.flagRequired && os.Getenv("TEST_GITLAB_EXTERNAL") == "" {
|
||||
continue
|
||||
}
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := repoURL{
|
||||
@ -116,8 +121,9 @@ func TestRepoURL_IsValid(t *testing.T) {
|
||||
|
||||
func TestRepoURL_DetectGitlab(t *testing.T) {
|
||||
tests := []struct {
|
||||
repouri string
|
||||
expected bool
|
||||
repouri string
|
||||
expected bool
|
||||
flagRequired bool
|
||||
}{
|
||||
{
|
||||
repouri: "github.com/ossf/scorecard",
|
||||
@ -136,8 +142,9 @@ func TestRepoURL_DetectGitlab(t *testing.T) {
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
repouri: "https://salsa.debian.org/webmaster-team/webml",
|
||||
expected: true,
|
||||
repouri: "https://salsa.debian.org/webmaster-team/webml",
|
||||
expected: true,
|
||||
flagRequired: true,
|
||||
},
|
||||
{
|
||||
// Invalid repo
|
||||
@ -147,6 +154,9 @@ func TestRepoURL_DetectGitlab(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if tt.flagRequired && os.Getenv("TEST_GITLAB_EXTERNAL") == "" {
|
||||
continue
|
||||
}
|
||||
g := DetectGitLab(tt.repouri)
|
||||
if g != tt.expected {
|
||||
t.Errorf("got %s isgitlab: %t expected %t", tt.repouri, g, tt.expected)
|
||||
|
264
clients/gitlabrepo/tarball.go
Normal file
264
clients/gitlabrepo/tarball.go
Normal file
@ -0,0 +1,264 @@
|
||||
// Copyright 2021 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 (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
sce "github.com/ossf/scorecard/v4/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
repoDir = "project*"
|
||||
repoFilename = "gitlabproject*.tar.gz"
|
||||
)
|
||||
|
||||
var (
|
||||
errTarballNotFound = errors.New("tarball not found")
|
||||
errTarballCorrupted = errors.New("corrupted tarball")
|
||||
errZipSlip = errors.New("ZipSlip path detected")
|
||||
)
|
||||
|
||||
func extractAndValidateArchivePath(path, dest string) (string, error) {
|
||||
const splitLength = 2
|
||||
// The tarball will have a top-level directory which contains all the repository files.
|
||||
// Discard the directory and only keep the actual files.
|
||||
names := strings.SplitN(path, "/", splitLength)
|
||||
if len(names) < splitLength {
|
||||
return dest, nil
|
||||
}
|
||||
if names[1] == "" {
|
||||
return dest, nil
|
||||
}
|
||||
// Check for ZipSlip: https://snyk.io/research/zip-slip-vulnerability
|
||||
cleanpath := filepath.Join(dest, names[1])
|
||||
if !strings.HasPrefix(cleanpath, filepath.Clean(dest)+string(os.PathSeparator)) {
|
||||
return "", fmt.Errorf("%w: %s", errZipSlip, names[1])
|
||||
}
|
||||
return cleanpath, nil
|
||||
}
|
||||
|
||||
type tarballHandler struct {
|
||||
errSetup error
|
||||
once *sync.Once
|
||||
ctx context.Context
|
||||
repo *gitlab.Project
|
||||
repourl *repoURL
|
||||
commitSHA string
|
||||
tempDir string
|
||||
tempTarFile string
|
||||
files []string
|
||||
}
|
||||
|
||||
func (handler *tarballHandler) init(ctx context.Context, repourl *repoURL, repo *gitlab.Project, commitSHA string) {
|
||||
handler.errSetup = nil
|
||||
handler.once = new(sync.Once)
|
||||
handler.ctx = ctx
|
||||
handler.repo = repo
|
||||
handler.repourl = repourl
|
||||
handler.commitSHA = commitSHA
|
||||
}
|
||||
|
||||
func (handler *tarballHandler) setup() error {
|
||||
handler.once.Do(func() {
|
||||
// cleanup any previous state.
|
||||
if err := handler.cleanup(); err != nil {
|
||||
handler.errSetup = sce.WithMessage(sce.ErrScorecardInternal, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// setup tem dir/files and download repo tarball.
|
||||
if err := handler.getTarball(); errors.Is(err, errTarballNotFound) {
|
||||
log.Printf("unable to get tarball %v. Skipping...", err)
|
||||
return
|
||||
} else if err != nil {
|
||||
handler.errSetup = sce.WithMessage(sce.ErrScorecardInternal, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// extract file names and content from tarball.
|
||||
if err := handler.extractTarball(); errors.Is(err, errTarballCorrupted) {
|
||||
log.Printf("unable to extract tarball %v. Skipping...", err)
|
||||
} else if err != nil {
|
||||
handler.errSetup = sce.WithMessage(sce.ErrScorecardInternal, err.Error())
|
||||
}
|
||||
})
|
||||
return handler.errSetup
|
||||
}
|
||||
|
||||
func (handler *tarballHandler) getTarball() error {
|
||||
url := fmt.Sprintf("%s/api/v4/projects/%d/repository/archive.tar.gz?sha=%s",
|
||||
handler.repourl.Host(), handler.repo.ID, handler.commitSHA)
|
||||
req, err := http.NewRequestWithContext(handler.ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("http.NewRequestWithContext: %w", err)
|
||||
}
|
||||
req.Header.Set("PRIVATE-TOKEN", os.Getenv("GITLAB_AUTH_TOKEN"))
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("http.DefaultClient.Do: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Handler 400/404 errors.
|
||||
switch resp.StatusCode {
|
||||
case http.StatusNotFound, http.StatusBadRequest:
|
||||
return fmt.Errorf("%w: %s", errTarballNotFound, url)
|
||||
}
|
||||
|
||||
// Create a temp file. This automatically appends a random number to the name.
|
||||
tempDir, err := os.MkdirTemp("", repoDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("os.MkdirTemp: %w", err)
|
||||
}
|
||||
repoFile, err := os.CreateTemp(tempDir, repoFilename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("os.CreateTemp: %w", err)
|
||||
}
|
||||
defer repoFile.Close()
|
||||
if _, err := io.Copy(repoFile, resp.Body); err != nil {
|
||||
// If the incomming tarball is corrupted or the server times out.
|
||||
return fmt.Errorf("%w io.Copy: %v", errTarballNotFound, err)
|
||||
}
|
||||
|
||||
handler.tempDir = tempDir
|
||||
handler.tempTarFile = repoFile.Name()
|
||||
return nil
|
||||
}
|
||||
|
||||
// nolint: gocognit
|
||||
func (handler *tarballHandler) extractTarball() error {
|
||||
in, err := os.OpenFile(handler.tempTarFile, os.O_RDONLY, 0o644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("os.OpenFile: %w", err)
|
||||
}
|
||||
gz, err := gzip.NewReader(in)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: gzip.NewReader %v %v", errTarballCorrupted, handler.tempTarFile, err)
|
||||
}
|
||||
tr := tar.NewReader(gz)
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w tarReader.Next: %v", errTarballCorrupted, err)
|
||||
}
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
dirpath, err := extractAndValidateArchivePath(header.Name, handler.tempDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dirpath == filepath.Clean(handler.tempDir) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := os.Mkdir(dirpath, 0o755); err != nil {
|
||||
return fmt.Errorf("error during os.Mkdir: %w", err)
|
||||
}
|
||||
case tar.TypeReg:
|
||||
if header.Size <= 0 {
|
||||
continue
|
||||
}
|
||||
filenamepath, err := extractAndValidateArchivePath(header.Name, handler.tempDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Dir(filenamepath)); os.IsNotExist(err) {
|
||||
if err := os.Mkdir(filepath.Dir(filenamepath), 0o755); err != nil {
|
||||
return fmt.Errorf("os.Mkdir: %w", err)
|
||||
}
|
||||
}
|
||||
outFile, err := os.Create(filenamepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("os.Create: %w", err)
|
||||
}
|
||||
|
||||
//nolint: gosec
|
||||
// Potential for DoS vulnerability via decompression bomb.
|
||||
// Since such an attack will only impact a single shard, ignoring this for now.
|
||||
if _, err := io.Copy(outFile, tr); err != nil {
|
||||
return fmt.Errorf("%w io.Copy: %v", errTarballCorrupted, err)
|
||||
}
|
||||
outFile.Close()
|
||||
handler.files = append(handler.files,
|
||||
strings.TrimPrefix(filenamepath, filepath.Clean(handler.tempDir)+string(os.PathSeparator)))
|
||||
case tar.TypeXGlobalHeader, tar.TypeSymlink:
|
||||
continue
|
||||
default:
|
||||
log.Printf("Unknown file type %s: '%s'", header.Name, string(header.Typeflag))
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *tarballHandler) listFiles(predicate func(string) (bool, error)) ([]string, error) {
|
||||
if err := handler.setup(); err != nil {
|
||||
return nil, fmt.Errorf("error during tarballHandler.setup: %w", err)
|
||||
}
|
||||
ret := make([]string, 0)
|
||||
for _, file := range handler.files {
|
||||
matches, err := predicate(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if matches {
|
||||
ret = append(ret, file)
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (handler *tarballHandler) getFileContent(filename string) ([]byte, error) {
|
||||
if err := handler.setup(); err != nil {
|
||||
fmt.Printf("err: %v\n", err)
|
||||
return nil, fmt.Errorf("error during tarballHandler.setup: %w", err)
|
||||
}
|
||||
fmt.Printf("handler.tempDir: %v\n", handler.tempDir)
|
||||
fmt.Printf("filename: %v\n", filename)
|
||||
content, err := os.ReadFile(filepath.Join(handler.tempDir, filename))
|
||||
if err != nil {
|
||||
return content, fmt.Errorf("os.ReadFile: %w", err)
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func (handler *tarballHandler) cleanup() error {
|
||||
if err := os.RemoveAll(handler.tempDir); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("os.Remove: %w", err)
|
||||
}
|
||||
|
||||
// Remove old file so we don't iterate through them.
|
||||
handler.files = nil
|
||||
return nil
|
||||
}
|
177
clients/gitlabrepo/tarball_test.go
Normal file
177
clients/gitlabrepo/tarball_test.go
Normal file
@ -0,0 +1,177 @@
|
||||
// Copyright 2021 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"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
)
|
||||
|
||||
type listfileTest struct {
|
||||
predicate func(string) (bool, error)
|
||||
err error
|
||||
outcome []string
|
||||
}
|
||||
|
||||
type getcontentTest struct {
|
||||
err error
|
||||
filename string
|
||||
output []byte
|
||||
}
|
||||
|
||||
func isSortedString(x, y string) bool {
|
||||
return x < y
|
||||
}
|
||||
|
||||
func setup(inputFile string) (tarballHandler, error) {
|
||||
tempDir, err := os.MkdirTemp("", repoDir)
|
||||
if err != nil {
|
||||
return tarballHandler{}, fmt.Errorf("test failed to create TempDir: %w", err)
|
||||
}
|
||||
tempFile, err := os.CreateTemp(tempDir, repoFilename)
|
||||
if err != nil {
|
||||
return tarballHandler{}, fmt.Errorf("test failed to create TempFile: %w", err)
|
||||
}
|
||||
testFile, err := os.OpenFile(inputFile, os.O_RDONLY, 0o644)
|
||||
if err != nil {
|
||||
return tarballHandler{}, fmt.Errorf("unable to open testfile: %w", err)
|
||||
}
|
||||
if _, err := io.Copy(tempFile, testFile); err != nil {
|
||||
return tarballHandler{}, fmt.Errorf("unable to do io.Copy: %w", err)
|
||||
}
|
||||
tarballHandler := tarballHandler{
|
||||
tempDir: tempDir,
|
||||
tempTarFile: tempFile.Name(),
|
||||
once: new(sync.Once),
|
||||
}
|
||||
tarballHandler.once.Do(func() {
|
||||
// We don't want to run the code in tarballHandler.setup(), so if we execute tarballHandler.once.Do() right
|
||||
// here, it won't get executed later when setup() is called.
|
||||
})
|
||||
return tarballHandler, nil
|
||||
}
|
||||
|
||||
// nolint: gocognit
|
||||
func TestExtractTarball(t *testing.T) {
|
||||
t.Parallel()
|
||||
testcases := []struct {
|
||||
name string
|
||||
inputFile string
|
||||
listfileTests []listfileTest
|
||||
getcontentTests []getcontentTest
|
||||
}{
|
||||
{
|
||||
name: "Basic",
|
||||
inputFile: "testdata/basic.tar.gz",
|
||||
listfileTests: []listfileTest{
|
||||
{
|
||||
// Returns all files in the tarball.
|
||||
predicate: func(string) (bool, error) { return true, nil },
|
||||
outcome: []string{"file0", "dir1/file1", "dir1/dir2/file2"},
|
||||
},
|
||||
{
|
||||
// Skips all files inside `dir1/dir2` directory.
|
||||
predicate: func(fn string) (bool, error) { return !strings.HasPrefix(fn, "dir1/dir2"), nil },
|
||||
outcome: []string{"file0", "dir1/file1"},
|
||||
},
|
||||
{
|
||||
// Skips all files.
|
||||
predicate: func(fn string) (bool, error) { return false, nil },
|
||||
outcome: []string{},
|
||||
},
|
||||
},
|
||||
getcontentTests: []getcontentTest{
|
||||
{
|
||||
filename: "file0",
|
||||
output: []byte("content0\n"),
|
||||
},
|
||||
{
|
||||
filename: "dir1/file1",
|
||||
output: []byte("content1\n"),
|
||||
},
|
||||
{
|
||||
filename: "dir1/dir2/file2",
|
||||
output: []byte("content2\n"),
|
||||
},
|
||||
{
|
||||
filename: "does/not/exist",
|
||||
err: os.ErrNotExist,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
testcase := testcase
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Setup
|
||||
handler, err := setup(testcase.inputFile)
|
||||
if err != nil {
|
||||
t.Fatalf("test setup failed: %v", err)
|
||||
}
|
||||
|
||||
// Extract tarball.
|
||||
if err := handler.extractTarball(); err != nil {
|
||||
t.Fatalf("test failed: %v", err)
|
||||
}
|
||||
|
||||
// Test ListFiles API.
|
||||
for _, listfiletest := range testcase.listfileTests {
|
||||
matchedFiles, err := handler.listFiles(listfiletest.predicate)
|
||||
if !errors.Is(err, listfiletest.err) {
|
||||
t.Errorf("test failed: expected - %v, got - %v", listfiletest.err, err)
|
||||
continue
|
||||
}
|
||||
if !cmp.Equal(listfiletest.outcome,
|
||||
matchedFiles,
|
||||
cmpopts.SortSlices(isSortedString)) {
|
||||
t.Errorf("test failed: expected - %q, got - %q", listfiletest.outcome, matchedFiles)
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetFileContent API.
|
||||
for _, getcontenttest := range testcase.getcontentTests {
|
||||
content, err := handler.getFileContent(getcontenttest.filename)
|
||||
if getcontenttest.err != nil && !errors.Is(err, getcontenttest.err) {
|
||||
t.Errorf("test failed: expected - %v, got - %v", getcontenttest.err, err)
|
||||
}
|
||||
if getcontenttest.err == nil && !cmp.Equal(getcontenttest.output, content) {
|
||||
t.Errorf("test failed: expected - %s, got - %s", string(getcontenttest.output), string(content))
|
||||
}
|
||||
}
|
||||
|
||||
// Test that files get deleted.
|
||||
if err := handler.cleanup(); err != nil {
|
||||
t.Errorf("test failed: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(handler.tempDir); !os.IsNotExist(err) {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
if len(handler.files) != 0 {
|
||||
t.Error("client.files not cleaned up!")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
BIN
clients/gitlabrepo/testdata/basic.tar.gz
vendored
Normal file
BIN
clients/gitlabrepo/testdata/basic.tar.gz
vendored
Normal file
Binary file not shown.
@ -259,6 +259,10 @@ func (client *localDirClient) GetCreatedAt() (time.Time, error) {
|
||||
return time.Time{}, fmt.Errorf("GetCreatedAt: %w", clients.ErrUnsupportedFeature)
|
||||
}
|
||||
|
||||
func (client *localDirClient) GetOrgRepoClient(ctx context.Context) (clients.RepoClient, error) {
|
||||
return nil, fmt.Errorf("GetOrgRepoClient: %w", clients.ErrUnsupportedFeature)
|
||||
}
|
||||
|
||||
// CreateLocalDirClient returns a client which implements RepoClient interface.
|
||||
func CreateLocalDirClient(ctx context.Context, logger *log.Logger) clients.RepoClient {
|
||||
return &localDirClient{
|
||||
|
@ -46,11 +46,6 @@ func (r *repoLocal) String() string {
|
||||
return r.URI()
|
||||
}
|
||||
|
||||
// Org implements Repo.Org.
|
||||
func (r *repoLocal) Org() clients.Repo {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValid implements Repo.IsValid.
|
||||
func (r *repoLocal) IsValid() error {
|
||||
f, err := os.Stat(r.path)
|
||||
|
@ -23,7 +23,6 @@ import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
clients "github.com/ossf/scorecard/v4/clients"
|
||||
)
|
||||
|
||||
// MockRepo is a mock of Repo interface.
|
||||
@ -93,20 +92,6 @@ func (mr *MockRepoMockRecorder) Metadata() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Metadata", reflect.TypeOf((*MockRepo)(nil).Metadata))
|
||||
}
|
||||
|
||||
// Org mocks base method.
|
||||
func (m *MockRepo) Org() clients.Repo {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Org")
|
||||
ret0, _ := ret[0].(clients.Repo)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Org indicates an expected call of Org.
|
||||
func (mr *MockRepoMockRecorder) Org() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Org", reflect.TypeOf((*MockRepo)(nil).Org))
|
||||
}
|
||||
|
||||
// String mocks base method.
|
||||
func (m *MockRepo) String() string {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -20,6 +20,7 @@
|
||||
package mockrepo
|
||||
|
||||
import (
|
||||
"context"
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
|
||||
@ -139,6 +140,15 @@ func (mr *MockRepoClientMockRecorder) GetFileContent(filename interface{}) *gomo
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileContent", reflect.TypeOf((*MockRepoClient)(nil).GetFileContent), filename)
|
||||
}
|
||||
|
||||
// GetOrgRepoClient mocks base method.
|
||||
func (m *MockRepoClient) GetOrgRepoClient(ctx context.Context) (clients.RepoClient, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetOrgRepoClient")
|
||||
ret0, _ := ret[0].(clients.RepoClient)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// InitRepo mocks base method.
|
||||
func (m *MockRepoClient) InitRepo(repo clients.Repo, commitSHA string, commitDepth int) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -15,6 +15,7 @@
|
||||
package ossfuzz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -188,6 +189,11 @@ func (c *client) GetDefaultBranch() (*clients.BranchRef, error) {
|
||||
return nil, fmt.Errorf("GetDefaultBranch: %w", clients.ErrUnsupportedFeature)
|
||||
}
|
||||
|
||||
// GetOrgRepoClient implements RepoClient.GetOrgRepoClient.
|
||||
func (c *client) GetOrgRepoClient(ctx context.Context) (clients.RepoClient, error) {
|
||||
return nil, fmt.Errorf("GetOrgRepoClient: %w", clients.ErrUnsupportedFeature)
|
||||
}
|
||||
|
||||
// GetDefaultBranchName implements RepoClient.GetDefaultBranchName.
|
||||
func (c *client) GetDefaultBranchName() (string, error) {
|
||||
return "", fmt.Errorf("GetDefaultBranchName: %w", clients.ErrUnsupportedFeature)
|
||||
|
@ -19,7 +19,6 @@ type Repo interface {
|
||||
URI() string
|
||||
Host() string
|
||||
String() string
|
||||
Org() Repo
|
||||
IsValid() error
|
||||
Metadata() []string
|
||||
AppendMetadata(metadata ...string)
|
||||
|
@ -16,6 +16,7 @@
|
||||
package clients
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
@ -40,6 +41,7 @@ type RepoClient interface {
|
||||
GetCreatedAt() (time.Time, error)
|
||||
GetDefaultBranchName() (string, error)
|
||||
GetDefaultBranch() (*BranchRef, error)
|
||||
GetOrgRepoClient(context.Context) (RepoClient, error)
|
||||
ListCommits() ([]Commit, error)
|
||||
ListIssues() ([]Issue, error)
|
||||
ListLicenses() ([]License, error)
|
||||
|
@ -40,6 +40,7 @@ type tokenType int
|
||||
const (
|
||||
patTokenType tokenType = iota
|
||||
githubWorkflowDefaultTokenType
|
||||
gitlabPATTokenType
|
||||
)
|
||||
|
||||
var tokType tokenType
|
||||
@ -58,6 +59,8 @@ var _ = BeforeSuite(func() {
|
||||
tokType = patTokenType
|
||||
case "GITHUB_TOKEN":
|
||||
tokType = githubWorkflowDefaultTokenType
|
||||
case "GITLAB_PAT":
|
||||
tokType = gitlabPATTokenType
|
||||
default:
|
||||
panic(fmt.Sprintf("invald TOKEN_TYPE: %s", tt))
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
"github.com/ossf/scorecard/v4/checks"
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
"github.com/ossf/scorecard/v4/clients/githubrepo"
|
||||
"github.com/ossf/scorecard/v4/clients/gitlabrepo"
|
||||
"github.com/ossf/scorecard/v4/clients/localdir"
|
||||
scut "github.com/ossf/scorecard/v4/utests"
|
||||
)
|
||||
@ -172,5 +173,68 @@ var _ = Describe("E2E TEST:"+checks.CheckSecurityPolicy, func() {
|
||||
Expect(scut.ValidateTestReturn(nil, "policy found", &expected, &result, &dl)).Should(BeTrue())
|
||||
Expect(x.Close()).Should(BeNil())
|
||||
})
|
||||
It("Should return valid security policy - GitLab", func() {
|
||||
skipIfTokenIsNot(gitlabPATTokenType, "GitLab only")
|
||||
|
||||
dl := scut.TestDetailLogger{}
|
||||
// project url is gitlab.com/bramw/baserow.
|
||||
repo, err := gitlabrepo.MakeGitlabRepo("gitlab.com/ossf-test/baserow")
|
||||
Expect(err).Should(BeNil())
|
||||
repoClient, err := gitlabrepo.CreateGitlabClientWithToken(context.Background(), os.Getenv("GITLAB_AUTH_TOKEN"), repo)
|
||||
Expect(err).Should(BeNil())
|
||||
err = repoClient.InitRepo(repo, clients.HeadSHA, 0)
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
req := checker.CheckRequest{
|
||||
Ctx: context.Background(),
|
||||
RepoClient: repoClient,
|
||||
Repo: repo,
|
||||
Dlogger: &dl,
|
||||
}
|
||||
// TODO: update expected based on what is returned from gitlab project.
|
||||
expected := scut.TestReturn{
|
||||
Error: nil,
|
||||
Score: 9,
|
||||
NumberOfWarn: 1,
|
||||
NumberOfInfo: 3,
|
||||
NumberOfDebug: 0,
|
||||
}
|
||||
result := checks.SecurityPolicy(&req)
|
||||
// New version.
|
||||
Expect(scut.ValidateTestReturn(nil, "policy found", &expected, &result, &dl)).Should(BeTrue())
|
||||
Expect(repoClient.Close()).Should(BeNil())
|
||||
})
|
||||
It("Should return valid security policy at commitSHA - GitLab", func() {
|
||||
skipIfTokenIsNot(gitlabPATTokenType, "GitLab only")
|
||||
|
||||
dl := scut.TestDetailLogger{}
|
||||
// project url is gitlab.com/bramw/baserow.
|
||||
repo, err := gitlabrepo.MakeGitlabRepo("gitlab.com/ossf-test/baserow")
|
||||
Expect(err).Should(BeNil())
|
||||
repoClient, err := gitlabrepo.CreateGitlabClientWithToken(context.Background(), os.Getenv("GITLAB_AUTH_TOKEN"), repo)
|
||||
Expect(err).Should(BeNil())
|
||||
// url to commit is https://gitlab.com/bramw/baserow/-/commit/28e6224b7d86f7b30bad6adb6b42f26a814c2f58
|
||||
err = repoClient.InitRepo(repo, "28e6224b7d86f7b30bad6adb6b42f26a814c2f58", 0)
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
req := checker.CheckRequest{
|
||||
Ctx: context.Background(),
|
||||
RepoClient: repoClient,
|
||||
Repo: repo,
|
||||
Dlogger: &dl,
|
||||
}
|
||||
|
||||
expected := scut.TestReturn{
|
||||
Error: nil,
|
||||
Score: 9,
|
||||
NumberOfWarn: 1,
|
||||
NumberOfInfo: 3,
|
||||
NumberOfDebug: 0,
|
||||
}
|
||||
result := checks.SecurityPolicy(&req)
|
||||
// New version.
|
||||
Expect(scut.ValidateTestReturn(nil, "policy found", &expected, &result, &dl)).Should(BeTrue())
|
||||
Expect(repoClient.Close()).Should(BeNil())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user