abide_dev_utils 0.6.0 → 0.8.0

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.
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
3
4
  require 'yaml'
4
5
  require 'selenium-webdriver'
5
6
  require 'abide_dev_utils/errors/comply'
6
7
  require 'abide_dev_utils/gcloud'
7
8
  require 'abide_dev_utils/output'
8
9
  require 'abide_dev_utils/prompt'
10
+ require 'pry'
9
11
 
10
12
  module AbideDevUtils
11
13
  # Holds module methods and a class for dealing with Puppet Comply
@@ -16,56 +18,55 @@ module AbideDevUtils
16
18
  ReportScraper.new(url, config, **opts).build_report(password)
17
19
  end
18
20
 
19
- def self.check_for_regressions(url, password, config = nil, **opts)
20
- current_report = build_report(url, password, config, **opts)
21
- last_report = if opts.fetch(:remote_report_storage, '') == 'gcloud'
22
- fetch_report
21
+ def self.compare_reports(report_a, report_b, **opts)
22
+ report_name = opts.fetch(:report_name, nil)
23
+ current_report = ScanReport.new.from_yaml(report_a)
24
+ last_report = if opts.fetch(:remote_storage, '') == 'gcloud'
25
+ report_name = report_b if report_name.nil?
26
+ ScanReport.new.from_yaml(ScanReport.fetch_report(name: report_b))
23
27
  else
24
- File.open(opts[:last_report], 'r', &:read)
28
+ report_name = File.basename(report_b) if report_name.nil?
29
+ ScanReport.new.from_yaml(File.read(report_b))
25
30
  end
26
- result, details = good_comparison?(report_comparison(current_report, last_report))
31
+ result, details = current_report.report_comparison(last_report, check_goodness: true)
27
32
  if result
28
- puts 'A-OK'
33
+ AbideDevUtils::Output.simple('No negative differences detected...')
34
+ AbideDevUtils::Output.simple(JSON.pretty_generate(details))
29
35
  else
30
- puts 'Uh-Oh'
31
- puts details
36
+ AbideDevUtils::Output.simple('Negative differences detected!', stream: $stderr)
37
+ AbideDevUtils::Output.simple(JSON.pretty_generate(details), stream: $stderr)
32
38
  end
39
+ if opts.fetch(:upload, false) && !opts.fetch(:remote_storage, '').empty? && !report_name.nil?
40
+ AbideDevUtils::Output.simple('Uploading current report...')
41
+ ScanReport.upload_report(File.expand_path(report_a), name: report_name)
42
+ AbideDevUtils::Output.simple('Successfully uploaded report.')
43
+ end
44
+ result
33
45
  end
34
46
 
35
47
  # Class that uses Selenium WebDriver to gather scan reports from Puppet Comply
36
48
  class ReportScraper
49
+ attr_reader :timeout,
50
+ :username,
51
+ :status,
52
+ :ignorelist,
53
+ :onlylist,
54
+ :max_pagination,
55
+ :screenshot_on_error,
56
+ :page_source_on_error
57
+
37
58
  def initialize(url, config = nil, **opts)
38
59
  @url = url
39
60
  @config = config
40
61
  @opts = opts
41
- end
42
-
43
- def timeout
44
- @timeout ||= fetch_option(:timeout, 10).to_i
45
- end
46
-
47
- def username
48
- @username ||= fetch_option(:username, 'comply')
49
- end
50
-
51
- def status
52
- @status ||= fetch_option(:status)
53
- end
54
-
55
- def ignorelist
56
- @ignorelist ||= fetch_option(:ignorelist, [])
57
- end
58
-
59
- def onlylist
60
- @onlylist ||= fetch_option(:onlylist, [])
61
- end
62
-
63
- def screenshot_on_error
64
- @screenshot_on_error ||= fetch_option(:screenshot_on_error, true)
65
- end
66
-
67
- def page_source_on_error
68
- @page_source_on_error ||= fetch_option(:page_source_on_error, true)
62
+ @timeout = fetch_option(:timeout, 10).to_i
63
+ @username = fetch_option(:username, 'comply')
64
+ @status = fetch_option(:status)
65
+ @ignorelist = fetch_option(:ignorelist, [])
66
+ @onlylist = fetch_option(:onlylist, [])
67
+ @max_pagination = fetch_option(:max_pagination, 5).to_i
68
+ @screenshot_on_error = fetch_option(:screenshot_on_error, false)
69
+ @page_source_on_error = fetch_option(:page_source_on_error, false)
69
70
  end
