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>
This commit is contained in:
Jonathan Hartman 2018-10-04 11:06:17 -07:00 committed by Jared Quick
parent 3248af1fe3
commit 7451917223
5 changed files with 93 additions and 4 deletions

View file

@ -126,7 +126,7 @@ module Inspec::Resources
def ps_configuration_for_linux def ps_configuration_for_linux
if busybox_ps? if busybox_ps?
command = 'ps -o pid,vsz,rss,tty,stat,time,ruser,args' 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+(.*)$/ regex = /^\s*(\d+)\s+(\d+(?:\.\d+)?[gm]?)\s+(\d+(?:\.\d+)?[gm]?)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/
field_map = { field_map = {
pid: 1, pid: 1,
vsz: 2, vsz: 2,
@ -163,6 +163,18 @@ module Inspec::Resources
@busybox_ps ||= inspec.command('ps --help').stderr.include?('BusyBox') @busybox_ps ||= inspec.command('ps --help').stderr.include?('BusyBox')
end 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) def build_process_list(command, regex, field_map)
cmd = inspec.command(command) cmd = inspec.command(command)
all = cmd.stdout.split("\n")[1..-1] all = cmd.stdout.split("\n")[1..-1]
@ -187,8 +199,12 @@ module Inspec::Resources
end end
# ensure pid, vsz, and rss are integers for backward compatibility # ensure pid, vsz, and rss are integers for backward compatibility
[:pid, :vsz, :rss].each do |int_param| process_data[:pid] = process_data[:pid].to_i if process_data.key?(:pid)
process_data[int_param] = process_data[int_param].to_i if process_data.key?(int_param)
# 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 end
# strip any newlines off the command # strip any newlines off the command

View file

@ -206,6 +206,11 @@ class MockLoader
mock.mock_command('', stdout, '', 0) mock.mock_command('', stdout, '', 0)
} }
cmd_stderr = lambda { |x = nil|
stderr = x.nil? ? '' : File.read(File.join(scriptpath, 'unit/mock/cmd', x))
mock.mock_command('', '', stderr, 1)
}
empty = lambda { empty = lambda {
mock.mock_command('', '', '', 0) mock.mock_command('', '', '', 0)
} }
@ -235,7 +240,6 @@ class MockLoader
'ps axo pid,pcpu,pmem,vsz,rss,tty,stat,start,time,user,command' => cmd.call('ps-axo'), '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 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 -o pid,vsz,rss,tty,stat,time,ruser,args' => cmd.call('ps-busybox'),
'ps --help' => empty.call,
'env' => cmd.call('env'), 'env' => cmd.call('env'),
'${Env:PATH}' => cmd.call('$env-PATH'), '${Env:PATH}' => cmd.call('$env-PATH'),
# registry key test using winrm 2.0 # registry key test using winrm 2.0
@ -539,6 +543,7 @@ class MockLoader
# allow the ss and/or netstat commands to exist so the later mock is called # allow the ss and/or netstat commands to exist so the later mock is called
if @platform && @platform[:name] == 'alpine' if @platform && @platform[:name] == 'alpine'
mock_cmds.merge!( mock_cmds.merge!(
'ps --help' => cmd_stderr.call('ps-help-busybox'),
%{bash -c 'type "netstat"'} => cmd_exit_1.call(), %{bash -c 'type "netstat"'} => cmd_exit_1.call(),
%{bash -c 'type "ss"'} => cmd_exit_1.call(), %{bash -c 'type "ss"'} => cmd_exit_1.call(),
%{which "ss"} => cmd_exit_1.call(), %{which "ss"} => cmd_exit_1.call(),
@ -547,6 +552,7 @@ class MockLoader
) )
else else
mock_cmds.merge!( mock_cmds.merge!(
'ps --help' => empty.call(),
%{bash -c 'type "ss"'} => empty.call(), %{bash -c 'type "ss"'} => empty.call(),
%{bash -c 'type "netstat"'} => empty.call(), %{bash -c 'type "netstat"'} => empty.call(),
'ss -tulpen' => cmd.call('ss-tulpen'), 'ss -tulpen' => cmd.call('ss-tulpen'),

View file

@ -1,3 +1,5 @@
PID VSZ RSS TT STAT TIME RUSER COMMAND PID VSZ RSS TT STAT TIME RUSER COMMAND
1 1536 4 136,0 S 0:00 root /bin/sh 1 1536 4 136,0 S 0:00 root /bin/sh
5 1528 4 136,0 R 0:00 joe /some/other/coolprogram 5 1528 4 136,0 R 0:00 joe /some/other/coolprogram
82 24m 2m ? S 3:50 frank /a/bigger/program
83 2.6g 1g ? S 39:00 tim /the/biggest/program

View file

@ -0,0 +1,8 @@
BusyBox v1.28.1 (2018-06-08 10:27:33 UTC) multi-call binary.
Usage: ps [-o COL1,COL2=HEADER] [-T]
Show list of processes
-o COL1,COL2=HEADER Select columns for display
-T Show threads

View file

@ -141,6 +141,63 @@ describe 'Inspec::Resources::Processes' do
}) })
end end
it 'handles regular processes from busybox' do
resource = MockLoader.new(:alpine).load_resource('processes', '/some/other/coolprogram')
_(resource.entries.length).must_equal 1
_(resource.entries[0].to_h).must_equal({
label: nil,
pid: 5,
cpu: nil,
mem: nil,
vsz: 1528,
rss: 4,
tty: '136,0',
stat: 'R',
start: nil,
time: '0:00',
user: 'joe',
command: '/some/other/coolprogram',
})
end
it 'handles human readable megabytes from busybox' do
resource = MockLoader.new(:alpine).load_resource('processes', '/a/bigger/program')
_(resource.entries.length).must_equal 1
_(resource.entries[0].to_h).must_equal({
label: nil,
pid: 82,
cpu: nil,
mem: nil,
vsz: 24576,
rss: 2048,
tty: '?',
stat: 'S',
start: nil,
time: '3:50',
user: 'frank',
command: '/a/bigger/program',
})
end
it 'handles human readable gigabytes from busybox' do
resource = MockLoader.new(:alpine).load_resource('processes', '/the/biggest/program')
_(resource.entries.length).must_equal 1
_(resource.entries[0].to_h).must_equal({
label: nil,
pid: 83,
cpu: nil,
mem: nil,
vsz: 2726297,
rss: 1048576,
tty: '?',
stat: 'S',
start: nil,
time: '39:00',
user: 'tim',
command: '/the/biggest/program',
})
end
it 'command name matches with output (string)' do it 'command name matches with output (string)' do
resource = MockLoader.new(:windows).load_resource('processes', 'winlogon.exe') resource = MockLoader.new(:windows).load_resource('processes', 'winlogon.exe')
_(resource.to_s).must_equal 'Processes winlogon.exe' _(resource.to_s).must_equal 'Processes winlogon.exe'