dradis-burp 3.12.0 → 3.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -1
- data/dradis-burp.gemspec +1 -1
- data/lib/burp/html/issue.rb +157 -0
- data/lib/burp/issue.rb +1 -118
- data/lib/burp/xml/issue.rb +127 -0
- data/lib/dradis-burp.rb +2 -0
- data/lib/dradis/plugins/burp.rb +2 -1
- data/lib/dradis/plugins/burp/engine.rb +13 -2
- data/lib/dradis/plugins/burp/field_processor.rb +6 -2
- data/lib/dradis/plugins/burp/gem_version.rb +1 -1
- data/lib/dradis/plugins/burp/html/importer.rb +144 -0
- data/lib/dradis/plugins/burp/xml/importer.rb +150 -0
- data/lib/tasks/thorfile.rb +12 -3
- data/spec/burp_upload_spec.rb +68 -4
- data/spec/fixtures/files/burp.html +229 -0
- data/templates/evidence.fields +1 -1
- data/templates/html_evidence.fields +13 -0
- data/templates/html_evidence.sample +36 -0
- data/templates/html_evidence.template +50 -0
- metadata +15 -7
- data/lib/dradis/plugins/burp/importer.rb +0 -138
data/lib/dradis-burp.rb
CHANGED
data/lib/dradis/plugins/burp.rb
CHANGED
@@ -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
|
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 =
|
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
|
-
|
@@ -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,150 @@
|
|
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
|
+
def initialize(args={})
|
16
|
+
args[:plugin] = Dradis::Plugins::Burp
|
17
|
+
super(args)
|
18
|
+
end
|
19
|
+
def import(params = {})
|
20
|
+
file_content = File.read(params[:file])
|
21
|
+
|
22
|
+
if file_content =~ /base64="false"/
|
23
|
+
error = "Burp input contains HTTP request / response data that hasn't been Base64-encoded.\n"
|
24
|
+
error << 'Please re-export your scanner results making sure the Base-64 encode option is selected.'
|
25
|
+
|
26
|
+
logger.fatal{ error }
|
27
|
+
content_service.create_note text: error
|
28
|
+
return false
|
29
|
+
end
|
30
|
+
|
31
|
+
logger.info { 'Parsing Burp Scanner XML output file...' }
|
32
|
+
doc = Nokogiri::XML(file_content)
|
33
|
+
logger.info { 'Done.' }
|
34
|
+
|
35
|
+
if doc.root.name != 'issues'
|
36
|
+
error = "Document doesn't seem to be in the Burp Scanner XML format."
|
37
|
+
logger.fatal { error }
|
38
|
+
content_service.create_note text: error
|
39
|
+
return false
|
40
|
+
end
|
41
|
+
|
42
|
+
# This will be filled in by the Processor while iterating over the issues
|
43
|
+
@hosts = []
|
44
|
+
@affected_host = nil
|
45
|
+
@issue_text = nil
|
46
|
+
@evidence_text = nil
|
47
|
+
|
48
|
+
doc.xpath('issues/issue').each do |xml_issue|
|
49
|
+
process_issue(xml_issue)
|
50
|
+
end
|
51
|
+
|
52
|
+
logger.info { 'Burp Scanner results successfully imported' }
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
# Creates the Nodes/properties
|
57
|
+
def process_issue(xml_issue)
|
58
|
+
host_label = xml_issue.at('host')['ip']
|
59
|
+
host_label = xml_issue.at('host').text if host_label.empty?
|
60
|
+
affected_host = content_service.create_node(label: host_label, type: :host)
|
61
|
+
logger.info { "\taffects: #{host_label}" }
|
62
|
+
|
63
|
+
unless @hosts.include?(affected_host.label)
|
64
|
+
@hosts << affected_host.label
|
65
|
+
url = xml_issue.at('host').text
|
66
|
+
affected_host.set_property(:hostname, url)
|
67
|
+
affected_host.save
|
68
|
+
end
|
69
|
+
|
70
|
+
# Burp extensions don't follow the "unique type for every Issue" logic
|
71
|
+
# so we have to deal with them separately
|
72
|
+
burp_extension_type = '134217728'.freeze
|
73
|
+
if xml_issue.at('type').text.to_str == burp_extension_type
|
74
|
+
process_extension_issues(affected_host, xml_issue)
|
75
|
+
else
|
76
|
+
process_burp_issues(affected_host, xml_issue)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# If the Issues come from the Burp app, use the type as the plugin_ic
|
81
|
+
def process_burp_issues(affected_host, xml_issue)
|
82
|
+
issue_name = xml_issue.at('name').text
|
83
|
+
issue_type = xml_issue.at('type').text.to_i
|
84
|
+
|
85
|
+
logger.info { "Adding #{issue_name} (#{issue_type})" }
|
86
|
+
|
87
|
+
create_issue(
|
88
|
+
affected_host: affected_host,
|
89
|
+
id: issue_type,
|
90
|
+
xml_issue: xml_issue
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
# If the Issues come from a Burp extension (type = 134217728), then
|
95
|
+
# use the name (spaces removed) as the plugin_id
|
96
|
+
def process_extension_issues(affected_host, xml_issue)
|
97
|
+
ext_name = xml_issue.at('name').text
|
98
|
+
ext_name = ext_name.gsub!(" ", "")
|
99
|
+
|
100
|
+
logger.info { "Adding #{ext_name}" }
|
101
|
+
|
102
|
+
create_issue(
|
103
|
+
affected_host: affected_host,
|
104
|
+
id: ext_name,
|
105
|
+
xml_issue: xml_issue
|
106
|
+
)
|
107
|
+
end
|
108
|
+
|
109
|
+
def create_issue(affected_host:, id:, xml_issue:)
|
110
|
+
issue_text =
|
111
|
+
template_service.process_template(
|
112
|
+
template: 'issue',
|
113
|
+
data: xml_issue
|
114
|
+
)
|
115
|
+
|
116
|
+
if issue_text.include?(::Burp::INVALID_UTF_REPLACE)
|
117
|
+
logger.info do
|
118
|
+
"\tdetected invalid UTF-8 bytes in your issue. " \
|
119
|
+
"Replacing them with '#{::Burp::INVALID_UTF_REPLACE}'."
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
issue = content_service.create_issue(text: issue_text, id: id)
|
124
|
+
|
125
|
+
logger.info do
|
126
|
+
"\tadding evidence for this instance to #{affected_host.label}."
|
127
|
+
end
|
128
|
+
|
129
|
+
evidence_text =
|
130
|
+
template_service.process_template(
|
131
|
+
template: 'evidence',
|
132
|
+
data: xml_issue
|
133
|
+
)
|
134
|
+
|
135
|
+
if evidence_text.include?(::Burp::INVALID_UTF_REPLACE)
|
136
|
+
logger.info do
|
137
|
+
"\tdetected invalid UTF-8 bytes in your evidence. " \
|
138
|
+
"Replacing them with '#{::Burp::INVALID_UTF_REPLACE}'."
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
content_service.create_evidence(
|
143
|
+
issue: issue,
|
144
|
+
node: affected_host,
|
145
|
+
content: evidence_text
|
146
|
+
)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
data/lib/tasks/thorfile.rb
CHANGED
@@ -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
|
12
|
+
exit(-1)
|
13
13
|
end
|
14
14
|
|
15
15
|
detect_and_set_project_scope
|
16
16
|
|
17
|
-
importer =
|
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
|
|
data/spec/burp_upload_spec.rb
CHANGED
@@ -3,17 +3,17 @@ require 'ostruct'
|
|
3
3
|
|
4
4
|
describe 'Burp upload plugin' do
|
5
5
|
|
6
|
-
describe Burp::Issue do
|
6
|
+
describe Burp::Xml::Issue do
|
7
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
|
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),
|
@@ -113,4 +113,68 @@ describe 'Burp upload plugin' do
|
|
113
113
|
end
|
114
114
|
|
115
115
|
end
|
116
|
+
|
117
|
+
describe Dradis::Plugins::Burp::Html::Importer do
|
118
|
+
before(:each) do
|
119
|
+
# Stub template service
|
120
|
+
templates_dir = File.expand_path('../../templates', __FILE__)
|
121
|
+
expect_any_instance_of(Dradis::Plugins::TemplateService)
|
122
|
+
.to receive(:default_templates_dir).and_return(templates_dir)
|
123
|
+
|
124
|
+
# Init services
|
125
|
+
plugin = Dradis::Plugins::Burp::Html
|
126
|
+
|
127
|
+
@content_service = Dradis::Plugins::ContentService::Base.new(
|
128
|
+
logger: Logger.new(STDOUT),
|
129
|
+
plugin: plugin
|
130
|
+
)
|
131
|
+
|
132
|
+
@importer = plugin::Importer.new(
|
133
|
+
content_service: @content_service,
|
134
|
+
)
|
135
|
+
|
136
|
+
# Stub dradis-plugins methods
|
137
|
+
#
|
138
|
+
# They return their argument hashes as objects mimicking
|
139
|
+
# Nodes, Issues, etc
|
140
|
+
allow(@content_service).to receive(:create_node) do |args|
|
141
|
+
obj = OpenStruct.new(args)
|
142
|
+
obj.define_singleton_method(:set_property) { |_, __| }
|
143
|
+
obj
|
144
|
+
end
|
145
|
+
allow(@content_service).to receive(:create_issue) do |args|
|
146
|
+
OpenStruct.new(args)
|
147
|
+
end
|
148
|
+
allow(@content_service).to receive(:create_evidence) do |args|
|
149
|
+
OpenStruct.new(args)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
it "creates nodes, issues, and evidence as needed" do
|
154
|
+
|
155
|
+
# Host node
|
156
|
+
#
|
157
|
+
# create_node should be called once for each issue in the xml,
|
158
|
+
# but ContentService knows it's already created and NOOPs
|
159
|
+
expect(@content_service).to receive(:create_node)
|
160
|
+
.with(hash_including label: 'github.com/dradis/dradis-burp')
|
161
|
+
.exactly(1).times
|
162
|
+
|
163
|
+
# # create_issue should be called once for each issue in the xml
|
164
|
+
expect(@content_service).to receive(:create_issue) do |args|
|
165
|
+
expect(args[:text]).to include("#[Title]#\nStrict transport security not enforced")
|
166
|
+
expect(args[:id]).to eq(16777984)
|
167
|
+
OpenStruct.new(args)
|
168
|
+
end.once
|
169
|
+
expect(@content_service).to receive(:create_evidence) do |args|
|
170
|
+
expect(args[:content]).to include("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
171
|
+
expect(args[:issue].text).to include("#[Title]#\nStrict transport security not enforced")
|
172
|
+
expect(args[:node].label).to eq("github.com/dradis/dradis-burp")
|
173
|
+
end.once
|
174
|
+
|
175
|
+
# Run the import
|
176
|
+
@importer.import(file: 'spec/fixtures/files/burp.html')
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
116
180
|
end
|