dradis-nessus 3.18.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/issue_template.md +16 -0
- data/.github/pull_request_template.md +36 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +56 -0
- data/CONTRIBUTING.md +3 -0
- data/Gemfile +23 -0
- data/LICENSE +339 -0
- data/README.md +31 -0
- data/Rakefile +2 -0
- data/dradis-nessus.gemspec +35 -0
- data/lib/dradis-nessus.rb +9 -0
- data/lib/dradis/plugins/nessus.rb +11 -0
- data/lib/dradis/plugins/nessus/engine.rb +13 -0
- data/lib/dradis/plugins/nessus/field_processor.rb +55 -0
- data/lib/dradis/plugins/nessus/gem_version.rb +19 -0
- data/lib/dradis/plugins/nessus/importer.rb +177 -0
- data/lib/dradis/plugins/nessus/version.rb +13 -0
- data/lib/nessus/host.rb +82 -0
- data/lib/nessus/report_item.rb +118 -0
- data/lib/tasks/thorfile.rb +21 -0
- data/spec/dradis/plugins/nessus/field_processor_spec.rb +41 -0
- data/spec/dradis/plugins/nessus/importer_spec.rb +55 -0
- data/spec/fixtures/files/example_v2.nessus +2076 -0
- data/spec/fixtures/files/host-01.xml +18 -0
- data/spec/fixtures/files/report_item-with-list.xml +45 -0
- data/spec/nessus/host_spec.rb +29 -0
- data/spec/spec_helper.rb +10 -0
- data/templates/evidence.fields +17 -0
- data/templates/evidence.sample +53 -0
- data/templates/evidence.template +5 -0
- data/templates/report_host.fields +8 -0
- data/templates/report_host.sample +12 -0
- data/templates/report_host.template +14 -0
- data/templates/report_item.fields +33 -0
- data/templates/report_item.sample +43 -0
- data/templates/report_item.template +20 -0
- metadata +172 -0
data/README.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Nessus add-on for Dradis
|
2
|
+
|
3
|
+
[![Build Status](https://secure.travis-ci.org/dradis/dradis-nessus.png?branch=master)](http://travis-ci.org/dradis/dradis-nessus) [![Code Climate](https://codeclimate.com/github/dradis/dradis-nessus.png)](https://codeclimate.com/github/dradis/dradis-nessus.png)
|
4
|
+
|
5
|
+
The Nessus upload add-on will enable user to upload Nessus output files in the nessus client format (.nessus) to create a structure of nodes/notes that contain the same information about the hosts/ports/services as the original file.
|
6
|
+
|
7
|
+
The parser only supports version 2 of nessus xml format. Other formats (nbe, nsr) are not supported at the moment.
|
8
|
+
|
9
|
+
Also, the xml parser only extracts the results of a scan. It is not able to parse the scan policy itself which is also part of the xml file.
|
10
|
+
|
11
|
+
The add-on requires Dradis 3.0 or higher.
|
12
|
+
|
13
|
+
|
14
|
+
## More information
|
15
|
+
|
16
|
+
See the Dradis Framework's [README.md](https://github.com/dradis/dradisframework/blob/master/README.md)
|
17
|
+
|
18
|
+
|
19
|
+
## Contributing
|
20
|
+
|
21
|
+
See the Dradis Framework's [CONTRIBUTING.md](https://github.com/dradis/dradisframework/blob/master/CONTRIBUTING.md)
|
22
|
+
|
23
|
+
|
24
|
+
## License
|
25
|
+
|
26
|
+
Dradis Framework and all its components are released under [GNU General Public License version 2.0](http://www.gnu.org/licenses/old-licenses/gpl-2.0.html) as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.
|
27
|
+
|
28
|
+
|
29
|
+
## Feature requests and bugs
|
30
|
+
|
31
|
+
Please use the [Dradis Framework issue tracker](https://github.com/dradis/dradis-ce/issues) for add-on improvements and bug reports.
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
$:.push File.expand_path('../lib', __FILE__)
|
2
|
+
require 'dradis/plugins/nessus/version'
|
3
|
+
version = Dradis::Plugins::Nessus::VERSION::STRING
|
4
|
+
|
5
|
+
|
6
|
+
# Describe your gem and declare its dependencies:
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.platform = Gem::Platform::RUBY
|
9
|
+
spec.name = 'dradis-nessus'
|
10
|
+
spec.version = version
|
11
|
+
spec.summary = 'Nessus upload add-on for the Dradis Framework.'
|
12
|
+
spec.description = 'This add-on allows you to upload and parse output produced from Tenable\'s Nessus Scanner into Dradis.'
|
13
|
+
|
14
|
+
spec.license = 'GPL-2'
|
15
|
+
|
16
|
+
spec.authors = ['Daniel Martin']
|
17
|
+
spec.email = ['etd@nomejortu.com']
|
18
|
+
spec.homepage = 'http://dradisframework.org'
|
19
|
+
|
20
|
+
spec.files = `git ls-files`.split($\)
|
21
|
+
spec.executables = spec.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
22
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
23
|
+
|
24
|
+
# By not including Rails as a dependency, we can use the gem with different
|
25
|
+
# versions of Rails (a sure recipe for disaster, I'm sure), which is needed
|
26
|
+
# until we bump Dradis Pro to 4.1.
|
27
|
+
# s.add_dependency 'rails', '~> 4.1.1'
|
28
|
+
spec.add_dependency 'dradis-plugins', '~> 3.6'
|
29
|
+
spec.add_dependency 'nokogiri'
|
30
|
+
|
31
|
+
spec.add_development_dependency 'bundler', '~> 1.6'
|
32
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
33
|
+
spec.add_development_dependency 'rspec-rails'
|
34
|
+
spec.add_development_dependency 'combustion', '~> 0.5.2'
|
35
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Dradis
|
2
|
+
module Plugins
|
3
|
+
module Nessus
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
isolate_namespace Dradis::Plugins::Nessus
|
6
|
+
|
7
|
+
include ::Dradis::Plugins::Base
|
8
|
+
description 'Processes Nessus XML v2 format (.nessus)'
|
9
|
+
provides :upload
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Dradis
|
2
|
+
module Plugins
|
3
|
+
module Nessus
|
4
|
+
|
5
|
+
class FieldProcessor < Dradis::Plugins::Upload::FieldProcessor
|
6
|
+
|
7
|
+
def post_initialize(args={})
|
8
|
+
@nessus_object = (data.name == 'ReportHost') ? ::Nessus::Host.new(data) : ::Nessus::ReportItem.new(data)
|
9
|
+
end
|
10
|
+
|
11
|
+
def value(args={})
|
12
|
+
field = args[:field]
|
13
|
+
|
14
|
+
# fields in the template are of the form <foo>.<field>, where <foo>
|
15
|
+
# is common across all fields for a given template (and meaningless).
|
16
|
+
_, name = field.split('.')
|
17
|
+
|
18
|
+
if name.end_with?('entries')
|
19
|
+
# report_item.bid_entries
|
20
|
+
# report_item.cve_entries
|
21
|
+
# report_item.xref_entries
|
22
|
+
entries = @nessus_object.try(name)
|
23
|
+
if entries.any?
|
24
|
+
entries.to_a.join("\n")
|
25
|
+
else
|
26
|
+
'n/a'
|
27
|
+
end
|
28
|
+
else
|
29
|
+
output = @nessus_object.try(name) || 'n/a'
|
30
|
+
|
31
|
+
if field == 'report_item.description' && output =~ /^\s+-/
|
32
|
+
format_bullet_point_lists(output)
|
33
|
+
else
|
34
|
+
output
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def format_bullet_point_lists(input)
|
41
|
+
input.split("\n").map do |paragraph|
|
42
|
+
if paragraph =~ /(.*)\s+:\s*$/m
|
43
|
+
$1 + ':'
|
44
|
+
elsif paragraph =~ /^\s+-\s+(.*)$/m
|
45
|
+
'* ' + $1.gsub(/\s{3,}/, ' ').gsub(/\n/, ' ')
|
46
|
+
else
|
47
|
+
paragraph
|
48
|
+
end
|
49
|
+
end.join("\n")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Dradis
|
2
|
+
module Plugins
|
3
|
+
module Nessus
|
4
|
+
# Returns the version of the currently loaded Nessus 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 = 18
|
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,177 @@
|
|
1
|
+
module Dradis::Plugins::Nessus
|
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
|
+
logger.info{'Parsing nessus output file...'}
|
11
|
+
doc = Nokogiri::XML( file_content )
|
12
|
+
logger.info{'Done.'}
|
13
|
+
|
14
|
+
if doc.xpath('/NessusClientData_v2/Report').empty?
|
15
|
+
error = "No reports were detected in the uploaded file (/NessusClientData_v2/Report). Ensure you uploaded a Nessus XML v2 (.nessus) report."
|
16
|
+
logger.fatal{ error }
|
17
|
+
content_service.create_note text: error
|
18
|
+
return false
|
19
|
+
end
|
20
|
+
|
21
|
+
doc.xpath('/NessusClientData_v2/Report').each do |xml_report|
|
22
|
+
report_label = xml_report.attributes['name'].value
|
23
|
+
logger.info{ "Processing report: #{report_label}" }
|
24
|
+
# No need to create a report node for each report. It may be good to
|
25
|
+
# create a plugin.output/nessus.reports with info for each scan, but
|
26
|
+
# for the time being we just append stuff to the Host
|
27
|
+
# report_node = parent.children.find_or_create_by_label(report_label)
|
28
|
+
|
29
|
+
xml_report.xpath('./ReportHost').each do |xml_host|
|
30
|
+
process_report_host(xml_host)
|
31
|
+
end #/ReportHost
|
32
|
+
logger.info{ "Report processed." }
|
33
|
+
end #/Report
|
34
|
+
|
35
|
+
return true
|
36
|
+
end # /import
|
37
|
+
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# Internal: Parses the specific "Nessus SYN Scanner" and similar plugin into
|
42
|
+
# Dradis node properties.
|
43
|
+
#
|
44
|
+
# host_node - The Dradis Node that represents the host in the project.
|
45
|
+
# xml_report_item - The Nokogiri XML node representing the Service Detection
|
46
|
+
# <ReportItem> tag.
|
47
|
+
#
|
48
|
+
# Returns nothing.
|
49
|
+
#
|
50
|
+
# Plugins processed using this method:
|
51
|
+
# - [11219] Nessus SYN Scanner
|
52
|
+
# - [34220] Netstat Portscanner (WMI)
|
53
|
+
def process_nessus_syn_scanner(host_node, xml_report_item)
|
54
|
+
process_service(
|
55
|
+
host_node,
|
56
|
+
xml_report_item,
|
57
|
+
{ 'syn-scanner' => xml_report_item.at_xpath('./plugin_output').try(:text)}
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Internal: Process each /NessusClientData_v2/Report/ReportHost creating a
|
62
|
+
# Dradis node and adding some properties to it (:ip, :os, etc.).
|
63
|
+
#
|
64
|
+
# xml_host - The Nokogiri XML node representing the parent host for
|
65
|
+
# this issue.
|
66
|
+
#
|
67
|
+
# Returns nothing.
|
68
|
+
#
|
69
|
+
def process_report_host(xml_host)
|
70
|
+
|
71
|
+
# 1. Create host node
|
72
|
+
host_label = xml_host.attributes['name'].value
|
73
|
+
host_label += " (#{xml_host.attributes['fqdn'].value})" if xml_host.attributes['fqdn']
|
74
|
+
|
75
|
+
host_node = content_service.create_node(label: host_label, type: :host)
|
76
|
+
logger.info{ "\tHost: #{host_label}" }
|
77
|
+
|
78
|
+
# 2. Add host info note and host properties
|
79
|
+
host_note_text = template_service.process_template(template: 'report_host', data: xml_host)
|
80
|
+
content_service.create_note(text: host_note_text, node: host_node)
|
81
|
+
|
82
|
+
if host_node.respond_to?(:properties)
|
83
|
+
nh = ::Nessus::Host.new(xml_host)
|
84
|
+
host_node.set_property(:fqdn, nh.fqdn) if nh.try(:fqdn)
|
85
|
+
host_node.set_property(:ip, nh.ip) if nh.try(:ip)
|
86
|
+
host_node.set_property(:mac_address, nh.mac_address) if nh.try(:mac_address)
|
87
|
+
host_node.set_property(:netbios_name, nh.netbios_name) if nh.try(:netbios_name)
|
88
|
+
host_node.set_property(:os, nh.operating_system) if nh.try(:operating_system)
|
89
|
+
host_node.save
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
# 3. Add Issue and associated Evidence for this host/port combination
|
94
|
+
xml_host.xpath('./ReportItem').each do |xml_report_item|
|
95
|
+
case xml_report_item.attributes['pluginID'].value
|
96
|
+
when '0'
|
97
|
+
when '11219', '34220' # Nessus SYN scanner, Netstat Portscanner (WMI)
|
98
|
+
process_nessus_syn_scanner(host_node, xml_report_item)
|
99
|
+
when '22964' # Service Detection
|
100
|
+
process_service_detection(host_node, xml_report_item)
|
101
|
+
else
|
102
|
+
process_report_item(xml_host, host_node, xml_report_item)
|
103
|
+
end
|
104
|
+
end #/ReportItem
|
105
|
+
end
|
106
|
+
|
107
|
+
# Internal: Process each /NessusClientData_v2/Report/ReportHost/ReportItem
|
108
|
+
# and creates the corresponding Issue and Evidence in Dradis.
|
109
|
+
#
|
110
|
+
# xml_host - The Nokogiri XML node representing the parent host for
|
111
|
+
# this issue.
|
112
|
+
# host_node - The Dradis Node that represents the host in the project.
|
113
|
+
# xml_report_item - The Nokogiri XML node representing the Service Detection
|
114
|
+
# <ReportItem> tag.
|
115
|
+
#
|
116
|
+
# Returns nothing.
|
117
|
+
#
|
118
|
+
def process_report_item(xml_host, host_node, xml_report_item)
|
119
|
+
# 3.1. Add Issue to the project
|
120
|
+
plugin_id = xml_report_item.attributes['pluginID'].value
|
121
|
+
logger.info{ "\t\t => Creating new issue (plugin_id: #{plugin_id})" }
|
122
|
+
|
123
|
+
issue_text = template_service.process_template(template: 'report_item', data: xml_report_item)
|
124
|
+
|
125
|
+
issue = content_service.create_issue(text: issue_text, id: plugin_id)
|
126
|
+
|
127
|
+
# 3.2. Add Evidence to link the port/protocol and Issue
|
128
|
+
port_info = xml_report_item.attributes['protocol'].value
|
129
|
+
port_info += "/"
|
130
|
+
port_info += xml_report_item.attributes['port'].value
|
131
|
+
|
132
|
+
logger.info{ "\t\t\t => Adding reference to this host" }
|
133
|
+
evidence_content = template_service.process_template(template: 'evidence', data: xml_report_item)
|
134
|
+
|
135
|
+
content_service.create_evidence(issue: issue, node: host_node, content: evidence_content)
|
136
|
+
|
137
|
+
# 3.3. Compliance check information
|
138
|
+
end
|
139
|
+
|
140
|
+
# Internal: Parses the specific "Service Detection" plugin into Dradis node
|
141
|
+
# properties.
|
142
|
+
#
|
143
|
+
# host_node - The Dradis Node that represents the host in the project.
|
144
|
+
# xml_report_item - The Nokogiri XML node representing the Service Detection
|
145
|
+
# <ReportItem> tag.
|
146
|
+
#
|
147
|
+
# Returns nothing.
|
148
|
+
#
|
149
|
+
def process_service_detection(host_node, xml_report_item)
|
150
|
+
output = xml_report_item.at_xpath('./plugin_output').try(:text) || xml_report_item.at_xpath('./description').try(:text)
|
151
|
+
process_service(
|
152
|
+
host_node,
|
153
|
+
xml_report_item,
|
154
|
+
{ 'service-detection' => output }
|
155
|
+
)
|
156
|
+
end
|
157
|
+
|
158
|
+
def process_service(host_node, xml_report_item, service_extra)
|
159
|
+
name = xml_report_item['svc_name']
|
160
|
+
port = xml_report_item['port'].to_i
|
161
|
+
protocol = xml_report_item['protocol']
|
162
|
+
logger.info { "\t\t => Creating new service: #{protocol}/#{port}" }
|
163
|
+
|
164
|
+
host_node.set_service(
|
165
|
+
service_extra.merge({
|
166
|
+
name: name,
|
167
|
+
port: port,
|
168
|
+
protocol: protocol,
|
169
|
+
state: :open,
|
170
|
+
source: :nessus
|
171
|
+
})
|
172
|
+
)
|
173
|
+
|
174
|
+
host_node.save
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
data/lib/nessus/host.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
module Nessus
|
2
|
+
# This class represents each of the /NessusClientData_v2/Report/ReportHost
|
3
|
+
# elements in the Nessus 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 Host
|
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 are all desdendents of the ./HostProperties
|
17
|
+
# node.
|
18
|
+
def supported_tags
|
19
|
+
[
|
20
|
+
# attributes
|
21
|
+
:name,
|
22
|
+
|
23
|
+
# simple tags
|
24
|
+
:ip, :fqdn, :operating_system, :mac_address, :netbios_name,
|
25
|
+
:scan_start_time, :scan_stop_time
|
26
|
+
]
|
27
|
+
end
|
28
|
+
|
29
|
+
# Each of the entries associated with this host. Returns an array of
|
30
|
+
# Nessus::ReportItem objects
|
31
|
+
def report_items
|
32
|
+
@xml.xpath('./ReportItem').collect { |xml_report_item| ReportItem.new(xml_report_item) }
|
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
|
+
# corresponding <tag/> element inside the ./HostProperties child.
|
47
|
+
def method_missing(method, *args)
|
48
|
+
# We could remove this check and return nil for any non-recognized tag.
|
49
|
+
# The problem would be that it would make tricky to debug problems with
|
50
|
+
# typos. For instance: <>.potr would return nil instead of raising an
|
51
|
+
# exception
|
52
|
+
unless supported_tags.include?(method)
|
53
|
+
super
|
54
|
+
return
|
55
|
+
end
|
56
|
+
|
57
|
+
# first we try the attributes: name
|
58
|
+
translations_table = {}
|
59
|
+
method_name = translations_table.fetch(method, method.to_s)
|
60
|
+
return @xml.attributes[method_name].value if @xml.attributes.key?(method_name)
|
61
|
+
|
62
|
+
|
63
|
+
# translation of Host properties
|
64
|
+
translations_table = {
|
65
|
+
ip: 'host-ip',
|
66
|
+
fqdn: 'host-fqdn',
|
67
|
+
operating_system: 'operating-system',
|
68
|
+
mac_address: 'mac-address',
|
69
|
+
netbios_name: 'netbios-name',
|
70
|
+
scan_start_time: 'HOST_START',
|
71
|
+
scan_stop_time: 'HOST_END'
|
72
|
+
}
|
73
|
+
method_name = translations_table.fetch(method, method.to_s)
|
74
|
+
|
75
|
+
if property = @xml.at_xpath("./HostProperties/tag[@name='#{method_name}']")
|
76
|
+
return property.text
|
77
|
+
else
|
78
|
+
return nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|