Skip to content

Commit ca64d7f

Browse files
feat(report): Include dependencies into scan result and cyclondex for supply chain security on Integration with GitHub Security Alerts (#1584)
* feat(report): Enhance scan result and cyclondex for supply chain security on Integration with GitHub Security Alerts * derive ecosystem/version from dependency graph * fix vars name && fetch manifest info on GSA && arrange ghpkgToPURL structure * fix miscs * typo in error message * fix ecosystem equally to trivy * miscs * refactoring * recursive dependency graph pagination * change var name && update comments * omit map type of ghpkgToPURL in signatures * fix vars name * goimports * make fmt * fix comment Co-authored-by: MaineK00n <[email protected]>
1 parent 554ecc4 commit ca64d7f

17 files changed

+340
-40
lines changed

cache/bolt.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func (b Bolt) Close() error {
4848
return b.db.Close()
4949
}
5050

51-
// CreateBucketIfNotExists creates a bucket that is specified by arg.
51+
// CreateBucketIfNotExists creates a bucket that is specified by arg.
5252
func (b *Bolt) createBucketIfNotExists(name string) error {
5353
return b.db.Update(func(tx *bolt.Tx) error {
5454
_, err := tx.CreateBucketIfNotExists([]byte(name))

config/config.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ var Revision string
2121
// Conf has Configuration
2222
var Conf Config
2323

24-
//Config is struct of Configuration
24+
// Config is struct of Configuration
2525
type Config struct {
2626
logging.LogOpts
2727

detector/detector.go

+4
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,10 @@ func DetectGitHubCves(r *models.ScanResult, githubConfs map[string]config.GitHub
303303
}
304304
logging.Log.Infof("%s: %d CVEs detected with GHSA %s/%s",
305305
r.FormatServerName(), n, owner, repo)
306+
307+
if err = DetectGitHubDependencyGraph(r, owner, repo, setting.Token); err != nil {
308+
return xerrors.Errorf("Failed to access GitHub Dependency graph: %w", err)
309+
}
306310
}
307311
return nil
308312
}

detector/github.go

+149-4
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func DetectGitHubSecurityAlerts(r *models.ScanResult, owner, repo, token string,
2929
// TODO Use `https://github.com/shurcooL/githubv4` if the tool supports vulnerabilityAlerts Endpoint
3030
// Memo : https://developer.github.com/v4/explorer/
3131
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 } } } } } } } "}`
3333
after := ""
3434

3535
for {
@@ -79,11 +79,19 @@ func DetectGitHubSecurityAlerts(r *models.ScanResult, owner, repo, token string,
7979
continue
8080
}
8181

82-
pkgName := fmt.Sprintf("%s %s",
82+
repoURLPkgName := fmt.Sprintf("%s %s",
8383
alerts.Data.Repository.URL, v.Node.SecurityVulnerability.Package.Name)
8484

8585
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+
},
8795
FixedIn: v.Node.SecurityVulnerability.FirstPatchedVersion.Identifier,
8896
AffectedRange: v.Node.SecurityVulnerability.VulnerableVersionRange,
8997
Dismissed: len(v.Node.DismissReason) != 0,
@@ -175,7 +183,10 @@ type SecurityAlerts struct {
175183
Identifier string `json:"identifier"`
176184
} `json:"firstPatchedVersion"`
177185
} `json:"securityVulnerability"`
178-
SecurityAdvisory struct {
186+
VulnerableManifestFilename string `json:"vulnerableManifestFilename"`
187+
VulnerableManifestPath string `json:"vulnerableManifestPath"`
188+
VulnerableRequirements string `json:"vulnerableRequirements"`
189+
SecurityAdvisory struct {
179190
Description string `json:"description"`
180191
GhsaID string `json:"ghsaId"`
181192
Permalink string `json:"permalink"`
@@ -199,3 +210,137 @@ type SecurityAlerts struct {
199210
} `json:"repository"`
200211
} `json:"data"`
201212
}
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+
}

detector/wordpress.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import (
2121
"golang.org/x/xerrors"
2222
)
2323

