dradis-burp 3.12.0 → 3.17.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,3 +6,5 @@ require 'dradis/plugins/burp'
6
6
 
7
7
  # Load supporting Burp classes
8
8
  require 'burp/issue'
9
+ require 'burp/html/issue'
10
+ require 'burp/xml/issue'
@@ -7,5 +7,6 @@ end
7
7
 
8
8
  require 'dradis/plugins/burp/engine'
9
9
  require 'dradis/plugins/burp/field_processor'
10
- require 'dradis/plugins/burp/importer'
10
+ require 'dradis/plugins/burp/html/importer'
11
11
  require 'dradis/plugins/burp/version'
12
+ require 'dradis/plugins/burp/xml/importer'
@@ -5,10 +5,21 @@ module Dradis
5
5
  isolate_namespace Dradis::Plugins::Burp
6
6
 
7
7
  include ::Dradis::Plugins::Base
8
- description 'Processes Burp Scanner XML output'
8
+ description 'Processes Burp Scanner output'
9
9
  provides :upload
10
+
11
+ # Because this plugin provides two export modules, we have to overwrite
12
+ # the default .uploaders() method.
13
+ #
14
+ # See:
15
+ # Dradis::Plugins::Upload::Base in dradis-plugins
16
+ def self.uploaders
17
+ [
18
+ Dradis::Plugins::Burp::Html,
19
+ Dradis::Plugins::Burp::Xml
20
+ ]
21
+ end
10
22
  end
11
23
  end
12
24
  end
13
25
  end
14
-
@@ -4,7 +4,12 @@ module Dradis
4
4
  class FieldProcessor < Dradis::Plugins::Upload::FieldProcessor
5
5
 
6
6
  def post_initialize(args={})
7
- @burp_object = ::Burp::Issue.new(data)
7
+ @burp_object =
8
+ if data.is_a?(Nokogiri::XML::Element)
9
+ ::Burp::Xml::Issue.new(data)
10
+ elsif data.is_a?(Nokogiri::XML::NodeSet)
11
+ ::Burp::Html::Issue.new(data)
12
+ end
8
13
  end
9
14
 
10
15
  def value(args={})
@@ -20,4 +25,3 @@ module Dradis
20
25
  end
21
26
  end
22
27
  end
23
-
@@ -8,7 +8,7 @@ module Dradis
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 3
11
- MINOR = 12
11
+ MINOR = 17
12
12
  TINY = 0
13
13
  PRE = nil
14
14
 
