@@ -29,7 +29,7 @@ func DetectGitHubSecurityAlerts(r *models.ScanResult, owner, repo, token string,
29
29
// TODO Use `https://github.com/shurcooL/githubv4` if the tool supports vulnerabilityAlerts Endpoint
30
30
// Memo : https://developer.github.com/v4/explorer/
31
31
const jsonfmt = `{"query":
32
- "query { repository(owner:\"%s\", name:\"%s\") { url vulnerabilityAlerts(first: %d, states:[OPEN], %s) { pageInfo { endCursor hasNextPage startCursor } edges { node { id dismissReason dismissedAt securityVulnerability{ package { name ecosystem } severity vulnerableVersionRange firstPatchedVersion { identifier } } securityAdvisory { description ghsaId permalink publishedAt summary updatedAt withdrawnAt origin severity references { url } identifiers { type value } } } } } } } "}`
32
+ "query { repository(owner:\"%s\", name:\"%s\") { url vulnerabilityAlerts(first: %d, states:[OPEN], %s) { pageInfo { endCursor hasNextPage startCursor } edges { node { id dismissReason dismissedAt securityVulnerability{ package { name ecosystem } severity vulnerableVersionRange firstPatchedVersion { identifier } } vulnerableManifestFilename vulnerableManifestPath vulnerableRequirements securityAdvisory { description ghsaId permalink publishedAt summary updatedAt withdrawnAt origin severity references { url } identifiers { type value } } } } } } } "}`
33
33
after := ""
34
34
35
35
for {
@@ -79,11 +79,19 @@ func DetectGitHubSecurityAlerts(r *models.ScanResult, owner, repo, token string,
79
79
continue
80
80
}
81
81
82
- pkgName := fmt .Sprintf ("%s %s" ,
82
+ repoURLPkgName := fmt .Sprintf ("%s %s" ,
83
83
alerts .Data .Repository .URL , v .Node .SecurityVulnerability .Package .Name )
84
84
85
85
m := models.GitHubSecurityAlert {
86
- PackageName : pkgName ,
86
+ PackageName : repoURLPkgName ,
87
+ Repository : alerts .Data .Repository .URL ,
88
+ Package : models.GSAVulnerablePackage {
89
+ Name : v .Node .SecurityVulnerability .Package .Name ,
90
+ Ecosystem : v .Node .SecurityVulnerability .Package .Ecosystem ,
91
+ ManifestFilename : v .Node .VulnerableManifestFilename ,
92
+ ManifestPath : v .Node .VulnerableManifestPath ,
93
+ Requirements : v .Node .VulnerableRequirements ,
94
+ },
87
95
FixedIn : v .Node .SecurityVulnerability .FirstPatchedVersion .Identifier ,
88
96
AffectedRange : v .Node .SecurityVulnerability .VulnerableVersionRange ,
89
97
Dismissed : len (v .Node .DismissReason ) != 0 ,
@@ -175,7 +183,10 @@ type SecurityAlerts struct {
175
183
Identifier string `json:"identifier"`
176
184
} `json:"firstPatchedVersion"`
177
185
} `json:"securityVulnerability"`
178
- SecurityAdvisory struct {
186
+ VulnerableManifestFilename string `json:"vulnerableManifestFilename"`
187
+ VulnerableManifestPath string `json:"vulnerableManifestPath"`
188
+ VulnerableRequirements string `json:"vulnerableRequirements"`
189
+ SecurityAdvisory struct {
179
190
Description string `json:"description"`
180
191
GhsaID string `json:"ghsaId"`
181
192
Permalink string `json:"permalink"`
@@ -199,3 +210,137 @@ type SecurityAlerts struct {
199
210
} `json:"repository"`
200
211
} `json:"data"`
201
212
}
213
+
214
+ // DetectGitHubDependencyGraph access to owner/repo on GitHub and fetch dependency graph of the repository via GitHub API v4 GraphQL and then set to the given ScanResult.
215
+ // https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-the-dependency-graph
216
+ func DetectGitHubDependencyGraph (r * models.ScanResult , owner , repo , token string ) (err error ) {
217
+ src := oauth2 .StaticTokenSource (
218
+ & oauth2.Token {AccessToken : token },
219
+ )
220
+ //TODO Proxy
221
+ httpClient := oauth2 .NewClient (context .Background (), src )
222
+ r .GitHubManifests = models.DependencyGraphManifests {}
223
+
224
+ return fetchDependencyGraph (r , httpClient , owner , repo , "" , "" )
225
+ }
226
+
227
+ // recursive function
228
+ func fetchDependencyGraph (r * models.ScanResult , httpClient * http.Client , owner , repo , after , dependenciesAfter string ) (err error ) {
229
+ const queryFmt = `{"query":
230
+ "query { repository(owner:\"%s\", name:\"%s\") { url dependencyGraphManifests(first: %d, withDependencies: true%s) { pageInfo { endCursor hasNextPage } edges { node { blobPath filename repository { url } parseable exceedsMaxSize dependenciesCount dependencies%s { pageInfo { endCursor hasNextPage } edges { node { packageName packageManager repository { url } requirements hasDependencies } } } } } } } }"}`
231
+
232
+ queryStr := fmt .Sprintf (queryFmt , owner , repo , 100 , after , dependenciesAfter )
233
+ ctx , cancel := context .WithTimeout (context .Background (), 10 * time .Second )
234
+ req , err := http .NewRequestWithContext (ctx , http .MethodPost ,
235
+ "https://api.github.com/graphql" ,
236
+ bytes .NewBuffer ([]byte (queryStr )),
237
+ )
238
+ defer cancel ()
239
+ if err != nil {
240
+ return err
241
+ }
242
+
243
+ // https://docs.github.com/en/graphql/overview/schema-previews#access-to-a-repository-s-dependency-graph-preview
244
+ // TODO remove this header if it is no longer preview status in the future.
245
+ req .Header .Set ("Accept" , "application/vnd.github.hawkgirl-preview+json" )
246
+ req .Header .Set ("Content-Type" , "application/json" )
247
+
248
+ resp , err := httpClient .Do (req )
249
+ if err != nil {
250
+ return err
251
+ }
252
+ defer resp .Body .Close ()
253
+
254
+ body , err := io .ReadAll (resp .Body )
255
+ if err != nil {
256
+ return err
257
+ }
258
+
259
+ graph := DependencyGraph {}
260
+ if err := json .Unmarshal (body , & graph ); err != nil {
261
+ return err
262
+ }
263
+
264
+ if graph .Data .Repository .URL == "" {
265
+ return errof .New (errof .ErrFailedToAccessGithubAPI ,
266
+ fmt .Sprintf ("Failed to access to GitHub API. Response: %s" , string (body )))
267
+ }
268
+
269
+ dependenciesAfter = ""
270
+ for _ , m := range graph .Data .Repository .DependencyGraphManifests .Edges {
271
+ manifest , ok := r .GitHubManifests [m .Node .Filename ]
272
+ if ! ok {
273
+ manifest = models.DependencyGraphManifest {
274
+ Filename : m .Node .Filename ,
275
+ Repository : m .Node .Repository .URL ,
276
+ Dependencies : []models.Dependency {},
277
+ }
278
+ }
279
+ for _ , d := range m .Node .Dependencies .Edges {
280
+ manifest .Dependencies = append (manifest .Dependencies , models.Dependency {
281
+ PackageName : d .Node .PackageName ,
282
+ PackageManager : d .Node .PackageManager ,
283
+ Repository : d .Node .Repository .URL ,
284
+ Requirements : d .Node .Requirements ,
285
+ })
286
+ }
287
+ r .GitHubManifests [m .Node .Filename ] = manifest
288
+
289
+ if m .Node .Dependencies .PageInfo .HasNextPage {
290
+ dependenciesAfter = fmt .Sprintf (`(after: \"%s\")` , m .Node .Dependencies .PageInfo .EndCursor )
291
+ }
292
+ }
293
+ if dependenciesAfter != "" {
294
+ return fetchDependencyGraph (r , httpClient , owner , repo , after , dependenciesAfter )
295
+ }
296
+
297
+ if graph .Data .Repository .DependencyGraphManifests .PageInfo .HasNextPage {
298
+ after = fmt .Sprintf (`, after: \"%s\"` , graph .Data .Repository .DependencyGraphManifests .PageInfo .EndCursor )
299
+ return fetchDependencyGraph (r , httpClient , owner , repo , after , dependenciesAfter )
300
+ }
301
+
302
+ return nil
303
+ }
304
+
305
+ type DependencyGraph struct {
306
+ Data struct {
307
+ Repository struct {
308
+ URL string `json:"url"`
309
+ DependencyGraphManifests struct {
310
+ PageInfo struct {
311
+ EndCursor string `json:"endCursor"`
312
+ HasNextPage bool `json:"hasNextPage"`
313
+ } `json:"pageInfo"`
314
+ Edges []struct {
315
+ Node struct {
316
+ BlobPath string `json:"blobPath"`
317
+ Filename string `json:"filename"`
318
+ Repository struct {
319
+ URL string `json:"url"`
320
+ }
321
+ Parseable bool `json:"parseable"`
322
+ ExceedsMaxSize bool `json:"exceedsMaxSize"`
323
+ DependenciesCount int `json:"dependenciesCount"`
324
+ Dependencies struct {
325
+ PageInfo struct {
326
+ EndCursor string `json:"endCursor"`
327
+ HasNextPage bool `json:"hasNextPage"`
328
+ } `json:"pageInfo"`
329
+ Edges []struct {
330
+ Node struct {
331
+ PackageName string `json:"packageName"`
332
+ PackageManager string `json:"packageManager"`
333
+ Repository struct {
334
+ URL string `json:"url"`
335
+ }
336
+ Requirements string `json:"requirements"`
337
+ HasDependencies bool `json:"hasDependencies"`
338
+ } `json:"node"`
339
+ } `json:"edges"`
340
+ } `json:"dependencies"`
341
+ } `json:"node"`
342
+ } `json:"edges"`
343
+ } `json:"dependencyGraphManifests"`
344
+ } `json:"repository"`
345
+ } `json:"data"`
346
+ }
0 commit comments