#!/bin/bash
#
#   System hardening script for converting an RHEL4 system to one
#    as certified under EAL4/CAPP
#
#   Copyright (C) 2004, 2005  Red Hat, Inc.
#
#   Licensed under the terms of the Common Public License Version 1.0.
#   See the files COPYING and CPL.txt distributed with the script
#   for a description of your rights and responsibilities.
#

# maintainer note on style:
#  unipolar branches use && or || for readability
#  variables are in CAPS
#  backtick expansions $(use parens syntax)
#  functions are prefixed with a : comment for use with set -x
#  all code is inside a function (for tracking syntax errors w/set -x)
#  function names use StudlyCaps
#  local and readonly are used where appropriate
#  global vars and constants have a leading _
#  to check syntax set _CHECK_SYNTAX_ in env and run with bash -x
#    done this way functions are loaded but Main() is not called

:
: -- %Function Definitions
:

: --KeyEscape
# transform arbitrary key into something guaranteed shell safe
# uses hex encoding for simplicity. Limitation: keys need to have
# a reasonable length (try <1000 bytes)
KeyEscape () {
    echo -n "$1" | od -An -tx1 -w1000 | sed 's/ //g'
}

: --AssocPut
# store a value in associative array (aka hash table / dictionary)
# Usage: AssocPut VAR KEY VALUE
# DON'T do something like: stuff | AssocPut VAR VAL, that
AssocPut() {
    local Var="$1"
    local Key="$(KeyEscape "$2")"
    local Value="$3"

    eval ${Var}__${Key}=\"$Value\"
}

: --AssocGet
# Usage: Value=$(AssocGet VAR KEY) 
AssocGet() {
    local Var="$1"
    local Key="$(KeyEscape "$2")"

    eval echo \"\$${Var}__${Key}\"
}

: --AssocLineSet
# Usage: AssocLineSet VAR FILE VAL
# sets variables in a subshell and immediately forgets them again
AssocLineSet() {
    local Var="$1"
    local File="$2"
    local Value="$3"

    while read Key
    do
	AssocPut "$Var" "$Key" "$Value"
    done < "$File"
}

: --AssocExists
# returns boolean true if Key has nonzero Value in Var
AssocExists() {
    local Var="$1"
    local Key="$2"

    [ ! -z "$(AssocGet "$Var" "$Key")" ]
}

AssocTestCase () {
    # test that separate vars are independent
    # test quoting for key names and values
    AssocPut Xv 'a 1' 'X 1'
    AssocPut Xv 'a 2' 'X 2'
    AssocPut Yv 'a 1' 'Y 1'
    
    [ ."$(AssocGet Xv 'a 1')" = .'X 1' ] || Die "Internal error, assoc arrays don't work"
    [ ."$(AssocGet Xv 'a 2')" = .'X 2' ] || Die "Internal error, assoc arrays don't work"
    [ ."$(AssocGet Yv 'a 1')" = .'Y 1' ] || Die "Internal error, assoc arrays don't work"

    # test autosetting values from a file
    AssocLineSet Users <(cut -d: -f1 /etc/passwd) t
    AssocExists Users root || Die "no root user?"

    # read name/value pairs from file
    while read User Uid
    do
        AssocPut UserUid "$User" "$Uid"
    done < <(awk -F: '{print $1, $3}' /etc/passwd)
    [ "$(AssocGet UserUid root)" = '0' ] || Die "root user uid != 0?"
}
#AssocTestCase

: --DisableUsbfs
DisableUsbfs() {

    Title "Disable usbfs"

    local Sysinit=/etc/rc.d/rc.sysinit

    if ShallI "Disable usbfs mount in $Sysinit"; then
	Log  "Disabling usbfs mount in $Sysinit"

	sed '/mount.*usb/s/^#*/#/' <$Sysinit >$Sysinit.new
	
	Replace $Sysinit removing $Sysinit.new
    else
	Log "disabling usbfs declined"
	_FAILURE=1
    fi
}

: --AliasSU
#  establish an alias for SU

AliasSU() {

    Title " set an alias for 'su' to discourage misuse"

    # Don't run again if the alias is already present
    grep '^alias su=' /etc/profile >/dev/null && return

    if ShallI "(optional) Add 'su' alias to /etc/profile"; then

	Log  "Adding 'su' alias to /etc/profile"

	cp /etc/profile /etc/profile.new
	echo >> /etc/profile.new '
# su alias, added for CAPP/EAL4 configuration
alias su="echo \"Always use '\''/bin/su -'\'' (see '"$_ECG_FULL"')\""
'
	Replace /etc/profile removing /etc/profile.new
    else
	Log "su alias creation declined"
	# this was optional, no failure
    fi
}


: --Archive
#  cycle out a stale logfile

Archive() {
 
    local DATE=$(date +"%Y%m%d-%H%M")
    local K= NAME= N=

    for K in "$@" ; do
	K=${K%%/}
	if [ -e "$K" ] ; then
	    NAME=$K-$DATE
	    N=0
	    while [ -e "$NAME" ] ; do
		NAME=$K-$DATE-$N
		N=$((N+1))
	    done
	    echo moving "$K" to "$NAME"
	    mv "$K" "$NAME"
	fi
    done
}

:  --IEcho
#   Print message only if interactive

IEcho() {
    [ "$_INTERACTIVE" ] && echo "$@"
}

:  --Ask
#   take user input string, print response to stdout
 
Ask() {
    local ANS= DEFAULT="$2"

    [ "$DEFAULT" ] || DEFAULT=y
 
    # non-interactive operation defaults to answering default to all questions
    # no output there.
    if [ "$_INTERACTIVE" ]; then
	echo -n "$1 [$DEFAULT] " >/dev/tty
	read ANS
    else
	Log "   Ask(): not interactive, using default '$DEFAULT'" >/dev/null
	ANS="$DEFAULT"
    fi
 
    [ "$ANS" ] || ANS="$DEFAULT"
    echo "$ANS"
}

:  --AskYN
#   take user input for y/n question
 
AskYN() {
    local ANS=$(Ask "$1 (y/n)" "$2")

    case "$ANS" in
	[yY]*)
	    return 0
	    ;;
	*)  return 1
	    ;;
    esac
}

: -- FindMissingPackages
#  Search for missing packages on install media

