mirror of
https://github.com/inspec/inspec
synced 2024-11-10 15:14:23 +00:00
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:
parent
2245bba021
commit
f2d64938b7
6 changed files with 197 additions and 64 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
20
test/unit/mock/cmd/dism-iis-webserver
Normal file
20
test/unit/mock/cmd/dism-iis-webserver
Normal 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.
|
||||
|
7
test/unit/mock/cmd/get-windows-feature-iis-webserver
Normal file
7
test/unit/mock/cmd/get-windows-feature-iis-webserver
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue