dradis-nessus 3.18.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 +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