FindMissingPackages() {
    local PKG_MISSING="$1"
    local SEARCH_PATH

    local NATIVE_ARCH=$(GetNativeArchRe)
    local CROSS_ARCH=$(GetCrossArchRe)

    _RPM_TMP=$(mktemp -d /tmp/rpm.XXXXXX) || Die "Can't make temp dir"

    while [ ! -z "$PKG_MISSING" ]
    do
        local PKG
        local MNT

        LogBlock "Missing packages: $PKG_MISSING"
	IEcho "
Enter one or more path names (shell wildcards permitted, space separated) where
I should look for RPM packages. Put paths containing updates first.

If a path name starts with '/media/cdrom', I will try mounting that CD-ROM,
and will unmount it again after searching for packages there.

Enter 'help' or 'h' for help.
"

        SEARCH_PATH=$(Ask "RPM path?" "$_RPM_PATH")

	case $SEARCH_PATH in
	quit|q)
		AskYN "Delete RPM file copies in $_RPM_TMP?" "y" && {
	            rm $_RPM_TMP/*.rpm
                    rmdir $_RPM_TMP
		}
		Die "Installation aborted. The system is NOT in the evaluated configuration."
		;;

	help|h|\?)
		IEcho "
If the script appears to be stuck in a loop, you either have missing packages
or wrong versions.

Commands:

    'help' or 'h': this message

    'quit' or 'q': exit (abort installation)

    'install' or 'i': install only the currently copied packages, then quit.
"
		continue
		;;
	install|inst|i)
	        AskYN "Install these packages?" "y" && {
		    Log "Installing the packages" 
		    RunWithLog rpm -Uvh --replacefiles $_RPM_TMP/*.rpm || {
			AskYN "Try again ignoring pam and audit related packages?" "y" && {
			    RunWithLog rpm -Uvh --replacefiles \
				$(ls $_RPM_TMP/*.rpm | grep -v '/audit' | grep -v '/pam')
			}
		    }
	        }

		AskYN "Delete RPM file copies in $_RPM_TMP?" "y" && {
	            rm $_RPM_TMP/*.rpm
                    rmdir $_RPM_TMP
		}
		Die "Installation incomplete. The system is NOT in the evaluated configuration."
		;;
	esac

	TickOn

	# mount CD-ROMs
        for MNT in $SEARCH_PATH
        do
            # mount CD(s) if not mounted
            expr "$MNT" : "/media/cdrom" >/dev/null && {
                mount | grep "$MNT " >/dev/null || {
		    # ignore mount errors
		    mount $MNT || true
		}
            }
        done

	# search for and copy missing packages
        for PKG in $PKG_MISSING
        do
            local RPM
	    local CANDIDATES
	    local PKG_NAME
	    local ARCH_RE
	    local SUFFIX
	    local RPMNAME
	    local RPMVERSION
	    local REQVERSION
	    	
	    # Looking for pkg/cross or pkg/any ?
	    SUFFIX=$(echo $PKG | sed -n 's/.*\/\(.*\)$/\1/p')
	    # remove suffix from name
	    PKG_NAME=$(echo $PKG | sed 's/\/.*//')

	    case "$SUFFIX" in
	    cross)
		ARCH_RE="$CROSS_ARCH"
		;;
	    any)
		ARCH_RE="($NATIVE_ARCH|$CROSS_ARCH)"
		;;
            *)
		ARCH_RE="$NATIVE_ARCH"
		;;
	    esac

	    # priority sorting: go through search path in order, reverse
	    # sorting each search path directory to put higher numbers first
            for RPM in $(find $SEARCH_PATH -name "${PKG_NAME}*" | egrep "\\.${ARCH_RE}\\.rpm\$" 2>/dev/null)
            do
		#Log "Examining $RPM"
                RPMNAME=$(rpm -q --qf='%{NAME}\n' -p "$RPM" 2>/dev/null)
                RPMVERSION=$(rpm -q --qf='%{VERSION}-%{RELEASE}\n' -p "$RPM" 2>/dev/null)

		# ensure that name matches exactly, don't install
		# PKG-devel or libPKG instead of PKG
                [ ."$RPMNAME" != ."$PKG_NAME" ] && continue

		# check for specific version requirement for full name (including /cross suffix)
		REQVERSION=$(AssocGet REQVERSIONSET "$PKG")

		# if that isn't set, look for version requirement for base name
		# (needed for upgrading kernel/cross on ppc64)
		[ -z "$REQVERSION" ] && REQVERSION=$(AssocGet REQVERSIONSET "$PKG_NAME")

		# refuse packages whose version doesn't match
		[ -z "$REQVERSION" -o "$REQVERSION" = "$RPMVERSION" ]  || {
		    Log "Ignoring $RPM, need version $REQVERSION"
		    continue
		}

		# Ok, found it. Copy, remove from missing list, and exit loop.
		Log "Copying $RPM"
                cp $RPM $_RPM_TMP
		PKG_MISSING=$(echo "$PKG_MISSING" | grep -v "^$PKG\$")
		break
            done
        done

	# unmount CD-ROMs
        for MNT in $SEARCH_PATH
        do
            # unmount CD(s) if mounted
            expr "$MNT" : "/media/cdrom" >/dev/null && {
		mount | grep "$MNT " >/dev/null && {
		    # ignore umount errors
		    umount $MNT || true
		}
            }
        done

	TickOff
	
	[ -z "$_INTERACTIVE" -a ! -z "$PKG_MISSING" ] && {
	    Die "Did not find all needed packages. Missing: $PKG_MISSING"
	}
    done

    Log "Found all packages."
}

: -- GetInstalledPackages
GetInstalledPackages () {
    local NAME ARCH

    local CROSS_ARCH=$(GetCrossArchRe)

    rpm -qa --queryformat='%{NAME} %{ARCH}\n' \
    | while read NAME ARCH
    do
	if echo $ARCH | egrep "^$CROSS_ARCH\$" >/dev/null
	then
	    echo "$NAME/cross"
	else
	    echo "$NAME"
	fi
    done
}

: --CheckPackages
#  remove unwanted packages, verify that required packages are
#   installed.

