Skip to content

Commit 78bcbfe

Browse files
Merge pull request #17 from freedomofpress/add-logging
Add logging to securedrop-export
2 parents d06362e + de1e4a2 commit 78bcbfe

12 files changed

+184
-60
lines changed

.circleci/config.yml

+17-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
version: 2
22
jobs:
3-
build:
3+
lint:
44
docker:
5-
- image: circleci/python:3.5-stretch
5+
- image: circleci/python:3.5
66
steps:
77
- checkout
8+
- run:
9+
name: Install test requirements and run lint
10+
command: |
11+
virtualenv .venv
12+
source .venv/bin/activate
13+
pip install --require-hashes -r test-requirements.txt
14+
make lint
15+
- run:
16+
name: Check Python dependencies for CVEs
17+
command: |
18+
set -e
19+
source .venv/bin/activate
20+
make safety
821
922
test:
1023
docker:
@@ -22,14 +35,10 @@ jobs:
2235
source .venv/bin/activate
2336
pip install --require-hashes -r test-requirements.txt
2437
make test
25-
- run:
26-
name: Check Python dependencies for CVEs
27-
command: |
28-
set -e
29-
source .venv/bin/activate
30-
make safety
3138
workflows:
3239
version: 2
3340
securedrop_export_ci:
3441
jobs:
42+
- lint
3543
- test
44+

.flake8

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[flake8]
2+
max-line-length = 99

Makefile

+4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ update-pip-requirements: ## Updates all Python requirements files via pip-compil
1616
test:
1717
pytest -v tests/
1818

19+
.PHONY: lint
20+
lint:
21+
flake8 securedrop_export/ tests/
22+
1923
# Explaination of the below shell command should it ever break.
2024
# 1. Set the field separator to ": ##" and any make targets that might appear between : and ##
2125
# 2. Use sed-like syntax to remove the make targets

securedrop_export/VERSION

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.1
1+
0.1.2

securedrop_export/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = '0.1.2'

securedrop_export/entrypoint.py

+38
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,56 @@
1+
import logging
12
import os
23
import shutil
34
import sys
45

6+
from logging.handlers import TimedRotatingFileHandler
7+
from securedrop_export import __version__
58
from securedrop_export import export
69
from securedrop_export import main
710

811
CONFIG_PATH = "/etc/sd-export-config.json"
12+
DEFAULT_HOME = os.path.join(os.path.expanduser("~"), ".securedrop_export")
13+
14+
15+
def configure_logging():
16+
"""
17+
All logging related settings are set up by this function.
18+
"""
19+
log_folder = os.path.join(DEFAULT_HOME, 'logs')
20+
if not os.path.exists(log_folder):
21+
os.makedirs(log_folder)
22+
23+
log_file = os.path.join(DEFAULT_HOME, 'logs', 'export.log')
24+
25+
# set logging format
26+
log_fmt = ('%(asctime)s - %(name)s:%(lineno)d(%(funcName)s) '
27+
'%(levelname)s: %(message)s')
28+
formatter = logging.Formatter(log_fmt)
29+
30+
handler = TimedRotatingFileHandler(log_file)
31+
handler.setFormatter(formatter)
32+
handler.setLevel(logging.DEBUG)
33+
34+
# set up primary log
35+
log = logging.getLogger()
36+
log.setLevel(logging.DEBUG)
37+
log.addHandler(handler)
938

1039

1140
def start():
41+
try:
42+
configure_logging()
43+
except Exception:
44+
msg = "ERROR_LOGGING"
45+
export.SDExport.exit_gracefully(msg)
46+
47+
logging.info('Starting SecureDrop Export {}'.format(__version__))
1248
my_sub = export.SDExport(sys.argv[1], CONFIG_PATH)
49+
1350
try:
1451
# Halt immediately if target file is absent
1552
if not os.path.exists(my_sub.archive):
53+
logging.info('Archive is not found {}.'.format(my_sub.archive))
1654
msg = "ERROR_FILE_NOT_FOUND"
1755
my_sub.exit_gracefully(msg)
1856
main.__main__(my_sub)

