nexpose_servicenow 0.4.15

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.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NmE4MDhhMzQ0NDE1ZmZhN2M1NjBjYzI5ODRlYmViMzczOTlkNDRmZg==
5
+ data.tar.gz: !binary |-
6
+ NmEyMjJjOGM2YmUzZGRmYThhZGMzNGI5MWFjNGNkMmU3MzYxMjc4Mg==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ OTMzZmFmYTM3YjBkYjliZGI4ZWY4Mzk2YTIxYzg2M2QxYjE4Mzg0NDI5NmUx
10
+ NmJmNGMzYzEzODRkNGI5YTk2ZTJjNTA0NTFjODRlM2E2NjZjZDkyNzZmYjhl
11
+ MTEwZWRhOWFiZjI0ZTQzZGI5ODZjYzZlZDAzY2U4ZTdlMTI3ZDM=
12
+ data.tar.gz: !binary |-
13
+ Y2ZiZTllNWM2ZDgyM2IwOWRkM2RmY2MwNDQ0NjE5NTI3ODM2YzQ2MGZkYzMw
14
+ M2U0YzdhNGQ4MzI4Mjg2MWM3NjVjOTYxZjI0MjJhOGJhNzkyMmEwOTI3ZjE2
15
+ NzUxNjg1YjgxMjAwODZiZjVjMDVmODI1Mjc2NjQ0MjIzZDc4MmQ=
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in nexpose_servicenow.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 David Valente
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,16 @@
1
+ # NexposeServicenow
2
+
3
+ ## Installation
4
+
5
+ gem install nexpose_servicenow
6
+
7
+ ## Usage
8
+
9
+ ## Development
10
+
11
+ ## Contributing
12
+
13
+ ## License
14
+
15
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
16
+
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'nexpose_servicenow'
3
+
4
+ NexposeServiceNow::Main.start(ARGV)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,173 @@
1
+ require 'csv'
2
+ require 'optparse'
3
+ require 'nexpose'
4
+ require 'uri'
5
+ require 'nexpose_servicenow/queries'
6
+ require 'nexpose_servicenow/nexpose_helper'
7
+ require 'nexpose_servicenow/arg_parser'
8
+ require 'nexpose_servicenow/chunker'
9
+ require 'nexpose_servicenow/nx_logger'
10
+ require 'nexpose_servicenow/historical_data'
11
+ require "nexpose_servicenow/version"
12
+
13
+ module NexposeServiceNow
14
+ class Main
15
+ def self.start(args)
16
+ options = ArgParser.parse(args)
17
+
18
+ log = setup_logging(options)
19
+ log.log_message("Options: #{options}")
20
+
21
+ if options[:nexpose_ids].first.to_s == "0"
22
+ log.log_message('Retrieving array of all site IDs')
23
+ options[:nexpose_ids] = get_nexpose_helper(options).all_sites.sort
24
+ end
25
+
26
+ report_details = NexposeHelper.get_report_names(options[:query],
27
+ options[:nexpose_ids])
28
+ report_details.each do |r|
29
+ r[:report_name] = NexposeHelper.get_filepath(r[:report_name],
30
+ options[:output_dir])
31
+ end
32
+
33
+ update_last_scan_data(options)
34
+ create_report(report_details, options)
35
+
36
+ log.log_message("Initialising #{options[:mode]} mode")
37
+ self.send("#{options[:mode]}_mode", report_details, options)
38
+ end
39
+
40
+ def self.get_nexpose_helper(options)
41
+ NexposeHelper.new(options[:nexpose_url],
42
+ options[:nexpose_port],
43
+ options[:nexpose_username],
44
+ options[:nexpose_password])
45
+ end
46
+
47
+ def self.setup_logging(options)
48
+ log = NexposeServiceNow::NxLogger.instance
49
+ log.setup_statistics_collection(NexposeServiceNow::VENDOR,
50
+ NexposeServiceNow::PRODUCT,
51
+ NexposeServiceNow::VERSION)
52
+ log.setup_logging(true,
53
+ options[:log_level] || 'info',
54
+ false)
55
+ log
56
+ end
57
+
58
+ #Merges in the details from the last time the
59
+ #integration ran reports.
60
+ def self.update_last_scan_data(options)
61
+ return unless options[:mode] == "latest_scans"
62
+ historical_data = HistoricalData.new(options)
63
+ historical_data.update_last_scan_data
64
+ end
65
+
66
+ #Create a report if explicitly required or else an existing
67
+ #report file isn't found
68
+ def self.create_report(report_details, options)
69
+ return if options[:mode].start_with? 'update_'
70
+
71
+ unless options[:gen_report]
72
+ #If any file is missing, perform all queries
73
+ return if report_details.all? { |f| File.exists?(f[:report_name]) }
74
+ end
75
+
76
+ #Filter it down to sites which actively need queried
77
+ sites_to_scan = filter_sites(options)
78
+ nexpose_helper = get_nexpose_helper(options)
79
+ hist_data = HistoricalData.new(options)
80
+ vuln_query = options[:query].to_s.start_with? 'vulnerabili'
81
+ last_run = nil
82
+
83
+ start_time = Time.new
84
+ query_options = { last_scans: hist_data.last_scan_ids(sites_to_scan) }
85
+ query_options[:vuln_query_date] = hist_data.last_vuln_run if vuln_query
86
+
87
+ filename = nexpose_helper.create_report(options[:query],
88
+ sites_to_scan,
89
+ options[:id_type],
90
+ options[:output_dir],
91
+ query_options)
92
+
93
+ hist_data.create_last_vuln_data(start_time, sites_to_scan) if vuln_query
94
+
95
+ #A single String may be returned or an Array of Strings
96
+ if filename.class.to_s == "Array"
97
+ filename.map! { |f| File.expand_path(options[:output_dir], f) }
98
+ return filename.join("\n")
99
+ end
100
+
101
+ File.expand_path(options[:output_dir], filename)
102
+ end
103
+
104
+ def self.filter_sites(options)
105
+ #These queries always run to make sure certain data is up to date
106
+ exceptions = ['vulnerabili', 'asset_groups', 'sites', 'tags']
107
+ if exceptions.any? { |e| options[:query].to_s.start_with? e }
108
+ return options[:nexpose_ids]
109
+ end
110
+
111
+ #Always run the query for latest scans or vulnerabilities
112
+ if options[:mode] == 'latest_scans' ||
113
+ options[:mode] == 'get_chunk'
114
+ return options[:nexpose_ids]
115
+ end
116
+
117
+ historical_data = HistoricalData.new(options)
118
+ sites_to_scan = historical_data.sites_to_scan
119
+
120
+ return sites_to_scan unless (sites_to_scan.nil? || sites_to_scan.empty?)
121
+
122
+ log = NexposeServiceNow::NxLogger.instance
123
+ log.log_message "Sites #{options[:nexpose_ids]} are up to date."
124
+ log.log_message "Query requested was: #{options[:query]}."
125
+ exit 0
126
+ end
127
+
128
+ #Print the chunk info
129
+ def self.chunk_info_mode(report_details, options)
130
+ chunker = Chunker.new(report_details, options[:row_limit])
131
+ filtered_sites = filter_sites(options)
132
+ puts chunker.preprocess(filtered_sites)
133
+ end
134
+
135
+ #Prints a chunk of CSV to the console
136
+ def self.get_chunk_mode(report_details, options)
137
+ #Get the byte offset and length
138
+ chunker = Chunker.new(report_details, options[:row_limit])
139
+ filtered_sites = filter_sites(options)
140
+
141
+ puts chunker.read_chunk(options[:chunk_start],
142
+ options[:chunk_length],
143
+ filtered_sites.first)
144
+ end
145
+
146
+ def self.latest_scans_mode(report_details, options)
147
+ historical_data = HistoricalData.new(options)
148
+ puts historical_data.filter_report
149
+ end
150
+
151
+ def self.remove_last_scan_mode(report_details, options)
152
+ historical_data = HistoricalData.new(options)
153
+ historical_data.remove_last_scan_data
154
+ end
155
+
156
+ def self.update_last_scan_mode(report_details, options)
157
+ historical_data = HistoricalData.new(options)
158
+ historical_data.set_last_scan(options[:nexpose_ids].first,
159
+ options[:last_scan_data])
160
+ end
161
+
162
+ def self.remove_last_vuln_mode(report_details, options)
163
+ historical_data = HistoricalData.new(options)
164
+ historical_data.remove_last_vuln_data
165
+ end
166
+
167
+ def self.update_last_vuln_mode(report_details, options)
168
+ historical_data = HistoricalData.new(options)
169
+ historical_data.set_last_vuln(options[:last_scan_data],
170
+ options[:nexpose_ids])
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,173 @@
1
+ require 'optparse'
2
+ require 'json'
3
+ require_relative './queries'
4
+ require_relative './nx_logger'
5
+
6
+ module NexposeServiceNow
7
+ class ArgParser
8
+ NX_ID_TYPES = %i[site tag]
9
+ MODES = %i[chunk_info get_chunk latest_scans
10
+ remove_last_scan update_last_scan
11
+ remove_last_vuln update_last_vuln]
12
+
13
+ QUERY_NAMES = Queries.methods(false)
14
+
15
+ QUERY_ALIASES = { 'devices' => 'cmdb_ci_outofband_device',
16
+ 'vuln_items' => 'sn_vul_vulnerable_item',
17
+ 'vuln_entries' => 'sn_vul_third_party_entry' }
18
+
19
+ def self.parse(args)
20
+ options = Hash.new
21
+
22
+ opt_parser = OptionParser.new do |opts|
23
+ opts.banner = "Usage: example.rb [options]"
24
+
25
+ opts.on("-o", "--output-dir DIRECTORY",
26
+ "Directory in which to save reports") do |output_dir|
27
+ options[:output_dir] = output_dir
28
+ end
29
+
30
+ opts.on("-m", "--mode MODE",
31
+ "Mode for program output. (#{MODES.join(', ')})") do |mode|
32
+ options[:mode] = mode
33
+ end
34
+
35
+ opts.on("-g", "--generate-report BOOLEAN",
36
+ "True to generate and download new report.") do |gen|
37
+ char = gen.downcase[0]
38
+ options[:gen_report] = char == 'y' || char == 't'
39
+ end
40
+
41
+ opts.separator ""
42
+ opts.separator "Query options:"
43
+
44
+ opts.on("-q", "--query QUERY", QUERY_NAMES,
45
+ "Select query (#{QUERY_NAMES.join(', ')})") do |query|
46
+ options[:query] = query
47
+ end
48
+
49
+ opts.on("-t", "--type TYPE", NX_ID_TYPES,
50
+ "Select type (#{NX_ID_TYPES.join(', ')})") do |type|
51
+ options[:id_type] = type
52
+ end
53
+
54
+ opts.on("-i", "--items x,y,z", Array,
55
+ "IDs of the nexpose items to scan") do |items|
56
+ options[:nexpose_ids] = items
57
+ end
58
+
59
+ opts.separator ""
60
+ opts.separator "Nexpose options:"
61
+
62
+ opts.on("-n", "--nexpose-address URL",
63
+ "URL of the Nexpose server") do |url|
64
+ port = url.slice!(/:(\d+)$/)
65
+ port.slice! ':' unless port.nil?
66
+
67
+ url.slice! /https:\/\//
68
+ options[:nexpose_url] = url
69
+ options[:nexpose_port] = port || '3780'
70
+ @full_url = options[:nexpose_url] + ':' + options[:nexpose_port]
71
+ end
72
+
73
+ opts.on("-u", "--user USER",
74
+ "Username for Nexpose console") do |username|
75
+ options[:nexpose_username] = username
76
+ end
77
+
78
+ opts.on("-p", "--password PASSWORD",
79
+ "Password for the Nexpose user") do |password|
80
+ options[:nexpose_password] = password
81
+ end
82
+
83
+ opts.separator ""
84
+ opts.separator "Chunk info mode options:"
85
+
86
+ opts.on("-r", "--row-limit LIMIT",
87
+ "Maximum number of rows per chunk (with header).") do |limit|
88
+ options[:row_limit] = limit.to_i
89
+ options[:row_limit] = 9_999_999 if options[:row_limit] <= 0
90
+ end
91
+
92
+ opts.separator ""
93
+ opts.separator "Get chunk mode options:"
94
+
95
+ opts.on("-s", "--start START",
96
+ "The chunk starting offset.") do |start|
97
+ options[:chunk_start] = start.to_i
98
+ end
99
+
100
+ opts.on("-l", "--length LENGTH",
101
+ "The chunk length.") do |length|
102
+ options[:chunk_length] = length.to_i
103
+ end
104
+
105
+ opts.separator ""
106
+ opts.separator "Last scan file modification options:"
107
+
108
+ opts.on("-d", "--data DATA",
109
+ "Date or scan ID to be inserted in last scan file.") do |data|
110
+ options[:last_scan_data] = data
111
+ end
112
+
113
+ opts.separator ""
114
+ opts.separator "Common options:"
115
+
116
+ opts.on_tail("-h", "--help", "Show this message") do
117
+ puts opts
118
+ exit
119
+ end
120
+ end
121
+
122
+ opt_parser.parse!(args)
123
+ options = self.validate_input(options)
124
+ options = self.get_env_settings(options)
125
+ options
126
+ end
127
+
128
+ #TODO: Validate input depending on mode AND whether generating a report
129
+ # is required.
130
+ def self.validate_input(options)
131
+ #Insert defaults. Some are mode-specific.
132
+ options[:output_dir] ||= '.'
133
+ options[:row_limit] ||= 9_999_999
134
+ options[:id_type] ||= 'site'
135
+ options[:nexpose_ids] ||= []
136
+
137
+ options[:query] = 'latest_scans' if options[:mode] == 'latest_scans'
138
+
139
+ #By default, a report won't be generated if a chunk's being retrieved
140
+ if options[:gen_report].nil?
141
+ options[:gen_report] = options[:mode] == "chunk_info" ||
142
+ options[:mode] == "latest_scans"
143
+ end
144
+
145
+ options
146
+ end
147
+
148
+ def self.get_env_settings(options)
149
+ #Only need these if a query is being performed
150
+ return options if options[:gen_report] == false
151
+
152
+ log = NexposeServiceNow::NxLogger.instance
153
+ log.log_message "Retrieving environment variables."
154
+
155
+ # Retrieve environment variable settings
156
+ %i[url port username password].each do |setting|
157
+ option = "nexpose_#{setting}"
158
+ setting = ENV[option.upcase]
159
+ sym = option.intern
160
+ options[sym] ||= setting
161
+
162
+ if options[sym].nil?
163
+ error = "Option #{sym} wasn't supplied."
164
+ log.log_error_message error
165
+ $stderr.puts "ERROR: #{error}"
166
+ exit -1
167
+ end
168
+ end
169
+
170
+ options
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,106 @@
1
+
2
+ module NexposeServiceNow
3
+ class Chunker
4
+ def initialize(report_details, row_limit)
5
+ @row_limit = row_limit
6
+ @size_limit = 4_500_000
7
+ @report_details = report_details
8
+ @header = get_header
9
+
10
+ setup_logging
11
+ end
12
+
13
+ def setup_logging
14
+ @log = NexposeServiceNow::NxLogger.instance
15
+ @log.log_message("Chunker File Limit: #{@size_limit}MB");
16
+ @log.log_message("Chunk Row Limit: #{@row_limit}")
17
+ end
18
+
19
+ #Grab the header from the first file
20
+ def get_header
21
+ file = File.open(@report_details.first[:report_name], "r")
22
+ header = file.readline
23
+ file.close
24
+
25
+ header
26
+ end
27
+
28
+ def preprocess(nexpose_ids=nil)
29
+ @log.log_message("Breaking file #{@file_path} down into chunks.")
30
+
31
+ all_chunks = []
32
+ @report_details.each do |report|
33
+ chunks = process_file(report[:report_name], report[:id])
34
+ all_chunks.concat chunks
35
+ end
36
+
37
+ @log.log_message("Files broken down into #{all_chunks.count} chunks")
38
+
39
+ puts all_chunks.to_json
40
+ end
41
+
42
+ def process_file(file_path, site_id=nil)
43
+ relative_size_limit = @size_limit - @header.bytesize
44
+ chunk = { site_id: site_id,
45
+ start: @header.bytesize,
46
+ length: 0,
47
+ row_count: 0 }
48
+
49
+ chunks = []
50
+ csv_file = CSV.open(file_path, "r", headers: true)
51
+ while(true)
52
+ position = csv_file.pos
53
+ line = csv_file.shift
54
+ row_length = line.to_s.bytesize
55
+
56
+ if line.nil?
57
+ chunks << chunk
58
+ break
59
+ elsif chunk[:length]+row_length < relative_size_limit &&
60
+ chunk[:row_count] + 1 < @row_limit
61
+ chunk[:length] += row_length
62
+ chunk[:row_count] += 1
63
+ else
64
+ chunks << chunk
65
+
66
+ #Initialise chunk with this row information
67
+ chunk = { site_id: site_id,
68
+ start: position,
69
+ length: row_length,
70
+ row_count: 1 }
71
+ end
72
+ end
73
+ csv_file.close
74
+
75
+ #Should we include the row count?
76
+ chunks.each do |c|
77
+ c.delete :row_count
78
+
79
+ #Should we do this...?
80
+ c.delete :site_id if c[:site_id].nil? || c[:site_id] == -1
81
+ end
82
+
83
+ chunks
84
+ end
85
+
86
+ def get_file(site_id=nil)
87
+ #-1 indicates a single query report
88
+ return @report_details.first[:report_name] if site_id.to_i <= 0
89
+
90
+ report = @report_details.find { |r| r[:id].to_s == site_id.to_s }
91
+ report[:report_name]
92
+ end
93
+
94
+ def read_chunk(start, length, site_id=nil)
95
+ @log.log_message("Returning chunk. Start: #{start}, Length: #{length}, File: #{@file_path}")
96
+
97
+ #If the header isn't in the chunk, prepend it
98
+ header = start == 0 ? "" : @header
99
+
100
+ file = File.open(get_file(site_id), "rb")
101
+ file.seek(start)
102
+ puts header + file.read(length)
103
+ file.close
104
+ end
105
+ end
106
+ end