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

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package lsof
     5  
     6  import (
     7  	"bufio"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"regexp"
    12  	"strconv"
    13  	"strings"
    14  )
    15  
    16  func init() {
    17  	for i := range allFileTypes {
    18  		ft := allFileTypes[i]
    19  		fileTypeMap[string(ft)] = ft
    20  	}
    21  }
    22  
    23  // FileType is the type of the node associated with the file
    24  type FileType string
    25  
    26  const (
    27  	FileTypeUnknown          FileType = ""
    28  	FileTypeIPv4             FileType = "IPv4"
    29  	FileTypeIPv6             FileType = "IPv6"
    30  	FileTypeAX25             FileType = "ax25"
    31  	FileTypeInternetDomain   FileType = "inet"
    32  	FileTypeHPLinkLevel      FileType = "lla"
    33  	FileTypeAFRouteSocket    FileType = "rte"
    34  	FileTypeSocket           FileType = "sock"
    35  	FileTypeUnixDomainSocket FileType = "unix"
    36  	FileTypeHPUXX25          FileType = "x.25"
    37  	FileTypeBlockSpecial     FileType = "BLK"
    38  	FileTypeCharacterSpecial FileType = "CHR"
    39  	FileTypeLinuxMapFile     FileType = "DEL"
    40  	FileTypeDir              FileType = "DIR"
    41  	FileTypeDOOR             FileType = "DOOR"
    42  	FileTypeFIFO             FileType = "FIFO"
    43  	FileTypeKQUEUE           FileType = "KQUEUE"
    44  	FileTypeSymbolicLink     FileType = "LINK"
    45  	FileTypeRegularFile      FileType = "REG"
    46  	FileTypeStreamSocket     FileType = "STSO"
    47  	FileTypeUnnamedType      FileType = "UNNM"
    48  )
    49  
    50  var allFileTypes = []FileType{
    51  	FileTypeUnknown,
    52  	FileTypeIPv4,
    53  	FileTypeIPv6,
    54  	FileTypeAX25,
    55  	FileTypeInternetDomain,
    56  	FileTypeHPLinkLevel,
    57  	FileTypeAFRouteSocket,
    58  	FileTypeSocket,
    59  	FileTypeUnixDomainSocket,
    60  	FileTypeHPUXX25,
    61  	FileTypeBlockSpecial,
    62  	FileTypeCharacterSpecial,
    63  	FileTypeLinuxMapFile,
    64  	FileTypeDir,
    65  	FileTypeDOOR,
    66  	FileTypeFIFO,
    67  	FileTypeKQUEUE,
    68  	FileTypeSymbolicLink,
    69  	FileTypeRegularFile,
    70  	FileTypeStreamSocket,
    71  	FileTypeUnnamedType,
    72  }
    73  
    74  var fileTypeMap = map[string]FileType{}
    75  
    76  func fileTypeFromString(s string) FileType {
    77  	ft, ok := fileTypeMap[s]
    78  	if !ok {
    79  		return FileTypeUnknown
    80  	}
    81  	return ft
    82  }
    83  
    84  // FileDescriptor defines a file in use by a process
    85  type FileDescriptor struct {
    86  	FileDescriptor string
    87  	Type           FileType
    88  	// file name, comment, Internet address
    89  	Name                string
    90  	AccessMode          string
    91  	LockStatus          string
    92  	Flags               string
    93  	DeviceCharacterCode string
    94  	Offset              string
    95  	Protocol            string
    96  	// QR
    97  	TcpReadQueueSize string
    98  	// QS
    99  	TcpSendQueueSize string
   100  	// SO
   101  	TcpSocketOptions string
   102  	// SS
   103  	TcpSocketStates string
   104  	// ST
   105  	TcpConnectionState string
   106  	// TF
   107  	TcpFlags string
   108  	// WR
   109  	TcpWindowReadSize string
   110  	// WW
   111  	TcpWindowWriteSize string
   112  }
   113  
   114  // n127.0.0.1:3000->127.0.0.1:54335
   115  var networkNameRegexp = regexp.MustCompile(`(.*):(.*)->(.*):(.*)`)
   116  var networkLoopbackRegexp = regexp.MustCompile(`(.*):(.*)`)
   117  
   118  // local and remote Internet addresses of a network file
   119  func (f *FileDescriptor) NetworkFile() (string, int64, string, int64, error) {
   120  	if f.Name == "no PCB" { // socket files that do not have a protocol block
   121  		return "", 0, "", 0, nil
   122  	}
   123  	if f.Name == "*:*" {
   124  		return "*", 0, "*", 0, nil
   125  	}
   126  
   127  	if strings.Contains(f.Name, "->") {
   128  		matches := networkNameRegexp.FindStringSubmatch(f.Name)
   129  		if len(matches) != 5 {
   130  			return "", 0, "", 0, errors.New("network name not supported: " + f.Name)
   131  		}
   132  
   133  		localPort, err := strconv.Atoi(matches[2])
   134  		if err != nil {
   135  			return "", 0, "", 0, errors.New("network name not supported: " + f.Name)
   136  		}
   137  
   138  		remotePort, err := strconv.Atoi(matches[4])
   139  		if err != nil {
   140  			return "", 0, "", 0, errors.New("network name not supported: " + f.Name)
   141  		}
   142  
   143  		return matches[1], int64(localPort), matches[3], int64(remotePort), nil
   144  	}
   145  
   146  	// loop-back address [::1]:17223 or *:56863
   147  	address := networkLoopbackRegexp.FindStringSubmatch(f.Name)
   148  	if len(address) < 3 {
   149  		return "", 0, "", 0, errors.New("network name not supported: " + f.Name)
   150  	}
   151  	localPort := 0
   152  	var err error
   153  	if address[2] != "*" {
   154  		localPort, err = strconv.Atoi(address[2])
   155  		if err != nil {
   156  			return "", 0, "", 0, errors.New("network name not supported: " + f.Name)
   157  		}
   158  	}
   159  
   160  	return address[1], int64(localPort), "", 0, nil
   161  }
   162  
   163  // maps lsof state to tcp states
   164  var lsofTcpStateMapping = map[string]int64{
   165  	"ESTABLISHED": 1,  // "established"
   166  	"SYN_SENT":    2,  //  "syn sent"
   167  	"SYN_RCDV":    3,  //  "syn recv"
   168  	"FIN_WAIT1":   4,  //  "fin wait1"
   169  	"FIN_WAIT_2":  5,  //   "fin wait2",
   170  	"TIME_WAIT":   6,  //  "time wait",
   171  	"CLOSED":      7,  // "close"
   172  	"CLOSE_WAIT":  8,  //   "close wait"
   173  	"LAST_ACK":    9,  //  "last ack"
   174  	"LISTEN":      10, // "listen"
   175  	"CLOSING":     11, // "closing"
   176  	// "SYN_RCDV":    12, // "new syn recv"
   177  }
   178  
   179  func (f *FileDescriptor) TcpState() int64 {
   180  	return lsofTcpStateMapping[f.TcpConnectionState]
   181  }
   182  
   183  var tcpInfoRegex = regexp.MustCompile(`(.*)=(.*)`)
   184  
   185  // https://man7.org/linux/man-pages/man8/lsof.8.html#OUTPUT_FOR_OTHER_PROGRAMS
   186  func (f *FileDescriptor) parseField(s string) error {
   187  	key := s[0]
   188  	value := s[1:]
   189  	switch key {
   190  	case 'f':
   191  		f.FileDescriptor = value
   192  	case 'a':
   193  		f.AccessMode = value
   194  	case 'l':
   195  		f.LockStatus = value
   196  	case 't':
   197  		f.Type = fileTypeFromString(value)
   198  	case 'G':
   199  		f.Flags = value
   200  	case 'd':
   201  		f.DeviceCharacterCode = value
   202  	case 'o':
   203  		f.Offset = value
   204  	case 'P':
   205  		f.Protocol = value
   206  	case 'n':
   207  		f.Name = value
   208  	case 'T':
   209  		// TCP/TPI information
   210  		// we need to parse it separately
   211  		keyPair := tcpInfoRegex.FindStringSubmatch(value)
   212  		switch keyPair[1] {
   213  		case "QR":
   214  			f.TcpReadQueueSize = keyPair[2]
   215  		case "QS":
   216  			f.TcpSendQueueSize = keyPair[2]
   217  		case "SO":
   218  			f.TcpSocketOptions = keyPair[2]
   219  		case "SS":
   220  			f.TcpSocketStates = keyPair[2]
   221  		case "ST":
   222  			f.TcpConnectionState = keyPair[2]
   223  		case "TF":
   224  			f.TcpFlags = keyPair[2]
   225  		case "WR":
   226  			f.TcpWindowReadSize = keyPair[2]
   227  		case "WW":
   228  			f.TcpWindowWriteSize = keyPair[2]
   229  		}
   230  	default:
   231  		// nothing do to, skip all unsupported fields
   232  	}
   233  
   234  	return nil
   235  }
   236  
   237  type Process struct {
   238  	PID             string
   239  	UID             string
   240  	GID             string
   241  	Command         string
   242  	ParentPID       string
   243  	SocketInode     string
   244  	FileDescriptors []FileDescriptor
   245  }
   246  
   247  // https://man7.org/linux/man-pages/man8/lsof.8.html#OUTPUT_FOR_OTHER_PROGRAMS
   248  func (p *Process) parseField(s string) error {
   249  	if s == "" {
   250  		return fmt.Errorf("Empty field")
   251  	}
   252  	key := s[0]
   253  	value := s[1:]
   254  	switch key {
   255  	case 'p':
   256  		p.PID = value
   257  	case 'R':
   258  		p.ParentPID = value
   259  	case 'g':
   260  		p.GID = value
   261  	case 'd':
   262  		p.SocketInode = value
   263  	case 'c':
   264  		p.Command = value
   265  	case 'u':
   266  		p.UID = value
   267  	default:
   268  		// nothing do to, skip all unsupported fields
   269  	}
   270  	return nil
   271  }
   272  
   273  // parseFileLines parses all attributes until the next file is defined
   274  func (p *Process) parseFileLines(lines []string) error {
   275  	file := FileDescriptor{}
   276  	for _, line := range lines {
   277  		if strings.HasPrefix(line, "f") && file.FileDescriptor != "" {
   278  			// New file
   279  			p.FileDescriptors = append(p.FileDescriptors, file)
   280  			file = FileDescriptor{}
   281  		}
   282  		err := file.parseField(line)
   283  		if err != nil {
   284  			return err
   285  		}
   286  	}
   287  	if file.FileDescriptor != "" {
   288  		p.FileDescriptors = append(p.FileDescriptors, file)
   289  	}
   290  	return nil
   291  }
   292  
   293  // parseProcessLines parses all entries for one process. All
   294  // files reported by lsof are centered around a process. One
   295  // process can include multiple file descriptors. An open port
   296  // is a file as well.
   297  func parseProcessLines(lines []string) (Process, error) {
   298  	p := Process{}
   299  	for index, line := range lines {
   300  		if strings.HasPrefix(line, "f") {
   301  			err := p.parseFileLines(lines[index:])
   302  			if err != nil {
   303  				return p, err
   304  			}
   305  		} else if strings.HasPrefix(line, "o") {
   306  			break
   307  		} else {
   308  			err := p.parseField(line)
   309  			if err != nil {
   310  				return p, err
   311  			}
   312  		}
   313  	}
   314  	return p, nil
   315  }
   316  
   317  func Parse(r io.Reader) ([]Process, error) {
   318  	processes := []Process{}
   319  	scanner := bufio.NewScanner(r)
   320  	processData := []string{}
   321  	for scanner.Scan() {
   322  		line := scanner.Text()
   323  
   324  		// ignore empty lines
   325  		if strings.TrimSpace(line) == "" {
   326  			continue
   327  		}
   328  
   329  		// we collect all the data until a new process occurs
   330  		// for the first process we have no data, therefore we skip that
   331  		if strings.HasPrefix(line, "p") && len(processData) > 0 {
   332  			process, err := parseProcessLines(processData)
   333  			if err != nil {
   334  				return nil, err
   335  			}
   336  			processes = append(processes, process)
   337  			processData = []string{}
   338  		}
   339  		processData = append(processData, line)
   340  	}
   341  
   342  	// handle the last process because there is no additional 'p' that tiggers it
   343  	if len(processData) > 0 {
   344  		process, err := parseProcessLines(processData)
   345  		if err != nil {
   346  			return nil, err
   347  		}
   348  		processes = append(processes, process)
   349  	}
   350  	return processes, nil
   351  }