File Manager
Viewing File: package.rb
require "inspec/resources/directory"
require "inspec/utils/simpleconfig"
# 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 <<~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
EXAMPLE
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)
elsif ["freebsd"].include?(os[:name])
@pkgman = FreebsdPkg.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 unless @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").delete_if { |e| e =~ /^WARNING/i }
pkg = pkg_info[0].split(" - ")[0]
{
name: pkg.partition("-")[0],
installed: true,
version: pkg.partition("-")[2],
type: "pkg",
}
end
end
class FreebsdPkg < PkgManagement
def 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,
version: params["Version"],
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}\.*" -or $_.PSChildName -match "^\s*#{package_name.shellescape}\.*" } |
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