mirror of https://github.com/CISOfy/lynis.git
3755 lines
158 KiB
Bash
3755 lines
158 KiB
Bash
#!/bin/sh
|
|
|
|
#################################################################################
|
|
#
|
|
# Lynis
|
|
# ------------------
|
|
#
|
|
# Copyright 2007-2013, Michael Boelen
|
|
# Copyright 2007-2021, CISOfy
|
|
#
|
|
# Website : https://cisofy.com
|
|
# Blog : http://linux-audit.com
|
|
# GitHub : https://github.com/CISOfy/lynis
|
|
#
|
|
# Lynis comes with ABSOLUTELY NO WARRANTY. This is free software, and you are
|
|
# welcome to redistribute it under the terms of the GNU General Public License.
|
|
# See LICENSE file for usage of this software.
|
|
#
|
|
#################################################################################
|
|
#
|
|
# Functions
|
|
#
|
|
#################################################################################
|
|
#
|
|
# Function Description
|
|
# ----------------------- -------------------------------------------------
|
|
# AddHP Add Hardening points to plot a graph later
|
|
# AddSetting Addition of setting
|
|
# AddSystemGroup Adds a system to a group
|
|
# CheckFilePermissions Check file permissions
|
|
# CheckItem Test for presence of a string in report file
|
|
# CheckUpdates Determine if a new version of Lynis is available
|
|
# CleanUp Clean up files before closing program
|
|
# CountTests Count number of performed tests
|
|
# ContainsString Find the needle (string) in the haystack (another string)
|
|
# CreateTempFile Create a temporary file
|
|
# Debug Display additional information on the screen (not suited for cronjob)
|
|
# DigitsOnly Return only the digits from a string
|
|
# DirectoryExists Check if a directory exists on the disk
|
|
# DiscoverProfiles Determine available profiles on system
|
|
# Display Output text to screen with colors and indentation
|
|
# DisplayError Show an error on screen
|
|
# DisplayException Show an exception on screen
|
|
# DisplayManual Output text to screen without any layout
|
|
# DisplayToolTip Show a tip for improving usage of the tool
|
|
# DisplayWarning Show a clear warning on screen
|
|
# Equals Compares two strings
|
|
# ExitClean Stop the program (cleanly), with exit code 0
|
|
# ExitCustom Stop the program (cleanly), with custom exit code
|
|
# ExitFatal Stop the program (cleanly), with exit code 1
|
|
# FileExists Check if a file exists on the disk
|
|
# FileInstalledByPackage Check if a file is linked to a package
|
|
# FileIsEmpty Check if a file is empty
|
|
# FileIsReadable Check if a file is readable or directory accessible
|
|
# GetHostID Retrieve an unique ID for this host
|
|
# GetReportData Request data from report
|
|
# HasCorrectFilePermissions Check file permissions and see if they match expected values
|
|
# HasData Checks for data in variable
|
|
# InsertSection Insert a section block
|
|
# InsertPluginSection Insert a section block for plugins
|
|
# IsContainer Determine if program runs in a container
|
|
# IsDebug Check if --debug is used
|
|
# IsDeveloperMode Check if --developer is used
|
|
# IsDeveloperVersion Check if program is a developer release
|
|
# IsEmpty Check for empty result or variable
|
|
# IsNotebook System detection
|
|
# IsOwnedByRoot Determine if file or directory is owned by root
|
|
# IsRunning Check if a process is running
|
|
# IsVerbose Check if --verbose is used
|
|
# IsVirtualMachine Check if this system is a virtual machine
|
|
# IsWorldExecutable Check if a file is world executable
|
|
# IsWorldReadable Check if a file is world readable
|
|
# IsWorldWritable Check if a file is world writable
|
|
# LogText Log text strings to logfile, prefixed with date/time
|
|
# LogTextBreak Insert a separator in log file
|
|
# PackageIsInstalled Test for installed package
|
|
# ParseNginx Parse nginx configuration lines
|
|
# ParseProfiles Parse all available profiles
|
|
# ParseTestValues Parse a set of values
|
|
# PortIsListening Check if machine is listening on specified protocol and port
|
|
# Progress Show progress on screen
|
|
# Readonly Mark a variable as read-only data
|
|
# Register Register a test (for logging and execution)
|
|
# RandomString Show a random string
|
|
# RemoveColors Reset all colors
|
|
# RemovePIDFile Remove PID file
|
|
# RemoveTempFiles Remove temporary files
|
|
# Report Add string of data to report file
|
|
# ReportDetails Store details of tests which include smaller atomic tests in report
|
|
# ReportException Add an exception to the report file (for debugging purposes)
|
|
# ReportManual Log manual actions to report file
|
|
# ReportSuggestion Add a suggestion to report file
|
|
# ReportWarning Add a warning and priority to report file
|
|
# SafeFile Security tests to perform on a file before using it
|
|
# SafePerms Check if a file has safe permissions
|
|
# SafeInput Test provided string to see if it contains unwanted characters
|
|
# SearchItem Search a string in a file
|
|
# ShowComplianceFinding Display a particular finding regarding compliance or a security standard
|
|
# ShowSymlinkPath Show a path behind a symlink
|
|
# SkipAtomicTest Test if a subtest needs to be skipped
|
|
# Status Show execution status, such as active test being performed
|
|
# StoreNginxSettings Save parsed nginx settings to file
|
|
# TestValue Evaluate a value in a string or key
|
|
# ViewCategories Show available category of tests
|
|
# ViewGroups Display test groups
|
|
# WaitForKeyPress Wait for user to press a key to continue
|
|
#
|
|
#################################################################################
|
|
|
|
|
|
################################################################################
|
|
# Name : AddHP()
|
|
# Description : Add hardening points and count them
|
|
#
|
|
# Parameters : $1 = points to add (0 or higher)
|
|
# $2 = maximum points (at least value of $1 or higher)
|
|
# Returns : <nothing>
|
|
# Usage : AddHP 1 3
|
|
################################################################################
|
|
|
|
AddHP() {
|
|
HPADD=$1; HPADDMAX=$2
|
|
HPPOINTS=$((HPPOINTS + HPADD))
|
|
HPTOTAL=$((HPTOTAL + HPADDMAX))
|
|
if [ ${HPADD} -eq ${HPADDMAX} ]; then
|
|
LogText "Hardening: assigned maximum number of hardening points for this item (${HPADDMAX}). Currently having ${HPPOINTS} points (out of ${HPTOTAL})"
|
|
else
|
|
LogText "Hardening: assigned partial number of hardening points (${HPADD} of ${HPADDMAX}). Currently having ${HPPOINTS} points (out of ${HPTOTAL})"
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : AddSetting()
|
|
# Description : Addition of a setting for display with 'lynis show settings'
|
|
#
|
|
# Parameters : $1 = setting
|
|
# $2 = value
|
|
# $3 = description
|
|
# Returns : <nothing>
|
|
# Usage : AddSetting debug 1 'Debug mode'
|
|
################################################################################
|
|
|
|
AddSetting() {
|
|
if [ $# -eq 3 ]; then
|
|
SETTING="$1"
|
|
VALUE="$2"
|
|
DESCRIPTION="$3"
|
|
if [ -z "${SETTINGS_FILE}" ]; then
|
|
CreateTempFile
|
|
SETTINGS_FILE="${TEMP_FILE}"
|
|
fi
|
|
FIND=$(grep -E "^${SETTING};" ${SETTINGS_FILE})
|
|
if [ -z "${FIND}" ]; then
|
|
echo "${SETTING};${VALUE};${DESCRIPTION};" >> ${SETTINGS_FILE}
|
|
else
|
|
Debug "Setting '${SETTING}' was already configured, overwriting previous line '${FIND}' in ${SETTINGS_FILE} with value '${VALUE}'"
|
|
# Delete line first, then add new value (inline search and replace is messy)
|
|
CreateTempFile
|
|
TEMP_SETTINGS_FILE="${TEMP_FILE}"
|
|
cat ${SETTINGS_FILE} > ${TEMP_SETTINGS_FILE}
|
|
sed -e '/^'"${SETTING}"';/d' ${TEMP_SETTINGS_FILE} > ${SETTINGS_FILE}
|
|
rm "${TEMP_SETTINGS_FILE}"
|
|
echo "${SETTING};${VALUE};${DESCRIPTION};" >> ${SETTINGS_FILE}
|
|
fi
|
|
else
|
|
echo "Error: incorrect call to AddSetting. Needs 3 arguments."
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : AddSystemGroup()
|
|
# Description : Adds a system to a group, which can be used for categorizing
|
|
#
|
|
# Parameters : $1 = group name
|
|
# Returns : <nothing>
|
|
# Usage : AddSystemGroup "test"
|
|
################################################################################
|
|
|
|
AddSystemGroup() {
|
|
Report "system_group[]=$1"
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : CheckFilePermissions()
|
|
# Description : Check file permissions
|
|
#
|
|
# Parameters : Full path to file or directory
|
|
# Returns : PERMS (FILE_NOT_FOUND | OK | BAD)
|
|
# Notes : This function might be replaced in future
|
|
################################################################################
|
|
|
|
CheckFilePermissions() {
|
|
CHECKFILE="$1"
|
|
if [ ! -d ${CHECKFILE} -a ! -f ${CHECKFILE} ]; then
|
|
PERMS="FILE_NOT_FOUND"
|
|
FILEVALUE=""
|
|
else
|
|
# If 'file' is an directory, use -d
|
|
if [ -d ${CHECKFILE} ]; then
|
|
FILEVALUE=$(ls -d -l ${CHECKFILE} | cut -c 2-10)
|
|
PROFILEVALUE=$(grep '^permdir' ${PROFILE} | grep "=${CHECKFILE}:" | cut -d: -f2)
|
|
else
|
|
FILEVALUE=$(ls -l ${CHECKFILE} | cut -c 2-10)
|
|
PROFILEVALUE=$(grep '^permfile' ${PROFILE} | grep "=${CHECKFILE}:" | cut -d: -f2)
|
|
fi
|
|
if [ "${FILEVALUE}" = "${PROFILEVALUE}" ]; then PERMS="OK"; else PERMS="BAD"; fi
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : CheckItem()
|
|
# Description : Check if a specific item exists in the report
|
|
#
|
|
# Parameters : $1 = key
|
|
# $2 = value
|
|
# Returns : exit code (0 = True, 1 = False)
|
|
# Usage : if CheckItem "key" "value"; then ....; fi
|
|
################################################################################
|
|
|
|
CheckItem() {
|
|
RETVAL=255
|
|
if [ $# -eq 2 ]; then
|
|
# Don't search in /dev/null, it's too empty there
|
|
if [ ! "${REPORTFILE}" = "/dev/null" ]; then
|
|
# Check if we can find the main type (with or without brackets)
|
|
LogText "Test: search string $2 in earlier discovered results"
|
|
FIND=$(grep -E "^$1(\[\])?=" ${REPORTFILE} | grep -E "$2")
|
|
if HasData "${FIND}"; then
|
|
RETVAL=0
|
|
LogText "Result: found search string (result: $FIND)"
|
|
else
|
|
LogText "Result: search string NOT found"
|
|
RETVAL=1
|
|
fi
|
|
else
|
|
LogText "Skipping search, as /dev/null is being used"
|
|
fi
|
|
return ${RETVAL}
|
|
else
|
|
ReportException ${TEST_NO} "Error in function call to CheckItem"
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : CheckUpdates()
|
|
# Description : Determine if there is an update available
|
|
#
|
|
# Returns : <nothing>
|
|
# Usage : CheckUpdates
|
|
# Use PROGRAM_LV (latest version) and compare it with actual version (PROGRAM_AC)
|
|
################################################################################
|
|
|
|
CheckUpdates() {
|
|
PROGRAM_LV="0000000000"; DB_MALWARE_LV="0000000000"; DB_FILEPERMS_LV="0000000000"
|
|
if [ ${RUN_UPDATE_CHECK} -eq 1 ]; then
|
|
LYNIS_LV_RECORD="lynis-latest-version.cisofy.com."
|
|
FIND=$(which dig 2> /dev/null | grep -v "no [^ ]* in")
|
|
if [ -n "${FIND}" ]; then
|
|
PROGRAM_LV=$(dig +short +time=3 -t txt lynis-latest-version.cisofy.com 2> /dev/null | grep -v "connection timed out" | sed 's/[".]//g' | grep "^[1-9][0-9][0-9]$")
|
|
else
|
|
FIND=$(which host 2> /dev/null | grep -v "no [^ ]* in ")
|
|
if [ -n "${FIND}" ]; then
|
|
PROGRAM_LV=$(host -t txt -W 3 lynis-latest-version.cisofy.com 2> /dev/null | grep -v "connection timed out" | awk '{ if ($1=="lynis-latest-version.cisofy.com" && $3=="text") { print $4 }}' | sed 's/"//g' | grep "^[1-9][0-9][0-9]$")
|
|
if [ "${PROGRAM_LV}" = "" ]; then PROGRAM_LV=0; fi
|
|
else
|
|
FIND=$(which drill 2> /dev/null | grep -v "no [^ ]* in ")
|
|
if [ -n "${FIND}" ]; then
|
|
PROGRAM_LV=$(drill txt ${LYNIS_LV_RECORD} | awk '{ if ($1=="lynis-latest-version.cisofy.com." && $4=="TXT") { print $5 }}' | tr -d '"' | grep "^[1-9][0-9][0-9]$")
|
|
if [ -z "${PROGRAM_LV}" ]; then PROGRAM_LV=0; fi
|
|
else
|
|
LogText "Result: dig, drill or host not installed, update check skipped"
|
|
UPDATE_CHECK_SKIPPED=1
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : CleanUp()
|
|
# Description : Delete PID and temporary files, stop execution (exit code 1)
|
|
#
|
|
# Parameters : <none>
|
|
# Returns : <nothing>
|
|
# Usage : this function is triggered by a manual break by user
|
|
################################################################################
|
|
|
|
CleanUp() {
|
|
echo ""; echo "Interrupt detected."
|
|
RemovePIDFile
|
|
RemoveTempFiles
|
|
Display --text "Cleaning up..." --result DONE --color GREEN
|
|
ExitFatal
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : ContainsString()
|
|
# Description : Search a specific string (or regular expression) in another
|
|
#
|
|
# Returns : exit code (0 = True, 1 = False)
|
|
# Usage : if ContainsString "needle" "there is a needle in the haystack"; echo "Found"; else "Not found"; fi
|
|
################################################################################
|
|
|
|
ContainsString() {
|
|
RETVAL=1
|
|
if [ $# -ne 2 ]; then ReportException "ContainsString" "Incorrect number of arguments for ContainsStrings function"; fi
|
|
FIND=$(echo "$2" | grep -E "$1")
|
|
if [ ! "${FIND}" = "" ]; then RETVAL=0; fi
|
|
return ${RETVAL}
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : CountTests()
|
|
# Description : Counter for the number of tests performed
|
|
#
|
|
# Parameters : <none>
|
|
# Returns : <nothing>
|
|
# Usage : Call CountTests to increase number by 1
|
|
################################################################################
|
|
|
|
CountTests() {
|
|
CTESTS_PERFORMED=$((CTESTS_PERFORMED + 1))
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : CreateTempFile()
|
|
# Description : Creates a temporary file
|
|
#
|
|
# Returns : TEMP_FILE (variable)
|
|
# Usage : CreateTempFile
|
|
# if [ ! "${TEMP_FILE}" = "" ]; then
|
|
# MYTMPFILE="${TEMP_FILE}"
|
|
# echo "My temporary file is ${MYTMPFILE}"
|
|
# fi
|
|
################################################################################
|
|
|
|
CreateTempFile() {
|
|
TEMP_FILE=""
|
|
if [ "${OS}" = "AIX" ]; then
|
|
RANDOMSTRING1="lynis-$(od -N4 -tu /dev/random | awk 'NR==1 {print $2} {}')"
|
|
TEMP_FILE="/tmp/${RANDOMSTRING1}"
|
|
touch ${TEMP_FILE}
|
|
else
|
|
TEMP_FILE=$(mktemp /tmp/lynis.XXXXXXXXXX) || exit 1
|
|
fi
|
|
if [ ! "${TEMP_FILE}" = "" ]; then
|
|
LogText "Action: created temporary file ${TEMP_FILE}"
|
|
else
|
|
Fatal "Could not create a temporary file"
|
|
fi
|
|
# Add temporary file to queue for cleanup later
|
|
TEMP_FILES="${TEMP_FILES} ${TEMP_FILE}"
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : DirectoryExists()
|
|
# Description : Check if a directory exists
|
|
#
|
|
# Returns : exit code (0 = True, 1 = False)
|
|
# Usage : if DirectoryExists; then echo "it exists"; else echo "It does not exist"; fi
|
|
################################################################################
|
|
|
|
# Determine if a directory exists
|
|
DirectoryExists() {
|
|
if [ $# -eq 0 ]; then ExitFatal "Missing parameter when calling DirectoryExists function"; fi
|
|
DIRECTORY_FOUND=0
|
|
LogText "Test: checking if directory $1 exists"
|
|
if [ -d $1 ]; then
|
|
LogText "Result: directory $1 exists"
|
|
DIRECTORY_FOUND=1
|
|
return 0
|
|
else
|
|
LogText "Result: directory $1 NOT found"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : Debug()
|
|
# Description : Show additional information on screen
|
|
#
|
|
# Input : $1 = text
|
|
# Returns : <nothing>
|
|
# Usage : Debug "More details"
|
|
################################################################################
|
|
|
|
Debug() {
|
|
if [ ${DEBUG} -eq 1 -a $# -gt 0 ]; then echo "${PURPLE}[DEBUG]${NORMAL} $1"; fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : DigitsOnly()
|
|
# Description : Only extract numbers from a string
|
|
#
|
|
# Returns : Digits only string (VALUE)
|
|
################################################################################
|
|
|
|
DigitsOnly() {
|
|
VALUE=$1
|
|
LogText "Value is now: ${VALUE}"
|
|
if [ ! "${AWKBINARY}" = "" ]; then
|
|
VALUE=$(echo ${VALUE} | grep -Eo '[0-9]{1,}')
|
|
fi
|
|
LogText "Returning value: ${VALUE}"
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : DiscoverProfiles()
|
|
# Description : Determine which profiles we have available
|
|
#
|
|
# Returns : <nothing>
|
|
# Usage : DiscoverProfiles
|
|
################################################################################
|
|
|
|
DiscoverProfiles() {
|
|
# Try to find a default and custom profile, unless one was specified manually
|
|
if [ "${PROFILE}" = "" ]; then
|
|
CUSTOM_PROFILE=""
|
|
DEFAULT_PROFILE=""
|
|
PROFILEDIR=""
|
|
tPROFILE_NAMES="default.prf custom.prf"
|
|
if [ ${USE_CWD} -eq 1 ]; then
|
|
tPROFILE_TARGETS="."
|
|
else
|
|
tPROFILE_TARGETS="/usr/local/etc/lynis /etc/lynis /usr/local/lynis ."
|
|
fi
|
|
for PNAME in ${tPROFILE_NAMES}; do
|
|
for PLOC in ${tPROFILE_TARGETS}; do
|
|
# Only use one default.prf
|
|
if [ "${PNAME}" = "default.prf" -a ! "${DEFAULT_PROFILE}" = "" ]; then
|
|
Debug "Already discovered default.prf - skipping this file (${PLOC}/${PNAME})"
|
|
elif [ "${PNAME}" = "custom.prf" -a ! "${CUSTOM_PROFILE}" = "" ]; then
|
|
Debug "Already discovered custom.prf - skipping this file (${PLOC}/${PNAME})"
|
|
else
|
|
if [ "${PLOC}" = "." ]; then FILE="${WORKDIR}/${PNAME}"; else FILE="${PLOC}/${PNAME}"; fi
|
|
if [ -r ${FILE} ]; then
|
|
PROFILES="${PROFILES} ${FILE}"
|
|
case ${PNAME} in
|
|
"custom.prf") CUSTOM_PROFILE="${FILE}" ;;
|
|
"default.prf") DEFAULT_PROFILE="${FILE}" ;;
|
|
esac
|
|
# Set profile directory to last match (Lynis could be both installed, and run as a separate download)
|
|
if [ "${PLOC}" = "." ]; then PROFILEDIR="${WORKDIR}"; else PROFILEDIR="${PLOC}"; fi
|
|
fi
|
|
fi
|
|
done
|
|
done
|
|
# Search any profiles defined with --profile
|
|
for FILE in ${SEARCH_PROFILES}; do
|
|
if [ -r "${FILE}" ]; then
|
|
Debug "Found profile defined with --profile"
|
|
PROFILES="${PROFILES} ${FILE}"
|
|
else
|
|
ExitFatal "Could not find or read profile (${FILE})"
|
|
fi
|
|
done
|
|
fi
|
|
if [ "${PROFILES}" = "" ]; then
|
|
echo "${RED}Fatal error: ${WHITE}No profile defined and could not find default profile${NORMAL}"
|
|
echo "Search paths used --> ${tPROFILE_TARGETS}"
|
|
ExitCustom 66
|
|
else
|
|
PROFILES=$(echo ${PROFILES} | sed 's/^ //')
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : Display()
|
|
# Description : Show text on screen, with markup
|
|
#
|
|
# Input : <multiple parameters, see test>
|
|
# Returns : <nothing>
|
|
################################################################################
|
|
|
|
Display() {
|
|
INDENT=0; TEXT=""; RESULT=""; COLOR=""; SPACES=0; SHOWDEBUG=0
|
|
while [ $# -ge 1 ]; do
|
|
case $1 in
|
|
--color)
|
|
shift
|
|
case $1 in
|
|
GREEN) COLOR=$GREEN ;;
|
|
RED) COLOR=$RED ;;
|
|
WHITE) COLOR=$WHITE ;;
|
|
YELLOW) COLOR=$YELLOW ;;
|
|
esac
|
|
;;
|
|
--debug)
|
|
SHOWDEBUG=1
|
|
;;
|
|
--indent)
|
|
shift
|
|
INDENT=$1
|
|
;;
|
|
--result)
|
|
shift
|
|
RESULT=$1
|
|
;;
|
|
--text)
|
|
shift
|
|
TEXT=$1
|
|
;;
|
|
*)
|
|
echo "INVALID OPTION (Display): $1"
|
|
ExitFatal
|
|
;;
|
|
esac
|
|
# Go to next parameter
|
|
shift
|
|
done
|
|
|
|
if [ -z "${RESULT}" ]; then
|
|
RESULTPART=""
|
|
else
|
|
if [ ${CRONJOB} -eq 0 ]; then
|
|
RESULTPART=" [ ${COLOR}${RESULT}${NORMAL} ]"
|
|
else
|
|
RESULTPART=" [ ${RESULT} ]"
|
|
fi
|
|
fi
|
|
|
|
if [ -n "${TEXT}" ]; then
|
|
SHOW=0
|
|
if [ ${SHOW_WARNINGS_ONLY} -eq 1 ]; then
|
|
if [ "${RESULT}" = "WARNING" ]; then SHOW=1; fi
|
|
elif [ ${QUIET} -eq 0 ]; then SHOW=1
|
|
fi
|
|
|
|
if [ ${SHOW} -eq 1 ]; then
|
|
# Display:
|
|
# - for full shells, count with -m instead of -c, to support language locale (older busybox does not have -m)
|
|
# - wc needs LANG to deal with multi-bytes characters but LANG has been unset in include/consts
|
|
LINESIZE=$(export LC_ALL= ; export LANG="${DISPLAY_LANG}";echo "${TEXT}" | wc -m | tr -d ' ')
|
|
if [ ${SHOWDEBUG} -eq 1 ]; then DEBUGTEXT=" [${PURPLE}DEBUG${NORMAL}]"; else DEBUGTEXT=""; fi
|
|
if [ ${INDENT} -gt 0 ]; then SPACES=$((62 - INDENT - LINESIZE)); fi
|
|
if [ ${SPACES} -lt 0 ]; then SPACES=0; fi
|
|
if [ ${CRONJOB} -eq 0 ]; then
|
|
# Check if we already have already discovered a proper echo command tool. It not, set it default to 'echo'.
|
|
if [ "${ECHOCMD}" = "" ]; then ECHOCMD="echo"; fi
|
|
${ECHOCMD} "\033[${INDENT}C${TEXT}\033[${SPACES}C${RESULTPART}${DEBUGTEXT}"
|
|
else
|
|
echo "${TEXT}${RESULTPART}"
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : DisplayError()
|
|
# Description : Show error on screen
|
|
#
|
|
# Input : $1 = text (string), $2 = optional exit code (integer)
|
|
# Returns : <nothing>
|
|
################################################################################
|
|
|
|
DisplayError() {
|
|
EXITCODE=""
|
|
if [ $# -gt 1 ]; then EXITCODE=$2; fi
|
|
${ECHOCMD} ""
|
|
${ECHOCMD} "${WARNING}Error${NORMAL}: ${BOLD}$1${NORMAL}"
|
|
${ECHOCMD} ""
|
|
if [ -n "${EXITCODE}" ]; then ExitCustom ${EXITCODE}; fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : DisplayException()
|
|
# Description : Show a discovered exception on screen
|
|
#
|
|
# Parameters : $1 = function or test
|
|
# $2 = text
|
|
# Returns : <nothing>
|
|
# Note : This function is usually triggered by ReportException
|
|
################################################################################
|
|
|
|
DisplayException() {
|
|
${ECHOCMD:-echo} ""
|
|
${ECHOCMD:-echo} "================================================================="
|
|
${ECHOCMD:-echo} ""
|
|
${ECHOCMD:-echo} " ${WARNING}Exception found!${NORMAL}"
|
|
${ECHOCMD:-echo} ""
|
|
${ECHOCMD:-echo} " Function/test: [$1]"
|
|
${ECHOCMD:-echo} " Message: ${BOLD}$2${NORMAL}"
|
|
${ECHOCMD:-echo} ""
|
|
${ECHOCMD:-echo} " Help improving the Lynis community with your feedback!"
|
|
${ECHOCMD:-echo} ""
|
|
${ECHOCMD:-echo} " Steps:"
|
|
${ECHOCMD:-echo} " - Ensure you are running the latest version ($0 update check)"
|
|
${ECHOCMD:-echo} " - If so, create a GitHub issue at ${PROGRAM_SOURCE}"
|
|
${ECHOCMD:-echo} " - Include relevant parts of the log file or configuration file"
|
|
${ECHOCMD:-echo} ""
|
|
${ECHOCMD:-echo} " Thanks!"
|
|
${ECHOCMD:-echo} ""
|
|
${ECHOCMD:-echo} "================================================================="
|
|
${ECHOCMD:-echo} ""
|
|
sleep 5
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : DisplayManual()
|
|
# Description : Show text on screen, without any markup
|
|
#
|
|
# Input : $1 = text (string)
|
|
# Returns : <nothing>
|
|
################################################################################
|
|
|
|
DisplayManual() {
|
|
if [ ${QUIET} -eq 0 ]; then ${ECHOCMD} "$1"; fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : DisplayToolTip()
|
|
# Description : Show tooltip on screen
|
|
#
|
|
# Input : $1 = text
|
|
# Returns : <nothing>
|
|
################################################################################
|
|
|
|
DisplayToolTip() {
|
|
# Display tooltip when enabled and no tip has been displayed yet
|
|
if [ ${SHOW_TOOL_TIPS} -eq 1 -a ${TOOLTIP_SHOWED} -eq 0 -a ${QUIET} -eq 0 ]; then
|
|
# Check if we already have already discovered a proper echo command tool. It not, set it default to 'echo'.
|
|
if [ "${ECHOCMD}" = "" ]; then ECHOCMD="echo"; fi
|
|
if [ ${CRONJOB} -eq 0 ]; then
|
|
printf "\n"
|
|
${ECHOCMD} " ${BG_BLUE}[TIP]${NORMAL}: ${LIGHTBLUE}$1${NORMAL}"
|
|
printf "\n"
|
|
else
|
|
${ECHOCMD} " [TIP]: $1"
|
|
fi
|
|
TOOLTIP_SHOWED=1
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : DisplayWarning
|
|
# Description : Show a warning on the screen
|
|
#
|
|
# Parameters : $1 = text
|
|
# Returns : <nothing>
|
|
################################################################################
|
|
|
|
DisplayWarning() {
|
|
if [ ${CRONJOB} -eq 0 ]; then
|
|
printf "\n"
|
|
${ECHOCMD:-echo} " ${BG_WARNING}[WARNING]${NORMAL}: $1${NORMAL}"
|
|
printf "\n"
|
|
else
|
|
${ECHOCMD} " [WARNING]: $1"
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : Equals()
|
|
# Description : Compare two strings after special characters were stripped
|
|
#
|
|
# Parameters : $1 = string1
|
|
# $2 = string2
|
|
# Returns : exit code (0 = True, 1 = False)
|
|
# Usage : if Equals "${MYDIR}" "/etc"; then echo "Found"; else "Not found"; fi
|
|
################################################################################
|
|
|
|
Equals() {
|
|
RETVAL=1
|
|
if [ $# -ne 2 ]; then ReportException "Equals" "Incorrect number of arguments for $0 function"; fi
|
|
|
|
# Strip any strange control characters
|
|
INPUT1=$(echo $1 | tr -d '[:cntrl:]<>' | ${SEDBINARY} 's/__space__/ /g' | ${SEDBINARY} 's/:space:/ /g')
|
|
INPUT2=$(echo $2 | tr -d '[:cntrl:]<>' | ${SEDBINARY} 's/__space__/ /g' | ${SEDBINARY} 's/:space:/ /g')
|
|
if [ "${INPUT1}" = "${INPUT2}" ]; then RETVAL=0; fi
|
|
|
|
return ${RETVAL}
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : ExitClean()
|
|
# Description : Perform a normal exit of the program, and clean up resources
|
|
#
|
|
# Parameters : <nothing>
|
|
# Returns : <nothing>
|
|
# Usage : ExitClean
|
|
################################################################################
|
|
|
|
ExitClean() {
|
|
RemovePIDFile
|
|
RemoveTempFiles
|
|
LogText "${PROGRAM_NAME} ended successfully."
|
|
exit 0
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : ExitCustom()
|
|
# Description : Perform a normal exit of the program, and clean up resources
|
|
#
|
|
# Parameters : $1 = exit code (optional)
|
|
# Returns : <nothing>
|
|
# Usage : ExitCustom 35
|
|
################################################################################
|
|
|
|
ExitCustom() {
|
|
RemovePIDFile
|
|
RemoveTempFiles
|
|
# Exit with the exit code given, otherwise use 1
|
|
if [ $# -eq 1 ]; then
|
|
LogText "${PROGRAM_NAME} ended with exit code $1."
|
|
exit $1
|
|
else
|
|
LogText "${PROGRAM_NAME} ended with exit code 1."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : ExitFatal()
|
|
# Description : Perform exit of the program (with code 1), clean up resources
|
|
#
|
|
# Parameters : $1 = text string (optional)
|
|
# Returns : <nothing>
|
|
# Usage : ExitFatal
|
|
################################################################################
|
|
|
|
ExitFatal() {
|
|
RemovePIDFile
|
|
RemoveTempFiles
|
|
LogText "${PROGRAM_NAME} ended with exit code 1."
|
|
if [ $# -eq 1 ]; then
|
|
${ECHOCMD:-echo} ""
|
|
${ECHOCMD:-echo} "${RED}Fatal error${NORMAL}: ${WHITE}$1${NORMAL}"
|
|
${ECHOCMD:-echo} ""
|
|
fi
|
|
exit 1
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : FileExists()
|
|
# Description : Determine if a file exists
|
|
#
|
|
# Parameters : $1 = path
|
|
# Returns : 0 (found), 1 (not found)
|
|
# FILE_FOUND (0:found, 1:not found) - deprecated usage
|
|
################################################################################
|
|
|
|
FileExists() {
|
|
if [ $# -eq 0 ]; then ExitFatal "Missing parameter when calling FileExists function"; fi
|
|
FILE_FOUND=0
|
|
LogText "Test: checking if file $1 exists"
|
|
if [ -f $1 ]; then
|
|
LogText "Result: file $1 exists"
|
|
FILE_FOUND=1
|
|
return 0
|
|
else
|
|
LogText "Result: file $1 NOT found"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : FileInstalledByPackage()
|
|
# Description : Check if a file is part of a package
|
|
# Returns : 0 (true), 1 (default: unknown or false)
|
|
################################################################################
|
|
|
|
FileInstalledByPackage() {
|
|
exitcode=1
|
|
file=$1
|
|
find=""
|
|
if [ -n "${DPKGBINARY}" ]; then
|
|
find=$(${DPKGBINARY} -S "${file}" 2> /dev/null | ${AWKBINARY} -F: '{print $1}')
|
|
elif [ -n "${RPMBINARY}" ]; then
|
|
find=$(${RPMBINARY} -qf "${file}" 2> /dev/null | ${AWKBINARY} -F- '{print $1}')
|
|
fi
|
|
if [ -n "${find}" ]; then
|
|
LogText "Result: file '${file}' belongs to package (${find})"
|
|
exitcode=0
|
|
else
|
|
LogText "Result: file '${file}' does most likely not belong to a package"
|
|
fi
|
|
return ${exitcode}
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : FileIsEmpty()
|
|
# Description : Check if a file is empty
|
|
#
|
|
# Returns : 0 (empty), 1 (not empty)
|
|
# EMPTY (0 or 1) - deprecated usage
|
|
# Usage : if FileIsEmpty /etc/passwd; then
|
|
################################################################################
|
|
|
|
FileIsEmpty() {
|
|
if [ $# -eq 0 ]; then ExitFatal "Missing parameter when calling FileIsEmpty function"; fi
|
|
EMPTY=0
|
|
LogText "Test: checking if file $1 is empty"
|
|
if [ ! -s "$1" ]; then
|
|
LogText "Result: file $1 is empty"
|
|
EMPTY=1
|
|
return 0
|
|
else
|
|
LogText "Result: file $1 is NOT empty"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : FileIsReadable()
|
|
# Description : Check if a file readable or directory is accessible
|
|
#
|
|
# Returns : Return code (0 = readable, 1 = not readable)
|
|
# Usage : if FileIsReadable /etc/shadow; then echo "File is readable"; fi
|
|
################################################################################
|
|
|
|
FileIsReadable() {
|
|
if [ $# -eq 0 ]; then ExitFatal "Function FileIsReadable() called without a file name"; fi
|
|
sFILE=$1
|
|
CANREAD=0
|
|
RETVAL=1
|
|
escaped_file=$(echo ${sFILE} | sed 's/\*/\\*/; s/?/\\?/')
|
|
LogText "Test: check if we can access ${sFILE} (escaped: ${escaped_file})"
|
|
|
|
# Check for symlink
|
|
if [ -L "${escaped_file}" ]; then
|
|
ShowSymlinkPath ${escaped_file}
|
|
if [ -n "${SYMLINK}" ]; then escaped_file="${SYMLINK}"; fi
|
|
fi
|
|
|
|
# Only check the file if it isn't a symlink (after previous check)
|
|
if [ -L "${escaped_file}" ]; then
|
|
OTHERPERMS="-"
|
|
LogText "Result: unclear if we can read this file, as this is a symlink"
|
|
ReportException "FileIsReadable" "Can not determine symlink ${sFILE}"
|
|
elif [ -d "${escaped_file}" ]; then
|
|
OTHERPERMS=$(${LSBINARY} -d -l "${escaped_file}" 2> /dev/null | ${CUTBINARY} -c 8)
|
|
elif [ -f "${escaped_file}" ]; then
|
|
OTHERPERMS=$(${LSBINARY} -d -l "${escaped_file}" 2> /dev/null | ${CUTBINARY} -c 8)
|
|
else
|
|
OTHERPERMS="-"
|
|
fi
|
|
|
|
# Also check if we are the actual owner of the file (use -d to get directory itself, if its a directory)
|
|
FILEOWNER=$(ls -dln "${escaped_file}" 2> /dev/null | ${AWKBINARY} -F" " '{ print $3 }')
|
|
if [ "${FILEOWNER}" = "${MYID}" ]; then
|
|
LogText "Result: file is owned by our current user ID (${MYID}), checking if it is readable"
|
|
if [ -L "${sFILE}" ]; then
|
|
LogText "Result: unclear if we can read this file, as this is a symlink"
|
|
ReportException "FileIsReadable" "Can not determine symlink ${escaped_file}"
|
|
elif [ -d "${escaped_file}" ]; then
|
|
OTHERPERMS=$(${LSBINARY} -d -l "${escaped_file}" 2> /dev/null | ${CUTBINARY} -c 2)
|
|
elif [ -f "${escaped_file}" ]; then
|
|
OTHERPERMS=$(${LSBINARY} -l "${escaped_file}" 2> /dev/null | ${CUTBINARY} -c 2)
|
|
fi
|
|
else
|
|
LogText "Result: file is not owned by current user ID (${MYID}), but UID ${FILEOWNER}"
|
|
fi
|
|
|
|
# Check if we are root, or have the read bit
|
|
if [ "${MYID}" = "0" -o "${OTHERPERMS}" = "r" ]; then
|
|
CANREAD=1
|
|
LogText "Result: file ${escaped_file} is readable (or directory accessible)."
|
|
return 0
|
|
else
|
|
return 1
|
|
LogText "Result: file ${escaped_file} is NOT readable (or directory accessible), symlink, or does not exist. (OTHERPERMS: ${OTHERPERMS})"
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : GetHostID()
|
|
# Description : Create an unique id for the system
|
|
#
|
|
# Returns : 0 = fetched or created IDs, 1 = failed, 2 = skipped
|
|
# Usage : GetHostID
|
|
################################################################################
|
|
|
|
GetHostID() {
|
|
if [ ${SKIP_GETHOSTID} -eq 1 ]; then
|
|
Debug "Skipping HostID generation due to SKIP_GETHOSTID"
|
|
return 2
|
|
fi
|
|
|
|
if [ -n "${HOSTID}" -a -n "${HOSTID2}" ]; then
|
|
Debug "Skipping creation of host identifiers, as they are already configured (via profile)"
|
|
HOSTID_GEN="profile"
|
|
return 2
|
|
fi
|
|
|
|
if [ -f "${ROOTDIR}etc/lynis/hostids" ]; then
|
|
HOSTID=$(grep "^hostid=" ${ROOTDIR}etc/lynis/hostids | awk -F= '{print $2}')
|
|
HOSTID2=$(grep "^hostid2=" ${ROOTDIR}etc/lynis/hostids | awk -F= '{print $2}')
|
|
Debug "Used hostids file to fetch values"
|
|
HOSTID_GEN="hostids-file"
|
|
return 0
|
|
fi
|
|
|
|
FIND=""
|
|
# Avoid some hashes (empty, only zeros)
|
|
BLACKLISTED_HASHES="6ef1338f520d075957424741d7ed35ab5966ae97 adc83b19e793491b1c6ea0fd8b46cd9f32e592fc"
|
|
# Check which utilities we can use (e.g. lynis show hostids). Normally these are detected during binaries collecting.
|
|
if [ "${SHA1SUMBINARY}" = "" ]; then SHA1SUMBINARY=$(which sha1sum 2> /dev/null | grep -v "no [^ ]* in "); fi
|
|
if [ "${SHA1SUMBINARY}" = "" ]; then SHA1SUMBINARY=$(which sha1 2> /dev/null | grep -v "no [^ ]* in "); fi
|
|
if [ "${SHA256SUMBINARY}" = "" ]; then SHA256SUMBINARY=$(which sha256sum 2> /dev/null | grep -v "no [^ ]* in "); fi
|
|
if [ "${SHA256SUMBINARY}" = "" ]; then SHA256SUMBINARY=$(which sha256 2> /dev/null | grep -v "no [^ ]* in "); fi
|
|
if [ "${CSUMBINARY}" = "" ]; then CSUMBINARY=$(which csum 2> /dev/null | grep -v "no [^ ]* in "); fi
|
|
if [ "${OPENSSLBINARY}" = "" ]; then OPENSSLBINARY=$(which openssl 2> /dev/null | grep -v "no [^ ]* in "); fi
|
|
if [ "${IFCONFIGBINARY}" = "" ]; then IFCONFIGBINARY=$(which ifconfig 2> /dev/null | grep -v "no [^ ]* in "); fi
|
|
if [ "${IPBINARY}" = "" ]; then IPBINARY=$(which ip 2> /dev/null | grep -v "no [^ ]* in "); fi
|
|
|
|
# If using openssl, use the best hash type it supports
|
|
if [ ! "${OPENSSLBINARY}" = "" ]; then
|
|
OPENSSL_HASHLIST=$(openssl dgst -h 2>&1)
|
|
for OPENSSL_HASHTYPE in sha256 sha1 md5 ; do
|
|
if echo "${OPENSSL_HASHLIST}" | grep "^-${OPENSSL_HASHTYPE} " >/dev/null ; then
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [ ! "${SHA1SUMBINARY}" = "" -o ! "${OPENSSLBINARY}" = "" -o ! "${CSUMBINARY}" = "" ]; then
|
|
LogText "Info: found hashing tool, start generation of HostID"
|
|
case "${OS}" in
|
|
|
|
"AIX")
|
|
# Common interfaces: en0 en1 en2, ent0 ent1 ent2
|
|
FIND=$(entstat en0 2>/dev/null | grep "Hardware Address" | awk -F ": " '{ print $2 }')
|
|
if [ "${FIND}" = "" ]; then
|
|
FIND=$(entstat ent0 2>/dev/null | grep "Hardware Address" | awk -F ": " '{ print $2 }')
|
|
fi
|
|
if [ ! "${FIND}" = "" ]; then
|
|
# We have a MAC address, now hashing it
|
|
if [ -n "${SHA1SUMBINARY}" ]; then
|
|
HOSTID=$(echo ${FIND} | ${SHA1SUMBINARY} | awk '{ print $1 }')
|
|
elif [ -n "${CSUMBINARY}" ]; then
|
|
HOSTID=$(echo ${FIND} | ${CSUMBINARY} -h SHA1 - | awk '{ print $1 }')
|
|
elif [ -n "${OPENSSLBINARY}" ]; then
|
|
HOSTID=$(echo ${FIND} | ${OPENSSLBINARY} sha -sha1 | awk '{ print $2 }')
|
|
else
|
|
ReportException "GetHostID" "No sha1, sha1sum, csum or openssl binary available on AIX"
|
|
fi
|
|
else
|
|
ReportException "GetHostID" "No output from entstat on interfaces: en0, ent0"
|
|
fi
|
|
;;
|
|
|
|
"DragonFly" | "FreeBSD")
|
|
FIND=$(${IFCONFIGBINARY} | grep ether | head -n 1 | awk '{ print $2 }' | tr '[:upper:]' '[:lower:]')
|
|
if HasData "${FIND}"; then
|
|
HOSTID=$(echo ${FIND} | sha1)
|
|
else
|
|
ReportException "GetHostID" "No MAC address returned on DragonFly or FreeBSD"
|
|
fi
|
|
;;
|
|
|
|
"HP-UX")
|
|
FIND=$(nwmgr -q info -c lan0 2> /dev/null | awk '{ if ($1=="MAC" && $2=="Address") { print $4 }}')
|
|
if HasData "${FIND}"; then
|
|
if [ -n "${OPENSSLBINARY}" ]; then
|
|
HOSTID=$(echo ${FIND} | ${OPENSSLBINARY} sha -sha1 | awk '{ print $2 }')
|
|
else
|
|
ReportException "GetHostID" "No openssl binary available on this HP-UX system"
|
|
fi
|
|
else
|
|
ReportException "GetHostID" "No MAC address found by using nwmgr"
|
|
fi
|
|
;;
|
|
|
|
"Linux")
|
|
# Try fetching information from /sys in case 'ip' is not available or does not give expected results
|
|
if IsEmpty "${FIND}" && [ -d /sys/class/net ]; then
|
|
NET_INTERFACES=$(${FINDBINARY} /sys/class/net ! -type d -exec realpath {} \; 2> /dev/null | sort | awk -F'/' '!/virtual/ && /devices/ {for (x=1;x<=NF;x++) if ($x~"net") print $(x+1)}')
|
|
for INTERFACE in ${NET_INTERFACES}; do
|
|
if grep -q -s 'up' "/sys/class/net/${INTERFACE}/operstate"; then
|
|
LogText "Interface '${INTERFACE}' is up, fetching MAC address"
|
|
FIND=$(head -n 1 "/sys/class/net/${INTERFACE}/address" | tr '[:upper:]' '[:lower:]')
|
|
if HasData "${FIND}"; then
|
|
HOSTID_GEN="linux-sys-interface-up"
|
|
break
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Next is to try ip, as it is available to most modern Linux distributions
|
|
if IsEmpty "${FIND}" && [ -n "${IPBINARY}" ]; then
|
|
LogText "Info: trying output from 'ip' to generate HostID"
|
|
# Determine if we have the common available eth0 interface. If so, give that priority.
|
|
# Note: apply sorting in case there would be multiple MAC addresses linked to increase predictable end result
|
|
FIND=$(${IPBINARY} addr show eth0 2> /dev/null | grep -E "link/ether " | awk '{ print $2 }' | tr '[:upper:]' '[:lower:]' | sort | head -n 1)
|
|
if HasData "${FIND}"; then
|
|
HOSTID_GEN="linux-ip-interface-eth0"
|
|
else
|
|
# If eth0 does not exist, which is also common, then trying the next option:
|
|
# 1) First fetch all links that are UP
|
|
# 2) Filter entries that have a MAC address and filter out Docker related MAC addresses starting with '02:42:'
|
|
# 3) Convert everything to lowercase
|
|
# 4) Sort the entries, so that the output is more predictable between runs when the same interfaces are available
|
|
# 5) Select first entry
|
|
FIND=$(${IPBINARY} -family link addr show up 2> /dev/null | awk '{if($1=="link/ether" && $2 !~ "^02:42:"){print $2}}' | tr '[:upper:]' '[:lower:]' | sort | head -n 1)
|
|
if HasData "${FIND}"; then
|
|
HOSTID_GEN="linux-ip-interface-up-other"
|
|
else
|
|
ReportException "GetHostID" "Can't create hostid (no MAC addresses found)"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Finally try ifconfig
|
|
if IsEmpty "${FIND}" && [ -n "${IFCONFIGBINARY}" ]; then
|
|
LogText "Info: no information found from 'ip' or in /sys, trying output from 'ifconfig'"
|
|
# Determine if we have the eth0 interface (not all Linux distributions have this, e.g. Arch)
|
|
HASETH0=$(${IFCONFIGBINARY} | grep "^eth0")
|
|
# Check if we can find it with HWaddr on the line
|
|
FIND=$(${IFCONFIGBINARY} 2> /dev/null | grep "^eth0" | grep -v "eth0:" | grep HWaddr | awk '{ print $5 }' | tr '[:upper:]' '[:lower:]')
|
|
|
|
# If nothing found, then try first for alternative interface. Else other versions of ifconfig (e.g. Slackware/Arch)
|
|
if IsEmpty "${FIND}"; then
|
|
FIND=$(${IFCONFIGBINARY} 2> /dev/null | grep HWaddr)
|
|
if IsEmpty "${FIND}"; then
|
|
# If possible directly address eth0 to avoid risking gathering the incorrect MAC address.
|
|
# If not, then falling back to getting first interface. Better than nothing.
|
|
if HasData "${HASETH0}"; then
|
|
FIND=$(${IFCONFIGBINARY} eth0 2> /dev/null | grep "ether " | awk '{ print $2 }' | tr '[:upper:]' '[:lower:]')
|
|
if HasData "${FIND}"; then
|
|
HOSTID_GEN="linux-ifconfig-interface-eth0-ether"
|
|
fi
|
|
else
|
|
FIND=$(${IFCONFIGBINARY} 2> /dev/null | grep "ether " | awk '{ print $2 }' | head -n 1 | tr '[:upper:]' '[:lower:]')
|
|
if IsEmpty "${FIND}"; then
|
|
ReportException "GetHostID" "No eth0 found (and no ether was found with ifconfig)"
|
|
else
|
|
HOSTID_GEN="linux-ifconfig-interface-first-ether"
|
|
LogText "Result: No eth0 found (but ether found), using first network interface to determine hostid (with ifconfig)"
|
|
fi
|
|
fi
|
|
else
|
|
FIND=$(${IFCONFIGBINARY} 2> /dev/null | grep HWaddr | head -n 1 | awk '{ print $5 }' | tr '[:upper:]' '[:lower:]')
|
|
HOSTID_GEN="linux-ifconfig-interface-first-hwaddr"
|
|
fi
|
|
else
|
|
HOSTID_GEN="linux-ifconfig-interface-eth0-hwaddr"
|
|
fi
|
|
fi
|
|
|
|
# Check if we found a MAC address to generate the HostID
|
|
if HasData "${FIND}"; then
|
|
LogText "Info: using hardware address '${FIND}' to create HostID"
|
|
HOSTID=$(echo ${FIND} | ${SHA1SUMBINARY} | awk '{ print $1 }')
|
|
LogText "Result: Found HostID: ${HOSTID}"
|
|
else
|
|
ReportException "GetHostID" "HostID could not be generated"
|
|
fi
|
|
;;
|
|
|
|
"macOS")
|
|
FIND=$(${IFCONFIGBINARY} en0 | grep ether | head -n 1 | awk '{ print $2 }' | tr '[:upper:]' '[:lower:]')
|
|
if [ ! "${FIND}" = "" ]; then
|
|
HOSTID=$(echo ${FIND} | shasum | awk '{ print $1 }')
|
|
else
|
|
ReportException "GetHostID" "No MAC address returned on macOS"
|
|
fi
|
|
LYNIS_HOSTID2_PART1=$(hostname -s)
|
|
if [ -n "${LYNIS_HOSTID2_PART1}" ]; then
|
|
LogText "Info: using hostname ${LYNIS_HOSTID2_PART1}"
|
|
LYNIS_HOSTID2_PART2=$(sysctl -n kern.uuid 2> /dev/null)
|
|
if [ -n "${LYNIS_HOSTID2_PART2}" ]; then
|
|
LogText "Info: using UUID ${LYNIS_HOSTID2_PART2}"
|
|
else
|
|
LogText "Info: could not create HOSTID2 as kern.uuid sysctl key is missing"
|
|
fi
|
|
HOSTID2=$(echo "${LYNIS_HOSTID2_PART1}${LYNIS_HOSTID2_PART2}" | shasum -a 256 | awk '{ print $1 }')
|
|
else
|
|
LogText "Info: could not create HOSTID2 as hostname is missing"
|
|
fi
|
|
;;
|
|
|
|
"NetBSD")
|
|
FIND=$(${IFCONFIGBINARY} -a | grep "address:" | head -n 1 | awk '{ print $2 }' | tr '[:upper:]' '[:lower:]')
|
|
if HasData "${FIND}"; then
|
|
HOSTID=$(echo ${FIND} | sha1)
|
|
else
|
|
ReportException "GetHostID" "No MAC address returned on NetBSD"
|
|
fi
|
|
;;
|
|
|
|
"OpenBSD")
|
|
FIND=$(${IFCONFIGBINARY} | grep "lladdr " | head -n 1 | awk '{ print $2 }' | tr '[:upper:]' '[:lower:]')
|
|
if HasData "${FIND}"; then
|
|
HOSTID=$(echo ${FIND} | sha1)
|
|
else
|
|
ReportException "GetHostID" "No MAC address returned on OpenBSD"
|
|
fi
|
|
;;
|
|
|
|
"Solaris")
|
|
INTERFACES_TO_TEST="net0 e1000g1 e1000g0"
|
|
FOUND=0
|
|
for I in ${INTERFACES_TO_TEST}; do
|
|
FIND=$(${IFCONFIGBINARY} -a | grep "^${I}")
|
|
if [ ! "${FIND}" = "" ]; then
|
|
FOUND=1; LogText "Found interface ${I} on Solaris"
|
|
break
|
|
fi
|
|
done
|
|
if [ ${FOUND} -eq 1 ]; then
|
|
FIND=$(${IFCONFIGBINARY} ${I} | grep ether | awk '{ if ($1=="ether") { print $2 }}')
|
|
if [ -n "${SHA1SUMBINARY}" ]; then
|
|
HOSTID=$(echo ${FIND} | ${SHA1SUMBINARY} | awk '{ print $1 }')
|
|
elif [ -n "${OPENSSLBINARY}" ]; then
|
|
HOSTID=$(echo ${FIND} | ${OPENSSLBINARY} sha -sha1 | awk '{ print $2 }')
|
|
else
|
|
ReportException "GetHostID" "Can not find sha1/sha1sum or openssl"
|
|
fi
|
|
else
|
|
ReportException "GetHostID" "No interface found on Solaris to create HostID"
|
|
fi
|
|
;;
|
|
|
|
*)
|
|
ReportException "GetHostID" "Can't create HOSTID as OS is not supported yet by this function"
|
|
;;
|
|
esac
|
|
|
|
# Remove HOSTID if it contains a default MAC address with a related hash value
|
|
if [ -n "${HOSTID}" ]; then
|
|
for CHECKHASH in ${BLACKLISTED_HASHES}; do
|
|
if [ "${CHECKHASH}" = "${HOSTID}" ]; then
|
|
LogText "Result: hostid is a blacklisted value"
|
|
HOSTID=""
|
|
fi
|
|
done
|
|
fi
|
|
|
|
else
|
|
ReportException "GetHostID" "Can't create HOSTID as there is no SHA1 hash tool available (sha1, sha1sum, openssl)"
|
|
fi
|
|
|
|
# Search machine ID
|
|
# This applies to IDs generated for systemd
|
|
# Optional: DBUS creates ID as well with dbus-uuidgen and is stored in /var/lib/dbus-machine-id (might be symlinked to /etc/machine-id)
|
|
sMACHINEIDFILE="/etc/machine-id"
|
|
if [ -f ${sMACHINEIDFILE} ]; then
|
|
FIND=$(head -n 1 ${sMACHINEIDFILE} | grep "^[a-f0-9]")
|
|
if [ "${FIND}" = "" ]; then
|
|
MACHINEID="${FIND}"
|
|
fi
|
|
fi
|
|
|
|
if [ -z "${HOSTID}" ]; then
|
|
LogText "Result: no HOSTID available, trying to use SSH key as unique source"
|
|
# Create host ID when a MAC address was not found
|
|
SSH_KEY_FILES="ssh_host_ed25519_key.pub ssh_host_ecdsa_key.pub ssh_host_dsa_key.pub ssh_host_rsa_key.pub"
|
|
if [ -d /etc/ssh ]; then
|
|
for I in ${SSH_KEY_FILES}; do
|
|
if [ -z "${HOSTID}" ]; then
|
|
if [ -f /etc/ssh/${I} ]; then
|
|
LogText "Result: found ${I} in /etc/ssh"
|
|
if [ -n "${SHA1SUMBINARY}" ]; then
|
|
HOSTID=$(${SHA1SUMBINARY} /etc/ssh/${I} | awk '{ print $1 }')
|
|
LogText "result: Created HostID with SSH key ($I): ${HOSTID}"
|
|
HOSTID_GEN="fallback-ssh-public-key"
|
|
else
|
|
ReportException "GetHostID" "Can't create HOSTID with SSH key, as sha1sum binary is missing"
|
|
fi
|
|
fi
|
|
fi
|
|
done
|
|
else
|
|
LogText "Result: no /etc/ssh directory found, skipping"
|
|
fi
|
|
fi
|
|
|
|
# Generation of HostID version 2
|
|
if [ -z "${HOSTID2}" ]; then
|
|
LogText "Info: start generation of HostID (version 2)"
|
|
FOUND=0
|
|
DATA_SSH=""
|
|
# Use public keys
|
|
SSH_KEY_FILES="ssh_host_ed25519_key.pub ssh_host_ecdsa_key.pub ssh_host_dsa_key.pub ssh_host_rsa_key.pub"
|
|
if [ -d /etc/ssh ]; then
|
|
for I in ${SSH_KEY_FILES}; do
|
|
if [ ${FOUND} -eq 0 ]; then
|
|
if [ -f /etc/ssh/${I} ]; then
|
|
LogText "Result: found file ${I} in /etc/ssh, using that as candidate to create hostid2"
|
|
DATA_SSH=$(cat /etc/ssh/${I})
|
|
FOUND=1
|
|
fi
|
|
fi
|
|
done
|
|
else
|
|
LogText "Result: no /etc/ssh directory found, skipping"
|
|
fi
|
|
|
|
STRING_TO_HASH=""
|
|
if [ ${FOUND} -eq 1 -a -n "${DATA_SSH}" ]; then
|
|
LogText "Using SSH public key to create hostid2"
|
|
STRING_TO_HASH="${DATA_SSH}"
|
|
HOSTID2_GEN="ssh-public-key"
|
|
else
|
|
if [ -n "${MACHINEID}" ]; then
|
|
LogText "Using the machine ID to create hostid2"
|
|
STRING_TO_HASH="${MACHINEID}"
|
|
HOSTID2_GEN="machine-id"
|
|
fi
|
|
fi
|
|
# Check if we have a string to turn into a host identifier
|
|
if [ -n "${STRING_TO_HASH}" ]; then
|
|
# Create hashes
|
|
if [ -n "${SHA256SUMBINARY}" ]; then
|
|
HASH2=$(echo ${STRING_TO_HASH} | ${SHA256SUMBINARY} | awk '{ print $1 }')
|
|
HASH_HOSTNAME=$(echo ${HOSTNAME} | ${SHA256SUMBINARY} | awk '{ print $1 }')
|
|
elif [ -n "${OPENSSLBINARY}" ]; then
|
|
HASH2=$(echo ${STRING_TO_HASH} | ${OPENSSLBINARY} dgst -${OPENSSL_HASHTYPE} | awk '{ print $2 }')
|
|
HASH_HOSTNAME=$(echo ${HOSTNAME} | ${OPENSSLBINARY} dgst -${OPENSSL_HASHTYPE} | awk '{ print $2 }')
|
|
fi
|
|
LogText "Hash (hostname): ${HASH_HOSTNAME}"
|
|
LogText "Hash (ssh or machineid): ${HASH2}"
|
|
HOSTID2="${HASH2}"
|
|
fi
|
|
fi
|
|
|
|
# Show an exception if no HostID could be created, to ensure each system (and scan) has one
|
|
if [ -z "${HOSTID}" ]; then
|
|
ReportException "GetHostID" "No unique host identifier could be created."
|
|
return 1
|
|
elif [ -n "${HOSTID2}" ]; then
|
|
return 0
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : GetReportData()
|
|
# Description : Request data from report
|
|
# Returns : Data (when matches were found)
|
|
# Returns : exit code (0 = True, 1 = False, meaning search was cancelled)
|
|
# stdout (output of search result)
|
|
################################################################################
|
|
|
|
GetReportData() {
|
|
KEY=""
|
|
VALID_CHARS="[:alnum:]/:;\-,\._\[\]\n "
|
|
if [ $# -eq 0 ]; then ExitFatal "No parameters provided to GetReportData() function"; fi
|
|
|
|
while [ $# -ge 1 ]; do
|
|
case $1 in
|
|
--key)
|
|
shift
|
|
KEY="$1"
|
|
;;
|
|
--valid-chars)
|
|
shift
|
|
VALID_CHARS="$1"
|
|
;;
|
|
*)
|
|
ExitFatal "Invalid option provided to GetReportData() function"
|
|
;;
|
|
esac
|
|
# Go to next parameter
|
|
shift
|
|
done
|
|
|
|
if [ "${REPORTFILE}" = "/dev/null" ]; then
|
|
return 1
|
|
else
|
|
${AWKBINARY} -v pattern="^${KEY}" -F= '$1 ~ pattern {print $2}' ${REPORTFILE} | ${TRBINARY} -cd "${VALID_CHARS}" | ${TRBINARY} '[:blank:]' '__space__'
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : HasCorrectFilePermissions()
|
|
# Description : Check file permissions
|
|
#
|
|
# Parameters : $1 = Full path to file or directory
|
|
# $2 = Permissions
|
|
# Returns : exit code (0 = correct, 1 = not correct, 2 = file does not exist)
|
|
################################################################################
|
|
|
|
HasCorrectFilePermissions() {
|
|
if [ $# -ne 2 ]; then Fatal "Incorrect usage of HasCorrectFilePermissions"; fi
|
|
CHECKFILE="$1"
|
|
CHECKPERMISSION_FULL="$2"
|
|
# Check for symlink
|
|
if [ -L ${CHECKFILE} ]; then
|
|
ShowSymlinkPath ${CHECKFILE}
|
|
if [ ! "${SYMLINK}" = "" ]; then CHECKFILE="${SYMLINK}"; fi
|
|
fi
|
|
if [ ! -d ${CHECKFILE} -a ! -f ${CHECKFILE} ]; then
|
|
return 2
|
|
else
|
|
for CHECK_PERMISSION in ${CHECKPERMISSION_FULL}; do
|
|
DATA=$(echo ${CHECK_PERMISSION} | ${GREPBINARY} -E "[rwx]")
|
|
if [ $? -eq 0 ]; then
|
|
# add a dummy character as first character so it looks like output is a normal file
|
|
CHECK_PERMISSION=$(echo "-${CHECK_PERMISSION}" | ${AWKBINARY} '{k=0;for(i=0;i<=8;i++)k+=((substr($1,i+2,1)~/[rwx]/)*2^(8-i));if(k)printf("%0o",k)}')
|
|
fi
|
|
|
|
# Add leading zeros if necessary
|
|
CHECK_PERMISSION=$(echo "${CHECK_PERMISSION}" | ${AWKBINARY} '{printf "%03d",$1}')
|
|
|
|
# First try stat command
|
|
LogText "Test: checking if file ${CHECKFILE} has the permissions set to ${CHECK_PERMISSION} (${CHECKPERMISSION_FULL}) or more restrictive"
|
|
if [ -n "${STATBINARY}" ]; then
|
|
case ${OS} in
|
|
*BSD | "macOS")
|
|
# BSD and macOS have no --format, only short notation
|
|
DATA=$(${STATBINARY} -f "%OLp" ${CHECKFILE})
|
|
;;
|
|
*)
|
|
# busybox does not support format
|
|
if [ ${SHELL_IS_BUSYBOX} -eq 0 ]; then
|
|
DATA=$(${STATBINARY} --format=%a ${CHECKFILE})
|
|
else
|
|
DATA=$(${STATBINARY} -c %a ${CHECKFILE})
|
|
fi
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
# See if we can use the find binary
|
|
if [ -z "${DATA}" ]; then
|
|
case ${OS} in
|
|
"AIX" | *BSD)
|
|
Debug "Skipping find command, as this operating system does not support -printf parameter"
|
|
;;
|
|
*)
|
|
# Only use find when OS is NOT AIX and binaries are NOT busybox
|
|
if [ -d "${CHECKFILE}" ]; then
|
|
MAXDEPTH="-maxdepth 0"
|
|
else
|
|
MAXDEPTH=""
|
|
fi
|
|
|
|
if [ ${SHELL_IS_BUSYBOX} -eq 0 ]; then
|
|
DATA=$(${FINDBINARY} "${CHECKFILE}" ${MAXDEPTH} -printf "%m")
|
|
else
|
|
DATA=$(${FINDBINARY} "${CHECKFILE}" ${MAXDEPTH} -exec stat -c %a {} \;)
|
|
fi
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
# Finally use ls command
|
|
if [ -z "${DATA}" ]; then
|
|
# If 'file' is an directory, use -d
|
|
if [ -d ${CHECKFILE} ]; then
|
|
DATA=$(${LSBINARY} -d -l ${CHECKFILE} | cut -c 2-10)
|
|
else
|
|
DATA=$(${LSBINARY} -l ${CHECKFILE} | cut -c 2-10)
|
|
fi
|
|
fi
|
|
|
|
# Convert permissions to octal when needed
|
|
case ${DATA} in
|
|
[-r][-w][-x][-r][-w][-x][-r][-w][-x] )
|
|
LogText "Converting value ${DATA} to octal"
|
|
# add a dummy character as first character so it looks like output is a normal file
|
|
DATA=$(echo "-${DATA}" | ${AWKBINARY} '{k=0;for(i=0;i<=8;i++)k+=((substr($1,i+2,1)~/[rwx]/)*2^(8-i));if(k)printf("%0o",k)}')
|
|
;;
|
|
esac
|
|
|
|
# Add leading zeros if necessary
|
|
DATA=$(echo "${DATA}" | ${AWKBINARY} '{printf "%03d",$1}')
|
|
|
|
if [ -n "${DATA}" ]; then
|
|
if [ "${DATA}" -le "${CHECK_PERMISSION}" ]; then
|
|
LogText "Outcome: correct permissions (${DATA})"
|
|
return 0
|
|
fi
|
|
else
|
|
ReportException "HasCorrectFilePermissions:02" "No data value found, which is unexpected"
|
|
fi
|
|
done
|
|
|
|
LogText "Outcome: permissions of file ${CHECKFILE} are not matching expected value (${DATA} != ${CHECK_PERMISSION})"
|
|
# No match, return exit code 1
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : HasData()
|
|
# Description : Check for a filled variable
|
|
#
|
|
# Returns : exit code (0 = True, 1 = False)
|
|
# Usage : if HasData "${FIND}"; then
|
|
################################################################################
|
|
|
|
HasData() {
|
|
if [ $# -eq 1 ]; then
|
|
if [ -n "$1" ]; then return 0; else return 1; fi
|
|
else
|
|
ExitFatal "Function HasData called without parameters - look in log to determine where this happened, or use sh -x lynis to see all details."
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : InsertSection()
|
|
# Description : Show a section block on screen
|
|
#
|
|
# Returns : <nothing>
|
|
# Usage : InsertSection
|
|
################################################################################
|
|
|
|
InsertSection() {
|
|
if [ ${QUIET} -eq 0 ]; then
|
|
echo ""
|
|
echo "[+] ${SECTION}$1${NORMAL}"
|
|
echo "------------------------------------"
|
|
fi
|
|
LogTextBreak
|
|
LogText "Action: Performing tests from category: $1"
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : InsertPlugionSection()
|
|
# Description : Insert section block for plugins (different color)
|
|
#
|
|
# Returns : <nothing>
|
|
# Usage : InsertPluginSection
|
|
################################################################################
|
|
|
|
InsertPluginSection() {
|
|
if [ ${QUIET} -eq 0 ]; then
|
|
echo ""
|
|
echo "[+] ${MAGENTA}$1${NORMAL}"
|
|
echo "------------------------------------"
|
|
fi
|
|
LogText "Action: Performing plugin tests"
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : IsContainer()
|
|
# Description : Determine if we are running in a container
|
|
#
|
|
# Parameters : <none>
|
|
# Returns : exit code (0 = true, 1 = false)
|
|
# variable: CONTAINER_TYPE
|
|
################################################################################
|
|
|
|
IsContainer() {
|
|
FOUND=0
|
|
# Early on we can't use FileIsReadable yet
|
|
if [ -e /proc/1/cgroup ]; then
|
|
FIND=$(grep -i docker ${ROOTDIR}proc/1/cgroup 2> /dev/null)
|
|
if [ $? -eq 0 ]; then
|
|
LogText "Result: found Docker in control groups (/proc/1/cgroup), so we are running in Docker container"
|
|
CONTAINER_TYPE="Docker"; FOUND=1
|
|
EXITCODE=0
|
|
fi
|
|
fi
|
|
if [ -e /proc/1/environ ]; then
|
|
FIND=$(grep -qa 'container=lxc' ${ROOTDIR}proc/1/environ 2> /dev/null)
|
|
if [ $? -eq 0 ]; then
|
|
LogText "Result: found LXC in environment (/proc/1/environ), so we are running in LXC container"
|
|
CONTAINER_TYPE="LXC"; FOUND=1
|
|
EXITCODE=0
|
|
fi
|
|
fi
|
|
if [ ${FOUND} -eq 0 ]; then
|
|
CONTAINER_TYPE=""
|
|
EXITCODE=1
|
|
fi
|
|
return ${EXITCODE}
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : IsDebug()
|
|
# Description : Check if --debug option is used to show more details
|
|
#
|
|
# Parameters : <none>
|
|
# Returns : exit code (0 = True, 1 = False)
|
|
################################################################################
|
|
|
|
IsDebug() {
|
|
if [ ${DEBUG} -eq 1 ]; then return 0; else return 1; fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : IsDeveloperMode()
|
|
# Description : Check if we are in development mode (--developer)
|
|
#
|
|
# Parameters : <none>
|
|
# Returns : exit code (0 = True, 1 = False)
|
|
# Notes : This is set with command line option or as a profile setting
|
|
################################################################################
|
|
|
|
IsDeveloperMode() {
|
|
if [ ${DEVELOPER_MODE} -eq 1 ]; then return 0; else return 1; fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : IsDeveloperVersion()
|
|
# Description : Check if this version is development or stable release
|
|
#
|
|
# Parameters : <none>
|
|
# Returns : exit code (0 = True, 1 = False)
|
|
################################################################################
|
|
|
|
IsDeveloperVersion() {
|
|
if [ "${PROGRAM_RELEASE_TYPE}" = "pre-release" ]; then return 0; else return 1; fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : IsEmpty()
|
|
# Description : Check for variable that has no data in it
|
|
#
|
|
# Returns : exit code (0 = True, 1 = False)
|
|
# Usage : if IsEmpty "${FIND}"; then
|
|
################################################################################
|
|
|
|
IsEmpty() {
|
|
if [ $# -eq 0 ]; then
|
|
ExitFatal "Function IsEmpty called without parameters - look in log to determine where this happened, or use sh -x lynis to see all details."
|
|
else
|
|
if [ -z "$1" ]; then return 0; else return 1; fi
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : IsRunning()
|
|
# Description : Check if a process is running
|
|
#
|
|
# Parameters : $1 = search argument
|
|
# $2 = optional arguments
|
|
# Returns : 0 (process is running), 1 (process not running)
|
|
# RUNNING (1 = running, 0 = not running) - will be deprecated
|
|
# Notes : PSOPTIONS are declared globally, to prevent testing each call
|
|
# Fallback is used on binaries as IsRunning is used for 'show' command
|
|
################################################################################
|
|
|
|
IsRunning() {
|
|
if [ $# -eq 0 ]; then ExitFatal "Missing parameter when calling IsRunning function"; fi
|
|
pgrep_options="-x"
|
|
search=""
|
|
FIND=""
|
|
PSOPTIONS=""
|
|
PARTIAL_SEARCH=1
|
|
|
|
while [ $# -ge 1 ]; do
|
|
case $1 in
|
|
--full)
|
|
pgrep_options="-f" # replace -x with -f
|
|
PARTIAL_SEARCH=0
|
|
;;
|
|
--user)
|
|
shift
|
|
users="$1"
|
|
;;
|
|
*)
|
|
search="$1"
|
|
;;
|
|
esac
|
|
shift # Go to next parameter
|
|
done
|
|
|
|
if [ -z "${search}" ]; then ExitFatal "Missing process to search for when using IsRunning function"; fi
|
|
RUNNING=0
|
|
if [ -x "${PGREPBINARY}" ] && [ "${OS}" != "AIX" ]; then
|
|
# When --user is used, perform a search using the -u option
|
|
# Initialize users for strict mode
|
|
if [ -n "${users:-}" ]; then
|
|
for u in ${users}; do
|
|
user_uid=$(getent passwd "${u}" 2> /dev/null | ${AWKBINARY:-awk} -F: '{print $3}')
|
|
# Only perform search if user exists and we had no match yet
|
|
if [ -n "${user_uid}" ]; then
|
|
if [ -z "${FIND}" ]; then
|
|
LogText "Performing pgrep scan using uid ${user_uid}"
|
|
FIND=$(${PGREPBINARY:-pgrep} ${pgrep_options} -u "${user_uid}" "${search}" | ${TRBINARY:-tr} '\n' ' ')
|
|
fi
|
|
fi
|
|
done
|
|
else
|
|
LogText "Performing pgrep scan without uid"
|
|
FIND=$(${PGREPBINARY:-pgrep} ${pgrep_options} "${search}" | ${TRBINARY:-tr} '\n' ' ')
|
|
fi
|
|
else
|
|
if [ "${SHELL_IS_BUSYBOX}" -eq 1 ]; then
|
|
# This search is not foolproof
|
|
LogText "Performing simple ps scan (busybox)"
|
|
PSOPTIONS=" -o args="
|
|
FIND=$(${PSBINARY:-ps} ${PSOPTIONS} | ${GREPBINARY:-grep} -E "( |/)${search}" | ${GREPBINARY:-grep} -v "grep")
|
|
else
|
|
if [ -n "${users}" ]; then
|
|
for u in ${users}; do
|
|
user_uid=$(getent passwd "${u}" 2> /dev/null | ${AWKBINARY:-awk} -F: '{print $3}')
|
|
# Only perform search if user exists and we had no match yet
|
|
if [ -n "${user_uid}" ]; then
|
|
if [ -z "${FIND}" ]; then
|
|
if [ ${PARTIAL_SEARCH} -eq 1 ]; then
|
|
LogText "Performing ps scan using partial match and for uid ${user_uid}"
|
|
FIND=$(${PSBINARY:-ps} -u "${user_uid}" -o comm= "${search}" | ${AWKBINARY:-awk} -v pattern="${search}" '$0 ~ pattern {print}')
|
|
else
|
|
LogText "Performing ps scan using exact match and for uid ${user_uid}"
|
|
FIND=$(${PSBINARY:-ps} -u "${user_uid}" -o comm= "${search}" | ${AWKBINARY:-awk} -v pattern="^${search}$" '$0 ~ pattern {print}')
|
|
fi
|
|
fi
|
|
fi
|
|
done
|
|
else
|
|
case "${OS}" in
|
|
"Linux")
|
|
PSOPTIONS=" -o args= -C ${search}"
|
|
;;
|
|
esac
|
|
if [ ${PARTIAL_SEARCH} -eq 1 ]; then
|
|
LogText "Performing ps scan using partial match and without uid"
|
|
FIND=$(${PSBINARY:-ps} ${PSOPTIONS} | ${AWKBINARY:-awk} -v pattern="${search}" '$0 ~ pattern {print}')
|
|
else
|
|
LogText "Performing ps scan using exact match and without uid"
|
|
FIND=$(${PSBINARY:-ps} ${PSOPTIONS} | ${AWKBINARY:-awk} -v pattern="^${search}$" '$0 ~ pattern {print}')
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [ -n "${FIND}" ]; then
|
|
RUNNING=1
|
|
LogText "IsRunning: process '${search}' found (${FIND})"
|
|
return 0
|
|
else
|
|
LogText "IsRunning: process '${search}' not found"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : IsNotebook
|
|
# Description : Check if file or directory is owned by root
|
|
# Returns : exit code (0 = True, 1 = False, 255 = Unknown)
|
|
################################################################################
|
|
|
|
IsNotebook() {
|
|
FIND=$(which laptop-detect 2> /dev/null | grep -v "no [^ ]* in ")
|
|
if [ -n "${FIND}" ]; then
|
|
Debug "Testing if we are a notebook"
|
|
laptop-detect
|
|
if [ $? -eq 0 ]; then SYSTEM_IS_NOTEBOOK=1; Debug "System is a notebook according to laptop-detect"
|
|
elif [ $? -eq 1 ]; then SYSTEM_IS_NOTEBOOK=0; Debug "System is a NOT a notebook according to laptop-detect"; fi
|
|
Report "notebook=${SYSTEM_IS_NOTEBOOK}"
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : IsOwnedByRoot
|
|
# Description : Check if file or directory is owned by root
|
|
# Returns : 0 (true), 1 (false), or 255 (unknown)
|
|
################################################################################
|
|
|
|
IsOwnedByRoot() {
|
|
PERMS=""
|
|
if [ $# -eq 1 ]; then
|
|
FILE="$1"
|
|
case $OS in
|
|
"AIX")
|
|
if [ ! "${ISTATBINARY}" = "" ]; then PERMS=$(${ISTATBINARY} ${FILE} | sed "s/Owner: //" | sed "s/[a-zA-Z() ]//g"); fi
|
|
;;
|
|
"Linux")
|
|
if [ ! "${STATBINARY}" = "" ]; then PERMS=$(${STATBINARY} -c "%u:%g" ${FILE}); fi
|
|
;;
|
|
"FreeBSD")
|
|
if [ ! "${STATBINARY}" = "" ]; then PERMS=$(${STATBINARY} -f "%u:%g" ${FILE}); fi
|
|
;;
|
|
esac
|
|
# Fallback with ls (for other platforms, or when a test did not reveal any output)
|
|
if [ "${PERMS}" = "" ]; then
|
|
PERMS=$(ls -n ${FILE} | ${AWKBINARY} '{ print $3":"$4 }')
|
|
fi
|
|
else
|
|
ReportException "IsOwnedByRoot" "Functions needs 1 argument"
|
|
return 255
|
|
fi
|
|
if [ "${PERMS}" = "0:0" ]; then
|
|
if IsDeveloperMode; then LogText "Debug: found incorrect file permissions on ${FILE}"; fi
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : IsVerbose()
|
|
# Description : Check if --verbose option is used to show more details on screen
|
|
#
|
|
# Parameters : <none>
|
|
# Returns : exit code (0 =true, 1 =false)
|
|
################################################################################
|
|
|
|
IsVerbose() {
|
|
if [ ${VERBOSE} -eq 1 ]; then return 0; else return 1; fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : IsVirtualMachine()
|
|
# Description : Determine whether it is a virtual machine
|
|
# Parameters : <none>
|
|
# Returns : exit code (0 = True, 1 = False, 2 = Unknown)
|
|
# variable: ISVIRTUALMACHINE (0-2)
|
|
# variable: VMTYPE
|
|
# variable: VMFULLTYPE
|
|
################################################################################
|
|
|
|
IsVirtualMachine() {
|
|
LogText "Test: Determine if this system is a virtual machine"
|
|
# 0 = no, 1 = yes, 2 = unknown
|
|
ISVIRTUALMACHINE=2; VMTYPE="unknown"; VMFULLTYPE="Unknown"
|
|
SHORT=""
|
|
|
|
if [ ${SKIP_VM_DETECTION} -eq 1 ]; then
|
|
return 2
|
|
fi
|
|
|
|
# lxc environ detection
|
|
if [ -z "${SHORT}" ]; then
|
|
if [ -f /proc/1/environ ]; then
|
|
FIND=$(grep -qa 'container=lxc' /proc/1/environ 2> /dev/null)
|
|
if [ $? -eq 0 ]; then
|
|
SHORT=lxc
|
|
LogText "Result: found ${SHORT}"
|
|
fi
|
|
fi
|
|
else
|
|
LogText "Result: skipped lxc environ detection test, as we already found machine type"
|
|
fi
|
|
|
|
# facter
|
|
if [ -z "${SHORT}" ]; then
|
|
if [ -x /usr/bin/facter ] || [ -x /usr/local/bin/facter ]; then
|
|
case "$(facter is_virtual)" in
|
|
"true")
|
|
SHORT=$(facter virtual)
|
|
LogText "Result: found ${SHORT}"
|
|
;;
|
|
"false")
|
|
LogText "Result: facter says this machine is not a virtual"
|
|
;;
|
|
esac
|
|
else
|
|
LogText "Result: facter utility not found"
|
|
fi
|
|
else
|
|
LogText "Result: skipped facter test, as we already found machine type"
|
|
fi
|
|
|
|
# systemd
|
|
if [ -z "${SHORT}" ]; then
|
|
if [ -x /usr/bin/systemd-detect-virt ]; then
|
|
LogText "Test: trying to guess virtualization technology with systemd-detect-virt"
|
|
FIND=$(/usr/bin/systemd-detect-virt)
|
|
if [ -n "${FIND}" ]; then
|
|
LogText "Result: found ${FIND}"
|
|
SHORT="${FIND}"
|
|
fi
|
|
else
|
|
LogText "Result: systemd-detect-virt not found"
|
|
fi
|
|
else
|
|
LogText "Result: skipped systemd test, as we already found machine type"
|
|
fi
|
|
|
|
# lscpu
|
|
# Values: VMware
|
|
if [ -z "${SHORT}" ]; then
|
|
if [ -x /usr/bin/lscpu ]; then
|
|
LogText "Test: trying to guess virtualization with lscpu"
|
|
FIND=$(lscpu | grep -i "^Hypervisor Vendor" | awk -F: '{ print $2 }' | sed 's/ //g')
|
|
if [ -n "${FIND}" ]; then
|
|
LogText "Result: found ${FIND}"
|
|
SHORT="${FIND}"
|
|
else
|
|
LogText "Result: can't find hypervisor vendor with lscpu"
|
|
fi
|
|
else
|
|
LogText "Result: lscpu not found"
|
|
fi
|
|
else
|
|
LogText "Result: skipped lscpu test, as we already found machine type"
|
|
fi
|
|
|
|
# dmidecode
|
|
# Values: VMware Virtual Platform / VirtualBox
|
|
if [ -z "${SHORT}" ]; then
|
|
# Try to find dmidecode in case we did not check binaries (e.g. lynis show environment)
|
|
if [ ${CHECK_BINARIES} -eq 0 ]; then DMIDECODEBINARY=$(command -v dmidecode 2> /dev/null); fi
|
|
if [ -n "${DMIDECODEBINARY}" -a -x "${DMIDECODEBINARY}" -a ${PRIVILEGED} -eq 1 ]; then
|
|
LogText "Test: trying to guess virtualization with dmidecode"
|
|
FIND=$(${DMIDECODEBINARY} -s system-product-name | awk '{ print $1 }')
|
|
if [ -n "${FIND}" ]; then
|
|
LogText "Result: found ${FIND}"
|
|
SHORT="${FIND}"
|
|
else
|
|
LogText "Result: can't find product name with dmidecode"
|
|
fi
|
|
else
|
|
LogText "Result: dmidecode not found (or no access)"
|
|
fi
|
|
else
|
|
LogText "Result: skipped dmidecode test, as we already found machine type"
|
|
fi
|
|
|
|
# Other options
|
|
# SaltStack: salt-call grains.get virtual
|
|
# < needs snippet >
|
|
|
|
# Try common guest processes
|
|
if [ -z "${SHORT}" ]; then
|
|
LogText "Test: trying to guess virtual machine type by running processes"
|
|
|
|
# VMware
|
|
if IsRunning vmware-guestd; then SHORT="vmware"
|
|
elif IsRunning vmtoolsd; then SHORT="vmware"
|
|
fi
|
|
|
|
# VirtualBox based on guest services
|
|
if IsRunning vboxguest-service; then SHORT="virtualbox"
|
|
elif IsRunning VBoxClient; then SHORT="virtualbox"
|
|
elif IsRunning VBoxService; then SHORT="virtualbox"
|
|
fi
|
|
else
|
|
LogText "Result: skipped processes test, as we already found platform"
|
|
fi
|
|
|
|
# Amazon EC2
|
|
if [ -z "${SHORT}" ]; then
|
|
LogText "Test: checking specific files for Amazon"
|
|
if [ -f /etc/ec2_version -a -s /etc/ec2_version ]; then
|
|
SHORT="amazon-ec2"
|
|
else
|
|
LogText "Result: system not hosted on Amazon"
|
|
fi
|
|
else
|
|
LogText "Result: skipped Amazon EC2 test, as we already found platform"
|
|
fi
|
|
|
|
# sysctl values
|
|
if [ -z "${SHORT}" ]; then
|
|
LogText "Test: trying to guess virtual machine type by sysctl keys"
|
|
|
|
# FreeBSD: hw.hv_vendor (remains empty for VirtualBox)
|
|
# NetBSD: machdep.dmi.system-product
|
|
# OpenBSD: hw.product
|
|
FIND=$(sysctl -a 2> /dev/null | grep -E "(hw.product|machdep.dmi.system-product)" | head -n 1 | sed 's/ = /=/' | awk -F= '{ print $2 }')
|
|
if [ ! "${FIND}" = "" ]; then
|
|
SHORT="${FIND}"
|
|
fi
|
|
else
|
|
LogText "Result: skipped sysctl test, as we already found platform"
|
|
fi
|
|
|
|
# lshw
|
|
if [ -z "${SHORT}" ]; then
|
|
if [ ${PRIVILEGED} -eq 1 ]; then
|
|
if [ -x /usr/bin/lshw ]; then
|
|
LogText "Test: trying to guess virtualization with lshw"
|
|
FIND=$(lshw -quiet -class system 2> /dev/null | awk '{ if ($1=="product:") { print $2 }}')
|
|
if HasData "${FIND}"; then
|
|
LogText "Result: found ${FIND}"
|
|
SHORT="${FIND}"
|
|
fi
|
|
else
|
|
LogText "Result: lshw not found"
|
|
fi
|
|
else
|
|
LogText "Result: skipped lshw test, as we are non-privileged and need more permissions to run lshw"
|
|
fi
|
|
else
|
|
LogText "Result: skipped lshw test, as we already found machine type"
|
|
fi
|
|
|
|
# Check if we caught some string along all tests
|
|
if [ -n "${SHORT}" ]; then
|
|
# Lowercase and see if we found a match
|
|
SHORT=$(echo ${SHORT} | awk '{ print $1 }' | tr '[:upper:]' '[:lower:]')
|
|
|
|
case ${SHORT} in
|
|
amazon-ec2) ISVIRTUALMACHINE=1; VMTYPE="amazon-ec2"; VMFULLTYPE="Amazon AWS EC2 Instance" ;;
|
|
bochs) ISVIRTUALMACHINE=1; VMTYPE="bochs"; VMFULLTYPE="Bochs CPU emulation" ;;
|
|
docker) ISVIRTUALMACHINE=1; VMTYPE="docker"; VMFULLTYPE="Docker container" ;;
|
|
kvm) ISVIRTUALMACHINE=1; VMTYPE="kvm"; VMFULLTYPE="KVM" ;;
|
|
lxc) ISVIRTUALMACHINE=1; VMTYPE="lxc"; VMFULLTYPE="Linux Containers" ;;
|
|
lxc-libvirt) ISVIRTUALMACHINE=1; VMTYPE="lxc-libvirt"; VMFULLTYPE="libvirt LXC driver (Linux Containers)" ;;
|
|
microsoft) ISVIRTUALMACHINE=1; VMTYPE="microsoft"; VMFULLTYPE="Microsoft Virtual PC" ;;
|
|
openvz) ISVIRTUALMACHINE=1; VMTYPE="openvz"; VMFULLTYPE="OpenVZ" ;;
|
|
oracle|virtualbox) ISVIRTUALMACHINE=1; VMTYPE="virtualbox"; VMFULLTYPE="Oracle VM VirtualBox" ;;
|
|
qemu) ISVIRTUALMACHINE=1; VMTYPE="qemu"; VMFULLTYPE="QEMU" ;;
|
|
systemd-nspawn) ISVIRTUALMACHINE=1; VMTYPE="systemd-nspawn"; VMFULLTYPE="Systemd Namespace container" ;;
|
|
uml) ISVIRTUALMACHINE=1; VMTYPE="uml"; VMFULLTYPE="User-Mode Linux (UML)" ;;
|
|
vmware) ISVIRTUALMACHINE=1; VMTYPE="vmware"; VMFULLTYPE="VMware product" ;;
|
|
xen) ISVIRTUALMACHINE=1; VMTYPE="xen"; VMFULLTYPE="XEN" ;;
|
|
zvm) ISVIRTUALMACHINE=1; VMTYPE="zvm"; VMFULLTYPE="IBM z/VM" ;;
|
|
openstack) ISVIRTUALMACHINE=1; VMTYPE="openstack"; VMFULLTYPE="Openstack Nova" ;;
|
|
*) LogText "Result: Unknown virtualization type, so most likely system is physical" ;;
|
|
esac
|
|
fi
|
|
|
|
# Check final status
|
|
if [ ${ISVIRTUALMACHINE} -eq 1 ]; then
|
|
LogText "Result: found virtual machine (type: ${VMTYPE}, ${VMFULLTYPE})"
|
|
Report "vm=1"
|
|
Report "vmtype=${VMTYPE}"
|
|
elif [ ${ISVIRTUALMACHINE} -eq 2 ]; then
|
|
LogText "Result: unknown if this system is a virtual machine"
|
|
Report "vm=2"
|
|
else
|
|
LogText "Result: system seems to be non-virtual"
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : IsWorldReadable()
|
|
# Description : Determines if a file is readable for all users (world)
|
|
#
|
|
# Input : $1 = path (string)
|
|
# Returns : exit code (0 = readable, 1 = not readable, 255 = error)
|
|
# Usage : if IsWorldReadable /etc/motd; then echo "File is readable"; fi
|
|
################################################################################
|
|
|
|
IsWorldReadable() {
|
|
if [ $# -eq 0 ]; then ExitFatal "Missing parameter when calling IsWorldReadable function"; fi
|
|
sFILE=$1
|
|
# Check for symlink
|
|
if [ -L ${sFILE} ]; then
|
|
ShowSymlinkPath ${sFILE}
|
|
if [ ! "${SYMLINK}" = "" ]; then sFILE="${SYMLINK}"; fi
|
|
fi
|
|
if [ -f ${sFILE} -o -d ${sFILE} ]; then
|
|
FINDVAL=$(ls -ld ${sFILE} | cut -c 8)
|
|
if [ "${FINDVAL}" = "r" ]; then return 0; else return 1; fi
|
|
else
|
|
return 255
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : IsWorldExecutable()
|
|
# Description : Determines if a file is executable for all users (world)
|
|
#
|
|
# Input : $1 = path (string)
|
|
# Returns : exit code (0 = executable, 1 = not executable, 255 = error)
|
|
# Usage : if IsWorldExecutable /bin/ps; then echo "File is executable"; fi
|
|
################################################################################
|
|
|
|
# Function IsWorldExecutable
|
|
IsWorldExecutable() {
|
|
if [ $# -eq 0 ]; then ExitFatal "Missing parameter when calling IsWorldExecutable function"; fi
|
|
sFILE=$1
|
|
# Check for symlink
|
|
if [ -L ${sFILE} ]; then
|
|
ShowSymlinkPath ${sFILE}
|
|
if [ ! "${SYMLINK}" = "" ]; then sFILE="${SYMLINK}"; fi
|
|
fi
|
|
if [ -f ${sFILE} -o -d ${sFILE} ]; then
|
|
FINDVAL=$(ls -l ${sFILE} | cut -c 10)
|
|
if [ "${FINDVAL}" = "x" ]; then return 0; else return 1; fi
|
|
else
|
|
return 255
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : IsWorldWritable()
|
|
# Description : Determines if a file is writable for all users
|
|
#
|
|
# Parameters : $1 = path
|
|
# Returns : exit code (0 = writable, 1 = not writable, 255 = error)
|
|
# Usage : if IsWorldWritable /etc/motd; then echo "File is writable"; fi
|
|
################################################################################
|
|
|
|
IsWorldWritable() {
|
|
if [ $# -eq 0 ]; then ExitFatal "Missing parameter when calling IsWorldWritable function"; fi
|
|
sFILE=$1
|
|
FileIsWorldWritable=""
|
|
# Check for symlink
|
|
if [ -L ${sFILE} ]; then
|
|
ShowSymlinkPath ${sFILE}
|
|
if [ ! "${SYMLINK}" = "" ]; then sFILE="${SYMLINK}"; fi
|
|
fi
|
|
# Only check if target is a file or directory
|
|
if [ -f ${sFILE} -o -d ${sFILE} ]; then
|
|
FINDVAL=$(ls -ld ${sFILE} | cut -c 9)
|
|
if IsDeveloperMode; then Debug "File mode of ${sFILE} is ${FINDVAL}"; fi
|
|
if [ "${FINDVAL}" = "w" ]; then return 0; else return 1; fi
|
|
else
|
|
return 255
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : LogText()
|
|
# Description : Function logtext (redirect data ($1) to log file)
|
|
#
|
|
# Parameters : $1 = text (string)
|
|
# Returns : <nothing>
|
|
# Usage : LogText "This line goes into the log file"
|
|
################################################################################
|
|
|
|
LogText() {
|
|
if [ ! "${LOGFILE}" = "" -a ${LOGTEXT} -eq 1 ]; then CDATE=$(date "+%Y-%m-%d %H:%M:%S"); echo "${CDATE} $1" >> ${LOGFILE}; fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : LogTextBreak()
|
|
# Description : Add a separator to log file between sections, tests etc
|
|
# Returns : <nothing>
|
|
################################################################################
|
|
|
|
LogTextBreak() {
|
|
if [ ! "${LOGFILE}" = "" -a ${LOGTEXT} -eq 1 ]; then
|
|
CDATE=$(date "+%Y-%m-%d %H:%M:%S")
|
|
echo "${CDATE} ====" >> ${LOGFILE}
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : PackageIsInstalled()
|
|
# Description : Determines if a package is installed
|
|
# Returns : exit code
|
|
# Notes : this function is not used yet, but created in advance to allow
|
|
# the addition of support for all operating systems
|
|
################################################################################
|
|
|
|
PackageIsInstalled() {
|
|
exit_code=255
|
|
|
|
# First parameter is package name (or __dummy__ for initial test to see if package manager is available and works as expected)
|
|
if [ $# -eq 1 ]; then
|
|
package="$1"
|
|
else
|
|
Fatal "Incorrect usage of PackageIsInstalled function"
|
|
fi
|
|
|
|
if [ -n "${DNFBINARY}" ]; then
|
|
output=$(${DNFBINARY} --quiet --cacheonly --noplugins --assumeno info --installed ${package} > /dev/null 2>&1)
|
|
exit_code=$?
|
|
elif [ -n "${DPKGBINARY}" ]; then
|
|
output=$(${DPKGBINARY} -l ${package} 2> /dev/null | ${GREPBINARY} "^ii")
|
|
exit_code=$?
|
|
elif [ -n "${EQUERYBINARY}" ]; then
|
|
output=$(${EQUERYBINARY} --quiet ${package} > /dev/null 2>&1)
|
|
exit_code=$? # 0=package installed, 3=package not installed
|
|
elif [ -n "${PACMANBINARY}" ]; then
|
|
output=$(${PACMANBINARY} -Qs ${PKG} | grep "local/${PKG} " >/dev/null 2>&1)
|
|
exit_code=$? # 0=package installed, 1=package not installed
|
|
elif [ -n "${PKG_BINARY}" ]; then
|
|
output=$(${PKG_BINARY} -N info ${package} >/dev/null 2>&1)
|
|
exit_code=$? # 0=package installed, 70=invalid package
|
|
elif [ -n "${PKGINFOBINARY}" ]; then
|
|
output=$(${PKGINFOBINARY} -q -e ${package} >/dev/null 2>&1)
|
|
exit_code=$? # 0=package installed, 1=package not installed
|
|
# Slackware also has RPM for some reason and that's why this test precedes the RPMBINARY test
|
|
elif [ "${OS_NAME}" = "Slackware Linux" -a -d "${ROOTDIR}/var/lib/pkgtools/packages" ]; then
|
|
output=$( ls ${ROOTDIR}/var/lib/pkgtools/packages/ 2> /dev/null | ${GREPBINARY} "^${package}-[^-]\+-[^-]\+-[^-]\+$" )
|
|
exit_code=$?
|
|
elif [ -n "${RPMBINARY}" ]; then
|
|
output=$(${RPMBINARY} --quiet -q ${package} > /dev/null 2>&1)
|
|
exit_code=$?
|
|
elif [ -n "${SWUPDBINARY}" ]; then
|
|
output=$(${SWUPDBINARY} bundle-list > /dev/null 2>&1 | ${GREPBINARY} "^${package}$")
|
|
exit_code=$?
|
|
elif [ -n "${ZYPPERBINARY}" ]; then
|
|
output=$(${ZYPPERBINARY} --quiet --non-interactive search --installed -i ${package} 2> /dev/null | grep "^i")
|
|
if [ -n "${output}" ]; then exit_code=0; else exit_code=1; fi
|
|
elif [ -n "${XBPSBINARY}" ]; then
|
|
output=$(${XBPSBINARY} ${package} 2> /dev/null | ${GREPBINARY} "^ii")
|
|
exit_code=$?
|
|
elif [ -n "${APKBINARY}" ]; then
|
|
output=$(${APKBINARY} list --installed ${package} 2> /dev/null | ${GREPBINARY} ${package})
|
|
exit_code=$?
|
|
else
|
|
if [ "${package}" != "__dummy__" ]; then
|
|
ReportException "PackageIsInstalled:01 (test=${TEST_NO:-unknown})"
|
|
fi
|
|
fi
|
|
|
|
# Give thumbs up if dummy package is used during initial test for package manager availability
|
|
if [ "${package}" = "__dummy__" ]; then
|
|
# There should be no positive match on this dummy package
|
|
if [ ${exit_code} -eq 0 ]; then
|
|
exit_code=1
|
|
elif [ ${exit_code} -eq 255 ]; then
|
|
exit_code=1
|
|
else
|
|
exit_code=0
|
|
fi
|
|
fi
|
|
|
|
return ${exit_code}
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : ParseProfiles()
|
|
# Description : Check file permissions and parse data from profiles
|
|
# Parameters : <none>
|
|
# Returns : <nothing>
|
|
################################################################################
|
|
|
|
ParseProfiles() {
|
|
SafePerms ${INCLUDEDIR}/profiles
|
|
. ${INCLUDEDIR}/profiles
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : ParseTestValues()
|
|
# Description : Parse values from a specific test
|
|
#
|
|
# Parameters : $1 = service (e.g. ssh)
|
|
# Returns : CHECK_VALUES_ARRAY variable
|
|
################################################################################
|
|
|
|
ParseTestValues() {
|
|
RETVAL=1
|
|
FOUND=0
|
|
CHECK_VALUES_ARRAY=""
|
|
if [ $# -gt 0 -a ! "${CHECK_OPTION_ARRAY}" = "" ]; then
|
|
for I in ${CHECK_OPTION_ARRAY}; do
|
|
Debug "Array value: ${I}"
|
|
SPLIT=$(echo ${I} | sed 's/,/\n/g')
|
|
for ITEM in ${SPLIT}; do
|
|
FUNCTION=""
|
|
VALUE=""
|
|
SEARCH=""
|
|
SERVICE=""
|
|
ITEM_KEY=$(echo ${ITEM} | awk -F: '{print $1}')
|
|
ITEM_VALUE=$(echo ${ITEM} | awk -F: '{print $2}')
|
|
Debug "Array item: ${ITEM_KEY} with value ${ITEM_VALUE}"
|
|
case ${ITEM_KEY} in
|
|
"function")
|
|
case ${ITEM_VALUE} in
|
|
"equals")
|
|
FUNCTION="eq"
|
|
;;
|
|
esac
|
|
;;
|
|
"service")
|
|
if [ "${ITEM_VALUE}" = "$1" ]; then
|
|
FOUND=1
|
|
fi
|
|
;;
|
|
"value")
|
|
VALUE="${ITEM_VALUE}"
|
|
;;
|
|
*)
|
|
echo "Incorrect call to function ParseTestValues"; ExitFatal
|
|
;;
|
|
esac
|
|
if [ ${FOUND} -eq 1 ]; then
|
|
CHECK_VALUES_ARRAY="${CHECK_VALUES_ARRAY} ${SEARCH}:${VALUE}:${FUNCTION}:"
|
|
FOUND=0
|
|
fi
|
|
done
|
|
done
|
|
RETVAL=1
|
|
fi
|
|
return ${RETVAL}
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : ParseNginx()
|
|
# Description : Parse nginx configuration lines
|
|
#
|
|
# Parameters : $1 = file (should be readable and tested upfront)
|
|
# Returns : <nothing>
|
|
################################################################################
|
|
|
|
ParseNginx() {
|
|
COUNT=0
|
|
BREADCRUMB=""
|
|
if [ $# -eq 0 ]; then ExitFatal "No arguments provided to ParseNginx()"; fi
|
|
CONFIG_FILE=$1
|
|
|
|
# Create temporary files
|
|
CreateTempFile || ExitFatal "Could not create temporary file"
|
|
TMP_NGINX_FILE_RAW="${TEMP_FILE}"
|
|
CreateTempFile || ExitFatal "Could not create temporary file"
|
|
TMP_NGINX_FILE="${TEMP_FILE}"
|
|
|
|
# Strip out spaces, tabs and line breaks
|
|
awk '{$1=$1;print $0}' ${CONFIG_FILE} > ${TMP_NGINX_FILE_RAW}
|
|
# Now clean up the file further (combine lines, remove commented lines and empty lines)
|
|
sed 's#\\$##g' ${TMP_NGINX_FILE_RAW} | grep -v "^#" | grep -v "^$" > ${TMP_NGINX_FILE}
|
|
|
|
LogText "Action: parsing configuration file ${CONFIG_FILE}"
|
|
COUNT=$(( COUNT + 1))
|
|
FIND=$(sed 's/ /:space:/g' ${TMP_NGINX_FILE})
|
|
DEPTH=0
|
|
for I in ${FIND}; do
|
|
I=$(echo ${I} | sed 's/:space:/ /g' | sed 's/;$//' | sed 's/ #.*$//')
|
|
OPTION=$(echo ${I} | awk '{ print $1 }')
|
|
# Use quotes here to prevent wildcard expansion
|
|
VALUE=$(echo "${I}"| cut -d' ' -f2-)
|
|
LogText "Result: found option ${OPTION} in ${CONFIG_FILE} with value '${VALUE}'"
|
|
STORE_SETTING=1
|
|
case ${OPTION} in
|
|
"events")
|
|
BREADCRUMB="${BREADCRUMB}/events"
|
|
DEPTH=$(( DEPTH + 1))
|
|
STORE_SETTING=0
|
|
NGINX_EVENTS_COUNTER=$(( NGINX_EVENTS_COUNTER + 1 ))
|
|
;;
|
|
"http")
|
|
BREADCRUMB="${BREADCRUMB}/http"
|
|
DEPTH=$(( DEPTH + 1))
|
|
STORE_SETTING=0
|
|
NGINX_HTTP_COUNTER=$(( NGINX_HTTP_COUNTER + 1 ))
|
|
;;
|
|
"location")
|
|
BREADCRUMB="${BREADCRUMB}/location"
|
|
DEPTH=$(( DEPTH + 1))
|
|
STORE_SETTING=0
|
|
NGINX_LOCATION_COUNTER=$(( NGINX_LOCATION_COUNTER + 1 ))
|
|
;;
|
|
"server")
|
|
BREADCRUMB="${BREADCRUMB}/server"
|
|
DEPTH=$(( DEPTH + 1))
|
|
STORE_SETTING=0
|
|
NGINX_SERVER_COUNTER=$(( NGINX_SERVER_COUNTER + 1 ))
|
|
;;
|
|
"}")
|
|
BREADCRUMB=$(echo ${BREADCRUMB} | awk -F/ 'sub(FS $NF,x)')
|
|
DEPTH=$(( DEPTH - 1))
|
|
STORE_SETTING=0
|
|
;;
|
|
access_log)
|
|
if [ "${VALUE}" = "off" ]; then
|
|
LogText "Result: found logging disabled for one virtual host"
|
|
NGINX_ACCESS_LOG_DISABLED=1
|
|
else
|
|
if [ ! "${VALUE}" = "" ]; then
|
|
# If multiple values follow, select first one
|
|
VALUE=$(echo ${VALUE} | awk '{ print $1 }')
|
|
# Find both, log files provided with full or relative path
|
|
if [ ! -f ${VALUE} -a ! -f "${CONFIG_FILE%nginx.conf}${VALUE}" ]; then
|
|
LogText "Result: could not find log file ${VALUE} referenced in nginx configuration"
|
|
NGINX_ACCESS_LOG_MISSING=1
|
|
fi
|
|
fi
|
|
fi
|
|
;;
|
|
# Headers
|
|
add_header)
|
|
HEADER=$(echo ${VALUE} | awk '{ print $1 }')
|
|
HEADER_VALUE=$(echo ${VALUE} | cut -d' ' -f2-)
|
|
LogText "Result: found header ${HEADER} with value ${HEADER_VALUE}"
|
|
#Report "nginx_header[]=${HEADER}|${HEADER_VALUE}|"
|
|
;;
|
|
alias)
|
|
NGINX_ALIAS_FOUND=1
|
|
;;
|
|
allow)
|
|
NGINX_ALLOW_FOUND=1
|
|
;;
|
|
autoindex)
|
|
;;
|
|
deny)
|
|
NGINX_DENY_FOUND=1
|
|
;;
|
|
expires)
|
|
NGINX_EXPIRES_FOUND=1
|
|
;;
|
|
error_log)
|
|
# Check if debug is appended
|
|
FIND=$(echo ${VALUE} | awk '{ if ($2=="debug") { print 1 } else { print 0 }}')
|
|
if [ ${FIND} -eq 1 ]; then
|
|
NGINX_ERROR_LOG_DEBUG=1
|
|
fi
|
|
# Check if log file exists
|
|
FILE=$(echo ${VALUE} | awk '{ print $1 }')
|
|
if [ ! "${FILE}" = "" ]; then
|
|
# Find both, log files provided with full or relative path
|
|
if [ ! -f ${FILE} -a ! -f "${CONFIG_FILE%nginx.conf}${FILE}" ]; then
|
|
NGINX_ERROR_LOG_MISSING=1
|
|
fi
|
|
else
|
|
LogText "Warning: did not find a filename after error_log in nginx configuration"
|
|
fi
|
|
;;
|
|
error_page)
|
|
;;
|
|
fastcgi_intercept_errors)
|
|
;;
|
|
fastcgi_param)
|
|
NGINX_FASTCGI_FOUND=1
|
|
NGINX_FASTCGI_PARAMS_FOUND=1
|
|
;;
|
|
fastcgi_pass)
|
|
NGINX_FASTCGI_FOUND=1
|
|
NGINX_FASTCGI_PASS_FOUND=1
|
|
;;
|
|
fastcgi_pass_header)
|
|
;;
|
|
include)
|
|
if [ -f "${VALUE}" ]; then
|
|
FOUND=0
|
|
for CONF in ${NGINX_CONF_FILES}; do
|
|
if [ "${CONF}" = "${VALUE}" ]; then FOUND=1; LogText "Found this file already in our configuration files array, not adding to queue"; fi
|
|
done
|
|
for CONF in ${NGINX_CONF_FILES_ADDITIONS}; do
|
|
if [ "${CONF}" = "${VALUE}" ]; then FOUND=1; LogText "Found this file already in our configuration files array (additions), not adding to queue"; fi
|
|
done
|
|
if [ ${FOUND} -eq 0 ]; then NGINX_CONF_FILES_ADDITIONS="${NGINX_CONF_FILES_ADDITIONS} ${VALUE}"; fi
|
|
# Check if include value is a relative path only
|
|
elif [ -f "${CONFIG_FILE%nginx.conf}${VALUE%;*}" ]; then
|
|
VALUE="${CONFIG_FILE%nginx.conf}${VALUE}"
|
|
FOUND=0
|
|
for CONF in ${NGINX_CONF_FILES}; do
|
|
if [ "${CONF}" = "${VALUE}" ]; then FOUND=1; LogText "Found this file already in our configuration files array, not adding to queue"; fi
|
|
done
|
|
for CONF in ${NGINX_CONF_FILES_ADDITIONS}; do
|
|
if [ "${CONF}" = "${VALUE}" ]; then FOUND=1; LogText "Found this file already in our configuration files array (additions), not adding to queue"; fi
|
|
done
|
|
if [ ${FOUND} -eq 0 ]; then NGINX_CONF_FILES_ADDITIONS="${NGINX_CONF_FILES_ADDITIONS} ${VALUE}"; fi
|
|
# Check for additional config files included as follows
|
|
# "include sites-enabled/*.conf" (relative path)
|
|
# "include /etc/nginx/sites-enabled/*.conf" (absolute path)
|
|
elif [ $(echo "${VALUE}" | grep -F -c "*.conf") -gt 0 ]; then
|
|
# Check if path is absolute or relative
|
|
case $VALUE in
|
|
/*)
|
|
# Absolute path, so wildcard pattern is already correct
|
|
CONF_WILDCARD=${VALUE%;*}
|
|
;;
|
|
*)
|
|
# Relative path, so construct absolute path for wildcard pattern
|
|
CONF_WILDCARD=${CONFIG_FILE%nginx.conf}${VALUE%;*}
|
|
;;
|
|
esac
|
|
for FOUND_CONF in ${CONF_WILDCARD}; do
|
|
if [ "${FOUND_CONF}" = "${CONF_WILDCARD}" ]; then
|
|
LogText "Found no match for wildcard pattern: ${CONF_WILDCARD}"
|
|
break
|
|
fi
|
|
FOUND=0
|
|
for CONF in ${NGINX_CONF_FILES}; do
|
|
if [ "${CONF}" = "${FOUND_CONF}" ]; then FOUND=1; LogText "Found this file already in our configuration files array, not adding to queue"; fi
|
|
done
|
|
for CONF in ${NGINX_CONF_FILES_ADDITIONS}; do
|
|
if [ "${CONF}" = "${FOUND_CONF}" ]; then FOUND=1; LogText "Found this file already in our configuration files array (additions), not adding to queue"; fi
|
|
done
|
|
if [ ${FOUND} -eq 0 ]; then NGINX_CONF_FILES_ADDITIONS="${NGINX_CONF_FILES_ADDITIONS} ${FOUND_CONF}"; fi
|
|
done
|
|
else
|
|
LogText "Result: this include does not point to a file"
|
|
fi
|
|
;;
|
|
index)
|
|
;;
|
|
keepalive_timeout)
|
|
;;
|
|
listen)
|
|
NGINX_LISTEN_FOUND=1
|
|
# Test for ssl on listen statement
|
|
FIND_SSL=$(echo ${VALUE} | grep ssl)
|
|
if [ ! "${FIND_SSL}" = "" ]; then NGINX_SSL_ON=1; fi
|
|
;;
|
|
location)
|
|
NGINX_LOCATION_FOUND=1
|
|
;;
|
|
return)
|
|
NGINX_RETURN_FOUND=1
|
|
;;
|
|
root)
|
|
NGINX_ROOT_FOUND=1
|
|
;;
|
|
server_name)
|
|
;;
|
|
ssl)
|
|
if [ "${VALUE}" = "on" ]; then NGINX_SSL_ON=1; fi
|
|
;;
|
|
ssl_certificate)
|
|
LogText "Found SSL certificate in nginx configuration"
|
|
;;
|
|
ssl_certificate_key)
|
|
;;
|
|
ssl_ciphers)
|
|
NGINX_SSL_CIPHERS=1
|
|
;;
|
|
ssl_prefer_server_ciphers)
|
|
if [ "${VALUE}" = "on" ]; then NGINX_SSL_PREFER_SERVER_CIPHERS=1; fi
|
|
;;
|
|
ssl_protocols)
|
|
NGINX_SSL_PROTOCOLS=1
|
|
VALUE=$(echo ${VALUE} | sed 's/;$//' | tr '[:upper:]' '[:lower:]')
|
|
for ITEM in ${VALUE}; do
|
|
LogText "Result: found protocol ${ITEM}"
|
|
case ${ITEM} in
|
|
"sslv2" | "sslv3" | "tlsv1")
|
|
NGINX_WEAK_SSL_PROTOCOL_FOUND=1
|
|
;;
|
|
esac
|
|
Report "ssl_tls_protocol_enabled[]=${ITEM}"
|
|
ReportDetails --service nginx --field protocol --value "${ITEM}"
|
|
done
|
|
;;
|
|
ssl_session_cache)
|
|
;;
|
|
ssl_session_timeout)
|
|
;;
|
|
types)
|
|
;;
|
|
*)
|
|
LogText "Found unknown option ${OPTION} in nginx configuration"
|
|
;;
|
|
esac
|
|
if [ ${STORE_SETTING} -eq 1 ]; then
|
|
CONFIG_TREE="${BREADCRUMB}"
|
|
if [ -z "${CONFIG_TREE}" ]; then CONFIG_TREE="/"; fi
|
|
if [ -z "${OPTION}" ]; then OPTION="NA"; fi
|
|
if [ -z "${VALUE}" ]; then VALUE="NA"; fi
|
|
StoreNginxSettings --config ${CONFIG_FILE} --tree ${CONFIG_TREE} --depth ${DEPTH} --setting ${OPTION} --value "${VALUE}"
|
|
fi
|
|
done
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : PortIsListening()
|
|
# Description : Check if machine is listening on specified protocol and port
|
|
#
|
|
# Parameters : $1 = protocol
|
|
# $2 = port
|
|
# Returns : exit code (0 = listening, 1 = not listening, 255 = can't perform test)
|
|
# Usage : if PortIsListening "TCP" 22; then echo "Port is listening"; fi
|
|
################################################################################
|
|
|
|
PortIsListening() {
|
|
if [ -z "${LSOFBINARY}" ]; then
|
|
return 255
|
|
else
|
|
if [ $# -eq 2 ] && [ $1 = "TCP" -o $1 = "UDP" ]; then
|
|
LogText "Test: find service listening on $1:$2"
|
|
if [ $1 = "TCP" ]; then FIND=$(${LSOFBINARY}${LSOF_EXTRA_OPTIONS} -i${1} -s${1}:LISTEN -P -n | grep ":${2} "); else FIND=$(${LSOFBINARY}${LSOF_EXTRA_OPTIONS} -i${1} -P -n | grep ":${2} "); fi
|
|
if [ ! "${FIND}" = "" ]; then
|
|
LogText "Result: found service listening on port $2 ($1)"
|
|
return 0
|
|
else
|
|
LogText "Result: did not find service listening on port $2 ($1)"
|
|
return 1
|
|
fi
|
|
else
|
|
return 255
|
|
ReportException ${TEST_NO} "Error in function call to PortIsListening"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : Progress()
|
|
# Description : Displays progress on screen with dots
|
|
#
|
|
# Parameters : $1 = --finish or text (string)
|
|
# Returns : <nothing>
|
|
# Tip : Use this function from Register with the --progress parameter
|
|
################################################################################
|
|
|
|
Progress() {
|
|
if [ ${CRONJOB} -eq 0 ]; then
|
|
if [ ${QUIET} -eq 0 ]; then
|
|
if [ "$1" = "--finish" ]; then
|
|
${ECHOCMD} ""
|
|
else
|
|
# If the No-Break version of echo is known, use that (usually breaks in combination with -e)
|
|
if [ ! "${ECHONB}" = "" ]; then
|
|
${ECHONB} "$1"
|
|
else
|
|
${ECHOCMD} -en "$1"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : RandomString()
|
|
# Description : Displays progress on screen with dots
|
|
#
|
|
# Parameters : $1 = number (amount of characters, optional)
|
|
# Returns : RANDOMSTRING variable
|
|
# Usage : RandomString 32
|
|
################################################################################
|
|
|
|
RandomString() {
|
|
# Check a (pseudo) random character device
|
|
if [ -c /dev/urandom ]; then FILE="/dev/urandom"
|
|
elif [ -c /dev/random ]; then FILE="/dev/random"
|
|
else
|
|
Display "Can not use RandomString function, as there is no random device to be used"
|
|
fi
|
|
if [ $# -eq 0 ]; then SIZE=16; else SIZE=$1; fi
|
|
CSIZE=$((SIZE / 2))
|
|
RANDOMSTRING=$(head -c ${CSIZE} /dev/urandom | od -An -x | tr -d ' ' | cut -c 1-${SIZE})
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : Readonly()
|
|
# Description : Mark a variable as read-only data
|
|
#
|
|
# Returns : <nothing>
|
|
# Notes : new function, not in use yet
|
|
################################################################################
|
|
|
|
Readonly() {
|
|
if [ $# -eq 1 ]; then
|
|
if type -t typeset; then
|
|
typeset -r $1
|
|
else
|
|
Debug "No typeset available to mark variable '$1' as read-only variable"
|
|
fi
|
|
else
|
|
ExitFatal "Expected 1 parameter, received none or multiple"
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : Register()
|
|
# Description : Register a test and see if it has to be run
|
|
#
|
|
# Parameters : multiple, see test
|
|
# Returns : SKIPTEST (0 or 1)
|
|
################################################################################
|
|
|
|
GetTimestamp() {
|
|
ts=0
|
|
# Detect if the implementation of date supports nanoseconds,
|
|
if [ "${OS}" = "Linux" ]; then
|
|
current_nanoseconds=$(date "+%N")
|
|
# Verify if the result of the command is a number
|
|
if [ -n "$current_nanoseconds" ] && [ "$current_nanoseconds" -eq "$current_nanoseconds" ] 2>/dev/null; then
|
|
ts=$(date "+%s%N")
|
|
else
|
|
ts=$(date "+%s")
|
|
fi
|
|
else
|
|
ts=$(date "+%s")
|
|
fi
|
|
echo $ts
|
|
}
|
|
|
|
Register() {
|
|
# Do not insert a log break, if previous test was not logged
|
|
if [ ${SKIPLOGTEST} -eq 0 ]; then LogTextBreak; fi
|
|
ROOT_ONLY=0; SKIPTEST=0; SKIPLOGTEST=0; SKIPREASON=""; PREQS_MET=""
|
|
TEST_CATEGORY=""; TEST_NEED_NETWORK=""; TEST_NEED_OS=""; TEST_NEED_PKG_MGR=0; TEST_NEED_PLATFORM=""
|
|
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
|
while [ $# -ge 1 ]; do
|
|
case $1 in
|
|
--category)
|
|
shift
|
|
TEST_CATEGORY=$1
|
|
;;
|
|
--description)
|
|
shift
|
|
TEST_DESCRIPTION=$1
|
|
;;
|
|
--platform)
|
|
shift
|
|
TEST_NEED_PLATFORM=$1
|
|
;;
|
|
--network)
|
|
shift
|
|
TEST_NEED_NETWORK=$1
|
|
;;
|
|
--os)
|
|
shift
|
|
TEST_NEED_OS=$1
|
|
;;
|
|
--package-manager-required)
|
|
TEST_NEED_PKG_MGR=1
|
|
;;
|
|
--preqs-met)
|
|
shift
|
|
PREQS_MET=$1
|
|
;;
|
|
--progress)
|
|
Progress "."
|
|
;;
|
|
--root-only)
|
|
shift
|
|
if [ "$1" = "YES" -o "$1" = "yes" ]; then
|
|
ROOT_ONLY=1
|
|
elif [ "$1" = "NO" -o "$1" = "no" ]; then
|
|
ROOT_ONLY=0
|
|
else
|
|
Debug "Invalid option for --root-only parameter of Register function"
|
|
fi
|
|
;;
|
|
--skip-reason)
|
|
shift
|
|
SKIPREASON="$1"
|
|
;;
|
|
--test-no)
|
|
shift
|
|
TEST_NO=$1
|
|
;;
|
|
--weight)
|
|
shift
|
|
TEST_WEIGHT=$1
|
|
;;
|
|
|
|
*)
|
|
echo "INVALID OPTION (Register): $1"
|
|
exit 1
|
|
;;
|
|
esac
|
|
# Go to next parameter
|
|
shift
|
|
done
|
|
|
|
# Measure timing
|
|
CURRENT_TS=$(GetTimestamp)
|
|
if [ ${PREVIOUS_TS} -gt 0 ]; then
|
|
SLOW_TEST=0
|
|
TIME_THRESHOLD=$SLOW_TEST_THRESHOLD # seconds
|
|
|
|
# Calculate timing and determine if we use seconds or nanoseconds (more precise)
|
|
TIME_DIFF=$((CURRENT_TS - PREVIOUS_TS))
|
|
if [ ${CURRENT_TS} -gt 1000000000000000000 ]; then
|
|
TIME_DIFF_FORMAT="nanoseconds"
|
|
TIME_THRESHOLD=$((TIME_THRESHOLD * 1000000000))
|
|
if [ ${TIME_DIFF} -gt ${TIME_THRESHOLD} ]; then
|
|
SLOW_TEST=1
|
|
# Convert back to seconds for readability
|
|
TIME_DIFF_FORMAT="seconds"
|
|
TIME_DIFF=$(echo ${TIME_DIFF} | ${AWKBINARY} '{printf "%f",$1/1000000000}')
|
|
fi
|
|
else
|
|
TIME_DIFF_FORMAT="seconds"
|
|
if [ ${TIME_DIFF} -gt ${TIME_THRESHOLD} ]; then
|
|
SLOW_TEST=1
|
|
fi
|
|
fi
|
|
if [ ${SLOW_TEST} -eq 1 ]; then
|
|
DisplayWarning "Test ${PREVIOUS_TEST} had a long execution: ${TIME_DIFF} ${TIME_DIFF_FORMAT}"
|
|
Report "slow_test[]=${PREVIOUS_TEST},${TIME_DIFF}"
|
|
fi
|
|
fi
|
|
|
|
# Skip test if it's configured in profile (old style)
|
|
if [ ${SKIPTEST} -eq 0 ]; then
|
|
FIND=$(echo "${TEST_SKIP_ALWAYS}" | grep "${TEST_NO}" | tr '[:lower:]' '[:upper:]')
|
|
if [ ! "${FIND}" = "" ]; then SKIPTEST=1; SKIPREASON="Skipped by configuration"; fi
|
|
fi
|
|
|
|
# Check if this test is on the list to skip
|
|
if [ ${SKIPTEST} -eq 0 ]; then
|
|
VALUE=$(echo ${TEST_NO} | tr '[:lower:]' '[:upper:]')
|
|
for I in ${SKIP_TESTS}; do
|
|
if [ "${I}" = "${VALUE}" ]; then SKIPTEST=1; SKIPREASON="Skipped by profile setting (skip-test)"; fi
|
|
done
|
|
fi
|
|
|
|
# Skip if test is not in the list
|
|
if [ ${SKIPTEST} -eq 0 -a ! "${TESTS_TO_PERFORM}" = "" ]; then
|
|
FIND=$(echo "${TESTS_TO_PERFORM}" | grep "${TEST_NO}")
|
|
if [ "${FIND}" = "" ]; then SKIPTEST=1; SKIPREASON="Test not in list of tests to perform"; fi
|
|
fi
|
|
|
|
# Do not run scans which have a higher intensity than what we prefer
|
|
if [ ${SKIPTEST} -eq 0 -a "${TEST_WEIGHT}" = "H" -a "${SCAN_TEST_HEAVY}" = "NO" ]; then SKIPTEST=1; SKIPREASON="Test to system intensive for scan mode (H)"; fi
|
|
if [ ${SKIPTEST} -eq 0 -a "${TEST_WEIGHT}" = "M" -a "${SCAN_TEST_MEDIUM}" = "NO" ]; then SKIPTEST=1; SKIPREASON="Test to system intensive for scan mode (M)"; fi
|
|
|
|
# Test if our OS is the same as the requested OS (can be multiple values)
|
|
if [ ${SKIPTEST} -eq 0 -a -n "${TEST_NEED_OS}" ]; then
|
|
HASMATCH=0
|
|
for I in ${TEST_NEED_OS}; do
|
|
if [ "${I}" = "${OS}" ]; then HASMATCH=1; fi
|
|
done
|
|
if [ ${HASMATCH} -eq 0 ]; then
|
|
SKIPTEST=1; SKIPREASON="Incorrect guest OS (${TEST_NEED_OS} only)"
|
|
if [ ${LOG_INCORRECT_OS} -eq 0 ]; then SKIPLOGTEST=1; fi
|
|
fi
|
|
fi
|
|
|
|
# Skip test when it belongs to another category (default is 'all')
|
|
if [ ${SKIPTEST} -eq 0 -a -n "${TEST_CATEGORY_TO_CHECK}" -a ! "${TEST_CATEGORY_TO_CHECK}" = "all" -a ! "${TEST_CATEGORY}" = "${TEST_CATEGORY_TO_CHECK}" ]; then
|
|
SKIPTEST=1; SKIPREASON="Incorrect category (${TEST_CATEGORY_TO_CHECK} only)"
|
|
fi
|
|
|
|
# Check for correct hardware platform
|
|
if [ ${SKIPTEST} -eq 0 -a -n "${TEST_NEED_PLATFORM}" ]; then
|
|
HASMATCH=0
|
|
for I in ${TEST_NEED_PLATFORM}; do
|
|
if [ "${I}" = "${HARDWARE}" ]; then HASMATCH=1; fi
|
|
done
|
|
if [ ${HASMATCH} -eq 0 ]; then
|
|
SKIPTEST=1; SKIPREASON="Incorrect hardware platform (${TEST_NEED_PLATFORM} only)"
|
|
fi
|
|
fi
|
|
|
|
# Check for required (and discovered) package manager
|
|
if [ ${SKIPTEST} -eq 0 -a ${TEST_NEED_PKG_MGR} -eq 1 -a ${HAS_PACKAGE_MANAGER} -eq 0 ]; then SKIPTEST=1; SKIPREASON="Requires a known package manager to test presence of a particular package"; fi
|
|
|
|
# Not all prerequisites met, like missing tool
|
|
if [ ${SKIPTEST} -eq 0 -a "${PREQS_MET}" = "NO" ]; then SKIPTEST=1; if [ -z "${SKIPREASON}" ]; then SKIPREASON="Prerequisites not met (ie missing tool, other type of Linux distribution)"; fi; fi
|
|
|
|
# Skip if a test is root only and we are running a non-privileged test
|
|
if [ ${SKIPTEST} -eq 0 -a ${ROOT_ONLY} -eq 1 -a ! ${MYID} = "0" ]; then
|
|
SKIPTEST=1; SKIPREASON="This test needs root permissions"
|
|
SKIPPED_TESTS_ROOTONLY="${SKIPPED_TESTS_ROOTONLY}====${TEST_NO}:space:-:space:${TEST_DESCRIPTION}"
|
|
#SkipTest "${TEST_NO}:Test:space:requires:space:root:space:permissions:-:-:"
|
|
fi
|
|
|
|
# Skip test?
|
|
if [ ${SKIPTEST} -eq 0 ]; then
|
|
# First wait X seconds (depending pause_between_tests)
|
|
if [ ${TEST_PAUSE_TIME} -gt 0 ]; then sleep ${TEST_PAUSE_TIME}; fi
|
|
|
|
# Increase counter for every registered test which is performed
|
|
CountTests
|
|
if [ ${SKIPLOGTEST} -eq 0 ]; then
|
|
LogText "Performing test ID ${TEST_NO} (${TEST_DESCRIPTION})"
|
|
if IsVerbose; then Debug "Performing test ID ${TEST_NO} (${TEST_DESCRIPTION})"; fi
|
|
fi
|
|
TESTS_EXECUTED="${TEST_NO}|${TESTS_EXECUTED}"
|
|
else
|
|
if [ ${SKIPLOGTEST} -eq 0 ]; then LogText "Skipped test ${TEST_NO} (${TEST_DESCRIPTION})"; fi
|
|
if [ ${SKIPLOGTEST} -eq 0 ]; then LogText "Reason to skip: ${SKIPREASON}"; fi
|
|
TESTS_SKIPPED="${TEST_NO}|${TESTS_SKIPPED}"
|
|
fi
|
|
|
|
# Save timestamp for next time the Register function is called
|
|
PREVIOUS_TEST="${TEST_NO}"
|
|
PREVIOUS_TS="${CURRENT_TS}"
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : RemoveColors()
|
|
# Description : Disabled colors by overriding them (defined in include/consts)
|
|
#
|
|
# Notes : Can be activated using --no-colors
|
|
################################################################################
|
|
|
|
RemoveColors() {
|
|
COLORS=0 # disable most color selections
|
|
|
|
# Normal color names
|
|
CYAN=""
|
|
BLUE=""
|
|
BROWN=""
|
|
DARKGRAY=""
|
|
GRAY=""
|
|
GREEN=""
|
|
LIGHTBLUE=""
|
|
MAGENTA=""
|
|
PURPLE=""
|
|
RED=""
|
|
YELLOW=""
|
|
WHITE=""
|
|
|
|
# Colors with background
|
|
BG_BLUE=""
|
|
BG_WARNING=""
|
|
|
|
# Semantic names
|
|
BAD=""
|
|
BOLD=""
|
|
HEADER=""
|
|
NORMAL=""
|
|
NOTICE=""
|
|
OK=""
|
|
WARNING=""
|
|
SECTION=""
|
|
}
|
|
|
|
################################################################################
|
|
# Name : RemovePIDFile()
|
|
# Description : When defined, remove the file storing the process ID
|
|
#
|
|
# Parameters : <none>
|
|
# Returns : <nothing>
|
|
################################################################################
|
|
|
|
# Remove PID file
|
|
RemovePIDFile() {
|
|
# Test if PIDFILE is defined, before checking file presence
|
|
if [ -n "${PIDFILE}" ]; then
|
|
if [ -f "${PIDFILE}" ]; then
|
|
rm -f "${PIDFILE}"
|
|
LogText "PID file removed (${PIDFILE})"
|
|
else
|
|
LogText "PID file not found (${PIDFILE})"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : RemoveTempFiles()
|
|
# Description : When created, delete any temporary file
|
|
################################################################################
|
|
|
|
# Remove any temporary files
|
|
RemoveTempFiles() {
|
|
if [ ! "${TEMP_FILES}" = "" ]; then
|
|
LogText "Temporary files: ${TEMP_FILES}"
|
|
# Clean up temp files
|
|
for FILE in ${TEMP_FILES}; do
|
|
# Temporary files should be in /tmp
|
|
TMPFILE=$(echo ${FILE} | grep -E "^/tmp/lynis" | grep -v "\.\.")
|
|
if [ -n "${TMPFILE}" ]; then
|
|
if [ -f "${TMPFILE}" ]; then
|
|
LogText "Action: removing temporary file ${TMPFILE}"
|
|
rm -f "${TMPFILE}"
|
|
else
|
|
LogText "Info: temporary file ${TMPFILE} was already removed"
|
|
fi
|
|
else
|
|
LogText "Found invalid temporary file (${FILE}), not removed. Check your /tmp directory."
|
|
fi
|
|
done
|
|
else
|
|
LogText "No temporary files to be deleted"
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : Report()
|
|
# Description : Store data in the report file
|
|
#
|
|
# Parameters : $1 = data (format: key=value)
|
|
# Returns : <nothing>
|
|
################################################################################
|
|
|
|
Report() {
|
|
if [ ${CREATE_REPORT_FILE} -eq 1 ]; then echo "$1" >> ${REPORTFILE}; fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : ReportDetails()
|
|
# Description : Adds specific details to the report, in particular when many
|
|
# smaller atomic tests are performed. For example sysctl keys,
|
|
# and SSH settings.
|
|
#
|
|
# Parameters : <multiple fields, see test>
|
|
# Returns : <nothing>
|
|
# Notes : Need to check for values (strip out semicolons from values)
|
|
# Only add fields which are filled in
|
|
################################################################################
|
|
|
|
ReportDetails() {
|
|
TEST_DESCRIPTION=""
|
|
TEST_FIELD=""
|
|
TEST_ID=""
|
|
TEST_OTHER=""
|
|
TEST_PREFERRED_VALUE=""
|
|
TEST_SERVICE=""
|
|
TEST_VALUE=""
|
|
while [ $# -ge 1 ]; do
|
|
|
|
case $1 in
|
|
--description)
|
|
shift
|
|
TEST_DESCRIPTION="desc:$1;"
|
|
;;
|
|
--field)
|
|
shift
|
|
TEST_FIELD="field:$1;"
|
|
;;
|
|
--preferredvalue|--preferred-value)
|
|
shift
|
|
TEST_PREFERRED_VALUE="prefval:$1;"
|
|
;;
|
|
# Other details
|
|
--other)
|
|
shift
|
|
TEST_OTHER="other:$1;"
|
|
;;
|
|
--service)
|
|
shift
|
|
TEST_SERVICE="$1"
|
|
;;
|
|
--test)
|
|
shift
|
|
TEST_ID=$1
|
|
;;
|
|
--value)
|
|
shift
|
|
TEST_VALUE="value:$1;"
|
|
;;
|
|
*)
|
|
echo "INVALID OPTION (ReportDetails): $1"
|
|
ExitFatal
|
|
;;
|
|
esac
|
|
shift # Go to next parameter
|
|
done
|
|
if [ "${TEST_ID}" = "" ]; then TEST_ID="-"; fi
|
|
if [ "${TEST_SERVICE}" = "" ]; then TEST_SERVICE="-"; fi
|
|
Report "details[]=${TEST_ID}|${TEST_SERVICE}|${TEST_DESCRIPTION}${TEST_FIELD}${TEST_PREFERRED_VALUE}${TEST_VALUE}${TEST_OTHER}|"
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : ReportException()
|
|
# Description : Store an exceptional event in the report
|
|
#
|
|
# Parameters : $1 = test ID + colon + 2 numeric characters (TEST-1234:01)
|
|
# $2 = string (text)
|
|
# Notes : Allow this function with only 1 parameter in strict mode with ${2-Text}
|
|
################################################################################
|
|
|
|
ReportException() {
|
|
Report "exception_event[]=$1|${2-NoInfo}|"
|
|
LogText "Exception: test has an exceptional event ($1) with text ${2-NoText}"
|
|
if [ ${QUIET} -eq 0 ]; then
|
|
DisplayException "$1" "$2"
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : ReportManual()
|
|
# Description : Add an item to the report that requires manual intervention
|
|
#
|
|
# Parameters : $1 = string (text)
|
|
################################################################################
|
|
|
|
ReportManual() {
|
|
Report "manual_event[]=$1"
|
|
LogText "Manual: one or more manual actions are required for further testing of this control/plugin"
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : ReportSuggestion()
|
|
# Description : Log a suggestion to the report file
|
|
#
|
|
# Parameters : <ID> <Suggestion> <Details> <Solution>
|
|
# $1 = Test ID - Lynis ID (use CUST-.... for your own tests)
|
|
# $2 = Suggestion - Suggestion text to be displayed
|
|
# $3 = Details - Specific item or details
|
|
# $4 = Solution - Optional link for additional information:
|
|
# * url:https://example.org/how-to-solve-link
|
|
# * text:Additional explanation
|
|
# * - (dash) for none
|
|
# Returns : <nothing>
|
|
################################################################################
|
|
|
|
ReportSuggestion() {
|
|
TOTAL_SUGGESTIONS=$((TOTAL_SUGGESTIONS + 1))
|
|
if [ $# -eq 0 ]; then echo "Not enough arguments provided for function ReportSuggestion"; ExitFatal; fi
|
|
if [ $# -ge 1 ]; then TEST="$1"; else TEST="UNKNOWN"; fi
|
|
if [ $# -ge 2 ]; then MESSAGE="$2"; else MESSAGE="UNKNOWN"; fi
|
|
if [ $# -ge 3 ]; then DETAILS="$3"; else DETAILS="-"; fi
|
|
if [ $# -ge 4 ]; then SOLUTION="$4"; else SOLUTION="-"; fi
|
|
if [ $# -ge 5 ]; then echo "Too many arguments for function ReportSuggestion"; ExitFatal; fi
|
|
Report "suggestion[]=${TEST}|${MESSAGE}|${DETAILS}|${SOLUTION}|"
|
|
LogText "Suggestion: ${MESSAGE} [test:${TEST}] [details:${DETAILS}] [solution:${SOLUTION}]"
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : ReportWarning()
|
|
# Description : Log a warning to the report file
|
|
#
|
|
# Parameters : $1 = test ID
|
|
# $2 = message (optional)
|
|
# $3 = details (optional)
|
|
# $4 = possible solution (optional)
|
|
# Returns : <nothing>
|
|
################################################################################
|
|
|
|
ReportWarning() {
|
|
TOTAL_WARNINGS=$((TOTAL_WARNINGS + 1))
|
|
# Old style used L/M/H level as second parameter. This is deprecated as of 3.x
|
|
if [ "$2" = "L" -o "$2" = "M" -o "$2" = "H" ]; then
|
|
ExitFatal "Deprecated usage of ReportWarning() function"
|
|
else
|
|
# New style warning format:
|
|
# <ID> <Warning> <Details> <Solution>
|
|
#
|
|
# <ID> Lynis ID (use CUST-.... for your own tests)
|
|
# <Warning> Warning text to be displayed
|
|
# <Details> Specific item or details
|
|
# <Solution> Optional link for additional information:
|
|
# * url:http://site/link
|
|
# * text:Additional explanation
|
|
# * - for none
|
|
if [ $# -eq 0 ]; then echo "Not enough arguments provided for function ReportWarning"; ExitFatal; fi
|
|
if [ $# -ge 1 ]; then TEST="$1"; else TEST="UNKNOWN"; fi
|
|
if [ $# -ge 2 ]; then MESSAGE="$2"; else MESSAGE="UNKNOWN"; fi
|
|
if [ $# -ge 3 ]; then DETAILS="$3"; else DETAILS="-"; fi
|
|
if [ $# -ge 4 ]; then SOLUTION="$4"; else SOLUTION="-"; fi
|
|
if [ $# -ge 5 ]; then echo "Too many arguments for function ReportWarning"; ExitFatal; fi
|
|
fi
|
|
Report "warning[]=${TEST}|${MESSAGE}|${DETAILS}|${SOLUTION}|"
|
|
LogText "Warning: ${MESSAGE} [test:${TEST}] [details:${DETAILS}] [solution:${SOLUTION}]"
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : SafeInput()
|
|
# Description : Test provided string to see if it contains unwanted characters
|
|
#
|
|
# Input : string + optional class (parameter 2)
|
|
# Returns : exit code (0 = input considered to be safe, 1 = validation failed)
|
|
################################################################################
|
|
|
|
SafeInput() {
|
|
exitcode=1
|
|
# Test against the string with a generic test set
|
|
if [ $# -eq 1 ]; then
|
|
input="$1"
|
|
# Only allow common set of characters: a-z, A-Z, 0-9, /._-:=
|
|
cleaned=$(echo "$input" | sed 's/[^a-zA-Z0-9\/\._:=-]//g')
|
|
# If two parameters are specified, then test input against specified class
|
|
elif [ $# -eq 2 ]; then
|
|
input="$1"
|
|
testchars="$2"
|
|
cleaned=$(echo $1 | tr -cd "${testchars}")
|
|
else
|
|
ExitFatal "No argument or too many arguments provided to SafeInput()"
|
|
fi
|
|
# Test if the cleaned string is the same as the original input
|
|
if [ "${cleaned}" = "${input}" ]; then
|
|
exitcode=0
|
|
fi
|
|
return ${exitcode}
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : SafeFile()
|
|
# Description : Check if a file is safe to use
|
|
#
|
|
# Parameters : $1 = file name
|
|
# Returns : exit code (0 = OK, 1 = issue)
|
|
################################################################################
|
|
|
|
SafeFile() {
|
|
unsafe=0
|
|
if [ $# -ne 1 ]; then
|
|
ExitFatal "No argument or too many arguments provided to SafeFile()"
|
|
else
|
|
FILE="$1"
|
|
|
|
# Generic checks
|
|
if [ -g "${FILE}" ]; then
|
|
LogText "Security alert: file has setgid attribute"
|
|
unsafe=1
|
|
# sticky bit
|
|
elif [ -k "${FILE}" ]; then
|
|
LogText "Security alert: file has sticky bit"
|
|
unsafe=1
|
|
# symbolic link
|
|
elif [ -L "${FILE}" ]; then
|
|
LogText "Security alert: file is a symbolic link"
|
|
unsafe=1
|
|
elif [ -f "${FILE}" ]; then
|
|
LogText "Security check: file is normal"
|
|
else
|
|
unsafe=1
|
|
fi
|
|
|
|
# Perform additional checks based on privilege level
|
|
if [ ${PRIVILEGED} -eq 0 ]; then
|
|
# File is not owned by active user, but still able to write
|
|
if [ ! -O "${FILE}" -a -w "${FILE}" ]; then
|
|
unsafe=1
|
|
LogText "Security alert: file is not owned by active user, but can write to it"
|
|
fi
|
|
fi
|
|
|
|
# Check file permissions
|
|
if ! SafePerms "${FILE}"; then
|
|
unsafe=1
|
|
fi
|
|
|
|
fi
|
|
|
|
return ${unsafe}
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : SafePerms()
|
|
# Description : Check if a file has safe permissions to be used
|
|
#
|
|
# Parameters : $1 = file name
|
|
# $2 = type of file (optional)
|
|
# Returns : 0 (file permissions OK) or break
|
|
################################################################################
|
|
|
|
SafePerms() {
|
|
exitcode=1
|
|
IS_PARAMETERS=0
|
|
IS_PROFILE=0
|
|
|
|
if [ ${IGNORE_FILE_PERMISSION_ISSUES} -eq 0 ]; then
|
|
PERMS_OK=0
|
|
LogText "Checking permissions of $1"
|
|
|
|
if [ $# -gt 0 ]; then
|
|
|
|
if [ $# -eq 2 ]; then
|
|
case "$2" in
|
|
"parameters")
|
|
IS_PARAMETERS=1
|
|
;;
|
|
"profile")
|
|
IS_PROFILE=1
|
|
;;
|
|
esac
|
|
else
|
|
FIND=$(echo $1 | grep "/parameters")
|
|
if [ $? -eq 0 ]; then IS_PARAMETERS=1; fi
|
|
fi
|
|
# Check file permissions
|
|
if [ ! -f "$1" ]; then
|
|
LogText "Fatal error: file $1 does not exist."
|
|
ExitFatal "Fatal error: file $1 does not exist"
|
|
else
|
|
PERMS=$(ls -l $1)
|
|
|
|
# Owner permissions
|
|
OWNER=$(echo ${PERMS} | awk -F" " '{ print $3 }')
|
|
OWNERID=$(ls -n $1 | awk -F" " '{ print $3 }')
|
|
if [ ${PENTESTINGMODE} -eq 0 -a ${IS_PARAMETERS} -eq 0 ]; then
|
|
if [ ! "${OWNER}" = "root" -a ! "${OWNERID}" = "0" ]; then
|
|
echo "Fatal error: file $1 should be owned by user 'root' when running it as root (found: ${OWNER})."
|
|
ExitFatal
|
|
fi
|
|
fi
|
|
# Group permissions
|
|
GROUP=$(echo ${PERMS} | awk -F" " '{ print $4 }')
|
|
GROUPID=$(ls -n $1 | awk -F" " '{ print $4 }')
|
|
|
|
if [ ${PENTESTINGMODE} -eq 0 -a ${IS_PARAMETERS} -eq 0 ]; then
|
|
if [ ! "${GROUP}" = "root" -a ! "${GROUP}" = "wheel" -a ! "${GROUPID}" = "0" ]; then
|
|
echo "Fatal error: group owner of directory $1 should be owned by root group, wheel or similar (found: ${GROUP})."
|
|
ExitFatal
|
|
fi
|
|
fi
|
|
|
|
# Owner permissions
|
|
OWNER_PERMS=$(echo ${PERMS} | cut -c2-4)
|
|
if [ ! "${OWNER_PERMS}" = "rw-" -a ! "${OWNER_PERMS}" = "r--" ]; then
|
|
echo "Fatal error: permissions of file $1 are not strict enough. Access to 'owner' should be read-write, or read. Change with: chmod u=rw $1"
|
|
ExitFatal
|
|
fi
|
|
|
|
# Group permissions
|
|
# TODO - harden this even more by setting default to read-only for group (like 'other')
|
|
GROUP_PERMS=$(echo ${PERMS} | cut -c5-7)
|
|
if [ ! "${GROUP_PERMS}" = "rw-" -a ! "${GROUP_PERMS}" = "r--" -a ! "${GROUP_PERMS}" = "---" ]; then
|
|
echo "Fatal error: permissions of file $1 are not strict enough. Access to 'group' should be read-write, read, or none. Change with: chmod g=r $1"
|
|
ExitFatal
|
|
fi
|
|
|
|
# Other permissions
|
|
OTHER_PERMS=$(echo ${PERMS} | cut -c8-10)
|
|
if [ ! "${OTHER_PERMS}" = "---" -a ! "${OTHER_PERMS}" = "r--" ]; then
|
|
echo "Fatal error: permissions of file $1 are not strict enough. Access to 'other' should be denied or read-only. Change with: chmod o=r $1"
|
|
ExitFatal
|
|
fi
|
|
# Set PERMS_OK to 1 if no fatal errors occurred
|
|
PERMS_OK=1
|
|
LogText "File permissions are OK"
|
|
exitcode=0
|
|
fi
|
|
else
|
|
ReportException "SafePerms()" "Invalid number of arguments for function"
|
|
fi
|
|
else
|
|
PERMS_OK=1
|
|
exitcode=0
|
|
fi
|
|
return ${exitcode}
|
|
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : SearchItem()
|
|
# Description : Search if a specific string exists in in a file
|
|
#
|
|
# Parameters : $1 = search key (string)
|
|
# $2 = file (string)
|
|
# $3 = optional arguments:
|
|
# --sensitive - don't store results in log
|
|
# Returns : True (0) or False (1)
|
|
################################################################################
|
|
|
|
SearchItem() {
|
|
PERFORM_SCAN=0
|
|
MASK_LOG=0
|
|
RETVAL=1
|
|
if [ $# -lt 2 ]; then
|
|
ExitFatal "Not enough arguments for function SearchItem()"
|
|
elif [ $# -ge 2 ]; then
|
|
FILE=$2
|
|
STRING=$1
|
|
PERFORM_SCAN=1
|
|
fi
|
|
|
|
# Parse any additional arguments
|
|
if [ $# -gt 2 ]; then
|
|
shift; shift # Skip the first two (string and file)
|
|
while [ $# -ge 1 ]; do
|
|
case $1 in
|
|
"--sensitive") MASK_LOG=1 ;;
|
|
esac
|
|
shift # Go to next parameter
|
|
done
|
|
fi
|
|
|
|
if [ ${PERFORM_SCAN} -eq 1 ]; then
|
|
# Don't search in /dev/null, it's too empty there
|
|
if [ -f ${FILE} ]; then
|
|
# Check if we can find the main type (with or without brackets)
|
|
LogText "Test: search string ${STRING} in file ${FILE}"
|
|
FIND=$(grep -E "${STRING}" ${FILE})
|
|
if [ -n "${FIND}" ]; then
|
|
LogText "Result: found search string '${STRING}'"
|
|
if [ ${MASK_LOG} -eq 0 ]; then LogText "Full string returned: ${FIND}"; fi
|
|
RETVAL=0
|
|
else
|
|
LogText "Result: search search string '${STRING}' NOT found"
|
|
RETVAL=1
|
|
fi
|
|
else
|
|
LogText "Skipping search, file (${FILE}) does not exist"
|
|
ReportException "${TEST_NO}" "Test is trying to search for a string in nonexistent file"
|
|
fi
|
|
else
|
|
ReportException "${TEST_NO}" "Search test is skipped, which is unexpected"
|
|
fi
|
|
return ${RETVAL}
|
|
}
|
|
|
|
|
|
|
|
|
|
################################################################################
|
|
# Name : ShowComplianceFinding()
|
|
# Description : Display a section of a compliance standard which is not fulfilled
|
|
#
|
|
# Parameters : <several parameters, see test>
|
|
# Returns : <nothing>
|
|
################################################################################
|
|
|
|
ShowComplianceFinding() {
|
|
REASON=""
|
|
STANDARD_NAME=""
|
|
STANDARD_VERSION=""
|
|
STANDARD_SECTION=""
|
|
STANDARD_SECTION_TITLE=""
|
|
ACTUAL_VALUE=""
|
|
EXPECTED_VALUE=""
|
|
while [ $# -ge 1 ]; do
|
|
case $1 in
|
|
--standard)
|
|
shift
|
|
STANDARD_NAME=$1
|
|
;;
|
|
--version)
|
|
shift
|
|
STANDARD_VERSION=$1
|
|
;;
|
|
--section)
|
|
shift
|
|
STANDARD_SECTION=$1
|
|
;;
|
|
--section-title)
|
|
shift
|
|
STANDARD_SECTION_TITLE=$1
|
|
;;
|
|
--reason)
|
|
shift
|
|
REASON=$1
|
|
;;
|
|
--actual)
|
|
shift
|
|
ACTUAL_VALUE=$1
|
|
;;
|
|
--expected)
|
|
shift
|
|
EXPECTED_VALUE=$1
|
|
;;
|
|
|
|
*)
|
|
echo "INVALID OPTION (ShowComplianceFinding): $1"
|
|
exit 1
|
|
;;
|
|
esac
|
|
# Go to next parameter
|
|
shift
|
|
done
|
|
# Should we show this non-compliance on screen?
|
|
SHOW=0
|
|
case ${STANDARD_NAME} in
|
|
cis)
|
|
if [ ${COMPLIANCE_ENABLE_CIS} -eq 1 ]; then SHOW=1; fi
|
|
STANDARD_FRIENDLY_NAME="CIS"
|
|
;;
|
|
hipaa)
|
|
if [ ${COMPLIANCE_ENABLE_HIPAA} -eq 1 ]; then SHOW=1; fi
|
|
STANDARD_FRIENDLY_NAME="HIPAA"
|
|
;;
|
|
iso27001)
|
|
if [ ${COMPLIANCE_ENABLE_ISO27001} -eq 1 ]; then SHOW=1; fi
|
|
STANDARD_FRIENDLY_NAME="ISO27001"
|
|
;;
|
|
pci-dss)
|
|
if [ ${COMPLIANCE_ENABLE_PCI_DSS} -eq 1 ]; then SHOW=1; fi
|
|
STANDARD_FRIENDLY_NAME="PCI DSS"
|
|
;;
|
|
esac
|
|
# Only display if standard is enabled in the profile and mark system as non-compliant
|
|
if [ ${SHOW} -eq 1 ]; then
|
|
COMPLIANCE_FINDINGS_FOUND=1
|
|
DisplayManual " [${WHITE}${STANDARD_FRIENDLY_NAME} ${STANDARD_VERSION}${NORMAL}] - ${CYAN}Section ${STANDARD_SECTION}${NORMAL} - ${WHITE}${STANDARD_SECTION_TITLE}${NORMAL}"
|
|
DisplayManual " - Details: ${REASON}"
|
|
DisplayManual " - Configuration: ${RED}${ACTUAL_VALUE}${NORMAL} / ${EXPECTED_VALUE}"
|
|
DisplayManual ""
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : ShowSymlinkPath()
|
|
# Description : Check if we can find the path behind a symlink
|
|
#
|
|
# Parameters : $1 = file (string)
|
|
# Returns : FOUNDPATH (0 not found, 1 found path), SYMLINK (new path)
|
|
################################################################################
|
|
|
|
ShowSymlinkPath() {
|
|
if [ $# -eq 0 ]; then ExitFatal "Function ShowSymlinkPath() called without a file name"; fi
|
|
sFILE=$1
|
|
FOUNDPATH=0
|
|
SYMLINK_USE_PYTHON=0
|
|
SYMLINK_USE_READLINK=0
|
|
LogText "Action: checking symlink for file ${sFILE}"
|
|
# Check for symlink
|
|
if [ -L ${sFILE} ]; then
|
|
|
|
# macOS does not know -f option, nor do some others
|
|
if [ "${OS}" = "macOS" ]; then
|
|
# If a Python binary is found, use the one in path
|
|
if [ ${BINARY_SCAN_FINISHED} -eq 0 -a "${PYTHONBINARY}" = "" ]; then
|
|
FIND=$(which python 2> /dev/null | grep -v "no [^ ]* in ")
|
|
if [ ! "${FIND}" = "" ]; then LogText "Setting temporary pythonbinary variable"; PYTHONBINARY="${FIND}"; fi
|
|
fi
|
|
|
|
if [ ! "${PYTHONBINARY}" = "" ]; then
|
|
SYMLINK_USE_PYTHON=1
|
|
LogText "Note: using Python to determine symlinks"
|
|
tFILE=$(python -c "import os,sys; print(os.path.realpath(os.path.expanduser(sys.argv[1])))" $1)
|
|
fi
|
|
else
|
|
if [ ${BINARY_SCAN_FINISHED} -eq 0 -a "${READLINKBINARY}" = "" ]; then
|
|
FIND=$(which readlink 2> /dev/null | grep -v "no [^ ]* in ")
|
|
if [ ! "${FIND}" = "" ]; then LogText "Setting temporary readlinkbinary variable"; READLINKBINARY="${FIND}"; fi
|
|
fi
|
|
|
|
if [ ! "${READLINKBINARY}" = "" ]; then
|
|
SYMLINK_USE_READLINK=1
|
|
LogText "Note: Using real readlink binary to determine symlink on ${sFILE}"
|
|
tFILE=$(${READLINKBINARY} -f ${sFILE})
|
|
LogText "Result: readlink shows ${tFILE} as output"
|
|
fi
|
|
fi
|
|
# Check if we can find the file now
|
|
if [ "${tFILE}" = "" ]; then
|
|
LogText "Result: command did not return any value"
|
|
elif [ -f ${tFILE} ]; then
|
|
sFILE="${tFILE}"
|
|
LogText "Result: symlink found, pointing to file ${sFILE}"
|
|
FOUNDPATH=1
|
|
elif [ -b ${tFILE} ]; then
|
|
sFILE="${tFILE}"
|
|
LogText "Result: symlink found, pointing to block device ${sFILE}"
|
|
FOUNDPATH=1
|
|
elif [ -c ${tFILE} ]; then
|
|
sFILE="${tFILE}"
|
|
LogText "Result: symlink found, pointing to character device ${sFILE}"
|
|
FOUNDPATH=1
|
|
elif [ -d ${tFILE} ]; then
|
|
sFILE="${tFILE}"
|
|
LogText "Result: symlink found, pointing to directory ${sFILE}"
|
|
FOUNDPATH=1
|
|
else
|
|
# Check the full path of the symlink, strip the filename, copy the path and linked filename together
|
|
tDIR=$(echo ${sFILE} | awk '{match($1, "^.*/"); print substr($1, 1, RLENGTH-1)}')
|
|
tFILE="${tDIR}/${tFILE}"
|
|
if [ -L ${tFILE} ]; then
|
|
LogText "Result: this symlink links to another symlink"
|
|
# Ensure that we use a second try with the right tool as well
|
|
if [ ${SYMLINK_USE_PYTHON} -eq 1 ]; then
|
|
tFILE=$(python -c "import os,sys; print(os.path.realpath(os.path.expanduser(sys.argv[1])))" ${tFILE})
|
|
elif [ ${SYMLINK_USE_READLINK} -eq 1 ]; then
|
|
tFILE=$(${READLINKBINARY} -f ${tFILE})
|
|
fi
|
|
# Check if we now have a normal file
|
|
if [ -f ${tFILE} ]; then
|
|
sFILE="${tFILE}"
|
|
LogText "Result: symlink finally found, seems to be file ${sFILE}"
|
|
FOUNDPATH=1
|
|
elif [ -d ${tFILE} ]; then
|
|
sFILE="${tFILE}"
|
|
LogText "Result: symlink finally found, seems to be directory ${sFILE}"
|
|
FOUNDPATH=1
|
|
else
|
|
LogText "Result: could not find file ${tFILE}, most likely too complicated symlink or too often linked"
|
|
fi
|
|
elif [ -f ${tFILE} ]; then
|
|
sFILE="${tFILE}"
|
|
LogText "Result: symlink found, seems to be file ${sFILE}"
|
|
FOUNDPATH=1
|
|
elif [ -d ${tFILE} ]; then
|
|
sFILE="${tFILE}"
|
|
LogText "Result: symlink found, seems to be directory ${sFILE}"
|
|
FOUNDPATH=1
|
|
else
|
|
LogText "Result: file ${tFILE} in ${tDIR} not found"
|
|
fi
|
|
fi
|
|
else
|
|
LogText "Result: file ${sFILE} is not a symlink"
|
|
fi
|
|
# Now check if our new location is actually a file or directory destination
|
|
if [ -L ${sFILE} ]; then
|
|
LogText "Result: unable to determine symlink, or location ${sFILE} is just another symlink"
|
|
FOUNDPATH=0
|
|
fi
|
|
if [ ${FOUNDPATH} -eq 1 ]; then
|
|
SYMLINK="${sFILE}"
|
|
else
|
|
SYMLINK=""
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : SkipAtomicTest()
|
|
# Description : Test if an atomic test should be skipped
|
|
#
|
|
# Input : String (particular test)
|
|
# Returns : 0 (skip test) or 1 (do not skip test)
|
|
# Usage : if SkipAtomicTest "ssh-7408:port"; then echo "Skip this atomic test"; fi
|
|
################################################################################
|
|
|
|
SkipAtomicTest() {
|
|
RETVAL=255
|
|
if [ $# -eq 1 ]; then
|
|
STRING=""
|
|
RETVAL=1
|
|
# Check if this test is on the list to skip
|
|
for item in ${SKIP_TESTS}; do
|
|
STRING=$(echo $1 | tr '[:lower:]' '[:upper:]')
|
|
if [ "${item}" = "${STRING}" ]; then RETVAL=0; LogText "Atomic test ($1) skipped by configuration (skip-test)"; fi
|
|
done
|
|
else
|
|
ReportException "SkipAtomicTest()" "Function called without right number of arguments (1)"
|
|
fi
|
|
return $RETVAL
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : Status()
|
|
# Description : Reports back the status of tool
|
|
#
|
|
# Returns : text to screen
|
|
# Notes : kill --signal USR1 <PID> or pkill --signal USR1 lynis
|
|
################################################################################
|
|
|
|
Status() {
|
|
echo ""
|
|
echo "Date / time : $(date "+%Y-%m-%d %H:%M:%S")"
|
|
echo "Active test : ${TEST_NO:-NONE}"
|
|
echo ""
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : StoreNginxSettings()
|
|
# Description : Store parsed settings from nginx (by ParseNginx)
|
|
# Input : multiple options
|
|
# Returns : <nothing>
|
|
################################################################################
|
|
|
|
StoreNginxSettings() {
|
|
CONFIG_DEPTH=0; CONFIG_FILE=""; CONFIG_SETTING=""; CONFIG_TREE=""; CONFIG_VALUE=""
|
|
while [ $# -ge 1 ]; do
|
|
case $1 in
|
|
--config)
|
|
shift
|
|
CONFIG_FILE=$1
|
|
;;
|
|
--depth)
|
|
shift
|
|
CONFIG_DEPTH=$1
|
|
;;
|
|
# none | events | server | unknown
|
|
--tree)
|
|
shift
|
|
CONFIG_TREE=$1
|
|
case ${CONFIG_TREE} in
|
|
"/") CONFIG_COUNTER=0 ;;
|
|
"/events") CONFIG_COUNTER=${NGINX_EVENTS_COUNTER=0} ;;
|
|
"/http") CONFIG_COUNTER=${NGINX_HTTP_COUNTER=0} ;;
|
|
"/server") CONFIG_COUNTER=${NGINX_SERVER_COUNTER=0} ;;
|
|
"/server/location") CONFIG_COUNTER=${NGINX_LOCATION_COUNTER=0} ;;
|
|
*)
|
|
Debug "Unknown configuration tree of nginx ${CONFIG_TREE}"
|
|
;;
|
|
esac
|
|
;;
|
|
--setting)
|
|
shift
|
|
CONFIG_SETTING=$1
|
|
;;
|
|
--value)
|
|
shift
|
|
CONFIG_VALUE=$1
|
|
;;
|
|
*)
|
|
echo "INVALID OPTION (StoreNginxSettings): $1 $2"
|
|
#ExitFatal
|
|
;;
|
|
esac
|
|
# Go to next parameter
|
|
shift
|
|
done
|
|
if [ -z "${CONFIG_DEPTH}" ]; then CONFIG_DEPTH="0"; fi
|
|
if [ -z "${CONFIG_SETTING}" ]; then CONFIG_SETTING="NA"; fi
|
|
if [ -z "${CONFIG_TREE}" ]; then CONFIG_TREE="/"; fi
|
|
if [ -z "${CONFIG_VALUE}" ]; then CONFIG_VALUE="NA"; fi
|
|
Report "nginx_config[]=|file=${CONFIG_FILE}|depth=${CONFIG_DEPTH}|tree=${CONFIG_TREE}|number=${CONFIG_COUNTER}|setting=${CONFIG_SETTING}|value=${CONFIG_VALUE}|"
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : TestValue()
|
|
# Description : Test if a value is good/bad (e.g. according to best practices)
|
|
#
|
|
# Input : --function <value> --value <value> --search <value>
|
|
# Returns : 0 (True) or 1 (False)
|
|
# Usage : if TestValue --function contains --value "Full Text" --search "Text"; then echo "Found!"; fi
|
|
################################################################################
|
|
|
|
TestValue() {
|
|
RETVAL=255
|
|
FIND=""
|
|
VALUE=""
|
|
RESULT=""
|
|
SEARCH=""
|
|
CMP1=""; CMP2=""
|
|
while [ $# -ge 1 ]; do
|
|
case $1 in
|
|
--function)
|
|
shift
|
|
FUNCTION=$1
|
|
;;
|
|
--value)
|
|
shift
|
|
VALUE=$1
|
|
;;
|
|
--search)
|
|
shift
|
|
SEARCH=$1
|
|
;;
|
|
*)
|
|
echo "INVALID OPTION USED (TestValue): $1"
|
|
exit 1
|
|
;;
|
|
esac
|
|
# Go to next parameter
|
|
shift
|
|
done
|
|
|
|
if [ "${VALUE}" = "" ]; then echo "No value provided to function (TestValue)"; fi
|
|
|
|
# Apply the related function
|
|
case ${FUNCTION} in
|
|
"contains")
|
|
FIND=$(echo ${VALUE} | grep -E "${SEARCH}")
|
|
if [ "${FIND}" = "" ]; then RETVAL=1; else RETVAL=0; fi
|
|
;;
|
|
#"gt" | "greater-than") COLOR=$GREEN ;;
|
|
"equals")
|
|
CMP1=$(echo ${SEARCH} | tr '[:upper:]' '[:lower:'])
|
|
CMP2=$(echo ${VALUE} | tr '[:upper:]' '[:lower:'])
|
|
if [ "${CMP1}" = "${CMP2}" ]; then RETVAL=0; else RETVAL=1; fi
|
|
;;
|
|
#"not-equal") COLOR=$WHITE ;;
|
|
#"lt" | "less-than") COLOR=$YELLOW ;;
|
|
*) echo "INVALID OPTION USED (TestValue, parameter of function: $1)"; exit 1 ;;
|
|
esac
|
|
|
|
if [ "${RETVAL}" -lt 2 ]; then
|
|
return ${RESULT}
|
|
else
|
|
Fatal "ERROR: No result returned from function (TestValue). Incorrect usage?"
|
|
#ExitFatal
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : ViewCategories()
|
|
# Description : Show all available categories
|
|
#
|
|
# Input : <nothing>
|
|
# Returns : <nothing>
|
|
# Usage : ViewCategories
|
|
################################################################################
|
|
|
|
ViewCategories() {
|
|
for CATEGORY in ${TEST_AVAILABLE_CATEGORIES}; do echo "${CATEGORY}"; done
|
|
ExitClean
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : ViewGroups()
|
|
# Description : Show what group of tests are available
|
|
#
|
|
# Input : <nothing>
|
|
# Returns : <nothing>
|
|
# Usage : ViewGroups
|
|
################################################################################
|
|
|
|
ViewGroups() {
|
|
if [ -n "${INCLUDEDIR}" ]; then
|
|
for I in $(ls ${INCLUDEDIR}/tests_* | xargs -n 1 basename | sed 's/tests_//' | grep -v "custom.template"); do
|
|
echo "${I}"
|
|
done
|
|
fi
|
|
ExitClean
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Name : WaitForKeyPress()
|
|
# Description : Wait for the user to press [ENTER], unless quickmode is set
|
|
#
|
|
# Input : <nothing>
|
|
# Returns : <nothing>
|
|
# Usage : WaitForKeyPress
|
|
################################################################################
|
|
|
|
WaitForKeyPress() {
|
|
if [ ${QUICKMODE} -eq 0 ]; then
|
|
echo ""; echo "[ Press [ENTER] to continue, or [CTRL]+C to stop ]"
|
|
read -r void
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
#
|
|
# Legacy functions - Do not use!
|
|
#
|
|
################################################################################
|
|
# For compatibility reasons they are listed here, but will be phased out in
|
|
# steps. If they still get used, they will trigger errors on screen.
|
|
################################################################################
|
|
|
|
counttests() {
|
|
DisplayWarning "Deprecated function used (counttests)"
|
|
if IsDeveloperMode; then Debug "Warning: old counttests function is used. Please replace any reference with CountTests."; fi
|
|
CountTests
|
|
}
|
|
|
|
logtext() {
|
|
DisplayWarning "Deprecated function used (logtext)"
|
|
if IsDeveloperMode; then Debug "Warning: old logtext function is used. Please replace any reference with LogText."; fi
|
|
LogText "$1"
|
|
}
|
|
|
|
logtextbreak() {
|
|
DisplayWarning "Deprecated function used (logtextbreak)"
|
|
if IsDeveloperMode; then Debug "Warning: old logtextbreak function is used. Please replace any reference with LogTextBreak."; fi
|
|
LogTextBreak "$1"
|
|
}
|
|
|
|
report() {
|
|
DisplayWarning "Deprecated function used (report)"
|
|
if IsDeveloperMode; then Debug "Warning: old report function is used. Please replace any reference with Report."; fi
|
|
Report "$1"
|
|
}
|
|
|
|
wait_for_keypress() {
|
|
DisplayWarning "Deprecated function used (wait_for_keypress)"
|
|
if IsDeveloperMode; then Debug "Warning: old wait_for_keypress function is used. Please replace any reference with WaitForKeyPress."; fi
|
|
WaitForKeyPress
|
|
}
|
|
|
|
ShowResult() {
|
|
DisplayWarning "Deprecated function used (ShowResult)"
|
|
if IsDeveloperMode; then Debug "Warning: old ShowResult() function is used. Please replace any reference with WaitForKeyPress."; fi
|
|
}
|
|
|
|
|
|
#================================================================================
|
|
# Lynis is part of Lynis Enterprise and released under GPLv3 license
|
|
# Copyright 2007-2021 - Michael Boelen, CISOfy - https://cisofy.com
|