inspec-core 4.52.9 → 4.56.17

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5fc9c31a9983866ff40fb11326a3c2bac04342e1d473b845e81639bbca88a8c
4
- data.tar.gz: 6fd55b54b5c1510572fa963d2d4833a8f3132ae5cbfb1871a4662f8b3da720d9
3
+ metadata.gz: 9930c475b5a1e282ab613db458f60c77561db82d6c009d6c7254f5a3789132f5
4
+ data.tar.gz: 8a11ae471b5954568be5bf6fc88e52d974a5ac9cb83caf2a6560dd3dd26d55fe
5
5
  SHA512:
6
- metadata.gz: d9f356b30301430dbeeeb599b696aacfc4ec1caaadafd61e6af40462db60fa400cd2bf4add6a682fe379c65ed4adb0f926a858e6b072066f06296821725f7f92
7
- data.tar.gz: 25381065952aa3d460cedd9c56a5b389625b2ed11841f2085047e81444c3aa1d788338468c640f5c27403b4074a5649b825a3289e0b63cdc96d70e8450cc664c
6
+ metadata.gz: 0106f09ddcc1077650b6781631188dcb4231829b4e2b737680f49c6736b3c08001523d34a9398eb7499147dde9d22c4bbdd9058721c484424fe69933535e7557
7
+ data.tar.gz: b04b2c02c6d1009cee30a5ca32521d4c62e62fcf72727d0f720af5fa54da01029dbcd1b25656460f73d9d4cb7f3bf3b94b7e1f049479770a0a4848aa1f25034f
data/Gemfile CHANGED
@@ -11,11 +11,6 @@ gem "inspec-bin", path: "./inspec-bin"
11
11
 
12
12
  gem "ffi", ">= 1.9.14", "!= 1.13.0", "!= 1.14.2"
13
13
 
14
- if Gem.ruby_version.to_s.start_with?("2.5")
15
- # 16.7.23 required ruby 2.6+
16
- gem "chef-utils", "< 16.7.23" # TODO: remove when we drop ruby 2.5
17
- end
18
-
19
14
  # inspec tests depend text output that changed in the 3.10 release
20
15
  # but our runtime dep is still 3.9+
21
16
  gem "rspec", ">= 3.10"
@@ -30,11 +25,7 @@ end
30
25
  group :test do
31
26
  gem "chefstyle", "~> 2.0.3"
32
27
  gem "concurrent-ruby", "~> 1.0"
33
- if Gem.ruby_version.to_s.start_with?("2.5")
34
- gem "html-proofer", "= 3.19.1" , platforms: :ruby # do not attempt to run proofer on windows
35
- else
36
- gem "html-proofer", platforms: :ruby # do not attempt to run proofer on windows
37
- end
28
+ gem "html-proofer", platforms: :ruby # do not attempt to run proofer on windows
38
29
  gem "json_schemer", ">= 0.2.1", "< 0.2.19"
39
30
  gem "m"
40
31
  gem "minitest-sprint", "~> 1.0"
@@ -45,7 +36,8 @@ group :test do
45
36
  gem "pry", "~> 0.10"
46
37
  gem "rake", ">= 10"
47
38
  gem "ruby-progressbar", "~> 1.8"
48
- gem "simplecov", "~> 0.18"
39
+ gem "simplecov", "~> 0.21"
40
+ gem "simplecov_json_formatter"
49
41
  gem "webmock", "~> 3.0"
50
42
  end
51
43
 
data/inspec-core.gemspec CHANGED
@@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
13
13
  spec.license = "Apache-2.0"
14
14
  spec.require_paths = ["lib"]
15
15
 
16
- spec.required_ruby_version = ">= 2.5"
16
+ spec.required_ruby_version = ">= 2.6"
17
17
 
18
18
  # the gemfile and gemspec are necessary for appbundler so don't remove it
19
19
  spec.files =
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
28
28
  spec.add_dependency "thor", ">= 0.20", "< 2.0"
29
29
  spec.add_dependency "method_source", ">= 0.8", "< 2.0"
30
30
  spec.add_dependency "rubyzip", ">= 1.2.2", "< 3.0"
31
- spec.add_dependency "rspec", ">= 3.9", "< 3.11"
31
+ spec.add_dependency "rspec", ">= 3.9", "<= 3.11"
32
32
  spec.add_dependency "rspec-its", "~> 1.2"
33
33
  spec.add_dependency "pry", "~> 0.13"
34
34
  spec.add_dependency "hashie", ">= 3.4", "< 5.0"
data/lib/inspec/config.rb CHANGED
@@ -367,7 +367,11 @@ module Inspec
367
367
  .find_activators(plugin_type: :reporter)\
368
368
  .map(&:activator_name).map(&:to_s)
369
369
 
370
- valid_types = rspec_built_in_formatters + inspec_reporters_that_are_not_yet_plugins + plugin_reporters
370
+ streaming_reporters = Inspec::Plugin::V2::Registry.instance\
371
+ .find_activators(plugin_type: :streaming_reporter)\
372
+ .map(&:activator_name).map(&:to_s)
373
+
374
+ valid_types = rspec_built_in_formatters + inspec_reporters_that_are_not_yet_plugins + plugin_reporters + streaming_reporters
371
375
 
372
376
  reporters.each do |reporter_name, reporter_config|
373
377
  raise NotImplementedError, "'#{reporter_name}' is not a valid reporter type." unless valid_types.include?(reporter_name)
@@ -102,7 +102,8 @@ module Inspec
102
102
  end
103
103
 
104
104
  def fetcher
105
- @fetcher ||= Inspec::CachedFetcher.new(opts, @cache)
105
+ @runner_options ||= (Inspec::Config.cached || {})
106
+ @fetcher ||= Inspec::CachedFetcher.new(opts, @cache, @runner_options)
106
107
  end
107
108
 
108
109
  # load dependencies of the dependency
@@ -70,6 +70,7 @@ module Inspec::Formatters
70
70
  name: platform(:name),
71
71
  release: platform(:release),
72
72
  target: backend_target,
73
+ target_id: platform(:uuid),
73
74
  }
74
75
  end
75
76
 
@@ -205,12 +206,13 @@ module Inspec::Formatters
205
206
  def platform(field)
206
207
  return nil if @backend.nil?
207
208
 
