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  }