dradis-nexpose 4.12.0 → 4.14.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b5b342a73c3a4576df7bd68d24142393480c9abed09cd8f7a8c2f9aefe67339e
4
- data.tar.gz: 4d24a2ed95a57a001e9f909740fd95067332065804d282598c2b86d31db6b644
3
+ metadata.gz: fed2206ffe5931c65cc3b1ae1c9007091b99b3f9134a9b81b4ae8b4d5496fc5f
4
+ data.tar.gz: bf34162969ecdd0d105d551149b51f7c20795a2bfa9b1f97c741b36e5711cfe5
5
5
  SHA512:
6
- metadata.gz: '0080d88c9b79f02f1a9610d85aa350f26e3c059fae144c15a1ccb66c6acf69e69e076063c2d7451915e9a625892f1fc9a519f099aed3e9577edfa4278cb24b5d'
7
- data.tar.gz: 9231e9e9a60fe0b06316f024b0f089c76bda3c4bcb4000f0eb4079da893ee295daf59cfabe5f47b85b6f989f2e72b1f87ed988c499ccc64a880a29c9a52e8bbd
6
+ metadata.gz: 9fa49ed385686a5910de33e05f09caafbb624e5227af713c5defd4d0485b38b0d446194bd652b03b77d43861440e1d08a2eb597ea7b13b2f8ccae9a8f1440510
7
+ data.tar.gz: d84b92cc9d54926f788d78dda9b87e7f1a4262ad7682278f72af62dc4917ca4877ea9e0a8754a3d4a117bee44633dbd4ce2e539e197069b523a5c7615b2d1acf
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ v4.14.0 (October 2024)
2
+ - Separate general importer into Full & Simple importers
3
+
4
+ v4.13.0 (July 2024)
5
+ - No changes
6
+
1
7
  v4.12.0 (May 2024)
2
8
  - Migrate integration to use Mappings Manager
3
9
  - Update Dradis links in README
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.add_dependency 'dradis-plugins', '~> 4.0'
27
27
  spec.add_dependency 'nokogiri', '~> 1.3'
28
28
 
29
- spec.add_development_dependency 'bundler'
29
+ spec.add_development_dependency 'bundler', '~> 2.0'
30
30
  spec.add_development_dependency 'rake', '~> 10.0'
31
31
  spec.add_development_dependency 'rspec-rails'
32
32
  spec.add_development_dependency 'combustion', '~> 0.5.2'
@@ -5,5 +5,17 @@ module Dradis::Plugins::Nexpose
5
5
  include ::Dradis::Plugins::Base
6
6
  description 'Processes Nexpose XML format'
7
7
  provides :upload
8
+
9
+ # Because this plugin provides two export modules, we have to overwrite
10
+ # the default .uploaders() method.
11
+ #
12
+ # See:
13
+ # Dradis::Plugins::Upload::Base in dradis-plugins
14
+ def self.uploaders
15
+ [
16
+ Dradis::Plugins::Nexpose::Full,
17
+ Dradis::Plugins::Nexpose::Simple
18
+ ]
19
+ end
8
20
  end
9
21
  end
@@ -0,0 +1,204 @@
1
+ module Dradis::Plugins::Nexpose
2
+ module Full
3
+ def self.meta
4
+ package = Dradis::Plugins::Nexpose
5
+ {
6
+ name: package::Engine::plugin_name,
7
+ description: 'Upload Full NeXpose output file (.xml)',
8
+ version: package.version
9
+ }
10
+ end
11
+
12
+ class Importer < Dradis::Plugins::Upload::Importer
13
+
14
+ def self.templates
15
+ { evidence: 'full_evidence', issue: 'full_vulnerability' }
16
+ end
17
+
18
+ def initialize(args = {})
19
+ args[:plugin] = Dradis::Plugins::Nexpose
20
+ super(args)
21
+ end
22
+
23
+ # The framework will call this function if the user selects this plugin from
24
+ # the dropdown list and uploads a file.
25
+ # @returns true if the operation was successful, false otherwise
26
+ def import(params = {})
27
+ file_content = File.read(params[:file])
28
+
29
+ logger.info { 'Parsing NeXpose-Full XML output file...' }
30
+ doc = Nokogiri::XML(file_content)
31
+ logger.info { 'Done.' }
32
+
33
+ unless doc.root.name == 'NexposeReport'
34
+ error = "The document doesn't seem to be in NeXpose-Full XML format. Ensure you uploaded a NeXpose-Full XML report."
35
+ logger.fatal{ error }
36
+ content_service.create_note text: error
37
+ return false
38
+ end
39
+
40
+ process_full(doc)
41
+
42
+ logger.info { 'NeXpose-Full format successfully imported' }
43
+ true
44
+ end
45
+
46
+ private
47
+
48
+ def process_full(doc)
49
+ note_text = nil
50
+
51
+ @vuln_list = []
52
+ evidence = Hash.new { |h, k| h[k] = {} }
53
+
54
+ # First, extract scans
55
+ scan_node = content_service.create_node(label: 'Nexpose Scan Summary')
56
+ logger.info { "\tProcessing scan summary" }
57
+
58
+ doc.xpath('//scans/scan').each do |xml_scan|
59
+ note_text = mapping_service.apply_mapping(source: 'full_scan', data: xml_scan)
60
+ content_service.create_note(node: scan_node, text: note_text)
61
+ end
62
+
63
+ # Second, we parse the nodes
64
+ doc.xpath('//nodes/node').each do |xml_node|
65
+ nexpose_node = Nexpose::Node.new(xml_node)
66
+
67
+ host_node = content_service.create_node(label: nexpose_node.address, type: :host)
68
+ logger.info { "\tProcessing host: #{nexpose_node.address}" }
69
+
70
+ # add the summary note for this host
71
+ note_text = mapping_service.apply_mapping(source: 'full_node', data: nexpose_node)
72
+ content_service.create_note(node: host_node, text: note_text)
73
+
74
+ if host_node.respond_to?(:properties)
75
+ logger.info { "\tAdding host properties to #{nexpose_node.address}" }
76
+ host_node.set_property(:ip, nexpose_node.address)
77
+ host_node.set_property(:hostname, nexpose_node.names)
78
+ host_node.set_property(:os, nexpose_node.fingerprints)
79
+ host_node.set_property(:risk_score, nexpose_node.risk_score)
80
+ host_node.save
81
+ end
82
+
83
+ # inject this node's address into any vulnerabilities identified
84
+ #
85
+ # TODO: There is room for improvement here, we could have a hash that
86
+ # linked vulns with test/service and host to create proper content for
87
+ # Evidence.
88
+ nexpose_node.tests.each do |node_test|
89
+ test_id = node_test[:id].to_s.downcase
90
+
91
+ # We can't use the straightforward version below because Nexpose uses
92
+ # mixed-case some times (!)
93
+ # xml_vuln = doc.xpath("//VulnerabilityDefinitions/vulnerability[@id='#{node_test[:id]}']").first
94
+ # See:
95
+ # http://stackoverflow.com/questions/1625446/problem-with-upper-case-and-lower-case-xpath-functions-in-selenium-ide/1625859#1625859
96
+ xml_vuln = doc.xpath("//VulnerabilityDefinitions/vulnerability[translate(@id,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='#{test_id}']").first
97
+ xml_vuln.add_child('<hosts/>') unless xml_vuln.last_element_child.name == 'hosts'
98
+
99
+ if xml_vuln.xpath("./hosts/host[text()='#{nexpose_node.address}']").empty?
100
+ xml_vuln.last_element_child.add_child("<host>#{nexpose_node.address}</host>")
101
+ end
102
+
103
+ evidence[test_id][nexpose_node.address] ||= []
104
+ evidence[test_id][nexpose_node.address] << node_test
105
+ end
106
+
107
+ nexpose_node.endpoints.each do |endpoint|
108
+ # endpoint_node = content_service.create_node(label: endpoint.label, parent: host_node)
109
+ logger.info { "\t\tEndpoint: #{endpoint.label}" }
110
+
111
+ if host_node.respond_to?(:properties)
112
+ logger.info { "\t\tAdding to Services table" }
113
+ host_node.set_service(
114
+ port: endpoint.port.to_i,
115
+ protocol: endpoint.protocol,
116
+ state: endpoint.status,
117
+ name: endpoint.services.map(&:name).join(', '),
118
+ source: :nexpose,
119
+ # reason: port.reason,
120
+ # product: port.try('service').try('product'),
121
+ # version: port.try('service').try('version')
122
+ )
123
+ end
124
+
125
+ endpoint.services.each do |service|
126
+
127
+ # add the summary note for this service
128
+ note_text = mapping_service.apply_mapping(source: 'full_service', data: service)
129
+ # content_service.create_note(node: endpoint_node, text: note_text)
130
+ content_service.create_note(node: host_node, text: note_text)
131
+
132
+ # inject this node's address into any vulnerabilities identified
133
+ service.tests.each do |service_test|
134
+ test_id = service_test[:id].to_s.downcase
135
+
136
+ # For some reason Nexpose fails to include the 'http-iis-0011' vulnerability definition
137
+ next if test_id == 'http-iis-0011'
138
+
139
+ # We can't use the straightforward version below because Nexpose uses
140
+ # mixed-case some times (!)
141
+ # xml_vuln = doc.xpath("//VulnerabilityDefinitions/vulnerability[@id='#{service_test[:id]}']").first
142
+ # See:
143
+ # http://stackoverflow.com/questions/1625446/problem-with-upper-case-and-lower-case-xpath-functions-in-selenium-ide/1625859#1625859
144
+ #
145
+ xml_vuln = doc.xpath("//VulnerabilityDefinitions/vulnerability[translate(@id,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='#{test_id}']").first
146
+ xml_vuln.add_child('<hosts/>') unless xml_vuln.last_element_child.name == 'hosts'
147
+
148
+ if xml_vuln.xpath("./hosts/host[text()='#{nexpose_node.address}']").empty?
149
+ xml_vuln.last_element_child.add_child("<host>#{nexpose_node.address}</host>")
150
+ end
151
+
152
+ evidence[test_id][nexpose_node.address] ||= []
153
+ evidence[test_id][nexpose_node.address] << service_test
154
+ end
155
+ end
156
+ end
157
+
158
+ # add note under this node for each vulnerable ./node/test/
159
+ host_node.save
160
+ end
161
+
162
+ # Third, parse vulnerability definitions
163
+ logger.info { "\tProcessing issue definitions:" }
164
+
165
+ doc.xpath('//VulnerabilityDefinitions/vulnerability').each do |xml_vulnerability|
166
+ id = xml_vulnerability['id'].downcase
167
+ # if @vuln_list.include?(id)
168
+ issue_text = mapping_service.apply_mapping(
169
+ source: 'full_vulnerability',
170
+ data: xml_vulnerability
171
+ )
172
+
173
+ # retrieve hosts affected by this issue (injected in step 2)
174
+ #
175
+ # There is no need for the below as Issues are linked to hosts via the
176
+ # corresponding Evidence instance
177
+ #
178
+ # note_text << "\n\n#[host]#\n"
179
+ # note_text << xml_vulnerability.xpath('./hosts/host').collect(&:text).join("\n")
180
+ # note_text << "\n\n"
181
+
182
+ # 3.1 create the Issue
183
+ issue = content_service.create_issue(text: issue_text, id: id)
184
+ logger.info { "\tIssue: #{issue.fields ? issue.fields['Title'] : id}" }
185
+
186
+ # 3.2 associate with the nodes via Evidence.
187
+ # TODO: there is room for improvement here by providing proper Evidence content
188
+ xml_vulnerability.xpath('./hosts/host').map(&:text).each do |host_name|
189
+ # if the node exists, this just returns it
190
+ host_node = content_service.create_node(label: host_name, type: :host)
191
+
192
+ evidence[id][host_name].each do |evidence|
193
+ evidence_content = mapping_service.apply_mapping(
194
+ source: 'full_evidence',
195
+ data: evidence
196
+ )
197
+ content_service.create_evidence(content: evidence_content, issue: issue, node: host_node)
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
@@ -8,7 +8,7 @@ module Dradis
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 4
11
- MINOR = 12
11
+ MINOR = 14
12
12
  TINY = 0
13
13
  PRE = nil
14
14
 
@@ -11,7 +11,8 @@ module Dradis::Plugins::Nexpose
11
11
  'Hostname' => '{{ nexpose[node.site_name] }}',
12
12
  'Details' => "Status: {{ nexpose[node.status] }}\nDevice id: {{ nexpose[node.device_id] }}\nHW address: {{ nexpose[node.hardware_address] }}",
13
13
  'Names' => '{{ nexpose[node.names] }}',
14
- 'Software' => '{{ nexpose[node.software] }}'
14
+ 'Software' => '{{ nexpose[node.software] }}',
15
+ 'Fingerprints' => '{{ nexpose[node.fingerprints] }}'
15
16
  },
