diff --git a/github/github.go b/github/github.go index 9b22c74b..ea6c91d9 100644 --- a/github/github.go +++ b/github/github.go @@ -5,14 +5,13 @@ import ( "context" "encoding/json" "fmt" + "io/ioutil" "net/http" "time" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/errof" "github.com/future-architect/vuls/models" - "github.com/future-architect/vuls/util" - "github.com/k0kubun/pp" "golang.org/x/oauth2" ) @@ -25,8 +24,9 @@ func FillGitHubSecurityAlerts(r *models.ScanResult, owner, repo, token string) ( httpClient := oauth2.NewClient(context.Background(), src) // TODO Use `https://github.com/shurcooL/githubv4` if the tool supports vulnerabilityAlerts Endpoint + // Memo : https://developer.github.com/v4/explorer/ const jsonfmt = `{"query": - "query { repository(owner:\"%s\", name:\"%s\") { url, vulnerabilityAlerts(first: %d, %s) { pageInfo{ endCursor, hasNextPage, startCursor}, edges { node { id, externalIdentifier, externalReference, fixedIn, packageName, dismissReason, dismissedAt } } } } }"}` + "query { repository(owner:\"%s\", name:\"%s\") { url vulnerabilityAlerts(first: %d, %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 } } } } } } } "}` after := "" for { @@ -43,7 +43,7 @@ func FillGitHubSecurityAlerts(r *models.ScanResult, owner, repo, token string) ( // To toggle this preview and access data, need to provide a custom media type in the Accept header: // MEMO: I tried to get the affected version via GitHub API. Bit it seems difficult to determin the affected version if there are multiple dependency files such as package.json. // TODO remove this header if it is no longer preview status in the future. - req.Header.Set("Accept", "application/vnd.github.vixen-preview+json") + req.Header.Set("Accept", "application/vnd.github.package-deletes-preview+json") req.Header.Set("Content-Type", "application/json") resp, err := httpClient.Do(req) @@ -51,16 +51,23 @@ func FillGitHubSecurityAlerts(r *models.ScanResult, owner, repo, token string) ( return 0, err } defer resp.Body.Close() - alerts := SecurityAlerts{} - if json.NewDecoder(resp.Body).Decode(&alerts); err != nil { + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { return 0, err } - util.Log.Debugf("%s", pp.Sprint(alerts)) + alerts := SecurityAlerts{} + if json.Unmarshal(body, &alerts); err != nil { + return 0, err + } + + // util.Log.Debugf("%s", pp.Sprint(alerts)) + // util.Log.Debugf("%s", string(body)) if alerts.Data.Repository.URL == "" { return 0, errof.New( errof.ErrFailedToAccessGithubAPI, - fmt.Sprintf("Failed to access to GitHub API. Response: %#v", alerts), + fmt.Sprintf("Failed to access to GitHub API. Response: %s", string(body)), ) } @@ -70,31 +77,45 @@ func FillGitHubSecurityAlerts(r *models.ScanResult, owner, repo, token string) ( } pkgName := fmt.Sprintf("%s %s", - alerts.Data.Repository.URL, v.Node.PackageName) + alerts.Data.Repository.URL, v.Node.SecurityVulnerability.Package.Name) m := models.GitHubSecurityAlert{ PackageName: pkgName, - FixedIn: v.Node.FixedIn, - AffectedRange: v.Node.AffectedRange, + FixedIn: v.Node.SecurityVulnerability.FirstPatchedVersion.Identifier, + AffectedRange: v.Node.SecurityVulnerability.VulnerableVersionRange, Dismissed: len(v.Node.DismissReason) != 0, DismissedAt: v.Node.DismissedAt, DismissReason: v.Node.DismissReason, } - cveID := v.Node.ExternalIdentifier - - if val, ok := r.ScannedCves[cveID]; ok { - val.GitHubSecurityAlerts = val.GitHubSecurityAlerts.Add(m) - r.ScannedCves[cveID] = val - nCVEs++ - } else { - v := models.VulnInfo{ - CveID: cveID, - Confidences: models.Confidences{models.GitHubMatch}, - GitHubSecurityAlerts: models.GitHubSecurityAlerts{m}, + cveIDs, other := []string{}, []string{} + for _, identifier := range v.Node.SecurityAdvisory.Identifiers { + if identifier.Type == "CVE" { + cveIDs = append(cveIDs, identifier.Value) + } else { + other = append(other, identifier.Value) + } + } + + // If CVE-ID has not been assigned, use the GHSA ID etc as a ID. + if len(cveIDs) == 0 { + cveIDs = other + } + + for _, cveID := range cveIDs { + if val, ok := r.ScannedCves[cveID]; ok { + val.GitHubSecurityAlerts = val.GitHubSecurityAlerts.Add(m) + r.ScannedCves[cveID] = val + nCVEs++ + } else { + v := models.VulnInfo{ + CveID: cveID, + Confidences: models.Confidences{models.GitHubMatch}, + GitHubSecurityAlerts: models.GitHubSecurityAlerts{m}, + } + r.ScannedCves[cveID] = v + nCVEs++ } - r.ScannedCves[cveID] = v - nCVEs++ } } if !alerts.Data.Repository.VulnerabilityAlerts.PageInfo.HasNextPage { @@ -109,26 +130,50 @@ func FillGitHubSecurityAlerts(r *models.ScanResult, owner, repo, token string) ( type SecurityAlerts struct { Data struct { Repository struct { - URL string `json:"url,omitempty"` + URL string `json:"url"` VulnerabilityAlerts struct { PageInfo struct { - EndCursor string `json:"endCursor,omitempty"` - HasNextPage bool `json:"hasNextPage,omitempty"` - StartCursor string `json:"startCursor,omitempty"` - } `json:"pageInfo,omitempty"` + EndCursor string `json:"endCursor"` + HasNextPage bool `json:"hasNextPage"` + StartCursor string `json:"startCursor"` + } `json:"pageInfo"` Edges []struct { Node struct { - ID string `json:"id,omitempty"` - ExternalIdentifier string `json:"externalIdentifier,omitempty"` - ExternalReference string `json:"externalReference,omitempty"` - FixedIn string `json:"fixedIn,omitempty"` - AffectedRange string `json:"affectedRange,omitempty"` - PackageName string `json:"packageName,omitempty"` - DismissReason string `json:"dismissReason,omitempty"` - DismissedAt time.Time `json:"dismissedAt,omitempty"` - } `json:"node,omitempty"` - } `json:"edges,omitempty"` - } `json:"vulnerabilityAlerts,omitempty"` - } `json:"repository,omitempty"` - } `json:"data,omitempty"` + ID string `json:"id"` + DismissReason string `json:"dismissReason"` + DismissedAt time.Time `json:"dismissedAt"` + SecurityVulnerability struct { + Package struct { + Name string `json:"name"` + Ecosystem string `json:"ecosystem"` + } `json:"package"` + Severity string `json:"severity"` + VulnerableVersionRange string `json:"vulnerableVersionRange"` + FirstPatchedVersion struct { + Identifier string `json:"identifier"` + } `json:"firstPatchedVersion"` + } `json:"securityVulnerability"` + SecurityAdvisory struct { + Description string `json:"description"` + GhsaID string `json:"ghsaId"` + Permalink string `json:"permalink"` + PublishedAt time.Time `json:"publishedAt"` + Summary string `json:"summary"` + UpdatedAt time.Time `json:"updatedAt"` + WithdrawnAt time.Time `json:"withdrawnAt"` + Origin string `json:"origin"` + Severity string `json:"severity"` + References []struct { + URL string `json:"url"` + } `json:"references"` + Identifiers []struct { + Type string `json:"type"` + Value string `json:"value"` + } `json:"identifiers"` + } `json:"securityAdvisory"` + } `json:"node"` + } `json:"edges"` + } `json:"vulnerabilityAlerts"` + } `json:"repository"` + } `json:"data"` }