inspec-core 4.20.10 → 4.22.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -0
  3. data/inspec-core.gemspec +1 -1
  4. data/lib/inspec/base_cli.rb +1 -1
  5. data/lib/inspec/exceptions.rb +1 -0
  6. data/lib/inspec/input_registry.rb +2 -1
  7. data/lib/inspec/metadata.rb +6 -1
  8. data/lib/inspec/plugin/v2/plugin_types/reporter.rb +11 -5
  9. data/lib/inspec/profile.rb +30 -9
  10. data/lib/inspec/reporters/base.rb +11 -5
  11. data/lib/inspec/reporters/cli.rb +1 -0
  12. data/lib/inspec/reporters/json.rb +9 -4
  13. data/lib/inspec/resources/apt.rb +2 -0
  14. data/lib/inspec/resources/interface.rb +55 -0
  15. data/lib/inspec/resources/interfaces.rb +119 -0
  16. data/lib/inspec/resources/mount.rb +1 -1
  17. data/lib/inspec/resources/mysql_session.rb +26 -7
  18. data/lib/inspec/resources/postgres_session.rb +1 -1
  19. data/lib/inspec/resources/service.rb +2 -2
  20. data/lib/inspec/resources/users.rb +1 -1
  21. data/lib/inspec/run_data.rb +1 -1
  22. data/lib/inspec/run_data/profile.rb +4 -4
  23. data/lib/inspec/runner.rb +8 -2
  24. data/lib/inspec/runner_rspec.rb +4 -1
  25. data/lib/inspec/schema.rb +2 -0
  26. data/lib/inspec/schema/exec_json.rb +4 -3
  27. data/lib/inspec/schema/primitives.rb +1 -1
  28. data/lib/inspec/version.rb +1 -1
  29. data/lib/plugins/inspec-reporter-html2/README.md +53 -0
  30. data/lib/plugins/inspec-reporter-html2/lib/inspec-reporter-html2.rb +18 -0
  31. data/lib/plugins/inspec-reporter-html2/lib/inspec-reporter-html2/reporter.rb +24 -0
  32. data/lib/plugins/inspec-reporter-html2/lib/inspec-reporter-html2/version.rb +8 -0
  33. data/lib/plugins/inspec-reporter-html2/templates/body.html.erb +46 -0
  34. data/lib/plugins/inspec-reporter-html2/templates/control.html.erb +77 -0
  35. data/lib/plugins/inspec-reporter-html2/templates/default.css +107 -0
  36. data/lib/plugins/inspec-reporter-html2/templates/default.js +79 -0
  37. data/lib/plugins/inspec-reporter-html2/templates/profile.html.erb +23 -0
  38. data/lib/plugins/inspec-reporter-html2/templates/result.html.erb +15 -0
  39. data/lib/plugins/inspec-reporter-html2/templates/selector.html.erb +8 -0
  40. metadata +22 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef611d3b2bb1d1c8ddca64adb1ed3eab50fcbec55a45cb49334ed2084c45e7da
4
- data.tar.gz: 96664cb6183b137db4f84cd26aed2b19496475045bbb458ea9d9004e92aad3f7
3
+ metadata.gz: b909b05a8f7510d2833aee0e464f0ed9cec64409fe5825fc05078c67b01e4a43
4
+ data.tar.gz: ee56167e3ab21f7eed5fd938e5728c3f48a7621fe71d1f3294cb4f2991298a79
5
5
  SHA512:
6
- metadata.gz: f1d44fc61e4663862a0628a0fcc4c4e3538bdb35d50cee94f1130c47fed5a982297da9c559128b0dddc2a7375658252924959020f24f518202b25939357340de
7
- data.tar.gz: e33e28b5863ce15a54c8f2f76dd3a458762c24fb7d365ba145ae06b6b0be14134b90da5f2610d834ae71a170e5f6b180c78a8ac8763f484b3bbcfa808eec8565
6
+ metadata.gz: 116d85fb0ef947cda4beb31c825cb02e134e2c9130338e062cc4e82d2d58a59e6f7fbdc8df4908759c0d508692efc00dac40550d0ce6ae9da72e904336f1fed6
7
+ data.tar.gz: e6208c479f04da18199aec2b1c8dc4c8e101bb08609b32fb9d026f485f69d7e848d62a68a6087171fa38a3486f44777d0ad56c64fe2743195b4cc1abb58f8ad9
data/Gemfile CHANGED
@@ -31,6 +31,7 @@ group :test do
31
31
  gem "m"
