owner.go 7.9 KB

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