-
Notifications
You must be signed in to change notification settings - Fork 518
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ Add GitHub git compatibility mode (#4474)
* add git handler for GitHub repositories This is primarily aimed at helping in cases where a repository's .gitattributes file causes files to not be analyzed. Signed-off-by: Spencer Schrock <[email protected]> * use variadic options to configure GitHub repoclient This will let us use the new entrypoint in a backwards compatible way, similar to the scorecard.Run change made in the v5 release. Signed-off-by: Spencer Schrock <[email protected]> * add flag to enable github git mode Signed-off-by: Spencer Schrock <[email protected]> * rename flag to be forge agnostic export-ignore is not a github specific feature, and other forges, like gitlab, suffer from the same bug. Signed-off-by: Spencer Schrock <[email protected]> * move git file handler to internal package This will allow sharing with GitLab in a followup PR Signed-off-by: Spencer Schrock <[email protected]> * add a test Signed-off-by: Spencer Schrock <[email protected]> * use new toplevel gitmode argument also moves a func around for smaller PR diff. Signed-off-by: Spencer Schrock <[email protected]> * add path traversal test Signed-off-by: Spencer Schrock <[email protected]> * change flag to file-mode Signed-off-by: Spencer Schrock <[email protected]> * fix repo typo in options test the value isn't used to connect to anything though. Signed-off-by: Spencer Schrock <[email protected]> --------- Signed-off-by: Spencer Schrock <[email protected]>
- Loading branch information
1 parent
6fc296e
commit b0143fc
Showing
8 changed files
with
474 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
// Copyright 2025 OpenSSF 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 gitfile defines functionality to list and fetch files after temporarily cloning a git repo. | ||
package gitfile | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
"sync" | ||
|
||
"github.com/go-git/go-git/v5" | ||
"github.com/go-git/go-git/v5/plumbing" | ||
"github.com/go-git/go-git/v5/plumbing/object" | ||
|
||
"github.com/ossf/scorecard/v5/clients" | ||
) | ||
|
||
var errPathTraversal = errors.New("requested file outside repo") | ||
|
||
const repoDir = "repo*" | ||
|
||
type Handler struct { | ||
errSetup error | ||
ctx context.Context | ||
once *sync.Once | ||
cloneURL string | ||
gitRepo *git.Repository | ||
tempDir string | ||
commitSHA string | ||
} | ||
|
||
func (h *Handler) Init(ctx context.Context, cloneURL, commitSHA string) { | ||
h.errSetup = nil | ||
h.once = new(sync.Once) | ||
h.ctx = ctx | ||
h.cloneURL = cloneURL | ||
h.commitSHA = commitSHA | ||
} | ||
|
||
func (h *Handler) setup() error { | ||
h.once.Do(func() { | ||
tempDir, err := os.MkdirTemp("", repoDir) | ||
if err != nil { | ||
h.errSetup = err | ||
return | ||
} | ||
h.tempDir = tempDir | ||
h.gitRepo, err = git.PlainClone(h.tempDir, false, &git.CloneOptions{ | ||
URL: h.cloneURL, | ||
// TODO: auth may be required for private repos | ||
Depth: 1, // currently only use the git repo for files, dont need history | ||
SingleBranch: true, | ||
}) | ||
if err != nil { | ||
h.errSetup = err | ||
return | ||
} | ||
|
||
// assume the commit SHA is reachable from the default branch | ||
// this isn't as flexible as the tarball handler, but good enough for now | ||
if h.commitSHA != clients.HeadSHA { | ||
wt, err := h.gitRepo.Worktree() | ||
if err != nil { | ||
h.errSetup = err | ||
return | ||
} | ||
if err := wt.Checkout(&git.CheckoutOptions{Hash: plumbing.NewHash(h.commitSHA)}); err != nil { | ||
h.errSetup = fmt.Errorf("checkout specified commit: %w", err) | ||
return | ||
} | ||
} | ||
}) | ||
return h.errSetup | ||
} | ||
|
||
func (h *Handler) GetLocalPath() (string, error) { | ||
if err := h.setup(); err != nil { | ||
return "", fmt.Errorf("setup: %w", err) | ||
} | ||
return h.tempDir, nil | ||
} | ||
|
||
func (h *Handler) ListFiles(predicate func(string) (bool, error)) ([]string, error) { | ||
if err := h.setup(); err != nil { | ||
return nil, fmt.Errorf("setup: %w", err) | ||
} | ||
ref, err := h.gitRepo.Head() | ||
if err != nil { | ||
return nil, fmt.Errorf("git.Head: %w", err) | ||
} | ||
|
||
commit, err := h.gitRepo.CommitObject(ref.Hash()) | ||
if err != nil { | ||
return nil, fmt.Errorf("git.CommitObject: %w", err) | ||
} | ||
|
||
tree, err := commit.Tree() | ||
if err != nil { | ||
return nil, fmt.Errorf("git.Commit.Tree: %w", err) | ||
} | ||
|
||
var files []string | ||
err = tree.Files().ForEach(func(f *object.File) error { | ||
shouldInclude, err := predicate(f.Name) | ||
if err != nil { | ||
return fmt.Errorf("error applying predicate to file %s: %w", f.Name, err) | ||
} | ||
|
||
if shouldInclude { | ||
files = append(files, f.Name) | ||
} | ||
return nil | ||
}) | ||
if err != nil { | ||
return nil, fmt.Errorf("git.Tree.Files: %w", err) | ||
} | ||
|
||
return files, nil | ||
} | ||
|
||
func (h *Handler) GetFile(filename string) (*os.File, error) { | ||
if err := h.setup(); err != nil { | ||
return nil, fmt.Errorf("setup: %w", err) | ||
} | ||
|
||
// check for path traversal | ||
path := filepath.Join(h.tempDir, filename) | ||
if !strings.HasPrefix(path, filepath.Clean(h.tempDir)+string(os.PathSeparator)) { | ||
return nil, errPathTraversal | ||
} | ||
|
||
f, err := os.Open(path) | ||
if err != nil { | ||
return nil, fmt.Errorf("open file: %w", err) | ||
} | ||
return f, nil | ||
} | ||
|
||
func (h *Handler) Cleanup() error { | ||
if err := os.RemoveAll(h.tempDir); err != nil && !os.IsNotExist(err) { | ||
return fmt.Errorf("os.Remove: %w", err) | ||
} | ||
return nil | ||
} |
Oops, something went wrong.