owner.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  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. "net/http"
  9. "os"
  10. "os/signal"
  11. "path"
  12. "strings"
  13. "fmt"
  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. return err
  177. owner.failF("Unabled to create temp directories for cloning")
  178. }
  179. owner.path = dir
  180. } else {
  181. if _, err := os.Stat(opts.ClonePath); os.IsNotExist(err) {
  182. os.Mkdir(owner.path, os.ModePerm)
  183. }
  184. }
  185. return nil
  186. // TODO could be handled via option
  187. // owner.reportPath = filepath.Join(gitLeaksPath, "report", owner.name)
  188. }
  189. // rmTmp removes the temporary repo
  190. func (owner *Owner) rmTmp() {
  191. os.RemoveAll(owner.path)
  192. os.Exit(EXIT_FAILURE)
  193. }
  194. // ownerType returns the owner type extracted from opts.
  195. // If no owner type is provided, gitleaks assumes the owner is ambiguous
  196. // and the user is running gitleaks on a single repo
  197. func ownerType() string {
  198. if opts.OrgMode {
  199. return "org"
  200. } else if opts.UserMode {
  201. return "user"
  202. }
  203. return ""
  204. }
  205. // ownerName returns the owner name extracted from the urls provided in opts.
  206. // If no RepoURL, OrgURL, or UserURL is provided, then owner will log an error
  207. // and gitleaks will exit.
  208. func ownerName() (string) {
  209. if opts.RepoMode {
  210. splitSlashes := strings.Split(opts.URL, "/")
  211. return splitSlashes[len(splitSlashes)-2]
  212. } else if opts.UserMode|| opts.OrgMode {
  213. _, ownerName := path.Split(opts.URL)
  214. return ownerName
  215. }
  216. // local repo
  217. return ""
  218. }
  219. // githubTokenClient creates an oauth client from your github access token.
  220. // Gitleaks will attempt to retrieve your github access token from a cli argument
  221. // or an env var - "GITHUB_TOKEN".
  222. // Might be good to eventually parse the token from a Config or creds file in
  223. // $GITLEAKS_HOME
  224. func githubTokenClient() *http.Client {
  225. var token string
  226. if opts.Token != "" {
  227. token = opts.Token
  228. } else {
  229. token = os.Getenv("GITHUB_TOKEN")
  230. }
  231. if token == "" {
  232. return nil
  233. }
  234. tokenService := oauth2.StaticTokenSource(
  235. &oauth2.Token{AccessToken: token},
  236. )
  237. tokenClient := oauth2.NewClient(context.Background(), tokenService)
  238. return tokenClient
  239. }