nexpose_ticketing 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|