# encoding: utf-8 # author: Christoph Hartmann # author: Dominik Richter # author: Stephan Renatus # license: All rights reserved require 'hashie' module Inspec::Resources class Runlevels < Hash attr_accessor :owner def self.from_hash(owner, hash = {}, filter = nil) res = Runlevels.new(owner) filter = filter.first if filter.is_a?(Array) && filter.length <= 1 ks = case filter when nil hash.keys when Regexp hash.keys.find_all { |x| x.to_s =~ filter } when Array f = filter.map(&:to_s) hash.keys.find_all { |x| f.include?(x.to_s) } when Numeric hash.keys.include?(filter) ? [filter] : [] else hash.keys.find_all { |x| x == filter } end ks.each { |k| res[k] = hash[k] } res end def initialize(owner, default = false) @owner = owner super(default) end def filter(f) Runlevels.from_hash(owner, self, f) end # Check if all runlevels are enabled # # @return [boolean] true if all runlevels are enabled def enabled? values.all? end # Check if all runlevels are disabled # # @return [boolean] true if all runlevels are disabled def disabled? !values.any? end def to_s "#{owner} runlevels #{keys.join(', ')}" end end # We detect the init system for each operating system, based on the operating # system. # # Fedora 15 : systemd # RedHat 7 : systemd # Ubuntu 15.04 : systemd # Ubuntu < 15.04 : upstart # # TODO: extend the logic to detect the running init system, independently of OS class Service < Inspec.resource(1) # rubocop:disable ClassLength name 'service' desc 'Use the service InSpec audit resource to test if the named service is installed, running and/or enabled.' example " describe service('service_name') do it { should be_installed } it { should be_enabled } it { should be_running } its('type') { should be 'systemd' } end describe service('service_name').runlevels(3, 5) do it { should be_enabled } end describe service('service_name').params do its('UnitFileState') { should eq 'enabled' } end " attr_reader :service_ctl def initialize(service_name, service_ctl = nil) @service_name = service_name @service_mgmt = nil @service_ctl ||= service_ctl @cache = nil @service_mgmt = select_service_mgmt return skip_resource 'The `service` resource is not supported on your OS yet.' if @service_mgmt.nil? end def select_service_mgmt # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength os = inspec.os family = os[:family] # Ubuntu # @see: https://wiki.ubuntu.com/SystemdForUpstartUsers # Ubuntu 15.04 : Systemd # Systemd runs with PID 1 as /sbin/init. # Upstart runs with PID 1 as /sbin/upstart. # Ubuntu < 15.04 : Upstart # Upstart runs with PID 1 as /sbin/init. # Systemd runs with PID 1 as /lib/systemd/systemd. if %w{ubuntu}.include?(family) version = inspec.os[:release].to_f if version < 15.04 Upstart.new(inspec, service_ctl) else Systemd.new(inspec, service_ctl) end elsif %w{debian}.include?(family) version = inspec.os[:release].to_i if version > 7 Systemd.new(inspec, service_ctl) else SysV.new(inspec, service_ctl || '/usr/sbin/service') end elsif %w{redhat fedora centos}.include?(family) version = inspec.os[:release].to_i if (%w{ redhat centos }.include?(family) && version >= 7) || (family == 'fedora' && version >= 15) Systemd.new(inspec, service_ctl) else SysV.new(inspec, service_ctl || '/sbin/service') end elsif %w{wrlinux}.include?(family) SysV.new(inspec, service_ctl) elsif %w{darwin}.include?(family) LaunchCtl.new(inspec, service_ctl) elsif os.windows? WindowsSrv.new(inspec) elsif %w{freebsd}.include?(family) BSDInit.new(inspec, service_ctl) elsif %w{arch}.include?(family) Systemd.new(inspec, service_ctl) elsif %w{suse opensuse}.include?(family) if inspec.os[:release].to_i >= 12 Systemd.new(inspec, service_ctl) else SysV.new(inspec, service_ctl || '/sbin/service') end elsif %w{aix}.include?(family) SrcMstr.new(inspec) elsif %w{amazon}.include?(family) Upstart.new(inspec, service_ctl) elsif os.solaris? Svcs.new(inspec) end end def info return nil if @service_mgmt.nil? @cache ||= @service_mgmt.info(@service_name) end # verifies if the service is enabled def enabled?(_level = nil) return false if info.nil? info[:enabled] end def params return {} if info.nil? Hashie::Mash.new(info[:params] || {}) end # verifies the service is registered def installed?(_name = nil, _version = nil) return false if info.nil? info[:installed] end # verifies the service is currently running def running?(_under = nil) return false if info.nil? info[:running] end # get all runlevels that are available and their configuration def runlevels(*args) return Runlevels.new(self) if info.nil? or info[:runlevels].nil? Runlevels.from_hash(self, info[:runlevels], args) end # returns the service type from info def type return nil if info.nil? info[:type] end # returns the service name from info def name return @service_name if info.nil? info[:name] end # returns the service description from info def description return nil if info.nil? info[:description] end def to_s "Service #{@service_name}" end private :info end class ServiceManager attr_reader :inspec, :service_ctl def initialize(inspec, service_ctl = nil) @inspec = inspec @service_ctl ||= service_ctl end end # @see: http://www.freedesktop.org/software/systemd/man/systemctl.html # @see: http://www.freedesktop.org/software/systemd/man/systemd-system.conf.html class Systemd < ServiceManager def initialize(inspec, service_ctl = nil) @service_ctl = service_ctl || 'systemctl' super end def info(service_name) cmd = inspec.command("#{service_ctl} show --all #{service_name}") return nil if cmd.exit_status.to_i != 0 # parse data params = SimpleConfig.new( cmd.stdout.chomp, assignment_re: /^\s*([^=]*?)\s*=\s*(.*?)\s*$/, multiple_values: false, ).params # LoadState values eg. loaded, not-found installed = params['LoadState'] == 'loaded' # test via 'systemctl is-active service' # SubState values running running = params['SubState'] == 'running' # test via systemctl --quiet is-enabled # ActiveState values eg.g inactive, active enabled = %w{enabled static}.include? params['UnitFileState'] { name: params['Id'], description: params['Description'], installed: installed, running: running, enabled: enabled, type: 'systemd', params: params, } end end # AIX services class SrcMstr < ServiceManager attr_reader :name def info(service_name) @name = service_name running = status? return nil if running.nil? { name: service_name, description: nil, installed: true, running: running, enabled: enabled?, type: 'srcmstr', } end private def status? status_cmd = inspec.command("lssrc -s #{@name}") return nil if status_cmd.exit_status.to_i != 0 status_cmd.stdout.split(/\n/).last.chomp =~ /active$/ ? true : false end def enabled? enabled_rc_tcpip? || enabled_inittab? end # #rubocop:disable Style/TrailingComma def enabled_rc_tcpip? inspec.command( "grep -v ^# /etc/rc.tcpip | grep 'start ' | grep -Eq '(/{0,1}| )#{name} '", ).exit_status == 0 end def enabled_inittab? inspec.command("lsitab #{name}").exit_status == 0 end end # @see: http://upstart.ubuntu.com class Upstart < ServiceManager def initialize(service_name, service_ctl = nil) @service_ctl = service_ctl || 'initctl' super end def info(service_name) # get the status of upstart service status = inspec.command("#{service_ctl} status #{service_name}") # fallback for systemv services, those are not handled via `initctl` return SysV.new(inspec).info(service_name) if status.exit_status.to_i != 0 # @see: http://upstart.ubuntu.com/cookbook/#job-states # grep for running to indicate the service is there running = !status.stdout[%r{start/running}].nil? { name: service_name, description: nil, installed: true, running: running, enabled: info_enabled(status, service_name), type: 'upstart', } end private def info_enabled(status, service_name) # check if a service is enabled # http://upstart.ubuntu.com/cookbook/#determine-if-a-job-is-disabled # $ initctl show-config $job | grep -q "^ start on" && echo enabled || echo disabled # Ubuntu 10.04 show-config is not supported # @see http://manpages.ubuntu.com/manpages/maverick/man8/initctl.8.html support_for_show_config = Gem::Version.new('1.3') if version >= support_for_show_config config = inspec.command("#{service_ctl} show-config #{service_name}").stdout else # use config file as fallback config = inspec.file("/etc/init/#{service_name}.conf").content end # disregard if the config does not exist return nil if config.nil? enabled = !config[/^\s*start on/].nil? # implement fallback for Ubuntu 10.04 if inspec.os[:family] == 'ubuntu' && inspec.os[:release].to_f >= 10.04 && inspec.os[:release].to_f < 12.04 && status.exit_status == 0 enabled = true end enabled end def version @version ||= ( out = inspec.command("#{service_ctl} --version").stdout Gem::Version.new(out[/\(upstart ([^\)]+)\)/, 1]) ) end end class SysV < ServiceManager RUNLEVELS = { 0=>false, 1=>false, 2=>false, 3=>false, 4=>false, 5=>false, 6=>false }.freeze def initialize(service_name, service_ctl = nil) @service_ctl = service_ctl || 'service' super end def info(service_name) # check if service is installed # read all available services via ls /etc/init.d/ srvlist = inspec.command('ls -1 /etc/init.d/') return nil if srvlist.exit_status != 0 # check if the service is in list service = srvlist.stdout.split("\n").select { |srv| srv == service_name } # abort if we could not find any service return nil if service.empty? # read all enabled services from runlevel # on rhel via: 'chkconfig --list', is not installed by default # bash: for i in `find /etc/rc*.d -name S*`; do basename $i | sed -r 's/^S[0-9]+//'; done | sort | uniq enabled_services_cmd = inspec.command('find /etc/rc*.d -name S*') service_line = %r{rc(?[0-6])\.d/S[^/]*?#{Regexp.escape service_name}$} all_services = enabled_services_cmd.stdout.split("\n").map { |line| service_line.match(line) }.compact enabled = !all_services.empty? # Determine a list of runlevels which this service is activated for runlevels = RUNLEVELS.dup all_services.each { |x| runlevels[x[:runlevel].to_i] = true } # check if service is really running # service throws an exit code if the service is not installed or # not enabled cmd = inspec.command("#{service_ctl} #{service_name} status") running = cmd.exit_status == 0 { name: service_name, description: nil, installed: true, running: running, enabled: enabled, runlevels: runlevels, type: 'sysv', } end end # @see: https://www.freebsd.org/doc/en/articles/linux-users/startup.html # @see: https://www.freebsd.org/cgi/man.cgi?query=rc.conf&sektion=5 class BSDInit < ServiceManager def initialize(service_name, service_ctl = nil) @service_ctl = service_ctl || 'service' super end def info(service_name) # check if service is enabled # services are enabled in /etc/rc.conf and /etc/defaults/rc.conf # via #{service_name}_enable="YES" # service SERVICE status returns the following result if not activated: # Cannot 'status' sshd. Set sshd_enable to YES in /etc/rc.conf or use 'onestatus' instead of 'status'. # gather all enabled services cmd = inspec.command("#{service_ctl} -e") return nil if cmd.exit_status != 0 # search for the service srv = /(^.*#{service_name}$)/.match(cmd.stdout) return nil if srv.nil? || srv[0].nil? enabled = true # check if the service is running # if the service is not available or not running, we always get an error code cmd = inspec.command("#{service_ctl} #{service_name} onestatus") running = cmd.exit_status == 0 { name: service_name, description: nil, installed: true, running: running, enabled: enabled, type: 'bsd-init', } end end class Runit < ServiceManager def initialize(service_name, service_ctl = nil) @service_ctl = service_ctl || 'sv' super end # rubocop:disable Style/DoubleNegation def info(service_name) # get the status of runit service cmd = inspec.command("#{service_ctl} status #{service_name}") # return nil unless cmd.exit_status == 0 # NOTE(sr) why do we do this? installed = cmd.exit_status == 0 running = installed && !!(cmd.stdout =~ /^run:/) enabled = installed && (running || !!(cmd.stdout =~ /normally up/) || !!(cmd.stdout =~ /want up/)) { name: service_name, description: nil, installed: installed, running: running, enabled: enabled, type: 'runit', } end end # MacOS / Darwin # new launctl on macos 10.10 class LaunchCtl < ServiceManager def initialize(service_name, service_ctl = nil) @service_ctl = service_ctl || 'launchctl' super end def info(service_name) # get the status of upstart service cmd = inspec.command("#{service_ctl} list") return nil if cmd.exit_status != 0 # search for the service srv = /(^.*#{service_name}.*)/.match(cmd.stdout) return nil if srv.nil? || srv[0].nil? # extract values from service parsed_srv = /^(?[0-9-]+)\t(?[0-9]+)\t(?\S*)$/.match(srv[0]) enabled = !parsed_srv['name'].nil? # it's in the list # check if the service is running pid = parsed_srv['pid'] running = pid != '-' # extract service label srv = parsed_srv['name'] || service_name { name: srv, description: nil, installed: true, running: running, enabled: enabled, type: 'darwin', } end end # Determine the service state from Windows # Uses Powershell to retrieve the information class WindowsSrv < ServiceManager # Determine service details # PS: Get-Service -Name 'dhcp'| Select-Object -Property Name, DisplayName, Status | ConvertTo-Json # { # "Name": "dhcp", # "DisplayName": "DHCP Client", # "Status": 4 # } # # Until StartMode is not added to Get-Service, we need to do a workaround # @see: https://connect.microsoft.com/PowerShell/feedback/details/424948/i-would-like-to-see-the-property-starttype-added-to-get-services # Use the following powershell to determine the start mode # PS: Get-WmiObject -Class Win32_Service | Where-Object {$_.Name -eq $name -or $_.DisplayName -eq $name} | Select-Object -Prop # erty Name, StartMode, State, Status | ConvertTo-Json # { # "Name": "Dhcp", # "StartMode": "Auto", # "State": "Running", # "Status": "OK" # } # # Windows Services have the following status code: # @see: https://msdn.microsoft.com/en-us/library/windows/desktop/ms685996(v=vs.85).aspx # - 1: Stopped # - 2: Starting # - 3: Stopping # - 4: Running # - 5: Continue Pending # - 6: Pause Pending # - 7: Paused def info(service_name) cmd = inspec.command("New-Object -Type PSObject | Add-Member -MemberType NoteProperty -Name Service -Value (Get-Service -Name #{service_name}| Select-Object -Property Name, DisplayName, Status) -PassThru | Add-Member -MemberType NoteProperty -Name WMI -Value (Get-WmiObject -Class Win32_Service | Where-Object {$_.Name -eq '#{service_name}' -or $_.DisplayName -eq '#{service_name}'} | Select-Object -Property StartMode) -PassThru | ConvertTo-Json") # 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 service = JSON.parse(cmd.stdout) rescue JSON::ParserError => _e return nil end # check that we got a response return nil if service.nil? || service['Service'].nil? { name: service['Service']['Name'], description: service['Service']['DisplayName'], installed: true, running: service_running?(service), enabled: service_enabled?(service), type: 'windows', } end private # detect if service is enabled def service_enabled?(service) !service['WMI'].nil? && !service['WMI']['StartMode'].nil? && service['WMI']['StartMode'] == 'Auto' end # detect if service is running def service_running?(service) !service['Service']['Status'].nil? && service['Service']['Status'] == 4 end end # Solaris services class Svcs < ServiceManager def initialize(service_name, service_ctl = nil) @service_ctl = service_ctl || 'svcs' super end def info(service_name) # get the status of runit service cmd = inspec.command("#{service_ctl} -l #{service_name}") return nil if cmd.exit_status != 0 params = SimpleConfig.new( cmd.stdout.chomp, assignment_re: /^(\w+)\s*(.*)$/, multiple_values: false, ).params installed = cmd.exit_status == 0 running = installed && (params['state'] == 'online') enabled = installed && (params['enabled'] == 'true') { name: service_name, description: params['name'], installed: installed, running: running, enabled: enabled, type: 'svcs', } end end # specific resources for specific service managers class SystemdService < Service name 'systemd_service' desc 'Use the systemd_service InSpec audit resource to test if the named service (controlled by systemd) is installed, running and/or enabled.' example " # to override service mgmt auto-detection describe systemd_service('service_name') do it { should be_installed } it { should be_enabled } it { should be_running } end # to set a non-standard systemctl path describe systemd_service('service_name', '/path/to/systemctl') do it { should be_running } end " def select_service_mgmt Systemd.new(inspec, service_ctl) end end class UpstartService < Service name 'upstart_service' desc 'Use the upstart_service InSpec audit resource to test if the named service (controlled by upstart) is installed, running and/or enabled.' example " # to override service mgmt auto-detection describe upstart_service('service_name') do it { should be_installed } it { should be_enabled } it { should be_running } end # to set a non-standard initctl path describe upstart_service('service_name', '/path/to/initctl') do it { should be_running } end " def select_service_mgmt Upstart.new(inspec, service_ctl) end end class SysVService < Service name 'sysv_service' desc 'Use the sysv_service InSpec audit resource to test if the named service (controlled by SysV) is installed, running and/or enabled.' example " # to override service mgmt auto-detection describe sysv_service('service_name') do it { should be_installed } it { should be_enabled } it { should be_running } end # to set a non-standard service path describe sysv_service('service_name', '/path/to/service') do it { should be_running } end " def select_service_mgmt SysV.new(inspec, service_ctl) end end class BSDService < Service name 'bsd_service' desc 'Use the bsd_service InSpec audit resource to test if the named service (controlled by BSD init) is installed, running and/or enabled.' example " # to override service mgmt auto-detection describe bsd_service('service_name') do it { should be_installed } it { should be_enabled } it { should be_running } end # to set a non-standard service path describe bsd_service('service_name', '/path/to/service') do it { should be_running } end " def select_service_mgmt BSDInit.new(inspec, service_ctl) end end class LaunchdService < Service name 'launchd_service' desc 'Use the launchd_service InSpec audit resource to test if the named service (controlled by launchd) is installed, running and/or enabled.' example " # to override service mgmt auto-detection describe launchd_service('service_name') do it { should be_installed } it { should be_enabled } it { should be_running } end # to set a non-standard launchctl path describe launchd_service('service_name', '/path/to/launchctl') do it { should be_running } end " def select_service_mgmt LaunchCtl.new(inspec, service_ctl) end end class RunitService < Service name 'runit_service' desc 'Use the runit_service InSpec audit resource to test if the named service (controlled by runit) is installed, running and/or enabled.' example " # to override service mgmt auto-detection describe runit_service('service_name') do it { should be_installed } it { should be_enabled } it { should be_running } end # to set a non-standard sv path describe runit_service('service_name', '/path/to/sv') do it { should be_running } end " def select_service_mgmt Runit.new(inspec, service_ctl) end end end