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,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
@@ -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
@@ -0,0 +1,10 @@
1
+ """
2
+ Test for the XMPP Output Bot
3
+ """
4
+
5
+ import os
6
+
7
+ if os.environ.get("INTELMQ_TEST_EXOTIC"):
8
+ import intelmq_extensions.bots.outputs.xmpp.output # noqa
9
+
10
+ # This file is a stub.
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