208
- begin
209
- @backend.platform[field]
210
- rescue Train::Error => e
211
- Inspec::Log.warn(e.message)
212
- nil
213
- end
209
+ @backend.platform[field]
210
+ rescue Train::PlatformUuidDetectionFailed
211
+ Inspec::Log.warn("Could not find platform target_id.")
212
+ nil
213
+ rescue Train::Error => e
214
+ Inspec::Log.warn(e.message)
215
+ nil
214
216
  end
215
217
 
216
218
  def backend_target
@@ -30,6 +30,8 @@ module Inspec
30
30
 
31
31
  c3 = Class.new do
32
32
  include Inspec::DSL::RequireOverride
33
+ include Inspec::Resources
34
+
33
35
  def initialize(require_loader)
34
36
  @require_loader = require_loader
35
37
  @inspec_binding = nil
@@ -0,0 +1,10 @@
1
+ module Inspec::Plugin::V2::PluginType
2
+ class StreamingReporter < Inspec::Plugin::V2::PluginBase # TBD Superclass may need to change
3
+ register_plugin_type(:streaming_reporter)
4
+
5
+ #====================================================================#
6
+ # StreamingReporter plugin type API
7
+ #====================================================================#
8
+ # Implementation classes must implement these methods.
9
+ end
10
+ end
@@ -68,6 +68,7 @@ module Inspec
68
68
  end
69
69
 
70
70
  def reload_dsl
71
+ @resource_registry.merge!(Inspec::Resource.new_registry)
71
72
  @control_eval_context = nil
72
73
  end
73
74
 
@@ -263,9 +264,3 @@ module Inspec
263
264
  end # DomainSpecificLunacy
264
265
  end # ProfileContext
265
266
  end
266
-
267
- if RUBY_VERSION < "2.5"
268
- class Module
269
- public :define_method
270
- end
271
- end
@@ -21,7 +21,7 @@ module Inspec::Reporters
21
21
  final_report[:type] = "inspec_report"
22
22
 
23
23
  final_report[:end_time] = Time.now.utc.strftime("%FT%TZ")
24
- final_report[:node_uuid] = @config["node_uuid"] || @config["target_id"]
24
+ final_report[:node_uuid] = report[:platform][:target_id] || @config["node_uuid"] || @config["target_id"]
25
25
  raise Inspec::ReporterError, "Cannot find a UUID for your node. Please specify one via json-config." if final_report[:node_uuid].nil?
26
26
 
27
27
  final_report[:report_uuid] = @config["report_uuid"] || uuid_from_string(final_report[:end_time] + final_report[:node_uuid])
@@ -29,7 +29,7 @@ module Inspec::Reporters
29
29
  {
30
30
  name: run_data[:platform][:name],
31
31
  release: run_data[:platform][:release],
32
- target_id: @config["target_id"],
32
+ target_id: run_data[:platform][:target_id] || @config["target_id"],
33
33
  }.reject { |_k, v| v.nil? }
34
34
  end
35
35
 
@@ -5,6 +5,8 @@ module Inspec::Resources
5
5
  class Bash < Cmd
6
6
  name "bash"
7
7
  supports platform: "unix"
8
+ supports platform: "esx"
9
+
8
10
  desc "Run a command or script in BASH."
9
11
  example <<~EXAMPLE
10
12
  describe bash('ls -al /') do
@@ -61,6 +61,24 @@ module Inspec::Resources
61
61
  res.force_encoding("utf-8")
62
62
  end
63
63
 
64
+ # returns hash containing list of users/groups and their file permissions.
65
+ def user_permissions
66
+ return {} unless exist?
67
+
68
+ return skip_reource"`user_permissions` is not supported on your OS yet." unless inspec.os.windows?
69
+
70
+ @perms_provider.user_permissions(file)
71
+ end
72
+
73
+ # returns true if inheritance is enabled on file or folder
74
+ def inherited?
75
+ return false unless exist?
76
+
77
+ return skip_resource "`inherited?` is not supported on your OS yet." unless inspec.os.windows?
78
+
79
+ @perms_provider.inherited?(file)
80
+ end
81
+
64
82
  def contain(*_)
65
83
  raise "Contain is not supported. Please use standard RSpec matchers."
66
84
  end
@@ -244,6 +262,26 @@ module Inspec::Resources
244
262
  end
245
263
 
246
264
  class WindowsFilePermissions < FilePermissions
265
+
266
+ def user_permissions(file)
267
+ script = <<-EOH
268
+ $Acl = Get-Acl -Path #{file.path}
269
+ $Result = foreach ($Access in $acl.Access) {
270
+ [PSCustomObject]@{
271
+ $Access.IdentityReference.Value = $Access.FileSystemRights.ToString()
272
+ }
273
+ }
274
+ $Result | ConvertTo-Json
275
+ EOH
276
+ result = inspec.powershell(script)
277
+ JSON.load(result.stdout).inject(&:merge) unless result.stdout.empty?
278
+ end
279
+
280
+ def inherited?(file)
281
+ cmd = inspec.command("(Get-Acl -Path #{file.path}).access| Where-Object {$_.IsInherited -eq $true} | measure | % { $_.Count }")
282
+ cmd.stdout.chomp == "0" ? false : true
283
+ end
284
+
247
285
  def check_file_permission_by_mask(_file, _access_type, _usergroup, _specific_user)
248
286
  raise "`check_file_permission_by_mask` is not supported on Windows"
249
287
  end
@@ -32,6 +32,17 @@ module Inspec::Resources
32
32
  .register_column(:interfaces, field: "interfaces")
33
33
  .register_column(:sources, field: "sources")
34
34
  .register_column(:services, field: "services")
35
+ .register_column(:target, field: "target")
36
+ .register_column(:ports, field: "ports")
37
+ .register_column(:protocols, field: "protocols")
38
+ .register_column(:forward_ports, field: "forward_ports")
39
+ .register_column(:source_ports, field: "source_ports")
40
+ .register_column(:icmp_blocks, field: "icmp_blocks")
41
+ .register_column(:rich_rules, field: "rich_rules")
42
+ .register_custom_matcher(:icmp_block_inversion?) { |x| x.params[0]["icmp_block_inversion"] }
43
+ .register_custom_matcher(:has_icmp_block_inversion_enabled?) { |x| x.params[0]["icmp_block_inversion"] }
44
+ .register_custom_matcher(:masquerade?) { |x| x.params[0]["masquerade"] }
45
+ .register_custom_matcher(:has_masquerade_enabled?) { |x| x.params[0]["masquerade"] }
35
46
 
