Merge pull request #5981 from inspec/ss/enhance-service-resource

CFINSPEC-93: Enhance `service` resource
This commit is contained in:
Clinton Wolfe 2022-04-18 18:04:16 -04:00 committed by GitHub
commit 7fddb8c772
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 160 additions and 26 deletions

View file

@ -11,55 +11,62 @@ platform = "os"
parent = "inspec/resources/os" parent = "inspec/resources/os"
+++ +++
Use the `service` Chef InSpec audit resource to test if the named service is installed, running and/or enabled. Use the `service` Chef InSpec audit resource to test whether the installation of the named service is successful and enabled.
Under some circumstances, it may be necessary to specify the service manager by using one of the following service manager-specific resources: `bsd_service`, `launchd_service`, `runit_service`, `systemd_service`, `sysv_service`, or `upstart_service`. These resources are based on the `service` resource. It may be necessary to specify the service manager by using one of the following service manager-specific resources: `bsd_service`, `launchd_service`, `runit_service`, `systemd_service`, `sysv_service`, and `upstart_service`. These resources are based on the `service` resource.
## Availability ## Availability
### Installation ### Installation
This resource is distributed along with Chef InSpec itself. You can use it automatically. The Chef InSpec distributes this resource.
### Version ### Version
This resource first became available in v1.0.0 of InSpec. This resource is available from Chef Inspec version 1.0.0.
## Syntax ## Syntax
A `service` resource block declares the name of a service and then one (or more) matchers to test the state of the service: A `service` resource block declares the name of a service and one or more matchers to test the service state.
describe service('service_name') do ```ruby
describe service('NAME') do
it { should be_installed } it { should be_installed }
it { should be_enabled } it { should be_enabled }
it { should be_running } it { should be_running }
end end
```
where > where
>
- `('service_name')` must specify a service name > - `('service_name')` must specify a service name
- `be_installed`, `be_enabled`, and `be_running` are valid matchers for this resource > - `be_installed`, `be_enabled`, and `be_running` are valid matchers for this resource
## Examples ## Examples
The following examples show how to use this Chef InSpec audit resource. The following examples show how to use this Chef InSpec audit resource.
### Test if the postgresql service is both running and enabled ### Test if the PostgreSQL service is both running and enabled
describe service('postgresql') do ```ruby
describe service('PostgreSQL') do
it { should be_enabled } it { should be_enabled }
it { should be_running } it { should be_running }
end end
```
### Test if the mysql service is both running and enabled ### Test if the MYSQL service is running and enabled
describe service('mysqld') do ```ruby
describe service('MYSQL') do
it { should be_enabled } it { should be_enabled }
it { should be_running } it { should be_running }
end end
```
### Test if ClamAV (an antivirus engine) is installed and running ### Test if the installation of ClamAV (an antivirus engine) is successful and running
```ruby
describe package('clamav') do describe package('clamav') do
it { should be_installed } it { should be_installed }
its('version') { should eq '0.98.7' } its('version') { should eq '0.98.7' }
@ -70,62 +77,93 @@ The following examples show how to use this Chef InSpec audit resource.
it { should_not be_installed } it { should_not be_installed }
it { should_not be_running } it { should_not be_running }
end end
```
### Test Unix System V run levels ### Test Unix SystemV run levels
On targets that are using SystemV services, the existing run levels can also be checked: On targets that are using SystemV services, the existing run levels can also be checked:
describe service('sshd').runlevels do ```ruby
describe service('SSH').runlevels do
its('keys') { should include(2) } its('keys') { should include(2) }
end end
describe service('sshd').runlevels(2,4) do describe service('SSH').runlevels(2,4) do
it { should be_enabled } it { should be_enabled }
end end
```
### Override the service manager ### Override the service manager
Under some circumstances, it may be required to override the logic in place to select the right service manager. For example, to check a service managed by Upstart: It may be required to override the logic to select the right service manager. For example, to check a service managed by Upstart.
describe upstart_service('service') do ```ruby
describe upstart_service('SERVICE') do
it { should_not be_enabled } it { should_not be_enabled }
it { should be_installed } it { should be_installed }
it { should be_running } it { should be_running }
end end
```
This is also possible with `systemd_service`, `runit_service`, `sysv_service`, `bsd_service`, and `launchd_service`. Provide the control command when it is not to be found at the default location. For example, if the `sv` command for services managed by runit is not in the `PATH`: This is also possible with `systemd_service`, `runit_service`, `sysv_service`, `bsd_service`, and `launchd_service`. If not found at the default location, provide the **control** command. For example, if the `sv` command for services managed by `runit` is not in the `PATH`.
describe runit_service('service', '/opt/chef/embedded/sbin/sv') do ```ruby
describe runit_service('SERVICE', '/opt/chef/embedded/sbin/sv') do
it { should be_enabled } it { should be_enabled }
it { should be_installed } it { should be_installed }
it { should be_running } it { should be_running }
end end
```
### Verify that IIS is running ### Verify IIS is running
```ruby
describe service('W3SVC') do describe service('W3SVC') do
it { should be_installed } it { should be_installed }
it { should be_running } it { should be_running }
end end
```
## Matchers ## Matchers
For a full list of available matchers, please visit our [matchers page](/inspec/matchers/). For a full list of available matchers, please visit the [matchers page](/inspec/matchers/).
### be_enabled ### be_enabled
The `be_enabled` matcher tests if the named service is enabled: The `be_enabled` matcher tests if the named service is enabled:
```ruby
it { should be_enabled } it { should be_enabled }
```
### be_installed ### be_installed
The `be_installed` matcher tests if the named service is installed: The `be_installed` matcher tests if the named service is installed.
```ruby
it { should be_installed } it { should be_installed }
```
### be_running ### be_running
The `be_running` matcher tests if the named service is running: The `be_running` matcher tests if the named service is running.
```ruby
it { should be_running } it { should be_running }
```
### be_monitored_by
The `be_monitored_by` matcher accepts the name of a monitoring tool as input and test if the named service is monitored respectively. The monitoring tool supports `monit` and `god` resources.
```ruby
it { should be_monitored_by("god") }
```
### have_start_mode
The `have_start_mode` matcher tests accept a mode as input and test if the named service's start mode is the same as specified in the input. This matcher is supported on the Windows systems only.
```ruby
it { should have_start_mode('Manual') }
```

