owner.go 6.9 KB

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