dradis-acunetix 3.6.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.
@@ -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