inspec-core 4.21.3 → 4.23.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/inspec-core.gemspec +3 -5
  4. data/lib/bundles/inspec-supermarket/cli.rb +1 -1
  5. data/lib/inspec/base_cli.rb +5 -1
  6. data/lib/inspec/config.rb +19 -1
  7. data/lib/inspec/exceptions.rb +1 -0
  8. data/lib/inspec/input.rb +4 -3
  9. data/lib/inspec/input_registry.rb +9 -2
  10. data/lib/inspec/metadata.rb +6 -1
  11. data/lib/inspec/plugin/v2/plugin_types/reporter.rb +4 -25
  12. data/lib/inspec/profile.rb +30 -9
  13. data/lib/inspec/reporters.rb +0 -3
  14. data/lib/inspec/reporters/automate.rb +3 -3
  15. data/lib/inspec/reporters/base.rb +7 -23
  16. data/lib/inspec/reporters/cli.rb +1 -0
  17. data/lib/inspec/reporters/json.rb +9 -4
  18. data/lib/inspec/resources/apt.rb +2 -0
  19. data/lib/inspec/resources/bridge.rb +1 -1
  20. data/lib/inspec/resources/host.rb +1 -1
  21. data/lib/inspec/resources/mount.rb +1 -1
  22. data/lib/inspec/resources/mysql_session.rb +31 -8
  23. data/lib/inspec/resources/postgres.rb +1 -1
  24. data/lib/inspec/resources/postgres_session.rb +6 -4
  25. data/lib/inspec/resources/processes.rb +1 -1
  26. data/lib/inspec/resources/service.rb +2 -2
  27. data/lib/inspec/resources/users.rb +1 -1
  28. data/lib/inspec/resources/windows_firewall.rb +110 -0
  29. data/lib/inspec/resources/windows_firewall_rule.rb +137 -0
  30. data/lib/inspec/run_data.rb +1 -1
  31. data/lib/inspec/run_data/profile.rb +7 -6
  32. data/lib/inspec/runner.rb +8 -2
  33. data/lib/inspec/runner_rspec.rb +4 -1
  34. data/lib/inspec/schema.rb +2 -0
  35. data/lib/inspec/schema/exec_json.rb +4 -3
  36. data/lib/inspec/schema/primitives.rb +1 -1
  37. data/lib/inspec/utils/parser.rb +1 -1
  38. data/lib/inspec/utils/run_data_filters.rb +104 -0
  39. data/lib/inspec/version.rb +1 -1
  40. data/lib/plugins/inspec-compliance/lib/inspec-compliance/api.rb +4 -4
  41. data/lib/plugins/inspec-compliance/lib/inspec-compliance/cli.rb +1 -1
  42. data/lib/plugins/inspec-reporter-html2/templates/profile.html.erb +5 -2
  43. data/lib/plugins/inspec-reporter-junit/README.md +15 -0
  44. data/lib/plugins/inspec-reporter-junit/lib/inspec-reporter-junit.rb +12 -0
  45. data/lib/{inspec/reporters/junit.rb → plugins/inspec-reporter-junit/lib/inspec-reporter-junit/reporter.rb} +22 -26
  46. data/lib/plugins/inspec-reporter-junit/lib/inspec-reporter-junit/version.rb +5 -0
  47. metadata +19 -36
  48. data/README.md +0 -474
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cefaf29ca67cbd5cdc90ac2bc3a41e990f38f966761d324489d46f8c9a32fdc5
4
- data.tar.gz: 933e72e1021a81bffac47c3de56378ea0945682c6ed8f51b0e9751bd42859398
3
+ metadata.gz: a2786d0d36c3218a4d79f4578c4a814469115787d4470fa81d422b05b2025462
4
+ data.tar.gz: a0d30c7b81dbb4e58e6b9e8c0447f0855ce1f94f223c1f4f5861a03dfce2ce6c
5
5
  SHA512:
6
- metadata.gz: 0b4919f052f1f9f7bccdbf4116c2d33fded92f508f6c3057bed3a56a8214c7fa20754e281e3f50f04a5807be4c8693740e2952a0441f96447c44d2bc43059c67
7
- data.tar.gz: 1c6b5778d3fe67831b7bd680dd1475f1ca7ddbb1becc24fbbeb1d245e8f6698f2c36d94d5f565564572039a87a94a6f82e9d7ddd50b70475a45c280f43766eca
6
+ metadata.gz: 915bbf9f4302c5fe2e8a41bc131b935bc42861cd4f76d0163ef1aeddc231cdbaf1f653ffd621bb79e5a531baa941e59852b34550d114bf30d95d4eb2ff5aa876
7
+ data.tar.gz: d7b5f80f550bf092aaa0faaf660225f99e3e4d6b600d9a89a45c0648b752a00cac5e30aaaf74fdb940bb135b2ae2ae02112f9617adbe6c25abc46c10d9492c5d
data/Gemfile CHANGED
@@ -19,7 +19,7 @@ group :omnibus do
19
19
  end