@@ -0,0 +1,144 @@
1
+ module Dradis::Plugins::Burp
2
+ # This module knows how to parse Burp HTML format.
3
+ module Html
4
+ def self.meta
5
+ package = Dradis::Plugins::Burp
6
+ {
7
+ name: package::Engine::plugin_name,
8
+ description: 'Upload Burp Scanner output file (.html)',
9
+ version: package.version
10
+ }
11
+ end
12
+
13
+ class Importer < Dradis::Plugins::Upload::Importer
14
+ def initialize(args={})
15
+ args[:plugin] = Dradis::Plugins::Burp
16
+ super(args)
17
+ end
18
+
19
+ def import(params = {})
20
+ logger.info { 'Parsing Burp Scanner HTML output file...' }
21
+ @doc = Nokogiri::HTML(File.read(params[:file]))
22
+ logger.info { 'Done.' }
23
+
24
+ # Issue headers are like: <span class="BODH0" id="X">
25
+ issue_headers = @doc.xpath("//span[contains(@class, 'BODH0')]")
26
+
27
+ if issue_headers.count.zero?
28
+ error = "Document doesn't seem to be in the Burp Scanner HTML format."
29
+ logger.fatal { error }
30
+ content_service.create_note text: error
31
+ return false
32
+ end
33
+
34
+ issue_headers.each do |header|
35
+ issue_id = header.attr('id')
36
+ html = extract_html_fragment_for(issue_id)
37
+ process_html_issue(html)
38
+ end
39
+
40
+ logger.info { 'Burp Scanner results successfully imported' }
41
+ true
42
+ end
43
+
44
+ def process_html_issue(html_issue)
45
+ header = html_issue.first
46
+ title = header.text.gsub(/^\d+\.\S/, '')
47
+ burp_id =
48
+ if (link = header.css('a').first)
49
+ link.attr('href')[/\/([0-9a-f]+)_.*/, 1].to_i(16)
50
+ else
51
+ title
52
+ end
53
+ issue_id = html_issue.attr('id').value
54
+ issue_text =
55
+ template_service.process_template(
56
+ template: 'issue',
57
+ data: html_issue
58
+ )
59
+
60
+ logger.info { "Processing issue #{issue_id}: #{title}" }
61
+ issue = content_service.create_issue(text: issue_text, id: burp_id)
62
+
63
+ # Evidence headers are like:
64
+ # <span class="BODH1" id="X.Y">
65
+ # where:
66
+ # X is the issue index
67
+ # Y is the evidence index
68
+ evidence_headers = html_issue.xpath(
69
+ "//span[contains(@class, 'BODH1') and starts-with(@id, '#{issue_id}.')]"
70
+ )
71
+
72
+ # If there are no evidence headers inside this issue, this is a
73
+ # "single evidence" case: our evidence html is the issue html itself
74
+ if evidence_headers.count.zero?
75
+ process_html_evidence(html_issue, issue)
76
+ else
77
+ evidence_headers.each do |header|
78
+ evidence_id = header.attr('id')
79
+ html = extract_html_fragment_for(evidence_id)
80
+ process_html_evidence(html, issue)
81
+ end
82
+ end
83
+ end
84
+
85
+ def process_html_evidence(html_evidence, issue)
86
+ evidence_id = html_evidence.attr('id').value
87
+ logger.info { "Processing evidence #{evidence_id}" }
88
+
89
+ host_td = html_evidence.search("td:starts-with('Host:')").first
90
+ host_label = host_td.next_element.text.split('//').last
91
+ host = content_service.create_node(label: host_label, type: :host)
92
+
93
+ evidence_text =
94
+ template_service.process_template(
95
+ template: 'html_evidence',
96
+ data: html_evidence
97
+ )
98
+
99
+ content_service.create_evidence(
100
+ issue: issue,
101
+ node: host,
102
+ content: evidence_text
103
+ )
104
+ end
105
+
106
+ # Html for an issue and evidence is not nested inside an html element.
107
+ #
108
+ # An issue is the html fragment from <span id="X"> (where X is a single
109
+ # integer number: 1, 2, 3...) until the next span like that or the end of
110
+ # the file.
111
+ #
112
+ # An evidence is the html fragment from <span id="X.Y"> (where X is the
113
+ # issue index and Y the evidence index: 1.1, 1.2,...,2.1, 2.2,...) until
114
+ # the next evidence span (id="X.Z"), the next issue span (id="Y"), or the
115
+ # end of the file.
116
+ #
117
+ # This method extracts all the html related to as specific issue id or
118
+ # evidence id.
119
+ def extract_html_fragment_for(id)
120
+ next_id = if /\d+\.\d+/ =~ id
121
+ id_parts = id.split('.')
122
+ "#{id_parts[0]}.#{id_parts[1].to_i + 1}"
123
+ else
124
+ id.to_i + 1
125
+ end
126
+
127
+ start_element = @doc.xpath("//span[@id='#{id}']")
128
+ return nil if start_element.empty?
129
+
130
+ ending_element = @doc.xpath("//span[@id='#{next_id}']")
131
+ if ending_element.empty? && /\d+\.\d+/ =~ id
132
+ next_id = id.split('.')[0].to_i + 1
133
+ ending_element = @doc.xpath("//span[@id='#{next_id}']")
134
+ end
135
+
136
+ xpath = "//*[preceding-sibling::span[@id='#{id}']"
137
+ xpath += " and following-sibling::span[@id='#{next_id}']" unless ending_element.empty?
138
+ xpath += ']'
139
+
140
+ start_element + @doc.xpath(xpath)
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,144 @@
1
+ module Dradis::Plugins::Burp
2
+
3
+ # This module knows how to parse Burp XML format.
4
+ module Xml
5
+ def self.meta
6
+ package = Dradis::Plugins::Burp
7
+ {
8
+ name: package::Engine::plugin_name,
9
+ description: 'Upload Burp Scanner output file (.xml)',
10
+ version: package.version
11
+ }
12
+ end
13
+
14
+ class Importer < Dradis::Plugins::Upload::Importer
15
+ BURP_EXTENSION_TYPE = '134217728'.freeze
16
+ BURP_SEVERITIES = ['Information', 'Low', 'Medium', 'High'].freeze
17
+
18
+ def initialize(args={})
19
+ args[:plugin] = Dradis::Plugins::Burp
20
+ super(args)
21
+ end
22
+
23
+ def import(params = {})
24
+ file_content = File.read(params[:file])
25
+
26
+ if file_content =~ /base64="false"/
27
+ error = "Burp input contains HTTP request / response data that hasn't been Base64-encoded.\n"
28
+ error << 'Please re-export your scanner results making sure the Base-64 encode option is selected.'
29
+
30
+ logger.fatal{ error }
31
+ content_service.create_note text: error
32
+ return false
33
+ end
34
+
35
+ logger.info { 'Parsing Burp Scanner XML output file...' }
36
+ doc = Nokogiri::XML(file_content)
37
+ logger.info { 'Done.' }
38
+
39
+ if doc.root.name != 'issues'
40
+ error = 'Document doesn\'t seem to be in the Burp Scanner XML format.'
41
+ logger.fatal { error }
42
+ content_service.create_note text: error
43
+ return false
44
+ end
45
+
46
+ # This will be filled in by the Processor while iterating over the issues
47
+ @issues = []
48
+ @severities = Hash.new(0)
49
+
50
+ # We need to look ahead through all issues to bring the highest severity
51
+ # of each instance to the Issue level.
52
+ doc.xpath('issues/issue').each do |xml_issue|
53
+ issue_id = issue_id_for(xml_issue)
54
+ issue_severity = BURP_SEVERITIES.index(xml_issue.at('severity').text)
55
+
56
+ @severities[issue_id] = issue_severity if issue_severity > @severities[issue_id]
57
+ @issues << xml_issue
58
+ end
59
+
60
+ @issues.each { |xml_issue| process_issue(xml_issue) }
61
+
62
+ logger.info { 'Burp Scanner results successfully imported' }
63
+ true
64
+ end
65
+
66
+ private
67
+ def create_issue(affected_host:, id:, xml_issue:)
68
+ xml_evidence = xml_issue.clone
69
+
70
+ # Ensure that the Issue contains the highest Severity value
71
+ xml_issue.at('severity').content = BURP_SEVERITIES[@severities[id]]
72
+
73
+ issue_text =
74
+ template_service.process_template(
75
+ template: 'issue',
76
+ data: xml_issue
77
+ )
78
+
79
+ if issue_text.include?(::Burp::INVALID_UTF_REPLACE)
80
+ logger.info do
81
+ "\tdetected invalid UTF-8 bytes in your issue. " \
82
+ "Replacing them with '#{::Burp::INVALID_UTF_REPLACE}'."
83
+ end
84
+ end
85
+
86
+ issue = content_service.create_issue(text: issue_text, id: id)
87
+
88
+ logger.info do
89
+ "\tadding evidence for this instance to #{affected_host.label}."
90
+ end
91
+
92
+ evidence_text =
93
+ template_service.process_template(
94
+ template: 'evidence',
95
+ data: xml_evidence
96
+ )
97
+
98
+ if evidence_text.include?(::Burp::INVALID_UTF_REPLACE)
99
+ logger.info do
100
+ "\tdetected invalid UTF-8 bytes in your evidence. " \
101
+ "Replacing them with '#{::Burp::INVALID_UTF_REPLACE}'."
102
+ end
103
+ end
104
+
105
+ content_service.create_evidence(
106
+ issue: issue,
107
+ node: affected_host,
108
+ content: evidence_text
109
+ )
110
+ end
111
+
112
+ # Burp extensions don't follow the "unique type for every Issue" logic
113
+ # so we have to deal with them separately
114
+ def issue_id_for(xml_issue)
115
+ if xml_issue.at('type').text == BURP_EXTENSION_TYPE
116
+ xml_issue.at('name').text.gsub!(' ', '')
117
+ else
118
+ xml_issue.at('type').text.to_i
119
+ end
120
+ end
121
+
122
+ # Creates the Nodes/properties
123
+ def process_issue(xml_issue)
124
+ host_url = xml_issue.at('host').text
125
+ host_label = xml_issue.at('host')['ip']
126
+ host_label = host_url if host_label.empty?
127
+ issue_id = issue_id_for(xml_issue)
128
+
129
+ affected_host = content_service.create_node(label: host_label, type: :host)
130
+ affected_host.set_property(:hostname, host_url)
131
+ affected_host.save
132
+
133
+ logger.info { "Adding #{xml_issue.at('name').text} (#{issue_id})"}
134
+ logger.info { "\taffects: #{host_label}" }
135
+
136
+ create_issue(
137
+ affected_host: affected_host,
138
+ id: issue_id,
139
+ xml_issue: xml_issue
140
+ )
141
+ end
142
+ end
143
+ end
144
+ end
@@ -3,18 +3,27 @@ class BurpTasks < Thor
3
3
 
