nexpose_ticketing 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MWYyZjg5NzU4ZDJhN2MxZmE5OTY4MjFhMjc4OTA0YzhjNjA5ZDA3ZA==
5
+ data.tar.gz: !binary |-
6
+ MzgwMjQ4M2ViNmFkMGVhMjQ5NWM0NGFiOGIwNTgxZDBkNjJhOGU5OA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NzQ3N2I1ZjJiMDdlMmJmOGJkZTYwNjI4YTBlZTU1N2IzNGEyZWY2YmY5NTBl
10
+ NTQ2MmRjODcwOGY5NTlmNjNhNGMyYTljZTgxNzU2NGU0MGY2ZTIxMThiMGUw
11
+ ZTNiZWM1ODQ3MjI0ODA2NmZmYWVkMDJmZDY5MTI5MzY4NjIxZDU=
12
+ data.tar.gz: !binary |-
13
+ NTUxYzM5YmUzYmVlMWRiNmM3NjhiZTgxYWEyNWE4YzEzNDdhZGQ2ZjkyZTcw
14
+ NDQ1ZDBkZjE1YmQ5Nzg3ZmQwZDY3NDM4YzcyZWFkMmM2MDM3NzRmODFiYjdk
15
+ YjhjMDNhNzQyYTJjNTA3MDNhMDkxMjY0OWEyMTgxOGU3YzlmMzk=
data/README.markdown ADDED
@@ -0,0 +1,38 @@
1
+ # Nexpose Ticketing Engine.
2
+
3
+ This is the official gem package for the Ruby Nexpose Ticketing engine.
4
+
5
+ To share your scripts, or to discuss different approaches, please visit the Rapid7 forums for Nexpose: https://community.rapid7.com/community/nexpose
6
+
7
+ For assistance with using the gem please email the Rapid7 integrations support team at integrations_support@rapid7.com.
8
+
9
+ # Usage
10
+
11
+ To use the JIRA implementation please follow these steps:
12
+ * Edit the jira.config file under config and add the necessary data.
13
+ * Edit the ticket_service.config file under config and add the necessary data.
14
+ * Run the jira file under the bin folder. If installed with Gem 'console> jira' should suffice.
15
+
16
+ A logger is implemented by default, and the log can be found under the log folder; please refer to the log file in case of an error.
17
+
18
+
19
+ ## Contributions
20
+
21
+ This package is currently a work in progress. Currently there's only a JIRA implementation, with more on the works.
22
+
23
+ To develop your own implementation for Ticketing service 'foo':
24
+
25
+ * Create a helper class that implements the following methods:
26
+ ** create_ticket(tickets) - This method should implement the transport class for the 'foo' service (https, smtp, SOAP, etc).
27
+ ** prepare_tickets(tickets) - This method will call the selected preparation type: default or ip.
28
+ ** prepare_tickets_default(vulnerability_list) - This method will take the vulnerability_list in CSV format and transform it into 'foo' accepted data (JSON, XML, etc) per vulnerability.
29
+ ** prepare_tickets_by_ip(vulnerability_list) - This method will take the vulnerability_list in CSV format and transform it into 'food' accepted data (JSON, XML, etc) collapsing all vulnerabilities by IP.
30
+ * Create your 'foo' caller under bin. See the file 'jira' for reference.
31
+
32
+ Please see jira_helper.rb under helpers for an helper example, and two_vulns_report.csv under the test folder for a sample CSV report.
33
+
34
+ We welcome contributions to this package. We ask only that pull requests and patches adhere to our coding standards.
35
+
36
+ * Favor returning classes over key-value maps. Classes tend to be easier for users to manipulate and use.
37
+ * Unless otherwise noted, code should adhere to the Ruby Style Guide: https://github.com/bbatsov/ruby-style-guide
38
+ * Use YARDoc comment style to improve the API documentation of the gem.
data/bin/nexpose_jira ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ require 'yaml'
3
+ require 'nexpose_ticketing'
4
+ # Path to the JIRA Configuration file.
5
+ JIRA_CONFIG_PATH = File.join(File.dirname(__FILE__),'../lib/nexpose_ticketing/config/jira.config')
6
+ #JIRA_CONFIG_PATH = '../lib/nexpose_ticketing/config/jira.config'
7
+
8
+ # Read in JIRA options from jira.config.
9
+ jira_options = begin
10
+ YAML.load_file(JIRA_CONFIG_PATH)
11
+ rescue ArgumentError => e
12
+ raise "Could not parse YAML #{JIRA_CONFIG_PATH} : #{e.message}"
13
+ end
14
+
15
+ # Initialize Ticket Service using JIRA.
16
+ NexposeTicketing.start(jira_options)
17
+
@@ -0,0 +1,11 @@
1
+ ---
2
+ # This configuration file defines all the particular options necessary to support the helper.
3
+ # Fields marked (M) are mandatory.
4
+ #
5
+ # (M)) Helper class name.
6
+ :helper_name: JiraHelper
7
+ # Optional parameters, these are implementation specific.
8
+ :jira_url: https://url/rest/api/2/issue/
9
+ :username: jirausername
10
+ :password: jirapassword
11
+ :project: projectname
@@ -0,0 +1,27 @@
1
+ ---
2
+ # This configuration file defines all the particular options necessary to run the service.
3
+ # Fields marked (M) are mandatory.
4
+ #
5
+ # Service options:
6
+ :options:
7
+ # (M) Enables logging to the log directory.
8
+ :logging_enabled: true
9
+ # Filters the reports to specific sites one per line, leave empty for no site.
10
+ :sites:
11
+ - '1'
12
+ # Minimum floor severity to report on. Number between 0 and 10.
13
+ :severity: 8
14
+ # (M) Name of the report historial file saved in disk.
15
+ :file_name: last_scan_data.csv
16
+ # (M) Defines the ticket creation mode:
17
+ # 'D' Default IP *-* Vulnerability
18
+ # 'I' IP address -* Vulnerability
19
+ :ticket_mode: I
20
+ # Nexpose options.
21
+ :nexpose_data:
22
+ # (M) Nexpose console hostname.
23
+ :nxconsole: 127.0.0.1
24
+ # (M) Nexpose username.
25
+ :nxuser: nxusername
26
+ # (M) Nexpose password.
27
+ :nxpasswd: nxpassword
@@ -0,0 +1,104 @@
1
+ # This class serves as the JIRA interface
2
+ # that creates issues within JIRA from vulnerabilities
3
+ # found in Nexpose.
4
+ # Copyright:: Copyright (c) 2014 Rapid7, LLC.
5
+ require 'json'
6
+ require 'net/http'
7
+ require 'net/https'
8
+ require 'uri'
9
+ require 'csv'
10
+ class JiraHelper
11
+ attr_accessor :jira_data, :options
12
+ def initialize(jira_data, options)
13
+ @jira_data = jira_data
14
+ @options = options
15
+ end
16
+
17
+ def create_ticket(tickets)
18
+ fail 'Ticket(s) cannot be empty.' if tickets.empty? || tickets.nil?
19
+ tickets.each do |ticket|
20
+ headers = { 'Content-Type' => 'application/json',
21
+ 'Accept' => 'application/json' }
22
+ url = URI.parse("#{@jira_data[:jira_url]}")
23
+ req = Net::HTTP::Post.new(@jira_data[:jira_url], headers)
24
+ req.basic_auth @jira_data[:username], @jira_data[:password]
25
+ req.body = ticket
26
+ resp = Net::HTTP.new(url.host, url.port)
27
+ # Enable this line for debugging the https call.
28
+ # resp.set_debug_output $stderr
29
+ resp.use_ssl = true if @jira_data[:jira_url].to_s.start_with?('https')
30
+ resp.verify_mode = OpenSSL::SSL::VERIFY_NONE
31
+ resp.start { |http| http.request(req) }
32
+ end
33
+ end
34
+
35
+ # Prepares tickets from the CSV.
36
+ def prepare_tickets(vulnerability_list)
37
+ @ticket = Hash.new(-1)
38
+ case @options[:ticket_mode]
39
+ # 'D' Default IP *-* Vulnerability
40
+ when 'D'
41
+ prepare_tickets_default(vulnerability_list)
42
+ # 'I' IP address -* Vulnerability
43
+ when 'I'
44
+ prepare_tickets_by_ip(vulnerability_list)
45
+ else
46
+ fail 'No ticketing mode selected.'
47
+ end
48
+ end
49
+
50
+ # Prepares and creates tickets in default mode.
51
+ def prepare_tickets_default(vulnerability_list)
52
+ tickets = []
53
+ CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
54
+ # JiraHelper doesn't like new line characters in their summaries.
55
+ summary = row['summary'].gsub(/\n/, ' ')
56
+ ticket = {
57
+ 'fields' => {
58
+ 'project' => {
59
+ 'key' => "#{@jira_data[:project]}" },
60
+ 'summary' => "#{row['ip_address']} => #{summary}",
61
+ 'description' => "#{row['fix']} \n\n #{row['url']}",
62
+ 'issuetype' => {
63
+ 'name' => 'Task' }
64
+ }
65
+ }.to_json
66
+ tickets.push(ticket)
67
+ end
68
+ tickets
69
+ end
70
+
71
+ # Prepares and creates tickets in IP mode.
72
+ def prepare_tickets_by_ip(vulnerability_list)
73
+ tickets = []
74
+ current_ip = -1
75
+ CSV.parse(vulnerability_list.chomp, headers: :first_row) do |row|
76
+ if current_ip == -1
77
+ current_ip = row['ip_address']
78
+ @ticket = {
79
+ 'fields' => {
80
+ 'project' => {
81
+ 'key' => "#{@jira_data[:project]}" },
82
+ 'summary' => "#{row['ip_address']} => Vulnerabilities",
83
+ 'description' => '',
84
+ 'issuetype' => {
85
+ 'name' => 'Task' }
86
+ }
87
+ }
88
+ end
89
+ if current_ip == row['ip_address']
90
+ @ticket['fields']['description'] += "\n ==============================\n
91
+ #{row['summary']} \n ==============================\n
92
+ \n #{row['fix']}\n\n #{row['url']}"
93
+ end
94
+ unless current_ip == row['ip_address']
95
+ @ticket = @ticket.to_json
96
+ tickets.push(@ticket)
97
+ current_ip = -1
98
+ redo
99
+ end
100
+ end
101
+ tickets.push(@ticket.to_json) unless @ticket.nil?
102
+ tickets
103
+ end
104
+ end
@@ -0,0 +1,60 @@
1
+ module NexposeTicketing
2
+ # This class serves as repository of SQL queries
3
+ # to be executed by the SQL Repository Exporter
4
+ # for Nexpose.
5
+ # Copyright:: Copyright (c) 2014 Rapid7, LLC.
6
+ module Queries
7
+ # Gets all the latests scans.
8
+ # Returns |site.id| |last_scan_id| |finished|
9
+ def Queries.last_scans
10
+ 'SELECT ds.site_id, ds.last_scan_id, dsc.finished
11
+ FROM dim_site ds
12
+ JOIN dim_scan dsc ON ds.last_scan_id = dsc.scan_id'
13
+ end
14
+
15
+ # Gets all delta vulns for all sites.
16
+ # Returns |asset_id| |ip_address| |current_scan| |vulnerability_id|
17
+ # |solution_id| |nexpose_id| |url| |summary| |fix|
18
+ def Queries.all_delta_vulns
19
+ "SELECT subs.asset_id, da.ip_address, subs.current_scan, subs.vulnerability_id, davs.solution_id, ds.nexpose_id, ds.url,
20
+ proofAsText(ds.summary) as summary, proofAsText(ds.fix) as fix
21
+ FROM (SELECT fasv.asset_id, fasv.vulnerability_id, s.current_scan
22
+ FROM fact_asset_scan_vulnerability_finding fasv
23
+ JOIN
24
+ (
25
+ SELECT asset_id, previousScan(asset_id) AS baseline_scan, lastScan(asset_id) AS current_scan
26
+ FROM dim_asset) s
27
+ ON s.asset_id = fasv.asset_id AND (fasv.scan_id = s.baseline_scan OR fasv.scan_id = s.current_scan)
28
+ GROUP BY fasv.asset_id, fasv.vulnerability_id, s.current_scan
29
+ HAVING baselineComparison(fasv.scan_id, current_scan) = 'New'
30
+ ) subs
31
+ JOIN dim_asset_vulnerability_solution davs USING (vulnerability_id)
32
+ JOIN dim_solution ds USING (solution_id)
33
+ JOIN dim_asset da ON subs.asset_id = da.asset_id
34
+ ORDER BY da.ip_address"
35
+ end
36
+
37
+ # Gets all delta vulns happening after reported scan id
38
+ # Returns |asset_id| |ip_address| |current_scan| |vulnerability_id|
39
+ # |solution_id| |nexpose_id| |url| |summary| |fix|
40
+ def Queries.delta_vulns_since_scan(reported_scan)
41
+ "SELECT subs.asset_id, da.ip_address, subs.current_scan, subs.vulnerability_id, davs.solution_id, ds.nexpose_id, ds.url,
42
+ proofAsText(ds.summary) as summary, proofAsText(ds.fix) as fix
43
+ FROM (SELECT fasv.asset_id, fasv.vulnerability_id, s.current_scan
44
+ FROM fact_asset_scan_vulnerability_finding fasv
45
+ JOIN
46
+ (
47
+ SELECT asset_id, previousScan(asset_id) AS baseline_scan, lastScan(asset_id) AS current_scan
48
+ FROM dim_asset) s
49
+ ON s.asset_id = fasv.asset_id AND (fasv.scan_id > #{reported_scan} OR fasv.scan_id = s.current_scan)
50
+ GROUP BY fasv.asset_id, fasv.vulnerability_id, s.current_scan
51
+ HAVING baselineComparison(fasv.scan_id, current_scan) = 'New'
52
+ ) subs
53
+ JOIN dim_asset_vulnerability_solution davs USING (vulnerability_id)
54
+ JOIN dim_solution ds USING (solution_id)
55
+ JOIN dim_asset da ON subs.asset_id = da.asset_id
56
+ AND subs.current_scan > #{reported_scan}
57
+ ORDER BY da.ip_address"
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,103 @@
1
+ module NexposeTicketing
2
+ # Repository class that creates and returns generated reports.
3
+ class TicketRepository
4
+ require 'csv'
5
+ require 'nexpose'
6
+ require 'nexpose_ticketing/queries'
7
+
8
+ def nexpose_login (nexpose_data)
9
+ @nsc = Nexpose::Connection.new(nexpose_data[:nxconsole], nexpose_data[:nxuser], nexpose_data[:nxpasswd])
10
+ @nsc.login
11
+ end
12
+
13
+ # Reads the site scan history from disk.
14
+ #
15
+ # * *Args* :
16
+ # - +csv_file_name+ - CSV File name.
17
+ #
18
+ # * *Returns* :
19
+ # - A hash with site_ids => last_scan_id
20
+ #
21
+ def read_last_scans(csv_file_name)
22
+ file_site_histories = Hash.new(-1)
23
+ CSV.foreach(csv_file_name, headers: true) do |row|
24
+ file_site_histories[row['site_id']] = row['last_scan_id']
25
+ end
26
+ file_site_histories
27
+ end
28
+
29
+ # Saves the last scan info to disk.
30
+ #
31
+ # * *Args* :
32
+ # - +csv_file_name+ - CSV File name.
33
+ #
34
+ def save_last_scans(csv_file_name, saved_file = nil, report_config = Nexpose::AdhocReportConfig.new(nil, 'sql'))
35
+ report_config.add_filter('version', '1.1.0')
36
+ report_config.add_filter('query', Queries.last_scans)
37
+ report_output = report_config.generate(@nsc)
38
+ csv_output = CSV.parse(report_output.chomp, headers: :first_row )
39
+ saved_file.open(csv_file_name, 'w') { |file| file.puts(csv_output) } unless saved_file.nil?
40
+ if saved_file.nil?
41
+ File.open(csv_file_name, 'w') { |file| file.puts(csv_output) }
42
+ end
43
+ end
44
+
45
+ # Gets the last scan information from nexpose.
46
+ #
47
+ # * *Returns* :
48
+ # - A hash with site_ids => last_scan_id
49
+ #
50
+ def last_scans(report_config = Nexpose::AdhocReportConfig.new(nil, 'sql'))
51
+ report_config.add_filter('version', '1.1.0')
52
+ report_config.add_filter('query', Queries.last_scans)
53
+ report_output = report_config.generate(@nsc).chomp
54
+ nexpose_sites = Hash.new(-1)
55
+ CSV.parse(report_output, headers: :first_row) do |row|
56
+ nexpose_sites[row['site_id']] = row['last_scan_id'].to_i
57
+ end
58
+ nexpose_sites
59
+ end
60
+
61
+ # Gets all the vulnerabilities for a new site or fresh install.
62
+ #
63
+ # * *Args* :
64
+ # - +site_options+ - A Hash with site(s) and severity level.
65
+ #
66
+ # * *Returns* :
67
+ # - Returns CSV |asset_id| |ip_address| |current_scan| |vulnerability_id| |solution_id| |nexpose_id| |url| |summary| |fix|
68
+ #
69
+ def all_vulns(site_options = {}, report_config = Nexpose::AdhocReportConfig.new(nil, 'sql'))
70
+ sites = Array(site_options[:sites])
71
+ severity = site_options[:severity].nil? ? 0 : site_options[:severity]
72
+ report_config.add_filter('version', '1.1.0')
73
+ report_config.add_filter('query', Queries.all_delta_vulns)
74
+ unless sites.empty?
75
+ sites.each do |site_id|
76
+ report_config.add_filter('site', site_id)
77
+ end
78
+ end
79
+ report_config.add_filter('vuln-severity', severity)
80
+ report_config.generate(@nsc)
81
+ end
82
+
83
+ # Gets the delta vulns from base scan reported_scan_id and the newest / latest scan from a site.
84
+ #
85
+ # * *Args* :
86
+ # - +site_options+ - A Hash with site(s), reported_scan_id and severity level.
87
+ #
88
+ # * *Returns* :
89
+ # - Returns CSV |asset_id| |ip_address| |current_scan| |vulnerability_id| |solution_id| |nexpose_id| |url| |summary| |fix|
90
+ #
91
+ def delta_vulns_sites(site_options = {}, report_config = Nexpose::AdhocReportConfig.new(nil, 'sql'))
92
+ site = site_options[:site_id]
93
+ reported_scan_id = site_options[:scan_id]
94
+ fail 'Site cannot be null or empty' if site.nil? || reported_scan_id.nil?
95
+ severity = site_options[:severity].nil? ? 0 : site_options[:severity]
96
+ report_config.add_filter('version', '1.1.0')
97
+ report_config.add_filter('query', Queries.delta_vulns_since_scan(reported_scan_id))
98
+ report_config.add_filter('site', site)
99
+ report_config.add_filter('vuln-severity', severity)
100
+ report_config.generate(@nsc)
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,197 @@
1
+ module NexposeTicketing
2
+ #
3
+ # The Nexpose Ticketing service.
4
+ #
5
+ =begin
6
+
7
+ Copyright (C) 2014, Rapid7 LLC
8
+ All rights reserved.
9
+
10
+ Redistribution and use in source and binary forms, with or without modification,
11
+ are permitted provided that the following conditions are met:
12
+
13
+ * Redistributions of source code must retain the above copyright notice,
14
+ this list of conditions and the following disclaimer.
15
+
16
+ * Redistributions in binary form must reproduce the above copyright notice,
17
+ this list of conditions and the following disclaimer in the documentation
18
+ and/or other materials provided with the distribution.
19
+
20
+ * Neither the name of Rapid7 LLC nor the names of its contributors
21
+ may be used to endorse or promote products derived from this software
22
+ without specific prior written permission.
23
+
24
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
25
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
28
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
29
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
30
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
31
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
32
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
33
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34
+
35
+ =end
36
+
37
+ #
38
+ # WARNING! This code makes an SSL connection to the Nexpose server, but does NOT
39
+ # verify the certificate at this time. This can be a security issue if
40
+ # an attacker is able to man-in-the-middle the connection between the
41
+ # Metasploit console and the Nexpose server. In the common case of
42
+ # running Nexpose and Metasploit on the same host, this is a low risk.
43
+ #
44
+
45
+ #
46
+ # WARNING! This code is still rough and going through substantive changes. While
47
+ # you can build tools using this library today, keep in mind that
48
+ # method names and parameters may change in the future.
49
+ #
50
+ class TicketService
51
+ require 'csv'
52
+ require 'yaml'
53
+ require 'fileutils'
54
+ require 'nexpose_ticketing/ticket_repository'
55
+
56
+ TICKET_SERVICE_CONFIG_PATH = File.join(File.dirname(__FILE__),'/config/ticket_service.config')
57
+ LOGGER_FILE = File.join(File.dirname(__FILE__),'/log/ticket_service.log')
58
+
59
+ attr_accessor :helper_data, :nexpose_data, :options, :ticket_repository, :first_time, :nexpose_site_histories
60
+
61
+ def setup(helper_data)
62
+ # Gets the Ticket Service configuration.
63
+ service_data = begin
64
+ YAML.load_file(TICKET_SERVICE_CONFIG_PATH)
65
+ rescue ArgumentError => e
66
+ raise "Could not parse YAML #{TICKET_SERVICE_CONFIG_PATH} : #{e.message}"
67
+ end
68
+ @helper_data = helper_data
69
+ @nexpose_data = service_data[:nexpose_data]
70
+ @options = service_data[:options]
71
+ @options[:file_name] = "#{@options[:file_name]}"
72
+
73
+ # Setups logging if enabled.
74
+ setup_logging(@options[:logging_enabled])
75
+
76
+ # Loads all the helpers.
77
+ log_message('Loading helpers.')
78
+ Dir[File.join(File.dirname(__FILE__),'/helpers/*.rb')].each do |file|
79
+ log_message("Loading helper: #{file}")
80
+ require_relative file
81
+ end
82
+ log_message("Enabling helper: #{@helper_data[:helper_name]}.")
83
+ @helper = eval(@helper_data[:helper_name]).new(@helper_data, @options)
84
+ @ticket_repository = NexposeTicketing::TicketRepository.new
85
+ @ticket_repository.nexpose_login(@nexpose_data)
86
+ @first_time = false
87
+ end
88
+
89
+ def setup_logging(enabled = false)
90
+ if enabled
91
+ require 'logger'
92
+ directory = File.dirname(LOGGER_FILE)
93
+ FileUtils.mkdir_p(directory) unless File.directory?(directory)
94
+ @log = Logger.new(LOGGER_FILE, 'monthly')
95
+ @log.level = Logger::INFO
96
+ log_message('Logging enabled, starting service.')
97
+ end
98
+ end
99
+
100
+ # Logs a message if logging is enabled.
101
+ def log_message(message)
102
+ @log.info(message) if @options[:logging_enabled]
103
+ end
104
+
105
+ # Prepares all the local and nexpose historical data.
106
+ def prepare_historical_data(ticket_repository, options, historical_scan_file = File.join(File.dirname(__FILE__),"#{options[:file_name]}"))
107
+ if File.exists?(historical_scan_file)
108
+ log_message("Reading historical CSV file: #{historical_scan_file}.")
109
+ file_site_histories = ticket_repository.read_last_scans(historical_scan_file)
110
+ else
111
+ log_message('No historical CSV file found. Generating.')
112
+ ticket_repository.save_last_scans(historical_scan_file)
113
+ log_message('Historical CSV file generated.')
114
+ file_site_histories = ticket_repository.read_last_scans(historical_scan_file)
115
+ @first_time = true
116
+ end
117
+ file_site_histories
118
+ end
119
+
120
+ # Generates a full site(s) report ticket(s).
121
+ def all_site_report(ticket_repository, options, helper, historical_scan_file = File.join(File.dirname(__FILE__),"#{options[:file_name]}") )
122
+ log_message('First time run, generating full vulnerability report.') if @first_time
123
+ log_message('No site(s) specified, generating full vulnerability report.') if options[:sites].empty?
124
+ all_delta_vulns = ticket_repository.all_vulns(severity: options[:severity])
125
+ log_message('Preparing tickets.')
126
+ tickets = helper.prepare_tickets(all_delta_vulns)
127
+ helper.create_ticket(tickets)
128
+ log_message("Done processing, updating historical CSV file #{historical_scan_file}.")
129
+ ticket_repository.save_last_scans(historical_scan_file)
130
+ log_message('Done updating historical CSV file, service shutting down.')
131
+ end
132
+
133
+ # There's possibly a new scan with new data.
134
+ def delta_site_report(ticket_repository, options, helper, file_site_histories, historical_scan_file = File.join(File.dirname(__FILE__),"#{options[:file_name]}"))
135
+ # Compares the Scan information from the File && Nexpose.
136
+ no_processing = true
137
+ @nexpose_site_histories.each do |site_id, scan_id|
138
+ # There's no entry in the file, so it's a new site in Nexpose.
139
+ if file_site_histories[site_id].nil? || file_site_histories[site_id] == -1
140
+ full_new_site_report(site_id, ticket_repository, options, helper)
141
+ no_processing = false
142
+ # Site has been scanned since last seen according to the file.
143
+ elsif file_site_histories[site_id].to_i < nexpose_site_histories[site_id]
144
+ delta_site_new_scan(ticket_repository, site_id, options, helper, file_site_histories)
145
+ no_processing = false
146
+ end
147
+ end
148
+ # Done processing, update the CSV to the latest scan info.
149
+ log_message("Nothing new to process, updating historical CSV file #{options[:file_name]}.") if no_processing
150
+ log_message("Done processing, updating historical CSV file #{options[:file_name]}.") unless no_processing
151
+ ticket_repository.save_last_scans(historical_scan_file)
152
+ log_message('Done updating historical CSV file, service shutting down.')
153
+ no_processing
154
+ end
155
+
156
+ # There's a new site we haven't seen before.
157
+ def full_new_site_report(site_id, ticket_repository, options, helper)
158
+ log_message("New site id: #{site_id} detected. Generating report.")
159
+ new_site_vuln = ticket_repository.all_vulns(sites: [site_id], severity: options[:severity])
160
+ log_message('Report generated, preparing tickets.')
161
+ ticket = helper.prepare_tickets(new_site_vuln)
162
+ helper.create_ticket(ticket)
163
+ end
164
+
165
+ # There's a new scan with possibly new vulnerabilities.
166
+ def delta_site_new_scan(ticket_repository, site_id, options, helper, file_site_histories)
167
+ log_message("New scan detected for site: #{site_id}. Generating report.")
168
+ new_scan_vuln = ticket_repository.delta_vulns_sites(scan_id: file_site_histories[site_id], site_id: site_id, severity: options[:severity])
169
+ # Preparse for an empty report: No new vulns between scans.
170
+ preparse = CSV.new(new_scan_vuln.chomp, headers: :first_row)
171
+ empty_report = preparse.shift.nil?
172
+ log_message("No new vulnerabilities found in new scan for site: #{site_id}.") && empty_report
173
+ log_message("New vulnerabilities found in new scan for site #{site_id}, preparing tickets.") unless empty_report
174
+ unless empty_report
175
+ ticket = helper.prepare_tickets(new_scan_vuln)
176
+ helper.create_ticket(ticket)
177
+ end
178
+ end
179
+
180
+ # Starts the Ticketing Service.
181
+ def start
182
+ # Checks if the csv historical file already exists && reads it, otherwise create it && assume first time run.
183
+ file_site_histories = prepare_historical_data(@ticket_repository, @options)
184
+ # If we didn't specify a site || first time run, then it gets all the vulnerabilities.
185
+ if @options[:sites].empty? || @first_time
186
+ all_site_report(@ticket_repository, @options, @helper)
187
+ else
188
+ log_message('Obtaining last scan information.')
189
+ @nexpose_site_histories = @ticket_repository.last_scans
190
+ # Only run if a scan has been ran ever in Nexpose.
191
+ unless @nexpose_site_histories.empty?
192
+ delta_site_report(@ticket_repository, @options, @helper, file_site_histories)
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,8 @@
1
+ require 'nexpose_ticketing/ticket_service'
2
+ module NexposeTicketing
3
+ def self.start(args)
4
+ ts = NexposeTicketing::TicketService.new
5
+ ts.setup(args)
6
+ ts.start
7
+ end
8
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'nexpose_ticketing'
5
+ s.version = '0.0.1'
6
+ s.homepage = 'https://github.com/rapid7/nexpose_ticketing'
7
+ s.summary = 'Ruby Nexpose Ticketing Engine.'
8
+ s.description = 'This gem provides a Ruby implementation of different integrations with ticketing services for Nexpose.'
9
+ s.license = 'BSD'
10
+ s.authors = ['Damian Finol']
11
+ s.email = ['damian_finol@rapid7.com']
12
+ s.files = Dir['[A-Z]*'] + Dir['lib/**/*']
13
+ s.require_paths = ['lib']
14
+ s.extra_rdoc_files = ['README.markdown']
15
+ s.required_ruby_version = '>= 1.9'
16
+ s.platform = 'ruby'
17
+ s.executables << 'nexpose_jira'
18
+ s.add_dependency('nexpose', '>= 0.6.0')
19
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nexpose_ticketing
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Damian Finol
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-02-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nexpose
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ! '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.6.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ! '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.6.0
27
+ description: This gem provides a Ruby implementation of different integrations with
28
+ ticketing services for Nexpose.
29
+ email:
30
+ - damian_finol@rapid7.com
31
+ executables:
32
+ - nexpose_jira
33
+ extensions: []
34
+ extra_rdoc_files:
35
+ - README.markdown
36
+ files:
37
+ - README.markdown
38
+ - bin/nexpose_jira
39
+ - lib/nexpose_ticketing.rb
40
+ - lib/nexpose_ticketing/config/jira.config
41
+ - lib/nexpose_ticketing/config/ticket_service.config
42
+ - lib/nexpose_ticketing/helpers/jira_helper.rb
43
+ - lib/nexpose_ticketing/queries.rb
44
+ - lib/nexpose_ticketing/ticket_repository.rb
45
+ - lib/nexpose_ticketing/ticket_service.rb
46
+ - nexpose_ticketing.gemspec
47
+ homepage: https://github.com/rapid7/nexpose_ticketing
48
+ licenses:
49
+ - BSD
50
+ metadata: {}
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ! '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '1.9'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ! '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubyforge_project:
67
+ rubygems_version: 2.2.2
68
+ signing_key:
69
+ specification_version: 4
70
+ summary: Ruby Nexpose Ticketing Engine.
71
+ test_files: []