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 }