dradis-burp 3.18.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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.github/issue_template.md +16 -0
  3. data/.github/pull_request_template.md +36 -0
  4. data/.gitignore +10 -0
  5. data/.rspec +2 -0
  6. data/CHANGELOG.md +57 -0
  7. data/CONTRIBUTING.md +3 -0
  8. data/Gemfile +23 -0
  9. data/LICENSE +339 -0
  10. data/README.md +29 -0
  11. data/Rakefile +1 -0
  12. data/dradis-burp.gemspec +34 -0
  13. data/lib/burp/html/issue.rb +157 -0
  14. data/lib/burp/issue.rb +43 -0
  15. data/lib/burp/xml/issue.rb +127 -0
  16. data/lib/dradis-burp.rb +10 -0
  17. data/lib/dradis/plugins/burp.rb +12 -0
  18. data/lib/dradis/plugins/burp/engine.rb +25 -0
  19. data/lib/dradis/plugins/burp/field_processor.rb +27 -0
  20. data/lib/dradis/plugins/burp/gem_version.rb +19 -0
  21. data/lib/dradis/plugins/burp/html/importer.rb +144 -0
  22. data/lib/dradis/plugins/burp/version.rb +13 -0
  23. data/lib/dradis/plugins/burp/xml/importer.rb +144 -0
  24. data/lib/tasks/thorfile.rb +30 -0
  25. data/spec/burp_upload_spec.rb +220 -0
  26. data/spec/fixtures/files/burp.html +229 -0
  27. data/spec/fixtures/files/burp.xml +100 -0
  28. data/spec/fixtures/files/burp_issue_severity.xml +118 -0
  29. data/spec/fixtures/files/invalid-utf-issue.xml +21 -0
  30. data/spec/fixtures/files/without-base64.xml +709 -0
  31. data/spec/spec_helper.rb +9 -0
  32. data/templates/evidence.fields +8 -0
  33. data/templates/evidence.sample +76 -0
  34. data/templates/evidence.template +20 -0
  35. data/templates/html_evidence.fields +13 -0
  36. data/templates/html_evidence.sample +36 -0
  37. data/templates/html_evidence.template +50 -0
  38. data/templates/issue.fields +8 -0
  39. data/templates/issue.sample +23 -0
  40. data/templates/issue.template +30 -0
  41. metadata +174 -0
