github.com/vchain-us/vcn@v0.9.11-0.20210921212052-a2484d23c0b3/pkg/bom/python/python.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 python
    10  
    11  import (
    12  	"encoding/hex"
    13  	"encoding/json"
    14  	"errors"
    15  	"io/ioutil"
    16  	"net/http"
    17  	"os"
    18  	"path/filepath"
    19  	"strings"
    20  
    21  	"github.com/vchain-us/vcn/pkg/bom/artifact"
    22  )
    23  
    24  const AssetType = "python"
    25  
    26  // pythonArtifact implements Artifact interface
    27  type pythonArtifact struct {
    28  	artifact.GenericArtifact
    29  	path string
    30  }
    31  
    32  type pypiResponse struct {
    33  	Info  pypiInfo   `json:"info"`
    34  	Files []pypiFile `json:"urls"`
    35  }
    36  
    37  type pypiInfo struct {
    38  	License string `json:"license"`
    39  }
    40  
    41  type pypiFile struct {
    42  	Digests pypiDigests `json:"digests"`
    43  }
    44  
    45  type pypiDigests struct {
    46  	Md5    string `json:"md5,omitempty"`
    47  	Sha256 string `json:"sha256,omitempty"`
    48  }
    49  
    50  const (
    51  	pipenvFileName = "Pipfile.lock"
    52  	poetryFileName = "poetry.lock"
    53  	pipFileName    = "requirements.txt"
    54  	pypiApiPrefix  = "https://pypi.org/pypi/"
    55  )
    56  
    57  // New returns new Artifact, or nil if the path doesn't point to directory with Python package
    58  func New(path string) artifact.Artifact {
    59  	fi, err := os.Stat(path)
    60  	if err != nil {
    61  		return nil
    62  	}
    63  	if fi.IsDir() {
    64  		_, err := os.Stat(filepath.Join(path, pipenvFileName))
    65  		if err == nil {
    66  			return &pythonArtifactFromPipEnv{pythonArtifact: pythonArtifact{path: path}}
    67  		}
    68  		_, err = os.Stat(filepath.Join(path, poetryFileName))
    69  		if err == nil {
    70  			return &pythonArtifactFromPoetry{pythonArtifact: pythonArtifact{path: path}}
    71  		}
    72  		_, err = os.Stat(filepath.Join(path, pipFileName))
    73  		if err == nil {
    74  			return &pythonArtifactFromPip{pythonArtifact: pythonArtifact{path: path}}
    75  		}
    76  	} else {
    77  		filename := filepath.Base(path)
    78  		path = filepath.Dir(path)
    79  		switch filename {
    80  		case pipenvFileName:
    81  			return &pythonArtifactFromPipEnv{pythonArtifact: pythonArtifact{path: path}}
    82  		case poetryFileName:
    83  			return &pythonArtifactFromPoetry{pythonArtifact: pythonArtifact{path: path}}
    84  		case pipFileName:
    85  			return &pythonArtifactFromPip{pythonArtifact: pythonArtifact{path: path}}
    86  		}
    87  	}
    88  
    89  	return nil
    90  }
    91  
    92  func (p pythonArtifact) Type() string {
    93  	return AssetType
    94  }
    95  
    96  func (p pythonArtifact) Path() string {
    97  	return p.path
    98  }
    99  
   100  // combine multiple hashes into single hash by XORing them. Return Base16-encoded hash
   101  // hash entry has a form of "<hash_type>:<base16-encoded hash>", where "<hash_type>:" is optional
   102  func CombineHashes(hashes []string) (string, artifact.HashType, error) {
   103  	if len(hashes) == 0 {
   104  		return "", artifact.HashInvalid, nil
   105  	}
   106  	hashType := artifact.HashInvalid
   107  	var res []byte
   108  	for _, v := range hashes {
   109  		fields := strings.SplitN(v, ":", 2)
   110  		hash := fields[0]
   111  		if len(fields) >= 2 {
   112  			hash = fields[1]
   113  			switch fields[0] {
   114  			case "sha1":
   115  				hashType = artifact.HashSHA1
   116  			case "sha256":
   117  				hashType = artifact.HashSHA256
   118  			case "md5":
   119  				hashType = artifact.HashMD5
   120  			}
   121  		}
   122  		comp, err := hex.DecodeString(hash)
   123  		if err != nil {
   124  			return "", artifact.HashInvalid, errors.New("malformed hash value")
   125  		}
   126  		if res == nil {
   127  			res = comp
   128  		} else {
   129  			if len(comp) != len(res) {
   130  				// should never happen - all hashes must be of the same length
   131  				return "", artifact.HashInvalid, errors.New("malformed hash value")
   132  			}
   133  			// XOR hash
   134  			for i := 0; i < len(res); i++ {
   135  				res[i] ^= comp[i]
   136  			}
   137  		}
   138  	}
   139  
   140  	return hex.EncodeToString(res), hashType, nil
   141  }
   142  
   143  // query PyPI.org for module license and hash, combine all available hashes using XOR
   144  func QueryPkgDetails(name, version string) (string, artifact.HashType, string, error) {
   145  	resp, err := http.Get(pypiApiPrefix + name + "/" + version + "/json")
   146  	if err != nil {
   147  		return "", artifact.HashInvalid, "", err
   148  	}
   149  	defer resp.Body.Close()
   150  	if resp.StatusCode != http.StatusOK {
   151  		return "", artifact.HashInvalid, "", errors.New("cannot query PyPI for package details")
   152  	}
   153  	body, err := ioutil.ReadAll(resp.Body)
   154  	if err != nil {
   155  		return "", artifact.HashInvalid, "", err
   156  	}
   157  
   158  	var rsp pypiResponse
   159  	err = json.Unmarshal(body, &rsp)
   160  	if err != nil {
   161  		return "", artifact.HashInvalid, "", err
   162  	}
   163  
   164  	// assuming that all files have the same type of hash, with priority for SHA-256
   165  	hashType := artifact.HashMD5
   166  	if rsp.Files[0].Digests.Sha256 != "" {
   167  		hashType = artifact.HashSHA256
   168  	}
   169  	hashes := make([]string, len(rsp.Files))
   170  	for i, file := range rsp.Files {
   171  		if hashType == artifact.HashSHA256 {
   172  			hashes[i] = file.Digests.Sha256
   173  		} else {
   174  			hashes[i] = file.Digests.Md5
   175  		}
   176  	}
   177  
   178  	hash, _, err := CombineHashes(hashes)
   179  	if err != nil {
   180  		return "", artifact.HashInvalid, "", errors.New("malformed hash value")
   181  	}
   182  
   183  	return rsp.Info.License, hashType, hash, nil
   184  }