dradis-qualys 4.1.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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