algosec-sdk 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +25 -0
- data/.rubocop.yml +68 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +7 -0
- data/LICENSE +20 -0
- data/README.md +208 -0
- data/Rakefile +21 -0
- data/algosec-sdk.gemspec +28 -0
- data/lib/algosec-sdk.rb +8 -0
- data/lib/algosec-sdk/client.rb +69 -0
- data/lib/algosec-sdk/exceptions.rb +37 -0
- data/lib/algosec-sdk/helpers/business_flow_helper.rb +347 -0
- data/lib/algosec-sdk/helpers/flow_comparisons.rb +48 -0
- data/lib/algosec-sdk/rest.rb +160 -0
- data/lib/algosec-sdk/version.rb +4 -0
- data/rakelib/end-to-end-integration.rake +72 -0
- metadata +175 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
# (c) Copyright 2018 AlgoSec Systems
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# You may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
6
|
+
#
|
7
|
+
# Unless required by applicable law or agreed to in writing, software distributed
|
8
|
+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
9
|
+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
|
10
|
+
# language governing permissions and limitations under the License.
|
11
|
+
|
12
|
+
# Contains all the custom Exception classes
|
13
|
+
module ALGOSEC_SDK
|
14
|
+
# Client configuration is invalid
|
15
|
+
class InvalidClient < StandardError
|
16
|
+
end
|
17
|
+
|
18
|
+
# Could not make request
|
19
|
+
class InvalidRequest < StandardError
|
20
|
+
end
|
21
|
+
|
22
|
+
# 400
|
23
|
+
class BadRequest < StandardError
|
24
|
+
end
|
25
|
+
|
26
|
+
# 401
|
27
|
+
class Unauthorized < StandardError
|
28
|
+
end
|
29
|
+
|
30
|
+
# 404
|
31
|
+
class NotFound < StandardError
|
32
|
+
end
|
33
|
+
|
34
|
+
# Other bad response codes
|
35
|
+
class RequestError < StandardError
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,347 @@
|
|
1
|
+
require_relative 'flow_comparisons'
|
2
|
+
require 'set'
|
3
|
+
require 'ipaddress'
|
4
|
+
|
5
|
+
module ALGOSEC_SDK
|
6
|
+
module NetworkObjectType
|
7
|
+
HOST = 'Host'.freeze
|
8
|
+
RANGE = 'Range'.freeze
|
9
|
+
GROUP = 'Group'.freeze
|
10
|
+
ABSTRACT = 'Abstract'.freeze
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module ALGOSEC_SDK
|
15
|
+
module NetworkObjectSearchType
|
16
|
+
INTERSECT = 'INTERSECT'.freeze
|
17
|
+
CONTAINED = 'CONTAINED'.freeze
|
18
|
+
CONTAINING = 'CONTAINING'.freeze
|
19
|
+
EXACT = 'EXACT'.freeze
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module ALGOSEC_SDK
|
24
|
+
# Contains helper methods for BusinessFlow
|
25
|
+
module BusinessFlowHelper
|
26
|
+
# Request login to get session cookie credentials
|
27
|
+
# @raise [RuntimeError] if the request failed
|
28
|
+
# @return [Array<Hash>] flows
|
29
|
+
def login
|
30
|
+
response_handler(rest_post('/BusinessFlow/rest/v1/login'))
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get list of application flows for an application revision id
|
34
|
+
# @param [String, Symbol] app_revision_id
|
35
|
+
# @raise [RuntimeError] if the request failed
|
36
|
+
# @return [Array<Hash>] flows
|
37
|
+
def get_application_flows(app_revision_id)
|
38
|
+
response = rest_get("/BusinessFlow/rest/v1/applications/#{app_revision_id}/flows")
|
39
|
+
flows = response_handler(response)
|
40
|
+
flows.map { |flow| flow['flowType'] == 'APPLICATION_FLOW' ? flow : nil }.compact
|
41
|
+
end
|
42
|
+
|
43
|
+
# Get application flows from the server as a hash from flow name it it's content
|
44
|
+
# @param [String, Symbol] app_revision_id
|
45
|
+
# @raise [RuntimeError] if the request failed
|
46
|
+
# @return [Hash] flows as a hash from name to flow
|
47
|
+
def get_application_flows_hash(app_revision_id)
|
48
|
+
Hash[get_application_flows(app_revision_id).map { |flow| [flow['name'], flow] }]
|
49
|
+
end
|
50
|
+
|
51
|
+
# Delete a specific flow
|
52
|
+
# @param [String] app_revision_id
|
53
|
+
# @param [String] flow_id
|
54
|
+
# @raise [RuntimeError] if the request failed
|
55
|
+
# @return true
|
56
|
+
def delete_flow_by_id(app_revision_id, flow_id)
|
57
|
+
response = rest_delete("/BusinessFlow/rest/v1/applications/#{app_revision_id}/flows/#{flow_id}")
|
58
|
+
response_handler(response)
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
62
|
+
# Get connectivity status for a flow
|
63
|
+
# @param [String] app_revision_id
|
64
|
+
# @param [String] flow_id
|
65
|
+
# @raise [RuntimeError] if the request failed
|
66
|
+
# @return [String] Connectivity Status dict that contain flowId, queryLink and status keys
|
67
|
+
def get_flow_connectivity(app_revision_id, flow_id)
|
68
|
+
response = rest_post("/BusinessFlow/rest/v1/applications/#{app_revision_id}/flows/#{flow_id}/check_connectivity")
|
69
|
+
response_handler(response)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Create a flow
|
73
|
+
# @param [String] app_revision_id The application revision id to create the flow in
|
74
|
+
# @param [Object] flow_name
|
75
|
+
# @param [Array<String>] sources
|
76
|
+
# @param [Array<String>] destinations
|
77
|
+
# @param [Array<String>] network_users
|
78
|
+
# @param [Array<String>] network_apps
|
79
|
+
# @param [Array<String>] network_services
|
80
|
+
# @param [String] comment
|
81
|
+
# @param [String] type
|
82
|
+
# @raise [RuntimeError] if the request failed
|
83
|
+
# @return Newly created application flow
|
84
|
+
# rubocop:disable Metrics/ParameterLists
|
85
|
+
def create_application_flow(
|
86
|
+
app_revision_id,
|
87
|
+
flow_name,
|
88
|
+
sources,
|
89
|
+
destinations,
|
90
|
+
network_services,
|
91
|
+
network_users,
|
92
|
+
network_apps,
|
93
|
+
comment,
|
94
|
+
type = 'APPLICATION',
|
95
|
+
custom_fields = []
|
96
|
+
)
|
97
|
+
# rubocop:enable Metrics/ParameterLists
|
98
|
+
|
99
|
+
# Create the missing network objects from the sources and destinations
|
100
|
+
create_missing_network_objects(sources + destinations)
|
101
|
+
create_missing_services(network_services)
|
102
|
+
|
103
|
+
get_named_objects = ->(name_list) { name_list.map { |name| { name: name } } }
|
104
|
+
|
105
|
+
new_flow = {
|
106
|
+
name: flow_name,
|
107
|
+
sources: get_named_objects.call(sources),
|
108
|
+
destinations: get_named_objects.call(destinations),
|
109
|
+
users: network_users,
|
110
|
+
network_applications: get_named_objects.call(network_apps),
|
111
|
+
services: get_named_objects.call(network_services),
|
112
|
+
comment: comment,
|
113
|
+
type: type,
|
114
|
+
custom_fields: custom_fields
|
115
|
+
}
|
116
|
+
response = rest_post("/BusinessFlow/rest/v1/applications/#{app_revision_id}/flows/new", body: [new_flow])
|
117
|
+
flows = response_handler(response)
|
118
|
+
# AlgoSec return a list of created flows, we created only one
|
119
|
+
flows[0]
|
120
|
+
end
|
121
|
+
|
122
|
+
# Fetch an application flow by it's name
|
123
|
+
# @param [String] app_revision_id The application revision id to fetch the flow from
|
124
|
+
# @param [Object] flow_name
|
125
|
+
# @raise [RuntimeError] if the request failed
|
126
|
+
# @return The requested flow
|
127
|
+
def get_application_flow_by_name(app_revision_id, flow_name)
|
128
|
+
flows = get_application_flows(app_revision_id)
|
129
|
+
requested_flow = flows.find do |flow|
|
130
|
+
break flow if flow['name'] == flow_name
|
131
|
+
end
|
132
|
+
|
133
|
+
if requested_flow.nil?
|
134
|
+
raise(
|
135
|
+
"Unable to find flow by name. Application revision id: #{app_revision_id}, flow_name: #{flow_name}."
|
136
|
+
)
|
137
|
+
end
|
138
|
+
requested_flow
|
139
|
+
end
|
140
|
+
|
141
|
+
# Get latest application revision id by application name
|
142
|
+
# @param [String, Symbol] app_name
|
143
|
+
# @raise [RuntimeError] if the request failed
|
144
|
+
# @return [Boolean] application revision id
|
145
|
+
def get_app_revision_id_by_name(app_name)
|
146
|
+
response = rest_get("/BusinessFlow/rest/v1/applications/name/#{app_name}")
|
147
|
+
app = response_handler(response)
|
148
|
+
app['revisionID']
|
149
|
+
end
|
150
|
+
|
151
|
+
# Apply application draft
|
152
|
+
# @param [String] app_revision_id
|
153
|
+
# @raise [RuntimeError] if the request failed
|
154
|
+
# @return true
|
155
|
+
def apply_application_draft(app_revision_id)
|
156
|
+
response = rest_post("/BusinessFlow/rest/v1/applications/#{app_revision_id}/apply")
|
157
|
+
response_handler(response)
|
158
|
+
true
|
159
|
+
end
|
160
|
+
|
161
|
+
# Create a new network service
|
162
|
+
# @param [String] service_name
|
163
|
+
# @param content List of lists in the form of (protocol, port)
|
164
|
+
# @raise [RuntimeError] if the request failed
|
165
|
+
# @return true if service created or already exists
|
166
|
+
def create_network_service(service_name, content)
|
167
|
+
content = content.map { |service| { protocol: service[0], port: service[1] } }
|
168
|
+
new_service = { name: service_name, content: content }
|
169
|
+
response = rest_post('/BusinessFlow/rest/v1/network_services/new', body: new_service)
|
170
|
+
response_handler(response)
|
171
|
+
true
|
172
|
+
end
|
173
|
+
|
174
|
+
# Create a new network object
|
175
|
+
# @param [NetworkObjectType] type type of the object to be created
|
176
|
+
# @param [String] content Define the newly created network object. Content depend upon the selected type
|
177
|
+
# @param [String] name Name of the new network object
|
178
|
+
# @raise [RuntimeError] if the request failed
|
179
|
+
# @return Newly created object
|
180
|
+
def create_network_object(type, content, name)
|
181
|
+
new_object = { type: type, name: name, content: content }
|
182
|
+
response = rest_post('/BusinessFlow/rest/v1/network_objects/new', body: new_object)
|
183
|
+
response_handler(response)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Search a network object
|
187
|
+
# @param [String] ip_or_subnet The ip or subnet to search the object with
|
188
|
+
# @param [NetworkObjectSearchType] search_type type of the object search method
|
189
|
+
# @raise [RuntimeError] if theh request failed
|
190
|
+
# @return List of objects from the search result
|
191
|
+
def search_network_object(ip_or_subnet, search_type)
|
192
|
+
response = rest_get(
|
193
|
+
'/BusinessFlow/rest/v1/network_objects/find',
|
194
|
+
query: { address: ip_or_subnet, type: search_type }
|
195
|
+
)
|
196
|
+
response_handler(response)
|
197
|
+
end
|
198
|
+
|
199
|
+
# Return a plan for modifying application flows based on current and newly proposed application flows definition
|
200
|
+
# @param [Array<Hash>] server_app_flows List of app flows currently defined on the server
|
201
|
+
# @param [Array<Hash>] new_app_flows List of network flows hash definitions
|
202
|
+
# @raise [RuntimeError] if the request failed
|
203
|
+
# @return 3 lists of flow names: flows_to_delete, flows_to_create, flows_to_modify
|
204
|
+
def plan_application_flows(server_app_flows, new_app_flows)
|
205
|
+
current_flow_names = Set.new(server_app_flows.keys)
|
206
|
+
new_flow_names = Set.new(new_app_flows.keys)
|
207
|
+
# Calculate the flows_to_delete, flows_to_create and flows_to_modify and unchanging_flows
|
208
|
+
flows_to_delete = current_flow_names - new_flow_names
|
209
|
+
flows_to_create = new_flow_names - current_flow_names
|
210
|
+
flows_to_modify = Set.new((new_flow_names & current_flow_names).map do |flow_name|
|
211
|
+
flow_on_server = server_app_flows[flow_name]
|
212
|
+
new_flow_definition = new_app_flows[flow_name]
|
213
|
+
ALGOSEC_SDK::AreFlowsEqual.flows_equal?(new_flow_definition, flow_on_server) ? nil : flow_name
|
214
|
+
end.compact)
|
215
|
+
|
216
|
+
[flows_to_delete, flows_to_create, flows_to_modify]
|
217
|
+
end
|
218
|
+
|
219
|
+
# Create/modify/delete application2 flows to match a given flow plan returned by 'plan_application_flows'
|
220
|
+
# @param [Integer] app_name The app to create the flows for
|
221
|
+
# @param [Array<Hash>] new_app_flows List of network flows hash definitions
|
222
|
+
# @param [Array<Hash>] flows_from_server List of network flows objects fetched from the server
|
223
|
+
# @param [Array<String>] flows_to_delete List of network flow names for deletion
|
224
|
+
# @param [Array<String>] flows_to_create List of network flow names to create
|
225
|
+
# param [Array<String>] flows_to_modify List of network flow names to delete and re-create with the new definition
|
226
|
+
# @raise [RuntimeError] if any of the requests failed
|
227
|
+
# @return True
|
228
|
+
def implement_app_flows_plan(
|
229
|
+
app_name,
|
230
|
+
new_app_flows,
|
231
|
+
flows_from_server,
|
232
|
+
flows_to_delete,
|
233
|
+
flows_to_create,
|
234
|
+
flows_to_modify
|
235
|
+
)
|
236
|
+
# Get the app revision id
|
237
|
+
app_revision_id = get_app_revision_id_by_name(app_name)
|
238
|
+
|
239
|
+
# This param is used to determine if it is necessary to update the app_revision_id
|
240
|
+
is_draft_revision = false
|
241
|
+
|
242
|
+
# Delete all the flows for deletion and modification
|
243
|
+
(flows_to_delete | flows_to_modify).each do |flow_name_to_delete|
|
244
|
+
delete_flow_by_id(app_revision_id, flows_from_server[flow_name_to_delete]['flowID'])
|
245
|
+
next if is_draft_revision
|
246
|
+
app_revision_id = get_app_revision_id_by_name(app_name)
|
247
|
+
# Refetch the fresh flows from the server, as a new application revision has been created
|
248
|
+
# and it's flow IDs have been change. Only that way we can make sure that the following flow deletions
|
249
|
+
# by name will work as expected
|
250
|
+
flows_from_server = get_application_flows(app_revision_id)
|
251
|
+
is_draft_revision = true
|
252
|
+
end
|
253
|
+
# Create all the new + modified flows
|
254
|
+
(flows_to_create | flows_to_modify).each do |flow_name_to_create|
|
255
|
+
new_flow_data = new_app_flows[flow_name_to_create]
|
256
|
+
create_application_flow(
|
257
|
+
app_revision_id,
|
258
|
+
flow_name_to_create,
|
259
|
+
# Document those key fields somewhere so users know how what is the format of app_flows object
|
260
|
+
# that is provided to this function
|
261
|
+
new_flow_data['sources'],
|
262
|
+
new_flow_data['destinations'],
|
263
|
+
new_flow_data['services'],
|
264
|
+
new_flow_data.fetch('users', []),
|
265
|
+
new_flow_data.fetch('applications', []),
|
266
|
+
new_flow_data.fetch('comment', '')
|
267
|
+
)
|
268
|
+
unless is_draft_revision
|
269
|
+
app_revision_id = get_app_revision_id_by_name(app_name)
|
270
|
+
is_draft_revision = true
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
apply_application_draft(app_revision_id) if is_draft_revision
|
275
|
+
end
|
276
|
+
|
277
|
+
# Update application flows of an application to match a requested flows configuration.
|
278
|
+
# @param [Integer] app_name The app to create the flows for
|
279
|
+
# @param [Object] new_app_flows Hash of new app flows, pointing from the flow name to the flow definition
|
280
|
+
# @raise [RuntimeError] if the request failed
|
281
|
+
# @return The updated list of flow objects from the server, including their new flowID
|
282
|
+
def define_application_flows(app_name, new_app_flows)
|
283
|
+
flows_from_server = get_application_flows_hash(get_app_revision_id_by_name(app_name))
|
284
|
+
flows_to_delete, flows_to_create, flows_to_modify = plan_application_flows(flows_from_server, new_app_flows)
|
285
|
+
implement_app_flows_plan(
|
286
|
+
app_name,
|
287
|
+
new_app_flows,
|
288
|
+
flows_from_server,
|
289
|
+
flows_to_delete,
|
290
|
+
flows_to_create,
|
291
|
+
flows_to_modify
|
292
|
+
)
|
293
|
+
|
294
|
+
# Stage 2: Run connectivity check for all the unchanged flows. Check with Chef is this non-deterministic approach
|
295
|
+
# is OK with them for the cookbook.
|
296
|
+
#
|
297
|
+
# Return the current list of created flows if successful
|
298
|
+
get_application_flows(get_app_revision_id_by_name(app_name))
|
299
|
+
end
|
300
|
+
|
301
|
+
# Create all the missing network objects which are simple IPv4 ip or subnet
|
302
|
+
# @param [Array<String>] network_object_names List of the network object names
|
303
|
+
# @raise [RuntimeError] if the request failed
|
304
|
+
# @return Newly created objects
|
305
|
+
def create_missing_network_objects(network_object_names)
|
306
|
+
# TODO: Add unitests that objects are being create only once (if the same object is twice in the incoming list)
|
307
|
+
network_object_names = Set.new(network_object_names)
|
308
|
+
ipv4_or_subnet_objects = network_object_names.map do |object_name|
|
309
|
+
begin
|
310
|
+
IPAddress.parse object_name
|
311
|
+
search_result = search_network_object(object_name, NetworkObjectSearchType::EXACT)
|
312
|
+
# If no object was found in search, we'll count this object for creation
|
313
|
+
search_result.empty? ? object_name : nil
|
314
|
+
rescue ArgumentError
|
315
|
+
# The parsed object name was not IP Address or IP Subnet, ignore it
|
316
|
+
nil
|
317
|
+
end
|
318
|
+
end.compact
|
319
|
+
|
320
|
+
# Create all the objects. If the error from the server tells us that the object already exists, ignore the error
|
321
|
+
ipv4_or_subnet_objects.map do |ipv4_or_subnet|
|
322
|
+
create_network_object(NetworkObjectType::HOST, ipv4_or_subnet, ipv4_or_subnet)
|
323
|
+
end.compact
|
324
|
+
end
|
325
|
+
|
326
|
+
# Create all the missing network services which are of simple protocol/port pattern
|
327
|
+
# @param [Array<String>] service_names List of the network service names
|
328
|
+
# @raise [RuntimeError] if the request failed
|
329
|
+
# @return Newly created objects
|
330
|
+
def create_missing_services(service_names)
|
331
|
+
parsed_services = service_names.map do |service_name|
|
332
|
+
protocol, port = service_name.scan(%r{(TCP|UDP)/(\d+)}i).last
|
333
|
+
[service_name, [protocol, port]] if !protocol.nil? && !port.nil?
|
334
|
+
end.compact
|
335
|
+
# Create all the objects. If the error from the server tells us that the object already exists, ignore the error
|
336
|
+
parsed_services.map do |parsed_service|
|
337
|
+
service_name, service_content = parsed_service
|
338
|
+
begin
|
339
|
+
create_network_service(service_name, [service_content])
|
340
|
+
rescue StandardError => e
|
341
|
+
# If the error is different from "service already exists", the exception will be re-raised
|
342
|
+
raise e if e.to_s.index('Service name already exists').nil?
|
343
|
+
end
|
344
|
+
end.compact
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module ALGOSEC_SDK
|
4
|
+
ANY_OBJECT = { 'id' => 0, 'name' => 'Any' }.freeze
|
5
|
+
ANY_NETWORK_APPLICATION = { 'revisionID' => 0, 'name' => 'Any' }.freeze
|
6
|
+
# A module to determine if a local flow definition is equal to a flow defined on the server
|
7
|
+
module AreFlowsEqual
|
8
|
+
def self.are_sources_equal_in_flow(source_object_names, server_flow_sources)
|
9
|
+
flow_source_object_names = Set.new(server_flow_sources.map { |source| source['name'] })
|
10
|
+
Set.new(source_object_names) == Set.new(flow_source_object_names)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.are_dest_equal_in_flow(dest_object_names, server_flow_dests)
|
14
|
+
flow_dest_object_names = Set.new(server_flow_dests.map { |dest| dest['name'] })
|
15
|
+
Set.new(dest_object_names) == Set.new(flow_dest_object_names)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.are_services_equal_in_flow(service_names, server_flow_services)
|
19
|
+
network_flow_service_names = Set.new(server_flow_services.map { |service| service['name'] })
|
20
|
+
Set.new(service_names) == Set.new(network_flow_service_names)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.are_apps_equal_in_flow(application_names, server_flow_apps)
|
24
|
+
return application_names == [] if server_flow_apps == [ANY_NETWORK_APPLICATION]
|
25
|
+
flow_application_names = server_flow_apps.map do |network_application|
|
26
|
+
network_application['name']
|
27
|
+
end
|
28
|
+
|
29
|
+
Set.new(application_names) == Set.new(flow_application_names)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.are_users_equal_in_flow(network_users, server_flow_users)
|
33
|
+
return network_users == [] if server_flow_users == [ANY_OBJECT]
|
34
|
+
flow_users = server_flow_users.map { |user| user['name'] }
|
35
|
+
Set.new(network_users) == Set.new(flow_users)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.flows_equal?(new_flow, flow_from_server)
|
39
|
+
[
|
40
|
+
are_sources_equal_in_flow(new_flow['sources'], flow_from_server['sources']),
|
41
|
+
are_dest_equal_in_flow(new_flow['destinations'], flow_from_server['destinations']),
|
42
|
+
are_services_equal_in_flow(new_flow['services'], flow_from_server['services']),
|
43
|
+
are_apps_equal_in_flow(new_flow.fetch('applications', []), flow_from_server.fetch('networkApplications', [])),
|
44
|
+
are_users_equal_in_flow(new_flow.fetch('users', []), flow_from_server.fetch('networkUsers', []))
|
45
|
+
].all?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/http'
|
3
|
+
require 'openssl'
|
4
|
+
require 'json'
|
5
|
+
require 'jsonclient'
|
6
|
+
|
7
|
+
module ALGOSEC_SDK
|
8
|
+
# Adds the ability for httpclient to set proper content-type for Hash AND Array body
|
9
|
+
class AdvancedJSONClient < JSONClient
|
10
|
+
def argument_to_hash_for_json(args)
|
11
|
+
hash = argument_to_hash(args, :body, :header, :follow_redirect)
|
12
|
+
if hash[:body].is_a?(Hash) || hash[:body].is_a?(Array)
|
13
|
+
hash[:header] = json_header(hash[:header])
|
14
|
+
hash[:body] = JSON.generate(hash[:body])
|
15
|
+
end
|
16
|
+
hash
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ALGOSEC_SDK
|
22
|
+
# Contains all the methods for making API REST calls
|
23
|
+
module Rest
|
24
|
+
def init_http_client
|
25
|
+
@http_client = ALGOSEC_SDK::AdvancedJSONClient.new(force_basic_auth: true)
|
26
|
+
@http_client.proxy = nil if @disable_proxy
|
27
|
+
@http_client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE unless @ssl_enabled
|
28
|
+
@http_client.set_auth(@host, @user, @password)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Make a restful API request to the AlgoSec
|
32
|
+
# @param [Symbol] type the rest method/type Options are :get, :post, :put, :patch, and :delete
|
33
|
+
# @param [String] path the path for the request. Usually starts with "/rest/"
|
34
|
+
# @param [Hash] options the options for the request
|
35
|
+
# @option options [String] :body Hash to be converted into json and set as the request body
|
36
|
+
# @option options [String] :Content-Type ('application/json') Set to nil or :none to have this option removed
|
37
|
+
# @raise [InvalidRequest] if the request is invalid
|
38
|
+
# @raise [SocketError] if a connection could not be made
|
39
|
+
# @raise [OpenSSL::SSL::SSLError] if SSL validation of the AlgoSec's certificate failed
|
40
|
+
# @return [NetHTTPResponse] The response object
|
41
|
+
def rest_api(type, path, options = {})
|
42
|
+
raise InvalidRequest, 'Must specify path' unless path
|
43
|
+
raise InvalidRequest, 'Must specify type' unless type
|
44
|
+
@logger.debug "Making :#{type} rest call to #{@host}#{path}"
|
45
|
+
|
46
|
+
uri = "#{@host}#{path}"
|
47
|
+
response = send_request(type, uri, options)
|
48
|
+
@logger.debug " Response: Code=#{response.status}. Headers=#{response.headers}\n Body=#{response.body}"
|
49
|
+
response
|
50
|
+
rescue OpenSSL::SSL::SSLError => e
|
51
|
+
msg = 'SSL verification failed for the request. Please either:'
|
52
|
+
msg += "\n 1. Install the necessary certificate(s) into your cert store"
|
53
|
+
msg += ". Using cert store: #{ENV['SSL_CERT_FILE']}" if ENV['SSL_CERT_FILE']
|
54
|
+
msg += "\n 2. Set the :ssl_enabled option to false for your AlgoSec client (not recommended)"
|
55
|
+
@logger.error msg
|
56
|
+
raise e
|
57
|
+
rescue SocketError => e
|
58
|
+
msg = "Failed to connect to AlgoSec host #{@host}!\n"
|
59
|
+
@logger.error msg
|
60
|
+
e.message.prepend(msg)
|
61
|
+
raise e
|
62
|
+
end
|
63
|
+
|
64
|
+
# Make a restful GET request
|
65
|
+
# Parameters & return value align with those of the {ALGOSEC_SDK::Rest::rest_api} method above
|
66
|
+
def rest_get(path, options = {})
|
67
|
+
rest_api(:get, path, options)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Make a restful POST request
|
71
|
+
# Parameters & return value align with those of the {ALGOSEC_SDK::Rest::rest_api} method above
|
72
|
+
def rest_post(path, options = {})
|
73
|
+
rest_api(:post, path, options)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Make a restful PUT request
|
77
|
+
# Parameters & return value align with those of the {ALGOSEC_SDK::Rest::rest_api} method above
|
78
|
+
def rest_put(path, options = {})
|
79
|
+
rest_api(:put, path, options)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Make a restful PATCH request
|
83
|
+
# Parameters & return value align with those of the {ALGOSEC_SDK::Rest::rest_api} method above
|
84
|
+
def rest_patch(path, options = {})
|
85
|
+
rest_api(:patch, path, options)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Make a restful DELETE request
|
89
|
+
# Parameters & return value align with those of the {ALGOSEC_SDK::Rest::rest_api} method above
|
90
|
+
def rest_delete(path, options = {})
|
91
|
+
rest_api(:delete, path, options)
|
92
|
+
end
|
93
|
+
|
94
|
+
RESPONSE_CODE_OK = 200
|
95
|
+
RESPONSE_CODE_CREATED = 201
|
96
|
+
RESPONSE_CODE_ACCEPTED = 202
|
97
|
+
RESPONSE_CODE_NO_CONTENT = 204
|
98
|
+
RESPONSE_CODE_BAD_REQUEST = 400
|
99
|
+
RESPONSE_CODE_UNAUTHORIZED = 401
|
100
|
+
RESPONSE_CODE_NOT_FOUND = 404
|
101
|
+
|
102
|
+
# Handle the response for rest call.
|
103
|
+
# If an asynchronous task was started, this waits for it to complete.
|
104
|
+
# @param [HTTPResponse] response
|
105
|
+
# @raise [ALGOSEC_SDK::BadRequest] if the request failed with a 400 status
|
106
|
+
# @raise [ALGOSEC_SDK::Unauthorized] if the request failed with a 401 status
|
107
|
+
# @raise [ALGOSEC_SDK::NotFound] if the request failed with a 404 status
|
108
|
+
# @raise [ALGOSEC_SDK::RequestError] if the request failed with any other status
|
109
|
+
# @return [Hash] The parsed JSON body
|
110
|
+
def response_handler(response)
|
111
|
+
case response.status
|
112
|
+
when RESPONSE_CODE_OK # Synchronous read/query
|
113
|
+
response.body
|
114
|
+
when RESPONSE_CODE_CREATED # Synchronous add
|
115
|
+
response.body
|
116
|
+
# when RESPONSE_CODE_ACCEPTED # Asynchronous add, update or delete
|
117
|
+
# return response.body #
|
118
|
+
# @logger.debug "Waiting for task: #{response.headers['location']}"
|
119
|
+
# task = wait_for(response.headers['location'])
|
120
|
+
# return true unless task['associatedResource'] && task['associatedResource']['resourceUri']
|
121
|
+
# resource_data = rest_get(task['associatedResource']['resourceUri'])
|
122
|
+
# return JSON.parse(resource_data.body)
|
123
|
+
when RESPONSE_CODE_NO_CONTENT # Synchronous delete
|
124
|
+
{}
|
125
|
+
when RESPONSE_CODE_BAD_REQUEST
|
126
|
+
raise BadRequest, "400 BAD REQUEST #{response.body}"
|
127
|
+
when RESPONSE_CODE_UNAUTHORIZED
|
128
|
+
raise Unauthorized, "401 UNAUTHORIZED #{response.body}"
|
129
|
+
when RESPONSE_CODE_NOT_FOUND
|
130
|
+
raise NotFound, "404 NOT FOUND #{response.body}"
|
131
|
+
else
|
132
|
+
raise RequestError, "#{response.status} #{response.body}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
# @param type [Symbol] The type of request object to build (get, post, put, patch, or delete)
|
139
|
+
# @param uri [String] full URI string
|
140
|
+
# @param options [Hash] Options for building the request. All options except "body" are set as headers.
|
141
|
+
# @raise [ALGOSEC_SDK::InvalidRequest] if the request type is not recognized
|
142
|
+
def send_request(type, uri, options)
|
143
|
+
case type.downcase
|
144
|
+
when 'get', :get
|
145
|
+
response = @http_client.get(uri, options)
|
146
|
+
when 'post', :post
|
147
|
+
response = @http_client.post(uri, options)
|
148
|
+
when 'put', :put
|
149
|
+
response = @http_client.put(uri, options)
|
150
|
+
when 'patch', :patch
|
151
|
+
response = @http_client.patch(uri, options)
|
152
|
+
when 'delete', :delete
|
153
|
+
response = @http_client.delete(uri, options)
|
154
|
+
else
|
155
|
+
raise InvalidRequest, "Invalid rest method: #{type}. Valid methods are: get, post, put, patch, delete"
|
156
|
+
end
|
157
|
+
response
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|