36
47
  filter.install_filter_methods_on_resource(self, :params)
37
48
 
@@ -64,28 +75,28 @@ module Inspec::Resources
64
75
  end
65
76
 
66
77
  def has_service_enabled_in_zone?(query_service, query_zone = default_zone)
67
- firewalld_command("--zone=#{query_zone} --query-service=#{query_service}") == "yes"
78
+ firewalld_command("--permanent --zone=#{query_zone} --query-service=#{query_service}") == "yes"
68
79
  end
69
80
 
70
81
  def service_ports_enabled_in_zone(query_service, query_zone = default_zone)
71
82
  # return: String of ports open
72
83
  # example: ['22/tcp', '4722/tcp']
73
- firewalld_command("--zone=#{query_zone} --service=#{query_service} --get-ports --permanent").split(" ")
84
+ firewalld_command("--permanent --zone=#{query_zone} --service=#{query_service} --get-ports").split(" ")
74
85
  end
75
86
 
76
87
  def service_protocols_enabled_in_zone(query_service, query_zone = default_zone)
77
- # return: String of protocoals open
88
+ # return: String of protocols open
78
89
  # example: ['icmp', 'ipv4', 'igmp']
79
- firewalld_command("--zone=#{query_zone} --service=#{query_service} --get-protocols --permanent").split(" ")
90
+ firewalld_command("--permanent --zone=#{query_zone} --service=#{query_service} --get-protocols").split(" ")
80
91
  end
81
92
 
82
93
  def has_port_enabled_in_zone?(query_port, query_zone = default_zone)
83
- firewalld_command("--zone=#{query_zone} --query-port=#{query_port}") == "yes"
94
+ firewalld_command("--permanent --zone=#{query_zone} --query-port=#{query_port}") == "yes"
84
95
  end
85
96
 
86
97
  def has_rule_enabled?(rule, query_zone = default_zone)
87
98
  rule = "rule #{rule}" unless rule.start_with?("rule")
88
- firewalld_command("--zone=#{query_zone} --query-rich-rule='#{rule}'") == "yes"
99
+ firewalld_command("--permanent --zone=#{query_zone} --query-rich-rule='#{rule}'") == "yes"
89
100
  end
90
101
 
91
102
  def to_s
@@ -120,19 +131,82 @@ module Inspec::Resources
120
131
  "interfaces" => line.split(":")[1].split(" "),
121
132
  "services" => services_bound(zone),
122
133
  "sources" => sources_bound(zone),
134
+ "target" => target_bound(zone),
135
+ "icmp_block_inversion" => icmp_block_inversion_bound?(zone),
136
+ "ports" => ports_bound(zone),
137
+ "protocols" => protocols_bound(zone),
138
+ "masquerade" => masquerade_bound?(zone),
139
+ "forward_ports" => forward_ports_bound(zone),
140
+ "source_ports" => source_ports_bound(zone),
141
+ "icmp_blocks" => icmp_blocks_bound(zone),
142
+ "rich_rules" => rich_rules_bound(zone),
123
143
  }
124
144
  end
125
145
 
146
+ def target_bound(query_zone)
147
+ # result: a target bound for the zone
148
+ # example: 'DROP'
149
+ firewalld_command("--permanent --zone=#{query_zone} --get-target").strip
150
+ end
151
+
152
+ def icmp_block_inversion_bound?(query_zone)
153
+ # result: true/false whether inversion of icmp blocks has been enabled for a zone
154
+ # example: true
155
+ firewalld_command("--permanent --zone=#{query_zone} --query-icmp-block-inversion") == "yes"
156
+ end
157
+
158
+ def ports_bound(query_zone)
159
+ # result: a list of ports bound for a zone
160
+ # example: ['80/tcp', '443/tcp']
161
+ firewalld_command("--permanent --zone=#{query_zone} --list-ports").split(" ")
162
+ end
163
+
164
+ def protocols_bound(query_zone)
165
+ # result: a list of protocols added for a zone
166
+ # example: ['icmp', 'ipv4', 'igmp']
167
+ firewalld_command("--permanent --zone=#{query_zone} --list-protocols").split(" ")
168
+ end
169
+
170
+ def masquerade_bound?(query_zone)
171
+ # result: true/false whether IPv4 masquerading has been enabled for a zone
172
+ # example: true
173
+ firewalld_command("--permanent --zone=#{query_zone} --query-masquerade") == "yes"
174
+ end
175
+
176
+ def forward_ports_bound(query_zone)
177
+ # result: a list of IPv4 forward ports bound to a zone
178
+ # example: ['port=80:proto=tcp:toport=88', 'port=12345:proto=tcp:toport=54321:toaddr=192.168.1.3']
179
+ firewalld_command("--permanent --zone=#{query_zone} --list-forward-ports").split("\n")
180
+ end
181
+
182
+ def source_ports_bound(query_zone)
183
+ # result: a list of source ports bound to a zone
184
+ # example: ['80/tcp', '8080/tcp']
185
+ firewalld_command("--permanent --zone=#{query_zone} --list-source-ports").split(" ")
186
+ end
187
+
188
+ def icmp_blocks_bound(query_zone)
189
+ # result: a list of internet ICMP type blocks bound to a zone
190
+ # example: ['echo-request', 'echo-reply']
191
+ firewalld_command("--permanent --zone=#{query_zone} --list-icmp-blocks").split(" ")
192
+ end
193
+
194
+ def rich_rules_bound(query_zone)
195
+ # result: a list of rich language rules bound to a zone
196
+ # example: ['rule protocol value="ah" accept', 'rule service name="ftp" log limit value="1/m" audit accept']
197
+ firewalld_command("--permanent --zone=#{query_zone} --list-rich-rules").split("\n")
198
+ end
199
+
126
200
  def sources_bound(query_zone)
127
201
  # result: a list containing either an ip address or ip address with a mask, or a ipset or an ipset with the ipset prefix.
128
202
  # example: ['192.168.0.4', '192.168.0.0/16', '2111:DB28:ABC:12::', '2111:db89:ab3d:0112::0/64']
