#!/bin/sh ################################################################################# # # Lynis # ------------------ # # Copyright 2007-2015, Michael Boelen - CISOfy (michael.boelen@cisofy.com) # Website: https://cisofy.com # # This software is licensed under GPL, version 3. See LICENSE file for # usage of this software. # ################################################################################# # # Functions # ################################################################################# # # Function Description # ----------------------- ------------------------------------------------- # AddHP Add Hardening points to plot a graph later # AddSystemGroup Adds a system to a group # CheckFilePermissions Check file permissions # CheckUpdates Determine if a new version of Lynis is available # counttests Count number of performed tests # Debug Display additional information on the screen (not suited for cronjob) # DirectoryExists Check if a directory exists on the disk # Display Output text to screen with colors and identation # ExitClean Stop the program (cleanly), with exit code 0 # ExitFatal Stop the program (cleanly), with exit code 1 # FileExists Check if a file exists on the disk # 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 # InsertSection Insert a section block # InsertPluginSection Insert a section block for plugins # IsRunning Check if a process is running # 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 # ParseNginx Parse nginx configuration lines # Progress Show progress on screen # ReportException Add an exception to the report file (for debugging purposes) # ReportSuggestion Add a suggestion to report file # ReportWarning Add a warning and priority to report file # Register Register a test (for logging and execution) # SafePerms Check if a directory has safe permissions # SearchItem Search a string in a file # ShowSymlinkPath Show a path behind a symlink # ViewCategories Display tests categories # logtext Log text strings to logfile, prefixed with date/time # report Add string of data to report file # ################################################################################# # Add Hardening Points AddHP() { HPADD=$1; HPADDMAX=$2 HPPOINTS=`expr ${HPPOINTS} + ${HPADD}` HPTOTAL=`expr ${HPTOTAL} + ${HPADDMAX}` logtext "Hardening: assigned ${HPADD} hardening points (max for this item: ${HPADDMAX}), current: ${HPPOINTS}, total: ${HPTOTAL}" } ################################################################################ # Name : AddSystemGroup # Description : Adds a system to a group, which can be used for categorizing # Returns : ################################################################################ AddSystemGroup() { report "system_group[]=$1" } # Check file permissions # Parameter 1 is file/dir # Result: FILE_NOT_FOUND | OK | BAD CheckFilePermissions() { CHECKFILE=$1 if [ ! -d $CHECKFILE -a ! -f $CHECKFILE ]; then PERMS="FILE_NOT_FOUND" else # If 'file' is an directory, use -d if [ -d ${CHECKFILE} ]; then FILEVALUE=`ls -d -l ${CHECKFILE} | cut -c 2-10` PROFILEVALUE=`cat ${PROFILE} | grep '^permdir' | grep ":${CHECKFILE}:" | cut -d: -f3` else FILEVALUE=`ls -l ${CHECKFILE} | cut -c 2-10` PROFILEVALUE=`cat ${PROFILE} | grep '^permfile' | grep ":${CHECKFILE}:" | cut -d: -f3` fi if [ "${FILEVALUE}" = "${PROFILEVALUE}" ]; then PERMS="OK"; else PERMS="BAD"; fi fi } ################################################################################ # Name : CheckItem() # Description : Check if a specific item exists in the report # Returns : ################################################################################ CheckItem() { ITEM_FOUND=0 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=`egrep "^$1(\[\])?=" ${REPORTFILE} | egrep "$2"` if [ ! "${FIND}" = "" ]; then ITEM_FOUND=1 logtext "Result: found search string (result: $FIND)" else logtext "Result: search string NOT found" fi else logtext "Skipping search, as /dev/null is being used" fi else ReportException ${TEST_NO} "Error in function call to CheckItem" fi } # Check updates CheckUpdates() { PROGRAM_LV="0000000000"; DB_MALWARE_LV="0000000000"; DB_FILEPERMS_LV="0000000000" LYNIS_LV_RECORD="lynis-latest-version.cisofy.com." FIND=`which dig 2> /dev/null` if [ ! "${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` if [ ! "${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` if [ ! "${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 [ "${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 } # Count the number of performed tests counttests() { CTESTS_PERFORMED=`expr ${CTESTS_PERFORMED} + 1` } # Determine if a directory exists DirectoryExists() { DIRECTORY_FOUND=0 logtext "Test: checking if directory $1 exists" if [ -d $1 ]; then logtext "Result: directory $1 exists" DIRECTORY_FOUND=1 else logtext "Result: directory $1 NOT found" fi } # More information on the screen Debug() { if [ ${DEBUG} -eq 1 ]; then echo "DEBUG: $1"; fi } # Display text Display() { INDENT=0; TEXT=""; RESULT=""; COLOR=""; SPACES=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 ;; --indent) shift INDENT=$1 ;; --result) shift RESULT=$1 ;; --text) shift TEXT=$1 ;; *) echo "INVALID OPTION (Display): $1" exit 1 ;; esac # Go to next parameter shift done if [ "${RESULT}" = "" ]; then RESULTPART="" else if [ ${CRONJOB} -eq 0 ]; then RESULTPART=" [ ${COLOR}${RESULT}${NORMAL} ]" else RESULTPART=" [ ${RESULT} ]" fi fi if [ ! "${TEXT}" = "" ]; then # Show warnings always, and other messages if no quiet is being used if [ ${QUIET} -eq 0 -o "${RESULT}" = "WARNING" ]; then # Display (counting with -m instead of -c, to support language locale) LINESIZE=`echo "${TEXT}" | wc -m | tr -d ' '` if [ ${INDENT} -gt 0 ]; then SPACES=`expr 62 - ${INDENT} - ${LINESIZE}`; 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}" else echo "${TEXT}${RESULTPART}" fi fi fi } # Clean exit (removing temp files, PID files) ExitClean() { RemovePIDFile exit 0 } # Clean exit (removing temp files, PID files), with error code 1 ExitFatal() { RemovePIDFile exit 1 } # Determine if a file exists FileExists() { FILE_FOUND=0 logtext "Test: checking if file $1 exists" if [ -f $1 ]; then logtext "Result: file $1 exists" FILE_FOUND=1 else logtext "Result: file $1 NOT found" fi } ################################################################################ # Name : FileIsEmpty # Description : Check if a file is empty # Returns : EMPTY (0 or 1) ################################################################################ FileIsEmpty() { EMPTY=0 logtext "Test: checking if file $1 is empty" if [ -z $1 ]; then logtext "Result: file $1 is empty" EMPTY=1 else logtext "Result: file $1 is NOT empty" fi } ################################################################################ # Name : FileIsReadable # Description : Check if a file readable or directory is accessible # Returns : CANREAD (0 or 1) ################################################################################ FileIsReadable() { sFILE=$1 CANREAD=0 logtext "Test: testing if we can access ${sFILE}" # Check for symlink if [ -L ${sFILE} ]; then ShowSymlinkPath ${sFILE} if [ ! "${SYMLINK}" = "" ]; then sFILE="${SYMLINK}"; fi #if [ ! "${READLINKBINARY}" = "" ]; then # tFILE=`${READLINKBINARY} ${sFILE}` # # Check if we can find the file now # if [ -f ${tFILE} ]; then # sFILE="${tFILE}" # logtext "Result: symlink found, pointing to file ${sFILE}" # elif [ -d ${tFILE} ]; then # sFILE="${tFILE}" # logtext "Result: symlink found, pointing to directory ${sFILE}" # 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 [ -f ${tFILE} ]; then # sFILE="${tFILE}" # logtext "Result: symlink found, seems to be file ${sFILE}" # elif [ -d ${tFILE} ]; then # sFILE="${tFILE}" # logtext "Result: symlink found, seems to be directory ${sFILE}" # fi # fi #fi fi # Only check the file if it isn't a symlink (after previous check) if [ -L ${sFILE} ]; 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 ${sFILE} ]; then OTHERPERMS=`ls -d -l ${sFILE} | cut -c 8` elif [ -f ${sFILE} ]; then OTHERPERMS=`ls -d -l ${sFILE} | cut -c 8` else OTHERPERMS="-" fi # Also check if we are the actual owner of the file FILEOWNER=`ls -n ${sFILE} | awk -F" " '{ print $3 }'` if [ "${FILEOWNER}" = "${MYID}" ]; then logtext "Result: file is owned by our current user ID (${MYID}), checking if it is readable" if [ -d ${sFILE} ]; then OTHERPERMS=`ls -d -l ${sFILE} | cut -c 2` elif [ -f ${sFILE} ]; then OTHERPERMS=`ls -d -l ${sFILE} | cut -c 2` fi fi # Check if we have the read bit if [ "${OTHERPERMS}" = "r" ]; then CANREAD=1 logtext "Result: file ${sFILE} is readable (or directory accessible)." else logtext "Result: file ${sFILE} is NOT readable (or directory accessible), symlink, or does not exist." fi } # Get Host ID GetHostID() { HOSTID="-" FIND="" if [ ! "${SHA1SUMBINARY}" = "" -o ! "${OPENSSLBINARY}" = "" -o ! "${CSUMBINARY}" = "" ]; then 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 [ ! "${SHA1SUMBINARY}" = "" ]; then HOSTID=`echo ${FIND} | ${SHA1SUMBINARY} | awk '{ print $1 }'` elif [ ! "${CSUMBINARY}" = "" ]; then HOSTID=`echo ${FIND} | ${CSUMBINARY} -h SHA1 - | awk '{ print $1 }'` elif [ ! "${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 [ ! "${FIND}" = "" ]; then HOSTID=`echo ${FIND} | sha1` else ReportException "GetHostID" "No MAC address returned on DragonFly or FreeBSD" fi ;; "Linux") # Define preferred interfaces #PREFERRED_INTERFACES="eth0 eth1 eth2 enp0s25" # Only use ifconfig if no ip binary has been found if [ ! "${IFCONFIGBINARY}" = "" ]; then # Determine if we have ETH0 at all (not all Linux distro 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 [ "${FIND}" = "" ]; then FIND=`${IFCONFIGBINARY} 2> /dev/null | grep HWaddr` if [ "${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 [ ! "${HASETH0}" = "" ]; then FIND=`${IFCONFIGBINARY} eth0 2> /dev/null | grep "ether " | awk '{ print $2 }' | tr '[:upper:]' '[:lower:]'` else FIND=`${IFCONFIGBINARY} 2> /dev/null | grep "ether " | awk '{ print $2 }' | head -1 | tr '[:upper:]' '[:lower:]'` if [ "${FIND}" = "" ]; then ReportException "GetHostID" "No eth0 found (and no ether was found with ifconfig)" else logtext "Result: No eth0 found (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:]'` ReportException "GetHostID" "No eth0 found (but HWaddr was found), using first network interface to determine hostid, with ifconfig" fi fi else # See if we can use ip binary instead if [ ! "${IPBINARY}" = "" ]; then # Determine if we have the common available eth0 interface FIND=`${IPBINARY} addr show eth0 2> /dev/null | egrep "link/ether " | head -1 | awk '{ print $2 }' | tr '[:upper:]' '[:lower:]'` if [ "${FIND}" = "" ]; then # Determine the MAC address of first interface with the ip command FIND=`${IPBINARY} addr show 2> /dev/null | egrep "link/ether " | head -1 | awk '{ print $2 }' | tr '[:upper:]' '[:lower:]'` if [ "${FIND}" = "" ]; then ReportException "GetHostID" "Can't create hostid (no MAC addresses found)" fi fi else ReportException "GetHostID" "Can't create hostid, missing both ifconfig and ip binary" fi fi # Check if we found a HostID if [ ! "${FIND}" = "" ]; then HOSTID=`echo ${FIND} | ${SHA1SUMBINARY} | awk '{ print $1 }'` logtext "Result: Found HostID: ${HOSTID}" else ReportException "GetHostID" "Can't create HOSTID, command ip not found" 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 Mac OS" fi ;; "NetBSD") FIND=`${IFCONFIGBINARY} -a | grep "address:" | head -1 | awk '{ print $2 }' | tr '[:upper:]' '[:lower:]'` if [ ! "${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 [ ! "${FIND}" = "" ]; then HOSTID=`echo ${FIND} | sha1` else ReportException "GetHostID" "No MAC address returned on OpenBSD" fi ;; "Solaris") INTERFACES_TO_TEST="e1000g1 net0" 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" fi done if [ ${FOUND} -eq 1 ]; then FIND=`${IFCONFIGBINARY} ${I} | grep ether | awk '{ if ($1=="ether") { print $2 }}'` if [ ! "${SHA1SUMBINARY}" = "" ]; then HOSTID=`echo ${FIND} | ${SHA1SUMBINARY} | awk '{ print $1 }'` else if [ ! "${OPENSSLBINARY}" = "" ]; then HOSTID=`echo ${FIND} | ${OPENSSLBINARY} sha -sha1 | awk '{ print $2 }'` else ReportException "GetHostID" "Can not find sha1/sha1sum or openssl" fi fi else ReportException "GetHostID" "No interface found op Solaris to create HostID" fi ;; *) ReportException "GetHostID" "Can't create HOSTID as OS is not supported yet by this function" ;; esac 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 # Create host ID when a MAC address was not found #SSH_KEY_FILES="ssh_host_ecdsa_key ssh_host_ecdsa_key.pub ssh_host_dsa_key ssh_host_dsa_key.pub ssh_host_rsa_key ssh_host_rsa_key.pub" #for I in ${SSH_KEY_FILES}; do #done } # Insert section block InsertSection() { if [ ${QUIET} -eq 0 ]; then echo "" echo "[+] ${SECTION}$1${NORMAL}" echo "------------------------------------" fi logtextbreak logtext "Action: Performing tests from category: $1" } # Insert section block for plugins InsertPluginSection() { if [ ${QUIET} -eq 0 ]; then echo "" echo "[+] ${MAGENTA}$1${NORMAL}" echo "------------------------------------" fi logtext "Action: Performing plugin tests" } # Is a process running? # Returns: RUNNING IsRunning() { RUNNING=0 FIND=`${PSBINARY} ax | egrep "( |/)$1" | grep -v "grep"` if [ ! "${FIND}" = "" ]; then RUNNING=1 logtext "IsRunning: process '$1' found (${FIND})" else logtext "IsRunning: process '$1' not found" fi } ################################################################################ # Name : IsVirtualMachine() # Description : Check if a specific item exists in the report # Returns : ISVIRTUALMACHINE (0-2) # VMTYPE # 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="" # facter if [ -x /usr/bin/facter ]; then case "`facter is_virtual`" in "true") VMTYPE=`facter virtual` logtext "Result: found virtual machine (type: ${VMTYPE})" report "vm=1" report "vmtype=${VMTYPE}" return ;; "false") return ;; esac fi # systemd if [ "${SHORT}" = "" -a -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 [ ! "${FIND}" = "" ]; then SHORT="${FIND}" fi fi # dmidecode # Values: VMware Virtual Platform / VirtualBox if [ "${SHORT}" = "" ]; then logtext "Test: trying to guess virtualization with dmidecode" if [ -x /usr/sbin/dmidecode ]; then SHORT=`dmidecode -s system-product-name | awk '{ print $1 }'` fi fi # lshw if [ "${SHORT}" = "" ]; then if [ -x /usr/bin/lshw ]; then SHORT=`lshw -quiet -class system | awk '{ if ($1=="product:") { print $2 }}'` fi fi # Try common guest processes if [ "${SHORT}" = "" ]; then logtext "Test: trying to guess virtual machine type by running processes" # VMware IsRunning vmware-guestd if [ ${RUNNING} -eq 1 ]; then SHORT="vmware"; fi IsRunning vmtoolsd if [ ${RUNNING} -eq 1 ]; then SHORT="vmware"; fi # VirtualBox based on guest services IsRunning vboxguest-service if [ ${RUNNING} -eq 1 ]; then SHORT="virtualbox"; fi IsRunning VBoxClient if [ ${RUNNING} -eq 1 ]; then SHORT="virtualbox"; fi fi # Amazon EC2 if [ "${SHORT}" = "" ]; then logtext "Test: checking specific files for Amazon" if [ -f /etc/ec2_version -a ! -z /etc/ec2_version ]; then SHORT="amazon-ec2"; fi fi # sysctl values if [ "${SHORT}" = "" ]; then logtext "Test: trying to guess virtual machine type by sysctl keys" # NetBSD: machdep.dmi.system-product # OpenBSD: hw.product SHORT=`sysctl -a 2> /dev/null | egrep "(hw.product|machdep.dmi.system-product)" | head -1 | sed 's/ = /=/' | awk -F= '{ print $2 }'` fi # Check if we catched some string along all tests if [ ! "${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" ;; *) 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" fi } # Function IsWorldReadable IsWorldReadable() { sFILE=$1 # Check for symlink if [ -L ${sFILE} ]; then ShowSymlinkPath ${sFILE} if [ ! "${SYMLINK}" = "" ]; then sFILE="${SYMLINK}" fi fi # Only check the file if it isn't a symlink (after previous check) if [ -f ${sFILE} -a ! -L ${sFILE} ]; then FINDVAL=`ls -l ${sFILE} | cut -c 8` if [ "${FINDVAL}" = "r" ]; then return 1; else return 0; fi else return 255 fi } # Function IsWorldExecutable IsWorldExecutable() { sFILE=$1 # Check for symlink if [ -L ${sFILE} ]; then ShowSymlinkPath ${sFILE} if [ ! "${SYMLINK}" = "" ]; then sFILE="${SYMLINK}" fi fi # Only check the file if it isn't a symlink (after previous check) if [ -f ${sFILE} -a ! -L ${sFILE} ]; then FINDVAL=`ls -l ${sFILE} | cut -c 10` if [ "${FINDVAL}" = "x" ]; then return 1; else return 0; fi else return 255 fi } # Function IsWorldWritable IsWorldWritable() { sFILE=$1 FileIsWorldWritable="" # Only check the file if it isn't a symlink (after previous check) if [ -f ${sFILE} -a ! -L ${sFILE} ]; then FINDVAL=`ls -l ${sFILE} | cut -c 9` if [ "${FINDVAL}" = "w" ]; then return 1; else return 0; fi else return 255 fi } # Function logtext (redirect data ($1) to log file) logtext() { if [ ! "${LOGFILE}" = "" ]; then CDATE=`date "+[%H:%M:%S]"` echo "${CDATE} $1" >> ${LOGFILE} fi } ################################################################################ # Name : logtextbreak() # Description : Add a separator to log file between sections, tests etc # Returns : logtextbreak() { if [ ! "${LOGFILE}" = "" ]; then CDATE=`date "+[%H:%M:%S]"` echo "${CDATE} ===---------------------------------------------------------------===" >> ${LOGFILE} fi } ################################################################################ # Name : Maid() # Description : Cleanup service # Returns : Maid() { echo ""; echo "Interrupt detected." # Remove PID RemovePIDFile # Clean up temp files if [ ! "${TMPFILE}" = "" ]; then if [ -f ${TMPFILE} ]; then rm -f ${TMPFILE}; fi; fi if [ ! "${TMPFILE2}" = "" ]; then if [ -f ${TMPFILE2} ]; then rm -f ${TMPFILE2}; fi; fi Display --text "Cleaning up..." --result DONE --color GREEN # Exit with exit code 1 exit 1 } # Parse nginx configuration lines ParseNginx() { FIND=`cat ${REPORTFILE} | grep "^nginx_config_option=" | awk -F= '{ if ($1=="nginx_config_option") { print $2 }}' | sed 's/ /:space:/g'` for I in ${FIND}; do I=`echo ${I} | sed 's/:space:/ /g' | sed 's/;$//'` OPTION=`echo ${I} | awk '{ print $1 }'` VALUE=`echo ${I}| cut -d' ' -f2-` logtext "Result: found option ${OPTION} with parameters ${VALUE}" case ${OPTION} in 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 }'` if [ ! -f ${VALUE} ]; then logtext "Result: could not find referenced log file ${VALUE} 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 if [ ! -f ${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) ;; 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 #report "nginx_ssl_protocols=${VALUE}" ;; ssl_session_cache) ;; ssl_session_timeout) ;; types) ;; *) logtext "Found unknown option ${OPTION} in nginx configuration" ;; esac done } ################################################################################ # Name : Progress() # Description : Displays progress on screen with dots # Input : finish or text # Returns : nothing # Tip : Use this function from Register with the --progress parameter Progress() { if [ ${CRONJOB} -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 } ################################################################################ # Name : RealFilename() # Description : Return file behind a symlink # Returns : sFILE RealFilename() { sFILE=$1 FileIsWorldExecutable="" SYMLINK=0 # Check for symlink if [ -L ${sFILE} ]; then if [ ! "${READLINKBINARY}" = "" ]; then tFILE=`${READLINKBINARY} -f ${sFILE}` # Check if we can find the file now if [ -f ${tFILE} ]; then rFILE="${tFILE}" logtext "Result: symlink found, pointing to ${sFILE}" SYMLINK=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 [ -f ${tFILE} ]; then rFILE="${tFILE}" logtext "Result: symlink found, seems to be ${rFILE}" fi fi fi else # No symlink rFILE="${sFILE}" fi } ################################################################################ # Name : Register() # Description : Register a test and see if it has to be run # Returns : SKIPTEST (0 or 1) 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; TEST_NEED_OS=""; PREQS_MET="" TEST_NEED_NETWORK=""; TEST_NEED_PLATFORM="" TOTAL_TESTS=`expr ${TOTAL_TESTS} + 1` while [ $# -ge 1 ]; do case $1 in --description) shift TEST_DESCRIPTION=$1 ;; --platform) shift TEST_NEED_PLATFORM=$1 ;; --network) shift TEST_NEED_NETWORK=$1 ;; --os) shift TEST_NEED_OS=$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 ;; --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 # Skip if a test is root only and we are running a non-privileged test if [ ${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 it's configured in profile if [ ${SKIPTEST} -eq 0 ]; then FIND=`echo "${TEST_SKIP_ALWAYS}" | grep "${TEST_NO}"` if [ ! "${FIND}" = "" ]; then SKIPTEST=1; SKIPREASON="Skipped by configuration"; fi 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 # Skip test if OS is different than requested if [ ${SKIPTEST} -eq 0 -a ! -z "${TEST_NEED_OS}" -a ! "${OS}" = "${TEST_NEED_OS}" ]; then SKIPTEST=1; SKIPREASON="Incorrect guest OS (${TEST_NEED_OS} only)" if [ ${LOG_INCORRECT_OS} -eq 0 ]; then SKIPLOGTEST=1 fi fi # Check for correct hardware platform if [ ${SKIPTEST} -eq 0 -a ! -z "${TEST_NEED_PLATFORM}" -a ! "${HARDWARE}" = "${TEST_NEED_PLATFORM}" ]; then SKIPTEST=1; SKIPREASON="Incorrect hardware platform"; fi # Not all prerequisites met, like missing tool if [ ${SKIPTEST} -eq 0 -a "${PREQS_MET}" = "NO" ]; then SKIPTEST=1; SKIPREASON="Prerequisities not met (ie missing tool, other type of Linux distribution)"; 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)"; 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 } # Remove PID file RemovePIDFile() { # Test if PIDFILE is defined, before checking file presence if [ ! "${PIDFILE}" = "" ]; then if [ -f ${PIDFILE} ]; then rm -f $PIDFILE; logtext "PID file removed (${PIDFILE})" else logtext "PID file not found (${PIDFILE})" fi fi } # Dump to report file report() { echo "$1" >> ${REPORTFILE} } # Log exceptions ReportException() { # 1 parameters # :<2 char numeric>|text| report "exception_event[]=$1|$2|" logtext "Exception: test has an exceptional event ($1) with text $2" } # Log manual actions to report file ReportManual() { # 1 parameters # :<2 char numeric> report "manual_event[]=$1" logtext "Manual: one or more manual actions are required for further testing of this control/plugin" } # Report data (TESTID STATUS IMPACT MESSAGE) ReportResult() { if [ $1 = "" ]; then TESTID="UNKNOWN"; fi # Status: OK, WARNING, NEUTRAL, SUGGESTION # Impact: HIGH, SEVERE, LOW, #report "result[]=TESTID-${TESTID},STATUS-$2,IMPACT-$3,MESSAGE-$4-" # Reset ID before next test TESTID="" } # Log suggestions to report file ReportSuggestion() { # 2 parameters # report "suggestion[]=$1|$2|" logtext "Suggestion: $2 [$1]" } # Log warning to report file ReportWarning() { # 3 parameters # if [ "$2" = "L" -o "$2" = "M" -o "$2" = "H" ]; then # old style warning report "warning[]=$1|$3|" logtext "Warning: $3 [$1]" else # new style warning report "warning[]=$1|$2|" logtext "Warning: $2 [test:$1]" fi } SafePerms() { PERMS_OK=0 logtext "Checking permissions of $1" if [ $# -eq 1 ]; then IS_PARAMETERS_FILE=`echo $1 | grep "/parameters"` # Check file permissions if [ ! -f "$1" ]; then logtext "Fatal error: file $1 does not exist. Quitting." echo "Fatal error: file $1 does not exist" ExitFatal 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_FILE}" = "" ]; 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 else logtext "Note: Owner permissions of file $1 to be expected similar as the UID executing the process" 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_FILE}" = "" ]; 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 else logtext "Note: Group permissions of file $1 to be expected similar as the UID executing the process" 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." ExitFatal fi # Set PERMS_OK to 1 if no fatal errors occurred PERMS_OK=1 logtext "File permissions are OK" fi else logtext "Fatal error: invalid amount of parameters when calling function SafePerms()" echo "Invalid amount of parameters for function SafePerms()" ExitFatal fi } ################################################################################ # Name : SearchItem() # Description : Search if a specific string exists in in a file # Parameters : $1 = search string # : $2 = file # Returns : ################################################################################ SearchItem() { ITEM_FOUND=0 if [ $# -eq 2 ]; then # Don't search in /dev/null, it's too empty there if [ -f $2 ]; then # Check if we can find the main type (with or without brackets) logtext "Test: search string $1 in file $2" FIND=`egrep "$1" $2` if [ ! "${FIND}" = "" ]; then ITEM_FOUND=1 logtext "Result: found string" logtext "Full string: ${FILE}" else logtext "Result: search string NOT found" fi else logtext "Skipping search, file does not exist" ReportException ${TEST_NO} "Test is trying to search for a string in nonexistent file" fi else ReportException ${TEST_NO} "Error in function call to CheckItem" fi } # Show result code ShowResult() { case $1 in OK) echo "[ ${OK}OK${NORMAL} ]" ;; WARNING) echo "[ ${WARNING}WARNING${NORMAL} ]" # log the warning to our log file #logtext "Warning: $2" # add the warning to our report file #report "warning=$2" ;; esac } ################################################################################ # Name : ShowSymlinkPath() # Description : Check if we can find the path behind a symlink # Parameters : $1 = file # Returns : FOUNDPATH (0 not found, 1 found path)) ################################################################################ ShowSymlinkPath() { sFILE=$1 FOUNDPATH=0 SYMLINK_USE_PYTHON=0 SYMLINK_USE_READLINK=0 # Check for symlink if [ -L ${sFILE} ]; then # Mac OS 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` 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` 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 symlinks" 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}` else tFILE=`${READLINKBINARY} -f ${tFILE}` fi 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 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 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 } ViewCategories() { if [ ! "${INCLUDEDIR}" = "" ]; then InsertSection "Available test categories" for I in `ls ${INCLUDEDIR}/tests_* | xargs -n 1 basename | sed 's/tests_//' | grep -v "custom.template"`; do echo " - ${I}" done fi echo "" exit 0 } # Wait for [ENTER] or manually break wait_for_keypress() { if [ ! ${QUICKMODE} -eq 1 ]; then echo ""; echo "[ ${WHITE}Press [ENTER] to continue, or [CTRL]+C to stop${NORMAL} ]" read void fi } #================================================================================ # Lynis is part of Lynis Enterprise and released under GPLv3 license # Copyright 2007-2015 - Michael Boelen, CISOfy - https://cisofy.com