View file

@ -271,6 +271,30 @@ module Inspec::Resources
info[:startname] info[:startname]
end end
# matcher equivalent to startmode property; compares start-up mode
# supported only on windows.
def has_start_mode?(mode)
raise Inspec::Exceptions::ResourceSkipped, "The `has_start_mode` matcher is not supported on your OS yet." unless inspec.os.windows?
mode == startmode
end
# matcher to check if the service is monitored by the given monitoring tool/software
def monitored_by?(monitoring_tool)
# Currently supported monitoring tools are: monit & god
# To add support for new monitoring tools, extend the case statement with additional monitoring tool and
# add the definition and logic in a new class (inheriting the base class MonitoringTool: optional)
case monitoring_tool
when "monit"
current_monitoring_tool = Monit.new(inspec, @service_name)
when "god"
current_monitoring_tool = God.new(inspec, @service_name)
else
puts "The monitoring tool #{monitoring_tool} is not yet supported by InSpec."
end
current_monitoring_tool.is_service_monitored?
end
def to_s def to_s
"Service #{@service_name}" "Service #{@service_name}"
end end
@ -893,4 +917,53 @@ module Inspec::Resources
Runit.new(inspec, service_ctl) Runit.new(inspec, service_ctl)
end end
end end
# Helper class for monitored_by matcher
class MonitoringTool
attr_reader :inspec, :service_name
def initialize(inspec, service_name)
@inspec = inspec
@service_name ||= service_name
end
def find_utility_or_error(utility_name)
[ "/usr/sbin/#{utility_name}" , "/sbin/#{utility_name}" , "/usr/bin/#{utility_name}" , "/bin/#{utility_name}" , "#{utility_name}" ].each do |cmd|
return cmd if inspec.command(cmd).exist?
end
raise Inspec::Exceptions::ResourceFailed, "Could not find `#{utility_name}`"
end
end
class Monit < MonitoringTool
def is_service_monitored?
utility = find_utility_or_error("monit")
utility_cmd = inspec.command("#{utility} summary")
raise Inspec::Exceptions::ResourceFailed, "Executing #{utility} summary failed: #{utility_cmd.stderr}" if utility_cmd.exit_status.to_i != 0
monitoring_info = utility_cmd.stdout.split("\n")
monitoring_info.map! { |info| info.strip.squeeze(" ") }
is_monitored = false
monitoring_info.each do |info|
if info =~ /^#{service_name} OK.*/
is_monitored = true
break
end
end
is_monitored
end
end
class God < MonitoringTool
def is_service_monitored?
utility = find_utility_or_error("god")
utility_cmd = inspec.command("#{utility} status #{service_name}")
raise Inspec::Exceptions::ResourceFailed, "Executing #{utility} status #{service_name} failed: #{utility_cmd.stderr}" if utility_cmd.exit_status.to_i != 0
monitoring_info = utility_cmd.stdout.strip
monitoring_info =~ /^#{service_name}: up/
end
end
end end