16
17
  full_scan: {
17
18
  'Title' => '{{ nexpose[scan.name] }} ({{ nexpose[scan.scan_id] }})',
@@ -0,0 +1,117 @@
1
+ module Dradis::Plugins::Nexpose
2
+ module Simple
3
+ def self.meta
4
+ package = Dradis::Plugins::Nexpose
5
+ {
6
+ name: package::Engine::plugin_name,
7
+ description: 'Upload Simple NeXpose output file (.xml)',
8
+ version: package.version
9
+ }
10
+ end
11
+
12
+ class Importer < Dradis::Plugins::Upload::Importer
13
+ def self.templates
14
+ {}
15
+ end
16
+
17
+ def initialize(args = {})
18
+ args[:plugin] = Dradis::Plugins::Nexpose
19
+ super(args)
20
+ end
21
+
22
+ # The framework will call this function if the user selects this plugin from
23
+ # the dropdown list and uploads a file.
24
+ # @returns true if the operation was successful, false otherwise
25
+ def import(params = {})
26
+ file_content = File.read( params[:file] )
27
+
28
+ logger.info { 'Parsing NeXpose output file...' }
29
+ doc = Nokogiri::XML(file_content)
30
+ logger.info { 'Done.' }
31
+
32
+ unless doc.root.name == 'NeXposeSimpleXML'
33
+ error = "The document doesn't seem to be in either NeXpose-Simple or NeXpose-Full XML format. Ensure you uploaded a Nexpose XML report."
34
+ logger.fatal{ error }
35
+ content_service.create_note text: error
36
+ return false
37
+ end
38
+
39
+ process_simple(doc)
40
+
41
+ logger.info { 'NeXpose-Simple format uploaded successfully' }
42
+ true
43
+ end
44
+
45
+ private
46
+
47
+ def process_simple(doc)
48
+ hosts = process_nexpose_simple_xml(doc)
49
+ notes_simple(hosts)
50
+ end
51
+
52
+ def notes_simple(hosts)
53
+ return if hosts.nil?
54
+
55
+ hosts.each do |host|
56
+ host_node = content_service.create_node(label: host['address'], type: :host)
57
+ content_service.create_note node: host_node, text: "Host Description : #{host['description']} \nScanner Fingerprint certainty : #{host['fingerprint']}"
58
+
59
+ generic_findings_node = content_service.create_node(label: 'Generic Findings', parent: host_node)
60
+ host['generic_vulns'].each do |id, finding|
61
+ content_service.create_note node: generic_findings_node, text: "Finding ID : #{id} \n \n Finding Refs :\n-------\n #{finding}"
62
+ end
63
+
64
+ port_text = nil
65
+ host['ports'].each do |port_label, findings|
66
+ port_node = content_service.create_node(label: port_label, parent: host_node)
67
+
68
+ findings.each do |id, finding|
69
+ port_text = mapping_service.apply_mapping(source: 'simple_port', data: {id: id, finding: finding})
70
+ port_text << "\n#[host]#\n#{host['address']}\n\n"
71
+ content_service.create_note node: port_node, text: port_text
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def process_nexpose_simple_xml(doc)
78
+ results = doc.search('device')
79
+ hosts = Array.new
80
+ results.each do |host|
81
+ current_host = Hash.new
82
+ current_host['address'] = host['address']
83
+ current_host['fingerprint'] = host.search('fingerprint')[0].nil? ? "N/A" : host.search('fingerprint')[0]['certainty']
84
+ current_host['description'] = host.search('description')[0].nil? ? "N/A" : host.search('description')[0].text
85
+ #So there's two sets of vulns in a NeXpose simple XML report for each host
86
+ #Theres some generic ones at the top of the report
87
+ #And some service specific ones further down the report.
88
+ #So we need to get the generic ones before moving on
89
+ current_host['generic_vulns'] = Hash.new
90
+ host.xpath('vulnerabilities/vulnerability').each do |vuln|
91
+ current_host['generic_vulns'][vuln['id']] = ''
92
+ vuln.xpath('id').each do |id|
93
+ current_host['generic_vulns'][vuln['id']] << id['type'] + " : " + id.text + "\n"
94
+ end
95
+ end
96
+
97
+ current_host['ports'] = Hash.new
98
+ host.xpath('services/service').each do |service|
99
+ protocol = service['protocol']
100
+ portid = service['port']
101
+ port_label = protocol + '-' + portid
102
+ current_host['ports'][port_label] = Hash.new
103
+ service.xpath('vulnerabilities/vulnerability').each do |vuln|
104
+ current_host['ports'][port_label][vuln['id']] = ''
105
+ vuln.xpath('id').each do |id|
106
+ current_host['ports'][port_label][vuln['id']] << id['type'] + " : " + id.text + "\n"
107
+ end
108
+ end
109
+ end
110
+
111
+ hosts << current_host
112
+ end
113
+ return hosts
114
+ end
115
+ end
116
+ end
117
+ end
@@ -7,6 +7,7 @@ end
7
7
 
8
8
  require 'dradis/plugins/nexpose/engine'
9
9
  require 'dradis/plugins/nexpose/field_processor'
10
- require 'dradis/plugins/nexpose/importer'
10
+ require 'dradis/plugins/nexpose/full/importer'
11
11
  require 'dradis/plugins/nexpose/mapping'
12
+ require 'dradis/plugins/nexpose/simple/importer'
12
13
  require 'dradis/plugins/nexpose/version'
@@ -1,10 +1,27 @@
1
1
  class NexposeTasks < Thor
2
2
  include Rails.application.config.dradis.thor_helper_module
3
3
 
4
- namespace "dradis:plugins:nexpose"
4
+ namespace 'dradis:plugins:nexpose:upload'
5
5
 
6
- desc "upload FILE", "upload NeXpose results"
7
- def upload(file_path)
6
+ desc 'full FILE', 'upload NeXpose full results'
7
+ def full(file_path)
8
+ detect_and_set_project_scope
9
+
10
+ importer = Dradis::Plugins::Nexpose::Full::Importer.new(task_options)
11
+ importer.import(file: file_path)
12
+ end
13
+
14
+ desc 'simple FILE', 'upload NeXpose simple results'
15
+ def simple(file_path)
16
+ detect_and_set_project_scope
17
+
18
+ importer = Dradis::Plugins::Nexpose::Simple::Importer.new(task_options)
19
+ importer.import(file: file_path)
20
+ end
21
+
22
+ private
23
+
24
+ def process_upload(importer, file_path)
8
25
  require 'config/environment'
9
26
 
10
27
  unless File.exists?(file_path)
@@ -12,10 +29,6 @@ class NexposeTasks < Thor
12
29
  exit -1
13
30
  end
14
31
 
15
- detect_and_set_project_scope
16
-
17
- importer = Dradis::Plugins::Nexpose::Importer.new(task_options)
18
32
  importer.import(file: file_path)
19
33
  end
20
-
21
34
  end
@@ -0,0 +1,108 @@
1
+ # To run, execute from Dradis main app folder:
2
+ # bin/rspec [dradis-nexpose path]/spec/nexpose/full/importer_spec.rb
3
+ require 'rails_helper'
4
+ require 'ostruct'
5
+ require File.expand_path('../../../../../dradis-plugins/spec/support/spec_macros.rb', __FILE__)
6
+
7
+ include Dradis::Plugins::SpecMacros
8
+
9
+ module Dradis::Plugins
10
+ describe Nexpose::Full::Importer do
11
+ before do
12
+ @fixtures_dir = File.expand_path('../../../fixtures/files/', __FILE__)
13
+ end
14
+
15
+ before(:each) do
16
+ stub_content_service
17
+ @importer = described_class.new(content_service: @content_service)
18
+ end
19
+
20
+ def run_import!
21
+ @importer.import(file: @fixtures_dir + '/full.xml')
22
+ end
23
+
24
+ it 'creates nodes and associated notes as needed' do
25
+ expect(@content_service).to receive(:create_node).with(hash_including label: 'Nexpose Scan Summary').once
26
+
27
+ expect(@content_service).to receive(:create_note) do |args|
28
+ expect(args[:text]).to include("#[Title]#\nUSDA_Internal (4)")
29
+ expect(args[:node].label).to eq('Nexpose Scan Summary')
30
+ end.once
31
+
32
+ expect(@content_service).to receive(:create_node) do |args|
33
+ expect(args[:label]).to eq('1.1.1.1')
34
+ expect(args[:type]).to eq(:host)
35
+ create(:node, args.except(:type))
36
+ end
37
+
38
+ expect(@content_service).to receive(:create_note) do |args|
39
+ expect(args[:text]).to include("#[Title]#\n1.1.1.1")
40
+ expect(args[:text]).to include("#[Fingerprints]#\nIOS")
41
+ expect(args[:node].label).to eq('1.1.1.1')
42
+ end.once
43
+
44
+ expect(@content_service).to receive(:create_note) do |args|
45
+ expect(args[:text]).to include("#[Title]#\nService name: NTP")
46
+ expect(args[:node].label).to eq('1.1.1.1')
47
+ end.once
48
+
49
+ expect(@content_service).to receive(:create_note) do |args|
50
+ expect(args[:text]).to include("#[Title]#\nService name: SNMP")
51
+ expect(args[:node].label).to eq('1.1.1.1')
52
+ end.once
53
+
54
+ run_import!
55
+ end
56
+
57
+ it 'creates issues from vulnerability elements as needed' do
58
+ expect(@content_service).to receive(:create_issue) do |args|
59
+ expect(args[:text]).to include("#[Title]#\nApache HTTPD: error responses can expose cookies (CVE-2012-0053)")
60
+ expect(args[:id]).to eq('ntp-clock-variables-disclosure')
61
+ OpenStruct.new(args)
62
+ end.once
63
+
64
+ expect(@content_service).to receive(:create_issue) do |args|
65
+ expect(args[:text]).to include("#[Title]#\nApache HTTPD: ETag Inode Information Leakage (CVE-2003-1418)")
66
+ expect(args[:id]).to eq('test-02')
67
+ OpenStruct.new(args)
68
+ end.once
69
+
70
+ run_import!
71
+ end
72
+
73
+ it 'creates evidence as needed' do
74
+ expect(@content_service).to receive(:create_evidence) do |args|
75
+ expect(args[:content]).to include('The following NTP variables were found from a readvar')
76
+ expect(args[:issue].id).to eq('ntp-clock-variables-disclosure')
77
+ expect(args[:node].label).to eq('1.1.1.1')
78
+ end.once
79
+
80
+ expect(@content_service).to receive(:create_evidence) do |args|
81
+ expect(args[:content]).to include('Missing HTTP header "Content-Security-Policy"')
82
+ expect(args[:issue].id).to eq('test-02')
83
+ expect(args[:node].label).to eq('1.1.1.1')
84
+ end
85
+
86
+ run_import!
87
+ end
88
+
89
+ describe 'With duplicate nodes' do
90
+ it 'creates evidence for each instance of the node' do
91
+ expect(@content_service).to receive(:create_node).with(hash_including label: 'Nexpose Scan Summary').once
92
+ expect(@content_service).to receive(:create_node) do |args|
93
+ expect(args[:label]).to eq('1.1.1.1')
94
+ expect(args[:type]).to eq(:host)
95
+ create(:node, args.except(:type))
96
+ end
97
+
98
+ expect(@content_service).to receive(:create_evidence) do |args|
99
+ expect(args[:content]).to include("#[ID]#\nntp-clock-variables-disclosure\n\n")
100
+ expect(args[:issue].id).to eq('ntp-clock-variables-disclosure')
101
+ expect(args[:node].label).to eq('1.1.1.1')
102
+ end.twice
103
+
104
+ @importer.import(file: @fixtures_dir + '/full_with_duplicate_node.xml')
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,60 @@
1
+ # To run, execute from Dradis main app folder:
2
+ # bin/rspec [dradis-nexpose path]/spec/nexpose/simple/importer_spec.rb
3
+ require 'rails_helper'
4
+ require 'ostruct'
5
+ require File.expand_path('../../../../../dradis-plugins/spec/support/spec_macros.rb', __FILE__)
6
+
7
+ include Dradis::Plugins::SpecMacros
8
+
9
+ module Dradis::Plugins
10
+ describe Nexpose::Simple::Importer do
11
+ before do
12
+ @fixtures_dir = File.expand_path('../../../fixtures/files/', __FILE__)
13
+ end
14
+
15
+ before(:each) do
16
+ stub_content_service
17
+ @importer = described_class.new(content_service: @content_service)
18
+ end
19
+
20
+ def run_import!
21
+ @importer.import(file: @fixtures_dir + '/simple.xml')
22
+ end
23
+
24
+ it 'creates nodes and associated notes as needed' do
25
+ expect(@content_service).to receive(:create_node).with(hash_including label: '1.1.1.1', type: :host).once
26
+
27
+ expect(@content_service).to receive(:create_note) do |args|
28
+ expect(args[:text]).to include('Host Description : Linux 2.6.9-89.ELsmp')
29
+ expect(args[:text]).to include('Scanner Fingerprint certainty : 0.80')
30
+ expect(args[:node].label).to eq('1.1.1.1')
31
+ end.once
32
+
33
+ expect(@content_service).to receive(:create_node) do |args|
34
+ expect(args[:label]).to eq('Generic Findings')
35
+ expect(args[:parent].label).to eq('1.1.1.1')
36
+ OpenStruct.new(args)
37
+ end.once
38
+
39
+ expect(@content_service).to receive(:create_node) do |args|
40
+ expect(args[:label]).to eq('udp-000')
41
+ expect(args[:parent].label).to eq('1.1.1.1')
42
+ OpenStruct.new(args)
43
+ end.once
44
+
45
+ expect(@content_service).to receive(:create_note) do |args|
46
+ expect(args[:text]).to include("#[Id]#\nntpd-crypto")
47
+ expect(args[:text]).to include("#[host]#\n1.1.1.1")
48
+ expect(args[:node].label).to eq('udp-000')
49
+ end.once
50
+
51
+ expect(@content_service).to receive(:create_note) do |args|
52
+ expect(args[:text]).to include("#[Id]#\nntp-clock-radio")
53
+ expect(args[:text]).to include("#[host]#\n1.1.1.1")
54
+ expect(args[:node].label).to eq('udp-000')
55
+ end.once
56
+
57
+ run_import!
58
+ end
59
+ end
60
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dradis-nexpose
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.12.0
4
+ version: 4.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Martin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-05-07 00:00:00.000000000 Z
11
+ date: 2024-10-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dradis-plugins
@@ -42,16 +42,16 @@ dependencies:
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ">="
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '0'
47
+ version: '2.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ">="
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '0'
54
+ version: '2.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rake
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -117,11 +117,10 @@ files:
117
117
  - lib/dradis/plugins/nexpose.rb
118
118
  - lib/dradis/plugins/nexpose/engine.rb
119
119
  - lib/dradis/plugins/nexpose/field_processor.rb
120
- - lib/dradis/plugins/nexpose/formats/full.rb
121
- - lib/dradis/plugins/nexpose/formats/simple.rb
120
+ - lib/dradis/plugins/nexpose/full/importer.rb
122
121
  - lib/dradis/plugins/nexpose/gem_version.rb
123
- - lib/dradis/plugins/nexpose/importer.rb
124
122
  - lib/dradis/plugins/nexpose/mapping.rb
123
+ - lib/dradis/plugins/nexpose/simple/importer.rb
125
124
  - lib/dradis/plugins/nexpose/version.rb
126
125
  - lib/nexpose/endpoint.rb
127
126
  - lib/nexpose/node.rb
@@ -134,7 +133,8 @@ files:
134
133
  - spec/fixtures/files/full_with_duplicate_node.xml
135
134
  - spec/fixtures/files/simple.xml
136
135
  - spec/fixtures/files/ssl.xml
137
- - spec/nexpose_upload_spec.rb
136
+ - spec/nexpose/full/importer_spec.rb
137
+ - spec/nexpose/simple/importer_spec.rb
138
138
  - spec/spec_helper.rb
139
139
  - templates/full_evidence.sample
140
140
  - templates/full_node.sample
@@ -161,7 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
161
161
  - !ruby/object:Gem::Version
162
162
  version: '0'
163
163
  requirements: []
164
- rubygems_version: 3.1.4
164
+ rubygems_version: 3.5.6
165
165
  signing_key:
166
166
  specification_version: 4
167
167
  summary: Nexpose add-on for the Dradis Framework.
@@ -170,5 +170,6 @@ test_files:
170
170
  - spec/fixtures/files/full_with_duplicate_node.xml
171
171
  - spec/fixtures/files/simple.xml
172
172
  - spec/fixtures/files/ssl.xml
173
- - spec/nexpose_upload_spec.rb
173
+ - spec/nexpose/full/importer_spec.rb
174
+ - spec/nexpose/simple/importer_spec.rb
174
175
  - spec/spec_helper.rb
@@ -1,163 +0,0 @@
1
- module Dradis::Plugins::Nexpose::Formats
2
- # This module knows how to parse Nexpose Ful XML format.
3
- module Full
4
- private
5
-
6
- def process_full(doc)
7
- note_text = nil
8
-
9
- @vuln_list = []
10
- evidence = Hash.new { |h, k| h[k] = {} }
11
-
12
- # First, extract scans
13
- scan_node = content_service.create_node(label: 'Nexpose Scan Summary')
14
- logger.info { "\tProcessing scan summary" }
15
-
16
- doc.xpath('//scans/scan').each do |xml_scan|
17
- note_text = mapping_service.apply_mapping(source: 'full_scan', data: xml_scan)
18
- content_service.create_note(node: scan_node, text: note_text)
19
- end
20
-
21
- # Second, we parse the nodes
22
- doc.xpath('//nodes/node').each do |xml_node|
23
- nexpose_node = Nexpose::Node.new(xml_node)
24
-
25
- host_node = content_service.create_node(label: nexpose_node.address, type: :host)
26
- logger.info { "\tProcessing host: #{nexpose_node.address}" }
27
-
28
- # add the summary note for this host
29
- note_text = mapping_service.apply_mapping(source: 'full_node', data: nexpose_node)
30
- content_service.create_note(node: host_node, text: note_text)
31
-
32
- if host_node.respond_to?(:properties)
33
- logger.info { "\tAdding host properties to #{nexpose_node.address}" }
34
- host_node.set_property(:ip, nexpose_node.address)
35
- host_node.set_property(:hostname, nexpose_node.names)
36
- host_node.set_property(:os, nexpose_node.fingerprints)
37
- host_node.set_property(:risk_score, nexpose_node.risk_score)
38
- host_node.save
39
- end
40
-
41
- # inject this node's address into any vulnerabilities identified
42
- #
43
- # TODO: There is room for improvement here, we could have a hash that
44
- # linked vulns with test/service and host to create proper content for
45
- # Evidence.
46
- nexpose_node.tests.each do |node_test|
47
- test_id = node_test[:id].to_s.downcase
48
-
49
- # We can't use the straightforward version below because Nexpose uses
50
- # mixed-case some times (!)
51
- # xml_vuln = doc.xpath("//VulnerabilityDefinitions/vulnerability[@id='#{node_test[:id]}']").first
52
- # See:
53
- # http://stackoverflow.com/questions/1625446/problem-with-upper-case-and-lower-case-xpath-functions-in-selenium-ide/1625859#1625859
54
- xml_vuln = doc.xpath("//VulnerabilityDefinitions/vulnerability[translate(@id,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='#{test_id}']").first
55
- xml_vuln.add_child('<hosts/>') unless xml_vuln.last_element_child.name == 'hosts'
56
-
57
- if xml_vuln.xpath("./hosts/host[text()='#{nexpose_node.address}']").empty?
58
- xml_vuln.last_element_child.add_child("<host>#{nexpose_node.address}</host>")
59
- end
60
-
61
- evidence[test_id][nexpose_node.address] ||= []
62
- evidence[test_id][nexpose_node.address] << node_test
63
- end
64
-
65
- nexpose_node.endpoints.each do |endpoint|
66
- # endpoint_node = content_service.create_node(label: endpoint.label, parent: host_node)
67
- logger.info { "\t\tEndpoint: #{endpoint.label}" }
68
-
69
- if host_node.respond_to?(:properties)
70
- logger.info { "\t\tAdding to Services table" }
71
- host_node.set_service(
72
- port: endpoint.port.to_i,
73
- protocol: endpoint.protocol,
74
- state: endpoint.status,
75
- name: endpoint.services.map(&:name).join(', '),
76
- source: :nexpose,
77
- # reason: port.reason,
78
- # product: port.try('service').try('product'),
79
- # version: port.try('service').try('version')
80
- )
81
- end
82
-
83
- endpoint.services.each do |service|
84
-
85
- # add the summary note for this service
86
- note_text = mapping_service.apply_mapping(source: 'full_service', data: service)
87
- # content_service.create_note(node: endpoint_node, text: note_text)
88
- content_service.create_note(node: host_node, text: note_text)
89
-
90
- # inject this node's address into any vulnerabilities identified
91
- service.tests.each do |service_test|
92
- test_id = service_test[:id].to_s.downcase
93
-
94
- # For some reason Nexpose fails to include the 'http-iis-0011' vulnerability definition
95
- next if test_id == 'http-iis-0011'
96
-
97
- # We can't use the straightforward version below because Nexpose uses
98
- # mixed-case some times (!)
99
- # xml_vuln = doc.xpath("//VulnerabilityDefinitions/vulnerability[@id='#{service_test[:id]}']").first
100
- # See:
101
- # http://stackoverflow.com/questions/1625446/problem-with-upper-case-and-lower-case-xpath-functions-in-selenium-ide/1625859#1625859
102
- #
103
- xml_vuln = doc.xpath("//VulnerabilityDefinitions/vulnerability[translate(@id,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='#{test_id}']").first
104
- xml_vuln.add_child('<hosts/>') unless xml_vuln.last_element_child.name == 'hosts'
105
-
106
- if xml_vuln.xpath("./hosts/host[text()='#{nexpose_node.address}']").empty?
107
- xml_vuln.last_element_child.add_child("<host>#{nexpose_node.address}</host>")
108
- end
109
-
110
- evidence[test_id][nexpose_node.address] ||= []
111
- evidence[test_id][nexpose_node.address] << service_test
112
- end
113
- end
114
- end
115
-
116
- # add note under this node for each vulnerable ./node/test/
117
- host_node.save
118
- end
119
-
120
- # Third, parse vulnerability definitions
121
- logger.info { "\tProcessing issue definitions:" }
122
-
123
- doc.xpath('//VulnerabilityDefinitions/vulnerability').each do |xml_vulnerability|
124
- id = xml_vulnerability['id'].downcase
125
- # if @vuln_list.include?(id)
126
- issue_text = mapping_service.apply_mapping(
127
- source: 'full_vulnerability',
128
- data: xml_vulnerability
129
- )
130
-
131
- # retrieve hosts affected by this issue (injected in step 2)
132
- #
133
- # There is no need for the below as Issues are linked to hosts via the
134
- # corresponding Evidence instance
135
- #
136
- # note_text << "\n\n#[host]#\n"
137
- # note_text << xml_vulnerability.xpath('./hosts/host').collect(&:text).join("\n")
138
- # note_text << "\n\n"
139
-
140
- # 3.1 create the Issue
141
- issue = content_service.create_issue(text: issue_text, id: id)
142
- logger.info { "\tIssue: #{issue.fields ? issue.fields['Title'] : id}" }
143
-
144
- # 3.2 associate with the nodes via Evidence.
145
- # TODO: there is room for improvement here by providing proper Evidence content
146
- xml_vulnerability.xpath('./hosts/host').map(&:text).each do |host_name|
147
- # if the node exists, this just returns it
148
- host_node = content_service.create_node(label: host_name, type: :host)
149
-
150
- evidence[id][host_name].each do |evidence|
151
- evidence_content = mapping_service.apply_mapping(
152
- source: 'full_evidence',
153
- data: evidence
154
- )
155
- content_service.create_evidence(content: evidence_content, issue: issue, node: host_node)
156
- end
157
- end
158
-
159
- # end
160
- end
161
- end # /parse_nexpose_full_xml
162
- end
163
- end
@@ -1,76 +0,0 @@
1
- module Dradis::Plugins::Nexpose::Formats
2
-
3
- # This module knows how to parse Nexpose Simple XML format.
4
- module Simple
5
- private
6
-
7
- def process_simple(doc)
8
- hosts = process_nexpose_simple_xml(doc)
9
- notes_simple(hosts)
10
- end
11
-
12
- def notes_simple(hosts)
13
- return if hosts.nil?
14
-
15
- hosts.each do |host|
16
- host_node = content_service.create_node(label: host['address'], type: :host)
17
- content_service.create_note node: host_node, text: "Host Description : #{host['description']} \nScanner Fingerprint certainty : #{host['fingerprint']}"
18
-
19
- generic_findings_node = content_service.create_node(label: 'Generic Findings', parent: host_node)
20
- host['generic_vulns'].each do |id, finding|
21
- content_service.create_note node: generic_findings_node, text: "Finding ID : #{id} \n \n Finding Refs :\n-------\n #{finding}"
22
- end
23
-
24
- port_text = nil
25
- host['ports'].each do |port_label, findings|
26
- port_node = content_service.create_node(label: port_label, parent: host_node)
27
-
28
- findings.each do |id, finding|
29
- port_text = mapping_service.apply_mapping(source: 'simple_port', data: {id: id, finding: finding})
30
- port_text << "\n#[host]#\n#{host['address']}\n\n"
31
- content_service.create_note node: port_node, text: port_text
32
- end
33
- end
34
- end
35
- end
36
-
37
- def process_nexpose_simple_xml(doc)
38
- results = doc.search('device')
39
- hosts = Array.new
40
- results.each do |host|
41
- current_host = Hash.new
42
- current_host['address'] = host['address']
43
- current_host['fingerprint'] = host.search('fingerprint')[0].nil? ? "N/A" : host.search('fingerprint')[0]['certainty']
44
- current_host['description'] = host.search('description')[0].nil? ? "N/A" : host.search('description')[0].text
45
- #So there's two sets of vulns in a NeXpose simple XML report for each host
46
- #Theres some generic ones at the top of the report
47
- #And some service specific ones further down the report.
48
- #So we need to get the generic ones before moving on
49
- current_host['generic_vulns'] = Hash.new
50
- host.xpath('vulnerabilities/vulnerability').each do |vuln|
51
- current_host['generic_vulns'][vuln['id']] = ''
52
- vuln.xpath('id').each do |id|
53
- current_host['generic_vulns'][vuln['id']] << id['type'] + " : " + id.text + "\n"
54
- end
55
- end
56
-
57
- current_host['ports'] = Hash.new
58
- host.xpath('services/service').each do |service|
59
- protocol = service['protocol']
60
- portid = service['port']
61
- port_label = protocol + '-' + portid
62
- current_host['ports'][port_label] = Hash.new
63
- service.xpath('vulnerabilities/vulnerability').each do |vuln|
64
- current_host['ports'][port_label][vuln['id']] = ''
65
- vuln.xpath('id').each do |id|
66
- current_host['ports'][port_label][vuln['id']] << id['type'] + " : " + id.text + "\n"
67
- end
68
- end
69
- end
70
-
71
- hosts << current_host
72
- end
73
- return hosts
74
- end
75
- end
76
- end
@@ -1,38 +0,0 @@
1
- require 'dradis/plugins/nexpose/formats/full'
2
- require 'dradis/plugins/nexpose/formats/simple'
3
-
4
- module Dradis::Plugins::Nexpose
5
- class Importer < Dradis::Plugins::Upload::Importer
6
-
7
- include Formats::Full
8
- include Formats::Simple
9
-
10
- def self.templates
11
- { evidence: 'full_evidence', issue: 'full_vulnerability' }
12
- end
13
-
14
- # The framework will call this function if the user selects this plugin from
15
- # the dropdown list and uploads a file.
16
- # @returns true if the operation was successful, false otherwise
17
- def import(params={})
18
- file_content = File.read( params[:file] )
19
-
20
- logger.info { 'Parsing NeXpose output file...' }
21
- doc = Nokogiri::XML(file_content)
22
- logger.info { 'Done.' }
23
-
24
- if doc.root.name == 'NeXposeSimpleXML'
25
- logger.info { 'NeXpose-Simple format detected' }
26
- process_simple(doc)
27
- elsif doc.root.name == 'NexposeReport'
28
- logger.info { 'NeXpose-Full format detected' }
29
- process_full(doc)
30
- else
31
- error = "The document doesn't seem to be in either NeXpose-Simple or NeXpose-Full XML format. Ensure you uploaded a Nexpose XML report."
32
- logger.fatal{ error }
33
- content_service.create_note text: error
34
- return false
35
- end
36
- end
37
- end
38
- end
@@ -1,221 +0,0 @@
1
- require 'rails_helper'
2
- require 'ostruct'
3
-
4
- describe 'Nexpose upload plugin' do
5
- before do
6
- @fixtures_dir = File.expand_path('../fixtures/files/', __FILE__)
7
- end
8
-
9
- describe 'importer' do
10
- before(:each) do
11
- # Stub template service
12
- templates_dir = File.expand_path('../../templates', __FILE__)
13
- expect_any_instance_of(Dradis::Plugins::TemplateService)
14
- .to receive(:default_templates_dir).and_return(templates_dir)
15
-
16
- # Init services
17
- plugin = Dradis::Plugins::Nexpose
18
-
19
- @content_service = Dradis::Plugins::ContentService::Base.new(
20
- logger: Logger.new(STDOUT),
21
- plugin: plugin
22
- )
23
-
24
- @importer = plugin::Importer.new(
25
- content_service: @content_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
- describe 'Importer: Simple' do
47
- it 'creates nodes, issues, notes and an evidences as needed' do
48
-
49
- expect(@content_service).to receive(:create_node).with(hash_including label: '1.1.1.1', type: :host).once
50
-
51
- expect(@content_service).to receive(:create_note) do |args|
52
- expect(args[:text]).to include('Host Description : Linux 2.6.9-89.ELsmp')
53
- expect(args[:text]).to include('Scanner Fingerprint certainty : 0.80')
54
- expect(args[:node].label).to eq('1.1.1.1')
55
- end.once
56
-
57
- expect(@content_service).to receive(:create_node) do |args|
58
- expect(args[:label]).to eq('Generic Findings')
59
- expect(args[:parent].label).to eq('1.1.1.1')
60
- OpenStruct.new(args)
61
- end.once
62
-
63
- expect(@content_service).to receive(:create_node) do |args|
64
- expect(args[:label]).to eq('udp-000')
65
- expect(args[:parent].label).to eq('1.1.1.1')
66
- OpenStruct.new(args)
67
- end.once
68
-
69
- expect(@content_service).to receive(:create_note) do |args|
70
- expect(args[:text]).to include("#[Id]#\nntpd-crypto")
71
- expect(args[:text]).to include("#[host]#\n1.1.1.1")
72
- expect(args[:node].label).to eq('udp-000')
73
- end.once
74
-
75
- expect(@content_service).to receive(:create_note) do |args|
76
- expect(args[:text]).to include("#[Id]#\nntp-clock-radio")
77
- expect(args[:text]).to include("#[host]#\n1.1.1.1")
78
- expect(args[:node].label).to eq('udp-000')
79
- end.once
80
-
81
- @importer.import(file: @fixtures_dir + '/simple.xml')
82
- end
83
- end
84
-
85
- describe 'Importer: Full' do
86
- it 'creates nodes, issues, notes and an evidences as needed' do
87
- expect(@content_service).to receive(:create_node).with(hash_including label: 'Nexpose Scan Summary').once
88
- expect(@content_service).to receive(:create_note) do |args|
89
- expect(args[:text]).to include("#[Title]#\nUSDA_Internal (4)")
90
- expect(args[:node].label).to eq('Nexpose Scan Summary')
91
- end.once
92
-
93
- expect(@content_service).to receive(:create_node) do |args|
94
- expect(args[:label]).to eq('1.1.1.1')
95
- expect(args[:type]).to eq(:host)
96
- create(:node, args.except(:type))
97
- end
98
-
99
- expect(@content_service).to receive(:create_note) do |args|
100
- expect(args[:text]).to include("#[Title]#\n1.1.1.1")
101
- expect(args[:node].label).to eq('1.1.1.1')
102
- end.once
103
-
104
- expect(@content_service).to receive(:create_note) do |args|
105
- expect(args[:text]).to include("#[Title]#\nService name: NTP")
106
- expect(args[:node].label).to eq('1.1.1.1')
107
- end.once
108
-
109
- expect(@content_service).to receive(:create_note) do |args|
110
- expect(args[:text]).to include("#[Title]#\nService name: SNMP")
111
- expect(args[:node].label).to eq('1.1.1.1')
112
- end.once
113
-
114
- expect(@content_service).to receive(:create_issue) do |args|
115
- expect(args[:text]).to include("#[Title]#\nApache HTTPD: error responses can expose cookies (CVE-2012-0053)")
116
- expect(args[:id]).to eq('ntp-clock-variables-disclosure')
117
- OpenStruct.new(args)
118
- end.once
119
-
120
- expect(@content_service).to receive(:create_issue) do |args|
121
- expect(args[:text]).to include("#[Title]#\nApache HTTPD: ETag Inode Information Leakage (CVE-2003-1418)")
122
- expect(args[:id]).to eq('test-02')
123
- OpenStruct.new(args)
124
- end.once
125
-
126
- expect(@content_service).to receive(:create_evidence) do |args|
127
- expect(args[:content]).to include("#[ID]#\nntp-clock-variables-disclosure\n\n")
128
- expect(args[:issue].id).to eq('ntp-clock-variables-disclosure')
129
- expect(args[:node].label).to eq('1.1.1.1')
130
- end.once
131
-
132
- expect(@content_service).to receive(:create_evidence) do |args|
133
- expect(args[:content]).to include("#[ID]#\ntest-02\n\n")
134
- expect(args[:issue].id).to eq('test-02')
135
- expect(args[:node].label).to eq('1.1.1.1')
136
- end.once
137
-
138
- @importer.import(file: @fixtures_dir + '/full.xml')
139
-
140
- expect(Node.find_by(label: '1.1.1.1').properties[:os]).to eq('IOS')
141
- end
142
-
143
- it 'wraps ciphers inside ssl issues in code blocks' do
144
- expect(@content_service).to receive(:create_issue) do |args|
145
- expect(args[:text]).to include('bc. ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256')
146
- OpenStruct.new(args)
147
- end.once
148
-
149
- @importer.import(file: @fixtures_dir + '/ssl.xml')
150
- end
151
-
152
- # Regression test for github.com/dradis/dradis-nexpose/issues/1
153
- it 'populates solutions regardless of if they are wrapped in paragraphs or lists' do
154
- expect(@content_service).to receive(:create_issue) do |args|
155
- expect(args[:text]).to include("#[Solution]#\n\nApache HTTPD >= 2.0 and < 2.0.65")
156
- OpenStruct.new(args)
157
- end.once
158
-
159
- expect(@content_service).to receive(:create_issue) do |args|
160
- expect(args[:text]).to include("#[Solution]#\n")
161
- expect(args[:text]).to include('You can remove inode information from the ETag header')
162
- OpenStruct.new(args)
163
- end.once
164
-
165
- @importer.import(file: @fixtures_dir + '/full.xml')
166
- end
167
-
168
- it 'populates tests regardless of if they contain paragraphs or containerblockelements' do
169
- expect(@content_service).to receive(:create_evidence) do |args|
170
- expect(args[:content]).to include("#[Content]#\nThe following NTP variables")
171
- OpenStruct.new(args)
172
- end.once
173
-
174
- expect(@content_service).to receive(:create_evidence) do |args|
175
- expect(args[:content]).to include("#[Content]#\nVulnerable URL:")
176
- OpenStruct.new(args)
177
- end.once
178
-
179
- @importer.import(file: @fixtures_dir + '/full.xml')
180
- end
181
-
182
- it 'transforms html entities (&lt; and &gt;)' do
183
- expect(@content_service).to receive(:create_issue) do |args|
184
- expect(args[:text]).to include("#[Solution]#\n\nApache HTTPD >= 2.0 and < 2.0.65")
185
- OpenStruct.new(args)
186
- end
187
-
188
- @importer.import(file: @fixtures_dir + '/full.xml')
189
- end
190
- end
191
-
192
- describe 'Importer: Full with duplicate nodes' do
193
- it 'creates evidence for each instance of the node' do
194
- expect(@content_service).to receive(:create_node).with(hash_including label: 'Nexpose Scan Summary').once
195
- expect(@content_service).to receive(:create_node) do |args|
196
- expect(args[:label]).to eq('1.1.1.1')
197
- expect(args[:type]).to eq(:host)
198
- create(:node, args.except(:type))
199
- end
200
-
201
- expect(@content_service).to receive(:create_evidence) do |args|
202
- expect(args[:content]).to include("#[ID]#\nntp-clock-variables-disclosure\n\n")
203
- expect(args[:issue].id).to eq('ntp-clock-variables-disclosure')
204
- expect(args[:node].label).to eq('1.1.1.1')
205
- end.twice
206
-
207
- @importer.import(file: @fixtures_dir + '/full_with_duplicate_node.xml')
208
- end
209
- end
210
- end
211
-
212
- it 'parses the fingerprints field' do
213
- doc = Nokogiri::XML(File.read(@fixtures_dir + '/full.xml'))
214
-
215
- ts = Dradis::Plugins::TemplateService.new(plugin: Dradis::Plugins::Nexpose)
216
- ts.set_template(template: 'full_node', content: "#[Fingerprints]#\n%node.fingerprints%\n")
217
- result = ts.process_template(data: doc.at_xpath('//nodes/node'), template: 'full_node')
218
-
219
- expect(result).to include('IOS')
220
- end
221
- end