inspec 2.2.112 → 2.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -2
  3. data/CHANGELOG.md +42 -19
  4. data/README.md +1 -1
  5. data/Rakefile +16 -3
  6. data/docs/dev/integration-testing.md +31 -0
  7. data/docs/dev/plugins.md +4 -2
  8. data/docs/dsl_inspec.md +104 -4
  9. data/docs/plugins.md +57 -0
  10. data/docs/resources/aws_ebs_volume.md.erb +76 -0
  11. data/docs/resources/aws_ebs_volumes.md.erb +86 -0
  12. data/docs/style.md +178 -0
  13. data/examples/plugins/inspec-resource-lister/Gemfile +12 -0
  14. data/examples/plugins/inspec-resource-lister/LICENSE +13 -0
  15. data/examples/plugins/inspec-resource-lister/README.md +62 -0
  16. data/examples/plugins/inspec-resource-lister/Rakefile +40 -0
  17. data/examples/plugins/inspec-resource-lister/inspec-resource-lister.gemspec +45 -0
  18. data/examples/plugins/inspec-resource-lister/lib/inspec-resource-lister.rb +16 -0
  19. data/examples/plugins/inspec-resource-lister/lib/inspec-resource-lister/cli_command.rb +70 -0
  20. data/examples/plugins/inspec-resource-lister/lib/inspec-resource-lister/plugin.rb +55 -0
  21. data/examples/plugins/inspec-resource-lister/lib/inspec-resource-lister/version.rb +10 -0
  22. data/examples/plugins/inspec-resource-lister/test/fixtures/README.md +24 -0
  23. data/examples/plugins/inspec-resource-lister/test/functional/README.md +18 -0
  24. data/examples/plugins/inspec-resource-lister/test/functional/inspec_resource_lister_test.rb +110 -0
  25. data/examples/plugins/inspec-resource-lister/test/helper.rb +26 -0
  26. data/examples/plugins/inspec-resource-lister/test/unit/README.md +17 -0
  27. data/examples/plugins/inspec-resource-lister/test/unit/cli_args_test.rb +64 -0
  28. data/examples/plugins/inspec-resource-lister/test/unit/plugin_def_test.rb +51 -0
  29. data/examples/profile/controls/example.rb +9 -8
  30. data/inspec.gemspec +2 -1
  31. data/lib/inspec/attribute_registry.rb +1 -1
  32. data/lib/inspec/globals.rb +4 -0
  33. data/lib/inspec/objects/control.rb +18 -3
  34. data/lib/inspec/plugin/v2.rb +14 -3
  35. data/lib/inspec/plugin/v2/activator.rb +7 -2
  36. data/lib/inspec/plugin/v2/installer.rb +426 -0
  37. data/lib/inspec/plugin/v2/loader.rb +137 -30
  38. data/lib/inspec/plugin/v2/registry.rb +13 -4
  39. data/lib/inspec/profile.rb +2 -1
  40. data/lib/inspec/reporters/json.rb +11 -1
  41. data/lib/inspec/resource.rb +6 -15
  42. data/lib/inspec/rule.rb +18 -9
  43. data/lib/inspec/runner_rspec.rb +1 -1
  44. data/lib/inspec/schema.rb +1 -0
  45. data/lib/inspec/version.rb +1 -1
  46. data/lib/plugins/inspec-plugin-manager-cli/README.md +6 -0
  47. data/lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli.rb +18 -0
  48. data/lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb +420 -0
  49. data/lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/plugin.rb +12 -0
  50. data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/config_dirs/empty/.gitkeep +0 -0
  51. data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/plugins/inspec-egg-white-omelette/lib/inspec-egg-white-omelette.rb +2 -0
  52. data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/plugins/inspec-egg-white-omelette/lib/inspec-egg-white-omelette/.gitkeep +0 -0
  53. data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/plugins/inspec-wrong-structure/.gitkeep +0 -0
  54. data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/plugins/wrong-name/lib/wrong-name.rb +1 -0
  55. data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/plugins/wrong-name/lib/wrong-name/.gitkeep +0 -0
  56. data/lib/plugins/inspec-plugin-manager-cli/test/functional/inspec-plugin_test.rb +651 -0
  57. data/lib/plugins/inspec-plugin-manager-cli/test/unit/cli_args_test.rb +71 -0
  58. data/lib/plugins/inspec-plugin-manager-cli/test/unit/plugin_def_test.rb +20 -0
  59. data/lib/plugins/shared/core_plugin_test_helper.rb +101 -2
  60. data/lib/plugins/things-for-train-integration.rb +14 -0
  61. data/lib/resource_support/aws.rb +2 -0
  62. data/lib/resources/aws/aws_ebs_volume.rb +122 -0
  63. data/lib/resources/aws/aws_ebs_volumes.rb +63 -0
  64. data/lib/resources/port.rb +10 -6
  65. metadata +56 -11
  66. data/docs/ruby_usage.md +0 -204
