dradis-qualys 4.1.0 → 4.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/lib/dradis/plugins/qualys/asset/importer.rb +115 -0
  4. data/lib/dradis/plugins/qualys/engine.rb +13 -0
  5. data/lib/dradis/plugins/qualys/field_processor.rb +23 -3
  6. data/lib/dradis/plugins/qualys/gem_version.rb +1 -1
  7. data/lib/dradis/plugins/qualys/vuln/importer.rb +107 -0
  8. data/lib/dradis/plugins/qualys/was/importer.rb +113 -0
  9. data/lib/dradis/plugins/qualys.rb +4 -1
  10. data/lib/dradis-qualys.rb +4 -0
  11. data/lib/qualys/asset/evidence.rb +74 -0
  12. data/lib/qualys/asset/vulnerability.rb +87 -0
  13. data/lib/qualys/element.rb +31 -29
  14. data/lib/qualys/was/qid.rb +85 -0
  15. data/lib/qualys/was/vulnerability.rb +68 -0
  16. data/lib/tasks/thorfile.rb +15 -1
  17. data/spec/fixtures/files/simple_asset.xml +126 -0
  18. data/spec/fixtures/files/simple_was.xml +134 -0
  19. data/spec/qualys/asset/importer_spec.rb +41 -0
  20. data/spec/qualys/{importer_spec.rb → vuln/importer_spec.rb} +5 -50
  21. data/spec/qualys/was/importer_spec.rb +41 -0
  22. data/spec/spec_helper.rb +3 -0
  23. data/spec/support/spec_macros.rb +46 -0
  24. data/templates/asset-evidence.fields +9 -0
  25. data/templates/asset-evidence.sample +14 -0
  26. data/templates/asset-evidence.template +11 -0
  27. data/templates/asset-issue.fields +14 -0
  28. data/templates/asset-issue.sample +21 -0
  29. data/templates/asset-issue.template +22 -0
  30. data/templates/element.fields +1 -0
  31. data/templates/element.template +4 -0
  32. data/templates/was-evidence.fields +6 -0
  33. data/templates/was-evidence.sample +44 -0
  34. data/templates/was-evidence.template +11 -0
  35. data/templates/was-issue.fields +16 -0
  36. data/templates/was-issue.sample +24 -0
  37. data/templates/was-issue.template +28 -0
  38. metadata +34 -6
  39. data/lib/dradis/plugins/qualys/importer.rb +0 -88
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 953114d5501cf866740c9ab4657191ab2fc123998bf29a5c7303292740b6a3c9
4
- data.tar.gz: 3cb44c03b7a3ac23d8a07e73cae291ffb9a5cdf8f5f8f27b57739788da164204
3
+ metadata.gz: 2da61b9fd75db0c8211be93f7449d215ac487a68f96bfed06890a0faeeca9df1
4
+ data.tar.gz: 612d0c2f9c12cec4f3c2c65eeb3a4d4ad1ae8bd9a091ef67894e077fc0e73079
5
5
  SHA512:
6
- metadata.gz: c98b9d282b4b7fc7dd74e70a9755b8df60776f3a918a9283f1ce5081ec9cbcffee16e63566ade648405d44f56fae635d2d94baeed949fe08e211fc6c4efaf2a1
7
- data.tar.gz: 835a5c1e0f1956b61354c72fe821ddd31c455a69c8a8df2393514f3c80a663c4365d821ee424383af9268a534cced11c22a4477b03770a7f0fc0461e8631d712
6
+ metadata.gz: 998662ea3dd5a075ffc5eb764b781d3f4e56b5af56243317270616eeb59eb9cf7607823cb359b783f8f5f4417a224c8c4c068154f4382160fa89ef71ccb66851
7
+ data.tar.gz: ee2968f166612fd5bab2188ea0aba072dd50ca46f99b178a2ea4b24acb7816f888ccdfbdb781af0afcdc4c9c1e3591c0758a1ea146ae6f8b7652c6f4cd60da53
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ v4.4.0 (June 2022)
2
+ - Registers template mappings locally
3
+
4
+ v4.3.0 (April 2022)
5
+ - Adds Qualys Asset Scanner (ASSET) support
6
+ - Bugs fixes:
7
+ - Fixes WAS CVSS3 mapping and not importing fields to the issue
8
+
9
+ v4.2.0 (February 2022)
10
+ - Adds 'element.qualys_collection' as issue field
11
+ - Adds Qualys Web Application Scanner (WAS) support
12
+
1
13
  v4.1.0 (November 2021)
