#!/bin/bash
# Copyright (C) 2020 Canonical, Ltd.
# Author: Mark Morlino <mark.morlino@canonical.com>
# License: GPLv3

error() {
	echo "Error: $*" >&2
	exit 4
}

echo "WARNING: This version (v1.0.10) of CVEScan has been deprecated and will be"
echo "         removed in the next release of the CVEScan snap. Please use"
echo "         v2.5.0+ instead. See https://github.com/canonical/sec-cvescan for"
echo "         more details."

needs_arg() { [[ -z $OPTARG ]] && error "No argument provided for --${OPT} option" ; }
no_arg() {    [[ -n $OPTARG ]] && error "No argument allowed for --${OPT} option" ; }

usage() {
	cat <<-EOF
		Usage $0
		          -c <cve-identifier>
		         --cve=<cve-identifier>
		             Report if this system is vulnerable to a specific CVE

		          -p <critical|high|medium|all>
		         --priority=<critical|high|medium|all>
		             critical = show only critical CVEs
		             high     = show critical and high CVEs (default)
		             medium   = show critical and high and medium CVEs
		             all      = show all CVES (no filtering based on priority)

		          -s
		         --silent
		             Enable script/silent mode
		             To be used with '-c <cve-identifier>', do not print text output
		             exit 0 if not vulnerable, exit 1 if vulnerable

		          -m <xenial|bionic|eoan|focal>
		         --manifest=<xenial|bionic|eoan|focal>
		             Enable manifest mode
		             Do not scan localhost. Instead run a scan against a Ubuntu Official
		                 Cloud Image package manifest file.
		             The script will download and scan a OCI server manifest file.

		          -f <manifestfile>
		         --file=<manifestfile>
		             Used with '-m' option to override the default behavior. Specify
		             a manifest file to scan instead of downloading an OCI manifest.
		             The file needs to be readable under snap confinement,
		             \$HOME will likely work, /tmp will likely not work.

		          -n
		         --nagios
		             Enable nagios mode for use with nrpe
		             Typical nagios-style "OK|WARNING|CRITICAL|UNKNOWN" messages
		                 and exit codes of 0, 1, 2, or 3
		             0/OK = not vulnerable to any known and patchable CVEs of the
		                 specified priority or higher
		             1/WARNING = vulnerable to at least one known CVE of the specified
		                 priority or higher for which there is no available update
		             2/CRITICAL = vulnerable to at least one known and patchable CVE of
		                 the specified priority or higher
		             3/UNKNOWN = something went wrong with the script, or oscap

		          -l
		         --list
		             Show only a list of CVE IDs instead of URLs.
		             Default is to output URLs linking to the Ubuntu CVE tracker

		          -r
		         --reuse
		             Re-use zip, oval, xml, and htm files from cached versions if
		                 possible
		             Default is to redownload and regenerate everything
		             Warning: this may produce inaccurate results

		          -t
		         --test
		             Test mode, use test OVAL data to validate that cvescan and oscap are
		                 working as expected
		             In test mode files are not downloaded
		             In test mode the remove and verbose options are enabled
		                 automatically

		          -u
		         --updates
		             Only show CVEs affecting packages only if there is an update available
		             Default: show any CVEs affecting this system or manifest file, even if
		                 no update is available

		          -v
		         --verbose
		             Enable verbose messages

		          -x
		         --experimental
		             Enable eXperimental mode
		             Use experimental (also called "alpha") OVAL data files
		             The alpha OVAL files include information about package updates
		                 available for users of Ubuntu Advantage running systems with ESM
		                 Apps enabled
	EOF
	exit 3
}

# calls to lsb_release removed and implemented in bash for snap compatibility
if ! test -f /etc/lsb-release ; then
	error "No /etc/lsb-release file found, not running"
else
	. /etc/lsb-release
fi

if [[ ! $DISTRIB_ID =~ ^Ubuntu$ ]] ; then
	error "DISTRIB_ID=${DISTRIB_ID} in /etc/lsb-release is not Ubuntu, not running"
fi

