inspec 0.14.8 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -2
  3. data/bin/inspec +3 -4
  4. data/examples/inheritance/README.md +19 -0
  5. data/examples/inheritance/controls/example.rb +11 -0
  6. data/examples/inheritance/inspec.yml +10 -0
  7. data/lib/bundles/inspec-compliance/cli.rb +1 -4
  8. data/lib/bundles/inspec-supermarket/cli.rb +1 -4
  9. data/lib/inspec/dsl.rb +48 -55
  10. data/lib/inspec/profile.rb +6 -2
  11. data/lib/inspec/profile_context.rb +21 -8
  12. data/lib/inspec/runner.rb +17 -12
  13. data/lib/inspec/runner_rspec.rb +1 -0
  14. data/lib/inspec/version.rb +1 -1
  15. data/lib/resources/apache.rb +20 -18
  16. data/lib/resources/apache_conf.rb +92 -90
  17. data/lib/resources/apt.rb +92 -90
  18. data/lib/resources/audit_policy.rb +35 -33
  19. data/lib/resources/auditd_conf.rb +41 -39
  20. data/lib/resources/auditd_rules.rb +155 -153
  21. data/lib/resources/bond.rb +1 -1
  22. data/lib/resources/bridge.rb +97 -95
  23. data/lib/resources/command.rb +47 -45
  24. data/lib/resources/csv.rb +23 -21
  25. data/lib/resources/directory.rb +1 -1
  26. data/lib/resources/etc_group.rb +116 -114
  27. data/lib/resources/file.rb +1 -1
  28. data/lib/resources/gem.rb +39 -37
  29. data/lib/resources/group.rb +100 -98
  30. data/lib/resources/host.rb +103 -101
  31. data/lib/resources/inetd_conf.rb +42 -40
  32. data/lib/resources/ini.rb +15 -13
  33. data/lib/resources/interface.rb +106 -104
  34. data/lib/resources/iptables.rb +36 -34
  35. data/lib/resources/json.rb +64 -62
  36. data/lib/resources/kernel_module.rb +30 -28
  37. data/lib/resources/kernel_parameter.rb +44 -42
  38. data/lib/resources/limits_conf.rb +41 -39
  39. data/lib/resources/login_def.rb +38 -36
  40. data/lib/resources/mount.rb +43 -41
  41. data/lib/resources/mysql.rb +67 -65
  42. data/lib/resources/mysql_conf.rb +89 -87
  43. data/lib/resources/mysql_session.rb +46 -44
  44. data/lib/resources/npm.rb +35 -33
  45. data/lib/resources/ntp_conf.rb +44 -42
  46. data/lib/resources/oneget.rb +46 -44
  47. data/lib/resources/os.rb +22 -20
  48. data/lib/resources/os_env.rb +47 -45
  49. data/lib/resources/package.rb +213 -211
  50. data/lib/resources/parse_config.rb +59 -57
  51. data/lib/resources/passwd.rb +89 -87
  52. data/lib/resources/pip.rb +60 -58
  53. data/lib/resources/port.rb +352 -350
  54. data/lib/resources/postgres.rb +26 -24
  55. data/lib/resources/postgres_conf.rb +66 -64
  56. data/lib/resources/postgres_session.rb +47 -45
  57. data/lib/resources/processes.rb +56 -54
  58. data/lib/resources/registry_key.rb +150 -148
  59. data/lib/resources/script.rb +30 -28
  60. data/lib/resources/security_policy.rb +56 -54
  61. data/lib/resources/service.rb +638 -636
  62. data/lib/resources/shadow.rb +98 -96
  63. data/lib/resources/ssh_conf.rb +58 -56
  64. data/lib/resources/user.rb +363 -361
  65. data/lib/resources/windows_feature.rb +46 -44
  66. data/lib/resources/xinetd.rb +111 -109
  67. data/lib/resources/yaml.rb +16 -14
  68. data/lib/resources/yum.rb +107 -105
  69. data/lib/utils/base_cli.rb +18 -0
  70. data/test/helper.rb +2 -2
  71. data/test/unit/profile_context_test.rb +1 -1
  72. data/test/unit/resources/file_test.rb +1 -1
  73. data/test/unit/resources/mount_test.rb +1 -1
  74. metadata +5 -2
@@ -6,51 +6,53 @@
6
6
 
7
7
  require 'utils/simpleconfig'
8
8
 