2
14
  - Add <dd>, <dt> support
3
15
  - Remove orphaned <b> tags
@@ -0,0 +1,115 @@
1
+ module Dradis::Plugins::Qualys
2
+ module Asset
3
+ ROOT_PATH_NAME = 'ASSET_DATA_REPORT'.freeze
4
+ def self.meta
5
+ package = Dradis::Plugins::Qualys
6
+
7
+ {
8
+ name: package::Engine::plugin_name,
9
+ description: 'Upload Qualys Asset results (.xml)',
10
+ version: package.version
11
+ }
12
+ end
13
+
14
+ class Importer < Dradis::Plugins::Upload::Importer
15
+ def self.templates
16
+ { evidence: 'asset-evidence', issue: 'asset-issue' }
17
+ end
18
+
19
+ def initialize(args={})
20
+ args[:plugin] = Dradis::Plugins::Qualys
21
+ super(args)
22
+
23
+ @issue_lookup = {}
24
+ end
25
+
26
+ # The framework will call this function if the user selects this plugin from
27
+ # the dropdown list and uploads a file.
28
+ # @returns true if the operation was successful, false otherwise
29
+ def import(params={})
30
+ file_content = File.read( params[:file] )
31
+
32
+ logger.info { 'Parsing Qualys ASSET XML output file...' }
33
+ doc = Nokogiri::XML(file_content)
34
+ logger.info { 'Done.' }
35
+
36
+ if doc.root.name != ROOT_PATH_NAME
37
+ error = 'No scan results were detected in the uploaded file. Ensure you uploaded a Qualys ASSET XML file.'
38
+ logger.fatal { error }
39
+ content_service.create_note text: error
40
+ return false
41
+ end
42
+
43
+ doc.xpath('ASSET_DATA_REPORT/GLOSSARY/VULN_DETAILS_LIST/VULN_DETAILS').each do |xml_issue|
44
+ process_issue(xml_issue)
45
+ end
46
+
47
+ doc.xpath('ASSET_DATA_REPORT/HOST_LIST/HOST').each do |xml_node|
48
+ process_node(xml_node)
49
+ end
50
+
51
+ true
52
+ end
53
+
54
+ private
55
+
56
+ attr_accessor :issue_lookup
57
+
58
+ def process_node(xml_node)
59
+ logger.info { 'Creating node...' }
60
+
61
+ # Create host node
62
+ host_node = content_service.create_node(
63
+ label: xml_node.at_xpath('IP').text,
64
+ type: :host
65
+ )
66
+
67
+ %w[dns host_id operating_system qg_hostid tracking_method].each do |key|
68
+ prop = xml_node.at_xpath(key.upcase)
69
+ host_node.set_property(key.to_sym, prop.text) if prop
70
+ end
71
+
72
+ tags = xml_node.at_xpath('ASSET_TAGS/ASSET_TAG')
73
+ if tags
74
+ tags.each do |tag|
75
+ host_node.set_property(:asset_tags, tag.text)
76
+ end
77
+ end
78
+
79
+ host_node.save
80
+
81
+ xml_node.xpath('./VULN_INFO_LIST/VULN_INFO').each do |xml_evidence|
82
+ process_evidence(xml_evidence, host_node)
83
+ end
84
+ end
85
+
86
+ def process_issue(xml_vuln)
87
+ qid = xml_vuln.at_xpath('QID').text
88
+ logger.info { "\t => Creating new issue (plugin_id: #{ qid })" }
89
+ issue_text = template_service.process_template(template: 'asset-issue', data: xml_vuln)
90
+ issue = content_service.create_issue(text: issue_text, id: qid)
91
+
92
+ issue_lookup[qid.to_i] = issue
93
+ end
94
+
95
+ def process_evidence(xml_evidence, node)
96
+ qid = xml_evidence.at_xpath('./QID').text
97
+
98
+ issue = issue_lookup[qid.to_i]
99
+ if issue
100
+ issue_id = issue.respond_to?(:id) ? issue.id : issue.to_issue.id
101
+
102
+ logger.info { "\t => Creating new evidence (plugin_id: #{qid})" }
103
+ logger.info { "\t\t => Issue: #{issue.title} (plugin_id: #{issue_id})" }
104
+ logger.info { "\t\t => Node: #{node.label} (#{node.id})" }
105
+ else
106
+ logger.info { "\t => Couldn't find QID for issue with ID=#{qid}" }
107
+ return
108
+ end
109
+
110
+ evidence_content = template_service.process_template(template: 'asset-evidence', data: xml_evidence)
111
+ content_service.create_evidence(issue: issue, node: node, content: evidence_content)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -5,5 +5,18 @@ module Dradis::Plugins::Qualys
5
5
  include ::Dradis::Plugins::Base
