Path: blob/dev/pkg/external/customtemplates/github.go
2844 views
package customtemplates12import (3"context"4httpclient "net/http"5"path/filepath"6"strings"78"github.com/go-git/go-git/v5"9"github.com/go-git/go-git/v5/plumbing/transport/http"10"github.com/google/go-github/github"11"github.com/pkg/errors"12"github.com/projectdiscovery/gologger"13"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"14"github.com/projectdiscovery/nuclei/v3/pkg/types"15"github.com/projectdiscovery/utils/errkit"16fileutil "github.com/projectdiscovery/utils/file"17folderutil "github.com/projectdiscovery/utils/folder"18"golang.org/x/oauth2"19)2021var _ Provider = &customTemplateGitHubRepo{}2223type customTemplateGitHubRepo struct {24owner string25reponame string26gitCloneURL string27githubToken string28}2930// This function download the custom github template repository31func (customTemplate *customTemplateGitHubRepo) Download(ctx context.Context) {32clonePath := customTemplate.getLocalRepoClonePath(config.DefaultConfig.CustomGitHubTemplatesDirectory)3334if !fileutil.FolderExists(clonePath) {35err := customTemplate.cloneRepo(clonePath, customTemplate.githubToken)36if err != nil {37gologger.Error().Msgf("%s", err)38} else {39gologger.Info().Msgf("Repo %s/%s cloned successfully at %s", customTemplate.owner, customTemplate.reponame, clonePath)40}41return42}43}4445func (customTemplate *customTemplateGitHubRepo) Update(ctx context.Context) {46downloadPath := config.DefaultConfig.CustomGitHubTemplatesDirectory47clonePath := customTemplate.getLocalRepoClonePath(downloadPath)4849// If folder does not exist then clone/download the repo50if !fileutil.FolderExists(clonePath) {51customTemplate.Download(ctx)52return53}5455// Attempt to pull changes and handle the result56customTemplate.handlePullChanges(clonePath)57}5859// handlePullChanges attempts to pull changes and logs the appropriate message60func (customTemplate *customTemplateGitHubRepo) handlePullChanges(clonePath string) {61err := customTemplate.pullChanges(clonePath, customTemplate.githubToken)6263switch {64case err == nil:65customTemplate.logPullSuccess()66case errors.Is(err, git.NoErrAlreadyUpToDate):67customTemplate.logAlreadyUpToDate(err)68default:69customTemplate.logPullError(err)70}71}7273// logPullSuccess logs a success message when changes are pulled74func (customTemplate *customTemplateGitHubRepo) logPullSuccess() {75gologger.Info().Msgf("Repo %s/%s successfully pulled the changes.\n", customTemplate.owner, customTemplate.reponame)76}7778// logAlreadyUpToDate logs an info message when repo is already up to date79func (customTemplate *customTemplateGitHubRepo) logAlreadyUpToDate(err error) {80gologger.Info().Msgf("%s", err)81}8283// logPullError logs an error message when pull fails84func (customTemplate *customTemplateGitHubRepo) logPullError(err error) {85gologger.Error().Msgf("%s", err)86}8788// NewGitHubProviders returns new instance of GitHub providers for downloading custom templates89func NewGitHubProviders(options *types.Options) ([]*customTemplateGitHubRepo, error) {90providers := []*customTemplateGitHubRepo{}91gitHubClient := getGHClientIncognito()9293if options.GitHubTemplateDisableDownload {94return providers, nil95}9697for _, repoName := range options.GitHubTemplateRepo {98owner, repo, err := getOwnerAndRepo(repoName)99if err != nil {100gologger.Error().Msgf("%s", err)101continue102}103githubRepo, err := getGitHubRepo(gitHubClient, owner, repo, options.GitHubToken)104if err != nil {105gologger.Error().Msgf("%s", err)106continue107}108customTemplateRepo := &customTemplateGitHubRepo{109owner: owner,110reponame: repo,111gitCloneURL: githubRepo.GetCloneURL(),112githubToken: options.GitHubToken,113}114providers = append(providers, customTemplateRepo)115116customTemplateRepo.restructureRepoDir()117}118return providers, nil119}120121func (customTemplateRepo *customTemplateGitHubRepo) restructureRepoDir() {122customGitHubTemplatesDirectory := config.DefaultConfig.CustomGitHubTemplatesDirectory123oldRepoClonePath := filepath.Join(customGitHubTemplatesDirectory, customTemplateRepo.reponame+"-"+customTemplateRepo.owner)124newRepoClonePath := customTemplateRepo.getLocalRepoClonePath(customGitHubTemplatesDirectory)125126if fileutil.FolderExists(oldRepoClonePath) && !fileutil.FolderExists(newRepoClonePath) {127_ = folderutil.SyncDirectory(oldRepoClonePath, newRepoClonePath)128}129}130131// getOwnerAndRepo returns the owner, repo, err from the given string132// e.g., it takes input projectdiscovery/nuclei-templates and133// returns owner => projectdiscovery, repo => nuclei-templates134func getOwnerAndRepo(reponame string) (owner string, repo string, err error) {135s := strings.Split(reponame, "/")136if len(s) != 2 {137err = errors.Errorf("wrong Repo name: %s", reponame)138return139}140owner = s[0]141repo = s[1]142return143}144145// returns *github.Repository if passed github repo name146func getGitHubRepo(gitHubClient *github.Client, repoOwner, repoName, githubToken string) (*github.Repository, error) {147var retried bool148getRepo:149repo, _, err := gitHubClient.Repositories.Get(context.Background(), repoOwner, repoName)150if err != nil {151// retry with authentication152if gitHubClient = getGHClientWithToken(githubToken); gitHubClient != nil && !retried {153retried = true154goto getRepo155}156return nil, err157}158if repo == nil {159return nil, errors.Errorf("problem getting repository: %s/%s", repoOwner, repoName)160}161return repo, nil162}163164// download the git repo to a given path165func (ctr *customTemplateGitHubRepo) cloneRepo(clonePath, githubToken string) error {166cloneOpts := &git.CloneOptions{167URL: ctr.gitCloneURL,168Auth: getAuth(ctr.owner, githubToken),169SingleBranch: true,170Depth: 1,171}172173err := cloneOpts.Validate()174if err != nil {175return err176}177178r, err := git.PlainClone(clonePath, false, cloneOpts)179if err != nil {180return errors.Errorf("%s/%s: %s", ctr.owner, ctr.reponame, err.Error())181}182183// Add the user as well in the config. By default, user is not set184config, _ := r.Storer.Config()185config.User.Name = ctr.owner186187return r.SetConfig(config)188}189190// performs the git pull on given repo191func (ctr *customTemplateGitHubRepo) pullChanges(repoPath, githubToken string) error {192pullOpts := &git.PullOptions{193RemoteName: "origin",194Auth: getAuth(ctr.owner, githubToken),195SingleBranch: true,196Depth: 1,197}198199err := pullOpts.Validate()200if err != nil {201return err202}203204r, err := git.PlainOpen(repoPath)205if err != nil {206return err207}208209w, err := r.Worktree()210if err != nil {211return err212}213214err = w.Pull(pullOpts)215if err != nil {216return errkit.Wrapf(err, "%s/%s", ctr.owner, ctr.reponame)217}218219return nil220}221222// All Custom github repos are cloned in the format of 'owner/reponame' for uniqueness223func (ctr *customTemplateGitHubRepo) getLocalRepoClonePath(downloadPath string) string {224return filepath.Join(downloadPath, ctr.owner, ctr.reponame)225}226227// returns the auth object with username and github token as password228func getAuth(username, password string) *http.BasicAuth {229if username != "" && password != "" {230return &http.BasicAuth{Username: username, Password: password}231}232return nil233}234235func getGHClientWithToken(token string) *github.Client {236if token != "" {237ctx := context.Background()238ts := oauth2.StaticTokenSource(239&oauth2.Token{AccessToken: token},240)241oauthClient := oauth2.NewClient(ctx, ts)242return github.NewClient(oauthClient)243244}245return nil246}247248func getGHClientIncognito() *github.Client {249var tc *httpclient.Client250return github.NewClient(tc)251}252253254