9
- class InetdConf < Inspec.resource(1)
10
- name 'inetd_conf'
11
- desc 'Use the inetd_conf InSpec audit resource to test if a service is enabled in the inetd.conf file on Linux and UNIX platforms. inetd---the Internet service daemon---listens on dedicated ports, and then loads the appropriate program based on a request. The inetd.conf file is typically located at /etc/inetd.conf and contains a list of Internet services associated to the ports on which that service will listen. Only enabled services may handle a request; only services that are required by the system should be enabled.'
12
- example "
13
- describe inetd_conf do
14
- its('shell') { should eq nil }
15
- its('login') { should eq nil }
16
- its('exec') { should eq nil }
9
+ module Inspec::Resources
10
+ class InetdConf < Inspec.resource(1)
11
+ name 'inetd_conf'
12
+ desc 'Use the inetd_conf InSpec audit resource to test if a service is enabled in the inetd.conf file on Linux and UNIX platforms. inetd---the Internet service daemon---listens on dedicated ports, and then loads the appropriate program based on a request. The inetd.conf file is typically located at /etc/inetd.conf and contains a list of Internet services associated to the ports on which that service will listen. Only enabled services may handle a request; only services that are required by the system should be enabled.'
13
+ example "
14
+ describe inetd_conf do
15
+ its('shell') { should eq nil }
16
+ its('login') { should eq nil }
17
+ its('exec') { should eq nil }
18
+ end
19
+ "
20
+
21
+ def initialize(path = nil)
22
+ @conf_path = path || '/etc/inetd.conf'
17
23
  end
18
- "
19
24
 
20
- def initialize(path = nil)
21
- @conf_path = path || '/etc/inetd.conf'
22
- end
23
-
24
- def method_missing(name)
25
- read_params[name.to_s]
26
- end
27
-
28
- def read_params
29
- return @params if defined?(@params)
30
-
31
- # read the file
32
- file = inspec.file(@conf_path)
33
- if !file.file?
34
- skip_resource "Can't find file \"#{@conf_path}\""
35
- return @params = {}
25
+ def method_missing(name)
26
+ read_params[name.to_s]
36
27
  end
37
28
 