20
20
 
21
21
  group :test do
22
- gem "chefstyle", "~> 0.13.0"
22
+ gem "chefstyle", "~> 1.2.1"
23
23
  gem "minitest", "~> 5.5"
24
24
  gem "minitest-sprint", "~> 1.0"
25
25
  gem "rake", ">= 10"
@@ -17,16 +17,16 @@ Gem::Specification.new do |spec|
17
17
 
18
18
  # the gemfile and gemspec are necessary for appbundler so don't remove it
19
19
  spec.files =
20
- Dir.glob("{{lib,etc}/**/*,README.md,LICENSE,Gemfile,inspec-core.gemspec}")
20
+ Dir.glob("{{lib,etc}/**/*,LICENSE,Gemfile,inspec-core.gemspec}")
21
21
  .grep_v(%r{(?<!inspec-init/templates/profiles/)(aws|azure|gcp)})
22
22
  .grep_v(%r{lib/plugins/.*/test/})
23
23
  .reject { |f| File.directory?(f) }
24
24
 
25
25
  # Implementation dependencies
26
26
  spec.add_dependency "chef-telemetry", "~> 1.0"
27
- spec.add_dependency "license-acceptance", ">= 0.2.13", "< 2.0"
27
+ spec.add_dependency "license-acceptance", ">= 0.2.13", "< 3.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"
@@ -43,9 +43,7 @@ Gem::Specification.new do |spec|
43
43
  spec.add_dependency "addressable", "~> 2.4"
44
44
  spec.add_dependency "parslet", "~> 1.5"
45
45
  spec.add_dependency "semverse", "~> 3.0"
46
- spec.add_dependency "htmlentities", "~> 4.3" # TODO: remove when #4853 fixed
47
46
  spec.add_dependency "multipart-post", "~> 2.0"
48
- spec.add_dependency "term-ansicolor", "~> 1.7"
49
47
 
50
48
  spec.add_dependency "train-core", "~> 3.0"
51
49
  end
@@ -5,7 +5,7 @@ module Supermarket
5
5
  class SupermarketCLI < Inspec::BaseCLI
6
6
  namespace "supermarket"
7
7
 
8
- # TODO: find another solution, once https://github.com/erikhuda/thor/issues/261 is fixed
8
+ # TODO: find another solution, once https://github.com/erikhuda/thor/issues/261 is fixed.
9
9
  def self.banner(command, _namespace = nil, _subcommand = false)
10
10
  "#{basename} #{subcommand_prefix} #{command.usage}"
11
11
  end
@@ -60,7 +60,7 @@ module Inspec
60
60
  true
61
61
  end
62
62
 
63
- def self.target_options # rubocop:disable MethodLength
63
+ def self.target_options # rubocop:disable Metrics/MethodLength
64
64
  option :target, aliases: :t, type: :string,
65
65
  desc: "Simple targeting option using URIs, e.g. ssh://user:pass@host:port"
66
66
  option :backend, aliases: :b, type: :string,
@@ -158,6 +158,10 @@ module Inspec
158
158
  option :silence_deprecations, type: :array,
159
159
  banner: "[all]|[GROUP GROUP...]",
160
160
  desc: "Suppress deprecation warnings. See install_dir/etc/deprecations.json for list of GROUPs or use 'all'."
161
+ option :diff, type: :boolean, default: true,
162
+ desc: "Use --no-diff to suppress 'diff' output of failed textual test results."
163
+ option :sort_results_by, type: :string, default: "file", banner: "--sort-results-by=none|control|file|random",
164
+ desc: "After normal execution order, results are sorted by control ID, or by file (default), or randomly. None uses legacy unsorted mode."
161
165
  end
162
166
 
163
167
  def self.format_platform_info(params: {}, indent: 0, color: 39)
@@ -344,7 +344,6 @@ module Inspec
344
344
  cli
345
345
  json
346
346
  json-automate
347
- junit
348
347
  yaml
349
348
  }
350
349
 
@@ -406,6 +405,18 @@ module Inspec
406
405
  @plugin_cfg = data
407
406
  end
408
407
 
408
+ def validate_sort_results_by!(option_value)
409
+ expected = %w{
410
+ none
411
+ control
412
+ file
413
+ random
414
+ }
415
+ return if expected.include? option_value
416
+
417
+ raise Inspec::ConfigError::Invalid, "--sort-results-by must be one of #{expected.join(", ")}"
418
+ end
419
+
409
420
  #-----------------------------------------------------------------------#
410
421
  # Merging Options