CheckPackages() {

    Title " Configure package selection"
    Log "Analyzing the current package selection"

    TickOn

    local ALLIPKGS=$(GetInstalledPackages | sort)
    local REQPKGS=$( StripComments $_PKG_REQ  | sort)
    local OPTPKGS=$( StripComments $_PKG_OPT  | sort)
    local ARCHPKGS=$(StripComments $_PKG_ARCH | sort)

    AssocLineSet ALLIPKGSET <(GetInstalledPackages) t
    AssocLineSet REQPKGSET <(StripComments $_PKG_REQ) t
    AssocLineSet OPTPKGSET <(StripComments $_PKG_OPT) t
    AssocLineSet ARCHPKGSET <(StripComments $_PKG_ARCH) t
    AssocLineSet PERMITTEDPKGSET <(echo "$REQPKGS"; echo "$OPTPKGS"; echo "$ARCHPKGS") t
    
    local TO_RM= TO_REINSTALL=
    local PKG_NATIVE= PKG_CROSS= PKG= PKG_NAME= OVERLAP=
    local VERSIONREQ= PACKREQ= ARCHREQ=
    local INST_VER= INST_ARCH=
    local PACKINST= PKG_MISSING=
    local NATIVE_ARCH_RE=$(GetNativeArchRe)
    local CROSS_ARCH_RE=$(GetCrossArchRe)

    LogBlock "installed packages on the system: $ALLIPKGS" >/dev/null

    # remove vsftpd from list on WS, it's not available
    grep WS /etc/redhat-release >/dev/null && {
    	REQPKGS=$(echo "$REQPKGS" | grep -v 'vsftpd')
    }

    [ "$_ADD_OPTIONAL" ] && {
	#LogBlock "Optional packages: $OPTPKGS"

	# add all optional packages except for the binary kernels
	REQPKGS=$(echo "$REQPKGS"; echo "$OPTPKGS" | egrep -v '^(kernel|kernel-smp)$')

	# If arch has no cross-compile variants, remove these from list
	[ "$CROSS_ARCH_RE" = "unavailable" ] && {
		REQPKGS=$(echo "$REQPKGS" | grep -v '/cross')
	}
    }
    
    # check for missing or obsolete packages
    for PACKREQ in $REQPKGS $ARCHPKGS; do
	AssocExists ALLIPKGSET "$PACKREQ" || {
	    PKG_MISSING="$PKG_MISSING
$PACKREQ"
	}
    done

    # version check for cert critical packages
    #
    while read PACKREQ ARCHREQ VERSIONREQ
    do
	# skip comments and blank lines
	expr "$PACKREQ" : '#' >/dev/null && continue
	expr "$PACKREQ" : '.' >/dev/null || continue

	# save required version for later - we don't want to grab
	# the wrong one when adding new packages (i.e. glibc-kernheaders)
	AssocPut REQVERSIONSET "$PACKREQ" "$VERSIONREQ"

	# We may have more than one of these pkgs on the system,
	# i.e. a ppc64 and a ppc (32-bit) version. Loop over all installed
	# versions, and require reinstall if any one of them does not match.
	for INST_VER in $(rpm -q $PACKREQ --qf='%{VERSION}-%{RELEASE}/%{ARCH}\n' 2>/dev/null | sed 's/ /_/g')
	do
	    # ignore if not installed
	    expr "$INST_VER" : '.*is.not.installed' >/dev/null && continue	

	    # check architecture
	    INST_ARCH=$(echo "$INST_VER" | sed 's/.*\///')

	    # does the conf file specify an architecture this applies to?
	    [ "$ARCHREQ" != '*' ] && {
		# ignore architectures other than installed one
		[ "$INST_ARCH" != "$ARCHREQ" ] && continue
	    }
	    INST_VER=$(echo "$INST_VER" | sed 's/\/.*//')

	    [ ."$VERSIONREQ" != ."$INST_VER" ] && {
		# special case - if installed one is cross arch, update that one
		if echo "$INST_ARCH" | egrep -q "^$CROSS_ARCH_RE\$"; then
		    PACK="$PACKREQ/cross"
		else
		    PACK="$PACKREQ"
		fi

		# ignore if not permitted - it'll be deleted later
		AssocExists PERMITTEDPKGSET "$PACK" || continue

		Log "$PACK: $INST_VER installed, need $VERSIONREQ"

		# Add package to list of packages to update
		PKG_MISSING="$PKG_MISSING
$PACK"
	    }
	done
    done < $_PKG_CERT

    # make sure packages aren't duplicated in list
    PKG_MISSING="$(echo "$PKG_MISSING" | sort | uniq)"

    TickOff

    # make list of pkgs to remove, don't remove them yet - need to have replacements
    # ready for biarch packages with one installed variant

    TickOn
    for PACKINST in $ALLIPKGS; do
	# ignore GPG public keys
	[ "$PACKINST" = "gpg-pubkey" ] && continue

	AssocExists PERMITTEDPKGSET "$PACKINST" || {
	    PKG_NAME=$(echo "$PACKINST" | sed 's/\/.*//')

	    PKG_NATIVE=$(rpm -q --qf='%{NAME}.%{ARCH}\n' $PKG_NAME \
		  | egrep "\\.$NATIVE_ARCH_RE\$")
	    PKG_CROSS=$(rpm -q --qf='%{NAME}.%{ARCH}\n' $PKG_NAME \
		  | egrep "\\.$CROSS_ARCH_RE\$")
	    if expr "$PACKINST" : '.*/cross' >/dev/null
	    then
		if [ ! -z "$PKG_NATIVE" ] \
		&& AssocExists PERMITTEDPKGSET "$PKG_NAME"
		then
		    # removing just one variant of biarch package, preserve the
		    # one we want to keep if there are overlapping files (other
		    # than documentation, config files, etc.)
		    #
		    # Warning: these packages are temporarily removed, need to
		    # be careful not to list anything that RPM needs to run such
		    # as popt or beecrypt
		    #
		    # [use bash process substitution to avoid temporary files]
		    OVERLAP=$(
			comm -12 <(rpm -ql $PKG_NATIVE) <(rpm -ql $PKG_CROSS) \
			| egrep -v '/usr/(share|include)|/etc/|/var'
		    )
		    if [ "$OVERLAP" ]
		    then
			TO_REINSTALL="$TO_REINSTALL $PKG_NAME"
			PKG_MISSING="$PKG_MISSING
$PKG_NAME"
			# make sure we get the current version when replacing it
			# unless a specific version is required already
			AssocExists REQVERSIONSET "$PKG_NAME" || {
			    INST_VER=$(rpm -q --qf='%{VERSION}-%{RELEASE}\n' $PKG_NAME | head -1)
			    AssocPut REQVERSIONSET "$PKG_NAME" "$INST_VER"
			}
			# reinstallation includes delete using --allmatches as
			# a first step, but need to delete it anyway to avoid
			# dependency problems.
			TO_RM="$TO_RM $PKG_CROSS"
		    else
			# no overlap, just removing it is safe
			TO_RM="$TO_RM $PKG_CROSS"
		    fi
		else
		    TO_RM="$TO_RM $PKG_CROSS"
		fi
	    else
	    	TO_RM="$TO_RM $PKG_NATIVE"
	    fi
	}
    done
    TickOff

    [ "$TO_REINSTALL" ] && {
	Log "Packages with overlapping files to reinstall: $TO_REINSTALL"
	echo
    }

    echo "$TO_RM" | grep 'eal.*certification' && {
    	Die "BUG: trying to remove certification RPM?! Emergency stop."
    }

    echo "$TO_RM" | grep 'eal.-config' && {
    	Die "BUG: trying to remove certification RPM?! Emergency stop."
    }


    if [ "$PKG_MISSING" -o "$TO_RM" -o "$TO_REINSTALL" ]
    then
    	Log $(echo "$PKG_MISSING" | grep . | wc -l) "package(s) missing"
	if [ -z "$PKG_MISSING" ] || ShallI "Search for missing packages on install media"; then
	    local QUESTION=
	    FindMissingPackages "$PKG_MISSING"

	    if [ -z "$TO_RM" -a -z "$TO_REINSTALL" ]; then
		Log "All installed packages are permitted."
		QUESTION="Install new and updated packages?"
	    else
		Log "Before adding or updating packages, I want to remove the following packages:"
		Log "$TO_RM"
		Log "and remove and reinstall the following packages:"
		Log "$TO_REINSTALL"
		QUESTION="Delete unwanted packages and install new and updated packages?"
	    fi

	    if AskYN "$QUESTION" "y"; then
		[ "$TO_RM" ] && {
		    # Special case - "mdadm" depends on "smtpdaemon" virtual package,
		    # need to install postfix before removing sendmail
		    echo "$PKG_MISSING" | grep -q postfix && [ -f $_RPM_TMP/postfix* ] && {
			Log "Installing replacement MTA postfix"
			RunWithLog rpm -ivh --replacefiles $_RPM_TMP/postfix*.rpm \
			|| Die "can't install postfix"
			PKG_MISSING="$(echo "$PKG_MISSING" | grep -v "postfix")"
			rm -f $_RPM_TMP/postfix*.rpm
		    }

		    # ok, now remove all unwanted packages
		    TickOn
		    RunWithLog rpm -e $TO_RM || {
			Die "
rpm package removal was unsuccessful.
Please do it manually.
You can find the list of packages to be removed in the logfile $_LOGFILE."
		    }
		    TickOff
		    Log "Successfully removed packages."
		}

		# Remove (violently) all those that need to be replaced
		[ "$TO_REINSTALL" ] && {
		    TickOn
		    RunWithLog rpm -e --allmatches --nodeps $TO_REINSTALL \
		    || Die "Can't force remove packages: $TO_REINSTALL"
		    Log "Successfully removed packages pending replacement."
		    TickOff
		}

		Log "Installing and upgrading packages" 

		# Special case - do tcp-wrappers before glibc, otherwise
		# sshd will fail on restart
		[ -f $_RPM_TMP/tcp_wrappers*.rpm ] && {
		    RunWithLog rpm -Uvh --replacefiles $_RPM_TMP/tcp_wrappers*.rpm \
		    || Die "tcp_wrappers upgrade failed"
		    rm -f $_RPM_TMP/tcp_wrappers*.rpm
		}
		
		RunWithLog rpm -Uvh --replacefiles $_RPM_TMP/*.rpm \
		|| Die "Package installation failed"
	    else 
		Log "Package update refused"
		_FAILURE=1
	    fi
	    rm -f $_RPM_TMP/*.rpm
	    rmdir $_RPM_TMP

	    # now re-run check
	    CheckPackages
	    return $?
	else
	    Die "Missing packages: $PKG_MISSING"
	fi
    else
    	Log "All required packages are installed."
    fi

}

:  --CloseLog
#  you need an explanation?

CloseLog() {
    [ -e /dev/fd/88 ] && exec 88>&-
}

: --ConfigurePostfix
ConfigurePostfix() {

    Title "Configure Postfix"

    local MainCF=/etc/postfix/main.cf

    if ShallI "Disable user specified programs in .forward files"; then
	Log  "Disabling user specified .forward programs in $MainCF"

	if ! grep -q '^allow_mail_to_commands = alias$' $MainCF
	then
		cp $MainCF $MainCF.new
		echo 'allow_mail_to_commands = alias' >> $MainCF.new
		Replace $MainCF removing $MainCF.new
	fi
    else
	Log "disabling user specified .forward programs declined"
	_FAILURE=1
    fi
}

:  --ConfigureCups
ConfigureCups() {

    Title "Configure Cups"

    local CupsConf=/etc/cups/cupsd.conf

    if ShallI "Configure CUPS to run as non-root user"; then
	Log  "Configuring CUPS to run as non-root-user"

	if ! grep -q '^RunAsUser Yes$' $CupsConf
	then
		if egrep -q '^(User|Group)' $CupsConf \
		|| ! grep -q '^#Group' $CupsConf
		then
			Die "$CupsConf was changed from default, can't modify it"
		fi
		sed '/#Group/{
aUser lp
aGroup sys
aRunAsUser Yes
}' < $CupsConf > $CupsConf.new
		
		Replace $CupsConf removing $CupsConf.new
	fi
    else
	Log "reconfiguring CUPS declined"
	_FAILURE=1
    fi
}

:  --ConfigureAudit
#   just in case....

ConfigureAudit() {

    Title "Configure audit subsystem"

    if ShallI "(optional) Install recommended auditd configuration"
    then
	Replace $_AUDITD_CONF_DEST/$_AUDITD_CONF with $_BASE/$_AUDITD_CONF
    else
	Log "Not modifying auditd configuration."
    fi

    Log "Note: this script does NOT configure audit rules automatically, please configure audit manually according to your local requirements. See ECG and audit.rules(5) man page, and refer to the sample /usr/share/doc/audit-*/capp.rules file included in the audit package for detailed examples."
}


