intelmq-extensions 1.8.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.
Files changed (100) hide show
  1. intelmq_extensions/__init__.py +0 -0
  2. intelmq_extensions/bots/__init__.py +0 -0
  3. intelmq_extensions/bots/collectors/blackkite/__init__.py +0 -0
  4. intelmq_extensions/bots/collectors/blackkite/_client.py +167 -0
  5. intelmq_extensions/bots/collectors/blackkite/collector.py +182 -0
  6. intelmq_extensions/bots/collectors/disp/__init__.py +0 -0
  7. intelmq_extensions/bots/collectors/disp/_client.py +121 -0
  8. intelmq_extensions/bots/collectors/disp/collector.py +104 -0
  9. intelmq_extensions/bots/collectors/xmpp/__init__.py +0 -0
  10. intelmq_extensions/bots/collectors/xmpp/collector.py +210 -0
  11. intelmq_extensions/bots/experts/__init__.py +0 -0
  12. intelmq_extensions/bots/experts/certat_contact_intern/__init__.py +0 -0
  13. intelmq_extensions/bots/experts/certat_contact_intern/expert.py +139 -0
  14. intelmq_extensions/bots/experts/copy_extra/__init__.py +0 -0
  15. intelmq_extensions/bots/experts/copy_extra/expert.py +27 -0
  16. intelmq_extensions/bots/experts/event_group_splitter/__init__.py +0 -0
  17. intelmq_extensions/bots/experts/event_group_splitter/expert.py +117 -0
  18. intelmq_extensions/bots/experts/event_splitter/__init__.py +0 -0
  19. intelmq_extensions/bots/experts/event_splitter/expert.py +41 -0
  20. intelmq_extensions/bots/experts/squelcher/__init__.py +0 -0
  21. intelmq_extensions/bots/experts/squelcher/expert.py +316 -0
  22. intelmq_extensions/bots/experts/vulnerability_lookup/__init__.py +0 -0
  23. intelmq_extensions/bots/experts/vulnerability_lookup/expert.py +136 -0
  24. intelmq_extensions/bots/outputs/__init__.py +0 -0
  25. intelmq_extensions/bots/outputs/mattermost/__init__.py +0 -0
  26. intelmq_extensions/bots/outputs/mattermost/output.py +113 -0
  27. intelmq_extensions/bots/outputs/to_logs/__init__.py +0 -0
  28. intelmq_extensions/bots/outputs/to_logs/output.py +12 -0
  29. intelmq_extensions/bots/outputs/xmpp/__init__.py +0 -0
  30. intelmq_extensions/bots/outputs/xmpp/output.py +180 -0
  31. intelmq_extensions/bots/parsers/__init__.py +0 -0
  32. intelmq_extensions/bots/parsers/blackkite/__init__.py +0 -0
  33. intelmq_extensions/bots/parsers/blackkite/_transformers.py +202 -0
  34. intelmq_extensions/bots/parsers/blackkite/parser.py +65 -0
  35. intelmq_extensions/bots/parsers/disp/__init__.py +0 -0
  36. intelmq_extensions/bots/parsers/disp/parser.py +125 -0
  37. intelmq_extensions/bots/parsers/malwaredomains/__init__.py +0 -0
  38. intelmq_extensions/bots/parsers/malwaredomains/parser.py +63 -0
  39. intelmq_extensions/cli/__init__.py +0 -0
  40. intelmq_extensions/cli/create_reports.py +161 -0
  41. intelmq_extensions/cli/intelmqcli.py +657 -0
  42. intelmq_extensions/cli/lib.py +670 -0
  43. intelmq_extensions/cli/utils.py +12 -0
  44. intelmq_extensions/etc/harmonization.conf +434 -0
  45. intelmq_extensions/etc/squelcher.conf +52 -0
  46. intelmq_extensions/lib/__init__.py +0 -0
  47. intelmq_extensions/lib/api_helpers.py +105 -0
  48. intelmq_extensions/lib/blackkite.py +29 -0
  49. intelmq_extensions/tests/__init__.py +0 -0
  50. intelmq_extensions/tests/base.py +336 -0
  51. intelmq_extensions/tests/bots/__init__.py +0 -0
  52. intelmq_extensions/tests/bots/collectors/__init__.py +0 -0
  53. intelmq_extensions/tests/bots/collectors/blackkite/__init__.py +0 -0
  54. intelmq_extensions/tests/bots/collectors/blackkite/base.py +45 -0
  55. intelmq_extensions/tests/bots/collectors/blackkite/test_client.py +154 -0
  56. intelmq_extensions/tests/bots/collectors/blackkite/test_collector.py +287 -0
  57. intelmq_extensions/tests/bots/collectors/disp/__init__.py +0 -0
  58. intelmq_extensions/tests/bots/collectors/disp/base.py +147 -0
  59. intelmq_extensions/tests/bots/collectors/disp/test_client.py +134 -0
  60. intelmq_extensions/tests/bots/collectors/disp/test_collector.py +137 -0
  61. intelmq_extensions/tests/bots/collectors/xmpp/__init__.py +0 -0
  62. intelmq_extensions/tests/bots/collectors/xmpp/test_collector.py +10 -0
  63. intelmq_extensions/tests/bots/experts/__init__.py +0 -0
  64. intelmq_extensions/tests/bots/experts/certat_contact_intern/__init__.py +0 -0
  65. intelmq_extensions/tests/bots/experts/certat_contact_intern/test_expert.py +176 -0
  66. intelmq_extensions/tests/bots/experts/copy_extra/__init__.py +0 -0
  67. intelmq_extensions/tests/bots/experts/copy_extra/test_expert.py +42 -0
  68. intelmq_extensions/tests/bots/experts/event_group_splitter/__init__.py +0 -0
  69. intelmq_extensions/tests/bots/experts/event_group_splitter/test_expert.py +302 -0
  70. intelmq_extensions/tests/bots/experts/event_splitter/__init__.py +0 -0
  71. intelmq_extensions/tests/bots/experts/event_splitter/test_expert.py +101 -0
  72. intelmq_extensions/tests/bots/experts/squelcher/__init__.py +0 -0
  73. intelmq_extensions/tests/bots/experts/squelcher/test_expert.py +548 -0
  74. intelmq_extensions/tests/bots/experts/vulnerability_lookup/__init__.py +0 -0
  75. intelmq_extensions/tests/bots/experts/vulnerability_lookup/test_expert.py +203 -0
  76. intelmq_extensions/tests/bots/outputs/__init__.py +0 -0
  77. intelmq_extensions/tests/bots/outputs/mattermost/__init__.py +0 -0
  78. intelmq_extensions/tests/bots/outputs/mattermost/test_output.py +138 -0
  79. intelmq_extensions/tests/bots/outputs/xmpp/__init__.py +0 -0
  80. intelmq_extensions/tests/bots/outputs/xmpp/test_output.py +10 -0
  81. intelmq_extensions/tests/bots/parsers/__init__.py +0 -0
  82. intelmq_extensions/tests/bots/parsers/blackkite/__init__.py +0 -0
  83. intelmq_extensions/tests/bots/parsers/blackkite/data.py +69 -0
  84. intelmq_extensions/tests/bots/parsers/blackkite/test_parser.py +197 -0
  85. intelmq_extensions/tests/bots/parsers/disp/__init__.py +0 -0
  86. intelmq_extensions/tests/bots/parsers/disp/test_parser.py +282 -0
  87. intelmq_extensions/tests/bots/parsers/malwaredomains/__init__.py +0 -0
  88. intelmq_extensions/tests/bots/parsers/malwaredomains/test_parser.py +62 -0
  89. intelmq_extensions/tests/cli/__init__.py +0 -0
  90. intelmq_extensions/tests/cli/test_create_reports.py +97 -0
  91. intelmq_extensions/tests/cli/test_intelmqcli.py +158 -0
  92. intelmq_extensions/tests/lib/__init__.py +0 -0
  93. intelmq_extensions/tests/lib/base.py +81 -0
  94. intelmq_extensions/tests/lib/test_api_helpers.py +126 -0
  95. intelmq_extensions-1.8.1.dist-info/METADATA +60 -0
  96. intelmq_extensions-1.8.1.dist-info/RECORD +100 -0
  97. intelmq_extensions-1.8.1.dist-info/WHEEL +5 -0
  98. intelmq_extensions-1.8.1.dist-info/entry_points.txt +33 -0
  99. intelmq_extensions-1.8.1.dist-info/licenses/LICENSE +661 -0
  100. intelmq_extensions-1.8.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,336 @@
