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 +15 -0
- data/README.markdown +38 -0
- data/bin/nexpose_jira +17 -0
- data/lib/nexpose_ticketing/config/jira.config +11 -0
- data/lib/nexpose_ticketing/config/ticket_service.config +27 -0
- data/lib/nexpose_ticketing/helpers/jira_helper.rb +104 -0
- data/lib/nexpose_ticketing/queries.rb +60 -0
- data/lib/nexpose_ticketing/ticket_repository.rb +103 -0
- data/lib/nexpose_ticketing/ticket_service.rb +197 -0
- data/lib/nexpose_ticketing.rb +8 -0
- data/nexpose_ticketing.gemspec +19 -0
- metadata +71 -0
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,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: []
|