Merge pull request #5036 from inspec/cw/interface-improvements

Additions to the interface resource
This commit is contained in:
James Stocks 2020-06-25 11:27:53 +01:00 committed by GitHub
commit af5fd7bd03
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 393 additions and 22 deletions

View file

@ -5,10 +5,11 @@ platform: os
# interface
Use the `interface` Chef InSpec audit resource to test basic network adapter properties, such as name, status, and link speed (in MB/sec).
Use the `interface` Chef InSpec audit resource to test basic network adapter properties, such as name, status, IP addresses, and link speed (in MB/sec).
* On Linux platforms, `/sys/class/net/#{iface}` is used as source
* On the Windows platform, the `Get-NetAdapter` cmdlet is used as source
* On BSD and MacOS platforms, the `ifconfig` command is used as source. Link speed may not be available.
<br>
@ -20,7 +21,7 @@ This resource is distributed along with Chef InSpec itself. You can use it autom
### Version
This resource first became available in v1.0.0 of InSpec.
This resource first became available in v1.0.0 of Chef InSpec.
## Syntax
@ -30,61 +31,71 @@ An `interface` resource block declares network interface properties to be tested
it { should be_up }
its('speed') { should eq 1000 }
its('name') { should eq eth0 }
its('ipv4_addresses') { should include '10.0.0.5' }
end
<br>
## Properties
`name`, `speed`
`ipv4_address`, `ipv4_addresses`, `ipv4_addresses_netmask`, `ipv4_cidrs`, `ipv6_addresses`, `ipv6_cidrs`, `name`, `speed`
<br>
## Resource Property Examples
### name
### ipv4_address
The `name` property tests if the named network interface exists:
Returns the first `ipv4_addresses` entry as a String. Note: this property is incompatible with ServerSpec, which returns the value including the CIDR range, such as '10.0.0.5/32'.
its('name') { should eq eth0 }
### speed
The `speed` property tests the speed of the network interface, in MB/sec:
its('speed') { should eq 1000 }
its('ipv4_address') { should eq '10.0.0.5' }
### ipv4_addresses
The `ipv4_addresses` property tests if the specified address exists on the named network interface:
The `ipv4_addresses` property returns an Array of IPv4 addresses as Strings. You may then test if the specified address exists on the named network interface:
its('ipv4_addresses') { should include '127.0.0.1' }
### ipv4_addresses_netmask
### ipv4\_addresses\_netmask
The `ipv4_addresses_netmask` property tests if the specified address and netmask exists on the named network interface:
The `ipv4_addresses_netmask` property returns an Array of Strings with each containing the IPv4 address, a slash, and the netmask. You may then test if the specified address and netmask exists on the named network interface:
its('ipv4_addresses_netmask') { should include '127.0.0.1/255.0.0.0' }
### ipv6_address
Returns the first `ipv6_address` entry. Note: this property is incompatible with ServerSpec, which returns the value including the CIDR range.
its('ipv6_address') { should eq '2089:98b::faeb' }
### ipv6_addresses
The `ipv6_addresses` property tests if the specified address exists on the named network interface:
The `ipv6_addresses` property returns an Array of Strings and tests if the specified address exists on the named network interface:
its('ipv6_addresses') { should include '::1' }
### ipv4_cidrs
The `ipv4_cidrs` property tests if the specified address and netmask combination exists on the named network interface:
The `ipv4_cidrs` property returns an Array of Strings and tests if the specified address and netmask combination exists on the named network interface:
its('ipv4_cidrs') { should include '127.0.0.1/8' }
### ipv6_cidrs
The `ipv6_cidrs` property tests if the specified address and netmask combination exists on the named network interface:
The `ipv6_cidrs` property returns an Array of Strings and tests if the specified address and netmask combination exists on the named network interface:
its('ipv6_cidrs') { should include '::1/128' }
<br>
### name
The `name` property returns the name of the interface:
its('name') { should eq 'eth0' }
### speed
The `speed` property tests the speed of the network interface, in MB/sec. Note: On BSD and MacOS platforms, this value may be nil, because it difficult to obtain reliably.
its('speed') { should eq 1000 }
## Matchers
@ -96,13 +107,19 @@ The `be_up` matcher tests if the network interface is available:
it { should be_up }
### have_an_ipv4_address
### exist
The `exist` matcher tests if the network interface exists:
it { should exist }
### have\_an\_ipv4\_address
The `have_an_ipv4_address` matcher tests if the network interface has any IPv4 addresses assigned:
it { should have_an_ipv4_address }
### have_an_ipv6_address
### have\_an\_ipv6\_address
The `have_an_ipv6_address` matcher tests if the network interface has any IPv6 addresses assigned:

