support address matchers on interface resource

Adds missing functionality to `interface`. Fixes #1830

```
describe interface("eth0") do
  its(ipv4_addresses) { should include 1.2.3.4 }
end
```

And so on... see diff/docs for additional matchers.

Signed-off-by: Matt Kulka <mkulka@parchment.com>
This commit is contained in:
Matt Kulka 2019-03-18 15:53:51 -07:00
parent 7887d25251
commit 633cea6673
7 changed files with 173 additions and 3 deletions

View file

@ -44,16 +44,46 @@ An `interface` resource block declares network interface properties to be tested
### name
The `name` matcher tests if the named network interface exists:
The `name` property tests if the named network interface exists:
its('name') { should eq eth0 }
### speed
The `speed` matcher tests the speed of the network interface, in MB/sec:
The `speed` property tests the speed of the network interface, in MB/sec:
its('speed') { should eq 1000 }
### ipv4_addresses
The `ipv4_addresses` property tests if the specified address exists on the named network interface:
its('ipv4_addresses') { should include '127.0.0.1' }
### ipv4_addresses_netmask
The `ipv4_addresses_netmask` property tests 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_addresses
The `ipv6_addresses` property 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:
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:
its('ipv6_cidrs') { should include '::1/128' }
<br>
## Matchers
@ -66,3 +96,14 @@ The `be_up` matcher tests if the network interface is available:
it { should be_up }
### 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
The `have_an_ipv6_address` matcher tests if the network interface has any IPv6 addresses assigned:
it { should have_an_ipv6_address }

View file

@ -13,6 +13,8 @@ module Inspec::Resources
it { should exist }
it { should be_up }
its('speed') { should eq 1000 }
its('ipv4_addresses') { should include '127.0.0.1' }
its('ipv6_cidrs') { should include '::1/128' }
end
"
def initialize(iface)
@ -41,6 +43,42 @@ module Inspec::Resources
interface_info.nil? ? nil : interface_info[:speed]
end
def ipv4_address?
!ipv4_addresses.nil? && !ipv4_addresses.empty?
end
def ipv6_address?
!ipv6_addresses.nil? && !ipv6_addresses.empty?
end
def ipv4_addresses
ipv4_cidrs.map { |i| i.split('/')[0] }
end
def ipv6_addresses
ipv6_cidrs.map { |i| i.split('/')[0] }
end
def ipv4_addresses_netmask
ipv4_cidrs.map { |i| i.split('/') }.map do |addr, netlen|
binmask = "#{'1' * netlen.to_i}#{'0' * (32 - netlen.to_i)}".to_i(2)
netmask = []
(1..4).each do |_byte|
netmask.unshift(binmask & 255)
binmask = binmask >> 8
end
"#{addr}/#{netmask.join('.')}"
end
end
def ipv4_cidrs
interface_info.nil? ? [] : interface_info[:ipv4_addresses]
end
def ipv6_cidrs
interface_info.nil? ? [] : interface_info[:ipv6_addresses]
end
def to_s
"Interface #{@iface}"
end
@ -87,28 +125,54 @@ module Inspec::Resources
speed = convert_to_i(speed)
end
family_addresses = addresses(iface)
{
name: iface,
up: state,
speed: speed,
ipv4_addresses: family_addresses['inet'],
ipv6_addresses: family_addresses['inet6'],
}
end
private
def addresses(iface)
addrs_by_family = { 'inet6' => [], 'inet' => [] }
[4, 6].each do |v|
cmd = inspec.command("/sbin/ip -br -#{v} address show dev #{iface}")
next unless cmd.exit_status.to_i == 0
family = v == 6 ? 'inet6' : 'inet'
cmd.stdout.each_line do |line|
_dev, _state, *addrs = line.split(/\s+/)
addrs_by_family[family] = addrs
end
end
addrs_by_family
end
end
class WindowsInterface < InterfaceInfo
def interface_info(iface)
# gather all network interfaces
cmd = inspec.command('Get-NetAdapter | Select-Object -Property Name, InterfaceDescription, Status, State, MacAddress, LinkSpeed, ReceiveLinkSpeed, TransmitLinkSpeed, Virtual | ConvertTo-Json')
cmd = inspec.command('Get-NetAdapter | Select-Object -Property Name, InterfaceDescription, Status, State, ' \
'MacAddress, LinkSpeed, ReceiveLinkSpeed, TransmitLinkSpeed, Virtual | ConvertTo-Json')
addr_cmd = inspec.command('Get-NetIPAddress | Select-Object -Property IPv6Address, IPv4Address, InterfaceAlias,' \
' PrefixLength | ConvertTo-Json')
# filter network interface
begin
net_adapter = JSON.parse(cmd.stdout)
addresses = JSON.parse(addr_cmd.stdout)
rescue JSON::ParserError => _e
return nil
end
# ensure we have an array of groups
net_adapter = [net_adapter] if !net_adapter.is_a?(Array)
addresses = [addresses] if !addresses.is_a?(Array)
# select the requested interface
adapters = net_adapter.each_with_object([]) do |adapter, adapter_collection|
@ -117,6 +181,8 @@ module Inspec::Resources
name: adapter['Name'],
up: adapter['State'] == 2,
speed: adapter['ReceiveLinkSpeed'] / 1000,
ipv4_addresses: addresses_for_proto(addresses, adapter['Name'], 'IPv4'),
ipv6_addresses: addresses_for_proto(addresses, adapter['Name'], 'IPv6'),
}
adapter_collection.push(info) if info[:name].casecmp(iface) == 0
end
@ -125,5 +191,13 @@ module Inspec::Resources
warn "[Possible Error] detected multiple network interfaces with the name #{iface}" if adapters.size > 1
adapters[0]
end
private
def addresses_for_proto(all_addresses, iface, proto)
all_addresses.select { |i| i['InterfaceAlias'] == iface }
.map { |i| "#{i["#{proto}Address"]}/#{i['PrefixLength']}" unless i["#{proto}Address"].nil? }
.compact
end
end
end

View file

@ -343,7 +343,10 @@ class MockLoader
# network interface
'fddd70e8b8510f5fcc0413cfdc41598c55d6922bb2a0a4075e2118633a0bf422' => cmd.call('find-net-interface'),
'c33821dece09c8b334e03a5bb9daefdf622007f73af4932605e758506584ec3f' => empty.call,
'/sbin/ip -br -4 address show dev eth0' => cmd.call('interface-addresses-4'),
'/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'),
# bridge on linux
'ls -1 /sys/class/net/br0/brif/' => cmd.call('ls-sys-class-net-br'),
# bridge on Windows