@@ -0,0 +1,27 @@
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 =
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
13
+ end
14
+
15
+ def value(args={})
16
+ field = args[:field]
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
+ @burp_object.try(name) || 'n/a'
22
+ end
23
+
24
+ end
25
+ end
26
+ end
27
+ end
@@ -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 = 18
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,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,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,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
@@ -0,0 +1,30 @@
1
+ class BurpTasks < Thor
2
+ include Rails.application.config.dradis.thor_helper_module
3
+
4
+ namespace "dradis:plugins:burp"
5
+
6
+ desc "upload FILE", "upload Burp XML or HTML results"
7
+ def upload(file_path)
8
+ require 'config/environment'
9
+
10
+ unless File.exists?(file_path)
11
+ $stderr.puts "** the file [#{file_path}] does not exist"
12
+ exit(-1)
13
+ end
14
+
15
+ detect_and_set_project_scope
16
+
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
+
27
+ importer.import(file: file_path)
28
+ end
29
+
30
+ end
@@ -0,0 +1,220 @@
1
+ require 'spec_helper'
2
+ require 'ostruct'
3
+
4
+ describe 'Burp upload plugin' do
5
+
6
+ describe Burp::Xml::Issue do
7
+ it 'handles invalid utf-8 bytes' do
8
+ doc = Nokogiri::XML(File.read('spec/fixtures/files/invalid-utf-issue.xml'))
9
+ xml_issue = doc.xpath('issues/issue').first
10
+ issue = Burp::Xml::Issue.new(xml_issue)
11
+
12
+ expect{ issue.request.encode('utf-8') }.to_not raise_error
13
+ end
14
+ end
15
+
16
+ describe Dradis::Plugins::Burp::Xml::Importer do
17
+ before(:each) do
18
+ # Stub template service
19
+ templates_dir = File.expand_path('../../templates', __FILE__)
20
+ expect_any_instance_of(Dradis::Plugins::TemplateService)
21
+ .to receive(:default_templates_dir).and_return(templates_dir)
22
+
23
+ # Init services
24
+ plugin = Dradis::Plugins::Burp::Xml
25
+
26
+ @content_service = Dradis::Plugins::ContentService::Base.new(
27
+ logger: Logger.new(STDOUT),
28
+ plugin: plugin
29
+ )
30
+
31
+ @importer = plugin::Importer.new(
32
+ content_service: @content_service,
33
+ )
34
+
35
+ # Stub dradis-plugins methods
36
+ #
37
+ # They return their argument hashes as objects mimicking
38
+ # Nodes, Issues, etc
39
+ allow(@content_service).to receive(:create_node) do |args|
40
+ obj = OpenStruct.new(args)
41
+ obj.define_singleton_method(:set_property) { |_, __| }
42
+ obj
43
+ end
44
+ allow(@content_service).to receive(:create_issue) do |args|
45
+ OpenStruct.new(args)
46
+ end
47
+ allow(@content_service).to receive(:create_evidence) do |args|
48
+ OpenStruct.new(args)
49
+ end
50
+ end
51
+
52
+ it 'creates nodes, issues, and evidence as needed' do
53
+
54
+ # Host node
55
+ #
56
+ # create_node should be called once for each issue in the xml,
57
+ # but ContentService knows it's already created and NOOPs
58
+ expect(@content_service).to receive(:create_node)
59
+ .with(hash_including label: '10.0.0.1')
60
+ .exactly(4).times
61
+
62
+ # create_issue should be called once for each issue in the xml
63
+ expect(@content_service).to receive(:create_issue) do |args|
64
+ expect(args[:text]).to include("#[Title]#\nIssue 1")
65
+ expect(args[:id]).to eq(8781630)
66
+ OpenStruct.new(args)
67
+ end.once
68
+ expect(@content_service).to receive(:create_evidence) do |args|
69
+ expect(args[:content]).to include("Lorem ipsum dolor sit amet")
70
+ expect(args[:issue].text).to include("#[Title]#\nIssue 1")
71
+ expect(args[:node].label).to eq("10.0.0.1")
72
+ end.once
73
+
74
+ expect(@content_service).to receive(:create_issue) do |args|
75
+ expect(args[:text]).to include("#[Title]#\nIssue 2")
76
+ expect(args[:id]).to eq(8781631)
77
+ OpenStruct.new(args)
78
+ end.once
79
+ expect(@content_service).to receive(:create_evidence) do |args|
80
+ expect(args[:content]).to include("Lorem ipsum dolor sit amet")
81
+ expect(args[:issue].text).to include("#[Title]#\nIssue 2")
82
+ expect(args[:node].label).to eq("10.0.0.1")
83
+ end.once
84
+
85
+ # Issue 3 is an Extension finding so we need to confirm
86
+ # that it triggers process_extension_issues instead of process_burp_issues
87
+ # and the plugin_id is not set to the Type (134217728)
88
+ expect(@content_service).to receive(:create_issue) do |args|
89
+ expect(args[:text]).to include("#[Title]#\nIssue 3")
90
+ expect(args[:id]).to eq("Issue3")
91
+ OpenStruct.new(args)
92
+ end.once
93
+ expect(@content_service).to receive(:create_evidence) do |args|
94
+ expect(args[:content]).to include("Lorem ipsum dolor sit amet")
95
+ expect(args[:issue].text).to include("#[Title]#\nIssue 3")
96
+ expect(args[:node].label).to eq("10.0.0.1")
97
+ end.once
98
+
99
+ expect(@content_service).to receive(:create_issue) do |args|
100
+ expect(args[:text]).to include("#[Title]#\nIssue 4")
101
+ expect(args[:id]).to eq(8781633)
102
+ OpenStruct.new(args)
103
+ end.once
104
+ expect(@content_service).to receive(:create_evidence) do |args|
105
+ expect(args[:content]).to include("Lorem ipsum dolor sit amet")
106
+ expect(args[:issue].text).to include("#[Title]#\nIssue 4")
107
+ expect(args[:node].label).to eq("10.0.0.1")
108
+ end.once
109
+
110
+
111
+ # Run the import
112
+ @importer.import(file: 'spec/fixtures/files/burp.xml')
113
+ end
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
+
219
+ end
220
+ end