inspec/lib/resources/processes.rb
Jonathan Hartman 7451917223 Support finding larger processes on Busybox (#3446)
For larger processes, Busybox's ps displays the vsz and rss columns in
megabytes or gigabytes, with no option I've found to override the behavior.

This change updates the process regex to account for that and converts
the values to kilobytes so they can still be cast as integers.

Signed-off-by: Jonathan Hartman <j@hartman.io>
2018-10-04 14:06:17 -04:00

218 lines
6.7 KiB
Ruby

# encoding: utf-8
# copyright: 2015, Vulcano Security GmbH
require 'utils/filter'
require 'ostruct'
module Inspec::Resources
class Processes < Inspec.resource(1)
name 'processes'
supports platform: 'unix'
supports platform: 'windows'
desc 'Use the processes InSpec audit resource to test properties for programs that are running on the system.'
example "
describe processes('mysqld') do
its('entries.length') { should eq 1 }
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 = /.*/)
@grep = grep
# turn into a regexp if it isn't one yet
if grep.class == String
# if windows ignore case as we can't make up our minds
if inspec.os.windows?
grep = '(?i)' + grep
else
grep = '(/[^/]*)*' + grep unless grep[0] == '/'
grep = '^' + grep + '(\s|$)'
end
grep = Regexp.new(grep)
end
all_cmds = ps_axo
@list = all_cmds.find_all do |hm|
hm[:command] =~ grep
end
end
def exists?
!@list.empty?
end
def to_s
"Processes #{@grep.class == String ? @grep : @grep.inspect}"
end
def list
warn '[DEPRECATION] `processes.list` is deprecated. Please use `processes.entries` instead. It will be removed in version 2.0.0.'
@list
end
filter = FilterTable.create
filter.register_column(:labels, field: 'label')
.register_column(:pids, field: 'pid')
.register_column(:cpus, field: 'cpu')
.register_column(:mem, field: 'mem')
.register_column(:vsz, field: 'vsz')
.register_column(:rss, field: 'rss')
.register_column(:tty, field: 'tty')
.register_column(:states, field: 'stat')
.register_column(:start, field: 'start')
.register_column(:time, field: 'time')
.register_column(:users, field: 'user')
.register_column(:commands, field: 'command')
.install_filter_methods_on_resource(self, :filtered_processes)
private
def filtered_processes
@list
end
def ps_axo
os = inspec.os
if os.linux?
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, field_map)
end
def ps_configuration_for_linux
if busybox_ps?
command = 'ps -o pid,vsz,rss,tty,stat,time,ruser,args'
regex = /^\s*(\d+)\s+(\d+(?:\.\d+)?[gm]?)\s+(\d+(?:\.\d+)?[gm]?)\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
[command, regex, field_map]
end
def busybox_ps?
@busybox_ps ||= inspec.command('ps --help').stderr.include?('BusyBox')
end
def convert_to_kilobytes(param)
return param.to_i unless param.is_a?(String)
if param.end_with?('g')
(param[0..-2].to_f * 1024 * 1024).to_i
elsif param.end_with?('m')
(param[0..-2].to_f * 1024).to_i
else
param.to_i
end
end
def build_process_list(command, regex, field_map)
cmd = inspec.command(command)
all = cmd.stdout.split("\n")[1..-1]
return [] if all.nil?
# 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
process_data[:pid] = process_data[:pid].to_i if process_data.key?(:pid)
# some ps variants (*cough* busybox) display vsz and rss as human readable MB or GB
[:vsz, :rss].each do |param|
next unless process_data.key?(param)
process_data[param] = convert_to_kilobytes(process_data[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