dradis-nexpose 4.13.0 → 4.14.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 +3 -0
- data/lib/dradis/plugins/nexpose/engine.rb +12 -0
- data/lib/dradis/plugins/nexpose/full/importer.rb +204 -0
- data/lib/dradis/plugins/nexpose/gem_version.rb +1 -1
- data/lib/dradis/plugins/nexpose/mapping.rb +2 -1
- data/lib/dradis/plugins/nexpose/simple/importer.rb +117 -0
- data/lib/dradis/plugins/nexpose.rb +2 -1
- data/lib/tasks/thorfile.rb +20 -7
- data/spec/nexpose/full/importer_spec.rb +108 -0
- data/spec/nexpose/simple/importer_spec.rb +60 -0
- metadata +8 -7
- data/lib/dradis/plugins/nexpose/formats/full.rb +0 -163
- data/lib/dradis/plugins/nexpose/formats/simple.rb +0 -76
- data/lib/dradis/plugins/nexpose/importer.rb +0 -38
- data/spec/nexpose_upload_spec.rb +0 -221
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fed2206ffe5931c65cc3b1ae1c9007091b99b3f9134a9b81b4ae8b4d5496fc5f
|
4
|
+
data.tar.gz: bf34162969ecdd0d105d551149b51f7c20795a2bfa9b1f97c741b36e5711cfe5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9fa49ed385686a5910de33e05f09caafbb624e5227af713c5defd4d0485b38b0d446194bd652b03b77d43861440e1d08a2eb597ea7b13b2f8ccae9a8f1440510
|
7
|
+
data.tar.gz: d84b92cc9d54926f788d78dda9b87e7f1a4262ad7682278f72af62dc4917ca4877ea9e0a8754a3d4a117bee44633dbd4ce2e539e197069b523a5c7615b2d1acf
|
data/CHANGELOG.md
CHANGED
@@ -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
|
@@ -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'
|
data/lib/tasks/thorfile.rb
CHANGED
@@ -1,10 +1,27 @@
|
|
1
1
|
class NexposeTasks < Thor
|
2
2
|
include Rails.application.config.dradis.thor_helper_module
|
3
3
|
|
4
|
-
namespace
|
4
|
+
namespace 'dradis:plugins:nexpose:upload'
|
5
5
|
|
6
|
-
desc
|
7
|
-
def
|
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.
|
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-
|
11
|
+
date: 2024-10-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dradis-plugins
|
@@ -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/
|
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/
|
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
|
@@ -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/
|
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
|
data/spec/nexpose_upload_spec.rb
DELETED
@@ -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 (< and >)' 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
|