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  }