129
- firewalld_command("--zone=#{query_zone} --list-sources").split(" ")
203
+ firewalld_command("--permanent --zone=#{query_zone} --list-sources").split(" ")
130
204
  end
131
205
 
132
206
  def services_bound(query_zone)
133
207
  # result: a list of services bound to a zone.
134
208
  # example: ['ssh', 'dhcpv6-client']
135
- firewalld_command("--zone=#{query_zone} --list-services").split(" ")
209
+ firewalld_command("--permanent --zone=#{query_zone} --list-services").split(" ")
136
210
  end
137
211
 
138
212
  def firewalld_command(command)
@@ -145,4 +219,4 @@ module Inspec::Resources
145
219
  result.stdout.strip
146
220
  end
147
221
  end
148
- end
222
+ end
@@ -162,7 +162,7 @@ module Inspec::Resources
162
162
 
163
163
  current_kernel = file_line.split(" ", 2)[1]
164
164
  lines.drop(index + 1).each do |kernel_line|
165
- if kernel_line =~ /^\s.*/
165
+ if kernel_line =~ /(?:^\s*\w+)/ && !(kernel_line =~ /^title.*/)
166
166
  option_type = kernel_line.split(" ")[0]
167
167
  line_options = kernel_line.split(" ").drop(1)
168
168
  if (menu_entry == conf["default"].to_i && @kernel == "default") || current_kernel == @kernel
@@ -33,6 +33,7 @@ module Inspec::Resources
33
33
  def initialize(params = {})
34
34
  @table = params[:table]
35
35
  @chain = params[:chain]
36
+ @ignore_comments = params[:ignore_comments] || false
36
37
 
37
38
  # we're done if we are on linux
38
39
  return if inspec.os.linux?
@@ -59,8 +60,13 @@ module Inspec::Resources
59
60
  cmd = inspec.command(iptables_cmd)
60
61
  return [] if cmd.exit_status.to_i != 0
61
62
 
62
- # split rules, returns array or rules
63
- @iptables_cache = cmd.stdout.split("\n").map(&:strip)
63
+ if @ignore_comments
64
+ # split rules, returns array or rules without any comment
65
+ @iptables_cache = remove_comments_from_rules(cmd.stdout.split("\n"))
66
+ else
67
+ # split rules, returns array or rules
68
+ @iptables_cache = cmd.stdout.split("\n").map(&:strip)
69
+ end
64
70
  end
65
71
 
66
72
  def to_s
@@ -69,6 +75,16 @@ module Inspec::Resources
69
75
 
70
76
  private
71
77
 
78
+ def remove_comments_from_rules(rules)
79
+ rules.each do |rule|
80
+ next if rule.nil?
81
+
82
+ rule.gsub!(/ -m comment --comment "([^"]*)"/, "")
83
+ rule.strip
84
+ end
85
+ rules
86
+ end
87
+
72
88
  def find_iptables_or_error
73
89
  %w{/usr/sbin/iptables /sbin/iptables iptables}.each do |cmd|
74
90
  return cmd if inspec.command(cmd).exist?
@@ -0,0 +1,58 @@
1
+ module Inspec::Resources
2
+ class KernelParameters < Inspec.resource(1)
3
+ name "kernel_parameters"
4
+ supports platform: "unix"
5
+ desc "Use the kernel_parameters InSpec audit resource to test kernel parameters on Linux platforms."
6
+ example <<~EXAMPLE
7
+ describe kernel_parameters.where(parameter: /^net./ ) do
8
+ its('parameters') { should include 'net.ipv4.conf.all.forwarding' }
9
+ end
10
+
11
+ describe kernel_parameters.where(parameter: "net.ipv4.conf.all.forwarding") do
12
+ its('values') { should eq [0] }
13
+ end
14
+
15
+ describe kernel_parameters do
16
+ its('parameters') { should include 'net.ipv4.conf.all.forwarding' }
17
+ its('values') { should include 0 }
18
+ end
19
+ EXAMPLE
20
+
21
+ filter = FilterTable.create
22
+ filter.register_custom_matcher(:exists?) { |x| !x.entries.empty? }
23
+ filter.register_column(:parameters, field: "parameter")
24
+ .register_column(:values, field: "value")
25
+ filter.install_filter_methods_on_resource(self, :params)
26
+
27
+ def initialize
28
+ # this resource is only supported on Linux
29
+ return skip_resource "The `kernel_parameters` resource is not supported on your OS." unless inspec.os.linux?
30
+ end
31
+
32
+ def to_s
33
+ "Kernel Parameters"
34
+ end
35
+
36
+ private
37
+
38
+ def params
39
+ cmd = inspec.command("/sbin/sysctl -a")
40
+ cmd.exit_status != 0 ? [] : parse_kernel_paramater(cmd.stdout)
41
+ end
42
+
43
+ def parse_kernel_paramater(stdout)
44
+ result = []
45
+ stdout.split("\n").each do |out|
46
+ splitted_output = out.split("=").map(&:strip)
47
+ result.push(
48
+ {
49
+ "parameter" => splitted_output[0],
50
+ "value" => splitted_output[1].to_i,
51
+ }
52
+ )
53
+ end
54
+ result
55
+ end
56
+
57
+ end
58
+ end
@@ -76,7 +76,7 @@ module Inspec::Resources
76
76
  if cmd.exit_status != 0 || out =~ /Sqlcmd: Error/
77
77
  raise Inspec::Exceptions::ResourceFailed, "Could not execute the sql query #{out}"
78
78
  else
79
- DatabaseHelper::SQLQueryResult.new(cmd, parse_csv_result(cmd))
79
+ DatabaseHelper::SQLQueryResult.new(cmd, parse_csv_result(cmd.stdout))
80
80
  end
81
81
  end
82
82
 
@@ -94,9 +94,17 @@ module Inspec::Resources
94
94
  !query("select getdate()").empty?
95
95
  end
96
96
 
97
- def parse_csv_result(cmd)
97
+ def parse_csv_result(stdout)
98
98
  require "csv" unless defined?(CSV)
