mirror of
https://github.com/inspec/inspec
synced 2024-11-15 01:17:08 +00:00
b93da53237
* Escaping package names for windows packages * Fixing missed package_name ref * Updating Mock SHA * Removing unwanted file * Linting fix Signed-off-by: Noel Georgi <18496730+frezbo@users.noreply.github.com>
388 lines
11 KiB
Ruby
388 lines
11 KiB
Ruby
# encoding: utf-8
|
|
|
|
# Resource to determine package information
|
|
#
|
|
# Usage:
|
|
# describe package('nginx') do
|
|
# it { should be_installed }
|
|
# end
|
|
module Inspec::Resources
|
|
class Package < Inspec.resource(1)
|
|
name 'package'
|
|
supports platform: 'unix'
|
|
supports platform: 'windows'
|
|
desc 'Use the package InSpec audit resource to test if the named package and/or package version is installed on the system.'
|
|
example "
|
|
describe package('nginx') do
|
|
it { should be_installed }
|
|
it { should_not be_held } # for dpkg platforms that support holding a version from being upgraded
|
|
its('version') { should eq 1.9.5 }
|
|
end
|
|
"
|
|
def initialize(package_name, opts = {}) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
@package_name = package_name
|
|
@name = @package_name
|
|
@cache = nil
|
|
# select package manager
|
|
@pkgman = nil
|
|
|
|
os = inspec.os
|
|
if os.debian?
|
|
@pkgman = Deb.new(inspec)
|
|
elsif os.redhat? || %w{suse amazon fedora}.include?(os[:family])
|
|
@pkgman = Rpm.new(inspec, opts)
|
|
elsif ['arch'].include?(os[:name])
|
|
@pkgman = Pacman.new(inspec)
|
|
elsif ['darwin'].include?(os[:family])
|
|
@pkgman = Brew.new(inspec)
|
|
elsif os.windows?
|
|
@pkgman = WindowsPkg.new(inspec)
|
|
elsif ['aix'].include?(os[:family])
|
|
@pkgman = BffPkg.new(inspec)
|
|
elsif os.solaris?
|
|
@pkgman = SolarisPkg.new(inspec)
|
|
elsif ['hpux'].include?(os[:family])
|
|
@pkgman = HpuxPkg.new(inspec)
|
|
elsif ['alpine'].include?(os[:name])
|
|
@pkgman = AlpinePkg.new(inspec)
|
|
else
|
|
raise Inspec::Exceptions::ResourceSkipped, 'The `package` resource is not supported on your OS yet.'
|
|
end
|
|
|
|
evaluate_missing_requirements
|
|
end
|
|
|
|
# returns true if the package is installed
|
|
def installed?(_provider = nil, _version = nil)
|
|
info[:installed] == true
|
|
end
|
|
|
|
# returns true it the package is held (if the OS supports it)
|
|
def held?(_provider = nil, _version = nil)
|
|
info[:held] == true
|
|
end
|
|
|
|
# returns the package description
|
|
def info
|
|
return @cache if !@cache.nil?
|
|
# All `@pkgman.info` methods return `{}`. This matches that
|
|
# behavior if `@pkgman` can't be determined, thus avoiding the
|
|
# `undefined method 'info' for nil:NilClass` error
|
|
return {} if @pkgman.nil?
|
|
@pkgman.info(@package_name)
|
|
end
|
|
|
|
# return the package version
|
|
def version
|
|
info = @pkgman.info(@package_name)
|
|
info[:version]
|
|
end
|
|
|
|
def to_s
|
|
"System Package #{@package_name}"
|
|
end
|
|
|
|
private
|
|
|
|
def evaluate_missing_requirements
|
|
missing_requirements_string = @pkgman.missing_requirements.uniq.join(', ')
|
|
return if missing_requirements_string.empty?
|
|
raise Inspec::Exceptions::ResourceSkipped, "The following requirements are not met for this resource: #{missing_requirements_string}"
|
|
end
|
|
end
|
|
|
|
class PkgManagement
|
|
attr_reader :inspec
|
|
def initialize(inspec)
|
|
@inspec = inspec
|
|
end
|
|
|
|
def missing_requirements
|
|
# Each provider can provide an Array of missing requirements that will be
|
|
# combined into a `ResourceSkipped` exception message.
|
|
[]
|
|
end
|
|
end
|
|
|
|
# Debian / Ubuntu
|
|
class Deb < PkgManagement
|
|
def info(package_name)
|
|
cmd = inspec.command("dpkg -s #{package_name}")
|
|
return {} if cmd.exit_status.to_i != 0
|
|
|
|
params = SimpleConfig.new(
|
|
cmd.stdout.chomp,
|
|
assignment_regex: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
|
|
multiple_values: false,
|
|
).params
|
|
# If the package is installed, Status is "install ok installed"
|
|
# If the package is installed and marked hold, Status is "hold ok installed"
|
|
# If the package is removed and not purged, Status is "deinstall ok config-files" with exit_status 0
|
|
# If the package is purged cmd fails with non-zero exit status
|
|
{
|
|
name: params['Package'],
|
|
installed: params['Status'].split(' ')[2] == 'installed',
|
|
held: params['Status'].split(' ')[0] == 'hold',
|
|
version: params['Version'],
|
|
type: 'deb',
|
|
}
|
|
end
|
|
end
|
|
|
|
# RHEL family
|
|
class Rpm < PkgManagement
|
|
def initialize(inspec, opts)
|
|
super(inspec)
|
|
|
|
@dbpath = opts.fetch(:rpm_dbpath, nil)
|
|
end
|
|
|
|
def missing_requirements
|
|
missing_requirements = []
|
|
|
|
unless @dbpath.nil? || inspec.directory(@dbpath).directory?
|
|
missing_requirements << "RPMDB #{@dbpath} does not exist"
|
|
end
|
|
|
|
missing_requirements
|
|
end
|
|
|
|
def info(package_name)
|
|
rpm_cmd = rpm_command(package_name)
|
|
cmd = inspec.command(rpm_cmd)
|
|
# CentOS does not return an error code if the package is not installed,
|
|
# therefore we need to check for emptyness
|
|
return {} if cmd.exit_status.to_i != 0 || cmd.stdout.chomp.empty?
|
|
params = SimpleConfig.new(
|
|
cmd.stdout.chomp,
|
|
assignment_regex: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
|
|
multiple_values: false,
|
|
).params
|
|
# On some (all?) systems, the linebreak before the vendor line is missing
|
|
if params['Version'] =~ /\s*Vendor:/
|
|
v = params['Version'].split(' ')[0]
|
|
else
|
|
v = params['Version']
|
|
end
|
|
# On some (all?) systems, the linebreak before the build line is missing
|
|
if params['Release'] =~ /\s*Build Date:/
|
|
r = params['Release'].split(' ')[0]
|
|
else
|
|
r = params['Release']
|
|
end
|
|
{
|
|
name: params['Name'],
|
|
installed: true,
|
|
version: "#{v}-#{r}",
|
|
type: 'rpm',
|
|
}
|
|
end
|
|
|
|
private
|
|
|
|
def rpm_command(package_name)
|
|
cmd = ''
|
|
cmd += 'rpm -qi'
|
|
cmd += " --dbpath #{@dbpath}" if @dbpath
|
|
cmd += ' ' + package_name
|
|
|
|
cmd
|
|
end
|
|
end
|
|
|
|
# MacOS / Darwin implementation
|
|
class Brew < PkgManagement
|
|
def info(package_name)
|
|
brew_path = inspec.command('brew').exist? ? 'brew' : '/usr/local/bin/brew'
|
|
cmd = inspec.command("#{brew_path} info --json=v1 #{package_name}")
|
|
|
|
# If no available formula exists, then `brew` will exit non-zero
|
|
return {} if cmd.exit_status.to_i != 0
|
|
|
|
pkg = JSON.parse(cmd.stdout)[0]
|
|
|
|
# If package exists but is not installed, then `brew` output will not
|
|
# contain `pkg['installed'][0]['version']
|
|
return {} unless pkg.dig('installed', 0, 'version')
|
|
|
|
{
|
|
name: pkg['name'],
|
|
installed: true,
|
|
version: pkg['installed'][0]['version'],
|
|
type: 'brew',
|
|
}
|
|
rescue JSON::ParserError => e
|
|
raise Inspec::Exceptions::ResourceFailed,
|
|
'Failed to parse JSON from `brew` command. ' \
|
|
"Error: #{e}"
|
|
end
|
|
end
|
|
|
|
# Arch Linux
|
|
class Pacman < PkgManagement
|
|
def info(package_name)
|
|
cmd = inspec.command("pacman -Qi #{package_name}")
|
|
return {} if cmd.exit_status.to_i != 0
|
|
|
|
params = SimpleConfig.new(
|
|
cmd.stdout.chomp,
|
|
assignment_regex: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
|
|
multiple_values: false,
|
|
).params
|
|
|
|
{
|
|
name: params['Name'],
|
|
installed: true,
|
|
version: params['Version'],
|
|
type: 'pacman',
|
|
}
|
|
end
|
|
end
|
|
|
|
class HpuxPkg < PkgManagement
|
|
def info(package_name)
|
|
cmd = inspec.command("swlist -l product | grep #{package_name}")
|
|
return {} if cmd.exit_status.to_i != 0
|
|
pkg = cmd.stdout.strip.split(' ')
|
|
{
|
|
name: pkg[0],
|
|
installed: true,
|
|
version: pkg[1],
|
|
type: 'pkg',
|
|
}
|
|
end
|
|
end
|
|
|
|
class AlpinePkg < PkgManagement
|
|
def info(package_name)
|
|
cmd = inspec.command("apk info -vv --no-network | grep #{package_name}")
|
|
return {} if cmd.exit_status.to_i != 0
|
|
|
|
pkg_info = cmd.stdout.split("\n").reject! { |e| e =~ /^WARNING/i }
|
|
pkg = pkg_info[0].split(' - ')[0]
|
|
|
|
{
|
|
name: pkg.partition('-')[0],
|
|
installed: true,
|
|
version: pkg.partition('-')[2],
|
|
type: 'pkg',
|
|
}
|
|
end
|
|
end
|
|
|
|
# Determines the installed packages on Windows using the Windows package registry entries.
|
|
# @see: http://blogs.technet.com/b/heyscriptingguy/archive/2013/11/15/use-powershell-to-find-installed-software.aspx
|
|
class WindowsPkg < PkgManagement
|
|
def info(package_name)
|
|
search_paths = [
|
|
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
|
|
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
|
|
]
|
|
|
|
# add 64 bit search paths
|
|
if inspec.os.arch == 'x86_64'
|
|
search_paths << 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
|
|
search_paths << 'HKCU:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
|
|
end
|
|
|
|
# Find the package
|
|
cmd = inspec.command <<-EOF.gsub(/^\s*/, '')
|
|
Get-ItemProperty (@("#{search_paths.join('", "')}") | Where-Object { Test-Path $_ }) |
|
|
Where-Object { $_.DisplayName -match "^\\s*#{package_name.shellescape}\\s*$" -or $_.PSChildName -match "^\\s*#{package_name.shellescape}\\s*$" } |
|
|
Select-Object -Property DisplayName,DisplayVersion | ConvertTo-Json
|
|
EOF
|
|
|
|
# We cannot rely on `exit_status` since PowerShell always exits 0 from the
|
|
# above command. Instead, if no package is found the output of the command
|
|
# will be `''` so we can use that to return `{}` to match the behavior of
|
|
# other package managers.
|
|
return {} if cmd.stdout == ''
|
|
|
|
begin
|
|
package = JSON.parse(cmd.stdout)
|
|
rescue JSON::ParserError => e
|
|
raise Inspec::Exceptions::ResourceFailed,
|
|
'Failed to parse JSON from PowerShell. ' \
|
|
"Error: #{e}"
|
|
end
|
|
|
|
# What if we match multiple packages? just pick the first one for now.
|
|
package = package[0] if package.is_a?(Array)
|
|
|
|
{
|
|
name: package['DisplayName'],
|
|
installed: true,
|
|
version: package['DisplayVersion'],
|
|
type: 'windows',
|
|
}
|
|
end
|
|
end
|
|
|
|
# AIX
|
|
class BffPkg < PkgManagement
|
|
def info(package_name)
|
|
cmd = inspec.command("lslpp -cL #{package_name}")
|
|
return {} if cmd.exit_status.to_i != 0
|
|
|
|
bff_pkg = cmd.stdout.split("\n").last.split(':')
|
|
{
|
|
name: bff_pkg[1],
|
|
installed: true,
|
|
version: bff_pkg[2],
|
|
type: 'bff',
|
|
}
|
|
end
|
|
end
|
|
|
|
# Solaris
|
|
class SolarisPkg < PkgManagement
|
|
def info(package_name)
|
|
if inspec.os[:release].to_i <= 10
|
|
solaris10_info(package_name)
|
|
else
|
|
solaris11_info(package_name)
|
|
end
|
|
end
|
|
|
|
# solaris 10
|
|
def solaris10_info(package_name)
|
|
cmd = inspec.command("pkginfo -l #{package_name}")
|
|
return {} if cmd.exit_status.to_i != 0
|
|
|
|
params = SimpleConfig.new(
|
|
cmd.stdout.chomp,
|
|
assignment_regex: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
|
|
multiple_values: false,
|
|
).params
|
|
|
|
# parse 11.10.0,REV=2006.05.18.01.46
|
|
v = params['VERSION'].split(',')
|
|
{
|
|
name: params['PKGINST'],
|
|
installed: true,
|
|
version: v[0] + '-' + v[1].split('=')[1],
|
|
type: 'pkg',
|
|
}
|
|
end
|
|
|
|
# solaris 11
|
|
def solaris11_info(package_name)
|
|
cmd = inspec.command("pkg info #{package_name}")
|
|
return {} if cmd.exit_status.to_i != 0
|
|
|
|
params = SimpleConfig.new(
|
|
cmd.stdout.chomp,
|
|
assignment_regex: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
|
|
multiple_values: false,
|
|
).params
|
|
|
|
{
|
|
name: params['Name'],
|
|
installed: true,
|
|
# 0.5.11-0.175.3.1.0.5.0
|
|
version: "#{params['Version']}-#{params['Branch']}",
|
|
type: 'pkg',
|
|
}
|
|
end
|
|
end
|
|
end
|