32
32
  gem "pry", "~> 0.10"
33
33
  gem "pry-byebug"
34
+ gem "html-proofer", platforms: :ruby # do not attempt to run proofer on windows
34
35
  end
35
36
 
36
37
  group :integration do
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.add_dependency "chef-telemetry", "~> 1.0"
27
27
  spec.add_dependency "license-acceptance", ">= 0.2.13", "< 2.0"
28
28
  spec.add_dependency "thor", ">= 0.20", "< 2.0"
29
- spec.add_dependency "json_schemer", "~> 0.2.1"
29
+ spec.add_dependency "json_schemer", ">= 0.2.1", "< 0.2.12"
30
30
  spec.add_dependency "method_source", ">= 0.8", "< 2.0"
31
31
  spec.add_dependency "rubyzip", "~> 1.2", ">= 1.2.2"
32
32
  spec.add_dependency "rspec", "~> 3.9"
@@ -231,7 +231,7 @@ module Inspec
231
231
 
232
232
  private
233
233
 
234
- ALL_OF_OUR_REPORTERS = %w{json json-min json-rspec json-automate junit html yaml documentation progress}.freeze # BUT WHY?!?!
234
+ ALL_OF_OUR_REPORTERS = %w{json json-min json-rspec json-automate junit html html2 yaml documentation progress}.freeze # BUT WHY?!?!
235
235
 
236
236
  def suppress_log_output?(opts)
237
237
  return false if opts["reporter"].nil?
@@ -4,6 +4,7 @@ module Inspec
4
4
  module Exceptions
5
5
  class InputsFileDoesNotExist < ArgumentError; end
6
6
  class InputsFileNotReadable < ArgumentError; end
7
+ class ProfileLoadFailed < StandardError; end
7
8
  class ResourceFailed < StandardError; end
8
9
  class ResourceSkipped < StandardError; end
9
10
  class SecretsBackendNotFound < ArgumentError; end
@@ -165,7 +165,8 @@ module Inspec
165
165
  raise ArgumentError, "ERROR: An '=' is required when using --input. Usage: --input input_name1=input_value1 input2=value2"
166
166
  end
167
167
  end
168
- input_name, input_value = pair.split("=")
168
+ pair = pair.match(/(.*?)=(.*)/)
169
+ input_name, input_value = pair[1], pair[2]
169
170
  input_value = parse_cli_input_value(input_name, input_value)
