// Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may not // use this file except in compliance with the License. A copy of the // License is located at // // http://aws.amazon.com/apache2.0/ // // or in the "license" file accompanying this file. This file is distributed // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, // either express or implied. See the License for the specific language governing // permissions and limitations under the License. //go:build windows // +build windows // Package network contains a network gatherer. package network import ( "encoding/json" "fmt" "os/exec" "regexp" "strings" "github.com/aws/amazon-ssm-agent/agent/appconfig" "github.com/aws/amazon-ssm-agent/agent/context" "github.com/aws/amazon-ssm-agent/agent/jsonutil" "github.com/aws/amazon-ssm-agent/agent/log" "github.com/aws/amazon-ssm-agent/agent/plugins/inventory/model" ) var validIPV4Address *regexp.Regexp var cmd = appconfig.PowerShellPluginCommandName const ( cmdArgsToGetFullDetailsForGivenMacAddress = `Get-wmiobject -class Win32_NetworkAdapterConfiguration | where-object {$_.MACAddress -eq "%s"} | Select-object @{Name="IPAddresses";Expression={$_.IPAddress}}, @{Name="DefaultIPGateway";Expression={$_.DefaultIPGateway}}, @{Name="MacAddress";Expression={$_.MACAddress}}, @{Name="DHCPServer";Expression={$_.DHCPServer}}, @{Name="DNSServers";Expression={$_.DNSServerSearchOrder}} ,@{Name="IPSubnet";Expression={$_.IPSubnet}} | ConvertTo-Json` //We list only ethernet & wireless type of network interfaces. For more details refer to https://msdn.microsoft.com/en-us/library/aa394217%28v=vs.85%29.aspx cmdArgsToGetListAllInterfaces = `Get-wmiobject -class Win32_NetworkAdapter | where-object {$_.AdapterTypeID -eq 0 -or $_.AdapterTypeID -eq 9} | Select-object @{Name="MACAddress";Expression={$_.MACAddress}}, @{Name="Description";Expression={$_.Description}}, @{Name="ProductName";Expression={$_.ProductName}}| ConvertTo-Json` regexForIpV4Addresses = `^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$` ) func init() { validIPV4Address = regexp.MustCompile(regexForIpV4Addresses) } // NetworkInterfaceConfiguration captures advanced info related to a network interface that we get from // Win32_NetworkAdapterConfiguration class in windows. // // Note: Since Go doesn't provide an easy way to get information like DHCP server address, DNS server address, Default IP // Gateway, Subnet Mask for a given network interface - we use powershell command in windows to get that data using // Win32_NetworkAdapterConfiguration class - https://msdn.microsoft.com/en-us/library/aa394217%28v=vs.85%29.aspx // // Following fields are of interest to us: // - DHCPServer (type string) // - DNSServerSearchOrder (type string array) // - DefaultIPGateway (type string array) // - IPSubnet (type string array) // // In order to successfully read the data - certain fields are defined as interface{}. That's because if there is only // 1 item in the string array - the command output shows the json object as a string instead of an array. If there are // multiple entries in the array then the data shows up as a jsonMap with fields similar to Format. // // Since inventory.NetworkData supports only string attributes, we only pick the 1st entry in string array. // E.g: If there are 2 DefaultIPGateway - ['a.b.c.d','a.b.c.e'] -> we will show 'a.b.c.d' as Gateway in // inventory.NetworkData for that interface. This could change in future. // TODO: current implementation only allows 1 dns server address, gateway address per network interface // however, this may not be the case in reality. We might change this behavior to allow comma separated values or even // have them as an array. type NetworkInterfaceConfiguration struct { MacAddress string DHCPServer string IPAddresses interface{} DNSServers interface{} IPSubnet interface{} DefaultIPGateway interface{} } // Format captures fields required for parsing windows command output. // A string array of wmi class - when converted to json in powershell shows the fields mentioned here. type Format struct { Count int Value []string } // NwInterface captures all relevant network interfaces from Win32_NetworkAdapter class in windows. // // NOTE: amazon-ssm-agent uses GoLang 1.5 version. There is a known issue in net package where // interface.HardwareAddr.String() appends 00:00 at the end on windows platform. Until we move to version > 1.6, we will // use Win32_NetworkAdapter class to get information regarding all network interfaces in windows. For more details // please refer to following link: https://msdn.microsoft.com/en-us/library/aa394216%28v=vs.85%29.aspx type NwInterface struct { MACAddress string Description string ProductName string } // decoupling exec.Command for easy testability var cmdExecutor = executeCommand func executeCommand(command string, args ...string) ([]byte, error) { return exec.Command(command, args...).CombinedOutput() } func convertToNetworkData(nwInterface NwInterface) (basicNwData model.NetworkData) { basicNwData.Name = nwInterface.ProductName basicNwData.MacAddress = nwInterface.MACAddress return } // CollectNetworkData collects network information for all relevant network interfaces in windows using powershell func CollectNetworkData(context context.T) (data []model.NetworkData) { var output, dataB []byte var err error var singleInterface NwInterface var multipleInterfaces []NwInterface log := context.Log() log.Infof("Collecting all networking interfaces by executing command:\n%v %v", cmd, cmdArgsToGetListAllInterfaces) if output, err = cmdExecutor(cmd, cmdArgsToGetListAllInterfaces); err == nil { cmdOutput := string(output) log.Debugf("Command output: %v", cmdOutput) //windows command can either return a single network interface or an array of network interfaces if err = json.Unmarshal(output, &singleInterface); err == nil { data = append(data, convertToNetworkData(singleInterface)) } else if err = json.Unmarshal(output, &multipleInterfaces); err == nil { for _, nwInterface := range multipleInterfaces { data = append(data, convertToNetworkData(nwInterface)) } } else { log.Infof("Unable to get network interface info because of unexpected command output - %v", cmdOutput) return } dataB, _ = json.Marshal(data) log.Debugf("Basic network interface data collected so far: %v", jsonutil.Indent(string(dataB))) //collecting advanced network information for those interfaces data = GetAdvancedNetworkData(context, data) } else { log.Debugf("Failed to execute command : %v %v with error - %v", cmd, cmdArgsToGetListAllInterfaces, err.Error()) log.Errorf("Command failed with error: %v", string(output)) log.Infof("Unable to get network data on windows platform") } return } // GetAdvancedNetworkData returns advanced network information in windows platform using powershell commands func GetAdvancedNetworkData(context context.T, data []model.NetworkData) []model.NetworkData { var dataB []byte var modifiedData []model.NetworkData log := context.Log() for _, datum := range data { dataB, _ = json.Marshal(datum) log.Debugf("Network interface information of - %v: \n%v", datum.Name, string(dataB)) datum = GetNetworkDataUsingPowershell(context, datum) modifiedData = append(modifiedData, datum) } dataB, _ = json.Marshal(modifiedData) log.Debugf("Modified Network Interface information - %v", string(dataB)) return modifiedData } // GetNetworkDataUsingPowershell gets network data by executing powershell command func GetNetworkDataUsingPowershell(context context.T, networkData model.NetworkData) model.NetworkData { var dataB, output []byte var err error log := context.Log() commandArgs := fmt.Sprintf(cmdArgsToGetFullDetailsForGivenMacAddress, networkData.MacAddress) log.Debugf("Powershell command being run - %v", commandArgs) log.Infof("Executing command: %v %v", cmd, commandArgs) if output, err = cmdExecutor(cmd, commandArgs); err == nil { cmdOutput := string(output) log.Debugf("Command output: %v", cmdOutput) if networkData, err = EditNetworkData(log, networkData, cmdOutput); err == nil { dataB, _ = json.Marshal(networkData) log.Debugf("Modified Network Interface information - %v", string(dataB)) } else { log.Errorf("Unable to add further information to network data because of error - %v", err.Error()) log.Infof("No modification to network data") } } else { log.Debugf("Failed to execute command : %v %v with error - %v", cmd, commandArgs, err.Error()) log.Errorf("Command failed with error: %v", string(output)) log.Infof("No modification to network data") } return networkData } // EditNetworkData returns the modified set of data after parsing the command output. In case it fails to parse the data, // it returns the unmodified data. func EditNetworkData(log log.T, data model.NetworkData, cmdOutput string) (model.NetworkData, error) { var dataB []byte var err error var config NetworkInterfaceConfiguration var dnsServerAddress, gatewayAddress, subnetMask, ipV4, ipV6 string dataB, _ = json.Marshal(data) //trim spaces str := strings.TrimSpace(cmdOutput) if err = json.Unmarshal([]byte(str), &config); err != nil { err = fmt.Errorf("Failed to read data from powershell command output because of error - %v", err.Error()) return data, nil } dataB, _ = json.Marshal(config) log.Debugf("Advanced network data of macaddress - %v - \n%v", data.MacAddress, jsonutil.Indent(string(dataB))) data.DHCPServer = config.DHCPServer if gatewayAddress, err = GetParsedData(log, config.DefaultIPGateway); err != nil { log.Debugf("Unable to get gateway address for macaddress - %v due to error - %v", data.MacAddress, err.Error()) } else { data.Gateway = gatewayAddress } if dnsServerAddress, err = GetParsedData(log, config.DNSServers); err != nil { log.Debugf("Unable to get dns server for macaddress - %v due to error - %v", data.MacAddress, err.Error()) } else { data.DNSServer = dnsServerAddress } if subnetMask, err = GetParsedData(log, config.IPSubnet); err != nil { log.Debugf("Unable to get gateway address for macaddress - %v due to error - %v", data.MacAddress, err.Error()) } else { data.SubnetMask = subnetMask } if ipV4, ipV6, err = GetIPAddresses(log, config.IPAddresses); err != nil { log.Debugf("Unable to get ip addresses for macaddress - %v due to error - %v", data.MacAddress, err.Error()) } else { data.IPV4 = ipV4 data.IPV6 = ipV6 } dataB, _ = json.Marshal(data) log.Debugf("logging modified data - %v", string(dataB)) return data, nil } // GetParsedData parses the command output and returns the parsedOutput. // // NOTE: Parsing logic is specific to the command that is executed. Any change in the command should follow changes here // too. func GetParsedData(log log.T, input interface{}) (parsedOutput string, err error) { //Note: As per the link - https://msdn.microsoft.com/en-us/library/aa394217%28v=vs.85%29.aspx // fields like DNSServerSearchOrder, DefaultGateway are string array. However, on just 1 entry - the data ends up // showing as a string. If there are multiple entries - ConvertTo-Json - makes it a map with fields similar to // Format struct. // there are only 2 possibilities - either given input is a string or a json map with fields similar to Format struct. // anything else means - the command executed to get the data has been changed. errorMsg := "Unable to read more data from %v due to error - %v" if str, possible := input.(string); possible { log.Debugf("Input %v can be transformed into string", input) parsedOutput = str } else { log.Debugf("Input %v can't be transformed into string", input) var format Format dataB, _ := json.Marshal(input) if err = json.Unmarshal(dataB, &format); err != nil { err = fmt.Errorf(errorMsg, input, err.Error()) } else { //verify if format.Value is not nil if len(format.Value) > 0 { //currently we return 1st element of string array - since DNSServer and Gateway is string //if that changes then we can return format.Value parsedOutput = format.Value[0] } else { err = fmt.Errorf("Unexpected data format") } } } log.Debugf("ParsedOutput - %v, error - %v", parsedOutput, err) return } // GetIPAddresses parses the command output and returns the parsedOutput. // // NOTE: Parsing logic is specific to the command that is executed. Any change in the command should follow changes here // too. func GetIPAddresses(log log.T, input interface{}) (ipV4, ipV6 string, err error) { // Note: As per the link - https://msdn.microsoft.com/en-us/library/aa394217%28v=vs.85%29.aspx IPAddress is // string array. However, on just 1 entry - the data ends up showing as a string. If there are multiple entries // ConvertTo-Json - makes it a map with fields similar to Format struct. // there are only 2 possibilities - either given input is a string or a json map with fields similar to Format // struct. Anything else means - the command executed to get the data has been changed. var ipV4Addresses, ipV6Addresses []string errorMsg := "Unable to read more data from %v due to error - %v" log.Debugf("Parsing ip addresses from %v", input) if str, possible := input.(string); possible { log.Debugf("Input %v can be transformed into string", input) if validIPV4Address.MatchString(str) { ipV4Addresses = append(ipV4Addresses, str) } } else { log.Debugf("Input %v can't be transformed into string", input) var format Format dataB, _ := json.Marshal(input) if err = json.Unmarshal(dataB, &format); err != nil { err = fmt.Errorf(errorMsg, input, err.Error()) } else { //verify if format.Value is not nil if len(format.Value) > 0 { for _, value := range format.Value { if validIPV4Address.MatchString(value) { ipV4Addresses = append(ipV4Addresses, value) } else { ipV6Addresses = append(ipV6Addresses, value) } } } else { err = fmt.Errorf("Unexpected data format") } } } //all ip addresses returned for a network interface will be returned as a ',' separated string ipV4 = strings.Join(ipV4Addresses, ",") ipV6 = strings.Join(ipV6Addresses, ",") log.Debugf("IPV4 - %v, IPV6 - %v, error - %v", ipV4, ipV6, err) return }