insightconnect-plugin-runtime 6.2.0__py3-none-any.whl → 6.2.4__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 +14 -15
- insightconnect_plugin_runtime/helper.py +103 -47
- insightconnect_plugin_runtime/schema.py +6 -9
- {insightconnect_plugin_runtime-6.2.0.dist-info → insightconnect_plugin_runtime-6.2.4.dist-info}/METADATA +28 -16
- {insightconnect_plugin_runtime-6.2.0.dist-info → insightconnect_plugin_runtime-6.2.4.dist-info}/RECORD +9 -9
- {insightconnect_plugin_runtime-6.2.0.dist-info → insightconnect_plugin_runtime-6.2.4.dist-info}/WHEEL +1 -1
- tests/unit/test_helpers.py +66 -16
- tests/unit/utils.py +1 -0
- {insightconnect_plugin_runtime-6.2.0.dist-info → insightconnect_plugin_runtime-6.2.4.dist-info}/top_level.txt +0 -0
|
@@ -1,27 +1,27 @@
|
|
|
1
|
+
import importlib.metadata as importlib_metadata
|
|
1
2
|
import json
|
|
2
|
-
import subprocess
|
|
3
|
-
import yaml
|
|
4
3
|
import os
|
|
5
|
-
import pkg_resources
|
|
6
4
|
import signal
|
|
7
|
-
|
|
8
|
-
from werkzeug.exceptions import InternalServerError, HTTPException
|
|
5
|
+
import subprocess
|
|
9
6
|
from typing import Any, Dict
|
|
10
7
|
|
|
11
8
|
import structlog
|
|
9
|
+
import yaml
|
|
10
|
+
from flask import Blueprint, abort, jsonify, make_response, request
|
|
11
|
+
from werkzeug.exceptions import HTTPException, InternalServerError
|
|
12
12
|
|
|
13
|
+
from insightconnect_plugin_runtime.api.schemas import (
|
|
14
|
+
ActionTriggerDetailsSchema,
|
|
15
|
+
ConnectionDetailsSchema,
|
|
16
|
+
PluginInfoSchema,
|
|
17
|
+
TaskDetailsSchema,
|
|
18
|
+
)
|
|
13
19
|
from insightconnect_plugin_runtime.exceptions import (
|
|
14
20
|
ClientException,
|
|
15
|
-
ServerException,
|
|
16
|
-
LoggedException,
|
|
17
21
|
ConnectionTestException,
|
|
22
|
+
LoggedException,
|
|
18
23
|
PluginException,
|
|
19
|
-
|
|
20
|
-
from insightconnect_plugin_runtime.api.schemas import (
|
|
21
|
-
PluginInfoSchema,
|
|
22
|
-
ActionTriggerDetailsSchema,
|
|
23
|
-
TaskDetailsSchema,
|
|
24
|
-
ConnectionDetailsSchema,
|
|
24
|
+
ServerException,
|
|
25
25
|
)
|
|
26
26
|
from insightconnect_plugin_runtime.util import OutputMasker
|
|
27
27
|
|
|
@@ -780,11 +780,10 @@ class Endpoints:
|
|
|
780
780
|
|
|
781
781
|
def get_plugin_sdk_version(self):
|
|
782
782
|
try:
|
|
783
|
-
version =
|
|
783
|
+
version = importlib_metadata.version("insightconnect-plugin-runtime")
|
|
784
784
|
except Exception:
|
|
785
785
|
self.logger.warn("Unable to get SDK version")
|
|
786
786
|
version = "0.0.0"
|
|
787
|
-
|
|
788
787
|
return version
|
|
789
788
|
|
|
790
789
|
def add_plugin_custom_config(
|
|
@@ -10,7 +10,7 @@ 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, Tuple
|
|
13
|
+
from typing import Any, Callable, Dict, List, Union, Tuple, Optional
|
|
14
14
|
from urllib import request
|
|
15
15
|
from hashlib import sha1
|
|
16
16
|
from json import JSONDecodeError
|
|
@@ -21,6 +21,7 @@ from insightconnect_plugin_runtime.exceptions import (
|
|
|
21
21
|
PluginException,
|
|
22
22
|
HTTPStatusCodes,
|
|
23
23
|
ResponseExceptionData,
|
|
24
|
+
APIException
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
CAMEL_CASE_REGEX = r"\b[a-z0-9]+([A-Z][a-z]+[0-9]*)*\b"
|
|
@@ -30,18 +31,48 @@ ENCODE_TYPE = "utf-8"
|
|
|
30
31
|
|
|
31
32
|
DEFAULTS_HOURS_AGO = 24
|
|
32
33
|
|
|
34
|
+
logger = logging.getLogger()
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
|
|
37
|
+
def hash_sha1(log: Dict[str, Any], keys: Optional[List[str]] = None) -> str:
|
|
35
38
|
"""
|
|
36
39
|
Iterate through a dictionary and hash each value.
|
|
40
|
+
Optionally only hash certain keys in the dictionary.
|
|
41
|
+
|
|
37
42
|
:param log: Dictionary to be hashed.
|
|
38
|
-
:
|
|
43
|
+
:param keys: Optional list of keys to hash on if provided
|
|
44
|
+
|
|
39
45
|
:return: Hex digest of hash.
|
|
40
|
-
:rtype: str
|
|
41
46
|
"""
|
|
47
|
+
|
|
42
48
|
hash_ = sha1() # nosec B303
|
|
43
|
-
|
|
49
|
+
|
|
50
|
+
# Leaving no room for developer error and ensuring they know exactly where it went wrong
|
|
51
|
+
# if they provide a key not in list format
|
|
52
|
+
if keys is not None and not isinstance(keys, list):
|
|
53
|
+
raise TypeError(
|
|
54
|
+
f"The 'keys' parameter must be a list or None in the 'hash_sha1' function, not {type(keys).__name__}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Hash all key-value pairs if no keys provided
|
|
58
|
+
if keys is None:
|
|
59
|
+
items_to_hash = log.items()
|
|
60
|
+
|
|
61
|
+
# Otherwise, only include specified keys
|
|
62
|
+
else:
|
|
63
|
+
items_to_hash = []
|
|
64
|
+
for key in keys:
|
|
65
|
+
if key in log:
|
|
66
|
+
items_to_hash.append((key, log[key]))
|
|
67
|
+
|
|
68
|
+
# Alert if the key is not found in the log
|
|
69
|
+
else:
|
|
70
|
+
raise KeyError(f"Key '{key}' not found in the provided log.")
|
|
71
|
+
|
|
72
|
+
# Iterate through items to hash and hash
|
|
73
|
+
for key, value in items_to_hash:
|
|
44
74
|
hash_.update(f"{key}{value}".encode(ENCODE_TYPE))
|
|
75
|
+
|
|
45
76
|
return hash_.hexdigest()
|
|
46
77
|
|
|
47
78
|
|
|
@@ -65,7 +96,7 @@ def compare_and_dedupe_hashes(
|
|
|
65
96
|
if hash_ not in previous_logs_hashes:
|
|
66
97
|
new_logs_hashes.append(hash_)
|
|
67
98
|
logs_to_return.append(log)
|
|
68
|
-
|
|
99
|
+
logger.info(
|
|
69
100
|
f"Original number of logs:{len(new_logs)}. Number of logs after de-duplication:{len(logs_to_return)}"
|
|
70
101
|
)
|
|
71
102
|
return logs_to_return, new_logs_hashes
|
|
@@ -81,6 +112,7 @@ def make_request(
|
|
|
81
112
|
exception_custom_configs: Dict[int, Exception] = {},
|
|
82
113
|
exception_data_location: str = None,
|
|
83
114
|
allowed_status_codes: List[str] = [],
|
|
115
|
+
max_response_size: int = None,
|
|
84
116
|
) -> Tuple[requests.Response, Dict]:
|
|
85
117
|
"""
|
|
86
118
|
Makes a HTTP request while checking for RequestErrors and JSONDecodeErrors
|
|
@@ -103,6 +135,8 @@ def make_request(
|
|
|
103
135
|
:type str:
|
|
104
136
|
:param allowed_status_codes: Status codes that will not raise an exception.
|
|
105
137
|
:type List[str]:
|
|
138
|
+
:param max_response_size: Raise an error if the stream content is bigger than this specified size
|
|
139
|
+
:type int:
|
|
106
140
|
|
|
107
141
|
:return: The request response and the response JSON.
|
|
108
142
|
:rtype: Tuple[Response, Dict]
|
|
@@ -118,6 +152,19 @@ def make_request(
|
|
|
118
152
|
cert=cert,
|
|
119
153
|
stream=stream,
|
|
120
154
|
)
|
|
155
|
+
|
|
156
|
+
# Before we close this session check and download all content, check if the returned content is too large.
|
|
157
|
+
# This may not be supported on all APIs as they need to support streaming and return the content-length
|
|
158
|
+
# header but adding this extra check will be beneficial for memory usage to those that do support it.
|
|
159
|
+
if stream and max_response_size:
|
|
160
|
+
resp_size = response.headers.get("content-length", "0")
|
|
161
|
+
if int(resp_size) > max_response_size:
|
|
162
|
+
raise APIException(
|
|
163
|
+
status_code=400,
|
|
164
|
+
cause=f"API response is exceeding allowed limit of {max_response_size} bytes.",
|
|
165
|
+
assistance="Please update the parameters to reduce the size of the data being returned.",
|
|
166
|
+
data=f"Content length returned was {resp_size} and max allowed is {max_response_size}",
|
|
167
|
+
)
|
|
121
168
|
except requests.exceptions.Timeout as exception:
|
|
122
169
|
raise PluginException(
|
|
123
170
|
preset=PluginException.Preset.TIMEOUT, data=str(exception)
|
|
@@ -135,7 +182,12 @@ def make_request(
|
|
|
135
182
|
raise PluginException(
|
|
136
183
|
preset=PluginException.Preset.UNKNOWN, data=str(exception)
|
|
137
184
|
)
|
|
138
|
-
response_handler(
|
|
185
|
+
response_handler(
|
|
186
|
+
response,
|
|
187
|
+
exception_custom_configs,
|
|
188
|
+
exception_data_location,
|
|
189
|
+
allowed_status_codes,
|
|
190
|
+
)
|
|
139
191
|
return response
|
|
140
192
|
|
|
141
193
|
|
|
@@ -190,7 +242,7 @@ def request_error_handling(
|
|
|
190
242
|
exception.response,
|
|
191
243
|
data_location=exception_data_location,
|
|
192
244
|
custom_configs=custom_configs,
|
|
193
|
-
allowed_status_codes=allowed_status_codes
|
|
245
|
+
allowed_status_codes=allowed_status_codes,
|
|
194
246
|
)
|
|
195
247
|
else:
|
|
196
248
|
raise PluginException(
|
|
@@ -246,7 +298,7 @@ def response_handler(
|
|
|
246
298
|
}
|
|
247
299
|
status_code_preset = status_code_presets.get(status_code)
|
|
248
300
|
exception = PluginException(preset=PluginException.Preset.UNKNOWN, data=data)
|
|
249
|
-
|
|
301
|
+
logger.info(f"Request to {response.url} failed. Status code: {status_code}")
|
|
250
302
|
if status_code in custom_configs.keys():
|
|
251
303
|
exception = custom_configs.get(status_code)
|
|
252
304
|
if hasattr(exception, "data") and data is not None:
|
|
@@ -441,15 +493,19 @@ def convert_dict_to_snake_case(
|
|
|
441
493
|
|
|
442
494
|
if isinstance(input_dict, list):
|
|
443
495
|
return [
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
496
|
+
(
|
|
497
|
+
convert_dict_to_snake_case(element)
|
|
498
|
+
if isinstance(element, (dict, list))
|
|
499
|
+
else element
|
|
500
|
+
)
|
|
447
501
|
for element in input_dict
|
|
448
502
|
]
|
|
449
503
|
return {
|
|
450
|
-
convert_to_snake_case(key):
|
|
451
|
-
|
|
452
|
-
|
|
504
|
+
convert_to_snake_case(key): (
|
|
505
|
+
convert_dict_to_snake_case(value)
|
|
506
|
+
if isinstance(value, (dict, list))
|
|
507
|
+
else value
|
|
508
|
+
)
|
|
453
509
|
for key, value in input_dict.items()
|
|
454
510
|
}
|
|
455
511
|
|
|
@@ -561,7 +617,7 @@ def rate_limiting(
|
|
|
561
617
|
error.cause
|
|
562
618
|
== PluginException.causes[PluginException.Preset.RATE_LIMIT]
|
|
563
619
|
):
|
|
564
|
-
|
|
620
|
+
logger.info(
|
|
565
621
|
f"Rate limiting error occurred. Retrying in {delay:.1f} seconds ({attempts_counter}/{max_tries})"
|
|
566
622
|
)
|
|
567
623
|
retry = True
|
|
@@ -603,12 +659,12 @@ def check_hashes(src, checksum):
|
|
|
603
659
|
if type(src) is str:
|
|
604
660
|
hashes = get_hashes_string(src)
|
|
605
661
|
else:
|
|
606
|
-
|
|
662
|
+
logger.error("CheckHashes: Argument must be a string")
|
|
607
663
|
raise Exception("CheckHashes")
|
|
608
664
|
for alg in hashes:
|
|
609
665
|
if hashes[alg] == checksum:
|
|
610
666
|
return True
|
|
611
|
-
|
|
667
|
+
logger.info("CheckHashes: No checksum match")
|
|
612
668
|
return False
|
|
613
669
|
|
|
614
670
|
|
|
@@ -620,9 +676,9 @@ def check_cachefile(cache_file):
|
|
|
620
676
|
cache_file = cache_dir + "/" + cache_file
|
|
621
677
|
if os.path.isdir(cache_dir):
|
|
622
678
|
if os.path.isfile(cache_file):
|
|
623
|
-
|
|
679
|
+
logger.info("CheckCacheFile: File %s exists", cache_file)
|
|
624
680
|
return True
|
|
625
|
-
|
|
681
|
+
logger.info("CheckCacheFile: File %s did not exist", cache_file)
|
|
626
682
|
return False
|
|
627
683
|
|
|
628
684
|
|
|
@@ -638,9 +694,9 @@ def open_file(file_path):
|
|
|
638
694
|
return f
|
|
639
695
|
return None
|
|
640
696
|
else:
|
|
641
|
-
|
|
697
|
+
logger.info("OpenFile: File %s is not a file or does not exist ", filename)
|
|
642
698
|
else:
|
|
643
|
-
|
|
699
|
+
logger.error(
|
|
644
700
|
"OpenFile: Directory %s is not a directory or does not exist", dirname
|
|
645
701
|
)
|
|
646
702
|
|
|
@@ -654,16 +710,16 @@ def open_cachefile(cache_file, append=False):
|
|
|
654
710
|
if os.path.isdir(cache_dir):
|
|
655
711
|
if os.path.isfile(cache_file):
|
|
656
712
|
f = open(cache_file, "a+" if append else "r+")
|
|
657
|
-
|
|
713
|
+
logger.info("OpenCacheFile: %s exists, returning it", cache_file)
|
|
658
714
|
else:
|
|
659
715
|
if not os.path.isdir(os.path.dirname(cache_file)):
|
|
660
716
|
os.makedirs(os.path.dirname(cache_file))
|
|
661
717
|
f = open(cache_file, "w+") # Open once to create the cache file
|
|
662
718
|
f.close()
|
|
663
|
-
|
|
719
|
+
logger.info("OpenCacheFile: %s created", cache_file)
|
|
664
720
|
f = open(cache_file, "a+" if append else "r+")
|
|
665
721
|
return f
|
|
666
|
-
|
|
722
|
+
logger.error("OpenCacheFile: %s directory or does not exist", cache_dir)
|
|
667
723
|
|
|
668
724
|
|
|
669
725
|
def remove_cachefile(cache_file):
|
|
@@ -676,7 +732,7 @@ def remove_cachefile(cache_file):
|
|
|
676
732
|
if os.path.isfile(cache_file):
|
|
677
733
|
os.remove(cache_file)
|
|
678
734
|
return True
|
|
679
|
-
|
|
735
|
+
logger.info("RemoveCacheFile: Cache file %s did not exist", cache_file)
|
|
680
736
|
return False
|
|
681
737
|
|
|
682
738
|
|
|
@@ -695,9 +751,9 @@ def lock_cache(lock_file):
|
|
|
695
751
|
os.makedirs(os.path.dirname(lock_file))
|
|
696
752
|
f = open(lock_file, "w")
|
|
697
753
|
f.close()
|
|
698
|
-
|
|
754
|
+
logger.info("Cache lock %s created", lock_file)
|
|
699
755
|
return True
|
|
700
|
-
|
|
756
|
+
logger.info("Cache lock %s failed, lock not created", lock_file)
|
|
701
757
|
return False
|
|
702
758
|
|
|
703
759
|
|
|
@@ -716,7 +772,7 @@ def unlock_cache(lock_file, wait_time):
|
|
|
716
772
|
time.sleep(wait_time)
|
|
717
773
|
os.remove(lock_file)
|
|
718
774
|
return True
|
|
719
|
-
|
|
775
|
+
logger.info("Cache unlock %s failed, lock not released", lock_file)
|
|
720
776
|
return False
|
|
721
777
|
|
|
722
778
|
|
|
@@ -746,11 +802,11 @@ def open_url(url, timeout=None, verify=True, **kwargs):
|
|
|
746
802
|
urlobj = request.urlopen(req, timeout=timeout, context=ctx)
|
|
747
803
|
return urlobj
|
|
748
804
|
except request.HTTPError as e:
|
|
749
|
-
|
|
805
|
+
logger.error("HTTPError: %s for %s", str(e.code), url)
|
|
750
806
|
if e.code == 304:
|
|
751
807
|
return None
|
|
752
808
|
except request.URLError as e:
|
|
753
|
-
|
|
809
|
+
logger.error("URLError: %s for %s", str(e.reason), url)
|
|
754
810
|
raise Exception("GetURL Failed")
|
|
755
811
|
|
|
756
812
|
|
|
@@ -778,17 +834,17 @@ def check_url(url):
|
|
|
778
834
|
return True
|
|
779
835
|
|
|
780
836
|
except requests.exceptions.HTTPError:
|
|
781
|
-
|
|
837
|
+
logger.error(
|
|
782
838
|
"Requests: HTTPError: status code %s for %s",
|
|
783
839
|
str(resp.status_code) if resp else None,
|
|
784
840
|
url,
|
|
785
841
|
)
|
|
786
842
|
except requests.exceptions.Timeout:
|
|
787
|
-
|
|
843
|
+
logger.error("Requests: Timeout for %s", url)
|
|
788
844
|
except requests.exceptions.TooManyRedirects:
|
|
789
|
-
|
|
845
|
+
logger.error("Requests: TooManyRedirects for %s", url)
|
|
790
846
|
except requests.ConnectionError:
|
|
791
|
-
|
|
847
|
+
logger.error("Requests: ConnectionError for %s", url)
|
|
792
848
|
return False
|
|
793
849
|
|
|
794
850
|
|
|
@@ -808,7 +864,7 @@ def exec_command(command):
|
|
|
808
864
|
rcode = p.poll()
|
|
809
865
|
return {"stdout": stdout, "stderr": stderr, "rcode": rcode}
|
|
810
866
|
except OSError as e:
|
|
811
|
-
|
|
867
|
+
logger.error(
|
|
812
868
|
"SubprocessError: %s %s: %s", str(e.filename), str(e.strerror), str(e.errno)
|
|
813
869
|
)
|
|
814
870
|
raise Exception("ExecCommand")
|
|
@@ -834,7 +890,7 @@ def encode_file(file_path):
|
|
|
834
890
|
return efile
|
|
835
891
|
return None
|
|
836
892
|
except (IOError, OSError) as e:
|
|
837
|
-
|
|
893
|
+
logger.error("EncodeFile: Failed to open file: %s", e.strerror)
|
|
838
894
|
raise Exception("EncodeFile")
|
|
839
895
|
finally:
|
|
840
896
|
if isinstance(f, IOBase):
|
|
@@ -857,17 +913,17 @@ def check_url_modified(url):
|
|
|
857
913
|
if resp.status_code == 200:
|
|
858
914
|
return True
|
|
859
915
|
except requests.exceptions.HTTPError:
|
|
860
|
-
|
|
916
|
+
logger.error(
|
|
861
917
|
"Requests: HTTPError: status code %s for %s",
|
|
862
918
|
str(resp.status_code) if resp else None,
|
|
863
919
|
url,
|
|
864
920
|
)
|
|
865
921
|
except requests.exceptions.Timeout:
|
|
866
|
-
|
|
922
|
+
logger.error("Requests: Timeout for %s", url)
|
|
867
923
|
except requests.exceptions.TooManyRedirects:
|
|
868
|
-
|
|
924
|
+
logger.error("Requests: TooManyRedirects for %s", url)
|
|
869
925
|
except requests.ConnectionError:
|
|
870
|
-
|
|
926
|
+
logger.error("Requests: ConnectionError for %s", url)
|
|
871
927
|
return False
|
|
872
928
|
|
|
873
929
|
|
|
@@ -893,7 +949,7 @@ def get_url_path_filename(url):
|
|
|
893
949
|
if name[n].endswith("."):
|
|
894
950
|
return name
|
|
895
951
|
except IndexError:
|
|
896
|
-
|
|
952
|
+
logger.error("Range: IndexError: URL basename is short: %s of %s", name, url)
|
|
897
953
|
return None
|
|
898
954
|
return None
|
|
899
955
|
|
|
@@ -913,16 +969,16 @@ def get_url_filename(url):
|
|
|
913
969
|
return name
|
|
914
970
|
return None
|
|
915
971
|
except requests.exceptions.MissingSchema:
|
|
916
|
-
|
|
972
|
+
logger.error("Requests: MissingSchema: Requires ftp|http(s):// for %s", url)
|
|
917
973
|
except requests.exceptions.HTTPError:
|
|
918
|
-
|
|
974
|
+
logger.error(
|
|
919
975
|
"Requests: HTTPError: status code %s for %s",
|
|
920
976
|
str(resp.status_code) if resp else None,
|
|
921
977
|
url,
|
|
922
978
|
)
|
|
923
979
|
except requests.exceptions.Timeout:
|
|
924
|
-
|
|
980
|
+
logger.error("Requests: Timeout for %s", url)
|
|
925
981
|
except requests.exceptions.TooManyRedirects:
|
|
926
|
-
|
|
982
|
+
logger.error("Requests: TooManyRedirects for %s", url)
|
|
927
983
|
except requests.ConnectionError:
|
|
928
|
-
|
|
984
|
+
logger.error("Requests: ConnectionError for %s", url)
|
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
import json
|
|
2
|
+
from pathlib import Path
|
|
2
3
|
|
|
3
|
-
import pkg_resources
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
def load_schema(file_name):
|
|
5
|
+
def load_schema(file_name: str) -> dict:
|
|
7
6
|
"""
|
|
8
7
|
Loads a json schema from the packages data folder.
|
|
9
8
|
:param file_name: name of the file
|
|
10
9
|
:return: JSON object as a dictionary
|
|
11
10
|
"""
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
schema = json.loads(schema)
|
|
18
|
-
return schema
|
|
12
|
+
with open(
|
|
13
|
+
Path(__file__).parent / "data" / file_name, "r", encoding="utf-8"
|
|
14
|
+
) as schema_file:
|
|
15
|
+
return json.loads(schema_file.read())
|
|
19
16
|
|
|
20
17
|
|
|
21
18
|
input_message_schema = load_schema("input_message_schema.json")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: insightconnect-plugin-runtime
|
|
3
|
-
Version: 6.2.
|
|
3
|
+
Version: 6.2.4
|
|
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,20 +12,28 @@ 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.32.
|
|
16
|
-
Requires-Dist:
|
|
17
|
-
Requires-Dist: jsonschema==4.
|
|
18
|
-
Requires-Dist: certifi==2024.
|
|
19
|
-
Requires-Dist: Flask==3.0
|
|
20
|
-
Requires-Dist: gunicorn==
|
|
15
|
+
Requires-Dist: requests==2.32.3
|
|
16
|
+
Requires-Dist: python_jsonschema_objects==0.5.2
|
|
17
|
+
Requires-Dist: jsonschema==4.22.0
|
|
18
|
+
Requires-Dist: certifi==2024.12.14
|
|
19
|
+
Requires-Dist: Flask==3.1.0
|
|
20
|
+
Requires-Dist: gunicorn==23.0.0
|
|
21
21
|
Requires-Dist: greenlet==3.1.1
|
|
22
|
-
Requires-Dist: gevent==24.
|
|
22
|
+
Requires-Dist: gevent==24.11.1
|
|
23
23
|
Requires-Dist: marshmallow==3.21.0
|
|
24
24
|
Requires-Dist: apispec==6.5.0
|
|
25
25
|
Requires-Dist: apispec-webframeworks==1.0.0
|
|
26
|
-
Requires-Dist: blinker==1.
|
|
27
|
-
Requires-Dist: structlog==24.
|
|
26
|
+
Requires-Dist: blinker==1.9.0
|
|
27
|
+
Requires-Dist: structlog==24.4.0
|
|
28
28
|
Requires-Dist: python-json-logger==2.0.7
|
|
29
|
+
Dynamic: author
|
|
30
|
+
Dynamic: author-email
|
|
31
|
+
Dynamic: classifier
|
|
32
|
+
Dynamic: description
|
|
33
|
+
Dynamic: description-content-type
|
|
34
|
+
Dynamic: home-page
|
|
35
|
+
Dynamic: requires-dist
|
|
36
|
+
Dynamic: summary
|
|
29
37
|
|
|
30
38
|
|
|
31
39
|
# InsightConnect Python Plugin Runtime 
|
|
@@ -48,10 +56,10 @@ to get started.
|
|
|
48
56
|
|
|
49
57
|
## Development of the InsightConnect Plugin Runtime
|
|
50
58
|
|
|
51
|
-
The Python Runtime codebase is built to support Python 3.11.
|
|
59
|
+
The Python Runtime codebase is built to support Python 3.11.11 as of version 6.2.3. The following dependencies will need
|
|
52
60
|
to be installed when developing or testing the Plugin Runtime:
|
|
53
61
|
|
|
54
|
-
- Python 3.11.
|
|
62
|
+
- Python 3.11.11
|
|
55
63
|
- Docker
|
|
56
64
|
- make
|
|
57
65
|
- tox
|
|
@@ -67,7 +75,7 @@ version and activate it. Then build, install, and confirm the package has been i
|
|
|
67
75
|
> source venv/bin/activate
|
|
68
76
|
> pip install -e ./
|
|
69
77
|
> pip list | grep insightconnect-plugin-runtime
|
|
70
|
-
insightconnect-plugin-runtime
|
|
78
|
+
insightconnect-plugin-runtime 6.2.3
|
|
71
79
|
```
|
|
72
80
|
|
|
73
81
|
#### Building the InsightConnect Plugin Runtime Docker Images
|
|
@@ -123,7 +131,7 @@ name as a parameter:
|
|
|
123
131
|
|
|
124
132
|
The plugin will be started in `http` mode and listening at `http:0.0.0.0:10001`:
|
|
125
133
|
```
|
|
126
|
-
[2020-02-13 23:21:13 -0500] [56567] [INFO] Starting gunicorn
|
|
134
|
+
[2020-02-13 23:21:13 -0500] [56567] [INFO] Starting gunicorn 23.0.0
|
|
127
135
|
[2020-02-13 23:21:13 -0500] [56567] [INFO] Listening at: http://0.0.0.0:10001 (56567)
|
|
128
136
|
[2020-02-13 23:21:13 -0500] [56567] [INFO] Using worker: threads
|
|
129
137
|
[2020-02-13 23:21:13 -0500] [56571] [INFO] Booting worker with pid: 56571
|
|
@@ -184,7 +192,7 @@ Running a specific test file:
|
|
|
184
192
|
|
|
185
193
|
| | Plugin | Slim Plugin |
|
|
186
194
|
|:------------------|:-------:|:-----------:|
|
|
187
|
-
| Python Version | 3.11.
|
|
195
|
+
| Python Version | 3.11.11 | 3.11.11 |
|
|
188
196
|
| OS | Alpine | Bullseye |
|
|
189
197
|
| Package installer | apk | apt |
|
|
190
198
|
| Shell | /bin/sh | /bin/bash |
|
|
@@ -211,6 +219,10 @@ contributed. Black is installed as a test dependency and the hook can be initial
|
|
|
211
219
|
after cloning this repository.
|
|
212
220
|
|
|
213
221
|
## Changelog
|
|
222
|
+
* 6.2.4 - Update `make_request` helper to support extra parameter of `max_response_size` to cap the response
|
|
223
|
+
* 6.2.3 - Updated dockerfiles for both `slim` and `full` SDK types to use Python 3.11.11 | Updated dependencies | Removed `pkg_resources` usage due to deprecation
|
|
224
|
+
* 6.2.2 - Fix instances where logging errors would lead to duplicate entries being output | Add option to hash only on provided keys for `hash_sha1` function
|
|
225
|
+
* 6.2.1 - Fix instances where logging would lead to duplicate entries being output
|
|
214
226
|
* 6.2.0 - Update base images to pull Python 3.11.10 | changed the pep-8 check in tox to `pycodestyle`
|
|
215
227
|
* 6.1.4 - Address vulnerabilities within local development requirements.txt and vulnerabilities in slim image.
|
|
216
228
|
* 6.1.3 - Addressing failing Python Slim package (bump packages).
|
|
@@ -4,10 +4,10 @@ insightconnect_plugin_runtime/cli.py,sha256=Pb-Janu-XfRlSXxPHh30OIquljWptrhhS51C
|
|
|
4
4
|
insightconnect_plugin_runtime/connection.py,sha256=4bHHV2B0UFGsAtvLu1fiYQRwx7fissUakHPUyjLQO0E,2340
|
|
5
5
|
insightconnect_plugin_runtime/dispatcher.py,sha256=ru7njnyyWE1-oD-VbZJ-Z8tELwvDf69rM7Iezs4rbnw,1774
|
|
6
6
|
insightconnect_plugin_runtime/exceptions.py,sha256=Pvcdkx81o6qC2qU661x-DzNjuIMP82x52nPMSEqEo4s,8491
|
|
7
|
-
insightconnect_plugin_runtime/helper.py,sha256=
|
|
7
|
+
insightconnect_plugin_runtime/helper.py,sha256=B0XqAXmn8CT1KQ6i5IoWLQrQ_HVOvuKrIKFuj_npQ-g,33770
|
|
8
8
|
insightconnect_plugin_runtime/metrics.py,sha256=hf_Aoufip_s4k4o8Gtzz90ymZthkaT2e5sXh5B4LcF0,3186
|
|
9
9
|
insightconnect_plugin_runtime/plugin.py,sha256=Yf4LNczykDVc31F9G8uuJ9gxEsgmxmAr0n4pcZzichM,26393
|
|
10
|
-
insightconnect_plugin_runtime/schema.py,sha256=
|
|
10
|
+
insightconnect_plugin_runtime/schema.py,sha256=6MVw5hqGATU1VLgwfOWfPsP3hy1OnsugCTsgX8sknes,521
|
|
11
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
|
|
@@ -15,7 +15,7 @@ insightconnect_plugin_runtime/trigger.py,sha256=Zq3cy68N3QxAGbNZKCID6CZF05Zi7YD2
|
|
|
15
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=8QQrxzW8jmQIkalud8fqYwB05uUw8sTiDNgO5ZekOCA,33353
|
|
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
21
|
insightconnect_plugin_runtime/clients/aws_client.py,sha256=bgSK3Txr1YonDiUN5JFEZ5cAyCXFM14JjfMus6lRV8o,23017
|
|
@@ -68,7 +68,7 @@ tests/unit/test_aws_action.py,sha256=pBE23Qn4aXKJqPmwiHMcEU5zPdyvbKO-eK-6jUlrsQw
|
|
|
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=Y4F-ij8WkEJkUU3mPvxlEchqE9NCdxDvR8bJzPVVNao,5328
|
|
71
|
-
tests/unit/test_helpers.py,sha256=
|
|
71
|
+
tests/unit/test_helpers.py,sha256=ym1tFi1VSKmdPaHEAlMEl1S7Ibu9-LrqZ2oqJv7bfbE,18685
|
|
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-6.2.
|
|
82
|
-
insightconnect_plugin_runtime-6.2.
|
|
83
|
-
insightconnect_plugin_runtime-6.2.
|
|
84
|
-
insightconnect_plugin_runtime-6.2.
|
|
80
|
+
tests/unit/utils.py,sha256=hcY0A2H_DMgCDXUTvDtCXMdMvRjLQgTaGcTpATb8YG0,2236
|
|
81
|
+
insightconnect_plugin_runtime-6.2.4.dist-info/METADATA,sha256=zprsS3dKV5_7yPthb5aUPyXTAItb-fJt-8NQn9M98bA,15719
|
|
82
|
+
insightconnect_plugin_runtime-6.2.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
83
|
+
insightconnect_plugin_runtime-6.2.4.dist-info/top_level.txt,sha256=AJtyJOpiFzHxsbHUICTcUKXyrGQ3tZxhrEHsPjJBvEA,36
|
|
84
|
+
insightconnect_plugin_runtime-6.2.4.dist-info/RECORD,,
|
tests/unit/test_helpers.py
CHANGED
|
@@ -4,6 +4,7 @@ from insightconnect_plugin_runtime.exceptions import (
|
|
|
4
4
|
HTTPStatusCodes,
|
|
5
5
|
ResponseExceptionData,
|
|
6
6
|
PluginException,
|
|
7
|
+
APIException
|
|
7
8
|
)
|
|
8
9
|
import requests
|
|
9
10
|
import os
|
|
@@ -378,16 +379,39 @@ class TestRequestsHelpers(TestCase):
|
|
|
378
379
|
json={"sample": "value"},
|
|
379
380
|
headers={"Content-Type": "application/json"},
|
|
380
381
|
)
|
|
381
|
-
response = helper.make_request(_request=request)
|
|
382
|
+
response = helper.make_request(_request=request, stream=True, max_response_size=100)
|
|
382
383
|
expected = {
|
|
383
384
|
"json": {"example": "sample"},
|
|
384
385
|
"status_code": 200,
|
|
385
386
|
"content": b"example",
|
|
386
387
|
"url": "https://example.com/success",
|
|
388
|
+
"content-length": "1",
|
|
387
389
|
}
|
|
388
390
|
self.assertEqual(response.content, expected.get("content"))
|
|
389
391
|
self.assertEqual(response.status_code, expected.get("status_code"))
|
|
390
392
|
self.assertEqual(response.url, expected.get("url"))
|
|
393
|
+
self.assertEqual(response.headers.get("content-length"), expected.get("content-length"))
|
|
394
|
+
|
|
395
|
+
@patch("requests.Session.send")
|
|
396
|
+
def test_make_request_enforces_max_response_size(self, mocked_request):
|
|
397
|
+
returned_max_size, test_max_size = "5000", 1527
|
|
398
|
+
|
|
399
|
+
response = requests.Response()
|
|
400
|
+
response.headers = {"content-length": returned_max_size, "content-type": "application/json"}
|
|
401
|
+
mocked_request.return_value = response
|
|
402
|
+
|
|
403
|
+
test_request = requests.Request(method="GET", url="https://event_source.com/api/v1/logs")
|
|
404
|
+
|
|
405
|
+
with self.assertRaises(APIException) as api_err:
|
|
406
|
+
helper.make_request(_request=test_request, stream=True, max_response_size=test_max_size)
|
|
407
|
+
|
|
408
|
+
self.assertEqual(400, api_err.exception.status_code)
|
|
409
|
+
exp_err_cause = f"API response is exceeding allowed limit of {test_max_size} bytes."
|
|
410
|
+
exp_err_data = f"Content length returned was {returned_max_size} and max allowed is {test_max_size}"
|
|
411
|
+
self.assertEqual(400, api_err.exception.status_code)
|
|
412
|
+
self.assertEqual(exp_err_cause, api_err.exception.cause)
|
|
413
|
+
self.assertEqual(exp_err_data, api_err.exception.data)
|
|
414
|
+
|
|
391
415
|
|
|
392
416
|
@parameterized.expand(
|
|
393
417
|
[
|
|
@@ -510,20 +534,46 @@ class TestRequestsHelpers(TestCase):
|
|
|
510
534
|
)
|
|
511
535
|
|
|
512
536
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
537
|
+
class TestHashing(TestCase):
|
|
538
|
+
def setUp(self) -> None:
|
|
539
|
+
self.log = {"example": "value", "sample": "value"}
|
|
516
540
|
|
|
541
|
+
def test_hash_sha1_no_keys(self):
|
|
542
|
+
# Test hash with no keys provided
|
|
543
|
+
expected_hash = "2e1ccc1a95e9b2044f13546c25fe380bbd039293"
|
|
544
|
+
self.assertEqual(helper.hash_sha1(self.log), expected_hash)
|
|
517
545
|
|
|
518
|
-
def
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
546
|
+
def test_hash_sha1_keys(self):
|
|
547
|
+
# Test hash with valid key provided
|
|
548
|
+
expected_hash = "61c908e52d66a763ceed0798b8e5f4b7f0328a21"
|
|
549
|
+
self.assertEqual(helper.hash_sha1(self.log, keys=["example"]), expected_hash)
|
|
550
|
+
|
|
551
|
+
def test_hash_sha1_keys_wrong_type(self):
|
|
552
|
+
# Test hash with wrong type for keys
|
|
553
|
+
with self.assertRaises(TypeError) as context:
|
|
554
|
+
helper.hash_sha1(self.log, keys="test")
|
|
555
|
+
|
|
556
|
+
self.assertEqual(
|
|
557
|
+
str(context.exception),
|
|
558
|
+
"The 'keys' parameter must be a list or None in the 'hash_sha1' function, not str"
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
def test_hash_sha1_keys_not_found(self):
|
|
562
|
+
# Test hash with key not found
|
|
563
|
+
with self.assertRaises(KeyError) as context:
|
|
564
|
+
helper.hash_sha1(self.log, keys=["example", "test"])
|
|
565
|
+
|
|
566
|
+
self.assertEqual(str(context.exception), "\"Key 'test' not found in the provided log.\"")
|
|
567
|
+
|
|
568
|
+
def test_compare_and_dedupe_hashes(self):
|
|
569
|
+
hashes = ["2e1ccc1a95e9b2044f13546c25fe380bbd039293"]
|
|
570
|
+
logs = [
|
|
571
|
+
{
|
|
572
|
+
"example": "value",
|
|
573
|
+
"sample": "value",
|
|
574
|
+
},
|
|
575
|
+
{"specimen": "new_value"},
|
|
576
|
+
]
|
|
577
|
+
assert [{"specimen": "new_value"}], [
|
|
578
|
+
"ad6ae80c0356e02b1561cb58408ee678eb1070bb"
|
|
579
|
+
] == helper.compare_and_dedupe_hashes(hashes, logs)
|
tests/unit/utils.py
CHANGED
|
File without changes
|