1
+ import contextlib
2
+ import copy
3
+ import io
4
+ import json
5
+ import os
6
+ import pathlib
7
+ import shutil
8
+ import sys
9
+ import tempfile
10
+ import unittest
11
+ from unittest import mock
12
+
13
+ import intelmq.lib.message as message
14
+ import intelmq.lib.test as test
15
+ import intelmq.lib.utils as utils
16
+ import pkg_resources
17
+ import psycopg2
18
+ from intelmq import CONFIG_DIR, RUNTIME_CONF_FILE
19
+ from intelmq.lib.harmonization import DateTime
20
+ from psycopg2 import sql
21
+ from rt import Rt
22
+
23
+ from intelmq_extensions.cli import lib
24
+
25
+ from ..cli.utils import merge_harmonization
26
+
27
+ ADDITIONAL_HARMONIZATION = ["contrib/constituency.harmonization.part.json"]
28
+ # DB_FIELDS = [] # selection of fields to save in DB when using test entries creation
29
+
30
+ POSTGRES_CONFIG = {
31
+ "host": os.getenv("INTELMQ_TEST_DATABASE_HOST", "localhost"),
32
+ "port": os.getenv("INTELMQ_TEST_DATABASE_PORT", 5432),
33
+ "database": "intelmq",
34
+ "user": "intelmq",
35
+ "password": "intelmq",
36
+ "sslmode": "allow",
37
+ "text_table": "boilerplates_tests", # TODO: move to tables config
38
+ }
39
+
40
+
41
+ def mocked_config_from_extensions(
42
+ bot_id="test-bot", sysconfig={}, group=None, module=None
43
+ ):
44
+ """The only one difference with original is the package used to load resources"""
45
+
46
+ def mocked(conf_file):
47
+ if conf_file == RUNTIME_CONF_FILE:
48
+ return {
49
+ bot_id: {
50
+ "description": "Instance of a bot for automated unit tests.",
51
+ "group": group,
52
+ "module": module,
53
+ "name": "Test Bot",
54
+ "parameters": sysconfig,
55
+ }
56
+ }
57
+ elif conf_file.startswith(CONFIG_DIR):
58
+ confname = os.path.join("etc/", os.path.split(conf_file)[-1])
59
+ fname = pkg_resources.resource_filename("intelmq_extensions", confname)
60
+ with open(fname) as fpconfig:
61
+ return json.load(fpconfig)
62
+ else:
63
+ return utils.load_configuration(conf_file)
64
+
65
+ return mocked
66
+
67
+
68
+ test.mocked_config = mocked_config_from_extensions
69
+
70
+
71
+ def load_harmonization():
72
+ harmonization = pkg_resources.resource_filename(
73
+ "intelmq_extensions", "etc/harmonization.conf"
74
+ )
75
+ extensions = []
76
+ for file in ADDITIONAL_HARMONIZATION:
77
+ with open(pathlib.Path(__file__).parent.parent.parent / file) as f:
78
+ extensions.append(json.load(f))
79
+ return merge_harmonization(extensions, harmonization)
80
+
81
+
82
+ class TestCaseMixin:
83
+ harmonization = load_harmonization()
84
+
85
+ TEST_EVENTS_TABLE = "tests"
86
+ TEST_TEXT_TABLE = "boilerplates_tests"
87
+
88
+ def __init__(self, *args, **kwargs) -> None:
89
+ super().__init__(*args, **kwargs)
90
+ self._tmp_dir = None
91
+
92
+ @property
93
+ def tmp_dir(self):
94
+ if not self._tmp_dir:
95
+ self._tmp_dir = tempfile.mkdtemp(prefix="intelmq_extensions_test_")
96
+ return self._tmp_dir
97
+
98
+ def tearDown(self):
99
+ if self._tmp_dir:
100
+ shutil.rmtree(self._tmp_dir)
101
+
102
+ super().tearDown()
103
+
104
+ def new_event(self):
105
+ return message.Event(harmonization=self.harmonization)
106
+
107
+ def get_mocked_logger(self, logger):
108
+ def log(name, *args, **kwargs):
109
+ logger.handlers = self.logger_handlers_backup
110
+ return logger
111
+
112
+ return log
113
+
114
+ @staticmethod
115
+ def connect_database(config: dict):
116
+ connection = psycopg2.connect(
117
+ database=config["database"],
118
+ user=config["user"],
119
+ password=config["password"],
120
+ host=config["host"],
121
+ port=config["port"],
122
+ sslmode=config["sslmode"],
123
+ )
124
+ connection.autocommit = True
125
+ return connection
126
+
127
+ def db_add_event(
128
+ self,
129
+ data: dict,
130
+ cc: str = "AT",
131
+ fqdn: str = "test.at",
132
+ notify: bool = True,
133
+ taxonomy: str = "test",
134
+ abuse_contact: str = "tes@test.at",
135
+ extra: dict = None,
136
+ ) -> int:
137
+ """Adds event to the test DB. Some common fields required by CLI have default values
138
+
139
+ data: any dict representing an event
140
+ """
141
+ extra = extra or dict()
142
+ event = message.Event(harmonization=self.harmonization)
143
+ event.add("source.geolocation.cc", cc)
144
+ event.add("source.fqdn", fqdn)
145
+ event.add("classification.taxonomy", taxonomy)
146
+ event.add("source.abuse_contact", abuse_contact)
147
+ event.add("notify", notify)
148
+ event.add("time.source", DateTime.generate_datetime_now())
149
+
150
+ event.update(data)
151
+
152
+ keys = list(event.keys()) + ["extra"]
153
+ values = list(event.values()) + [json.dumps(extra)]
154
+ inserted_id = self._db_insert(self.TEST_EVENTS_TABLE, keys, values)
155
+ self._clear_db_events.append(inserted_id)
156
+
157
+ for key, value in extra.items():
158
+ event.add(f"extra.{key}", value)
159
+
160
+ return inserted_id
161
+
162
+ def _db_insert(self, table: str, keys, values, return_key: str = "id"):
163
+ query = sql.SQL("INSERT INTO {} ({}) VALUES ({}) RETURNING {};").format(
164
+ sql.Identifier(table),
165
+ sql.SQL(",").join(map(sql.Identifier, keys)),
166
+ sql.SQL(",").join([sql.SQL("%s")] * len(keys)),
167
+ sql.Identifier(return_key),
168
+ )
169
+ self.cur.execute(query, list(values))
170
+ inserted_id = self.cur.fetchone()[return_key]
171
+
172
+ return inserted_id
173
+
174
+ def db_get_event(self, event_id: int):
175
+ query = sql.SQL("SELECT * from {} WHERE id=%s").format(
176
+ sql.Identifier(self.TEST_EVENTS_TABLE)
177
+ )
178
+ self.cur.execute(query, (event_id,))
179
+ return self.cur.fetchone()
180
+
181
+ def db_delete(self, table, ids: list, id_field: str = "id"):
182
+ query = sql.SQL("DELETE FROM {} WHERE {} = ANY(%s);").format(
183
+ sql.Identifier(table),
184
+ sql.Identifier(id_field),
185
+ )
186
+ self.cur.execute(query, (ids,))
187
+
188
+ def add_boilerplate(self, key: str, body: str):
189
+ self._db_insert(
190
+ self.TEST_TEXT_TABLE, ["key", "body"], [key, body], return_key="key"
191
+ )
192
+ self.addCleanup(
193
+ lambda: self.db_delete(self.TEST_TEXT_TABLE, [key], id_field="key")
194
+ )
195
+
196
+ def assertInLogs(self, msg_part):
197
+ self.log_stream: io.StringIO
198
+ self.log_stream.seek(0)
199
+ self.assertTrue(
200
+ any(msg_part in line for line in self.log_stream),
201
+ f"'{msg_part}' not found in captured logs",
202
+ )
203
+
204
+
205
+ class BotTestCase(TestCaseMixin, test.BotTestCase):
206
+ """Provides test class with additional changes required for extension bots"""
207
+
208
+
209
+ class CLITestCase(TestCaseMixin, unittest.TestCase):
210
+ """Provides helpers needed to test the intelmqcli"""
211
+
212
+ CLI_CONTROLLER: lib.IntelMQCLIContollerTemplate = None
213
+
214
+ @classmethod
215
+ def setUpClass(cls) -> None:
216
+ super().setUpClass()
217
+
218
+ cls.db_connection = cls.connect_database(POSTGRES_CONFIG)
219
+ cls.cur = cls.db_connection.cursor(
220
+ cursor_factory=psycopg2.extras.RealDictCursor
221
+ )
222
+
223
+ def setUp(self) -> None:
224
+ super().setUp()
225
+
226
+ self.config = {
227
+ "database": copy.deepcopy(POSTGRES_CONFIG),
228
+ "log_path": None, # Do not log tests to files
229
+ "log_level": "DEBUG",
230
+ "rt": {
231
+ "uri": "",
232
+ "user": None,
233
+ "password": None,
234
+ "incident_report_requestor": "test-intelmq",
235
+ "zip_threshold": 10000,
236
+ },
237
+ "filter": {"cc": "AT", "fqdn": ".at|.wien"},
238
+ "tables": {
239
+ "events": self.TEST_EVENTS_TABLE,
240
+ "v_events_filtered": self.TEST_EVENTS_TABLE,
241
+ "boilerplates": self.TEST_TEXT_TABLE,
242
+ },
243
+ }
244
+
245
+ self.log_stream = io.StringIO(newline="\n")
246
+ self.logger = utils.log(
247
+ self.id(),
248
+ log_path=False,
249
+ stream=self.log_stream,
250
+ log_format_stream=utils.LOG_FORMAT,
251
+ log_level=self.config["log_level"],
252
+ )
253
+ self.logger_handlers_backup = self.logger.handlers
254
+
255
+ self._clear_db_events = []
256
+ self.addCleanup(
257
+ lambda: self.db_delete(self.TEST_EVENTS_TABLE, self._clear_db_events)
258
+ )
259
+
260
+ self._prepare_rt_mock()
261
+ rt_patch = mock.patch(
262
+ "intelmq_extensions.cli.lib.rt.Rt", return_value=self.rt_mock
263
+ )
264
+ rt_patch.start()
265
+ self.addCleanup(rt_patch.stop)
266
+
267
+ def _prepare_rt_mock(self):
268
+ self.rt_mock = mock.Mock(spec_set=Rt(""))
269
+ self._rt_ticket_id = 0
270
+ self._rt_comment_id = 0
271
+
272
+ def _assign_id(id_name: str):
273
+ current = getattr(self, id_name)
274
+ setattr(self, id_name, current + 1)
275
+ return current + 1
276
+
277
+ self.rt_mock.create_ticket = mock.Mock(
278
+ spec=Rt.create_ticket,
279
+ side_effect=lambda *_, **__: _assign_id("_rt_ticket_id"),
280
+ )
281
+ self.rt_mock.comment = mock.Mock(
282
+ spec=Rt.comment, side_effect=lambda *_, **__: _assign_id("_rt_comment_id")
283
+ )
284
+
285
+ def assertRTTicketCount(self, count: int):
286
+ self.assertEqual(self._rt_ticket_id, count)
287
+
288
+ def get_rt_attachment(self, investigation_id):
289
+ for call in self.rt_mock.reply.call_args_list:
290
+ if call.args[0] == investigation_id:
291
+ files = call.kwargs.get("files", [])
292
+ return files[0] if files else None
293
+ return None
294
+
295
+ def get_rt_text(self, investigation_id):
296
+ for call in self.rt_mock.reply.call_args_list:
297
+ if call.args[0] == investigation_id:
298
+ return call.kwargs.get("text", "")
299
+ return None
300
+
301
+ def _print_on_failure(self):
302
+ if sys.version_info >= (3, 11):
303
+ # Python 3.11 removed fields used here
304
+ return
305
+ # Based on: https://gist.github.com/hynekcer/1b0a260ef72dae05fe9611904d7b9675
306
+ if hasattr(self._outcome, "errors"):
307
+ result = self.defaultTestResult()
308
+ self._feedErrorsToResult(result, self._outcome.errors)
309
+ else:
310
+ result = self._outcome.result
311
+
312
+ passed = all(test != self for test, _ in result.errors + result.failures)
313
+ if not passed:
314
+ for captured in ["stderr", "stdout"]:
315
+ if data := getattr(self, captured, None):
316
+ print(f"{'+' * 10} CLI {captured} {'+' * 10}")
317
+ print(*data if isinstance(data, list) else (data.getvalue(),))
318
+
319
+ def tearDown(self) -> None:
320
+ self._print_on_failure()
321
+
322
+ super().tearDown()
323
+
324
+ def run_cli(self, args: list, expect_code=0):
325
+ with contextlib.redirect_stderr(io.StringIO()) as f_stderr:
326
+ with contextlib.redirect_stdout(io.StringIO()) as f_stdout:
327
+ with unittest.mock.patch(
328
+ "intelmq.lib.utils.log", self.get_mocked_logger(self.logger)
329
+ ):
330
+ code = self.CLI_CONTROLLER(overridden_config=self.config).run(args)
331
+
332
+ f_stdout.seek(0)
333
+ self.stdout = f_stdout.readlines()
334
+ f_stderr.seek(0)
335
+ self.stderr = f_stderr.readlines()
336
+ self.assertEqual(expect_code, int(code))
File without changes
File without changes
@@ -0,0 +1,45 @@
1
+ """Base for BlackKite tests
2
+
3
+ SPDX-FileCopyrightText: 2023 CERT.at GmbH <https://cert.at/>
4
+ SPDX-License-Identifier: AGPL-3.0-or-later
5
+ """
6
+
7
+ import requests
8
+ from requests_mock import MockerCore
9
+
10
+ from ....lib.base import OAuthAccess_APIMockMixIn
11
+
12
+
13
+ class BlackKite_APIMockMixIn(OAuthAccess_APIMockMixIn):
14
+ def setup_api_mock(
15
+ self,
16
+ url: str = "https://blackkite.example.at/v1",
17
+ client_id: str = "client1",
18
+ client_secret: str = "secret1",
19
+ session: requests.Session = None,
20
+ ):
21
+ self._url = url
22
+ self.session = session
23
+ self.requests = MockerCore(session=self.session)
24
+ self.requests.start()
25
+ self.addCleanup(self.requests.stop)
26
+
27
+ self.setup_oauth_mock(
28
+ oauth_clientid=client_id,
29
+ oauth_clientsecret=client_secret,
30
+ oauth_url=f"{url}/oauth/token",
31
+ session=self.session,
32
+ )
33
+
34
+ def mock_request(
35
+ self, path: str, mocker: MockerCore = None, method: str = "get", **kwargs
36
+ ):
37
+ mocker = mocker or self.requests
38
+ mocking_method = getattr(mocker, method)
39
+ mocking_method(
40
+ f"{self._url}/{path}",
41
+ request_headers={
42
+ "Authorization": f"Bearer {self.mocked_access_token}",
43
+ },
44
+ **kwargs,
45
+ )
@@ -0,0 +1,154 @@
1
+ """Testing BlackKite API client
2
+
3
+ SPDX-FileCopyrightText: 2023 CERT.at GmbH <https://cert.at/>
4
+ SPDX-License-Identifier: AGPL-3.0-or-later
5
+ """
6
+
7
+ from unittest import TestCase
8
+
9
+ import requests
10
+
11
+ from intelmq_extensions.bots.collectors.blackkite._client import (
12
+ BlackKiteClient,
13
+ Output,
14
+ Severity,
15
+ Status,
16
+ )
17
+
18
+ from .....lib.blackkite import Category
19
+ from .base import BlackKite_APIMockMixIn
20
+
21
+
22
+ class BlackKiteClientTestCase(BlackKite_APIMockMixIn, TestCase):
23
+ def setUp(self) -> None:
24
+ super().setUp()
25
+ self.config = {
26
+ "url": "https://blackkite.example.at/v1",
27
+ "client_id": "client1",
28
+ "client_secret": "secret1",
29
+ }
30
+ self.setup_api_mock(
31
+ **self.config,
32
+ session=requests.Session(),
33
+ )
34
+
35
+ self.client = BlackKiteClient(session=self.session, page_size=10, **self.config)
36
+
37
+ def test_get_paginated_response_1_page(self):
38
+ self.mock_request(
39
+ "companies/?page_number=1&page_size=10",
40
+ json=[{"CompanyId": 1}, {"CompanyId": 2}],
41
+ headers={"X-Total-Items": "2"},
42
+ )
43
+
44
+ responses = list(self.client.get_paginated("companies/"))
45
+ self.assertEqual([{"CompanyId": 1}, {"CompanyId": 2}], responses)
46
+
47
+ def test_get_paginated_response_3_pages(self):
48
+ self.mock_request(
49
+ "incident/?page_number=1&page_size=10",
50
+ json=[{"CompanyId": 1}, {"CompanyId": 2}],
51
+ headers={"X-Total-Items": "23"},
52
+ )
53
+ self.mock_request(
54
+ "incident/?page_number=2&page_size=10",
55
+ json=[{"CompanyId": 3}, {"CompanyId": 4}],
56
+ headers={"X-Total-Items": "23"},
57
+ )
58
+ self.mock_request(
59
+ "incident/?page_number=3&page_size=10",
60
+ json=[{"CompanyId": 5}],
61
+ headers={"X-Total-Items": "23"},
62
+ )
63
+
64
+ responses = list(self.client.get_paginated("incident/"))
65
+ self.assertEqual(
66
+ [
67
+ {"CompanyId": 1},
68
+ {"CompanyId": 2},
69
+ {"CompanyId": 3},
70
+ {"CompanyId": 4},
71
+ {"CompanyId": 5},
72
+ ],
73
+ responses,
74
+ )
75
+
76
+ def test_get_status(self):
77
+ self.mock_request("status", json={"IsValid": True})
78
+
79
+ self.assertEqual({"IsValid": True}, self.client.status())
80
+
81
+ def test_get_companies(self):
82
+ self.mock_request(
83
+ "companies?page_number=1&page_size=10",
84
+ json=[{"CompanyId": 1}, {"CompanyId": 2}],
85
+ headers={"X-Total-Items": "2"},
86
+ )
87
+
88
+ responses = list(self.client.companies())
89
+ self.assertEqual([{"CompanyId": 1}, {"CompanyId": 2}], responses)
90
+
91
+ def test_list_findings_default(self):
92
+ self.mock_request(
93
+ (
94
+ "companies/1/findings/patchmanagement?page_number=1&page_size=10"
95
+ "&status=Active&severity=Critical"
96
+ ),
97
+ headers={"X-Total-Items": "2"},
98
+ json=[{"FindingId": 1}, {"FindingId": 2}],
99
+ )
100
+
101
+ result = list(self.client.list_findings("patchmanagement", company_id=1))
102
+ self.assertEqual([{"FindingId": 1}, {"FindingId": 2}], result)
103
+
104
+ def test_list_findings_custom_filters(self):
105
+ self.mock_request(
106
+ (
107
+ "companies/1/findings/dnshealth?page_number=1&page_size=10"
108
+ "&status=Active,Deleted&severity=Critical,High&output=Failed,Warning,Passed"
109
+ ),
110
+ headers={"X-Total-Items": "2"},
111
+ json=[{"FindingId": 1}, {"FindingId": 2}],
112
+ )
113
+
114
+ result = list(
115
+ self.client.list_findings(
116
+ "dnshealth",
117
+ company_id=1,
118
+ statuses=[Status.ACTIVE, Status.DELETED],
119
+ severities=[Severity.CRITICAL, Severity.HIGH],
120
+ outputs=[Output.FAILED, Output.WARNING, Output.PASSED],
121
+ )
122
+ )
123
+ self.assertEqual([{"FindingId": 1}, {"FindingId": 2}], result)
124
+
125
+ def test_get_category_findings_ignore_output_when_not_supported(self):
126
+ self.mock_request(
127
+ (
128
+ "companies/1/findings/patchmanagement?page_number=1&page_size=10"
129
+ "&status=Active,Deleted&severity=Critical,High"
130
+ ),
131
+ headers={"X-Total-Items": "2"},
132
+ json=[{"FindingId": 1}, {"FindingId": 2}],
133
+ complete_qs=True,
134
+ )
135
+
136
+ result = list(
137
+ self.client.get_findings_from_category(
138
+ Category.PatchManagement,
139
+ company_id=1,
140
+ statuses=[Status.ACTIVE, Status.DELETED],
141
+ severities=[Severity.CRITICAL, Severity.HIGH],
142
+ outputs=[Output.FAILED, Output.WARNING, Output.PASSED],
143
+ )
144
+ )
145
+ self.assertEqual([{"FindingId": 1}, {"FindingId": 2}], result)
146
+
147
+ def test_acknowledge_finding(self):
148
+ self.mock_request(
149
+ "companies/1/findings/2", json={"Status": "Acknowledged"}, method="patch"
150
+ )
151
+
152
+ self.client.acknowledge_finding(1, 2)
153
+
154
+ self.assertEqual(1, self.requests.call_count)