Class: Simp::BeakerHelpers::SSG

Inherits:
Object
  • Object
show all
Defined in:
lib/simp/beaker_helpers/ssg.rb

Overview

Helpers for working with the SCAP Security Guide

Constant Summary collapse

GIT_REPO =
'https://github.com/ComplianceAsCode/content.git'
GIT_BRANCH =

If this is not set, the highest numeric tag will be used

ENV['BEAKER_ssg_branch']
EL7_PACKAGES =
[
  'PyYAML',
  'cmake',
  'git',
  'openscap-python',
  'openscap-scanner',
  'openscap-utils',
  'python-jinja2',
  'python-lxml',
  'python-setuptools',
]
EL8_PACKAGES =
[
  'cmake',
  'git',
  'make',
  'openscap-python3',
  'openscap-utils',
  'openscap-scanner',
  'python3',
  'python3-jinja2',
  'python3-lxml',
  'python3-pyyaml',
  'python3-setuptools',
  'libarchive',
]
EL9_PACKAGES =
[
  'cmake',
  'git',
  'make',
  'openscap-python3',
  'openscap-utils',
  'openscap-scanner',
  'python3',
  'python3-jinja2',
  'python3-lxml',
  'python3-pyyaml',
  'python3-setuptools',
  'libarchive',
]
OS_INFO =
{
  'RedHat' => {
    '6' => {
      'required_packages' => EL7_PACKAGES,
      'ssg' => {
        'profile_target' => 'rhel6',
        'build_target'   => 'rhel6',
        'datastream'     => 'ssg-rhel6-ds.xml'
      }
    },
    '7' => {
      'required_packages' => EL7_PACKAGES,
      'ssg' => {
        'profile_target' => 'rhel7',
        'build_target'   => 'rhel7',
        'datastream'     => 'ssg-rhel7-ds.xml'
      }
    },
    '8' => {
      'required_packages' => EL8_PACKAGES,
      'ssg' => {
        'profile_target' => 'rhel8',
        'build_target'   => 'rhel8',
        'datastream'     => 'ssg-rhel8-ds.xml'
      }
    },
    '9' => {
      'required_packages' => EL9_PACKAGES,
      'ssg' => {
        'profile_target' => 'rhel9',
        'build_target'   => 'rhel9',
        'datastream'     => 'ssg-rhel9-ds.xml'
      }
    }
  },
  'CentOS' => {
    '6' => {
      'required_packages' => EL7_PACKAGES,
      'ssg' => {
        'profile_target' => 'rhel6',
        'build_target'   => 'centos6',
        'datastream'     => 'ssg-centos6-ds.xml'
      }
    },
    '7' => {
      'required_packages' => EL7_PACKAGES,
      'ssg' => {
        'profile_target' => 'centos7',
        'build_target'   => 'centos7',
        'datastream'     => 'ssg-centos7-ds.xml'
      }
    },
    '8' => {
      'required_packages' => EL8_PACKAGES,
      'ssg' => {
        'profile_target' => 'centos8',
        'build_target'   => 'centos8',
        'datastream'     => 'ssg-centos8-ds.xml'
      }
    },
    '9' => {
      'required_packages' => EL9_PACKAGES,
      'ssg' => {
        'profile_target' => 'cs9',
        'build_target'   => 'cs9',
        'datastream'     => 'ssg-cs9-ds.xml'
      }
    }
  },
  'Rocky' => {
    '8' => {
      'required_packages' => EL8_PACKAGES,
      'ssg' => {
        'profile_target' => 'centos8',
        'build_target'   => 'centos8',
        'datastream'     => 'ssg-centos8-ds.xml'
      }
    },
    '9' => {
      'required_packages' => EL9_PACKAGES,
      'ssg' => {
        'profile_target' => 'cs9',
        'build_target'   => 'cs9',
        'datastream'     => 'ssg-cs9-ds.xml'
      }
    }
  },
  'OracleLinux' => {
    '7' => {
      'required_packages' => EL7_PACKAGES,
      'ssg' => {
        'profile_target' => 'ol7',
        'build_target'   => 'ol7',
        'datastream'     => 'ssg-ol7-ds.xml'
      },
    },
    '8' => {
      'required_packages' => EL8_PACKAGES,
      'ssg' => {
        'profile_target' => 'ol8',
        'build_target'   => 'ol8',
        'datastream'     => 'ssg-ol8-ds.xml'
      }
    },
    '9' => {
      'required_packages' => EL9_PACKAGES,
      'ssg' => {
        'profile_target' => 'ol9',
        'build_target'   => 'ol9',
        'datastream'     => 'ssg-ol9-ds.xml'
      }
    }
  }
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(sut) ⇒ SSG

Create a new SSG helper for the specified host

Parameters:

  • sut

    The SUT against which to run



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/simp/beaker_helpers/ssg.rb', line 186

def initialize(sut)
  @sut = sut

  @os = pfact_on(@sut, 'os.name')
  @os_rel = pfact_on(@sut, 'os.release.major')

  sut.mkdir_p('scap_working_dir')

  @scap_working_dir = on(sut, 'cd scap_working_dir && pwd').stdout.strip

  unless OS_INFO[@os]
    fail("Error: The '#{@os}' Operating System is not supported")
  end

  OS_INFO[@os][@os_rel]['required_packages'].each do |pkg|
    install_latest_package_on(@sut, pkg)
  end

  @output_dir = File.absolute_path('sec_results/ssg')

  unless File.directory?(@output_dir)
    FileUtils.mkdir_p(@output_dir)
  end

  @result_file = "#{@sut.hostname}-ssg-#{Time.now.to_i}"

  get_ssg_datastream
end

Instance Attribute Details

#scap_working_dirObject

Returns the value of attribute scap_working_dir.



179
180
181
# File 'lib/simp/beaker_helpers/ssg.rb', line 179

def scap_working_dir
  @scap_working_dir
end

Class Method Details

.process_ssg_results(result_file, filter = nil, exclusions = nil) ⇒ Hash

Process the results of an SSG run

Parameters:

  • result_file (String)

    The oscap result XML file to process

  • filter (String, Array[String]) (defaults to: nil)

    A ‘short name’ filter that will be matched against the rule ID name

  • exclusions (String, Array[String]) (defaults to: nil)

    A ‘short name’ filter of items that will be removed from the ‘filter` matches

Returns:

  • (Hash)

    A Hash of statistics and a formatted report



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
# File 'lib/simp/beaker_helpers/ssg.rb', line 303

def self.process_ssg_results(result_file, filter=nil, exclusions=nil)
  require 'highline'
  require 'nokogiri'

  HighLine.colorize_strings

  fail("Could not find results XML file '#{result_file}'") unless File.exist?(result_file)

  puts "Processing #{result_file}"
  doc = Nokogiri::XML(File.open(result_file))

  # because I'm lazy
  doc.remove_namespaces!

  if filter
    filter = Array(filter)

    xpath_query = [
      '//rule-result[(',
    ]

    xpath_query << filter.map do |flt|
      "contains(@idref,'#{flt}')"
    end.join(' or ')

    xpath_query << ')' if filter.size > 1

    exclusions = Array(exclusions)
    unless exclusions.empty?
      xpath_query << 'and not('

      xpath_query << exclusions.map do |exl|
        "contains(@idref,'#{exl}')"
      end.join(' or ')

      xpath_query << ')' if exclusions.size > 0
    end

    xpath_query << ')]'

    xpath_query = xpath_query.join(' ')

    # XPATH to get the pertinent test results:
    #   Any node named 'rule-result' for which the attribute 'idref'
    #   contains any of the `filter` Strings and does not contain any of the
    #   `exclusions` Strings
    result_nodes = doc.xpath(xpath_query)
  else
    result_nodes = doc.xpath('//rule-result')
  end

  stats = {
    :failed  => [],
    :passed  => [],
    :skipped => [],
    :filter  => filter.nil? ? 'No Filter' : filter,
    :report  => nil,
    :score   => 0
  }

  result_nodes.each do |rule_result|
    # Results are recorded in a child node named 'result'.
    # Within the 'result' node, the actual result string is
    # the content of that node's (only) child node.

    result = rule_result.element_children.at('result')
    result_id = rule_result.attributes['idref'].value.to_s
    result_value = [
      'Title: ' + doc.xpath("//Rule[@id='#{result_id}']/title/text()").first.to_s,
      '  ID: ' + result_id,
    ]

    if result.child.content == 'fail'
      references = {}

      doc.xpath("//Rule[@id='#{result_id}']/reference").each do |ref|
        references[ref['href']] ||= []
        references[ref['href']] << ref.text
      end

      result_value << '  References:'
      references.each_pair do |src, items|
        result_value << "    *  #{src}"
        result_value << "      * #{items.join(', ')}"
      end
      result_value << '  Description: ' + doc.xpath("//Rule[@id='#{result_id}']/description").text.gsub("\n","\n    ")
    end

    result_value = result_value.join("\n")

    if result.child.content == 'fail'
      stats[:failed] << result_value.red
    elsif result.child.content == 'pass'
      stats[:passed] << result_value.green
    else
      stats[:skipped] << result_value.yellow
    end
  end

  report = []

  report << '== Skipped =='
  report << stats[:skipped].join("\n")

  report << '== Passed =='
  report << stats[:passed].join("\n")

  report << '== Failed =='
  report << stats[:failed].join("\n")


  report << 'OSCAP Statistics:'

  if filter
    report << "  * Used Filter: 'idref' ~= '#{stats[:filter]}'"
  end

  report << "  * Passed: #{stats[:passed].count.to_s.green}"
  report << "  * Failed: #{stats[:failed].count.to_s.red}"
  report << "  * Skipped: #{stats[:skipped].count.to_s.yellow}"

  score = 0

  if (stats[:passed].count + stats[:failed].count) > 0
    score = ((stats[:passed].count.to_f/(stats[:passed].count + stats[:failed].count)) * 100.0).round(0)
  end

  report << "\n Score: #{score}%"

  stats[:score]  = score
  stats[:report] = report.join("\n")

  return stats
end

Instance Method Details

#evaluate(profile, remediate = false) ⇒ Object



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/simp/beaker_helpers/ssg.rb', line 232

def evaluate(profile, remediate=false)
  cmd = "cd #{@scap_working_dir}; oscap xccdf eval"

  if remediate
    cmd += ' --remediate'
  end

  cmd += %( --profile #{profile} --results #{@result_file}.xml --report #{@result_file}.html #{OS_INFO[@os][@os_rel]['ssg']['datastream']})

  # We accept all exit codes here because there have occasionally been
  # failures in the SSG content and we're not testing that.

  on(@sut, cmd, :accept_all_exit_codes => true)

  ['xml', 'html'].each do |ext|
    path = "#{@scap_working_dir}/#{@result_file}.#{ext}"
    scp_from(@sut, path, @output_dir)

    fail("Could not retrieve #{path} from #{@sut}") unless File.exist?(File.join(@output_dir, "#{@result_file}.#{ext}"))
  end
end

#get_profilesObject



219
220
221
222
223
224
225
226
# File 'lib/simp/beaker_helpers/ssg.rb', line 219

def get_profiles
  cmd = "cd #{@scap_working_dir}; oscap info --profiles"
  on(@sut, "#{cmd} #{OS_INFO[@os][@os_rel]['ssg']['datastream']}")
    .stdout
    .strip
    .lines
    .map{|x| x.split(':').first}
end

#process_ssg_results(filter = nil, exclusions = nil) ⇒ Hash

Retrieve a subset of test results based on a match to filter

FIXME:

  • This is a hack! Should be searching for rules based on a set set of STIG ids, but don’t see those ids in the oscap results xml. Further mapping is required…

  • Create the same report structure as inspec

Parameters:

  • filter (String, Array[String]) (defaults to: nil)

    A ‘short name’ filter that will be matched against the rule ID name

  • exclusions (String, Array[String]) (defaults to: nil)

    A ‘short name’ filter of items that will be removed from the ‘filter` matches

Returns:

  • (Hash)

    A Hash of statistics and a formatted report



281
282
283
284
285
286
287
# File 'lib/simp/beaker_helpers/ssg.rb', line 281

def process_ssg_results(filter=nil, exclusions=nil)
  self.class.process_ssg_results(
    File.join(@output_dir, @result_file) + '.xml',
    filter,
    exclusions
  )
end

#profile_targetObject



215
216
217
# File 'lib/simp/beaker_helpers/ssg.rb', line 215

def profile_target
  OS_INFO[@os][@os_rel]['ssg']['profile_target']
end

#remediate(profile) ⇒ Object



228
229
230
# File 'lib/simp/beaker_helpers/ssg.rb', line 228

def remediate(profile)
  evaluate(profile, true)
end

#write_report(report) ⇒ Object

Output the report

Parameters:

  • report

    The results Hash



259
260
261
262
263
# File 'lib/simp/beaker_helpers/ssg.rb', line 259

def write_report(report)
  File.open(File.join(@output_dir, @result_file) + '.report', 'w') do |fh|
    fh.puts(report[:report].uncolor)
  end
end