dradis-openvas 3.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +3 -0
- data/CONTRIBUTING.md +3 -0
- data/Gemfile +23 -0
- data/Guardfile +8 -0
- data/LICENSE +339 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/dradis-openvas.gemspec +32 -0
- data/lib/dradis-openvas.rb +8 -0
- data/lib/dradis/plugins/openvas.rb +11 -0
- data/lib/dradis/plugins/openvas/engine.rb +9 -0
- data/lib/dradis/plugins/openvas/field_processor.rb +42 -0
- data/lib/dradis/plugins/openvas/gem_version.rb +19 -0
- data/lib/dradis/plugins/openvas/importer.rb +101 -0
- data/lib/dradis/plugins/openvas/version.rb +13 -0
- data/lib/openvas/result.rb +163 -0
- data/lib/openvas/v6/result.rb +12 -0
- data/lib/openvas/v7/result.rb +61 -0
- data/lib/tasks/thorfile.rb +26 -0
- data/spec/fixtures/files/result.xml +48 -0
- data/spec/fixtures/files/result2.xml +68 -0
- data/spec/fixtures/files/v7/report_v7.xml +427 -0
- data/spec/openvas/result_spec.rb +35 -0
- data/spec/spec_helper.rb +35 -0
- data/spec/support/fixture_loader.rb +5 -0
- data/templates/result.fields +18 -0
- data/templates/result.sample +48 -0
- data/templates/result.template +31 -0
- metadata +123 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
module Dradis::Plugins::OpenVAS
|
2
|
+
|
3
|
+
# This processor defers to ::OpenVAS::Result class to extract all the
|
4
|
+
# relevant information exposed through the :result template.
|
5
|
+
class FieldProcessor < Dradis::Plugins::Upload::FieldProcessor
|
6
|
+
|
7
|
+
def post_initialize(args={})
|
8
|
+
|
9
|
+
# Figure out if v6 or v7
|
10
|
+
@openvas_object = case detect_version(data)
|
11
|
+
when :v6
|
12
|
+
::OpenVAS::V6::Result.new(data)
|
13
|
+
when :v7
|
14
|
+
::OpenVAS::V7::Result.new(data)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def value(args={})
|
19
|
+
field = args[:field]
|
20
|
+
|
21
|
+
# fields in the template are of the form <foo>.<field>, where <foo>
|
22
|
+
# is common across all fields for a given template (and meaningless).
|
23
|
+
_, name = field.split('.')
|
24
|
+
|
25
|
+
@openvas_object.try(name) || 'n/a'
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
# There is no clear-cut way to determine the version of the file from the
|
30
|
+
# contents (see thread below) so we have to do some guess work.
|
31
|
+
#
|
32
|
+
# See:
|
33
|
+
# http://lists.wald.intevation.org/pipermail/openvas-discuss/2014-October/006907.html
|
34
|
+
# http://lists.wald.intevation.org/pipermail/openvas-discuss/2014-November/007092.html
|
35
|
+
def detect_version(xml_data)
|
36
|
+
# Gross over simplification. May need to smarten in the future.
|
37
|
+
xml_data.at_xpath('./description[contains(text(), "Summary:")]') ? :v6 : :v7
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Dradis
|
2
|
+
module Plugins
|
3
|
+
module OpenVAS
|
4
|
+
# Returns the version of the currently loaded OpenVAS as a <tt>Gem::Version</tt>
|
5
|
+
def self.gem_version
|
6
|
+
Gem::Version.new VERSION::STRING
|
7
|
+
end
|
8
|
+
|
9
|
+
module VERSION
|
10
|
+
MAJOR = 3
|
11
|
+
MINOR = 6
|
12
|
+
TINY = 0
|
13
|
+
PRE = nil
|
14
|
+
|
15
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Dradis::Plugins::OpenVAS
|
2
|
+
class Importer < Dradis::Plugins::Upload::Importer
|
3
|
+
|
4
|
+
# The framework will call this function if the user selects this plugin from
|
5
|
+
# the dropdown list and uploads a file.
|
6
|
+
# @returns true if the operation was successful, false otherwise
|
7
|
+
def import(params={})
|
8
|
+
file_content = File.read( params[:file] )
|
9
|
+
|
10
|
+
# Parse contents
|
11
|
+
logger.info{'Parsing OpenVAS output file...'}
|
12
|
+
@doc = Nokogiri::XML(file_content)
|
13
|
+
logger.info{'Done'}
|
14
|
+
|
15
|
+
|
16
|
+
# Detect valid OpenVAS XML
|
17
|
+
if @doc.xpath('/report').empty?
|
18
|
+
error = "No report results were detected in the uploaded file (/report). Ensure you uploaded an OpenVAS XML report."
|
19
|
+
logger.fatal{ error }
|
20
|
+
content_service.create_note text: error
|
21
|
+
return false
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
@doc.xpath('/report/report/results/result').each do |xml_result|
|
26
|
+
process_result(xml_result)
|
27
|
+
end
|
28
|
+
|
29
|
+
logger.info{ "Report processed." }
|
30
|
+
return true
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
attr_accessor :host_node
|
35
|
+
|
36
|
+
def process_result(xml_result)
|
37
|
+
# Extract host
|
38
|
+
host_label = xml_result.at_xpath('./host').text()
|
39
|
+
self.host_node = content_service.create_node(label: host_label, type: :host)
|
40
|
+
|
41
|
+
# Uniquely identify this issue
|
42
|
+
nvt_oid = xml_result.at_xpath('./nvt')[:oid]
|
43
|
+
|
44
|
+
logger.info{ "\t\t => Creating new issue (#{nvt_oid})" }
|
45
|
+
|
46
|
+
issue_text = template_service.process_template(template: 'result', data: xml_result)
|
47
|
+
issue = content_service.create_issue(text: issue_text, id: nvt_oid)
|
48
|
+
|
49
|
+
|
50
|
+
# Add evidence. It doesn't look like OpenVAS provides much in terms of
|
51
|
+
# instance-specific evidence though.
|
52
|
+
logger.info{ "\t\t => Adding reference to this host" }
|
53
|
+
|
54
|
+
port_info = xml_result.at_xpath('./port').text
|
55
|
+
evidence_content = "\n#[Port]#\n#{port_info}\n\n"
|
56
|
+
|
57
|
+
# There is no way of knowing where OpenVAS is going to place the evidence
|
58
|
+
# for each issue. For example:
|
59
|
+
#
|
60
|
+
# A) 1.3.6.1.4.1.25623.1.0.900498 - 'Apache Web ServerVersion Detection'
|
61
|
+
# uses the full <description> field:
|
62
|
+
#
|
63
|
+
# <description>Detected Apache Tomcat version: 2.2.22
|
64
|
+
# Location: 80/tcp
|
65
|
+
# CPE: cpe:/a:apache:http_server:2.2.22
|
66
|
+
#
|
67
|
+
# Concluded from version identification result:
|
68
|
+
# Server: Apache/2.2.22
|
69
|
+
# </description>
|
70
|
+
#
|
71
|
+
# B) 1.3.6.1.4.1.25623.1.0.103122 - 'Apache Web Server ETag Header
|
72
|
+
# Information Disclosure Weakness' uses the 'Information that was gathered'
|
73
|
+
# meta-field inside <description>
|
74
|
+
#
|
75
|
+
# <description>
|
76
|
+
# Summary:
|
77
|
+
# A weakness has been discovered in Apache web servers that are
|
78
|
+
# configured to use the FileETag directive. Due to the way in which
|
79
|
+
# [...]
|
80
|
+
#
|
81
|
+
# Solution:
|
82
|
+
# OpenBSD has released a patch to address this issue.
|
83
|
+
# [...]
|
84
|
+
#
|
85
|
+
# Information that was gathered:
|
86
|
+
# Inode: 5753015
|
87
|
+
# Size: 604
|
88
|
+
# </description>
|
89
|
+
#
|
90
|
+
# C) 1.3.6.1.4.1.25623.1.0.10766 - 'Apache UserDir Sensitive Information Disclosure'
|
91
|
+
# doesn't provide any per-instance information.
|
92
|
+
#
|
93
|
+
# Best thing to do is to include the full <description> field and let the user deal with it.
|
94
|
+
description = xml_result.at_xpath('./description').text()
|
95
|
+
evidence_content << "\n#[Description]#\n#{description}\n\n"
|
96
|
+
|
97
|
+
content_service.create_evidence(issue: issue, node: host_node, content: evidence_content)
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module OpenVAS
|
2
|
+
# This class represents each of the /report/report/results/result elements in
|
3
|
+
# the OpenVAS 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 Result
|
11
|
+
|
12
|
+
# Accepts an XML node from Nokogiri::XML.
|
13
|
+
def initialize(xml_node)
|
14
|
+
@xml = xml_node
|
15
|
+
end
|
16
|
+
|
17
|
+
# List of supported tags. They can be attributes, simple descendans or
|
18
|
+
# collections (e.g. <bid/>, <cve/>, <xref/>)
|
19
|
+
def supported_tags
|
20
|
+
[
|
21
|
+
# attributes
|
22
|
+
# NONE
|
23
|
+
|
24
|
+
# simple tags
|
25
|
+
:threat, :description, :original_threat, :notes, :overrides,
|
26
|
+
|
27
|
+
# nested tags
|
28
|
+
:name, :cvss_base, :risk_factor, :cve, :bid, :xref,
|
29
|
+
|
30
|
+
# fields inside :description
|
31
|
+
:summary, :info_gathered, :insight, :impact, :impact_level, :affected_software, :solution
|
32
|
+
]
|
33
|
+
end
|
34
|
+
|
35
|
+
# This allows external callers (and specs) to check for implemented
|
36
|
+
# properties
|
37
|
+
def respond_to?(method, include_private=false)
|
38
|
+
return true if supported_tags.include?(method.to_sym)
|
39
|
+
super
|
40
|
+
end
|
41
|
+
|
42
|
+
# This method is invoked by Ruby when a method that is not defined in this
|
43
|
+
# instance is called.
|
44
|
+
#
|
45
|
+
# In our case we inspect the @method@ parameter and try to find the
|
46
|
+
# attribute, simple descendent or collection that it maps to in the XML
|
47
|
+
# tree.
|
48
|
+
def method_missing(method, *args)
|
49
|
+
|
50
|
+
# We could remove this check and return nil for any non-recognized tag.
|
51
|
+
# The problem would be that it would make tricky to debug problems with
|
52
|
+
# typos. For instance: <>.potr would return nil instead of raising an
|
53
|
+
# exception
|
54
|
+
unless supported_tags.include?(method)
|
55
|
+
super
|
56
|
+
return
|
57
|
+
end
|
58
|
+
|
59
|
+
# first we try the attributes: port, svc_name, protocol, severity,
|
60
|
+
# plugin_id, plugin_name, plugin_family
|
61
|
+
# translations_table = {
|
62
|
+
# # @port = xml.attributes["port"]
|
63
|
+
# # @svc_name = xml.attributes["svc_name"]
|
64
|
+
# # @protocol = xml.attributes["protocol"]
|
65
|
+
# # @severity = xml.attributes["severity"]
|
66
|
+
# :plugin_id => 'pluginID',
|
67
|
+
# :plugin_name => 'pluginName',
|
68
|
+
# :plugin_family => 'pluginFamily'
|
69
|
+
# }
|
70
|
+
# method_name = translations_table.fetch(method, method.to_s)
|
71
|
+
# return @xml.attributes[method_name].value if @xml.attributes.key?(method_name)
|
72
|
+
method_name = method.to_s
|
73
|
+
|
74
|
+
|
75
|
+
# first we try the children tags: :threat, :description, :original_threat,
|
76
|
+
# :notes, :overrides
|
77
|
+
tag = @xml.at_xpath("./#{method_name}")
|
78
|
+
if tag
|
79
|
+
return tag.text
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
# nested tags: :name, :cvss_base, :risk_factor, :cve, :bid, :xref,
|
84
|
+
tag = @xml.at_xpath("./nvt/#{method_name}")
|
85
|
+
if tag
|
86
|
+
return tag.text
|
87
|
+
end
|
88
|
+
|
89
|
+
# fields inside :description
|
90
|
+
if description_fields.key?(method)
|
91
|
+
return description_fields[method]
|
92
|
+
end
|
93
|
+
|
94
|
+
# nothing found, the tag is valid but not present in this ReportItem
|
95
|
+
return nil
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
# This method parses the <description> tag of the <result> entry and
|
100
|
+
# extracts the available fields, in a similar way to what Note#fields
|
101
|
+
# does.
|
102
|
+
def description_fields
|
103
|
+
if @description_fields.nil?
|
104
|
+
delimiters = {
|
105
|
+
'Affected Software/OS:' => :affected_software,
|
106
|
+
'Impact:' => :impact,
|
107
|
+
'Impact Level:' => :impact_level,
|
108
|
+
'Information that was gathered:' => :info_gathered,
|
109
|
+
'Solution:' => :solution,
|
110
|
+
'Summary:' => :summary,
|
111
|
+
'Vulnerability Insight:' => :insight
|
112
|
+
}
|
113
|
+
|
114
|
+
@description_fields = {}
|
115
|
+
current_field = nil
|
116
|
+
buffer = ''
|
117
|
+
clean_line = nil
|
118
|
+
|
119
|
+
@xml.at_xpath('./description').text().each_line do |line|
|
120
|
+
clean_line = line.lstrip
|
121
|
+
if clean_line.empty?
|
122
|
+
buffer << "\n"
|
123
|
+
next
|
124
|
+
end
|
125
|
+
|
126
|
+
# For some reason Impact Level: is followed by the content instead of a new
|
127
|
+
# line like the other fields
|
128
|
+
if clean_line =~ /Impact Level: (.*)/
|
129
|
+
@description_fields[:impact_level] = $1
|
130
|
+
|
131
|
+
# we terminate the previous field and unassign :current_field
|
132
|
+
if current_field
|
133
|
+
@description_fields[delimiters[current_field]] = buffer
|
134
|
+
current_field = nil
|
135
|
+
buffer = ''
|
136
|
+
end
|
137
|
+
next
|
138
|
+
end
|
139
|
+
|
140
|
+
if delimiters.key?(clean_line.rstrip)
|
141
|
+
if current_field
|
142
|
+
# we need the conditional for the 1st iteration
|
143
|
+
@description_fields[delimiters[current_field]] = buffer
|
144
|
+
end
|
145
|
+
current_field = clean_line.rstrip
|
146
|
+
buffer = ''
|
147
|
+
next
|
148
|
+
end
|
149
|
+
|
150
|
+
buffer << clean_line
|
151
|
+
end
|
152
|
+
# wrap up the last field whose contents are already in the buffer.
|
153
|
+
@description_fields[delimiters[current_field]] = buffer
|
154
|
+
end
|
155
|
+
|
156
|
+
@description_fields
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
|
162
|
+
require 'openvas/v6/result'
|
163
|
+
require 'openvas/v7/result'
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module OpenVAS::V6
|
2
|
+
|
3
|
+
# The format is given by the OMPv4 :get_reports call (OpenVASv6 uses OMPv4).
|
4
|
+
#
|
5
|
+
# ::OpenVAS::Result already implements v6, so basically there is nothing to
|
6
|
+
# overwrite here. Great OO-design? Probably not!
|
7
|
+
#
|
8
|
+
# See:
|
9
|
+
# http://www.openvas.org/omp-4-0.html#command_get_reports
|
10
|
+
class Result < ::OpenVAS::Result
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module OpenVAS::V7
|
2
|
+
|
3
|
+
# The format is given by the OMPv5 :get_reports call (OpenVASv7 uses OMPv5).
|
4
|
+
#
|
5
|
+
# See:
|
6
|
+
# http://www.openvas.org/omp-5-0.html#command_get_reports
|
7
|
+
class Result < ::OpenVAS::Result
|
8
|
+
|
9
|
+
|
10
|
+
private
|
11
|
+
# This method parses the <tags> tag of the <result> entry and extracts the
|
12
|
+
# available fields. Previous versions of the format (e.g. OpenVAS v6)
|
13
|
+
# included these embedded fields in the <description> tag instead of the
|
14
|
+
# <tags> tag, hence the not-so-intuitive name.
|
15
|
+
def description_fields
|
16
|
+
if @tag_fields.nil?
|
17
|
+
delimiters = {
|
18
|
+
# Not supported via .fields
|
19
|
+
# 'cvss_base_vector='
|
20
|
+
'impact=' => :impact,
|
21
|
+
|
22
|
+
# Not supported via .fields
|
23
|
+
# 'vuldetect='
|
24
|
+
'insight=' => :insight,
|
25
|
+
'solution=' => :solution,
|
26
|
+
'summary=' => :summary,
|
27
|
+
'affected=' => :affected_software
|
28
|
+
|
29
|
+
# Missing fields, these used to be available under <description> but it
|
30
|
+
# doesn't look like they are under <tags>
|
31
|
+
# 'Impact Level:' => :impact_level,
|
32
|
+
# 'Information that was gathered:' => :info_gathered,
|
33
|
+
}
|
34
|
+
|
35
|
+
@tag_fields = {}
|
36
|
+
current_field = nil
|
37
|
+
buffer = ''
|
38
|
+
clean_line = nil
|
39
|
+
|
40
|
+
@xml.at_xpath('./nvt/tags').text().split('|').each do |tag_line|
|
41
|
+
clean_line = tag_line.lstrip
|
42
|
+
|
43
|
+
if clean_line.empty?
|
44
|
+
buffer << "\n"
|
45
|
+
next
|
46
|
+
end
|
47
|
+
|
48
|
+
delimiters.keys.each do |tag_name|
|
49
|
+
if tag_line.starts_with?(tag_name)
|
50
|
+
@tag_fields[delimiters[tag_name]] = clean_line[tag_name.length..-1]
|
51
|
+
next
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
@tag_fields
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|