View file

@ -0,0 +1,72 @@
---
title: About the interfaces Resource
platform: os
---
# interfaces
Use the `interfaces` Chef InSpec audit resource to test the properties of multiple network interfaces on the system.
## Syntax
An `interfaces` resource block may take no arguments, in which case it will list all interfaces:
describe interfaces do
its('names') { should include 'eth0' }
end
An `interfaces` resource block may take a where clause, filtering on a Filter Criterion:
# All eth- interfaces
describe interfaces.where(name: /^eth\d+/)
its('names') { should include 'eth0' }
end
Like any Chef InSpec resource, you may also use it for data lookup instead of testing:
# We are an IPv6 shop
interfaces.where(name: /^eth/).names do |name|
describe interface(name) do
it { should have_ipv6_address }
end
end
# Obtain the machine's main IP address
my_ip = interfaces.ipv4_address
## Filter Criteria
### name
String. The name of an interface.
## Properties
### count
The `count` property returns an Integer describing how many interfaces matched.
its("count") { should eq 6 }
### ipv4_address
Attempts to guess the "first" "real" IPv4 address on any interface. Looks for interfaces that are up and have IPv4 addresses assigned, then tries to filter out loopback, management (10/8) and local (192.168/16) IP addresses, returning the best of of those that it can; you may still get nil, or a loopback address. Note that if the machine is behind NAT this will not be the external IP address; use the `http` resource to query an IP lookup service for that.
its('ipv4_address') { should_not eq '127.0.0.1' }
### names
The `names` property returns an Array of Strings representing the names of the interfaces.
its("names") { should include "eth0" }
## Matchers
For a full list of available universal matchers, please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### exist
The `exist` matcher tests true if at least one interface exists on the system. This is almost always the case.
it { should exist }

View file

