#!/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 -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 -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 -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 -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 -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 -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 -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 -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 -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 -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"
        if [ ! -d ${CHECKFILE} -a ! -f ${CHECKFILE} ]; then
            return 2
        else
            for CHECK_PERMISSION in ${CHECKPERMISSION_FULL}; do
                DATA=$(echo ${CHECK_PERMISSION} | ${EGREPBINARY} "[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} 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})
                            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 [ ${SHELL_IS_BUSYBOX} -eq 0 ]; then
                                if [ -d ${CHECKFILE} ]; then
                                    DATA=$(${FINDBINARY} ${CHECKFILE} -maxdepth 0 -printf "%m")
                                else
                                    DATA=$(${FINDBINARY} ${CHECKFILE} -printf "%m")
                                fi
                            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} != ${CHECKPERMISSION_FULL})"
            # 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} | ${EGREPBINARY:-egrep} "( |/)${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 -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=""

        # 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
        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=$?
        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
        case "${OS}" in
            "Linux")
                ts=$(date "+%s%N")
            ;;
            *)
                ts=$(date "+%s")
            ;;
        esac
        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
        unset SKIPREASON

        # 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
        # By default remove only control characters
        if [ $# -eq 1 ]; then
            input="$1"
            cleaned=$(echo ${input} | tr -d '[:cntrl:]')
        # If know what to test against, then see if input matches the 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

        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 user, 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