@@ -0,0 +1,71 @@
1
+ require_relative '../../../shared/core_plugin_test_helper.rb'
2
+
3
+ #-----------------------------------------------------------------------#
4
+ # Thor option defs
5
+ #-----------------------------------------------------------------------#
6
+ class PluginManagerCliOptions < MiniTest::Test
7
+ include CorePluginUnitHelper
8
+ let(:cli_class) { InspecPlugins::PluginManager::CliCommand }
9
+
10
+ def setup
11
+ require_relative '../../lib/inspec-plugin-manager-cli/cli_command'
12
+ end
13
+
14
+ def test_list_args
15
+ arg_config = cli_class.all_commands['list'].options
16
+ assert_equal 1, arg_config.count, 'The list command should have 1 option'
17
+
18
+ assert_includes arg_config.keys, :all, 'The list command should have an --all option'
19
+ assert_equal :boolean, arg_config[:all].type, 'The --all option should be boolean'
20
+ assert_equal :a, arg_config[:all].aliases.first, 'The --all option should be aliased as -a'
21
+ refute_nil arg_config[:all].description, 'The --all option should have a description'
22
+ refute arg_config[:all].required, 'The --all option should not be required'
23
+
24
+ assert_equal 0, cli_class.instance_method(:list).arity, 'The list command should take no arguments'
25
+ end
26
+
27
+ def test_search_args
28
+ arg_config = cli_class.all_commands['search'].options
29
+ assert_equal 2, arg_config.count, 'The search command should have 2 options'
30
+
31
+ assert_includes arg_config.keys, :all, 'The search command should have an --all option'
32
+ assert_equal :boolean, arg_config[:all].type, 'The --all option should be boolean'
33
+ assert_equal :a, arg_config[:all].aliases.first, 'The --all option should be aliased as -a'
34
+ refute_nil arg_config[:all].description, 'The --all option should have a description'
35
+ refute arg_config[:all].required, 'The --all option should not be required'
36
+
37
+ assert_includes arg_config.keys, :exact, 'The search command should have an --exact option'
38
+ assert_equal :boolean, arg_config[:exact].type, 'The --exact option should be boolean'
39
+ assert_equal :e, arg_config[:exact].aliases.first, 'The --exact option should be aliased as -e'
40
+ refute_nil arg_config[:exact].description, 'The --exact option should have a description'
41
+ refute arg_config[:exact].required, 'The --exact option should not be required'
42
+
43
+ assert_equal 1, cli_class.instance_method(:search).arity, 'The search command should take one argument'
44
+ end
45
+
46
+ def test_install_args
47
+ arg_config = cli_class.all_commands['install'].options
48
+ assert_equal 1, arg_config.count, 'The install command should have 1 option'
49
+
50
+ assert_includes arg_config.keys, :version, 'The install command should have a --version option'
51
+ assert_equal :string, arg_config[:version].type, 'The --version option should be a string'
52
+ assert_equal :v, arg_config[:version].aliases.first, 'The --version option should be aliased as -v'
53
+ refute_nil arg_config[:version].description, 'The --version option should have a description'
54
+ refute arg_config[:version].required, 'The --version option should not be required'
55
+
56
+ assert_equal 1, cli_class.instance_method(:install).arity, 'The install command should take one argument'
57
+ end
58
+
59
+ def test_update_args
60
+ # TODO: allow specifying version
61
+ arg_config = cli_class.all_commands['update'].options
62
+ assert_equal 0, arg_config.count, 'The update command should have no options'
63
+ assert_equal 1, cli_class.instance_method(:update).arity, 'The update command should take one argument'
64
+ end
65
+
66
+ def test_uninstall_args
67
+ arg_config = cli_class.all_commands['uninstall'].options
68
+ assert_equal 0, arg_config.count, 'The uninstall command should have no options'
69
+ assert_equal 1, cli_class.instance_method(:uninstall).arity, 'The uninstall command should take one argument'
70
+ end
71
+ end
@@ -0,0 +1,20 @@
1
+ require_relative '../../../shared/core_plugin_test_helper.rb'
2
+
3
+ #-----------------------------------------------------------------------#
4
+ # Plugin Definition
5
+ #-----------------------------------------------------------------------#
6
+ class PluginManagerCliDefinitionTests < MiniTest::Test
7
+ include CorePluginUnitHelper
8
+
9
+ def test_plugin_registered
10
+ loader = Inspec::Plugin::V2::Loader.new
11
+ loader.load_all # We want to ensure it is auto-loaded
12
+
13
+ assert registry.known_plugin?(:'inspec-plugin-manager-cli'), 'inspec-plugin-manager-cli should be registered'
14
+ assert registry.loaded_plugin?(:'inspec-plugin-manager-cli'), 'inspec-plugin-manager-cli should be loaded'
15
+
16
+ status = registry[:'inspec-plugin-manager-cli']
17
+ assert_equal 2, status.api_generation, 'inspec-plugin-manager-cli should be v2'
18
+ assert_includes status.plugin_types, :cli_command, 'inspec-plugin-manager-cli should have cli_command activators'
19
+ end
20
+ end
@@ -11,12 +11,32 @@ require 'ostruct'
11
11
 