6
6
  description 'Processes Qualys output'
7
7
  provides :upload
8
+
9
+ # Because this plugin provides two export modules, we have to overwrite
10
+ # the default .uploaders() method.
11
+ #
12
+ # See:
13
+ # Dradis::Plugins::Upload::Base in dradis-plugins
14
+ def self.uploaders
15
+ [
16
+ Dradis::Plugins::Qualys::Asset,
17
+ Dradis::Plugins::Qualys::Vuln,
18
+ Dradis::Plugins::Qualys::WAS
19
+ ]
20
+ end
8
21
  end
9
22
  end
@@ -4,8 +4,19 @@ module Dradis
4
4
  class FieldProcessor < Dradis::Plugins::Upload::FieldProcessor
5
5
 
6
6
  def post_initialize(args={})
7
- @cat_object = data
8
- @qualys_object = ::Qualys::Element.new(data.elements.first)
7
+ case data.name
8
+ when 'CAT'
9
+ @cat_object = data
10
+ @qualys_object = ::Qualys::Element.new(data.elements.first)
11
+ when 'QID'
12
+ @qualys_object = ::Qualys::WAS::QID.new(data)
13
+ when 'VULNERABILITY'
14
+ @qualys_object = ::Qualys::WAS::Vulnerability.new(data)
15
+ when 'VULN_DETAILS'
16
+ @qualys_object = ::Qualys::Asset::Vulnerability.new(data)
17
+ when 'VULN_INFO'
18
+ @qualys_object = ::Qualys::Asset::Evidence.new(data)
19
+ end
9
20
  end
10
21
 
11
22
  def value(args={})
@@ -13,8 +24,14 @@ module Dradis
13
24
 
14
25
  # Fields in the template are of the form <foo>.<field>, where <foo>
15
26
  # is common across all fields for a given template (and meaningless).
16
- _, name = field.split('.')
27
+ # However we can use it to identify the type of scan we're processing.
28
+ type, name = field.split('.')
29
+
30
+ %{element evidence}.include?(type) ? value_network(name) : value_was(name)
31
+ end
17
32
 
33
+ private
34
+ def value_network(name)
18
35
  if %w{cat_fqdn cat_misc cat_port cat_protocol cat_value}.include?(name)
19
36
  attribute = name[4..-1]
20
37
  @cat_object[attribute] || 'n/a'
@@ -36,6 +53,9 @@ module Dradis
36
53
  end
37
54
  end
38
55
 
56
+ def value_was(name)
57
+ @qualys_object.try(name) || 'n/a'
58
+ end
39
59
  end
40
60
  end
41
61
  end
@@ -8,7 +8,7 @@ module Dradis
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 4
11
- MINOR = 1
11
+ MINOR = 4
12
12
  TINY = 0
