Skip to content

Commit b86e313

Browse files
committed
enh: collector_mail_attach Decrypt GPG attachments
1 parent 479114d commit b86e313

19 files changed

+276
-20
lines changed

.github/workflows/unittests.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ jobs:
7272
- name: Run basic testsuite
7373
if: ${{ matrix.type == 'basic' }}
7474
run: pytest --cov intelmq/ --cov-report=xml --cov-branch intelmq/
75-
75+
- name: Assure permissions
76+
run: chmod -R u=rwX,go= intelmq/tests/bots/collectors/mail/gpg_ring/
7677
- name: Run full testsuite
7778
if: ${{ matrix.type == 'full' }}
7879
run: pytest --cov intelmq/ --cov-report=xml --cov-branch intelmq/ contrib/
@@ -81,3 +82,4 @@ jobs:
8182
INTELMQ_TEST_DATABASES: 1
8283
INTELMQ_TEST_EXOTIC: 1
8384
INTELMQ_TEST_INSTALLATION: 1
85+
GPG_RING_PATH: intelmq/tests/bots/collectors/mail/gpg_ring/

.reuse/dep5

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,15 @@ License: AGPL-3.0-or-later
3333
Files: docs/_static/n6/data-flow.png docs/_static/n6/n6-schemat2.png
3434
Copyright: CERT.pl <[email protected]>
3535
License: AGPL-3.0-only
36+
37+
Files: intelmq/tests/bots/collectors/mail/gpg_attachment.eml
38+
Copyright: 2025 Edvard Rejthar, CSIRT.cz <[email protected]>
39+
License: AGPL-3.0-or-later
40+
41+
Files: intelmq/tests/bots/collectors/mail/gpg_wrong_attachment.eml
42+
Copyright: 2025 Edvard Rejthar, CSIRT.cz <[email protected]>
43+
License: AGPL-3.0-or-later
44+
45+
Files: intelmq/tests/bots/collectors/mail/gpg_ring/**
46+
Copyright: 2025 Edvard Rejthar, CSIRT.cz <[email protected]>
47+
License: AGPL-3.0-or-later

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Please refer to the [NEWS](NEWS.md) for a list of changes which have an affect o
2424

2525
### Bots
2626
#### Collectors
27+
- `intelmq.bots.collectors.mail.collector_mail_attach`: Decrypt GPG attachments (PR#2623 by Edvard Rejthar).
2728

2829
#### Parsers
2930
- `intelmq.bots.parsers.cymru.parser_cap_program`: Add mapping for TOR and ipv6-icmp protocol (PR#2621 by Mikk Margus Möll).

intelmq/bots/collectors/mail/REQUIREMENTS.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
# SPDX-License-Identifier: AGPL-3.0-or-later
33

44
imbox>=0.8.5
5+
python-gnupg>=0.5

intelmq/bots/collectors/mail/collector_mail_attach.py

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@
1010
1111
Uses the common mail iteration method from the lib file.
1212
"""
13+
from functools import cache, cached_property
1314
import re
15+
1416
from intelmq.lib.utils import unzip
15-
from intelmq.lib.exceptions import InvalidArgument
17+
from intelmq.lib.exceptions import InvalidArgument, MissingDependencyError
1618

1719
from ._lib import MailCollectorBot
1820

1921

2022
class MailAttachCollectorBot(MailCollectorBot):
2123
"""Monitor IMAP mailboxes and retrieve mail attachments"""
24+
2225
attach_regex: str = "csv.zip"
2326
extract_files: bool = True
2427
folder: str = "INBOX"
@@ -28,11 +31,19 @@ class MailAttachCollectorBot(MailCollectorBot):
2831
mail_user: str = "<user>"
2932
rate_limit: int = 60
3033
subject_regex: str = "<subject>"
34+
decrypt: bool = False
35+
""" Decrypt the attachment with GPG """
36+
37+
gpg_passphrase: str = ""
38+
""" The private key passhrase """
39+
40+
gpg_home: str = ""
41+
""" Change the GPG home directory """
3142

3243
def init(self):
3344
super().init()
3445
if self.attach_regex is None:
35-
raise InvalidArgument('attach_regex', expected='string')
46+
raise InvalidArgument("attach_regex", expected="string")
3647

3748
def process_message(self, uid, message):
3849
seen = False
@@ -42,12 +53,14 @@ def process_message(self, uid, message):
4253
continue
4354

4455
try:
45-
attach_filename = attach['filename']
56+
attach_filename = attach["filename"]
4657
except KeyError:
4758
# https://github.com/certtools/intelmq/issues/1538
48-
self.logger.debug('Skipping attachment because of missing filename.')
59+
self.logger.debug("Skipping attachment because of missing filename.")
4960
continue
50-
if attach_filename.startswith('"'): # for imbox versions older than 0.9.5, see also above
61+
if attach_filename.startswith(
62+
'"'
63+
): # for imbox versions older than 0.9.5, see also above
5164
attach_filename = attach_filename[1:-1]
5265

5366
if re.search(self.attach_regex, attach_filename):
@@ -57,18 +70,33 @@ def process_message(self, uid, message):
5770
report = self.new_report()
5871

5972
if self.extract_files:
60-
raw_reports = unzip(attach['content'].read(), self.extract_files,
61-
return_names=True, logger=self.logger)
73+
raw_reports = unzip(
74+
attach["content"].read(),
75+
self.extract_files,
76+
return_names=True,
77+
logger=self.logger,
78+
)
6279
else:
63-
raw_reports = ((attach_filename, attach['content'].read()), )
80+
raw_reports = ((attach_filename, attach["content"].read()),)
6481

6582
for file_name, raw_report in raw_reports:
83+
if self.decrypt:
84+
gpg = self._gpg().decrypt(
85+
raw_report, passphrase=self.gpg_passphrase
86+
)
87+
if gpg.ok:
88+
raw_report = gpg.data
89+
else:
90+
self.logger.error('Could not decrypt attachment %s: %s.', file_name, gpg.status)
91+
continue
6692
report = self.new_report()
6793
report.add("raw", raw_report)
6894
if file_name:
6995
report.add("extra.file_name", file_name)
7096
report["extra.email_subject"] = message.subject
71-
report["extra.email_from"] = ','.join(x['email'] for x in message.sent_from)
97+
report["extra.email_from"] = ",".join(
98+
x["email"] for x in message.sent_from
99+
)
72100
report["extra.email_message_id"] = message.message_id
73101
report["extra.email_date"] = message.date
74102
self.send_message(report)
@@ -80,5 +108,13 @@ def process_message(self, uid, message):
80108
self.logger.info("Email report read.")
81109
return seen
82110

111+
@cache
112+
def _gpg(self):
113+
try:
114+
from gnupg import GPG
115+
except ImportError:
116+
raise MissingDependencyError("python-gnupg", ">=0.5")
117+
return GPG(gnupghome=self.gpg_home)
118+
83119

84120
BOT = MailAttachCollectorBot
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
MIME-Version: 1.0
2+
Content-Type: multipart/mixed; boundary="===============0256050913907025376=="
3+
Subject: foobar zip
4+
From: Sebastian Wagner <[email protected]>
5+
6+
Message-ID: <[email protected]>
7+
Date: Tue, 3 Sep 2019 16:57:40 +0200
8+
Content-Language: en-US
9+
10+
--===============0256050913907025376==
11+
Content-Type: text/html; charset="utf-8"
12+
Content-Transfer-Encoding: 7bit
13+
14+
<html>
15+
<head>
16+
17+
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
18+
</head>
19+
<body text="#000000" bgcolor="#FFFFFF">
20+
Please look at the attachment<br>
21+
</body>
22+
</html>
23+
24+
--===============0256050913907025376==
25+
Content-Type: application/octet-stream
26+
Content-Transfer-Encoding: base64
27+
Content-Disposition: attachment; filename="foobar.txt.gpg"
28+
MIME-Version: 1.0
29+
30+
hQGMA9ig68HPFWOpAQv/fsRXK1mdmGw83CdbIOZeUlFVx2KKEONWVYIu/CHPxqxREYUWTva+XxYE
31+
LirKR9xK9lG19H6BSSZD3xXMahAShRzgs1dHH4UMxGqdpiXo5DbdVkTS7kg5+hG7QX20x+ExwsRd
32+
r76/gzY+Lmsv4hydxMHyG/w0OBsV/0mLWGAAVzer9Y8xkUP6jB16XzoSQGkfCP+7DpK224135JgU
33+
E6XSD2p+WKzALxXiET4IfinJnG8sSTHlkCQZgBp8lpDPdP2BbHo0KmvAofxaRqkvMfpVk7kUyy2W
34+
8tENKSEPu0sKEX8HZhT8Sw3X2pn8ggbAadHRjOzOO58MUvWJvwURqa1LIITA4pmyhw4svp1UzODb
35+
Q2Cd+MyWikjD4CebJ7P/JpnoDokux9qxWKnuuvmM4mqr6bvL+Sztud4V2Z60idokbzuEaJR6ZztT
36+
SBMatdh/OHcUnCaYCJiEOlrgk0hYcZ12XR5C8OBAI4St9F5eGjMB0l1tZz7CeQ6MrAoPY2pZ54XO
37+
0k0BA3PpG+K/khPOns8rW2mlYKgWfJqRdc91EasA4+iFoM+iFwyDQGBKbAdCu0BeIf9j7t5s5LOG
38+
iPWI5m2AIWf+aFNqKotGFAWXeSbMWw==
39+
40+
--===============0256050913907025376==--
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v:1:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
This is a revocation certificate for the OpenPGP key:
2+
3+
pub rsa3072 2019-12-11 [S]
4+
3C8124A8245618D286CF871E94CE2905DB00CDB7
5+
uid Example identity 2 <[email protected]>
6+
7+
A revocation certificate is a kind of "kill switch" to publicly
8+
declare that a key shall not anymore be used. It is not possible
9+
to retract such a revocation certificate once it has been published.
10+
11+
Use it to revoke this key in case of a compromise or loss of
12+
the secret key. However, if the secret key is still accessible,
13+
it is better to generate a new revocation certificate and give
14+
a reason for the revocation. For details see the description of
15+
of the gpg command "--generate-revocation" in the GnuPG manual.
16+
17+
To avoid an accidental use of this file, a colon has been inserted
18+
before the 5 dashes below. Remove this colon with a text editor
19+
before importing and publishing this revocation certificate.
20+
21+
:-----BEGIN PGP PUBLIC KEY BLOCK-----
22+
Comment: This is a revocation certificate
23+
24+
iQG2BCABCgAgFiEEPIEkqCRWGNKGz4celM4pBdsAzbcFAl3xFwwCHQAACgkQlM4p
25+
BdsAzbeofgv+JUQ4Qb40Z0p2ML+jciJjNiRdKYOUyhY1UKECwq6QlbW/MSkQMn4g
26+
esJfevFwGHKxaI4bw9ywFMtR/UB91YDY4D+lURm4qMuc8pFYEBSMhro0x8ToWz1E
27+
vupwAqTF484ybBrujg2UaOox9BbyhIbsiAH3ttB6wThCbXdhkxp/w6qUaWo+n5Ra
28+
5xWrtFkHJx+WIE4mzxqvnuN3RtA9tkr3L8oavw8Vy0j44lhkVE4Mrl/XDGOnryvv
29+
3WYuiHjOdTP5plb/p4veTvCfUZOcrmhTiwU8gQWMQfy3Nr1NmIhNwoXRTwB41ogt
30+
j3/mijdKVlRkzzmungIt0G3NJM8sAI9TiZ6KyaH4g3fVHU0ddfUwd5sCV3Q+UHfJ
31+
iUZeO1qHA8PA0NZwquRE2rnGdc63nHE/7AD9SjpvqIoBcvfoZCniCBm70pSmJvSP
32+
Csd2/TvaI8PQ28vAVY2LKqCX6JX1MvkiJOZbDGl+VBhCavQCQ8lR0d2b+wriDa3y
33+
2rhCXk0+ozc+
34+
=92kE
35+
-----END PGP PUBLIC KEY BLOCK-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
This is a revocation certificate for the OpenPGP key:
2+
3+
pub rsa3072 2019-12-11 [S]
4+
F14F2E8097E0CCDE93C4E871F4A4F26779FA03BB
5+
uid Example identity <[email protected]>
6+
7+
A revocation certificate is a kind of "kill switch" to publicly
8+
declare that a key shall not anymore be used. It is not possible
9+
to retract such a revocation certificate once it has been published.
10+
11+
Use it to revoke this key in case of a compromise or loss of
12+
the secret key. However, if the secret key is still accessible,
13+
it is better to generate a new revocation certificate and give
14+
a reason for the revocation. For details see the description of
15+
of the gpg command "--generate-revocation" in the GnuPG manual.
16+
17+
To avoid an accidental use of this file, a colon has been inserted
18+
before the 5 dashes below. Remove this colon with a text editor
19+
before importing and publishing this revocation certificate.
20+
21+
:-----BEGIN PGP PUBLIC KEY BLOCK-----
22+
Comment: This is a revocation certificate
23+
24+
iQG2BCABCgAgFiEE8U8ugJfgzN6TxOhx9KTyZ3n6A7sFAl3xFtwCHQAACgkQ9KTy
25+
Z3n6A7vCwwv/U1mKrClA3lxfI+lTAecMeYwlYtYcAveDjJkMs3AtAUQyqd6B7I5F
26+
RXQL50xjiB+S+yv3WoXd4eQbT9Z2g1OnMJfSHzENM0K+yosUJS+TpN3SU7YxJ9GR
27+
4J3eDxo0IyB0mL1QFQXcsWSu92yAyQpwJnscg6S0hvxiq8ij+OyDPNctDtpxcArr
28+
orh9+rGfLBNABemtKgVgzmedjyB7fdPYjgCi/xJL8eYUxlRUgwuwCS7A3DBaSdwU
29+
bd4dGvGTvzSmKp8OrbW1j7yf4Vq91qVcSqAdv1y0EpKwnnefAPJV8TsFr4wg0GpI
30+
ocKDGMiQMPekxfeDPrZcII4jjYXlk6WRzXPTQySZcHBN6o/TKBjdanZv5iV4JktU
31+
jIvru6M7JAM0PJPFj+AwY6hLV3p741qCYYEUlFs9Yjq7j4vhoHqgdVbmZSVcditn
32+
KLcM6F7jEx0odcbL157/xLHHqsCPVoZrr9uZVuN8J6uIYw0GTXFRlpMhDEpVg+Jd
33+
mB4WSiNPkql0
34+
=rwt1
35+
-----END PGP PUBLIC KEY BLOCK-----

0 commit comments

Comments
 (0)