:  --ConfigureFTP
#   disable anonymous ftp

ConfigureFTP() {

    Title " Configure FTP service"

    local V=$_VSFTPD_CONF

    [ -f $V ] || {
	Log "vsftpd not installed?, no configuration changes."
	return 0
    }


    if ShallI "(optional) Disable anonymous FTP access"; then

	sed 's/[#]*anonymous_enable=.*/anonymous_enable=NO/' < $V > $V.new
	Replace $V removing $V.new
    else
	Log "Anonymous FTP disable declined."
    fi
}


:  --ConfigureSSH
#   drop in an evaluated configuration for SSHD

ConfigureSSH() {

    Title " Configure OpenSSH"

    if ShallI "Update sshd configuration"; then
	Replace $_SSHD_DEST/$_SSHD_CONFIG with $_BASE/$_SSHD_CONFIG

    else
	Log "sshd configuration update declined."
	_FAILURE=1
    fi

    # enable sshd by default
    RunWithLog chkconfig --add sshd
    RunWithLog chkconfig --level 345 sshd on
}


:  --ConfigureXinetd
#  ssia
#   this will lock out everything except ftp

ConfigureXinetd() {

    Title " Configure xinetd"

    if ShallI "Update xinetd.conf"; then
	Replace $_XINETD_DEST/$_XINETD_CONF with $_BASE/$_XINETD_CONF

    else
	Log "xinetd configuration declined."
	_FAILURE=1
    fi
}


