nexposecli 0.1.7
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 +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/README.md +24 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/nexposecli +1723 -0
- data/bin/nexposecli.test +6 -0
- data/bin/setup +8 -0
- data/lib/nexposecli/argparse.rb +167 -0
- data/lib/nexposecli/version.rb +3 -0
- data/lib/nexposecli.rb +10 -0
- data/nexposecli.gemspec +25 -0
- data/spec/nexposecli_spec.rb +11 -0
- data/spec/spec_helper.rb +2 -0
- metadata +135 -0
data/bin/nexposecli
ADDED
@@ -0,0 +1,1723 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
##############################################################################
|
3
|
+
#
|
4
|
+
# File: evm.rb
|
5
|
+
#
|
6
|
+
# Author: Erik Gomez <gomeze@pobox.com>
|
7
|
+
# Erik Gomez <erik_gomez@rapid7.com>
|
8
|
+
#
|
9
|
+
# Purpose: UF ISC EVM Administrative tasks via cli
|
10
|
+
#
|
11
|
+
# Base Revision: $Id:$ (20141030@1227.01)
|
12
|
+
# Revision: $Id:$ (20160426@1315.01)
|
13
|
+
#
|
14
|
+
# Usage: ./evm.rb <action> <target> [<args>]
|
15
|
+
#
|
16
|
+
# -v verbose
|
17
|
+
# --help help
|
18
|
+
# ***NOTE*** This script is being refactored!!!
|
19
|
+
# It is currently a shameless copy of my UF code and
|
20
|
+
# argparse.rb class code from Jim Hranicky (jfh@ufl.edu)
|
21
|
+
#
|
22
|
+
##############################################################################
|
23
|
+
require 'rubygems'
|
24
|
+
require 'nexpose'
|
25
|
+
require 'nexposecli'
|
26
|
+
require 'socket'
|
27
|
+
require 'digest'
|
28
|
+
require 'securerandom'
|
29
|
+
require 'netaddr'
|
30
|
+
require 'logger'
|
31
|
+
require 'yaml'
|
32
|
+
require 'csv'
|
33
|
+
require 'set'
|
34
|
+
# for debug, this dumps the ruby objects to STDOUT
|
35
|
+
require 'pp'
|
36
|
+
|
37
|
+
##############################################################################
|
38
|
+
# Set Const
|
39
|
+
|
40
|
+
# Allowed Ops by field, in Set form
|
41
|
+
CVSS_SCORE_OPS = Set["IS", "IS_NOT", "IN_RANGE", "GREATER_THAN", "LESS_THAN"]
|
42
|
+
IP_RANGE_OPS = Set["IN", "NOT_IN"]
|
43
|
+
OS_OPS = Set["CONTAINS", "NOT_CONTAINS", "IS_EMPTY", "IS_NOT_EMPTY"]
|
44
|
+
RISK_SCORE_OPS = Set["IS", "IS_NOT", "IN_RANGE", "GREATER_THAN", "LESS_THAN"]
|
45
|
+
SITE_ID_OPS = Set["IN", "NOT_IN"]
|
46
|
+
SCAN_DATE_OPS = Set["ON_OR_BEFORE", "ON_OR_AFTER", "BETWEEN", "EARLIER_THAN", "WITHIN_THE_LAST"]
|
47
|
+
|
48
|
+
# Set default var values
|
49
|
+
$debug = false
|
50
|
+
uf_scanners = ''
|
51
|
+
|
52
|
+
@logpath = "./logs/"
|
53
|
+
@scanpath = "./"
|
54
|
+
# Attempting to use logfile per month: @logfile = "evm" + Time.now.strftime("%Y%m%d_%H%M%S") + ".log"
|
55
|
+
@logfile = "evm" + Time.now.strftime("%Y%m") + ".log"
|
56
|
+
@evm_reqid = SecureRandom.hex
|
57
|
+
@nsc_server = "<server>"
|
58
|
+
@nsc_user = "<user>"
|
59
|
+
@nsc_passwd = "<pass>"
|
60
|
+
@nsc_sites = Hash.new
|
61
|
+
|
62
|
+
scan_task_sleep = 300
|
63
|
+
max_scan_task_attempts = 3
|
64
|
+
|
65
|
+
##############################################################################
|
66
|
+
# Base procs
|
67
|
+
# wrapped puts for debug control
|
68
|
+
def uputs( facility, ulog )
|
69
|
+
# add string validation
|
70
|
+
@logger.info( @nsc_server + "," + @evm_reqid + " [" + facility.to_s + "] " + ulog.to_s)
|
71
|
+
if $debug
|
72
|
+
puts "[" + facility.to_s + "]" + ulog.to_s
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# wrapped pp for debug control
|
77
|
+
def upp( uobj )
|
78
|
+
if $debug
|
79
|
+
pp uobj
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# UF bail vs exit
|
84
|
+
def ubail(retval, msg)
|
85
|
+
uputs("BAIL", msg.to_s)
|
86
|
+
exit(retval)
|
87
|
+
end
|
88
|
+
|
89
|
+
# download, needs lots of work to ensure write path is clean and no overwrites
|
90
|
+
def download(url, filename, nsc)
|
91
|
+
return nil if url.nil? or url.empty?
|
92
|
+
uri = URI.parse(url)
|
93
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
94
|
+
http.use_ssl = true
|
95
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE # XXX: security issue
|
96
|
+
headers = {'Cookie' => "nexposeCCSessionID=#{nsc.session_id}"}
|
97
|
+
resp = http.get(uri.path, headers)
|
98
|
+
|
99
|
+
if filename == "NONE"
|
100
|
+
resp.body
|
101
|
+
else
|
102
|
+
file = File.open(filename, "w")
|
103
|
+
file.write(resp.body)
|
104
|
+
file.close
|
105
|
+
|
106
|
+
filename
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def read_config(conf)
|
111
|
+
# add check for file existence and perms
|
112
|
+
config = YAML.load_file(conf)
|
113
|
+
|
114
|
+
@nsc_server = config["config"]["server"]
|
115
|
+
@nsc_user = config["config"]["user"]
|
116
|
+
@nsc_passwd = config["config"]["password"]
|
117
|
+
@nsc_sites = config["sites"]
|
118
|
+
end
|
119
|
+
|
120
|
+
def scan_activity()
|
121
|
+
scanner_activity = Hash.new
|
122
|
+
# scan activity
|
123
|
+
scan_activity = @nsc.scan_activity
|
124
|
+
upp scan_activity
|
125
|
+
if scan_activity.length > 0
|
126
|
+
scan_activity.each do |scan|
|
127
|
+
timeDiff = DateTime.now.strftime('%s').to_i - scan.start_time.strftime('%s').to_i
|
128
|
+
tdSecs = timeDiff
|
129
|
+
tdHours = tdSecs / 3600
|
130
|
+
tdSecs -= tdHours * 3600
|
131
|
+
tdMins = tdSecs / 60
|
132
|
+
tdSecs -= tdMins * 60
|
133
|
+
timeDiffStr = "#{tdHours} hours, #{tdMins} minutes and #{tdSecs} seconds"
|
134
|
+
act_str = "Scan Id: #{scan.scan_id.to_s.ljust(5)} site: #{scan.site_id.to_s.ljust(4)} status: [#{scan.status.to_s.ljust(10)}]" +
|
135
|
+
" engine: #{scan.engine_id.to_s.ljust(3)} start time:#{scan.start_time} run time:#{timeDiffStr}"
|
136
|
+
if scanner_activity.has_key?scan.engine_id.to_s
|
137
|
+
scanner_activity[scan.scan_id.to_s] = { :site_id => scan.site_id.to_s, :status => scan.status.to_s, :scan_engine_id => scan.engine_id.to_s, :timeDiff => timeDiff, :act_str => act_str }
|
138
|
+
else
|
139
|
+
scanner_activity[scan.scan_id.to_s] = Hash.new
|
140
|
+
scanner_activity[scan.scan_id.to_s] = { :site_id => scan.site_id.to_s, :status => scan.status.to_s, :scan_engine_id => scan.engine_id.to_s, :timeDiff => timeDiff, :act_str => act_str }
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
scanner_activity
|
145
|
+
end
|
146
|
+
|
147
|
+
def valid_cidrv4(cstr)
|
148
|
+
if /^[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]\/([1-2]?[0-9]|3[0-2])$/ =~ cstr
|
149
|
+
true
|
150
|
+
else
|
151
|
+
false
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def valid_ipv4(ipstr)
|
156
|
+
if /^[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]$/ =~ ipstr
|
157
|
+
true
|
158
|
+
else
|
159
|
+
false
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def valid_ipv4_dottedcsv(ipstr)
|
164
|
+
if /^[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9],[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]$/ =~ ipstr
|
165
|
+
true
|
166
|
+
else
|
167
|
+
false
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def valid_ipv4_dottedcolon(ipstr)
|
172
|
+
if /^[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]:[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]$/ =~ ipstr
|
173
|
+
true
|
174
|
+
else
|
175
|
+
false
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def valid_ipv4_dotteddashed(ipstr)
|
180
|
+
if /^[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]-[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]$/ =~ ipstr
|
181
|
+
true
|
182
|
+
else
|
183
|
+
false
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def valid_ipv4_dotteddashedshort(ipstr)
|
188
|
+
if /^[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]\.[1-2]?[0-9]?[0-9]-[1-2]?[0-9]?[0-9]$/ =~ ipstr
|
189
|
+
true
|
190
|
+
else
|
191
|
+
false
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def ip2rangev4(cstr)
|
196
|
+
rangev4 = cstr.to_s + "," + cstr.to_s
|
197
|
+
end
|
198
|
+
|
199
|
+
def cidr2rangev4(cstr)
|
200
|
+
rangev4 = ""
|
201
|
+
netblock = NetAddr::CIDR.create(cstr)
|
202
|
+
rangev4 = netblock.first.to_s + "," + netblock.last.to_s
|
203
|
+
end
|
204
|
+
|
205
|
+
def dotteddashed2rangev4(cstr)
|
206
|
+
# consider validating the low,high values in order
|
207
|
+
rangev4 = cstr.split('-').first.to_s + "," + cstr.split('-').last.to_s
|
208
|
+
end
|
209
|
+
|
210
|
+
def dotteddashedshort2rangev4(cstr)
|
211
|
+
# consider validating the low,high values in order
|
212
|
+
ipv4l = cstr.split('-').first.to_s
|
213
|
+
ipv4h4th = cstr.split('-').last.to_s
|
214
|
+
ipv4h = ipv4l.sub(/\d+\Z/) {|x| x = ipv4h4th }
|
215
|
+
rangev4 = ipv4l.to_s + "," + ipv4h.to_s
|
216
|
+
end
|
217
|
+
|
218
|
+
def is_numeric(object)
|
219
|
+
true if Float(object) rescue false
|
220
|
+
end
|
221
|
+
|
222
|
+
def validate_searchstring(sfstr)
|
223
|
+
# Expectng field:op format for sfstr
|
224
|
+
valid_searchstring = nil
|
225
|
+
valid_search_field = nil
|
226
|
+
valid_search_op = nil
|
227
|
+
|
228
|
+
# Valid search fields and operators:
|
229
|
+
# CVSS_SCORE = IS, IS_NOT, IN_RANGE, GREATER_THAN, LESS_THAN (Float 0.0-10.0)
|
230
|
+
# IP_RANGE = IN, NOT_IN (IPv4 dotted notation)
|
231
|
+
# OS = CONTAINS, NOT_CONTAINS, IS_EMPTY, IS_NOT_EMPTY
|
232
|
+
# RISK_SCORE = IS, IS_NOT, IN_RANGE, GREATER_THAN, LESS_THAN (Fixnum)
|
233
|
+
# SITE_ID = IN, NOT_IN (Fixnum)
|
234
|
+
# SCAN_DATE = ON_OR_BEFORE, ON_OR_AFTER, BETWEEN (Value::ScanDate::FORMAT dates)
|
235
|
+
# SCAN_DATE = EARLIER_THAN, WITHIN_THE_LAST (Fixnum days)
|
236
|
+
|
237
|
+
# Grab search field and op
|
238
|
+
search_field = sfstr.split(':').first.to_s
|
239
|
+
search_op = sfstr.split(':').last.to_s
|
240
|
+
|
241
|
+
# Case by supported Search fields
|
242
|
+
isValid = false
|
243
|
+
case search_field
|
244
|
+
when "CVSS_SCORE"
|
245
|
+
isValid = true if CVSS_SCORE_OPS.include?(search_op)
|
246
|
+
when "IP_RANGE"
|
247
|
+
isValid = true if IP_RANGE_OPS.include?(search_op)
|
248
|
+
when "OS"
|
249
|
+
isValid = true if OS_OPS.include?(search_op)
|
250
|
+
when "RISK_SCORE"
|
251
|
+
isValid = true if RISK_SCORE_OPS.include?(search_op)
|
252
|
+
when "SITE_ID"
|
253
|
+
isValid = true if SITE_ID_OPS.include?(search_op)
|
254
|
+
when "SCAN_DATE"
|
255
|
+
isValid = true if SCAN_DATE_OPS.include?(search_op)
|
256
|
+
else
|
257
|
+
# Unsupported search field
|
258
|
+
end
|
259
|
+
if isValid
|
260
|
+
valid_search_field = search_field
|
261
|
+
valid_search_op = search_op
|
262
|
+
valid_searchstring = valid_search_field + "," + valid_search_op
|
263
|
+
else
|
264
|
+
valid_searchstring = "ERROR"
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
##############################################################################
|
269
|
+
#
|
270
|
+
# Conf
|
271
|
+
#
|
272
|
+
##############################################################################
|
273
|
+
# Parse cli and config options passed
|
274
|
+
ARGS = %q{
|
275
|
+
- comment : General Options
|
276
|
+
|
277
|
+
- name : help
|
278
|
+
desc : Print help
|
279
|
+
|
280
|
+
- name : verbose
|
281
|
+
short : v
|
282
|
+
desc : Run verbosely
|
283
|
+
|
284
|
+
- comment : EVM Administrative Actions
|
285
|
+
|
286
|
+
- name : create
|
287
|
+
short : c
|
288
|
+
desc : The create action is used for new objects
|
289
|
+
|
290
|
+
- name : list
|
291
|
+
short : l
|
292
|
+
desc : The list action is used to list of objects of the same type
|
293
|
+
|
294
|
+
- name : show
|
295
|
+
short : s
|
296
|
+
desc : The show action is used to display details of a single object
|
297
|
+
|
298
|
+
- name : update
|
299
|
+
short : u
|
300
|
+
desc : The update action is used to change properties of a single object
|
301
|
+
|
302
|
+
- name : delete
|
303
|
+
short : d
|
304
|
+
desc : The delete action is used to delete a single object
|
305
|
+
|
306
|
+
- name : run
|
307
|
+
desc : The run action is only used to issue commands to the COMMAND object
|
308
|
+
|
309
|
+
- comment : EVM Action Targets
|
310
|
+
|
311
|
+
- name : USER
|
312
|
+
short : U
|
313
|
+
desc : The USER target is used to alter or create the USER object
|
314
|
+
|
315
|
+
- name : ROLE
|
316
|
+
short : L
|
317
|
+
desc : The ROLE target is used to alter or create the ROLE object
|
318
|
+
|
319
|
+
- name : ENGINE
|
320
|
+
short : E
|
321
|
+
desc : The ENGINE target is used to alter or create the SCAN ENGINE object
|
322
|
+
|
323
|
+
- name : POOL
|
324
|
+
short : P
|
325
|
+
desc : The POOL target is used to alter or create the POOL object
|
326
|
+
|
327
|
+
- name : SCAN
|
328
|
+
short : S
|
329
|
+
desc : The SCAN target is used to alter or create the SCAN object
|
330
|
+
|
331
|
+
- name : SITE
|
332
|
+
short : T
|
333
|
+
desc : The SITE target is used to alter or create the SITE object
|
334
|
+
|
335
|
+
- name : ASSET
|
336
|
+
short : A
|
337
|
+
desc : The ASSET target is used to alter or create the ASSET object
|
338
|
+
|
339
|
+
- name : DASSET
|
340
|
+
short : D
|
341
|
+
desc : The DASSET target is used to alter or create the DASSET object
|
342
|
+
|
343
|
+
- name : TAG
|
344
|
+
short : G
|
345
|
+
desc : The TAG target is used to alter or create the TAG object
|
346
|
+
|
347
|
+
- name : REPORT
|
348
|
+
short : R
|
349
|
+
desc : The REPORT target is used to alter or create the REPORT object
|
350
|
+
|
351
|
+
- name : VULN
|
352
|
+
short : V
|
353
|
+
desc : The VULN target is used to alter or create the VULN object
|
354
|
+
|
355
|
+
- name : CONSOLE
|
356
|
+
desc : The CONSOLE target is used to alter the CONSOLE nsc connection object
|
357
|
+
|
358
|
+
- name : COMMAND
|
359
|
+
short : C
|
360
|
+
desc : The COMMAND target is only used in conjunction with the --run action
|
361
|
+
required : true
|
362
|
+
|
363
|
+
- comment : EVM Action Argument Values
|
364
|
+
|
365
|
+
- name : host
|
366
|
+
short : h
|
367
|
+
desc : The target ip or host to be acted upon by the action
|
368
|
+
required : true
|
369
|
+
|
370
|
+
- name : port
|
371
|
+
short : p
|
372
|
+
desc : The target port to be acted upon by the action
|
373
|
+
required : true
|
374
|
+
|
375
|
+
- name : name
|
376
|
+
short : n
|
377
|
+
desc : The target object name
|
378
|
+
required : true
|
379
|
+
|
380
|
+
- name : fullname
|
381
|
+
desc : The target object full name
|
382
|
+
required : true
|
383
|
+
|
384
|
+
- name : newname
|
385
|
+
desc : The target object new name
|
386
|
+
required : true
|
387
|
+
|
388
|
+
- name : description
|
389
|
+
short : t
|
390
|
+
desc : The text based description of the object being acted upon
|
391
|
+
required : true
|
392
|
+
|
393
|
+
- name : id
|
394
|
+
short : i
|
395
|
+
desc : The object id being acted upon
|
396
|
+
required : true
|
397
|
+
|
398
|
+
- name : site
|
399
|
+
desc : The site id of the object being acted upon
|
400
|
+
required : true
|
401
|
+
|
402
|
+
- name : range
|
403
|
+
short : r
|
404
|
+
desc : The comma separated (begin,end) range of ip addresses to be acted upon
|
405
|
+
required : true
|
406
|
+
|
407
|
+
- name : targets
|
408
|
+
desc : The network block or ip addresses to be acted upon, in CIDRv4, dotted dashed, or ip format
|
409
|
+
required : true
|
410
|
+
|
411
|
+
- name : argv
|
412
|
+
short : g
|
413
|
+
desc : Argument vector for the action, in the form key:value pairs
|
414
|
+
required : true
|
415
|
+
|
416
|
+
- name : filter
|
417
|
+
short : f
|
418
|
+
desc : Filters which are applied to the action, in the form key:value pairs
|
419
|
+
required : true
|
420
|
+
|
421
|
+
- name : filterv
|
422
|
+
desc : Filter value which are applied to the action. Formate varies by filter type
|
423
|
+
required : true
|
424
|
+
|
425
|
+
- name : action
|
426
|
+
short : a
|
427
|
+
desc : The subaction to be performed within the target action
|
428
|
+
required : true
|
429
|
+
|
430
|
+
- name : attempts
|
431
|
+
desc : The max number of attempts for iterative actions
|
432
|
+
required : true
|
433
|
+
|
434
|
+
- name : loop_sleep
|
435
|
+
desc : The sleep interval in seconds between action iterations
|
436
|
+
required : true
|
437
|
+
|
438
|
+
- comment : Nexpose Console credentials
|
439
|
+
|
440
|
+
- name : config
|
441
|
+
desc : The config yaml file containing the connection details of the Nexpose Console Server
|
442
|
+
required : true
|
443
|
+
|
444
|
+
- name : nsc_server
|
445
|
+
desc : The ip or hostname of the Nexpose Console Server
|
446
|
+
required : true
|
447
|
+
|
448
|
+
- name : nsc_user
|
449
|
+
desc : The username to login to the Nexpose Console Server
|
450
|
+
required : true
|
451
|
+
|
452
|
+
- name : nsc_pass
|
453
|
+
desc : The password to login to the Nexpose Console Server
|
454
|
+
required : true
|
455
|
+
|
456
|
+
- name : logpath
|
457
|
+
desc : The path for writing the logs
|
458
|
+
required : true
|
459
|
+
|
460
|
+
- name : scanpath
|
461
|
+
desc : The path for exported/imported scans
|
462
|
+
required : true
|
463
|
+
}
|
464
|
+
|
465
|
+
|
466
|
+
##############################################################################
|
467
|
+
#
|
468
|
+
# Main
|
469
|
+
#
|
470
|
+
##############################################################################
|
471
|
+
# Args parsing
|
472
|
+
ap = Nexposecli::ArgParse.new( ARGS )
|
473
|
+
|
474
|
+
begin
|
475
|
+
args = ap.parse
|
476
|
+
rescue Nexposecli::ArgParse::InvalidOption => e
|
477
|
+
STDERR.puts e
|
478
|
+
STDERR.puts ap.usage
|
479
|
+
exit(-1)
|
480
|
+
end
|
481
|
+
|
482
|
+
# start the logger
|
483
|
+
if args.logpath
|
484
|
+
# consider input validation, to avoid sec issues
|
485
|
+
@logpath = args.logpath.to_s
|
486
|
+
end
|
487
|
+
@logger = Logger.new(@logpath + @logfile)
|
488
|
+
@logger.level = Logger::INFO
|
489
|
+
uputs("LOG", "Automation tasks being run from: " + Socket.gethostname.to_s)
|
490
|
+
uputs("LOG", "Automation tasks being logged to: #{@logpath.to_s + @logfile.to_s}")
|
491
|
+
|
492
|
+
if args.scanpath
|
493
|
+
# consider input validation, to avoid sec issues
|
494
|
+
@scanpath = args.scanpath.to_s
|
495
|
+
end
|
496
|
+
|
497
|
+
$debug = TRUE if args.verbose
|
498
|
+
uputs("CLI", "Command-line args parsed for #{$0}")
|
499
|
+
uputs("CLI", "Args: #{args.inspect}")
|
500
|
+
|
501
|
+
if args.help
|
502
|
+
uputs("CLI", "Help was requested, displaying usage and exiting")
|
503
|
+
puts ap.usage("#{$0} [options] (v #{Nexposecli::VERSION})")
|
504
|
+
exit(0)
|
505
|
+
end
|
506
|
+
|
507
|
+
# test for a single action
|
508
|
+
uputs("CLI", "Checking for the requested action")
|
509
|
+
@action = 0
|
510
|
+
@action |= 1 if args.create
|
511
|
+
@action |= 2 if args.list
|
512
|
+
@action |= 4 if args.show
|
513
|
+
@action |= 8 if args.update
|
514
|
+
@action |= 16 if args.delete
|
515
|
+
@action |= 32 if args.run
|
516
|
+
uputs("ACTION", "The requested action value is: #{@action.to_s}")
|
517
|
+
raise "You can only submit one action per task, see --help (action submitted: #{@action.to_s})" unless [1,2,4,8,16,32].include?(@action)
|
518
|
+
|
519
|
+
uputs("TARGET", "Checking for the requested target")
|
520
|
+
@target = 0
|
521
|
+
@target |= 1 if args.USER
|
522
|
+
@target |= 2 if args.ENGINE
|
523
|
+
@target |= 4 if args.POOL
|
524
|
+
@target |= 8 if args.SCAN
|
525
|
+
@target |= 16 if args.SITE
|
526
|
+
@target |= 32 if args.ASSET
|
527
|
+
@target |= 64 if args.REPORT
|
528
|
+
@target |= 128 if args.VULN
|
529
|
+
@target |= 256 if args.COMMAND
|
530
|
+
@target |= 512 if args.DASSET
|
531
|
+
@target |= 1024 if args.TAG
|
532
|
+
@target |= 2048 if args.CONSOLE
|
533
|
+
@target |= 4096 if args.TEMPLATE
|
534
|
+
@target |= 8192 if args.ROLE
|
535
|
+
uputs("TARGET", "The requested target value is: #{@target.to_s}")
|
536
|
+
raise "You can only submit one target per task, see --help (#{@target})" unless [1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192].include?(@target)
|
537
|
+
|
538
|
+
# nsc conn vars
|
539
|
+
unless (
|
540
|
+
(args.nsc_server && args.nsc_user && args.nsc_pass) || args.config
|
541
|
+
)
|
542
|
+
raise 'Please supply Nexpose Console connection parameters listed under --help'
|
543
|
+
end
|
544
|
+
# nsc connection details
|
545
|
+
if args.config
|
546
|
+
uputs("CONF", "Reading config file: #{args.config.to_s}")
|
547
|
+
read_config(args.config.to_s)
|
548
|
+
else
|
549
|
+
uputs("CONF", "Using NSC creds from CLI args, be sure to clear shell history to avoid creds disclosure")
|
550
|
+
@nsc_server = args.nsc_server.to_s
|
551
|
+
@nsc_user = args.nsc_user.to_s
|
552
|
+
@nsc_passwd = args.nsc_pass.to_s
|
553
|
+
end
|
554
|
+
|
555
|
+
# Make the initial connection to the NSC server
|
556
|
+
uputs("CONNECT", "Attempting connection to NSC: #{@nsc_server.to_s} with user: #{@nsc_user.to_s}")
|
557
|
+
begin
|
558
|
+
@nsc = Nexpose::Connection.new( @nsc_server.to_s, @nsc_user.to_s, @nsc_passwd.to_s)
|
559
|
+
rescue SystemCallError => e
|
560
|
+
# EJG add conditional api error handling...
|
561
|
+
uputs("CONNECT", "An error occurred while attempting to connect to the specified server: #{e.to_s}")
|
562
|
+
STDERR.puts "ERROR [ " + e.to_s + " ]"
|
563
|
+
exit(-1)
|
564
|
+
end
|
565
|
+
uputs("CONNECT", "Connection to NSC: #{@nsc_server.to_s} with user: #{@nsc_user.to_s} successful")
|
566
|
+
|
567
|
+
uputs("LOGIN", "Attempting login to NSC: #{@nsc_server.to_s} with user: #{@nsc_user.to_s}")
|
568
|
+
begin
|
569
|
+
@nsc.login
|
570
|
+
rescue Nexpose::APIError => e
|
571
|
+
# EJG add conditional api error handling...
|
572
|
+
uputs("LOGIN", "An error occurred while attempting to connect to the specified server: #{e.to_s}")
|
573
|
+
STDERR.puts "ERROR [ " + e.to_s + " ]"
|
574
|
+
exit(-1)
|
575
|
+
end
|
576
|
+
|
577
|
+
if @nsc.session_id
|
578
|
+
uputs("CONNECT", 'NSC Login Successful')
|
579
|
+
else
|
580
|
+
uputs("CONNECT", 'NSC Login Failed')
|
581
|
+
exit
|
582
|
+
end
|
583
|
+
|
584
|
+
# opted to keep actions grouped by the target
|
585
|
+
case @target
|
586
|
+
when 1 # TARGET USER
|
587
|
+
case @action
|
588
|
+
when 1 # create
|
589
|
+
uputs("ACTION", 'create USER action requested')
|
590
|
+
name = args.name
|
591
|
+
full_name = "#{args.fullname}"
|
592
|
+
password = "nxpassword"
|
593
|
+
|
594
|
+
user = Nexpose::User.new(name,
|
595
|
+
full_name,
|
596
|
+
password,
|
597
|
+
role_name = 'user',
|
598
|
+
id = -1,
|
599
|
+
enabled = 1,
|
600
|
+
email = nil,
|
601
|
+
all_sites = false,
|
602
|
+
all_groups = false,
|
603
|
+
token = nil)
|
604
|
+
pp user
|
605
|
+
puts 'Not yet saved'
|
606
|
+
user.save(@nsc)
|
607
|
+
pp user
|
608
|
+
when 2 # list
|
609
|
+
uputs("ACTION", 'list USER action requested')
|
610
|
+
user_listing = @nsc.list_users
|
611
|
+
pp user_listing
|
612
|
+
when 4 # show
|
613
|
+
uputs("ACTION", 'show USER action requested')
|
614
|
+
userid = args.id.to_str
|
615
|
+
user = Nexpose::User.load(@nsc, userid)
|
616
|
+
pp user
|
617
|
+
when 8 # update
|
618
|
+
uputs("ACTION", 'update USER action requested')
|
619
|
+
puts 'Not yet implemented'
|
620
|
+
when 16 # delete
|
621
|
+
uputs("ACTION", 'delete USER action requested')
|
622
|
+
puts 'Not yet implemented'
|
623
|
+
else
|
624
|
+
uputs("ACTION", 'The action requested is not implemented for target')
|
625
|
+
puts 'The action requested is not implemented for target'
|
626
|
+
end
|
627
|
+
when 2 # TARGET ENGINE
|
628
|
+
case @action
|
629
|
+
when 1 # create
|
630
|
+
uputs("ACTION", 'create ENGINE action requested')
|
631
|
+
puts 'Not yet implemented'
|
632
|
+
when 2 # list
|
633
|
+
uputs("ACTION", 'list ENGINE action requested')
|
634
|
+
uf_scanners = @nsc.engines
|
635
|
+
if uf_scanners.length > 0
|
636
|
+
if args.action
|
637
|
+
case args.action
|
638
|
+
when "summary", "status"
|
639
|
+
scanner_activity = scan_activity()
|
640
|
+
nseSynopsis = Nexpose::DataTable._get_json_table(@nsc, '/data/engines', { 'sort' => -1, 'dir' => -1, 'startIndex' => -1, 'results' => -1, 'table-id' => 'engine-listing'})
|
641
|
+
upp nseSynopsis
|
642
|
+
nseSynopsis.each do |eng|
|
643
|
+
puts "\t-Id: #{eng['id']}\tStatus: #{eng['status'].to_s.ljust(10)} Last Update(id): #{eng['lastUpdateDate']}(#{eng['lastUpdateID']})\t Name: #{eng['name']}\n"
|
644
|
+
# EJG NEEDS work
|
645
|
+
if scanner_activity.has_key?(eng['Engine ID'].to_i)
|
646
|
+
scanner_activity[eng['Engine ID'].to_i].each do |scan| puts "\t " + scan end
|
647
|
+
end
|
648
|
+
end
|
649
|
+
else
|
650
|
+
end
|
651
|
+
else
|
652
|
+
uf_scanners.each do |scanner|
|
653
|
+
puts("\t- status: #{scanner.status}\t id:#{scanner.id}\t name:'#{scanner.name}'\n")
|
654
|
+
end
|
655
|
+
end
|
656
|
+
puts("\nNOTE: The Scan Engine Ids are used for Scan Engine Pool create, update, and delete.\n")
|
657
|
+
else
|
658
|
+
puts("This Nexpose Security Console has no defined Scan Engines.\n")
|
659
|
+
end
|
660
|
+
when 4 # show
|
661
|
+
uputs("ACTION", 'show ENGINE action requested')
|
662
|
+
puts 'Not yet implemented'
|
663
|
+
when 8 # update
|
664
|
+
uputs("ACTION", 'update ENGINE action requested')
|
665
|
+
puts 'Not yet implemented'
|
666
|
+
when 16 # delete
|
667
|
+
uputs("ACTION", 'delete ENGINE action requested')
|
668
|
+
puts 'Not yet implemented'
|
669
|
+
else
|
670
|
+
uputs("ACTION", 'The action requested is not implemented for target')
|
671
|
+
puts 'The action requested is not implemented for target'
|
672
|
+
end
|
673
|
+
when 4 # TARGET POOL
|
674
|
+
case @action
|
675
|
+
when 1 # create
|
676
|
+
uputs("ACTION", 'create POOL action requested')
|
677
|
+
raise 'Please submit the Scan Engine Ids for the Pool definition, see --help' unless ( args.id )
|
678
|
+
raise 'Please submit the Scan Engine Pool name, see --help' unless ( args.name )
|
679
|
+
uf_scanners = @nsc.engines # get the scan engine list to map name by id
|
680
|
+
# consider sanitizing the args.name
|
681
|
+
pool_name = args.name
|
682
|
+
pool = Nexpose::EnginePool.new(pool_name, 'silo')
|
683
|
+
engine_ids = args.id
|
684
|
+
engine_ids.each_line(',') {|s| engine = uf_scanners.find{ |scanner| scanner.id == s.to_i }; uputs("ACTION", "Adding Scan Engine: [#{engine.name}]"); pool.add(engine.name)}
|
685
|
+
pool.save(@nsc)
|
686
|
+
puts("The scan engine pool [ #{pool_name} ] was created and has the id: #{pool.id}")
|
687
|
+
uputs("ACTION", 'create POOL action completed for pool name: #{pool_name}, id: #{pool.id}')
|
688
|
+
when 2 # list
|
689
|
+
uputs("ACTION", 'list POOL action requested')
|
690
|
+
pool_listing = @nsc.list_engine_pools
|
691
|
+
if pool_listing.length > 0
|
692
|
+
pool_listing.each do |pool|
|
693
|
+
puts("Pool: id:#{pool.id}\tname:#{pool.name}\n")
|
694
|
+
upp pool
|
695
|
+
end
|
696
|
+
else
|
697
|
+
puts("This Nexpose Security Console has no defined Scan Engine Pools.\n")
|
698
|
+
end
|
699
|
+
when 4 # show
|
700
|
+
uputs("ACTION", 'show POOL action requested')
|
701
|
+
raise 'Please submit the Scan Engine Pool Id and Pool Name, see --help' unless ( args.id && args.name )
|
702
|
+
pool_name = args.name
|
703
|
+
pool_id = args.id
|
704
|
+
pool = Nexpose::EnginePool.load(@nsc, pool_name)
|
705
|
+
puts "\nScan Engine Pool name: [#{pool.name}] and id: [#{pool.id}] has the following Scan Engines as members:\n\n"
|
706
|
+
pool.engines.each do |engine|
|
707
|
+
puts "\t- Scan Engine name: [#{engine.name}] id: [#{engine.id}] at: [#{engine.address}]\n"
|
708
|
+
end
|
709
|
+
puts "\n"
|
710
|
+
upp pool
|
711
|
+
when 8 # update
|
712
|
+
uputs("ACTION", 'update POOL action requested')
|
713
|
+
puts 'Not yet implemented'
|
714
|
+
when 16 # delete
|
715
|
+
uputs("ACTION", 'delete POOL action requested')
|
716
|
+
raise 'Please submit the Scan Engine Pool Id and Pool Name, see --help' unless ( args.id && args.name )
|
717
|
+
pool_name = args.name
|
718
|
+
pool_id = args.id
|
719
|
+
pool = Nexpose::EnginePool.new(pool_name, 'silo', pool_id)
|
720
|
+
pool.delete(@nsc)
|
721
|
+
uputs("POOL", "The scan engine pool [ #{pool_name} ] with the id: #{pool.id} was deleted\n")
|
722
|
+
else
|
723
|
+
uputs("ACTION", 'The action requested is not implemented for target')
|
724
|
+
puts 'The action requested is not implemented for target'
|
725
|
+
end
|
726
|
+
when 8 # TARGET SCAN
|
727
|
+
case @action
|
728
|
+
when 1 # create
|
729
|
+
uputs("SCAN", 'create SCAN action requested')
|
730
|
+
unless (
|
731
|
+
args.id != nil && ( args.range || args.targets )
|
732
|
+
)
|
733
|
+
raise 'Please supply the site id and ip range, or targets to scan, see --help'
|
734
|
+
end
|
735
|
+
|
736
|
+
# CLI args for scan task attempts and sleep interval
|
737
|
+
if args.attempts
|
738
|
+
max_scan_task_attempts = args.attempts.to_i
|
739
|
+
end
|
740
|
+
if args.loop_sleep
|
741
|
+
scan_task_sleep = args.loop_sleep.to_i
|
742
|
+
end
|
743
|
+
|
744
|
+
# target arrays
|
745
|
+
targets_sha2 = false
|
746
|
+
target_cursor = 0
|
747
|
+
targets = []
|
748
|
+
target_ranges = []
|
749
|
+
|
750
|
+
# read all targets into an array, from cli arg or file
|
751
|
+
if args.targets
|
752
|
+
# expecting fields: target,...
|
753
|
+
uputs("SCAN", "Targets provided within: #{args.targets} file")
|
754
|
+
# look for .<sha2 digest> of targets state file
|
755
|
+
targets_sha2 = Digest::SHA2.file(args.targets).hexdigest
|
756
|
+
if File.file?("." + targets_sha2)
|
757
|
+
# read the state file
|
758
|
+
target_cursor = File.open("." + targets_sha2).first.to_i
|
759
|
+
uputs("SCAN", "A state file was found for #{args.targets}, the last completed task [#{target_cursor.to_s}]")
|
760
|
+
end
|
761
|
+
|
762
|
+
targets = CSV.read(args.targets, :headers=>true)
|
763
|
+
targets.each do |target|
|
764
|
+
target_ranges.push(target['target'].to_s)
|
765
|
+
end
|
766
|
+
elsif args.range
|
767
|
+
targeth = { "type" => "cli_range", "subnetid" => "0", "target" => args.range.to_s }
|
768
|
+
targets.push(targeth)
|
769
|
+
target_ranges.push(args.range.to_s)
|
770
|
+
else
|
771
|
+
STDERR.puts "No targets defined, see --help"
|
772
|
+
exit(-1)
|
773
|
+
end
|
774
|
+
|
775
|
+
## BEGIN targets loop
|
776
|
+
num_ranges = target_ranges.length
|
777
|
+
range_ctr = 0
|
778
|
+
target_ranges.each do |target_range|
|
779
|
+
range_ctr += 1
|
780
|
+
if range_ctr <= target_cursor
|
781
|
+
puts "Skipping scan task #{range_ctr}, a state file existed.\n"
|
782
|
+
next
|
783
|
+
end
|
784
|
+
target_complete = false
|
785
|
+
uputs("SCAN:#{range_ctr}", "Preparing SiteDevicesScan Type:#{targets[range_ctr - 1]['type']} Subnet Id:#{targets[range_ctr - 1]['subnetid']} Target:#{targets[range_ctr - 1]['target']}")
|
786
|
+
uputs("SCAN:#{range_ctr}", "Target details Type:#{targets[range_ctr - 1]['type']} Subnet Id:#{targets[range_ctr - 1]['subnetid']} Resource Id:#{targets[range_ctr - 1]['resourceid']} Description:#{targets[range_ctr - 1]['description']}")
|
787
|
+
|
788
|
+
# Make a connection and login to the NSC server
|
789
|
+
uputs("CONNECT", "Attempting connection to NSC: #{@nsc_server.to_s} with user: #{@nsc_user.to_s}")
|
790
|
+
begin
|
791
|
+
@nsc = Nexpose::Connection.new( @nsc_server.to_s, @nsc_user.to_s, @nsc_passwd.to_s)
|
792
|
+
@nsc.login
|
793
|
+
rescue SystemCallError => e
|
794
|
+
# EJG add conditional api error handling and consider wrapping coonect/login into a method...
|
795
|
+
uputs("LOGIN", "An error occurred while attempting to connect to the specified server: #{e.to_s}")
|
796
|
+
STDERR.puts "ERROR [ " + e.to_s + " ]"
|
797
|
+
exit(-1)
|
798
|
+
end
|
799
|
+
|
800
|
+
if @nsc.session_id
|
801
|
+
uputs("SCAN CONNECT", 'NSC Login Successful')
|
802
|
+
else
|
803
|
+
uputs("SCAN CONNECT", 'NSC Login Failed')
|
804
|
+
exit
|
805
|
+
end
|
806
|
+
|
807
|
+
# grab the site id
|
808
|
+
scan_site_id = args.id.to_i
|
809
|
+
if scan_site_id == 0 && targets[range_ctr - 1].has_key?("site")
|
810
|
+
scan_site_id = @nsc_sites[targets[range_ctr - 1]['site']]
|
811
|
+
uputs("SCAN:#{range_ctr}", "Scan Site Id from conf and targets file is being used: #{scan_site_id}")
|
812
|
+
end
|
813
|
+
|
814
|
+
site_engine_id = 0
|
815
|
+
scan_hosts = Array.new
|
816
|
+
site = ""
|
817
|
+
|
818
|
+
# Obtain the Site details based on the provided scan site id
|
819
|
+
begin
|
820
|
+
site = Nexpose::Site.load(@nsc, scan_site_id)
|
821
|
+
site_engine_id = site.engine_id
|
822
|
+
rescue Nexpose::APIError => e
|
823
|
+
# EJG add conditional api error handling...
|
824
|
+
STDERR.puts "ERROR [ " + e.to_s + " ]"
|
825
|
+
exit(-1)
|
826
|
+
end
|
827
|
+
|
828
|
+
# check if pool or engine... add
|
829
|
+
is_pool = true
|
830
|
+
uf_scanners = @nsc.engines
|
831
|
+
if uf_scanners.collect{|scanner| scanner.id}.include?(site_engine_id)
|
832
|
+
is_pool = false
|
833
|
+
end
|
834
|
+
|
835
|
+
# Obtain the Scan Engine Pool definition for the provided scan_site_id
|
836
|
+
pool_name = ""
|
837
|
+
pool_id = 0
|
838
|
+
if is_pool
|
839
|
+
pool_listing = @nsc.list_engine_pools
|
840
|
+
if pool_listing.length > 0
|
841
|
+
upp pool_listing
|
842
|
+
pool_listing.each do |pool|
|
843
|
+
if pool.id.to_s.include?(site_engine_id.to_s)
|
844
|
+
pool_id = pool.id
|
845
|
+
pool_name = pool.name
|
846
|
+
end
|
847
|
+
end
|
848
|
+
else
|
849
|
+
STDERR.puts "No Scan Engine Pools Defined, the id maybe a Scan Engine"
|
850
|
+
exit(-1)
|
851
|
+
end
|
852
|
+
end
|
853
|
+
|
854
|
+
if is_pool
|
855
|
+
pool = Nexpose::EnginePool.load(@nsc, pool_name)
|
856
|
+
pool_size = 0
|
857
|
+
pool_size = pool.engines.length
|
858
|
+
else
|
859
|
+
pool_size = 1
|
860
|
+
end
|
861
|
+
upp pool_size
|
862
|
+
|
863
|
+
## BEGIN Single Scan task
|
864
|
+
scan_task = true
|
865
|
+
scan_task_retries = 1
|
866
|
+
new_scan = ""
|
867
|
+
|
868
|
+
# In preparation for an iterative scan task attempt, close the nsc session
|
869
|
+
logout_success = @nsc.logout
|
870
|
+
|
871
|
+
while scan_task && ( scan_task_retries <= max_scan_task_attempts ) do
|
872
|
+
puts "- entering while scan_task loop"
|
873
|
+
|
874
|
+
# Make a connection and login to the NSC server
|
875
|
+
uputs("CONNECT", "Attempting connection to NSC: #{@nsc_server.to_s} with user: #{@nsc_user.to_s}")
|
876
|
+
begin
|
877
|
+
@nsc = Nexpose::Connection.new( @nsc_server.to_s, @nsc_user.to_s, @nsc_passwd.to_s)
|
878
|
+
@nsc.login
|
879
|
+
rescue SystemCallError => e
|
880
|
+
# EJG add conditional api error handling and consider wrapping coonect/login into a method...
|
881
|
+
uputs("LOGIN", "An error occurred while attempting to connect to the specified server: #{e.to_s}")
|
882
|
+
STDERR.puts "ERROR [ " + e.to_s + " ]"
|
883
|
+
exit(-1)
|
884
|
+
end
|
885
|
+
|
886
|
+
if @nsc.session_id
|
887
|
+
uputs("SCAN CONNECT2", 'NSC Login Successful')
|
888
|
+
else
|
889
|
+
uputs("SCAN CONNECT2", 'NSC Login Failed')
|
890
|
+
exit
|
891
|
+
end
|
892
|
+
|
893
|
+
# debug
|
894
|
+
puts Time.now.strftime("%Y%m%d_%H%M%S") + " Attempt: #{scan_task_retries} for target range: #{range_ctr} of: #{num_ranges}\n"
|
895
|
+
# Now check the current scan activity to determine if availability is good for the provided site
|
896
|
+
scan_activity = @nsc.scan_activity
|
897
|
+
upp scan_activity
|
898
|
+
scans_running = scan_activity.length
|
899
|
+
site_scans_running = 0
|
900
|
+
site_scan_ids = []
|
901
|
+
scan_activity.each do |scan_summary|
|
902
|
+
act_str = "Scan Id: #{scan_summary.scan_id.to_s.ljust(5)} site: #{scan_summary.site_id.to_s.ljust(4)} status: [#{scan_summary.status.to_s.ljust(10)}]" +
|
903
|
+
" engine: #{scan_summary.engine_id.to_s.ljust(3)} start time: #{scan_summary.start_time}"
|
904
|
+
uputs("SCAN_ACT:#{range_ctr}", act_str)
|
905
|
+
if scan_summary.site_id == scan_site_id && scan_summary.status.include?("running")
|
906
|
+
site_scan_ids.push(scan_summary.scan_id)
|
907
|
+
site_scans_running += 1
|
908
|
+
end
|
909
|
+
end
|
910
|
+
|
911
|
+
uputs("SCAN:#{range_ctr}", "Current Activity- Site:#{scan_site_id} isPool:#{is_pool.to_s} Pool:#{pool_id},#{pool_name} Pool Size:#{pool_size} Running Scans:#{scans_running} Site Scans:#{site_scans_running}")
|
912
|
+
nsc_err_flag = false
|
913
|
+
if site_scans_running < pool_size
|
914
|
+
if valid_ipv4(target_range)
|
915
|
+
valid_range = ip2rangev4(target_range)
|
916
|
+
elsif valid_cidrv4(target_range)
|
917
|
+
valid_range = cidr2rangev4(target_range)
|
918
|
+
elsif valid_ipv4_dottedcsv(target_range)
|
919
|
+
# add dotted csv validation for low,high order
|
920
|
+
valid_range = target_range
|
921
|
+
elsif valid_ipv4_dotteddashed(target_range)
|
922
|
+
valid_range = dotteddashed2rangev4(target_range)
|
923
|
+
elsif valid_ipv4_dotteddashedshort(target_range)
|
924
|
+
valid_range = dotteddashedshort2rangev4(target_range)
|
925
|
+
else
|
926
|
+
uputs("SCAN:#{range_ctr}", "API ERROR: An invalid target range was provided: #{target_range}")
|
927
|
+
next
|
928
|
+
# ubail(-1, "An invalid target range was provided: #{target_range}")
|
929
|
+
end
|
930
|
+
|
931
|
+
uputs("SCAN:#{range_ctr}", "Scan requested type/subnet id:#{targets[range_ctr - 1]['type']}/#{targets[range_ctr - 1]['subnetid']} target range: #{target_range}")
|
932
|
+
uputs("SCAN:#{range_ctr}", "Scan requested type/subnet id:#{targets[range_ctr - 1]['type']}/#{targets[range_ctr - 1]['subnetid']} validated target range: #{valid_range}")
|
933
|
+
|
934
|
+
# scan_hosts.push( { :range => valid_range.to_s } )
|
935
|
+
scan_hosts.push( Nexpose::IPRange.new(valid_range.split(',').first.to_s, valid_range.split(',').last.to_s) )
|
936
|
+
begin
|
937
|
+
# using nexpose gem 0.5.4 - EJG, and new changes
|
938
|
+
new_scan = @nsc.scan_assets(scan_site_id, scan_hosts)
|
939
|
+
rescue Nexpose::APIError => e
|
940
|
+
STDERR.puts e
|
941
|
+
uputs("SCAN:#{range_ctr}", "API ERROR: [#{e}]")
|
942
|
+
nsc_err_flag = true
|
943
|
+
scan_task_retries += 1
|
944
|
+
uputs("SCAN:#{range_ctr}", "...sleeping due to a recoverable error... for scan task: #{range_ctr} [#{target_range}] next attempt: #{scan_task_retries} in #{scan_task_sleep} secs")
|
945
|
+
sleep(scan_task_sleep)
|
946
|
+
end
|
947
|
+
|
948
|
+
if !nsc_err_flag
|
949
|
+
uputs("SCAN:#{range_ctr}", "Scanning target range: #{range_ctr} of: #{num_ranges}")
|
950
|
+
scan_task = false
|
951
|
+
target_complete = true
|
952
|
+
nsc_err_flag = false
|
953
|
+
end
|
954
|
+
else
|
955
|
+
puts "- entering else condition for no scanners"
|
956
|
+
logout_success = @nsc.logout
|
957
|
+
# no scan resources available, sleep and wait until retires/timeout exhausted
|
958
|
+
if scan_task_retries >= max_scan_task_attempts
|
959
|
+
puts "No scan resources available\n"
|
960
|
+
exit(-1)
|
961
|
+
end
|
962
|
+
scan_task_retries += 1
|
963
|
+
uputs("SCAN:#{range_ctr}", "...sleeping... for scan task: #{range_ctr} [#{target_range}] next attempt: #{scan_task_retries} in #{scan_task_sleep} secs")
|
964
|
+
sleep(scan_task_sleep)
|
965
|
+
end
|
966
|
+
end
|
967
|
+
# END of Single Range Scan task
|
968
|
+
|
969
|
+
if target_complete
|
970
|
+
if targets_sha2
|
971
|
+
File.open("." + targets_sha2, 'w') {|f| f.write(range_ctr) }
|
972
|
+
end
|
973
|
+
|
974
|
+
# Make a connection and login to the NSC server
|
975
|
+
uputs("CONNECT", "Attempting connection to NSC: #{@nsc_server.to_s} with user: #{@nsc_user.to_s}")
|
976
|
+
begin
|
977
|
+
@nsc = Nexpose::Connection.new( @nsc_server.to_s, @nsc_user.to_s, @nsc_passwd.to_s)
|
978
|
+
@nsc.login
|
979
|
+
rescue SystemCallError => e
|
980
|
+
# EJG add conditional api error handling and consider wrapping coonect/login into a method...
|
981
|
+
uputs("LOGIN", "An error occurred while attempting to connect to the specified server: #{e.to_s}")
|
982
|
+
STDERR.puts "ERROR [ " + e.to_s + " ]"
|
983
|
+
exit(-1)
|
984
|
+
end
|
985
|
+
|
986
|
+
if @nsc.session_id
|
987
|
+
uputs("ACT CONNECT", 'NSC Login Successful')
|
988
|
+
else
|
989
|
+
uputs("ACT CONNECT", 'NSC Login Failed')
|
990
|
+
exit
|
991
|
+
end
|
992
|
+
|
993
|
+
if new_scan.kind_of? Nexpose::Scan
|
994
|
+
uputs("SCAN:#{range_ctr}", "The scan id: #{new_scan.id.to_s} for Subnet Id:#{targets[range_ctr - 1]['subnetid']} and the target range: #{target_range.to_s} launched after #{scan_task_retries} attempt(s) on engine id: #{new_scan.engine.to_s}")
|
995
|
+
else
|
996
|
+
uputs("SCAN:#{range_ctr}", "The new scan for Subnet Id:#{targets[range_ctr - 1]['subnetid']} and the target range: #{target_range.to_s} encountered an error.")
|
997
|
+
puts "The new scan encountered an error. Unable to report on the scan id.\n"
|
998
|
+
end
|
999
|
+
end
|
1000
|
+
puts "End of a single scan task\n"
|
1001
|
+
|
1002
|
+
end
|
1003
|
+
logout_success = @nsc.logout
|
1004
|
+
# END of Target Ranges loop
|
1005
|
+
if targets_sha2
|
1006
|
+
File.open("." + targets_sha2, 'w') {|f| f.write("COMPLETED") }
|
1007
|
+
end
|
1008
|
+
when 2 # list
|
1009
|
+
uputs("SCAN", 'list SCAN action requested')
|
1010
|
+
scan_activity = scan_activity()
|
1011
|
+
upp scan_activity
|
1012
|
+
if scan_activity.length > 0
|
1013
|
+
# EJG adding timeDiff conditional action
|
1014
|
+
scan_activity.each do |scan_ids, scans|
|
1015
|
+
case args.action
|
1016
|
+
when "exceeds", "max"
|
1017
|
+
if args.filter && args.filter.include?("max_secs")
|
1018
|
+
max_secs = args.filter.split(':').last.to_i
|
1019
|
+
if scans[:timeDiff].to_i >= max_secs
|
1020
|
+
# puts scans[:timeDiff].to_s + "\n"
|
1021
|
+
puts scan_ids.to_s + "\n"
|
1022
|
+
upp(scans)
|
1023
|
+
end
|
1024
|
+
end
|
1025
|
+
else
|
1026
|
+
puts scans[:act_str].to_s + "\n"
|
1027
|
+
end
|
1028
|
+
end
|
1029
|
+
else
|
1030
|
+
puts "There are no scans in queue.\n"
|
1031
|
+
end
|
1032
|
+
when 4 # show
|
1033
|
+
uputs("SCAN", 'show SCAN action requested')
|
1034
|
+
if is_numeric(args.id)
|
1035
|
+
#scanSummary = Nexpose::DataTable._get_dyn_table(@nsc, "/ajax/scan_statistics.txml?scanid=#{args.id}")
|
1036
|
+
#scanDets = Hash.new
|
1037
|
+
#scanRuntime = DateTime.now.strftime('%s').to_i - (scanSummary[0]['Started'].to_i / 1000).to_i
|
1038
|
+
#scanDets = { "Scan Id" => args.id, "Pending Scans" => scanSummary[0]['Pending Scans'], "Active Scans" => scanSummary[0]['Currently Scanning'], "Scan Run Secs" => scanRuntime }
|
1039
|
+
#puts scanDets.to_s + "\n"
|
1040
|
+
|
1041
|
+
scanSummary = @nsc.scan_statistics(args.id)
|
1042
|
+
upp scanSummary
|
1043
|
+
else
|
1044
|
+
puts 'Not yet implemented'
|
1045
|
+
end
|
1046
|
+
when 8 # update
|
1047
|
+
# EJG
|
1048
|
+
uputs("SCAN", 'update SCAN action requested')
|
1049
|
+
if args.action
|
1050
|
+
case args.action
|
1051
|
+
when "stop", "halt"
|
1052
|
+
if is_numeric(args.id)
|
1053
|
+
uputs("SCAN", "stop SCAN action requested for scan id: #{args.id}")
|
1054
|
+
@nsc.stop_scan(args.id, 60)
|
1055
|
+
end
|
1056
|
+
when "export"
|
1057
|
+
if is_numeric(args.id)
|
1058
|
+
uputs("SCAN", "export SCAN action requested for scan id: #{args.id}")
|
1059
|
+
@nsc.export_scan(args.id, "#{@scanpath.to_s}/scan-#{args.id.to_s}.zip")
|
1060
|
+
else
|
1061
|
+
uputs("SCAN", "export SCAN action failed for scan id: #{args.id}, expecting scan id")
|
1062
|
+
end
|
1063
|
+
when "import"
|
1064
|
+
if is_numeric(args.id)
|
1065
|
+
uputs("SCAN", "import SCAN action requested for site id: #{args.id} and file: #{args.filterv}")
|
1066
|
+
@nsc.import_scan(args.id.to_i, @scanpath.to_s + '/' + args.filterv.to_s)
|
1067
|
+
# Poll until scan is complete before attempting to import the next scan.
|
1068
|
+
upp @nsc.site_scan_history(args.id.to_i)
|
1069
|
+
last_scan = @nsc.site_scan_history(args.id.to_i).max_by { |s| s.start_time }.scan_id
|
1070
|
+
while (@nsc.scan_status(last_scan) == 'running')
|
1071
|
+
sleep 10
|
1072
|
+
end
|
1073
|
+
uputs("SCAN", "import SCAN integration completed for site id: #{args.id} and file: #{args.filterv}")
|
1074
|
+
else
|
1075
|
+
uputs("SCAN", "import SCAN action failed for site id: #{args.id}, expecting site id")
|
1076
|
+
end
|
1077
|
+
else
|
1078
|
+
puts 'The action requested is not implemented for target'
|
1079
|
+
end
|
1080
|
+
else
|
1081
|
+
puts 'Not yet implemented'
|
1082
|
+
end
|
1083
|
+
when 16 # delete
|
1084
|
+
uputs("ACTION", 'delete SCAN action requested')
|
1085
|
+
puts 'Not yet implemented, and not likely to be implemented'
|
1086
|
+
else
|
1087
|
+
uputs("ACTION", 'The action requested is not implemented for target')
|
1088
|
+
puts 'The action requested is not implemented for target'
|
1089
|
+
end
|
1090
|
+
when 16 # TARGET SITE
|
1091
|
+
case @action
|
1092
|
+
when 1 # create
|
1093
|
+
uputs("ACTION", 'create SITE action requested')
|
1094
|
+
puts 'Not yet implemented, and not likely to be implemented'
|
1095
|
+
when 2 # list
|
1096
|
+
uputs("ACTION", 'list SITE action requested')
|
1097
|
+
site_listing = @nsc.list_sites
|
1098
|
+
site_listing.each do |site|
|
1099
|
+
puts("Site: id:#{site.id}\t name:'#{site.name}'\t--description:#{site.description}\n")
|
1100
|
+
end
|
1101
|
+
upp site_listing
|
1102
|
+
when 4 # show
|
1103
|
+
uputs("ACTION", 'show SITE action requested')
|
1104
|
+
site = ""
|
1105
|
+
if (args.id != nil)
|
1106
|
+
scan_site_id = args.id.to_i
|
1107
|
+
end
|
1108
|
+
begin
|
1109
|
+
site = Nexpose::Site.load(@nsc, scan_site_id)
|
1110
|
+
rescue Nexpose::APIError => e
|
1111
|
+
STDERR.puts "ERROR [ " + e.to_s + " ]"
|
1112
|
+
exit(-1)
|
1113
|
+
end
|
1114
|
+
puts("Site: id:#{site.id}\t name:'#{site.name}'\tconfig version:#{site.config_version}\n--description:#{site.description}\n")
|
1115
|
+
puts("--scan engine: #{site.engine}\tscan_template: '#{site.scan_template_name}'\n")
|
1116
|
+
puts("--assets:")
|
1117
|
+
site.assets.each do |iprange|
|
1118
|
+
puts(" IPRange from: #{iprange.from} - #{iprange.to}\n")
|
1119
|
+
end
|
1120
|
+
puts("--exclude:")
|
1121
|
+
site.exclude.each do |iprange|
|
1122
|
+
puts(" IPRange from: #{iprange.from} - #{iprange.to}\n")
|
1123
|
+
end
|
1124
|
+
upp site
|
1125
|
+
when 8 # update
|
1126
|
+
uputs("ACTION", 'update SITE action requested')
|
1127
|
+
site = ""
|
1128
|
+
if (args.id != nil)
|
1129
|
+
scan_site_id = args.id.to_i
|
1130
|
+
end
|
1131
|
+
begin
|
1132
|
+
site = Nexpose::Site.load(@nsc, scan_site_id)
|
1133
|
+
rescue Nexpose::APIError => e
|
1134
|
+
STDERR.puts "ERROR [ " + e.to_s + " ]"
|
1135
|
+
exit(-1)
|
1136
|
+
end
|
1137
|
+
|
1138
|
+
case args.filter
|
1139
|
+
when "exclude", "EXCLUDE"
|
1140
|
+
if args.action == "replace"
|
1141
|
+
site.exclude = []
|
1142
|
+
end
|
1143
|
+
# parse --filterv for IPRange to replace/add
|
1144
|
+
if valid_ipv4_dottedcsv(args.filterv)
|
1145
|
+
valid_xrange = args.filterv
|
1146
|
+
else
|
1147
|
+
puts("ERROR Expecting IPRange in X.X.X.X,Y.Y.Y.Y format: from,to\n")
|
1148
|
+
exit(-1)
|
1149
|
+
end
|
1150
|
+
site.exclude.push(Nexpose::IPRange.new(valid_xrange.split(',').first.to_s, valid_xrange.split(',').last.to_s))
|
1151
|
+
site.save(@nsc)
|
1152
|
+
else
|
1153
|
+
puts 'Not yet implemented'
|
1154
|
+
end
|
1155
|
+
when 16 # delete
|
1156
|
+
uputs("ACTION", 'delete SITE action requested')
|
1157
|
+
puts 'Not yet implemented'
|
1158
|
+
else
|
1159
|
+
uputs("ACTION", 'The action requested is not implemented for target')
|
1160
|
+
puts 'The action requested is not implemented for target'
|
1161
|
+
end
|
1162
|
+
when 32 # TARGET ASSET
|
1163
|
+
case @action
|
1164
|
+
when 1 # create
|
1165
|
+
uputs("ACTION", 'create ASSET action requested')
|
1166
|
+
puts 'Not yet implemented'
|
1167
|
+
when 2 # list
|
1168
|
+
uputs("ACTION", 'list ASSET action requested')
|
1169
|
+
assetgroups = @nsc.asset_groups
|
1170
|
+
uputs("ACTION", "There are #{assetgroups.size} asset groups.")
|
1171
|
+
assetgroups.each do |ags|
|
1172
|
+
puts "id: #{ags.id.to_s.ljust(10)} name: #{ags.name}"
|
1173
|
+
end
|
1174
|
+
upp assetgroups
|
1175
|
+
when 4 # show
|
1176
|
+
uputs("ACTION", 'show ASSET action requested')
|
1177
|
+
if is_numeric(args.id)
|
1178
|
+
ag = Nexpose::AssetGroup.load(@nsc, args.id)
|
1179
|
+
puts "There are #{ag.assets.size} assets in asset group id: #{args.id} name: \"#{ag.name}\".\n"
|
1180
|
+
ag.assets.each do |asset|
|
1181
|
+
puts "id: #{asset.id.to_s.ljust(10)} address: #{asset.address}"
|
1182
|
+
end
|
1183
|
+
else
|
1184
|
+
puts 'The Asset Group Id must be numeric.'
|
1185
|
+
end
|
1186
|
+
upp(ag)
|
1187
|
+
when 8 # update
|
1188
|
+
uputs("ACTION", 'update ASSET action requested')
|
1189
|
+
puts 'Not yet implemented'
|
1190
|
+
when 16 # delete
|
1191
|
+
uputs("ACTION", 'delete ASSET action requested')
|
1192
|
+
puts 'Not yet implemented'
|
1193
|
+
else
|
1194
|
+
uputs("ACTION", 'The action requested is not implemented for target')
|
1195
|
+
puts 'The action requested is not implemented for target'
|
1196
|
+
end
|
1197
|
+
when 64 # TARGET REPORT
|
1198
|
+
case @action
|
1199
|
+
when 1 # create
|
1200
|
+
uputs("ACTION", 'create REPORT action requested')
|
1201
|
+
|
1202
|
+
if (args.id != nil)
|
1203
|
+
report_config = Nexpose::ReportConfig.load(@nsc, args.id)
|
1204
|
+
report_config.filters.delete_if do |filter|
|
1205
|
+
if filter.type == "device"
|
1206
|
+
upp filter
|
1207
|
+
true
|
1208
|
+
end
|
1209
|
+
end
|
1210
|
+
uputs("DEBUG", 'report_config.filters post device filter deletion')
|
1211
|
+
upp(report_config.filters)
|
1212
|
+
|
1213
|
+
if (args.host != nil)
|
1214
|
+
# EJG pass site id through...
|
1215
|
+
device = @nsc.find_device_by_address( args.host, args.site)
|
1216
|
+
if (device != nil)
|
1217
|
+
report_config.id = -1
|
1218
|
+
scan_asset_device_id = device.id.to_i
|
1219
|
+
report_config.name = "Asset: " + args.host.to_s + " (" + Time.now.strftime("%Y%m%d%H%M%S") + ")"
|
1220
|
+
report_config.add_filter('device', scan_asset_device_id)
|
1221
|
+
report_config.save(@nsc)
|
1222
|
+
else
|
1223
|
+
puts "Device Id is nil. No match found for ip: " + args.host.to_s + "\n"
|
1224
|
+
end
|
1225
|
+
else
|
1226
|
+
report_config.name = "Default Report Name (" + Time.now.strftime("%Y%m%d%H%M%S") + ")"
|
1227
|
+
end
|
1228
|
+
puts "- Running the report now...\n"
|
1229
|
+
report_run = report_config.generate(@nsc)
|
1230
|
+
|
1231
|
+
# check for report run status
|
1232
|
+
report_summary = @nsc.last_report(report_config.id)
|
1233
|
+
while report_summary.status != "Generated"
|
1234
|
+
puts "- Sleeping... " + report_summary.status.to_s + "\n"
|
1235
|
+
sleep(2)
|
1236
|
+
report_summary = @nsc.last_report(report_config.id)
|
1237
|
+
end
|
1238
|
+
# pp report_summary
|
1239
|
+
|
1240
|
+
puts "---\n- Report Id: " + report_config.id.to_s + " \n"
|
1241
|
+
puts "---\n- The report can be found via:\n https://#{@nsc_server}:3780" + report_summary.uri.to_s + "\n"
|
1242
|
+
|
1243
|
+
# download("https://#{@nsc_server}:3780" + report_summary.uri.to_s, "./tmp/RemediationPlan-" + Time.now.strftime("%Y%m%d_%H%M%S") + ".pdf", nsc)
|
1244
|
+
download("https://#{@nsc_server}:3780" + report_summary.uri.to_s, "./tmp/doc.pdf", @nsc)
|
1245
|
+
end
|
1246
|
+
|
1247
|
+
when 2 # list
|
1248
|
+
uputs("ACTION", 'list REPORT action requested')
|
1249
|
+
report_listing = @nsc.list_reports
|
1250
|
+
report_listing.each do |report|
|
1251
|
+
puts("Report id:#{report.config_id}\ttemplate:#{report.template_id}\tname:#{report.name}\n")
|
1252
|
+
upp report
|
1253
|
+
end
|
1254
|
+
when 4 # show
|
1255
|
+
uputs("ACTION", 'show REPORT action requested')
|
1256
|
+
report = @nsc.last_report(args.id)
|
1257
|
+
upp report
|
1258
|
+
when 8 # update
|
1259
|
+
uputs("ACTION", 'update REPORT action requested')
|
1260
|
+
puts 'Not yet implemented'
|
1261
|
+
when 16 # delete
|
1262
|
+
uputs("ACTION", 'delete REPORT action requested')
|
1263
|
+
puts 'Not yet implemented'
|
1264
|
+
else
|
1265
|
+
uputs("ACTION", 'The action requested is not implemented for target')
|
1266
|
+
puts 'The action requested is not implemented for target'
|
1267
|
+
end
|
1268
|
+
when 128 # TARGET VULN
|
1269
|
+
case @action
|
1270
|
+
when 1 # create
|
1271
|
+
uputs("ACTION", 'create VULN action requested')
|
1272
|
+
puts 'Not yet implemented, and not likely to be implemented'
|
1273
|
+
when 2 # list
|
1274
|
+
uputs("ACTION", 'list VULN action requested')
|
1275
|
+
vuln_list = @nsc.all_vulns
|
1276
|
+
upp vuln_list
|
1277
|
+
when 4 # show
|
1278
|
+
uputs("ACTION", 'show VULN action requested')
|
1279
|
+
puts 'Not yet implemented'
|
1280
|
+
when 8 # update
|
1281
|
+
uputs("ACTION", 'update VULN action requested')
|
1282
|
+
puts 'Not yet implemented, and not likely to be implemented'
|
1283
|
+
when 16 # delete
|
1284
|
+
uputs("ACTION", 'delete VULN action requested')
|
1285
|
+
puts 'Not yet implemented'
|
1286
|
+
else
|
1287
|
+
uputs("ACTION", 'The action requested is not implemented for target')
|
1288
|
+
puts 'The action requested is not implemented for target'
|
1289
|
+
end
|
1290
|
+
when 256 # TARGET COMMAND
|
1291
|
+
case @action
|
1292
|
+
when 32 # run
|
1293
|
+
uputs("ACTION", 'run COMMAND action requested')
|
1294
|
+
puts "\nRunning [#{args.COMMAND}]\n\n"
|
1295
|
+
postd = @nsc.console_command(args.COMMAND)
|
1296
|
+
puts postd.to_s
|
1297
|
+
puts "\n"
|
1298
|
+
else
|
1299
|
+
uputs("ACTION", 'The action requested is not implemented for target: COMMAND')
|
1300
|
+
puts 'The action requested is not implemented for target: COMMAND'
|
1301
|
+
end
|
1302
|
+
when 512 # TARGET DASSET
|
1303
|
+
case @action
|
1304
|
+
when 1, 8 # create and update
|
1305
|
+
valid_filterv = nil
|
1306
|
+
base_field_str = "Nexpose::Search::Field::"
|
1307
|
+
base_op_str = "Nexpose::Search::Operator::"
|
1308
|
+
|
1309
|
+
# For now, only support default IP_RANGE defined dynamic asset groups
|
1310
|
+
field_str = "IP_RANGE"
|
1311
|
+
op_str = "IN"
|
1312
|
+
asset_crit = nil
|
1313
|
+
|
1314
|
+
# Insert loop for multi/single dyanmic asset group creation
|
1315
|
+
# EJG
|
1316
|
+
dags_sha2 = false
|
1317
|
+
dags_cursor = 0
|
1318
|
+
dags = []
|
1319
|
+
dag_filtervs = []
|
1320
|
+
|
1321
|
+
# read all dags into an array, from cli arg or file
|
1322
|
+
if args.targets
|
1323
|
+
# expecting fields: action,filter,filterv,name,description
|
1324
|
+
uputs("DASSET", "Dynamic Asset Groups provided within: #{args.targets} file")
|
1325
|
+
end
|
1326
|
+
|
1327
|
+
# if batch processing, grab @action value from field 1 in the targets file record
|
1328
|
+
valid_str = ""
|
1329
|
+
if args.filter
|
1330
|
+
valid_str = validate_searchstring(args.filter)
|
1331
|
+
end
|
1332
|
+
isValid = false
|
1333
|
+
if valid_str != "ERROR"
|
1334
|
+
isValid = true
|
1335
|
+
field_str = valid_str.split(',').first.to_s
|
1336
|
+
op_str = valid_str.split(',').last.to_s
|
1337
|
+
else
|
1338
|
+
end
|
1339
|
+
|
1340
|
+
if args.filterv
|
1341
|
+
# now test filterv based on filter_str and op_str
|
1342
|
+
if field_str == "IP_RANGE"
|
1343
|
+
valid_range = '127.0.0.1:127.0.0.1'
|
1344
|
+
if valid_ipv4(args.filterv)
|
1345
|
+
valid_range = ip2rangev4(args.filterv)
|
1346
|
+
elsif valid_cidrv4(args.filterv)
|
1347
|
+
valid_range = cidr2rangev4(args.filterv)
|
1348
|
+
elsif valid_ipv4_dottedcsv(args.filterv)
|
1349
|
+
# add dotted csv validation for low,high order
|
1350
|
+
valid_range = args.filterv
|
1351
|
+
elsif valid_ipv4_dottedcolon(args.filterv)
|
1352
|
+
# add dotted colon validation for low,high order
|
1353
|
+
valid_range = args.filterv
|
1354
|
+
elsif valid_ipv4_dotteddashed(args.filterv)
|
1355
|
+
valid_range = dotteddashed2rangev4(args.filterv)
|
1356
|
+
elsif valid_ipv4_dotteddashedshort(args.filterv)
|
1357
|
+
valid_range = dotteddashedshort2rangev4(args.filterv)
|
1358
|
+
else
|
1359
|
+
uputs("DASSET", "API ERROR: An invalid asset group range was provided: #{args.filterv.to_s}")
|
1360
|
+
end
|
1361
|
+
# to allow target file csv format, expect : delimited range or replace "," with ":" here.
|
1362
|
+
vrange = valid_range.dup
|
1363
|
+
vrange.tr!(",",":")
|
1364
|
+
|
1365
|
+
valid_filterv = [vrange.split(':').first.to_s, vrange.split(':').last.to_s]
|
1366
|
+
elsif field_str == "CVSS_SCORE"
|
1367
|
+
# test for float, and possibly IN_RANGE op
|
1368
|
+
valid_filterv = [7.0]
|
1369
|
+
elsif field_str == "RISK_SCORE" || field_str == "SITE_ID"
|
1370
|
+
# test for fixnum, and possibly IN_RANGE op
|
1371
|
+
valid_filterv = [args.filterv.to_i]
|
1372
|
+
elsif field_str == "OS"
|
1373
|
+
# test for str
|
1374
|
+
valid_filterv = [args.filterv.to_s]
|
1375
|
+
else
|
1376
|
+
# assume fixnum, with exception of IN_RANGE op
|
1377
|
+
valid_filterv = ["ERROR"]
|
1378
|
+
end
|
1379
|
+
uputs("DASSET", "The filterv value passed: #{args.filterv} vs the validated value: #{valid_filterv.to_s}")
|
1380
|
+
else
|
1381
|
+
end
|
1382
|
+
|
1383
|
+
if isValid
|
1384
|
+
asset_crit = Nexpose::Criterion.new(Object.const_get(base_field_str + field_str), Object.const_get(base_op_str + op_str), valid_filterv)
|
1385
|
+
|
1386
|
+
# DynamicAsset Group Name and Description, consider adding type validation and MAX length
|
1387
|
+
dag_name = 'api.test'
|
1388
|
+
if args.name
|
1389
|
+
dag_name = args.name.to_s
|
1390
|
+
end
|
1391
|
+
dag_descr = 'This DynamicAssetGroup has been defined programmatically via API and automation script.'
|
1392
|
+
if args.description
|
1393
|
+
dag_descr = args.description.to_s
|
1394
|
+
end
|
1395
|
+
|
1396
|
+
# new or load, based on create vs update
|
1397
|
+
if @action == 1
|
1398
|
+
uputs("ACTION", 'create DASSET action requested')
|
1399
|
+
criteria = Nexpose::Criteria.new(asset_crit)
|
1400
|
+
dag = Nexpose::DynamicAssetGroup.new(dag_name, criteria, dag_descr)
|
1401
|
+
else
|
1402
|
+
uputs("ACTION", 'update DASSET action requested')
|
1403
|
+
dag = Nexpose::DynamicAssetGroup.load(@nsc, args.id.to_i)
|
1404
|
+
dag.criteria.criteria << asset_crit
|
1405
|
+
end
|
1406
|
+
uputs("DASSET", "Attempting save the DynamicAssetGroup: #{@dag_name.to_s}")
|
1407
|
+
begin
|
1408
|
+
if dag.save(@nsc)
|
1409
|
+
uputs("DASSET", "Testing")
|
1410
|
+
upp dag
|
1411
|
+
uputs("DASSET", "Testing - End")
|
1412
|
+
puts "Dynamic Asset Group was successfully created/updated - id: #{dag.id.to_s.ljust(10)} name: \"#{dag.name.to_s}\""
|
1413
|
+
else
|
1414
|
+
puts "Dynamic Asset Group create/update failed - name: \"#{dag.name.to_s}\""
|
1415
|
+
end
|
1416
|
+
rescue Nexpose::APIError => e
|
1417
|
+
uputs("DASSET", "An error occurred while attempting to save the DynamicAssetGroup: #{e.to_s}")
|
1418
|
+
STDERR.puts "ERROR [ " + e.to_s + " ]"
|
1419
|
+
end
|
1420
|
+
upp dag
|
1421
|
+
|
1422
|
+
# End of DAG list loop
|
1423
|
+
else
|
1424
|
+
puts "ERROR: DynamicAssetGroup filter: #{args.filter} is invalid or unsupported."
|
1425
|
+
end
|
1426
|
+
when 2 # list
|
1427
|
+
uputs("ACTION", 'list DASSET action requested')
|
1428
|
+
assetgroups = @nsc.asset_groups
|
1429
|
+
uputs("ACTION", "There are #{assetgroups.size} asset groups.")
|
1430
|
+
assetgroups.each do |ags|
|
1431
|
+
if ags.dynamic
|
1432
|
+
agsnamepattern = ".*"
|
1433
|
+
if args.filter && args.filterv
|
1434
|
+
agsnamepattern = args.filterv
|
1435
|
+
end
|
1436
|
+
if /#{agsnamepattern}/.match(ags.name)
|
1437
|
+
puts "id: #{ags.id.to_s.ljust(10)} name: #{ags.name}"
|
1438
|
+
end
|
1439
|
+
end
|
1440
|
+
end
|
1441
|
+
upp assetgroups
|
1442
|
+
when 4 # show
|
1443
|
+
uputs("ACTION", 'show DASSET action requested')
|
1444
|
+
if is_numeric(args.id)
|
1445
|
+
dag = Nexpose::DynamicAssetGroup.load(@nsc, args.id)
|
1446
|
+
puts "Dynamic Asset Group id: #{dag.id.to_s.ljust(10)} name: \"#{dag.name.to_s}\""
|
1447
|
+
puts "- description: #{dag.description.to_s}"
|
1448
|
+
puts "- is defined by the following criteria:"
|
1449
|
+
dag.criteria.criteria.each do |criterion|
|
1450
|
+
puts "\t field: #{criterion.field.to_s.ljust(20)} operator: #{criterion.operator.to_s.ljust(15)} value: #{criterion.value}"
|
1451
|
+
end
|
1452
|
+
upp dag
|
1453
|
+
else
|
1454
|
+
puts 'The Dynamic Asset Group Id must be numeric.'
|
1455
|
+
end
|
1456
|
+
upp(dag)
|
1457
|
+
when 32 # original update
|
1458
|
+
uputs("ACTION", 'update DASSET action requested')
|
1459
|
+
if is_numeric(args.id)
|
1460
|
+
if args.action
|
1461
|
+
dag = Nexpose::DynamicAssetGroup.load(@nsc, args.id.to_i)
|
1462
|
+
|
1463
|
+
# actions = name, add criteria, match, flush and replace criteria, consider help to display valid search criteria
|
1464
|
+
# filter = Field, Operator, Value
|
1465
|
+
case args.action
|
1466
|
+
when "name"
|
1467
|
+
dag.name = args.filter.to_s
|
1468
|
+
when "append" # append new criteria
|
1469
|
+
field_str = "Nexpose::Search::Field::IP_RANGE"
|
1470
|
+
asset_crit = Nexpose::Criterion.new(Object.const_get(field_str), Nexpose::Search::Operator::IN, ["10.249.20.0", "10.249.20.255"])
|
1471
|
+
dag.criteria.criteria << asset_crit
|
1472
|
+
when "replace" # replace criteria
|
1473
|
+
dag.criteria.criteria.clear
|
1474
|
+
field_str = "Nexpose::Search::Field::OS"
|
1475
|
+
asset_crit = Nexpose::Criterion.new(Object.const_get(field_str), Nexpose::Search::Operator::CONTAINS, 'windows')
|
1476
|
+
dag.criteria.criteria << asset_crit
|
1477
|
+
else
|
1478
|
+
puts 'The action requested is not implemented for target'
|
1479
|
+
end
|
1480
|
+
else
|
1481
|
+
puts "No update action was specified."
|
1482
|
+
end
|
1483
|
+
|
1484
|
+
if dag.save(@nsc)
|
1485
|
+
puts "Dynamic Asset Group was successfully updated - id: #{dag.id.to_s.ljust(10)} name: \"#{dag.name.to_s}\""
|
1486
|
+
else
|
1487
|
+
puts "Dynamic Asset Group update failed - id: #{dag.id.to_s.ljust(10)} name: \"#{dag.name.to_s}\""
|
1488
|
+
end
|
1489
|
+
else
|
1490
|
+
puts 'The Dynamic Asset Group Id must be numeric.'
|
1491
|
+
end
|
1492
|
+
when 16 # delete
|
1493
|
+
# EJG
|
1494
|
+
uputs("ACTION", 'delete DASSET action requested')
|
1495
|
+
@nsc.delete_asset_group(args.id.to_i)
|
1496
|
+
puts "DASSET id #{args.id.to_s} has been deleted."
|
1497
|
+
else
|
1498
|
+
uputs("ACTION", 'The action requested is not implemented for target')
|
1499
|
+
puts 'The action requested is not implemented for target'
|
1500
|
+
end
|
1501
|
+
when 1024 # TARGET TAG
|
1502
|
+
case @action
|
1503
|
+
when 1, 8 # create and update
|
1504
|
+
valid_filterv = nil
|
1505
|
+
base_field_str = "Nexpose::Search::Field::"
|
1506
|
+
base_op_str = "Nexpose::Search::Operator::"
|
1507
|
+
|
1508
|
+
# For now, only support default IP_RANGE defined tags
|
1509
|
+
field_str = "IP_RANGE"
|
1510
|
+
op_str = "IN"
|
1511
|
+
asset_crit = nil
|
1512
|
+
|
1513
|
+
# Insert loop for multi/single tag creation
|
1514
|
+
# EJG
|
1515
|
+
tags_sha2 = false
|
1516
|
+
tags_cursor = 0
|
1517
|
+
tags = []
|
1518
|
+
tag_filtervs = []
|
1519
|
+
|
1520
|
+
# read all tags into an array, from cli arg or file
|
1521
|
+
if args.targets
|
1522
|
+
# expecting fields: action,filter,filterv,name,description
|
1523
|
+
uputs("TAG", "Tags provided within: #{args.targets} file")
|
1524
|
+
end
|
1525
|
+
|
1526
|
+
# if batch processing, grab @action value from field 1 in the targets file record
|
1527
|
+
valid_str = ""
|
1528
|
+
if args.filter
|
1529
|
+
valid_str = validate_searchstring(args.filter)
|
1530
|
+
end
|
1531
|
+
isValid = false
|
1532
|
+
if valid_str != "ERROR"
|
1533
|
+
isValid = true
|
1534
|
+
field_str = valid_str.split(',').first.to_s
|
1535
|
+
op_str = valid_str.split(',').last.to_s
|
1536
|
+
else
|
1537
|
+
end
|
1538
|
+
|
1539
|
+
if args.filterv
|
1540
|
+
# now test filterv based on filter_str and op_str
|
1541
|
+
if field_str == "IP_RANGE"
|
1542
|
+
valid_range = '127.0.0.1:127.0.0.1'
|
1543
|
+
if valid_ipv4(args.filterv)
|
1544
|
+
valid_range = ip2rangev4(args.filterv)
|
1545
|
+
elsif valid_cidrv4(args.filterv)
|
1546
|
+
valid_range = cidr2rangev4(args.filterv)
|
1547
|
+
elsif valid_ipv4_dottedcsv(args.filterv)
|
1548
|
+
# add dotted csv validation for low,high order
|
1549
|
+
valid_range = args.filterv
|
1550
|
+
elsif valid_ipv4_dottedcolon(args.filterv)
|
1551
|
+
# add dotted colon validation for low,high order
|
1552
|
+
valid_range = args.filterv
|
1553
|
+
elsif valid_ipv4_dotteddashed(args.filterv)
|
1554
|
+
valid_range = dotteddashed2rangev4(args.filterv)
|
1555
|
+
elsif valid_ipv4_dotteddashedshort(args.filterv)
|
1556
|
+
valid_range = dotteddashedshort2rangev4(args.filterv)
|
1557
|
+
else
|
1558
|
+
uputs("TAG", "API ERROR: An invalid tag range was provided: #{args.filterv.to_s}")
|
1559
|
+
end
|
1560
|
+
# to allow target file csv format, expect : delimited range or replace "," with ":" here.
|
1561
|
+
vrange = valid_range.dup
|
1562
|
+
vrange.tr!(",",":")
|
1563
|
+
|
1564
|
+
valid_filterv = [vrange.split(':').first.to_s, vrange.split(':').last.to_s]
|
1565
|
+
elsif field_str == "CVSS_SCORE"
|
1566
|
+
# test for float, and possibly IN_RANGE op
|
1567
|
+
valid_filterv = [7.0]
|
1568
|
+
elsif field_str == "RISK_SCORE" || field_str == "SITE_ID"
|
1569
|
+
# test for fixnum, and possibly IN_RANGE op
|
1570
|
+
valid_filterv = [args.filterv.to_i]
|
1571
|
+
elsif field_str == "OS"
|
1572
|
+
# test for str
|
1573
|
+
valid_filterv = [args.filterv.to_s]
|
1574
|
+
else
|
1575
|
+
# assume fixnum, with exception of IN_RANGE op
|
1576
|
+
valid_filterv = ["ERROR"]
|
1577
|
+
end
|
1578
|
+
uputs("TAG", "The filterv value passed: #{args.filterv} vs the validated value: #{valid_filterv.to_s}")
|
1579
|
+
else
|
1580
|
+
end
|
1581
|
+
|
1582
|
+
if isValid
|
1583
|
+
asset_crit = Nexpose::Tag::Criterion.new(Object.const_get(base_field_str + field_str), Object.const_get(base_op_str + op_str), valid_filterv)
|
1584
|
+
|
1585
|
+
# Tag Name and Description, consider adding type validation and MAX length
|
1586
|
+
tag_name = 'api.test'
|
1587
|
+
if args.name
|
1588
|
+
tag_name = args.name.to_s
|
1589
|
+
end
|
1590
|
+
|
1591
|
+
# new or load, based on create vs update
|
1592
|
+
if @action == 1
|
1593
|
+
uputs("ACTION", 'create TAG action requested')
|
1594
|
+
criteria = Nexpose::Tag::Criteria.new(asset_crit)
|
1595
|
+
tag = Nexpose::Tag.new(tag_name, Nexpose::Tag::Type::Generic::CUSTOM)
|
1596
|
+
tag.color = "#de7200"
|
1597
|
+
tag.search_criteria = criteria
|
1598
|
+
else
|
1599
|
+
uputs("ACTION", 'update TAG action requested')
|
1600
|
+
tag = Nexpose::Tag.load(@nsc, args.id.to_i)
|
1601
|
+
tag.search_criteria << asset_crit
|
1602
|
+
end
|
1603
|
+
uputs("TAG", "Attempting save the Tag: #{@tag_name.to_s}")
|
1604
|
+
begin
|
1605
|
+
if tag.save(@nsc)
|
1606
|
+
puts "Tag was successfully created/updated - id: #{tag.id.to_s.ljust(10)} name: \"#{tag.name.to_s}\""
|
1607
|
+
else
|
1608
|
+
puts "Tag create/update failed - name: \"#{tag.name.to_s}\""
|
1609
|
+
end
|
1610
|
+
rescue Nexpose::APIError => e
|
1611
|
+
uputs("TAG", "An error occurred while attempting to save the Tag: #{e.to_s}")
|
1612
|
+
STDERR.puts "ERROR [ " + e.to_s + " ]"
|
1613
|
+
end
|
1614
|
+
upp tag
|
1615
|
+
|
1616
|
+
# End of TAG list loop
|
1617
|
+
else
|
1618
|
+
puts "ERROR: Tag filter: #{args.filter} is invalid or unsupported."
|
1619
|
+
end
|
1620
|
+
when 2 # list
|
1621
|
+
uputs("ACTION", 'list TAG action requested')
|
1622
|
+
tags = @nsc.tags
|
1623
|
+
uputs("ACTION", "There are #{tags.size} asset tags.")
|
1624
|
+
tagsnamepattern = ".*"
|
1625
|
+
if args.filter && args.filterv
|
1626
|
+
tagsnamepattern = args.filterv
|
1627
|
+
end
|
1628
|
+
tags.each do |tag|
|
1629
|
+
if /#{tagsnamepattern}/.match(tag.name)
|
1630
|
+
puts "id: #{tag.id.to_s.ljust(10)} name: #{tag.name}"
|
1631
|
+
end
|
1632
|
+
end
|
1633
|
+
upp tags
|
1634
|
+
when 4 # show
|
1635
|
+
if args.id
|
1636
|
+
tag = Nexpose::Tag.load(@nsc, args.id)
|
1637
|
+
upp tag
|
1638
|
+
end
|
1639
|
+
else
|
1640
|
+
uputs("ACTION", 'The action requested is not implemented for target')
|
1641
|
+
puts 'The action requested is not implemented for target'
|
1642
|
+
end
|
1643
|
+
when 2048 # TARGET CONSOLE
|
1644
|
+
case @action
|
1645
|
+
when 32 # run
|
1646
|
+
if args.action
|
1647
|
+
case args.action
|
1648
|
+
when "backup"
|
1649
|
+
backupdescr = 'Nexpose Console Backup via automation.'
|
1650
|
+
if args.description
|
1651
|
+
backupdescr = args.description.to_s
|
1652
|
+
end
|
1653
|
+
active_scans = @nsc.scan_activity
|
1654
|
+
|
1655
|
+
# attempt to start the backup
|
1656
|
+
if active_scans.empty?
|
1657
|
+
platform_independent = true
|
1658
|
+
@nsc.backup(platform_independent, backupdescr)
|
1659
|
+
puts 'The backup job requested has been submitted and will run shortly.'
|
1660
|
+
uputs("BACKUP", 'The backup requested is running')
|
1661
|
+
else
|
1662
|
+
puts 'There are active scans, please run the backup once the scans have completed.'
|
1663
|
+
uputs("BACKUP", 'The backup requested can not be run at this time, there are active scans')
|
1664
|
+
end
|
1665
|
+
else
|
1666
|
+
puts 'The action requested is not implemented for target'
|
1667
|
+
end
|
1668
|
+
else
|
1669
|
+
puts 'No CONSOLE action was passed'
|
1670
|
+
end
|
1671
|
+
else
|
1672
|
+
uputs("ACTION", 'The action requested is not implemented for target')
|
1673
|
+
puts 'The action requested is not implemented for target'
|
1674
|
+
end
|
1675
|
+
when 8192 # TARGET ROLE
|
1676
|
+
case @action
|
1677
|
+
when 1 # create, via copy for now
|
1678
|
+
rolename = args.name
|
1679
|
+
newrole = Nexpose::Role.copy(@nsc, rolename, 'global' )
|
1680
|
+
newrole.name = args.newname.to_str
|
1681
|
+
newrole.full_name = "#{args.description.to_str}"
|
1682
|
+
newrole.save(@nsc)
|
1683
|
+
upp newrole
|
1684
|
+
puts "\n"
|
1685
|
+
when 2 # list
|
1686
|
+
uputs("ACTION", 'list ROLE action requested')
|
1687
|
+
postd = @nsc.roles
|
1688
|
+
upp postd
|
1689
|
+
puts "\n"
|
1690
|
+
when 4 # show
|
1691
|
+
rolename = args.name
|
1692
|
+
role = Nexpose::Role.load(@nsc, rolename, 'global' )
|
1693
|
+
upp role
|
1694
|
+
puts "\n"
|
1695
|
+
when 16 # delete
|
1696
|
+
rolename = args.name
|
1697
|
+
role = Nexpose::Role.load(@nsc, rolename, 'global' )
|
1698
|
+
role.delete(@nsc)
|
1699
|
+
tryrole = Nexpose::Role.load(@nsc, rolename, 'global' )
|
1700
|
+
upp tryrole
|
1701
|
+
puts "\n"
|
1702
|
+
else
|
1703
|
+
uputs("ACTION", 'The action requested is not implemented for target: ROLE')
|
1704
|
+
puts 'The action requested is not implemented for target: ROLE'
|
1705
|
+
end
|
1706
|
+
else
|
1707
|
+
# there is no default target
|
1708
|
+
uputs("ACTION", 'No default action requested')
|
1709
|
+
raise 'No action target was provided, see --help'
|
1710
|
+
end
|
1711
|
+
|
1712
|
+
# logout and close the connection
|
1713
|
+
uputs("COMMAND", 'The task requested has been completed, closing the NSC connection')
|
1714
|
+
begin
|
1715
|
+
logout_success = @nsc.logout
|
1716
|
+
rescue Nexpose::APIError => e
|
1717
|
+
STDERR.puts "ERROR [ " + e.to_s + " ]"
|
1718
|
+
uputs("CONNECT", "The NSC connection has already been closed, goodbye")
|
1719
|
+
exit(0)
|
1720
|
+
end
|
1721
|
+
uputs("CONNECT", "The NSC connection is closed, goodbye")
|
1722
|
+
##############################################################################
|
1723
|
+
# end
|