70
71
 
71
72
  def build_report(password)
@@ -119,6 +120,7 @@ module AbideDevUtils
119
120
  --headless
120
121
  --test-type
121
122
  --disable-gpu
123
+ --no-sandbox
122
124
  --no-first-run
123
125
  --no-default-browser-check
124
126
  --ignore-certificate-errors
@@ -133,27 +135,32 @@ module AbideDevUtils
133
135
  subject.find_element(**kwargs)
134
136
  end
135
137
 
136
- def wait_on(ignore_nse: false, ignore: [Selenium::WebDriver::Error::NoSuchElementError], &block)
138
+ def wait_on(timeout: @timeout,
139
+ ignore_nse: false,
140
+ quit_driver: true,
141
+ quiet: false,
142
+ ignore: [Selenium::WebDriver::Error::NoSuchElementError],
143
+ &block)
137
144
  raise 'wait_on must be passed a block' unless block
138
145
 
139
146
  value = nil
140
147
  if ignore_nse
141
148
  begin
142
- Selenium::WebDriver::Wait.new(ignore: [], timeout: timeout).until do
149
+ Selenium::WebDriver::Wait.new(ignore: [], timeout: timeout, interval: 1).until do
143
150
  value = yield
144
151
  end
145
152
  rescue Selenium::WebDriver::Error::NoSuchElementError
146
153
  return value
147
- rescue => e
148
- raise_error(e)
154
+ rescue StandardError => e
155
+ raise_error(e, AbideDevUtils::Comply::WaitOnError, quit_driver: quit_driver, quiet: quiet)
149
156
  end
150
157
  else
151
158
  begin
152
- Selenium::WebDriver::Wait.new(ignore: ignore, timeout: timeout).until do
159
+ Selenium::WebDriver::Wait.new(ignore: ignore, timeout: timeout, interval: 1).until do
153
160
  value = yield
154
161
  end
155
- rescue => e
156
- raise_error(e)
162
+ rescue StandardError => e
163
+ raise_error(e, AbideDevUtils::Comply::WaitOnError, quit_driver: quit_driver, quiet: quiet)
157
164
  end
158
165
  end
159
166
  value
@@ -169,20 +176,21 @@ module AbideDevUtils
169
176
  FileUtils.mkdir_p path
170
177
  end
171
178
 
172
- def raise_error(err)
173
- output.simple 'Something went wrong!'
179
+ def raise_error(original, err_class = nil, quit_driver: true, quiet: false)
180
+ output.simple 'Something went wrong!' unless quiet
174
181
  if screenshot_on_error
175
- output.simple 'Taking a screenshot of current page state...'
182
+ output.simple 'Taking a screenshot of current page state...' unless quiet
176
183
  screenshot
177
184
  end
178
185
 
179
186
  if page_source_on_error
180
- output.simple 'Saving page source of current page...'
187
+ output.simple 'Saving page source of current page...' unless quiet
181
188
  page_source
182
189
  end
183
190
 
184
- driver.quit
185
- raise err
191
+ driver.quit if quit_driver
192
+ actual_err_class = err_class.nil? ? original.class : err_class
193
+ raise actual_err_class, original.message
186
194
  end
187
195
 
188
196
  def screenshot
@@ -226,12 +234,56 @@ module AbideDevUtils
226
234
  raise ComplyLoginFailedError, error_text
227
235
  end
228
236
 
237
+ def filter_node_report_links(node_report_links)
238
+ if onlylist.empty? && ignorelist.empty?
239
+ output.simple 'No filters set, using all node reports...'
240
+ return node_report_links
241
+ end
242
+
243
+ unless onlylist.empty?
244
+ output.simple 'Onlylist found, filtering node reports...'
245
+ return node_report_links.select { |l| onlylist.include?(l[:name]) }
246
+ end
247
+
248
+ output.simple 'Ignorelist found, filtering node reports...'
249
+ node_report_links.reject { |l| ignorelist.include?(l[:name]) }
250
+ end
251
+
252
+ def find_node_report_table(subject)
253
+ wait_on { find_element(subject, class: 'metric-containers-failed-hosts-count') }
254
+ hosts = find_element(subject, class: 'metric-containers-failed-hosts-count')
255
+ table = find_element(hosts, class: 'rc-table')
256
+ wait_on { find_element(table, tag_name: 'tbody') }
257
+ find_element(table, tag_name: 'tbody')
258
+ end
259
+
260
+ def wait_for_node_report_links(table_body)
261
+ wait_on(timeout: 2, quit_driver: false, quiet: true) { table_body.find_element(tag_name: 'a') }
262
+ output.simple 'Found node report links...'
263
+ table_body.find_elements(tag_name: 'a')
264
+ rescue AbideDevUtils::Comply::WaitOnError
265
+ []
266
+ end
267
+
229
268
  def find_node_report_links
