Skip to content

Commit 6e2ef76

Browse files
zacharylc-mitreKyle Fagan
andauthored
XCCDF-Results to HDF (#103)
* *Added hyperlinks to README.md Added mapping tool for SCC XCCDF-Results to HDF Made changes to heimdall_tools.rb and cli.rb to account for xccdf_mapper* Signed-off-by: zacharylc <[email protected]> * Added test for XCCDF results output Signed-off-by: zacharylc-mitre <[email protected]> * Fixed Rubocop errors Signed-off-by: zacharylc <[email protected]> * Remove unused code within xccdf_results_mapper.rb Rename xccdf_mapper to xccdf_results_mapper * Update github with new mapper name * Update README.md linking SCC to xccdf-results Co-authored-by: Kyle Fagan <[email protected]>
1 parent e65b1df commit 6e2ef76

File tree

9 files changed

+20394
-205
lines changed

9 files changed

+20394
-205
lines changed

.github/workflows/build.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ jobs:
5656
jq 'del(.version, .platform.release)' sarif_output.json > sarif_output_jq.json
5757
jq 'del(.version, .platform.release)' ./sample_jsons/sarif_mapper/sarif_output.json > sarif_sample.json
5858
diff sarif_sample.json sarif_output_jq.json
59+
- name: Test xccdf mapper
60+
run: |
61+
heimdall_tools xccdf_results_mapper -x ./sample_jsons/xccdf_results_mapper/sample_input_report/xccdf-results.xml -o xccdf_output.json
62+
jq 'del(.version, .platform.release)' xccdf_output.json > xccdf_output_jq.json
63+
jq 'del(.version, .platform.release)' ./sample_jsons/xccdf_results_mapper/xccdf-hdf.json > xccdf_sample.json
64+
diff xccdf_sample.json xccdf_output_jq.json
5965
- name: Test zap mapper webgoat.json
6066
run: |
6167
heimdall_tools zap_mapper -j ./sample_jsons/zap_mapper/sample_input_jsons/webgoat.json -n "http://mymac.com:8191" -o zap_output_webgoat.json

.rubocop_todo.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This configuration was generated by
22
# `rubocop --auto-gen-config`
3-
# on 2021-06-07 14:26:06 UTC using RuboCop version 1.16.0.
3+
# on 2021-06-07 20:33:06 UTC using RuboCop version 1.16.0.
44
# The point is for the user to remove these configuration records
55
# one by one as the offenses are removed from the code base.
66
# Note that changes in the inspected code, or installation of new
@@ -37,12 +37,12 @@ Lint/UnusedMethodArgument:
3737
Exclude:
3838
- 'lib/heimdall_tools/hdf.rb'
3939

40-
# Offense count: 34
40+
# Offense count: 37
4141
# Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
4242
Metrics/AbcSize:
43-
Max: 73
43+
Max: 124
4444

45-
# Offense count: 4
45+
# Offense count: 5
4646
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
4747
# IgnoredMethods: refine
4848
Metrics/BlockLength:
@@ -53,7 +53,7 @@ Metrics/BlockLength:
5353
Metrics/BlockNesting:
5454
Max: 5
5555

56-
# Offense count: 8
56+
# Offense count: 9
5757
# Configuration parameters: CountComments, CountAsOne.
5858
Metrics/ClassLength:
5959
Max: 175
@@ -63,7 +63,7 @@ Metrics/ClassLength:
6363
Metrics/CyclomaticComplexity:
6464
Max: 17
6565

66-
# Offense count: 38
66+
# Offense count: 40
6767
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
6868
Metrics/MethodLength:
6969
Max: 56

README.md

Lines changed: 183 additions & 197 deletions
Large diffs are not rendered by default.

lib/heimdall_tools.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ module HeimdallTools
1818
autoload :NetsparkerMapper, 'heimdall_tools/netsparker_mapper'
1919
autoload :SarifMapper, 'heimdall_tools/sarif_mapper'
2020
autoload :ScoutSuiteMapper, 'heimdall_tools/scoutsuite_mapper'
21+
autoload :XCCDFResultsMapper, 'heimdall_tools/xccdf_results_mapper'
2122
end

lib/heimdall_tools/cli.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ def burpsuite_mapper
4141
File.write(options[:output], hdf)
4242
end
4343

44+
desc 'xccdf_results_mapper', 'xccdf_results_mapper translates SCAP client XCCDF-Results XML report to HDF format Json be viewed on Heimdall'
45+
long_desc Help.text(:xccdf_results_mapper)
46+
option :xml, required: true, aliases: '-x'
47+
option :output, required: true, aliases: '-o'
48+
def xccdf_results_mapper
49+
hdf = HeimdallTools::XCCDFResultsMapper.new(File.read(options[:xml])).to_hdf
50+
File.write(options[:output], hdf)
51+
end
52+
4453
desc 'nessus_mapper', 'nessus_mapper translates nessus xml report to HDF format Json be viewed on Heimdall'
4554
long_desc Help.text(:nessus_mapper)
4655
option :xml, required: true, aliases: '-x'

lib/heimdall_tools/nessus_mapper.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@
2525

2626
NA_PLUGIN_OUTPUT = 'This Nessus Plugin does not provide output message.'.freeze
2727

28-
# rubocop:disable Metrics/AbcSize
29-
3028
# Loading spinner sign
3129
$spinner = Enumerator.new do |e|
3230
loop do
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
require 'json'
2+
require 'csv'
3+
require 'heimdall_tools/hdf'
4+
require 'utilities/xml_to_hash'
5+
require 'nokogiri'
6+
7+
RESOURCE_DIR = Pathname.new(__FILE__).join('../../data')
8+
9+
# XCCDF mapping for converting SCAP client (SCC or OpenSCAP) outputs to HDF
10+
# SCC output from the RHEL7 Lockdown image was used for testing
11+
12+
U_CCI_LIST = File.join(RESOURCE_DIR, 'U_CCI_List.xml')
13+
14+
IMPACT_MAPPING = {
15+
critical: 0.9,
16+
high: 0.7,
17+
medium: 0.5,
18+
low: 0.3,
19+
na: 0.0
20+
}.freeze
21+
22+
# severity maps to high, medium, low with weights all being 10.0 from the xml
23+
# it doesn't really look like SCAP or SCC cares about that value, just if its high, med, or low
24+
25+
CWE_REGEX = 'CWE-(\d*):'.freeze
26+
CCI_REGEX = 'CCI-(\d*)'.freeze
27+
28+
DEFAULT_NIST_TAG = %w{SA-11 RA-5 Rev_4}.freeze
29+
30+
module HeimdallTools
31+
class XCCDFResultsMapper
32+
def initialize(scap_xml, _name = nil)
33+
@scap_xml = scap_xml
34+
read_cci_xml
35+
begin
36+
data = xml_to_hash(scap_xml)
37+
@results = data['Benchmark']['TestResult']
38+
@benchmarks = data['Benchmark']
39+
@groups = data['Benchmark']['Group']
40+
rescue StandardError => e
41+
raise "Invalid SCAP Client XCCDF output XML file provided Exception: #{e}"
42+
end
43+
end
44+
45+
# change for pass/fail based on output Benchmark.rule
46+
# Pass/Fail are the only two options included in the output file
47+
def finding(issue, count)
48+
finding = {}
49+
finding['status'] = issue['rule-result'][count]['result'].to_s
50+
if finding['status'] == 'pass'
51+
finding['status'] = 'passed'
52+
end
53+
if finding['status'] == 'fail'
54+
finding['status'] = 'failed'
55+
end
56+
finding['code_desc'] = NA_STRING
57+
finding['run_time'] = NA_FLOAT
58+
finding['start_time'] = issue['start-time']
59+
finding['message'] = NA_STRING
60+
finding['resource_class'] = NA_STRING
61+
[finding]
62+
end
63+
64+
def read_cci_xml
65+
@cci_xml = Nokogiri::XML(File.open(U_CCI_LIST))
66+
@cci_xml.remove_namespaces!
67+
rescue StandardError => e
68+
puts "Exception: #{e.message}"
69+
end
70+
71+
def cci_nist_tag(cci_refs)
72+
nist_tags = []
73+
cci_refs.each do |cci_ref|
74+
item_node = @cci_xml.xpath("//cci_list/cci_items/cci_item[@id='#{cci_ref}']")[0] unless @cci_xml.nil?
75+
unless item_node.nil?
76+
nist_ref = item_node.xpath('./references/reference[not(@version <= preceding-sibling::reference/@version) and not(@version <=following-sibling::reference/@version)]/@index').text
77+
end
78+
nist_tags << nist_ref
79+
end
80+
nist_tags
81+
end
82+
83+
def get_impact(severity)
84+
IMPACT_MAPPING[severity.to_sym]
85+
end
86+
87+
def parse_refs(refs)
88+
refs.map { |ref| ref['text'] if ref['text'].match?(CCI_REGEX) }.reject!(&:nil?)
89+
end
90+
91+
# Clean up output by removing the Satsifies block and the end of the description
92+
def satisfies_parse(satisf)
93+
temp_satisf = satisf.match('Satisfies: ([^;]*)<\/VulnDiscussion>')
94+
return temp_satisf[1].split(',') unless temp_satisf.nil?
95+
96+
NA_ARRAY
97+
end
98+
99+
def desc_tags(data, label)
100+
{ data: data || NA_STRING, label: label || NA_STRING }
101+
end
102+
103+
def collapse_duplicates(controls)
104+
unique_controls = []
105+
106+
controls.map { |x| x['id'] }.uniq.each do |id|
107+
collapsed_results = controls.select { |x| x['id'].eql?(id) }.map { |x| x['results'] }
108+
unique_control = controls.find { |x| x['id'].eql?(id) }
109+
unique_control['results'] = collapsed_results.flatten
110+
unique_controls << unique_control
111+
end
112+
unique_controls
113+
end
114+
115+
def to_hdf
116+
controls = []
117+
@groups.each_with_index do |group, i|
118+
@item = {}
119+
@item['id'] = group['Rule']['id'].split('.').last.split('_').drop(2).first.split('r').first.split('S')[1]
120+
@item['title'] = group['Rule']['title'].to_s
121+
@item['desc'] = group['Rule']['description'].to_s.split('Satisfies').first
122+
@item['descriptions'] = []
123+
@item['descriptions'] << desc_tags(group['Rule']['description'], 'default')
124+
@item['descriptions'] << desc_tags('NA', 'rationale')
125+
@item['descriptions'] << desc_tags(group['Rule']['check']['check-content-ref']['name'], 'check')
126+
@item['descriptions'] << desc_tags(group['Rule']['fixtext']['text'], 'fix')
127+
@item['impact'] = get_impact(group['Rule']['severity'])
128+
@item['refs'] = NA_ARRAY
129+
@item['tags'] = {}
130+
@item['tags']['severity'] = nil
131+
@item['tags']['gtitle'] = group['title']
132+
@item['tags']['satisfies'] = satisfies_parse(group['Rule']['description'])
133+
@item['tags']['gid'] = group['Rule']['id'].split('.').last.split('_').drop(2).first.split('r').first
134+
@item['tags']['legacy_id'] = group['Rule']['ident'][2]['text']
135+
@item['tags']['rid'] = group['Rule']['ident'][1]['text']
136+
@item['tags']['stig_id'] = @benchmarks['id']
137+
@item['tags']['fix_id'] = group['Rule']['fix']['id']
138+
@item['tags']['cci'] = parse_refs(group['Rule']['ident'])
139+
@item['tags']['nist'] = cci_nist_tag(@item['tags']['cci'])
140+
@item['code'] = NA_STRING
141+
@item['source_location'] = NA_HASH
142+
# results were in another location and using the top block "Benchmark" as a starting point caused odd issues. This works for now for the results.
143+
@item['results'] = finding(@results, i)
144+
controls << @item
145+
end
146+
147+
controls = collapse_duplicates(controls)
148+
results = HeimdallDataFormat.new(profile_name: @benchmarks['id'],
149+
version: @benchmarks['style'],
150+
duration: NA_FLOAT,
151+
title: @benchmarks['title'],
152+
maintainer: @benchmarks['reference']['publisher'],
153+
summary: @benchmarks['description'],
154+
license: @benchmarks['notice']['id'],
155+
copyright: @benchmarks['metadata']['creator'],
156+
copyright_email: '[email protected]',
157+
controls: controls)
158+
results.to_hdf
159+
end
160+
end
161+
end

0 commit comments

Comments
 (0)