From d7fad68541ab5fb83365345ee8020cbb62e2cece Mon Sep 17 00:00:00 2001 From: jtimberman Date: Fri, 3 Feb 2017 13:42:55 -0700 Subject: [PATCH] add "packages" resource This pull request adds a packages resource so that we can check for pattern matches against all the packages on a system. This initially implements only dpkg support for debian-based platforms so we can cover this use case: ```ruby describe packages(/^xserver-xorg.*/) do its("list") { should be_empty } end ``` This uses FilterTable so we can supply additional queries, too. ```ruby describe packages(/vi.+/).where { status != 'installed' } do its('statuses') { should be_empty } end ``` Users can specify the name as a string or a regular expression. If it is a string, we will escape it and convert it to a regular expression to use in matching against the full returned list of packages. If it is a regular expression, we take that as is and use it to filter the results. While some package management systems such as `dpkg` can take a shell glob argument to filter their results, we eschew this and require a regular expression to match multiple package names because we will need this to work across other platforms in the future. This means that the following: ```ruby packages("vim") ``` Will return *all* the "vim" packages on the system. The `packages` resource will take `"vim"`, turn it into `/vim/`, and greedily match anything with "vim" in the name. To match only a single package named `vim`, it needs to be an anchored regular expression. ```ruby packages(/^vim$/) ``` Signed-off-by: Joshua Timberman Use entries instead of list Added a few more tests and non installed package in output Signed-off-by: Alex Pop fix lint Signed-off-by: Alex Pop Signed-off-by: Joshua Timberman --- lib/inspec/resource.rb | 1 + lib/resources/packages.rb | 86 ++++++++++++++++++++++++++++ test/helper.rb | 3 + test/unit/mock/cmd/dpkg-query-W | 12 ++++ test/unit/resources/packages_test.rb | 59 +++++++++++++++++++ 5 files changed, 161 insertions(+) create mode 100644 lib/resources/packages.rb create mode 100644 test/unit/mock/cmd/dpkg-query-W create mode 100644 test/unit/resources/packages_test.rb diff --git a/lib/inspec/resource.rb b/lib/inspec/resource.rb index 14e90ea2b..9591a0bc8 100644 --- a/lib/inspec/resource.rb +++ b/lib/inspec/resource.rb @@ -107,6 +107,7 @@ require 'resources/oneget' require 'resources/os' require 'resources/os_env' require 'resources/package' +require 'resources/packages' require 'resources/parse_config' require 'resources/passwd' require 'resources/pip' diff --git a/lib/resources/packages.rb b/lib/resources/packages.rb new file mode 100644 index 000000000..7668a4721 --- /dev/null +++ b/lib/resources/packages.rb @@ -0,0 +1,86 @@ +# encoding: utf-8 +# copyright: 2017, Chef Software, Inc. +# author: Joshua Timberman +# author: Alex Pop +# license: All rights reserved + +require 'utils/filter' + +module Inspec::Resources + class Packages < Inspec.resource(1) + name 'packages' + desc 'Use the packages InSpec audit resource to test properties for multiple packages installed on the system' + example " + describe packages(/xserver-xorg.*/) do + its('entries') { should be_empty } + end + describe packages('vim').entries.length do + it { should be > 1 } + end + describe packages(/vi.+/).where { status != 'installed' } do + its('statuses') { should be_empty } + end + " + + def initialize(pattern) + @pattern = pattern_regexp(pattern) + all_pkgs = package_list + @list = all_pkgs.find_all do |hm| + hm[:name] =~ pattern_regexp(pattern) + end + end + + def to_s + "Packages #{@pattern.class == String ? @pattern : @pattern.inspect}" + end + + filter = FilterTable.create + filter.add_accessor(:where) + .add_accessor(:entries) + .add(:statuses, field: 'status', style: :simple) + .add(:names, field: 'name') + .add(:versions, field: 'version') + .connect(self, :filtered_packages) + + private + + def pattern_regexp(p) + if p.class == String + Regexp.new(Regexp.escape(p)) + elsif p.class == Regexp + p + else + fail 'invalid name argument to packages resource, please use a "string" or /regexp/' + end + end + + def filtered_packages + @list + end + + def package_list + os = inspec.os + + if os.debian? + command = "dpkg-query -W -f='${db:Status-Abbrev} ${Package} ${Version}\\n'" + else + fail "packages resource is not yet supported on #{os.name}" + end + build_package_list(command) + end + + Package = Struct.new(:status, :name, :version) + + def build_package_list(command) + cmd = inspec.command(command) + all = cmd.stdout.split("\n")[1..-1] + return [] if all.nil? + all.map do |m| + a = m.split + a[0] = 'installed' if a[0] =~ /^.i/ + a[2] = a[2].split(':').last + Package.new(*a) + end + end + end +end diff --git a/test/helper.rb b/test/helper.rb index 9466e614d..38d09cb7c 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -50,6 +50,7 @@ class MockLoader ubuntu1204: { name: 'ubuntu', family: 'debian', release: '12.04', arch: 'x86_64' }, ubuntu1404: { name: 'ubuntu', family: 'debian', release: '14.04', arch: 'x86_64' }, ubuntu1504: { name: 'ubuntu', family: 'debian', release: '15.04', arch: 'x86_64' }, + ubuntu1604: { name: 'ubuntu', family: 'debian', release: '16.04', arch: 'x86_64' }, mint17: { name: 'linuxmint', family: 'debian', release: '17.3', arch: 'x86_64' }, mint18: { name: 'linuxmint', family: 'debian', release: '18', arch: 'x86_64' }, windows: { name: 'windows', family: 'windows', release: '6.2.9200', arch: 'x86_64' }, @@ -249,6 +250,8 @@ class MockLoader 'pkginfo -l SUNWzfsr' => cmd.call('pkginfo-l-SUNWzfsr'), # solaris 11 package manager 'pkg info system/file-system/zfs' => cmd.call('pkg-info-system-file-system-zfs'), + # dpkg-query package list + "dpkg-query -W -f='${db:Status-Abbrev} ${Package} ${Version}\\n'" => cmd.call('dpkg-query-W'), # port netstat on solaris 10 & 11 'netstat -an -f inet -f inet6' => cmd.call('s11-netstat-an-finet-finet6'), # xinetd configuration diff --git a/test/unit/mock/cmd/dpkg-query-W b/test/unit/mock/cmd/dpkg-query-W new file mode 100644 index 000000000..71a9165c8 --- /dev/null +++ b/test/unit/mock/cmd/dpkg-query-W @@ -0,0 +1,12 @@ +ii bash 4.3-14ubuntu1.1 +rc fakeroot 1.20.2-1ubuntu1 +rc libfakeroot 1.20.2-1ubuntu1 +ii overlayroot 0.27ubuntu1.2 +ii vim 2:7.4.1689-3ubuntu1.2 +ii vim-common 2:7.4.1689-3ubuntu1.2 +ii xorg 1:7.7+13ubuntu3 +ii xorg-docs-core 1:1.7.1-1ubuntu1 +ii xserver-common 2:1.18.4-0ubuntu0.2 +ii xserver-xorg 1:7.7+13ubuntu3 +ii xserver-xorg-core 2:1.18.4-0ubuntu0.2 +ii xserver-xorg-input-all 1:7.7+13ubuntu3 diff --git a/test/unit/resources/packages_test.rb b/test/unit/resources/packages_test.rb new file mode 100644 index 000000000..60e37b18a --- /dev/null +++ b/test/unit/resources/packages_test.rb @@ -0,0 +1,59 @@ +# encoding: utf-8 +# author: Joshua Timberman + +require 'helper' +require 'inspec/resource' + +describe 'Inspec::Resources::Packages' do + it 'verify packages resource' do + resource = MockLoader.new(:ubuntu1604).load_resource('packages', /^vim$/) + _(resource.entries.length).must_equal 1 + _(resource.entries[0].to_h).must_equal({ + status: 'installed', + name: 'vim', + version: '7.4.1689-3ubuntu1.2', + }) + end + + it 'package name matches with output (string)' do + resource = MockLoader.new(:ubuntu1604).load_resource('packages', 'xserver-xorg') + _(resource.to_s).must_equal 'Packages /xserver\\-xorg/' + end + + it 'packages using where filters' do + resource = MockLoader.new(:ubuntu1604).load_resource('packages', /.+root$/) + _(resource.entries.length).must_equal 3 + _(resource.where { status != 'installed' }.names).must_equal(['fakeroot', 'libfakeroot']) + _(resource.where { version =~ /^0\.2.+/ }.entries[0].to_h).must_equal({ + status: "installed", + name: "overlayroot", + version: "0.27ubuntu1.2", + }) + end + + it 'package name matches with output (regex)' do + resource = MockLoader.new(:ubuntu1604).load_resource('packages', /vim/) + _(resource.to_s).must_equal 'Packages /vim/' + end + + it 'returns a list of packages with a wildcard' do + resource = MockLoader.new(:ubuntu1604).load_resource('packages', /^xserver-xorg.*/) + _(resource.statuses).must_equal ['installed'] + _(resource.entries.length).must_equal 3 + end + + + it 'fails on non debian platforms' do + proc { + resource = MockLoader.new(:centos6).load_resource('packages', 'bash') + resource.send(:entries, nil) + }.must_raise(RuntimeError) + end + + it 'fails if the packages name is not a string or regexp' do + proc { + resources = MockLoader.new(:ubuntu1604).load_resource('packages', [:a, :b]) + resources.send(:entries, nil) + }.must_raise(RuntimeError) + end +end