@ -47,10 +47,18 @@ module Inspec::Resources
ipv6_addresses && !ipv6_addresses.empty?
end
def ipv4_address
ipv4_addresses.first
end
def ipv4_addresses
ipv4_cidrs.map { |i| i.split("/")[0] }
end
def ipv6_address
ipv6_addresses.first
end
def ipv6_addresses
ipv6_cidrs.map { |i| i.split("/")[0] }
end
@ -85,6 +93,7 @@ module Inspec::Resources
@cache ||= begin
provider = LinuxInterface.new(inspec) if inspec.os.linux?
provider = WindowsInterface.new(inspec) if inspec.os.windows?
provider = BsdInterface.new(inspec) if inspec.os.bsd? # includes macOS
Hash(provider && provider.interface_info(@iface))
end
end
@ -98,6 +107,52 @@ module Inspec::Resources
end
end
class BsdInterface < InterfaceInfo
def interface_info(iface)
cmd = inspec.command("ifconfig #{iface}")
return nil if cmd.exit_status.to_i != 0
lines = cmd.stdout.split("\n")
iface_info = {
name: iface,
ipv4_addresses: [], # Actually CIDRs
ipv6_addresses: [], # are expected to go here
}
iface_info[:up] = lines[0].include?("UP")
lines.each do |line|
# IPv4 case
m = line.match(/^\s+inet\s+((?:\d{1,3}\.){3}\d{1,3})\s+netmask\s+(0x[a-f0-9]{8})/)
if m
ip = m[1]
hex_mask = m[2]
cidr = hex_mask.to_i(16).to_s(2).count("1")
iface_info[:ipv4_addresses] << "#{ip}/#{cidr}"
next
end
# IPv6 case
m = line.match(/^\s+inet6\s+([a-f0-9:]+)%#{iface}\s+prefixlen\s+(\d+)/)
if m
ip = m[1]
cidr = m[2]
iface_info[:ipv6_addresses] << "#{ip}/#{cidr}"
next
end
# Speed detect, crummy - can't detect wifi, finds any number in the string
# Ethernet autoselect (1000baseT <full-duplex>)
m = line.match(/^\s+media:\D+(\d+)/)
if m
iface_info[:speed] = m[1].to_i
next
end
end
iface_info
end
end
class LinuxInterface < InterfaceInfo
def interface_info(iface)
# will return "[mtu]\n1500\n[type]\n1"

View file

@ -0,0 +1,119 @@
require "inspec/utils/filter"
require "inspec/resources/command"
module Inspec::Resources
class Interfaces < Inspec.resource(1)
name "interfaces"
supports platform: "unix"
supports platform: "windows"
desc "Use the interfaces InSpec audit resource to test properties for multiple network interfaces installed on the system"
example <<~EXAMPLE
describe interfaces do
its('names') { should include 'eth0' }
end
EXAMPLE
attr_reader :iface_data
def to_s
"Interfaces"
end
filter = FilterTable.create
filter.register_column(:names, field: "name")
.install_filter_methods_on_resource(self, :scan_interfaces)
def ipv4_address
require "ipaddr"
# Loop over interface names
# Select those that are up and have an ipv4 address
interfaces = names.map { |n| inspec.interface(n) }.select do |i|
i.ipv4_address? && i.up?
end
addrs = interfaces.map(&:ipv4_addresses).flatten.map { |a| IPAddr.new(a) }
# Look for progressively "better" IP addresses
[
# Loopback and private IP ranges
IPAddr.new("127.0.0.0/8"),
IPAddr.new("192.168.0.0/16"),
IPAddr.new("172.16.0.0/12"),
IPAddr.new("10.0.0.0/8"),
].each do |private_range|
filtered_addrs = addrs.reject { |a| private_range.include?(a) }
if filtered_addrs.empty?
# Everything we had was a private or loopback IP. Return the "best" thing we were left with.
return addrs.first.to_s
end
addrs = filtered_addrs
end
addrs.first.to_s
end
private
def scan_interfaces
@iface_data ||= begin
provider = LinuxInterfaceLister.new(inspec) if inspec.os.linux?
provider = WindowsInterfaceLister.new(inspec) if inspec.os.windows?
provider = BsdInterfaceLister.new(inspec) if inspec.os.bsd? # includes macOS
Array(provider && provider.scan_interfaces)
end
end
class InterfaceLister
attr_reader :inspec
def initialize(inspec)
@inspec = inspec
end
end
class BsdInterfaceLister < InterfaceLister
def scan_interfaces
iface_data = []
cmd = inspec.command("ifconfig -a")
cmd.stdout.split("\n").each do |line|
# lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> metric 0 mtu 16384
m = line.match(/^(\S+):/)
if m
iface_data << { "name" => m[1] }
end
end
iface_data
end
end
class LinuxInterfaceLister < InterfaceLister
def scan_interfaces
iface_data = []
cmd = inspec.command("ls /sys/class/net")
cmd.stdout.split("\n").each do |iface|
iface_data << { "name" => iface }
end
iface_data
end
end
class WindowsInterfaceLister < InterfaceLister
def scan_interfaces
iface_data = []
cmd = inspec.command("Get-NetAdapter | Select-Object -Property Name | ConvertTo-Json")
begin
adapter_info = JSON.parse(cmd.stdout)
# May be a Hash if only one, or Array if multiple - normalize to Array
adapter_info = [ adapter_info ] if adapter_info.is_a? Hash
rescue JSON::ParserError => _e
return nil
end
adapter_info.each do |info|
iface_data << { "name" => info["Name"] }
end
iface_data
end
end
end
end

8
test/fixtures/cmd/Get-NetAdapter-Name vendored Normal file
View file

@ -0,0 +1,8 @@
[
{
"Name": "vEthernet (Intel(R) PRO 1000 MT Network Connection - Virtual Switch)"
},
{
"Name": "Ethernet0"
}
]

15
test/fixtures/cmd/ifconfig-a vendored Normal file
View file

@ -0,0 +1,15 @@
em0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
options=9b<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,VLAN_HWCSUM>
ether 08:00:27:5f:9d:3b
hwaddr 08:00:27:5f:9d:3b
inet 10.0.2.15 netmask 0xffffff00 broadcast 10.0.2.255
nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
media: Ethernet autoselect (1000baseT <full-duplex>)
status: active
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> metric 0 mtu 16384
options=680003<RXCSUM,TXCSUM,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6>
inet6 ::1 prefixlen 128
inet6 fe80::1%lo0 prefixlen 64 scopeid 0x2
inet 127.0.0.1 netmask 0xff000000
nd6 options=21<PERFORMNUD,AUTO_LINKLOCAL>
groups: lo

7
test/fixtures/cmd/ifconfig-em0 vendored Normal file
View file

@ -0,0 +1,7 @@
em0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
ether ac:bc:32:c7:30:e7
inet6 fe80::8b6:c2cc:2928:3b61%en0 prefixlen 64 secured scopeid 0x5
inet 1.2.3.4 netmask 0xffffff00 broadcast 1.2.3.255
nd6 options=201<PERFORMNUD,DAD>
media: Ethernet autoselect (1000baseT <full-duplex>)
status: active

7
test/fixtures/cmd/ifconfig-en0 vendored Normal file
View file

@ -0,0 +1,7 @@
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
ether ac:bc:32:c7:30:e7
inet6 fe80::8b6:c2cc:2928:3b61%en0 prefixlen 64 secured scopeid 0x5
inet 192.168.1.2 netmask 0xffffff00 broadcast 192.168.1.255
nd6 options=201<PERFORMNUD,DAD>
media: Ethernet autoselect (1000baseT <full-duplex>)
status: active

7
test/fixtures/cmd/ifconfig-lo0 vendored Normal file
View file

@ -0,0 +1,7 @@
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> metric 0 mtu 16384
options=680003<RXCSUM,TXCSUM,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6>
inet6 ::1 prefixlen 128
inet6 fe80::1%lo0 prefixlen 64 scopeid 0x2
inet 127.0.0.1 netmask 0xff000000
nd6 options=21<PERFORMNUD,AUTO_LINKLOCAL>
groups: lo

2
test/fixtures/cmd/ls-sys-class-net vendored Normal file
View file

@ -0,0 +1,2 @@
eth0
lo

View file

@ -314,6 +314,13 @@ class MockLoader
"/sbin/ip -br -6 address show dev eth0" => cmd.call("interface-addresses-6"),
"Get-NetAdapter | Select-Object -Property Name, InterfaceDescription, Status, State, MacAddress, LinkSpeed, ReceiveLinkSpeed, TransmitLinkSpeed, Virtual | ConvertTo-Json" => cmd.call("Get-NetAdapter"),
"Get-NetIPAddress | Select-Object -Property IPv6Address, IPv4Address, InterfaceAlias, PrefixLength | ConvertTo-Json" => cmd.call("Get-NetIPAddress"),
"ifconfig en0" => cmd.call("ifconfig-en0"),
# network interfaces
"ls /sys/class/net" => cmd.call("ls-sys-class-net"),
"ifconfig -a" => cmd.call("ifconfig-a"),
"ifconfig em0" => cmd.call("ifconfig-em0"),
"ifconfig lo0" => cmd.call("ifconfig-lo0"),
"Get-NetAdapter | Select-Object -Property Name | ConvertTo-Json" => cmd.call("Get-NetAdapter-Name"),
# bridge on linux
"ls -1 /sys/class/net/br0/brif/" => cmd.call("ls-sys-class-net-br"),
# bridge on Windows

View file

@ -12,9 +12,11 @@ describe "Inspec::Resources::Interface" do
_(resource.speed).must_equal 10000
_(resource.name).must_equal "eth0"
_(resource.ipv4_cidrs).must_include "127.0.0.1/8"
_(resource.ipv4_address).must_equal "127.0.0.1"
_(resource.ipv4_addresses).must_include "127.0.0.1"
_(resource.ipv4_addresses_netmask).must_include "127.0.0.1/255.0.0.0"
_(resource.ipv6_cidrs).must_include "::1/128"
_(resource.ipv6_address).must_equal "::1"
_(resource.ipv6_addresses).must_include "::1"
_(resource.ipv4_address?).must_equal true
_(resource.ipv6_address?).must_equal true
@ -27,8 +29,10 @@ describe "Inspec::Resources::Interface" do
_(resource.name).must_be_nil
_(resource.speed).must_be_nil
_(resource.ipv4_cidrs).must_be_empty
_(resource.ipv4_address).must_be_nil
_(resource.ipv4_addresses).must_be_empty
_(resource.ipv4_addresses_netmask).must_be_empty
_(resource.ipv6_address).must_be_nil
_(resource.ipv6_cidrs).must_be_empty
_(resource.ipv6_addresses).must_be_empty
_(resource.ipv4_address?).must_equal false
@ -42,10 +46,12 @@ describe "Inspec::Resources::Interface" do
_(resource.up?).must_equal false
_(resource.name).must_equal "Ethernet0"
_(resource.speed).must_equal 0
_(resource.ipv4_address).must_be_nil
_(resource.ipv4_address?).must_equal false
_(resource.ipv6_address?).must_equal false
_(resource.ipv4_addresses).must_be_empty
_(resource.ipv4_addresses_netmask).must_be_empty
_(resource.ipv6_address).must_be_nil
_(resource.ipv6_addresses).must_be_empty
_(resource.ipv4_cidrs).must_be_empty
_(resource.ipv6_cidrs).must_be_empty
@ -58,9 +64,11 @@ describe "Inspec::Resources::Interface" do
_(resource.name).must_equal "vEthernet (Intel(R) PRO 1000 MT Network Connection - Virtual Switch)"
_(resource.speed).must_equal 10000000
_(resource.ipv4_cidrs).must_include "127.0.0.1/8"
_(resource.ipv4_address).must_equal "127.0.0.1"
_(resource.ipv4_addresses).must_include "127.0.0.1"
_(resource.ipv4_addresses_netmask).must_include "127.0.0.1/255.0.0.0"
_(resource.ipv6_cidrs).must_include "::1/128"
_(resource.ipv6_address).must_equal "::1"
_(resource.ipv6_addresses).must_include "::1"
_(resource.ipv4_address?).must_equal true
_(resource.ipv6_address?).must_equal true
@ -81,6 +89,23 @@ describe "Inspec::Resources::Interface" do
_(resource.ipv6_cidrs).must_be_empty
end
it "verify interface on macos" do
resource = MockLoader.new(:osx104).load_resource("interface", "en0")
_(resource.exists?).must_equal true
_(resource.up?).must_equal true
_(resource.speed).must_equal 1000
_(resource.name).must_equal "en0"
_(resource.ipv4_cidrs).must_include "192.168.1.2/24"
_(resource.ipv4_address).must_equal "192.168.1.2"
_(resource.ipv4_addresses).must_include "192.168.1.2"
_(resource.ipv4_addresses_netmask).must_include "192.168.1.2/255.255.255.0"
_(resource.ipv6_cidrs).must_include "fe80::8b6:c2cc:2928:3b61/64"
_(resource.ipv6_address).must_equal "fe80::8b6:c2cc:2928:3b61"
_(resource.ipv6_addresses).must_include "fe80::8b6:c2cc:2928:3b61"
_(resource.ipv4_address?).must_equal true
_(resource.ipv6_address?).must_equal true
end
# undefined
it "verify interface on unsupported os" do
resource = MockLoader.new(:undefined).load_resource("interface", "eth0")

View file

@ -0,0 +1,30 @@
require "helper"
require "inspec/resource"
require "inspec/resources/interfaces"
describe "Inspec::Resources::Interfaces" do
# ubuntu 16.04
it "verify interface on ubuntu" do
resource = MockLoader.new(:ubuntu1604).load_resource("interfaces")
_(resource.exist?).must_equal true
_(resource.names).must_equal %w{eth0 lo}
_(resource.ipv4_address).must_equal "127.0.0.1"
end
# freebsd / macos
it "verify interface on freebsd" do
resource = MockLoader.new(:freebsd12).load_resource("interfaces")
_(resource.exist?).must_equal true
_(resource.names).must_equal %w{em0 lo0}
_(resource.ipv4_address).must_equal "1.2.3.4"
end
# windows
it "verify interfaces on windows" do
resource = MockLoader.new(:windows).load_resource("interfaces")
_(resource.exist?).must_equal true
_(resource.names).must_equal ["vEthernet (Intel(R) PRO 1000 MT Network Connection - Virtual Switch)", "Ethernet0"]
_(resource.ipv4_address).must_equal "127.0.0.1"
end
end