@@ -12,6 +12,7 @@ import (
12
12
"os/signal"
13
13
"regexp"
14
14
"strings"
15
+ "sync"
15
16
"syscall"
16
17
"time"
17
18
@@ -25,14 +26,11 @@ const (
25
26
CVERegex = "(?i)cve[-–_][0-9]{4}[-–_][0-9]{4,}"
26
27
)
27
28
28
- var ReadmeQuery struct {
29
- Repository struct {
30
- Object struct {
31
- Blob struct {
32
- Text string
33
- } `graphql:"... on Blob"`
34
- } `graphql:"object(expression: \"HEAD:README.md\")"`
35
- } `graphql:"repository(owner: $owner, name: $name)"`
29
+ type RateLimit struct {
30
+ Limit int
31
+ Remaining int
32
+ Cost int
33
+ ResetAt time.Time
36
34
}
37
35
38
36
type Repository struct {
@@ -56,27 +54,24 @@ type RepositoryResult struct {
56
54
Readme * string `json:"readme,omitempty"`
57
55
}
58
56
59
- var CVEQuery struct {
60
- Search struct {
61
- RepositoryCount int
62
- PageInfo struct {
63
- EndCursor githubv4.String
64
- StartCursor githubv4.String
65
- }
66
- Edges []struct {
67
- Node struct {
68
- Repo Repository `graphql:"... on Repository"`
69
- }
70
- }
71
- } `graphql:"search(query: $query, type: REPOSITORY, first: 100)"`
57
+ var ReadmeQuery struct {
58
+ RateLimit RateLimit `graphql:"rateLimit"`
59
+ Repository struct {
60
+ Object struct {
61
+ Blob struct {
62
+ Text string
63
+ } `graphql:"... on Blob"`
64
+ } `graphql:"object(expression: \"HEAD:README.md\")"`
65
+ } `graphql:"repository(owner: $owner, name: $name)"`
72
66
}
73
67
74
- var CVEPaginationQuery struct {
75
- Search struct {
68
+ var CVEQuery struct {
69
+ RateLimit RateLimit `graphql:"rateLimit"`
70
+ Search struct {
76
71
RepositoryCount int
77
72
PageInfo struct {
78
73
EndCursor githubv4.String
79
- StartCursor githubv4. String
74
+ HasNextPage bool
80
75
}
81
76
Edges []struct {
82
77
Node struct {
94
89
githubCreateDate = time .Date (2008 , 2 , 8 , 0 , 0 , 0 , 0 , time .UTC )
95
90
bar = & progressbar.ProgressBar {}
96
91
barInitialized = false
92
+ requestDelay int
93
+ adjustDelay bool
94
+ rateLimit * RateLimit
95
+ delayMutex = & sync.Mutex {}
96
+ outputFile string
97
+ silent bool
97
98
)
98
99
99
100
func getReadme (repoUrl string ) string {
@@ -105,11 +106,22 @@ func getReadme(repoUrl string) string {
105
106
"name" : githubv4 .String (strings .Trim (urlSplit [len (urlSplit )- 1 ], " " )),
106
107
}
107
108
109
+ errHandle:
110
+ start := time .Now ()
108
111
err := githubV4Client .Query (context .Background (), & ReadmeQuery , variables )
112
+ duration := time .Since (start ).Milliseconds () - int64 (time .Millisecond )
109
113
if err != nil {
110
- fmt .Println (err )
111
- return ""
114
+ delayMutex .Lock ()
115
+ rateLimit = & ReadmeQuery .RateLimit
116
+ handleGraphQLAPIError (err )
117
+ delayMutex .Unlock ()
118
+ goto errHandle
112
119
}
120
+ delayMutex .Lock ()
121
+ rateLimit = & ReadmeQuery .RateLimit
122
+ time .Sleep (time .Duration (int64 (requestDelay * rateLimit .Cost )* int64 (time .Millisecond ) - duration ))
123
+ delayMutex .Unlock ()
124
+
113
125
return ReadmeQuery .Repository .Object .Blob .Text
114
126
} else {
115
127
return ""
@@ -122,13 +134,24 @@ func getRepos(query string, startingDate time.Time, endingDate time.Time) {
122
134
startingDate .Format (time .RFC3339 ) + ".." + endingDate .Format (time .RFC3339 )
123
135
variables := map [string ]interface {}{
124
136
"query" : githubv4 .String (query ),
137
+ "after" : (* githubv4 .String )(nil ),
125
138
}
126
139
140
+ errHandle:
141
+ start := time .Now ()
127
142
err := githubV4Client .Query (context .Background (), & CVEQuery , variables )
143
+ duration := time .Since (start ).Milliseconds () - int64 (time .Millisecond )
128
144
if err != nil {
129
- fmt .Println (err )
130
- return
145
+ delayMutex .Lock ()
146
+ rateLimit = & CVEQuery .RateLimit
147
+ handleGraphQLAPIError (err )
148
+ delayMutex .Unlock ()
149
+ goto errHandle
131
150
}
151
+ delayMutex .Lock ()
152
+ rateLimit = & CVEQuery .RateLimit
153
+ time .Sleep (time .Duration (int64 (requestDelay * rateLimit .Cost )* int64 (time .Millisecond ) - duration ))
154
+ delayMutex .Unlock ()
132
155
133
156
maxRepos := CVEQuery .Search .RepositoryCount
134
157
if ! barInitialized {
@@ -140,6 +163,32 @@ func getRepos(query string, startingDate time.Time, endingDate time.Time) {
140
163
progressbar .OptionOnCompletion (func () { fmt .Println () }),
141
164
)
142
165
barInitialized = true
166
+ if adjustDelay {
167
+ go func () {
168
+ for {
169
+ delayMutex .Lock ()
170
+ remainingRepos := bar .GetMax () - len (reposResults )
171
+ remainingRequests := remainingRepos + remainingRepos / 100 + 1
172
+ if remainingRequests < rateLimit .Remaining {
173
+ requestDelay = 0
174
+ delayMutex .Unlock ()
175
+ break
176
+ } else {
177
+ if rateLimit .Remaining == 0 {
178
+ handleGraphQLAPIError (nil )
179
+ delayMutex .Unlock ()
180
+ continue
181
+ }
182
+ untilNextReset := rateLimit .ResetAt .Sub (time .Now ()).Milliseconds ()
183
+ if untilNextReset < 0 {
184
+ untilNextReset = time .Hour .Milliseconds ()
185
+ }
186
+ requestDelay = int (untilNextReset )/ rateLimit .Remaining + 1
187
+ }
188
+ delayMutex .Unlock ()
189
+ }
190
+ }()
191
+ }
143
192
}
144
193
if maxRepos >= 1000 {
145
194
dateDif := endingDate .Sub (startingDate ) / 2
@@ -170,20 +219,28 @@ func getRepos(query string, startingDate time.Time, endingDate time.Time) {
170
219
171
220
variables = map [string ]interface {}{
172
221
"query" : githubv4 .String (query ),
173
- "after" : CVEQuery .Search .PageInfo .EndCursor ,
222
+ "after" : githubv4 . NewString ( CVEQuery .Search .PageInfo .EndCursor ) ,
174
223
}
175
224
for reposCnt < maxRepos {
176
- time .Sleep ( time . Second )
177
-
178
- err = githubV4Client . Query ( context . Background (), & CVEPaginationQuery , variables )
225
+ start = time .Now ( )
226
+ err = githubV4Client . Query ( context . Background (), & CVEQuery , variables )
227
+ duration = time . Since ( start ). Milliseconds () - int64 ( time . Millisecond )
179
228
if err != nil {
180
- fmt .Println (err )
229
+ delayMutex .Lock ()
230
+ rateLimit = & CVEQuery .RateLimit
231
+ handleGraphQLAPIError (err )
232
+ delayMutex .Unlock ()
233
+ continue
181
234
}
235
+ delayMutex .Lock ()
236
+ rateLimit = & CVEQuery .RateLimit
237
+ time .Sleep (time .Duration (int64 (requestDelay * rateLimit .Cost )* int64 (time .Millisecond ) - duration ))
238
+ delayMutex .Unlock ()
182
239
183
- if len (CVEPaginationQuery .Search .Edges ) == 0 {
240
+ if len (CVEQuery .Search .Edges ) == 0 {
184
241
break
185
242
}
186
- for _ , nodeStruct := range CVEPaginationQuery .Search .Edges {
243
+ for _ , nodeStruct := range CVEQuery .Search .Edges {
187
244
if nodeStruct .Node .Repo .IsEmpty {
188
245
continue
189
246
}
@@ -203,7 +260,87 @@ func getRepos(query string, startingDate time.Time, endingDate time.Time) {
203
260
_ = bar .Add (1 )
204
261
}
205
262
206
- variables ["after" ] = CVEPaginationQuery .Search .PageInfo .EndCursor
263
+ variables ["after" ] = githubv4 .NewString (CVEQuery .Search .PageInfo .EndCursor )
264
+ }
265
+ }
266
+
267
+ func handleGraphQLAPIError (err error ) {
268
+ if err == nil || strings .Contains (err .Error (), "limit exceeded" ) {
269
+ untilNextReset := rateLimit .ResetAt .Sub (time .Now ())
270
+ if untilNextReset < time .Minute {
271
+ rateLimit .ResetAt = time .Now ().Add (untilNextReset ).Add (time .Hour )
272
+ time .Sleep (untilNextReset + 3 * time .Second )
273
+ return
274
+ } else {
275
+ processResults ()
276
+ writeOutput (outputFile , silent )
277
+ fmt .Println ("\n " + err .Error ())
278
+ fmt .Println ("Next reset at " + rateLimit .ResetAt .Format (time .RFC1123 ))
279
+ os .Exit (0 )
280
+ }
281
+ }
282
+ processResults ()
283
+ writeOutput (outputFile , silent )
284
+ fmt .Println ("\n " + err .Error ())
285
+ os .Exit (0 )
286
+ }
287
+
288
+ func writeOutput (fileName string , silent bool ) {
289
+ if len (reposResults ) == 0 {
290
+ return
291
+ }
292
+ output , err := os .Create (fileName )
293
+ if err != nil {
294
+ fmt .Println (err )
295
+ fmt .Println ("Couldn't create output file" )
296
+ }
297
+ defer output .Close ()
298
+
299
+ for id , repoURLs := range reposPerCVE {
300
+ for _ , r := range repoURLs {
301
+ _ , _ = io .WriteString (output , id + " - " + r + "\n " )
302
+ }
303
+ }
304
+
305
+ if ! silent {
306
+ data , _ := json .MarshalIndent (reposResults , "" , " " )
307
+ fmt .Println (string (data ))
308
+ }
309
+ }
310
+
311
+ func processResults () {
312
+ re := regexp .MustCompile (CVERegex )
313
+
314
+ for i , repo := range reposResults {
315
+ ids := make (map [string ]bool , 0 )
316
+
317
+ matches := re .FindAllStringSubmatch (repo .Url , - 1 )
318
+ matches = append (matches , re .FindAllStringSubmatch (repo .Description , - 1 )... )
319
+ matches = append (matches , re .FindAllStringSubmatch (* repo .Readme , - 1 )... )
320
+ for _ , topic := range repo .Topics {
321
+ matches = append (matches , re .FindAllStringSubmatch (topic , - 1 )... )
322
+ }
323
+
324
+ for _ , m := range matches {
325
+ if m != nil && len (m ) > 0 {
326
+ if m [0 ] != "" {
327
+ m [0 ] = strings .ToUpper (m [0 ])
328
+ m [0 ] = strings .ReplaceAll (m [0 ], "_" , "-" )
329
+ ids [strings .ReplaceAll (m [0 ], "–" , "-" )] = true
330
+ }
331
+ }
332
+ }
333
+
334
+ if len (ids ) > 0 {
335
+ reposResults [i ].CVEIDs = make ([]string , 0 )
336
+ for id := range ids {
337
+ reposResults [i ].CVEIDs = append (reposResults [i ].CVEIDs , id )
338
+ reposPerCVE [id ] = append (reposPerCVE [id ], repo .Url )
339
+ }
340
+ }
341
+
342
+ reposResults [i ].Readme = nil
343
+ reposResults [i ].Topics = nil
207
344
}
208
345
}
209
346
@@ -212,8 +349,10 @@ func main() {
212
349
tokenFile := flag .String ("token-file" , "" , "File to read Github token from" )
213
350
query := flag .String ("query-string" , "" , "GraphQL search query" )
214
351
queryFile := flag .String ("query-file" , "" , "File to read GraphQL search query from" )
215
- outputFile := flag .String ("o" , "" , "Output file name" )
216
- silent := flag .Bool ("silent" , false , "Don't print JSON output to stdout" )
352
+ flag .StringVar (& outputFile , "o" , "" , "Output file name" )
353
+ flag .BoolVar (& silent , "silent" , false , "Don't print JSON output to stdout" )
354
+ flag .IntVar (& requestDelay , "delay" , 0 , "Time delay after every GraphQL request [ms]" )
355
+ flag .BoolVar (& adjustDelay , "adjust-delay" , false , "Automatically adjust time delay between requests" )
217
356
flag .Parse ()
218
357
219
358
go func () {
@@ -225,7 +364,7 @@ func main() {
225
364
os .Exit (0 )
226
365
}()
227
366
228
- if (* token == "" && * tokenFile == "" ) || * outputFile == "" {
367
+ if (* token == "" && * tokenFile == "" ) || outputFile == "" {
229
368
fmt .Println ("Token and output file must be specified!" )
230
369
os .Exit (1 )
231
370
}
@@ -283,56 +422,7 @@ func main() {
283
422
searchQuery += " in:readme in:description in:name"
284
423
getRepos (searchQuery , githubCreateDate , time .Now ().UTC ())
285
424
286
- if len (reposResults ) > 0 {
287
- re := regexp .MustCompile (CVERegex )
288
-
289
- for i , repo := range reposResults {
290
- ids := make (map [string ]bool , 0 )
291
-
292
- matches := re .FindAllStringSubmatch (repo .Url , - 1 )
293
- matches = append (matches , re .FindAllStringSubmatch (repo .Description , - 1 )... )
294
- matches = append (matches , re .FindAllStringSubmatch (* repo .Readme , - 1 )... )
295
- for _ , topic := range repo .Topics {
296
- matches = append (matches , re .FindAllStringSubmatch (topic , - 1 )... )
297
- }
298
-
299
- for _ , m := range matches {
300
- if m != nil && len (m ) > 0 {
301
- if m [0 ] != "" {
302
- m [0 ] = strings .ToUpper (m [0 ])
303
- m [0 ] = strings .ReplaceAll (m [0 ], "_" , "-" )
304
- ids [strings .ReplaceAll (m [0 ], "–" , "-" )] = true
305
- }
306
- }
307
- }
308
-
309
- if len (ids ) > 0 {
310
- reposResults [i ].CVEIDs = make ([]string , 0 )
311
- for id := range ids {
312
- reposResults [i ].CVEIDs = append (reposResults [i ].CVEIDs , id )
313
- reposPerCVE [id ] = append (reposPerCVE [id ], repo .Url )
314
- }
315
- }
425
+ processResults ()
426
+ writeOutput (outputFile , silent )
316
427
317
- reposResults [i ].Readme = nil
318
- reposResults [i ].Topics = nil
319
- }
320
-
321
- output , err := os .Create (* outputFile )
322
- if err != nil {
323
- fmt .Println ("Couldn't create output file" )
324
- }
325
- defer output .Close ()
326
-
327
- for id , repoURLs := range reposPerCVE {
328
- for _ , r := range repoURLs {
329
- _ , _ = io .WriteString (output , id + " - " + r + "\n " )
330
- }
331
- }
332
-
333
- if ! * silent {
334
- data , _ := json .MarshalIndent (reposResults , "" , " " )
335
- fmt .Println (string (data ))
336
- }
337
- }
338
428
}
0 commit comments