securedrop_export/export.py

+64-8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import datetime
44
import json
5+
import logging
56
import os
67
import shutil
78
import signal
@@ -19,6 +20,8 @@
1920
BRLASER_DRIVER = "/usr/share/cups/drv/brlaser.drv"
2021
BRLASER_PPD = "/usr/share/cups/model/br7030.ppd"
2122

23+
logger = logging.getLogger(__name__)
24+
2225

2326
class Metadata(object):
2427
"""
@@ -40,20 +43,40 @@ def __init__(self, archive_path):
4043

4144
try:
4245
with open(self.metadata_path) as f:
46+
logging.info('Parsing archive metadata')
4347
json_config = json.loads(f.read())
4448
self.export_method = json_config.get("device", None)
4549
self.encryption_method = json_config.get("encryption_method", None)
46-
self.encryption_key = json_config.get("encryption_key", None)
50+
self.encryption_key = json_config.get(
51+
"encryption_key", None
52+
)
53+
logging.info(
54+
'Exporting to device {} with encryption_method {}'.format(
55+
self.export_method, self.encryption_method
56+
)
57+
)
4758

4859
except Exception:
60+
logging.error('Metadata parsing failure')
4961
raise
5062

5163
def is_valid(self):
64+
logging.info('Validating metadata contents')
5265
if self.export_method not in self.SUPPORTED_EXPORT_METHODS:
66+
logging.error(
67+
'Archive metadata: Export method {} is not supported'.format(
68+
self.export_method
69+
)
70+
)
5371
return False
5472

5573
if self.export_method == "disk":
5674
if self.encryption_method not in self.SUPPORTED_ENCRYPTION_METHODS:
75+
logging.error(
76+
'Archive metadata: Encryption method {} is not supported'.format(
77+
self.encryption_method
78+
)
79+
)
5780
return False
5881
return True
5982

@@ -79,11 +102,15 @@ def __init__(self, archive, config_path):
79102

80103
try:
81104
with open(config_path) as f:
105+
logging.info('Retrieving VM configuration')
82106
json_config = json.loads(f.read())
83107
self.pci_bus_id = json_config.get("pci_bus_id", None)
108+
logging.info('pci_bus_id is {}'.format(self.pci_bus_id))
84109
if self.pci_bus_id is None:
85-
raise
110+
logging.error('pci_bus_id is not set in VM configuration')
111+
raise
86112
except Exception:
113+
logger.error("error parsing VM configuration.")
87114
self.exit_gracefully("ERROR_CONFIG")
88115

89116
def exit_gracefully(self, msg, e=False):
@@ -95,12 +122,14 @@ def exit_gracefully(self, msg, e=False):
95122
"""
96123
sys.stderr.write(msg)
97124
sys.stderr.write("\n")
125+
logger.info('Exiting with message: {}'.format(msg))
98126
if e:
99127
try:
100128
# If the file archive was extracted, delete before returning
101129
if os.path.isdir(self.tmpdir):
102130
shutil.rmtree(self.tmpdir)
103131
e_output = e.output
132+
logger.error(e_output)
104133
except Exception:
105134
e_output = "<unknown exception>"
106135
sys.stderr.write(e_output)
@@ -127,6 +156,7 @@ def popup_message(self, msg):
127156

128157
def extract_tarball(self):
129158
try:
159+
logging.info('Extracting tarball {} into {}'.format(self.archive, self.tmpdir))
130160
with tarfile.open(self.archive) as tar:
131161
tar.extractall(self.tmpdir)
132162
except Exception:
@@ -137,18 +167,22 @@ def check_usb_connected(self):
137167

