go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers/os/resources/processes/unixps.go (about) 1 // Copyright (c) Mondoo, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package processes 5 6 import ( 7 "bufio" 8 "fmt" 9 "io" 10 "regexp" 11 "strconv" 12 13 "github.com/kballard/go-shellquote" 14 "github.com/rs/zerolog/log" 15 "go.mondoo.com/cnquery/providers-sdk/v1/inventory" 16 "go.mondoo.com/cnquery/providers/os/connection/shared" 17 ) 18 19 var ( 20 LINUX_PS_REGEX = regexp.MustCompile(`^\s*([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ].*)?$`) 21 UNIX_PS_REGEX = regexp.MustCompile(`^\s*([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ].*)$`) 22 ) 23 24 type ProcessEntry struct { 25 Pid int64 26 CPU string 27 Mem string 28 Vsz string 29 Rss string 30 Tty string 31 Stat string 32 Start string 33 Time string 34 Uid int64 35 Command string 36 } 37 38 func (p ProcessEntry) ToOSProcess() *OSProcess { 39 executable := "" 40 args, err := shellquote.Split(p.Command) 41 if err == nil && len(args) > 0 { 42 executable = args[0] 43 } 44 45 return &OSProcess{ 46 Pid: p.Pid, 47 Command: p.Command, 48 Executable: executable, 49 State: "", 50 } 51 } 52 53 func ParseLinuxPsResult(input io.Reader) ([]*ProcessEntry, error) { 54 processes := []*ProcessEntry{} 55 scanner := bufio.NewScanner(input) 56 for scanner.Scan() { 57 line := scanner.Text() 58 59 m := LINUX_PS_REGEX.FindStringSubmatch(line) 60 if len(m) != 12 { 61 log.Fatal().Str("psoutput", line).Msg("unexpected result while trying to parse process output") 62 } 63 if m[1] == "PID" { 64 // header 65 continue 66 } 67 68 pid, err := strconv.ParseInt(m[1], 10, 64) 69 if err != nil { 70 log.Error().Err(err).Msg("cannot parse ps pid " + m[1]) 71 continue 72 } 73 uid, err := strconv.ParseInt(m[10], 10, 64) 74 if err != nil { 75 log.Error().Err(err).Msg("cannot parse ps uid " + m[10]) 76 continue 77 } 78 79 // PID %CPU %MEM VSZ RSS TT STAT STARTED TIME UID COMMAND 80 p := &ProcessEntry{ 81 Pid: pid, 82 CPU: m[2], 83 Mem: m[3], 84 Vsz: m[4], 85 Rss: m[5], 86 Tty: m[6], 87 Stat: m[7], 88 Start: m[8], 89 Time: m[9], 90 Uid: uid, 91 Command: m[11], 92 } 93 processes = append(processes, p) 94 } 95 96 return processes, nil 97 } 98 99 func ParseUnixPsResult(input io.Reader) ([]*ProcessEntry, error) { 100 processes := []*ProcessEntry{} 101 scanner := bufio.NewScanner(input) 102 for scanner.Scan() { 103 line := scanner.Text() 104 m := UNIX_PS_REGEX.FindStringSubmatch(line) 105 if len(m) != 11 { 106 log.Fatal().Str("psoutput", line).Msg("unexpected result while trying to parse process output") 107 } 108 if m[1] == "PID" { 109 // header 110 continue 111 } 112 113 pid, err := strconv.ParseInt(m[1], 10, 64) 114 if err != nil { 115 log.Error().Err(err).Msg("cannot parse unix pid " + m[1]) 116 continue 117 } 118 uid, err := strconv.ParseInt(m[9], 10, 64) 119 if err != nil { 120 log.Error().Err(err).Msg("cannot parse unix uid " + m[9]) 121 continue 122 } 123 124 // PID %CPU %MEM VSZ RSS TTY STAT TIME UID COMMAND 125 p := &ProcessEntry{ 126 Pid: pid, 127 CPU: m[2], 128 Mem: m[3], 129 Vsz: m[4], 130 Rss: m[5], 131 Tty: m[6], 132 Stat: m[7], 133 Time: m[8], 134 Uid: uid, 135 Command: m[10], 136 } 137 processes = append(processes, p) 138 } 139 140 return processes, nil 141 } 142 143 type UnixProcessManager struct { 144 conn shared.Connection 145 platform *inventory.Platform 146 } 147 148 func (upm *UnixProcessManager) Name() string { 149 return "Unix Process Manager" 150 } 151 152 func (upm *UnixProcessManager) List() ([]*OSProcess, error) { 153 var entries []*ProcessEntry 154 // NOTE: improve proc parser instead of supporting multiple ps commands 155 if upm.platform.IsFamily("linux") { 156 c, err := upm.conn.RunCommand("ps axo pid,pcpu,pmem,vsz,rss,tty,stat,stime,time,uid,command") 157 if err != nil { 158 return nil, fmt.Errorf("processes> could not run command") 159 } 160 161 entries, err = ParseLinuxPsResult(c.Stdout) 162 if err != nil { 163 return nil, err 164 } 165 } else if upm.platform.IsFamily("darwin") { 166 // NOTE: special case on darwin is that the ps axo only shows processes for users with terminals 167 // TODO: the same applies to OpenBSD and may result in missing processes 168 c, err := upm.conn.RunCommand("ps Axo pid,pcpu,pmem,vsz,rss,tty,stat,stime,time,uid,command") 169 if err != nil { 170 return nil, fmt.Errorf("processes> could not run command") 171 } 172 173 entries, err = ParseLinuxPsResult(c.Stdout) 174 if err != nil { 175 return nil, err 176 } 177 } else { 178 // TODO: consider using different ps calls for different platforms to determine max information 179 // do not use stime since it is not available on FreeBSD 180 c, err := upm.conn.RunCommand("ps axo pid,pcpu,pmem,vsz,rss,tty,stat,time,uid,command") 181 if err != nil { 182 return nil, fmt.Errorf("processes> could not run command") 183 } 184 185 entries, err = ParseUnixPsResult(c.Stdout) 186 if err != nil { 187 return nil, err 188 } 189 } 190 191 log.Debug().Int("processes", len(entries)).Msg("found processes") 192 193 var ps []*OSProcess 194 for i := range entries { 195 ps = append(ps, entries[i].ToOSProcess()) 196 } 197 return ps, nil 198 } 199 200 func (upm *UnixProcessManager) Exists(pid int64) (bool, error) { 201 process, err := upm.Process(pid) 202 if err != nil { 203 return false, err 204 } 205 206 if process == nil { 207 return false, nil 208 } 209 210 return true, nil 211 } 212 213 func (upm *UnixProcessManager) Process(pid int64) (*OSProcess, error) { 214 processes, err := upm.List() 215 if err != nil { 216 return nil, err 217 } 218 219 for i := range processes { 220 if processes[i].Pid == pid { 221 return processes[i], nil 222 } 223 } 224 225 return nil, nil 226 }