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