owner.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "github.com/google/go-github/github"
  6. "golang.org/x/oauth2"
  7. "io/ioutil"
  8. "log"
  9. "net/http"
  10. "os"
  11. "os/signal"
  12. "path"
  13. "strings"
  14. )
  15. // Owner blah blah
  16. type Owner struct {
  17. name string
  18. url string
  19. accountType string
  20. path string
  21. reportPath string
  22. repos []Repo
  23. }
  24. // ownerPath is used by newOwner and is responsible for returning a path parsed from
  25. // opts.ClonePath, PWD, or a temporary directory. If a user provides --clone-path=$Home/Desktop/audits
  26. // then the owner path with be $HOME/Desktop/audits. If the user does not provide a --clone-path= argument
  27. // then ownerPath will return the current working directory. If the user sets the temporary option, then
  28. // ownerPath will be $TMPDIR/ownerName. For example running gitleaks on github.com/mozilla, ownerPath would
  29. // return $TMPDIR/mozilla
  30. func ownerPath(ownerName string) (string, error) {
  31. if opts.Tmp {
  32. dir, err := ioutil.TempDir("", ownerName)
  33. return dir, err
  34. } else if opts.ClonePath != "" {
  35. if _, err := os.Stat(opts.ClonePath); os.IsNotExist(err) {
  36. os.Mkdir(opts.ClonePath, os.ModePerm)
  37. }
  38. return opts.ClonePath, nil
  39. } else {
  40. return os.Getwd()
  41. }
  42. }
  43. // newOwner is the entry point for gitleaks after all the options have been parsed and
  44. // is responsible for returning an Owner pointer. If running in localmode then the Owner
  45. // that gets created will create a single repo specified in opts.RepoPath. Otherwise
  46. // newOwner will go out to github and fetch all the repos associated with the owner if
  47. // gitleaks is running in owner mode. If gitleaks is running in a non-local repo mode, then
  48. // newOwner will skip hitting the github api and go directly to cloning.
  49. func newOwner() *Owner {
  50. name := ownerName()
  51. ownerPath, err := ownerPath(name)
  52. if err != nil {
  53. failF("%v", err)
  54. }
  55. owner := &Owner{
  56. name: name,
  57. url: opts.URL,
  58. accountType: ownerType(),
  59. path: ownerPath,
  60. }
  61. // listen for ctrl-c
  62. // NOTE: need some help on how to actually shut down gracefully.
  63. // On interrupt a repo may still be trying to clone... This has no
  64. // actual effect other than extraneous logging.
  65. sigC := make(chan os.Signal, 1)
  66. signal.Notify(sigC, os.Interrupt, os.Interrupt)
  67. go func() {
  68. <-sigC
  69. if opts.Tmp {
  70. owner.rmTmp()
  71. }
  72. os.Exit(ExitFailure)
  73. }()
  74. // if running on local repo, just go right to it.
  75. if opts.LocalMode {
  76. repo := newLocalRepo(opts.RepoPath)
  77. owner.repos = append(owner.repos, *repo)
  78. return owner
  79. }
  80. err = owner.fetchRepos()
  81. if err != nil {
  82. owner.failF("%v", err)
  83. }
  84. return owner
  85. }
  86. // fetchRepos is used by newOwner and is responsible for fetching one or more
  87. // of the owner's repos. If opts.RepoURL is not the empty string then fetchRepos will
  88. // only grab the repo specified in opts.RepoURL. Otherwise, fetchRepos will reach out to
  89. // github's api and grab all repos associated with owner.
  90. func (owner *Owner) fetchRepos() error {
  91. var err error
  92. ctx := context.Background()
  93. if owner.accountType == "" {
  94. // single repo, ambiguous account type
  95. _, repoName := path.Split(opts.URL)
  96. repo := newRepo(repoName, opts.URL, owner.path+"/"+repoName)
  97. owner.repos = append(owner.repos, *repo)
  98. } else {
  99. // org or user account type, would fail if not valid before
  100. tokenClient := githubTokenClient()
  101. gitClient := github.NewClient(tokenClient)
  102. if owner.accountType == "org" {
  103. // org account type
  104. orgOpt := &github.RepositoryListByOrgOptions{
  105. ListOptions: github.ListOptions{PerPage: 10},
  106. }
  107. err = owner.fetchOrgRepos(ctx, orgOpt, gitClient)
  108. } else {
  109. // user account type
  110. userOpt := &github.RepositoryListOptions{
  111. ListOptions: github.ListOptions{PerPage: 10},
  112. }
  113. err = owner.fetchUserRepos(ctx, userOpt, gitClient)
  114. }
  115. }
  116. return err
  117. }
  118. // fetchOrgRepos used by fetchRepos is responsible for parsing github's org repo response. If no
  119. // github token is available then fetchOrgRepos might run into a rate limit in which case owner will
  120. // log an error and gitleaks will exit. The rate limit for no token is 50 req/hour... not much.
  121. func (owner *Owner) fetchOrgRepos(ctx context.Context, orgOpts *github.RepositoryListByOrgOptions,
  122. gitClient *github.Client) error {
  123. var (
  124. githubRepos []*github.Repository
  125. resp *github.Response
  126. err error
  127. )
  128. for {
  129. githubRepos, resp, err = gitClient.Repositories.ListByOrg(
  130. ctx, owner.name, orgOpts)
  131. owner.addRepos(githubRepos)
  132. if _, ok := err.(*github.RateLimitError); ok {
  133. log.Printf("hit rate limit retreiving %s, continuing with partial audit\n",
  134. owner.name)
  135. } else if err != nil {
  136. return fmt.Errorf("failed obtaining %s repos from githuib api, bad request", owner.name)
  137. } else if resp.NextPage == 0 {
  138. break
  139. }
  140. orgOpts.Page = resp.NextPage
  141. }
  142. return nil
  143. }
  144. // fetchUserRepos used by fetchRepos is responsible for parsing github's user repo response. If no
  145. // github token is available then fetchUserRepos might run into a rate limit in which case owner will
  146. // log an error and gitleaks will exit. The rate limit for no token is 50 req/hour... not much.
  147. // sorry for the redundancy
  148. func (owner *Owner) fetchUserRepos(ctx context.Context, userOpts *github.RepositoryListOptions,
  149. gitClient *github.Client) error {
  150. var (
  151. githubRepos []*github.Repository
  152. resp *github.Response
  153. err error
  154. )
  155. for {
  156. githubRepos, resp, err = gitClient.Repositories.List(
  157. ctx, owner.name, userOpts)
  158. owner.addRepos(githubRepos)
  159. if _, ok := err.(*github.RateLimitError); ok {
  160. log.Printf("hit rate limit retreiving %s, continuing with partial audit\n",
  161. owner.name)
  162. break
  163. } else if err != nil {
  164. return fmt.Errorf("failed obtaining %s repos from github api, bad request", owner.name)
  165. } else if resp.NextPage == 0 {
  166. break
  167. }
  168. userOpts.Page = resp.NextPage
  169. }
  170. return nil
  171. }
  172. // addRepos used by fetchUserRepos and fetchOrgRepos appends new repos from
  173. // github's org/user response.
  174. func (owner *Owner) addRepos(githubRepos []*github.Repository) {
  175. for _, repo := range githubRepos {
  176. owner.repos = append(owner.repos, *newRepo(*repo.Name, *repo.CloneURL, owner.path+"/"+*repo.Name))
  177. }
  178. }
  179. // auditRepos is responsible for auditing all the owner's
  180. // repos. auditRepos is used by main and will return the following exit codes
  181. // 0: The audit succeeded with no findings
  182. // 1: The audit failed, or wasn't attempted due to an execution failure.
  183. // 2: The audit succeeded, and secrets / patterns were found.
  184. func (owner *Owner) auditRepos() int {
  185. exitCode := ExitClean
  186. for _, repo := range owner.repos {
  187. leaksPst, err := repo.audit()
  188. if err != nil {
  189. failF("%v", err)
  190. }
  191. if leaksPst {
  192. exitCode = ExitLeaks
  193. }
  194. }
  195. if opts.Tmp {
  196. owner.rmTmp()
  197. }
  198. return exitCode
  199. }
  200. // failF prints a failure message out to stderr
  201. // and exits with a exit code 2
  202. func (owner *Owner) failF(format string, args ...interface{}) {
  203. fmt.Fprintf(os.Stderr, format, args...)
  204. os.Exit(ExitFailure)
  205. }
  206. // rmTmp removes the owner's temporary repo. rmTmp will only get called if temporary
  207. // mode is set. rmTmp is called on a SIGINT and after the audits have finished
  208. func (owner *Owner) rmTmp() {
  209. log.Printf("removing tmp gitleaks repo for %s\n", owner.path)
  210. os.RemoveAll(owner.path)
  211. }
  212. // ownerType returns the owner type extracted from opts.
  213. // If no owner type is provided, gitleaks assumes the owner is ambiguous
  214. // and the user is running gitleaks on a single repo
  215. func ownerType() string {
  216. if opts.OrgMode {
  217. return "org"
  218. } else if opts.UserMode {
  219. return "user"
  220. }
  221. return ""
  222. }
  223. // ownerName returns the owner name extracted from the urls provided in opts.
  224. // If no RepoURL, OrgURL, or UserURL is provided, then owner will log an error
  225. // and gitleaks will exit.
  226. func ownerName() string {
  227. if opts.RepoMode {
  228. splitSlashes := strings.Split(opts.URL, "/")
  229. return splitSlashes[len(splitSlashes)-2]
  230. } else if opts.UserMode || opts.OrgMode {
  231. _, ownerName := path.Split(opts.URL)
  232. return ownerName
  233. }
  234. // local repo
  235. return ""
  236. }
  237. // githubTokenClient creates an oauth client from your github access token.
  238. // Gitleaks will attempt to retrieve your github access token from a cli argument
  239. // or an env var - "GITHUB_TOKEN".
  240. func githubTokenClient() *http.Client {
  241. var token string
  242. if opts.Token != "" {
  243. token = opts.Token
  244. } else {
  245. token = os.Getenv("GITHUB_TOKEN")
  246. }
  247. if token == "" {
  248. return nil
  249. }
  250. tokenService := oauth2.StaticTokenSource(
  251. &oauth2.Token{AccessToken: token},
  252. )
  253. tokenClient := oauth2.NewClient(context.Background(), tokenService)
  254. return tokenClient
  255. }