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.
- intelmq_extensions/__init__.py +0 -0
- intelmq_extensions/bots/__init__.py +0 -0
- intelmq_extensions/bots/collectors/blackkite/__init__.py +0 -0
- intelmq_extensions/bots/collectors/blackkite/_client.py +167 -0
- intelmq_extensions/bots/collectors/blackkite/collector.py +182 -0
- intelmq_extensions/bots/collectors/disp/__init__.py +0 -0
- intelmq_extensions/bots/collectors/disp/_client.py +121 -0
- intelmq_extensions/bots/collectors/disp/collector.py +104 -0
- intelmq_extensions/bots/collectors/xmpp/__init__.py +0 -0
- intelmq_extensions/bots/collectors/xmpp/collector.py +210 -0
- intelmq_extensions/bots/experts/__init__.py +0 -0
- intelmq_extensions/bots/experts/certat_contact_intern/__init__.py +0 -0
- intelmq_extensions/bots/experts/certat_contact_intern/expert.py +139 -0
- intelmq_extensions/bots/experts/copy_extra/__init__.py +0 -0
- intelmq_extensions/bots/experts/copy_extra/expert.py +27 -0
- intelmq_extensions/bots/experts/event_group_splitter/__init__.py +0 -0
- intelmq_extensions/bots/experts/event_group_splitter/expert.py +117 -0
- intelmq_extensions/bots/experts/event_splitter/__init__.py +0 -0
- intelmq_extensions/bots/experts/event_splitter/expert.py +41 -0
- intelmq_extensions/bots/experts/squelcher/__init__.py +0 -0
- intelmq_extensions/bots/experts/squelcher/expert.py +316 -0
- intelmq_extensions/bots/experts/vulnerability_lookup/__init__.py +0 -0
- intelmq_extensions/bots/experts/vulnerability_lookup/expert.py +136 -0
- intelmq_extensions/bots/outputs/__init__.py +0 -0
- intelmq_extensions/bots/outputs/mattermost/__init__.py +0 -0
- intelmq_extensions/bots/outputs/mattermost/output.py +113 -0
- intelmq_extensions/bots/outputs/to_logs/__init__.py +0 -0
- intelmq_extensions/bots/outputs/to_logs/output.py +12 -0
- intelmq_extensions/bots/outputs/xmpp/__init__.py +0 -0
- intelmq_extensions/bots/outputs/xmpp/output.py +180 -0
- intelmq_extensions/bots/parsers/__init__.py +0 -0
- intelmq_extensions/bots/parsers/blackkite/__init__.py +0 -0
- intelmq_extensions/bots/parsers/blackkite/_transformers.py +202 -0
- intelmq_extensions/bots/parsers/blackkite/parser.py +65 -0
- intelmq_extensions/bots/parsers/disp/__init__.py +0 -0
- intelmq_extensions/bots/parsers/disp/parser.py +125 -0
- intelmq_extensions/bots/parsers/malwaredomains/__init__.py +0 -0
- intelmq_extensions/bots/parsers/malwaredomains/parser.py +63 -0
- intelmq_extensions/cli/__init__.py +0 -0
- intelmq_extensions/cli/create_reports.py +161 -0
- intelmq_extensions/cli/intelmqcli.py +657 -0
- intelmq_extensions/cli/lib.py +670 -0
- intelmq_extensions/cli/utils.py +12 -0
- intelmq_extensions/etc/harmonization.conf +434 -0
- intelmq_extensions/etc/squelcher.conf +52 -0
- intelmq_extensions/lib/__init__.py +0 -0
- intelmq_extensions/lib/api_helpers.py +105 -0
- intelmq_extensions/lib/blackkite.py +29 -0
- intelmq_extensions/tests/__init__.py +0 -0
- intelmq_extensions/tests/base.py +336 -0
- intelmq_extensions/tests/bots/__init__.py +0 -0
- intelmq_extensions/tests/bots/collectors/__init__.py +0 -0
- intelmq_extensions/tests/bots/collectors/blackkite/__init__.py +0 -0
- intelmq_extensions/tests/bots/collectors/blackkite/base.py +45 -0
- intelmq_extensions/tests/bots/collectors/blackkite/test_client.py +154 -0
- intelmq_extensions/tests/bots/collectors/blackkite/test_collector.py +287 -0
- intelmq_extensions/tests/bots/collectors/disp/__init__.py +0 -0
- intelmq_extensions/tests/bots/collectors/disp/base.py +147 -0
- intelmq_extensions/tests/bots/collectors/disp/test_client.py +134 -0
- intelmq_extensions/tests/bots/collectors/disp/test_collector.py +137 -0
- intelmq_extensions/tests/bots/collectors/xmpp/__init__.py +0 -0
- intelmq_extensions/tests/bots/collectors/xmpp/test_collector.py +10 -0
- intelmq_extensions/tests/bots/experts/__init__.py +0 -0
- intelmq_extensions/tests/bots/experts/certat_contact_intern/__init__.py +0 -0
- intelmq_extensions/tests/bots/experts/certat_contact_intern/test_expert.py +176 -0
- intelmq_extensions/tests/bots/experts/copy_extra/__init__.py +0 -0
- intelmq_extensions/tests/bots/experts/copy_extra/test_expert.py +42 -0
- intelmq_extensions/tests/bots/experts/event_group_splitter/__init__.py +0 -0
- intelmq_extensions/tests/bots/experts/event_group_splitter/test_expert.py +302 -0
- intelmq_extensions/tests/bots/experts/event_splitter/__init__.py +0 -0
- intelmq_extensions/tests/bots/experts/event_splitter/test_expert.py +101 -0
- intelmq_extensions/tests/bots/experts/squelcher/__init__.py +0 -0
- intelmq_extensions/tests/bots/experts/squelcher/test_expert.py +548 -0
- intelmq_extensions/tests/bots/experts/vulnerability_lookup/__init__.py +0 -0
- intelmq_extensions/tests/bots/experts/vulnerability_lookup/test_expert.py +203 -0
- intelmq_extensions/tests/bots/outputs/__init__.py +0 -0
- intelmq_extensions/tests/bots/outputs/mattermost/__init__.py +0 -0
- intelmq_extensions/tests/bots/outputs/mattermost/test_output.py +138 -0
- intelmq_extensions/tests/bots/outputs/xmpp/__init__.py +0 -0
- intelmq_extensions/tests/bots/outputs/xmpp/test_output.py +10 -0
- intelmq_extensions/tests/bots/parsers/__init__.py +0 -0
- intelmq_extensions/tests/bots/parsers/blackkite/__init__.py +0 -0
- intelmq_extensions/tests/bots/parsers/blackkite/data.py +69 -0
- intelmq_extensions/tests/bots/parsers/blackkite/test_parser.py +197 -0
- intelmq_extensions/tests/bots/parsers/disp/__init__.py +0 -0
- intelmq_extensions/tests/bots/parsers/disp/test_parser.py +282 -0
- intelmq_extensions/tests/bots/parsers/malwaredomains/__init__.py +0 -0
- intelmq_extensions/tests/bots/parsers/malwaredomains/test_parser.py +62 -0
- intelmq_extensions/tests/cli/__init__.py +0 -0
- intelmq_extensions/tests/cli/test_create_reports.py +97 -0
- intelmq_extensions/tests/cli/test_intelmqcli.py +158 -0
- intelmq_extensions/tests/lib/__init__.py +0 -0
- intelmq_extensions/tests/lib/base.py +81 -0
- intelmq_extensions/tests/lib/test_api_helpers.py +126 -0
- intelmq_extensions-1.8.1.dist-info/METADATA +60 -0
- intelmq_extensions-1.8.1.dist-info/RECORD +100 -0
- intelmq_extensions-1.8.1.dist-info/WHEEL +5 -0
- intelmq_extensions-1.8.1.dist-info/entry_points.txt +33 -0
- intelmq_extensions-1.8.1.dist-info/licenses/LICENSE +661 -0
- 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
|
|
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)
|