-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #445 from pierwill/uptime-notification-squashed
Add security notification script sdw-notify
- Loading branch information
Showing
11 changed files
with
661 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
0.1.2 | ||
0.1.3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(): | ||
""" | ||
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
|
||
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
""" | ||
Utility functions used by both the launcher and notifier scripts | ||
""" | ||
|
||
import fcntl | ||
import os | ||
import logging | ||
|
||
from logging.handlers import TimedRotatingFileHandler | ||
|
||
# The directory where status files and logs are stored | ||
BASE_DIRECTORY = os.path.join(os.path.expanduser("~"), ".securedrop_launcher") | ||
|
||
# Directory for lock files to avoid contention or multiple instantiation. | ||
LOCK_DIRECTORY = os.path.join("/run/user", str(os.getuid())) | ||
|
||
# Folder where logs are stored | ||
LOG_DIRECTORY = os.path.join(BASE_DIRECTORY, "logs") | ||
|
||
# Shared error string | ||
LOCK_ERROR = "Error obtaining lock on '{}'. Process may already be running." | ||
|
||
# Format for those logs | ||
LOG_FORMAT = ( | ||
"%(asctime)s - %(name)s:%(lineno)d(%(funcName)s) " "%(levelname)s: %(message)s" | ||
) | ||
|
||
sdlog = logging.getLogger(__name__) | ||
|
||
|
||
def configure_logging(log_file): | ||
""" | ||
All logging related settings are set up by this function. | ||
""" | ||
|
||
if not os.path.exists(LOG_DIRECTORY): | ||
os.makedirs(LOG_DIRECTORY) | ||
|
||
formatter = logging.Formatter((LOG_FORMAT)) | ||
|
||
handler = TimedRotatingFileHandler(os.path.join(LOG_DIRECTORY, log_file)) | ||
handler.setFormatter(formatter) | ||
handler.setLevel(logging.INFO) | ||
|
||
log = logging.getLogger() | ||
log.setLevel(logging.INFO) | ||
log.addHandler(handler) | ||
|
||
|
||
def obtain_lock(basename): | ||
""" | ||
Obtain an exclusive lock during the execution of this process. | ||
""" | ||
lock_file = os.path.join(LOCK_DIRECTORY, basename) | ||
try: | ||
lh = open(lock_file, "w") | ||
except PermissionError: # noqa: F821 | ||
sdlog.error( | ||
"Error writing to lock file '{}'. User may lack the " | ||
"required permissions.".format(lock_file) | ||
) | ||
return None | ||
|
||
try: | ||
# Obtain an exclusive, nonblocking lock | ||
fcntl.lockf(lh, fcntl.LOCK_EX | fcntl.LOCK_NB) | ||
except IOError: | ||
sdlog.error(LOCK_ERROR.format(lock_file)) | ||
return None | ||
|
||
return lh | ||
|
||
|
||
def can_obtain_lock(basename): | ||
""" | ||
We temporarily obtain a shared, nonblocking lock to a lockfile to determine | ||
whether the associated process is currently running. Returns True if it is | ||
safe to continue execution (no lock conflict), False if not. | ||
`basename` is the basename of a lockfile situated in the LOCK_DIRECTORY. | ||
""" | ||
lock_file = os.path.join(LOCK_DIRECTORY, basename) | ||
try: | ||
lh = open(lock_file, "r") | ||
except FileNotFoundError: # noqa: F821 | ||
# Process may not have run during this session, safe to continue | ||
return True | ||
|
||
try: | ||
# Obtain a nonblocking, shared lock | ||
fcntl.lockf(lh, fcntl.LOCK_SH | fcntl.LOCK_NB) | ||
except IOError: | ||
sdlog.error(LOCK_ERROR.format(lock_file)) | ||
return False | ||
|
||
return True |
Oops, something went wrong.