windows_feature resource: Add DISM support (#3224)

* windows_feature resource: Add DISM support

This modifies the `windows_feature` resource to fallback to DISM when
the `Get-WindowsFeature` command is not available.

* Allow specifying `:dism` or `:powershell`
* Replace stacktrace with smaller error message
* Add notes/todo about raise behavior
* Remove duplicated platform check

Signed-off-by: Jerry Aldrich <jerryaldrichiii@gmail.com>
This commit is contained in:
Jerry Aldrich 2018-07-25 13:00:06 -07:00 committed by Jared Quick
parent 2245bba021
commit f2d64938b7
6 changed files with 197 additions and 64 deletions

View file

@ -28,9 +28,21 @@ where
The following examples show how to use this InSpec audit resource.
### Test the DHCP Server feature
### Test the DHCP feature (Attempts PowerShell then DISM)
describe windows_feature('DHCP Server') do
describe windows_feature('DHCP') do
it{ should be_installed }
end
### Test the IIS-WebServer feature using DISM
describe windows_feature('IIS-WebServer', DISM) do
it{ should be_installed }
end
### Test the NetFx3 feature using DISM
describe windows_feature('NetFx3', :dism) do
it{ should be_installed }
end

View file

@ -1,84 +1,126 @@
# encoding: utf-8
# check for a Windows feature
# Usage:
# describe windows_feature('DHCP Server') do
# it{ should be_installed }
# end
#
# deprecated serverspec syntax:
# describe windows_feature('IIS-Webserver') do
# it{ should be_installed.by("dism") }
# end
#
# describe windows_feature('Web-Webserver') do
# it{ should be_installed.by("powershell") }
# end
#
# This implementation uses the Get-WindowsFeature commandlet:
# Get-WindowsFeature | Where-Object {$_.Name -eq 'XPS Viewer' -or $_.DisplayName -eq 'XPS Viewe
# r'} | Select-Object -Property Name,DisplayName,Description,Installed,InstallState | ConvertTo-Json
# {
# "Name": "XPS-Viewer",
# "DisplayName": "XPS Viewer",
# "Description": "The XPS Viewer is used to read, set permissions for, and digitally sign XPS documents.",
# "Installed": false,
# "InstallState": 0
# }
module Inspec::Resources
class WindowsFeature < Inspec.resource(1)
name 'windows_feature'
supports platform: 'windows'
desc 'Use the windows_feature InSpec audit resource to test features on Microsoft Windows.'
example "
describe windows_feature('dhcp') do
example <<-EOX
# By default this resource will use Get-WindowsFeature.
# Failing that, it will use DISM.
# Get-WindowsFeature Example
describe windows_feature('Web-WebServer', :powershell) do
it { should be_installed }
end
"
def initialize(feature)
# DISM Example
describe windows_feature('IIS-WebServer', :dism) do
it { should be_installed }
end
# Try PowerShell then DISM Example
describe windows_feature('IIS-WebServer') do
it { should be_installed }
end
EOX
def initialize(feature, method = nil)
@feature = feature
@method = method
@cache = nil
# verify that this resource is only supported on Windows
return skip_resource 'The `windows_feature` resource is not supported on your OS.' if !inspec.os.windows?
end
# returns true if the package is installed
def installed?(_provider = nil, _version = nil)
def installed?
info[:installed] == true
end
# returns the package description
def info
return @cache if !@cache.nil?
features_cmd = "Get-WindowsFeature | Where-Object {$_.Name -eq '#{@feature}' -or $_.DisplayName -eq '#{@feature}'} | Select-Object -Property Name,DisplayName,Description,Installed,InstallState | ConvertTo-Json"
cmd = inspec.command(features_cmd)
@cache = {
name: @feature,
type: 'windows-feature',
}
# cannot rely on exit code for now, successful command returns exit code 1
# return nil if cmd.exit_status != 0
# try to parse json
begin
params = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return @cache
case @method
when :powershell
@cache = info_via_powershell(@feature)
if @cache[:error]
# TODO: Allow handling `Inspec::Exception` outside of initialize
# See: https://github.com/inspec/inspec/issues/3237
# The below will fail the resource regardless of what is raised
raise Inspec::Exceptions::ResourceFailed, @cache[:error]
end
when :dism
@cache = info_via_dism(@feature)
else
@cache = info_via_powershell(@feature)
@cache = info_via_dism(@feature) if @cache[:error]
end
@cache = {
name: params['Name'],
description: params['Description'],
installed: params['Installed'],
type: 'windows-feature',
}
@cache
end
def to_s
"Windows Feature '#{@feature}'"
end
private
def info_via_dism(feature)
dism_command = "dism /online /get-featureinfo /featurename:#{feature}"
cmd = inspec.command(dism_command)
if cmd.exit_status != 0
feature_info = {
name: feature,
description: 'N/A',
installed: false,
}
else
result = cmd.stdout
feature_name_regex = /Feature Name : (.*)(\r\n|\n)/
description_regex = /Description : (.*)(\r\n|\n)/
feature_info = {
name: result.match(feature_name_regex).captures[0].chomp,
description: result.match(description_regex).captures[0].chomp,
installed: true,
}
end
feature_info[:method] = :dism
feature_info
end
def info_via_powershell(feature)
features_cmd = "Get-WindowsFeature | Where-Object {$_.Name -eq '#{feature}' -or $_.DisplayName -eq '#{feature}'} | Select-Object -Property Name,DisplayName,Description,Installed,InstallState | ConvertTo-Json"
cmd = inspec.command(features_cmd)
feature_info = {}
# The `Get-WindowsFeature` command is not available on the Windows
# non-server OS. This attempts to use the `dism` command to get the info.
if cmd.stderr =~ /The term 'Get-WindowsFeature' is not recognized/
feature_info[:name] = feature
feature_info[:error] = 'Could not find `Get-WindowsFeature`'
else
# We cannot rely on `cmd.exit_status != 0` because by default the
# command will exit 1 even on success. So, if we cannot parse the JSON
# we know that the feature is not installed.
begin
result = JSON.parse(cmd.stdout)
feature_info = {
name: result['Name'],
description: result['Description'],
installed: result['Installed'],
}
rescue JSON::ParserError => _e
feature_info[:name] = feature
feature_info[:installed] = false
end
end
feature_info[:method] = :powershell
feature_info
end
end
end

View file

@ -272,7 +272,9 @@ class MockLoader
'(choco list --local-only --exact --include-programs --limit-output \'git\') -Replace "\|", "=" | ConvertFrom-StringData | ConvertTo-JSON' => empty.call,
"New-Object -Type PSObject | Add-Member -MemberType NoteProperty -Name Service -Value (Get-Service -Name 'dhcp'| Select-Object -Property Name, DisplayName, Status) -PassThru | Add-Member -MemberType NoteProperty -Name WMI -Value (Get-WmiObject -Class Win32_Service | Where-Object {$_.Name -eq 'dhcp' -or $_.DisplayName -eq 'dhcp'} | Select-Object -Property StartMode) -PassThru | ConvertTo-Json" => cmd.call('get-service-dhcp'),
"New-Object -Type PSObject | Add-Member -MemberType NoteProperty -Name Pip -Value (Invoke-Command -ScriptBlock {where.exe pip}) -PassThru | Add-Member -MemberType NoteProperty -Name Python -Value (Invoke-Command -ScriptBlock {where.exe python}) -PassThru | ConvertTo-Json" => cmd.call('get-windows-pip-package'),
"Get-WindowsFeature | Where-Object {$_.Name -eq 'dhcp' -or $_.DisplayName -eq 'dhcp'} | Select-Object -Property Name,DisplayName,Description,Installed,InstallState | ConvertTo-Json" => cmd.call('get-windows-feature'),
"Get-WindowsFeature | Where-Object {$_.Name -eq 'DHCP' -or $_.DisplayName -eq 'DHCP'} | Select-Object -Property Name,DisplayName,Description,Installed,InstallState | ConvertTo-Json" => cmd.call('get-windows-feature'),
"Get-WindowsFeature | Where-Object {$_.Name -eq 'IIS-WebServer' -or $_.DisplayName -eq 'IIS-WebServer'} | Select-Object -Property Name,DisplayName,Description,Installed,InstallState | ConvertTo-Json" => cmd_exit_1.call('get-windows-feature-iis-webserver'),
"dism /online /get-featureinfo /featurename:IIS-WebServer" => cmd.call('dism-iis-webserver'),
'lsmod' => cmd.call('lsmod'),
'/sbin/sysctl -q -n net.ipv4.conf.all.forwarding' => cmd.call('sbin_sysctl'),
# ports on windows

View file

@ -0,0 +1,20 @@
Deployment Image Servicing and Management tool
Version: 10.0.16299.15
Image Version: 10.0.16299.15
Feature Information:
Feature Name : IIS-WebServer
Display Name : World Wide Web Services
Description : Installs the IIS 10.0 World Wide Web Services. Provides support for HTML web sites and optional support for ASP.NET, Classic ASP, and web server extensions.
Restart Required : Possible
State : Disabled
Custom Properties:
(No custom properties found)
The operation completed successfully.

View file

@ -0,0 +1,7 @@
The term 'Get-WindowsFeature' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:1 char:1
+ Get-WindowsFeature | Where-Object {$_.Name -eq 'IIS-Webserver' -or $_ ...
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (Get-WindowsFeature:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException

View file

@ -5,13 +5,63 @@
require 'helper'
require 'inspec/resource'
describe 'Inspec::Resources::Feature' do
describe 'feature' do
it 'verify windows feature parsing' do
resource = MockLoader.new(:windows).load_resource('windows_feature', 'dhcp')
pkg = { name: 'DHCP', description: 'Dynamic Host Configuration Protocol (DHCP) Server enables you to centrally configure, manage, and provide temporary IP addresses and related information for client computers.', installed: false, type: 'windows-feature' }
_(resource.info).must_equal pkg
_(resource.installed?).must_equal false
end
describe 'Inspec::Resources::WindowsFeature' do
it 'can retrieve feature info using PowerShell' do
resource = MockLoader.new(:windows).load_resource(
'windows_feature',
'DHCP',
:powershell,
)
params = {
name: 'DHCP',
description: 'Dynamic Host Configuration Protocol (DHCP) Server enables you to centrally configure, manage, and provide temporary IP addresses and related information for client computers.',
installed: false,
method: :powershell,
}
_(resource.info).must_equal params
_(resource.installed?).must_equal false
end
it 'can retrieve feature info using DISM' do
resource = MockLoader.new(:windows).load_resource(
'windows_feature',
'IIS-WebServer',
:dism,
)
params = {
name: 'IIS-WebServer',
description: 'Installs the IIS 10.0 World Wide Web Services. Provides support for HTML web sites and optional support for ASP.NET, Classic ASP, and web server extensions.',
installed: true,
method: :dism,
}
_(resource.info).must_equal params
_(resource.installed?).must_equal true
end
it 'uses DISM when Get-WindowsFeature does not exist' do
resource = MockLoader.new(:windows)
.load_resource('windows_feature', 'IIS-WebServer')
params = {
name: 'IIS-WebServer',
description: 'Installs the IIS 10.0 World Wide Web Services. Provides support for HTML web sites and optional support for ASP.NET, Classic ASP, and web server extensions.',
installed: true,
method: :dism,
}
_(resource.info).must_equal params
_(resource.installed?).must_equal true
end
it 'fails the resource if PowerShell method is used but command not found' do
resource = MockLoader.new(:windows).load_resource(
'windows_feature',
'IIS-WebServer',
:powershell,
)
e = proc {
resource.info
}.must_raise(Inspec::Exceptions::ResourceFailed)
e.message.must_match(/Could not find `Get-WindowsFeature`/)
end
end