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.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.rspec +2 -0
- data/CONTRIBUTING.md +3 -0
- data/Gemfile +23 -0
- data/LICENSE +339 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/dradis-burp.gemspec +34 -0
- data/lib/burp/issue.rb +149 -0
- data/lib/dradis-burp.rb +8 -0
- data/lib/dradis/plugins/burp.rb +11 -0
- data/lib/dradis/plugins/burp/engine.rb +22 -0
- data/lib/dradis/plugins/burp/field_processor.rb +23 -0
- data/lib/dradis/plugins/burp/gem_version.rb +19 -0
- data/lib/dradis/plugins/burp/importer.rb +79 -0
- data/lib/dradis/plugins/burp/version.rb +13 -0
- data/lib/tasks/thorfile.rb +40 -0
- data/spec/burp_upload_spec.rb +108 -0
- data/spec/fixtures/files/burp.xml +100 -0
- data/spec/spec_helper.rb +9 -0
- data/templates/evidence.fields +7 -0
- data/templates/evidence.sample +76 -0
- data/templates/evidence.template +26 -0
- data/templates/issue.fields +5 -0
- data/templates/issue.sample +12 -0
- data/templates/issue.template +18 -0
- metadata +158 -0
data/lib/burp/issue.rb
ADDED
@@ -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!(/"/, '"')
|
94
|
+
result.gsub!(/&/, '&')
|
95
|
+
result.gsub!(/</, '<')
|
96
|
+
result.gsub!(/>/, '>')
|
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/,'�')
|
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
|
data/lib/dradis-burp.rb
ADDED
@@ -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,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
|