: --Die
#  log an error and exit

Die() {
    Warn "Fatal: $@"
    TickOff
    exit 1
}


:  --Warn
#  write args to stderr, and log them

Warn() {
    echo "$@" 1>&2
    Log "$@" >/dev/null
}

:  -- GetNativeArchRe
GetNativeArchRe() {
    local NATIVE_ARCH=$(GetArch)

    # determine egrep regexes to match native packages for this arch.
    # "noarch" ones are always treated as native.
    case $NATIVE_ARCH in
    pSeries|iSeries)
    	# special case for ppc64 systems, use 32bit pkgs by default
	# DON'T install 'ppc64iseries' kernel, it's for old systems only
	NATIVE_ARCH='ppc'
	;;
    i?86|xSeries)
	# i386 has some pkgs named *.i686.rpm
	NATIVE_ARCH='i.86'
	;;
    esac
    echo "($NATIVE_ARCH|noarch)"
}

:  -- GetCrossArchRe
GetCrossArchRe() {
    local NATIVE_ARCH=$(GetArch)
    local CROSS_ARCH='unavailable'

    # determine egrep regexes to match cross-compile packages for this arch.
    # Watch out, i386 has some pkgs named *.i686.rpm
    case $NATIVE_ARCH in
    pSeries|iSeries)
	CROSS_ARCH='ppc64'
	;;
    s390x)
	CROSS_ARCH='s390'
	;;
    x86_64|ia64)
	CROSS_ARCH='i.86'
	;;
    esac

    echo "$CROSS_ARCH"
}

:  --GetArch
#  only need this because iSeries and pSeries are both ppc64

GetArch() {
    local ARCH=$(uname -i)

    if [ "$ARCH" != ppc64 ]; then
	echo $ARCH
	return
    fi

    [ -f /proc/cpuinfo ] || {
	mount /proc || {
	    Die "/proc/cpuinfo not available.  Is /proc mounted?"
	}
    }

    if grep '^machine.*:.*CHRP' /proc/cpuinfo > /dev/null; then
	echo "pSeries"
	return
    elif grep '^machine.*:.*iSeries' /proc/cpuinfo > /dev/null; then
	echo "iSeries"
	return
    fi

    Die "cannot determine if this ppc64 is iSeries or pSeries"

}


: --HardenPamConfig
#  replace sensitive PAM config files

HardenPamConfig() {
    # !@ FIXME -- these should be dropped into place by the RPM

    Title " Configure Pluggable Authentication Modules (PAM)"

    local PAMFILES=$(ls $_BASE/*.pam)
    local FILE=

    # Ensure that the opasswd file exists, otherwise password changing breaks
    [ -f /etc/security/opasswd ] || {
	RunWithLog touch /etc/security/opasswd
	RunWithLog chmod 600 /etc/security/opasswd
	RunWithLog $_RESTORECON /etc/security/opasswd
    }

    if ShallI "Replace files in $_PAM_DIR/"; then
        for FILE in $PAMFILES; do
	    Replace $_PAM_DIR/$(basename $FILE .pam) with $FILE
        done
    else
        Log "replacement of $_PAM_DIR/ files declined."
        _FAILURE=1
    fi

    if ShallI "Update $_LOGIN_DEFS_DEST/$_LOGIN_DEFS"; then
	Replace $_LOGIN_DEFS_DEST/$_LOGIN_DEFS with $_BASE/$_LOGIN_DEFS
    else
	Log "replacement of $_LOGIN_DEFS declined."
	_FAILURE=1
    fi
}


: --HardenPermissions
#  set permissions to those required for evaluated configuration

HardenPermissions() {

    Title " Configure SUID/SGID executable permissions"

    local P=$(StripComments $_PERMSFILE)
    local OUTPUT=

    IEcho "All but the following files will have their setuid/setgid bits removed:
"
    IEcho $P | tr '\040' '\012'
    IEcho

    if ShallI "Update setuid/setgid bits";then
	
	set xx $P; shift
	
	P=

	while [ "$1" ]; do P="$P ! -path $1"; shift;done
	
	TickOn
	if [ -z "$_PRINT_ONLY" ]
	then
	    OUTPUT=$(find / -not -fstype ext3 -prune -o \
		-type f \( -perm +4000 -o -perm +2000 \) \
		$P -print0 \
		| xargs -0r chmod -v -s 2>&1)
	else
	    OUTPUT=$(find / -not -fstype ext3 -prune -o \
		-type f \( -perm +4000 -o -perm +2000 \) \
		$P -print0 \
		| xargs -0r echo chmod -v -s 2>&1)
	fi
	TickOff
	Log "$OUTPUT"
    else
	Log "setuid/setgid bit removal declined."
	_FAILURE=1
    fi
    RunWithLog chgrp wheel /bin/su
    RunWithLog chmod 4710  /bin/su
}


: --HardenServiceLinks
#  clear out runlevel links for all services
#  link in only those called out in _SERVICES

HardenServiceLinks() {

    Title " Configure runlevel links for all services"

    local WERE_ENABLED=$(cd $_SERVICEDIR; ls S* | sed 's/...//' | sort)

    # runlevel link removal:
    if ShallI "Remove all runlevel links from $_SERVICEDIR"; then
	(
	    cd $_SERVICEDIR;
	    RunWithLog rm -f * nosuchfileordirectory_dummy
	)
	Log "all runlevel symlinks removed."
    else
	Log "Removal of runlevel symlinks declined."
	_FAILURE=1
    fi


    local SERVICE= 
    local REQUIRED=$( StripComments $_SVC_REQ | sort)
    local PERMITTED=$(StripComments $_SVC_OPT | sort)
    local S= FOUND= COUNT=0
    
    LogBlock "required services: $REQUIRED"
    
    if ShallI "Make links in $_SERVICEDIR for all required services."; then
	for SERVICE in $REQUIRED; do
	    [ -f $_SERVICEBASE/$SERVICE ] || {
		Log "Required Service script $SERVICE not in $_SERVICEBASE
Cannot configure service to start"
		_FAILURE=1
		continue
	    }
	    RunWithLog $_SERVICE_ENABLE $SERVICE $_SERVICE_CMD || {
		Log "$_SERVICE_ENABLE failed to enable $SERVICE"
		_FAILURE=1
		continue
	    }
	    COUNT=$((COUNT + 1))
	done
    else
	Log "symlink creation for required services declined"
	_FAILURE=1
    fi

    LogBlock "previously active services: $WERE_ENABLED"
    LogBlock "permitted additional services: $PERMITTED"

    if ShallI "Reactivate those permitted services you had been running"; then
	for SERVICE in $PERMITTED; do

	    FOUND=
	    for S in $WERE_ENABLED; do
		[ $S = $SERVICE ] && {
		    FOUND=1
		    break
		}
	    done
	    [ "$FOUND" ] && {
		RunWithLog $_SERVICE_ENABLE $SERVICE $_SERVICE_CMD || {
		    Log "$_SERVICE_ENABLE failed to enable $SERVICE"
		    continue
		}
		COUNT=$((COUNT + 1))
	    }
	done
    fi
    Log "$COUNT new runlevel symlinks created."
}



: --Log
#  log to descriptor 88 if available
#   if it's not, log to stdout

Log() {
    if [ -e /dev/fd/88 ]; then
	echo "$@" >&88
	[ "$_VERBOSE" ] && echo "$@"
    else
	echo "$@"
    fi
}

: --RunWithLog
# Run command, appending stdout and stderr to the log file
RunWithLog() {
    if [ "$_PRINT_ONLY" ]
    then
	[ -e /dev/fd/88 ] && echo "Not running: $*" >&88
	[ "$_VERBOSE" ] && echo "Not running: $*"
    else
	local RET
	local LINE

	if [ -e /dev/fd/88 ]; then
	    echo "Running: $*" >&88
	    [ "$_VERBOSE" ] && echo "Running: $*"

	    # can't use 'tee', it doesn't append in the right place causing lost output.
	    # Do it manually instead.
	    "$@" 2>&1 | while IFS='' read LINE
	    do
		echo "$LINE"
		echo "$LINE" >&88
	    done
	    
	    RET=${PIPESTATUS[0]} # bash-specific, but no other easy way to capture return code

	else
	    "$@"
	    RET=$?
	fi
	return $RET
    fi
}

Title() {
    [ "$_VERBOSE" ] && {
	echo
	echo "+++" "$@"
	echo
    }
    Log "+++" "$@" >/dev/null
}

:  --FmtEcho
# Print arguments as indented paragraph, skipping blank lines

FmtEcho() {
    echo "$@" | sed '/^ *$/d;' | fmt | sed '2,$s/^/    /' | fmt
}

:  --LogBlock
# Print arguments to stdout as FmtEcho if interactive, but send to 
# log file as a single line
LogBlock() {
    [ "$_VERBOSE" ] && FmtEcho "$@"
    Log $(echo "$@") >/dev/null
}


: --Main
#  the top of the tops
#

Main() {
    local ARGS=$*
    
    # open the log file
    NewLog " --- $(date) script running: $0 args $ARGS"

    # run checks to make sure all of our data files are here
    local K
    readonly _PKG_ARCH=$_BASE/pkgs-$(GetArch).conf    
    readonly _ALLFILES="$_ALLFILES $_PKG_ARCH"

    for K in $_BASE $_BASE/$_PAMDIR; do
	[ -d $K ] || Die "Required directory missing: $K"
    done

    for K in $_ALLFILES; do
	[ -f $K ] || Die "Required file missing: $K"
    done


    # run sanity checks
    [ $(/usr/bin/id -nu) != root ] && {
	Die "This script must be run as root."
    }
    [ ! -f /proc/1/cmdline ] && {
	Die "Please mount /proc"
    }
    mount | grep "type nfs" && {
	Warn "Warning: you have NFS filesystems mounted, which is only allowed with restrictions. See $_ECG."
    }
    mount | grep "type smb" && {
	Warn "Warning: you have SAMBA filesystems mounted, which is only allowed with restrictions. See $_ECG."
    }
    mount | grep "type vfat" && {
	Die "Please unmount all VFAT filesystems.  See $_ECG."
    }
    mount | grep "type ext2" && {
	Die "Please remount your EXT2 filesystems as EXT3.  See $_ECG."
    }
    mount | grep "type autofs" && {
	Die "Please unmount all AUTOFS filesystems.  See $_ECG."
    }
    # !@ there are probably more filesystems we should gripe about

    [ $(rpm -q kernel kernel-smp | grep -v 'not inst' | wc -l) -gt 1 ] && {
    	Warn "Warning: you have more than one kernel package installed, which may cause problems. You should uninstall unused kernels before running the script, see $_ECG."
    }

    # !@ check for other bootable systems in the GRUB config?

    # is dhclient running?
    ps ax | grep -v grep | grep dhclient >/dev/null && {
	Die "Please hard-configure your network (no DHCP)"
    }

    grep 'wheel:.*:.*:root,.' /etc/group >/dev/null || {
	Die "No trusted users. You won't be able to use 'su'. See $_ECG."
    }

    rpm -qa | grep 'eal3-certification' && {
    	Die "You have the conflicting 'eal3-certification' RPM package installed, please remove that before proceeding."
    }

    [ -d /etc/audit ] && {
	Die "You have obsolete files in the /etc/audit/ directory, please remove those before proceeding."
    }

    # !@ Perhaps we should disable USB and FireWire?  Modems and ISDN?

    # Are we running on a terminal? If yes, be more verbose.
    if [ -t 0 ]
    then
	_INTERACTIVE=yes
    else
	_INTERACTIVE=
    fi

    _VERBOSE=yes
    _ADD_OPTIONAL=

    # parse the command line
    while [ "$1" ]; do
	case $1 in
	    -h|--help)
		Usage
		exit 0
		;;
	    -i|--interactive)
		_INTERACTIVE=yes
		shift
		;;
	    -a|--automated)
		_INTERACTIVE=
		shift
		;;
	    -n|--no-action)
		_PRINT_ONLY=yes
		shift
		;;
	    -v|--verbose)
		_VERBOSE=yes
		shift
		;;
	    --add-optional)
		_ADD_OPTIONAL=yes
		shift
		;;
	    --rpm-path)
		shift
		_RPM_PATH="$1"
		shift
		;;
	    -q|--quiet)
		_VERBOSE=
		shift
		;;
	    *)  Usage
		exit 1
		;;
	esac
    done

    
    [ "$_INTERACTIVE" ] && {
	echo "
This script will reconfigure your system to the RHEL4 CAPP/EAL4+ evaluated
configuration.

You have chosen to run the reconfiguration in interactive mode.
The evaluated configuration requires that *all* the steps are
done. If you want to do this automatically, stop and re-run the
script in noninteractive mode ('-a' option).

The reconfiguration involves removing packages and modifying the
system configuration, which may result in a system that is
not useful to you. For example, the graphical desktop is removed.

Please read the documentation before proceeding.

"
	AskYN "Continue?" "n" || {
	    Die "Reconfiguration declined.  Your system was not modified."
	}
    }

    mount | grep ' / ' | grep "type ext3.*,acl" > /dev/null || {
	UpdateFSTAB || {
	    Die "root filesystem must be ext3 with ACL support on. See $_ECG."
	}
    }
    
    grep '[ \t]auto[ \t]*.*managed' /etc/fstab >/dev/null && {
    	UpdateFSTAB || {
	    Die "must not use automounter for removable media"
	}
    }

    DisableUsbfs
    CheckPackages
    HardenServiceLinks
    HardenPamConfig
    HardenPermissions
    AliasSU
    SecureTTY
    ConfigureSSH
    ConfigureXinetd
    ConfigureFTP
    ConfigureAudit
    ConfigurePostfix
    ConfigureCups
    SetRunLevel 3
    Reboot

    # if we got here, the reboot was declined
    CloseLog
    TickOff
    Warn "
Your system is NOT in the evaluated configuration.
Please check $_LOGFILE for more details."
    return $_FAILURE
}

: --NewLog
#  log an error message, opening the log if necessary
#   used to initiate logging or to log before we're sure
#   logging has been enabled

NewLog() {

    [ -e /dev/fd/88 ] || {
	# open the logfile
	Archive $_LOGFILE
	exec 88> $_LOGFILE	# May 16, Happy Birthday Wladziu!
    }

    (_VERBOSE=1; Log "$@")
}




: --Reboot
#  reboot the system

Reboot() {

    Title "System reboot"

    [ -x /sbin/zipl ] && {
	ShallI "Run 'zipl' to update the boot loader configuration?" && {
	    RunWithLog zipl
	}
    }
    
    if [ $(GetArch) = "iSeries" ]; then
	# must match certified kernel version
	local VMLINITRD=/boot/vmlinitrd-2.4.21-15.0.2.EL.peterm.eal.3
	[ -f $VMLINITRD ] || Die "Kernel $VMLINITRD missing ?!"

	if ShallI "Copy $VMLINITRD into /proc/iSeries/mf/B/vmlinux and activate"
	then
	    RunWithLog dd if=$VMLINITRD of=/proc/iSeries/mf/B/vmlinux bs=4k
	    RunWithLog sh -c "echo 'B' > /proc/iSeries/mf/side"
	else
	    Log "Kernel activation has been declined."
	    _FAILURE=1
	fi
    fi

    [ "$_BOOTFAIL.$_FAILURE" = . ] && {
	if [ "$_PRINT_ONLY" ] 
	then
	    Log "Reconfiguration not done due to running in no-change mode. Simulating reboot."
	else
	    Log "Reconfiguration successful."
	    IEcho
	    Log "It is now necessary to reboot the system. After the reboot,"
	    Log "your system configuration will match the evaluated configuration"
	    IEcho
	fi
	if ShallI "Reboot the system"; then
	    Log "rebooting the system now. Sleeping for 10 seconds..."
	    CloseLog
	    sync
	    RunWithLog sleep 10
	    RunWithLog shutdown -r now
	    echo "Waiting..."
	    RunWithLog sleep 600
	    exit 0
	else
	    Log "reboot declined.
Please note that the system must be rebooted for
the configuration to be complete."
	    _FAILURE=1
	fi
    }
}


: --Replace
#  drop in an updated version of a file

Replace() {

    local SRC=$3 CMD=$2 DEST=$1
    local MODE= UG=

    [ -f $DEST ] && {
	[ "$CMD" = with ] && cmp $SRC $DEST > /dev/null 2>&1 && {
	    Log "$SRC and $DEST are identical. No change."
	    return 0
	}
	Log "Archiving $DEST"
	MODE=$(stat -c '%a'    $DEST)
	UG=$(  stat -c '%U.%G' $DEST)
	expr $DEST : /etc/init.d >/dev/null || {
            # don't store backups inside /etc/init.d
	    RunWithLog Archive $DEST
	}
    }
    RunWithLog cp -p $SRC $DEST
    [ $MODE/$UG != / ] && {
	RunWithLog chmod $MODE $DEST
	RunWithLog chown $UG $DEST
    }
    RunWithLog $_RESTORECON $DEST
    [ "$CMD" = removing ] && rm -f $SRC
}


: --SecureTTY
#  make sure there are no pty entries in /etc/securetty

SecureTTY() {
    grep '^pts' /etc/securetty 2>&1 > /dev/null && {
	Die "root login over network connections is not permitted.
Please check and correct /etc/securetty"
    }
}

    
: --SetRunLevel
#  modify inittab for a specified runlevel

SetRunLevel() {
    local LEVEL=${1:-3}
    local MODE= UG=
    local I=/etc/inittab
    local CURR_LEVEL=$(sed -n 's/^id:\([0-9]*\):.*/\1/p' $I)

    [ ."$LEVEL" = ."$CURR_LEVEL" ] && return

    if ShallI "Change default runlevel to $LEVEL"; then
	Log "changing default runlevel to $LEVEL"

	sed 's/^id:.:initdefault:/id:'$LEVEL':initdefault:/' < $I > $I.new

	Replace $I removing $I.new
    else
	Log "initdefault change to $LEVEL declined."
	_FAILURE=1
	_BOOTFAIL=1
    fi
}


:  --ShallI
#  allow the user to confirm an action before doing it
 
ShallI() {
    [ "$_INTERACTIVE" ] || Log "*** $1"
    
    AskYN "$1?" "y"
}

: --StripComments
# strip comments from a file, write result on stdout
#  filename in $1 or will use stdin

StripComments() {
    sed 's/#.*//' < $1
}


:  --TickOff
#   turn off heartbeat

TickOff() {
    [ "$_TICKPID" ] && {
	kill -1 $_TICKPID
	wait $_TICKPID
	_TICKPID=
	echo -e " \b"
    }
}

:  --TickOn
#  heartbeat - draw spinning ticker on the screen

TickOn() {
    [ "$_TICKPID" ] && return
    [ "$_INTERACTIVE" ] || return

    (
	trap 'exit 0' 1 2 15

	while :; do
	    echo -ne '/\b'
	    sleep 1
	    echo -ne '-\b'
	    sleep 1
	    echo -ne '\\\b'
	    sleep 1
	    echo -ne '|\b'
	    sleep 1
	done
    ) &
    _TICKPID=$!
}


:  --UpdateFSTAB
#  fix up the ext2/ext3 mounts to have user_xattr,acl
#  remount the offending filesystems

UpdateFSTAB() {

    Title " Configure mount options in /etc/fstab"

    local LINE= REMOUNT= FS=

    while read LINE; do
	expr "$LINE" : '#|[ 	][ 	]*$' >/dev/null && {
	    echo "$LINE" >> /etc/fstab.new
	}
	set xx $LINE; shift
	if [ $3 == "ext3" -o $3 == "ext2" ]; then
	    printf "%s\t%s\t%s\t%s\t%s\t%s\n"           \
		$1 $2 ext3 "user_xattr,acl" $5 $6 >> /etc/fstab.new
	    REMOUNT="$REMOUNT $2"
	elif [ $3 == "auto" ]; then
	    printf "%s\t%s\t%s\t%s\t%s\t%s\n"           \
		$1 $2 iso9660 "fscontext=system_u:object_r:removable_t,noexec,nodev,nosuid,noauto" $5 $6 >> /etc/fstab.new
	    # no need to remount these
	else
	    echo "$LINE" >> /etc/fstab.new
	fi
    done < /etc/fstab > /etc/fstab.new

    [ "$REMOUNT" ] && {
	Replace /etc/fstab removing /etc/fstab.new
	for FS in $REMOUNT; do
	    mount $FS -o remount || _FAILURE=1
	done
    }
    [ ! "$_FAILURE" ]
    # don't add code here
    return $?
}


:
: -- %Global Constants
:

# the following variable gets set by "make install"
readonly _BASE=/usr/share/capp

readonly _ECG=ECG
readonly _ECG_FULL="Evaluated Configuration Guide"

readonly _LOGFILE=/var/log/capp-eal4-cert.log

# PAM locations
readonly _PAM_DIR=/etc/pam.d

readonly _PERMSFILE=$_BASE/capp-eal4-perms.conf

readonly _PKG_CERT=$_BASE/pkgs-cert-versions.conf
readonly _PKG_OPT=$_BASE/pkgs-opt.conf
_PKG_ARCH=		# needs code, gets set in Main

# req-pkgs.conf MUST include post-RHEL3U2 errata
#   required for certification

readonly _PKG_REQ=$_BASE/pkgs-req.conf

readonly _SYSCTL=/etc/sysctl.conf
readonly _SERVICEBASE=/etc/rc.d/init.d
readonly _SERVICEDIR=/etc/rc.d/rc3.d
readonly _SERVICE_ENABLE="chkconfig --level 3"
readonly _SERVICE_CMD=on
readonly _SVC_REQ=$_BASE/svc-req.conf
readonly _SVC_OPT=$_BASE/svc-allow.conf

readonly _SSHD_CONFIG=sshd_config
readonly _SSHD_DEST=/etc/ssh
readonly _XINETD_CONF=xinetd.conf
readonly _XINETD_DEST=/etc
readonly _LOGIN_DEFS=login.defs
readonly _LOGIN_DEFS_DEST=/etc
readonly _VSFTPD_CONF=/etc/vsftpd/vsftpd.conf
readonly _AUDITD_CONF=auditd.conf
readonly _AUDITD_CONF_DEST=/etc

#readonly _RESTORECON=touch
readonly _RESTORECON=restorecon

# this will get the name of the arch-dependent packages file
#  and go readonly in Main()
_ALLFILES="$_PERMSFILE $_PKG_CERT $_PKG_OPT $_PKG_REQ $_SVC_REQ $_SVC_OPT"
_ALLFILES="$_ALLFILES $_BASE/$_SSHD_CONFIG $_BASE/$_XINETD_CONF"
_ALLFILES="$_ALLFILES $_BASE/$_LOGIN_DEFS"


:
: -- %Global Variables
:
# set PATH to include /sbin and /usr/sbin. Be paranoid as well.
PATH=/bin:/usr/bin:/sbin:/usr/sbin
export PATH
#
# these are all default values
#
_BOOTFAIL=
_FAILURE=
_INTERACTIVE=yes
_PRINT_ONLY=
_ADD_OPTIONAL=
_RPM_PATH="/root/rpms /media/cdrom*"
_VERBOSE=
_TICKPID=
_RPM_TMP=

: --Usage
#  Print summary of supported options
Usage() {
    echo "Usage: $0 [OPTIONS]
Options:
    -h|--help           Print this message
    -i|--interactive    Prompt for permission before changes (default)
    -a|--automated      No prompts, take all default answers
    -n|--no-action      Nondestructive mode, print commands but don't run them
                        (can be combined with -a)
    -q|--quiet          Be less verbose
                        (see $_LOGFILE for detailed msgs)
    --rpm-path DIRS     Search for missing RPMs in DIRs (default $_RPM_PATH)
			(in order, may contain shell wildcards, must be quoted)
    --add-optional      Treat all optional packages as required
Example:
$0 -a --add-optional --rpm-path '/root/rpms /media/cd*'
" >&2
    exit 1
}

[ $_CHECK_SYNTAX_ ] || Main "$@"   # no code after this line!
