go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers/os/resources/port.go (about) 1 // Copyright (c) Mondoo, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package resources 5 6 import ( 7 "bufio" 8 "encoding/binary" 9 "encoding/hex" 10 "errors" 11 "fmt" 12 "io" 13 "net/netip" 14 "regexp" 15 "strconv" 16 "strings" 17 "sync" 18 "unsafe" 19 20 "github.com/rs/zerolog/log" 21 "go.mondoo.com/cnquery/llx" 22 "go.mondoo.com/cnquery/providers-sdk/v1/plugin" 23 "go.mondoo.com/cnquery/providers/os/connection/shared" 24 "go.mondoo.com/cnquery/providers/os/resources/lsof" 25 "go.mondoo.com/cnquery/providers/os/resources/ports" 26 "go.mondoo.com/cnquery/providers/os/resources/powershell" 27 ) 28 29 type mqlPortsInternal struct { 30 processes2ports plugin.TValue[map[int64]*mqlProcess] 31 lock sync.Mutex 32 } 33 34 func (p *mqlPorts) list() ([]interface{}, error) { 35 conn := p.MqlRuntime.Connection.(shared.Connection) 36 pf := conn.Asset().Platform 37 38 switch { 39 case pf.IsFamily("linux"): 40 return p.listLinux() 41 case pf.IsFamily("windows"): 42 return p.listWindows() 43 44 case pf.IsFamily("darwin") || pf.Name == "freebsd": 45 // both macOS and FreeBSD support lsof 46 // FreeBSD may need an installation via `pkg install sysutils/lsof` 47 return p.listMacos() 48 default: 49 return nil, errors.New("could not detect suitable ports manager for platform: " + pf.Name) 50 } 51 } 52 53 func (p *mqlPorts) listening() ([]interface{}, error) { 54 all := p.GetList() 55 if all.Error != nil { 56 return nil, all.Error 57 } 58 59 res := []interface{}{} 60 for i := range all.Data { 61 cur := all.Data[i] 62 port := cur.(*mqlPort) 63 if port.State.Data == "listen" { 64 res = append(res, cur) 65 } 66 } 67 68 return res, nil 69 } 70 71 // Linux Implementation 72 73 var reLinuxProcNet = regexp.MustCompile( 74 "^\\s*\\d+: " + 75 "([0-9A-F]+):([0-9A-F]+) " + // local_address 76 "([0-9A-F]+):([0-9A-F]+) " + // rem_address 77 "([0-9A-F]+) " + // state 78 "[^ ]+:[^ ]+ " + // tx/rx 79 "[^ ]+:[^ ]+ " + // tr/tm 80 "[^ ]+\\s+" + // retrnsmt 81 "(\\d+)\\s+" + // uid 82 "\\d+\\s+" + // timeout 83 "(\\d+)\\s+" + // inode 84 "", // lots of other stuff if we want it... 85 ) 86 87 // "lrwx------ 1 0 0 64 Dec 6 13:56 /proc/1/fd/12 -> socket:[37364]" 88 var reFindSockets = regexp.MustCompile( 89 "^[lrwx-]+\\s+" + 90 "\\d+\\s+" + 91 "\\d+\\s+" + // uid 92 "\\d+\\s+" + // gid 93 "\\d+\\s+" + 94 "[^ ]+\\s+" + // month, e.g. Dec 95 "\\d+\\s+" + // day 96 "\\d+:\\d+\\s+" + // time 97 "/proc/(\\d+)/fd/\\d+\\s+" + // path 98 "->\\s+" + 99 ".*socket:\\[(\\d+)\\].*\\s*") // target 100 101 var TCP_STATES = map[int64]string{ 102 1: "established", 103 2: "syn sent", 104 3: "syn recv", 105 4: "fin wait1", 106 5: "fin wait2", 107 6: "time wait", 108 7: "close", 109 8: "close wait", 110 9: "last ack", 111 10: "listen", 112 11: "closing", 113 12: "new syn recv", 114 } 115 116 func hex2ipv4(s string) (string, error) { 117 a, err := strconv.ParseUint(s[0:2], 16, 0) 118 if err != nil { 119 return "", err 120 } 121 122 b, err := strconv.ParseUint(s[2:4], 16, 0) 123 if err != nil { 124 return "", err 125 } 126 127 c, err := strconv.ParseUint(s[4:6], 16, 0) 128 if err != nil { 129 return "", err 130 } 131 132 d, err := strconv.ParseUint(s[6:8], 16, 0) 133 if err != nil { 134 return "", err 135 } 136 137 return (strconv.FormatUint(d, 10) + "." + 138 strconv.FormatUint(c, 10) + "." + 139 strconv.FormatUint(b, 10) + "." + 140 strconv.FormatUint(a, 10)), nil 141 } 142 143 func hex2ipv6(s string) (string, error) { 144 networkEndian := ipv6EndianTranslation(s) 145 ipBytes, err := hex.DecodeString(networkEndian) 146 if err != nil { 147 return "", err 148 } 149 150 var ipBytes16 [16]byte 151 152 copy(ipBytes16[:], ipBytes) 153 ip := netip.AddrFrom16(ipBytes16) 154 155 if ip.Next().Is6() { 156 // ipv6-friendly formatting with the [] brackets 157 return fmt.Sprintf("[%s]", ip.String()), nil 158 } else { 159 return "", err 160 } 161 } 162 163 func ipv6EndianTranslation(s string) string { 164 var nativeEndianness binary.ByteOrder 165 166 buf := [2]byte{} 167 *(*uint16)(unsafe.Pointer(&buf[0])) = uint16(0xABCD) 168 169 switch buf { 170 case [2]byte{0xCD, 0xAB}: 171 nativeEndianness = binary.LittleEndian 172 case [2]byte{0xAB, 0xCD}: 173 nativeEndianness = binary.BigEndian 174 default: 175 panic("neither little nor big endian detected...") 176 } 177 178 if nativeEndianness == binary.BigEndian { 179 return s 180 } 181 182 if len(s) != 32 { 183 // not an IPv6 address in hex format 184 return "" 185 } 186 187 // read 8 bytes at a time and little-to-big byte swap 188 // Ex: fe80:0000:0000:0000:5578:afa9:4caf:27a1 becomes 189 // 0000:80fe:0000:0000:a9af:7855:a127:af4c 190 swappedBytes := make([]byte, len(s)) 191 for i := 0; i < len(s); i += 8 { 192 swappedBytes[i] = s[i+6] 193 swappedBytes[i+1] = s[i+7] 194 swappedBytes[i+2] = s[i+4] 195 swappedBytes[i+3] = s[i+5] 196 197 swappedBytes[i+4] = s[i+2] 198 swappedBytes[i+5] = s[i+3] 199 swappedBytes[i+6] = s[i+0] 200 swappedBytes[i+7] = s[i+1] 201 } 202 203 return string(swappedBytes) 204 } 205 206 func (p *mqlPorts) users() (map[int64]*mqlUser, error) { 207 obj, err := CreateResource(p.MqlRuntime, "users", map[string]*llx.RawData{}) 208 if err != nil { 209 return nil, err 210 } 211 users := obj.(*mqlUsers) 212 213 err = users.refreshCache(nil) 214 if err != nil { 215 return nil, err 216 } 217 218 return users.usersByID, nil 219 } 220 221 func (p *mqlPorts) processesBySocket() (map[int64]*mqlProcess, error) { 222 p.lock.Lock() 223 defer p.lock.Unlock() 224 225 if p.processes2ports.Error != nil { 226 return nil, p.processes2ports.Error 227 } 228 if p.processes2ports.State&plugin.StateIsSet != 0 { 229 return p.processes2ports.Data, nil 230 } 231 232 // Prerequisites: processes 233 obj, err := CreateResource(p.MqlRuntime, "processes", map[string]*llx.RawData{}) 234 if err != nil { 235 p.processes2ports = plugin.TValue[map[int64]*mqlProcess]{ 236 State: plugin.StateIsSet, 237 Error: err, 238 } 239 return nil, err 240 } 241 processes := obj.(*mqlProcesses) 242 243 err = processes.refreshCache(nil) 244 if err != nil { 245 p.processes2ports = plugin.TValue[map[int64]*mqlProcess]{ 246 State: plugin.StateIsSet, 247 Error: err, 248 } 249 return nil, err 250 } 251 252 conn := p.MqlRuntime.Connection.(shared.Connection) 253 res := map[int64]*mqlProcess{} 254 if len(res) == 0 { 255 c, err := conn.RunCommand("find /proc -maxdepth 4 -path '/proc/*/fd/*' -exec ls -n {} \\;") 256 if err != nil { 257 p.processes2ports = plugin.TValue[map[int64]*mqlProcess]{ 258 State: plugin.StateIsSet, 259 Error: errors.New("processes> could not run command: " + err.Error()), 260 } 261 return nil, p.processes2ports.Error 262 } 263 264 processesBySocket := map[int64]*mqlProcess{} 265 scanner := bufio.NewScanner(c.Stdout) 266 for scanner.Scan() { 267 line := scanner.Text() 268 pid, inode, err := parseLinuxFindLine(line) 269 if err != nil || (pid == 0 && inode == 0) { 270 continue 271 } 272 273 processesBySocket[inode] = processes.ByPID[pid] 274 } 275 processes.BySocketID = processesBySocket 276 res = processesBySocket 277 } 278 279 p.processes2ports = plugin.TValue[map[int64]*mqlProcess]{ 280 Data: res, 281 State: plugin.StateIsSet, 282 } 283 return res, err 284 } 285 286 func parseLinuxFindLine(line string) (int64, int64, error) { 287 if strings.HasSuffix(line, "Permission denied") || strings.HasSuffix(line, "No such file or directory") { 288 return 0, 0, nil 289 } 290 291 m := reFindSockets.FindStringSubmatch(line) 292 if len(m) == 0 { 293 return 0, 0, nil 294 } 295 296 pid, err := strconv.ParseInt(m[1], 10, 64) 297 if err != nil { 298 log.Error().Err(err).Msg("cannot parse unix pid " + m[1]) 299 return 0, 0, err 300 } 301 302 inode, err := strconv.ParseInt(m[2], 10, 64) 303 if err != nil { 304 log.Error().Err(err).Msg("cannot parse socket inode " + m[2]) 305 return 0, 0, err 306 } 307 308 return pid, inode, nil 309 } 310 311 // See: 312 // - socket/address parsing: https://wiki.christophchamp.com/index.php?title=Unix_sockets 313 func (p *mqlPorts) parseProcNet(path string, protocol string, users map[int64]*mqlUser) ([]interface{}, error) { 314 conn := p.MqlRuntime.Connection.(shared.Connection) 315 fs := conn.FileSystem() 316 stat, err := fs.Stat(path) 317 if err != nil { 318 return nil, errors.New("cannot access stat for " + path) 319 } 320 if stat.IsDir() { 321 return nil, errors.New("something is wrong, looks like " + path + " is a folder") 322 } 323 324 fi, err := fs.Open(path) 325 if err != nil { 326 return nil, err 327 } 328 defer fi.Close() 329 330 var res []interface{} 331 scanner := bufio.NewScanner(fi) 332 for scanner.Scan() { 333 line := scanner.Text() 334 335 port, err := parseProcNetLine(line) 336 if err != nil { 337 return nil, fmt.Errorf("failed to parse proc net line: %v", err) 338 } 339 if port == nil { 340 continue 341 } 342 343 obj, err := CreateResource(p.MqlRuntime, "port", map[string]*llx.RawData{ 344 "protocol": llx.StringData(protocol), 345 "port": llx.IntData(port.Port), 346 "address": llx.StringData(port.Address), 347 "user": llx.ResourceData(users[port.Uid], "user"), 348 "state": llx.StringData(port.State), 349 "remoteAddress": llx.StringData(port.RemoteAddress), 350 "remotePort": llx.IntData(port.RemotePort), 351 }) 352 if err != nil { 353 return nil, err 354 } 355 356 po := obj.(*mqlPort) 357 po.inode = port.Inode 358 359 res = append(res, obj) 360 } 361 362 return res, nil 363 } 364 365 type procNetPort struct { 366 Address string 367 Port int64 368 RemoteAddress string 369 RemotePort int64 370 State string 371 Uid int64 372 Inode int64 373 } 374 375 func parseProcNetLine(line string) (*procNetPort, error) { 376 m := reLinuxProcNet.FindStringSubmatch(line) 377 port := &procNetPort{} 378 if len(m) == 0 { 379 return nil, nil 380 } 381 382 var address string 383 var err error 384 if len(m[1]) > 8 { 385 address, err = hex2ipv6(m[1]) 386 } else { 387 address, err = hex2ipv4(m[1]) 388 } 389 if err != nil { 390 return nil, errors.New("failed to parse port address: " + m[1]) 391 } 392 port.Address = address 393 394 localPort, err := strconv.ParseUint(m[2], 16, 64) 395 if err != nil { 396 return nil, errors.New("failed to parse port number: " + m[2]) 397 } 398 port.Port = int64(localPort) 399 400 var remoteAddress string 401 if len(m[1]) > 8 { 402 remoteAddress, err = hex2ipv6(m[3]) 403 } else { 404 remoteAddress, err = hex2ipv4(m[3]) 405 } 406 if err != nil { 407 return nil, errors.New("failed to parse port address: " + m[3]) 408 } 409 port.RemoteAddress = remoteAddress 410 411 remotePort, err := strconv.ParseUint(m[4], 16, 64) 412 if err != nil { 413 return nil, errors.New("failed to parse port number: " + m[4]) 414 } 415 port.RemotePort = int64(remotePort) 416 417 stateNum, err := strconv.ParseInt(m[5], 16, 64) 418 if err != nil { 419 return nil, errors.New("failed to parse state number: " + m[5]) 420 } 421 state, ok := TCP_STATES[stateNum] 422 if !ok { 423 state = "unknown" 424 } 425 port.State = state 426 427 uid, err := strconv.ParseUint(m[6], 10, 64) 428 if err != nil { 429 return nil, errors.New("failed to parse port UID: " + m[6]) 430 } 431 port.Uid = int64(uid) 432 433 inode, err := strconv.ParseUint(m[7], 10, 64) 434 if err != nil { 435 return nil, errors.New("failed to parse port Inode: " + m[7]) 436 } 437 port.Inode = int64(inode) 438 439 return port, nil 440 } 441 442 func (p *mqlPorts) listLinux() ([]interface{}, error) { 443 users, err := p.users() 444 if err != nil { 445 return nil, err 446 } 447 448 var ports []interface{} 449 tcpPorts, err := p.parseProcNet("/proc/net/tcp", "tcp4", users) 450 if err != nil { 451 return nil, err 452 } 453 ports = append(ports, tcpPorts...) 454 455 udpPorts, err := p.parseProcNet("/proc/net/udp", "udp4", users) 456 if err != nil { 457 return nil, err 458 } 459 ports = append(ports, udpPorts...) 460 461 tcpPortsV6, err := p.parseProcNet("/proc/net/tcp6", "tcp6", users) 462 if err != nil { 463 return nil, err 464 } 465 ports = append(ports, tcpPortsV6...) 466 467 udpPortsV6, err := p.parseProcNet("/proc/net/udp6", "udp6", users) 468 if err != nil { 469 return nil, err 470 } 471 ports = append(ports, udpPortsV6...) 472 473 return ports, nil 474 } 475 476 func (p *mqlPorts) processesByPid() (map[int64]*mqlProcess, error) { 477 // Prerequisites: processes 478 obj, err := CreateResource(p.MqlRuntime, "processes", map[string]*llx.RawData{}) 479 if err != nil { 480 return nil, err 481 } 482 processes := obj.(*mqlProcesses) 483 484 err = processes.refreshCache(nil) 485 if err != nil { 486 return nil, err 487 } 488 489 return processes.ByPID, nil 490 } 491 492 // Windows Implementation 493 494 func (p *mqlPorts) listWindows() ([]interface{}, error) { 495 processes, err := p.processesByPid() 496 if err != nil { 497 return nil, err 498 } 499 500 conn := p.MqlRuntime.Connection.(shared.Connection) 501 encodedCmd := powershell.Encode("Get-NetTCPConnection | ConvertTo-Json") 502 executedCmd, err := conn.RunCommand(encodedCmd) 503 if err != nil { 504 return nil, err 505 } 506 507 list, err := p.parseWindowsPorts(executedCmd.Stdout, processes) 508 if err != nil { 509 return nil, err 510 } 511 512 return list, nil 513 } 514 515 func (p *mqlPorts) parseWindowsPorts(r io.Reader, processes map[int64]*mqlProcess) ([]interface{}, error) { 516 portList, err := ports.ParseWindowsNetTCPConnections(r) 517 if err != nil { 518 return nil, err 519 } 520 521 var res []interface{} 522 for i := range portList { 523 port := portList[i] 524 525 var state string 526 switch port.State { 527 case ports.Listen: 528 state = TCP_STATES[10] 529 case ports.Closed: 530 state = TCP_STATES[7] 531 case ports.SynSent: 532 state = TCP_STATES[2] 533 case ports.SynReceived: 534 state = TCP_STATES[3] 535 case ports.Established: 536 state = TCP_STATES[1] 537 case ports.FinWait1: 538 state = TCP_STATES[4] 539 case ports.FinWait2: 540 state = TCP_STATES[5] 541 case ports.CloseWait: 542 state = TCP_STATES[8] 543 case ports.Closing: 544 state = TCP_STATES[11] 545 case ports.LastAck: 546 state = TCP_STATES[9] 547 case ports.TimeWait: 548 state = TCP_STATES[6] 549 case ports.DeleteTCB: 550 state = "deletetcb" 551 case ports.Bound: 552 state = "bound" 553 } 554 555 process := processes[port.OwningProcess] 556 557 protocol := "tcp4" 558 if strings.Contains(port.LocalAddress, ":") { 559 protocol = "tcp6" 560 } 561 562 obj, err := CreateResource(p.MqlRuntime, "port", map[string]*llx.RawData{ 563 "protocol": llx.StringData(protocol), 564 "port": llx.IntData(port.LocalPort), 565 "address": llx.StringData(port.LocalAddress), 566 "user": llx.ResourceData(nil, "user"), 567 "process": llx.ResourceData(process, "process"), 568 "state": llx.StringData(state), 569 "remoteAddress": llx.StringData(port.RemoteAddress), 570 "remotePort": llx.IntData(port.RemotePort), 571 }) 572 if err != nil { 573 log.Error().Err(err).Send() 574 return nil, err 575 } 576 577 res = append(res, obj) 578 } 579 return res, nil 580 } 581 582 // macOS Implementation 583 584 // listMacos reads the lsof information of all open files that are tcp sockets 585 func (p *mqlPorts) listMacos() ([]interface{}, error) { 586 users, err := p.users() 587 if err != nil { 588 return nil, err 589 } 590 591 processes, err := p.processesByPid() 592 if err != nil { 593 return nil, err 594 } 595 596 conn := p.MqlRuntime.Connection.(shared.Connection) 597 executedCmd, err := conn.RunCommand("lsof -nP -i -F") 598 if err != nil { 599 return nil, err 600 } 601 602 lsofProcesses, err := lsof.Parse(executedCmd.Stdout) 603 if err != nil { 604 return nil, err 605 } 606 607 // iterating over all processes to find the once that have network file descriptors 608 var res []interface{} 609 for i := range lsofProcesses { 610 process := lsofProcesses[i] 611 for j := range process.FileDescriptors { 612 fd := process.FileDescriptors[j] 613 if fd.Type != lsof.FileTypeIPv4 && fd.Type != lsof.FileTypeIPv6 { 614 continue 615 } 616 617 uid, err := strconv.Atoi(process.UID) 618 if err != nil { 619 return nil, err 620 } 621 user := users[int64(uid)] 622 623 pid, err := strconv.Atoi(process.PID) 624 if err != nil { 625 return nil, err 626 } 627 mqlProcess := processes[int64(pid)] 628 629 protocol := strings.ToLower(fd.Protocol) 630 if fd.Type == lsof.FileTypeIPv6 { 631 protocol = protocol + "6" 632 } else { 633 protocol = protocol + "4" 634 } 635 636 localAddress, localPort, remoteAddress, remotePort, err := fd.NetworkFile() 637 if err != nil { 638 return nil, err 639 } 640 // lsof presents a process listening on any ipv6 address as listening on "*" 641 // change this to a more ipv6-friendly formatting 642 if protocol == "ipv6" && strings.HasPrefix(localAddress, "*") { 643 localAddress = strings.Replace(localAddress, "*", "[::]", 1) 644 } 645 646 state, ok := TCP_STATES[fd.TcpState()] 647 if !ok { 648 state = "unknown" 649 } 650 651 obj, err := CreateResource(p.MqlRuntime, "port", map[string]*llx.RawData{ 652 "protocol": llx.StringData(protocol), 653 "port": llx.IntData(localPort), 654 "address": llx.StringData(localAddress), 655 "user": llx.ResourceData(user, "user"), 656 "process": llx.ResourceData(mqlProcess, "process"), 657 "state": llx.StringData(state), 658 "remoteAddress": llx.StringData(remoteAddress), 659 "remotePort": llx.IntData(remotePort), 660 }) 661 if err != nil { 662 log.Error().Err(err).Send() 663 return nil, err 664 } 665 666 res = append(res, obj) 667 } 668 } 669 670 return res, nil 671 } 672 673 type mqlPortInternal struct { 674 inode int64 675 } 676 677 func (s *mqlPort) id() (string, error) { 678 return fmt.Sprintf("port: %s/%s:%d/%s:%d/%s", 679 s.Protocol.Data, s.Address.Data, s.Port.Data, 680 s.RemoteAddress.Data, s.RemotePort.Data, s.State.Data), nil 681 } 682 683 func (s *mqlPort) tls(address string, port int64, proto string) (plugin.Resource, error) { 684 if address == "" || address == "0.0.0.0" { 685 address = "127.0.0.1" 686 } 687 688 socket, err := s.MqlRuntime.CreateSharedResource("socket", map[string]*llx.RawData{ 689 "protocol": llx.StringData(proto), 690 "port": llx.IntData(port), 691 "address": llx.StringData(address), 692 }) 693 if err != nil { 694 return nil, err 695 } 696 697 return s.MqlRuntime.CreateSharedResource("tls", map[string]*llx.RawData{ 698 "socket": llx.ResourceData(socket, "socket"), 699 "domainName": llx.StringData(""), 700 }) 701 } 702 703 func (s *mqlPort) process() (*mqlProcess, error) { 704 // At this point everything except for linux should have their port identified. 705 // For linux we need to scour the /proc system, which takes a long time. 706 // TODO: massively speed this up on linux with more approach. 707 conn := s.MqlRuntime.Connection.(shared.Connection) 708 pf := conn.Asset().Platform 709 if !pf.IsFamily("linux") { 710 return nil, errors.New("unable to detect process for this port") 711 } 712 713 obj, err := CreateResource(s.MqlRuntime, "ports", map[string]*llx.RawData{}) 714 if err != nil { 715 return nil, err 716 } 717 ports := obj.(*mqlPorts) 718 719 // TODO: refresh on the fly, eg when loading this from a recording 720 if s.inode == 0 { 721 return nil, errors.New("no iNode found for this port and cannot yet refresh it") 722 } 723 724 procs, err := ports.processesBySocket() 725 if err != nil { 726 return nil, err 727 } 728 proc := procs[s.inode] 729 if proc == nil { 730 s.Process = plugin.TValue[*mqlProcess]{State: plugin.StateIsSet | plugin.StateIsNull} 731 } 732 return proc, nil 733 }