insightconnect-plugin-runtime 5.4.9__py3-none-any.whl → 5.5.1__py3-none-any.whl
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.
- insightconnect_plugin_runtime/api/endpoints.py +33 -9
- insightconnect_plugin_runtime/clients/aws_client.py +41 -36
- insightconnect_plugin_runtime/exceptions.py +64 -8
- insightconnect_plugin_runtime/helper.py +260 -2
- insightconnect_plugin_runtime/plugin.py +25 -5
- insightconnect_plugin_runtime/server.py +37 -11
- insightconnect_plugin_runtime/util.py +3 -1
- {insightconnect_plugin_runtime-5.4.9.dist-info → insightconnect_plugin_runtime-5.5.1.dist-info}/METADATA +4 -2
- {insightconnect_plugin_runtime-5.4.9.dist-info → insightconnect_plugin_runtime-5.5.1.dist-info}/RECORD +13 -13
- tests/unit/test_helpers.py +215 -0
- tests/unit/utils.py +59 -1
- {insightconnect_plugin_runtime-5.4.9.dist-info → insightconnect_plugin_runtime-5.5.1.dist-info}/WHEEL +0 -0
- {insightconnect_plugin_runtime-5.4.9.dist-info → insightconnect_plugin_runtime-5.5.1.dist-info}/top_level.txt +0 -0
|
@@ -83,7 +83,17 @@ def handle_errors(error: HTTPException):
|
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
class Endpoints:
|
|
86
|
-
def __init__(
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
logger,
|
|
89
|
+
plugin,
|
|
90
|
+
spec,
|
|
91
|
+
debug,
|
|
92
|
+
workers,
|
|
93
|
+
threads,
|
|
94
|
+
master_pid,
|
|
95
|
+
config_options=None,
|
|
96
|
+
):
|
|
87
97
|
self.plugin = plugin
|
|
88
98
|
self.logger = structlog.get_logger("plugin")
|
|
89
99
|
self.spec = spec
|
|
@@ -167,7 +177,9 @@ class Endpoints:
|
|
|
167
177
|
Endpoints.validate_action_trigger_task_name(input_message, name, "task")
|
|
168
178
|
# No validation on the plugin custom config to leave this as configurable as possible.
|
|
169
179
|
# `add_plugin_custom_config` will pass any available values to the plugin for interpretation.
|
|
170
|
-
input_message = self.add_plugin_custom_config(
|
|
180
|
+
input_message = self.add_plugin_custom_config(
|
|
181
|
+
input_message, request.headers.get(ORG_ID)
|
|
182
|
+
)
|
|
171
183
|
output = self.run_action_trigger_task(input_message)
|
|
172
184
|
self.logger.info("Plugin task finished execution...")
|
|
173
185
|
return output
|
|
@@ -750,7 +762,9 @@ class Endpoints:
|
|
|
750
762
|
|
|
751
763
|
return version
|
|
752
764
|
|
|
753
|
-
def add_plugin_custom_config(
|
|
765
|
+
def add_plugin_custom_config(
|
|
766
|
+
self, input_data: Dict[str, Any], org_id: str
|
|
767
|
+
) -> Dict[str, Any]:
|
|
754
768
|
"""
|
|
755
769
|
Using the retrieved configs pulled from komand-props, pass the configuration that matches the requesting
|
|
756
770
|
Org ID which is passed to the task via a header (`X-IPIMS-ORGID`) from the plugin sidecar.
|
|
@@ -763,17 +777,27 @@ class Endpoints:
|
|
|
763
777
|
- all orgs for default runs will poll back 12 hours.
|
|
764
778
|
- all orgs in lookback mode will be 100 hours (task triggered with no state).
|
|
765
779
|
"""
|
|
766
|
-
additional_config = self.config_options.get(org_id) or self.config_options.get(
|
|
780
|
+
additional_config = self.config_options.get(org_id) or self.config_options.get(
|
|
781
|
+
"*"
|
|
782
|
+
)
|
|
767
783
|
if additional_config:
|
|
768
|
-
self.logger.info(
|
|
769
|
-
|
|
784
|
+
self.logger.info(
|
|
785
|
+
"Found config options; adding this to the request parameters..."
|
|
786
|
+
)
|
|
787
|
+
additional_config = (
|
|
788
|
+
additional_config.copy()
|
|
789
|
+
) # copy to preserve the referenced value in self.config_options
|
|
770
790
|
# As a safeguard we only pass the lookback config params if the plugin has no state
|
|
771
791
|
# This means we still need to manually delete the state for plugins on a per org basis.
|
|
772
792
|
# This also means first time customers for their 'initial' lookup would get the lookback value passed in.
|
|
773
|
-
if input_data.get("body", {}).get("state") and additional_config.get(
|
|
774
|
-
|
|
793
|
+
if input_data.get("body", {}).get("state") and additional_config.get(
|
|
794
|
+
"lookback"
|
|
795
|
+
):
|
|
796
|
+
self.logger.info(
|
|
797
|
+
"Found an existing plugin state, not passing lookback value..."
|
|
798
|
+
)
|
|
775
799
|
del additional_config["lookback"]
|
|
776
|
-
input_data.get("body", {}).update({
|
|
800
|
+
input_data.get("body", {}).update({"custom_config": additional_config})
|
|
777
801
|
self.logger.info(f"Custom config being sent to plugin: {additional_config}")
|
|
778
802
|
|
|
779
803
|
return input_data
|
|
@@ -29,13 +29,13 @@ class PaginationHelper:
|
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
31
|
def __init__(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
self,
|
|
33
|
+
input_token: List[str],
|
|
34
|
+
output_token: List[str],
|
|
35
|
+
result_key: List[str],
|
|
36
|
+
limit_key: str = None,
|
|
37
|
+
more_results: str = None,
|
|
38
|
+
non_aggregate_keys: List[str] = None,
|
|
39
39
|
):
|
|
40
40
|
self.input_token = input_token
|
|
41
41
|
self.output_token = output_token
|
|
@@ -70,9 +70,9 @@ class PaginationHelper:
|
|
|
70
70
|
is_paginated = False
|
|
71
71
|
|
|
72
72
|
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
self.more_results
|
|
74
|
+
and self.more_results in output.keys()
|
|
75
|
+
and output[self.more_results]
|
|
76
76
|
):
|
|
77
77
|
is_paginated = True
|
|
78
78
|
|
|
@@ -84,10 +84,10 @@ class PaginationHelper:
|
|
|
84
84
|
return is_paginated
|
|
85
85
|
|
|
86
86
|
def merge_responses(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
self,
|
|
88
|
+
input_: Dict[str, Any],
|
|
89
|
+
response_1: Dict[str, Any],
|
|
90
|
+
response_2: Dict[str, Any],
|
|
91
91
|
) -> Tuple[Dict[str, Any], bool]:
|
|
92
92
|
"""
|
|
93
93
|
Merges two output dictionaries together.
|
|
@@ -112,7 +112,8 @@ class PaginationHelper:
|
|
|
112
112
|
if len(response_1[response]) >= input_[self.limit_key]:
|
|
113
113
|
max_hit = True
|
|
114
114
|
response_1[response] = response_1[response][
|
|
115
|
-
|
|
115
|
+
: input_[self.limit_key]
|
|
116
|
+
]
|
|
116
117
|
|
|
117
118
|
return response_1, max_hit
|
|
118
119
|
|
|
@@ -302,7 +303,7 @@ class ActionHelper:
|
|
|
302
303
|
|
|
303
304
|
@classmethod
|
|
304
305
|
def format_output(
|
|
305
|
-
|
|
306
|
+
cls, output_schema: Union[Dict[str, Any], None], output: Dict[str, Any]
|
|
306
307
|
) -> Any:
|
|
307
308
|
"""
|
|
308
309
|
Formats a botocore response into a correct Komand response.
|
|
@@ -333,14 +334,14 @@ class AWSAction(Action):
|
|
|
333
334
|
"""
|
|
334
335
|
|
|
335
336
|
def __init__(
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
337
|
+
self,
|
|
338
|
+
name: str,
|
|
339
|
+
description: str,
|
|
340
|
+
input_: insightconnect_plugin_runtime.Input,
|
|
341
|
+
output: insightconnect_plugin_runtime.Output,
|
|
342
|
+
aws_service: str,
|
|
343
|
+
aws_command: str,
|
|
344
|
+
pagination_helper: PaginationHelper = None,
|
|
344
345
|
):
|
|
345
346
|
"""
|
|
346
347
|
|
|
@@ -362,7 +363,7 @@ class AWSAction(Action):
|
|
|
362
363
|
self.pagination_helper = pagination_helper
|
|
363
364
|
|
|
364
365
|
def _handle_botocore_function(
|
|
365
|
-
|
|
366
|
+
self, client_function: Callable, params: Dict
|
|
366
367
|
) -> Dict:
|
|
367
368
|
try:
|
|
368
369
|
response = client_function(**params)
|
|
@@ -480,11 +481,11 @@ class AWSAction(Action):
|
|
|
480
481
|
client_function = getattr(client, self.aws_command)
|
|
481
482
|
except AttributeError:
|
|
482
483
|
error_message = (
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
484
|
+
'Unable to find the command "'
|
|
485
|
+
+ self.aws_service
|
|
486
|
+
+ " "
|
|
487
|
+
+ self.aws_command
|
|
488
|
+
+ '"'
|
|
488
489
|
)
|
|
489
490
|
self.logger.error(error_message)
|
|
490
491
|
raise PluginException(cause=error_message)
|
|
@@ -533,17 +534,21 @@ class AWSAction(Action):
|
|
|
533
534
|
|
|
534
535
|
@staticmethod
|
|
535
536
|
def try_to_assume_role(
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
537
|
+
service_name: str,
|
|
538
|
+
assume_role_params: Dict[str, str],
|
|
539
|
+
auth_params: Dict[str, str],
|
|
539
540
|
):
|
|
540
541
|
session_name = str(uuid.uuid1())
|
|
541
542
|
sts_client = boto3.client("sts", **auth_params)
|
|
542
543
|
try:
|
|
543
544
|
assumed_role_object = sts_client.assume_role(
|
|
544
|
-
**clean(
|
|
545
|
-
|
|
546
|
-
|
|
545
|
+
**clean(
|
|
546
|
+
{
|
|
547
|
+
"RoleArn": assume_role_params.get(ROLE_ARN),
|
|
548
|
+
"RoleSessionName": session_name,
|
|
549
|
+
"ExternalId": assume_role_params.get(EXTERNAL_ID),
|
|
550
|
+
}
|
|
551
|
+
)
|
|
547
552
|
)
|
|
548
553
|
except ClientError as error:
|
|
549
554
|
raise PluginException(
|
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
class ResponseExceptionData:
|
|
3
|
+
RESPONSE_TEXT = "response_text"
|
|
4
|
+
RESPONSE_JSON = "response_json"
|
|
5
|
+
RESPONSE = "response"
|
|
6
|
+
EXCEPTION = "exception"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HTTPStatusCodes:
|
|
10
|
+
|
|
11
|
+
# 4xx Client Errors
|
|
12
|
+
BAD_REQUEST = 400
|
|
13
|
+
UNAUTHORIZED = 401
|
|
14
|
+
PAYMENT_REQUIRED = 402
|
|
15
|
+
FORBIDDEN = 403
|
|
16
|
+
NOT_FOUND = 404
|
|
17
|
+
METHOD_NOT_ALLOWED = 405
|
|
18
|
+
NOT_ACCEPTABLE = 406
|
|
19
|
+
PROXY_AUTHENTICATION_REQUIRED = 407
|
|
20
|
+
REQUEST_TIMEOUT = 408
|
|
21
|
+
CONFLICT = 409
|
|
22
|
+
GONE = 410
|
|
23
|
+
LENGTH_REQUIRED = 411
|
|
24
|
+
PRECONDITION_FAILED = 412
|
|
25
|
+
PAYLOAD_TOO_LARGE = 413
|
|
26
|
+
URI_TOO_LONG = 414
|
|
27
|
+
UNSUPPORTED_MEDIA_TYPE = 415
|
|
28
|
+
RANGE_NOT_SATISFIABLE = 416
|
|
29
|
+
EXPECTATION_FAILED = 417
|
|
30
|
+
I_AM_A_TEAPOT = 418
|
|
31
|
+
MISDIRECTED_REQUEST = 421
|
|
32
|
+
UNPROCESSABLE_ENTITY = 422
|
|
33
|
+
LOCKED = 423
|
|
34
|
+
FAILED_DEPENDENCY = 424
|
|
35
|
+
TOO_EARLY = 425
|
|
36
|
+
UPGRADE_REQUIRED = 426
|
|
37
|
+
PRECONDITION_REQUIRED = 428
|
|
38
|
+
TOO_MANY_REQUESTS = 429
|
|
39
|
+
REQUEST_HEADER_FIELDS_TOO_LARGE = 431
|
|
40
|
+
UNAVAILABLE_FOR_LEGAL_REASONS = 451
|
|
41
|
+
|
|
42
|
+
# 5xx Server Errors
|
|
43
|
+
INTERNAL_SERVER_ERROR = 500
|
|
44
|
+
NOT_IMPLEMENTED = 501
|
|
45
|
+
BAD_GATEWAY = 502
|
|
46
|
+
SERVICE_UNAVAILABLE = 503
|
|
47
|
+
GATEWAY_TIMEOUT = 504
|
|
48
|
+
HTTP_VERSION_NOT_SUPPORTED = 505
|
|
49
|
+
VARIANT_ALSO_NEGOTIATES = 506
|
|
50
|
+
INSUFFICIENT_STORAGE = 507
|
|
51
|
+
LOOP_DETECTED = 508
|
|
52
|
+
NOT_EXTENDED = 510
|
|
53
|
+
NETWORK_AUTHENTICATION_REQUIRED = 511
|
|
4
54
|
|
|
5
55
|
|
|
6
56
|
class ClientException(Exception):
|
|
@@ -65,6 +115,10 @@ class ConnectionTestException(Exception):
|
|
|
65
115
|
TIMEOUT = "timeout"
|
|
66
116
|
BAD_REQUEST = "bad_request"
|
|
67
117
|
INVALID_CREDENTIALS = "invalid_credentials"
|
|
118
|
+
METHOD_NOT_ALLOWED = "method_not_allowed"
|
|
119
|
+
CONFLICT = "conflict"
|
|
120
|
+
CONNECTION_ERROR = "connection_error"
|
|
121
|
+
REDIRECT_ERROR = "redirect_error"
|
|
68
122
|
|
|
69
123
|
# Dictionary of cause messages
|
|
70
124
|
causes = {
|
|
@@ -82,6 +136,10 @@ class ConnectionTestException(Exception):
|
|
|
82
136
|
Preset.TIMEOUT: "The connection timed out.",
|
|
83
137
|
Preset.BAD_REQUEST: "The server is unable to process the request.",
|
|
84
138
|
Preset.INVALID_CREDENTIALS: "Authentication failed: invalid credentials.",
|
|
139
|
+
Preset.METHOD_NOT_ALLOWED: "The request method is not allowed for this resource.",
|
|
140
|
+
Preset.CONFLICT: "Request cannot be completed due to a conflict with the current state of the resource.",
|
|
141
|
+
Preset.CONNECTION_ERROR: "Failed to connect to the server.",
|
|
142
|
+
Preset.REDIRECT_ERROR: "Request redirected more than the set limit for the server.",
|
|
85
143
|
}
|
|
86
144
|
|
|
87
145
|
# Dictionary of assistance/remediation messages
|
|
@@ -103,6 +161,10 @@ class ConnectionTestException(Exception):
|
|
|
103
161
|
Preset.BAD_REQUEST: "Verify your plugin input is correct and not malformed and try again. "
|
|
104
162
|
"If the issue persists, please contact support.",
|
|
105
163
|
Preset.INVALID_CREDENTIALS: "Please verify the credentials for your account and try again.",
|
|
164
|
+
Preset.METHOD_NOT_ALLOWED: "Please try a supported method for this resource.",
|
|
165
|
+
Preset.CONFLICT: "Please check your request, and try again.",
|
|
166
|
+
Preset.CONNECTION_ERROR: "Please check your network connection and try again.",
|
|
167
|
+
Preset.REDIRECT_ERROR: "Please check your request and try again.",
|
|
106
168
|
}
|
|
107
169
|
|
|
108
170
|
def __init__(self, cause=None, assistance=None, data=None, preset=None):
|
|
@@ -124,12 +186,6 @@ class ConnectionTestException(Exception):
|
|
|
124
186
|
|
|
125
187
|
self.data = str(data) if data else ""
|
|
126
188
|
|
|
127
|
-
# Safeguard to ensure the exception is logged across all plugins even if the plugin
|
|
128
|
-
# itself does not call `self.logger.error(<error info>)`
|
|
129
|
-
params = ["cause", "assistance", "data", "preset"]
|
|
130
|
-
info_log = ", ".join([f"{atr}='{getattr(self, atr)}'" for atr in params if getattr(self, atr)])
|
|
131
|
-
logger.error(f"Plugin exception instantiated. {info_log}")
|
|
132
|
-
|
|
133
189
|
def __str__(self):
|
|
134
190
|
if self.data:
|
|
135
191
|
return "Connection test failed!\n\n{cause} {assistance} Response was: {data}".format(
|
|
@@ -10,20 +10,278 @@ import subprocess
|
|
|
10
10
|
import time
|
|
11
11
|
from datetime import datetime, timedelta
|
|
12
12
|
from io import IOBase
|
|
13
|
-
from typing import Any, Callable, Dict, List, Union
|
|
13
|
+
from typing import Any, Callable, Dict, List, Union, Tuple
|
|
14
14
|
from urllib import request
|
|
15
|
+
from hashlib import sha1
|
|
16
|
+
from json import JSONDecodeError
|
|
15
17
|
|
|
16
18
|
import requests
|
|
17
19
|
|
|
18
|
-
from insightconnect_plugin_runtime.exceptions import
|
|
20
|
+
from insightconnect_plugin_runtime.exceptions import (
|
|
21
|
+
PluginException,
|
|
22
|
+
HTTPStatusCodes,
|
|
23
|
+
ResponseExceptionData,
|
|
24
|
+
)
|
|
19
25
|
|
|
20
26
|
CAMEL_CASE_REGEX = r"\b[a-z0-9]+([A-Z][a-z]+[0-9]*)*\b"
|
|
21
27
|
CAMEL_CASE_ACRONYM_REGEX = r"\b[a-z0-9]+([A-Z]+[0-9]*)*\b"
|
|
22
28
|
PASCAL_CASE_REGEX = r"\b[A-Z][a-z]+[0-9]*([A-Z][a-z]+[0-9]*)*\b"
|
|
29
|
+
ENCODE_TYPE = "utf-8"
|
|
23
30
|
|
|
24
31
|
DEFAULTS_HOURS_AGO = 24
|
|
25
32
|
|
|
26
33
|
|
|
34
|
+
def hash_sha1(log: dict) -> str:
|
|
35
|
+
"""
|
|
36
|
+
Iterate through a dictionary and hash each value.
|
|
37
|
+
:param log: Dictionary to be hashed.
|
|
38
|
+
:type dict:
|
|
39
|
+
:return: Hex digest of hash.
|
|
40
|
+
:rtype: str
|
|
41
|
+
"""
|
|
42
|
+
hash_ = sha1() # nosec B303
|
|
43
|
+
for key, value in log.items():
|
|
44
|
+
hash_.update(f"{key}{value}".encode(ENCODE_TYPE))
|
|
45
|
+
return hash_.hexdigest()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def compare_and_dedupe_hashes(
|
|
49
|
+
previous_logs_hashes: list, new_logs: list
|
|
50
|
+
) -> Tuple[list, list]:
|
|
51
|
+
"""
|
|
52
|
+
Iterate through two lists of values, hashing each. Compare hash value to a list of existing hash values.
|
|
53
|
+
If the hash exists, return both it and the value in separate lists once iterated.
|
|
54
|
+
:param previous_logs_hashes: List of existing hashes to compare against.
|
|
55
|
+
:type list:
|
|
56
|
+
:param new_logs: New values to hash and compare to existing list of hashes.
|
|
57
|
+
:type list:
|
|
58
|
+
:return: Hex digest of hash.
|
|
59
|
+
:rtype: Tuple[list, list]
|
|
60
|
+
"""
|
|
61
|
+
new_logs_hashes = []
|
|
62
|
+
logs_to_return = []
|
|
63
|
+
for log in new_logs:
|
|
64
|
+
hash_ = hash_sha1(log)
|
|
65
|
+
if hash_ not in previous_logs_hashes:
|
|
66
|
+
new_logs_hashes.append(hash_)
|
|
67
|
+
logs_to_return.append(log)
|
|
68
|
+
logging.info(
|
|
69
|
+
f"Original number of logs:{len(new_logs)}. Number of logs after de-duplication:{len(logs_to_return)}"
|
|
70
|
+
)
|
|
71
|
+
return logs_to_return, new_logs_hashes
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def make_request(
|
|
75
|
+
_request: requests.Request,
|
|
76
|
+
timeout: int = 60,
|
|
77
|
+
verify: bool = True,
|
|
78
|
+
cert: Union[str, Tuple]=None,
|
|
79
|
+
stream: bool = False,
|
|
80
|
+
allow_redirects: bool = True,
|
|
81
|
+
exception_custom_configs: Dict[int, Exception]={},
|
|
82
|
+
exception_data_location: str = None,
|
|
83
|
+
allowed_status_codes: list[str] = [],
|
|
84
|
+
) -> Tuple[requests.Response, dict]:
|
|
85
|
+
"""
|
|
86
|
+
Makes a HTTP request while checking for RequestErrors and JSONDecodeErrors
|
|
87
|
+
Returns the request response and the response JSON if required.
|
|
88
|
+
:param _request: Request object to utilize in request
|
|
89
|
+
:type Request:
|
|
90
|
+
:param timeout: Requests timeout paramater
|
|
91
|
+
:type int:
|
|
92
|
+
:param verify: Whether to verify the server's TLS certificate
|
|
93
|
+
:type bool:
|
|
94
|
+
:param cert: Certificate to include with request, str location or key/value pair
|
|
95
|
+
:type Union[str, dict]:
|
|
96
|
+
:param stream: Whether to immediately download the response content
|
|
97
|
+
:type bool:
|
|
98
|
+
:param allow_redirects: Set to true by default
|
|
99
|
+
:type bool:
|
|
100
|
+
:param exception_custom_configs: Custom exception values to be raised per HTTPStatusCode.
|
|
101
|
+
:type Dict[str, Exception]:
|
|
102
|
+
:param exception_data_location: Where the returned data should be retrieved. Can provide ResponseExceptionData values.
|
|
103
|
+
:type str:
|
|
104
|
+
:param allowed_status_codes: Status codes that will not raise an exception.
|
|
105
|
+
:type list[str]:
|
|
106
|
+
|
|
107
|
+
:return: The request response and the response JSON.
|
|
108
|
+
:rtype: Tuple[Response, dict]
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
with requests.Session() as session:
|
|
112
|
+
prepared_request = session.prepare_request(request=_request)
|
|
113
|
+
response = session.send(
|
|
114
|
+
prepared_request,
|
|
115
|
+
verify=verify,
|
|
116
|
+
timeout=timeout,
|
|
117
|
+
allow_redirects=allow_redirects,
|
|
118
|
+
cert=cert,
|
|
119
|
+
stream=stream,
|
|
120
|
+
)
|
|
121
|
+
except requests.exceptions.Timeout as exception:
|
|
122
|
+
raise PluginException(
|
|
123
|
+
preset=PluginException.Preset.TIMEOUT, data=str(exception)
|
|
124
|
+
)
|
|
125
|
+
except requests.exceptions.ConnectionError as exception:
|
|
126
|
+
raise PluginException(
|
|
127
|
+
preset=PluginException.Preset.CONNECTION_ERROR, data=str(exception)
|
|
128
|
+
)
|
|
129
|
+
except requests.exceptions.TooManyRedirects as exception:
|
|
130
|
+
raise PluginException(
|
|
131
|
+
preset=PluginException.Preset.REDIRECT_ERROR, data=str(exception)
|
|
132
|
+
)
|
|
133
|
+
except requests.exceptions.RequestException as exception:
|
|
134
|
+
if not isinstance(exception, requests.exceptions.HTTPError):
|
|
135
|
+
raise PluginException(
|
|
136
|
+
preset=PluginException.Preset.UNKNOWN, data=str(exception)
|
|
137
|
+
)
|
|
138
|
+
response_handler(response, exception_custom_configs, exception_data_location, allowed_status_codes)
|
|
139
|
+
return response
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def extract_json(response: requests.Response) -> dict:
|
|
143
|
+
"""Extract JSON from a request object while error handling a JSONDecodeError.
|
|
144
|
+
:param response: Response object ot utilize in extract
|
|
145
|
+
:type Response:
|
|
146
|
+
:returns: Dictionary of response JSON
|
|
147
|
+
:rtype: Dict
|
|
148
|
+
"""
|
|
149
|
+
try:
|
|
150
|
+
response_json = response.json()
|
|
151
|
+
return response_json
|
|
152
|
+
except JSONDecodeError as exception:
|
|
153
|
+
raise PluginException(
|
|
154
|
+
preset=PluginException.Preset.INVALID_JSON, data=str(exception)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def request_error_handling() -> Union[Any, None]:
|
|
159
|
+
"""request_error_handling. This decorator allows a method that makes a request to complete with error handling.
|
|
160
|
+
A plugin exception will be raised whenever an error is caught. Response.raise_for_status() must be called in the
|
|
161
|
+
wrapped method to handle HTTPErrors.
|
|
162
|
+
|
|
163
|
+
:returns: API call function data or None.
|
|
164
|
+
:rtype: Union[Any, None]
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def _decorate(func: Callable):
|
|
168
|
+
def _wrapper(self, *args, **kwargs):
|
|
169
|
+
try:
|
|
170
|
+
return func(self, *args, **kwargs)
|
|
171
|
+
except requests.exceptions.Timeout as exception:
|
|
172
|
+
raise PluginException(
|
|
173
|
+
preset=PluginException.Preset.TIMEOUT, data=str(exception)
|
|
174
|
+
)
|
|
175
|
+
except requests.exceptions.ConnectionError as exception:
|
|
176
|
+
raise PluginException(
|
|
177
|
+
preset=PluginException.Preset.CONNECTION_ERROR, data=str(exception)
|
|
178
|
+
)
|
|
179
|
+
except requests.exceptions.TooManyRedirects as exception:
|
|
180
|
+
raise PluginException(
|
|
181
|
+
preset=PluginException.Preset.REDIRECT_ERROR, data=str(exception)
|
|
182
|
+
)
|
|
183
|
+
except requests.exceptions.RequestException as exception:
|
|
184
|
+
if isinstance(exception, requests.exceptions.HTTPError):
|
|
185
|
+
response_handler(exception.response)
|
|
186
|
+
else:
|
|
187
|
+
raise PluginException(
|
|
188
|
+
preset=PluginException.Preset.UNKNOWN, data=str(exception)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return _wrapper
|
|
192
|
+
|
|
193
|
+
return _decorate
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def response_handler(
|
|
197
|
+
response: requests.Response,
|
|
198
|
+
custom_configs: Dict[int, Exception]={},
|
|
199
|
+
data_location: str = None,
|
|
200
|
+
allowed_status_codes: list[str] = [],
|
|
201
|
+
) -> None:
|
|
202
|
+
"""
|
|
203
|
+
Check response status codes and return a generic PluginException preset if a HTTPError is raised.
|
|
204
|
+
Excetion cause, assistance, and data can be overwritten by supplied parameters.
|
|
205
|
+
|
|
206
|
+
:param response: Response object whose status should be checked.
|
|
207
|
+
:type Response:
|
|
208
|
+
:param custom_configs: Custom exception values to be raised per HTTPStatusCode.
|
|
209
|
+
:type Dict[str, Exception]:
|
|
210
|
+
:param data_location: Where the returned data should be retrieved. Can provide ResponseExceptionData values.
|
|
211
|
+
:type str:
|
|
212
|
+
:param allowed_status_codes: Status codes that will not raise an exception.
|
|
213
|
+
:type list[str]:
|
|
214
|
+
|
|
215
|
+
:return: None.
|
|
216
|
+
:rtype: None
|
|
217
|
+
"""
|
|
218
|
+
try:
|
|
219
|
+
response.raise_for_status()
|
|
220
|
+
except requests.exceptions.HTTPError as exception:
|
|
221
|
+
|
|
222
|
+
data = _return_response_data(response, exception, data_location)
|
|
223
|
+
status_code = response.status_code
|
|
224
|
+
if status_code in allowed_status_codes:
|
|
225
|
+
return
|
|
226
|
+
status_code_presets = {
|
|
227
|
+
HTTPStatusCodes.BAD_REQUEST: PluginException.Preset.BAD_REQUEST,
|
|
228
|
+
HTTPStatusCodes.UNAUTHORIZED: PluginException.Preset.INVALID_CREDENTIALS,
|
|
229
|
+
HTTPStatusCodes.FORBIDDEN: PluginException.Preset.UNAUTHORIZED,
|
|
230
|
+
HTTPStatusCodes.NOT_FOUND: PluginException.Preset.NOT_FOUND,
|
|
231
|
+
HTTPStatusCodes.METHOD_NOT_ALLOWED: PluginException.Preset.METHOD_NOT_ALLOWED,
|
|
232
|
+
HTTPStatusCodes.REQUEST_TIMEOUT: PluginException.Preset.TIMEOUT,
|
|
233
|
+
HTTPStatusCodes.CONFLICT: PluginException.Preset.CONFLICT,
|
|
234
|
+
HTTPStatusCodes.TOO_MANY_REQUESTS: PluginException.Preset.RATE_LIMIT,
|
|
235
|
+
HTTPStatusCodes.INTERNAL_SERVER_ERROR: PluginException.Preset.SERVER_ERROR,
|
|
236
|
+
HTTPStatusCodes.SERVICE_UNAVAILABLE: PluginException.Preset.SERVICE_UNAVAILABLE,
|
|
237
|
+
}
|
|
238
|
+
status_code_preset = status_code_presets.get(status_code)
|
|
239
|
+
exception = PluginException(preset=PluginException.Preset.UNKNOWN, data=data)
|
|
240
|
+
logging.info(f"Request to {response.url} failed. Status code: {status_code}")
|
|
241
|
+
if status_code in custom_configs.keys():
|
|
242
|
+
exception = custom_configs.get(status_code)
|
|
243
|
+
if hasattr(exception, "data") and data is not None:
|
|
244
|
+
exception.data = data
|
|
245
|
+
elif status_code_preset:
|
|
246
|
+
exception = PluginException(preset=status_code_preset, data=data)
|
|
247
|
+
|
|
248
|
+
raise exception
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _return_response_data(
|
|
252
|
+
response: requests.Response,
|
|
253
|
+
exception: requests.HTTPError,
|
|
254
|
+
data_location: str = None,
|
|
255
|
+
) -> str:
|
|
256
|
+
"""
|
|
257
|
+
Retrieve data from HTTP Error given a provided data location
|
|
258
|
+
|
|
259
|
+
:param response: Response object whose status should be checked.
|
|
260
|
+
:type Response:
|
|
261
|
+
:param exception: HTTPError exception to retrieve data.
|
|
262
|
+
:type HTTPError:
|
|
263
|
+
:param data_location: Where the returned data should be retrieved. Can provide ResponseExceptionData values.
|
|
264
|
+
:type str:
|
|
265
|
+
|
|
266
|
+
:return: Exception data.
|
|
267
|
+
:rtype: str
|
|
268
|
+
"""
|
|
269
|
+
data = None
|
|
270
|
+
if data_location == ResponseExceptionData.EXCEPTION:
|
|
271
|
+
data = str(exception)
|
|
272
|
+
elif data_location == ResponseExceptionData.RESPONSE:
|
|
273
|
+
data = response
|
|
274
|
+
elif data_location == ResponseExceptionData.RESPONSE_TEXT:
|
|
275
|
+
data = response.text
|
|
276
|
+
elif data_location == ResponseExceptionData.RESPONSE_JSON:
|
|
277
|
+
try:
|
|
278
|
+
data = response.json()
|
|
279
|
+
except JSONDecodeError:
|
|
280
|
+
# Return full exception if JSON cannot be resolved
|
|
281
|
+
data = exception
|
|
282
|
+
return data
|
|
283
|
+
|
|
284
|
+
|
|
27
285
|
def extract_value(begin, key, end, s):
|
|
28
286
|
"""
|
|
29
287
|
Returns a string from a given key/pattern using provided regular expressions.
|
|
@@ -18,7 +18,12 @@ from insightconnect_plugin_runtime.exceptions import (
|
|
|
18
18
|
PluginException,
|
|
19
19
|
)
|
|
20
20
|
from insightconnect_plugin_runtime.metrics import MetricsBuilder
|
|
21
|
-
from insightconnect_plugin_runtime.util import
|
|
21
|
+
from insightconnect_plugin_runtime.util import (
|
|
22
|
+
is_running_in_cloud,
|
|
23
|
+
flush_logging_handlers,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = structlog.get_logger("plugin")
|
|
22
27
|
|
|
23
28
|
message_output_type = {
|
|
24
29
|
"action_start": "action_event",
|
|
@@ -222,7 +227,9 @@ class Plugin(object):
|
|
|
222
227
|
if status_code is not None:
|
|
223
228
|
output_message["status_code"] = status_code
|
|
224
229
|
|
|
225
|
-
if error_object is not None and isinstance(
|
|
230
|
+
if error_object is not None and isinstance(
|
|
231
|
+
error_object, (ConnectionTestException, PluginException)
|
|
232
|
+
):
|
|
226
233
|
output_message["exception"] = {
|
|
227
234
|
"cause": error_object.cause,
|
|
228
235
|
"assistance": error_object.assistance,
|
|
@@ -236,6 +243,9 @@ class Plugin(object):
|
|
|
236
243
|
|
|
237
244
|
if ex:
|
|
238
245
|
if isinstance(ex, ConnectionTestException):
|
|
246
|
+
error_data = ex.data if ex.data else None
|
|
247
|
+
info_log = f"cause={ex.cause}, assistance={ex.assistance}, data={error_data}"
|
|
248
|
+
logger.error(f"Plugin exception raised. {info_log}")
|
|
239
249
|
output_message["exception"] = {
|
|
240
250
|
"cause": ex.cause,
|
|
241
251
|
"assistance": ex.assistance,
|
|
@@ -441,7 +451,13 @@ class Plugin(object):
|
|
|
441
451
|
is_debug,
|
|
442
452
|
)
|
|
443
453
|
else:
|
|
444
|
-
|
|
454
|
+
(
|
|
455
|
+
output,
|
|
456
|
+
state,
|
|
457
|
+
has_more_pages,
|
|
458
|
+
status_code,
|
|
459
|
+
error_object,
|
|
460
|
+
) = self.start_step(
|
|
445
461
|
input_message["body"],
|
|
446
462
|
"task",
|
|
447
463
|
struct_logger,
|
|
@@ -571,7 +587,9 @@ class Plugin(object):
|
|
|
571
587
|
if step_key == "task":
|
|
572
588
|
state = message_body["state"]
|
|
573
589
|
step.state.validate(state)
|
|
574
|
-
custom_config = message_body.get(
|
|
590
|
+
custom_config = message_body.get(
|
|
591
|
+
"custom_config", {}
|
|
592
|
+
) # we don't validate this for now
|
|
575
593
|
|
|
576
594
|
# Validate required inputs
|
|
577
595
|
# Step inputs will be checked against schema for required properties existence
|
|
@@ -611,7 +629,9 @@ class Plugin(object):
|
|
|
611
629
|
parameters = inspect.signature(func)
|
|
612
630
|
if len(parameters.parameters) > 0:
|
|
613
631
|
if step_key == "task" and not is_test:
|
|
614
|
-
output, state, has_more_pages, status_code, error_object = func(
|
|
632
|
+
output, state, has_more_pages, status_code, error_object = func(
|
|
633
|
+
params, state, custom_config
|
|
634
|
+
)
|
|
615
635
|
else:
|
|
616
636
|
output = func(params)
|
|
617
637
|
else:
|
|
@@ -14,7 +14,13 @@ from gunicorn.arbiter import Arbiter
|
|
|
14
14
|
import structlog
|
|
15
15
|
from pythonjsonlogger.jsonlogger import JsonFormatter
|
|
16
16
|
from requests import get as request_get
|
|
17
|
-
from requests.exceptions import
|
|
17
|
+
from requests.exceptions import (
|
|
18
|
+
HTTPError,
|
|
19
|
+
MissingSchema,
|
|
20
|
+
Timeout,
|
|
21
|
+
JSONDecodeError,
|
|
22
|
+
ConnectionError,
|
|
23
|
+
)
|
|
18
24
|
from time import sleep
|
|
19
25
|
from werkzeug.utils import secure_filename
|
|
20
26
|
|
|
@@ -43,8 +49,12 @@ OPEN_API_VERSION = "2.0"
|
|
|
43
49
|
VERSION_MAPPING = {"legacy": "/", "v1": "/api/v1"}
|
|
44
50
|
CPS_RETRY = 5
|
|
45
51
|
RETRY_SLEEP = 10
|
|
46
|
-
CPS_ENDPOINT = os.getenv(
|
|
47
|
-
|
|
52
|
+
CPS_ENDPOINT = os.getenv(
|
|
53
|
+
"CPS_ENDPOINT"
|
|
54
|
+
) # endpoint to retrieve plugin custom configs (set in cloud deployments.tf)
|
|
55
|
+
DEFAULT_SCHEDULE_INTERVAL_MINUTES = (
|
|
56
|
+
3 # default to 3 as most integrations run on 5 minute interval
|
|
57
|
+
)
|
|
48
58
|
|
|
49
59
|
|
|
50
60
|
class PluginServer(gunicorn.app.base.BaseApplication):
|
|
@@ -185,32 +195,48 @@ class PluginServer(gunicorn.app.base.BaseApplication):
|
|
|
185
195
|
# Call out to komand-props to get configurations related to only the plugin pod running.
|
|
186
196
|
if is_running_in_cloud() and self.plugin.tasks:
|
|
187
197
|
for attempt in range(1, CPS_RETRY + 1):
|
|
188
|
-
self.logger.info(
|
|
198
|
+
self.logger.info(
|
|
199
|
+
f"Getting plugin configuration information... (attempt {attempt}/{CPS_RETRY})"
|
|
200
|
+
)
|
|
189
201
|
try:
|
|
190
202
|
request_response = request_get(CPS_ENDPOINT, timeout=30)
|
|
191
203
|
resp_json = request_response.json()
|
|
192
|
-
plugin = self.plugin.name.lower().replace(
|
|
204
|
+
plugin = self.plugin.name.lower().replace(
|
|
205
|
+
" ", "_"
|
|
206
|
+
) # match how we name our images
|
|
193
207
|
plugin_config = resp_json.get("plugins", {}).get(plugin, {})
|
|
194
208
|
|
|
195
209
|
self.config_options = plugin_config
|
|
196
210
|
self.logger.info("Plugin configuration successfully retrieved...")
|
|
197
211
|
return
|
|
198
212
|
except MissingSchema as missing_schema:
|
|
199
|
-
self.logger.error(
|
|
213
|
+
self.logger.error(
|
|
214
|
+
f"Invalid URL being requested: {CPS_ENDPOINT}, error={missing_schema}"
|
|
215
|
+
)
|
|
200
216
|
except Timeout as timeout:
|
|
201
|
-
self.logger.error(
|
|
217
|
+
self.logger.error(
|
|
218
|
+
f"Connection timeout hit. CPS={CPS_ENDPOINT}, error={timeout}"
|
|
219
|
+
)
|
|
202
220
|
except HTTPError as http_error:
|
|
203
|
-
self.logger.error(
|
|
221
|
+
self.logger.error(
|
|
222
|
+
f"Connection error when trying to reach CPS. CPS={CPS_ENDPOINT}, error={http_error}"
|
|
223
|
+
)
|
|
204
224
|
except JSONDecodeError as bad_json:
|
|
205
|
-
self.logger.error(
|
|
225
|
+
self.logger.error(
|
|
226
|
+
f"Got bad JSON back. Response content={request_response.content}, error={bad_json}"
|
|
227
|
+
)
|
|
206
228
|
except ConnectionError as http_connection_pool_error:
|
|
207
229
|
self.logger.info(
|
|
208
230
|
"Connection refused when trying to reach CPS, retrying as the connection is still establishing."
|
|
209
231
|
)
|
|
210
232
|
except Exception as error:
|
|
211
|
-
self.logger.error(
|
|
233
|
+
self.logger.error(
|
|
234
|
+
f"Hit an unexpected error when retrieving plugin custom configs, error={error}"
|
|
235
|
+
)
|
|
212
236
|
sleep(RETRY_SLEEP)
|
|
213
|
-
self.logger.error(
|
|
237
|
+
self.logger.error(
|
|
238
|
+
f"Unable to reach CPS after {CPS_RETRY} attempts. CPS={CPS_ENDPOINT}"
|
|
239
|
+
)
|
|
214
240
|
|
|
215
241
|
def register_api_spec(self):
|
|
216
242
|
"""Register all swagger schema definitions and path objects"""
|
|
@@ -32,7 +32,9 @@ class OutputMasker:
|
|
|
32
32
|
if connection and isinstance(connection, dict):
|
|
33
33
|
for key, value in connection.items():
|
|
34
34
|
if isinstance(value, dict):
|
|
35
|
-
connection_values.extend(
|
|
35
|
+
connection_values.extend(
|
|
36
|
+
OutputMasker.extract_connection_values(value)
|
|
37
|
+
)
|
|
36
38
|
elif isinstance(value, str) and value and key in keys_to_check:
|
|
37
39
|
connection_values.append(value)
|
|
38
40
|
return sorted(set(connection_values))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: insightconnect-plugin-runtime
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.5.1
|
|
4
4
|
Summary: InsightConnect Plugin Runtime
|
|
5
5
|
Home-page: https://github.com/rapid7/komand-plugin-sdk-python
|
|
6
6
|
Author: Rapid7 Integrations Alliance
|
|
@@ -12,7 +12,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
12
12
|
Classifier: Natural Language :: English
|
|
13
13
|
Classifier: Topic :: Software Development :: Build Tools
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
|
-
Requires-Dist: requests ==2.
|
|
15
|
+
Requires-Dist: requests ==2.32.0
|
|
16
16
|
Requires-Dist: python-jsonschema-objects ==0.5.2
|
|
17
17
|
Requires-Dist: jsonschema ==4.21.1
|
|
18
18
|
Requires-Dist: certifi ==2024.2.2
|
|
@@ -211,6 +211,8 @@ contributed. Black is installed as a test dependency and the hook can be initial
|
|
|
211
211
|
after cloning this repository.
|
|
212
212
|
|
|
213
213
|
## Changelog
|
|
214
|
+
* 5.5.1 - Address bug with typing for type `Tuple` in Python 3.8
|
|
215
|
+
* 5.5.0 - Updated helper class to add `make_request`, `response_handler`, `extract_json`, and `request_error_handling` for HTTP requests, and `hash_sha1` and `compare_and_dedupe_hashes` to provide support for hash comparisons | Add `METHOD_NOT_ALLOWED`, `CONFLICT`, `REDIRECT_ERROR`, and `CONNECTION_ERROR` to PluginException presets
|
|
214
216
|
* 5.4.9 - Updated aws_client to clean assume role json object to remove any none or empty string values.
|
|
215
217
|
* 5.4.8 - Address vulnerabilities within `gunicorn` and `idna` python packages.
|
|
216
218
|
* 5.4.7 - Address vulnerabilities within `insightconnect-python-3-plugin` image.
|
|
@@ -3,22 +3,22 @@ insightconnect_plugin_runtime/action.py,sha256=8gsOONf7mzY83O3DNjCBIafk7C7acnf7m
|
|
|
3
3
|
insightconnect_plugin_runtime/cli.py,sha256=Pb-Janu-XfRlSXxPHh30OIquljWptrhhS51C3clJqh4,8939
|
|
4
4
|
insightconnect_plugin_runtime/connection.py,sha256=4bHHV2B0UFGsAtvLu1fiYQRwx7fissUakHPUyjLQO0E,2340
|
|
5
5
|
insightconnect_plugin_runtime/dispatcher.py,sha256=ru7njnyyWE1-oD-VbZJ-Z8tELwvDf69rM7Iezs4rbnw,1774
|
|
6
|
-
insightconnect_plugin_runtime/exceptions.py,sha256=
|
|
7
|
-
insightconnect_plugin_runtime/helper.py,sha256=
|
|
6
|
+
insightconnect_plugin_runtime/exceptions.py,sha256=7aYNoGgmV6SugHAQqeQYm1zo2LZm0vgXEGqHmYD7NCo,8260
|
|
7
|
+
insightconnect_plugin_runtime/helper.py,sha256=EXPqe-2tG7Er_vOwEzQCPmGpBgauNbUIU0G_R-tWKgk,31155
|
|
8
8
|
insightconnect_plugin_runtime/metrics.py,sha256=hf_Aoufip_s4k4o8Gtzz90ymZthkaT2e5sXh5B4LcF0,3186
|
|
9
|
-
insightconnect_plugin_runtime/plugin.py,sha256=
|
|
9
|
+
insightconnect_plugin_runtime/plugin.py,sha256=9k01QqEh78OTs0pMRfnwZcRr-vFe7iyAtLz_QBa23e4,24127
|
|
10
10
|
insightconnect_plugin_runtime/schema.py,sha256=jTNc6KAMqFpaDVWrAYhkVC6e8I63P3X7uVlJkAr1hiY,583
|
|
11
|
-
insightconnect_plugin_runtime/server.py,sha256=
|
|
11
|
+
insightconnect_plugin_runtime/server.py,sha256=09fxsbKf2ZZvSqRP2Bv9e9-fspDyEFR8_YgIFeMnXqQ,12578
|
|
12
12
|
insightconnect_plugin_runtime/step.py,sha256=KdERg-789-s99IEKN61DR08naz-YPxyinPT0C_T81C4,855
|
|
13
13
|
insightconnect_plugin_runtime/task.py,sha256=d-H1EAzVnmSdDEJtXyIK5JySprxpF9cetVoFGtWlHrg,123
|
|
14
14
|
insightconnect_plugin_runtime/trigger.py,sha256=Zq3cy68N3QxAGbNZKCID6CZF05Zi7YD2sdy_qbedUY8,874
|
|
15
|
-
insightconnect_plugin_runtime/util.py,sha256=
|
|
15
|
+
insightconnect_plugin_runtime/util.py,sha256=qPkZ3LA55nYuNYdansEbnCnBccQkpzIpp9NA1B64Kvw,8444
|
|
16
16
|
insightconnect_plugin_runtime/variables.py,sha256=7FjJGnU7KUR7m9o-_tRq7Q3KiaB1Pp0Apj1NGgOwrJk,3056
|
|
17
17
|
insightconnect_plugin_runtime/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
insightconnect_plugin_runtime/api/endpoints.py,sha256=
|
|
18
|
+
insightconnect_plugin_runtime/api/endpoints.py,sha256=57pSoVSyZU6ERPZ6joMUf6eTDXVRCROLJkmtw4DJhUg,31720
|
|
19
19
|
insightconnect_plugin_runtime/api/schemas.py,sha256=jRmDrwLJTBl-iQOnyZkSwyJlCWg4eNjAnKfD9Eko4z0,2754
|
|
20
20
|
insightconnect_plugin_runtime/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
-
insightconnect_plugin_runtime/clients/aws_client.py,sha256=
|
|
21
|
+
insightconnect_plugin_runtime/clients/aws_client.py,sha256=p_s7YLYOeSrBnJl-1gStuNwse6nw4_sl7kz6AKThboY,20465
|
|
22
22
|
insightconnect_plugin_runtime/clients/oauth.py,sha256=bWtAGRMwdK4dw9vMPcw9usklyIHBDtZh55kMZ7sWROc,2453
|
|
23
23
|
insightconnect_plugin_runtime/data/input_message_schema.json,sha256=7_BcHi6UOBiVWGrrJHHn5IoddteXjL7GOKETdO9T2DE,1770
|
|
24
24
|
insightconnect_plugin_runtime/data/output_message_schema.json,sha256=Qya6U-NR5MfOlw4V98VpQzGBVq75eGMUQhI-j3yxOHI,1137
|
|
@@ -68,7 +68,7 @@ tests/unit/test_aws_action.py,sha256=uerrNB_cH0J9lnxEaVuVaMxTFZoPXuT9lmtpcwoG34Y
|
|
|
68
68
|
tests/unit/test_custom_encoder.py,sha256=KLYyVOTq9MEkZXyhVHqjm5LVSW6uJS4Davgghsw9DGk,2207
|
|
69
69
|
tests/unit/test_endpoints.py,sha256=LuXOfLBu47rDjGa5YEsOwTZBEdvQdl_C6-r46oxWZA8,6401
|
|
70
70
|
tests/unit/test_exceptions.py,sha256=t8c67n3ZQPpyzzYPN6TZHpNeE8uKlOSfHJPk71QONuU,4668
|
|
71
|
-
tests/unit/test_helpers.py,sha256=
|
|
71
|
+
tests/unit/test_helpers.py,sha256=NaZBnVVsQuRb_0uxPjCyptEd5408Ok_AIPjhetGLPjk,16043
|
|
72
72
|
tests/unit/test_metrics.py,sha256=PjjTrB9w7uQ2Q5UN-893-SsH3EGJuBseOMHSD1I004s,7979
|
|
73
73
|
tests/unit/test_oauth.py,sha256=nbFG0JH1x04ExXqSe-b5BGdt_hJs7DP17eUa6bQzcYI,2093
|
|
74
74
|
tests/unit/test_plugin.py,sha256=ZTNAZWwZhDIAbxkVuWhnz9FzmojbijgMmsLWM2mXQI0,4160
|
|
@@ -77,8 +77,8 @@ tests/unit/test_server_cloud_plugins.py,sha256=PuMDHTz3af6lR9QK1BtPScr7_cRbWheto
|
|
|
77
77
|
tests/unit/test_server_spec.py,sha256=je97BaktgK0Fiz3AwFPkcmHzYtOJJNqJV_Fw5hrvqX4,644
|
|
78
78
|
tests/unit/test_trigger.py,sha256=E53mAUoVyponWu_4IQZ0IC1gQ9lakBnTn_9vKN2IZfg,1692
|
|
79
79
|
tests/unit/test_variables.py,sha256=OUEOqGYZA3Nd5oKk5GVY3hcrWKHpZpxysBJcO_v5gzs,291
|
|
80
|
-
tests/unit/utils.py,sha256=
|
|
81
|
-
insightconnect_plugin_runtime-5.
|
|
82
|
-
insightconnect_plugin_runtime-5.
|
|
83
|
-
insightconnect_plugin_runtime-5.
|
|
84
|
-
insightconnect_plugin_runtime-5.
|
|
80
|
+
tests/unit/utils.py,sha256=VooVmfpIgxmglNdtmT32AkEDFxHxyRHLK8RsCWjjYRY,2153
|
|
81
|
+
insightconnect_plugin_runtime-5.5.1.dist-info/METADATA,sha256=-BI5OWO_Kg7Im2iXx12G2otyOjy_Noob-MUAF4ZGjLg,13572
|
|
82
|
+
insightconnect_plugin_runtime-5.5.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
83
|
+
insightconnect_plugin_runtime-5.5.1.dist-info/top_level.txt,sha256=AJtyJOpiFzHxsbHUICTcUKXyrGQ3tZxhrEHsPjJBvEA,36
|
|
84
|
+
insightconnect_plugin_runtime-5.5.1.dist-info/RECORD,,
|
tests/unit/test_helpers.py
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
from insightconnect_plugin_runtime import helper
|
|
3
|
+
from insightconnect_plugin_runtime.exceptions import (
|
|
4
|
+
HTTPStatusCodes,
|
|
5
|
+
ResponseExceptionData,
|
|
6
|
+
PluginException,
|
|
7
|
+
)
|
|
3
8
|
import requests
|
|
4
9
|
import os
|
|
10
|
+
from unittest import TestCase
|
|
11
|
+
from unittest.mock import patch
|
|
12
|
+
from tests.unit.utils import mock_request
|
|
13
|
+
from parameterized import parameterized
|
|
5
14
|
|
|
6
15
|
|
|
7
16
|
def test_extract_value_successful():
|
|
@@ -306,3 +315,209 @@ def test_get_url_content_disposition_success():
|
|
|
306
315
|
}
|
|
307
316
|
|
|
308
317
|
assert "test.html" == helper.get_url_content_disposition(headers)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class TestRequestsHelpers(TestCase):
|
|
321
|
+
def test_response_handler(self):
|
|
322
|
+
response = requests.Response()
|
|
323
|
+
response._content = (
|
|
324
|
+
b'{"message": "Unauthorized", "error": "invalid_credentials"}'
|
|
325
|
+
)
|
|
326
|
+
response.url = "https://example.com"
|
|
327
|
+
response.reason = "UNAUTHORIZED"
|
|
328
|
+
response.status_code = 401
|
|
329
|
+
response.headers["Content-Type"] = "application/json"
|
|
330
|
+
custom_configs = {
|
|
331
|
+
HTTPStatusCodes.UNAUTHORIZED: PluginException(
|
|
332
|
+
cause="Unauthorized custom", assistance="Check permissions custom"
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
response = helper.response_handler(
|
|
336
|
+
response, custom_configs, ResponseExceptionData.RESPONSE_JSON, [401]
|
|
337
|
+
)
|
|
338
|
+
self.assertEqual(response, None)
|
|
339
|
+
|
|
340
|
+
def test_response_handler_error(self):
|
|
341
|
+
response = requests.Response()
|
|
342
|
+
response._content = (
|
|
343
|
+
b'{"message": "Unauthorized", "error": "invalid_credentials"}'
|
|
344
|
+
)
|
|
345
|
+
response.url = "https://example.com"
|
|
346
|
+
response.reason = "UNAUTHORIZED"
|
|
347
|
+
response.status_code = 401
|
|
348
|
+
response.headers["Content-Type"] = "application/json"
|
|
349
|
+
custom_configs = {
|
|
350
|
+
HTTPStatusCodes.UNAUTHORIZED: PluginException(
|
|
351
|
+
cause="Unauthorized custom", assistance="Check permissions custom"
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
with self.assertRaises(PluginException) as assertion:
|
|
355
|
+
helper.response_handler(
|
|
356
|
+
response, custom_configs, ResponseExceptionData.RESPONSE_JSON
|
|
357
|
+
)
|
|
358
|
+
self.assertEqual(
|
|
359
|
+
assertion.exception.cause,
|
|
360
|
+
"Unauthorized custom",
|
|
361
|
+
)
|
|
362
|
+
self.assertEqual(
|
|
363
|
+
assertion.exception.assistance,
|
|
364
|
+
"Check permissions custom",
|
|
365
|
+
)
|
|
366
|
+
self.assertEqual(
|
|
367
|
+
assertion.exception.data,
|
|
368
|
+
{"message": "Unauthorized", "error": "invalid_credentials"},
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
@patch("requests.Session.send", side_effect=mock_request)
|
|
372
|
+
def test_make_request(self, mock_request):
|
|
373
|
+
request = requests.Request(
|
|
374
|
+
method="GET",
|
|
375
|
+
url="https://example.com/success",
|
|
376
|
+
params={"sample": "value"},
|
|
377
|
+
data={"sample": "value"},
|
|
378
|
+
json={"sample": "value"},
|
|
379
|
+
headers={"Content-Type": "application/json"},
|
|
380
|
+
)
|
|
381
|
+
response = helper.make_request(_request=request)
|
|
382
|
+
expected = {
|
|
383
|
+
"json": {"example": "sample"},
|
|
384
|
+
"status_code": 200,
|
|
385
|
+
"content": b"example",
|
|
386
|
+
"url": "https://example.com/success",
|
|
387
|
+
}
|
|
388
|
+
self.assertEqual(response.content, expected.get("content"))
|
|
389
|
+
self.assertEqual(response.status_code, expected.get("status_code"))
|
|
390
|
+
self.assertEqual(response.url, expected.get("url"))
|
|
391
|
+
|
|
392
|
+
@parameterized.expand(
|
|
393
|
+
[
|
|
394
|
+
["401", "GET", "https://example.com/401", {}, PluginException],
|
|
395
|
+
[
|
|
396
|
+
"timeout",
|
|
397
|
+
"GET",
|
|
398
|
+
"https://example.com/timeout",
|
|
399
|
+
{},
|
|
400
|
+
requests.exceptions.Timeout,
|
|
401
|
+
],
|
|
402
|
+
[
|
|
403
|
+
"connectionerror",
|
|
404
|
+
"GET",
|
|
405
|
+
"https://example.com/connectionerror",
|
|
406
|
+
{},
|
|
407
|
+
requests.exceptions.ConnectionError,
|
|
408
|
+
],
|
|
409
|
+
[
|
|
410
|
+
"toomanyredirects",
|
|
411
|
+
"GET",
|
|
412
|
+
"https://example.com/toomanyredirects",
|
|
413
|
+
{},
|
|
414
|
+
requests.exceptions.TooManyRedirects,
|
|
415
|
+
],
|
|
416
|
+
[
|
|
417
|
+
"unknownerror",
|
|
418
|
+
"GET",
|
|
419
|
+
"https://example.com/unknownerror",
|
|
420
|
+
{},
|
|
421
|
+
PluginException,
|
|
422
|
+
],
|
|
423
|
+
[
|
|
424
|
+
"custom404",
|
|
425
|
+
"GET",
|
|
426
|
+
"https://example.com/404",
|
|
427
|
+
{
|
|
428
|
+
404: PluginException(
|
|
429
|
+
cause="CustomCause", assistance="CustomAssistance"
|
|
430
|
+
)
|
|
431
|
+
},
|
|
432
|
+
PluginException,
|
|
433
|
+
],
|
|
434
|
+
]
|
|
435
|
+
)
|
|
436
|
+
@patch("requests.Session.send", side_effect=mock_request)
|
|
437
|
+
def test_make_request_error_handling(
|
|
438
|
+
self, test_name, method, url, exp_config, exception_type, mock_request
|
|
439
|
+
):
|
|
440
|
+
request = requests.Request(method=method, url=url, json=None)
|
|
441
|
+
with self.assertRaises(PluginException) as error:
|
|
442
|
+
helper.make_request(_request=request, exception_custom_configs=exp_config)
|
|
443
|
+
assert (isinstance(error, exception_type), True)
|
|
444
|
+
if test_name == "custom404":
|
|
445
|
+
self.assertEqual(
|
|
446
|
+
error.exception.cause,
|
|
447
|
+
"CustomCause",
|
|
448
|
+
)
|
|
449
|
+
self.assertEqual(
|
|
450
|
+
error.exception.assistance,
|
|
451
|
+
"CustomAssistance",
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
@parameterized.expand(
|
|
455
|
+
[
|
|
456
|
+
["401", "https://example.com/401", PluginException],
|
|
457
|
+
[
|
|
458
|
+
"timeout",
|
|
459
|
+
"https://example.com/timeout",
|
|
460
|
+
requests.exceptions.Timeout,
|
|
461
|
+
],
|
|
462
|
+
[
|
|
463
|
+
"connectionerror",
|
|
464
|
+
"https://example.com/connectionerror",
|
|
465
|
+
requests.exceptions.ConnectionError,
|
|
466
|
+
],
|
|
467
|
+
[
|
|
468
|
+
"toomanyredirects",
|
|
469
|
+
"https://example.com/toomanyredirects",
|
|
470
|
+
requests.exceptions.TooManyRedirects,
|
|
471
|
+
],
|
|
472
|
+
[
|
|
473
|
+
"unknownerror",
|
|
474
|
+
"https://example.com/unknownerror",
|
|
475
|
+
PluginException,
|
|
476
|
+
],
|
|
477
|
+
]
|
|
478
|
+
)
|
|
479
|
+
@patch("requests.request", side_effect=mock_request)
|
|
480
|
+
def test_request_error_handling(self, test_name, url, exception_type, mock_request):
|
|
481
|
+
@helper.request_error_handling()
|
|
482
|
+
def dummy_request(self):
|
|
483
|
+
response = requests.request("GET", url)
|
|
484
|
+
response.raise_for_status()
|
|
485
|
+
|
|
486
|
+
with self.assertRaises(PluginException) as error:
|
|
487
|
+
dummy_request(self)
|
|
488
|
+
assert (isinstance(error, exception_type), True)
|
|
489
|
+
|
|
490
|
+
def test_extract_json(self):
|
|
491
|
+
with self.assertRaises(PluginException) as error:
|
|
492
|
+
response = requests.Response()
|
|
493
|
+
response._content = (
|
|
494
|
+
b'{"message": "Unauthorized", "error": "invalid_credentials'
|
|
495
|
+
)
|
|
496
|
+
response.url = "https://example.com"
|
|
497
|
+
response.reason = "UNAUTHORIZED"
|
|
498
|
+
response.status_code = 401
|
|
499
|
+
response.headers["Content-Type"] = "application/json"
|
|
500
|
+
helper.extract_json(response)
|
|
501
|
+
assert (isinstance(error, PluginException), True)
|
|
502
|
+
self.assertEqual(
|
|
503
|
+
error.exception.cause, "Received an unexpected response from the server."
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def test_hash_sha1():
|
|
508
|
+
log = {"example": "value", "sample": "value"}
|
|
509
|
+
assert "2e1ccc1a95e9b2044f13546c25fe380bbd039293" == helper.hash_sha1(log)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def test_compare_and_dedupe_hashes():
|
|
513
|
+
hashes = ["2e1ccc1a95e9b2044f13546c25fe380bbd039293"]
|
|
514
|
+
logs = [
|
|
515
|
+
{
|
|
516
|
+
"example": "value",
|
|
517
|
+
"sample": "value",
|
|
518
|
+
},
|
|
519
|
+
{"specimen": "new_value"},
|
|
520
|
+
]
|
|
521
|
+
assert [{"specimen": "new_value"}], [
|
|
522
|
+
"ad6ae80c0356e02b1561cb58408ee678eb1070bb"
|
|
523
|
+
] == helper.compare_and_dedupe_hashes(hashes, logs)
|
tests/unit/utils.py
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
class MockResponse:
|
|
2
|
-
"""
|
|
5
|
+
"""Mocked response from CPS"""
|
|
6
|
+
|
|
3
7
|
def __init__(self, value):
|
|
4
8
|
self.json_value = value
|
|
5
9
|
|
|
@@ -9,6 +13,7 @@ class MockResponse:
|
|
|
9
13
|
|
|
10
14
|
class Logger:
|
|
11
15
|
"""Mocked logger to easily find last log triggered from SDK server."""
|
|
16
|
+
|
|
12
17
|
def __init__(self):
|
|
13
18
|
self.last_error = []
|
|
14
19
|
self.last_info = []
|
|
@@ -18,3 +23,56 @@ class Logger:
|
|
|
18
23
|
|
|
19
24
|
def error(self, log: str):
|
|
20
25
|
self.last_error.append(log)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_mock_response(
|
|
29
|
+
status_code: int, url: str, reason: str, content: str = None, data: dict = {}
|
|
30
|
+
):
|
|
31
|
+
response = requests.Response()
|
|
32
|
+
response.status_code = status_code
|
|
33
|
+
bytes_string = content.encode("utf-8")
|
|
34
|
+
response._content = bytes_string
|
|
35
|
+
response.url = url
|
|
36
|
+
response.reason = reason
|
|
37
|
+
|
|
38
|
+
def return_json():
|
|
39
|
+
return data
|
|
40
|
+
|
|
41
|
+
response.json = return_json
|
|
42
|
+
return response
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def mock_request(*args, **kwargs):
|
|
46
|
+
if isinstance(args[0], requests.PreparedRequest):
|
|
47
|
+
url = args[0].url
|
|
48
|
+
else:
|
|
49
|
+
url = args[1]
|
|
50
|
+
if url == "https://example.com/success?sample=value":
|
|
51
|
+
return get_mock_response(
|
|
52
|
+
200, "https://example.com/success", None, "example", {"example": "sample"}
|
|
53
|
+
)
|
|
54
|
+
if url == "https://example.com/401":
|
|
55
|
+
return get_mock_response(
|
|
56
|
+
401,
|
|
57
|
+
"https://example.com/401",
|
|
58
|
+
"UNAUTHORIZED",
|
|
59
|
+
"example",
|
|
60
|
+
{"example": "sample"},
|
|
61
|
+
)
|
|
62
|
+
if url == "https://example.com/404":
|
|
63
|
+
return get_mock_response(
|
|
64
|
+
404,
|
|
65
|
+
"https://example.com/404",
|
|
66
|
+
"NOT_FOUND",
|
|
67
|
+
"example",
|
|
68
|
+
{"example": "sample"},
|
|
69
|
+
)
|
|
70
|
+
if url == "https://example.com/timeout":
|
|
71
|
+
raise requests.exceptions.Timeout()
|
|
72
|
+
if url == "https://example.com/connectionerror":
|
|
73
|
+
raise requests.exceptions.ConnectionError()
|
|
74
|
+
if url == "https://example.com/toomanyredirects":
|
|
75
|
+
raise requests.exceptions.TooManyRedirects()
|
|
76
|
+
if url == "https://example.com/unknownerror":
|
|
77
|
+
raise requests.exceptions.ContentDecodingError
|
|
78
|
+
raise NotImplementedError("Not implemented")
|
|
File without changes
|
|
File without changes
|