230
269
  output.simple 'Finding nodes with scan reports...'
231
- hosts = wait_on { find_element(class: 'metric-containers-failed-hosts-count') }
232
- table = find_element(hosts, class: 'rc-table')
233
- table_body = find_element(table, tag_name: 'tbody')
234
- wait_on { table_body.find_elements(tag_name: 'a') }
270
+ node_report_links = []
271
+ (1..max_pagination).each do |page|
272
+ output.simple "Trying page #{page}..."
273
+ driver.get("#{@url}/dashboard?page=#{page}&limit=50")
274
+ table_body = find_node_report_table(driver)
275
+ elems = wait_for_node_report_links(table_body)
276
+ if elems.empty?
277
+ output.simple "No links found on page #{page}, stopping search..."
278
+ break
279
+ end
280
+
281
+ elems.each do |elem|
282
+ node_report_links << { name: elem.text, url: elem.attribute('href') }
283
+ end
284
+ end
285
+ driver.get(@url)
286
+ filter_node_report_links(node_report_links)
235
287
  end
236
288
 
237
289
  def connect(password)
@@ -253,63 +305,56 @@ module AbideDevUtils
253
305
  output.simple 'Building scan reports, this may take a while...'
254
306
  all_checks = {}
255
307
  original_window = driver.window_handle
256
- if !onlylist.empty?
257
- node_report_links.reject! { |l| !onlylist.include?(l.text) }
258
- elsif !ignorelist.empty?
259
- node_report_links.reject! { |l| ignorelist.include?(l.text) }
260
- end
261
308
  node_report_links.each do |link|
262
- begin
263
- node_name = link.text
264
- new_progress(node_name)
265
- link_url = link.attribute('href')
266
- driver.manage.new_window(:tab)
267
- progress.increment
268
- wait_on { driver.window_handles.length == 2 }
269
- progress.increment
270
- driver.switch_to.window driver.window_handles[1]
271
- driver.get(link_url)
272
- wait_on { find_element(class: 'details-scan-info') }
273
- progress.increment
274
- wait_on { find_element(class: 'details-table') }
275
- progress.increment
276
- report = { 'scan_results' => {} }
277
- scan_info_table = find_element(class: 'details-scan-info')
278
- scan_info_table_rows = scan_info_table.find_elements(tag_name: 'tr')
309
+ node_name = link[:name]
310
+ link_url = link[:url]
311
+ new_progress(node_name)
312
+ driver.manage.new_window(:tab)
313
+ progress.increment
314
+ wait_on { driver.window_handles.length == 2 }
315
+ progress.increment
316
+ driver.switch_to.window driver.window_handles[1]
317
+ driver.get(link_url)
318
+ wait_on { find_element(class: 'details-scan-info') }
319
+ progress.increment
320
+ wait_on { find_element(class: 'details-table') }
321
+ progress.increment
322
+ report = { 'scan_results' => {} }
323
+ scan_info_table = find_element(class: 'details-scan-info')
324
+ scan_info_table_rows = scan_info_table.find_elements(tag_name: 'tr')
325
+ progress.increment
326
+ check_table_body = find_element(tag_name: 'tbody')
327
+ check_table_rows = check_table_body.find_elements(tag_name: 'tr')
328
+ progress.increment
329
+ scan_info_table_rows.each do |row|
330
+ key = find_element(row, tag_name: 'h5').text
331
+ value = find_element(row, tag_name: 'strong').text
332
+ report[key.downcase.tr(':', '').tr(' ', '_')] = value
279
333
  progress.increment