99
- table = CSV.parse(cmd.stdout, headers: true)
99
+
100
+ # replaces \n with \r since multiline data in older versions of database returns faulty
101
+ # formatted multiline data, example name\r\n----\r\nThis is\na multiline field\r\n
102
+ out = stdout.gsub("\n", "\r")
103
+ out = out.gsub("\r\r", "\r")
104
+
105
+ # row separator used since row delimiters \n (in linux) or \r\n (in windows)
106
+ # are converted to \r for consistency and handling faulty formatted multiline data
107
+ table = CSV.parse(out, headers: true, row_sep: "\r")
100
108
 
101
109
  # remove first row, since it will be a seperator line
102
110
  table.delete(0)
@@ -26,6 +26,7 @@ module Inspec::Resources
26
26
  @cache = nil
27
27
  # select package manager
28
28
  @pkgman = nil
29
+ @latest_version = nil
29
30
 
30
31
  os = inspec.os
31
32
  if os.debian?
@@ -60,6 +61,15 @@ module Inspec::Resources
60
61
  info[:installed] == true
61
62
  end
62
63
 
64
+ def latest?(_provider = nil, _version = nil)
65
+ os = inspec.os
66
+ if os.solaris? || (%w{hpux aix}.include? os[:family])
67
+ raise Inspec::Exceptions::ResourceSkipped, "The `be_latest` matcher is not supported on your OS yet."
68
+ end
69
+
70
+ (!info[:only_version_no].nil? && !latest_version.nil?) && (info[:only_version_no] == latest_version)
71
+ end
72
+
63
73
  # returns true it the package is held (if the OS supports it)
64
74
  def held?(_provider = nil, _version = nil)
65
75
  info[:held] == true
@@ -82,6 +92,10 @@ module Inspec::Resources
82
92
  info[:version]
83
93
  end
84
94
 
95
+ def latest_version
96
+ @latest_version ||= ( @pkgman.latest_version(@package_name) || info[:latest_version] )
97
+ end
98
+
85
99
  def to_s
86
100
  "System Package #{@package_name}"
87
101
  end
@@ -107,6 +121,21 @@ module Inspec::Resources
107
121
  # combined into a `ResourceSkipped` exception message.
108
122
  []
109
123
  end
124
+
125
+ private
126
+
127
+ def fetch_latest_version(cmd_string)
128
+ cmd = inspec.command(cmd_string)
129
+ if cmd.exit_status != 0
130
+ raise Inspec::Exceptions::ResourceFailed, "Failed to fetch latest version. Error: #{cmd.stderr}"
131
+ else
132
+ fetch_version_no(cmd.stdout)
133
+ end
134
+ end
135
+
136
+ def fetch_version_no(output)
137
+ output.scan(/(?:(?:\d+)[.]){2,}(?:\d+)/).max_by { |s| Gem::Version.new(s) } unless output.nil?
138
+ end
110
139
  end
111
140
 
112
141
  # Debian / Ubuntu
@@ -124,14 +153,21 @@ module Inspec::Resources
124
153
  # If the package is installed and marked hold, Status is "hold ok installed"
125
154
  # If the package is removed and not purged, Status is "deinstall ok config-files" with exit_status 0
126
155
  # If the package is purged cmd fails with non-zero exit status
156
+
127
157
  {
128
158
  name: params["Package"],
129
159
  installed: params["Status"].split(" ")[2] == "installed",
130
160
  held: params["Status"].split(" ")[0] == "hold",
131
161
  version: params["Version"],
132
162
  type: "deb",
163
+ only_version_no: fetch_version_no(params["Version"]),
133
164
  }
134
165
  end
166
+
167
+ def latest_version(package_name)
168
+ cmd_string = "apt list #{package_name} -a"
169
+ fetch_latest_version(cmd_string)
170
+ end
135
171
  end
136
172
 
137
173
  # RHEL family
@@ -181,9 +217,15 @@ module Inspec::Resources
181
217
  installed: true,
182
218
  version: "#{v}-#{r}",
183
219
  type: "rpm",
220
+ only_version_no: "#{v}",
184
221
  }
185
222
  end
186
223
 
224
+ def latest_version(package_name)
225
+ cmd_string = "yum list #{package_name}"
226
+ fetch_latest_version(cmd_string)
227
+ end
228
+
187
229
  private
188
230
 
189
231
  def rpm_command(package_name)
@@ -216,11 +258,17 @@ module Inspec::Resources
216
258
  installed: true,
217
259
  version: pkg["installed"][0]["version"],
218
260
  type: "brew",
261
+ latest_version: pkg["versions"]["stable"],
262
+ only_version_no: pkg["installed"][0]["version"],
219
263
  }
220
264
  rescue JSON::ParserError => e
221
265
  raise Inspec::Exceptions::ResourceFailed,
222
266
  "Failed to parse JSON from `brew` command. Error: #{e}"
223
267
  end
268
+
269
+ def latest_version(package_name)
270
+ nil
271
+ end
224
272
  end
225
273
 
226
274
  # Arch Linux
@@ -240,8 +288,14 @@ module Inspec::Resources
240
288
  installed: true,
241
289
  version: params["Version"],
242
290
  type: "pacman",
291
+ only_version_no: fetch_version_no(params["Version"]),
243
292
  }
244
293
  end
294
+
295
+ def latest_version(package_name)
296
+ cmd_string = "pacman -Ss #{package_name} | grep #{package_name} | grep installed"
297
+ fetch_latest_version(cmd_string)
298
+ end
245
299
  end
246
300
 
247
301
  class HpuxPkg < PkgManagement
@@ -267,13 +321,20 @@ module Inspec::Resources
267
321
  pkg_info = cmd.stdout.split("\n").delete_if { |e| e =~ /^WARNING/i }
268
322
  pkg = pkg_info[0].split(" - ")[0]
269
323
 
324
+ version = pkg.partition("-")[2]
270
325
  {
271
326
  name: pkg.partition("-")[0],
272
327
  installed: true,
273
- version: pkg.partition("-")[2],
328
+ version: version,
274
329
  type: "pkg",
330
+ only_version_no: fetch_version_no(version),
275
331
  }
276
332
  end
333
+
334
+ def latest_version(package_name)
335
+ cmd_string = "apk info #{package_name}"
336
+ fetch_latest_version(cmd_string)
337
+ end
277
338
  end
278
339
 
279
340
  class FreebsdPkg < PkgManagement
@@ -292,8 +353,14 @@ module Inspec::Resources
292
353
  installed: true,