12
12
  # Utilities often needed
13
13
  require 'fileutils'
14
+ require 'tmpdir'
15
+ require 'pathname'
16
+ require 'forwardable'
14
17
 
15
18
  # Configure MiniTest to expose things like `let`
16
19
  class Module
17
20
  include Minitest::Spec::DSL
18
21
  end
19
22
 
23
+ module Inspec
24
+ class FuncTestRunResult
25
+ attr_reader :train_result
26
+ attr_reader :payload
27
+
28
+ extend Forwardable
29
+ def_delegator :train_result, :stdout
30
+ def_delegator :train_result, :stderr
31
+ def_delegator :train_result, :exit_status
32
+
33
+ def initialize(train_result)
34
+ @train_result = train_result
35
+ @payload = OpenStruct.new
36
+ end
37
+ end
38
+ end
39
+
20
40
  module CorePluginBaseHelper
21
41
  let(:repo_path) { File.expand_path(File.join(__FILE__, '..', '..', '..', '..')) }
22
42
  let(:inspec_bin_path) { File.join(repo_path, 'bin', 'inspec') }
@@ -40,11 +60,90 @@ module CorePluginFunctionalHelper
40
60
  elsif opts.key?(:env)
41
61
  prefix = opts[:env].to_a.map { |assignment| "#{assignment[0]}=#{assignment[1]}" }.join(' ')
42
62
  end
