-
Notifications
You must be signed in to change notification settings - Fork 45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add security notification script sdw-notify #445
Changes from all commits
6b0af24
15d2083
720b71e
1ce50e4
93068c1
ed956e0
c156c37
8815db9
f964af1
d7ecf30
009e78d
fb281b9
453cbe8
10003bd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
0.1.2 | ||
0.1.3 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# -*- coding: utf-8 -*- | ||
# vim: set syntax=yaml ts=2 sw=2 sts=2 et : | ||
## | ||
# Update /etc/crontab with any cron jobs we need to run regularly | ||
## | ||
|
||
# Identify the GUI user by group membership | ||
{% set gui_user = salt['cmd.shell']('groupmems -l -g qubes') %} | ||
|
||
# Add an hourly job, run as the GUI user, to display a warning if the | ||
# SecureDrop preflight updater has not run for longer than a defined | ||
# warning threshold. | ||
dom0-crontab-update-notify: | ||
file.blockreplace: | ||
- name: /etc/crontab | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
- append_if_not_found: True | ||
- marker_start: "### BEGIN securedrop-workstation ###" | ||
- marker_end: "### END securedrop-workstation ###" | ||
- content: | | ||
0 * * * * {{gui_user}} DISPLAY=:0 /opt/securedrop/launcher/sdw-notify.py | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,50 +1,27 @@ | ||
#!/usr/bin/env python3 | ||
from logging.handlers import TimedRotatingFileHandler | ||
from PyQt4 import QtGui | ||
from sdw_updater_gui.UpdaterApp import UpdaterApp | ||
from sdw_util import Util | ||
from sdw_updater_gui import Updater | ||
|
||
import logging | ||
import os | ||
import sys | ||
|
||
DEFAULT_HOME = os.path.join(os.path.expanduser("~"), ".securedrop_launcher") | ||
logger = "" | ||
|
||
|
||
def main(): | ||
configure_logging() | ||
logger = logging.getLogger(__name__) | ||
logger.info("Starting SecureDrop Launcher") | ||
sdlog = logging.getLogger(__name__) | ||
Util.configure_logging(Updater.LOG_FILE) | ||
lock_handle = Util.obtain_lock(Updater.LOCK_FILE) | ||
if lock_handle is None: | ||
# Preflight updater already running or problems accessing lockfile. | ||
# Logged. | ||
sys.exit(1) | ||
sdlog.info("Starting SecureDrop Launcher") | ||
app = QtGui.QApplication(sys.argv) | ||
form = UpdaterApp() | ||
form.show() | ||
sys.exit(app.exec_()) | ||
|
||
|
||
def configure_logging(): | ||
""" | ||
All logging related settings are set up by this function. | ||
""" | ||
log_folder = os.path.join(DEFAULT_HOME, "logs") | ||
if not os.path.exists(log_folder): | ||
os.makedirs(log_folder) | ||
|
||
log_file = os.path.join(DEFAULT_HOME, "logs", "launcher.log") | ||
|
||
# set logging format | ||
log_fmt = ( | ||
"%(asctime)s - %(name)s:%(lineno)d(%(funcName)s) " "%(levelname)s: %(message)s" | ||
) | ||
formatter = logging.Formatter(log_fmt) | ||
|
||
handler = TimedRotatingFileHandler(log_file) | ||
handler.setFormatter(formatter) | ||
handler.setLevel(logging.INFO) | ||
|
||
# set up primary log | ||
log = logging.getLogger() | ||
log.setLevel(logging.INFO) | ||
log.addHandler(handler) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
#!/usr/bin/env python3 | ||
""" | ||
Displays a warning to the user if the workstation has been running continuously | ||
for too long without checking for security updates. Writes output to a logfile, | ||
not stdout. All settings are in Notify utility module. | ||
""" | ||
|
||
import sys | ||
|
||
from sdw_notify import Notify | ||
from sdw_updater_gui import Updater | ||
from sdw_util import Util | ||
from PyQt4 import QtGui | ||
from PyQt4.QtGui import QMessageBox | ||
|
||
|
||
def main(): | ||
""" | ||
Show security warning, if and only if a warning is not already displayed, | ||
the preflight updater is running, and certain checks suggest that the | ||
system has not been updated for a specified period | ||
""" | ||
|
||
Util.configure_logging(Notify.LOG_FILE) | ||
if Util.can_obtain_lock(Updater.LOCK_FILE) is False: | ||
# Preflight updater is already running. Logged. | ||
sys.exit(1) | ||
|
||
# Hold on to lock handle during execution | ||
lock_handle = Util.obtain_lock(Notify.LOCK_FILE) | ||
if lock_handle is None: | ||
# Can't write to lockfile or notifier already running. Logged. | ||
sys.exit(1) | ||
|
||
warning_should_be_shown = Notify.is_update_check_necessary() | ||
if warning_should_be_shown is None: | ||
# Data integrity issue with update timestamp. Logged. | ||
sys.exit(1) | ||
elif warning_should_be_shown is True: | ||
show_update_warning() | ||
|
||
|
||
def show_update_warning(): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given how straightforward the QT logic is here, I think it's fine to leave as is, but if ever we introduce further complexity/stying here, I would recommend we decouple the main from the QT code, and simply instanciate the QT app object in the main, and handle all other QT tasks in a separate file/class |
||
""" | ||
Show a graphical warning reminding the user to check for security updates | ||
using the preflight updater. | ||
""" | ||
app = QtGui.QApplication([]) # noqa: F841 | ||
|
||
QMessageBox.warning( | ||
None, | ||
"Security check recommended", | ||
"This computer has not been checked for security updates recently. " | ||
"We recommend that you launch or restart the SecureDrop app to " | ||
"check for security updates.", | ||
QMessageBox.Ok, | ||
QMessageBox.Ok, | ||
) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
""" | ||
Utility library for warning the user that security updates have not been applied | ||
in some time. | ||
""" | ||
import logging | ||
import os | ||
|
||
from datetime import datetime | ||
|
||
sdlog = logging.getLogger(__name__) | ||
|
||
# The directory where status files and logs are stored | ||
BASE_DIRECTORY = os.path.join(os.path.expanduser("~"), ".securedrop_launcher") | ||
|
||
# The file and format that contains the timestamp of the last successful update | ||
LAST_UPDATED_FILE = os.path.join(BASE_DIRECTORY, "sdw-last-updated") | ||
LAST_UPDATED_FORMAT = "%Y-%m-%d %H:%M:%S" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: using the variable from Updater.py or storing in Utils.py could help reduce duplication here |
||
|
||
# The lockfile basename used to ensure this script can only be executed once. | ||
# Default path for lockfiles is specified in sdw_util | ||
LOCK_FILE = "sdw-notify.lock" | ||
|
||
# Log file name, base directories defined in sdw_util | ||
LOG_FILE = "sdw-notify.log" | ||
|
||
# The maximum uptime this script should permit (specified in seconds) before | ||
# showing a warning. This is to avoid situations where the user boots the | ||
# computer after several days and immediately sees a warning. | ||
UPTIME_GRACE_PERIOD = 1800 # 30 minutes | ||
|
||
# The amount of time without updates (specified in seconds) which this script | ||
# should permit before showing a warning to the user | ||
WARNING_THRESHOLD = 432000 # 5 days | ||
|
||
|
||
def is_update_check_necessary(): | ||
""" | ||
Perform a series of checks to determine if a security warning should be | ||
shown to the user, reminding them to check for available software updates | ||
using the preflight updater. | ||
""" | ||
last_updated_file_exists = os.path.exists(LAST_UPDATED_FILE) | ||
# For consistent logging | ||
grace_period_hours = UPTIME_GRACE_PERIOD / 60 / 60 | ||
warning_threshold_hours = WARNING_THRESHOLD / 60 / 60 | ||
|
||
# Get timestamp from last update (if it exists) | ||
if last_updated_file_exists: | ||
with open(LAST_UPDATED_FILE, "r") as f: | ||
last_update_time = f.readline().splitlines()[0] | ||
try: | ||
last_update_time = datetime.strptime(last_update_time, LAST_UPDATED_FORMAT) | ||
except ValueError: | ||
sdlog.error( | ||
"Data in {} not in the expected format. " | ||
"Expecting a timestamp in format '{}'. " | ||
"Showing security warning.".format( | ||
LAST_UPDATED_FILE, LAST_UPDATED_FORMAT | ||
) | ||
) | ||
return True | ||
|
||
now = datetime.now() | ||
updated_seconds_ago = (now - last_update_time).total_seconds() | ||
updated_hours_ago = updated_seconds_ago / 60 / 60 | ||
|
||
uptime_seconds = get_uptime_seconds() | ||
uptime_hours = uptime_seconds / 60 / 60 | ||
|
||
if not last_updated_file_exists: | ||
sdlog.warning( | ||
"Timestamp file '{}' does not exist. " | ||
"Updater may never have run. Showing security warning.".format( | ||
LAST_UPDATED_FILE | ||
) | ||
) | ||
return True | ||
else: | ||
if updated_seconds_ago > WARNING_THRESHOLD: | ||
if uptime_seconds > UPTIME_GRACE_PERIOD: | ||
sdlog.warning( | ||
"Last successful update ({0:.1f} hours ago) is above " | ||
"warning threshold ({1:.1f} hours). Uptime grace period of " | ||
"{2:.1f} hours has elapsed (uptime: {3:.1f} hours). " | ||
"Showing security warning.".format( | ||
updated_hours_ago, | ||
warning_threshold_hours, | ||
grace_period_hours, | ||
uptime_hours, | ||
) | ||
) | ||
return True | ||
else: | ||
sdlog.info( | ||
"Last successful update ({0:.1f} hours ago) is above " | ||
"warning threshold ({1:.1f} hours). Uptime grace period " | ||
"of {2:.1f} hours has not elapsed yet (uptime: {3:.1f} " | ||
"hours). Exiting without warning.".format( | ||
updated_hours_ago, | ||
warning_threshold_hours, | ||
grace_period_hours, | ||
uptime_hours, | ||
) | ||
) | ||
return False | ||
else: | ||
sdlog.info( | ||
"Last successful update ({0:.1f} hours ago) " | ||
"is below the warning threshold ({1:.1f} hours). " | ||
"Exiting without warning.".format( | ||
updated_hours_ago, warning_threshold_hours | ||
) | ||
) | ||
return False | ||
|
||
|
||
def get_uptime_seconds(): | ||
# Obtain current uptime | ||
with open("/proc/uptime", "r") as f: | ||
uptime_seconds = float(f.readline().split()[0]) | ||
return uptime_seconds |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lsof
is available on Mac and under Linux, and therefore seemed like the best fit to perform a cross-platform test for exclusive lock access. It is, however, not installed in this particular CircleCI container by default.