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 closest tag to the default branch will be used

ENV['BEAKER_ssg_branch']
EL_PACKAGES =
[
  'PyYAML',
  'cmake',
  'git',
  'openscap-python',
  'openscap-utils',
  'python-lxml',
  'python-jinja2'
]
EL8_PACKAGES =
[
  'python3',
  'python3-pyyaml',
  'cmake',
  'git',
  'openscap-python3',
  'openscap-utils',
  'python3-lxml',
  'python3-jinja2'
]
OS_INFO =
{
  'RedHat' => {
    '6' => {
      'required_packages' => EL_PACKAGES,
      'ssg' => {
        'profile_target' => 'rhel6',
        'build_target'   => 'rhel6',
        'datastream'     => 'ssg-rhel6-ds.xml'
      }
    },
    '7' => {
      'required_packages' => EL_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' => EL_PACKAGES,
      'ssg' => {
        'profile_target' => 'rhel6',
        'build_target'   => 'centos6',
        'datastream'     => 'ssg-centos6-ds.xml'
      }
    },
    '7' => {
      'required_packages' => EL_PACKAGES,
      'ssg' => {
        'profile_target' => 'rhel7',
        'build_target'   => 'centos7',
        'datastream'     => 'ssg-centos7-ds.xml'
      }
    },
    '8' => {
      'required_packages' => EL8_PACKAGES,
      'ssg' => {
        'profile_target' => 'rhel8',
        'build_target'   => 'centos8',
        'datastream'     => 'ssg-centos8-ds.xml'
      }
    }
  },
  'OracleLinux' => {
    '7' => {
      'required_packages' => EL_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



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/simp/beaker_helpers/ssg.rb', line 123

def initialize(sut)
  @sut = sut

  @os = fact_on(@sut, 'operatingsystem')
  @os_rel = fact_on(@sut, 'operatingsystemmajrelease')

  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|
    @sut.install_package(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.



116
117
118
# File 'lib/simp/beaker_helpers/ssg.rb', line 116

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



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
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
# File 'lib/simp/beaker_helpers/ssg.rb', line 232

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

    if exclusions
      exclusions = Array(exclusions)

      xpath_query << 'and not('

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

      xpath_query << ')' if exclusions.size > 1
    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
    ].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



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/simp/beaker_helpers/ssg.rb', line 161

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

  if remediate
    cmd += ' --remediate'
  end

  cmd += %( --fetch-remote-resources --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

#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



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

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



153
154
155
# File 'lib/simp/beaker_helpers/ssg.rb', line 153

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

#remediate(profile) ⇒ Object



157
158
159
# File 'lib/simp/beaker_helpers/ssg.rb', line 157

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

#write_report(report) ⇒ Object

Output the report

Parameters:

  • report

    The results Hash



188
189
190
191
192
# File 'lib/simp/beaker_helpers/ssg.rb', line 188

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