13
13
  PRE = nil
14
14
 
@@ -0,0 +1,107 @@
1
+ module Dradis::Plugins::Qualys
2
+ module Vuln
3
+ def self.meta
4
+ package = Dradis::Plugins::Qualys
5
+
6
+ {
7
+ name: package::Engine::plugin_name,
8
+ description: 'Upload Qualys Vulnerability Management results (.xml)',
9
+ version: package.version
10
+ }
11
+ end
12
+
13
+ class Importer < Dradis::Plugins::Upload::Importer
14
+ attr_accessor :host_node
15
+
16
+ def self.templates
17
+ { evidence: 'evidence', issue: 'element' }
18
+ end
19
+
20
+ def initialize(args={})
21
+ args[:plugin] = Dradis::Plugins::Qualys
22
+ super(args)
23
+ end
24
+
25
+ # The framework will call this function if the user selects this plugin from
26
+ # the dropdown list and uploads a file.
27
+ # @returns true if the operation was successful, false otherwise
28
+ def import(params={})
29
+ file_content = File.read( params[:file] )
30
+
31
+ logger.info{'Parsing Qualys output file...'}
32
+ @doc = Nokogiri::XML( file_content )
33
+ logger.info{'Done.'}
34
+
35
+ if @doc.root.name != 'SCAN'
36
+ error = "No scan results were detected in the uploaded file. Ensure you uploaded a Qualys XML file."
37
+ logger.fatal{ error }
38
+ content_service.create_note text: error
39
+ return false
40
+ end
41
+
42
+ @doc.xpath('SCAN/IP').each do |xml_host|
43
+ process_ip(xml_host)
44
+ end
45
+
46
+ return true
47
+ end
48
+
49
+ private
50
+
51
+ def process_ip(xml_host)
52
+ host_ip = xml_host['value']
53
+ logger.info{ "Host: %s" % host_ip }
54
+
55
+ self.host_node = content_service.create_node(label: host_ip, type: :host)
56
+
57
+ host_node.set_property(:ip, host_ip)
58
+ host_node.set_property(:hostname, xml_host['name'])
59
+ if (xml_os = xml_host.xpath('OS')) && xml_os.any?
60
+ host_node.set_property(:os, xml_os.text)
61
+ end
62
+ host_node.save
63
+
64
+ # We treat INFOS, SERVICES, PRACTICES, and VULNS the same way
65
+ # All of these are imported into Dradis as Issues
66
+ ['INFOS', 'SERVICES', 'PRACTICES', 'VULNS'].each do |collection|
67
+ xml_host.xpath(collection).each do |xml_collection|
68
+ process_collection(collection, xml_collection)
69
+ end
70
+ end
71
+ end
72
+
73
+ def process_collection(collection, xml_collection)
74
+ xml_cats = xml_collection.xpath('CAT')
75
+
76
+ xml_cats.each do |xml_cat|
77
+ logger.info{ "\t#{ collection } - #{ xml_cat['value'] }" }
78
+
79
+ empty_dup_xml_cat = xml_cat.dup
80
+ empty_dup_xml_cat.children.remove
81
+
82
+ # For each INFOS/CAT/INFO, SERVICES/CAT/SERVICE, VULNS/CAT/VULN, etc.
83
+ xml_cat.xpath(collection.chop).each do |xml_element|
84
+ dup_xml_cat = empty_dup_xml_cat.dup
85
+ dup_xml_cat.add_child(xml_element.dup)
86
+ cat_number = xml_element[:number]
87
+
88
+ process_vuln(cat_number, dup_xml_cat)
89
+
90
+ end
91
+ end
92
+ end
93
+
94
+ # Takes a <CAT> element containing a single <VULN> element and processes an
95
+ # Issue and Evidence template out of it.
96
+ def process_vuln(vuln_number, xml_cat)
97
+ logger.info{ "\t\t => Creating new issue (plugin_id: #{ vuln_number })" }
98
+ issue_text = template_service.process_template(template: 'element', data: xml_cat)
99
+ issue = content_service.create_issue(text: issue_text, id: vuln_number)
100
+
101
+ logger.info{ "\t\t => Creating new evidence" }
102
+ evidence_content = template_service.process_template(template: 'evidence', data: xml_cat)
103
+ content_service.create_evidence(issue: issue, node: self.host_node, content: evidence_content)
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,113 @@
1
+ module Dradis::Plugins::Qualys
2
+
3
+ # This module knows how to parse Qualys Web Application Scanner format.
4
+ module WAS
5
+ def self.meta
6
+ package = Dradis::Plugins::Qualys
7
+
8
+ {
9
+ name: package::Engine::plugin_name,
10
+ description: 'Upload Qualys WAS results (.xml)',
11
+ version: package.version
12
+ }
13
+ end
14
+
15
+ class Importer < Dradis::Plugins::Upload::Importer
16
+ def self.templates
17
+ { evidence: 'was-evidence', issue: 'was-issue' }
18
+ end
19
+
20
+ def initialize(args={})
21
+ args[:plugin] = Dradis::Plugins::Qualys
22
+ super(args)
23
+
24
+ @issue_lookup = {}
25
+ end
26
+
27
+ def import(params={})
28
+ file_content = File.read(params[:file])
29
+
30
+ logger.info { 'Parsing Qualys WAS XML output file...'}
31
+ doc = Nokogiri::XML(file_content)
32
+ logger.info { 'Done.' }
33
+
34
+ if doc.root.name != 'WAS_SCAN_REPORT'
35
+ error = 'Document doesn\'t seem to be in the Qualys WAS XML format.'
36
+ logger.fatal { error }
37
+ content_service.create_note text: error
38
+ return false
39
+ end
40
+
41
+ logger.info { 'Global Summary information'}
42
+
43
+ xml_global_summary = doc.at_xpath('WAS_SCAN_REPORT/SUMMARY/GLOBAL_SUMMARY')
44
+ logger.info { 'Security Risk: ' + xml_global_summary.at_xpath('./SECURITY_RISK').text }
45
+ logger.info { 'Vulnerabilities found: ' + xml_global_summary.at_xpath('./VULNERABILITY').text }
46
+
47
+ xml_webapp = doc.at_xpath('WAS_SCAN_REPORT/APPENDIX/WEBAPP')
48
+ process_webapp(xml_webapp)
49
+
50
+ doc.xpath('WAS_SCAN_REPORT/GLOSSARY/QID_LIST/QID').each do |xml_qid|
51
+ process_issue(xml_qid)
52
+ end
53
+
54
+ doc.xpath('WAS_SCAN_REPORT/RESULTS/VULNERABILITY_LIST/VULNERABILITY').each do |xml_vulnerability|
55
+ process_evidence(xml_vulnerability)
56
+ end
57
+
58
+ true
59
+ end
60
+
61
+ private
62
+ attr_accessor :webapp_node, :issue_lookup
63
+
64
+ def process_evidence(xml_vulnerability)
65
+ id = xml_vulnerability.at_xpath('./ID').text
66
+
67
+ issue = issue_lookup[xml_vulnerability.at_xpath('./QID').text.to_i]
68
+ if issue
69
+ issue_id = issue.respond_to?(:id) ? issue.id : issue.to_issue.id
70
+
71
+ logger.info{ "\t => Creating new evidence (plugin_id: #{id})" }
72
+ logger.info{ "\t\t => Issue: #{issue.title} (plugin_id: #{issue_id})" }
73
+ logger.info{ "\t\t => Node: #{webapp_node.label} (#{webapp_node.id})" }
74
+ else
75
+ logger.info{ "\t => Couldn't find QID for evidence with ID=#{id}" }
76
+ return
77
+ end
78
+
79
+ evidence_content = template_service.process_template(template: 'was-evidence', data: xml_vulnerability)
80
+ content_service.create_evidence(issue: issue, node: webapp_node, content: evidence_content)
81
+ end
82
+
83
+ def process_issue(xml_qid)
84
+ qid = xml_qid.at_xpath('QID').text
85
+ logger.info{ "\t => Creating new issue (plugin_id: #{ qid })" }
86
+ issue_text = template_service.process_template(template: 'was-issue', data: xml_qid)
87
+ issue = content_service.create_issue(text: issue_text, id: qid)
88
+
89
+ issue_lookup[qid.to_i] = issue
90
+ end
91
+
92
+ def process_webapp(xml_webapp)
93
+ id = xml_webapp.at_xpath('./ID').text
94
+ name = xml_webapp.at_xpath('./NAME').text
95
+ url = xml_webapp.at_xpath('./URL').text
96
+ scope = xml_webapp.at_xpath('./SCOPE').text
97
+
98
+ uri = URI(url)
99
+ @webapp_node = content_service.create_node(label: uri.host, type: :host)
100
+
101
+ webapp_node.set_property('qualys.webapp.id', id)
102
+ webapp_node.set_property('qualys.webapp.name', name)
103
+ webapp_node.set_property('qualys.webapp.url', url)
104
+ webapp_node.set_property('qualys.webapp.scope', scope)
105
+ webapp_node.save!
106
+
107
+ logger.info { 'Webapp name: ' + name }
108
+ logger.info { 'Webapp URL: ' + url }
109
+ logger.info { 'Webapp scope: ' + scope }
110
+ end
111
+ end
112
+ end
113
+ end
@@ -7,5 +7,8 @@ end
7
7
 
