dradis-nessus 3.18.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 +7 -0
  2. data/.github/issue_template.md +16 -0
  3. data/.github/pull_request_template.md +36 -0
  4. data/.gitignore +9 -0
  5. data/.rspec +2 -0
  6. data/CHANGELOG.md +56 -0
  7. data/CONTRIBUTING.md +3 -0
  8. data/Gemfile +23 -0
  9. data/LICENSE +339 -0
  10. data/README.md +31 -0
  11. data/Rakefile +2 -0
  12. data/dradis-nessus.gemspec +35 -0
  13. data/lib/dradis-nessus.rb +9 -0
  14. data/lib/dradis/plugins/nessus.rb +11 -0
  15. data/lib/dradis/plugins/nessus/engine.rb +13 -0
  16. data/lib/dradis/plugins/nessus/field_processor.rb +55 -0
  17. data/lib/dradis/plugins/nessus/gem_version.rb +19 -0
  18. data/lib/dradis/plugins/nessus/importer.rb +177 -0
  19. data/lib/dradis/plugins/nessus/version.rb +13 -0
  20. data/lib/nessus/host.rb +82 -0
  21. data/lib/nessus/report_item.rb +118 -0
  22. data/lib/tasks/thorfile.rb +21 -0
  23. data/spec/dradis/plugins/nessus/field_processor_spec.rb +41 -0
  24. data/spec/dradis/plugins/nessus/importer_spec.rb +55 -0
  25. data/spec/fixtures/files/example_v2.nessus +2076 -0
  26. data/spec/fixtures/files/host-01.xml +18 -0
  27. data/spec/fixtures/files/report_item-with-list.xml +45 -0
  28. data/spec/nessus/host_spec.rb +29 -0
  29. data/spec/spec_helper.rb +10 -0
  30. data/templates/evidence.fields +17 -0
  31. data/templates/evidence.sample +53 -0
  32. data/templates/evidence.template +5 -0
  33. data/templates/report_host.fields +8 -0
  34. data/templates/report_host.sample +12 -0
  35. data/templates/report_host.template +14 -0
  36. data/templates/report_item.fields +33 -0
  37. data/templates/report_item.sample +43 -0
  38. data/templates/report_item.template +20 -0
  39. metadata +172 -0
@@ -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.
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -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,9 @@
1
+ require 'dradis-plugins'
2
+
3
+ # Require engine
4
+ require 'dradis/plugins/nessus'
5
+
6
+
7
+ # Require supporting Nessus library
8
+ require 'nessus/host'
9
+ require 'nessus/report_item'
@@ -0,0 +1,11 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Nessus
4
+ end
5
+ end
6
+ end
7
+
8
+ require 'dradis/plugins/nessus/engine'
9
+ require 'dradis/plugins/nessus/field_processor'
10
+ require 'dradis/plugins/nessus/importer'
11
+ require 'dradis/plugins/nessus/version'
@@ -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
@@ -0,0 +1,13 @@
1
+ require_relative 'gem_version'
2
+
3
+ module Dradis
4
+ module Plugins
5
+ module Nessus
6
+ # Returns the version of the currently loaded Nessus as a
7
+ # <tt>Gem::Version</tt>.
8
+ def self.version
9
+ gem_version
10
+ end
11
+ end
12
+ end
13
+ end
@@ -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