diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..05bd31a4 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +ignore: W605 diff --git a/Makefile b/Makefile index af77dec0..24f27989 100644 --- a/Makefile +++ b/Makefile @@ -8,14 +8,13 @@ endif ## Builds and provisions all VMs required for testing workstation all: assert-dom0 validate clean update-fedora-templates \ - update-whonix-templates prep-whonix sd-workstation-template \ + update-whonix-templates prep-whonix prep-dom0 sd-workstation-template \ sd-whonix sd-svs sd-gpg \ sd-journalist sd-svs-disp clone: assert-dom0 ## Pulls the latest repo from work VM to dom0 @./scripts/clone-to-dom0 - sd-workstation-template: prep-salt ## Provisions base template for SDW AppVMs sudo qubesctl top.enable sd-workstation-template sudo qubesctl top.enable sd-workstation-template-files @@ -128,6 +127,11 @@ prep-whonix: ## enables apparmor on whonix-ws-14 and whonix-gw-14 qvm-prefs -s whonix-gw-14 kernelopts "nopat apparmor=1 security=apparmor" qvm-prefs -s whonix-ws-14 kernelopts "nopat apparmor=1 security=apparmor" +prep-dom0: prep-salt # Copies dom0 config files for VM updates + sudo qubesctl top.enable sd-vm-updates + sudo qubesctl top.enable sd-dom0-files + sudo qubesctl --targets dom0 state.highstate + list-vms: ## Prints all Qubes VMs managed by Workstation salt config @./scripts/list-vms diff --git a/dom0/sd-dom0-files.sls b/dom0/sd-dom0-files.sls new file mode 100644 index 00000000..8f71580e --- /dev/null +++ b/dom0/sd-dom0-files.sls @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# vim: set syntax=yaml ts=2 sw=2 sts=2 et : + +## +# Installs dom0 config scripts specific to tracking updates +# over time. These scripts should be ported to an RPM package. +## + + +# Copy script to system location so admins can run ad-hoc +dom0-update-securedrop-script: + file.managed: + - name: /usr/bin/securedrop-update + - source: salt://securedrop-update + - user: root + - group: root + - mode: 755 + +# Symlink update script into cron, for single point of update +dom0-update-securedrop-script-cron: + file.symlink: + - name: /etc/cron.daily/securedrop-update-cron + - target: /usr/bin/securedrop-update + +# Create directory for storing SecureDrop-specific icons +dom0-securedrop-icons-directory: + file.directory: + - name: /usr/share/securedrop/icons + - user: root + - group: root + - mode: 755 + - makedirs: True + +# Copy SecureDrop icon for use in GUI feedback. It's also present in +# the Salt directory, but the permissions on that dir don't permit +# normal user reads. +dom0-securedrop-icon: + file.managed: + - name: /usr/share/securedrop/icons/sd-logo.png + - source: salt://sd/sd-journalist/logo-small.png + - user: root + - group: root + - mode: 644 + # Dependency on parent dir should be explicitly declared, + # but the require syntax below was throwing an error that the + # referenced task was "not available". + # require: + # - dom0-securedrop-icons-directory diff --git a/dom0/sd-dom0-files.top b/dom0/sd-dom0-files.top new file mode 100644 index 00000000..3509270d --- /dev/null +++ b/dom0/sd-dom0-files.top @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# vim: set syntax=yaml ts=2 sw=2 sts=2 et : + +base: + dom0: + - sd-dom0-files diff --git a/dom0/sd-vm-updates.top b/dom0/sd-vm-updates.top new file mode 100644 index 00000000..f3b391d5 --- /dev/null +++ b/dom0/sd-vm-updates.top @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# vim: set syntax=yaml ts=2 sw=2 sts=2 et : + +# "Placeholder" config to trigger TemplateVM boots, +# so upgrades can be applied automatically via cron. +base: + qubes:type:template: + - match: pillar + - topd diff --git a/dom0/securedrop-update b/dom0/securedrop-update new file mode 100755 index 00000000..7a6c3812 --- /dev/null +++ b/dom0/securedrop-update @@ -0,0 +1,52 @@ +#!/bin/bash +# Utility for dom- to ensure all updates are regularly installed +set -e +set -u + +# Number of VMs to update in parallel. Default is 4, +# which can be memory-intensive. +SECUREDROP_MAX_CONCURRENCY=2 + + +# Ensure elevated privileges +if [[ "$EUID" -ne 0 ]]; then + echo "Script must be run as root! Exiting..." + exit 1 +fi + +# Display GUI feedback about update process +function securedrop-update-feedback() { + # Unpack msg as arg1 + local msg="$1" + shift + + # Running `notify-send` as root doesn't work, must be normal user. + # Setting 60s expire time (in ms) since it's a long-running cmd. + local qubes_user + qubes_user="$(id -nu 1000)" + su "$qubes_user" -c "notify-send \ + --app-name 'SecureDrop Workstation' \ + --icon /usr/share/securedrop/icons/sd-logo.png \ + --expire-time 60000 \ + 'SecureDrop: $msg'" +} + +# `qubesctl pkg.upgrade` will automatically update dom0 packages, as well, +# but we *first* want the freshest RPMs from dom0, *then* we'll want to +# update the VMs themselves. +securedrop-update-feedback "Updating dom0 configuration..." +sudo qubes-dom0-update -y + +securedrop-update-feedback "Updating application..." +qubesctl --skip-dom0 --templates \ + --max-concurrency "$SECUREDROP_MAX_CONCURRENCY" \ + pkg.upgrade refresh=true dist_upgrade=true + +securedrop-update-feedback "Updating VM configuration..." +qubesctl \ + --max-concurrency "$SECUREDROP_MAX_CONCURRENCY" \ + state.highstate + +securedrop-update-feedback \ + "Updates installed. Please reboot the workstation \ +to ensure the latest security fixes are applied." diff --git a/sd-journalist/sd-process-display b/sd-journalist/sd-process-display index 84696164..738c5440 100644 --- a/sd-journalist/sd-process-display +++ b/sd-journalist/sd-process-display @@ -1,7 +1,7 @@ import sys import PyQt4.QtCore as QtCore from PyQt4.QtGui import QDialog, QDialogButtonBox, QApplication, \ - QLabel, QVBoxLayout, QImage, QPixmap + QLabel, QVBoxLayout, QImage, QPixmap from PyQt4.QtCore import Qt import os import threading diff --git a/sd-svs/decrypt-sd-submission b/sd-svs/decrypt-sd-submission index d627aaad..ddf55634 100755 --- a/sd-svs/decrypt-sd-submission +++ b/sd-svs/decrypt-sd-submission @@ -37,7 +37,7 @@ try: # given a malicious tarball tar.extractall(tmpdir) -except Exception as e: +except Exception: send_progress("DECRYPTION_BUNDLE_OPEN_FAILURE") # although we're exiting with failure, we return a # 0 exit code so xdg does not try to re-open this file with @@ -139,7 +139,7 @@ try: print(os.path.join(tmpdir, "extracted")) distutils.dir_util.copy_tree(os.path.join(tmpdir, "extracted"), "/home/user/Sources/", update=1) -except Exception as e: +except Exception: send_progress("DECRYPTED_BUNDLE_COPY_FAILURE") shutil.rmtree(tmpdir) sys.exit(0) diff --git a/tests/integration/test_integration b/tests/integration/test_integration index dfa758ff..2d595517 100755 --- a/tests/integration/test_integration +++ b/tests/integration/test_integration @@ -142,13 +142,13 @@ def run_q(n): if run_q("full-success"): receiver.expected_states = [ - "DECRYPTION_PROCESS_START", - "SUBMISSION_BUNDLE_UNBUNDLED", - "SUBMISSION_FILES_EXTRACTED", - "SUBMISSION_FILE_DECRYPTION_SUCCEEDED", - "SUBMISSION_FILE_DECRYPTION_SUCCEEDED", - "DECRYPTED_BUNDLE_ON_SVS", - "DECRYPTED_FILES_AVAILABLE", + "DECRYPTION_PROCESS_START", + "SUBMISSION_BUNDLE_UNBUNDLED", + "SUBMISSION_FILES_EXTRACTED", + "SUBMISSION_FILE_DECRYPTION_SUCCEEDED", + "SUBMISSION_FILE_DECRYPTION_SUCCEEDED", + "DECRYPTED_BUNDLE_ON_SVS", + "DECRYPTED_FILES_AVAILABLE", ] receiver.cmd = ["qvm-open-in-vm", "sd-svs", "./all.sd-xfer"] receiver.test_name = "full successful open" @@ -156,8 +156,8 @@ if run_q("full-success"): if run_q("empty-download"): receiver.expected_states = [ - "DECRYPTION_PROCESS_START", - "DECRYPTION_BUNDLE_OPEN_FAILURE", + "DECRYPTION_PROCESS_START", + "DECRYPTION_BUNDLE_OPEN_FAILURE", ] receiver.cmd = ["qvm-open-in-vm", "sd-svs", "./empty.sd-xfer"] receiver.test_name = "empty sd-xfer" @@ -165,12 +165,12 @@ if run_q("empty-download"): if run_q("bad-gpg-key"): receiver.expected_states = [ - "DECRYPTION_PROCESS_START", - "SUBMISSION_BUNDLE_UNBUNDLED", - "SUBMISSION_FILES_EXTRACTED", - "SUBMISSION_FILE_DECRYPTION_FAILED", - "SUBMISSION_FILE_DECRYPTION_FAILED", - "SUBMISSION_FILE_NO_FILES_FOUND", + "DECRYPTION_PROCESS_START", + "SUBMISSION_BUNDLE_UNBUNDLED", + "SUBMISSION_FILES_EXTRACTED", + "SUBMISSION_FILE_DECRYPTION_FAILED", + "SUBMISSION_FILE_DECRYPTION_FAILED", + "SUBMISSION_FILE_NO_FILES_FOUND", ] receiver.cmd = ["qvm-open-in-vm", "sd-svs", "./bad-key.sd-xfer"] receiver.test_name = "bad encryption key" diff --git a/tests/test_svs.py b/tests/test_svs.py index 30d8e38f..3b933a51 100644 --- a/tests/test_svs.py +++ b/tests/test_svs.py @@ -10,28 +10,28 @@ def setUp(self): def test_decrypt_sd_submission(self): self.assertFilesMatch( - "/usr/bin/decrypt-sd-submission", - "sd-svs/decrypt-sd-submission") + "/usr/bin/decrypt-sd-submission", + "sd-svs/decrypt-sd-submission") def test_decrypt_sd_submission_desktop(self): self.assertFilesMatch( - "/usr/share/applications/decrypt-sd-submission.desktop", - "sd-svs/decrypt-sd-submission.desktop") + "/usr/share/applications/decrypt-sd-submission.desktop", + "sd-svs/decrypt-sd-submission.desktop") def test_decrypt_sd_user_profile(self): self.assertFilesMatch( - "/etc/profile.d/sd-svs-qubes-gpg-domain.sh", - "sd-svs/dot-profile") + "/etc/profile.d/sd-svs-qubes-gpg-domain.sh", + "sd-svs/dot-profile") def test_open_in_dvm_desktop(self): self.assertFilesMatch( - "/usr/share/applications/open-in-dvm.desktop", - "sd-svs/open-in-dvm.desktop") + "/usr/share/applications/open-in-dvm.desktop", + "sd-svs/open-in-dvm.desktop") def test_mimeapps(self): self.assertFilesMatch( - "/usr/share/applications/mimeapps.list", - "sd-svs/mimeapps.list") + "/usr/share/applications/mimeapps.list", + "sd-svs/mimeapps.list") def load_tests(loader, tests, pattern): diff --git a/tests/test_vms_platform.py b/tests/test_vms_platform.py index 06a94827..7c808630 100644 --- a/tests/test_vms_platform.py +++ b/tests/test_vms_platform.py @@ -38,6 +38,27 @@ def _validate_vm_platform(self, vm): platform = self._get_platform_info(vm) self.assertIn(platform, SUPPORTED_PLATFORMS) + def _ensure_packages_up_to_date(self, vm): + """ + Asserts that all available apt packages are installed; no pending + updates. + """ + cmd = "apt list --upgradable" + stdout, stderr = vm.run(cmd) + results = stdout.rstrip() + # `apt list` will always print "Listing..." to stdout, + # so expect only that string. + self.assertEqual(results, "Listing...") + + def test_all_sd_vms_uptodate(self): + """ + Asserts that all VMs have all available apt packages at the latest + versions, with no updates pending. + """ + for vm_name in WANTED_VMS: + vm = self.app.domains[vm_name] + self._ensure_packages_up_to_date(vm) + def test_sd_journalist_template(self): """ Asserts that the 'sd-journalist' VM is using a supported base OS.