dradis-nexpose 4.12.0 → 4.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/dradis-nexpose.gemspec +1 -1
- 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 +13 -12
- 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
data/dradis-nexpose.gemspec
CHANGED
|
@@ -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
|
|
@@ -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
|
|
@@ -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/
|
|
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
|
|
@@ -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.
|
|
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/
|
|
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
|