nexpose_ticketing 0.0.1

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 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: []