Skip to content

Commit 2285213

Browse files
authored
Generalize scap parsing (#15)
* s/report_xml/parsed_xml/g We're using this module for any SCAP xml content, not just reports Signed-off-by: Andrew Kofink <akofink@redhat.com> * Separate results from profiles/rules Signed-off-by: Andrew Kofink <akofink@redhat.com>
1 parent c8be36b commit 2285213

File tree

13 files changed

+122
-67
lines changed

13 files changed

+122
-67
lines changed

lib/openscap_parser.rb

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,10 @@ class Base
1818
include OpenscapParser::Profiles
1919
include OpenscapParser::Rules
2020
include OpenscapParser::RuleResults
21+
include OpenscapParser::TestResult
2122

2223
def initialize(report)
23-
report_xml(report)
24-
end
25-
26-
def score
27-
test_result_node.search('score').text.to_f
28-
end
29-
30-
def start_time
31-
@start_time ||= DateTime.parse(test_result_node['start-time'])
32-
end
33-
34-
def end_time
35-
@end_time ||= DateTime.parse(test_result_node['end-time'])
24+
parsed_xml(report)
3625
end
3726
end
3827
end

lib/openscap_parser/ds.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,23 @@ class Ds
66
include OpenscapParser::XmlFile
77

88
def initialize(report)
9-
report_xml report
9+
parsed_xml report
1010
end
1111

1212
def profiles
1313
@profiles ||= profile_nodes
1414
end
1515

1616
def valid?
17-
return true if @report_xml.root.name == 'data-stream-collection' && namespaces.keys.include?('xmlns:ds')
18-
return true if @report_xml.root.name == 'Tailoring' && namespaces.keys.include?('xmlns:xccdf')
17+
return true if @parsed_xml.root.name == 'data-stream-collection' && namespaces.keys.include?('xmlns:ds')
18+
return true if @parsed_xml.root.name == 'Tailoring' && namespaces.keys.include?('xmlns:xccdf')
1919
false
2020
end
2121

2222
private
2323

2424
def profile_nodes
25-
@report_xml.xpath(".//Profile").map do |node|
25+
@parsed_xml.xpath(".//Profile").map do |node|
2626
id_node = node.attribute('id')
2727
id = id_node.value if id_node
2828
title_node = node.at_xpath('./title')

lib/openscap_parser/profile.rb

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
module OpenscapParser
22
class Profile
3-
attr_acessor :id, :title, :description
3+
def initialize(profile_xml: nil)
4+
@profile_xml = profile_xml
5+
end
6+
7+
def id
8+
@id ||= @profile_xml['id']
9+
end
10+
11+
def title
12+
@title ||= @profile_xml.at_css('title') &&
13+
@profile_xml.at_css('title').text
14+
end
15+
16+
def description
17+
@description ||= @profile_xml.at_css('description') &&
18+
@profile_xml.at_css('description').text
19+
end
420

521
def to_h
622
{ :id => id, :title => title, :description => description }

lib/openscap_parser/profiles.rb

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
11
# frozen_string_literal: true
2-
require 'openscap_parser/test_result'
32

43
module OpenscapParser
54
# Methods related to saving profiles and finding which hosts
65
# they belong to
76
module Profiles
8-
include TestResult
9-
107
def self.included(base)
118
base.class_eval do
129
def profiles
13-
@profiles ||= {
14-
profile_node['id'] => profile_node.at_css('title').text
15-
}
16-
end
10+
@profiles ||= profile_nodes.inject({}) do |profiles, profile_node|
11+
profiles[profile_node['id']] = profile_node.at_css('title').text
1712

18-
private
13+
profiles
14+
end
15+
end
1916

20-
def profile_node
21-
@report_xml.at_xpath(".//Profile\
22-
[contains('#{test_result_node['id']}', @id)]")
17+
def profile_nodes(xpath = ".//Profile")
18+
xpath_nodes(xpath)
2319
end
2420
end
2521
end

lib/openscap_parser/rule.rb

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,22 @@ def severity
1616
end
1717

1818
def title
19-
@title ||= @rule_xml.at_css('title').children.first.text
19+
@title ||= @rule_xml.at_css('title') &&
20+
@rule_xml.at_css('title').text
2021
end
2122

2223
def description
23-
rule_node ||= @rule_xml.at_css('description')
24-
@description ||= newline_to_whitespace(rule_node.text) if rule_node && rule_node.text
24+
@description ||= newline_to_whitespace(
25+
@rule_xml.at_css('description') &&
26+
@rule_xml.at_css('description').text || ''
27+
)
2528
end
2629

2730
def rationale
28-
rationale_node ||= @rule_xml.at_css('rationale')
29-
@rationale ||= newline_to_whitespace(rationale_node.children.text) if rationale_node &&
30-
rationale_node.children &&
31-
rationale_node.children.text
31+
@rationale ||= newline_to_whitespace(
32+
@rule_xml.at_css('rationale') &&
33+
@rule_xml.at_css('rationale').text || ''
34+
)
3235
end
3336

3437
def references

lib/openscap_parser/rules.rb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ def rule_ids
1010
end
1111

1212
def rule_objects
13-
return @rule_objects unless @rule_objects.nil?
14-
15-
@rule_objects ||= @report_xml.search('Rule').map do |rule|
16-
Rule.new(rule_xml: rule)
13+
@rule_objects ||= rule_nodes.map do |rule_node|
14+
Rule.new(rule_xml: rule_node)
1715
end
1816
end
1917
alias :rules :rule_objects
18+
19+
def rule_nodes(xpath = ".//Rule")
20+
xpath_nodes(xpath)
21+
end
2022
end
2123
end
2224
end

lib/openscap_parser/test_result.rb

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,41 @@ module OpenscapParser
44
module TestResult
55
def self.included(base)
66
base.class_eval do
7-
private
7+
def score
8+
@score ||= test_result_node &&
9+
test_result_node.search('score').text.to_f
10+
end
11+
12+
def start_time
13+
@start_time ||= test_result_node &&
14+
DateTime.parse(test_result_node['start-time'])
15+
end
16+
17+
def end_time
18+
@end_time ||= test_result_node &&
19+
DateTime.parse(test_result_node['end-time'])
20+
end
21+
22+
def test_result_profiles
23+
@test_result_profiles ||= test_result_profile_nodes.inject({}) do |profiles, profile_node|
24+
profiles[profile_node['id']] = profile_node.at_css('title').text
25+
26+
profiles
27+
end
28+
end
829

930
def test_result_node
10-
@test_result_node ||= @report_xml.at_css('TestResult')
31+
@test_result_node ||= xpath_node('.//TestResult')
32+
end
33+
34+
def test_result_profile_id
35+
test_result_node.xpath('./profile/@idref').text
36+
end
37+
38+
private
39+
40+
def test_result_profile_nodes
41+
profile_nodes(".//Profile [@id='#{test_result_profile_id}']")
1142
end
1243
end
1344
end

lib/openscap_parser/xml_file.rb

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,19 @@ module OpenscapParser
55
module XmlFile
66
attr_reader :namespaces
77

8-
def report_xml(report_contents = '')
9-
@report_xml ||= ::Nokogiri::XML.parse(
8+
def parsed_xml(report_contents = '')
9+
@parsed_xml ||= ::Nokogiri::XML.parse(
1010
report_contents, nil, nil, Nokogiri::XML::ParseOptions.new.norecover)
11-
@namespaces = @report_xml.namespaces.clone
12-
@report_xml.remove_namespaces!
11+
@namespaces = @parsed_xml.namespaces.clone
12+
@parsed_xml.remove_namespaces!
13+
end
14+
15+
def xpath_node(xpath)
16+
@parsed_xml.at_xpath(xpath)
17+
end
18+
19+
def xpath_nodes(xpath)
20+
@parsed_xml.xpath(xpath)
1321
end
1422
end
1523
end

lib/openscap_parser/xml_report.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ def self.included(base)
1111
include OpenscapParser::XmlFile
1212

1313
def host
14-
@report_xml.search('target').text
14+
@parsed_xml.search('target').text
1515
end
1616

1717
def description
18-
@report_xml.search('description').first.text
18+
@parsed_xml.search('description').first.text
1919
end
2020
end
2121
end

test/openscap_parser/profiles_test.rb

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,54 @@
44

55
class ProfilesTest < Minitest::Test
66
describe 'profiles are parsed from' do
7-
include OpenscapParser::Profiles
8-
include OpenscapParser::XMLReport
7+
class TestParser
8+
include OpenscapParser::Profiles
9+
include OpenscapParser::XMLReport
10+
include OpenscapParser::TestResult
911

10-
def report_description
11-
'description'
12+
def report_description
13+
'description'
14+
end
1215
end
1316

1417
def setup
1518
@profiles = nil
1619
end
1720

1821
test 'standard fedora' do
19-
def test_result_node
20-
OpenStruct.new(id: ['xccdf_org.ssgproject.content_profile_standard'])
22+
class TestParser
23+
def test_result_profile_id
24+
'xccdf_org.ssgproject.content_profile_standard'
25+
end
2126
end
2227

23-
report_xml(file_fixture('xccdf_report.xml').read)
28+
test_parser = TestParser.new
29+
30+
test_parser.parsed_xml(file_fixture('xccdf_report.xml').read)
2431

2532
expected = {
2633
'xccdf_org.ssgproject.content_profile_standard' => \
2734
'Standard System Security Profile for Fedora'
2835
}
29-
assert_equal expected, profiles
36+
assert_equal expected, test_parser.test_result_profiles
3037
end
3138

3239
test 'ospp42 rhel' do
33-
def test_result_node
34-
OpenStruct.new(id: ['xccdf_org.open-scap_testresult_xccdf_org.'\
35-
'ssgproject.content_profile_ospp42'])
40+
class TestParser
41+
def test_result_profile_id
42+
'xccdf_org.ssgproject.content_profile_ospp42'
43+
end
3644
end
3745

38-
report_xml(file_fixture('rhel-xccdf-report.xml').read)
46+
test_parser = TestParser.new
47+
48+
test_parser.parsed_xml(file_fixture('rhel-xccdf-report.xml').read)
3949

4050
expected = {
4151
'xccdf_org.ssgproject.content_profile_ospp42' => 'OSPP - Protection '\
4252
'Profile for General Purpose Operating Systems v. 4.2'
4353
}
44-
assert_equal expected, profiles
54+
assert_equal expected, test_parser.test_result_profiles
4555
end
4656
end
4757
end

0 commit comments

Comments
 (0)