github.com/vchain-us/vcn@v0.9.11-0.20210921212052-a2484d23c0b3/pkg/bom/golang/golang.go (about) 1 /* 2 * Copyright (c) 2021 CodeNotary, Inc. All Rights Reserved. 3 * This software is released under GPL3. 4 * The full license information can be found under: 5 * https://www.gnu.org/licenses/gpl-3.0.en.html 6 * 7 */ 8 9 package golang 10 11 import ( 12 "encoding/base64" 13 "encoding/hex" 14 "encoding/json" 15 "errors" 16 "fmt" 17 "io/ioutil" 18 "net/http" 19 "os" 20 "path/filepath" 21 "strings" 22 23 "github.com/schollz/progressbar/v3" 24 "github.com/spf13/viper" 25 26 "github.com/vchain-us/vcn/pkg/bom/artifact" 27 ) 28 29 const AssetType = "go" 30 31 // goArtifact implements Artifact interface 32 type goArtifact struct { 33 artifact.GenericArtifact 34 path string 35 } 36 37 // New returns new GoArtifact object, or nil if filename doesn't referer to ELF, built from Go source, or isn't 38 // a directory containing Go files 39 func New(path string) artifact.Artifact { 40 fi, err := os.Stat(path) 41 if err != nil { 42 return nil 43 } 44 if fi.IsDir() { 45 // a directory - look for go.sum, if it is not there, checks the presence of Go files 46 _, err = os.Stat(filepath.Join(path, "go.sum")) 47 if err == nil { 48 return &goArtifactFromSum{goArtifact: goArtifact{path: path}} 49 } 50 files, err := filepath.Glob(filepath.Join(path, "*.go")) 51 if err == nil && len(files) > 0 { 52 return &goArtifactFromGoList{goArtifact: goArtifact{path: path}} 53 } 54 } else { 55 // not a directory - check if file is executable and contains Go build info section 56 file, err := openExe(path) 57 if err != nil { 58 return nil // not a ELF binary 59 } 60 if file.DataStart() == 0 { 61 file.Close() 62 return nil // cannot find build info 63 } 64 // exe file is closed by goArtifactFromExe.Dependencies() 65 return &goArtifactFromExe{goArtifact: goArtifact{path: path}, file: file} 66 } 67 return nil 68 } 69 70 func (p goArtifact) Type() string { 71 return AssetType 72 } 73 74 func (p goArtifact) Path() string { 75 return p.path 76 } 77 78 func ModHash(encoded string) (string, artifact.HashType, error) { 79 hashType := artifact.HashInvalid 80 fields := strings.SplitN(encoded, ":", 2) 81 if len(fields) != 2 { 82 return "", artifact.HashInvalid, errors.New("malformed hash value") 83 } 84 // At the time of writing "h1" (SHA256) is the only hash type, supported by Go 85 if fields[0] == "h1" { 86 hashType = artifact.HashSHA256 87 } else { 88 return "", artifact.HashInvalid, errors.New("unsupported hash type") 89 } 90 hash, err := base64.StdEncoding.DecodeString(fields[1]) 91 if err != nil { 92 return "", artifact.HashInvalid, fmt.Errorf("cannot decode base64 hash: %w", err) 93 } 94 95 return hex.EncodeToString(hash), hashType, nil 96 } 97 98 type result struct { 99 pkg string 100 license string 101 err error 102 } 103 104 func AddGithubLicenses(deps []artifact.Dependency, output artifact.OutputOptions) error { 105 githubMap := make(map[string]*artifact.Dependency, len(deps)) 106 for i := range deps { 107 if strings.HasPrefix(deps[i].Name, "github.com") { 108 githubMap[deps[i].Name] = &deps[i] 109 } 110 } 111 112 token := viper.GetString("github-token") 113 114 taskCount := len(githubMap) 115 if taskCount == 0 { 116 return nil // no github modules to process 117 } 118 119 var bar *progressbar.ProgressBar 120 if output == artifact.Progress { 121 bar = progressbar.Default(int64(len(githubMap))) 122 } 123 124 // init goroutine throttling - channels, start goroutines. 125 // We can be sure that there will be no more in-flight messages in channels than known modules 126 tasks := make(chan string, taskCount) 127 results := make(chan result, taskCount) 128 for i := 0; i < artifact.MaxGoroutines; i++ { 129 go worker(tasks, results, output, token, bar) 130 } 131 defer close(results) 132 defer close(tasks) // signal workers to stop 133 134 // send all 135 for k := range githubMap { 136 tasks <- k 137 } 138 139 // process responses 140 for taskCount > 0 { 141 taskCount-- 142 res := <-results 143 if res.err != nil { 144 continue 145 } 146 pkg, ok := githubMap[res.pkg] 147 if !ok { 148 // should never happen 149 return fmt.Errorf("internal error - got unknown package in response") 150 } 151 pkg.License = res.license 152 } 153 154 return nil 155 } 156 157 func worker(tasks <-chan string, results chan<- result, output artifact.OutputOptions, token string, bar *progressbar.ProgressBar) { 158 for task := range tasks { 159 // pkg path has a form "github.com/<user>/<repo>[/v<version>]", leave only "<user>/<repo>" 160 fields := strings.Split(task, "/") 161 lic, err := queryGithub(fields[1]+"/"+fields[2], token) 162 163 results <- result{pkg: task, license: lic, err: err} 164 if output == artifact.Progress { 165 bar.Add(1) 166 } 167 } 168 } 169 170 type githubRsp struct { 171 Lic githubLic `json:"license"` 172 } 173 174 type githubLic struct { 175 Id string `json:"spdx_id"` 176 } 177 178 func queryGithub(pkg, token string) (string, error) { 179 req, err := http.NewRequest(http.MethodGet, "https://api.github.com/repos/"+pkg, nil) 180 if err != nil { 181 return "", err 182 } 183 184 req.Header.Add("Accept", "application/vnd.github.v3+json") 185 if token != "" { 186 req.Header.Add("Authorization", "token "+token) 187 } 188 rsp, err := http.DefaultClient.Do(req) 189 if err != nil { 190 return "", err 191 } 192 defer rsp.Body.Close() 193 if rsp.StatusCode != http.StatusOK { 194 return "", fmt.Errorf("got response %d from github", rsp.StatusCode) 195 } 196 197 body, err := ioutil.ReadAll(rsp.Body) 198 if err != nil { 199 return "", err 200 } 201 202 var pkgData githubRsp 203 err = json.Unmarshal(body, &pkgData) 204 if err != nil { 205 return "", err 206 } 207 208 return pkgData.Lic.Id, nil 209 }