293
354
  version: params["Version"],
294
355
  type: "pkg",
356
+ only_version_no: params["Version"],
295
357
  }
296
358
  end
359
+
360
+ def latest_version(package_name)
361
+ cmd_string = "pkg version -v | grep #{package_name}"
362
+ fetch_latest_version(cmd_string)
363
+ end
297
364
  end
298
365
 
299
366
  # Determines the installed packages on Windows using the Windows package registry entries.
@@ -339,8 +406,14 @@ module Inspec::Resources
339
406
  installed: true,
340
407
  version: package["DisplayVersion"],
341
408
  type: "windows",
409
+ only_version_no: package["DisplayVersion"],
342
410
  }
343
411
  end
412
+
413
+ def latest_version(package_name)
414
+ cmd_string = "Get-Package #{package_name} -AllVersions"
415
+ fetch_latest_version(cmd_string)
416
+ end
344
417
  end
345
418
 
346
419
  # AIX
@@ -105,6 +105,21 @@ module Inspec::Resources
105
105
  children_keys(@options[:path], filter)
106
106
  end
107
107
 
108
+ # returns hash containing users / groups and their permission
109
+ def user_permissions
110
+ return {} unless exists?
111
+
112
+ get_permissions(@options[:path])
113
+ end
114
+
115
+ # returns true if inheritance is enabled for registry key.
116
+ def inherited?
117
+ return false unless exists?
118
+
119
+ cmd = inspec.command("(Get-Acl -Path 'Registry::#{@options[:path]}').access| Where-Object {$_.IsInherited -eq $true} | measure | % { $_.Count }")
120
+ cmd.stdout.chomp == "0" ? false : true
121
+ end
122
+
108
123
  # returns nil, if not existent or value
109
124
  def method_missing(*keys)
110
125
  # allow the use of array syntax in an `its` block so that users
@@ -283,6 +298,21 @@ module Inspec::Resources
283
298
 
284
299
  key.start_with?("\\") ? key : "\\#{key}"
285
300
  end
301
+
302
+ def get_permissions(path)
303
+ script = <<~EOH
304
+ $path = '#{path}'
305
+ $Acl = Get-Acl -Path ('Registry::' + $path)
306
+ $Result = foreach ($Access in $acl.Access) {
307
+ [PSCustomObject]@{
308
+ $Access.IdentityReference = $Access.RegistryRights.ToString()
309
+ }
310
+ }
311
+ $Result | ConvertTo-Json
312
+ EOH
313
+ result = inspec.powershell(script)
314
+ JSON.load(result.stdout).inject(&:merge) unless result.stdout.empty?
315
+ end
286
316
  end
287
317
 
288
318
  class WindowsRegistryKey < RegistryKey
@@ -84,8 +84,13 @@ module Inspec::Resources
84
84
 
85
85
  def initialize(selinux_path = "/etc/selinux/config")
86
86
  @path = selinux_path
87
- cmd = inspec.command("sestatus")
87
+ if inspec.os.redhat? && inspec.os.name == "amazon"
88
+ lcmd = "/usr/sbin/sestatus"
89
+ else
90
+ lcmd = "sestatus"
91
+ end
88
92
 
93
+ cmd = inspec.command(lcmd)
89
94
  if cmd.exit_status != 0
90
95
  # `sestatus` command not found error message comes in stdout so handling both here
91
96
  out = cmd.stdout + "\n" + cmd.stderr
@@ -0,0 +1,65 @@
1
+ require "inspec/resources/command"
2
+
3
+ module Inspec::Resources
4
+ class TimeZone < Cmd
5
+ name "timezone"
6
+ supports platform: "unix"
7
+ supports platform: "windows"
8
+
9
+ desc "Check for timezone configurations"
10
+ example <<~EXAMPLE
11
+ describe timezone do
12
+ its('identifier') { should eq 'Asia/Kolkata' }
13
+ its('name') { should eq 'IST' }
14
+ its('time_offset') { should eq '+0530' }
15
+ end
16
+ EXAMPLE
17
+
18
+ def initialize
19
+ @output = {}
20
+ os = inspec.os
21
+ cmd = if os.windows?
22
+ inspec.command("Get-TimeZone")
23
+ else
24
+ inspec.command("timedatectl status | grep -i 'Time zone'")
25
+ end
26
+ if cmd.exit_status != 0
27
+ raise Inspec::Exceptions::ResourceFailed, "Time Zone resource with error: #{cmd.stderr}"
28
+ else
29
+ if os.windows?
30
+ splitted_output = cmd.stdout.strip.gsub(/\r/, "").split("\n").select { |out| (out.include? "Id") || (out.include? "DisplayName") || (out.include? "BaseUtcOffset") }
31
+ @output["identifier"] = split_and_fetch_last(splitted_output[1])
32
+ @output["name"] = split_and_fetch_last(splitted_output[0])
33
+ @output["time_offset"] = split_and_fetch_last(splitted_output[2])
34
+ else
35
+ splitted_output = cmd.stdout.split(":")[-1]&.strip&.gsub(/[(),^]*/, "")&.split(" ") || []
36
+ @output["identifier"] = splitted_output[0]
37
+ @output["name"] = splitted_output[1]
38
+ @output["time_offset"] = splitted_output[2]
39
+ end
40
+ end
41
+ end
42
+
43
+ def identifier
44
+ @output["identifier"]
45
+ end
46
+
47
+ def name
48
+ @output["name"]
49
+ end
50
+
51
+ def time_offset
52
+ @output["time_offset"]
53
+ end
54
+
55
+ def to_s
56
+ "Time Zone resource"
57
+ end
58
+
59
+ private
60
+
61
+ def split_and_fetch_last(string_value)
62
+ string_value.split(" :")[-1].strip
63
+ end
64
+ end
65
+ end
@@ -41,6 +41,7 @@ require "inspec/resources/cassandradb_session"
41
41
  require "inspec/resources/cassandradb_conf"
42
42
  require "inspec/resources/cassandra"
43
43
  require "inspec/resources/crontab"
44
+ require "inspec/resources/timezone"
44
45
  require "inspec/resources/dh_params"
45
46
  require "inspec/resources/directory"
46
47
  require "inspec/resources/docker"
@@ -72,6 +73,7 @@ require "inspec/resources/ip6tables"
72
73
  require "inspec/resources/iptables"