411
422
  #-----------------------------------------------------------------------#
@@ -436,6 +447,7 @@ module Inspec
436
447
  finalize_parse_reporters(options)
437
448
  finalize_handle_sudo(options)
438
449
  finalize_compliance_login(options)
450
+ finalize_sort_results(options)
439
451
 
440
452
  Thor::CoreExt::HashWithIndifferentAccess.new(options)
441
453
  end
@@ -510,6 +522,12 @@ module Inspec
510
522
  end
511
523
  end
512
524
 
525
+ def finalize_sort_results(options)
526
+ if options.key?("sort_results_by")
527
+ validate_sort_results_by!(options["sort_results_by"])
528
+ end
529
+ end
530
+
513
531
  class Defaults
514
532
  DEFAULTS = {
515
533
  exec: {
@@ -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
@@ -171,7 +171,7 @@ module Inspec
171
171
  # are free to go higher.
172
172
  DEFAULT_PRIORITY_FOR_VALUE_SET = 60
173
173
 
174
- attr_reader :description, :events, :identifier, :name, :required, :title, :type
174
+ attr_reader :description, :events, :identifier, :name, :required, :sensitive, :title, :type
175
175
 
176
176
  def initialize(name, options = {})
177
177
  @name = name
@@ -264,6 +264,7 @@ module Inspec
264
264
  @required = options[:required] if options.key?(:required)
265
265
  @identifier = options[:identifier] if options.key?(:identifier) # TODO: determine if this is ever used
266
266
  @type = options[:type] if options.key?(:type)
267
+ @sensitive = options[:sensitive] if options.key?(:sensitive)
267
268
  end
268
269
 
269
270
  def make_creation_event(options)
@@ -320,7 +321,7 @@ module Inspec
320
321
 
321
322
  def to_hash
322
323
  as_hash = { name: name, options: {} }
323
- %i{description title identifier type required value}.each do |field|
324
+ %i{description title identifier type required value sensitive}.each do |field|
324
325
  val = send(field)
325
326
  next if val.nil?
326
327
 
@@ -334,7 +335,7 @@ module Inspec
334
335
  #--------------------------------------------------------------------------#
335
336
 
336
337
  def to_s
337
- "Input #{name} with #{current_value}"
338
+ "Input #{name} with value " + (sensitive ? "*** (senstive)" : "#{current_value}")
338
339
  end
339
340
 
340
341
  #--------------------------------------------------------------------------#
@@ -29,6 +29,8 @@ module Inspec
29
29
  def_delegator :inputs_by_profile, :select
30
30
  def_delegator :profile_aliases, :key?, :profile_alias?
31
31
 
32
+ attr_accessor :cache_inputs
33
+
32
34
  def initialize
33
35
  # Keyed on String profile_name => Hash of String input_name => Input object
34
36
  @inputs_by_profile = {}
@@ -43,6 +45,9 @@ module Inspec
43
45
  activator.activate!
44
46
  activator.implementation_class.new
45
47
  end
48
+
49
+ # Activate caching for inputs by default
50
+ @cache_inputs = true
46
51
  end
47
52
 
48
53
  #-------------------------------------------------------------#
@@ -84,7 +89,7 @@ module Inspec
84
89
 
85
90
  # Find or create the input
86
91
  inputs_by_profile[profile_name] ||= {}
87
- if inputs_by_profile[profile_name].key?(input_name)
92
+ if inputs_by_profile[profile_name].key?(input_name) && cache_inputs
88
93
  inputs_by_profile[profile_name][input_name].update(options)
89
94
  else
90
95
  inputs_by_profile[profile_name][input_name] = Inspec::Input.new(input_name, options)
@@ -165,7 +170,8 @@ module Inspec
165
170
  raise ArgumentError, "ERROR: An '=' is required when using --input. Usage: --input input_name1=input_value1 input2=value2"
166
171
  end
167
172
  end
168
- input_name, input_value = pair.split("=")
173
+ pair = pair.match(/(.*?)=(.*)/)
174
+ input_name, input_value = pair[1], pair[2]
169
175
  input_value = parse_cli_input_value(input_name, input_value)
170
176
  evt = Inspec::Input::Event.new(
171
177
  value: input_value,
@@ -315,6 +321,7 @@ module Inspec
315
321
  profile_name,
316
322
  type: input_options[:type],
317
323
  required: input_options[:required],
324
+ sensitive: input_options[:sensitive],
318
325
  event: evt
319
326
  )
320
327
  end
@@ -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.
@@ -1,18 +1,20 @@
1
1
  require_relative "../../../run_data"
2
+ require_relative "../../../utils/run_data_filters"
2
3
 
3
4
  module Inspec::Plugin::V2::PluginType
4
5
  class Reporter < Inspec::Plugin::V2::PluginBase
5
6
  register_plugin_type(:reporter)
7
+ include Inspec::Utils::RunDataFilters
6
8
 
7
9
  attr_reader :run_data
8
10
 
9
11
  def initialize(config)
10
12
  @config = config
11
13
 
12
- # Trim the run_data while still a Hash; if it is huge, this
14
+ # Filter the run_data while still a Hash; if it is huge, this
13
15
  # saves on conversion time
14
16
  @run_data = config[:run_data] || {}
15
- apply_report_resize_options
17
+ apply_run_data_filters_to_hash
16
18
 
17
19
  unless Inspec::RunData.compatible_schema?(self.class.run_data_schema_constraints)
18
20
  # Best we can do is warn here, the InSpec run has finished
@@ -24,29 +26,6 @@ module Inspec::Plugin::V2::PluginType
24
26
  @output = ""
25
27
  end
26
28
 
27
- # This is a temporary duplication of code from lib/inspec/reporters/base.rb
28
- # To be DRY'd up once the core reporters become plugins...
29
- # Apply options such as message truncation and removal of backtraces
30
- def apply_report_resize_options
31
- runtime_config = Inspec::Config.cached.respond_to?(:final_options) ? Inspec::Config.cached.final_options : {}
32
-
33
- message_truncation = runtime_config[:reporter_message_truncation] || "ALL"
34
- trunc = message_truncation == "ALL" ? -1 : message_truncation.to_i
35
- include_backtrace = runtime_config[:reporter_backtrace_inclusion].nil? ? true : runtime_config[:reporter_backtrace_inclusion]
36
-
37
- @run_data[:profiles]&.each do |p|
38
- p[:controls].each do |c|
39
- c[:results]&.map! do |r|
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
45
- end
46
- end
47
- end
48
- end
49
-
50
29
  def output(str, newline = true)
51
30
  @output << str
52
31
  @output << "\n" if newline
@@ -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)
@@ -2,7 +2,6 @@ require "inspec/reporters/base"
2
2
  require "inspec/reporters/cli"
3
3
  require "inspec/reporters/json"
4
4
  require "inspec/reporters/json_automate"
5
- require "inspec/reporters/junit"
6
5
  require "inspec/reporters/automate"
7
6
  require "inspec/reporters/yaml"
8
7
 
@@ -20,8 +19,6 @@ module Inspec::Reporters
20
19
  # right to introduce breaking changes to this reporter at any time.
21
20
  when "json-automate"
22
21
  reporter = Inspec::Reporters::JsonAutomate.new(config)
23
- when "junit"
24
- reporter = Inspec::Reporters::Junit.new(config)
25
22
  when "automate"
26
23
  reporter = Inspec::Reporters::Automate.new(config)
27
24
  when "yaml"
@@ -49,14 +49,14 @@ module Inspec::Reporters
49
49
 
50
50
  res = http.request(req)
51
51
  if res.is_a?(Net::HTTPSuccess)
52
- return true
52
+ true
53
53
  else
54
54
  Inspec::Log.error "send_report: POST to #{uri.path} returned: #{res.body}"
55
- return false
55
+ false
56
56
  end
57
57
  rescue => e
58
58
  Inspec::Log.error "send_report: POST to #{uri.path} returned: #{e.message}"
59
- return false
59
+ false
60
60
  end
61
61
  end
62
62
 
@@ -1,33 +1,17 @@
1
+ require_relative "../utils/run_data_filters"
2
+
1
3
  module Inspec::Reporters
2
4
  class Base
5
+ include Inspec::Utils::RunDataFilters
6
+
3
7
  attr_reader :run_data
4
8
 
5
9
  def initialize(config)
6
10
  @config = config
7
- @run_data = config[:run_data]
8
- apply_report_resize_options unless @run_data.nil?
9
- @output = ""
10
- end
11
+ @run_data = config[:run_data] || {}
12
+ apply_run_data_filters_to_hash
11
13
 
12
- # Apply options such as message truncation and removal of backtraces
13
- def apply_report_resize_options
14
- runtime_config = Inspec::Config.cached.respond_to?(:final_options) ? Inspec::Config.cached.final_options : {}
15
-
16
- message_truncation = runtime_config[:reporter_message_truncation] || "ALL"
17
- trunc = message_truncation == "ALL" ? -1 : message_truncation.to_i
18
- include_backtrace = runtime_config[:reporter_backtrace_inclusion].nil? ? true : runtime_config[:reporter_backtrace_inclusion]
19
-
20
- @run_data[:profiles]&.each do |p|
21
- p[:controls].each do |c|
22
- c[:results]&.map! do |r|
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
28
- end
29
- end
30
- end
14
+ @output = ""
31
15
  end
32
16
 
33
17
  def output(str, newline = true)