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 }