73
74
  require "inspec/resources/kernel_module"
74
75
  require "inspec/resources/kernel_parameter"
76
+ require "inspec/resources/kernel_parameters"
75
77
  require "inspec/resources/key_rsa"
76
78
  require "inspec/resources/ksh"
77
79
  require "inspec/resources/limits_conf"
@@ -123,6 +123,8 @@ module Inspec
123
123
  def set_optional_formatters
124
124
  return if @conf["reporter"].nil?
125
125
 
126
+ # This is a slightly modified version of the default RSpec JSON formatter
127
+ # No one in their right mind should be using this because we have a much better JSON reporter - named "json"
126
128
  if @conf["reporter"].key?("json-rspec")
127
129
  # We cannot pass in a nil output path. Rspec only accepts a valid string or a IO object.
128
130
  if @conf["reporter"]["json-rspec"]&.[]("file").nil?
@@ -133,6 +135,7 @@ module Inspec
133
135
  @conf["reporter"].delete("json-rspec")
134
136
  end
135
137
 
138
+ # These are built-in to rspec
136
139
  formats = @conf["reporter"].select { |k, _v| %w{documentation progress html}.include?(k) }
137
140
  formats.each do |k, v|
138
141
  # We cannot pass in a nil output path. Rspec only accepts a valid string or a IO object.
@@ -143,6 +146,33 @@ module Inspec
143
146
  end
144
147
  @conf["reporter"].delete(k)
145
148
  end
149
+
150
+ # Here we need to look for reporter names in the reporter option that
151
+ # are names of streaming reporter plugins. We load them, then tell RSpec to add them as formatters.
152
+ # They will have already been detected at this point (see v2_loader.load_all in cli.rb)
153
+ # but they will not be activated activated at this point.
154
+ # then list all plugins by type by name
155
+ reg = Inspec::Plugin::V2::Registry.instance
156
+ streaming_reporters = reg\
157
+ .find_activators(plugin_type: :streaming_reporter)\
158
+ .map(&:activator_name).map(&:to_s)
159
+
160
+ @conf["reporter"].each do |streaming_reporter_name, file_target|
161
+ # It could be a non-streaming reporter
162
+ next unless streaming_reporters.include? streaming_reporter_name
163
+
164
+ # Activate the plugin so the formatter ID gets registered with RSpec, presumably
165
+ activator = reg.find_activator(plugin_type: :streaming_reporter, activator_name: streaming_reporter_name.to_sym)
166
+ activator.activate!
167
+
168
+ # We cannot pass in a nil output path. Rspec only accepts a valid string or a IO object.
169
+ if file_target&.[]("file").nil?
170
+ RSpec.configuration.add_formatter(activator.implementation_class)
171
+ else
172
+ RSpec.configuration.add_formatter(activator.implementation_class, file_target["file"])
173
+ end
174
+ @conf["reporter"].delete(streaming_reporter_name)
175
+ end
146
176
  end
147
177
 
148
178
  # Configure the output formatter and stream to be used with RSpec.
@@ -114,6 +114,7 @@ module FilterTable
114
114
  raise(ArgumentError, "'#{decorate_symbols(raw_field_name)}' is not a recognized criterion - expected one of #{decorate_symbols(list_fields).join(", ")}'") unless field?(raw_field_name)
115
115
 
116
116
  populate_lazy_field(raw_field_name, desired_value) if is_field_lazy?(raw_field_name)
117
+ populate_lazy_instance_field(raw_field_name, desired_value) if is_field_lazy_instance?(raw_field_name)
117
118
  new_criteria_string += " #{raw_field_name} == #{desired_value.inspect}"
118
119
  filtered_raw_data = filter_raw_data(filtered_raw_data, raw_field_name, desired_value)
119
120
  end
@@ -188,6 +189,8 @@ module FilterTable
188
189
  is_field ||= list_fields.include?(proposed_field.to_sym)
189
190
  is_field ||= is_field_lazy?(proposed_field.to_s)
190
191
  is_field ||= is_field_lazy?(proposed_field.to_sym)
192
+ is_field ||= is_field_lazy_instance?(proposed_field.to_s)
193
+ is_field ||= is_field_lazy_instance?(proposed_field.to_sym)
191
194
 
192
195
  is_field
193
196
  end
@@ -210,6 +213,23 @@ module FilterTable
210
213
  mark_lazy_field_populated(field_name)
211
214
  end
212
215
 
216
+ def populate_lazy_instance_field(field_name, criterion)
217
+ return unless is_field_lazy_instance?(field_name)
218
+ return if field_populated?(field_name)
219
+
220
+ raw_data.each do |row|
221
+ next if row.key?(field_name) # skip row if pre-existing data is present
222
+
223
+ lazy_caller = callback_for_lazy_instance_field(field_name)
224
+ if lazy_caller.is_a?(Proc)
225
+ lazy_caller.call(row, criterion, resource_instance)
226
+ elsif lazy_caller.is_a?(Symbol)
227
+ resource_instance.send(lazy_caller, row, criterion, self)
228
+ end
229
+ end
230
+ mark_lazy_field_populated(field_name)
231
+ end
232
+
213
233
  def is_field_lazy?(sought_field_name)
214
234
  custom_properties_schema.values.any? do |property_struct|
215
235
  sought_field_name == property_struct.field_name && \
@@ -217,6 +237,13 @@ module FilterTable
217
237
  end
218
238
  end
219
239
 
240
+ def is_field_lazy_instance?(sought_field_name)
241
+ custom_properties_schema.values.any? do |property_struct|
242
+ sought_field_name == property_struct.field_name && \
243
+ property_struct.opts[:lazy_instance]
244
+ end
245
+ end
246
+
220
247
  def callback_for_lazy_field(field_name)
221
248
  return unless is_field_lazy?(field_name)
222
249
 
@@ -225,6 +252,14 @@ module FilterTable
225
252
  end.opts[:lazy]
226
253
  end
227
254
 
255
+ def callback_for_lazy_instance_field(field_name)
256
+ return unless is_field_lazy_instance?(field_name)
257
+
258
+ custom_properties_schema.values.find do |property_struct|
259
+ property_struct.field_name == field_name
260
+ end.opts[:lazy_instance]
261
+ end
262
+
228
263
  def field_populated?(field_name)