RELEASE=$DISTRIB_CODENAME
OVAL_FILE=com.ubuntu.${RELEASE}.cve.oval.xml
OVAL_ZIP=${OVAL_FILE}.bz2
OVAL_BASE_URL='https://people.canonical.com/~ubuntu-security/oval'
RESULTS=results.xml
REPORT=report.htm
LOG=oval.log
VERBOSE=false
REMOVE=true
SILENT=false
NAGIOS=false
MANIFEST=false
MANIFEST_FILE=manifest
ALL=true
PRIORITY=high
NOW=$(date +%s)
EXPIRE=86400
SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
XSLT_FILE=${SCRIPTDIR}/text.xsl
VERBOSE_OSCAP_OPTIONS=""
CURL_OPTIONS="--fail --silent --show-error"
TESTMODE=false
TESTCANARYFILE=cvescan.test
EXPERIMENTAL=false
PACKAGE_COUNT=$(dpkg -l | grep -E -c '^ii')
EXTRA_SED="-e s@^@http://people.canonical.com/~ubuntu-security/cve/@"

# idea for using getopts for short and long options comes from https://stackoverflow.com/a/28466267
while getopts ":c:p:lrsnm:f:tuvx-:" OPT ; do
	if [ "$OPT" = "-" ]; then
		OPT="${OPTARG%%=*}"
		OPTARG="${OPTARG#$OPT}"
		OPTARG="${OPTARG#=}"
	fi
	case "${OPT}" in
		c | cve )
			needs_arg
			if [[ ${OPTARG} =~ ^CVE-[0-9]{4}-[0-9]{1,6}$ ]] ; then
				CVE=${OPTARG}
			else
				usage
			fi
			;;
		p | priority )
			needs_arg
			if [[ ${OPTARG} =~ ^(critical|high|medium|all)$ ]] ; then
				PRIORITY=${OPTARG}
			else
				usage
			fi
			;;
		l | list )
			EXTRA_SED=""
			;;
		r | reuse )
			REMOVE=false
			;;
		s | silent )
			SILENT=true
			;;
		m | manifest )
			needs_arg
			MANIFEST=true
			if [[ ${OPTARG} =~ ^(xenial|bionic|eoan|focal)$ ]] ; then
				RELEASE=${OPTARG}
				OVAL_FILE=oci.com.ubuntu.${RELEASE}.cve.oval.xml
				OVAL_ZIP=${OVAL_FILE}.bz2
				MANIFEST_URL="https://cloud-images.ubuntu.com/${RELEASE}/current/${RELEASE}-server-cloudimg-amd64.manifest"
			else
				usage
			fi
			;;
		f | file )
			needs_arg
			if [[ -f ${OPTARG} ]] ; then
				if [[ ${OPTARG} =~ ^/ ]] ; then
					MANIFEST_FILE=${OPTARG}
				else
					MANIFEST_FILE=${PWD}/${OPTARG}
				fi
				MANIFEST_URL=""
			else
				error "Cannot find manifest file \"${OPTARG}\". Current directory is \"$PWD\"."
			fi
			;;
		n | nagios )
			NAGIOS=true
			;;
		t | test )
			TESTMODE=true
			;;
		u | updates )
			ALL=false
			;;
		v | verbose )
			VERBOSE=true
			;;
		x | experimental )
			EXPERIMENTAL=true
			;;
		??* | \? | * )
			usage
			;;
	esac
done
shift $((OPTIND-1))


if ! $TESTMODE && ! $MANIFEST && [[ ! $DISTRIB_CODENAME =~ trusty|xenial|bionic|eoan|focal ]] ; then
	error "DISTRIB_CODENAME=${DISTRIB_CODENAME} in /etc/lsb-release is not trusty|xenial|bionic|eoan|focal, not running"
fi

if [[ -n "$SNAP_USER_COMMON" ]] ; then
	$VERBOSE && echo "Running as a snap, changing to '$SNAP_USER_COMMON' directory"
	$VERBOSE && echo "Downloaded files, log files and temporary reports will be in '$SNAP_USER_COMMON'"
	cd "$SNAP_USER_COMMON" || error "failed to cd to $SNAP_USER_COMMON"