43
- TRAIN_CONNECTION.run_command("#{prefix} #{inspec_bin_path} #{command_line}")
63
+ Inspec::FuncTestRunResult.new(TRAIN_CONNECTION.run_command("#{prefix} #{inspec_bin_path} #{command_line}"))
64
+ end
65
+
66
+ # This helper does some fancy footwork to make InSpec think a plugin
67
+ # under development is temporarily installed.
68
+ # @param String command_line Invocation, without the word 'inspec'
69
+ # @param Hash opts options as for run_inspec_process, with more options:
70
+ # :pre_run: Proc(plugin_statefile_data, tmp_dir_path) - optional setup block.
71
+ # Modify plugin_statefile_data as needed; it will be written to a plugins.json
72
+ # in tmp_dir_path. You may also copy in other things to the tmp_dir_path. Your PWD
73
+ # will be in the tmp_dir, and it will exist and be empty.
74
+ # :post_run: Proc(FuncTestRunResult, tmp_dir_path) - optional result capture block.
75
+ # run_result will be populated, but you can add more to the ostruct .payload
76
+ # Your PWD will be the tmp_dir, and it will still exist (for a moment!)
77
+ def run_inspec_process_with_this_plugin(command_line, opts = {})
78
+ plugin_path = __find_plugin_path_from_caller
79
+
80
+ # If it looks like it is a core plugin under test, don't add it to the plugin file
81
+ # since the loader will auto-load it anyway
82
+ if plugin_path.include?('lib/plugins/inspec-')
83
+ plugin_file_data = __make_empty_plugin_file_data_structure
84
+ else
85
+ plugin_file_data = __make_plugin_file_data_structure_with_path(plugin_path)
86
+ end
87
+
88
+ Dir.mktmpdir do |tmp_dir|
89
+ opts[:pre_run]&.call(plugin_file_data, tmp_dir)
90
+ plugin_file_path = File.join(tmp_dir, 'plugins.json')
91
+ # HACK: If the block cleared the hash, take that to mean it will provide a plugins.json file of its own.
92
+ File.write(plugin_file_path, JSON.generate(plugin_file_data)) unless plugin_file_data.empty?
93
+ opts[:env] ||= {}
94
+ opts[:env]['INSPEC_CONFIG_DIR'] = tmp_dir
95
+ run_result = run_inspec_process(command_line, opts)
96
+
97
+ # Read the resulting plugins.json into memory, if any
98
+ if File.exist?(plugin_file_path)
99
+ run_result.payload.plugin_data = JSON.parse(File.read(plugin_file_path))
100
+ end
101
+
102
+ opts[:post_run]&.call(run_result, tmp_dir)
103
+ run_result
104
+ end
105
+ end
106
+
107
+ def __find_plugin_path_from_caller(frames_back = 2)
108
+ caller_path = Pathname.new(caller_locations(frames_back, 1).first.absolute_path)
109
+ # Typical caller path:
110
+ # /Users/cwolfe/sandbox/inspec-resource-lister/test/functional/inspec_resource_lister_test.rb
111
+ # We want:
112
+ # /Users/cwolfe/sandbox/inspec-resource-lister/lib/inspec-resource-lister.rb
113
+ cursor = caller_path
114
+ until cursor.basename.to_s == 'test' && cursor.parent.basename.to_s =~ /^(inspec|train)-/
115
+ cursor = cursor.parent
116
+ break if cursor.nil?
117
+ end
118
+ raise 'Could not comprehend plugin project directory structure' if cursor.nil?
119
+
120
+ project_dir = cursor.parent
121
+ plugin_name = project_dir.basename
122
+ entry_point = File.join(project_dir.to_s, 'lib', plugin_name.to_s + '.rb')
123
+ raise 'Could not find plugin entry point' unless File.exist?(entry_point)
124
+ entry_point
125
+ end
126
+
127
+ def __make_plugin_file_data_structure_with_path(path)
128
+ # TODO: dry this up, refs #3350
129
+ plugin_name = File.basename(path, '.rb')
130
+ data = __make_empty_plugin_file_data_structure
131
+ data['plugins'] << {
132
+ 'name' => plugin_name,
133
+ 'installation_type' => 'path',
134
+ 'installation_path' => path,
135
+ }
136
+ end
137
+
138
+ def __make_empty_plugin_file_data_structure
139
+ # TODO: dry this up, refs #3350
140
+ {
141
+ 'plugins_config_version' => '1.0.0',
142
+ 'plugins' => [],
143
+ }
44
144
  end
45
145
  end
46
146
 
47
147
  module CorePluginUnitHelper
48
148
  include CorePluginBaseHelper
49
- require 'inspec'
50
149
  end
@@ -0,0 +1,14 @@
1
+ # docs/plugins
2
+ # Update that train plugins are now possible
3
+
4
+ # test/unit/plugin/v2/loader_test.rb
5
+ # TODO: loading all plugins does not activate Train plugins
6
+
7
+ # lib/plugins/inspec-plugin-manager-cli/test/functional/inspec-plugin_test.rb
8
+ # ~ Should be able to install a train plugin - in place, will fail until plugin published
9
+ # ~ Should be able to search for a train plugin - in place, will fail until plugin published
10
+
11
+ # test/functional/plugins_test.rb
12
+ # % Should be able to suggest a train transport plugin when an unsupported --target schema is used and a gem search is successful
13
+ # % Should be able to suggest a train transport plugin when an unrecognized profile platform declaration is used and a gem search is successful
14
+ # - Should be able to run inspec detect targeting a test target
@@ -19,6 +19,8 @@ require 'resources/aws/aws_cloudwatch_log_metric_filter'
19
19
  require 'resources/aws/aws_config_delivery_channel'
20
20
  require 'resources/aws/aws_config_recorder'
21
21
  require 'resources/aws/aws_ec2_instance'
22
+ require 'resources/aws/aws_ebs_volume'
23
+ require 'resources/aws/aws_ebs_volumes'
22
24
  require 'resources/aws/aws_flow_log'
23
25
  require 'resources/aws/aws_ec2_instances'
24
26
  require 'resources/aws/aws_ecs_cluster'
