github.com/vchain-us/vcn@v0.9.11-0.20210921212052-a2484d23c0b3/pkg/bom/docker/dpkg.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 docker 10 11 import ( 12 "archive/tar" 13 "bufio" 14 "bytes" 15 "crypto/sha256" 16 "encoding/hex" 17 "errors" 18 "fmt" 19 "io" 20 "path/filepath" 21 "regexp" 22 "strings" 23 24 "github.com/vchain-us/vcn/pkg/bom/artifact" 25 "github.com/vchain-us/vcn/pkg/bom/executor" 26 ) 27 28 // dpkg implements packageManager interface 29 type dpkg struct { 30 cache []artifact.Dependency 31 index map[string]*artifact.Dependency 32 } 33 34 var ( 35 licensePattern = regexp.MustCompile(`^License: (\S*)`) 36 commonLicensePathPattern = regexp.MustCompile(`/usr/share/common-licenses/([0-9A-Za-z_.\-]+)`) 37 ) 38 39 func (pkg dpkg) Type() string { 40 return DPKG 41 } 42 43 // PackageByFile finds a package by the file it includes 44 func (pkg *dpkg) PackageByFile(e executor.Executor, file string, output artifact.OutputOptions) (artifact.Dependency, error) { 45 var err error 46 if pkg.cache == nil { 47 err := pkg.buildCache(e) 48 if err != nil { 49 return artifact.Dependency{}, err 50 } 51 } 52 53 stdOut, _, exitCode, err := e.Exec([]string{"dpkg", "-S", file}) 54 if err != nil { 55 return artifact.Dependency{}, fmt.Errorf("error starting 'dpkg' command: %w", err) 56 } 57 if exitCode != 0 { 58 return artifact.Dependency{}, ErrNotFound 59 } 60 61 lines := bytes.SplitN(stdOut, []byte{'\n'}, 2) 62 // expect format "<package>[:<arch>]: <file>' 63 fields := strings.SplitN(string(lines[0]), ":", 2) 64 65 p, ok := pkg.index[fields[0]] 66 if !ok { 67 return artifact.Dependency{}, fmt.Errorf("cannot find package %s in cache", fields[0]) 68 } 69 70 return *p, nil 71 } 72 73 // AllPackages finds all installed packages 74 func (pkg *dpkg) AllPackages(e executor.Executor, output artifact.OutputOptions) ([]artifact.Dependency, error) { 75 if pkg.cache == nil { 76 err := pkg.buildCache(e) 77 if err != nil { 78 return nil, err 79 } 80 } 81 82 return pkg.cache, nil 83 } 84 85 func (pkg *dpkg) buildCache(e executor.Executor) error { 86 buf, err := e.ReadFile("/var/lib/dpkg/status") 87 if err != nil { 88 buf, err = e.ReadFile("/var/lib/dpkg/status.d") 89 if err != nil { 90 return fmt.Errorf("error reading file from container: %w", err) 91 } 92 } 93 94 pkg.cache = make([]artifact.Dependency, 0) 95 scanner := bufio.NewScanner(bytes.NewBuffer(buf)) 96 curPkg := artifact.Dependency{} 97 for scanner.Scan() { 98 line := scanner.Text() 99 fields := strings.SplitN(line, ": ", 2) 100 if len(fields) != 2 { 101 continue 102 } 103 switch fields[0] { 104 case "Package": 105 if curPkg.Name != "" { 106 pkg.cache = append(pkg.cache, curPkg) 107 } 108 curPkg = artifact.Dependency{Name: fields[1]} 109 case "Version": 110 curPkg.Version = fields[1] 111 } 112 } 113 if curPkg.Name != "" { 114 pkg.cache = append(pkg.cache, curPkg) 115 } 116 117 pkg.index = make(map[string]*artifact.Dependency, len(pkg.cache)) 118 for i := range pkg.cache { 119 pkg.index[pkg.cache[i].Name] = &pkg.cache[i] 120 } 121 122 // calculate hashes for all packages 123 hashReader, err := e.ReadDir("/var/lib/dpkg/info") 124 if err != nil { 125 return err 126 } 127 defer hashReader.Close() 128 tr := tar.NewReader(hashReader) 129 130 for { 131 hdr, err := tr.Next() 132 if errors.Is(err, io.EOF) { 133 break 134 } 135 if !strings.HasSuffix(hdr.Name, ".md5sums") { 136 continue 137 } 138 // file name has a form of '/var/lib/dpkg/info/<pkg>[:arch].md5sums' 139 fields := strings.Split(strings.TrimSuffix(filepath.Base(hdr.Name), ".md5sums"), ":") 140 p, ok := pkg.index[fields[0]] 141 if !ok { 142 continue // unknown package - maybe md5sums file is a leftover 143 } 144 h := sha256.New() 145 if _, err := io.Copy(h, tr); err != nil { 146 return err 147 } 148 p.Hash = hex.EncodeToString(h.Sum(nil)) 149 p.HashType = artifact.HashSHA256 150 } 151 152 // collect license info 153 licReader, err := e.ReadDir("/usr/share/doc") 154 if err != nil { 155 return fmt.Errorf("error reading file from container: %w", err) 156 } 157 defer licReader.Close() 158 159 tr = tar.NewReader(licReader) 160 for { 161 hdr, err := tr.Next() 162 if err == io.EOF { 163 break 164 } 165 if err != nil { 166 return fmt.Errorf("error reading file from container: %w", err) 167 } 168 fields := strings.Split(hdr.Name, "/") 169 if len(fields) != 3 { // expect doc/<package_name>/copyright 170 continue 171 } 172 if fields[2] != "copyright" { 173 continue 174 } 175 pkg, ok := pkg.index[fields[1]] 176 if !ok { 177 continue 178 } 179 pkg.License = findLicense(tr) 180 } 181 return nil 182 } 183 184 // see https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#license-syntax for details 185 func findLicense(reader io.Reader) string { 186 scanner := bufio.NewScanner(reader) 187 188 license := "" 189 for scanner.Scan() { 190 line := scanner.Text() 191 match := licensePattern.FindStringSubmatch(line) 192 if len(match) > 0 { 193 license = match[1] 194 break 195 } 196 match = commonLicensePathPattern.FindStringSubmatch(line) 197 if len(match) > 0 { 198 license = match[1] 199 break 200 } 201 } 202 203 return license 204 }