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.
@@ -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
- from flask import jsonify, request, abort, make_response, Blueprint
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 = pkg_resources.require("insightconnect-plugin-runtime")[0].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
- def hash_sha1(log: Dict) -> str:
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
- :type Dict:
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
- for key, value in log.items():
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
- logging.info(
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(response, exception_custom_configs, exception_data_location, allowed_status_codes)
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
- logging.info(f"Request to {response.url} failed. Status code: {status_code}")
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
- convert_dict_to_snake_case(element)
445
- if isinstance(element, (dict, list))
446
- else element
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): convert_dict_to_snake_case(value)
451
- if isinstance(value, (dict, list))
452
- else value
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
- logging.info(
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
- logging.error("CheckHashes: Argument must be a string")
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
- logging.info("CheckHashes: No checksum match")
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
- logging.info("CheckCacheFile: File %s exists", cache_file)
679
+ logger.info("CheckCacheFile: File %s exists", cache_file)
624
680
  return True
625
- logging.info("CheckCacheFile: File %s did not exist", cache_file)
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
- logging.info("OpenFile: File %s is not a file or does not exist ", filename)
697
+ logger.info("OpenFile: File %s is not a file or does not exist ", filename)
642
698
  else:
643
- logging.error(
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
- logging.info("OpenCacheFile: %s exists, returning it", cache_file)
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
- logging.info("OpenCacheFile: %s created", cache_file)
719
+ logger.info("OpenCacheFile: %s created", cache_file)
664
720
  f = open(cache_file, "a+" if append else "r+")
665
721
  return f
666
- logging.error("OpenCacheFile: %s directory or does not exist", cache_dir)
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
- logging.info("RemoveCacheFile: Cache file %s did not exist", cache_file)
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
- logging.info("Cache lock %s created", lock_file)
754
+ logger.info("Cache lock %s created", lock_file)
699
755
  return True
700
- logging.info("Cache lock %s failed, lock not created", lock_file)
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
- logging.info("Cache unlock %s failed, lock not released", lock_file)
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
- logging.error("HTTPError: %s for %s", str(e.code), url)
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
- logging.error("URLError: %s for %s", str(e.reason), url)
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
- logging.error(
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
- logging.error("Requests: Timeout for %s", url)
843
+ logger.error("Requests: Timeout for %s", url)
788
844
  except requests.exceptions.TooManyRedirects:
789
- logging.error("Requests: TooManyRedirects for %s", url)
845
+ logger.error("Requests: TooManyRedirects for %s", url)
790
846
  except requests.ConnectionError:
791
- logging.error("Requests: ConnectionError for %s", url)
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
- logging.error(
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
- logging.error("EncodeFile: Failed to open file: %s", e.strerror)
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
- logging.error(
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
- logging.error("Requests: Timeout for %s", url)
922
+ logger.error("Requests: Timeout for %s", url)
867
923
  except requests.exceptions.TooManyRedirects:
868
- logging.error("Requests: TooManyRedirects for %s", url)
924
+ logger.error("Requests: TooManyRedirects for %s", url)
869
925
  except requests.ConnectionError:
870
- logging.error("Requests: ConnectionError for %s", url)
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
- logging.error("Range: IndexError: URL basename is short: %s of %s", name, url)
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
- logging.error("Requests: MissingSchema: Requires ftp|http(s):// for %s", url)
972
+ logger.error("Requests: MissingSchema: Requires ftp|http(s):// for %s", url)
917
973
  except requests.exceptions.HTTPError:
918
- logging.error(
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
- logging.error("Requests: Timeout for %s", url)
980
+ logger.error("Requests: Timeout for %s", url)
925
981
  except requests.exceptions.TooManyRedirects:
926
- logging.error("Requests: TooManyRedirects for %s", url)
982
+ logger.error("Requests: TooManyRedirects for %s", url)
927
983
  except requests.ConnectionError:
928
- logging.error("Requests: ConnectionError for %s", url)
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
- schema = pkg_resources.resource_stream(__name__, "/".join(("data", file_name)))
14
- schema = schema.read()
15
- if isinstance(schema, bytes):
16
- schema = schema.decode("utf-8")
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
1
+ Metadata-Version: 2.2
2
2
  Name: insightconnect-plugin-runtime
3
- Version: 6.2.0
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.2
16
- Requires-Dist: python-jsonschema-objects==0.5.2
17
- Requires-Dist: jsonschema==4.21.1
18
- Requires-Dist: certifi==2024.07.04
19
- Requires-Dist: Flask==3.0.2
20
- Requires-Dist: gunicorn==22.0.0
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.10.1
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.7.0
27
- Requires-Dist: structlog==24.1.0
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 ![Build Status](https://github.com/rapid7/komand-plugin-sdk-python/workflows/Continuous%20Integration/badge.svg)
@@ -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.10 as of version 6.2.0. The following dependencies will need
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.10
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 5.2.2
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 19.7.1
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.10 | 3.11.10 |
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=X23SRP1bJksAfIWTmRZ4JQTZ8nhfcr1NUIsTvvR8tdM,31549
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=jTNc6KAMqFpaDVWrAYhkVC6e8I63P3X7uVlJkAr1hiY,583
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=gIxuxO7AFqmic0k9b_2gWjjJ8ICtWDa88X4Tl4fEgik,33333
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=_1jlU-h-GNY9MLQ8nV_or4zkTI911AmHKtvFyamKjfU,16297
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=VooVmfpIgxmglNdtmT32AkEDFxHxyRHLK8RsCWjjYRY,2153
81
- insightconnect_plugin_runtime-6.2.0.dist-info/METADATA,sha256=omcme5-sOih-Wl98UPLsGBnFRRQNK_fLNC3x7RvSQNk,15034
82
- insightconnect_plugin_runtime-6.2.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
83
- insightconnect_plugin_runtime-6.2.0.dist-info/top_level.txt,sha256=AJtyJOpiFzHxsbHUICTcUKXyrGQ3tZxhrEHsPjJBvEA,36
84
- insightconnect_plugin_runtime-6.2.0.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.3.0)
2
+ Generator: setuptools (75.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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
- def test_hash_sha1():
514
- log = {"example": "value", "sample": "value"}
515
- assert "2e1ccc1a95e9b2044f13546c25fe380bbd039293" == helper.hash_sha1(log)
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 test_compare_and_dedupe_hashes():
519
- hashes = ["2e1ccc1a95e9b2044f13546c25fe380bbd039293"]
520
- logs = [
521
- {
522
- "example": "value",
523
- "sample": "value",
524
- },
525
- {"specimen": "new_value"},
526
- ]
527
- assert [{"specimen": "new_value"}], [
528
- "ad6ae80c0356e02b1561cb58408ee678eb1070bb"
529
- ] == helper.compare_and_dedupe_hashes(hashes, logs)
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
@@ -34,6 +34,7 @@ def get_mock_response(
34
34
  response._content = bytes_string
35
35
  response.url = url
36
36
  response.reason = reason
37
+ response.headers = {"content-length": "1", "content-type": "application/json"}
37
38
 
38
39
  def return_json():
39
40
  return data