138168
# If the USB is not attached via qvm-usb attach, lsusb will return empty string and a
139169
# return code of 1
170+
logging.info('Performing usb preflight')
140171
try:
141-
p = subprocess.check_output(["lsusb", "-s", "{}:".format(self.pci_bus_id)])
172+
p = subprocess.check_output(["lsusb", "-s", "{}:".format(self.pci_bus_id)])
173+
logging.info("lsusb -s {} : {}".format(self.pci_bus_id, p.decode("utf-8")))
142174
except subprocess.CalledProcessError:
143175
msg = "ERROR_USB_CONFIGURATION"
144176
self.exit_gracefully(msg)
145177
n_usb = len(p.decode("utf-8").rstrip().split("\n"))
146178
# If there is one device, it is the root hub.
147179
if n_usb == 1:
180+
logging.info('usb preflight - no external devices connected')
148181
msg = "USB_NOT_CONNECTED"
149182
self.exit_gracefully(msg)
150183
# If there are two devices, it's the root hub and another device (presumably for export)
151184
elif n_usb == 2:
185+
logging.info('usb preflight - external device connected')
152186
msg = "USB_CONNECTED"
153187
self.exit_gracefully(msg)
154188
# Else the result is unexpected
@@ -157,10 +191,11 @@ def check_usb_connected(self):
157191
self.exit_gracefully(msg)
158192

159193
def check_luks_volume(self):
194+
logging.info('Checking if volume is luks-encrypted')
160195
try:
161196
# cryptsetup isLuks returns 0 if the device is a luks volume
162197
# subprocess with throw if the device is not luks (rc !=0)
163-
p = subprocess.check_call(["sudo", "cryptsetup", "isLuks", DEVICE])
198+
subprocess.check_call(["sudo", "cryptsetup", "isLuks", DEVICE])
164199
msg = "USB_ENCRYPTED"
165200
self.exit_gracefully(msg)
166201
except subprocess.CalledProcessError:
@@ -169,16 +204,19 @@ def check_luks_volume(self):
169204

170205
def unlock_luks_volume(self, encryption_key):
171206
# the luks device is not already unlocked
207+
logging.info('Unlocking luks volume {}'.format(self.encrypted_device))
172208
if not os.path.exists(os.path.join("/dev/mapper/", self.encrypted_device)):
173209
p = subprocess.Popen(
174210
["sudo", "cryptsetup", "luksOpen", self.device, self.encrypted_device],
175211
stdin=subprocess.PIPE,
176212
stdout=subprocess.PIPE,
177-
stderr=subprocess.PIPE,
213+
stderr=subprocess.PIPE
178214
)
215+
logging.info('Passing key')
179216
p.communicate(input=str.encode(encryption_key, "utf-8"))
180217
rc = p.returncode
181218
if rc != 0:
219+
logging.error('Bad phassphrase for {}'.format(self.encrypted_device))
182220
msg = "USB_BAD_PASSPHRASE"
183221
self.exit_gracefully(msg)
184222

