dradis-burp 3.0.2

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,149 @@
1
+ module Burp
2
+ # This class represents each of the /issues/issue elements in the Burp
3
+ # Scanner 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 Issue
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 can be attributes, simple descendants or
17
+ # collections (e.g. <references/>, <tags/>)
18
+ def supported_tags
19
+ [
20
+ # attributes
21
+
22
+ # simple tags
23
+ :serial_number, :type, :name, :host, :path, :location, :severity,
24
+ :confidence, :background, :remediation_background, :detail,
25
+ :remediation_detail,
26
+
27
+ # nested tags
28
+ :request, :response
29
+ ]
30
+ end
31
+
32
+ # This allows external callers (and specs) to check for implemented
33
+ # properties
34
+ def respond_to?(method, include_private=false)
35
+ return true if supported_tags.include?(method.to_sym)
36
+ super
37
+ end
38
+
39
+ # This method is invoked by Ruby when a method that is not defined in this
40
+ # instance is called.
41
+ #
42
+ # In our case we inspect the @method@ parameter and try to find the
43
+ # attribute, simple descendent or collection that it maps to in the XML
44
+ # tree.
45
+ def method_missing(method, *args)
46
+
47
+ # We could remove this check and return nil for any non-recognized tag.
48
+ # The problem would be that it would make tricky to debug problems with
49
+ # typos. For instance: <>.potr would return nil instead of raising an
50
+ # exception
51
+ unless supported_tags.include?(method)
52
+ super
53
+ return
54
+ end
55
+
56
+ # First we try the attributes. In Ruby we use snake_case, but in XML
57
+ # CamelCase is used for some attributes
58
+ translations_table = {
59
+ :background => 'issueBackground',
60
+ :detail => 'issueDetail',
61
+ :remediation_background => 'remediationBackground',
62
+ :remediation_detail => 'remediationDetail',
63
+ :serial_number => 'serialNumber'
64
+ }
65
+
66
+ method_name = translations_table.fetch(method, method.to_s)
67
+
68
+ # no attributes in the <issue> node
69
+ # return @xml.attributes[method_name].value if @xml.attributes.key?(method_name)
70
+
71
+ # Then we try simple children tags: name, type, ...
72
+ tag = @xml.xpath("./#{method_name}").first
73
+ if tag && !tag.text.blank?
74
+ if tags_with_html_content.include?(method)
75
+ return cleanup_html(tag.text)
76
+ else
77
+ return tag.text
78
+ end
79
+ end
80
+
81
+ if (['request', 'response'].include?(method_name))
82
+ requestresponse_child(method_name)
83
+ else
84
+ # nothing found, the tag is valid but not present in this ReportItem
85
+ return nil
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def cleanup_html(source)
92
+ result = source.dup
93
+ result.gsub!(/&quot;/, '"')
94
+ result.gsub!(/&amp;/, '&')
95
+ result.gsub!(/&lt;/, '<')
96
+ result.gsub!(/&gt;/, '>')
97
+
98
+ result.gsub!(/<b>(.*?)<\/b>/, '*\1*')
99
+ result.gsub!(/<br\/>/, "\n")
100
+ result.gsub!(/<br>/, "\n")
101
+ result.gsub!(/<font.*?>(.*?)<\/font>/m, '\1')
102
+ result.gsub!(/<h2>(.*?)<\/h2>/, '*\1*')
103
+ result.gsub!(/<i>(.*?)<\/i>/, '\1')
104
+ result.gsub!(/<p>(.*?)<\/p>/, '\1')
105
+ result.gsub!(/<pre.*?>(.*?)<\/pre>/m){|m| "\n\nbc.. #{ $1 }\n\np. \n" }
106
+
107
+ result.gsub!(/<ul>/, "\n")
108
+ result.gsub!(/<\/ul>/, "\n")
109
+ result.gsub!(/<li>/, "\n* ")
110
+ result.gsub!(/<\/li>/, "\n")
111
+
112
+ result
113
+ end
114
+
115
+ # Some of the values have embedded HTML content that we need to strip
116
+ def tags_with_html_content
117
+ [:background, :detail, :remediation_background, :remediation_detail]
118
+ end
119
+
120
+ def requestresponse_child(field)
121
+ return 'n/a' unless @xml.at('requestresponse') && @xml.at("requestresponse/#{field}")
122
+
123
+ xml_node = @xml.at("requestresponse/#{field}")
124
+ result = "[unprocessable #{field}]"
125
+
126
+ if xml_node['base64'] == 'true'
127
+ result = Base64::strict_decode64(xml_node.text)
128
+
129
+ # don't pass binary data to the DB.
130
+ if result =~ /\0/
131
+ header, _ = result.split("\r\n\r\n")
132
+ result = header << "\r\n\r\n" << '[Binary Data Not Displayed]'
133
+ end
134
+ else
135
+ result = xml_node.text
136
+ end
137
+
138
+ # Just in case a null byte was left by Burp
139
+ result.gsub!(/\0/,'&#00;')
140
+
141
+ # We truncate the request/response because it can be pretty big.
142
+ # If it is > 1M MySQL will die when trying to INSERT
143
+ #
144
+ # TODO: maybe add a reference to this node's XPATH so the user can go
145
+ # back to the burp scanner file and look up the original request/response
146
+ result.truncate(50000, omission: '... (truncated)')
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,8 @@
1
+ # Hook to the framework base clases
2
+ require 'dradis-plugins'
3
+
4
+ # Load this add-on's engine
5
+ require 'dradis/plugins/burp'
6
+
7
+ # Load supporting Burp classes
8
+ require 'burp/issue'
@@ -0,0 +1,11 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Burp
4
+ end
5
+ end
6
+ end
7
+
8
+ require 'dradis/plugins/burp/engine'
9
+ require 'dradis/plugins/burp/field_processor'
10
+ require 'dradis/plugins/burp/importer'
11
+ require 'dradis/plugins/burp/version'
@@ -0,0 +1,22 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Burp
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Dradis::Plugins::Burp
6
+
7
+ include ::Dradis::Plugins::Base
8
+ description 'Processes Burp Scanner XML output'
9
+ provides :upload
10
+
11
+ # Configuring the gem
12
+ # class Configuration < Core::Configurator
13
+ # configure :namespace => 'burp'
14
+ # setting :category, :default => 'Burp Scanner output'
15
+ # setting :author, :default => 'Burp Scanner plugin'
16
+ # end
17
+
18
+ end
19
+ end
20
+ end
21
+ end
22
+
@@ -0,0 +1,23 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Burp
4
+ class FieldProcessor < Dradis::Plugins::Upload::FieldProcessor
5
+
6
+ def post_initialize(args={})
7
+ @burp_object = ::Burp::Issue.new(data)
8
+ end
9
+
10
+ def value(args={})
11
+ field = args[:field]
12
+ # fields in the template are of the form <foo>.<field>, where <foo>
13
+ # is common across all fields for a given template (and meaningless).
14
+ _, name = field.split('.')
15
+
16
+ @burp_object.try(name) || 'n/a'
17
+ end
18
+
19
+ end
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,19 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Burp
4
+ # Returns the version of the currently loaded Frontend 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 = 0
12
+ TINY = 2
13
+ PRE = nil
14
+
15
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,79 @@
1
+ module Dradis::Plugins::Burp
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
+ if file_content =~ /base64="false"/
11
+ error = "Burp input contains HTTP request / response data that hasn't been Base64-encoded.\n"
12
+ error << "Please re-export your scanner results making sure the Base-64 encode option is selected."
13
+
14
+ logger.fatal{ error }
15
+ content_service.create_note text: error
16
+ return false
17
+ end
18
+
19
+ logger.info{ 'Parsing Burp Scanner output file...' }
20
+ doc = Nokogiri::XML( file_content )
21
+ logger.info{'Done.'}
22
+
23
+ if doc.root.name != 'issues'
24
+ error = "Document doesn't seem to be in the Burp Scanner XML format."
25
+ logger.fatal{ error }
26
+ content_service.create_note text: error
27
+ return false
28
+ end
29
+
30
+ # This will be filled in by the Processor while iterating over the issues
31
+ hosts = []
32
+ affected_host = nil
33
+ issue_text = nil
34
+ evidence_text = nil
35
+
36
+ doc.xpath('issues/issue').each do |xml_issue|
37
+ issue_name = xml_issue.at('name').text
38
+ issue_type = xml_issue.at('type').text.to_i
39
+
40
+ logger.info{ "Adding #{ issue_name } (#{ issue_type })" }
41
+
42
+ host_label = xml_issue.at('host')['ip']
43
+ host_label = xml_issue.at('host').text if host_label.empty?
44
+ affected_host = content_service.create_node(label: host_label, type: :host)
45
+ logger.info{ "\taffects: #{ host_label }" }
46
+
47
+ if !hosts.include?(affected_host.label)
48
+ hosts << affected_host.label
49
+ url = xml_issue.at('host').text
50
+ host_description = "\#[HostInfo]\#\n#{ url }\n\n"
51
+ content_service.create_note(text: host_description, node: affected_host)
52
+ end
53
+
54
+ issue_text = template_service.process_template(
55
+ template: 'issue',
56
+ data: xml_issue)
57
+
58
+ issue = content_service.create_issue(
59
+ text: issue_text,
60
+ id: issue_type)
61
+
62
+ logger.info{ "\tadding evidence for this instance to #{ affected_host.label }."}
63
+
64
+ evidence_text = template_service.process_template(
65
+ template: 'evidence',
66
+ data: xml_issue)
67
+
68
+ content_service.create_evidence(
69
+ issue: issue,
70
+ node: affected_host,
71
+ content: evidence_text)
72
+
73
+ end
74
+ logger.info{ 'Burp Scanner results successfully imported' }
75
+ return true
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,13 @@
1
+ require_relative 'gem_version'
2
+
3
+ module Dradis
4
+ module Plugins
5
+ module Burp
6
+ # Returns the version of the currently loaded Action Mailer 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,40 @@
1
+ class BurpTasks < Thor
2
+ include Core::Pro::ProjectScopedTask if defined?(::Core::Pro)
3
+
4
+ namespace "dradis:plugins:burp"
5
+
6
+ desc "upload FILE", "upload Burp 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
+ content_service = nil
19
+ template_service = nil
20
+
21
+ template_service = Dradis::Plugins::TemplateService.new(plugin: Dradis::Plugins::Burp)
22
+ if defined?(Dradis::Pro)
23
+ detect_and_set_project_scope
24
+ content_service = Dradis::Pro::Plugins::ContentService.new(plugin: Dradis::Plugins::Burp)
25
+ else
26
+ content_service = Dradis::Plugins::ContentService.new(plugin: Dradis::Plugins::Burp)
27
+ end
28
+
29
+ importer = Dradis::Plugins::Burp::Importer.new(
30
+ logger: logger,
31
+ content_service: content_service,
32
+ template_service: template_service
33
+ )
34
+
35
+ importer.import(file: file_path)
36
+
37
+ logger.close
38
+ end
39
+
40
+ end
@@ -0,0 +1,108 @@
1
+ require 'spec_helper'
2
+ require 'ostruct'
3
+
4
+ describe 'Burp upload plugin' do
5
+
6
+ describe Burp::Issue do
7
+ pending "create some unit tests for the Burp::Issue wrapper class"
8
+ end
9
+
10
+ describe "Importer" do
11
+ before(:each) do
12
+ # Stub template service
13
+ templates_dir = File.expand_path('../../templates', __FILE__)
14
+ expect_any_instance_of(Dradis::Plugins::TemplateService)
15
+ .to receive(:default_templates_dir).and_return(templates_dir)
16
+
17
+ # Init services
18
+ plugin = Dradis::Plugins::Burp
19
+
20
+ @content_service = Dradis::Plugins::ContentService.new(plugin: plugin)
21
+ template_service = Dradis::Plugins::TemplateService.new(plugin: plugin)
22
+
23
+ @importer = plugin::Importer.new(
24
+ content_service: @content_service,
25
+ template_service: template_service
26
+ )
27
+
28
+ # Stub dradis-plugins methods
29
+ #
30
+ # They return their argument hashes as objects mimicking
31
+ # Nodes, Issues, etc
32
+ allow(@content_service).to receive(:create_node) do |args|
33
+ OpenStruct.new(args)
34
+ end
35
+ allow(@content_service).to receive(:create_note) do |args|
36
+ OpenStruct.new(args)
37
+ end
38
+ allow(@content_service).to receive(:create_issue) do |args|
39
+ OpenStruct.new(args)
40
+ end
41
+ allow(@content_service).to receive(:create_evidence) do |args|
42
+ OpenStruct.new(args)
43
+ end
44
+ end
45
+
46
+ it "creates nodes, issues, notes and an evidences as needed" do
47
+
48
+ # Host node and basic host info note
49
+ #
50
+ # create_node should be called once for each issue in the xml,
51
+ # but ContentService knows it's already created and NOOPs
52
+ expect(@content_service).to receive(:create_node)
53
+ .with(hash_including label: '10.0.0.1')
54
+ .exactly(4).times
55
+ # create_note should be calld just once
56
+ expect(@content_service).to receive(:create_note) do |args|
57
+ expect(args[:text]).to include("#[HostInfo]#\nhttp://www.test.com")
58
+ OpenStruct.new(args)
59
+ end.once
60
+
61
+ # create_issue should be called once for each issue in the xml
62
+ expect(@content_service).to receive(:create_issue) do |args|
63
+ expect(args[:text]).to include("#[Title]#\nIssue 1")
64
+ OpenStruct.new(args)
65
+ end.once
66
+ expect(@content_service).to receive(:create_evidence) do |args|
67
+ expect(args[:content]).to include("Lorem ipsum dolor sit amet")
68
+ expect(args[:issue].text).to include("#[Title]#\nIssue 1")
69
+ expect(args[:node].label).to eq("10.0.0.1")
70
+ end.once
71
+
72
+ expect(@content_service).to receive(:create_issue) do |args|
73
+ expect(args[:text]).to include("#[Title]#\nIssue 2")
74
+ OpenStruct.new(args)
75
+ end.once
76
+ expect(@content_service).to receive(:create_evidence) do |args|
77
+ expect(args[:content]).to include("Lorem ipsum dolor sit amet")
78
+ expect(args[:issue].text).to include("#[Title]#\nIssue 2")
79
+ expect(args[:node].label).to eq("10.0.0.1")
80
+ end.once
81
+
82
+ expect(@content_service).to receive(:create_issue) do |args|
83
+ expect(args[:text]).to include("#[Title]#\nIssue 3")
84
+ OpenStruct.new(args)
85
+ end.once
86
+ expect(@content_service).to receive(:create_evidence) do |args|
87
+ expect(args[:content]).to include("Lorem ipsum dolor sit amet")
88
+ expect(args[:issue].text).to include("#[Title]#\nIssue 3")
89
+ expect(args[:node].label).to eq("10.0.0.1")
90
+ end.once
91
+
92
+ expect(@content_service).to receive(:create_issue) do |args|
93
+ expect(args[:text]).to include("#[Title]#\nIssue 4")
94
+ OpenStruct.new(args)
95
+ end.once
96
+ expect(@content_service).to receive(:create_evidence) do |args|
97
+ expect(args[:content]).to include("Lorem ipsum dolor sit amet")
98
+ expect(args[:issue].text).to include("#[Title]#\nIssue 4")
99
+ expect(args[:node].label).to eq("10.0.0.1")
100
+ end.once
101
+
102
+
103
+ # Run the import
104
+ @importer.import(file: 'spec/fixtures/files/burp.xml')
105
+ end
106
+
107
+ end
108
+ end