dradis-acunetix 3.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,165 @@
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
136
+ end
137
+
138
+ def cleanup_decimals(source)
139
+ result = source.dup
140
+ result.gsub!(/([0-9])\,([0-9])/, '\1.\2')
141
+ result
142
+ end
143
+
144
+ def references_list
145
+ references = ''
146
+ xml.xpath('./References/Reference').each do |xml_reference|
147
+ references << xml_reference.at_xpath('./Database').text()
148
+ references << "\n"
149
+ references << xml_reference.at_xpath('./URL').text()
150
+ references << "\n\n"
151
+ end
152
+ references
153
+ end
154
+
155
+ # Some of the values have embedded HTML conent that we need to strip
156
+ def tags_with_html_content
157
+ [:details, :description, :detailed_information, :impact, :recommendation]
158
+ end
159
+
160
+ def tags_with_commas
161
+ [:cvss3_score, :cvss3_tempscore, :cvss3_envscore]
162
+ end
163
+
164
+ end
165
+ 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 = 6
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,62 @@
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
+
34
+ start_url = URI::parse(xml_scan.at_xpath('./StartURL').text()).host
35
+
36
+ self.scan_node = content_service.create_node(label: start_url, type: :host)
37
+ logger.info{ "\tScan start URL: #{start_url}" }
38
+
39
+ scan_note = template_service.process_template(template: 'scan', data: xml_scan)
40
+ content_service.create_note text: scan_note, node: scan_node
41
+
42
+ xml_scan.xpath('./ReportItems/ReportItem').each do |xml_report_item|
43
+ process_report_item(xml_report_item)
44
+ end
45
+ end
46
+
47
+ def process_report_item(xml_report_item)
48
+ plugin_id = "%s/%s" % [
49
+ xml_report_item.at_xpath('./ModuleName').text(),
50
+ xml_report_item.at_xpath('./Name').text()
51
+ ]
52
+ logger.info{ "\t\t => Creating new issue (plugin_id: #{plugin_id})" }
53
+
54
+ issue_text = template_service.process_template(template: 'report_item', data: xml_report_item)
55
+ issue = content_service.create_issue(text: issue_text, id: plugin_id)
56
+
57
+ logger.info{ "\t\t => Creating new evidence" }
58
+ evidence_content = template_service.process_template(template: 'evidence', data: xml_report_item)
59
+ content_service.create_evidence(issue: issue, node: scan_node, content: evidence_content)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,13 @@
1
+ require_relative 'gem_version'
2
+
3
+ module Dradis
4
+ module Plugins
5
+ module Acunetix
6
+ # Returns the version of the currently loaded Acunetix 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,25 @@
1
+ class AcunetixTasks < Thor
2
+ include Rails.application.config.dradis.thor_helper_module
3
+
4
+ namespace "dradis:plugins:acunetix"
5
+
6
+ desc "upload FILE", "upload Acunetix XML results"
7
+ def upload(file_path)
8
+ require 'config/environment'
9
+
10
+ logger = Logger.new(STDOUT)
11
+ logger.level = Logger::DEBUG
12
+
13
+ unless File.exists?(file_path)
14
+ $stderr.puts "** the file [#{file_path}] does not exist"
15
+ exit -1
16
+ end
17
+
18
+ detect_and_set_project_scope
19
+ importer = Dradis::Plugins::Acunetix::Importer.new(logger: logger)
20
+ importer.import(file: file_path)
21
+
22
+ logger.close
23
+ end
24
+
25
+ end
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+ require 'ostruct'
3
+
4
+ module Dradis::Plugins
5
+ describe 'Acunetix upload plugin' do
6
+ before(:each) do
7
+ # Stub template service
8
+ templates_dir = File.expand_path('../../templates', __FILE__)
9
+ allow_any_instance_of(TemplateService).to \
10
+ receive(:default_templates_dir).and_return(templates_dir)
11
+
12
+ @content_service = ContentService.new(plugin: Acunetix)
13
+ template_service = TemplateService.new(plugin: Acunetix)
14
+
15
+ @importer = Acunetix::Importer.new(
16
+ content_service: @content_service,
17
+ template_service: template_service
18
+ )
19
+
20
+ # Stub dradis-plugins methods
21
+ #
22
+ # They return their argument hashes as objects mimicking
23
+ # Nodes, Issues, etc
24
+ allow(@content_service).to receive(:create_node) do |args|
25
+ OpenStruct.new(args)
26
+ end
27
+ allow(@content_service).to receive(:create_note) do |args|
28
+ OpenStruct.new(args)
29
+ end
30
+ allow(@content_service).to receive(:create_issue) do |args|
31
+ OpenStruct.new(args)
32
+ end
33
+ allow(@content_service).to receive(:create_evidence) do |args|
34
+ OpenStruct.new(args)
35
+ end
36
+ end
37
+
38
+ it "creates nodes, issues, notes and an evidences as needed" do
39
+
40
+ expect(@content_service).to receive(:create_node).with(hash_including label: "testphp.vulnweb.com", type: :host).once
41
+ expect(@content_service).to receive(:create_note) do |args|
42
+ expect(args[:text]).to include("#[Title]#\nAcunetix scanner notes (7/10/2014, 11:56:03)")
43
+ expect(args[:node].label).to eq("testphp.vulnweb.com")
44
+ end.once
45
+
46
+ expect(@content_service).to receive(:create_issue) do |args|
47
+ expect(args[:text]).to include("#[Title]#\nHTML form without CSRF protection")
48
+ expect(args[:id]).to eq("Crawler/HTML form without CSRF protection")
49
+ OpenStruct.new(args)
50
+ end.once
51
+
52
+ expect(@content_service).to receive(:create_issue) do |args|
53
+ expect(args[:text]).to include("#[Title]#\nClickjacking: X-Frame-Options header missing")
54
+ expect(args[:id]).to eq("Scripting (Clickjacking_X_Frame_Options.script)/Clickjacking: X-Frame-Options header missing")
55
+ OpenStruct.new(args)
56
+ end.once
57
+
58
+ expect(@content_service).to receive(:create_evidence) do |args|
59
+ expect(args[:content]).to include("/")
60
+ expect(args[:issue].id).to eq("Crawler/HTML form without CSRF protection")
61
+ expect(args[:node].label).to eq("testphp.vulnweb.com")
62
+ end.once
63
+
64
+ expect(@content_service).to receive(:create_evidence) do |args|
65
+ expect(args[:content]).to include("Web Server")
66
+ expect(args[:issue].id).to eq("Scripting (Clickjacking_X_Frame_Options.script)/Clickjacking: X-Frame-Options header missing")
67
+ expect(args[:node].label).to eq("testphp.vulnweb.com")
68
+ end.once
69
+
70
+ @importer.import(file: 'spec/fixtures/files/simple.acunetix.xml')
71
+ end
72
+
73
+ # Regression test for github.com/dradis/dradis-nexpose/issues/1
74
+ describe "Source HTML parsing" do
75
+ it "identifies code/pre blocks and replaces them with the Textile equivalent" do
76
+
77
+ expect(@content_service).to receive(:create_issue) do |args|
78
+ expect(args[:text]).to include("#[Title]#\nSQL injection (verified)")
79
+ expect(args[:text]).not_to include("<code>")
80
+ expect(args[:text]).not_to include("<pre")
81
+ expect(args[:id]).to eq("Scripting (Sql_Injection.script)/SQL injection (verified)")
82
+ OpenStruct.new(args)
83
+ end.once
84
+
85
+ # expect(@content_service).to receive(:create_evidence) do |args|
86
+ # expect(args[:content]).to include("Web Server")
87
+ # expect(args[:issue].id).to eq("Scripting (Clickjacking_X_Frame_Options.script)")
88
+ # expect(args[:node].label).to eq("testphp.vulnweb.com")
89
+ # end.once
90
+
91
+ @importer.import(file: 'spec/fixtures/files/code-pre.acunetix.xml')
92
+ end
93
+ end
94
+
95
+ # Regression test to make sure that commas are replaced with decimals in the CVSSv3 scores
96
+ describe "CVSS clean up decimals" do
97
+ it "identifies commas used as decimals in CVSSv3 scores and replaces them with periods" do
98
+
99
+ expect(@content_service).to receive(:create_issue) do |args|
100
+ expect(args[:text]).to include("#[CVSS3Score]#\n5.3")
101
+ OpenStruct.new(args)
102
+ end
103
+
104
+ @importer.import(file: 'spec/fixtures/files/commas-format.acunetix.xml')
105
+ end
106
+ end
107
+
108
+ end
109
+ end