diff --git a/.gitignore b/.gitignore index b6e4761..62a9338 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +# Other +.vscode \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..f6abad6 --- /dev/null +++ b/Pipfile @@ -0,0 +1,14 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +PyQt5 = "*" +click = "*" + +[dev-packages] +black = "*" + +[requires] +python_version = "3.7" diff --git a/README.md b/README.md index 4ce148c..3db0387 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ -# bastion -Taking arbitrary untrusted PDFs, office documents, or images and convert them to a trusted PDF +# dangerzone + +Take arbitrary untrusted PDFs, office documents, or images and convert them to a trusted PDF. + +## Development environment + +You need [podman](https://podman.io/getting-started/installation) ([these instructions](https://kushaldas.in/posts/podman-on-debian-buster.html) are useful for installing Debian/Ubuntu). diff --git a/dangerzone/__init__.py b/dangerzone/__init__.py new file mode 100644 index 0000000..bcf4013 --- /dev/null +++ b/dangerzone/__init__.py @@ -0,0 +1,44 @@ +from PyQt5 import QtCore, QtWidgets +import sys +import signal +import click + +from .common import Common +from .main_window import MainWindow + +dangerzone_version = "0.1.0" + + +@click.command() +@click.option("--filename", default="", help="Document filename") +def main(filename): + print(f"dangerzone {dangerzone_version}") + + # Allow Ctrl-C to smoothly quit the program instead of throwing an exception + signal.signal(signal.SIGINT, signal.SIG_DFL) + + # Create the Qt app + app = QtWidgets.QApplication(sys.argv) + app.setQuitOnLastWindowClosed(False) + + # Common object + common = Common() + + # Main window + main_window = MainWindow(app, common) + + # If a filename wasn't passed in, get with with a dialog + if filename == "": + filename = QtWidgets.QFileDialog.getOpenFileName( + main_window, + "Open document", + filter="Documents (*.pdf *.docx *.doc *.xlsx *.xls *.pptx *.ppt *.odt *.fodt *.ods *.fods *.odp *.fodp *.odg *.fodg *.odf)", + ) + if filename[0] == "": + print("No document was not selected") + return + + filename = filename[0] + + main_window.start(filename) + sys.exit(app.exec_()) diff --git a/dangerzone/common.py b/dangerzone/common.py new file mode 100644 index 0000000..6bbdd0d --- /dev/null +++ b/dangerzone/common.py @@ -0,0 +1,29 @@ +import sys +import os +import inspect + + +class Common(object): + """ + The Common class is a singleton of shared functionality throughout the app + """ + + def __init__(self): + pass + + def get_resource_path(self, filename): + if getattr(sys, "dangerzone_dev", False): + # Look for resources directory relative to python file + prefix = os.path.join( + os.path.dirname( + os.path.dirname( + os.path.abspath(inspect.getfile(inspect.currentframe())) + ) + ), + "share", + ) + else: + print("Error, can only run in dev mode so far") + + resource_path = os.path.join(prefix, filename) + return resource_path diff --git a/dangerzone/main_window.py b/dangerzone/main_window.py new file mode 100644 index 0000000..258e403 --- /dev/null +++ b/dangerzone/main_window.py @@ -0,0 +1,75 @@ +from PyQt5 import QtCore, QtGui, QtWidgets + +from .tasks import PullImageTask, BuildContainerTask + + +class MainWindow(QtWidgets.QMainWindow): + def __init__(self, app, common): + super(MainWindow, self).__init__() + self.app = app + self.common = common + + self.setWindowTitle("dangerzone") + self.setMinimumWidth(600) + self.setMinimumHeight(500) + + self.task_label = QtWidgets.QLabel() + self.task_label.setAlignment(QtCore.Qt.AlignCenter) + self.task_label.setStyleSheet("QLabel { font-weight: bold; font-size: 20px; }") + + font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) + self.task_details = QtWidgets.QLabel() + self.task_details.setStyleSheet( + "QLabel { background-color: #ffffff; font-size: 12px; padding: 10px; }" + ) + self.task_details.setFont(font) + self.task_details.setAlignment(QtCore.Qt.AlignTop) + + self.details_scrollarea = QtWidgets.QScrollArea() + self.details_scrollarea.setWidgetResizable(True) + self.details_scrollarea.setWidget(self.task_details) + self.details_scrollarea.verticalScrollBar().rangeChanged.connect( + self.scroll_to_bottom + ) + + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.task_label) + layout.addWidget(self.details_scrollarea, stretch=1) + + central_widget = QtWidgets.QWidget() + central_widget.setLayout(layout) + self.setCentralWidget(central_widget) + + self.tasks = [PullImageTask, BuildContainerTask] + + def start(self, filename): + print(f"Input document: {filename}") + self.document_filename = filename + self.show() + + self.next_task() + + def next_task(self): + if len(self.tasks) == 0: + print("Tasks finished") + return + + self.current_task = self.tasks.pop(0)(self.common) + self.current_task.update_label.connect(self.update_label) + self.current_task.update_details.connect(self.update_details) + self.current_task.thread_finished.connect(self.next_task) + self.current_task.start() + + def update_label(self, s): + self.task_label.setText(s) + + def update_details(self, s): + self.task_details.setText(s) + + def scroll_to_bottom(self, minimum, maximum): + self.details_scrollarea.verticalScrollBar().setValue(maximum) + + def closeEvent(self, e): + print("closing") + e.accept() + self.app.quit() diff --git a/dangerzone/tasks.py b/dangerzone/tasks.py new file mode 100644 index 0000000..57a79e7 --- /dev/null +++ b/dangerzone/tasks.py @@ -0,0 +1,62 @@ +import subprocess +import time +from PyQt5 import QtCore, QtWidgets, QtGui + + +class TaskBase(QtCore.QThread): + thread_finished = QtCore.pyqtSignal() + update_label = QtCore.pyqtSignal(str) + update_details = QtCore.pyqtSignal(str) + + def __init__(self): + super(TaskBase, self).__init__() + + def execute_podman(self, args, watch="stdout"): + print(f"Executing: {' '.join(args)}") + output = "" + with subprocess.Popen( + args, + stdin=None, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=1, + universal_newlines=True, + ) as p: + if watch == "stdout": + pipe = p.stdout + else: + pipe = p.stderr + + for line in pipe: + output += line + self.update_details.emit(output) + + output += p.stdout.read() + self.update_details.emit(output) + + +class PullImageTask(TaskBase): + def __init__(self, common): + super(PullImageTask, self).__init__() + self.common = common + + def run(self): + self.update_label.emit("Pulling container image") + self.update_details.emit("") + args = ["podman", "pull", "ubuntu:18.04"] + self.execute_podman(args, watch="stderr") + self.thread_finished.emit() + + +class BuildContainerTask(TaskBase): + def __init__(self, common): + super(BuildContainerTask, self).__init__() + self.common = common + + def run(self): + containerfile = self.common.get_resource_path("Containerfile") + self.update_label.emit("Building container") + self.update_details.emit("") + args = ["podman", "build", "-t", "dangerzone", "-f", containerfile] + self.execute_podman(args) + self.thread_finished.emit() diff --git a/dev_scripts/dangerzone b/dev_scripts/dangerzone new file mode 100755 index 0000000..fe8511e --- /dev/null +++ b/dev_scripts/dangerzone @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Load dangerzone module and resources from the source code tree +import os, sys +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.dangerzone_dev = True + +import dangerzone +dangerzone.main() \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..437bf22 --- /dev/null +++ b/setup.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +import setuptools +import os +import sys +from dangerzone import dangerzone_version + + +setuptools.setup( + name="dangerzone", + version=dangerzone_version, + author="Micah Lee", + author_email="micah.lee@theintercept.com", + license="MIT", + description="Take arbitrary untrusted PDFs, office documents, or images and convert them to a trusted PDF", + url="https://github.com/firstlookmedia/dangerzone", + packages=["dangerzone"], + classifiers=( + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Intended Audience :: End Users/Desktop", + "Operating System :: OS Independent", + ), + entry_points={"console_scripts": ["dangerzone = dangerzone:main"]}, +) diff --git a/share/Containerfile b/share/Containerfile new file mode 100644 index 0000000..91c2c03 --- /dev/null +++ b/share/Containerfile @@ -0,0 +1,8 @@ +FROM ubuntu:18.04 + +RUN apt-get update && \ + apt-get install -y poppler-utils imagemagick + +RUN useradd -ms /bin/bash user +USER user:user +