owner.go 6.7 KB

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