owner.go 7.1 KB

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