170
171
  evt = Inspec::Input::Event.new(
171
172
  value: input_value,
@@ -9,7 +9,12 @@ require "inspec/version"
9
9
  require "inspec/utils/spdx"
10
10
 
11
11
  module Inspec
12
- # Extract metadata.rb information
12
+ # The Metadata class represents a profile's metadata.
13
+ # This includes the metadata stored in the profile's metadata.rb file, as well as inferred
14
+ # metadata like if this profile supports the current runtime and the intended target.
15
+ # This class does NOT represent the runtime state of a profile during execution.
16
+ # See lib/inspec/profile.rb for the runtime representation of a profile.
17
+ #
13
18
  # A Metadata object may be created and finalized with invalid data.
14
19
  # This allows the check CLI command to analyse the issues.
15
20
  # Use valid? to determine if the metadata is coherent.
@@ -31,17 +31,14 @@ module Inspec::Plugin::V2::PluginType
31
31
  runtime_config = Inspec::Config.cached.respond_to?(:final_options) ? Inspec::Config.cached.final_options : {}
32
32
 
33
33
  message_truncation = runtime_config[:reporter_message_truncation] || "ALL"
34
- trunc = message_truncation == "ALL" ? -1 : message_truncation.to_i
34
+ @trunc = message_truncation == "ALL" ? -1 : message_truncation.to_i
35
35
  include_backtrace = runtime_config[:reporter_backtrace_inclusion].nil? ? true : runtime_config[:reporter_backtrace_inclusion]
36
36
 
37
37
  @run_data[:profiles]&.each do |p|
38
38
  p[:controls].each do |c|
39
39
  c[:results]&.map! do |r|
40
40
  r.delete(:backtrace) unless include_backtrace
41
- if r.key?(:message) && r[:message] != "" && trunc > -1
42
- r[:message] = r[:message][0...trunc] + "[Truncated to #{trunc} characters]"
43
- end
44
- r
41
+ process_message_truncation(r)
45
42
  end
46
43
  end
47
44
  end
@@ -64,5 +61,14 @@ module Inspec::Plugin::V2::PluginType
64
61
  def self.run_data_schema_constraints
65
62
  raise NotImplementedError, "#{self.class} must implement a `run_data_schema_constraints` class method to declare its compatibiltity with the RunData API."
66
63
  end
64
+
65
+ private
66
+
67
+ def process_message_truncation(result)
68
+ if result.key?(:message) && result[:message] != "" && @trunc > -1 && result[:message].length > @trunc
69
+ result[:message] = result[:message][0...@trunc] + "[Truncated to #{@trunc} characters]"
70
+ end
71
+ result
72
+ end
67
73
  end
68
74
  end
@@ -94,6 +94,7 @@ module Inspec
94
94
  @input_values = options[:inputs]
95
95
  @tests_collected = false
96
96
  @libraries_loaded = false
97
+ @state = :loaded
97
98
  @check_mode = options[:check_mode] || false
98
99
  @parent_profile = options[:parent_profile]
99
100
  @legacy_profile_path = options[:profiles_path] || false
@@ -146,7 +147,12 @@ module Inspec
146
147
  options[:profile_context] ||
147
148
  Inspec::ProfileContext.for_profile(self, @backend)
148
149
 
149
- @supports_platform = metadata.supports_platform?(@backend)
150
+ if metadata.supports_platform?(@backend)
151
+ @supports_platform = true
152
+ else
153
+ @supports_platform = false
154
+ @state = :skipped
155
+ end
150
156
  @supports_runtime = metadata.supports_runtime?
151
157
  end
152
158
 
@@ -162,6 +168,10 @@ module Inspec
162
168
  @writable
163
169
  end
164
170
 
171
+ def failed?
172
+ @state == :failed
173
+ end
174
+
165
175
  #
166
176
  # Is this profile is supported on the current platform of the
167
177
  # backend machine and the current inspec version.
@@ -197,7 +207,7 @@ module Inspec
197
207
  end
198
208
 
199
209
  def collect_tests(include_list = @controls)
200
- unless @tests_collected
210
+ unless @tests_collected || failed?
201
211
  return unless supports_platform?
202
212
 
203
213
  locked_dependencies.each(&:collect_tests)
@@ -206,7 +216,12 @@ module Inspec
206
216
  next if content.nil? || content.empty?
207
217
 
208
218
  abs_path = source_reader.target.abs_path(path)
209
- @runner_context.load_control_file(content, abs_path, nil)
219
+ begin
220
+ @runner_context.load_control_file(content, abs_path, nil)
221
+ rescue => e
222
+ @state = :failed
223
+ raise Inspec::Exceptions::ProfileLoadFailed, "Failed to load source for #{path}: #{e}"
224
+ end
210
225
  end
211
226
  @tests_collected = true
212
227
  end
@@ -249,12 +264,13 @@ module Inspec
249
264
  d = dep.profile
250
265
  # this will force a dependent profile load so we are only going to add
251
266
  # this metadata if the parent profile is supported.
252
- if supports_platform? && !d.supports_platform?
267
+ if @supports_platform && !d.supports_platform?
253
268
  # since ruby 1.9 hashes are ordered so we can just use index values here
254
269
  # TODO: NO! this is a violation of encapsulation to an extreme
255
270
  metadata.dependencies[i][:status] = "skipped"
256
271
  msg = "Skipping profile: '#{d.name}' on unsupported platform: '#{d.backend.platform.name}/#{d.backend.platform.release}'."
257
- metadata.dependencies[i][:skip_message] = msg
272
+ metadata.dependencies[i][:status_message] = msg
273
+ metadata.dependencies[i][:skip_message] = msg # Repeat as skip_message for backward compatibility
258
274
  next
259
275
  elsif metadata.dependencies[i]
260
276
  # Currently wrapper profiles will load all dependencies, and then we
@@ -324,12 +340,13 @@ module Inspec
324
340
  res[:sha256] = sha256
325
341
  res[:parent_profile] = parent_profile unless parent_profile.nil?
326
342
 
327
- if !supports_platform?
343
+ if @supports_platform
344
+ res[:status_message] = @status_message || ""
345
+ res[:status] = failed? ? "failed" : "loaded"
346
+ else
328
347
  res[:status] = "skipped"
329
348
  msg = "Skipping profile: '#{name}' on unsupported platform: '#{backend.platform.name}/#{backend.platform.release}'."
330
- res[:skip_message] = msg
331
- else
332
- res[:status] = "loaded"
349
+ res[:status_message] = msg
333
350
  end
334
351
 
335
352
  # convert legacy os-* supports to their platform counterpart
@@ -455,6 +472,10 @@ module Inspec
455
472
  params[:controls].values.length
456
473
  end
457
474
 
475
+ def set_status_message(msg)
476
+ @status_message = msg.to_s
477
+ end
478
+
458
479
  # generates a archive of a folder profile
459
480
  # assumes that the profile was checked before
460
481
  def archive(opts)
@@ -14,17 +14,14 @@ module Inspec::Reporters
14
14
  runtime_config = Inspec::Config.cached.respond_to?(:final_options) ? Inspec::Config.cached.final_options : {}
15
15
 
16
16
  message_truncation = runtime_config[:reporter_message_truncation] || "ALL"
17
- trunc = message_truncation == "ALL" ? -1 : message_truncation.to_i
17
+ @trunc = message_truncation == "ALL" ? -1 : message_truncation.to_i
18
18
  include_backtrace = runtime_config[:reporter_backtrace_inclusion].nil? ? true : runtime_config[:reporter_backtrace_inclusion]
19
19
 
20
20
  @run_data[:profiles]&.each do |p|
21
21
  p[:controls].each do |c|
22
22
  c[:results]&.map! do |r|
23
23
  r.delete(:backtrace) unless include_backtrace
24
- if r.key?(:message) && r[:message] != "" && trunc > -1
25
- r[:message] = r[:message][0...trunc] + "[Truncated to #{trunc} characters]"
26
- end
27
- r
24
+ process_message_truncation(r)
28
25
  end
29
26
  end
30
27
  end
@@ -43,5 +40,14 @@ module Inspec::Reporters
43
40
  def render
44
41
  raise NotImplementedError, "#{self.class} must implement a `#render` method to format its output."
45
42
  end
43
+
44
+ private
45
+
46
+ def process_message_truncation(result)
47
+ if result.key?(:message) && result[:message] != "" && @trunc > -1 && result[:message].length > @trunc
48
+ result[:message] = result[:message][0...@trunc] + "[Truncated to #{@trunc} characters]"
49
+ end
50
+ result
51
+ end
46
52
  end
47
53
  end
@@ -72,6 +72,7 @@ module Inspec::Reporters
72
72
  "Profile" => format_profile_name(profile),
73
73
  "Version" => profile[:version] || "(not specified)",
74
74
  }
