dradis-acunetix 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 +13 -0
  5. data/.rspec +2 -0
  6. data/CHANGELOG.md +63 -0
  7. data/CONTRIBUTING.md +3 -0
  8. data/Gemfile +23 -0
  9. data/LICENSE +339 -0
  10. data/README.md +27 -0
  11. data/Rakefile +2 -0
  12. data/dradis-acunetix.gemspec +35 -0
  13. data/lib/acunetix/report_item.rb +168 -0
  14. data/lib/acunetix/scan.rb +93 -0
  15. data/lib/dradis-acunetix.rb +9 -0
  16. data/lib/dradis/plugins/acunetix.rb +12 -0
  17. data/lib/dradis/plugins/acunetix/engine.rb +9 -0
  18. data/lib/dradis/plugins/acunetix/field_processor.rb +25 -0
  19. data/lib/dradis/plugins/acunetix/gem_version.rb +19 -0
  20. data/lib/dradis/plugins/acunetix/importer.rb +78 -0
  21. data/lib/dradis/plugins/acunetix/version.rb +13 -0
  22. data/lib/tasks/thorfile.rb +20 -0
  23. data/spec/dradis-acunetix_spec.rb +109 -0
  24. data/spec/fixtures/files/code-pre.acunetix.xml +1732 -0
  25. data/spec/fixtures/files/commas-format.acunetix.xml +105 -0
  26. data/spec/fixtures/files/simple.acunetix.xml +1691 -0
  27. data/spec/fixtures/files/testphp.vulnweb.com.export.acunetix.xml +23976 -0
  28. data/spec/models/acunetix/scan_spec.rb +74 -0
  29. data/spec/spec_helper.rb +10 -0
  30. data/templates/evidence.fields +7 -0
  31. data/templates/evidence.sample +12 -0
  32. data/templates/evidence.template +16 -0
  33. data/templates/report_item.fields +18 -0
  34. data/templates/report_item.sample +151 -0
  35. data/templates/report_item.template +59 -0
  36. data/templates/scan.fields +12 -0
  37. data/templates/scan.sample +15 -0
  38. data/templates/scan.template +16 -0
  39. metadata +172 -0