@@ -0,0 +1,122 @@
1
+ class AwsEbsVolume < Inspec.resource(1)
2
+ name 'aws_ebs_volume'
3
+ desc 'Verifies settings for an EBS volume'
4
+
5
+ example <<-EOX
6
+ describe aws_ebs_volume('vol-123456') do
7
+ it { should be_encrypted }
8
+ its('size') { should cmp 8 }
9
+ end
10
+
11
+ describe aws_ebs_volume(name: 'my-volume') do
12
+ its('encrypted') { should eq true }
13
+ its('iops') { should cmp 100 }
14
+ end
15
+ EOX
16
+ supports platform: 'aws'
17
+
18
+ # TODO: rewrite to avoid direct injection, match other resources, use AwsSingularResourceMixin
19
+ def initialize(opts, conn = nil)
20
+ @opts = opts
21
+ @display_name = opts.is_a?(Hash) ? @opts[:name] : opts
22
+ @ec2_client = conn ? conn.ec2_client : inspec_runner.backend.aws_client(Aws::EC2::Client)
23
+ @ec2_resource = conn ? conn.ec2_resource : inspec_runner.backend.aws_resource(Aws::EC2::Resource, {})
24
+ end
25
+
26
+ # TODO: DRY up, see https://github.com/chef/inspec/issues/2633
27
+ # Copied from resource_support/aws/aws_resource_mixin.rb
28
+ def catch_aws_errors
29
+ yield
30
+ rescue Aws::Errors::MissingCredentialsError
31
+ # The AWS error here is unhelpful:
32
+ # "unable to sign request without credentials set"
33
+ Inspec::Log.error "It appears that you have not set your AWS credentials. You may set them using environment variables, or using the 'aws://region/aws_credentials_profile' target. See https://www.inspec.io/docs/reference/platforms for details."
34
+ fail_resource('No AWS credentials available')
35
+ rescue Aws::Errors::ServiceError => e
36
+ fail_resource(e.message)
37
+ end
38
+
39
+ # TODO: DRY up, see https://github.com/chef/inspec/issues/2633
40
+ # Copied from resource_support/aws/aws_singular_resource_mixin.rb
41
+ def inspec_runner
42
+ # When running under inspec-cli, we have an 'inspec' method that
43
+ # returns the runner. When running under unit tests, we don't
44
+ # have that, but we still have to call this to pass something
45
+ # (nil is OK) to the backend.
46
+ # TODO: remove with https://github.com/chef/inspec-aws/issues/216
47
+ # TODO: remove after rewrite to include AwsSingularResource
48
+ inspec if respond_to?(:inspec)
49
+ end
50
+
51
+ def id
52
+ return @volume_id if defined?(@volume_id)
53
+ catch_aws_errors do
54
+ if @opts.is_a?(Hash)
55
+ first = @ec2_resource.volumes(
56
+ {
57
+ filters: [{
58
+ name: 'tag:Name',
59
+ values: [@opts[:name]],
60
+ }],
61
+ },
62
+ ).first
63
+ # catch case where the volume is not known
64
+ @volume_id = first.id unless first.nil?
65
+ else
66
+ @volume_id = @opts
67
+ end
68
+ end
69
+ end
70
+ alias volume_id id
71
+
72
+ def exists?
73
+ !volume.nil?
74
+ end
75
+
76
+ def encrypted?
77
+ volume.encrypted
78
+ end
79
+
80
+ # attributes that we want to expose
81
+ %w{
82
+ availability_zone encrypted iops kms_key_id size snapshot_id state volume_type
83
+ }.each do |attribute|
84
+ define_method attribute do
85
+ catch_aws_errors do
86
+ volume.send(attribute) if volume
87
+ end
88
+ end
89
+ end
90
+
91
+ # Don't document this - it's a bit hard to use. Our current doctrine
92
+ # is to use dumb things, like arrays of strings - use security_group_ids instead.
93
+ def security_groups
94
+ catch_aws_errors do
95
+ @security_groups ||= volume.security_groups.map { |sg|
96
+ { id: sg.group_id, name: sg.group_name }
97
+ }
98
+ end
99
+ end
100
+
101
+ def security_group_ids
102
+ catch_aws_errors do
103
+ @security_group_ids ||= volume.security_groups.map(&:group_id)
104
+ end
105
+ end
106
+
107
+ def tags
108
+ catch_aws_errors do
109
+ @tags ||= volume.tags.map { |tag| { key: tag.key, value: tag.value } }
110
+ end
111
+ end
112
+
113
+ def to_s
114
+ "EBS Volume #{@display_name}"
115
+ end
116
+
117
+ private
118
+
119
+ def volume
120
+ catch_aws_errors { @volume ||= @ec2_resource.volume(id) }
121
+ end
122
+ end
@@ -0,0 +1,63 @@
1
+ class AwsEbsVolumes < Inspec.resource(1)
2
+ name 'aws_ebs_volumes'
3
+ desc 'Verifies settings for AWS EBS Volumes in bulk'
4
+ example '
5
+ describe aws_ebs_volumes do
6
+ it { should exist }
7
+ end
8
+ '
9
+ supports platform: 'aws'
10
+
11
+ include AwsPluralResourceMixin
12
+ def validate_params(resource_params)
13
+ unless resource_params.empty?
14
+ raise ArgumentError, 'aws_ebs_volumes does not accept resource parameters.'
15
+ end
16
+ resource_params
17
+ end
18
+
19
+ # Underlying FilterTable implementation.
20
+ filter = FilterTable.create
21
+ filter.register_custom_matcher(:exists?) { |x| !x.entries.empty? }
22
+ filter.register_column(:volume_ids, field: :volume_id)
23
+ filter.install_filter_methods_on_resource(self, :table)
24
+
25
+ def to_s
26
+ 'EBS Volumes'
27
+ end
28
+
29
+ def fetch_from_api
30
+ backend = BackendFactory.create(inspec_runner)
31
+ @table = []
32
+ pagination_opts = {}
33
+ loop do
34
+ api_result = backend.describe_volumes(pagination_opts)
35
+ @table += unpack_describe_volumes_response(api_result.volumes)
36
+ break unless api_result.next_token
37
+ pagination_opts = { next_token: api_result.next_token }
38
+ end
39
+ end
40
+
41
+ def unpack_describe_volumes_response(volumes)
42
+ volume_rows = []
43
+ volumes.each do |res|
44
+ volume_rows += res.attachments.map do |volume_struct|
45
+ {
46
+ volume_id: volume_struct.volume_id,
47
+ }
48
+ end
49
+ end
50
+ volume_rows
51
+ end
52
+
53
+ class Backend
54
+ class AwsClientApi < AwsBackendBase
55
+ BackendFactory.set_default_backend(self)
56
+ self.aws_client_class = Aws::EC2::Client
57
+
58
+ def describe_volumes(query)
59
+ aws_service_client.describe_volumes(query)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -473,22 +473,26 @@ module Inspec::Resources
473
473
 