38
- content = file.content
39
- if content.empty? && file.size > 0
40
- skip_resource "Can't read file \"#{@conf_path}\""
41
- return @params = {}
29
+ def read_params
30
+ return @params if defined?(@params)
31
+
32
+ # read the file
33
+ file = inspec.file(@conf_path)
34
+ if !file.file?
35
+ skip_resource "Can't find file \"#{@conf_path}\""
36
+ return @params = {}
37
+ end
38
+
39
+ content = file.content
40
+ if content.empty? && file.size > 0
41
+ skip_resource "Can't read file \"#{@conf_path}\""
42
+ return @params = {}
43
+ end
44
+ # parse the file
45
+ conf = SimpleConfig.new(
46
+ content,
47
+ assignment_re: /^\s*(\S+?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s*$/,
48
+ key_vals: 6,
49
+ multiple_values: false,
50
+ )
51
+ @params = conf.params
42
52
  end
43
- # parse the file
44
- conf = SimpleConfig.new(
45
- content,
46
- assignment_re: /^\s*(\S+?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s*$/,
47
- key_vals: 6,
48
- multiple_values: false,
49
- )
50
- @params = conf.params
51
- end
52
53
 
53
- def to_s
54
- 'inetd.conf'
54
+ def to_s
55
+ 'inetd.conf'
56
+ end
55
57
  end
56
58
  end
data/lib/resources/ini.rb CHANGED
@@ -4,20 +4,22 @@
4
4
 
5
5
  require 'utils/simpleconfig'
6
6
 
7
- class IniConfig < JsonConfig
8
- name 'ini'
9
- desc 'Use the ini InSpec audit resource to test data in a INI file.'
10
- example "
11
- descibe ini do
12
- its('auth_protocol') { should eq 'https' }
7
+ module Inspec::Resources
8
+ class IniConfig < JsonConfig
9
+ name 'ini'
10
+ desc 'Use the ini InSpec audit resource to test data in a INI file.'
11
+ example "
12
+ descibe ini do
13
+ its('auth_protocol') { should eq 'https' }
14
+ end
15
+ "
16
+ # override file load and parse hash with simple config
17
+ def parse(content)
18
+ SimpleConfig.new(content).params
13
19
  end
14
- "
15
- # override file load and parse hash with simple config
16
- def parse(content)
17
- SimpleConfig.new(content).params
18
- end
19
20
 
20
- def to_s
21
- "INI #{@path}"
21
+ def to_s
22
+ "INI #{@path}"
23
+ end
22
24
  end
23
25
  end
@@ -4,124 +4,126 @@
4
4
 
5
5
  require 'utils/convert'
6
6
 
7
- class NetworkInterface < Inspec.resource(1)
8
- name 'interface'
9
- desc 'Use the interface InSpec audit resource to test basic network adapter properties, such as name, status, state, address, and link speed (in MB/sec).'
10
- example "
11
- describe interface('eth0') do
12
- it { should exist }
13
- it { should be_up }
14
- its(:speed) { should eq 1000 }
7
+ module Inspec::Resources
8
+ class NetworkInterface < Inspec.resource(1)
9
+ name 'interface'
10
+ desc 'Use the interface InSpec audit resource to test basic network adapter properties, such as name, status, state, address, and link speed (in MB/sec).'
11
+ example "
12
+ describe interface('eth0') do
13
+ it { should exist }
14
+ it { should be_up }
15
+ its(:speed) { should eq 1000 }
16
+ end
17
+ "
18
+ def initialize(iface)
19
+ @iface = iface
20
+
21
+ @interface_provider = nil
22
+ if inspec.os.linux?
23
+ @interface_provider = LinuxInterface.new(inspec)
24
+ elsif inspec.os.windows?
25
+ @interface_provider = WindowsInterface.new(inspec)
26
+ else
27
+ return skip_resource 'The `interface` resource is not supported on your OS yet.'
28
+ end
15
29
  end
16
- "
17
- def initialize(iface)
18
- @iface = iface
19
-
20
- @interface_provider = nil
21
- if inspec.os.linux?
22
- @interface_provider = LinuxInterface.new(inspec)
23
- elsif inspec.os.windows?
24
- @interface_provider = WindowsInterface.new(inspec)
25
- else
26
- return skip_resource 'The `interface` resource is not supported on your OS yet.'
27
- end
28
- end
29
-
30
- def exists?
31
- !interface_info.nil? && !interface_info[:name].nil?
32
- end
33
-
34
- def up?
35
- interface_info.nil? ? false : interface_info[:up]
36
- end
37
-
38
- # returns link speed in Mbits/sec
39
- def speed
40
- interface_info.nil? ? nil : interface_info[:speed]
41
- end
42
-
43
- def to_s
44
- "Interface #{@iface}"
45
- end
46
-
47
- private
48
-
49
- def interface_info
50
- return @cache if defined?(@cache)
51
- @cache = @interface_provider.interface_info(@iface) if !@interface_provider.nil?
52
- end
53
- end
54
30
 
55
- class InterfaceInfo
56
- include Converter
57
- attr_reader :inspec
58
- def initialize(inspec)
59
- @inspec = inspec
60
- end
61
- end
62
-
63
- class LinuxInterface < InterfaceInfo
64
- def interface_info(iface)
65
- # will return "[mtu]\n1500\n[type]\n1"
66
- cmd = inspec.command("find /sys/class/net/#{iface}/ -type f -maxdepth 1 -exec sh -c 'echo \"[$(basename {})]\"; cat {} || echo -n' \\;")
67
- return nil if cmd.exit_status.to_i != 0
68
-
69
- # parse values, we only recieve values, therefore we threat them as keys
70
- params = SimpleConfig.new(cmd.stdout.chomp).params
71
-
72
- # abort if we got an empty result-set
73
- return nil if params.empty?
31
+ def exists?
32
+ !interface_info.nil? && !interface_info[:name].nil?
33
+ end
74
34
 
75
- # parse state
76
- state = false
77
- if params.key?('operstate')
78
- operstate, _value = params['operstate'].first
79
- state = operstate == 'up'
35
+ def up?
36
+ interface_info.nil? ? false : interface_info[:up]
80
37
  end
81
38
 
82
- # parse speed
83
- speed = nil
84
- if params.key?('speed')
85
- speed, _value = params['speed'].first
86
- speed = convert_to_i(speed)
39
+ # returns link speed in Mbits/sec
40
+ def speed
41
+ interface_info.nil? ? nil : interface_info[:speed]
87
42
  end
88
43
 
89
- {
90
- name: iface,
91
- up: state,
92
- speed: speed,
93
- }
94
- end
95
- end
44
+ def to_s
45
+ "Interface #{@iface}"
46
+ end
96
47
 
97
- class WindowsInterface < InterfaceInfo
98
- def interface_info(iface)
99
- # gather all network interfaces
100
- cmd = inspec.command('Get-NetAdapter | Select-Object -Property Name, InterfaceDescription, Status, State, MacAddress, LinkSpeed, ReceiveLinkSpeed, TransmitLinkSpeed, Virtual | ConvertTo-Json')
48
+ private
101
49
 
102
- # filter network interface
103
- begin
104
- net_adapter = JSON.parse(cmd.stdout)
105
- rescue JSON::ParserError => _e
106
- return nil
50
+ def interface_info
51
+ return @cache if defined?(@cache)
52
+ @cache = @interface_provider.interface_info(@iface) if !@interface_provider.nil?
107
53
  end
54
+ end
108
55
 
109
- # ensure we have an array of groups
110
- net_adapter = [net_adapter] if !net_adapter.is_a?(Array)
56
+ class InterfaceInfo
57
+ include Converter
58
+ attr_reader :inspec
59
+ def initialize(inspec)
60
+ @inspec = inspec
61
+ end
62
+ end
111
63
 
112
- # select the requested interface
113
- adapters = net_adapter.each_with_object([]) do |adapter, adapter_collection|
114
- # map object
115
- info = {
116
- name: adapter['Name'],
117
- up: adapter['State'] == 2,
118
- speed: adapter['ReceiveLinkSpeed'] / 1000,
64
+ class LinuxInterface < InterfaceInfo
65
+ def interface_info(iface)
66
+ # will return "[mtu]\n1500\n[type]\n1"
67
+ cmd = inspec.command("find /sys/class/net/#{iface}/ -type f -maxdepth 1 -exec sh -c 'echo \"[$(basename {})]\"; cat {} || echo -n' \\;")
68
+ return nil if cmd.exit_status.to_i != 0
69
+
70
+ # parse values, we only recieve values, therefore we threat them as keys
71
+ params = SimpleConfig.new(cmd.stdout.chomp).params
72
+
73
+ # abort if we got an empty result-set
74
+ return nil if params.empty?
75
+
76
+ # parse state
77
+ state = false
78
+ if params.key?('operstate')
79
+ operstate, _value = params['operstate'].first
80
+ state = operstate == 'up'
81
+ end
82
+
83
+ # parse speed
84
+ speed = nil
85
+ if params.key?('speed')
86
+ speed, _value = params['speed'].first
87
+ speed = convert_to_i(speed)
88
+ end
89
+
90
+ {
91
+ name: iface,
92
+ up: state,
93
+ speed: speed,
119
94
  }
120
- adapter_collection.push(info) if info[:name].casecmp(iface) == 0
121
95
  end
96
+ end
122
97
 
123
- return nil if adapters.size == 0
124
- warn "[Possible Error] detected multiple network interfaces with the name #{iface}" if adapters.size > 1
125
- adapters[0]
98
+ class WindowsInterface < InterfaceInfo
99
+ def interface_info(iface)
100
+ # gather all network interfaces
101
+ cmd = inspec.command('Get-NetAdapter | Select-Object -Property Name, InterfaceDescription, Status, State, MacAddress, LinkSpeed, ReceiveLinkSpeed, TransmitLinkSpeed, Virtual | ConvertTo-Json')
102
+
103
+ # filter network interface
104
+ begin
105
+ net_adapter = JSON.parse(cmd.stdout)
106
+ rescue JSON::ParserError => _e
107
+ return nil
108
+ end
109
+
110
+ # ensure we have an array of groups
111
+ net_adapter = [net_adapter] if !net_adapter.is_a?(Array)
112
+
113
+ # select the requested interface
114
+ adapters = net_adapter.each_with_object([]) do |adapter, adapter_collection|
115
+ # map object
116
+ info = {
117
+ name: adapter['Name'],
118
+ up: adapter['State'] == 2,
119
+ speed: adapter['ReceiveLinkSpeed'] / 1000,
120
+ }
121
+ adapter_collection.push(info) if info[:name].casecmp(iface) == 0
122
+ end
123
+
124
+ return nil if adapters.size == 0
125
+ warn "[Possible Error] detected multiple network interfaces with the name #{iface}" if adapters.size > 1
126
+ adapters[0]
127
+ end
126
128
  end
127
129
  end
@@ -21,48 +21,50 @@
21
21
  # @see http://ipset.netfilter.org/iptables.man.html
22
22
  # @see http://ipset.netfilter.org/iptables.man.html
23
23
  # @see https://www.frozentux.net/iptables-tutorial/iptables-tutorial.html
24
- class IpTables < Inspec.resource(1)
25
- name 'iptables'
26
- desc 'Use the iptables InSpec audit resource to test rules that are defined in iptables, which maintains tables of IP packet filtering rules. There may be more than one table. Each table contains one (or more) chains (both built-in and custom). A chain is a list of rules that match packets. When the rule matches, the rule defines what target to assign to the packet.'
27
- example "
28
- describe iptables do
29
- it { should have_rule('-P INPUT ACCEPT') }
30
- end
31
- "
24
+ module Inspec::Resources
25
+ class IpTables < Inspec.resource(1)
26
+ name 'iptables'
27
+ desc 'Use the iptables InSpec audit resource to test rules that are defined in iptables, which maintains tables of IP packet filtering rules. There may be more than one table. Each table contains one (or more) chains (both built-in and custom). A chain is a list of rules that match packets. When the rule matches, the rule defines what target to assign to the packet.'
28
+ example "
29
+ describe iptables do
30
+ it { should have_rule('-P INPUT ACCEPT') }
31
+ end
32
+ "
32
33
 
33
- def initialize(params = {})
34
- @table = params[:table]
35
- @chain = params[:chain]
34
+ def initialize(params = {})
35
+ @table = params[:table]
36
+ @chain = params[:chain]
36
37
 
37
- # we're done if we are on linux
38
- return if inspec.os.linux?
38
+ # we're done if we are on linux
39
+ return if inspec.os.linux?
39
40
 
40
- # ensures, all calls are aborted for non-supported os
41
- @iptables_cache = []
42
- skip_resource 'The `iptables` resource is not supported on your OS yet.'
43
- end
41
+ # ensures, all calls are aborted for non-supported os
42
+ @iptables_cache = []
43
+ skip_resource 'The `iptables` resource is not supported on your OS yet.'
44
+ end
44
45
 
45
- def has_rule?(rule = nil, _table = nil, _chain = nil)
46
- # checks if the rule is part of the ruleset
47
- # for now, we expect an exact match
48
- retrieve_rules.any? { |line| line.casecmp(rule) == 0 }
49
- end
46
+ def has_rule?(rule = nil, _table = nil, _chain = nil)
47
+ # checks if the rule is part of the ruleset
48
+ # for now, we expect an exact match
49
+ retrieve_rules.any? { |line| line.casecmp(rule) == 0 }
50
+ end
50
51
 
51
- def retrieve_rules
52
- return @iptables_cache if defined?(@iptables_cache)
52
+ def retrieve_rules
53
+ return @iptables_cache if defined?(@iptables_cache)
53
54
 
54
- # construct iptables command to read all rules
55
- table_cmd = "-t #{@table}" if @table
56
- iptables_cmd = format('iptables %s -S %s', table_cmd, @chain).strip
55
+ # construct iptables command to read all rules
56
+ table_cmd = "-t #{@table}" if @table
57
+ iptables_cmd = format('iptables %s -S %s', table_cmd, @chain).strip
57
58
 
58
- cmd = inspec.command(iptables_cmd)
59
- return [] if cmd.exit_status.to_i != 0
59
+ cmd = inspec.command(iptables_cmd)
60
+ return [] if cmd.exit_status.to_i != 0
60
61
 
61
- # split rules, returns array or rules
62
- @iptables_cache = cmd.stdout.split("\n").map(&:strip)
63
- end
62
+ # split rules, returns array or rules
63
+ @iptables_cache = cmd.stdout.split("\n").map(&:strip)
64
+ end
64
65
 
65
- def to_s
66
- format('Iptables %s %s', @table && "table: #{@table}", @chain && "chain: #{@chain}").strip
66
+ def to_s
67
+ format('Iptables %s %s', @table && "table: #{@table}", @chain && "chain: #{@chain}").strip
68
+ end
67
69
  end
68
70
  end
@@ -2,81 +2,83 @@
2
2
  # author: Christoph Hartmann
3
3
  # author: Dominik Richter
4
4
 
5
- class JsonConfig < Inspec.resource(1)
6
- name 'json'
7
- desc 'Use the json InSpec audit resource to test data in a JSON file.'
8
- example "
9
- describe json('policyfile.lock.json') do
10
- its('cookbook_locks.omnibus.version') { should eq('2.2.0') }
11
- end
12
- "
5
+ module Inspec::Resources
6
+ class JsonConfig < Inspec.resource(1)
7
+ name 'json'
8
+ desc 'Use the json InSpec audit resource to test data in a JSON file.'
9
+ example "
10
+ describe json('policyfile.lock.json') do
11
+ its('cookbook_locks.omnibus.version') { should eq('2.2.0') }
12
+ end
13
+ "
13
14
 
14
- # make params readable
15
- attr_reader :params
15
+ # make params readable
16
+ attr_reader :params
16
17
 
17
- def initialize(path)
18
- @path = path
19
- @file = inspec.file(@path)
20
- @file_content = @file.content
18
+ def initialize(path)
19
+ @path = path
20
+ @file = inspec.file(@path)
21
+ @file_content = @file.content
21
22
 
22
- # check if file is available
23
- if !@file.file?
24
- skip_resource "Can't find file \"#{@conf_path}\""
25
- return @params = {}
26
- end
23
+ # check if file is available
24
+ if !@file.file?
25
+ skip_resource "Can't find file \"#{@conf_path}\""
26
+ return @params = {}
27
+ end
27
28
 
28
- # check if file is readable
29
- if @file_content.empty? && @file.size > 0
30
- skip_resource "Can't read file \"#{@conf_path}\""
31
- return @params = {}
29
+ # check if file is readable
30
+ if @file_content.empty? && @file.size > 0
31
+ skip_resource "Can't read file \"#{@conf_path}\""
32
+ return @params = {}
33
+ end
34
+
35
+ @params = parse(@file_content)
32
36
  end
33
37
 
34
- @params = parse(@file_content)
35
- end
38
+ def parse(content)
39
+ require 'json'
40
+ JSON.parse(content)
41
+ end
36
42
 
37
- def parse(content)
38
- require 'json'
39
- JSON.parse(content)
40
- end
43
+ def value(key)
44
+ extract_value(key, @params)
45
+ end
41
46
 
42
- def value(key)
43
- extract_value(key, @params)
44
- end
47
+ # Shorthand to retrieve a parameter name via `#its`.
48
+ # Example: describe json('file') { its('paramX') { should eq 'Y' } }
49
+ #
50
+ # @param [String] name name of the field to retrieve
51
+ # @return [Object] the value stored at this position
52
+ def method_missing(*keys)
53
+ # catch bahavior of rspec its implementation
54
+ # @see https://github.com/rspec/rspec-its/blob/master/lib/rspec/its.rb#L110
55
+ keys.shift if keys.is_a?(Array) && keys[0] == :[]
56
+ value(keys)
57
+ end
45
58
 
46
- # Shorthand to retrieve a parameter name via `#its`.
47
- # Example: describe json('file') { its('paramX') { should eq 'Y' } }
48
- #
49
- # @param [String] name name of the field to retrieve
50
- # @return [Object] the value stored at this position
51
- def method_missing(*keys)
52
- # catch bahavior of rspec its implementation
53
- # @see https://github.com/rspec/rspec-its/blob/master/lib/rspec/its.rb#L110
54
- keys.shift if keys.is_a?(Array) && keys[0] == :[]
55
- value(keys)
56
- end
59
+ def to_s
60
+ "Json #{@path}"
61
+ end
57
62
 
58
- def to_s
59
- "Json #{@path}"
60
- end
63
+ private
61
64
 
62
- private
65
+ def extract_value(keys, value)
66
+ key = keys.shift
67
+ return nil if key.nil?
63
68
 
64
- def extract_value(keys, value)
65
- key = keys.shift
66
- return nil if key.nil?
69
+ # if value is an array, iterate over each child
70
+ if value.is_a?(Array)
71
+ value = value.map { |i|
72
+ extract_value([key], i)
73
+ }
74
+ else
75
+ value = value[key.to_s].nil? ? nil : value[key.to_s]
76
+ end
67
77
 
68
- # if value is an array, iterate over each child
69
- if value.is_a?(Array)
70
- value = value.map { |i|
71
- extract_value([key], i)
72
- }
73
- else
74
- value = value[key.to_s].nil? ? nil : value[key.to_s]
78
+ # if there are no more keys, just return the value
79
+ return value if keys.first.nil?
80
+ # if there are more keys, extract more
81
+ extract_value(keys.clone, value)
75
82
  end
76
-
77
- # if there are no more keys, just return the value
78
- return value if keys.first.nil?
79
- # if there are more keys, extract more
80
- extract_value(keys.clone, value)
81
83
  end
82
84
  end