else
        [[ -x $(command -v oscap) ]]    || error "Missing oscap command. Run 'sudo apt install libopenscap8 xsltproc curl'"
        [[ -x $(command -v xsltproc) ]] || error "Missing xsltproc command. Run 'sudo apt install libopenscap8 xsltproc curl'"
        [[ -x $(command -v curl) ]]     || error "Missing curl command. Run 'sudo apt install libopenscap8 xsltproc curl'"
fi

[[ -f "$XSLT_FILE" ]] || error "Missing text.xsl file at '$XSLT_FILE', this file should have installed with cvescan"

if [[ -f /var/log/dpkg.log && -f $RESULTS ]] ; then
	PACKAGE_CHANGE_TS=$(stat -c %Y /var/log/dpkg.log)
	RESULTS_TS=$(stat -c %Y $RESULTS)
	if [[ $PACKAGE_CHANGE_TS -gt $RESULTS_TS ]] ; then
		$VERBOSE && echo "Removing $RESULTS file because it is older than /var/log/dpkg.log"
		rm $RESULTS
	fi
fi

if $TESTMODE ; then
	VERBOSE=true
	echo "Running in test mode"

	MANIFEST=false
	echo "Disabling manifest mode (test mode uses test OVAL files distributed with cvescan)"

	REMOVE=true
	echo "Setting flag to remove all cache files"

	EXPERIMENTAL=false
	echo "Disabling experimental mode (test mode uses test OVAL files distributed with cvescan)"

	PRIORITY=all
	echo "Setting priority filter to 'all'"

	EXTRA_SED=""
	echo "Disabling URLs in output"

	OVAL_FILE=${SCRIPTDIR}/com.ubuntu.test.cve.oval.xml
	echo "Setting OVAL file to ${OVAL_FILE}"

	if [[ -f "$OVAL_FILE" ]] ; then
		echo "Using OVAL file ${OVAL_FILE} to test oscap"
	else
		error "Missing test OVAL file at '$OVAL_FILE', this file should have installed with cvescan"
	fi

elif [[ -f "$TESTCANARYFILE" ]] ; then
	$VERBOSE && echo "Detected previous run in test mode, cleaning up"
	$VERBOSE && echo "Removing file: '$TESTCANARYFILE'"
	rm "$TESTCANARYFILE"
	REMOVE=true
fi

if $VERBOSE ; then
	VERBOSE_OSCAP_OPTIONS="--verbose WARNING --verbose-log-file debug.log"
	CURL_OPTIONS="--fail"
fi

if [[ -n "$CVE" ]] ; then
	ALL=true
	PRIORITY=all
fi

$VERBOSE && $TESTMODE && echo "Running in TEST MODE"
$VERBOSE && $ALL && echo "Reporting on ALL CVEs, not just those that can be fixed by updates"
$VERBOSE && $NAGIOS && echo "Running in Nagios Mode"
$VERBOSE && echo "CVE Priority filter is '$PRIORITY'"
$VERBOSE && echo "Installed package count is $PACKAGE_COUNT"

if $MANIFEST ; then
	$VERBOSE && echo "Removing cached report and results files"
	rm -f "$REPORT" "$RESULTS"
	if [[ -n "$MANIFEST_URL" ]] ; then 
		$VERBOSE && echo "Removing cached manifest file"
		rm -f "$MANIFEST_FILE"
	fi
else
	$VERBOSE && echo "Removing cached manifest file"
	rm -f "$MANIFEST_FILE"
fi

if $EXPERIMENTAL ; then
	OVAL_BASE_URL=${OVAL_BASE_URL}/alpha
	OVAL_FILE=alpha.${OVAL_FILE}
	OVAL_ZIP=${OVAL_FILE}.bz2
	$VERBOSE && echo "Running in experimental mode, using 'alpha' OVAL file from ${OVAL_BASE_URL}/${OVAL_ZIP}"
fi

$VERBOSE && $REMOVE && ! $TESTMODE &&  echo "Removing file: $OVAL_FILE"
$REMOVE && ! $TESTMODE &&  rm -f "$OVAL_FILE"
$VERBOSE && $REMOVE && echo "Removing files: $OVAL_ZIP $REPORT $RESULTS $LOG debug.log"
$REMOVE && rm -f "$OVAL_ZIP" "$REPORT" "$RESULTS" "$LOG" debug.log

