diff --git a/.vscode/launch.json b/.vscode/launch.json index 569da0563a..8780adddfe 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -168,6 +168,15 @@ "args": [ "go", "./providers/os/resources/os.lr", "--dist", "./providers/os/dist", ], + }, + { + "name": "cnquery-status", + "type": "go", + "request": "launch", + "program": "${workspaceRoot}/apps/cnquery/cnquery.go", + "args": [ + "status", + ], } ] } diff --git a/cli/sysinfo/sysinfo.go b/cli/sysinfo/sysinfo.go index 811da3d144..8e35240b8e 100644 --- a/cli/sysinfo/sysinfo.go +++ b/cli/sysinfo/sysinfo.go @@ -6,6 +6,9 @@ package sysinfo import ( "errors" + "github.com/rs/zerolog/log" + "go.mondoo.com/cnquery/providers/os/resources/networkinterface" + "go.mondoo.com/cnquery" "go.mondoo.com/cnquery/cli/execruntime" "go.mondoo.com/cnquery/llx" @@ -44,12 +47,17 @@ func GatherSystemInfo(opts ...SystemInfoOption) (*SystemInfo, error) { opt(cfg) } + log.Debug().Msg("Gathering system information") if cfg.runtime == nil { + cfg.runtime = providers.Coordinator.NewRuntime() + + // TODO: we need to ensure that the os provider is available here + + // init runtime if err := cfg.runtime.UseProvider(providers.DefaultOsID); err != nil { return nil, err } - args, err := cfg.runtime.Provider.Instance.Plugin.ParseCLI(&plugin.ParseCLIReq{ Connector: "local", }) @@ -70,7 +78,8 @@ func GatherSystemInfo(opts ...SystemInfoOption) (*SystemInfo, error) { } exec := mql.New(cfg.runtime, nil) - raw, err := exec.Exec("asset { name arch title family build version kind runtime labels }", nil) + // TODO: it is not returning it as a MQL SingleValue, therefore we need to force it with return + raw, err := exec.Exec("return asset { name arch title family build version kind runtime labels ids }", nil) if err != nil { return sysInfo, err } @@ -87,20 +96,31 @@ func GatherSystemInfo(opts ...SystemInfoOption) (*SystemInfo, error) { Runtime: llx.TRaw2T[string](vals["runtime"]), Labels: llx.TRaw2TMap[string](vals["labels"]), } + + platformID := llx.TRaw2TArr[string](vals["ids"]) + if len(platformID) > 0 { + sysInfo.PlatformId = platformID[0] + } } else { return sysInfo, errors.New("returned asset detection type is incorrect") } - // TODO: platform IDs - // idDetector := providers.HostnameDetector - // if pi.IsFamily(platform.FAMILY_WINDOWS) { - // idDetector = providers.MachineIdDetector - // } - // sysInfo.PlatformId = info.IDs[0] - // TODO: outbound ip - // sysInfo.IP = ip - // TODO: hostname - // sysInfo.Hostname = hn + // determine hostname + osRaw, err := exec.Exec("return os.hostname", nil) + if err != nil { + return sysInfo, err + } + + if hostname, ok := osRaw.Value.(string); ok { + sysInfo.Hostname = hostname + } + + // determine ip address + // TODO: move this to MQL and expose that information in the graph + ipAddr, err := networkinterface.GetOutboundIP() + if err == nil { + sysInfo.IP = ipAddr.String() + } // detect the execution runtime execEnv := execruntime.Detect() diff --git a/providers/os/connection/mock/mock.go b/providers/os/connection/mock/mock.go index 7a0f8b0f9e..c4565e0ccf 100644 --- a/providers/os/connection/mock/mock.go +++ b/providers/os/connection/mock/mock.go @@ -118,7 +118,7 @@ func (c *Connection) ID() uint32 { } func (c *Connection) Type() shared.ConnectionType { - return "local" + return "mock" } func (c *Connection) Asset() *inventory.Asset { diff --git a/providers/os/resources/networkinterface/hostip.go b/providers/os/resources/networkinterface/hostip.go new file mode 100644 index 0000000000..9361104e68 --- /dev/null +++ b/providers/os/resources/networkinterface/hostip.go @@ -0,0 +1,106 @@ +package networkinterface + +import ( + "fmt" + "net" + "sort" + + "github.com/cockroachdb/errors" + "github.com/rs/zerolog/log" +) + +func filterNetworkInterface(interfaces []Interface, flagFilter func(flags net.Flags) bool) []Interface { + i := []Interface{} + for _, v := range interfaces { + if flagFilter(v.Flags) { + i = append(i, v) + } + } + return i +} + +// byIfaceIndex Interface by its index +type byIfaceIndex []Interface + +func (iface byIfaceIndex) Len() int { return len(iface) } +func (iface byIfaceIndex) Less(i, j int) bool { return iface[i].Index < iface[j].Index } +func (iface byIfaceIndex) Swap(i, j int) { iface[i], iface[j] = iface[j], iface[i] } + +// HostIP extracts the best-guess for the IP of the host +// It will search ip v4 first and fallback to v6 +func HostIP(interfaces []Interface) (ip string, err error) { + log.Debug().Int("interfaces", len(interfaces)).Msg("search ip") + // filter interfaces that are not up or a loopback/p2p interface + interfaces = filterNetworkInterface(interfaces, func(flags net.Flags) bool { + if (flags&net.FlagUp != 0) && + (flags&net.FlagLoopback == 0) && + (flags&net.FlagPointToPoint == 0) { + return true + } + return false + }) + + // sort interfaces by its index + sort.Sort(byIfaceIndex(interfaces)) + + var foundIPv4 net.IP + foundIPsv6 := []net.IP{} + + // search for IPv4 + for _, i := range interfaces { + addrs := i.Addrs + for _, addr := range addrs { + var foundIPv6 net.IP + switch v := addr.(type) { + case *net.IPAddr: + foundIPv4 = v.IP.To4() + foundIPv6 = v.IP.To16() + case *net.IPNet: + foundIPv4 = v.IP.To4() + foundIPv6 = v.IP.To16() + case *ipAddr: + foundIPv4 = v.IP.To4() + foundIPv6 = v.IP.To16() + } + + if foundIPv4 != nil { + return foundIPv4.String(), nil + } + if foundIPv6 != nil { + foundIPsv6 = append(foundIPsv6, foundIPv6) + } + } + } + + // search for IPv6 + if len(foundIPsv6) > 0 { + return foundIPsv6[0].String(), nil + } + + return "", fmt.Errorf("no IP address found") +} + +// GetOutboundIP returns the local IP that is used for outbound connections +// It does not establish a real connection and the destination does not need to valid. +// Since its using udp protocol (unlike TCP) a handshake nor connection is required, +// / then it gets the local up address if it would connect to that target +// conn.LocalAddr().String() returns the local ip and port +// +// # NOTE be aware that this code does not work on remote targets +// +// @see this approach is derived from https://stackoverflow.com/a/37382208 +func GetOutboundIP() (net.IP, error) { + conn, err := net.Dial("udp", "1.1.1.1:80") + if err != nil { + return nil, errors.Wrap(err, "could not determine outbound ip") + } + defer conn.Close() + + localAddr := conn.LocalAddr().(*net.UDPAddr) + + if localAddr == nil { + return nil, errors.New("could not determine outbound ip") + } + + return localAddr.IP, nil +} diff --git a/providers/os/resources/networkinterface/interface.go b/providers/os/resources/networkinterface/interface.go new file mode 100644 index 0000000000..d36f075934 --- /dev/null +++ b/providers/os/resources/networkinterface/interface.go @@ -0,0 +1,471 @@ +package networkinterface + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "net" + "regexp" + "strconv" + "strings" + + "github.com/cockroachdb/errors" + "github.com/rs/zerolog/log" + "go.mondoo.com/cnquery/providers-sdk/v1/inventory" + "go.mondoo.com/cnquery/providers/os/connection" + "go.mondoo.com/cnquery/providers/os/connection/shared" + "go.mondoo.com/cnquery/providers/os/resources/powershell" +) + +var errNoSuchInterface = errors.New("no such network interface") + +// mimics https://golang.org/src/net/interface.go to provide a similar api +type Interface struct { + Index int // positive integer that starts at one, zero is never used + MTU int // maximum transmission unit + Name string // e.g., "en0", "lo0", "eth0.100" + HardwareAddr net.HardwareAddr // IEEE MAC-48, EUI-48 and EUI-64 form + Flags net.Flags // e.g., FlagUp, FlagLoopback, FlagMulticast + Addrs []net.Addr + MulticastAddrs []net.Addr +} + +func New(conn shared.Connection) *InterfaceResource { + return &InterfaceResource{ + conn: conn, + } +} + +type InterfaceResource struct { + conn shared.Connection +} + +func (r *InterfaceResource) Interfaces() ([]Interface, error) { + asset := r.conn.Asset() + if asset == nil || asset.Platform == nil { + return nil, errors.New("cannot find OS information for interface detection") + } + + log.Debug().Strs("families", asset.Platform.Family).Msg("check if platform is supported for network interface") + if r.conn.Type() == connection.Local { + handler := &GoNativeInterfaceHandler{} + return handler.Interfaces() + } else if asset.Platform.Name == "macos" { + handler := &MacOSInterfaceHandler{ + conn: r.conn, + } + return handler.Interfaces() + } else if asset.Platform.IsFamily(inventory.FAMILY_LINUX) { + log.Debug().Msg("detected linux platform") + handler := &LinuxInterfaceHandler{ + conn: r.conn, + } + return handler.Interfaces() + } else if asset.Platform.Name == "windows" { + handler := &WindowsInterfaceHandler{ + conn: r.conn, + } + return handler.Interfaces() + } + + return nil, errors.New("interfaces does not support platform: " + asset.Platform.Name) +} + +func (r *InterfaceResource) InterfaceByName(name string) (*Interface, error) { + ifaces, err := r.Interfaces() + if err != nil { + return nil, err + } + + for i := range ifaces { + if ifaces[i].Name == name { + return &ifaces[i], nil + } + } + return nil, errNoSuchInterface +} + +type GoNativeInterfaceHandler struct{} + +func (i *GoNativeInterfaceHandler) Interfaces() ([]Interface, error) { + var goInterfaces []net.Interface + var err error + if goInterfaces, err = net.Interfaces(); err != nil { + return nil, errors.Wrap(err, "failed to load network interfaces") + } + + ifaces := make([]Interface, len(goInterfaces)) + for i := range goInterfaces { + + addrs, err := goInterfaces[i].Addrs() + if err != nil { + log.Debug().Err(err).Str("iface", goInterfaces[i].Name).Msg("could not retrieve ip addresses") + } + multicastAddrs, err := goInterfaces[i].MulticastAddrs() + if err != nil { + log.Debug().Err(err).Str("iface", goInterfaces[i].Name).Msg("could not retrieve multicast addresses") + } + + ifaces[i] = Interface{ + Name: goInterfaces[i].Name, + Index: goInterfaces[i].Index, + MTU: goInterfaces[i].MTU, + HardwareAddr: goInterfaces[i].HardwareAddr, + Flags: goInterfaces[i].Flags, + Addrs: addrs, + MulticastAddrs: multicastAddrs, + } + } + + return ifaces, nil +} + +type LinuxInterfaceHandler struct { + conn shared.Connection +} + +func (i *LinuxInterfaceHandler) Interfaces() ([]Interface, error) { + // TODO: support extracting the information via /sys/class/net/, /proc/net/fib_trie + // fetch all network adapter via ip addr show + cmd, err := i.conn.RunCommand("ip -o addr show") + if err != nil { + return nil, errors.Wrap(err, "could not fetch macos network adapter") + } + + return i.ParseIpAddr(cmd.Stdout) +} + +func (i *LinuxInterfaceHandler) ParseIpAddr(r io.Reader) ([]Interface, error) { + interfaces := map[string]Interface{} + + scanner := bufio.NewScanner(r) + ipaddrParse := regexp.MustCompile(`^(\d):\s([\w\d\./\:]+)\s*(inet|inet6)\s([\w\d\./\:]+)\s(.*)$`) + + for scanner.Scan() { + line := scanner.Text() + + m := ipaddrParse.FindStringSubmatch(line) + + // check that we have a match + if len(m) < 4 { + return nil, fmt.Errorf("cannot parse ip: %s", line) + } + + name := m[2] + + idx, err := strconv.Atoi(strings.TrimSpace(m[1])) + if err != nil { + log.Warn().Err(err).Msg("could not parse ip addr idx") + continue + } + + inet, ok := interfaces[name] + if !ok { + inet = Interface{ + Index: idx, + Name: name, + } + } + + var ip net.IP + if m[3] == "inet" { + ipv4Addr, _, err := net.ParseCIDR(m[4]) + if err != nil { + log.Error().Err(err).Msg("could not parse ipv4") + } + + ip = ipv4Addr + } else if m[3] == "inet6" { + ipv6Addr, _, err := net.ParseCIDR(m[4]) + if err != nil { + log.Error().Err(err).Msg("could not parse ipv6") + } + ip = ipv6Addr + } + + inet.Addrs = append(inet.Addrs, &ipAddr{IP: ip}) + + var flags net.Flags + flags |= net.FlagUp + + if strings.Contains(m[5], "host") { + flags |= net.FlagLoopback + } else { + flags |= net.FlagBroadcast + } + + inet.Flags = flags + + interfaces[name] = inet + } + + res := []Interface{} + for i := range interfaces { + res = append(res, interfaces[i]) + } + + return res, nil +} + +type MacOSInterfaceHandler struct { + conn shared.Connection +} + +func (i *MacOSInterfaceHandler) Interfaces() ([]Interface, error) { + // fetch all network adapter + cmd, err := i.conn.RunCommand("ifconfig") + if err != nil { + return nil, errors.Wrap(err, "could not fetch macos network adapter") + } + + return i.ParseMacOS(cmd.Stdout) +} + +var ( + IfconfigInterfaceLine = regexp.MustCompile(`^(.*):\ flags=(\d*)\<(.*)>\smtu\s(\d*)$`) + IfconfigInetLine = regexp.MustCompile(`^\s+inet(\d*)\s([\w\d.:%]+)\s`) +) + +func (i *MacOSInterfaceHandler) ParseMacOS(r io.Reader) ([]Interface, error) { + interfaces := []Interface{} + ifIndex := -1 + scanner := bufio.NewScanner(r) + + for scanner.Scan() { + line := scanner.Text() + + // new interface + m := IfconfigInterfaceLine.FindStringSubmatch(line) + if len(m) > 0 { + var mtu int + var err error + mtu, err = strconv.Atoi(strings.TrimSpace(m[4])) + if err != nil { + return nil, errors.Wrap(err, "cannot parse macos ifconfig mtu") + } + + var flags net.Flags + if len(m[3]) > 0 { + ifConfigFlags := strings.Split(m[3], ",") + for i := range ifConfigFlags { + switch strings.ToLower(ifConfigFlags[i]) { + case "up": + flags |= net.FlagUp + case "broadcast": + flags |= net.FlagBroadcast + case "multicast": + flags |= net.FlagMulticast + case "loopback": + flags |= net.FlagLoopback + case "pointtopoint": + flags |= net.FlagPointToPoint + } + } + } + + i := Interface{ + Index: ifIndex + 2, + Name: m[1], + MTU: mtu, + Flags: flags, + } + + interfaces = append(interfaces, i) + ifIndex++ + } + + // parse mac address + if strings.HasPrefix(line, " ether") { + macaddress := strings.TrimSpace(strings.TrimPrefix(line, " ether")) + mac, err := net.ParseMAC(macaddress) + if err != nil { + return nil, err + } + interfaces[ifIndex].HardwareAddr = mac + } + + m = IfconfigInetLine.FindStringSubmatch(line) + if len(m) > 0 { + ip := parseIpAddr(m[2]) + if ip != nil { + interfaces[ifIndex].Addrs = append(interfaces[ifIndex].Addrs, &ipAddr{IP: ip}) + } + } + + } + return interfaces, nil +} + +type WindowsInterface struct { + Name string `json:"Name"` + IfIndex int `json:"ifIndex"` + InterfaceType int `json:"InterfaceType"` + Status string `json:"Status"` + MacAddress string `json:"MacAddress"` +} + +type WindowsNetIp struct { + InterfaceAlias string `json:"InterfaceAlias"` + IPv4Address *string `json:"IPv4Address"` + IPv6Address *string `json:"IPv6Address"` +} + +const ( + WinGetNetAdapter = "Get-NetAdapter | Select-Object -Property Name, ifIndex, InterfaceType, InterfaceDescription, Status, State, MacAddress, LinkSpeed, ReceiveLinkSpeed, TransmitLinkSpeed, Virtual | ConvertTo-Json" + WinGetNetIPAddress = "Get-NetIPAddress | Select-Object -Property IPv6Address, IPv4Address, InterfaceAlias | ConvertTo-Json" +) + +const ( + IF_TYPE_OTHER = 1 + IF_TYPE_ETHERNET_CSMACD = 6 + IF_TYPE_ISO88025_TOKENRING = 9 + IF_TYPE_PPP = 23 + IF_TYPE_SOFTWARE_LOOPBACK = 24 + IF_TYPE_ATM = 37 + IF_TYPE_IEEE80211 = 71 + IF_TYPE_TUNNEL = 131 + IF_TYPE_IEEE1394 = 144 +) + +// derived from https://golang.org/src/net/interface_windows.go +func windowsInterfaceFlags(status string, ifType int) net.Flags { + var flags net.Flags + if status == "Up" { + flags |= net.FlagUp + } + + switch ifType { + case IF_TYPE_ETHERNET_CSMACD, IF_TYPE_ISO88025_TOKENRING, IF_TYPE_IEEE80211, IF_TYPE_IEEE1394: + flags |= net.FlagBroadcast | net.FlagMulticast + case IF_TYPE_PPP, IF_TYPE_TUNNEL: + flags |= net.FlagPointToPoint | net.FlagMulticast + case IF_TYPE_SOFTWARE_LOOPBACK: + flags |= net.FlagLoopback | net.FlagMulticast + case IF_TYPE_ATM: + flags |= net.FlagBroadcast | net.FlagPointToPoint | net.FlagMulticast + } + return flags +} + +type ipAddr struct { + IP net.IP +} + +// name of the network (for example, "tcp", "udp") +func (a *ipAddr) Network() string { + return "tcp" +} + +// string form of address (for example, "192.0.2.1:25", "[2001:db8::1]:80") +func (a *ipAddr) String() string { + return a.IP.String() +} + +func parseIpAddr(ip string) net.IP { + // filter network id https://sid-500.com/2017/01/10/cisco-ipv6-link-local-adressen-und-router-advertisements/ + // "fe80::ed94:1267:afb5:bb7%6" becomes "fe80::ed94:1267:afb5:bb7" + m := strings.Split(ip, "%") + return net.ParseIP(m[0]) +} + +func filterWinIpByInterface(iName string, ips []WindowsNetIp) []net.Addr { + addrs := []net.Addr{} + + for i := range ips { + if ips[i].InterfaceAlias == iName { + var ip net.IP + if ips[i].IPv4Address != nil { + ip = parseIpAddr(*ips[i].IPv4Address) + } else if ips[i].IPv6Address != nil { + ip = parseIpAddr(*ips[i].IPv6Address) + } + if ip != nil { + addrs = append(addrs, &ipAddr{IP: ip}) + } + } + } + + return addrs +} + +type WindowsInterfaceHandler struct { + conn shared.Connection +} + +func (i *WindowsInterfaceHandler) Interfaces() ([]Interface, error) { + // fetch all network adapter + cmd, err := i.conn.RunCommand(powershell.Wrap(WinGetNetAdapter)) + if err != nil { + return nil, errors.Wrap(err, "could not fetch windows network adapter") + } + winAdapter, err := i.ParseNetAdapter(cmd.Stdout) + if err != nil { + return nil, errors.Wrap(err, "could not parse windows network adapter list") + } + + // fetch all ip addresses + cmd, err = i.conn.RunCommand(powershell.Wrap(WinGetNetIPAddress)) + if err != nil { + return nil, errors.Wrap(err, "could not fetch windows ip addresses") + } + winIpAddresses, err := i.ParseNetIpAdresses(cmd.Stdout) + if err != nil { + return nil, errors.Wrap(err, "could not parse windows ip addresses") + } + + // map information together + interfaces := make([]Interface, len(winAdapter)) + for i := range winAdapter { + mac, err := net.ParseMAC(winAdapter[i].MacAddress) + if err != nil { + return nil, err + } + + interfaces[i] = Interface{ + Name: winAdapter[i].Name, + Index: winAdapter[i].IfIndex, + HardwareAddr: mac, + Flags: windowsInterfaceFlags(winAdapter[i].Status, winAdapter[i].InterfaceType), + Addrs: filterWinIpByInterface(winAdapter[i].Name, winIpAddresses), + } + } + + return interfaces, nil +} + +func (i *WindowsInterfaceHandler) ParseNetAdapter(r io.Reader) ([]WindowsInterface, error) { + data, err := io.ReadAll(r) + if err != nil { + return nil, err + } + + var winInterfaces []WindowsInterface + err = json.Unmarshal(data, &winInterfaces) + if err != nil { + + // try again without array (powershell returns single values different) + var winInterface WindowsInterface + err = json.Unmarshal(data, &winInterface) + if err != nil { + return nil, err + } + + return []WindowsInterface{winInterface}, nil + } + return winInterfaces, nil +} + +func (i *WindowsInterfaceHandler) ParseNetIpAdresses(r io.Reader) ([]WindowsNetIp, error) { + data, err := io.ReadAll(r) + if err != nil { + return nil, err + } + + var winNetIps []WindowsNetIp + err = json.Unmarshal(data, &winNetIps) + if err != nil { + return nil, err + } + return winNetIps, nil +} diff --git a/providers/os/resources/networkinterface/interface_test.go b/providers/os/resources/networkinterface/interface_test.go new file mode 100644 index 0000000000..c15d5f77ed --- /dev/null +++ b/providers/os/resources/networkinterface/interface_test.go @@ -0,0 +1,139 @@ +package networkinterface_test + +import ( + "go.mondoo.com/cnquery/providers-sdk/v1/inventory" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mondoo.com/cnquery/providers/os/connection/mock" + "go.mondoo.com/cnquery/providers/os/resources/networkinterface" +) + +func TestWindowsRemoteInterface(t *testing.T) { + mock, err := mock.New("./testdata/windows.toml", &inventory.Asset{ + Platform: &inventory.Platform{ + Name: "windows", + }, + }) + require.NoError(t, err) + + ifaces := networkinterface.New(mock) + list, err := ifaces.Interfaces() + require.NoError(t, err) + assert.Equal(t, 1, len(list)) + inet := list[0] + assert.Equal(t, "Ethernet", inet.Name) + assert.Equal(t, 6, inet.Index) + assert.Equal(t, 0, inet.MTU) + assert.Equal(t, "up|broadcast|multicast", inet.Flags.String()) + assert.Equal(t, "00:15:5d:f2:3b:1d", inet.HardwareAddr.String()) + + assert.Equal(t, 2, len(inet.Addrs)) + assert.Equal(t, "fe80::ed94:1267:afb5:bb76", inet.Addrs[0].String()) + assert.Equal(t, "192.168.178.112", inet.Addrs[1].String()) + // the windows resource does not support multicast addresses + assert.True(t, len(inet.MulticastAddrs) == 0) + + ip, err := networkinterface.HostIP(list) + require.NoError(t, err) + assert.Equal(t, "192.168.178.112", ip) +} + +func TestMacOsRegex(t *testing.T) { + line := "lo0: flags=8049 mtu 16384" + + m := networkinterface.IfconfigInterfaceLine.FindStringSubmatch(line) + assert.Equal(t, "lo0", m[1]) + assert.Equal(t, "UP,LOOPBACK,RUNNING,MULTICAST", m[3]) + assert.Equal(t, "16384", m[4]) +} + +func TestMacOSRemoteInterface(t *testing.T) { + mock, err := mock.New("./testdata/macos.toml", &inventory.Asset{ + Platform: &inventory.Platform{ + Name: "macos", + }, + }) + require.NoError(t, err) + + ifaces := networkinterface.New(mock) + list, err := ifaces.Interfaces() + require.NoError(t, err) + assert.Equal(t, 17, len(list)) + inet := list[0] + assert.Equal(t, "lo0", inet.Name) + assert.Equal(t, 1, inet.Index) + assert.Equal(t, 16384, inet.MTU) + assert.Equal(t, "up|loopback|multicast", inet.Flags.String()) + assert.Equal(t, "", inet.HardwareAddr.String()) + assert.True(t, len(inet.Addrs) > 0) + assert.True(t, len(inet.MulticastAddrs) == 0) + + inetAdapter, err := ifaces.InterfaceByName("en0") + require.NoError(t, err) + assert.Equal(t, "en0", inetAdapter.Name) + assert.Equal(t, "8c:85:90:80:1b:e9", inetAdapter.HardwareAddr.String()) + + ip, err := networkinterface.HostIP(list) + require.NoError(t, err) + assert.Equal(t, "192.168.178.45", ip) +} + +func TestLinuxRemoteInterface(t *testing.T) { + mock, err := mock.New("./testdata/linux_remote.toml", &inventory.Asset{ + Platform: &inventory.Platform{ + Name: "linux", + Family: []string{"linux"}, + }, + }) + require.NoError(t, err) + + ifaces := networkinterface.New(mock) + list, err := ifaces.Interfaces() + require.NoError(t, err) + assert.True(t, len(list) == 2) + + inet, err := ifaces.InterfaceByName("lo") + require.NoError(t, err) + assert.Equal(t, "lo", inet.Name) + assert.Equal(t, 1, inet.Index) + assert.Equal(t, 0, inet.MTU) + assert.Equal(t, "up|loopback", inet.Flags.String()) + assert.Equal(t, "", inet.HardwareAddr.String()) + assert.True(t, len(inet.Addrs) == 2) + assert.True(t, len(inet.MulticastAddrs) == 0) + + inet, err = ifaces.InterfaceByName("eth0") + require.NoError(t, err) + assert.Equal(t, "eth0", inet.Name) + assert.Equal(t, 2, inet.Index) + assert.Equal(t, 0, inet.MTU) + assert.Equal(t, "up|broadcast", inet.Flags.String()) + assert.Equal(t, "", inet.HardwareAddr.String()) + assert.True(t, len(inet.Addrs) == 2) + assert.True(t, len(inet.MulticastAddrs) == 0) + + ip, err := networkinterface.HostIP(list) + require.NoError(t, err) + assert.Equal(t, "10.128.0.4", ip) +} + +func TestLinuxRemoteInterfaceFlannel(t *testing.T) { + mock, err := mock.New("./testdata/linux_flannel.toml", &inventory.Asset{ + Platform: &inventory.Platform{ + Name: "linux", + Family: []string{"linux"}, + }, + }) + require.NoError(t, err) + + ifaces := networkinterface.New(mock) + list, err := ifaces.Interfaces() + require.NoError(t, err) + assert.True(t, len(list) == 4) + + ip, err := networkinterface.HostIP(list) + require.NoError(t, err) + assert.Equal(t, "192.168.101.90", ip) +} diff --git a/providers/os/resources/networkinterface/testdata/linux.toml b/providers/os/resources/networkinterface/testdata/linux.toml new file mode 100644 index 0000000000..3600e42887 --- /dev/null +++ b/providers/os/resources/networkinterface/testdata/linux.toml @@ -0,0 +1,200 @@ +[commands."uname -s"] +stdout = "Linux" + +[commands."uname -m"] +stdout = "x86_64" + +[commands."uname -r"] +stdout = "4.9.125-linuxkit" + +[files."/etc/redhat-release"] +content = "CentOS Linux release 7.5.1804 (Core)" + +[files."/etc/centos-release"] +content = "CentOS Linux release 7.5.1804 (Core)" + +[files."/etc/os-release"] +content = """ +NAME="CentOS Linux" +VERSION="7 (Core)" +ID="centos" +ID_LIKE="rhel fedora" +VERSION_ID="7" +PRETTY_NAME="CentOS Linux 7 (Core)" +ANSI_COLOR="0;31" +CPE_NAME="cpe:/o:centos:centos:7" +HOME_URL="https://www.centos.org/" +BUG_REPORT_URL="https://bugs.centos.org/" + +CENTOS_MANTISBT_PROJECT="CentOS-7" +CENTOS_MANTISBT_PROJECT_VERSION="7" +REDHAT_SUPPORT_PRODUCT="centos" +REDHAT_SUPPORT_PRODUCT_VERSION="7" +""" + +# /sys/class/net/lo/netdev_group +# /sys/class/net/lo/addr_len +# /sys/class/net/lo/flags +# /sys/class/net/lo/gro_flush_timeout +# /sys/class/net/lo/mtu +# /sys/class/net/lo/dormant +# /sys/class/net/lo/link_mode +# /sys/class/net/lo/phys_port_id +# /sys/class/net/lo/duplex +# /sys/class/net/lo/dev_port +# /sys/class/net/lo/phys_switch_id +# /sys/class/net/lo/proto_down +# /sys/class/net/lo/operstate +# /sys/class/net/lo/broadcast +# /sys/class/net/lo/speed +# /sys/class/net/lo/addr_assign_type +# /sys/class/net/lo/type +# /sys/class/net/lo/tx_queue_len +# /sys/class/net/lo/carrier_changes +# /sys/class/net/lo/name_assign_type +# /sys/class/net/lo/ifalias +# /sys/class/net/lo/iflink +# /sys/class/net/lo/phys_port_name +# /sys/class/net/lo/dev_id +# /sys/class/net/lo/ifindex +# /sys/class/net/lo/address +# /sys/class/net/lo/uevent +# /sys/class/net/lo/carrier + +[files."/sys/class/net/lo/ifindex"] +content = """ +1 +""" + +[files."/sys/class/net/lo/address"] +content = """ +00:00:00:00:00:00 +""" + +[files."/sys/class/net/lo/mtu"] +content = """ +65536 +""" + +[files."/sys/class/net/lo/flags"] +content = """ +0x9 +""" + +[commands."/sbin/ip -br -4 address show dev lo"] +stdout = """ +lo UNKNOWN 127.0.0.1/8 +""" + +[commands."/sbin/ip -br -6 address show dev lo"] +stdout = "" + +# /sys/class/net/eth0/ +# /sys/class/net/eth0/netdev_group +# /sys/class/net/eth0/addr_len +# /sys/class/net/eth0/flags +# /sys/class/net/eth0/statistics +# /sys/class/net/eth0/statistics/rx_nohandler +# /sys/class/net/eth0/statistics/tx_fifo_errors +# /sys/class/net/eth0/statistics/rx_frame_errors +# /sys/class/net/eth0/statistics/rx_missed_errors +# /sys/class/net/eth0/statistics/collisions +# /sys/class/net/eth0/statistics/tx_aborted_errors +# /sys/class/net/eth0/statistics/tx_dropped +# /sys/class/net/eth0/statistics/tx_carrier_errors +# /sys/class/net/eth0/statistics/rx_crc_errors +# /sys/class/net/eth0/statistics/tx_errors +# /sys/class/net/eth0/statistics/tx_packets +# /sys/class/net/eth0/statistics/rx_compressed +# /sys/class/net/eth0/statistics/rx_fifo_errors +# /sys/class/net/eth0/statistics/tx_bytes +# /sys/class/net/eth0/statistics/rx_over_errors +# /sys/class/net/eth0/statistics/rx_length_errors +# /sys/class/net/eth0/statistics/rx_dropped +# /sys/class/net/eth0/statistics/rx_errors +# /sys/class/net/eth0/statistics/multicast +# /sys/class/net/eth0/statistics/tx_window_errors +# /sys/class/net/eth0/statistics/rx_packets +# /sys/class/net/eth0/statistics/tx_heartbeat_errors +# /sys/class/net/eth0/statistics/rx_bytes +# /sys/class/net/eth0/statistics/tx_compressed +# /sys/class/net/eth0/gro_flush_timeout +# /sys/class/net/eth0/mtu +# /sys/class/net/eth0/dormant +# /sys/class/net/eth0/subsystem +# /sys/class/net/eth0/link_mode +# /sys/class/net/eth0/phys_port_id +# /sys/class/net/eth0/power +# /sys/class/net/eth0/power/runtime_suspended_time +# /sys/class/net/eth0/power/autosuspend_delay_ms +# /sys/class/net/eth0/power/runtime_active_time +# /sys/class/net/eth0/power/control +# /sys/class/net/eth0/power/runtime_status +# /sys/class/net/eth0/duplex +# /sys/class/net/eth0/dev_port +# /sys/class/net/eth0/queues +# /sys/class/net/eth0/queues/tx-0 +# /sys/class/net/eth0/queues/tx-0/byte_queue_limits +# /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit_min +# /sys/class/net/eth0/queues/tx-0/byte_queue_limits/inflight +# /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit +# /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit_max +# /sys/class/net/eth0/queues/tx-0/byte_queue_limits/hold_time +# /sys/class/net/eth0/queues/tx-0/tx_timeout +# /sys/class/net/eth0/queues/tx-0/xps_cpus +# /sys/class/net/eth0/queues/tx-0/tx_maxrate +# /sys/class/net/eth0/queues/rx-0 +# /sys/class/net/eth0/queues/rx-0/rps_cpus +# /sys/class/net/eth0/queues/rx-0/rps_flow_cnt +# /sys/class/net/eth0/phys_switch_id +# /sys/class/net/eth0/proto_down +# /sys/class/net/eth0/operstate +# /sys/class/net/eth0/broadcast +# /sys/class/net/eth0/speed +# /sys/class/net/eth0/addr_assign_type +# /sys/class/net/eth0/type +# /sys/class/net/eth0/tx_queue_len +# /sys/class/net/eth0/carrier_changes +# /sys/class/net/eth0/name_assign_type +# /sys/class/net/eth0/ifalias +# /sys/class/net/eth0/iflink +# /sys/class/net/eth0/phys_port_name +# /sys/class/net/eth0/dev_id +# /sys/class/net/eth0/ifindex +# /sys/class/net/eth0/address +# /sys/class/net/eth0/uevent +# /sys/class/net/eth0/carrier + +[files."/sys/class/net/eth0/ifindex"] +content = """ +15 +""" + +[files."/sys/class/net/eth0/address"] +content = """ +02:42:ac:11:00:03 +""" + +[files."/sys/class/net/eth0/flags"] +content = """ +0x1003 +""" + + +[files."/sys/class/net/eth0/speed"] +content = """ +10000 +""" + +[files."/sys/class/net/eth0/mtu"] +content = """ +1500 +""" + +[commands."/sbin/ip -br -4 address show dev eth0"] +stdout = """ +eth0@if16 UP 172.17.0.3/16 +""" + +[commands."/sbin/ip -br -6 address show dev eth0"] +stdout = "" \ No newline at end of file diff --git a/providers/os/resources/networkinterface/testdata/linux_flannel.toml b/providers/os/resources/networkinterface/testdata/linux_flannel.toml new file mode 100644 index 0000000000..b96e78e77f --- /dev/null +++ b/providers/os/resources/networkinterface/testdata/linux_flannel.toml @@ -0,0 +1,35 @@ +[commands."uname -s"] +stdout = "Linux" + +[commands."uname -m"] +stdout = "x86_64" + +[commands."uname -r"] +stdout = "4.9.0-12-amd64" + +[files."/etc/debian_version"] +content = "9.12" + +[files."/etc/os-release"] +content = """ +PRETTY_NAME="Debian GNU/Linux 9 (stretch)" +NAME="Debian GNU/Linux" +VERSION_ID="9" +VERSION="9 (stretch)" +VERSION_CODENAME=stretch +ID=debian +HOME_URL="https://www.debian.org/" +SUPPORT_URL="https://www.debian.org/support" +BUG_REPORT_URL="https://bugs.debian.org/" +""" + +[commands."ip -o addr show"] +stdout = """ +1: lo inet 127.0.0.1/8 scope host lo\\ valid_lft forever preferred_lft forever +1: lo inet6 ::1/128 scope host \\ valid_lft forever preferred_lft forever +2: eth0 inet 192.168.101.90/16 brd 192.168.255.255 scope global dynamic eth0\\ valid_lft 3450sec preferred_lft 3450sec +2: eth0 inet6 fe80::8c8:ccff:fe75:486e/64 scope link \\ valid_lft forever preferred_lft forever +3: docker0 inet 172.17.0.1/16 scope global docker0\\ valid_lft forever preferred_lft forever +4: flannel.1 inet 10.244.0.0/32 scope global flannel.1\\ valid_lft forever preferred_lft forever +4: flannel.1 inet6 fe80::98b4:e7ff:fe1e:3c35/64 scope link \\ valid_lft forever preferred_lft forever +""" \ No newline at end of file diff --git a/providers/os/resources/networkinterface/testdata/linux_remote.toml b/providers/os/resources/networkinterface/testdata/linux_remote.toml new file mode 100644 index 0000000000..eae97ac7c9 --- /dev/null +++ b/providers/os/resources/networkinterface/testdata/linux_remote.toml @@ -0,0 +1,32 @@ +[commands."uname -s"] +stdout = "Linux" + +[commands."uname -m"] +stdout = "x86_64" + +[commands."uname -r"] +stdout = "4.9.0-12-amd64" + +[files."/etc/debian_version"] +content = "9.12" + +[files."/etc/os-release"] +content = """ +PRETTY_NAME="Debian GNU/Linux 9 (stretch)" +NAME="Debian GNU/Linux" +VERSION_ID="9" +VERSION="9 (stretch)" +VERSION_CODENAME=stretch +ID=debian +HOME_URL="https://www.debian.org/" +SUPPORT_URL="https://www.debian.org/support" +BUG_REPORT_URL="https://bugs.debian.org/" +""" + +[commands."ip -o addr show"] +stdout = """ +1: lo inet 127.0.0.1/8 scope host lo\\ valid_lft forever preferred_lft forever +1: lo inet6 ::1/128 scope host \\ valid_lft forever preferred_lft forever +2: eth0 inet 10.128.0.4/32 brd 10.128.0.4 scope global eth0\\ valid_lft forever preferred_lft forever +2: eth0 inet6 fe80::4001:aff:fe80:4/64 scope link \\ valid_lft forever preferred_lft forever +""" \ No newline at end of file diff --git a/providers/os/resources/networkinterface/testdata/macos.toml b/providers/os/resources/networkinterface/testdata/macos.toml new file mode 100644 index 0000000000..4ace72889e --- /dev/null +++ b/providers/os/resources/networkinterface/testdata/macos.toml @@ -0,0 +1,135 @@ +[commands."uname -s"] +stdout = "Darwin" + +[commands."uname -m"] +stdout = "x86_64" + +[commands."uname -r"] +stdout = "18.6.0" + +[commands."/usr/bin/sw_vers"] +stdout = """ +ProductName: Mac OS X +ProductVersion: 10.14.5 +BuildVersion: 18F132 +""" + +[files."/System/Library/CoreServices/SystemVersion.plist"] +content = """ + + + + + ProductBuildVersion + 18F132 + ProductCopyright + 1983-2019 Apple Inc. + ProductName + Mac OS X + ProductUserVisibleVersion + 10.14.5 + ProductVersion + 10.14.5 + iOSSupportVersion + 12.3 + + +""" + +[commands."ifconfig"] +stdout = """ +lo0: flags=8049 mtu 16384 + options=1203 + inet 127.0.0.1 netmask 0xff000000 + inet6 ::1 prefixlen 128 + inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 + inet 127.94.0.2 netmask 0xff000000 + inet 127.94.0.1 netmask 0xff000000 + nd6 options=201 +gif0: flags=8010 mtu 1280 +stf0: flags=0<> mtu 1280 +en0: flags=8863 mtu 1500 + options=400 + ether 8c:85:90:80:1b:e9 + inet6 fe80::14f6:9e9e:ca94:99d7%en0 prefixlen 64 secured scopeid 0x5 + inet 192.168.178.45 netmask 0xffffff00 broadcast 192.168.178.255 + nd6 options=201 + media: autoselect + status: active +p2p0: flags=8843 mtu 2304 + options=400 + ether 0e:85:90:80:1b:e9 + media: autoselect + status: inactive +awdl0: flags=8943 mtu 1484 + options=400 + ether 4a:77:1c:1d:a5:7a + inet6 fe80::4877:1cff:fe1d:a57a%awdl0 prefixlen 64 scopeid 0x7 + nd6 options=201 + media: autoselect + status: active +llw0: flags=8863 mtu 1500 + options=400 + ether 4a:77:1c:1d:a5:7a + inet6 fe80::4877:1cff:fe1d:a57a%llw0 prefixlen 64 scopeid 0x8 + nd6 options=201 + media: autoselect + status: active +bridge0: flags=8863 mtu 1500 + options=63 + ether 82:44:91:6b:30:01 + Configuration: + id 0:0:0:0:0:0 priority 0 hellotime 0 fwddelay 0 + maxage 0 holdcnt 0 proto stp maxaddr 100 timeout 1200 + root id 0:0:0:0:0:0 priority 0 ifcost 0 port 0 + ipfilter disabled flags 0x2 + member: en1 flags=3 + ifmaxaddr 0 port 10 priority 0 path cost 0 + member: en2 flags=3 + ifmaxaddr 0 port 11 priority 0 path cost 0 + member: en3 flags=3 + ifmaxaddr 0 port 12 priority 0 path cost 0 + member: en4 flags=3 + ifmaxaddr 0 port 13 priority 0 path cost 0 + nd6 options=201 + media: + status: inactive +en1: flags=8963 mtu 1500 + options=460 + ether 82:44:91:6b:30:01 + media: autoselect + status: inactive +en2: flags=8963 mtu 1500 + options=460 + ether 82:44:91:6b:30:00 + media: autoselect + status: inactive +en3: flags=8963 mtu 1500 + options=460 + ether 82:44:91:6b:30:05 + media: autoselect + status: inactive +en4: flags=8963 mtu 1500 + options=460 + ether 82:44:91:6b:30:04 + media: autoselect + status: inactive +en5: flags=8863 mtu 1500 + ether ac:de:48:00:11:22 + inet6 fe80::aede:48ff:fe00:1122%en5 prefixlen 64 scopeid 0x4 + nd6 options=201 + media: autoselect + status: active +utun0: flags=8051 mtu 1380 + inet6 fe80::40ab:25ad:ec90:75a2%utun0 prefixlen 64 scopeid 0xe + nd6 options=201 +utun1: flags=8051 mtu 2000 + inet6 fe80::f263:275a:39b3:361f%utun1 prefixlen 64 scopeid 0xf + nd6 options=201 +utun2: flags=8051 mtu 1380 + inet6 fe80::c8e1:e924:c5ea:316b%utun2 prefixlen 64 scopeid 0x10 + nd6 options=201 +utun3: flags=8051 mtu 1380 + inet6 fe80::a445:d94b:5379:81d0%utun3 prefixlen 64 scopeid 0x11 + nd6 options=201 +""" \ No newline at end of file diff --git a/providers/os/resources/networkinterface/testdata/windows.toml b/providers/os/resources/networkinterface/testdata/windows.toml new file mode 100644 index 0000000000..cc8dcc9f95 --- /dev/null +++ b/providers/os/resources/networkinterface/testdata/windows.toml @@ -0,0 +1,56 @@ +[commands."wmic os get * /format:csv"] +stdout = """Node,BootDevice,BuildNumber,BuildType,Caption,CodeSet,CountryCode,CreationClassName,CSCreationClassName,CSDVersion,CSName,CurrentTimeZone,DataExecutionPrevention_32BitApplications,DataExecutionPrevention_Available,DataExecutionPrevention_Drivers,DataExecutionPrevention_SupportPolicy,Debug,Description,Distributed,EncryptionLevel,ForegroundApplicationBoost,FreePhysicalMemory,FreeSpaceInPagingFiles,FreeVirtualMemory,InstallDate,LargeSystemCache,LastBootUpTime,LocalDateTime,Locale,Manufacturer,MaxNumberOfProcesses,MaxProcessMemorySize,MUILanguages,Name,NumberOfLicensedUsers,NumberOfProcesses,NumberOfUsers,OperatingSystemSKU,Organization,OSArchitecture,OSLanguage,OSProductSuite,OSType,OtherTypeDescription,PAEEnabled,PlusProductID,PlusVersionNumber,PortableOperatingSystem,Primary,ProductType,RegisteredUser,SerialNumber,ServicePackMajorVersion,ServicePackMinorVersion,SizeStoredInPagingFiles,Status,SuiteMask,SystemDevice,SystemDirectory,SystemDrive,TotalSwapSpaceSize,TotalVirtualMemorySize,TotalVisibleMemorySize,Version,WindowsDirectory +VAGRANT,\\Device\\HarddiskVolume1,17763,Multiprocessor Free,Microsoft Windows Server 2019 Datacenter Evaluation,1252,1,Win32_OperatingSystem,Win32_ComputerSystem,,VAGRANT,-420,TRUE,TRUE,TRUE,3,FALSE,,FALSE,256,2,721716,979372,1922780,20190906065515.000000-420,,20190908011749.580533-420,20190908042731.608000-420,0409,Microsoft Corporation,4294967295,137438953344,{en-US},Microsoft Windows Server 2019 Datacenter Evaluation|C:\\Windows|\\Device\\Harddisk0\\Partition2,0,69,1,80,Vagrant,64-bit,1033,400,18,,,,,FALSE,TRUE,3,,00431-20000-00000-AA838,0,0,1179648,OK,400,\\Device\\HarddiskVolume2,C:\\Windows\\system32,C:,,3276340,2096692,10.0.17763,C:\\Windows +""" + +[commands."powershell -c \"Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion' -Name CurrentBuild, UBR, EditionID | ConvertTo-Json\""] +stdout=""" +{ + "CurrentBuild": "17763", + "EditionID": "ServerDatacenterEval", + "UBR": 720 +} +""" + +[commands."powershell -c \"Get-NetAdapter | Select-Object -Property Name, ifIndex, InterfaceType, InterfaceDescription, Status, State, MacAddress, LinkSpeed, ReceiveLinkSpeed, TransmitLinkSpeed, Virtual | ConvertTo-Json\""] +stdout = """ +{ + "Name": "Ethernet", + "ifIndex": 6, + "InterfaceType": 6, + "InterfaceDescription": "Microsoft Hyper-V Network Adapter", + "Status": "Up", + "State": 2, + "MacAddress": "00-15-5D-F2-3B-1D", + "LinkSpeed": "866.5 Mbps", + "ReceiveLinkSpeed": 866500000, + "TransmitLinkSpeed": 866500000, + "Virtual": false +} +""" + +[commands."powershell -c \"Get-NetIPAddress | Select-Object -Property IPv6Address, IPv4Address, InterfaceAlias | ConvertTo-Json\""] +stdout = """ +[ + { + "IPv6Address": "fe80::ed94:1267:afb5:bb76%6", + "IPv4Address": null, + "InterfaceAlias": "Ethernet" + }, + { + "IPv6Address": "::1", + "IPv4Address": null, + "InterfaceAlias": "Loopback Pseudo-Interface 1" + }, + { + "IPv6Address": null, + "IPv4Address": "192.168.178.112", + "InterfaceAlias": "Ethernet" + }, + { + "IPv6Address": null, + "IPv4Address": "127.0.0.1", + "InterfaceAlias": "Loopback Pseudo-Interface 1" + } +] +""" \ No newline at end of file