280
- check_table_body = find_element(tag_name: 'tbody')
281
- check_table_rows = check_table_body.find_elements(tag_name: 'tr')
282
- progress.increment
283
- scan_info_table_rows.each do |row|
284
- key = find_element(row, tag_name: 'h5').text
285
- value = find_element(row, tag_name: 'strong').text
286
- report[key.downcase.tr(':', '').tr(' ', '_')] = value
287
- progress.increment
288
- end
289
- check_table_rows.each do |row|
290
- chk_objs = row.find_elements(tag_name: 'td')
291
- chk_objs.map!(&:text)
292
- if status.nil? || status.include?(chk_objs[1].downcase)
293
- name_parts = chk_objs[0].match(/^([0-9.]+) (.+)$/)
294
- key = normalize_cis_rec_name(name_parts[2])
295
- unless report['scan_results'].key?(chk_objs[1])
296
- report['scan_results'][chk_objs[1]] = {}
297
- end
298
- report['scan_results'][chk_objs[1]][key] = {
299
- 'name' => name_parts[2].chomp,
300
- 'number' => name_parts[1].chomp
301
- }
334
+ end
335
+ check_table_rows.each do |row|
336
+ chk_objs = row.find_elements(tag_name: 'td')
337
+ chk_objs.map!(&:text)
338
+ if status.nil? || status.include?(chk_objs[1].downcase)
339
+ name_parts = chk_objs[0].match(/^([0-9.]+) (.+)$/)
340
+ key = normalize_cis_rec_name(name_parts[2])
341
+ unless report['scan_results'].key?(chk_objs[1])
342
+ report['scan_results'][chk_objs[1]] = {}
302
343
  end
303
- progress.increment
344
+ report['scan_results'][chk_objs[1]][key] = {
345
+ 'name' => name_parts[2].chomp,
346
+ 'number' => name_parts[1].chomp
347
+ }
304
348
  end
305
- all_checks[node_name] = report
306
- driver.close
307
- output.simple "Created report for #{node_name}"
308
- rescue => e
309
- raise_error(e)
310
- ensure
311
- driver.switch_to.window original_window
349
+ progress.increment
312
350
  end
351
+ all_checks[node_name] = report
352
+ driver.close
353
+ output.simple "Created report for #{node_name}"
354
+ rescue ::StandardError => e
355
+ raise_error(e)
356
+ ensure
357
+ driver.switch_to.window original_window
313
358
  end
314
359
  all_checks
315
360
  end
@@ -317,6 +362,8 @@ module AbideDevUtils
317
362
 
318
363
  # Contains multiple NodeScanReport objects
319
364
  class ScanReport
365
+ attr_reader :node_scan_reports
366
+
320
367
  def from_yaml(report)
321
368
  @scan_report = if report.is_a? Hash
322
369
  report
@@ -325,7 +372,65 @@ module AbideDevUtils
325
372
  else
326
373
  YAML.safe_load(report)
327
374
  end
328
- build_node_scan_reports
375
+ @node_scan_reports = build_node_scan_reports
376
+ self
377
+ end
378
+
379
+ def to_h
380
+ node_scan_reports.each_with_object({}) do |node, h|
381
+ h[node.name] = node.hash
382
+ end
383
+ end
384
+
385
+ def to_yaml
386
+ to_h.to_yaml
387
+ end
388
+
389
+ def self.storage_bucket
390
+ @storage_bucket ||= AbideDevUtils::GCloud.storage_bucket
391
+ end
392
+
393
+ def self.fetch_report(name: 'comply_report.yaml')
394
+ report = storage_bucket.file(name)
395
+ report.download.read
396
+ end
397
+
398
+ def self.upload_report(report, name: 'comply_report.yaml')
399
+ storage_bucket.create_file(report, name)
400
+ end
401
+
402
+ def report_comparison(other, check_goodness: false)
403
+ comparison = []
404
+ node_scan_reports.zip(other.node_scan_reports).each do |cr, lr|
405
+ comparison << { cr.name => { diff: {}, node_presense: :new } } if lr.nil?
406
+ comparison << { lr.name => { diff: {}, node_presense: :dropped } } if cr.nil?
407
+ comparison << { cr.name => { diff: cr.diff(lr), node_presence: :same } } unless cr.nil? || lr.nil?
408
+ end
409
+ comparison.inject(&:merge)
410
+ return good_comparison?(comparison) if check_goodness
411
+
412
+ compairison
413
+ end
414
+
415
+ def good_comparison?(report_comparison)
416
+ good = true
417
+ not_good = {}
418
+ report_comparison.each do |node_report|
419
+ node_name = node_report.keys[0]
420
+ report = node_report[node_name]
421
+ next if report[:diff].empty?
422
+
423
+ not_good[node_name] = {}
424
+ unless report.dig(:diff, :passing, :other).nil?
425
+ good = false
426
+ not_good[node_name][:new_not_passing] = report[:diff][:passing][:other]
427
+ end
428
+ unless report.dig(:diff, :failing, :self).nil?
429
+ good = false
430
+ not_good[node_name][:new_failing] = report[:diff][:failing][:self]
431
+ end
432
+ end
433
+ [good, not_good]
329
434
  end