4
test/fixtures/cmd/monit-summary vendored Normal file
View file

@ -0,0 +1,4 @@
Monit 5.26.0 uptime: 3d 17h 59m
Service Name Status Type
ip-172-31-83-240 OK System
ssh OK Process

View file

@ -316,6 +316,9 @@ class MockLoader
"service sendmail onestatus" => cmd.call("service-sendmail-onestatus"), "service sendmail onestatus" => cmd.call("service-sendmail-onestatus"),
# mock for FreeBSD10Init info # mock for FreeBSD10Init info
"service -l" => cmd.call("service-l"), "service -l" => cmd.call("service-l"),
# service mock for monit
"monit summary" => cmd.call("monit-summary"),
%{sh -c 'type "monit"'} => empty.call,
# services for system 5 e.g. centos6, debian 6 # services for system 5 e.g. centos6, debian 6
"service sshd status" => cmd.call("service-sshd-status"), "service sshd status" => cmd.call("service-sshd-status"),
'find /etc/rc*.d /etc/init.d/rc*.d -name "S*"' => cmd.call("find-etc-rc-d-name-S"), 'find /etc/rc*.d /etc/init.d/rc*.d -name "S*"' => cmd.call("find-etc-rc-d-name-S"),

View file

@ -544,5 +544,21 @@ describe "Inspec::Resources::Service" do
it "checks disabled false if some services are not disabled" do it "checks disabled false if some services are not disabled" do
_(service.runlevels(0, 4).enabled?).must_equal false _(service.runlevels(0, 4).enabled?).must_equal false
end end
# windows
it "verify serverspec compatible matchers on windows" do
resource = MockLoader.new(:windows).load_resource("service", "dhcp")
_(resource.name).must_equal "dhcp"
_(resource.has_start_mode?("Auto")).must_equal true
end
# ubuntu
it "verify serverspec compatible matchers on ubuntu" do
resource = MockLoader.new(:ubuntu1404).load_resource("service", "ssh")
_(resource.name).must_equal "ssh"
_(resource.monitored_by?("monit")).must_equal true
ex = _ { resource.has_start_mode?("Auto") }.must_raise(Inspec::Exceptions::ResourceSkipped)
_(ex.message).must_include "The `has_start_mode` matcher is not supported on your OS yet."
end
end end
end end