8
8
  require 'dradis/plugins/qualys/engine'
9
9
  require 'dradis/plugins/qualys/field_processor'
10
- require 'dradis/plugins/qualys/importer'
11
10
  require 'dradis/plugins/qualys/version'
11
+
12
+ require 'dradis/plugins/qualys/asset/importer'
13
+ require 'dradis/plugins/qualys/vuln/importer'
14
+ require 'dradis/plugins/qualys/was/importer'
data/lib/dradis-qualys.rb CHANGED
@@ -6,3 +6,7 @@ require 'dradis/plugins/qualys'
6
6
 
7
7
  # Load supporting Qualys classes
8
8
  require 'qualys/element'
9
+ require 'qualys/asset/evidence'
10
+ require 'qualys/asset/vulnerability'
11
+ require 'qualys/was/qid'
12
+ require 'qualys/was/vulnerability'
@@ -0,0 +1,74 @@
1
+ module Qualys::Asset
2
+ # This class represents each of the ASSET_DATA_REPORT/GLOSSARY/VULN_INFO_LIST/
3
+ # VULN_INFO elements in the Qualys Asset XML document.
4
+ #
5
+ # It provides a convenient way to access the information scattered all over
6
+ # the XML in attributes and nested tags.
7
+ #
8
+ # Instead of providing separate methods for each supported property we rely
9
+ # on Ruby's #method_missing to do most of the work.
10
+ class Evidence
11
+ # Accepts an XML node from Nokogiri::XML.
12
+ def initialize(xml_node)
13
+ @xml = xml_node
14
+ end
15
+
16
+ # List of supported tags. They can be attributes, simple descendans or
17
+ # collections (e.g. <references/>, <tags/>)
18
+ def supported_tags
19
+ [
20
+ # simple tags
21
+ :first_round, :last_round, :result, :ssl, :times_found,
22
+ :type, :vuln_status,
23
+
24
+ :cvss_base, :cvss3_final
25
+ ]
26
+ end
27
+
28
+ # This allows external callers (and specs) to check for implemented
29
+ # properties
30
+ def respond_to?(method, include_private=false)
31
+ return true if supported_tags.include?(method.to_sym)
32
+ super
33
+ end
34
+
35
+ # This method is invoked by Ruby when a method that is not defined in this
36
+ # instance is called.
37
+ #
38
+ # In our case we inspect the @method@ parameter and try to find the
39
+ # attribute, simple descendent or collection that it maps to in the XML
40
+ # tree.
41
+ def method_missing(method, *args)
42
+ # We could remove this check and return nil for any non-recognized tag.
43
+ # The problem would be that it would make tricky to debug problems with
44
+ # typos. For instance: <>.potr would return nil instead of raising an
45
+ # exception
46
+ unless supported_tags.include?(method)
47
+ super
48
+ return
49
+ end
50
+
51
+ process_field_value(method.to_s)
52
+ end
53
+
54
+ def process_field_value(method)
55
+ tag = @xml.at_xpath("./#{method.upcase}")
56
+
57
+ if tag && !tag.text.blank?
58
+ if tags_with_html_content.include?(method)
59
+ Qualys.cleanup_html(tag.text)
60
+ else
61
+ tag.text
62
+ end
63
+ else
64
+ 'n/a'
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def tags_with_html_content
71
+ %w[result]
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,87 @@
1
+ module Qualys::Asset
2
+ # This class represents each of the ASSET_DATA_REPORT/GLOSSARY/VULN_DETAILS_LIST/
3
+ # VULN_DETAILS elements in the Qualys Asset XML document.
4
+ #
5
+ # It provides a convenient way to access the information scattered all over
6
+ # the XML in attributes and nested tags.
7
+ #
8
+ # Instead of providing separate methods for each supported property we rely
9
+ # on Ruby's #method_missing to do most of the work.
10
+ class Vulnerability
11
+ # Accepts an XML node from Nokogiri::XML.
12
+ def initialize(xml_node)
13
+ @xml = xml_node
14
+ end
15
+
16
+ # List of supported tags. They can be attributes, simple descendans or
17
+ # collections (e.g. <references/>, <tags/>)
18
+ def supported_tags
19
+ [
20
+ # simple tags
21
+ :category, :impact, :last_update, :pci_flag, :qid, :result,
22
+ :severity, :solution, :threat, :title,
23
+
24
+ :cvss3_base, :cvss3_temporal, :cvss_base, :cvss_temporal
25
+ ]
26
+ end
27
+
28
+ # This allows external callers (and specs) to check for implemented
29
+ # properties
30
+ def respond_to?(method, include_private=false)
31
+ return true if supported_tags.include?(method.to_sym)
32
+ super
33
+ end
34
+
35
+ # This method is invoked by Ruby when a method that is not defined in this
36
+ # instance is called.
37
+ #
38
+ # In our case we inspect the @method@ parameter and try to find the
39
+ # attribute, simple descendent or collection that it maps to in the XML
40
+ # tree.
41
+ def method_missing(method, *args)
42
+ # We could remove this check and return nil for any non-recognized tag.
43
+ # The problem would be that it would make tricky to debug problems with
44
+ # typos. For instance: <>.potr would return nil instead of raising an
45
+ # exception
46
+ unless supported_tags.include?(method)
47
+ super
48
+ return
49
+ end
50
+
51
+ process_field_value(method.to_s)
52
+ end
53
+
54
+ def process_field_value(method)
55
+ tag = @xml.at_xpath("./#{method.upcase}")
56
+
57
+ if method.starts_with?('cvss')
58
+ process_cvss_field(method)
59
+ elsif tag && !tag.text.blank?
60
+ if tags_with_html_content.include?(method)
61
+ Qualys.cleanup_html(tag.text)
62
+ else
63
+ tag.text
64
+ end
65
+ else
66
+ 'n/a'
67
+ end
68
+ end
69
+
70
+ def process_cvss_field(method)
71
+ translations_table = {
72
+ cvss_base: 'CVSS_SCORE/CVSS_BASE',
73
+ cvss_temporal: 'CVSS_SCORE/CVSS_TEMPORAL',
74
+ cvss3_base: 'CVSS3_SCORE/CVSS3_BASE',
75
+ cvss3_temporal: 'CVSS3_SCORE/CVSS3_TEMPORAL'
76
+ }
77
+
78
+ @xml.xpath("./#{translations_table[method.to_sym]}").text
79
+ end
80
+
81
+ private
82
+
83
+ def tags_with_html_content
84
+ %w[threat impact solution]
85
+ end
86
+ end
87
+ end