mirror of
https://github.com/inspec/inspec
synced 2024-11-27 07:00:39 +00:00
processes resource: support busybox ps (#2222)
This change enhances the processes resource to support the busybox ps command which is common on Alpine, for example. The way we map ps fields to the structs needed by FilterTable have also been refactored to be more flexible so we can support multiple formats in the future. Also, the processes resource now allows the grep argument to be optional thus allowing a user to query all resources without passing in a match-all regex. Signed-off-by: Adam Leff <adam@leff.co>
This commit is contained in:
parent
999d115fb8
commit
939ee5ecfc
5 changed files with 125 additions and 21 deletions
|
@ -18,7 +18,7 @@ A `processes` resource block declares the name of the process to be tested, and
|
|||
|
||||
where
|
||||
|
||||
* `processes('process_name')` specifies the name of a process to check. If this is a string, it will be converted to a Regexp. For more specificity, pass a Regexp directly.
|
||||
* `processes('process_name')` specifies the name of a process to check. If this is a string, it will be converted to a Regexp. For more specificity, pass a Regexp directly. If left blank, all processes will be returned.
|
||||
* `property_name` may be used to test user (`its('users')`) and state properties (`its('states')`)
|
||||
|
||||
<br>
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
# author: Christoph Hartmann
|
||||
|
||||
require 'utils/filter'
|
||||
require 'ostruct'
|
||||
|
||||
module Inspec::Resources
|
||||
class Processes < Inspec.resource(1)
|
||||
class Processes < Inspec.resource(1) # rubocop:disable Metrics/ClassLength
|
||||
name 'processes'
|
||||
desc 'Use the processes InSpec audit resource to test properties for programs that are running on the system.'
|
||||
example "
|
||||
|
@ -15,12 +16,18 @@ module Inspec::Resources
|
|||
its('users') { should eq ['mysql'] }
|
||||
its('states') { should include 'S' }
|
||||
end
|
||||
|
||||
describe processes(/.+/).where { label != 'unconfined' && pid < 1000 } do
|
||||
its('users') { should cmp [] }
|
||||
end
|
||||
|
||||
# work with all processes
|
||||
describe processes do
|
||||
its('entries.length') { should be <= 100 }
|
||||
end
|
||||
"
|
||||
|
||||
def initialize(grep)
|
||||
def initialize(grep = /.*/)
|
||||
@grep = grep
|
||||
# turn into a regexp if it isn't one yet
|
||||
if grep.class == String
|
||||
|
@ -80,39 +87,118 @@ module Inspec::Resources
|
|||
os = inspec.os
|
||||
|
||||
if os.linux?
|
||||
command = 'ps axo label,pid,pcpu,pmem,vsz,rss,tty,stat,start,time,user:32,command'
|
||||
regex = /^(.+?)\s+(\d+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+(\w{3} \d{2}|\d{2}:\d{2}:\d{2})\s+([^ ]+)\s+([^ ]+)\s+(.*)$/
|
||||
command, regex, field_map = ps_configuration_for_linux
|
||||
elsif os.windows?
|
||||
command = '$Proc = Get-Process -IncludeUserName | Where-Object {$_.Path -ne $null } | Select-Object PriorityClass,Id,CPU,PM,VirtualMemorySize,NPM,SessionId,Responding,StartTime,TotalProcessorTime,UserName,Path | ConvertTo-Csv -NoTypeInformation;$Proc.Replace("""","").Replace("`r`n","`n")'
|
||||
# Wanted to use /(?:^|,)([^,]*)/; works on rubular.com not sure why here?
|
||||
regex = /^(.+),(.+),(.+),(.+),(.+),(.+),(.+),(.+),(.+),(.+),(.+),(.+)$/
|
||||
field_map = {
|
||||
pid: 2,
|
||||
cpu: 3,
|
||||
mem: 4,
|
||||
vsz: 5,
|
||||
rss: 6,
|
||||
tty: 7,
|
||||
stat: 8,
|
||||
start: 9,
|
||||
time: 10,
|
||||
user: 11,
|
||||
command: 12,
|
||||
}
|
||||
else
|
||||
command = 'ps axo pid,pcpu,pmem,vsz,rss,tty,stat,start,time,user,command'
|
||||
regex = /^\s*([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+(.*)$/
|
||||
field_map = {
|
||||
pid: 1,
|
||||
cpu: 2,
|
||||
mem: 3,
|
||||
vsz: 4,
|
||||
rss: 5,
|
||||
tty: 6,
|
||||
stat: 7,
|
||||
start: 8,
|
||||
time: 9,
|
||||
user: 10,
|
||||
command: 11,
|
||||
}
|
||||
end
|
||||
build_process_list(command, regex, os)
|
||||
build_process_list(command, regex, field_map)
|
||||
end
|
||||
|
||||
Process = Struct.new(:label, :pid,
|
||||
:cpu, :mem, :vsz,
|
||||
:rss, :tty, :stat,
|
||||
:start, :time, :user, :command)
|
||||
def ps_configuration_for_linux
|
||||
if busybox_ps?
|
||||
command = 'ps -o pid,vsz,rss,tty,stat,time,ruser,args'
|
||||
regex = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/
|
||||
field_map = {
|
||||
pid: 1,
|
||||
vsz: 2,
|
||||
rss: 3,
|
||||
tty: 4,
|
||||
stat: 5,
|
||||
time: 6,
|
||||
user: 7,
|
||||
command: 8,
|
||||
}
|
||||
else
|
||||
command = 'ps axo label,pid,pcpu,pmem,vsz,rss,tty,stat,start,time,user:32,command'
|
||||
regex = /^(.+?)\s+(\d+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+(\w{3} \d{2}|\d{2}:\d{2}:\d{2})\s+([^ ]+)\s+([^ ]+)\s+(.*)$/
|
||||
field_map = {
|
||||
label: 1,
|
||||
pid: 2,
|
||||
cpu: 3,
|
||||
mem: 4,
|
||||
vsz: 5,
|
||||
rss: 6,
|
||||
tty: 7,
|
||||
stat: 8,
|
||||
start: 9,
|
||||
time: 10,
|
||||
user: 11,
|
||||
command: 12,
|
||||
}
|
||||
end
|
||||
|
||||
def build_process_list(command, regex, os)
|
||||
[command, regex, field_map]
|
||||
end
|
||||
|
||||
def busybox_ps?
|
||||
@busybox_ps ||= inspec.command('ps --help').stderr.include?('BusyBox')
|
||||
end
|
||||
|
||||
def build_process_list(command, regex, field_map)
|
||||
cmd = inspec.command(command)
|
||||
all = cmd.stdout.split("\n")[1..-1]
|
||||
return [] if all.nil?
|
||||
lines = all.map do |line|
|
||||
line.match(regex)
|
||||
end.compact
|
||||
lines.map do |m|
|
||||
a = m.to_a[1..-1] # grab all matching groups
|
||||
a.unshift(nil) unless os.linux? || os.windows?
|
||||
a[1] = a[1].to_i
|
||||
a[4] = a[4].to_i
|
||||
a[5] = a[5].to_i
|
||||
Process.new(*a)
|
||||
|
||||
# map all the process lines into match objects, fetch the available fields,
|
||||
# and then build an OpenStruct of the process data for each process
|
||||
all.map do |line|
|
||||
line = line.match(regex)
|
||||
|
||||
# skip this line if we couldn't match the regular expression
|
||||
next if line.nil?
|
||||
|
||||
# skip this entry if there's no command for this line
|
||||
next if line[field_map[:command]].nil?
|
||||
|
||||
# build a hash of process data that we'll turn into a struct for FilterTable
|
||||
process_data = {}
|
||||
[:label, :pid, :cpu, :mem, :vsz, :rss, :tty, :stat, :start, :time, :user, :command].each do |param|
|
||||
# not all operating systems support all fields, so skip the field if we don't have it
|
||||
process_data[param] = line[field_map[param]] if field_map.key?(param)
|
||||
end
|
||||
|
||||
# ensure pid, vsz, and rss are integers for backward compatibility
|
||||
[:pid, :vsz, :rss].each do |int_param|
|
||||
process_data[int_param] = process_data[int_param].to_i if process_data.key?(int_param)
|
||||
end
|
||||
|
||||
# strip any newlines off the command
|
||||
process_data[:command].strip!
|
||||
|
||||
# return an OpenStruct of the process for future use by FilterTable
|
||||
OpenStruct.new(process_data)
|
||||
end.compact
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -43,6 +43,7 @@ Inspec::Log.logger = Logger.new(nil)
|
|||
class MockLoader
|
||||
# collects emulation operating systems
|
||||
OPERATING_SYSTEMS = {
|
||||
alpine: { name: 'alpine', family: 'alpine', release: '3.6.2', arch: 'x86_64' },
|
||||
arch: { name: 'arch', family: 'arch', release: nil, arch: nil },
|
||||
centos5: { name: 'centos', family: 'redhat', release: '5.11', arch: 'x86_64' },
|
||||
centos6: { name: 'centos', family: 'redhat', release: '6.6', arch: 'x86_64' },
|
||||
|
@ -194,6 +195,8 @@ class MockLoader
|
|||
mock.commands = {
|
||||
'ps axo pid,pcpu,pmem,vsz,rss,tty,stat,start,time,user,command' => cmd.call('ps-axo'),
|
||||
'ps axo label,pid,pcpu,pmem,vsz,rss,tty,stat,start,time,user:32,command' => cmd.call('ps-axoZ'),
|
||||
'ps -o pid,vsz,rss,tty,stat,time,ruser,args' => cmd.call('ps-busybox'),
|
||||
'ps --help' => empty.call,
|
||||
'Get-Content win_secpol.cfg' => cmd.call('secedit-export'),
|
||||
'secedit /export /cfg win_secpol.cfg' => cmd.call('success'),
|
||||
'Remove-Item win_secpol.cfg' => cmd.call('success'),
|
||||
|
|
3
test/unit/mock/cmd/ps-busybox
Normal file
3
test/unit/mock/cmd/ps-busybox
Normal file
|
@ -0,0 +1,3 @@
|
|||
PID VSZ RSS TT STAT TIME RUSER COMMAND
|
||||
1 1536 4 136,0 S 0:00 root /bin/sh
|
||||
5 1528 4 136,0 R 0:00 joe /some/other/coolprogram
|
|
@ -160,4 +160,16 @@ describe 'Inspec::Resources::Processes' do
|
|||
resource = MockLoader.new(:windows).load_resource('processes', 'unicorn.exe')
|
||||
_(resource.exists?).must_equal false
|
||||
end
|
||||
|
||||
it 'returns the correct command for busybox ps' do
|
||||
resource = MockLoader.new(:alpine).load_resource('processes')
|
||||
resource.expects(:busybox_ps?).returns(true)
|
||||
resource.send(:ps_configuration_for_linux)[0].must_equal 'ps -o pid,vsz,rss,tty,stat,time,ruser,args'
|
||||
end
|
||||
|
||||
it 'returns the correct command for non-busybox linux' do
|
||||
resource = MockLoader.new(:centos7).load_resource('processes')
|
||||
resource.expects(:busybox_ps?).returns(false)
|
||||
resource.send(:ps_configuration_for_linux)[0].must_equal 'ps axo label,pid,pcpu,pmem,vsz,rss,tty,stat,start,time,user:32,command'
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue