From 4ace52a190e6863104eb22ba59d2bfd99c937847 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Thu, 14 Oct 2021 23:56:03 -0400 Subject: [PATCH] Now prints a more user-friendly error message when installed as a Snap package and permission errors are encountered. Updated the Snap build process as well. --- Makefile.snap | 6 ---- PACKAGING.md | 19 ++-------- build_snap.sh | 72 ++++++++++++++++++++++++++++++++++++++ snapcraft.yaml | 5 ++- src/ssh_audit/globals.py | 19 ++++++++-- src/ssh_audit/policy.py | 8 +++++ src/ssh_audit/ssh_audit.py | 31 ++++++++++++---- 7 files changed, 126 insertions(+), 34 deletions(-) delete mode 100644 Makefile.snap create mode 100755 build_snap.sh diff --git a/Makefile.snap b/Makefile.snap deleted file mode 100644 index a5e0292..0000000 --- a/Makefile.snap +++ /dev/null @@ -1,6 +0,0 @@ -all: - echo -e "\n\nDid you remember to bump the version number in snapcraft.yaml?\n\n" - snapcraft --use-lxd - -clean: - rm -rf parts/ prime/ snap/ stage/ build/ dist/ src/*.egg-info/ ssh-audit*.snap diff --git a/PACKAGING.md b/PACKAGING.md index c3b1b91..9d50532 100644 --- a/PACKAGING.md +++ b/PACKAGING.md @@ -51,27 +51,14 @@ To download from production server and verify: $ pip3 install ssh-audit ``` + # Snap To create the snap package, run a fully-updated Ubuntu Server 20.04 VM. -Install pre-requisites with: - +Create the snap package with: ``` - $ sudo apt install make snapcraft - $ sudo snap install review-tools lxd -``` - -Initialize LXD: - -``` - $ sudo lxd init --auto -``` - -Bump the version number in snapcraft.yaml. Then run: - -``` - $ make -f Makefile.snap + $ ./build_snap.sh ``` Upload the snap with: diff --git a/build_snap.sh b/build_snap.sh new file mode 100755 index 0000000..42ed880 --- /dev/null +++ b/build_snap.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash + +# +# The MIT License (MIT) +# +# Copyright (C) 2021 Joe Testa (jtesta@positronsecurity.com) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +################################################################################ +# build_snap.sh +# +# Builds a Snap package. +################################################################################ + + +# Pre-requisites +sudo apt install -y make snapcraft +sudo snap install review-tools lxd 2> /dev/null + +# Initialize LXD. +sudo lxd init --auto + +# Reset the filesystem from any previous runs. +rm -rf parts/ prime/ snap/ stage/ build/ dist/ src/*.egg-info/ ssh-audit*.snap +git checkout snapcraft.yaml 2> /dev/null +git checkout src/ssh_audit/globals.py 2> /dev/null + +# Get the version from the globals.py file. +version=$(grep VERSION src/ssh_audit/globals.py | awk 'BEGIN {FS="="} ; {print $2}' | tr -d '[:space:]') + +# Strip the quotes around the version (along with the initial 'v' character) and append "-1" to make the default Snap version (i.e.: 'v2.5.0' => '2.5.0-1') +default_snap_version="${version:2:-1}-1" +echo -e -n "\nEnter Snap package version [default: ${default_snap_version}]: " +read -r snap_version + +# If no version was specified, use the default version. +if [[ $snap_version == '' ]]; then + snap_version=$default_snap_version + echo -e "Using default snap version: ${snap_version}\n" +fi + +# Ensure that the snap version fits the format of X.X.X-X. +if [[ ! $snap_version =~ ^[0-9]\.[0-9]\.[0-9]\-[0-9]$ ]]; then + echo "Error: version string does not match format X.X.X-X!" + exit 1 +fi + +# Append the version field to the end of the file. Not pretty, but it works. +echo -e "\nversion: '${snap_version}'" >> snapcraft.yaml + +# Set the SNAP_PACKAGE variable to True so that file permission errors give more user-friendly +sed -i 's/SNAP_PACKAGE = False/SNAP_PACKAGE = True/' src/ssh_audit/globals.py + +snapcraft --use-lxd && echo -e "\nDone.\n" diff --git a/snapcraft.yaml b/snapcraft.yaml index ef5b735..7b39caf 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -1,5 +1,5 @@ name: ssh-audit -version: '2.5.0-1' +# 'version' field will be automatically added by build_snap.sh. license: 'MIT' summary: ssh-audit description: | @@ -12,10 +12,9 @@ confinement: strict apps: ssh-audit: command: bin/ssh-audit - plugs: [network,network-bind] + plugs: [network,network-bind,home] parts: ssh-audit: plugin: python - # python-version: python3 source: . diff --git a/src/ssh_audit/globals.py b/src/ssh_audit/globals.py index dbeb2c3..aab3cc1 100644 --- a/src/ssh_audit/globals.py +++ b/src/ssh_audit/globals.py @@ -21,7 +21,20 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -VERSION = 'v2.6.0-dev' -SSH_HEADER = 'SSH-{0}-OpenSSH_8.2' # SSH software to impersonate -GITHUB_ISSUES_URL = 'https://github.com/jtesta/ssh-audit/issues' # The URL to the Github issues tracker. +# The version to display. +VERSION = 'v2.6.0' + +# SSH software to impersonate +SSH_HEADER = 'SSH-{0}-OpenSSH_8.2' + +# The URL to the Github issues tracker. +GITHUB_ISSUES_URL = 'https://github.com/jtesta/ssh-audit/issues' + +# The man page. Only filled in on Windows systems. WINDOWS_MAN_PAGE = '' + +# True when installed from a Snap package, otherwise False. +SNAP_PACKAGE = False + +# Error message when installed as a Snap package and a file access fails. +SNAP_PERMISSIONS_ERROR = 'Error while accessing file. It appears that ssh-audit was installed as a Snap package. In that case, there are two options: 1.) only try to read & write files in the $HOME/snap/ssh-audit/common/ directory, or 2.) grant permissions to read & write files in $HOME using the following command: "sudo snap connect ssh-audit:home :home"' diff --git a/src/ssh_audit/policy.py b/src/ssh_audit/policy.py index fd9d2c7..e6ef168 100644 --- a/src/ssh_audit/policy.py +++ b/src/ssh_audit/policy.py @@ -30,6 +30,7 @@ from datetime import date from ssh_audit import exitcodes from ssh_audit.ssh2_kex import SSH2_Kex # pylint: disable=unused-import from ssh_audit.banner import Banner # pylint: disable=unused-import +from ssh_audit.globals import SNAP_PACKAGE, SNAP_PERMISSIONS_ERROR # Validates policy files and performs policy testing @@ -122,6 +123,13 @@ class Policy: except FileNotFoundError: print("Error: policy file not found: %s" % policy_file) sys.exit(exitcodes.UNKNOWN_ERROR) + except PermissionError as e: + # If installed as a Snap package, print a more useful message with potential work-arounds. + if SNAP_PACKAGE: + print(SNAP_PERMISSIONS_ERROR) + else: + print("Error: insufficient permissions: %s" % str(e)) + sys.exit(exitcodes.UNKNOWN_ERROR) lines = [] if policy_data is not None: diff --git a/src/ssh_audit/ssh_audit.py b/src/ssh_audit/ssh_audit.py index 98a0ac6..6e5bad7 100755 --- a/src/ssh_audit/ssh_audit.py +++ b/src/ssh_audit/ssh_audit.py @@ -36,6 +36,8 @@ import traceback from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401 from typing import Callable, Optional, Union, Any # noqa: F401 +from ssh_audit.globals import SNAP_PACKAGE +from ssh_audit.globals import SNAP_PERMISSIONS_ERROR from ssh_audit.globals import VERSION from ssh_audit.globals import WINDOWS_MAN_PAGE from ssh_audit.algorithm import Algorithm @@ -560,18 +562,27 @@ def make_policy(aconf: AuditConf, banner: Optional['Banner'], kex: Optional['SSH if aconf.policy_file is None: raise RuntimeError('Internal error: cannot write policy file since filename is None!') - # Open with mode 'x' (creates the file, or fails if it already exist). - succeeded = True + succeeded = False + err = '' try: + # Open with mode 'x' (creates the file, or fails if it already exist). with open(aconf.policy_file, 'x', encoding='utf-8') as f: f.write(policy_data) + succeeded = True except FileExistsError: - succeeded = False + err = "Error: file already exists: %s" % aconf.policy_file + except PermissionError as e: + # If installed as a Snap package, print a more useful message with potential work-arounds. + if SNAP_PACKAGE: + print(SNAP_PERMISSIONS_ERROR) + sys.exit(exitcodes.UNKNOWN_ERROR) + else: + err = "Error: insufficient permissions: %s" % str(e) if succeeded: print("Wrote policy to %s. Customize as necessary, then run a policy scan with -P option." % aconf.policy_file) else: - print("Error: file already exists: %s" % aconf.policy_file) + print(err) def process_commandline(out: OutputBuffer, args: List[str], usage_cb: Callable[..., None]) -> 'AuditConf': # pylint: disable=too-many-statements @@ -681,8 +692,16 @@ def process_commandline(out: OutputBuffer, args: List[str], usage_cb: Callable[. # If a file containing a list of targets was given, read it. if aconf.target_file is not None: - with open(aconf.target_file, 'r', encoding='utf-8') as f: - aconf.target_list = f.readlines() + try: + with open(aconf.target_file, 'r', encoding='utf-8') as f: + aconf.target_list = f.readlines() + except PermissionError as e: + # If installed as a Snap package, print a more useful message with potential work-arounds. + if SNAP_PACKAGE: + print(SNAP_PERMISSIONS_ERROR) + else: + print("Error: insufficient permissions: %s" % str(e)) + sys.exit(exitcodes.UNKNOWN_ERROR) # Strip out whitespace from each line in target file, and skip empty lines. aconf.target_list = [target.strip() for target in aconf.target_list if target not in ("", "\n")]