if ! $TESTMODE && [[ ! -f $OVAL_FILE || $((NOW - $(stat -c %Y "$OVAL_FILE"))) -gt $EXPIRE ]] ; then
	rm -f ${RESULTS} ${REPORT} ${LOG} debug.log
	$VERBOSE && echo "Downloading ${OVAL_BASE_URL}/${OVAL_ZIP}"
	if curl ${CURL_OPTIONS} "${OVAL_BASE_URL}/${OVAL_ZIP}" -o "${OVAL_ZIP}"; then
		$VERBOSE && echo "Unzipping ${OVAL_ZIP}"
		if ! bunzip2 -f "$OVAL_ZIP" ; then
			error "Failed to unzip $OVAL_ZIP"
		fi
	else
		error "Failed to curl, '${OVAL_BASE_URL}/${OVAL_ZIP}' Is this a currently supported Ubuntu version?"
	fi
fi

if $MANIFEST ; then
	rm -f ${RESULTS} ${REPORT} ${LOG} debug.log
	if [[ -n $MANIFEST_URL ]] ; then
		$VERBOSE && echo "Downloading ${MANIFEST_URL}"
		if ! curl ${CURL_OPTIONS} "${MANIFEST_URL}" -o "${MANIFEST_FILE}" ; then
			error "Failed to curl ${MANIFEST_FILE} from ${MANIFEST_URL}"
		fi
	else
		$VERBOSE && echo "Using manifest file ${MANIFEST_FILE}"
		$VERBOSE && echo "cp ${MANIFEST_FILE} manifest (in $PWD)"
		cp ${MANIFEST_FILE} manifest
	fi
	PACKAGE_COUNT=$(wc -l ${MANIFEST_FILE} | cut -f1 -d' ')
	$VERBOSE && echo "Manifest package count is $PACKAGE_COUNT"
fi

if [[ ! -f $RESULTS || $((NOW - $(stat -c %Y "$RESULTS"))) -gt $EXPIRE ]] ; then
	$VERBOSE && echo "Running oval scan oscap oval eval $VERBOSE_OSCAP_OPTIONS --results $RESULTS $OVAL_FILE (output logged to ${PWD}/${LOG})"
	# shellcheck disable=SC2086
	if ! oscap oval eval $VERBOSE_OSCAP_OPTIONS --results "$RESULTS" "$OVAL_FILE" >$LOG 2>&1 ; then
		error "Failed to run oval scan"
	fi
fi

if [[ ! -f $REPORT || $((NOW - $(stat -c %Y "$REPORT"))) -gt $EXPIRE ]] ; then
	$VERBOSE && echo "Generating html report ${PWD}/${REPORT} from results xml ${PWD}/${RESULTS} (output logged to ${PWD}/${LOG})"
	$VERBOSE && echo "Open ${PWD}/${REPORT} in a browser to see complete and unfiltered scan results"
	if ! oscap oval generate report --output $REPORT $RESULTS >>$LOG 2>&1 ; then
		error "Failed to generate oval report"
	fi
fi

$VERBOSE && echo "Running xsltproc to generate CVE list - fixable/unfixable and filtered by priority"
CVE_LIST_ALL_FILTERED=$(xsltproc --stringparam showAll true --stringparam priority "$PRIORITY" "$XSLT_FILE" "$RESULTS" | sed -e /^$/d $EXTRA_SED)
if [[ -z "$CVE_LIST_ALL_FILTERED" ]] ; then
	CVE_COUNT_ALL_FILTERED=0
else
	CVE_COUNT_ALL_FILTERED=$(wc -l <<<"$CVE_LIST_ALL_FILTERED")
fi
$VERBOSE && echo "$CVE_COUNT_ALL_FILTERED vulnerabilities found with priority of $PRIORITY or higher:"
$VERBOSE && echo "$CVE_LIST_ALL_FILTERED"