4
4
  namespace "dradis:plugins:burp"
5
5
 
6
- desc "upload FILE", "upload Burp XML results"
6
+ desc "upload FILE", "upload Burp XML or HTML results"
7
7
  def upload(file_path)
8
8
  require 'config/environment'
9
9
 
10
10
  unless File.exists?(file_path)
11
11
  $stderr.puts "** the file [#{file_path}] does not exist"
12
- exit -1
12
+ exit(-1)
13
13
  end
14
14
 
15
15
  detect_and_set_project_scope
16
16
 
17
- importer = Dradis::Plugins::Burp::Importer.new(task_options)
17
+ importer =
18
+ if File.extname(file_path) == '.xml'
19
+ Dradis::Plugins::Burp::Xml::Importer.new(task_options)
20
+ elsif File.extname(file_path) == '.html'
21
+ Dradis::Plugins::Burp::Html::Importer.new(task_options)
22
+ else
23
+ $stderr.puts "** Unsupported file. Must be .xml or .html"
24
+ exit(-2)
25
+ end
26
+
18
27
  importer.import(file: file_path)
19
28
  end
20
29
 
@@ -3,17 +3,17 @@ require 'ostruct'
3
3
 
4
4
  describe 'Burp upload plugin' do
5
5
 
6
- describe Burp::Issue do
7
- it "handles invalid utf-8 bytes" do
6
+ describe Burp::Xml::Issue do
7
+ it 'handles invalid utf-8 bytes' do
8
8
  doc = Nokogiri::XML(File.read('spec/fixtures/files/invalid-utf-issue.xml'))
