nexpose_paloalto 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +27 -0
- data/LICENSE.txt +21 -0
- data/README.md +63 -0
- data/Rakefile +2 -0
- data/lib/bin/nexpose_paloalto.rb +37 -0
- data/lib/paloalto.rb +147 -0
- data/lib/paloalto/nexpose_helper.rb +104 -0
- data/lib/paloalto/ngfw.rb +268 -0
- data/lib/paloalto/nx_logger.rb +27 -0
- data/lib/paloalto/version.rb +3 -0
- data/nexpose_paloalto.gemspec +28 -0
- metadata +128 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 14c752bd0571d6cb5572c01e07eef1ee27178c72
|
4
|
+
data.tar.gz: a1f4eca110f7765547a98e6b571d3e0c1bdd80b8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6bebc678b2e03d9b01720fb84362f6f97927b5a44e464d3c49170304a68abca80ba114bbc09492145a6bcd99d38e2d23e9dc2ae6e3598925e641e0801b400763
|
7
|
+
data.tar.gz: ddb4b6a8a0e1456d46dd2b1b0f098892946f520cd6c428e0f39bbbb23fa88a76b2c382ad2f0eaa9947b1590f19129e99ed68d9ed2dcd8a7e8a261dcb4404db6c
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
nexpose_paloalto (0.1.0)
|
5
|
+
curb (~> 0.8.7)
|
6
|
+
nexpose (~> 0.9)
|
7
|
+
nokogiri (~> 1.6)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
curb (0.8.7)
|
13
|
+
mini_portile (0.6.1)
|
14
|
+
nexpose (0.9.8)
|
15
|
+
rex (= 2.0.7)
|
16
|
+
nokogiri (1.6.4.1)
|
17
|
+
mini_portile (~> 0.6.0)
|
18
|
+
rake (10.3.2)
|
19
|
+
rex (2.0.7)
|
20
|
+
|
21
|
+
PLATFORMS
|
22
|
+
ruby
|
23
|
+
|
24
|
+
DEPENDENCIES
|
25
|
+
bundler (~> 1.8)
|
26
|
+
nexpose_paloalto!
|
27
|
+
rake (~> 10.0)
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Rapid7
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# Paloalto
|
2
|
+
|
3
|
+
Nexpose -> Palo Alto integration Gem.
|
4
|
+
|
5
|
+
With this Gem an integration between Nexpose Dynamic Asset Groups and Palo Alto's PAN TAGs. By using this integration,
|
6
|
+
dynamic asset groups could be setup in Nexpose that correspond to groups in PAN with applicable policies.
|
7
|
+
|
8
|
+
For example:
|
9
|
+
* Create a DAG in Nexpose which affects a particular vulnerability (Heartbleed).
|
10
|
+
* Define a policy in PAN to block SSL (mitigates temporarily Heartbleed).
|
11
|
+
* Run this Gem with the Hearbleed DAG in the configuration.
|
12
|
+
* All assets identified by Nexpose will be registered in PAN.
|
13
|
+
* Apply PAN Heartbleed Policy on the created group.
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem 'paloalto'
|
21
|
+
```
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
|
25
|
+
$ bundle
|
26
|
+
|
27
|
+
Or install it yourself as:
|
28
|
+
|
29
|
+
$ gem install paloalto
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
Follow these steps once installed:
|
34
|
+
|
35
|
+
* Modify the file nexpose_paloalto.rb under the bin folder, and add the DAGs to report on.
|
36
|
+
|
37
|
+
* Add the following Environment variables, with your respective information:
|
38
|
+
|
39
|
+
NEXPOSE_URL
|
40
|
+
|
41
|
+
NEXPOSE_USERNAME
|
42
|
+
|
43
|
+
NEXPOSE_PASSWORD
|
44
|
+
|
45
|
+
|
46
|
+
PAN_URL
|
47
|
+
|
48
|
+
PAN_USERNAME
|
49
|
+
|
50
|
+
PAN_PASSWORD
|
51
|
+
|
52
|
+
|
53
|
+
For Linux systems, make sure they are added to the current environment where the gem is run.
|
54
|
+
|
55
|
+
For Windows systems, make sure they are on the Environment Variables section in your Control Panel.
|
56
|
+
|
57
|
+
* Run the command 'nexpose_paloalto' under the bin folder.
|
58
|
+
|
59
|
+
* (Optional) Review the log file under the logs folder in the Gem path.
|
60
|
+
|
61
|
+
|
62
|
+
For any support requests, please email integrations_support@rapid7.com with a description of the issue and any logs
|
63
|
+
available.
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'paloalto'
|
2
|
+
|
3
|
+
# Obtain Nexpose settings from Environment Variables.
|
4
|
+
nexpose_settings = Hash.new
|
5
|
+
raise 'Must configure nexpose settings before starting' if ENV['NEXPOSE_URL'].nil? || ENV['NEXPOSE_USERNAME'].nil? || ENV['NEXPOSE_PASSWORD'].nil?
|
6
|
+
nexpose_settings[:nexpose_url] = ENV['NEXPOSE_URL']
|
7
|
+
nexpose_settings[:nexpose_username] = ENV['NEXPOSE_USERNAME']
|
8
|
+
nexpose_settings[:nexpose_password] = ENV['NEXPOSE_PASSWORD']
|
9
|
+
|
10
|
+
# Obtain PAN's info.
|
11
|
+
pan_settings = Hash.new
|
12
|
+
raise 'Must configure Palo Alto settings before starting' if ENV['PAN_URL'].nil? || ENV['PAN_USERNAME'].nil? || ENV['PAN_PASSWORD'].nil?
|
13
|
+
pan_settings[:pan_url] = ENV['PAN_URL']
|
14
|
+
pan_settings[:pan_username] = ENV['PAN_USERNAME']
|
15
|
+
pan_settings[:pan_password] = ENV['PAN_PASSWORD']
|
16
|
+
|
17
|
+
# Nexpose options
|
18
|
+
# Dynamic Asset Groups to use, separated by commas:
|
19
|
+
# dag = [1, 2]
|
20
|
+
dag = []
|
21
|
+
|
22
|
+
# Sites to use. We recommend using DAGs, separated by commas:
|
23
|
+
# sites = [1, 2]
|
24
|
+
sites = []
|
25
|
+
|
26
|
+
# Report Time_out. Leave default value of 10800.
|
27
|
+
report_timeout = 10800
|
28
|
+
|
29
|
+
# Do not edit below this line.
|
30
|
+
# Check that everything is in place before we start it.
|
31
|
+
raise 'Must configure a site or a dag before starting' if dag.empty? && sites.empty?
|
32
|
+
nexpose_settings[:dag] = dag
|
33
|
+
nexpose_settings[:sites] = sites
|
34
|
+
nexpose_settings[:timeout] = report_timeout
|
35
|
+
|
36
|
+
# Start integration with all the parameters.
|
37
|
+
Paloalto.start_integration(nexpose_settings, pan_settings)
|
data/lib/paloalto.rb
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'paloalto/version'
|
2
|
+
require 'paloalto/nexpose_helper'
|
3
|
+
require 'paloalto/ngfw'
|
4
|
+
require 'paloalto/nx_logger'
|
5
|
+
|
6
|
+
module Paloalto
|
7
|
+
def self.start_integration(nexpose_settings, pan_settings)
|
8
|
+
# Asset query.
|
9
|
+
asset_query = "select asset_id, da.ip_address, string_agg(DISTINCT '<' || dt.tag_name, '>') || '>' as tags
|
10
|
+
from dim_site_asset
|
11
|
+
LEFT OUTER JOIN dim_asset da USING (asset_id)
|
12
|
+
LEFT OUTER JOIN dim_tag_asset dta using (asset_id)
|
13
|
+
LEFT OUTER JOIN dim_tag dt using (tag_id)
|
14
|
+
GROUP BY asset_id, da.ip_address"
|
15
|
+
|
16
|
+
nexpose_url = nexpose_settings[:nexpose_url]
|
17
|
+
nexpose_username = nexpose_settings[:nexpose_username]
|
18
|
+
nexpose_password = nexpose_settings[:nexpose_password]
|
19
|
+
|
20
|
+
pan_url = pan_settings[:pan_url]
|
21
|
+
pan_username = pan_settings[:pan_username]
|
22
|
+
pan_password = pan_settings[:pan_password]
|
23
|
+
|
24
|
+
report_timeout = nexpose_settings[:timeout]
|
25
|
+
|
26
|
+
#Setup logging
|
27
|
+
@log = Paloalto::NXLogger.new
|
28
|
+
|
29
|
+
#Nexpose sites and DAGs to import. Uses Site Id and DAG ID e.g. 'sites = [1,2,3,4]'. Leave as nil to run on all sites and DAGs the user has access to or
|
30
|
+
# set as an empty array e.g. 'dags=[]' to not run on any sites/dags.
|
31
|
+
sites = nexpose_settings[:sites]
|
32
|
+
dags = nexpose_settings[:dag]
|
33
|
+
|
34
|
+
@log.log_message("Running with user configured site IDs <#{sites}> and dynamic asset group IDs #{dags}.")
|
35
|
+
|
36
|
+
# Log in to nexpose.
|
37
|
+
nsc = Paloalto::NexposeHelper.login(nexpose_url, nexpose_username, nexpose_password)
|
38
|
+
|
39
|
+
#Gather the sites
|
40
|
+
all_sites = nsc.sites
|
41
|
+
all_sites.delete_if {|site| !(sites.include? site.id)} unless sites.nil?
|
42
|
+
all_sites_names = []
|
43
|
+
all_sites.each { |site| (all_sites_names ||= []) << site.name.to_s }
|
44
|
+
|
45
|
+
# Handle any Nexpose Dynamic Asset Groups
|
46
|
+
# These will have a single tag (The Nexpose group name) with a Dynamic group name the same.
|
47
|
+
nexpose_dag_query_results = Paloalto::NexposeHelper.generate_dag_asset_groups({timeout: report_timeout}, nsc)
|
48
|
+
all_nexpose_dag_details = Paloalto::NexposeHelper.parse_dag_details(nexpose_dag_query_results)
|
49
|
+
all_nexpose_dag_details.delete_if {|dag_details| !(dags.include? dag_details[0].to_i)} unless dags.nil?
|
50
|
+
|
51
|
+
@log.log_message("User has access to the following site IDs <#{all_sites.each {|site| site.id}}> and dynamic asset group IDs #{all_nexpose_dag_details.each {|dag| dag[0]}}.")
|
52
|
+
|
53
|
+
# Login to PAN.
|
54
|
+
pan_key = Paloalto::Ngfw.login(pan_url, pan_username, pan_password)
|
55
|
+
|
56
|
+
#Get the device config
|
57
|
+
device_config_xml = Paloalto::Ngfw.retrieve_device_config(pan_url, pan_key)
|
58
|
+
|
59
|
+
#Get the device names
|
60
|
+
device_name = Paloalto::Ngfw.parse_device_name(device_config_xml)
|
61
|
+
vsys_name = Paloalto::Ngfw.parse_vsys_name(device_config_xml)
|
62
|
+
|
63
|
+
@log.log_message("Found device configuration. Name <#{device_name}> and vsys <#{vsys_name}>.")
|
64
|
+
|
65
|
+
#Get this device's config
|
66
|
+
vsys_config = Paloalto::Ngfw.retrieve_device_config(pan_url, pan_key, device_name, vsys_name)
|
67
|
+
|
68
|
+
#Get the existing tags
|
69
|
+
existing_tags = Paloalto::Ngfw.parse_existing_tags(vsys_config)
|
70
|
+
|
71
|
+
#Gather the tags we want to create
|
72
|
+
wanted_tags = []
|
73
|
+
all_sites_names.each {|site_name| wanted_tags << site_name.gsub(/[()]/, "")}
|
74
|
+
wanted_tags << 'Nexpose'
|
75
|
+
all_nexpose_dag_details.each {|details| wanted_tags << details[1].gsub(/[()']/, "")}
|
76
|
+
|
77
|
+
#Find which new tags need to be created
|
78
|
+
tags_to_create = wanted_tags - existing_tags
|
79
|
+
|
80
|
+
@log.log_message("New tags to be created <#{tags_to_create}>.")
|
81
|
+
|
82
|
+
#Find the existing DAGs
|
83
|
+
existing_dags = Paloalto::Ngfw.parse_existing_dags(vsys_config)
|
84
|
+
|
85
|
+
#Gather the dags we want to create
|
86
|
+
wanted_dags = []
|
87
|
+
all_sites_names.each {|site_name| wanted_dags << site_name.gsub(/[()]/, "")}
|
88
|
+
wanted_dags << 'Nexpose'
|
89
|
+
all_nexpose_dag_details.each {|details| wanted_dags << details[1].gsub(/[()']/, "")}
|
90
|
+
|
91
|
+
#Find which new dags need to be created
|
92
|
+
dags_to_create = wanted_dags - existing_dags
|
93
|
+
|
94
|
+
@log.log_message("New DAGs to be created <#{dags_to_create}>.")
|
95
|
+
|
96
|
+
#Create the new tags
|
97
|
+
@log.log_message("Creating tags...")
|
98
|
+
tags_element=''
|
99
|
+
tags_to_create.each {|tag_to_create| tags_element << Paloalto::Ngfw.generate_tag_xml(tag_to_create, 'color3', "Nexpose tag for asset grouping: #{tag_to_create}")}
|
100
|
+
response = Paloalto::Ngfw.create_tags(pan_url, pan_key, device_name, vsys_name, tags_element) unless tags_element.empty?
|
101
|
+
|
102
|
+
#Create the new dags
|
103
|
+
@log.log_message("Creating DAGs...")
|
104
|
+
dags_element=''
|
105
|
+
dags_to_create.each {|dag_to_create| dags_element << Paloalto::Ngfw.generate_dag_xml(dag_to_create, "'Nexpose' AND '#{dag_to_create}'", dag_to_create)}
|
106
|
+
Paloalto::Ngfw.create_dags(pan_url, pan_key, device_name, vsys_name, dags_element) unless dags_element.empty?
|
107
|
+
|
108
|
+
@log.log_message('Committing the changes...')
|
109
|
+
|
110
|
+
#Commit the changes
|
111
|
+
response = Paloalto::Ngfw.commit(pan_url, pan_key)
|
112
|
+
|
113
|
+
@log.log_message("Commit response <#{response}>")
|
114
|
+
|
115
|
+
all_sites.each do |site|
|
116
|
+
#Fetch this site's details
|
117
|
+
@log.log_message("Getting asset details for site <#{site.id}>.")
|
118
|
+
report_output = Paloalto::NexposeHelper.generate_report({timeout: report_timeout, site: site.id.to_s, query: asset_query}, nsc)
|
119
|
+
asset_details = Paloalto::NexposeHelper.parse_asset_query_details(report_output, site.name)
|
120
|
+
|
121
|
+
@log.log_message("Unregistering assets for site <#{site.id}>.")
|
122
|
+
# Unregister devices. Note have to do this because any "add" updates will append instead of overwriting.
|
123
|
+
Paloalto::Ngfw.unregister_devices(pan_url, pan_key, asset_details, site.id)
|
124
|
+
|
125
|
+
@log.log_message("Registering asset details for site <#{site.id}>.")
|
126
|
+
# Register the new devices
|
127
|
+
Paloalto::Ngfw.register_devices(pan_url, pan_key, asset_details, site.id)
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
#Repeat for Nexpose DAGs
|
132
|
+
all_nexpose_dag_details.each do |dag_details|
|
133
|
+
#Fetch this DAG's details
|
134
|
+
@log.log_message("Getting DAG details details for DAG <#{dag_details[0]}>.")
|
135
|
+
report_output = Paloalto::NexposeHelper.generate_report({timeout: report_timeout, query: Paloalto::NexposeHelper.generate_dag_assets_query(dag_details[0])}, nsc)
|
136
|
+
dag_parsed_details = Paloalto::NexposeHelper.parse_dag_query_details(report_output, dag_details[1].gsub(/[()']/, ""))
|
137
|
+
|
138
|
+
@log.log_message("Registering asset details for DAG <#{dag_details[0]}>.")
|
139
|
+
#Add devices. Note we do not remove these devices first as it will clean wipe the site configuration
|
140
|
+
Paloalto::Ngfw.register_devices(pan_url, pan_key, dag_parsed_details, "dag_#{dag_details[0]}")
|
141
|
+
end
|
142
|
+
|
143
|
+
@log.log_message('Exiting..')
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Paloalto
|
2
|
+
module NexposeHelper
|
3
|
+
require 'nexpose'
|
4
|
+
require 'csv'
|
5
|
+
require 'paloalto/nx_logger'
|
6
|
+
|
7
|
+
# Logs in to Nexpose using the url, username and password.
|
8
|
+
def self.login(url=nil, username=nil, password=nil)
|
9
|
+
raise 'Nexpose connection must be set in environment variables.' if url.nil? || username.nil? || password.nil?
|
10
|
+
nsc = Nexpose::Connection.new(url, username, password)
|
11
|
+
nsc.login
|
12
|
+
nsc
|
13
|
+
end
|
14
|
+
|
15
|
+
# Generates the ReportConfig with the parameters necessary.
|
16
|
+
# Report Params is a hash with the following hashes set:
|
17
|
+
# 'site' = Site to report on.
|
18
|
+
# 'query' = SQL Query.
|
19
|
+
# 'timeout' = Timeout in ms.
|
20
|
+
# 'filename' = File name to save to disk.
|
21
|
+
def self.generate_report(report_params=nil, nsc=nil)
|
22
|
+
raise 'Report options must be set in the config file.' if report_params.nil? || nsc.nil?
|
23
|
+
report_config = Nexpose::AdhocReportConfig.new(nil, 'sql')
|
24
|
+
report_config.add_filter('site', report_params[:site]) unless report_params[:site].nil?
|
25
|
+
report_config.add_filter('version', '1.2.0')
|
26
|
+
report_config.add_filter('query', report_params[:query])
|
27
|
+
report_output = report_config.generate(nsc, report_params[:timeout])
|
28
|
+
return report_output
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.generate_dag_asset_groups(report_params=nil, nsc=nil)
|
32
|
+
report_params[:query] = 'select asset_group_id, name from dim_asset_group'
|
33
|
+
return generate_report(report_params, nsc)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Parses a generated asset query.
|
37
|
+
# Returns an array of arrays with an entry containing an assets IP and Nexpose TAGs
|
38
|
+
def self.parse_asset_query_details(report_output, site_name)
|
39
|
+
raise 'Need to pass asset query report output to parse!' if report_output.nil?
|
40
|
+
csv_output = CSV.parse(report_output.chomp, headers: :first_row)
|
41
|
+
asset_details =[]
|
42
|
+
csv_output.each do |row|
|
43
|
+
ip_details = []
|
44
|
+
ip_details << row[1].to_s
|
45
|
+
ip_details << "#{row[2].to_s}<Nexpose><#{site_name}>"
|
46
|
+
asset_details << ip_details
|
47
|
+
end
|
48
|
+
return asset_details
|
49
|
+
end
|
50
|
+
|
51
|
+
# Parses a generated dag query.
|
52
|
+
# Returns an array of arrays with an entry containg an assets IP and Nexpose DAG name.
|
53
|
+
def self.parse_dag_query_details(report_output, dag_name)
|
54
|
+
raise 'Need to pass dag query report output to parse!' if report_output.nil?
|
55
|
+
csv_output = CSV.parse(report_output.chomp, headers: :first_row)
|
56
|
+
asset_details =[]
|
57
|
+
csv_output.each do |row|
|
58
|
+
ip_details = []
|
59
|
+
ip_details << row[1].to_s
|
60
|
+
ip_details << "<Nexpose><#{dag_name}>"
|
61
|
+
asset_details << ip_details
|
62
|
+
end
|
63
|
+
return asset_details
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.parse_dag_names(report_output)
|
67
|
+
csv_output = CSV.parse(report_output.chomp, headers: :first_row)
|
68
|
+
nexpose_group_names = []
|
69
|
+
csv_output.each do |row|
|
70
|
+
nexpose_group_names << row[1].to_s
|
71
|
+
end
|
72
|
+
return nexpose_group_names
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.parse_dag_group_ids(report_output)
|
76
|
+
csv_output = CSV.parse(report_output.chomp, headers: :first_row)
|
77
|
+
nexpose_group_ids = []
|
78
|
+
csv_output.each do |row|
|
79
|
+
nexpose_group_ids << row[0].to_s
|
80
|
+
end
|
81
|
+
return nexpose_group_ids
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.parse_dag_details(report_output)
|
85
|
+
csv_output = CSV.parse(report_output.chomp, headers: :first_row)
|
86
|
+
nexpose_group_details = []
|
87
|
+
csv_output.each do |row|
|
88
|
+
row_details = []
|
89
|
+
row_details << row[0].to_s
|
90
|
+
row_details << row[1].to_s
|
91
|
+
nexpose_group_details << row_details
|
92
|
+
end
|
93
|
+
return nexpose_group_details
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.generate_dag_assets_query(nexpose_dag_id)
|
97
|
+
return "select asset_id, da.ip_address, dag.name
|
98
|
+
from dim_asset_group_asset daga
|
99
|
+
LEFT OUTER JOIN dim_asset da USING (asset_id)
|
100
|
+
LEFT OUTER JOIN dim_asset_group dag USING (asset_group_id)
|
101
|
+
where asset_group_id = #{nexpose_dag_id}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,268 @@
|
|
1
|
+
module Paloalto
|
2
|
+
module Ngfw
|
3
|
+
require 'curb'
|
4
|
+
require 'nokogiri'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
#Curls a request to a specified URL.
|
8
|
+
#Returns Nokogiri::XML::Document
|
9
|
+
def self.curlRequest(url)
|
10
|
+
raise 'Need a URL to curl a request' if url.nil?
|
11
|
+
|
12
|
+
log_message("Sending request to URL")
|
13
|
+
|
14
|
+
curlReq = Curl::Easy.new(url)
|
15
|
+
curlReq.multipart_form_post = true
|
16
|
+
curlReq.ssl_verify_peer = false
|
17
|
+
curlReq.ssl_verify_host = false
|
18
|
+
curlReq.perform
|
19
|
+
|
20
|
+
log_message("Request response <#{curlReq.body}>")
|
21
|
+
|
22
|
+
return Nokogiri::XML(curlReq.body)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.curlRequestWithFile(url, filename)
|
26
|
+
raise 'Need a URL and a file to curl a request' if url.nil? || filename.nil?
|
27
|
+
|
28
|
+
log_message("Sending request to URL <#{url}> with file <#{filename}>")
|
29
|
+
|
30
|
+
curlReq = Curl::Easy.new(url)
|
31
|
+
curlReq.multipart_form_post = true
|
32
|
+
curlReq.ssl_verify_peer = false
|
33
|
+
curlReq.ssl_verify_host = false
|
34
|
+
curlReq.http_post(Curl::PostField.file("fileupload",filename))
|
35
|
+
|
36
|
+
log_message("Request response <#{curlReq.body}>")
|
37
|
+
|
38
|
+
return Nokogiri::XML(curlReq.body)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Logs a message
|
42
|
+
def self.log_message(message)
|
43
|
+
require 'logger'
|
44
|
+
logger_file = File.join(File.dirname(__FILE__), './logs/rapid7_palo_alto.log')
|
45
|
+
directory = File.dirname(logger_file)
|
46
|
+
FileUtils.mkdir_p(directory) unless File.directory?(directory)
|
47
|
+
@log = Logger.new(logger_file, 'monthly')
|
48
|
+
@log.level = Logger::INFO
|
49
|
+
@log.info(message)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Performs login using URL, pan_username, pan_password.
|
53
|
+
# Returns Nokogiri::XML::Element
|
54
|
+
# TODO: Handle invalid login.
|
55
|
+
def self.login(url=nil, pan_username=nil, pan_password=nil)
|
56
|
+
raise 'URL, Username and Password must be set in environment variables.' if url.nil? || pan_username.nil? || pan_password.nil?
|
57
|
+
|
58
|
+
url="https://#{url}/api/?type=keygen&user=#{pan_username}&password=#{pan_password}"
|
59
|
+
response_xml = curlRequest(url)
|
60
|
+
return response_xml.at_xpath('//key').child
|
61
|
+
end
|
62
|
+
|
63
|
+
# Commits a candidate configuration.
|
64
|
+
# TODO: Handle invalid command/key. Validate before commit and check commit job status.
|
65
|
+
def self.commit(pan_address=nil, key=nil)
|
66
|
+
raise 'Need to login first.' if key.nil?
|
67
|
+
raise 'Need to define a PAN URL.' if pan_address.nil?
|
68
|
+
|
69
|
+
url="https://#{pan_address}/api/?type=commit&key=#{key}&cmd=<commit></commit>"
|
70
|
+
response_xml = curlRequest(url)
|
71
|
+
|
72
|
+
#Get job id
|
73
|
+
job_id=response_xml.at_xpath('//job').child
|
74
|
+
url = "https://#{pan_address}/api/?type=op&key=#{key}&cmd=<show><jobs><id>#{job_id}</id></jobs></show>"
|
75
|
+
6.times do
|
76
|
+
response = curlRequest(url)
|
77
|
+
complete = response.at_xpath('/response[@status="success"]/result/job/result')
|
78
|
+
if complete.nil?
|
79
|
+
log_message("Commit response <#{response}>")
|
80
|
+
raise 'Error trying to parse commit response! See log for details.'
|
81
|
+
end
|
82
|
+
complete = complete.child
|
83
|
+
log_message("Current commit status <#{complete}>")
|
84
|
+
(complete.to_s == 'OK') ? break : sleep(20)
|
85
|
+
end
|
86
|
+
return response_xml
|
87
|
+
end
|
88
|
+
|
89
|
+
# Queries the Firewall for it's configuration (device and vsys names etc)
|
90
|
+
# Returns Nokogiri::XML::Document
|
91
|
+
def self.retrieve_device_config(pan_address=nil, key=nil, device_name=nil,vsys_name=nil)
|
92
|
+
raise 'Need to login first.' if key.nil?
|
93
|
+
raise 'Need to define a PAN URL.' if pan_address.nil?
|
94
|
+
|
95
|
+
if (!device_name.nil?)
|
96
|
+
if(!vsys_name.nil?)
|
97
|
+
url="https://#{pan_address}/api/?type=config&action=show&key=#{key}&xpath=/config/devices/entry[@name='#{device_name}']/vsys/entry[@name='#{vsys_name}']"
|
98
|
+
else
|
99
|
+
url="https://#{pan_address}/api/?type=config&action=show&key=#{key}&xpath=/config/devices/entry[@name='#{device_name}']"
|
100
|
+
end
|
101
|
+
else
|
102
|
+
url="https://#{pan_address}/api/?type=config&action=show&key=#{key}&xpath=/config/devices"
|
103
|
+
end
|
104
|
+
response_xml = curlRequest(url)
|
105
|
+
return response_xml
|
106
|
+
end
|
107
|
+
|
108
|
+
# Parse the device config and return the device name.
|
109
|
+
# Returns device name
|
110
|
+
def self.parse_device_name(device_config_xml=nil)
|
111
|
+
raise 'Need to pass device config to parse device name!' if device_config_xml.nil?
|
112
|
+
return device_config_xml.at_xpath('/response/result/devices/entry').values.first
|
113
|
+
end
|
114
|
+
|
115
|
+
# Parse the device config and return the vsys name.
|
116
|
+
# Returns device vsys name
|
117
|
+
def self.parse_vsys_name(device_config_xml=nil)
|
118
|
+
raise 'Need to pass device config to parse vsys name!' if device_config_xml.nil?
|
119
|
+
return device_config_xml.at_xpath('/response/result/devices/entry/vsys/entry').values.first
|
120
|
+
end
|
121
|
+
|
122
|
+
# Parse the device config and return the existing tags.
|
123
|
+
# Returns an array containing existing tags
|
124
|
+
def self.parse_existing_tags(device_config_xml=nil)
|
125
|
+
raise 'Need to pass device config to parse existing tags!' if device_config_xml.nil?
|
126
|
+
existing_tags=[]
|
127
|
+
device_config_xml.xpath('//tag/entry').each {|tag| existing_tags << tag.values}
|
128
|
+
return existing_tags
|
129
|
+
end
|
130
|
+
|
131
|
+
# Parse the device config and return the existing DAGs.
|
132
|
+
# Returns an array containing existing DAGs
|
133
|
+
def self.parse_existing_dags(device_config_xml=nil)
|
134
|
+
raise 'Need to pass device config to parse existing DAGs!' if device_config_xml.nil?
|
135
|
+
existing_dags=[]
|
136
|
+
device_config_xml.xpath('//address-group/entry').each {|tag| existing_dags << tag.values if tag.to_s.include?("<dynamic")}
|
137
|
+
return existing_dags
|
138
|
+
end
|
139
|
+
|
140
|
+
# Generates PAN API formatted XML for adding a TAG.
|
141
|
+
# Returns PAN API formatted XML String ready for sending
|
142
|
+
def self.generate_tag_xml(tag_name, tag_colour, tag_comments)
|
143
|
+
return URI.escape("<entry name=\"#{tag_name}\"><color>#{tag_colour}</color><comments>#{tag_comments}</comments></entry>")
|
144
|
+
end
|
145
|
+
|
146
|
+
# Generates PAN API formatted XML for adding a DAG.
|
147
|
+
# Returns PAN API formatted XML String ready for sending
|
148
|
+
def self.generate_dag_xml(dag_name, dag_filter, dag_tag_member)
|
149
|
+
return URI.escape("<entry name=\"#{dag_name}\"><dynamic><filter>#{dag_filter}</filter></dynamic><tag><member>#{dag_tag_member}</member></tag></entry>")
|
150
|
+
end
|
151
|
+
|
152
|
+
# Posts a XML formatted file to PAN.
|
153
|
+
# Options is a hash with:
|
154
|
+
# 'pan_address' : PAN URL.
|
155
|
+
# 'filename' : File with XML to post.
|
156
|
+
# TODO: Handle error codes.
|
157
|
+
def self.post_dag_file(pan_address = nil, filename = nil, key=nil)
|
158
|
+
raise 'Options and key must be set' if pan_address.nil? || key.nil? || filename.nil?
|
159
|
+
|
160
|
+
c = Curl::Easy.new("https://#{pan_address}/api/?type=user-id&key=#{key}&action=set")
|
161
|
+
c.multipart_form_post = true
|
162
|
+
c.ssl_verify_peer = false
|
163
|
+
c.ssl_verify_host = false
|
164
|
+
c.http_post(Curl::PostField.file('thing[file]', filename))
|
165
|
+
end
|
166
|
+
|
167
|
+
def self.create_tags(pan_address=nil, key=nil, device_name, vsys_name, tags_element)
|
168
|
+
raise 'Options and key must be set' if pan_address.nil? || key.nil? || device_name.nil? || vsys_name.nil? || tags_element.nil?
|
169
|
+
url="https://#{pan_address}/api/?type=config&action=set&key=#{key}&xpath=/config/devices/entry[@name='#{device_name}']/vsys/entry[@name='#{vsys_name}']/tag&element=#{tags_element}"
|
170
|
+
return curlRequest(url)
|
171
|
+
end
|
172
|
+
|
173
|
+
def self.create_dags(pan_address=nil, key=nil, device_name=nil, vsys_name=nil, tags_element=nil)
|
174
|
+
raise 'Options and key must be set' if pan_address.nil? || key.nil? || device_name.nil? || vsys_name.nil? || tags_element.nil?
|
175
|
+
url="https://#{pan_address}/api/?type=config&action=set&key=#{key}&xpath=/config/devices/entry[@name='#{device_name}']/vsys/entry[@name='#{vsys_name}']/address-group&element=#{tags_element}"
|
176
|
+
return curlRequest(url)
|
177
|
+
end
|
178
|
+
|
179
|
+
def self.generate_add_asset_xml(ip_addresses_details = [])
|
180
|
+
raise 'Need asset details to generate add asset xml.' if ip_addresses_details.nil?
|
181
|
+
|
182
|
+
xml = ''
|
183
|
+
ip_addresses_details.each do |ip_address, tags_string|
|
184
|
+
xml << "<entry ip=\"#{ip_address}\"><tag>"
|
185
|
+
tags_string.split(/<(.*?)>/).each do |tag|
|
186
|
+
xml << "<member>#{tag}</member>" unless tag.empty?
|
187
|
+
end
|
188
|
+
xml << "</tag></entry>"
|
189
|
+
end
|
190
|
+
#return URI.escape(xml)
|
191
|
+
return xml
|
192
|
+
end
|
193
|
+
|
194
|
+
def self.generate_remove_asset_xml(ip_addresses_details = [])
|
195
|
+
raise 'Need asset details to generate remove asset xml.' if ip_addresses_details.nil?
|
196
|
+
|
197
|
+
xml = ''
|
198
|
+
ip_addresses_details.each do |ip_address|
|
199
|
+
xml << "<entry ip=\"#{ip_address}\"></entry>"
|
200
|
+
end
|
201
|
+
return xml
|
202
|
+
end
|
203
|
+
|
204
|
+
def self.unregister_devices(pan_address=nil, key=nil, asset_details=nil, site_id=nil)
|
205
|
+
raise 'Options and key must be set' if pan_address.nil? || key.nil? || asset_details.nil? || site_id.nil?
|
206
|
+
|
207
|
+
#Extract a list of IPs from the asset details
|
208
|
+
ip_list=[]
|
209
|
+
asset_details.each {|ip_addess, tags| ip_list << ip_addess}
|
210
|
+
|
211
|
+
#Createa folder to hold the files
|
212
|
+
folder = "./paloalto/logs"
|
213
|
+
FileUtils.mkdir_p(folder) unless File.directory?(folder)
|
214
|
+
|
215
|
+
remove_file_list=[]
|
216
|
+
xml="<uid-message><version>1.0</version><type>update</type><payload><unregister>#{generate_remove_asset_xml(ip_list)}</unregister></payload></uid-message>"
|
217
|
+
remove_file = File.open("#{folder}/#{site_id}_remove.xml", 'w')
|
218
|
+
remove_file.puts(xml)
|
219
|
+
remove_file_list << remove_file.path
|
220
|
+
remove_file.close
|
221
|
+
|
222
|
+
remove_file_list.each do |filename|
|
223
|
+
curlRequestWithFile("https://#{pan_address}/api/?type=user-id&action=set&key=#{key}&client=wget&file-name=\"#{filename}\"", filename)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def self.register_devices(pan_address=nil, key=nil, asset_details=nil, site_id=nil)
|
228
|
+
raise 'Options and key must be set' if pan_address.nil? || key.nil? || asset_details.nil? || site_id.nil?
|
229
|
+
|
230
|
+
#Createa folder to hold the files
|
231
|
+
folder = "./paloalto/logs"
|
232
|
+
FileUtils.mkdir_p(folder) unless File.directory?(folder)
|
233
|
+
|
234
|
+
add_file_list=[]
|
235
|
+
#Add devices
|
236
|
+
xml="<uid-message><version>1.0</version><type>update</type><payload><register>#{generate_add_asset_xml(asset_details)}</register></payload></uid-message>"
|
237
|
+
add_file =File.open("#{folder}/#{site_id}_add.xml", 'w')
|
238
|
+
add_file.puts(xml)
|
239
|
+
add_file_list << add_file.path
|
240
|
+
add_file.close
|
241
|
+
|
242
|
+
add_file_list.each do |filename|
|
243
|
+
curlRequestWithFile("https://#{pan_address}/api/?type=user-id&action=set&key=#{key}&client=wget&&file-name=\"#{filename}\"", filename)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def self.register_dag_devices(pan_address=nil, key=nil, dag_parsed_details=nil)
|
248
|
+
raise 'Options and key must be set' if pan_address.nil? || key.nil? || dag_parsed_details.nil?
|
249
|
+
|
250
|
+
#Createa folder to hold the files
|
251
|
+
folder = "./paloalto/logs"
|
252
|
+
FileUtils.mkdir_p(folder) unless File.directory?(folder)
|
253
|
+
|
254
|
+
add_file_list=[]
|
255
|
+
#Add devices
|
256
|
+
xml="<uid-message><version>1.0</version><type>update</type><payload><register>#{generate_add_asset_xml(asset_details)}</register></payload></uid-message>"
|
257
|
+
add_file =File.open("#{folder}/dag_#{dag_details[0]}_add.xml", 'w')
|
258
|
+
add_file.puts(xml)
|
259
|
+
add_file_list << add_file.path
|
260
|
+
add_file.close
|
261
|
+
|
262
|
+
add_file_list.each do |filename|
|
263
|
+
puts curlRequestWithFile("https://#{pan_address}/api/?type=user-id&action=set&key=#{key}&client=wget&&file-name=\"#{filename}\"", filename)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
end
|
268
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Paloalto
|
2
|
+
class NXLogger
|
3
|
+
LOGGER_FILE = File.join(File.dirname(__FILE__), './logs/rapid7_palo_alto.log')
|
4
|
+
|
5
|
+
attr_accessor :options
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
setup_logging()
|
9
|
+
end
|
10
|
+
|
11
|
+
def setup_logging(enabled = true)
|
12
|
+
if enabled
|
13
|
+
require 'logger'
|
14
|
+
directory = File.dirname(LOGGER_FILE)
|
15
|
+
FileUtils.mkdir_p(directory) unless File.directory?(directory)
|
16
|
+
@log = Logger.new(LOGGER_FILE, 'monthly')
|
17
|
+
@log.level = Logger::INFO
|
18
|
+
log_message('Logging enabled for helper.')
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Logs a message
|
23
|
+
def log_message(message)
|
24
|
+
@log.info(message)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'paloalto/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "nexpose_paloalto"
|
8
|
+
spec.version = Paloalto::VERSION
|
9
|
+
spec.authors = ['Damian Finol', 'JJ Cassidy']
|
10
|
+
spec.email = ['integrations_support@rapid7.com']
|
11
|
+
|
12
|
+
spec.summary = 'Nexpose Palo Alto Gem Integration'
|
13
|
+
spec.description = 'This Gem allows usage of Nexpose Dynamic Asset groups with Palo Alto TAGs.'
|
14
|
+
spec.homepage = "http://www.rapid7.com"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = Dir['[A-Z]*'] + Dir['lib/**/*'] + Dir['bin/**']
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.8"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_runtime_dependency 'nexpose', "~> 0.9"
|
25
|
+
spec.add_runtime_dependency 'curb', "~> 0.8.7"
|
26
|
+
spec.add_runtime_dependency 'nokogiri', "~> 1.6"
|
27
|
+
spec.required_ruby_version = '~> 2.0'
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nexpose_paloalto
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Damian Finol
|
8
|
+
- JJ Cassidy
|
9
|
+
autorequire:
|
10
|
+
bindir: exe
|
11
|
+
cert_chain: []
|
12
|
+
date: 2015-04-01 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '1.8'
|
21
|
+
type: :development
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '1.8'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rake
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '10.0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '10.0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: nexpose
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0.9'
|
49
|
+
type: :runtime
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0.9'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: curb
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 0.8.7
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 0.8.7
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: nokogiri
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - "~>"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '1.6'
|
77
|
+
type: :runtime
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '1.6'
|
84
|
+
description: This Gem allows usage of Nexpose Dynamic Asset groups with Palo Alto
|
85
|
+
TAGs.
|
86
|
+
email:
|
87
|
+
- integrations_support@rapid7.com
|
88
|
+
executables: []
|
89
|
+
extensions: []
|
90
|
+
extra_rdoc_files: []
|
91
|
+
files:
|
92
|
+
- Gemfile
|
93
|
+
- Gemfile.lock
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- lib/bin/nexpose_paloalto.rb
|
98
|
+
- lib/paloalto.rb
|
99
|
+
- lib/paloalto/nexpose_helper.rb
|
100
|
+
- lib/paloalto/ngfw.rb
|
101
|
+
- lib/paloalto/nx_logger.rb
|
102
|
+
- lib/paloalto/version.rb
|
103
|
+
- nexpose_paloalto.gemspec
|
104
|
+
homepage: http://www.rapid7.com
|
105
|
+
licenses:
|
106
|
+
- MIT
|
107
|
+
metadata: {}
|
108
|
+
post_install_message:
|
109
|
+
rdoc_options: []
|
110
|
+
require_paths:
|
111
|
+
- lib
|
112
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '2.0'
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
requirements: []
|
123
|
+
rubyforge_project:
|
124
|
+
rubygems_version: 2.4.4
|
125
|
+
signing_key:
|
126
|
+
specification_version: 4
|
127
|
+
summary: Nexpose Palo Alto Gem Integration
|
128
|
+
test_files: []
|