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'
]
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'
      }
    }
  },
  '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'
      }
    }
  },
  'Rocky' => {
    '8' => {
      'required_packages' => EL8_PACKAGES,
      'ssg' => {
        'profile_target' => 'centos8',
        'build_target'   => 'centos8',
        'datastream'     => 'ssg-centos8-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'
      }
    }
  }
}

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



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/simp/beaker_helpers/ssg.rb', line 139

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.



132
133
134
# File 'lib/simp/beaker_helpers/ssg.rb', line 132

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



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
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
# File 'lib/simp/beaker_helpers/ssg.rb', line 256

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



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/simp/beaker_helpers/ssg.rb', line 185

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



172
173
174
175
176
177
178
179
# File 'lib/simp/beaker_helpers/ssg.rb', line 172

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



234
235
236
237
238
239
240
# File 'lib/simp/beaker_helpers/ssg.rb', line 234

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



168
169
170
# File 'lib/simp/beaker_helpers/ssg.rb', line 168

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

#remediate(profile) ⇒ Object



181
182
183
# File 'lib/simp/beaker_helpers/ssg.rb', line 181

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

#write_report(report) ⇒ Object

Output the report

Parameters:

  • report

    The results Hash



212
213
214
215
216
# File 'lib/simp/beaker_helpers/ssg.rb', line 212

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