474
474
  def parse_netstat_line(line)
475
475
  # parse each line
476
- # 1 - Proto, 2 - Recv-Q, 3 - Send-Q, 4 - Local Address, 5 - Foreign Address, 6 - State, 7 - Inode, 8 - PID/Program name
477
- parsed = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)?\s+(\S+)\s+(\S+)\s+(\S+)/.match(line)
476
+ # 1 - Proto, 2 - Recv-Q, 3 - Send-Q, 4 - Local Address, 5 - Foreign Address, 6 - State, 7 - User, 8 - Inode, 9 - PID/Program name
477
+ # * UDP lines have an empty State column and the Busybox variant lacks
478
+ # the User and Inode columns.
479
+ reg = /^(?<proto>\S+)\s+(\S+)\s+(\S+)\s+(?<local_addr>\S+)\s+(?<foreign_addr>\S+)\s+(\S+)?\s+((\S+)\s+(\S+)\s+)?(?<pid_prog>\S+)/
480
+ parsed = reg.match(line)
481
+
478
482
  return {} if parsed.nil? || line.match(/^proto/i)
479
483
 
480
484
  # parse ip4 and ip6 addresses
481
- protocol = parsed[1].downcase
485
+ protocol = parsed[:proto].downcase
482
486
 
483
487
  # detect protocol if not provided
484
- protocol += '6' if parsed[4].count(':') > 1 && %w{tcp udp}.include?(protocol)
488
+ protocol += '6' if parsed[:local_addr].count(':') > 1 && %w{tcp udp}.include?(protocol)
485
489
 
486
490
  # extract host and port information
487
- host, port = parse_net_address(parsed[4], protocol)
491
+ host, port = parse_net_address(parsed[:local_addr], protocol)
488
492
  return {} if host.nil?
489
493
 
490
494
  # extract PID
491
- process = parsed[9].split('/')
495
+ process = parsed[:pid_prog].split('/')
492
496
  pid = process[0]
493
497
  pid = pid.to_i if pid =~ /^\d+$/
494
498
  process = process[1]