@@ -187,6 +225,7 @@ def mount_volume(self):
187225
if not os.path.exists(self.mountpoint):
188226
subprocess.check_call(["sudo", "mkdir", self.mountpoint])
189227
try:
228+
logging.info('Mounting {} to {}'.format(self.encrypted_device, self.mountpoint))
190229
subprocess.check_call(
191230
[
192231
"sudo",
@@ -198,6 +237,8 @@ def mount_volume(self):
198237
subprocess.check_call(["sudo", "chown", "-R", "user:user", self.mountpoint])
199238
except subprocess.CalledProcessError:
200239
# clean up
240+
logging.error('Error mounting {} to {}'.format(self.encrypted_device, self.mountpoint))
241+
logging.info('Locking luks volume {}'.format(self.encrypted_device))
201242
subprocess.check_call(
202243
["sudo", "cryptsetup", "luksClose", self.encrypted_device]
203244
)
@@ -210,19 +251,25 @@ def copy_submission(self):
210251
target_path = os.path.join(self.mountpoint, self.target_dirname)
211252
subprocess.check_call(["mkdir", target_path])
212253
export_data = os.path.join(self.tmpdir, "export_data/")
254+
logging.info('Copying file to {}'.format(self.target_dirname))
213255
subprocess.check_call(["cp", "-r", export_data, target_path])
256+
logging.info('File copied successfully to {}'.format(self.target_dirname))
214257
self.popup_message("Files exported successfully to disk.")
215258
except (subprocess.CalledProcessError, OSError):
216259
msg = "ERROR_USB_WRITE"
217260
self.exit_gracefully(msg)
218261
finally:
219262
# Finally, we sync the filesystem, unmount the drive and lock the
220263
# luks volume, and exit 0
264+
logging.info('Syncing filesystems')
221265
subprocess.check_call(["sync"])
266+
logging.info('Unmounting drive from {}'.format(self.mountpoint))
222267
subprocess.check_call(["sudo", "umount", self.mountpoint])
268+
logging.info('Locking luks volume {}'.format(self.encrypted_device))
223269
subprocess.check_call(
224270
["sudo", "cryptsetup", "luksClose", self.encrypted_device]
225271
)
272+
logging.info('Deleting temporary directory {}'.format(self.tmpdir))
226273
subprocess.check_call(["rm", "-rf", self.tmpdir])
227274
sys.exit(0)
228275

@@ -234,15 +281,18 @@ def wait_for_print(self):
234281
printer_idle_string = "printer {} is idle".format(self.printer_name)
235282
while True:
236283
try:
284+
logging.info('Running lpstat waiting for printer {}'.format(self.printer_name))
237285
output = subprocess.check_output(["lpstat", "-p", self.printer_name])
238286
if printer_idle_string in output.decode("utf-8"):
287+
logging.info('Print completed')
239288
return True
240289
else:
241290
time.sleep(5)
242291
except subprocess.CalledProcessError:
243292
msg = "ERROR_PRINT"
244293
self.exit_gracefully(msg)
245294
except TimeoutException:
295+
logging.error('Timeout waiting for printer {}'.format(self.printer_name))
246296
msg = "ERROR_PRINT"
247297
self.exit_gracefully(msg)
248298
return True
@@ -260,15 +310,19 @@ def get_printer_uri(self):
260310
for line in output.split():
261311
if "usb://" in line.decode("utf-8"):
262312
printer_uri = line.decode("utf-8")
313+
logging.info('lpinfo usb printer: {}'.format(printer_uri))
263314

264315
# verify that the printer is supported, else exit
265316
if printer_uri == "":
266317
# No usb printer is connected
318+
logging.info('No usb printers connected')
267319
self.exit_gracefully("ERROR_PRINTER_NOT_FOUND")
268320
elif "Brother" in printer_uri:
321+
logging.info('Printer {} is supported'.format(printer_uri))
269322
return printer_uri
270323
else:
271324
# printer url is a make that is unsupported
325+
logging.info('Printer {} is unsupported'.format(printer_uri))
272326
self.exit_gracefully("ERROR_PRINTER_NOT_SUPPORTED")
273327

274328
def install_printer_ppd(self, uri):
@@ -349,22 +403,24 @@ def is_open_office_file(self, filename):
349403

350404
def print_file(self, file_to_print):
351405
try:
352-
# if the file to print is an (open)office document, we need to call unoconf to convert
353-
# the file to pdf as printer drivers do not immediately support this format out of the box
406+
# If the file to print is an (open)office document, we need to call unoconf to
407+
# convert the file to pdf as printer drivers do not support this format
354408
if self.is_open_office_file(file_to_print):
409+
logging.info('Converting Office document to pdf'.format(self.printer_name))
355410
folder = os.path.dirname(file_to_print)
356411
converted_filename = file_to_print + ".pdf"
357412
converted_path = os.path.join(folder, converted_filename)
358413
subprocess.check_call(["unoconv", "-o", converted_path, file_to_print])
359414
file_to_print = converted_path
360415

416+
logging.info('Sending file to printer {}:{}'.format(self.printer_name))
361417
subprocess.check_call(["xpp", "-P", self.printer_name, file_to_print])
362418
except subprocess.CalledProcessError:
363419
msg = "ERROR_PRINT"
364420
self.exit_gracefully(msg)
365421

366422

367-
## class ends here
423+
# class ends here
368424
class TimeoutException(Exception):
369425
pass
370426

0 commit comments

Comments
 (0)