From 7451917223100818a5eeaaeb80b731787e41943a Mon Sep 17 00:00:00 2001 From: Jonathan Hartman Date: Thu, 4 Oct 2018 11:06:17 -0700 Subject: [PATCH] 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 --- lib/resources/processes.rb | 22 +++++++++-- test/helper.rb | 8 +++- test/unit/mock/cmd/ps-busybox | 2 + test/unit/mock/cmd/ps-help-busybox | 8 ++++ test/unit/resources/processes_test.rb | 57 +++++++++++++++++++++++++++ 5 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 test/unit/mock/cmd/ps-help-busybox diff --git a/lib/resources/processes.rb b/lib/resources/processes.rb index 658f9624e..14df64600 100644 --- a/lib/resources/processes.rb +++ b/lib/resources/processes.rb @@ -126,7 +126,7 @@ module Inspec::Resources 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+(.*)$/ + 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, @@ -163,6 +163,18 @@ module Inspec::Resources @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] @@ -187,8 +199,12 @@ module Inspec::Resources 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) + 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 diff --git a/test/helper.rb b/test/helper.rb index 9f627e017..aaa54813f 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -206,6 +206,11 @@ class MockLoader 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 { 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 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, 'env' => cmd.call('env'), '${Env:PATH}' => cmd.call('$env-PATH'), # 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 if @platform && @platform[:name] == 'alpine' mock_cmds.merge!( + 'ps --help' => cmd_stderr.call('ps-help-busybox'), %{bash -c 'type "netstat"'} => cmd_exit_1.call(), %{bash -c 'type "ss"'} => cmd_exit_1.call(), %{which "ss"} => cmd_exit_1.call(), @@ -547,6 +552,7 @@ class MockLoader ) else mock_cmds.merge!( + 'ps --help' => empty.call(), %{bash -c 'type "ss"'} => empty.call(), %{bash -c 'type "netstat"'} => empty.call(), 'ss -tulpen' => cmd.call('ss-tulpen'), diff --git a/test/unit/mock/cmd/ps-busybox b/test/unit/mock/cmd/ps-busybox index e1bab4965..328007d3c 100644 --- a/test/unit/mock/cmd/ps-busybox +++ b/test/unit/mock/cmd/ps-busybox @@ -1,3 +1,5 @@ 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 + 82 24m 2m ? S 3:50 frank /a/bigger/program + 83 2.6g 1g ? S 39:00 tim /the/biggest/program diff --git a/test/unit/mock/cmd/ps-help-busybox b/test/unit/mock/cmd/ps-help-busybox new file mode 100644 index 000000000..7fb84a040 --- /dev/null +++ b/test/unit/mock/cmd/ps-help-busybox @@ -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 diff --git a/test/unit/resources/processes_test.rb b/test/unit/resources/processes_test.rb index 3f6128620..a5e0c8303 100644 --- a/test/unit/resources/processes_test.rb +++ b/test/unit/resources/processes_test.rb @@ -141,6 +141,63 @@ describe 'Inspec::Resources::Processes' do }) 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 resource = MockLoader.new(:windows).load_resource('processes', 'winlogon.exe') _(resource.to_s).must_equal 'Processes winlogon.exe'