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,180 @@
1
+ """
2
+ XMPP Output Bot
3
+ Connects to a XMPP Server and sends data to a user.
4
+
5
+ TLS is used by default.
6
+
7
+ Tested with Python >= 3.4
8
+ Tested with slixmpp >= 1.0.0-beta5
9
+
10
+ Copyright (C) 2016 by Bundesamt für Sicherheit in der Informationstechnik
11
+ Software engineering by Intevation GmbH
12
+
13
+ Parameters:
14
+ ca_certs: string to a CA-bundle file or false/empty string for no checks
15
+ hierarchical_output: boolean (false by default)
16
+ xmpp_user: string
17
+ xmpp_server: string
18
+ xmpp_password: boolean
19
+ xmpp_to_user: string
20
+ xmpp_to_server: string
21
+ xmpp_room: string
22
+ xmpp_room_nick: string
23
+ xmpp_room_password: string
24
+ use_muc: boolean
25
+ """
26
+
27
+ from intelmq.lib.bot import Bot
28
+ from intelmq.lib.exceptions import MissingDependencyError
29
+
30
+ try:
31
+ import slixmpp
32
+
33
+ class XMPPClient(slixmpp.ClientXMPP):
34
+ def __init__(self, jid, password, room, room_nick, room_password, logger):
35
+ slixmpp.ClientXMPP.__init__(self, jid, password)
36
+
37
+ self.xmpp_room = room
38
+ self.xmpp_room_nick = room_nick
39
+ self.xmpp_room_password = room_password
40
+
41
+ self.logger = logger
42
+ self.add_event_handler("session_start", self.session_start)
43
+
44
+ self.logger.info("Initialized XMPP Client.")
45
+
46
+ def session_start(self, event):
47
+ self.send_presence()
48
+ self.logger.debug("Session started.")
49
+
50
+ try:
51
+ self.get_roster()
52
+ except slixmpp.exceptions.IqError as err:
53
+ self.logger.error("There was an error getting the roster.")
54
+ self.logger.error(err.iq["error"]["condition"])
55
+ self.disconnect()
56
+ except slixmpp.exceptions.IqTimeout:
57
+ self.logger.error("Server is taking too long to respond.")
58
+ self.disconnect()
59
+
60
+ if (
61
+ self.xmpp_room
62
+ ): # and self.plugin.get('xep_0045') # this check should also exist!
63
+ self.logger.debug("Joining room: %s.", self.xmpp_room)
64
+ pwd = self.xmpp_room_password if self.xmpp_room_password else ""
65
+ self.plugin["xep_0045"].joinMUC(
66
+ self.xmpp_room, self.xmpp_room_nick, password=pwd, wait=True
67
+ )
68
+
69
+ except ImportError:
70
+ slixmpp = None
71
+
72
+
73
+ class XMPPOutputBot(Bot):
74
+ xmpp = None
75
+
76
+ def init(self):
77
+ self.logger.warning(
78
+ "The output bot 'intelmq.bots.outputs.xmpp.output' "
79
+ "is deprecated. It will be removed in version 3.0."
80
+ "Please see https://github.com/certtools/intelmq/blob/"
81
+ "develop/NEWS.md#xmpp-bots for more details."
82
+ )
83
+ if slixmpp is None:
84
+ raise MissingDependencyError("slixmpp")
85
+
86
+ # Retrieve Parameters from configuration
87
+ xmpp_user = getattr(self.parameters, "xmpp_user", None)
88
+ xmpp_server = getattr(self.parameters, "xmpp_server", None)
89
+ xmpp_password = getattr(self.parameters, "xmpp_password", None)
90
+
91
+ if None in (xmpp_user, xmpp_server, xmpp_password):
92
+ raise ValueError("No User / Password provided.")
93
+ else:
94
+ xmpp_login = xmpp_user + "@" + xmpp_server
95
+
96
+ self.muc = getattr(self.parameters, "use_muc", None)
97
+ xmpp_to_user = getattr(self.parameters, "xmpp_to_user", None)
98
+ xmpp_to_server = getattr(self.parameters, "xmpp_to_server", None)
99
+ xmpp_room = getattr(self.parameters, "xmpp_room", None) if self.muc else None
100
+ xmpp_room_nick = (
101
+ getattr(self.parameters, "xmpp_room_nick", None) if self.muc else None
102
+ )
103
+ xmpp_room_password = (
104
+ getattr(self.parameters, "xmpp_room_password", None) if self.muc else None
105
+ )
106
+
107
+ ca_certs = getattr(self.parameters, "ca_certs", None)
108
+
109
+ # Be sure the receiver was set up
110
+ if not self.muc and None in (xmpp_to_user, xmpp_to_server):
111
+ raise ValueError("No receiver for direct messages provided.")
112
+ else:
113
+ self.xmpp_receiver = xmpp_to_user + "@" + xmpp_to_server
114
+
115
+ if self.muc and not xmpp_room:
116
+ raise ValueError("No room provided.")
117
+ else:
118
+ self.xmpp_receiver = xmpp_room
119
+
120
+ if self.muc:
121
+ if not xmpp_room_nick:
122
+ # create the room_nick from user and server
123
+ xmpp_room_nick = xmpp_login
124
+
125
+ self.xmpp = XMPPClient(
126
+ xmpp_login,
127
+ xmpp_password,
128
+ xmpp_room,
129
+ xmpp_room_nick,
130
+ xmpp_room_password,
131
+ self.logger,
132
+ )
133
+
134
+ if ca_certs:
135
+ # Set CA-Certificates
136
+ self.xmpp.ca_certs = ca_certs
137
+
138
+ if self.xmpp.connect(reattempt=False):
139
+ self.xmpp.process()
140
+ # Add Handlers and register Plugins
141
+ self.xmpp.register_plugin("xep_0030") # Service Discovery
142
+ self.xmpp.register_plugin("xep_0045") # Multi-User Chat
143
+ else:
144
+ raise ValueError("Could not connect to XMPP-Server.")
145
+
146
+ def process(self):
147
+ event = self.receive_message()
148
+
149
+ jevent = event.to_json(
150
+ hierarchical=self.parameters.hierarchical_output, with_type=True
151
+ )
152
+
153
+ try:
154
+ # TODO: proper error handling.
155
+ # Right now it cannot be detected if the message was sent successfully.
156
+ if self.muc:
157
+ self.logger.debug("Trying to send to room %s.", self.xmpp_receiver)
158
+ self.xmpp.send_message(
159
+ mto=self.xmpp_receiver, mbody=jevent, mtype="groupchat"
160
+ )
161
+ else:
162
+ self.logger.debug("Trying to send to %s.", self.xmpp_receiver)
163
+ self.xmpp.send_message(mto=self.xmpp_receiver, mbody=jevent)
164
+ except slixmpp.exceptions.XMPPError as err:
165
+ self.logger.error("There was an error when sending the event.")
166
+ self.logger.error(err.iq["error"]["condition"])
167
+
168
+ self.acknowledge_message()
169
+
170
+ def shutdown(self):
171
+ if self.xmpp:
172
+ if self.xmpp.disconnect():
173
+ self.logger.info("Disconnected from XMPP Server.")
174
+ else:
175
+ self.logger.error("Could not disconnect from XMPP Server.")
176
+ else:
177
+ self.logger.info("There was no XMPPClient I could stop.")
178
+
179
+
180
+ BOT = XMPPOutputBot
File without changes
File without changes
@@ -0,0 +1,202 @@
1
+ """Transformers for the BlackKite
2
+
3
+ SPDX-FileCopyrightText: 2023 CERT.at GmbH <https://cert.at/>
4
+ SPDX-License-Identifier: AGPL-3.0-or-later
5
+ """
6
+
7
+ from abc import ABC
8
+ from datetime import datetime
9
+ from typing import Iterable, Sequence
10
+
11
+ import dateutil.parser
12
+ from dateutil.tz import UTC
13
+ from intelmq.lib.harmonization import IPAddress
14
+
15
+ from intelmq_extensions.lib.blackkite import Category
16
+
17
+
18
+ class BaseTransformer(ABC):
19
+ DEFAULT_CLASSIFICATION = ("", "", "")
20
+ CLASSIFICATION_MAP: dict[str, tuple] = {}
21
+ _COMMON_FIELDS = (
22
+ ("extra.feed_event_id", "FindingId"),
23
+ # In general, the Domain field does not represent the affected domain.
24
+ # It can be a primary domain of the affected URL, or just the domain
25
+ # configured for the given company, without direct relation to the finding.
26
+ # FIXME: waiting for BlackKite response on how to get the affected URL
27
+ ("source.fqdn", "", "get_domain"),
28
+ ("source.ip", "IpAddress", "get_ip"),
29
+ ("event_description.text", "Detail", "get_description"),
30
+ # The date that Black Kite first seen the finding.
31
+ ("time.source", "FindingDate"),
32
+ ("feed.documentation", "ControlId", "get_documentation_url"),
33
+ )
34
+ SPECIFIC_FIELDS = ()
35
+
36
+ def __init__(self) -> None:
37
+ self._data = None
38
+ self._transformed = {}
39
+
40
+ def to_events_data(self, incident_data: dict) -> Iterable[dict]:
41
+ self._data = incident_data
42
+ self._transformed = {}
43
+ self._finding_id = incident_data.get("ControlId")
44
+ self.transform()
45
+ for event_data in self.transform_single():
46
+ yield event_data
47
+
48
+ def _map(self, event_key: str, finding_key: str, mapper: str = None):
49
+ if finding_key in self._data:
50
+ if not mapper:
51
+ self._transformed[event_key] = self._data[finding_key]
52
+ else:
53
+ self._transformed[event_key] = getattr(self, mapper)(finding_key)
54
+
55
+ def _map_many(self, mappings: Sequence[tuple]):
56
+ for mapping in mappings:
57
+ self._map(*mapping)
58
+
59
+ def _map_classification(self):
60
+ taxonomy, type_, identifier = self.CLASSIFICATION_MAP.get(
61
+ self._finding_id, self.DEFAULT_CLASSIFICATION
62
+ )
63
+ self._transformed["classification.taxonomy"] = taxonomy
64
+ self._transformed["classification.type"] = type_
65
+ self._transformed["classification.identifier"] = identifier
66
+
67
+ def _map_feed_data(self):
68
+ category = self._data.get("ControlId", "").split("-")[0]
69
+ self._transformed["feed.code"] = f"blackkite-{category.lower()}"
70
+ self._transformed["feed.name"] = f"BlackKite {category.upper()}"
71
+
72
+ def get_description(self, _):
73
+ description = []
74
+ if title := self._data.get("Title"):
75
+ description.append(title)
76
+ if detail := self._data.get("Detail"):
77
+ description.append(detail)
78
+ return ". ".join(description)
79
+
80
+ def transform(self):
81
+ self._map_classification()
82
+ self._map_feed_data()
83
+ self._map_many(self._COMMON_FIELDS)
84
+ self._map_many(self.SPECIFIC_FIELDS)
85
+
86
+ def transform_single(self) -> Iterable[dict]:
87
+ """If an incident produce multiple events,
88
+ this method should do the extraction and transformation"""
89
+
90
+ yield self._transformed
91
+
92
+ def get_documentation_url(self, _):
93
+ return f"https://cyber.riskscore.cards/kb/{self._finding_id}"
94
+
95
+ def lower(self, field: str) -> str:
96
+ return (self._data.get(field) or "").lower()
97
+
98
+ def get_ip(self, field: str):
99
+ # BlackKite can send domain in the IPAddress field
100
+ ip_data = self._data.get(field)
101
+ if IPAddress.is_valid(ip_data, sanitize=True):
102
+ return ip_data
103
+ return None
104
+
105
+ def get_domain(self, field: str):
106
+ # BlackKite can send domain in the IPAddress field
107
+ # as it's then a better domain, this should be used
108
+ if ip_data := self._data.get("IpAddress"):
109
+ if not IPAddress.is_valid(ip_data):
110
+ return ip_data
111
+
112
+ if field:
113
+ return self._data.get(field)
114
+
115
+ def get_first(self, field: str) -> str:
116
+ """Get first item from the list"""
117
+ items = self._data.get(field)
118
+ if items:
119
+ return items[0]
120
+ return None
121
+
122
+
123
+ class PatchManagementTransformer(BaseTransformer):
124
+ DEFAULT_CLASSIFICATION = ("vulnerable", "vulnerable-system", "bk-patchmanagement")
125
+ CLASSIFICATION_MAP = {
126
+ "PATCH-010": ("vulnerable", "vulnerable-system", "end-of-live")
127
+ }
128
+ SPECIFIC_FIELDS = (
129
+ ("extra.product_name", "ProductName"),
130
+ # lower to match with data from other sources
131
+ ("extra.vulnerabilities", "CveId", "lower"),
132
+ ("event_description.url", "References", "get_first"),
133
+ )
134
+
135
+ def _map_cpe(self):
136
+ cpes = self._data.get("Cpes")
137
+ if not cpes:
138
+ return
139
+ cpe_data = cpes[0].split(":")
140
+ self._transformed["extra.vendor"] = cpe_data[3]
141
+ self._transformed["extra.product"] = cpe_data[4]
142
+
143
+ def _map_cve_to_identifier(self):
144
+ if cve_id := self._data.get("CveId"):
145
+ self._transformed["classification.identifier"] = cve_id.lower()
146
+
147
+ def transform(self) -> Iterable[dict]:
148
+ super().transform()
149
+ self._map_cpe()
150
+ self._map_cve_to_identifier()
151
+
152
+
153
+ class ApplicationSecurityTransformer(BaseTransformer):
154
+ CLASSIFICATION_MAP = {
155
+ "APPSEC-014": ("vulnerable", "potentially-unwanted-accessible", "bk-appsec")
156
+ }
157
+
158
+
159
+ class CredentialManagementTransformer(BaseTransformer):
160
+ DEFAULT_CLASSIFICATION = (
161
+ "information-content-security",
162
+ "data-leak",
163
+ "leaked-credentials",
164
+ )
165
+ SPECIFIC_FIELDS = (
166
+ ("source.fqdn", "EmailorUsername", "get_domain_from_email"),
167
+ ("source.account", "EmailorUsername"),
168
+ ("extra.account", "EmailorUsername"),
169
+ ("extra.password", "PasswordType"),
170
+ ("extra.compromise_time_full", "LeakDate"),
171
+ ("extra.compromise_time", "LeakDate", "get_compromise_time"),
172
+ ("extra.leak_source", "Source"),
173
+ )
174
+ _DESCRIPTION = (
175
+ "A user with email in your domain"
176
+ " was found in leaked credentials related to: {source}"
177
+ )
178
+ _COMPROMISE_TIME_FORMAT = "%Y-%m"
179
+
180
+ def get_description(self, _):
181
+ return self._DESCRIPTION.format(source=self._data.get("Source"))
182
+
183
+ def get_compromise_time(self, _):
184
+ # return the month only, as in DISP parser
185
+ date_str = self._data.get("LeakDate")
186
+ if not date_str:
187
+ return None
188
+ date: datetime = dateutil.parser.parse(date_str).astimezone(tz=UTC)
189
+ return date.strftime(self._COMPROMISE_TIME_FORMAT)
190
+
191
+ def get_domain_from_email(self, field):
192
+ data = self._data.get(field)
193
+ if "@" in data:
194
+ return data.split("@")[-1]
195
+ return None
196
+
197
+
198
+ TRANSFORMERS_MAPPER: dict[Category, BaseTransformer] = {
199
+ Category.PatchManagement: PatchManagementTransformer(),
200
+ Category.ApplicationSecurity: ApplicationSecurityTransformer(),
201
+ Category.CredentialManagement: CredentialManagementTransformer(),
202
+ }
@@ -0,0 +1,65 @@
1
+ """Parser for BlackKite feeds
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
+ from dataclasses import dataclass
9
+ from typing import Union
10
+
11
+ import intelmq.lib.message as message
12
+ from intelmq.lib import utils
13
+ from intelmq.lib.bot import ParserBot
14
+
15
+ from intelmq_extensions.lib.blackkite import Category
16
+
17
+ from ._transformers import TRANSFORMERS_MAPPER
18
+
19
+
20
+ @dataclass
21
+ class PreparedData:
22
+ raw_report: str
23
+ company: dict
24
+ event_data: dict
25
+
26
+
27
+ class BlackKiteParserBot(ParserBot):
28
+ def init(self):
29
+ pass
30
+
31
+ def parse(self, report: message.Report):
32
+ raw_report = utils.base64_decode(report.get("raw"))
33
+ self._current_line = raw_report
34
+
35
+ data = json.loads(raw_report)
36
+ company = data["company"]
37
+ finding = data["finding"]
38
+
39
+ category = Category(finding["ControlId"].split("-")[0])
40
+ for event_data in TRANSFORMERS_MAPPER[category].to_events_data(finding):
41
+ self._current_line = PreparedData(raw_report, company, event_data)
42
+ yield self._current_line
43
+
44
+ def parse_line(self, data: PreparedData, report: message.Report):
45
+ event = self.new_event(report)
46
+ event.add("raw", data.raw_report)
47
+
48
+ event.add("extra.monitored_asset", data.company.get("DomainName"))
49
+ event.add("extra.blackkite_company_id", data.company.get("CompanyId"))
50
+
51
+ for key, value in data.event_data.items():
52
+ event.add(key, value, overwrite=True)
53
+
54
+ if "time.source" not in event:
55
+ event.add("time.source", event.get("time.observation"))
56
+
57
+ return event
58
+
59
+ def recover_line(self, line: Union[str, None, PreparedData] = None) -> str:
60
+ if isinstance(line, str):
61
+ return super().recover_line(line)
62
+ return super().recover_line(line.raw_report)
63
+
64
+
65
+ BOT = BlackKiteParserBot
File without changes
@@ -0,0 +1,125 @@
1
+ """Parsing DISP data feed.
2
+
3
+ Currently only for credentials tracking
4
+
5
+ SPDX-FileCopyrightText: 2023 CERT.at GmbH <https://cert.at/>
6
+ SPDX-License-Identifier: AGPL-3.0-or-later
7
+ """
8
+
9
+ import json
10
+ from copy import deepcopy
11
+ from datetime import datetime
12
+ from urllib import parse
13
+
14
+ import intelmq.lib.message as message
15
+ from dateutil.parser import ParserError
16
+ from dateutil.parser import parse as dt_parse
17
+ from dateutil.tz import UTC
18
+ from dns.exception import DNSException
19
+ from intelmq.lib import utils
20
+ from intelmq.lib.bot import ParserBot
21
+ from intelmq.lib.exceptions import IntelMQException
22
+ from intelmq.lib.harmonization import URL, DateTime
23
+
24
+
25
+ class InvalidData(IntelMQException, ValueError):
26
+ """Given message is invalid comparing with parser requirements"""
27
+
28
+
29
+ class DISPParserBot(ParserBot):
30
+ compromise_time_format: str = "%Y-%m" # empty, 'original' or format string
31
+ redact_url_path: bool = True
32
+ resolve_ip: bool = False
33
+
34
+ def parse(self, report: message.Report):
35
+ raw_report = utils.base64_decode(report.get("raw"))
36
+ report_data = json.loads(raw_report)
37
+
38
+ incident = report_data.get("incident")
39
+ evidences = report_data.get("evidences", {}).get("credentials")
40
+
41
+ if not evidences:
42
+ self.logger.error("Report doesn't contain any evidences")
43
+ raise InvalidData("No evidences in DISP report")
44
+
45
+ for evidence in evidences:
46
+ current = {"incident": deepcopy(incident), "evidence": evidence}
47
+ self._current_line = json.dumps(current)
48
+ yield current
49
+
50
+ def parse_line(self, line: dict, report: message.Report):
51
+ event = self.new_event(report)
52
+ event.add("raw", json.dumps(line))
53
+
54
+ incident, evidence = line.get("incident"), line.get("evidence")
55
+ self._map_incident(incident, event)
56
+ self._map_evidence(evidence, event)
57
+ return event
58
+
59
+ def _map_incident(self, incident: dict, event: message.Event):
60
+ event.add("event_description.text", incident.get("title"))
61
+ event.add("extra.feed_event_id", incident.get("id"))
62
+ event.add(
63
+ "time.source", DateTime.from_epoch_millis(incident.get("validationDate"))
64
+ )
65
+ event.add("extra.monitored_asset", incident.get("relatedAssets", [""])[0])
66
+
67
+ def _map_evidence(self, evidence: dict, event: message.Event):
68
+ url = evidence.get("url")
69
+ parsed_url = parse.urlsplit(url)
70
+ event.add("source.fqdn", parsed_url.netloc)
71
+
72
+ if self.resolve_ip:
73
+ try:
74
+ event.add("source.ip", URL.to_ip(url))
75
+ except DNSException:
76
+ self.logger.warning("Cannot get IP for domain %s.", url, exc_info=True)
77
+
78
+ path = parsed_url.path
79
+ if self.redact_url_path and parsed_url.path and parsed_url.path != "/":
80
+ path = "/[REDACTED]"
81
+ event.add(
82
+ "source.url",
83
+ f"{parsed_url.scheme}://{parsed_url.netloc}{path}",
84
+ sanitize=True,
85
+ )
86
+ event.add("source.urlpath", path)
87
+ event.add("extra.full_url", self._disarm_url(url))
88
+
89
+ event.add("source.account", evidence.get("username"))
90
+ event.add("extra.account", evidence.get("username"))
91
+ event.add("extra.password", evidence.get("password"))
92
+ event.add("extra.application", evidence.get("application"))
93
+ event.add("extra.compromise_time", self._parse_compromise_time(evidence))
94
+ event.add("extra.compromise_time_full", evidence.get("date"))
95
+
96
+ event.add("malware.name", evidence.get("malware"))
97
+
98
+ @staticmethod
99
+ def _disarm_url(url: str):
100
+ """Prevents URLs being usable"""
101
+ url = url.replace("http://", "hxxp://")
102
+ return url.replace("https://", "hxxps://")
103
+
104
+ def _parse_compromise_time(self, evidence: dict):
105
+ compromise_time = evidence.get("date")
106
+ if not compromise_time:
107
+ return None
108
+
109
+ if self.compromise_time_format == "original":
110
+ return compromise_time
111
+
112
+ if not self.compromise_time_format:
113
+ return ""
114
+
115
+ try:
116
+ dt: datetime = dt_parse(compromise_time).astimezone(tz=UTC)
117
+ except ParserError:
118
+ self.logger.warning(
119
+ "Error parsing compromise time %s.", compromise_time, exc_info=True
120
+ )
121
+ return None
122
+ return dt.strftime(self.compromise_time_format)
123
+
124
+
125
+ BOT = DISPParserBot
@@ -0,0 +1,63 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ The descriptions give a hint about what the entry is about and is very mixed.
4
+ Most prominent description is "phishing", most of them are malware names.
5
+ More types could be mapped better, only the most obvious ones are done currently.
6
+ """
7
+ import datetime
8
+
9
+ from intelmq.lib import utils
10
+ from intelmq.lib.bot import Bot
11
+
12
+
13
+ class MalwareDomainsParserBot(Bot):
14
+ def is_valid_date(self, strd):
15
+ try:
16
+ datetime.datetime.strptime(strd, "%Y%m%d")
17
+ return True
18
+ except Exception:
19
+ return False
20
+
21
+ def process(self):
22
+ report = self.receive_message()
23
+
24
+ raw_report = utils.base64_decode(report.get("raw"))
25
+
26
+ for row in raw_report.splitlines():
27
+ row = row.rstrip()
28
+
29
+ if row.startswith("#") or len(row) == 0:
30
+ continue
31
+
32
+ values = row.split("\t")[1:]
33
+
34
+ event = self.new_event(report)
35
+
36
+ event.add("source.fqdn", values[1])
37
+ if values[2] == "phishing":
38
+ event.add("classification.identifier", values[2])
39
+ event.add("classification.type", "phishing")
40
+ elif values[2] == "C&C":
41
+ event.add("classification.identifier", values[2])
42
+ event.add("classification.type", "c2-server")
43
+ else:
44
+ event.add("classification.identifier", values[2])
45
+ event.add("classification.type", "malware")
46
+ event.add("event_description.text", values[2])
47
+
48
+ for i in range(4, len(values)):
49
+ if self.is_valid_date(values[i]):
50
+ event.add(
51
+ "time.source", # times are GMT, verified via email
52
+ values[i] + "T00:00:00+00:00",
53
+ overwrite=True,
54
+ )
55
+ break
56
+
57
+ event.add("raw", row)
58
+
59
+ self.send_message(event)
60
+ self.acknowledge_message()
61
+
62
+
63
+ BOT = MalwareDomainsParserBot
File without changes