dradis-acunetix 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 +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