View file

@ -0,0 +1,14 @@
[
{
"IPv6Address": "::1",
"IPv4Address": null,
"InterfaceAlias": "vEthernet (Intel(R) PRO 1000 MT Network Connection - Virtual Switch)",
"PrefixLength": 128
},
{
"IPv6Address": null,
"IPv4Address": "127.0.0.1",
"InterfaceAlias": "vEthernet (Intel(R) PRO 1000 MT Network Connection - Virtual Switch)",
"PrefixLength": 8
}
]

View file

@ -0,0 +1 @@
eth0 UNKNOWN 127.0.0.1/8

View file

@ -0,0 +1 @@
eth0 UNKNOWN ::1/128

View file

@ -13,6 +13,13 @@ describe 'Inspec::Resources::Interface' do
_(resource.exists?).must_equal true
_(resource.up?).must_equal true
_(resource.speed).must_equal 10000
_(resource.ipv4_cidrs).must_include '127.0.0.1/8'
_(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_addresses).must_include '::1'
_(resource.ipv4_address?).must_equal true
_(resource.ipv6_address?).must_equal true
end
it 'verify invalid interface on ubuntu' do
@ -20,13 +27,28 @@ describe 'Inspec::Resources::Interface' do
_(resource.exists?).must_equal false
_(resource.up?).must_equal false
_(resource.speed).must_be_nil
_(resource.ipv4_cidrs).must_be_empty
_(resource.ipv4_addresses).must_be_empty
_(resource.ipv4_addresses_netmask).must_be_empty
_(resource.ipv6_cidrs).must_be_empty
_(resource.ipv6_addresses).must_be_empty
_(resource.ipv4_address?).must_equal false
_(resource.ipv6_address?).must_equal false
end
# windows
it 'verify interface on windows' do
resource = MockLoader.new(:windows).load_resource('interface', 'ethernet0')
_(resource.exists?).must_equal true
_(resource.up?).must_equal false
_(resource.speed).must_equal 0
_(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_addresses).must_be_empty
_(resource.ipv4_cidrs).must_be_empty
_(resource.ipv6_cidrs).must_be_empty
end
it 'verify interface on windows' do
@ -34,6 +56,13 @@ describe 'Inspec::Resources::Interface' do
_(resource.exists?).must_equal true
_(resource.up?).must_equal true
_(resource.speed).must_equal 10000000
_(resource.ipv4_cidrs).must_include '127.0.0.1/8'
_(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_addresses).must_include '::1'
_(resource.ipv4_address?).must_equal true
_(resource.ipv6_address?).must_equal true
end
it 'verify invalid interface on windows' do
@ -41,6 +70,13 @@ describe 'Inspec::Resources::Interface' do
_(resource.exists?).must_equal false
_(resource.up?).must_equal false
_(resource.speed).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_addresses).must_be_empty
_(resource.ipv4_cidrs).must_be_empty
_(resource.ipv6_cidrs).must_be_empty
end
# undefined