9
9
  xml_issue = doc.xpath('issues/issue').first
10
- issue = Burp::Issue.new(xml_issue)
10
+ issue = Burp::Xml::Issue.new(xml_issue)
11
11
 
12
12
  expect{ issue.request.encode('utf-8') }.to_not raise_error
13
13
  end
14
14
  end
15
15
 
16
- describe "Importer" do
16
+ describe Dradis::Plugins::Burp::Xml::Importer do
17
17
  before(:each) do
18
18
  # Stub template service
19
19
  templates_dir = File.expand_path('../../templates', __FILE__)
@@ -21,7 +21,7 @@ describe 'Burp upload plugin' do
21
21
  .to receive(:default_templates_dir).and_return(templates_dir)
22
22
 
23
23
  # Init services
24
- plugin = Dradis::Plugins::Burp
24
+ plugin = Dradis::Plugins::Burp::Xml
25
25
 
26
26
  @content_service = Dradis::Plugins::ContentService::Base.new(
27
27
  logger: Logger.new(STDOUT),
@@ -49,7 +49,7 @@ describe 'Burp upload plugin' do
49
49
  end
50
50
  end
51
51
 
52
- it "creates nodes, issues, and evidence as needed" do
52
+ it 'creates nodes, issues, and evidence as needed' do
53
53
 
54
54
  # Host node
55
55
  #
@@ -112,5 +112,109 @@ describe 'Burp upload plugin' do
112
112
  @importer.import(file: 'spec/fixtures/files/burp.xml')
113
113
  end
114
114
 
115
+ it 'returns the highest <severity> at the Issue level' do
116
+
117
+ # create_issue should be called once for each issue in the xml
118
+ expect(@content_service).to receive(:create_issue) do |args|
119
+ expect(args[:id]).to eq(8781630)
120
+ expect(args[:text]).to include("#[Title]#\nIssue 1")
121
+ expect(args[:text]).to include("#[Severity]#\nCritical")
122
+ OpenStruct.new(args)
123
+ end
124
+
125
+ expect(@content_service).to receive(:create_evidence) do |args|
126
+ expect(args[:content]).to include("#[Severity]#\nInformation")
127
+ expect(args[:issue].text).to include("#[Title]#\nIssue 1")
128
+ expect(args[:node].label).to eq('10.0.0.1')
129
+ end.once
130
+ expect(@content_service).to receive(:create_evidence) do |args|
131
+ expect(args[:content]).to include("#[Severity]#\nHigh")
132
+ expect(args[:issue].text).to include("#[Title]#\nIssue 1")
133
+ expect(args[:node].label).to eq('10.0.0.1')
134
+ OpenStruct.new(args)
135
+ end.once
136
+ expect(@content_service).to receive(:create_evidence) do |args|
137
+ expect(args[:content]).to include("#[Severity]#\nMedium")
138
+ expect(args[:issue].text).to include("#[Title]#\nIssue 1")
139
+ expect(args[:node].label).to eq('10.0.0.1')
140
+ end.once
141
+ expect(@content_service).to receive(:create_evidence) do |args|
142
+ expect(args[:content]).to include("#[Severity]#\nCritical")
143
+ expect(args[:issue].text).to include("#[Title]#\nIssue 1")
144
+ expect(args[:node].label).to eq('10.0.0.1')
145
+ end.once
146
+ expect(@content_service).to receive(:create_evidence) do |args|
147
+ expect(args[:content]).to include("#[Severity]#\nLow")
148
+ expect(args[:issue].text).to include("#[Title]#\nIssue 1")
149
+ expect(args[:node].label).to eq('10.0.0.1')
150
+ end.once
151
+
152
+ # Run the import
153
+ @importer.import(file: 'spec/fixtures/files/burp_issue_severity.xml')
154
+ end
155
+ end
156
+
157
+ describe Dradis::Plugins::Burp::Html::Importer do
158
+ before(:each) do
159
+ # Stub template service
160
+ templates_dir = File.expand_path('../../templates', __FILE__)
161
+ expect_any_instance_of(Dradis::Plugins::TemplateService)
162
+ .to receive(:default_templates_dir).and_return(templates_dir)
163
+
164
+ # Init services
165
+ plugin = Dradis::Plugins::Burp::Html
166
+
167
+ @content_service = Dradis::Plugins::ContentService::Base.new(
168
+ logger: Logger.new(STDOUT),
169
+ plugin: plugin
170
+ )
171
+
172
+ @importer = plugin::Importer.new(
173
+ content_service: @content_service,
174
+ )
175
+
176
+ # Stub dradis-plugins methods
177
+ #
178
+ # They return their argument hashes as objects mimicking
179
+ # Nodes, Issues, etc
180
+ allow(@content_service).to receive(:create_node) do |args|
181
+ obj = OpenStruct.new(args)
182
+ obj.define_singleton_method(:set_property) { |_, __| }
183
+ obj
184
+ end
185
+ allow(@content_service).to receive(:create_issue) do |args|
186
+ OpenStruct.new(args)
187
+ end
188
+ allow(@content_service).to receive(:create_evidence) do |args|
189
+ OpenStruct.new(args)
190
+ end
191
+ end
192
+
193
+ it "creates nodes, issues, and evidence as needed" do
194
+
195
+ # Host node
196
+ #
197
+ # create_node should be called once for each issue in the xml,
198
+ # but ContentService knows it's already created and NOOPs
199
+ expect(@content_service).to receive(:create_node)
200
+ .with(hash_including label: 'github.com/dradis/dradis-burp')
201
+ .exactly(1).times
202
+
203
+ # # create_issue should be called once for each issue in the xml
204
+ expect(@content_service).to receive(:create_issue) do |args|
205
+ expect(args[:text]).to include("#[Title]#\nStrict transport security not enforced")
206
+ expect(args[:id]).to eq(16777984)
207
+ OpenStruct.new(args)
208
+ end.once
209
+ expect(@content_service).to receive(:create_evidence) do |args|
210
+ expect(args[:content]).to include("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
211
+ expect(args[:issue].text).to include("#[Title]#\nStrict transport security not enforced")
212
+ expect(args[:node].label).to eq("github.com/dradis/dradis-burp")
213
+ end.once
214
+
215
+ # Run the import
216
+ @importer.import(file: 'spec/fixtures/files/burp.html')
217
+ end
218
+
115
219
  end
116
220
  end