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,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
|
|
File without changes
|
|
@@ -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
|