24-
//WpCveInfos is for wpscan json
24+
// WpCveInfos is for wpscan json
2525
type WpCveInfos struct {
2626
ReleaseDate string `json:"release_date"`
2727
ChangelogURL string `json:"changelog_url"`
@@ -33,7 +33,7 @@ type WpCveInfos struct {
3333
Error string `json:"error"`
3434
}
3535

36-
//WpCveInfo is for wpscan json
36+
// WpCveInfo is for wpscan json
3737
type WpCveInfo struct {
3838
ID string `json:"id"`
3939
Title string `json:"title"`
@@ -44,7 +44,7 @@ type WpCveInfo struct {
4444
FixedIn string `json:"fixed_in"`
4545
}
4646

47-
//References is for wpscan json
47+
// References is for wpscan json
4848
type References struct {
4949
URL []string `json:"url"`
5050
Cve []string `json:"cve"`

logging/logutil.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
formatter "github.com/kotakanbe/logrus-prefixed-formatter"
1616
)
1717

18-
//LogOpts has options for logging
18+
// LogOpts has options for logging
1919
type LogOpts struct {
2020
Debug bool `json:"debug,omitempty"`
2121
DebugSQL bool `json:"debugSQL,omitempty"`
@@ -45,7 +45,6 @@ func NewNormalLogger() Logger {
4545
return Logger{Entry: logrus.Entry{Logger: logrus.New()}}
4646
}
4747

48-
// NewNormalLogger creates normal logger
4948
func NewIODiscardLogger() Logger {
5049
l := logrus.New()
5150
l.Out = io.Discard

models/github.go

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package models
2+
3+
import (
4+
"strings"
5+
6+
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
7+
)
8+
9+
// DependencyGraphManifests has a map of DependencyGraphManifest
10+
// key: Filename
11+
type DependencyGraphManifests map[string]DependencyGraphManifest
12+
13+
type DependencyGraphManifest struct {
14+
Filename string `json:"filename"`
15+
Repository string `json:"repository"`
16+
Dependencies []Dependency `json:"dependencies"`
17+
}
18+
19+
// Ecosystem returns a name of ecosystem(or package manager) of manifest(lock) file in trivy way
20+
// https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-the-dependency-graph#supported-package-ecosystems
21+
func (m DependencyGraphManifest) Ecosystem() string {
22+
switch {
23+
case strings.HasSuffix(m.Filename, "Cargo.lock"),
24+
strings.HasSuffix(m.Filename, "Cargo.toml"):
25+
return ftypes.Cargo // Rust
26+
case strings.HasSuffix(m.Filename, "composer.lock"),
27+
strings.HasSuffix(m.Filename, "composer.json"):
28+
return ftypes.Composer // PHP
29+
case strings.HasSuffix(m.Filename, ".csproj"),
30+
strings.HasSuffix(m.Filename, ".vbproj"),
31+
strings.HasSuffix(m.Filename, ".nuspec"),
32+
strings.HasSuffix(m.Filename, ".vcxproj"),
33+
strings.HasSuffix(m.Filename, ".fsproj"),
34+
strings.HasSuffix(m.Filename, "packages.config"):
35+
return ftypes.NuGet // .NET languages (C#, F#, VB), C++
36+
case strings.HasSuffix(m.Filename, "go.sum"),
37+
strings.HasSuffix(m.Filename, "go.mod"):
38+
return ftypes.GoModule // Go
39+
case strings.HasSuffix(m.Filename, "pom.xml"):
40+
return ftypes.Pom // Java, Scala
41+
case strings.HasSuffix(m.Filename, "package-lock.json"),
42+
strings.HasSuffix(m.Filename, "package.json"):
43+
return ftypes.Npm // JavaScript
44+
case strings.HasSuffix(m.Filename, "yarn.lock"):
45+
return ftypes.Yarn // JavaScript
46+
case strings.HasSuffix(m.Filename, "requirements.txt"),
47+
strings.HasSuffix(m.Filename, "requirements-dev.txt"),
48+
strings.HasSuffix(m.Filename, "setup.py"):
49+
return ftypes.Pip // Python
50+
case strings.HasSuffix(m.Filename, "Pipfile.lock"),
51+
strings.HasSuffix(m.Filename, "Pipfile"):
52+
return ftypes.Pipenv // Python
53+
case strings.HasSuffix(m.Filename, "poetry.lock"),
54+
strings.HasSuffix(m.Filename, "pyproject.toml"):
55+
return ftypes.Poetry // Python
56+
case strings.HasSuffix(m.Filename, "Gemfile.lock"),
57+
strings.HasSuffix(m.Filename, "Gemfile"):
58+
return ftypes.Bundler // Ruby
59+
case strings.HasSuffix(m.Filename, ".gemspec"):
60+
return ftypes.GemSpec // Ruby
61+
case strings.HasSuffix(m.Filename, "pubspec.lock"),
62+
strings.HasSuffix(m.Filename, "pubspec.yaml"):
63+
return "pub" // Dart
64+
case strings.HasSuffix(m.Filename, ".yml"),
65+
strings.HasSuffix(m.Filename, ".yaml"):
66+
return "actions" // GitHub Actions workflows
67+
default:
68+
return "unknown"
69+
}
70+
}
71+
72+
type Dependency struct {
73+
PackageName string `json:"packageName"`
74+
PackageManager string `json:"packageManager"`
75+
Repository string `json:"repository"`
76+
Requirements string `json:"requirements"`
77+
}
78+
79+
func (d Dependency) Version() string {
80+
s := strings.Split(d.Requirements, " ")
81+
if len(s) == 2 && s[0] == "=" {
82+
return s[1]
83+
}
84+
// in case of ranged version
85+
return ""
86+
}

models/scanresults.go

+10-9
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,16 @@ type ScanResult struct {
4545
Errors []string `json:"errors"`
4646
Warnings []string `json:"warnings"`
4747

48-
ScannedCves VulnInfos `json:"scannedCves"`
49-
RunningKernel Kernel `json:"runningKernel"`
50-
Packages Packages `json:"packages"`
51-
SrcPackages SrcPackages `json:",omitempty"`
52-
EnabledDnfModules []string `json:"enabledDnfModules,omitempty"` // for dnf modules
53-
WordPressPackages WordPressPackages `json:",omitempty"`
54-
LibraryScanners LibraryScanners `json:"libraries,omitempty"`
55-
CweDict CweDict `json:"cweDict,omitempty"`
56-
Optional map[string]interface{} `json:",omitempty"`
48+
ScannedCves VulnInfos `json:"scannedCves"`
49+
RunningKernel Kernel `json:"runningKernel"`
50+
Packages Packages `json:"packages"`
51+
SrcPackages SrcPackages `json:",omitempty"`
52+
EnabledDnfModules []string `json:"enabledDnfModules,omitempty"` // for dnf modules
53+
WordPressPackages WordPressPackages `json:",omitempty"`
54+
GitHubManifests DependencyGraphManifests `json:"gitHubManifests,omitempty"`
55+
LibraryScanners LibraryScanners `json:"libraries,omitempty"`
56+
CweDict CweDict `json:"cweDict,omitempty"`
57+
Optional map[string]interface{} `json:",omitempty"`
5758
Config struct {
5859
Scan config.Config `json:"scan"`
5960
Report config.Config `json:"report"`

0 commit comments

Comments
 (0)