| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274 |
- package main
- import (
- "context"
- "fmt"
- "github.com/google/go-github/github"
- "golang.org/x/oauth2"
- "io/ioutil"
- "log"
- "net/http"
- "os"
- "os/signal"
- "path"
- "strings"
- )
- // Owner represents the owner of a repo or group of repos.
- // Owners can fall under three categories depending on how
- // Gitleaks is ran; ambiguous, user, or organization.
- // An ambiguous implies that gitleaks is running on a single
- // repo from github or locally.
- type Owner struct {
- name string
- url string
- accountType string
- path string
- reportPath string
- repos []Repo
- }
- // ownerPath is used by newOwner and is responsible for returning a path parsed from
- // opts.ClonePath, PWD, or a temporary directory. If a user provides --clone-path=$Home/Desktop/audits
- // then the owner path with be $HOME/Desktop/audits. If the user does not provide a --clone-path= argument
- // then ownerPath will return the current working directory. If the user sets the temporary option, then
- // ownerPath will be $TMPDIR/ownerName. For example running gitleaks on github.com/mozilla, ownerPath would
- // return $TMPDIR/mozilla
- func ownerPath(ownerName string) (string, error) {
- if opts.Tmp {
- dir, err := ioutil.TempDir("", ownerName)
- return dir, err
- } else if opts.ClonePath != "" {
- if _, err := os.Stat(opts.ClonePath); os.IsNotExist(err) {
- os.Mkdir(opts.ClonePath, os.ModePerm)
- }
- return opts.ClonePath, nil
- } else {
- return os.Getwd()
- }
- }
- // newOwner is the entry point for gitleaks after all the options have been parsed and
- // is responsible for returning an Owner pointer. If running in localmode then the Owner
- // that gets created will create a single repo specified in opts.RepoPath. Otherwise
- // newOwner will go out to github and fetch all the repos associated with the owner if
- // gitleaks is running in owner mode. If gitleaks is running in a non-local repo mode, then
- // newOwner will skip hitting the github api and go directly to cloning.
- func newOwner() *Owner {
- name := ownerName()
- ownerPath, err := ownerPath(name)
- if err != nil {
- log.Fatal(err)
- }
- owner := &Owner{
- name: name,
- url: opts.URL,
- accountType: ownerType(),
- path: ownerPath,
- }
- // listen for ctrl-c
- // NOTE: need some help on how to actually shut down gracefully.
- // On interrupt a repo may still be trying to clone... This has no
- // actual effect other than extraneous logging.
- sigC := make(chan os.Signal, 1)
- signal.Notify(sigC, os.Interrupt, os.Interrupt)
- go func() {
- <-sigC
- if opts.Tmp {
- owner.rmTmp()
- }
- os.Exit(ExitFailure)
- }()
- err = owner.fetchRepos()
- if err != nil {
- log.Fatal(err)
- }
- return owner
- }
- // fetchRepos is used by newOwner and is responsible for fetching one or more
- // of the owner's repos. If opts.RepoURL is not the empty string then fetchRepos will
- // only grab the repo specified in opts.RepoURL. Otherwise, fetchRepos will reach out to
- // github's api and grab all repos associated with owner.
- func (owner *Owner) fetchRepos() error {
- var err error
- ctx := context.Background()
- // local mode, single repo, ambiguous account type
- if opts.LocalMode {
- _, repoName := path.Split(opts.RepoPath)
- repo := newRepo(repoName, "", opts.RepoPath)
- owner.repos = append(owner.repos, *repo)
- return nil
- }
- if owner.accountType == "" {
- // single repo, ambiguous account type
- _, repoName := path.Split(opts.URL)
- repo := newRepo(repoName, opts.URL, owner.path+"/"+repoName)
- owner.repos = append(owner.repos, *repo)
- } else {
- // org or user account type, would fail if not valid before
- tokenClient := githubTokenClient()
- gitClient := github.NewClient(tokenClient)
- if owner.accountType == "org" {
- // org account type
- orgOpt := &github.RepositoryListByOrgOptions{
- ListOptions: github.ListOptions{PerPage: 10},
- }
- err = owner.fetchOrgRepos(ctx, orgOpt, gitClient)
- } else {
- // user account type
- userOpt := &github.RepositoryListOptions{
- ListOptions: github.ListOptions{PerPage: 10},
- }
- err = owner.fetchUserRepos(ctx, userOpt, gitClient)
- }
- }
- return err
- }
- // fetchOrgRepos used by fetchRepos is responsible for parsing github's org repo response. If no
- // github token is available then fetchOrgRepos might run into a rate limit in which case owner will
- // log an error and gitleaks will exit. The rate limit for no token is 50 req/hour... not much.
- func (owner *Owner) fetchOrgRepos(ctx context.Context, orgOpts *github.RepositoryListByOrgOptions,
- gitClient *github.Client) error {
- var (
- githubRepos []*github.Repository
- resp *github.Response
- err error
- )
- for {
- githubRepos, resp, err = gitClient.Repositories.ListByOrg(
- ctx, owner.name, orgOpts)
- owner.addRepos(githubRepos)
- if _, ok := err.(*github.RateLimitError); ok {
- log.Printf("hit rate limit retreiving %s, continuing with partial audit\n",
- owner.name)
- } else if err != nil {
- return fmt.Errorf("failed obtaining %s repos from githuib api, bad request", owner.name)
- } else if resp.NextPage == 0 {
- break
- }
- orgOpts.Page = resp.NextPage
- }
- return nil
- }
- // fetchUserRepos used by fetchRepos is responsible for parsing github's user repo response. If no
- // github token is available then fetchUserRepos might run into a rate limit in which case owner will
- // log an error and gitleaks will exit. The rate limit for no token is 50 req/hour... not much.
- // sorry for the redundancy
- func (owner *Owner) fetchUserRepos(ctx context.Context, userOpts *github.RepositoryListOptions,
- gitClient *github.Client) error {
- var (
- githubRepos []*github.Repository
- resp *github.Response
- err error
- )
- for {
- githubRepos, resp, err = gitClient.Repositories.List(
- ctx, owner.name, userOpts)
- owner.addRepos(githubRepos)
- if _, ok := err.(*github.RateLimitError); ok {
- log.Printf("hit rate limit retreiving %s, continuing with partial audit\n",
- owner.name)
- break
- } else if err != nil {
- return fmt.Errorf("failed obtaining %s repos from github api, bad request", owner.name)
- } else if resp.NextPage == 0 {
- break
- }
- userOpts.Page = resp.NextPage
- }
- return nil
- }
- // addRepos used by fetchUserRepos and fetchOrgRepos appends new repos from
- // github's org/user response.
- func (owner *Owner) addRepos(githubRepos []*github.Repository) {
- for _, repo := range githubRepos {
- owner.repos = append(owner.repos, *newRepo(*repo.Name, *repo.CloneURL, owner.path+"/"+*repo.Name))
- }
- }
- // auditRepos is responsible for auditing all the owner's
- // repos. auditRepos is used by main and will return the following exit codes
- // 0: The audit succeeded with no findings
- // 1: The audit failed, or wasn't attempted due to an execution failure.
- // 2: The audit succeeded, and secrets / patterns were found.
- func (owner *Owner) auditRepos() int {
- exitCode := ExitClean
- for _, repo := range owner.repos {
- leaksPst, err := repo.audit()
- if err != nil {
- log.Fatal(err)
- }
- if leaksPst {
- exitCode = ExitLeaks
- }
- }
- if opts.Tmp {
- owner.rmTmp()
- }
- return exitCode
- }
- // rmTmp removes the owner's temporary repo. rmTmp will only get called if temporary
- // mode is set. rmTmp is called on a SIGINT and after the audits have finished
- func (owner *Owner) rmTmp() {
- log.Printf("removing tmp gitleaks repo for %s\n", owner.path)
- os.RemoveAll(owner.path)
- }
- // ownerType returns the owner type extracted from opts.
- // If no owner type is provided, gitleaks assumes the owner is ambiguous
- // and the user is running gitleaks on a single repo
- func ownerType() string {
- if opts.OrgMode {
- return "org"
- } else if opts.UserMode {
- return "user"
- }
- return ""
- }
- // ownerName returns the owner name extracted from the urls provided in opts.
- // If no RepoURL, OrgURL, or UserURL is provided, then owner will log an error
- // and gitleaks will exit.
- func ownerName() string {
- if opts.RepoMode {
- splitSlashes := strings.Split(opts.URL, "/")
- return splitSlashes[len(splitSlashes)-2]
- } else if opts.UserMode || opts.OrgMode {
- _, ownerName := path.Split(opts.URL)
- return ownerName
- }
- // local repo
- return ""
- }
- // githubTokenClient creates an oauth client from your github access token.
- // Gitleaks will attempt to retrieve your github access token from a cli argument
- // or an env var - "GITHUB_TOKEN".
- func githubTokenClient() *http.Client {
- var token string
- if opts.Token != "" {
- token = opts.Token
- } else {
- token = os.Getenv("GITHUB_TOKEN")
- }
- if token == "" {
- return nil
- }
- tokenService := oauth2.StaticTokenSource(
- &oauth2.Token{AccessToken: token},
- )
- tokenClient := oauth2.NewClient(context.Background(), tokenService)
- return tokenClient
- }
|