dradis-nexpose 4.13.0 → 4.15.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 +7 -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/nexpose/node.rb +13 -11
- data/lib/tasks/thorfile.rb +20 -7
- data/spec/fixtures/files/full.xml +9 -0
- data/spec/fixtures/files/full_with_duplicate_node.xml +4 -1
- data/spec/nexpose/full/importer_spec.rb +108 -0
- data/spec/nexpose/simple/importer_spec.rb +60 -0
- data/spec/nexpose_upload_spec.rb +32 -195
- metadata +8 -5
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 741c488c76cb9554ce3b2a61997374262453a38e3256cad93c1885602d872bca
|
|
4
|
+
data.tar.gz: c896b751650744b60e8ddfeae40c4cba4277901d404e95bdb594674024227038
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 93068f684c000bcf4cb4f5944b38dd3e57ae7152c7c9f046b8db9e65c46a76323f5b399feffa47474cb62af5da17e26746af419c4a9eacd824437d5f6d0cae0e
|
|
7
|
+
data.tar.gz: e812c05c84e2b12e36fdb858eadd2daf02bd87b5c69cf5cc7b4cbaa9cbc8fe7eb213e12d58c8518a0c631fcb08bb0ff78973cdbd264d8f741021a84817adee8c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
v4.15.0 (December 2024)
|
|
2
|
+
- No changes
|
|
3
|
+
|
|
4
|
+
v4.14.0 (October 2024)
|
|
5
|
+
- Separate general importer into Full & Simple importers
|
|
6
|
+
|
|
1
7
|
v4.13.0 (July 2024)
|
|
2
8
|
- No changes
|
|
3
9
|
|
|
4
10
|
v4.12.0 (May 2024)
|
|
5
11
|
- Migrate integration to use Mappings Manager
|
|
6
12
|
- Update Dradis links in README
|
|
13
|
+
- Use 'n/a' as OS node property if there is no product value
|
|
7
14
|
|
|
8
15
|
v4.11.0 (January 2024)
|
|
9
16
|
- Add port/protocol to evidences
|
|
@@ -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/nexpose/node.rb
CHANGED
|
@@ -83,22 +83,24 @@ module Nexpose
|
|
|
83
83
|
# Finally the enumerations: names
|
|
84
84
|
if method_name == 'names'
|
|
85
85
|
@xml.xpath('./names/name').collect(&:text)
|
|
86
|
-
|
|
87
86
|
elsif ['fingerprints', 'software'].include?(method_name)
|
|
88
|
-
|
|
89
|
-
xpath_selector = {
|
|
90
|
-
'fingerprints' => './fingerprints/os',
|
|
91
|
-
'software' => './software/fingerprint'
|
|
92
|
-
}[method_name]
|
|
93
|
-
|
|
94
|
-
xml_os = @xml.at_xpath(xpath_selector)
|
|
95
|
-
return '' if xml_os.nil?
|
|
96
|
-
|
|
97
|
-
xml_os.attributes['product'].value
|
|
87
|
+
get_fingerprint_product(method_name)
|
|
98
88
|
else
|
|
99
89
|
# nothing found, the tag is valid but not present in this ReportItem
|
|
100
90
|
return nil
|
|
101
91
|
end
|
|
102
92
|
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# returns the first 'product' value from the <fingerprints> or <software> element
|
|
97
|
+
def get_fingerprint_product(method_name)
|
|
98
|
+
xpath_selector = {
|
|
99
|
+
'fingerprints' => './fingerprints/os',
|
|
100
|
+
'software' => './software/fingerprint'
|
|
101
|
+
}[method_name]
|
|
102
|
+
|
|
103
|
+
@xml.at_xpath(xpath_selector + '/@product')&.value || 'n/a'
|
|
104
|
+
end
|
|
103
105
|
end
|
|
104
106
|
end
|
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
|
|
@@ -11,6 +11,15 @@
|
|
|
11
11
|
<fingerprints>
|
|
12
12
|
<os certainty="0.80" family="IOS" product="IOS" vendor="Cisco" arch="x86_64"/>
|
|
13
13
|
</fingerprints>
|
|
14
|
+
<software>
|
|
15
|
+
<fingerprint
|
|
16
|
+
certainty="1.00"
|
|
17
|
+
software-class="General"
|
|
18
|
+
vendor="Sun"
|
|
19
|
+
family="Java"
|
|
20
|
+
product="JRE"
|
|
21
|
+
version="1.6.0.22"/>
|
|
22
|
+
</software>
|
|
14
23
|
<tests/>
|
|
15
24
|
<endpoints>
|
|
16
25
|
<endpoint port="123" protocol="udp" status="open">
|
|
@@ -9,8 +9,11 @@
|
|
|
9
9
|
<name>localhost:5000</name>
|
|
10
10
|
</names>
|
|
11
11
|
<fingerprints>
|
|
12
|
-
<os certainty="0.80" family="IOS"
|
|
12
|
+
<os certainty="0.80" family="IOS" vendor="Cisco" arch="x86_64"/>
|
|
13
13
|
</fingerprints>
|
|
14
|
+
<software>
|
|
15
|
+
<fingerprint certainty="1.00" vendor="Sun" family="Java"/>
|
|
16
|
+
</software>
|
|
14
17
|
<tests/>
|
|
15
18
|
<endpoints>
|
|
16
19
|
<endpoint port="123" protocol="udp" status="open">
|
|
@@ -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
|
data/spec/nexpose_upload_spec.rb
CHANGED
|
@@ -6,216 +6,53 @@ describe 'Nexpose upload plugin' do
|
|
|
6
6
|
@fixtures_dir = File.expand_path('../fixtures/files/', __FILE__)
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
describe '
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
9
|
+
describe 'Importer: Full with fingerprints elements' do
|
|
10
|
+
def apply_mapping(doc, field)
|
|
11
|
+
ms = Dradis::Plugins::MappingService.new(
|
|
12
|
+
integration: Dradis::Plugins::Nexpose
|
|
22
13
|
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
mapping_fields = [OpenStruct.new(
|
|
15
|
+
destination_field: 'Fingerprints',
|
|
16
|
+
content: "{{ nexpose[#{field}] }}"
|
|
17
|
+
)]
|
|
18
|
+
|
|
19
|
+
@result = ms.apply_mapping(
|
|
20
|
+
data: doc.at_xpath('//nodes/node'),
|
|
21
|
+
mapping_fields: mapping_fields,
|
|
22
|
+
source: 'full_node'
|
|
26
23
|
)
|
|
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
24
|
end
|
|
45
25
|
|
|
46
|
-
describe '
|
|
47
|
-
it '
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
26
|
+
describe 'with fingerprints > OS elements' do
|
|
27
|
+
it 'uses the os product value' do
|
|
28
|
+
doc = Nokogiri::XML(File.read(@fixtures_dir + '/full.xml'))
|
|
29
|
+
apply_mapping(doc, 'node.fingerprints')
|
|
62
30
|
|
|
63
|
-
expect(@
|
|
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')
|
|
31
|
+
expect(@result).to include('IOS')
|
|
82
32
|
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
33
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
OpenStruct.new(args)
|
|
124
|
-
end.once
|
|
34
|
+
it 'defaults to n/a if there is no os product value' do
|
|
35
|
+
doc = Nokogiri::XML(File.read(@fixtures_dir + '/full_with_duplicate_node.xml'))
|
|
36
|
+
apply_mapping(doc, 'node.fingerprints')
|
|
125
37
|
|
|
126
|
-
expect(@
|
|
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')
|
|
38
|
+
expect(@result).to include('n/a')
|
|
180
39
|
end
|
|
40
|
+
end
|
|
181
41
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
end
|
|
42
|
+
describe 'with software > fingerprint elements' do
|
|
43
|
+
it 'uses the product value given software/fingerprints' do
|
|
44
|
+
doc = Nokogiri::XML(File.read(@fixtures_dir + '/full.xml'))
|
|
45
|
+
apply_mapping(doc, 'node.software')
|
|
187
46
|
|
|
188
|
-
@
|
|
47
|
+
expect(@result).to include('JRE')
|
|
189
48
|
end
|
|
190
|
-
end
|
|
191
49
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
50
|
+
it 'defaults to n/a if there is no os product value' do
|
|
51
|
+
doc = Nokogiri::XML(File.read(@fixtures_dir + '/full_with_duplicate_node.xml'))
|
|
52
|
+
apply_mapping(doc, 'node.software')
|
|
200
53
|
|
|
201
|
-
expect(@
|
|
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')
|
|
54
|
+
expect(@result).to include('n/a')
|
|
208
55
|
end
|
|
209
56
|
end
|
|
210
57
|
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
58
|
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.15.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-12-20 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,6 +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
|
|
136
|
+
- spec/nexpose/full/importer_spec.rb
|
|
137
|
+
- spec/nexpose/simple/importer_spec.rb
|
|
137
138
|
- spec/nexpose_upload_spec.rb
|
|
138
139
|
- spec/spec_helper.rb
|
|
139
140
|
- templates/full_evidence.sample
|
|
@@ -170,5 +171,7 @@ test_files:
|
|
|
170
171
|
- spec/fixtures/files/full_with_duplicate_node.xml
|
|
171
172
|
- spec/fixtures/files/simple.xml
|
|
172
173
|
- spec/fixtures/files/ssl.xml
|
|
174
|
+
- spec/nexpose/full/importer_spec.rb
|
|
175
|
+
- spec/nexpose/simple/importer_spec.rb
|
|
173
176
|
- spec/nexpose_upload_spec.rb
|
|
174
177
|
- 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
|