330
435
 
331
436
  private
@@ -341,20 +446,22 @@ module AbideDevUtils
341
446
 
342
447
  # Class representation of a Comply node scan report
343
448
  class NodeScanReport
344
- attr_reader :name, :passing, :failing, :not_checked, :informational, :benchmark, :last_scan, :profile
449
+ attr_reader :name, :passing, :failing, :error, :not_checked, :informational, :benchmark, :last_scan, :profile
345
450
 
346
- DIFF_PROPERTIES = %i[passing failing not_checked informational].freeze
451
+ DIFF_PROPERTIES = %i[passing failing error not_checked informational].freeze
347
452
 
348
453
  def initialize(node_name, node_hash)
349
454
  @name = node_name
350
455
  @hash = node_hash
351
456
  @passing = node_hash.dig('scan_results', 'Pass') || {}
352
457
  @failing = node_hash.dig('scan_results', 'Fail') || {}
458
+ @error = node_hash.dig('scan_results', 'Error') || {}
353
459
  @not_checked = node_hash.dig('scan_results', 'Not checked') || {}
354
460
  @informational = node_hash.dig('scan_results', 'Informational') || {}
355
461
  @benchmark = node_hash['benchmark']
356
462
  @last_scan = node_hash['last_scan']
357
463
  @profile = node_hash.fetch('custom_profile', nil) || node_hash.fetch('profile', nil)
464
+ create_equality_methods
358
465
  end
359
466
 
360
467
  def diff(other)
@@ -365,21 +472,17 @@ module AbideDevUtils
365
472
  diff
366
473
  end
367
474
 
368
- def method_missing(method_name, *args, &_block)
369
- case method_name
370
- when method_name.match?(/^(passing|failing|not_checked|informational)_equal?$/)
371
- property_equal?(method_name.delete_suffix('_equal?'), *args)
372
- when method_name.match?(/^(to_h|to_yaml)$/)
373
- @hash.send(method_name.to_sym)
374
- end
375
- end
475
+ private
376
476
 
377
- def respond_to_missing?(method_name, _include_private = false)
378
- method_name.match?(/^(((passing|failing|not_checked|informational)_equal?)|to_h|to_yaml)$/)
477
+ def create_equality_methods
478
+ DIFF_PROPERTIES.each do |prop|
479
+ meth_name = "#{prop.to_s}_equal?"
480
+ self.class.define_method(meth_name) do |other|
481
+ property_equal?(prop, other)
482
+ end
483
+ end
379
484
  end
380
485
 
381
- private
382
-
383
486
  def property_diff(property, other)
