github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/fanal/artifact/repo/git.go (about)

     1  package repo
     2  
     3  import (
     4  	"context"
     5  	"net/url"
     6  	"os"
     7  
     8  	"github.com/go-git/go-git/v5"
     9  	"github.com/go-git/go-git/v5/plumbing"
    10  	"github.com/go-git/go-git/v5/plumbing/transport/http"
    11  	"github.com/hashicorp/go-multierror"
    12  	"golang.org/x/xerrors"
    13  
    14  	"github.com/devseccon/trivy/pkg/fanal/artifact"
    15  	"github.com/devseccon/trivy/pkg/fanal/artifact/local"
    16  	"github.com/devseccon/trivy/pkg/fanal/cache"
    17  	"github.com/devseccon/trivy/pkg/fanal/types"
    18  )
    19  
    20  type Artifact struct {
    21  	url   string
    22  	local artifact.Artifact
    23  }
    24  
    25  func NewArtifact(target string, c cache.ArtifactCache, artifactOpt artifact.Option) (
    26  	artifact.Artifact, func(), error) {
    27  
    28  	var cleanup func()
    29  	var errs error
    30  
    31  	// Try the local repository
    32  	art, err := tryLocalRepo(target, c, artifactOpt)
    33  	if err == nil {
    34  		return art, func() {}, nil
    35  	}
    36  	errs = multierror.Append(errs, err)
    37  
    38  	// Try the remote git repository
    39  	art, cleanup, err = tryRemoteRepo(target, c, artifactOpt)
    40  	if err == nil {
    41  		return art, cleanup, nil
    42  	}
    43  	errs = multierror.Append(errs, err)
    44  
    45  	// Return errors
    46  	return nil, cleanup, errs
    47  }
    48  
    49  func (a Artifact) Inspect(ctx context.Context) (types.ArtifactReference, error) {
    50  	ref, err := a.local.Inspect(ctx)
    51  	if err != nil {
    52  		return types.ArtifactReference{}, xerrors.Errorf("remote repository error: %w", err)
    53  	}
    54  
    55  	if a.url != "" {
    56  		ref.Name = a.url
    57  	}
    58  	ref.Type = types.ArtifactRepository
    59  
    60  	return ref, nil
    61  }
    62  
    63  func (Artifact) Clean(_ types.ArtifactReference) error {
    64  	return nil
    65  }
    66  
    67  func tryLocalRepo(target string, c cache.ArtifactCache, artifactOpt artifact.Option) (artifact.Artifact, error) {
    68  	if _, err := os.Stat(target); err != nil {
    69  		return nil, xerrors.Errorf("no such path: %w", err)
    70  	}
    71  
    72  	art, err := local.NewArtifact(target, c, artifactOpt)
    73  	if err != nil {
    74  		return nil, xerrors.Errorf("local repo artifact error: %w", err)
    75  	}
    76  	return Artifact{
    77  		local: art,
    78  	}, nil
    79  }
    80  
    81  func tryRemoteRepo(target string, c cache.ArtifactCache, artifactOpt artifact.Option) (artifact.Artifact, func(), error) {
    82  	cleanup := func() {}
    83  	u, err := newURL(target)
    84  	if err != nil {
    85  		return nil, cleanup, err
    86  	}
    87  
    88  	tmpDir, err := cloneRepo(u, artifactOpt)
    89  	if err != nil {
    90  		return nil, cleanup, xerrors.Errorf("repository clone error: %w", err)
    91  	}
    92  
    93  	cleanup = func() { _ = os.RemoveAll(tmpDir) }
    94  
    95  	art, err := local.NewArtifact(tmpDir, c, artifactOpt)
    96  	if err != nil {
    97  		return nil, cleanup, xerrors.Errorf("fs artifact: %w", err)
    98  	}
    99  
   100  	return Artifact{
   101  		url:   target,
   102  		local: art,
   103  	}, cleanup, nil
   104  
   105  }
   106  
   107  func cloneRepo(u *url.URL, artifactOpt artifact.Option) (string, error) {
   108  	tmpDir, err := os.MkdirTemp("", "trivy-remote-repo")
   109  	if err != nil {
   110  		return "", xerrors.Errorf("failed to create a temp dir: %w", err)
   111  	}
   112  
   113  	cloneOptions := git.CloneOptions{
   114  		URL:             u.String(),
   115  		Auth:            gitAuth(),
   116  		Progress:        os.Stdout,
   117  		InsecureSkipTLS: artifactOpt.Insecure,
   118  	}
   119  
   120  	// suppress clone output if noProgress
   121  	if artifactOpt.NoProgress {
   122  		cloneOptions.Progress = nil
   123  	}
   124  
   125  	if artifactOpt.RepoCommit == "" {
   126  		cloneOptions.Depth = 1
   127  	}
   128  
   129  	if artifactOpt.RepoBranch != "" {
   130  		cloneOptions.ReferenceName = plumbing.NewBranchReferenceName(artifactOpt.RepoBranch)
   131  		cloneOptions.SingleBranch = true
   132  	}
   133  
   134  	if artifactOpt.RepoTag != "" {
   135  		cloneOptions.ReferenceName = plumbing.NewTagReferenceName(artifactOpt.RepoTag)
   136  		cloneOptions.SingleBranch = true
   137  	}
   138  
   139  	r, err := git.PlainClone(tmpDir, false, &cloneOptions)
   140  	if err != nil {
   141  		return "", xerrors.Errorf("git clone error: %w", err)
   142  	}
   143  
   144  	if artifactOpt.RepoCommit != "" {
   145  		w, err := r.Worktree()
   146  		if err != nil {
   147  			return "", xerrors.Errorf("git worktree error: %w", err)
   148  		}
   149  
   150  		err = w.Checkout(&git.CheckoutOptions{
   151  			Hash: plumbing.NewHash(artifactOpt.RepoCommit),
   152  		})
   153  		if err != nil {
   154  			return "", xerrors.Errorf("git checkout error: %w", err)
   155  		}
   156  	}
   157  
   158  	return tmpDir, nil
   159  }
   160  
   161  func newURL(rawurl string) (*url.URL, error) {
   162  	u, err := url.Parse(rawurl)
   163  	if err != nil {
   164  		return nil, xerrors.Errorf("url parse error: %w", err)
   165  	}
   166  	// "https://" can be omitted
   167  	// e.g. github.com/devseccon/trivy
   168  	if u.Scheme == "" {
   169  		u.Scheme = "https"
   170  	}
   171  
   172  	return u, nil
   173  }
   174  
   175  // Helper function to check for a GitHub/GitLab token from env vars in order to
   176  // make authenticated requests to access private repos
   177  func gitAuth() *http.BasicAuth {
   178  	var auth *http.BasicAuth
   179  
   180  	// The username can be anything for HTTPS Git operations
   181  	gitUsername := "fanal-aquasecurity-scan"
   182  
   183  	// We first check if a GitHub token was provided
   184  	githubToken := os.Getenv("GITHUB_TOKEN")
   185  	if githubToken != "" {
   186  		auth = &http.BasicAuth{
   187  			Username: gitUsername,
   188  			Password: githubToken,
   189  		}
   190  		return auth
   191  	}
   192  
   193  	// Otherwise we check if a GitLab token was provided
   194  	gitlabToken := os.Getenv("GITLAB_TOKEN")
   195  	if gitlabToken != "" {
   196  		auth = &http.BasicAuth{
   197  			Username: gitUsername,
   198  			Password: gitlabToken,
   199  		}
   200  		return auth
   201  	}
   202  
   203  	// If no token was provided, we simply return a nil,
   204  	// which will make the request to be unauthenticated
   205  	return nil
   206  
   207  }