From 0926c738d339c8948bbddb0d4aef95e96014acb3 Mon Sep 17 00:00:00 2001 From: Julien Vehent Date: Fri, 12 Sep 2014 17:14:31 -0400 Subject: [PATCH] [major] rewrite of connected module in netstat module, support all OSes --- conf/mig-agent-conf.go.inc | 2 +- examples/actions/example_v2.json | 31 ++- src/mig/clients/console/available_modules.go | 2 +- src/mig/modules/connected/connected.go | 171 ------------ src/mig/modules/netstat/netstat.go | 277 +++++++++++++++++++ src/mig/modules/netstat/netstat_darwin.go | 63 +++++ src/mig/modules/netstat/netstat_linux.go | 61 ++++ src/mig/modules/netstat/netstat_windows.go | 63 +++++ 8 files changed, 493 insertions(+), 177 deletions(-) delete mode 100644 src/mig/modules/connected/connected.go create mode 100644 src/mig/modules/netstat/netstat.go create mode 100644 src/mig/modules/netstat/netstat_darwin.go create mode 100644 src/mig/modules/netstat/netstat_linux.go create mode 100644 src/mig/modules/netstat/netstat_windows.go diff --git a/conf/mig-agent-conf.go.inc b/conf/mig-agent-conf.go.inc index 7b5f105e..1ba194b0 100644 --- a/conf/mig-agent-conf.go.inc +++ b/conf/mig-agent-conf.go.inc @@ -10,7 +10,7 @@ import( "time" _ "mig/modules/filechecker" - _ "mig/modules/connected" + _ "mig/modules/netstat" _ "mig/modules/upgrade" _ "mig/modules/agentdestroy" _ "mig/modules/example" diff --git a/examples/actions/example_v2.json b/examples/actions/example_v2.json index 57bcca48..372affca 100644 --- a/examples/actions/example_v2.json +++ b/examples/actions/example_v2.json @@ -126,11 +126,34 @@ } }, { - "module": "connected", + "module": "netstat", "parameters": { - "check for connected IPs": [ - "98.143.145.80", - "96.46.4.237" + "maclocal": [ + "8c:70:5a:c8:be:50" + ], + "macpeer": [ + "30:05:5c:00:80:3a" + ], + "cidrlocal": [ + "10.1.2.3/32", + "fe80::8e70:5aff:fec8:be50/32" + ], + "cidrpeer": [ + "98.143.145.80/32", + "96.46.4.0/24", + "FE80:0000:0000:0000:0202:B3FF:FE1E:8329/128" + ], + "udplisten": [ + "0.0.0.0:53" + ], + "udppeer": [ + "192.168.1.2->0.0.0.0:53" + ], + "tcplisten": [ + "10.1.2.3:443" + ], + "tcppeer": [ + "18.32.25.65->10.1.2.3:80" ] } }, diff --git a/src/mig/clients/console/available_modules.go b/src/mig/clients/console/available_modules.go index 076c34c7..5db9bfa0 100644 --- a/src/mig/clients/console/available_modules.go +++ b/src/mig/clients/console/available_modules.go @@ -7,7 +7,7 @@ package main import ( _ "mig/modules/agentdestroy" - _ "mig/modules/connected" _ "mig/modules/filechecker" + _ "mig/modules/netstat" _ "mig/modules/upgrade" ) diff --git a/src/mig/modules/connected/connected.go b/src/mig/modules/connected/connected.go deleted file mode 100644 index 7b3e6148..00000000 --- a/src/mig/modules/connected/connected.go +++ /dev/null @@ -1,171 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. -// -// Contributor: Julien Vehent jvehent@mozilla.com [:ulfr] - -// Connected is a module that looks for IP addresses currently connected -// to the system. It does so by reading conntrack data on Linux. MacOS and -// Windows are not yet implemented. -package connected - -import ( - "bufio" - "encoding/json" - "fmt" - "mig" - "os" - "regexp" - "runtime" - "strings" -) - -func init() { - mig.RegisterModule("connected", func() interface{} { - return new(Runner) - }) -} - -type Runner struct { - Parameters params - Results results - conns params -} - -type params map[string][]string - -type results struct { - FoundAnything bool `json:"foundanything"` - Elements map[string]map[string]singleresult `json:"elements,omitempty"` - Errors []string `json:"errors,omitempty"` - Statistics statistics `json:"statistics,omitempty"` -} - -type statistics struct { - OpenFailed int `json:"openfailed"` - TotalConn int `json:"totalconn"` -} - -// singleresult contains information on the result of a single test -type singleresult struct { - MatchCount int `json:"matchcount,omitempty"` - Connections []string `json:"connections,omitempty"` -} - -func newResults() *results { - return &results{Elements: make(map[string]map[string]singleresult), FoundAnything: false} -} - -// Validate ensures that the parameters contain valid IPv4 addresses -func (r Runner) ValidateParameters() (err error) { - for _, values := range r.Parameters { - for _, value := range values { - ipre := regexp.MustCompile(`\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b`) - if !ipre.MatchString(value) { - return fmt.Errorf("Parameter '%s' is not a valid IP", value) - } - } - } - return -} - -func (r Runner) Run(args []byte) string { - err := json.Unmarshal(args, &r.Parameters) - if err != nil { - r.Results.Errors = append(r.Results.Errors, fmt.Sprintf("%v", err)) - return r.buildResults() - } - - err = r.ValidateParameters() - if err != nil { - r.Results.Errors = append(r.Results.Errors, fmt.Sprintf("%v", err)) - return r.buildResults() - } - - switch runtime.GOOS { - case "linux": - r.conns = r.checkLinuxConnectedIPs() - default: - panic("OS not supported") - } - return r.buildResults() -} - -// checkLinuxConnectedIPs checks the content of /proc/net/ip_conntrack -// and /proc/net/nf_conntrack -func (r Runner) checkLinuxConnectedIPs() map[string][]string { - var list []string - connections := make(map[string][]string) - for _, ips := range r.Parameters { - for _, newIP := range ips { - addit := true - for _, ip := range list { - if newIP == ip { - addit = false - } - } - if addit { - list = append(list, newIP) - } - } - } - // TODO: read connection data from /proc/net/{tcp,udp} instead - sources := []string{"/proc/net/ip_conntrack", "/proc/net/nf_conntrack"} - for _, srcfile := range sources { - // check those regexes against conntrack - fd, err := os.Open(srcfile) - if err != nil { - r.Results.Statistics.OpenFailed++ - } - defer fd.Close() - scanner := bufio.NewScanner(fd) - for scanner.Scan() { - if err := scanner.Err(); err != nil { - panic(err) - } - for _, ip := range list { - if strings.Contains(scanner.Text(), ip) { - connections[ip] = append(connections[ip], scanner.Text()) - } - } - r.Results.Statistics.TotalConn++ - } - } - return connections -} - -// buildResults transforms the connectedIPs map into a Results -// map that is serialized in JSON and returned as a string -func (r Runner) buildResults() string { - results := newResults() - results.Errors = r.Results.Errors - results.Statistics = r.Results.Statistics - for ip, lines := range r.conns { - // find mapping between IP and test name, and store the result - for name, testips := range r.Parameters { - for _, testip := range testips { - if testip == ip { - if _, ok := results.Elements[name]; !ok { - results.Elements[name] = map[string]singleresult{ - ip: singleresult{ - MatchCount: len(lines), - Connections: lines, - }, - } - } else { - results.Elements[name][ip] = singleresult{ - MatchCount: len(lines), - Connections: lines, - } - } - } - } - } - results.FoundAnything = true - } - jsonOutput, err := json.Marshal(*results) - if err != nil { - panic(err) - } - return string(jsonOutput[:]) -} diff --git a/src/mig/modules/netstat/netstat.go b/src/mig/modules/netstat/netstat.go new file mode 100644 index 00000000..014c0646 --- /dev/null +++ b/src/mig/modules/netstat/netstat.go @@ -0,0 +1,277 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Contributor: Julien Vehent jvehent@mozilla.com [:ulfr] + +// netstat is a module that retrieves network information about the endpoint, +// such as mac addresses, local and connected IPs, listening TCP and UDP +// sockets and peers +package netstat + +import ( + "encoding/json" + "fmt" + "mig" + "net" + "regexp" + "strconv" + "strings" +) + +func init() { + mig.RegisterModule("netstat", func() interface{} { + return new(Runner) + }) +} + +type Runner struct { + Parameters params + Results results +} + +type params struct { + LocalMAC []string `json:"localmac,omitempty"` + LocalIP []string `json:"localip,omitempty"` + NeighborMAC []string `json:"neighbormac,omitempty"` + NeighborIP []string `json:"neighborip,omitempty"` + ConnectedIP []string `json:"connectedip,omitempty"` + ListeningPort []string `json:"listeningport,omitempty"` +} + +type results struct { + LocalMAC []result `json:"localmac,omitempty"` + LocalIP []result `json:"localip,omitempty"` + NeighborMAC []result `json:"neighbormac,omitempty"` + NeighborIP []result `json:"neighborip,omitempty"` + ConnectedIP []result `json:"connectedip,omitempty"` + ListeningPort []result `json:"listeningport,omitempty"` + FoundAnything bool `json:"foundanything"` + Success bool `json:"success"` + Errors []string `json:"errors,omitempty"` +} + +type result struct { + Item string `json:"item"` + Found bool `json:"found"` + LocalMACAddr string `json:"localmacaddr,omitempty"` + RemoteMACAddr string `json:"remotemacaddr,omitempty"` + LocalAddr string `json:"localaddr,omitempty"` + LocalPort string `json:"localport,omitempty"` + RemoteAddr string `json:"remoteaddr,omitempty"` + RemotePort string `json:"remoteport,omitempty"` +} + +func (r Runner) ValidateParameters() (err error) { + for _, val := range r.Parameters.LocalMAC { + err = validateMAC(val) + if err != nil { + return + } + } + for _, val := range r.Parameters.NeighborMAC { + err = validateMAC(val) + if err != nil { + return + } + } + for _, val := range r.Parameters.LocalIP { + err = validateIP(val) + if err != nil { + return + } + } + for _, val := range r.Parameters.ConnectedIP { + err = validateIP(val) + if err != nil { + return + } + } + for _, val := range r.Parameters.ListeningPort { + err = validatePort(val) + if err != nil { + return + } + } + return +} + +func validateMAC(regex string) (err error) { + _, err = regexp.Compile(regex) + if err != nil { + return fmt.Errorf("Invalid MAC regexp '%s'. Compilation failed with '%v'. Must be a valid regular expression.", regex, err) + } + return +} + +// if a '/' is found, validate as CIDR, otherwise validate as IP +func validateIP(val string) error { + if strings.IndexAny(val, "/") > 0 { + _, _, err := net.ParseCIDR(val) + if err != nil { + return fmt.Errorf("invalid IPv{4,6} CIDR %s: %v. Must be an IP or a CIDR.", val, err) + } + return nil + } + ip := net.ParseIP(val) + if ip == nil { + return fmt.Errorf("invalid IPv{4,6} %s. Must be an IP or a CIDR.", val) + } + return nil +} + +func validatePort(val string) error { + port, err := strconv.Atoi(val) + if err != nil { + return err + } + if port < 0 || port > 65535 { + return fmt.Errorf("port out of range. must be between 1 and 65535") + } + return nil +} + +func (r Runner) Run(args []byte) (resStr string) { + defer func() { + if e := recover(); e != nil { + // return error in json + r.Results.Success = false + r.Results.Errors = append(r.Results.Errors, fmt.Sprintf("%v", e)) + resJson, _ := json.Marshal(r.Results) + resStr = string(resJson[:]) + return + } + }() + + err := json.Unmarshal(args, &r.Parameters) + if err != nil { + panic(err) + } + + err = r.ValidateParameters() + if err != nil { + panic(err) + } + for _, val := range r.Parameters.LocalMAC { + var result result + result.Item = val + result.Found, result.LocalMACAddr, err = HasLocalMAC(val) + if err != nil { + r.Results.Errors = append(r.Results.Errors, fmt.Sprintf("%v", err)) + } + r.Results.LocalMAC = append(r.Results.LocalMAC, result) + if result.Found { + r.Results.FoundAnything = true + } + } + for _, val := range r.Parameters.NeighborMAC { + var result result + result.Item = val + result.Found, result.RemoteMACAddr, result.RemoteAddr, err = HasSeenMac(val) + if err != nil { + r.Results.Errors = append(r.Results.Errors, fmt.Sprintf("%v", err)) + } + r.Results.NeighborMAC = append(r.Results.NeighborMAC, result) + if result.Found { + r.Results.FoundAnything = true + } + } + for _, val := range r.Parameters.LocalIP { + var result result + result.Item = val + result.Found, result.LocalAddr, err = HasLocalIP(val) + if err != nil { + r.Results.Errors = append(r.Results.Errors, fmt.Sprintf("%v", err)) + } + r.Results.LocalIP = append(r.Results.LocalIP, result) + if result.Found { + r.Results.FoundAnything = true + } + } + for _, val := range r.Parameters.ConnectedIP { + result, err := HasIPConnected(val) + if err != nil { + r.Results.Errors = append(r.Results.Errors, fmt.Sprintf("%v", err)) + } + r.Results.ConnectedIP = append(r.Results.ConnectedIP, result) + if result.Found { + r.Results.FoundAnything = true + } + } + for _, val := range r.Parameters.ListeningPort { + result, err := HasListeningPort(val) + if err != nil { + r.Results.Errors = append(r.Results.Errors, fmt.Sprintf("%v", err)) + } + r.Results.ListeningPort = append(r.Results.ListeningPort, result) + if result.Found { + r.Results.FoundAnything = true + } + } + + r.Results.Success = true + jsonOutput, err := json.Marshal(r.Results) + if err != nil { + panic(err) + } + resStr = string(jsonOutput[:]) + return +} + +// HasLocalMac compares an input mac address with the mac addresses +// of the local interfaces, and returns found=true when found +func HasLocalMAC(macstr string) (found bool, addr string, err error) { + found = false + re, err := regexp.Compile("(?i)" + macstr) + if err != nil { + return found, addr, err + } + ifaces, err := net.Interfaces() + if err != nil { + return found, addr, err + } + for _, iface := range ifaces { + if re.MatchString(iface.HardwareAddr.String()) { + found = true + addr = iface.HardwareAddr.String() + return found, addr, err + } + } + return found, addr, err +} + +// HasLocalIP compares an input ip address with the ip addresses +// of the local interfaces, and returns found=true when found +func HasLocalIP(ipStr string) (found bool, addr string, err error) { + found = false + if strings.IndexAny(ipStr, "/") > 0 { + _, ipnet, err := net.ParseCIDR(ipStr) + if err != nil { + return found, addr, err + } + ifaceAddrs, err := net.InterfaceAddrs() + if err != nil { + return found, addr, err + } + for _, ifaceAddr := range ifaceAddrs { + addr = strings.Split(ifaceAddr.String(), "/")[0] + if ipnet.Contains(net.ParseIP(addr)) { + found = true + return found, addr, err + } + } + return found, addr, err + } + ifaceAddrs, err := net.InterfaceAddrs() + if err != nil { + return found, addr, err + } + for _, ifaceAddr := range ifaceAddrs { + addr = strings.Split(ifaceAddr.String(), "/")[0] + if ipStr == addr { + found = true + return found, addr, err + } + } + return found, addr, err +} diff --git a/src/mig/modules/netstat/netstat_darwin.go b/src/mig/modules/netstat/netstat_darwin.go new file mode 100644 index 00000000..e7da3d42 --- /dev/null +++ b/src/mig/modules/netstat/netstat_darwin.go @@ -0,0 +1,63 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Contributor: Julien Vehent jvehent@mozilla.com [:ulfr] +package netstat + +import ( + "bufio" + "bytes" + "fmt" + "os/exec" + "regexp" + "runtime" + "strings" +) + +// HasSeenMac on darwin looks at the output of `arp -a` for a matching mac address +// and returns its MAC and IP address if found +func HasSeenMac(val string) (found bool, macaddr, addr string, err error) { + found = false + out, err := exec.Command("arp", "-a").Output() + if err != nil { + return found, macaddr, addr, err + } + // arp -a has a static format: + // () at on [ifscope ] + // fedbox (172.21.0.3) at 8c:70:5a:c8:be:50 on en1 ifscope [ethernet] + re, err := regexp.Compile(val) + if err != nil { + return found, macaddr, addr, err + } + buf := bytes.NewReader(out) + reader := bufio.NewReader(buf) + for { + lineBytes, _, err := reader.ReadLine() + line := fmt.Sprintf("%s", lineBytes) + fields := strings.Fields(line) + if len(fields) < 4 { + continue + } + if re.MatchString(fields[3]) { + found = true + // remove heading and trailing parenthesis + if len(fields[1]) > 2 { + addr = fields[1][1 : len(fields[1])-1] + } + macaddr = fields[3] + return found, macaddr, addr, err + } + } + return +} + +func HasIPConnected(val string) (r result, err error) { + err = fmt.Errorf("HasIPConnected() is not implemented on %s", runtime.GOOS) + return +} + +func HasListeningPort(val string) (r result, err error) { + err = fmt.Errorf("HasListeningPort() is not implemented on %s", runtime.GOOS) + return +} diff --git a/src/mig/modules/netstat/netstat_linux.go b/src/mig/modules/netstat/netstat_linux.go new file mode 100644 index 00000000..c0b7994e --- /dev/null +++ b/src/mig/modules/netstat/netstat_linux.go @@ -0,0 +1,61 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Contributor: Julien Vehent jvehent@mozilla.com [:ulfr] +package netstat + +import ( + "bufio" + "fmt" + "os" + "regexp" + "runtime" + "strings" +) + +// HasSeenMac on linux looks for a matching mac address in /proc/net/arp +// and returns its MAC and IP address if found +func HasSeenMac(val string) (found bool, macaddr, addr string, err error) { + found = false + fd, err := os.Open("/proc/net/arp") + if err != nil { + return found, macaddr, addr, err + } + // /proc/net/arp has a static format: + // IP address HW type Flags HW address Mask Device + // we split the string on fields, and compare field #4 with our search regex + re, err := regexp.Compile(val) + if err != nil { + return found, macaddr, addr, err + } + scanner := bufio.NewScanner(fd) + scanner.Scan() // skip the header + for scanner.Scan() { + if err := scanner.Err(); err != nil { + panic(err) + } + fields := strings.Fields(scanner.Text()) + if len(fields) < 4 { + continue + } + if re.MatchString(fields[3]) { + found = true + addr = fields[0] + macaddr = fields[3] + return found, macaddr, addr, err + } + } + fd.Close() + return +} + +func HasIPConnected(val string) (r result, err error) { + err = fmt.Errorf("HasIPConnected() is not implemented on %s", runtime.GOOS) + return +} + +func HasListeningPort(val string) (r result, err error) { + err = fmt.Errorf("HasListeningPort() is not implemented on %s", runtime.GOOS) + return +} diff --git a/src/mig/modules/netstat/netstat_windows.go b/src/mig/modules/netstat/netstat_windows.go new file mode 100644 index 00000000..bc5402aa --- /dev/null +++ b/src/mig/modules/netstat/netstat_windows.go @@ -0,0 +1,63 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Contributor: Julien Vehent jvehent@mozilla.com [:ulfr] +package netstat + +import ( + "bufio" + "bytes" + "fmt" + "os/exec" + "regexp" + "runtime" + "strings" +) + +// HasSeenMac on windows looks at the output of `arp -a` for a matching mac address +// and returns its MAC and IP address if found +func HasSeenMac(val string) (found bool, macaddr, addr string, err error) { + found = false + out, err := exec.Command("arp", "-a").Output() + if err != nil { + return found, macaddr, addr, err + } + // arp -a has a static format: + // ) + // fedbox (172.21.0.3) at 8c:70:5a:c8:be:50 on en1 ifscope [ethernet] + re, err := regexp.Compile(val) + if err != nil { + return found, macaddr, addr, err + } + buf := bytes.NewReader(out) + reader := bufio.NewReader(buf) + for { + lineBytes, _, err := reader.ReadLine() + line := fmt.Sprintf("%s", lineBytes) + fields := strings.Fields(line) + if len(fields) < 3 { + continue + } + // match against a second variable with '-' characters converted to ':' + // because windows likes to display mac address as 8c-70-5a-c8-be-50 + convertedMac := strings.Replace(fields[1], "-", ":", 5) + if re.MatchString(fields[1]) || re.MatchString(convertedMac) { + found = true + addr = fields[0] + macaddr = convertedMac + return found, macaddr, addr, err + } + } + return +} + +func HasIPConnected(val string) (r result, err error) { + err = fmt.Errorf("HasIPConnected() is not implemented on %s", runtime.GOOS) + return +} + +func HasListeningPort(val string) (r result, err error) { + err = fmt.Errorf("HasListeningPort() is not implemented on %s", runtime.GOOS) + return +}