384
487
  {
385
488
  self: send(property).keys - other.send(property).keys,
@@ -388,54 +491,8 @@ module AbideDevUtils
388
491
  end
389
492
 
390
493
  def property_equal?(property, other_property)
391
- send(property.to_sym) == other_property
392
- end
393
- end
394
-
395
- def self.storage_bucket
396
- @storage_bucket ||= AbideDevUtils::GCloud.storage_bucket
397
- end
398
-
399
- def self.fetch_report
400
- report = storage_bucket.file('comply_report.yaml')
401
- report.download.read
402
- end
403
-
404
- def self.upload_report(report)
405
- file_to_upload = report.is_a?(Hash) ? report.to_yaml : report
406
- storage_bucket.create_file(file_to_upload, 'comply_report.yaml')
407
- end
408
-
409
- def self.report_comparison(current, last)
410
- current_report = ScanReport.new.from_yaml(current)
411
- last_report = ScanReport.new.from_yaml(last)
412
-
413
- comparison = []
414
- current_report.zip(last_report).each do |cr, lr|
415
- comparison << { cr.name => { diff: {}, node_presense: :new } } if lr.nil?
416
- comparison << { lr.name => { diff: {}, node_presense: :dropped } } if cr.nil?
417
- comparison << { cr.name => { diff: cr.diff(lr), node_presence: :same } } unless cr.nil? || lr.nil?
418
- end
419
- comparison.inject(&:merge)
420
- end
421
-
422
- def self.good_comparison?(report_comparison)
423
- good = true
424
- not_good = {}
425
- report_comparison.each do |node_name, report|
426
- next if report[:diff].empty?
427
-
428
- not_good[node_name] = {}
429
- unless report[:diff][:passing][:other].empty?
430
- good = false
431
- not_good[node_name][:new_not_passing] = report[:diff][:passing][:other]
432
- end
433
- unless report[:diff][:failing][:self].empty?
434
- good = false
435
- not_good[node_name][:new_failing] = report[:diff][:failing][:self]
436
- end
494
+ send(property) == other_property
437
495
  end
438
- [good, not_good]
439
496
  end
440
497
  end
441
498
  end
@@ -8,6 +8,10 @@ module AbideDevUtils
8
8
  class ComplyLoginFailedError < GenericError
9
9
  @default = 'Failed to login to Comply:'
10
10
  end
11
+
12
+ class WaitOnError < GenericError
13
+ @default = 'wait_on failed due to error:'
14
+ end
11
15
  end
12
16
  end
13
17
  end
@@ -29,6 +29,11 @@ module AbideDevUtils
29
29
  @default = 'Path is not a directory:'
30
30
  end
31
31
 
32
+ # Raised when a file extension is not correct
33
+ class FileExtensionIncorrectError < GenericError
34
+ @default = 'File extension does not match specified extension:'
35
+ end
36
+
32
37
  # Raised when a searched for service is not found in the parser
33
38
  class ServiceNotFoundError < GenericError
34
39
  @default = 'Service not found:'
@@ -12,5 +12,13 @@ module AbideDevUtils
12
12
  class StrategyInvalidError < GenericError
13
13
  @default = 'Invalid strategy selected. Should be either \'name\' or \'num\''
14
14
  end
15
+
16
+ class ControlPartsError < GenericError
17
+ @default = 'Failed to extract parts from control name:'
18
+ end
19
+
20
+ class ProfilePartsError < GenericError
21
+ @default = 'Failed to extract parts from profile name:'
22
+ end
15
23
  end
16
24
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
3
4
  require 'abide_dev_utils/errors/gcloud'
4
5
 
5
6
  module AbideDevUtils
@@ -14,7 +15,7 @@ module AbideDevUtils
14
15
  require 'google/cloud/storage'
15
16
  @bucket = Google::Cloud::Storage.new(
16
17
  project_id: project || ENV['ABIDE_GCLOUD_PROJECT'],
17
- credentials: credentials || ENV['ABIDE_GCLOUD_CREDENTIALS']
18
+ credentials: credentials || JSON.parse(ENV['ABIDE_GCLOUD_CREDENTIALS'])
18
19
  ).bucket(name || ENV['ABIDE_GCLOUD_BUCKET'])
19
20
  end
20
21
  end
@@ -22,9 +22,13 @@ module AbideDevUtils
22
22
  end
23
23
 
24
24
  def self.yaml(in_obj, console: false, file: nil)
25
- AbideDevUtils::Validate.hashable(in_obj)
26
- # Use object's #to_yaml method if it exists, convert to hash if not
27
- yaml_out = in_obj.respond_to?(:to_yaml) ? in_obj.to_yaml : in_obj.to_h.to_yaml
25
+ yaml_out = if in_obj.is_a? String
26
+ in_obj
27
+ else
28
+ AbideDevUtils::Validate.hashable(in_obj)
29
+ # Use object's #to_yaml method if it exists, convert to hash if not
30
+ in_obj.respond_to?(:to_yaml) ? in_obj.to_yaml : in_obj.to_h.to_yaml
31
+ end
28
32
  simple(yaml_out) if console
29
33
  FWRITER.write_yaml(yaml_out, file: file) unless file.nil?
30
34
  end