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,203 @@
|
|
|
1
|
+
"""Testing events splitter
|
|
2
|
+
|
|
3
|
+
SPDX-FileCopyrightText: 2025 CERT.at GmbH <https://cert.at/>
|
|
4
|
+
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import unittest
|
|
10
|
+
from unittest import mock
|
|
11
|
+
|
|
12
|
+
import intelmq.lib.test as test
|
|
13
|
+
import requests
|
|
14
|
+
from requests_mock import MockerCore
|
|
15
|
+
|
|
16
|
+
from intelmq_extensions.bots.experts.vulnerability_lookup.expert import (
|
|
17
|
+
VulnerabilityLookupExpertBot,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from ....base import BotTestCase
|
|
21
|
+
|
|
22
|
+
INPUT = {
|
|
23
|
+
"__type": "Event",
|
|
24
|
+
"classification.identifier": "zeus",
|
|
25
|
+
"classification.type": "vulnerable-system",
|
|
26
|
+
"notify": True,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@test.skip_redis()
|
|
31
|
+
class TestVulnerabilityLookupExpertBot(BotTestCase, unittest.TestCase):
|
|
32
|
+
@classmethod
|
|
33
|
+
def set_bot(cls):
|
|
34
|
+
cls.use_cache = True
|
|
35
|
+
cls.bot_reference = VulnerabilityLookupExpertBot
|
|
36
|
+
|
|
37
|
+
def mock_http_session(self):
|
|
38
|
+
session = requests.Session()
|
|
39
|
+
session_mock = mock.patch(
|
|
40
|
+
"intelmq_extensions.bots.experts.vulnerability_lookup.expert.create_request_session",
|
|
41
|
+
return_value=session,
|
|
42
|
+
)
|
|
43
|
+
session_mock.start()
|
|
44
|
+
self.addCleanup(session_mock.stop)
|
|
45
|
+
|
|
46
|
+
self.requests = MockerCore(session=session)
|
|
47
|
+
self.requests.start()
|
|
48
|
+
self.addCleanup(self.requests.stop)
|
|
49
|
+
|
|
50
|
+
def _load_data_file(self, filename: str):
|
|
51
|
+
current_dir = os.path.dirname(os.path.realpath(__file__))
|
|
52
|
+
file_path = os.path.join(current_dir, "data", filename)
|
|
53
|
+
with open(file_path, "r") as f:
|
|
54
|
+
return json.load(f)
|
|
55
|
+
|
|
56
|
+
def mock_request(
|
|
57
|
+
self,
|
|
58
|
+
path: str,
|
|
59
|
+
text: str = None,
|
|
60
|
+
json_: dict = None,
|
|
61
|
+
data_file: str = None,
|
|
62
|
+
**kwargs,
|
|
63
|
+
):
|
|
64
|
+
if text is not None:
|
|
65
|
+
kwargs["text"] = text
|
|
66
|
+
elif json_ is not None:
|
|
67
|
+
kwargs["json"] = json_
|
|
68
|
+
else:
|
|
69
|
+
kwargs["json"] = self._load_data_file(data_file)
|
|
70
|
+
self.requests.get(
|
|
71
|
+
f"https://vulnerability.circl.lu/api/{path}",
|
|
72
|
+
status_code=200,
|
|
73
|
+
**kwargs,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def setUp(self):
|
|
77
|
+
super().setUp()
|
|
78
|
+
self.mock_http_session()
|
|
79
|
+
|
|
80
|
+
def tearDown(self):
|
|
81
|
+
self.cache.flushdb()
|
|
82
|
+
return super().tearDown()
|
|
83
|
+
|
|
84
|
+
def test_not_existing_vuln(self):
|
|
85
|
+
self.mock_request("vulnerability/zeus", "null\n")
|
|
86
|
+
|
|
87
|
+
self.input_message = INPUT
|
|
88
|
+
self.run_bot()
|
|
89
|
+
|
|
90
|
+
self.assertMessageEqual(0, INPUT)
|
|
91
|
+
self.assertEqual(1, self.requests.call_count)
|
|
92
|
+
|
|
93
|
+
def test_filter_classification(self):
|
|
94
|
+
self.input_message = {**INPUT, "classification.type": "information-disclosure"}
|
|
95
|
+
self.run_bot()
|
|
96
|
+
|
|
97
|
+
self.assertMessageEqual(
|
|
98
|
+
0, {**INPUT, "classification.type": "information-disclosure"}
|
|
99
|
+
)
|
|
100
|
+
self.assertEqual(0, self.requests.call_count)
|
|
101
|
+
|
|
102
|
+
def test_no_epss(self):
|
|
103
|
+
self.mock_request(
|
|
104
|
+
"vulnerability/cve-2023-49606", data_file="CVE-2023-49606.json"
|
|
105
|
+
)
|
|
106
|
+
self.mock_request("epss/cve-2023-49606", json_={"data": []})
|
|
107
|
+
|
|
108
|
+
self.input_message = {**INPUT, "classification.identifier": "CVE-2023-49606"}
|
|
109
|
+
self.run_bot()
|
|
110
|
+
|
|
111
|
+
expected = {
|
|
112
|
+
**INPUT,
|
|
113
|
+
"classification.identifier": "CVE-2023-49606",
|
|
114
|
+
"event_description.url": "https://vulnerability.circl.lu/vuln/cve-2023-49606",
|
|
115
|
+
"event_description.text": "A use-after-free vulnerability exists in the HTTP Connection Headers parsing in Tinyproxy 1.11.1 and Tinyproxy 1.10.0. A specially crafted HTTP header can trigger reuse of previously freed memory, which leads to memory corruption and could lead to remote code execution. An attacker needs to make an unauthenticated HTTP request to trigger this vulnerability.", # noqa: E501
|
|
116
|
+
"extra.cvss3_1": 9.8,
|
|
117
|
+
}
|
|
118
|
+
self.assertEqual(2, self.requests.call_count)
|
|
119
|
+
self.assertMessageEqual(0, expected)
|
|
120
|
+
|
|
121
|
+
def test_cve_with_epss_and_cut_description(self):
|
|
122
|
+
self.mock_request(
|
|
123
|
+
"vulnerability/CVE-2024-23113", data_file="CVE-2024-23113.json"
|
|
124
|
+
)
|
|
125
|
+
self.mock_request("epss/CVE-2024-23113", data_file="CVE-2024-23113_epss.json")
|
|
126
|
+
|
|
127
|
+
self.input_message = {**INPUT, "classification.identifier": "CVE-2024-23113"}
|
|
128
|
+
self.run_bot(parameters={"description_length": 100})
|
|
129
|
+
|
|
130
|
+
expected = {
|
|
131
|
+
**INPUT,
|
|
132
|
+
"classification.identifier": "CVE-2024-23113",
|
|
133
|
+
"event_description.url": "https://vulnerability.circl.lu/vuln/cve-2024-23113",
|
|
134
|
+
"event_description.text": "A use of externally-controlled format string in Fortinet FortiOS versions 7.4.0 through 7.4.2, 7.2.0...", # noqa: E501
|
|
135
|
+
"extra.cvss3_1": 9.8,
|
|
136
|
+
"extra.epss": "0.416590000",
|
|
137
|
+
}
|
|
138
|
+
self.assertEqual(2, self.requests.call_count)
|
|
139
|
+
self.assertMessageEqual(0, expected)
|
|
140
|
+
|
|
141
|
+
def test_cache_is_used(self):
|
|
142
|
+
self.mock_request(
|
|
143
|
+
"vulnerability/CVE-2024-23113", data_file="CVE-2024-23113.json"
|
|
144
|
+
)
|
|
145
|
+
self.mock_request("epss/CVE-2024-23113", data_file="CVE-2024-23113_epss.json")
|
|
146
|
+
|
|
147
|
+
self.input_message = [
|
|
148
|
+
{**INPUT, "classification.identifier": "CVE-2024-23113"},
|
|
149
|
+
{**INPUT, "classification.identifier": "CVE-2024-23113"},
|
|
150
|
+
]
|
|
151
|
+
self.run_bot(parameters={"description_length": 100}, iterations=2)
|
|
152
|
+
|
|
153
|
+
expected = {
|
|
154
|
+
**INPUT,
|
|
155
|
+
"classification.identifier": "CVE-2024-23113",
|
|
156
|
+
"event_description.url": "https://vulnerability.circl.lu/vuln/cve-2024-23113",
|
|
157
|
+
"event_description.text": "A use of externally-controlled format string in Fortinet FortiOS versions 7.4.0 through 7.4.2, 7.2.0...", # noqa: E501
|
|
158
|
+
"extra.cvss3_1": 9.8,
|
|
159
|
+
"extra.epss": "0.416590000",
|
|
160
|
+
}
|
|
161
|
+
self.assertEqual(2, self.requests.call_count)
|
|
162
|
+
self.assertMessageEqual(0, expected)
|
|
163
|
+
self.assertMessageEqual(1, expected)
|
|
164
|
+
|
|
165
|
+
def test_multiple_cvss_and_description_with_new_lines(self):
|
|
166
|
+
self.mock_request("vulnerability/cve-2025-2742", data_file="CVE-2025-2742.json")
|
|
167
|
+
self.mock_request("epss/cve-2025-2742", json_={"data": []})
|
|
168
|
+
|
|
169
|
+
self.input_message = {**INPUT, "classification.identifier": "CVE-2025-2742"}
|
|
170
|
+
self.run_bot(parameters={"description_length": 100})
|
|
171
|
+
|
|
172
|
+
expected = {
|
|
173
|
+
**INPUT,
|
|
174
|
+
"classification.identifier": "CVE-2025-2742",
|
|
175
|
+
"event_description.url": "https://vulnerability.circl.lu/vuln/cve-2025-2742",
|
|
176
|
+
"event_description.text": "A vulnerability classified as critical was found in zhijiantianya ruoyi-vue-pro 2.4.1. This vuln...", # noqa: E501
|
|
177
|
+
"extra.cvss3_0": 5.4,
|
|
178
|
+
"extra.cvss3_1": 5.4,
|
|
179
|
+
"extra.cvss4_0": 5.3,
|
|
180
|
+
}
|
|
181
|
+
self.assertEqual(2, self.requests.call_count)
|
|
182
|
+
self.assertMessageEqual(0, expected)
|
|
183
|
+
|
|
184
|
+
def test_ghsa(self):
|
|
185
|
+
self.mock_request(
|
|
186
|
+
"vulnerability/ghsa-r6xg-mjqp-qqg4", data_file="GHSA-r6xg-mjqp-qqg4.json"
|
|
187
|
+
)
|
|
188
|
+
self.mock_request("epss/ghsa-r6xg-mjqp-qqg4", json_={"data": []})
|
|
189
|
+
|
|
190
|
+
self.input_message = {
|
|
191
|
+
**INPUT,
|
|
192
|
+
"classification.identifier": "GHSA-r6xg-mjqp-qqg4",
|
|
193
|
+
}
|
|
194
|
+
self.run_bot(parameters={"description_length": 100})
|
|
195
|
+
|
|
196
|
+
expected = {
|
|
197
|
+
**INPUT,
|
|
198
|
+
"classification.identifier": "GHSA-r6xg-mjqp-qqg4",
|
|
199
|
+
"event_description.url": "https://vulnerability.circl.lu/vuln/ghsa-r6xg-mjqp-qqg4",
|
|
200
|
+
"event_description.text": "A vulnerability classified as critical was found in zhijiantianya ruoyi-vue-pro 2.4.1. This vulnerab...", # noqa: E501
|
|
201
|
+
}
|
|
202
|
+
self.assertEqual(2, self.requests.call_count)
|
|
203
|
+
self.assertMessageEqual(0, expected)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest import mock
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
from requests_mock import MockerCore
|
|
6
|
+
|
|
7
|
+
from intelmq_extensions.bots.outputs.mattermost.output import MattermostOutputBot
|
|
8
|
+
|
|
9
|
+
from ....base import BotTestCase
|
|
10
|
+
|
|
11
|
+
INPUT = {
|
|
12
|
+
"__type": "Event",
|
|
13
|
+
"classification.identifier": "zeus",
|
|
14
|
+
"classification.type": "infected-system",
|
|
15
|
+
"notify": False,
|
|
16
|
+
"source.asn": 1,
|
|
17
|
+
"source.ip": "192.0.2.1",
|
|
18
|
+
"feed.name": "Example Feed",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestMattermostOutputBot(BotTestCase, unittest.TestCase):
|
|
23
|
+
MM_URL = "https://my.mm.instance"
|
|
24
|
+
BOT_TOKEN = "XYZ"
|
|
25
|
+
CHANNEL = "channel-1"
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def set_bot(cls):
|
|
29
|
+
cls.bot_reference = MattermostOutputBot
|
|
30
|
+
cls.sysconfig = {
|
|
31
|
+
"mm_url": cls.MM_URL,
|
|
32
|
+
"bot_token": cls.BOT_TOKEN,
|
|
33
|
+
"channel_id": cls.CHANNEL,
|
|
34
|
+
"author_name": None,
|
|
35
|
+
"author_icon": None,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def mock_http_session(self):
|
|
39
|
+
session = requests.Session()
|
|
40
|
+
session_mock = mock.patch(
|
|
41
|
+
"intelmq_extensions.bots.outputs.mattermost.output.create_request_session",
|
|
42
|
+
return_value=session,
|
|
43
|
+
)
|
|
44
|
+
session_mock.start()
|
|
45
|
+
self.addCleanup(session_mock.stop)
|
|
46
|
+
|
|
47
|
+
self.requests = MockerCore(session=session)
|
|
48
|
+
self.requests.start()
|
|
49
|
+
self.addCleanup(self.requests.stop)
|
|
50
|
+
|
|
51
|
+
def mock_request(
|
|
52
|
+
self,
|
|
53
|
+
message: str = None,
|
|
54
|
+
attachment: dict = None,
|
|
55
|
+
card: str = None,
|
|
56
|
+
path: str = "api/v4/posts",
|
|
57
|
+
method: str = "post",
|
|
58
|
+
**kwargs,
|
|
59
|
+
):
|
|
60
|
+
data = {"channel_id": self.CHANNEL}
|
|
61
|
+
if message is not None:
|
|
62
|
+
data["message"] = message
|
|
63
|
+
data["props"] = {}
|
|
64
|
+
if attachment is not None:
|
|
65
|
+
data["props"]["attachments"] = [attachment]
|
|
66
|
+
if card is not None:
|
|
67
|
+
data["props"]["card"] = card
|
|
68
|
+
mocking_method = getattr(self.requests, method)
|
|
69
|
+
mocking_method(
|
|
70
|
+
f"{self.MM_URL}/{path}",
|
|
71
|
+
request_headers={
|
|
72
|
+
"Authorization": f"Bearer {self.BOT_TOKEN}",
|
|
73
|
+
},
|
|
74
|
+
**kwargs,
|
|
75
|
+
)
|
|
76
|
+
return data
|
|
77
|
+
|
|
78
|
+
def setUp(self):
|
|
79
|
+
super().setUp()
|
|
80
|
+
self.mock_http_session()
|
|
81
|
+
|
|
82
|
+
def test_simple_static_message(self):
|
|
83
|
+
expected_payload = self.mock_request(message="This is a static message")
|
|
84
|
+
|
|
85
|
+
self.input_message = INPUT
|
|
86
|
+
self.run_bot(parameters={"message": "This is a static message"})
|
|
87
|
+
|
|
88
|
+
self.assertEqual(1, self.requests.call_count)
|
|
89
|
+
self.assertEqual(expected_payload, self.requests.last_request.json())
|
|
90
|
+
|
|
91
|
+
def test_get_info_from_event_if_allowed(self):
|
|
92
|
+
expected_payload = self.mock_request(
|
|
93
|
+
message="message zeus",
|
|
94
|
+
attachment={
|
|
95
|
+
"pretext": "pretext zeus",
|
|
96
|
+
"text": "text zeus",
|
|
97
|
+
"fallback": "fallback zeus",
|
|
98
|
+
"title": "title zeus",
|
|
99
|
+
"title_link": "title_link {ev[classification.identifier]}",
|
|
100
|
+
"author_name": "author_name {ev[classification.identifier]}",
|
|
101
|
+
"author_icon": "author_icon {ev[classification.identifier]}",
|
|
102
|
+
"author_link": "author_link {ev[classification.identifier]}",
|
|
103
|
+
"color": "color {ev[classification.identifier]}",
|
|
104
|
+
"footer": "footer zeus",
|
|
105
|
+
"fields": [
|
|
106
|
+
{"short": True, "title": "field title zeus", "value": "value zeus"}
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
card="card zeus",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
self.input_message = INPUT
|
|
113
|
+
self.run_bot(
|
|
114
|
+
parameters={
|
|
115
|
+
"message": "message {ev[classification.identifier]}",
|
|
116
|
+
"card": "card {ev[classification.identifier]}",
|
|
117
|
+
"pretext": "pretext {ev[classification.identifier]}",
|
|
118
|
+
"text": "text {ev[classification.identifier]}",
|
|
119
|
+
"fallback": "fallback {ev[classification.identifier]}",
|
|
120
|
+
"title": "title {ev[classification.identifier]}",
|
|
121
|
+
"title_link": "title_link {ev[classification.identifier]}",
|
|
122
|
+
"author_name": "author_name {ev[classification.identifier]}",
|
|
123
|
+
"author_icon": "author_icon {ev[classification.identifier]}",
|
|
124
|
+
"author_link": "author_link {ev[classification.identifier]}",
|
|
125
|
+
"color": "color {ev[classification.identifier]}",
|
|
126
|
+
"footer": "footer {ev[classification.identifier]}",
|
|
127
|
+
"fields": [
|
|
128
|
+
{
|
|
129
|
+
"short": True,
|
|
130
|
+
"title": "field title {ev[classification.identifier]}",
|
|
131
|
+
"value": "value {ev[classification.identifier]}",
|
|
132
|
+
}
|
|
133
|
+
],
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
self.assertEqual(1, self.requests.call_count)
|
|
138
|
+
self.assertEqual(expected_payload, self.requests.last_request.json())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Test data for parser tests. Extracted for readiness
|
|
2
|
+
|
|
3
|
+
SPDX-FileCopyrightText: 2023 CERT.at GmbH <https://cert.at/>
|
|
4
|
+
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
DEFAULT_COMPANY = {
|
|
8
|
+
"CompanyId": 1,
|
|
9
|
+
"CompanyName": "Austrian National CERT",
|
|
10
|
+
"DomainName": "cert.at",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
PATCH_MANAGEMENT = {
|
|
14
|
+
"FindingId": 1,
|
|
15
|
+
"Status": "Active",
|
|
16
|
+
"Severity": "Critical",
|
|
17
|
+
"ControlId": "PATCH-001",
|
|
18
|
+
"Domain": "example.cert.at",
|
|
19
|
+
"IpAddress": "127.0.0.1",
|
|
20
|
+
"ProductName": "openssh_/5.9",
|
|
21
|
+
"Cpes": [
|
|
22
|
+
"cpe:2.3:a:openbsd:openssh:7.6:p1:*:*:*:*:*:*",
|
|
23
|
+
"cpe:2.3:a:openbsd:openssh:7.6:-:*:*:*:*:*:*",
|
|
24
|
+
],
|
|
25
|
+
"PublishDate": "2023-08-02T07:30:44.38",
|
|
26
|
+
"CvssScore": 9.8,
|
|
27
|
+
"CwssScore": None,
|
|
28
|
+
"CveId": "CVE-2023-38408",
|
|
29
|
+
"CweId": "CWE-428",
|
|
30
|
+
"Detail": (
|
|
31
|
+
"The PKCS#11 feature in ssh-agent in OpenSSH before 9.3p2 has an insufficiently "
|
|
32
|
+
"trustworthy search path, leading to remote code execution if an agent is forwarded "
|
|
33
|
+
"to an attacker-controlled system. (Code in /usr/lib is not necessarily safe for "
|
|
34
|
+
"loading into ssh-agent.) NOTE: this issue exists because of an incomplete fix for"
|
|
35
|
+
" CVE-2016-10009."
|
|
36
|
+
),
|
|
37
|
+
"References": ["https://nvd.nist.gov/vuln/detail/CVE-2023-38408"],
|
|
38
|
+
"FindingDate": "2023-08-28T13:37:22.717",
|
|
39
|
+
"LastCheckDate": "2023-10-03T11:48:54.257",
|
|
40
|
+
"Ticket": None,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
APPLICATION_SECURITY = {
|
|
44
|
+
"FindingId": 658443,
|
|
45
|
+
"Status": "Active",
|
|
46
|
+
"Severity": "Critical",
|
|
47
|
+
"ControlId": "APPSEC-014",
|
|
48
|
+
"Domain": "example.cert.at",
|
|
49
|
+
"Output": "Failed",
|
|
50
|
+
"Title": "Cleartext Transmission of Sensitive Information",
|
|
51
|
+
"Detail": "Not encrypted communication on xxx.",
|
|
52
|
+
"FindingDate": "2023-08-28T13:37:22.717",
|
|
53
|
+
"LastCheckDate": "2023-10-03T11:48:58.937",
|
|
54
|
+
"Ticket": None,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
CREDENTIAL_MANAGEMENT = {
|
|
58
|
+
"FindingId": 44823698,
|
|
59
|
+
"Status": "Active",
|
|
60
|
+
"Severity": "Low",
|
|
61
|
+
"ControlId": "LEAK-003",
|
|
62
|
+
"Domain": "cert.at",
|
|
63
|
+
"EmailorUsername": "user@example.cert.at",
|
|
64
|
+
"Source": "cert_com_leak",
|
|
65
|
+
"LeakDate": "2022-09-22T00:00:00",
|
|
66
|
+
"LeakInfo": "****",
|
|
67
|
+
"PasswordType": "PLAIN",
|
|
68
|
+
"Ticket": None,
|
|
69
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Tests for BlackKiteParserBot
|
|
2
|
+
|
|
3
|
+
SPDX-FileCopyrightText: 2023 CERT.at GmbH <https://cert.at/>
|
|
4
|
+
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import unittest
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
import intelmq.lib.message as message
|
|
12
|
+
from dateutil.tz import UTC
|
|
13
|
+
|
|
14
|
+
from intelmq_extensions.bots.parsers.blackkite.parser import BlackKiteParserBot
|
|
15
|
+
|
|
16
|
+
from ....base import BotTestCase
|
|
17
|
+
from .data import (
|
|
18
|
+
APPLICATION_SECURITY,
|
|
19
|
+
CREDENTIAL_MANAGEMENT,
|
|
20
|
+
DEFAULT_COMPANY,
|
|
21
|
+
PATCH_MANAGEMENT,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestBlackKiteParserBot(BotTestCase, unittest.TestCase):
|
|
26
|
+
@classmethod
|
|
27
|
+
def set_bot(cls):
|
|
28
|
+
cls.bot_reference = BlackKiteParserBot
|
|
29
|
+
cls.sysconfig = {}
|
|
30
|
+
|
|
31
|
+
def _create_report_dict(self, finding_data: dict, company_data: dict = None):
|
|
32
|
+
company_data = company_data or DEFAULT_COMPANY
|
|
33
|
+
raw = {"company": company_data, "finding": finding_data}
|
|
34
|
+
|
|
35
|
+
data = {
|
|
36
|
+
"feed.accuracy": 100.0,
|
|
37
|
+
"time.observation": datetime(2023, 1, 1, tzinfo=UTC).isoformat(),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
report = message.Report(data, harmonization=self.harmonization)
|
|
41
|
+
report.add("raw", json.dumps(raw))
|
|
42
|
+
return report.to_dict(with_type=True)
|
|
43
|
+
|
|
44
|
+
def _create_event_dict(
|
|
45
|
+
self,
|
|
46
|
+
ip: str = "127.0.0.1",
|
|
47
|
+
fqdn: str = None,
|
|
48
|
+
event_id: int = 1,
|
|
49
|
+
monitored_asset: str = "cert.at",
|
|
50
|
+
identifier: str = "bk-patch",
|
|
51
|
+
taxonomy: str = "vulnerable",
|
|
52
|
+
type_: str = "vulnerable-system",
|
|
53
|
+
**kwargs,
|
|
54
|
+
):
|
|
55
|
+
data = {
|
|
56
|
+
"classification.identifier": identifier,
|
|
57
|
+
"classification.taxonomy": taxonomy,
|
|
58
|
+
"classification.type": type_,
|
|
59
|
+
"source.fqdn": fqdn,
|
|
60
|
+
"source.ip": ip,
|
|
61
|
+
"extra.feed_event_id": event_id,
|
|
62
|
+
"extra.blackkite_company_id": 1,
|
|
63
|
+
"feed.accuracy": 100.0,
|
|
64
|
+
"extra.monitored_asset": monitored_asset, # Domain from Company
|
|
65
|
+
"time.observation": datetime(2023, 1, 1, tzinfo=UTC).isoformat(),
|
|
66
|
+
**kwargs,
|
|
67
|
+
}
|
|
68
|
+
event = message.Event(data, harmonization=self.harmonization)
|
|
69
|
+
if "time.source" not in event:
|
|
70
|
+
event.add("time.source", "2023-08-28T13:37:22.717")
|
|
71
|
+
event.add("raw", json.dumps({"company": {}, "finding": {}}))
|
|
72
|
+
return event.to_dict(with_type=True)
|
|
73
|
+
|
|
74
|
+
def test_parse_patch_management_finding(self):
|
|
75
|
+
self.input_message = self._create_report_dict(PATCH_MANAGEMENT)
|
|
76
|
+
|
|
77
|
+
self.run_bot()
|
|
78
|
+
|
|
79
|
+
expected = {
|
|
80
|
+
"identifier": "cve-2023-38408",
|
|
81
|
+
"event_description.text": PATCH_MANAGEMENT["Detail"],
|
|
82
|
+
"event_description.url": "https://nvd.nist.gov/vuln/detail/CVE-2023-38408",
|
|
83
|
+
"extra.vendor": "openbsd",
|
|
84
|
+
"extra.product": "openssh",
|
|
85
|
+
"extra.product_name": "openssh_/5.9",
|
|
86
|
+
"extra.vulnerabilities": "cve-2023-38408",
|
|
87
|
+
# Cvss score? Severity?
|
|
88
|
+
"feed.documentation": "https://cyber.riskscore.cards/kb/PATCH-001",
|
|
89
|
+
"feed.code": "blackkite-patch",
|
|
90
|
+
"feed.name": "BlackKite PATCH",
|
|
91
|
+
}
|
|
92
|
+
self.assertMessageEqual(
|
|
93
|
+
0, self._create_event_dict(**expected), compare_raw=False
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def test_handling_domain_in_ip_field(self):
|
|
97
|
+
"This is a workaround for bug in BlackKite, already reported. See Taiga#808"
|
|
98
|
+
msg = PATCH_MANAGEMENT.copy()
|
|
99
|
+
msg["IpAddress"] = "override.example.at"
|
|
100
|
+
self.input_message = self._create_report_dict(msg)
|
|
101
|
+
|
|
102
|
+
self.run_bot()
|
|
103
|
+
|
|
104
|
+
expected = {
|
|
105
|
+
"source.ip": None,
|
|
106
|
+
# "source.fqdn": "override.example.at", FIXME: Wait for explanation from BlackKite
|
|
107
|
+
"identifier": "cve-2023-38408",
|
|
108
|
+
"event_description.text": PATCH_MANAGEMENT["Detail"],
|
|
109
|
+
"event_description.url": "https://nvd.nist.gov/vuln/detail/CVE-2023-38408",
|
|
110
|
+
"extra.vendor": "openbsd",
|
|
111
|
+
"extra.product": "openssh",
|
|
112
|
+
"extra.product_name": "openssh_/5.9",
|
|
113
|
+
"extra.vulnerabilities": "cve-2023-38408",
|
|
114
|
+
"feed.documentation": "https://cyber.riskscore.cards/kb/PATCH-001",
|
|
115
|
+
"feed.code": "blackkite-patch",
|
|
116
|
+
"feed.name": "BlackKite PATCH",
|
|
117
|
+
}
|
|
118
|
+
self.assertMessageEqual(
|
|
119
|
+
0, self._create_event_dict(**expected), compare_raw=False
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def test_parse_patch_management_finding_without_cve(self):
|
|
123
|
+
# e.g. EoL software
|
|
124
|
+
msg = PATCH_MANAGEMENT.copy()
|
|
125
|
+
msg["ControlId"] = "PATCH-010"
|
|
126
|
+
msg["CveId"] = None
|
|
127
|
+
self.input_message = self._create_report_dict(msg)
|
|
128
|
+
|
|
129
|
+
self.run_bot()
|
|
130
|
+
|
|
131
|
+
expected = {
|
|
132
|
+
"identifier": "end-of-live",
|
|
133
|
+
"event_description.text": msg["Detail"],
|
|
134
|
+
"event_description.url": "https://nvd.nist.gov/vuln/detail/CVE-2023-38408",
|
|
135
|
+
"extra.vendor": "openbsd",
|
|
136
|
+
"extra.product": "openssh",
|
|
137
|
+
"extra.product_name": "openssh_/5.9",
|
|
138
|
+
# Cvss score? Severity?
|
|
139
|
+
"feed.documentation": "https://cyber.riskscore.cards/kb/PATCH-010",
|
|
140
|
+
"feed.code": "blackkite-patch",
|
|
141
|
+
"feed.name": "BlackKite PATCH",
|
|
142
|
+
}
|
|
143
|
+
self.assertMessageEqual(
|
|
144
|
+
0, self._create_event_dict(**expected), compare_raw=False
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def test_parse_application_security_finding(self):
|
|
148
|
+
self.input_message = self._create_report_dict(APPLICATION_SECURITY)
|
|
149
|
+
|
|
150
|
+
self.run_bot()
|
|
151
|
+
|
|
152
|
+
expected = {
|
|
153
|
+
"classification.taxonomy": "vulnerable",
|
|
154
|
+
"classification.type": "potentially-unwanted-accessible",
|
|
155
|
+
"identifier": "bk-appsec",
|
|
156
|
+
"event_description.text": (
|
|
157
|
+
"Cleartext Transmission of Sensitive Information. "
|
|
158
|
+
"Not encrypted communication on xxx."
|
|
159
|
+
),
|
|
160
|
+
"feed.documentation": "https://cyber.riskscore.cards/kb/APPSEC-014",
|
|
161
|
+
"extra.feed_event_id": 658443,
|
|
162
|
+
"source.ip": None,
|
|
163
|
+
"feed.code": "blackkite-appsec",
|
|
164
|
+
"feed.name": "BlackKite APPSEC",
|
|
165
|
+
}
|
|
166
|
+
self.assertMessageEqual(
|
|
167
|
+
0, self._create_event_dict(**expected), compare_raw=False
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def test_parse_credential_management_finding(self):
|
|
171
|
+
self.input_message = self._create_report_dict(CREDENTIAL_MANAGEMENT)
|
|
172
|
+
|
|
173
|
+
self.run_bot()
|
|
174
|
+
|
|
175
|
+
expected = {
|
|
176
|
+
"classification.taxonomy": "information-content-security",
|
|
177
|
+
"classification.type": "data-leak",
|
|
178
|
+
"classification.identifier": "leaked-credentials",
|
|
179
|
+
"source.fqdn": "example.cert.at",
|
|
180
|
+
"extra.monitored_asset": "cert.at",
|
|
181
|
+
"feed.documentation": "https://cyber.riskscore.cards/kb/LEAK-003",
|
|
182
|
+
"source.account": "user@example.cert.at",
|
|
183
|
+
"extra.account": "user@example.cert.at", # intentionally twice
|
|
184
|
+
"extra.password": "PLAIN", # BK doesn't provide us more info
|
|
185
|
+
"extra.compromise_time_full": "2022-09-22T00:00:00",
|
|
186
|
+
"extra.compromise_time": "2022-09",
|
|
187
|
+
"extra.leak_source": "cert_com_leak",
|
|
188
|
+
"source.ip": None,
|
|
189
|
+
"extra.feed_event_id": 44823698,
|
|
190
|
+
# This feed doesn't have a proper source time, use observation instead
|
|
191
|
+
"time.source": datetime(2023, 1, 1, tzinfo=UTC).isoformat(),
|
|
192
|
+
"feed.code": "blackkite-leak",
|
|
193
|
+
"feed.name": "BlackKite LEAK",
|
|
194
|
+
}
|
|
195
|
+
self.assertMessageEqual(
|
|
196
|
+
0, self._create_event_dict(**expected), compare_raw=False
|
|
197
|
+
)
|
|
File without changes
|