75
+ header["Failure Message"] = profile[:status_message] if profile[:status] == "failed"
75
76
  header["Target"] = run_data[:platform][:target] unless run_data[:platform][:target].nil?
76
77
  header["Target ID"] = @config["target_id"] unless @config["target_id"].nil?
77
78
 
@@ -45,8 +45,8 @@ module Inspec::Reporters
45
45
  end
46
46
 
47
47
  def profiles
48
- run_data[:profiles].map { |p|
49
- {
48
+ run_data[:profiles].map do |p|
49
+ res = {
50
50
  name: p[:name],
51
51
  version: p[:version],
52
52
  sha256: p[:sha256],
@@ -64,10 +64,15 @@ module Inspec::Reporters
64
64
  groups: profile_groups(p),
65
65
  controls: profile_controls(p),
66
66
  status: p[:status],
67
- skip_message: p[:skip_message],
67
+ status_message: p[:status_message],
68
68
  waiver_data: p[:waiver_data],
69
69
  }.reject { |_k, v| v.nil? }
70
- }
70
+
71
+ # For backwards compatibility
72
+ res[:skip_message] = res[:status_message] if res[:status] == "skipped"
73
+
74
+ res
75
+ end
71
76
  end
72
77
 
73
78
  def profile_groups(profile)
@@ -90,12 +90,14 @@ module Inspec::Resources
90
90
  # deb "http://archive.ubuntu.com/ubuntu/" wily main restricted ...
91
91
  # deb http://archive.ubuntu.com/ubuntu/ wily main restricted ...
92
92
  # deb [trusted=yes] http://archive.ubuntu.com/ubuntu/ wily main restricted ...
93
+ # deb cdrom:[Ubuntu 15.10 _Wily Werewolf_ - Release amd64 (20151021)]/ wily main restricted ...
93
94
 
94
95
  words = line.split
95
96
  words.delete_at 1 if words[1] && words[1].start_with?("[")
96
97
  type, url, distro, *components = words
97
98
  url = url.delete('"') if url
98
99
 
100
+ next if words[1] && words[1].start_with?("cdrom:") # skip unsupported apt-cdrom repos
99
101
  next if components.empty?
100
102
  next unless URI::HTTP === URI.parse(url)
101
103
  next unless %w{deb deb-src}.include? type
@@ -47,10 +47,18 @@ module Inspec::Resources
47
47
  ipv6_addresses && !ipv6_addresses.empty?
48
48
  end
49
49
 
50
+ def ipv4_address
51
+ ipv4_addresses.first
52
+ end
53
+
50
54
  def ipv4_addresses
51
55
  ipv4_cidrs.map { |i| i.split("/")[0] }
52
56
  end
53
57
 
58
+ def ipv6_address
59
+ ipv6_addresses.first
60
+ end
61
+
54
62
  def ipv6_addresses
55
63
  ipv6_cidrs.map { |i| i.split("/")[0] }
56
64
  end
@@ -85,6 +93,7 @@ module Inspec::Resources
85
93
  @cache ||= begin
86
94
  provider = LinuxInterface.new(inspec) if inspec.os.linux?
87
95
  provider = WindowsInterface.new(inspec) if inspec.os.windows?
96
+ provider = BsdInterface.new(inspec) if inspec.os.bsd? # includes macOS
88
97
  Hash(provider && provider.interface_info(@iface))
89
98
  end
90
99
  end
@@ -98,6 +107,52 @@ module Inspec::Resources
98
107
  end
99
108
  end
100
109
 
110
+ class BsdInterface < InterfaceInfo
111
+ def interface_info(iface)
112
+ cmd = inspec.command("ifconfig #{iface}")
113
+ return nil if cmd.exit_status.to_i != 0
114
+
115
+ lines = cmd.stdout.split("\n")
116
+ iface_info = {
117
+ name: iface,
118
+ ipv4_addresses: [], # Actually CIDRs
119
+ ipv6_addresses: [], # are expected to go here
120
+ }
121
+
122
+ iface_info[:up] = lines[0].include?("UP")
123
+ lines.each do |line|
124
+ # IPv4 case
125
+ m = line.match(/^\s+inet\s+((?:\d{1,3}\.){3}\d{1,3})\s+netmask\s+(0x[a-f0-9]{8})/)
126
+ if m
127
+ ip = m[1]
128
+ hex_mask = m[2]
129
+ cidr = hex_mask.to_i(16).to_s(2).count("1")
130
+ iface_info[:ipv4_addresses] << "#{ip}/#{cidr}"
131
+ next
132
+ end
133
+
134
+ # IPv6 case
135
+ m = line.match(/^\s+inet6\s+([a-f0-9:]+)%#{iface}\s+prefixlen\s+(\d+)/)
136
+ if m
137
+ ip = m[1]
138
+ cidr = m[2]
139
+ iface_info[:ipv6_addresses] << "#{ip}/#{cidr}"
140
+ next
141
+ end
142
+
143
+ # Speed detect, crummy - can't detect wifi, finds any number in the string
144
+ # Ethernet autoselect (1000baseT <full-duplex>)
145
+ m = line.match(/^\s+media:\D+(\d+)/)
146
+ if m
147
+ iface_info[:speed] = m[1].to_i
148
+ next
149
+ end
150
+ end
151
+
152
+ iface_info
153
+ end
154
+ end
155
+
101
156
  class LinuxInterface < InterfaceInfo
102
157
  def interface_info(iface)
103
158
  # will return "[mtu]\n1500\n[type]\n1"
@@ -0,0 +1,119 @@
1
+ require "inspec/utils/filter"
2
+ require "inspec/resources/command"
3
+
4
+ module Inspec::Resources
5
+ class Interfaces < Inspec.resource(1)
6
+ name "interfaces"
7
+ supports platform: "unix"
8
+ supports platform: "windows"
9
+ desc "Use the interfaces InSpec audit resource to test properties for multiple network interfaces installed on the system"
10
+ example <<~EXAMPLE
11
+ describe interfaces do
12
+ its('names') { should include 'eth0' }
13
+ end
14
+ EXAMPLE
15
+
16
+ attr_reader :iface_data
17
+
18
+ def to_s
19
+ "Interfaces"
20
+ end
21
+
22
+ filter = FilterTable.create
23
+ filter.register_column(:names, field: "name")
24
+ .install_filter_methods_on_resource(self, :scan_interfaces)
25
+
26
+ def ipv4_address
27
+ require "ipaddr"
28
+
29
+ # Loop over interface names
30
+ # Select those that are up and have an ipv4 address
31
+ interfaces = names.map { |n| inspec.interface(n) }.select do |i|
32
+ i.ipv4_address? && i.up?
33
+ end
34
+
35
+ addrs = interfaces.map(&:ipv4_addresses).flatten.map { |a| IPAddr.new(a) }
36
+
37
+ # Look for progressively "better" IP addresses
38
+ [
39
+ # Loopback and private IP ranges
40
+ IPAddr.new("127.0.0.0/8"),
41
+ IPAddr.new("192.168.0.0/16"),
42
+ IPAddr.new("172.16.0.0/12"),
43
+ IPAddr.new("10.0.0.0/8"),
44
+ ].each do |private_range|
45
+ filtered_addrs = addrs.reject { |a| private_range.include?(a) }
46
+ if filtered_addrs.empty?
47
+ # Everything we had was a private or loopback IP. Return the "best" thing we were left with.
48
+ return addrs.first.to_s
49
+ end
50
+
51
+ addrs = filtered_addrs
52
+ end
53
+ addrs.first.to_s
54
+ end
55
+
56
+ private
57
+
58
+ def scan_interfaces
59
+ @iface_data ||= begin
60
+ provider = LinuxInterfaceLister.new(inspec) if inspec.os.linux?
61
+ provider = WindowsInterfaceLister.new(inspec) if inspec.os.windows?
62
+ provider = BsdInterfaceLister.new(inspec) if inspec.os.bsd? # includes macOS
63
+ Array(provider && provider.scan_interfaces)
64
+ end
65
+ end
66
+
67
+ class InterfaceLister
68
+ attr_reader :inspec
69
+ def initialize(inspec)
70
+ @inspec = inspec
71
+ end
72
+ end
73
+
74
+ class BsdInterfaceLister < InterfaceLister
75
+ def scan_interfaces
76
+ iface_data = []
77
+ cmd = inspec.command("ifconfig -a")
78
+ cmd.stdout.split("\n").each do |line|
79
+ # lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> metric 0 mtu 16384
80
+ m = line.match(/^(\S+):/)
81
+ if m
82
+ iface_data << { "name" => m[1] }
83
+ end
84
+ end
85
+ iface_data
86
+ end
87
+ end
88
+
89
+ class LinuxInterfaceLister < InterfaceLister
90
+ def scan_interfaces
91
+ iface_data = []
92
+ cmd = inspec.command("ls /sys/class/net")
93
+ cmd.stdout.split("\n").each do |iface|
94
+ iface_data << { "name" => iface }
95
+ end
96
+ iface_data
97
+ end
98
+ end
99
+
100
+ class WindowsInterfaceLister < InterfaceLister
101
+ def scan_interfaces
102
+ iface_data = []
103
+ cmd = inspec.command("Get-NetAdapter | Select-Object -Property Name | ConvertTo-Json")
104
+ begin
105
+ adapter_info = JSON.parse(cmd.stdout)
106
+ # May be a Hash if only one, or Array if multiple - normalize to Array
107
+ adapter_info = [ adapter_info ] if adapter_info.is_a? Hash
108
+ rescue JSON::ParserError => _e
109
+ return nil
110
+ end
111
+ adapter_info.each do |info|
112
+ iface_data << { "name" => info["Name"] }
113
+ end
114
+ iface_data
115
+ end
116
+ end
117
+
118
+ end
119
+ end