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,670 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Utilities for intelmqcli.
|
|
4
|
+
|
|
5
|
+
Static data (queries)
|
|
6
|
+
"""
|
|
7
|
+
import argparse
|
|
8
|
+
import copy
|
|
9
|
+
import datetime
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
import intelmq
|
|
16
|
+
import intelmq.lib.utils as utils
|
|
17
|
+
import pkg_resources
|
|
18
|
+
import psycopg2
|
|
19
|
+
import psycopg2.extras
|
|
20
|
+
import rt
|
|
21
|
+
from intelmq import CONFIG_DIR
|
|
22
|
+
|
|
23
|
+
INTELMQCLI_CONF_FILE = os.path.join(CONFIG_DIR, "intelmqcli.conf")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"BASE_WHERE",
|
|
28
|
+
"CSV_FIELDS",
|
|
29
|
+
"EPILOG",
|
|
30
|
+
"QUERY_DISTINCT_CONTACTS_BY_INCIDENT",
|
|
31
|
+
"QUERY_EVENTS_BY_ASCONTACT_INCIDENT",
|
|
32
|
+
"QUERY_FEED_NAMES",
|
|
33
|
+
"QUERY_GET_TEXT",
|
|
34
|
+
"QUERY_IDENTIFIER_NAMES",
|
|
35
|
+
"QUERY_INSERT_CONTACT",
|
|
36
|
+
"QUERY_OPEN_EVENTS_BY_FEEDCODE",
|
|
37
|
+
"QUERY_HALF_PROC_INCIDENTS",
|
|
38
|
+
"QUERY_OPEN_EVENT_IDS_BY_TAXONOMY",
|
|
39
|
+
"QUERY_OPEN_EVENT_REPORTS_BY_TAXONOMY",
|
|
40
|
+
"QUERY_OPEN_FEEDCODES",
|
|
41
|
+
"QUERY_OPEN_TAXONOMIES",
|
|
42
|
+
"QUERY_TAXONOMY_NAMES",
|
|
43
|
+
"QUERY_TEXT_NAMES",
|
|
44
|
+
"QUERY_TYPE_NAMES",
|
|
45
|
+
"QUERY_UPDATE_CONTACT",
|
|
46
|
+
"USAGE",
|
|
47
|
+
"getTerminalHeight",
|
|
48
|
+
"IntelMQCLIContollerTemplate",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
EPILOG = """
|
|
52
|
+
Searches for all unprocessed incidents. Incidents will be filtered by country
|
|
53
|
+
code and the TLD of a domain according to configuration.
|
|
54
|
+
The search can be restricted to one source feed.
|
|
55
|
+
|
|
56
|
+
After the start, intelmqcli will immediately connect to RT with the given
|
|
57
|
+
credentials. The incidents will be shown grouped by the contact address if
|
|
58
|
+
known or the ASN otherwise.
|
|
59
|
+
|
|
60
|
+
You have 3 options here:
|
|
61
|
+
* Select one group by giving the id (number in first column) and show the email
|
|
62
|
+
and all events in detail
|
|
63
|
+
* Automatic sending of all incidents with 'a'
|
|
64
|
+
* Quit with 'q'
|
|
65
|
+
|
|
66
|
+
For the detailed view, the recipient, the subject and the mail text will be
|
|
67
|
+
shown, and below the technical data as csv. If the terminal is not big enough,
|
|
68
|
+
the data will not be shown in full. In this case, you can press 't' for the
|
|
69
|
+
table mode. less will be opened with the full text and data, whereas the data
|
|
70
|
+
will be formated as table, which is much easier to read and interpret.
|
|
71
|
+
The requestor (recipient of the mail) can be changed manually by pressing 'r'
|
|
72
|
+
and in the following prompt the address is asked. After sending, you can
|
|
73
|
+
optionally save the (new) address to the database linked to the ASNs.
|
|
74
|
+
If you are ready to submit the incidents to RT and send the mails out, press
|
|
75
|
+
's'.
|
|
76
|
+
'b' for back jumps to the incident overview and 'q' quits.
|
|
77
|
+
|
|
78
|
+
Exit codes:
|
|
79
|
+
0 if no errors happend or only errors which could be handled. Check the
|
|
80
|
+
output if recoverable errors happened.
|
|
81
|
+
1 if unrecoverable errors happened
|
|
82
|
+
2 if user input or configuration is faulty
|
|
83
|
+
"""
|
|
84
|
+
USAGE = """
|
|
85
|
+
intelmqcli
|
|
86
|
+
intelmqcli --dry-run
|
|
87
|
+
intelmqcli --verbose
|
|
88
|
+
intelmqcli --batch
|
|
89
|
+
intelmqcli --quiet
|
|
90
|
+
intelmqcli --compress-csv
|
|
91
|
+
intelmqcli --list-feeds
|
|
92
|
+
intelmqcli --list-identifiers
|
|
93
|
+
intelmqcli --list-taxonomies
|
|
94
|
+
intelmqcli --taxonomy='taxonomy'
|
|
95
|
+
intelmqcli --type='type'
|
|
96
|
+
intelmqcli --identifier='identifier'
|
|
97
|
+
intelmqcli --list-types
|
|
98
|
+
intelmqcli --list-texts
|
|
99
|
+
intelmqcli --text='boilerplate name'
|
|
100
|
+
intelmqcli --feed='feedcode' """
|
|
101
|
+
|
|
102
|
+
SUBJECT = {
|
|
103
|
+
"abusive-content": "Abusive content (spam, ...)",
|
|
104
|
+
"malicious code": "Malicious code (malware, botnet, ...)",
|
|
105
|
+
"malicious-code": "Malicious code (malware, botnet, ...)",
|
|
106
|
+
"information-gathering": "Information Gathering (scanning, ...)",
|
|
107
|
+
"intrusion-attempts": "Intrusion Attempt",
|
|
108
|
+
"intrusions": "Network intrusion",
|
|
109
|
+
"availability": "Availability (DDOS, ...)",
|
|
110
|
+
"information-content-security": "Information Content Security (dropzone,...)",
|
|
111
|
+
"fraud": "Fraud",
|
|
112
|
+
"vulnerable": "Vulnerable device",
|
|
113
|
+
"other": "Other",
|
|
114
|
+
"test": "Test",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
QUERY_FEED_NAMES = 'SELECT DISTINCT "feed.code" from {events}'
|
|
118
|
+
|
|
119
|
+
QUERY_IDENTIFIER_NAMES = 'SELECT DISTINCT "classification.identifier" from {events}'
|
|
120
|
+
|
|
121
|
+
QUERY_TAXONOMY_NAMES = 'SELECT DISTINCT "classification.taxonomy" from {events}'
|
|
122
|
+
|
|
123
|
+
QUERY_TYPE_NAMES = 'SELECT DISTINCT "classification.type" from {events}'
|
|
124
|
+
|
|
125
|
+
QUERY_TEXT_NAMES = 'SELECT DISTINCT "key" from {boilerplates}'
|
|
126
|
+
|
|
127
|
+
""" This is the list of fields (and their respective order) which we intend to
|
|
128
|
+
send out. This is based on the order and fields of shadowserver.
|
|
129
|
+
|
|
130
|
+
Shadowserver format:
|
|
131
|
+
timestamp,"ip","protocol","port","hostname","packets","size","asn","geo","region","city","naics","sic","sector"
|
|
132
|
+
"""
|
|
133
|
+
CSV_FIELDS = [
|
|
134
|
+
"time.source",
|
|
135
|
+
"source.ip",
|
|
136
|
+
"protocol.transport",
|
|
137
|
+
"source.port",
|
|
138
|
+
"protocol.application",
|
|
139
|
+
"source.fqdn",
|
|
140
|
+
"source.local_hostname",
|
|
141
|
+
"source.local_ip",
|
|
142
|
+
"source.url",
|
|
143
|
+
"source.asn",
|
|
144
|
+
"source.geolocation.cc",
|
|
145
|
+
"source.geolocation.city",
|
|
146
|
+
"classification.taxonomy",
|
|
147
|
+
"classification.type",
|
|
148
|
+
"classification.identifier",
|
|
149
|
+
"destination.ip",
|
|
150
|
+
"destination.port",
|
|
151
|
+
"destination.fqdn",
|
|
152
|
+
"destination.url",
|
|
153
|
+
"feed",
|
|
154
|
+
"event_description.text",
|
|
155
|
+
"event_description.url",
|
|
156
|
+
"malware.name",
|
|
157
|
+
"extra",
|
|
158
|
+
"comment",
|
|
159
|
+
"additional_field_freetext",
|
|
160
|
+
"feed.documentation",
|
|
161
|
+
"version: 1.2",
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
QUERY_UPDATE_CONTACT = """
|
|
165
|
+
UPDATE as_contacts SET
|
|
166
|
+
contacts = %s
|
|
167
|
+
WHERE
|
|
168
|
+
asnum = %s
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
QUERY_INSERT_CONTACT = """
|
|
172
|
+
INSERT INTO as_contacts (
|
|
173
|
+
asnum, contacts, comment, unreliable
|
|
174
|
+
) VALUES (
|
|
175
|
+
%s, %s, %s, FALSE
|
|
176
|
+
)
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
QUERY_GET_TEXT = """
|
|
180
|
+
SELECT
|
|
181
|
+
body
|
|
182
|
+
FROM {texttab}
|
|
183
|
+
WHERE
|
|
184
|
+
key = %s
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
_BASE_FILTERS = """
|
|
188
|
+
"notify" = TRUE AND
|
|
189
|
+
"time.source" >= now() - interval %s AND
|
|
190
|
+
"sent_at" IS NULL AND
|
|
191
|
+
"feed.code" IS NOT NULL AND
|
|
192
|
+
"classification.taxonomy" IS NOT NULL AND
|
|
193
|
+
(UPPER("source.geolocation.cc") = %s OR lower("source.fqdn") similar to %s
|
|
194
|
+
OR lower("source.reverse_dns") similar to %s {asset})
|
|
195
|
+
"""
|
|
196
|
+
BASE_WHERE = (
|
|
197
|
+
"""
|
|
198
|
+
"source.abuse_contact" IS NOT NULL AND
|
|
199
|
+
"""
|
|
200
|
+
+ _BASE_FILTERS
|
|
201
|
+
)
|
|
202
|
+
_MONITORED_ASSETS_FILTER = """
|
|
203
|
+
OR "extra" ->> 'monitored_asset' != ''
|
|
204
|
+
"""
|
|
205
|
+
# PART 1: CREATE REPORTS
|
|
206
|
+
_OPEN_FEEDCODES = """
|
|
207
|
+
SELECT
|
|
208
|
+
DISTINCT "feed.code"
|
|
209
|
+
FROM "{events}"
|
|
210
|
+
WHERE
|
|
211
|
+
"rtir_report_id" IS NULL AND
|
|
212
|
+
"""
|
|
213
|
+
QUERY_OPEN_FEEDCODES = _OPEN_FEEDCODES + BASE_WHERE
|
|
214
|
+
_OPEN_EVENTS = """
|
|
215
|
+
SELECT *
|
|
216
|
+
FROM "{events}"
|
|
217
|
+
WHERE
|
|
218
|
+
"feed.code" = %s AND
|
|
219
|
+
"rtir_report_id" IS NULL AND
|
|
220
|
+
"""
|
|
221
|
+
QUERY_OPEN_EVENTS_BY_FEEDCODE = _OPEN_EVENTS + BASE_WHERE
|
|
222
|
+
# For internal-only notifications, ignore abuse data
|
|
223
|
+
INTERNAL_QUERY_OPEN_FEEDCODES = _OPEN_FEEDCODES + _BASE_FILTERS
|
|
224
|
+
INTERNAL_QUERY_OPEN_EVENTS_BY_FEEDCODE = _OPEN_EVENTS + _BASE_FILTERS
|
|
225
|
+
# PART 2: INCIDENTS
|
|
226
|
+
QUERY_OPEN_TAXONOMIES = (
|
|
227
|
+
"""
|
|
228
|
+
SELECT
|
|
229
|
+
DISTINCT "classification.taxonomy"
|
|
230
|
+
FROM "{events}"
|
|
231
|
+
WHERE
|
|
232
|
+
"rtir_report_id" IS NOT NULL AND
|
|
233
|
+
"rtir_incident_id" IS NULL AND
|
|
234
|
+
"""
|
|
235
|
+
+ BASE_WHERE
|
|
236
|
+
)
|
|
237
|
+
QUERY_OPEN_EVENT_REPORTS_BY_TAXONOMY = (
|
|
238
|
+
"""
|
|
239
|
+
SELECT
|
|
240
|
+
DISTINCT "rtir_report_id"
|
|
241
|
+
FROM "{events}"
|
|
242
|
+
WHERE
|
|
243
|
+
"rtir_report_id" IS NOT NULL AND
|
|
244
|
+
"rtir_incident_id" IS NULL AND
|
|
245
|
+
"classification.taxonomy" = %s AND
|
|
246
|
+
"""
|
|
247
|
+
+ BASE_WHERE
|
|
248
|
+
)
|
|
249
|
+
QUERY_OPEN_EVENT_IDS_BY_TAXONOMY = (
|
|
250
|
+
"""
|
|
251
|
+
SELECT
|
|
252
|
+
"id"
|
|
253
|
+
FROM "{events}"
|
|
254
|
+
WHERE
|
|
255
|
+
"rtir_report_id" IS NOT NULL AND
|
|
256
|
+
"rtir_incident_id" IS NULL AND
|
|
257
|
+
"classification.taxonomy" = %s AND
|
|
258
|
+
"""
|
|
259
|
+
+ BASE_WHERE
|
|
260
|
+
)
|
|
261
|
+
QUERY_HALF_PROC_INCIDENTS = (
|
|
262
|
+
"""
|
|
263
|
+
SELECT
|
|
264
|
+
DISTINCT "rtir_incident_id",
|
|
265
|
+
"classification.taxonomy"
|
|
266
|
+
FROM "{events}"
|
|
267
|
+
WHERE
|
|
268
|
+
"rtir_report_id" IS NOT NULL AND
|
|
269
|
+
"rtir_incident_id" IS NOT NULL AND
|
|
270
|
+
rtir_investigation_id IS NULL AND
|
|
271
|
+
"""
|
|
272
|
+
+ BASE_WHERE
|
|
273
|
+
)
|
|
274
|
+
# PART 3: INVESTIGATIONS
|
|
275
|
+
QUERY_DISTINCT_CONTACTS_BY_INCIDENT = (
|
|
276
|
+
"""
|
|
277
|
+
SELECT
|
|
278
|
+
DISTINCT "source.abuse_contact"
|
|
279
|
+
FROM {events}
|
|
280
|
+
WHERE
|
|
281
|
+
rtir_report_id IS NOT NULL AND
|
|
282
|
+
rtir_incident_id = %s AND
|
|
283
|
+
rtir_investigation_id IS NULL AND
|
|
284
|
+
"""
|
|
285
|
+
+ BASE_WHERE
|
|
286
|
+
)
|
|
287
|
+
DRY_QUERY_DISTINCT_CONTACTS_BY_TAXONOMY = (
|
|
288
|
+
"""
|
|
289
|
+
SELECT
|
|
290
|
+
DISTINCT "source.abuse_contact"
|
|
291
|
+
FROM {events}
|
|
292
|
+
WHERE
|
|
293
|
+
rtir_report_id IS NOT NULL AND
|
|
294
|
+
"rtir_incident_id" IS NULL AND
|
|
295
|
+
rtir_investigation_id IS NULL AND
|
|
296
|
+
"classification.taxonomy" = %s AND
|
|
297
|
+
"""
|
|
298
|
+
+ BASE_WHERE
|
|
299
|
+
)
|
|
300
|
+
QUERY_EVENTS_BY_ASCONTACT_INCIDENT = (
|
|
301
|
+
"""
|
|
302
|
+
SELECT
|
|
303
|
+
to_char("time.source",
|
|
304
|
+
'YYYY-MM-DD"T"HH24:MI:SSOF') as "time.source",
|
|
305
|
+
id,
|
|
306
|
+
"feed.code" as feed,
|
|
307
|
+
"source.ip",
|
|
308
|
+
"source.port",
|
|
309
|
+
"source.url",
|
|
310
|
+
"source.asn",
|
|
311
|
+
"source.geolocation.cc",
|
|
312
|
+
"source.geolocation.city",
|
|
313
|
+
"source.fqdn",
|
|
314
|
+
"source.local_hostname",
|
|
315
|
+
"source.local_ip",
|
|
316
|
+
"classification.identifier",
|
|
317
|
+
"classification.taxonomy",
|
|
318
|
+
"classification.type",
|
|
319
|
+
"comment",
|
|
320
|
+
"destination.ip",
|
|
321
|
+
"destination.port",
|
|
322
|
+
"destination.fqdn",
|
|
323
|
+
"destination.url",
|
|
324
|
+
"event_description.text",
|
|
325
|
+
"event_description.url",
|
|
326
|
+
"shareable_extra_info" AS "extra",
|
|
327
|
+
"feed.documentation",
|
|
328
|
+
"malware.name",
|
|
329
|
+
"protocol.application",
|
|
330
|
+
"protocol.transport"
|
|
331
|
+
FROM {v_events_filtered}
|
|
332
|
+
WHERE
|
|
333
|
+
rtir_report_id IS NOT NULL AND
|
|
334
|
+
rtir_incident_id = %s AND
|
|
335
|
+
rtir_investigation_id IS NULL AND
|
|
336
|
+
"source.abuse_contact" = %s AND
|
|
337
|
+
"""
|
|
338
|
+
+ BASE_WHERE
|
|
339
|
+
)
|
|
340
|
+
DRY_QUERY_EVENTS_BY_ASCONTACT_TAXONOMY = (
|
|
341
|
+
QUERY_EVENTS_BY_ASCONTACT_INCIDENT[
|
|
342
|
+
: QUERY_EVENTS_BY_ASCONTACT_INCIDENT.find("WHERE") + 6
|
|
343
|
+
]
|
|
344
|
+
+ """
|
|
345
|
+
rtir_report_id IS NOT NULL AND
|
|
346
|
+
rtir_investigation_id IS NULL AND
|
|
347
|
+
"classification.taxonomy" = %s AND
|
|
348
|
+
"source.abuse_contact" = %s AND
|
|
349
|
+
"""
|
|
350
|
+
+ BASE_WHERE
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
DEFAULT_TABLES = {
|
|
354
|
+
"events": "events",
|
|
355
|
+
"v_events_filtered": "v_events_filtered",
|
|
356
|
+
"boilerplates": "boilerplates",
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def getTerminalHeight():
|
|
361
|
+
try:
|
|
362
|
+
return int(subprocess.check_output(["stty", "size"]).strip().split()[0])
|
|
363
|
+
except Exception:
|
|
364
|
+
return 80 # If running in
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class IntelMQCLIContollerTemplate:
|
|
368
|
+
additional_where = ""
|
|
369
|
+
usage = ""
|
|
370
|
+
epilog = ""
|
|
371
|
+
additional_params = ()
|
|
372
|
+
dryrun = False
|
|
373
|
+
quiet = False
|
|
374
|
+
|
|
375
|
+
def __init__(self, overridden_config: dict = None):
|
|
376
|
+
self._asset_filter = ""
|
|
377
|
+
self.overridden_config = overridden_config
|
|
378
|
+
|
|
379
|
+
usage_configuration = (
|
|
380
|
+
"\n\nThe configuration can be found by default at %r."
|
|
381
|
+
% INTELMQCLI_CONF_FILE
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
self.parser = argparse.ArgumentParser(
|
|
385
|
+
prog=self.appname,
|
|
386
|
+
usage=self.usage + usage_configuration,
|
|
387
|
+
epilog=self.epilog,
|
|
388
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
389
|
+
)
|
|
390
|
+
VERSION = pkg_resources.get_distribution("intelmq").version
|
|
391
|
+
self.parser.add_argument("--version", action="version", version=VERSION)
|
|
392
|
+
self.parser.add_argument(
|
|
393
|
+
"-v", "--verbose", action="store_true", help="Print verbose messages."
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
self.parser.add_argument(
|
|
397
|
+
"--config", help="Config file path", default=INTELMQCLI_CONF_FILE
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
self.parser.add_argument(
|
|
401
|
+
"-f",
|
|
402
|
+
"--feed",
|
|
403
|
+
nargs="+",
|
|
404
|
+
help="Show only incidents reported by one of the given feeds.",
|
|
405
|
+
)
|
|
406
|
+
self.parser.add_argument(
|
|
407
|
+
"--skip-feed",
|
|
408
|
+
nargs="+",
|
|
409
|
+
help="Skip incidents reported by one of the given feeds.",
|
|
410
|
+
)
|
|
411
|
+
self.parser.add_argument(
|
|
412
|
+
"--taxonomy", nargs="+", help="Select only events with given taxonomy."
|
|
413
|
+
)
|
|
414
|
+
self.parser.add_argument(
|
|
415
|
+
"--skip-taxonomy", nargs="+", help="Skip events with given taxonomy."
|
|
416
|
+
)
|
|
417
|
+
self.parser.add_argument(
|
|
418
|
+
"-a",
|
|
419
|
+
"--asn",
|
|
420
|
+
type=int,
|
|
421
|
+
nargs="+",
|
|
422
|
+
help="Specify one or more AS numbers (integers) to process.",
|
|
423
|
+
)
|
|
424
|
+
self.parser.add_argument(
|
|
425
|
+
"--skip-asn",
|
|
426
|
+
type=int,
|
|
427
|
+
nargs="+",
|
|
428
|
+
help="Specify one or more AS numbers (integers) to skip.",
|
|
429
|
+
)
|
|
430
|
+
self.parser.add_argument(
|
|
431
|
+
"--type",
|
|
432
|
+
nargs="+",
|
|
433
|
+
help="Specify one or more classifications types to process.",
|
|
434
|
+
)
|
|
435
|
+
self.parser.add_argument(
|
|
436
|
+
"--skip-type",
|
|
437
|
+
nargs="+",
|
|
438
|
+
help="Specify one or more classifications types to skip.",
|
|
439
|
+
)
|
|
440
|
+
self.parser.add_argument(
|
|
441
|
+
"--identifier",
|
|
442
|
+
nargs="+",
|
|
443
|
+
help="Specify one or more classifications identifiers to process.",
|
|
444
|
+
)
|
|
445
|
+
self.parser.add_argument(
|
|
446
|
+
"--skip-identifier",
|
|
447
|
+
nargs="+",
|
|
448
|
+
help="Specify one or more classifications identifiers to skip.",
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
self.parser.add_argument(
|
|
452
|
+
"-b",
|
|
453
|
+
"--batch",
|
|
454
|
+
action="store_true",
|
|
455
|
+
help='Run in batch mode (defaults to "yes" to all).',
|
|
456
|
+
)
|
|
457
|
+
self.parser.add_argument(
|
|
458
|
+
"-q",
|
|
459
|
+
"--quiet",
|
|
460
|
+
action="store_true",
|
|
461
|
+
help="Do not output anything, except for error messages."
|
|
462
|
+
" Useful in combination with --batch.",
|
|
463
|
+
)
|
|
464
|
+
self.parser.add_argument(
|
|
465
|
+
"-n",
|
|
466
|
+
"--dry-run",
|
|
467
|
+
action="store_true",
|
|
468
|
+
help="Do not store anything or change anything. Just simulate.",
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
self.parser.add_argument(
|
|
472
|
+
"--time-interval",
|
|
473
|
+
nargs="+",
|
|
474
|
+
default="4 days",
|
|
475
|
+
help="time interval, parseable by postgres." 'defaults to "4 days".',
|
|
476
|
+
)
|
|
477
|
+
self.parser.add_argument(
|
|
478
|
+
"--ton",
|
|
479
|
+
"--time-observation-newer-than",
|
|
480
|
+
nargs=1,
|
|
481
|
+
help="Select only events with 'time.observation' newer than the "
|
|
482
|
+
"given ISO-formatted datetime.",
|
|
483
|
+
)
|
|
484
|
+
self.parser.add_argument(
|
|
485
|
+
"--monitored-assets",
|
|
486
|
+
action="store_true",
|
|
487
|
+
help="Include tickets that has extra.monitored_asset regardless of geo-filters",
|
|
488
|
+
)
|
|
489
|
+
self.parser.add_argument(
|
|
490
|
+
"--stdout-only", action="store_true", help="Log only to stdout"
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
def setup(self, args: list):
|
|
494
|
+
self.args = self.parser.parse_args(args)
|
|
495
|
+
|
|
496
|
+
self.config = self._load_config()
|
|
497
|
+
|
|
498
|
+
if self.args.verbose:
|
|
499
|
+
self.verbose = True
|
|
500
|
+
if self.args.dry_run:
|
|
501
|
+
self.dryrun = True
|
|
502
|
+
if self.args.batch:
|
|
503
|
+
self.batch = True
|
|
504
|
+
if self.args.quiet:
|
|
505
|
+
self.quiet = True
|
|
506
|
+
self.time_interval = "".join(self.args.time_interval)
|
|
507
|
+
|
|
508
|
+
if self.quiet:
|
|
509
|
+
stream = None
|
|
510
|
+
else:
|
|
511
|
+
stream = sys.stderr
|
|
512
|
+
|
|
513
|
+
self.logger = utils.log(
|
|
514
|
+
"intelmqcli",
|
|
515
|
+
syslog=(
|
|
516
|
+
None if self.args.stdout_only else self.config.get("syslog", "/dev/log")
|
|
517
|
+
),
|
|
518
|
+
log_level=self.config["log_level"].upper(),
|
|
519
|
+
stream=stream,
|
|
520
|
+
log_format_stream="%(message)s",
|
|
521
|
+
log_path=(
|
|
522
|
+
None
|
|
523
|
+
if self.args.stdout_only
|
|
524
|
+
else self.config.get("log_path", intelmq.DEFAULT_LOGGING_PATH)
|
|
525
|
+
),
|
|
526
|
+
)
|
|
527
|
+
self.logger.info(
|
|
528
|
+
"Started %r at %s.", " ".join(sys.argv), datetime.datetime.now().isoformat()
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
if self.args.feed:
|
|
532
|
+
self.additional_where += """ AND "feed.code" = ANY(%s::VARCHAR[]) """
|
|
533
|
+
self.additional_params += ("{" + ",".join(self.args.feed) + "}",)
|
|
534
|
+
if self.args.skip_feed:
|
|
535
|
+
self.additional_where += """ AND "feed.code" != ANY(%s::VARCHAR[]) """
|
|
536
|
+
self.additional_params += ("{" + ",".join(self.args.skip_feed) + "}",)
|
|
537
|
+
if self.args.asn:
|
|
538
|
+
self.additional_where += """ AND "source.asn" = ANY(%s::INT[]) """
|
|
539
|
+
self.additional_params += ("{" + ",".join(map(str, self.args.asn)) + "}",)
|
|
540
|
+
if self.args.skip_asn:
|
|
541
|
+
self.additional_where += """ AND "source.asn" != ANY(%s::INT[]) """
|
|
542
|
+
self.additional_params += (
|
|
543
|
+
"{" + ",".join(map(str, self.args.skip_asn)) + "}",
|
|
544
|
+
)
|
|
545
|
+
if self.args.taxonomy:
|
|
546
|
+
self.additional_where += (
|
|
547
|
+
""" AND "classification.taxonomy" = ANY(%s::VARCHAR[]) """
|
|
548
|
+
)
|
|
549
|
+
self.additional_params += ("{" + ",".join(self.args.taxonomy) + "}",)
|
|
550
|
+
if self.args.skip_taxonomy:
|
|
551
|
+
self.additional_where += (
|
|
552
|
+
""" AND "classification.taxonomy" != ANY(%s::VARCHAR[]) """
|
|
553
|
+
)
|
|
554
|
+
self.additional_params += ("{" + ",".join(self.args.skip_taxonomy) + "}",)
|
|
555
|
+
if self.args.type:
|
|
556
|
+
self.additional_where += (
|
|
557
|
+
""" AND "classification.type" = ANY(%s::VARCHAR[]) """
|
|
558
|
+
)
|
|
559
|
+
self.additional_params += ("{" + ",".join(self.args.type) + "}",)
|
|
560
|
+
if self.args.skip_type:
|
|
561
|
+
self.additional_where += (
|
|
562
|
+
""" AND "classification.type" != ANY(%s::VARCHAR[]) """
|
|
563
|
+
)
|
|
564
|
+
self.additional_params += ("{" + ",".join(self.args.skip_type) + "}",)
|
|
565
|
+
if self.args.identifier:
|
|
566
|
+
self.additional_where += (
|
|
567
|
+
""" AND "classification.identifier" = ANY(%s::VARCHAR[]) """
|
|
568
|
+
)
|
|
569
|
+
self.additional_params += ("{" + ",".join(self.args.identifier) + "}",)
|
|
570
|
+
if self.args.skip_identifier:
|
|
571
|
+
self.additional_where += (
|
|
572
|
+
""" AND "classification.identifier" != ALL(%s::VARCHAR[]) """
|
|
573
|
+
)
|
|
574
|
+
self.additional_params += ("{" + ",".join(self.args.skip_identifier) + "}",)
|
|
575
|
+
if self.args.ton:
|
|
576
|
+
self.additional_where += """ AND "time.observation" >= %s """
|
|
577
|
+
self.additional_params += (self.args.ton[0],)
|
|
578
|
+
if self.args.monitored_assets:
|
|
579
|
+
self._asset_filter = _MONITORED_ASSETS_FILTER
|
|
580
|
+
|
|
581
|
+
if self.config.get("constituency"):
|
|
582
|
+
conditions = []
|
|
583
|
+
if self.config["constituency"].get("default"):
|
|
584
|
+
conditions.append(""" "constituency" is null """)
|
|
585
|
+
|
|
586
|
+
if key := self.config["constituency"].get("key"):
|
|
587
|
+
conditions.append(""" "constituency" = %s """)
|
|
588
|
+
self.additional_params += (key,)
|
|
589
|
+
|
|
590
|
+
if conditions:
|
|
591
|
+
self.additional_where += f""" AND ({' OR '.join(conditions)}) """
|
|
592
|
+
self.logger.debug("Initializing with constituency: %s", conditions)
|
|
593
|
+
|
|
594
|
+
self.rt = rt.Rt(
|
|
595
|
+
self.config["rt"]["uri"],
|
|
596
|
+
self.config["rt"]["user"],
|
|
597
|
+
self.config["rt"]["password"],
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
def _load_config(self):
|
|
601
|
+
if self.overridden_config:
|
|
602
|
+
return copy.deepcopy(self.overridden_config)
|
|
603
|
+
|
|
604
|
+
with open(self.args.config) as conf_handle:
|
|
605
|
+
return json.load(conf_handle)
|
|
606
|
+
|
|
607
|
+
def connect_database(self):
|
|
608
|
+
self.con = psycopg2.connect(
|
|
609
|
+
database=self.config["database"]["database"],
|
|
610
|
+
user=self.config["database"]["user"],
|
|
611
|
+
password=self.config["database"]["password"],
|
|
612
|
+
host=self.config["database"]["host"],
|
|
613
|
+
port=self.config["database"]["port"],
|
|
614
|
+
sslmode=self.config["database"]["sslmode"],
|
|
615
|
+
)
|
|
616
|
+
self.con.autocommit = False # Starts transaction in the beginning
|
|
617
|
+
self.cur = self.con.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
|
618
|
+
|
|
619
|
+
def execute(self, query, parameters=(), extend=True):
|
|
620
|
+
"""
|
|
621
|
+
Passes query to database.
|
|
622
|
+
|
|
623
|
+
Parameters:
|
|
624
|
+
extend:
|
|
625
|
+
If True, the parameters for BASE_WHERE are added (time interval,
|
|
626
|
+
country code, FQDN from config, and additional parameters)
|
|
627
|
+
If False, parameters are used as given.
|
|
628
|
+
"""
|
|
629
|
+
if extend:
|
|
630
|
+
query = query + self.additional_where
|
|
631
|
+
parameters = (
|
|
632
|
+
parameters
|
|
633
|
+
+ (
|
|
634
|
+
self.time_interval,
|
|
635
|
+
self.config["filter"]["cc"],
|
|
636
|
+
"%%.(%s)" % self.config["filter"]["fqdn"],
|
|
637
|
+
"%%.(%s)" % self.config["filter"]["fqdn"],
|
|
638
|
+
)
|
|
639
|
+
+ self.additional_params
|
|
640
|
+
)
|
|
641
|
+
query = self._format_query(query)
|
|
642
|
+
self.logger.debug(self.cur.mogrify(query, parameters))
|
|
643
|
+
if not self.dryrun or query.strip().upper().startswith("SELECT"):
|
|
644
|
+
self.cur.execute(query, parameters)
|
|
645
|
+
|
|
646
|
+
def _format_query(self, query: str):
|
|
647
|
+
return query.format(
|
|
648
|
+
**self.config.get("tables", DEFAULT_TABLES), asset=self._asset_filter
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
def executemany(self, query, parameters=(), extend=True):
|
|
652
|
+
"""Passes query to database."""
|
|
653
|
+
if extend:
|
|
654
|
+
query = query + self.additional_where
|
|
655
|
+
parameters = [
|
|
656
|
+
param + (self.time_interval,) + self.additional_params
|
|
657
|
+
for param in parameters
|
|
658
|
+
]
|
|
659
|
+
query = self._format_query(query)
|
|
660
|
+
if (
|
|
661
|
+
self.config["log_level"].upper() == "DEBUG"
|
|
662
|
+
): # on other log levels we can skip the iteration
|
|
663
|
+
for param in parameters:
|
|
664
|
+
self.logger.debug(self.cur.mogrify(query, param))
|
|
665
|
+
if not parameters:
|
|
666
|
+
self.logger.debug(self.cur.mogrify(query))
|
|
667
|
+
if not self.dryrun or query.strip().upper().startswith(
|
|
668
|
+
"SELECT"
|
|
669
|
+
): # no update in dry run
|
|
670
|
+
self.cur.executemany(query, parameters)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from intelmq import HARMONIZATION_CONF_FILE
|
|
2
|
+
from intelmq.lib.utils import load_configuration
|
|
3
|
+
from mergedeep import merge
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def merge_harmonization(
|
|
7
|
+
additional_definitions: list[dict], harmonization_path: str = None
|
|
8
|
+
):
|
|
9
|
+
harmonization_path = harmonization_path or HARMONIZATION_CONF_FILE
|
|
10
|
+
harmonization = load_configuration(harmonization_path)
|
|
11
|
+
|
|
12
|
+
return merge(*additional_definitions, harmonization)
|