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 }