@@ -0,0 +1,27 @@
1
+ # Acunetix add-on for Dradis
2
+
3
+ [![Build Status](https://secure.travis-ci.org/dradis/dradis-acunetix.png?branch=master)](http://travis-ci.org/dradis/dradis-acunetix) [![Code Climate](https://codeclimate.com/github/dradis/dradis-acunetix.png)](https://codeclimate.com/github/dradis/dradis-acunetix.png)
4
+
5
+ The Acunetix add-on enables users to upload Acunexit XML files to create a structure of nodes/notes that contain the same information about the hosts/ports/services as the original file.
6
+
7
+ The add-on requires [Dradis CE](https://dradisframework.org/) > 3.0, or [Dradis Pro](https://dradisframework.com/pro/).
8
+
9
+
10
+ ## More information
11
+
12
+ See the Dradis Framework's [README.md](https://github.com/dradis/dradisframework/blob/master/README.md)
13
+
14
+
15
+ ## Contributing
16
+
17
+ See the Dradis Framework's [CONTRIBUTING.md](https://github.com/dradis/dradisframework/blob/master/CONTRIBUTING.md)
18
+
19
+
20
+ ## License
21
+
22
+ 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.
23
+
24
+
25
+ ## Feature requests and bugs
26
+
27
+ 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/acunetix/version'
3
+ version = Dradis::Plugins::Acunetix::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-acunetix'
10
+ spec.version = version
11
+ spec.summary = 'Acunetix add-on for the Dradis Framework.'
12
+ spec.description = 'This add-on allows you to upload and parse output produced from Acunetix Web Vulnerability 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', '~> 1.3'
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,168 @@
1
+ module Acunetix
2
+ # This class represents each of the /ScanGroup/Scan/ReportItems/ReportItem
3
+ # elements in the Acunetix 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 ReportItem
11
+ attr_accessor :xml
12
+
13
+ # Accepts an XML node from Nokogiri::XML.
14
+ def initialize(xml_node)
15
+ @xml = xml_node
16
+ end
17
+
18
+ # List of supported tags. They can be attributes, simple descendans or
19
+ # collections.
20
+ def supported_tags
21
+ [
22
+ # attributes
23
+ # :color
24
+
25
+ # simple tags
26
+ :name, :module_name, :severity, :type, :impact, :description,
27
+ :detailed_information, :recommendation, :cwe,
28
+
29
+ # tags that correspond to Evidence
30
+ :details, :affects, :parameter, :aop_source_file, :aop_source_line,
31
+ :aop_additional, :is_false_positive,
32
+
33
+
34
+ # nested tags
35
+ :request, :response,
36
+ :cvss_descriptor, :cvss_score,
37
+ :cvss3_descriptor, :cvss3_score, :cvss3_tempscore, :cvss3_envscore,
38
+
39
+ # multiple tags
40
+ :cve_list, :references
41
+ ]
42
+ end
43
+
44
+ # This allows external callers (and specs) to check for implemented
45
+ # properties
46
+ def respond_to?(method, include_private=false)
47
+ return true if supported_tags.include?(method.to_sym)
48
+ super
49
+ end
50
+
51
+ # This method is invoked by Ruby when a method that is not defined in this
52
+ # instance is called.
53
+ #
54
+ # In our case we inspect the @method@ parameter and try to find the
55
+ # attribute, simple descendent or collection that it maps to in the XML
56
+ # tree.
57
+ def method_missing(method, *args)
58
+
59
+ # We could remove this check and return nil for any non-recognized tag.
60
+ # The problem would be that it would make tricky to debug problems with
61
+ # typos. For instance: <>.potr would return nil instead of raising an
62
+ # exception
63
+ unless supported_tags.include?(method)
64
+ super
65
+ return
66
+ end
67
+
68
+ # Any fields where a simple .camelcase() won't work we need to translate,
69
+ # this includes acronyms (e.g. :cwe would become 'Cwe') and simple nested
70
+ # tags.
71
+ translations_table = {
72
+ cwe: 'CWE',
73
+ aop_source_file: 'AOPSourceFile',
74
+ aop_source_line: 'AOPSourceLine',
75
+ aop_additional: 'AOPAdditional',
76
+ request: 'TechnicalDetails/Request',
77
+ response: 'TechnicalDetails/Response',
78
+ cvss_descriptor: 'CVSS/Descriptor',
79
+ cvss_score: 'CVSS/Score',
80
+ cvss3_descriptor: 'CVSS3/Descriptor',
81
+ cvss3_score: 'CVSS3/Score',
82
+ cvss3_tempscore: 'CVSS3/TempScore',
83
+ cvss3_envscore: 'CVSS3/EnvScore'
84
+ }
85
+ method_name = translations_table.fetch(method, method.to_s.camelcase)
86
+ # first we try the attributes:
87
+ # return @xml.attributes[method_name].value if @xml.attributes.key?(method_name)
88
+
89
+
90
+ # There is a ./References tag, but we want to short-circuit that one to
91
+ # do custom processing.
92
+ return references_list() if method == :references
93
+
94
+ # then we try the children tags
95
+ tag = xml.at_xpath("./#{method_name}")
96
+ if tag && !tag.text.blank?
97
+ if tags_with_html_content.include?(method)
98
+ return cleanup_html(tag.text)
99
+ elsif tags_with_commas.include?(method)
100
+ return cleanup_decimals(tag.text)
101
+ else
102
+ return tag.text
103
+ end
104
+ else
105
+ 'n/a'
106
+ end
107
+
108
+ return 'unimplemented' if method == :cve_list
109
+
110
+ # nothing found
111
+ return nil
112
+ end
113
+
114
+ private
115
+
116
+ def cleanup_html(source)
117
+ result = source.dup
118
+ result.gsub!(/&quot;/, '"')
119
+ result.gsub!(/&amp;/, '&')
120
+ result.gsub!(/&lt;/, '<')
121
+ result.gsub!(/&gt;/, '>')
122
+
123
+ result.gsub!(/<b>(.*?)<\/b>/) { "*#{$1.strip}*" }
124
+ result.gsub!(/<br\/>/, "\n")
125
+ result.gsub!(/<font.*?>(.*?)<\/font>/m, '\1')
126
+ result.gsub!(/<h2>(.*?)<\/h2>/) { "*#{$1.strip}*" }
127
+ result.gsub!(/<i>(.*?)<\/i>/, '\1')
128
+ result.gsub!(/<p>(.*?)<\/p>/, '\1')
129
+ result.gsub!(/<code><pre.*?>(.*?)<\/pre><\/code>/m){|m| "\n\nbc.. #{$1.strip}\n\np. \n" }
130
+ result.gsub!(/<pre.*?>(.*?)<\/pre>/m){|m| "\n\nbc.. #{$1.strip}\n\np. \n" }
131
+ result.gsub!(/<ul>(.*?)<\/ul>/m){"#{$1.strip}\n"}
132
+
133
+ result.gsub!(/<li>(.*?)<\/li>/){"\n* #{$1.strip}"}
134
+
135
+ result.gsub!(/<strong>(.*?)<\/strong>/) { "*#{$1.strip}*" }
136
+ result.gsub!(/<span.*?>(.*?)<\/span>/m){"#{$1.strip}\n"}
137
+
138
+ result
139
+ end
140
+
141
+ def cleanup_decimals(source)
142
+ result = source.dup
143
+ result.gsub!(/([0-9])\,([0-9])/, '\1.\2')
144
+ result
145
+ end
146
+
147
+ def references_list
148
+ references = ''
149
+ xml.xpath('./References/Reference').each do |xml_reference|
150
+ references << xml_reference.at_xpath('./Database').text()
151
+ references << "\n"
152
+ references << xml_reference.at_xpath('./URL').text()
153
+ references << "\n\n"
154
+ end
155
+ references
156
+ end
157
+
158
+ # Some of the values have embedded HTML conent that we need to strip
159
+ def tags_with_html_content
160
+ [:details, :description, :detailed_information, :impact, :recommendation]
161
+ end
162
+
163
+ def tags_with_commas
164
+ [:cvss3_score, :cvss3_tempscore, :cvss3_envscore]
165
+ end
166
+
167
+ end
168
+ end
@@ -0,0 +1,93 @@
1
+ module Acunetix
2
+ # This class represents each of the /ScanGroup/Scan elements in the Acunetix
3
+ # 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 Scan
11
+ attr_accessor :xml
12
+ # Accepts an XML node from Nokogiri::XML.
13
+ def initialize(xml_node)
14
+ @xml = xml_node
15
+ unless @xml.name == "Scan"
16
+ raise "Invalid XML; root node must be called 'Scan'"
17
+ end
18
+ end
19
+
20
+ # List of supported tags. They are all descendents of the ./Scan node.
21
+ SUPPORTED_TAGS = [
22
+ # attributes
23
+
24
+ # simple tags
25
+ :name, :short_name, :start_url, :start_time, :finish_time, :scan_time,
26
+ :aborted, :responsive, :banner, :os, :web_server, :technologies, :ip,
27
+ :fqdn, :operating_system, :mac_address, :netbios_name, :scan_start_time,
28
+ :scan_stop_time
29
+ ]
30
+
31
+ # This allows external callers (and specs) to check for implemented
32
+ # properties
33
+ def respond_to?(method, include_private=false)
34
+ return true if SUPPORTED_TAGS.include?(method.to_sym)
35
+ super
36
+ end
37
+
38
+ # This method is invoked by Ruby when a method that is not defined in this
39
+ # instance is called.
40
+ #
41
+ # In our case we inspect the @method@ parameter and try to find the
42
+ # corresponding <tag/> element inside the ./Scan child.
43
+ def method_missing(method, *args)
44
+ # We could remove this check and return nil for any non-recognized tag.
45
+ # The problem would be that it would make tricky to debug problems with
46
+ # typos. For instance: <>.potr would return nil instead of raising an
47
+ # exception
48
+ super and return unless SUPPORTED_TAGS.include?(method)
49
+
50
+ if tag = xml.at_xpath("./#{tag_name_for_method(method)}")
51
+ tag.text
52
+ else
53
+ nil
54
+ end
55
+ end
56
+
57
+
58
+ def report_items
59
+ @xml.xpath('./ReportItems/ReportItem')
60
+ end
61
+
62
+
63
+ def service
64
+ "port #{start_url_port}, #{banner}"
65
+ end
66
+
67
+
68
+ def start_url_host
69
+ start_uri.host
70
+ end
71
+ alias_method :hostname, :start_url_host
72
+
73
+
74
+ def start_url_port
75
+ start_uri.port
76
+ end
77
+
78
+ private
79
+
80
+ def start_uri
81
+ @start_uri ||= URI::parse(start_url)
82
+ end
83
+
84
+ def tag_name_for_method(method)
85
+ # Any fields where a simple .camelcase() won't work we need to translate,
86
+ # this includes acronyms (e.g. :scan_url would become 'ScanUrl').
87
+ {
88
+ start_url: 'StartURL'
89
+ }[method] || method.to_s.camelcase
90
+ end
91
+
92
+ end
93
+ end
@@ -0,0 +1,9 @@
1
+ # hook to the framework base clases
2
+ require 'dradis-plugins'
3
+
4
+ # load this add-on's engine
5
+ require 'dradis/plugins/acunetix'
6
+
7
+ # load supporting Acunetix classes
8
+ require 'acunetix/report_item'
9
+ require 'acunetix/scan'
@@ -0,0 +1,12 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Acunetix
4
+ end
5
+ end
6
+ end
7
+
8
+ require 'dradis/plugins/acunetix/engine'
9
+ require 'dradis/plugins/acunetix/field_processor'
10
+ require 'dradis/plugins/acunetix/importer'
11
+ require 'dradis/plugins/acunetix/version'
12
+
@@ -0,0 +1,9 @@
1
+ module Dradis::Plugins::Acunetix
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Dradis::Plugins::Acunetix
4
+
5
+ include ::Dradis::Plugins::Base
6
+ description 'Processes Acunetix XML format'
7
+ provides :upload
8
+ end
9
+ end
@@ -0,0 +1,25 @@
1
+ module Dradis::Plugins::Acunetix
2
+ # This processor defers to ::Acunetix::Scan for the scan template and to
3
+ # ::Acunetix::ReportItem for the report_item and evidence templates.
4
+ class FieldProcessor < Dradis::Plugins::Upload::FieldProcessor
5
+
6
+ def post_initialize(args={})
7
+ if data.name == "Scan"
8
+ @acunetix_object = ::Acunetix::Scan.new(data)
9
+ else
10
+ @acunetix_object = ::Acunetix::ReportItem.new(data)
11
+ end
12
+ end
13
+
14
+ def value(args={})
15
+ field = args[:field]
16
+
17
+ # fields in the template are of the form <foo>.<field>, where <foo>
18
+ # is common across all fields for a given template (and meaningless).
19
+ _, name = field.split('.')
20
+
21
+ @acunetix_object.try(name) || 'n/a'
22
+ end
23
+ end
24
+
25
+ end
@@ -0,0 +1,19 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Acunetix
4
+ # Returns the version of the currently loaded Acunetix 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,78 @@
1
+ module Dradis::Plugins::Acunetix
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.fetch(:file) )
9
+
10
+ logger.info{'Parsing Acunetix output file...'}
11
+ @doc = Nokogiri::XML( file_content )
12
+ logger.info{'Done.'}
13
+
14
+ if @doc.xpath('/ScanGroup/Scan').empty?
15
+ error = "No scan results were detected in the uploaded file (/ScanGroup/Scan). Ensure you uploaded an Acunetix XML report."
16
+ logger.fatal{ error }
17
+ content_service.create_note text: error
18
+ return false
19
+ end
20
+
21
+ @doc.xpath('/ScanGroup/Scan').each do |xml_scan|
22
+ process_scan(xml_scan)
23
+ end
24
+
25
+ return true
26
+ end # /import
27
+
28
+
29
+ private
30
+ attr_accessor :scan_node
31
+
32
+ def process_scan(xml_scan)
33
+ url = xml_scan.at_xpath('./StartURL').text()
34
+ start_url = URI::parse(url).host || url # urls wo/ protocol returned nil
35
+
36
+ self.scan_node = content_service.create_node(label: start_url, type: :host)
37
+ logger.info{ "\tScan start URL: #{start_url}" }
38
+
39
+ # Define Node properties
40
+ if scan_node.respond_to?(:properties)
41
+ scan_node.set_property(:short_name, xml_scan.at_xpath('./ShortName').text() )
42
+ scan_node.set_property(:start_url, start_url)
43
+ scan_node.set_property(:start_time, xml_scan.at_xpath('./StartTime').text() )
44
+ scan_node.set_property(:finish_time, xml_scan.at_xpath('./FinishTime').text() )
45
+ scan_node.set_property(:scan_time, xml_scan.at_xpath('./ScanTime').text() )
46
+ scan_node.set_property(:aborted, xml_scan.at_xpath('./Aborted').text() )
47
+ scan_node.set_property(:responsive, xml_scan.at_xpath('./Responsive').text() )
48
+ scan_node.set_property(:banner, xml_scan.at_xpath('./Banner').text() )
49
+ scan_node.set_property(:os, xml_scan.at_xpath('./Os').text() )
50
+ scan_node.set_property(:web_server, xml_scan.at_xpath('./WebServer').text() )
51
+ scan_node.set_property(:technologies, xml_scan.at_xpath('./Technologies').text() )
52
+ scan_node.save
53
+ end
54
+
55
+ scan_note = template_service.process_template(template: 'scan', data: xml_scan)
56
+ content_service.create_note text: scan_note, node: scan_node
57
+
58
+ xml_scan.xpath('./ReportItems/ReportItem').each do |xml_report_item|
59
+ process_report_item(xml_report_item)
60
+ end
61
+ end
62
+
63
+ def process_report_item(xml_report_item)
64
+ plugin_id = "%s/%s" % [
65
+ xml_report_item.at_xpath('./ModuleName').text(),
66
+ xml_report_item.at_xpath('./Name').text()
67
+ ]
68
+ logger.info{ "\t\t => Creating new issue (plugin_id: #{plugin_id})" }
69
+
70
+ issue_text = template_service.process_template(template: 'report_item', data: xml_report_item)
71
+ issue = content_service.create_issue(text: issue_text, id: plugin_id)
72
+
73
+ logger.info{ "\t\t => Creating new evidence" }
74
+ evidence_content = template_service.process_template(template: 'evidence', data: xml_report_item)
75
+ content_service.create_evidence(issue: issue, node: scan_node, content: evidence_content)
76
+ end
77
+ end
78
+ end