nexpose_paloalto 0.1.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 +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: []
|