owner.go 8.4 KB

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