229
264
  @populated_lazy_columns[field_name]
230
265
  end
@@ -349,12 +384,18 @@ module FilterTable
349
384
  # args of the row struct; also the Struct class will already have provided
350
385
  # a setter for each field.
351
386
  @custom_properties.values.each do |property_info|
352
- next unless property_info.opts[:lazy]
387
+ next unless property_info.opts[:lazy] || property_info.opts[:lazy_instance]
353
388
 
354
389
  field_name = property_info.field_name.to_sym
355
390
  row_eval_context_type.send(:define_method, field_name) do
356
391
  unless filter_table.field_populated?(field_name)
357
- filter_table.populate_lazy_field(field_name, NoCriteriaProvided) # No access to criteria here
392
+ if property_info.opts[:lazy]
393
+ filter_table.populate_lazy_field(field_name, NoCriteriaProvided)
394
+ end # No access to criteria here
395
+ if property_info.opts[:lazy_instance]
396
+ filter_table.populate_lazy_instance_field(field_name,
397
+ NoCriteriaProvided)
398
+ end
358
399
  # OK, the underlying raw data has the value in the first row
359
400
  # (because we would trigger population only on the first row)
360
401
  # We could just return the value, but we need to set it on this Struct in case it is referenced multiple times
@@ -449,7 +490,10 @@ module FilterTable
449
490
  result = where(nil)
450
491
  if custom_property_struct.opts[:lazy]
451
492
  result.populate_lazy_field(custom_property_struct.field_name, filter_criteria_value)
493
+ elsif custom_property_struct.opts[:lazy_instance]
494
+ result.populate_lazy_instance_field(custom_property_struct.field_name, filter_criteria_value)
452
495
  end
496
+
453
497
  result = where(nil).get_column_values(custom_property_struct.field_name) # TODO: the where(nil). is likely unneeded
454
498
  result = result.flatten.uniq.compact if custom_property_struct.opts[:style] == :simple
455
499
  result
@@ -65,7 +65,7 @@ module Inspec
65
65
  c[:results]&.each do |r|
66
66
  next unless r[:message] # :message only set on failure
67
67
 
68
- pos = r[:message].index("\n\nDiff:")
68
+ pos = r[:message].index(/\n{1,2}Diff.*:/)
69
69
  next unless pos # Only textual tests get Diffs
70
70
 
71
71
  r[:message] = r[:message].slice(0, pos)
@@ -1,3 +1,3 @@
1
1
  module Inspec
2
- VERSION = "4.52.9".freeze
2
+ VERSION = "4.56.17".freeze
3
3
  end
@@ -84,7 +84,7 @@ module InspecPlugins
84
84
  return {} if data.nil? || data.empty?
85
85
 
86
86
  parsed = JSON.parse(data)
87
- return {} unless parsed.key?("version") && !parsed["version"].empty?
87
+ return {} unless parsed.key?("build_timestamp") && !parsed["build_timestamp"].empty?
88
88
 
89
89
  parsed
90
90
  end
@@ -219,9 +219,10 @@ module InspecPlugins
219
219
  def version
220
220
  config = InspecPlugins::Compliance::Configuration.new
221
221
  info = InspecPlugins::Compliance::API.version(config)
222
- if !info.nil? && info["version"]
223
- puts "Name: #{info["api"]}"
224
- puts "Version: #{info["version"]}"
222
+ if !info.nil? && info["build_timestamp"]
223
+ # key info["api"] is not longer available in latest version api response
224
+ puts "Name: automate"
225
+ puts "Version: #{info["build_timestamp"]}"
225
226
  else
226
227
  puts "Could not determine server version."
227
228
  exit 1
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inspec-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.52.9
4
+ version: 4.56.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chef InSpec Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-20 00:00:00.000000000 Z
11
+ date: 2022-03-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: chef-telemetry
@@ -117,7 +117,7 @@ dependencies:
117
117
  - - ">="
118
118
  - !ruby/object:Gem::Version
119
119
  version: '3.9'
120
- - - "<"
120
+ - - "<="
121
121
  - !ruby/object:Gem::Version
122
122
  version: '3.11'
123
123
  type: :runtime
@@ -127,7 +127,7 @@ dependencies:
127
127
  - - ">="
128
128
  - !ruby/object:Gem::Version
129
129
  version: '3.9'
130
- - - "<"
130
+ - - "<="
131
131
  - !ruby/object:Gem::Version
132
132
  version: '3.11'
133
133
  - !ruby/object:Gem::Dependency
@@ -479,6 +479,7 @@ files:
479
479
  - lib/inspec/plugin/v2/plugin_types/input.rb
480
480
  - lib/inspec/plugin/v2/plugin_types/mock.rb
481
481
  - lib/inspec/plugin/v2/plugin_types/reporter.rb
482
+ - lib/inspec/plugin/v2/plugin_types/streaming_reporter.rb
482
483
  - lib/inspec/plugin/v2/registry.rb
483
484
  - lib/inspec/plugin/v2/status.rb
484
485
  - lib/inspec/profile.rb
@@ -554,6 +555,7 @@ files:
554
555
  - lib/inspec/resources/json.rb
555
556
  - lib/inspec/resources/kernel_module.rb
556
557
  - lib/inspec/resources/kernel_parameter.rb
558
+ - lib/inspec/resources/kernel_parameters.rb
557
559
  - lib/inspec/resources/key_rsa.rb
558
560
  - lib/inspec/resources/ksh.rb
559
561
  - lib/inspec/resources/launchd_service.rb
@@ -619,6 +621,7 @@ files:
619
621
  - lib/inspec/resources/sys_info.rb
620
622
  - lib/inspec/resources/systemd_service.rb
621
623
  - lib/inspec/resources/sysv_service.rb
624
+ - lib/inspec/resources/timezone.rb
622
625
  - lib/inspec/resources/toml.rb
623
626
  - lib/inspec/resources/upstart_service.rb
624
627
  - lib/inspec/resources/user.rb
@@ -788,7 +791,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
788
791
  requirements:
789
792
  - - ">="
790
793
  - !ruby/object:Gem::Version
791
- version: '2.5'
794
+ version: '2.6'
792
795
  required_rubygems_version: !ruby/object:Gem::Requirement
793
796
  requirements:
794
797
  - - ">="