2020-02-28 15:15:34 +09:00
package ghsa
import (
"context"
"fmt"
"log"
"math"
"os"
"path/filepath"
"strings"
"time"
"github.com/cheggaaa/pb"
githubql "github.com/shurcooL/githubv4"
"github.com/shurcooL/graphql"
"github.com/spf13/afero"
"golang.org/x/xerrors"
2023-09-20 09:23:39 +03:00
"github.com/aquasecurity/vuln-list-update/utils"
2020-02-28 15:15:34 +09:00
)
// https://developer.github.com/v4/enum/securityadvisoryecosystem/
type SecurityAdvisoryEcosystem string
var (
Composer SecurityAdvisoryEcosystem = "COMPOSER"
Maven SecurityAdvisoryEcosystem = "MAVEN"
Npm SecurityAdvisoryEcosystem = "NPM"
Nuget SecurityAdvisoryEcosystem = "NUGET"
Pip SecurityAdvisoryEcosystem = "PIP"
Rubygems SecurityAdvisoryEcosystem = "RUBYGEMS"
2021-09-02 18:06:08 +02:00
Go SecurityAdvisoryEcosystem = "GO"
2022-08-25 12:49:16 +06:00
Rust SecurityAdvisoryEcosystem = "RUST"
2022-06-29 14:27:31 +00:00
Erlang SecurityAdvisoryEcosystem = "ERLANG"
2023-01-02 18:35:42 +03:00
Pub SecurityAdvisoryEcosystem = "PUB"
2023-08-23 13:27:30 +06:00
Swift SecurityAdvisoryEcosystem = "SWIFT"
Ecosystems = [ ] SecurityAdvisoryEcosystem { Composer , Maven , Npm , Nuget , Pip , Rubygems , Go , Erlang , Rust , Pub , Swift }
2020-02-28 15:15:34 +09:00
wait = func ( i int ) time . Duration {
sleep := math . Pow ( float64 ( i ) , 2 ) + float64 ( utils . RandInt ( ) % 10 )
return time . Duration ( sleep ) * time . Second
}
)
const (
ghsaDir = "ghsa"
retry = 5
maxResponseSize = 100
)
type Config struct {
vulnListDir string
appFs afero . Fs
retry int
client GithubClient
}
type GithubClient interface {
Query ( ctx context . Context , q interface { } , variables map [ string ] interface { } ) error
}
func NewConfig ( client GithubClient ) Config {
return Config {
vulnListDir : utils . VulnListDir ( ) ,
appFs : afero . NewOsFs ( ) ,
retry : retry ,
client : client ,
}
}
func ( c Config ) Update ( ) error {
log . Print ( "Fetching GitHub Security Advisory" )
for _ , ecosystem := range Ecosystems {
err := c . update ( ecosystem )
if err != nil {
return xerrors . Errorf ( "failed to update github security advisory ,%s: %w" , ecosystem , err )
}
}
return nil
}
func ( c Config ) update ( ecosystem SecurityAdvisoryEcosystem ) error {
log . Printf ( "Fetching GitHub Security Advisory: %s" , ecosystem )
dir := filepath . Join ( c . vulnListDir , ghsaDir , strings . ToLower ( string ( ecosystem ) ) )
if err := os . RemoveAll ( dir ) ; err != nil {
return xerrors . Errorf ( "unable to remove github security advisory directory: %w" , err )
}
if err := os . MkdirAll ( dir , os . ModePerm ) ; err != nil {
return xerrors . Errorf ( "failed to mkdir: %w" , err )
}
ghsas , err := c . fetchGithubSecurityAdvisories ( ecosystem )
if err != nil {
return xerrors . Errorf ( "failed to fetch github security advisory: %w" , err )
}
ghsaJsonMap := make ( map [ string ] GithubSecurityAdvisoryJson )
for _ , ghsa := range ghsas {
2022-04-26 13:06:42 +06:00
// skip bad ghsa
if ghsa . Package . Name == "" {
continue
}
2020-02-28 15:15:34 +09:00
ghsa . Package . Name = strings . TrimSpace ( ghsa . Package . Name )
ghsaJson , ok := ghsaJsonMap [ ghsa . Advisory . GhsaId + ghsa . Package . Name ]
if ok {
va := Version {
FirstPatchedVersion : ghsa . FirstPatchedVersion ,
VulnerableVersionRange : ghsa . VulnerableVersionRange ,
}
ghsaJson . Versions = append ( ghsaJson . Versions , va )
ghsaJsonMap [ ghsa . Advisory . GhsaId + ghsa . Package . Name ] = ghsaJson
} else {
ghsaJsonMap [ ghsa . Advisory . GhsaId + ghsa . Package . Name ] = GithubSecurityAdvisoryJson {
Severity : ghsa . Severity ,
UpdatedAt : ghsa . UpdatedAt ,
Package : ghsa . Package ,
Advisory : ghsa . Advisory ,
Versions : [ ] Version {
{
FirstPatchedVersion : ghsa . FirstPatchedVersion ,
VulnerableVersionRange : ghsa . VulnerableVersionRange ,
} ,
} ,
}
}
}
bar := pb . StartNew ( len ( ghsaJsonMap ) )
for _ , ghsaJson := range ghsaJsonMap {
2023-08-23 13:27:30 +06:00
pkgName := strings . Replace ( ghsaJson . Package . Name , ":" , "/" , - 1 )
// Part Swift advisories have `https://` prefix or `.git` suffix
// e.g. https://github.com/github/advisory-database/blob/76f65b0d0fdac39c8b0e834ab03562b5f80d5b27/advisories/github-reviewed/2023/06/GHSA-r6ww-5963-7r95/GHSA-r6ww-5963-7r95.json#L21
// https://github.com/github/advisory-database/blob/76f65b0d0fdac39c8b0e834ab03562b5f80d5b27/advisories/github-reviewed/2023/07/GHSA-jq43-q8mx-r7mq/GHSA-jq43-q8mx-r7mq.json#L21
// Trim them to get correct directory
if ecosystem == Swift {
pkgName = strings . TrimPrefix ( pkgName , "https///" )
pkgName = strings . TrimSuffix ( pkgName , ".git" )
}
dir := filepath . Join ( c . vulnListDir , ghsaDir , strings . ToLower ( string ( ecosystem ) ) , pkgName )
2020-02-28 15:15:34 +09:00
err := c . saveGSHA ( dir , ghsaJson . Advisory . GhsaId , ghsaJson )
if err != nil {
return xerrors . Errorf ( "failed to save github security advisory: %w" , err )
}
bar . Increment ( )
}
bar . Finish ( )
return nil
}
func ( c Config ) fetchGithubSecurityAdvisories ( ecosystem SecurityAdvisoryEcosystem ) ( [ ] GithubSecurityAdvisory , error ) {
var getVulnerabilitiesQuery GetVulnerabilitiesQuery
var ghsas [ ] GithubSecurityAdvisory
variables := map [ string ] interface { } {
"ecosystem" : ecosystem ,
"total" : graphql . Int ( maxResponseSize ) ,
"cursor" : ( * githubql . String ) ( nil ) ,
}
for {
var err error
for i := 0 ; i <= c . retry ; i ++ {
if i > 0 {
sleep := wait ( i )
log . Printf ( "retry after %s" , sleep )
time . Sleep ( sleep )
}
err = c . client . Query ( context . Background ( ) , & getVulnerabilitiesQuery , variables )
2022-04-26 13:06:42 +06:00
if err == nil || len ( getVulnerabilitiesQuery . Nodes ) > 0 {
2020-02-28 15:15:34 +09:00
break
}
}
2022-04-26 13:06:42 +06:00
// GitHub GraphQL API may return error and one of nodes == nil
// We must write other nodes.
// Bad node will be skipped in 'update' function
if err != nil && len ( getVulnerabilitiesQuery . Nodes ) == 0 {
2020-02-28 15:15:34 +09:00
return nil , xerrors . Errorf ( "graphql api error: %w" , err )
}
ghsas = append ( ghsas , getVulnerabilitiesQuery . Nodes ... )
if ! getVulnerabilitiesQuery . PageInfo . HasNextPage {
break
}
variables [ "cursor" ] = githubql . NewString ( getVulnerabilitiesQuery . PageInfo . EndCursor )
}
return ghsas , nil
}
func ( c Config ) saveGSHA ( dirName string , ghsaID string , data interface { } ) error {
2021-01-04 14:44:37 +02:00
fileName := fmt . Sprintf ( "%s.json" , ghsaID )
if err := utils . WriteJSON ( c . appFs , dirName , fileName , data ) ; err != nil {
2020-02-28 15:15:34 +09:00
return xerrors . Errorf ( "failed to write file: %w" , err )
}
return nil
}