$VERBOSE && echo "Running xsltproc to generate CVE list - fixable and filtered by priority"
CVE_LIST_FIXABLE_FILTERED=$(xsltproc --stringparam showAll false --stringparam priority "$PRIORITY" "$XSLT_FILE" "$RESULTS" | sed -e /^$/d $EXTRA_SED)
if [[ -z "$CVE_LIST_FIXABLE_FILTERED" ]] ; then
	CVE_COUNT_FIXABLE_FILTERED=0
else
	CVE_COUNT_FIXABLE_FILTERED=$(wc -l <<<"$CVE_LIST_FIXABLE_FILTERED")
fi
$VERBOSE && echo "$CVE_COUNT_FIXABLE_FILTERED CVEs found with priority of $PRIORITY or higher that can be fixed with package updates:"
$VERBOSE && echo "$CVE_LIST_FIXABLE_FILTERED"

$VERBOSE && [[ -n "$SNAP_USER_COMMON" ]] && echo "Full HTML report available in ${PWD}/${REPORT}"

if $TESTMODE ; then
	echo "Writing test canary file ${PWD}/${TESTCANARYFILE}"
	touch $TESTCANARYFILE
	if [[ 		$CVE_COUNT_ALL_FILTERED -eq 2 &&
			$CVE_LIST_ALL_FILTERED =~ CVE-1970-0300 &&
			$CVE_LIST_ALL_FILTERED =~ CVE-1970-0400 &&
			! $CVE_LIST_ALL_FILTERED =~ CVE-1970-0200 &&
			! $CVE_LIST_ALL_FILTERED =~ CVE-1970-0500 ]] ; then
		echo "first test passed"
	else
		error "first test failed"
	fi

	if [[ $CVE_COUNT_FIXABLE_FILTERED -eq 1 && $CVE_LIST_FIXABLE_FILTERED =~ ^CVE-1970-0400$ ]] ; then
		echo "second test passed"
	else
		error "second test failed"
	fi
fi

$VERBOSE && echo "Normal non-verbose output will appear below"
$VERBOSE && echo

if $NAGIOS  ; then
	if [[ -z "$CVE_LIST_FIXABLE_FILTERED" ]] ; then
		echo "OK: no known $PRIORITY or higher CVEs that can be fixed by updating"
		exit 0
	elif [ -n "$CVE_LIST_FIXABLE_FILTERED" ] ; then
		echo "CRITICAL: $CVE_COUNT_FIXABLE_FILTERED CVEs with priority $PRIORITY or higher that can be fixed with package updates"
		echo "$CVE_LIST_FIXABLE_FILTERED"
		exit 2
	elif [ -n "$CVE_LIST_ALL_FILTERED" ] ; then
		echo "WARNING: $CVE_COUNT_ALL_FILTERED CVEs with priority $PRIORITY or higher"
		echo "$CVE_LIST_ALL_FILTERED"
		exit 1
	else
		echo "UNKNOWN: something went wrong with $0"
		exit 3
	fi
elif [ -n "$CVE" ]  ; then
	if   grep -q "$CVE" <<<"$CVE_LIST_FIXABLE_FILTERED" ; then
		$SILENT || echo "$CVE patch available to install"
		exit 1
	elif grep -q "$CVE" <<<"$CVE_LIST_ALL_FILTERED" ; then
		$SILENT || echo "$CVE patch not available"
		exit 1
	else
		$SILENT || echo "$CVE patch applied or system not known to be affected"
		exit 0
	fi
else
	if $ALL ; then
		$SILENT || echo "Inspected $PACKAGE_COUNT packages. Found $CVE_COUNT_ALL_FILTERED CVEs"
		if [ -n "$CVE_LIST_ALL_FILTERED" ] ; then
			echo "$CVE_LIST_ALL_FILTERED"
			exit 1
		else
			exit 0
		fi
	else
		$SILENT || echo "Inspected $PACKAGE_COUNT packages. Found $CVE_COUNT_FIXABLE_FILTERED CVEs"
		if [ -n "$CVE_LIST_FIXABLE_FILTERED" ] ; then
			echo "$CVE_LIST_FIXABLE_FILTERED"
			exit 1
		else
			exit 0
		fi
	fi
fi
		
