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  }