go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers/os/resources/packages/dpkg_packages.go (about)

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package packages
     5  
     6  import (
     7  	"bufio"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"regexp"
    12  	"strings"
    13  
    14  	"github.com/rs/zerolog/log"
    15  	"github.com/spf13/afero"
    16  	"go.mondoo.com/cnquery/providers/os/connection/shared"
    17  )
    18  
    19  const (
    20  	DpkgPkgFormat = "deb"
    21  )
    22  
    23  var (
    24  	DPKG_REGEX        = regexp.MustCompile(`^(.+):\s(.+)$`)
    25  	DPKG_ORIGIN_REGEX = regexp.MustCompile(`^\s*([^\(]*)(?:\((.*)\))?\s*$`)
    26  )
    27  
    28  // ParseDpkgPackages parses the dpkg database content located in /var/lib/dpkg/status
    29  func ParseDpkgPackages(input io.Reader) ([]Package, error) {
    30  	const STATE_RESET = 0
    31  	const STATE_DESC = 1
    32  	pkgs := []Package{}
    33  
    34  	add := func(pkg Package) {
    35  		// do sanitization checks to ensure we have minimal information
    36  		if pkg.Name != "" && pkg.Version != "" {
    37  			pkgs = append(pkgs, pkg)
    38  		} else {
    39  			log.Debug().Msg("ignored deb packages since information is missing")
    40  		}
    41  	}
    42  
    43  	scanner := bufio.NewScanner(input)
    44  	pkg := Package{Format: DpkgPkgFormat}
    45  	state := STATE_RESET
    46  	var key string
    47  	for scanner.Scan() {
    48  		line := scanner.Text()
    49  
    50  		// reset package definition once we reach a newline
    51  		if len(line) == 0 {
    52  			add(pkg)
    53  			pkg = Package{Format: DpkgPkgFormat}
    54  		}
    55  
    56  		m := DPKG_REGEX.FindStringSubmatch(line)
    57  		key = ""
    58  		if m != nil {
    59  			key = m[1]
    60  			state = STATE_RESET
    61  		}
    62  		switch {
    63  		case key == "Package":
    64  			pkg.Name = strings.TrimSpace(m[2])
    65  		case key == "Version":
    66  			pkg.Version = strings.TrimSpace(m[2])
    67  		case key == "Architecture":
    68  			pkg.Arch = strings.TrimSpace(m[2])
    69  		case key == "Status":
    70  			pkg.Status = strings.TrimSpace(m[2])
    71  		case key == "Source":
    72  			o := DPKG_ORIGIN_REGEX.FindStringSubmatch(m[2])
    73  			if o != nil && len(o) >= 1 {
    74  				pkg.Origin = strings.TrimSpace(o[1])
    75  			} else {
    76  				log.Error().Str("origin", m[2]).Msg("cannot parse dpkg origin")
    77  			}
    78  		// description supports multi-line statements, start desc
    79  		case key == "Description":
    80  			pkg.Description = strings.TrimSpace(m[2])
    81  			state = STATE_DESC
    82  		// next desc line, append to previous one
    83  		case state == STATE_DESC:
    84  			pkg.Description += "\n" + strings.TrimSpace(line)
    85  		}
    86  	}
    87  
    88  	// if the last line is not an empty line we have things in flight, lets check it
    89  	add(pkg)
    90  
    91  	return pkgs, nil
    92  }
    93  
    94  var DPKG_UPDATE_REGEX = regexp.MustCompile(`^Inst\s([a-zA-Z0-9.\-_]+)\s\[([a-zA-Z0-9.\-\+]+)\]\s\(([a-zA-Z0-9.\-\+]+)\s*(.*)\)(.*)$`)
    95  
    96  func ParseDpkgUpdates(input io.Reader) (map[string]PackageUpdate, error) {
    97  	pkgs := map[string]PackageUpdate{}
    98  	scanner := bufio.NewScanner(input)
    99  	for scanner.Scan() {
   100  		line := scanner.Text()
   101  		m := DPKG_UPDATE_REGEX.FindStringSubmatch(line)
   102  		if m != nil {
   103  			pkgs[m[1]] = PackageUpdate{
   104  				Name:      m[1],
   105  				Version:   m[2],
   106  				Available: m[3],
   107  			}
   108  		}
   109  	}
   110  	return pkgs, nil
   111  }
   112  
   113  // Debian, Ubuntu
   114  type DebPkgManager struct {
   115  	conn shared.Connection
   116  }
   117  
   118  func (dpm *DebPkgManager) Name() string {
   119  	return "Debian Package Manager"
   120  }
   121  
   122  func (dpm *DebPkgManager) Format() string {
   123  	return DpkgPkgFormat
   124  }
   125  
   126  func (dpm *DebPkgManager) List() ([]Package, error) {
   127  	fs := dpm.conn.FileSystem()
   128  	dpkgStatusFile := "/var/lib/dpkg/status"
   129  	dpkgStatusDir := "/var/lib/dpkg/status.d"
   130  	_, fErr := fs.Stat(dpkgStatusFile)
   131  	dStat, dErr := fs.Stat(dpkgStatusDir)
   132  
   133  	if fErr != nil && dErr != nil {
   134  		log.Debug().Err(fErr).Str("path", dpkgStatusFile).Msg("cannot find status file")
   135  		log.Debug().Err(dErr).Str("path", dpkgStatusDir).Msg("cannot find status dir")
   136  		return nil, fmt.Errorf("could not find dpkg package list")
   137  	}
   138  
   139  	pkgList := []Package{}
   140  	// main pkg file for debian systems
   141  	if fErr == nil {
   142  		log.Debug().Str("file", dpkgStatusFile).Msg("parse dpkg status file")
   143  		fi, err := dpm.conn.FileSystem().Open(dpkgStatusFile)
   144  		if err != nil {
   145  			return nil, fmt.Errorf("could not read dpkg package list")
   146  		}
   147  		defer fi.Close()
   148  
   149  		list, err := ParseDpkgPackages(fi)
   150  		if err != nil {
   151  			return nil, fmt.Errorf("could not parse dpkg package list")
   152  		}
   153  		pkgList = append(pkgList, list...)
   154  	}
   155  
   156  	// e.g. google distroless images stores their pkg data in /var/lib/dpkg/status.d/
   157  	if dErr == nil && dStat.IsDir() == true {
   158  		afutil := afero.Afero{Fs: fs}
   159  		wErr := afutil.Walk(dpkgStatusDir, func(path string, f os.FileInfo, fErr error) error {
   160  			if f == nil || f.IsDir() {
   161  				return nil
   162  			}
   163  
   164  			log.Debug().Str("path", path).Msg("walk file")
   165  			fi, err := dpm.conn.FileSystem().Open(path)
   166  			if err != nil {
   167  				log.Debug().Err(err).Str("path", path).Msg("could open file")
   168  				return fmt.Errorf("could not read dpkg package list")
   169  			}
   170  
   171  			list, err := ParseDpkgPackages(fi)
   172  			fi.Close()
   173  			if err != nil {
   174  				log.Debug().Err(err).Str("path", path).Msg("could not parse")
   175  				return fmt.Errorf("could not parse dpkg package list")
   176  			}
   177  
   178  			log.Debug().Int("pkgs", len(list)).Msg("completed parsing")
   179  			pkgList = append(pkgList, list...)
   180  			return nil
   181  		})
   182  		if wErr != nil {
   183  			return nil, wErr
   184  		}
   185  	}
   186  
   187  	return pkgList, nil
   188  }
   189  
   190  func (dpm *DebPkgManager) Available() (map[string]PackageUpdate, error) {
   191  	// TODO: run this as a complete shell script in motor
   192  	// DEBIAN_FRONTEND=noninteractive apt-get update >/dev/null 2>&1
   193  	// readlock() { cat /proc/locks | awk '{print $5}' | grep -v ^0 | xargs -I {1} find /proc/{1}/fd -maxdepth 1 -exec readlink {} \; | grep '^/var/lib/dpkg/lock$'; }
   194  	// while test -n "$(readlock)"; do sleep 1; done
   195  	// DEBIAN_FRONTEND=noninteractive apt-get upgrade --dry-run
   196  	dpm.conn.RunCommand("DEBIAN_FRONTEND=noninteractive apt-get update >/dev/null 2>&1")
   197  
   198  	cmd, err := dpm.conn.RunCommand("DEBIAN_FRONTEND=noninteractive apt-get upgrade --dry-run")
   199  	if err != nil {
   200  		log.Debug().Err(err).Msg("mql[packages]> could not read package updates")
   201  		return nil, fmt.Errorf("could not read package update list")
   202  	}
   203  	return ParseDpkgUpdates(cmd.Stdout)
   204  }