#---------------------------------------------------------------
# Project         : Mandrake Linux
# Module          : share
# File            : libmsec.py
# Version         : $Id$
# Author          : Frederic Lepied
# Created On      : Mon Dec 10 22:52:17 2001
# Purpose         : all access points of the msec utility.
#---------------------------------------------------------------

import ConfigFile
import Config
from Log import *

import os
import grp
import Perms
import gettext
import pwd
import re
import string
import commands

try:
    cat = gettext.Catalog('msec')
    _ = cat.gettext
except IOError:
    _ = str

SUFFIX='.msec'
_interactive=0
_same_level=1

# list of config files

ATALLOW = '/etc/at.allow'
AUTOLOGIN = '/etc/sysconfig/autologin'
BASTILLENOLOGIN = '/etc/bastille-no-login'
CRON = '/etc/cron.d/msec'
CRONALLOW = '/etc/cron.allow'
GDM = '/etc/pam.d/gdm'
GDMCONF = '/etc/X11/gdm/gdm.conf'
HALT = '/etc/security/console.apps/halt'
HOSTCONF = '/etc/host.conf'
HOSTSDENY = '/etc/hosts.deny'
INITTAB = '/etc/inittab'
ISSUE = '/etc/issue'
ISSUENET = '/etc/issue.net'
KDE = '/etc/pam.d/kde'
KDMRC = '/usr/share/config/kdm/kdmrc'
LDSOPRELOAD = '/etc/ld.so.preload'
LILOCONF = '/etc/lilo.conf'
LOGINDEFS = '/etc/login.defs'
MENULST = '/boot/grub/menu.lst'
MSEC = '/etc/sysconfig/msec'
MSECBIN = '/usr/sbin/msec'
MSECCRON = '/etc/cron.hourly/msec'
MSEC_XINIT = '/etc/X11/xinit.d/msec'
PASSWD = '/etc/pam.d/passwd'
POWEROFF = '/etc/security/console.apps/poweroff'
REBOOT = '/etc/security/console.apps/reboot'
SECURETTY = '/etc/securetty'
SECURITYCONF = '/var/lib/msec/security.conf'
SECURITYCRON = '/etc/cron.daily/msec'
SECURITYSH = '/usr/share/msec/security.sh'
SERVER = '/etc/security/msec/server'
SHADOW = '/etc/shadow'
SHUTDOWN = '/etc/security/console.apps/shutdown'
SHUTDOWNALLOW = '/etc/shutdown.allow'
SSHDCONFIG = '/etc/ssh/sshd_config'
STARTX = '/usr/X11R6/bin/startx '
SU = '/etc/pam.d/su'
SYSCTLCONF = '/etc/sysctl.conf'
SYSLOGCONF = '/etc/syslog.conf'
SYSTEM_AUTH = '/etc/pam.d/system-auth'
XDM = '/etc/pam.d/xdm'
XSERVERS = '/etc/X11/xdm/Xservers'

# constants to keep in sync with shadow.py
NONE=0
ALL=1
LOCAL=2

# config files => actions

ConfigFile.add_config_assoc(INITTAB, '/sbin/telinit q')
ConfigFile.add_config_assoc('/etc(?:/rc.d)?/init.d/(.+)', '[ -f /var/lock/subsys/@1 ] && @0 reload')
ConfigFile.add_config_assoc(SYSCTLCONF, '/sbin/sysctl -e -p /etc/sysctl.conf')
ConfigFile.add_config_assoc(SSHDCONFIG, '[ -f /var/lock/subsys/sshd ] && /etc/rc.d/init.d/sshd restart')
ConfigFile.add_config_assoc(LILOCONF, '[ `/usr/sbin/detectloader` = LILO ] && /sbin/lilo')
ConfigFile.add_config_assoc(SYSLOGCONF, '[ -f /var/lock/subsys/syslog ] && service syslog reload')
ConfigFile.add_config_assoc('^/etc/issue$', '/usr/bin/killall mingetty')

#

def changing_level():
    'D'
    global _same_level
    _same_level=0
    
# configuration rules

def set_secure_level(level):
    msec = ConfigFile.get_config_file(MSEC)

    val = msec.get_shell_variable('SECURE_LEVEL')

    if not val or int(val) != level:
        _interactive and log(_('Setting secure level to %s') % level)
        msec.set_shell_variable('SECURE_LEVEL', level)

def get_secure_level():
    'D'
    msec = ConfigFile.get_config_file(MSEC)
    return msec.get_shell_variable('SECURE_LEVEL')

def set_server_level(level):
    _interactive and log(_('Setting server level to %s') % level)
    securityconf = ConfigFile.get_config_file(SECURITYCONF)
    securityconf.set_shell_variable('SERVER_LEVEL', level)

def get_server_level():
    'D'
    securityconf = ConfigFile.get_config_file(SECURITYCONF)
    level = securityconf.get_shell_variable('SERVER_LEVEL')
    if level: return level
    msec = ConfigFile.get_config_file(MSEC)
    return msec.get_shell_variable('SECURE_LEVEL')

def create_server_link():
    '''  If SERVER_LEVEL (or SECURE_LEVEL if absent) is greater than 3
in /etc/security/msec/security.conf, creates the symlink /etc/security/msec/server
to point to /etc/security/msec/server.<SERVER_LEVEL>. The /etc/security/msec/server
is used by chkconfig --add to decide to add a service if it is present in the file
during the installation of packages.'''
    level = get_server_level()
    server = ConfigFile.get_config_file(SERVER)
    if level in ('0', '1', '2', '3'):
        _interactive and log(_('Allowing chkconfig --add from rpm'))
        server.exists() and server.unlink()
    else:
        _interactive and log(_('Restricting chkconfig --add from rpm'))
        server.symlink(SERVER + '.' + str(level))

# helper function for set_root_umask and set_user_umask
def set_umask(variable, umask, msg):
    'D'
    msec = ConfigFile.get_config_file(MSEC)
    
    if msec.exists():
        val = msec.get_shell_variable(variable)
    else:
        val = None

    # don't lower security when not changing security level
    if _same_level:
        if val:
            octal = int(umask, 8) | int(val, 8)
            umask = '0%o' % octal
                
    if val != umask:
        _interactive and log(_('Setting %s umask to %s') % (msg, umask))
        msec.set_shell_variable(variable, umask)
    
def set_root_umask(umask):
    '''  Set the root umask.'''
    set_umask('UMASK_ROOT', umask, 'root')

def set_user_umask(umask):
    '''  Set the user umask.'''
    set_umask('UMASK_USER', umask, 'users')

# the listen_tcp argument is kept for backward compatibility
def allow_x_connections(arg, listen_tcp=None):
    '''  Allow/Forbid X connections. First arg specifies what is done
on the client side: ALL (all connections are allowed), LOCAL (only
local connection) and NONE (no connection).'''
    
    msec = ConfigFile.get_config_file(MSEC_XINIT)

    val = msec.exists() and msec.get_match('/usr/X11R6/bin/xhost\s*\+\s*([^#]*)')

    if val:
        if val == '':
            val = ALL
        elif val == 'localhost':
            val = LOCAL
        else:
            val = NONE
    else:
        val = NONE
        
    # don't lower security when not changing security level
    if _same_level:
        if val == NONE or (val == LOCAL and arg == ALL):
            return
        
    if arg == ALL:
        if val != arg:
            _interactive and log(_('Allowing users to connect X server from everywhere'))
            msec.exists() and msec.replace_line_matching('/usr/X11R6/bin/xhost', '/usr/X11R6/bin/xhost +', 1)

    elif arg == LOCAL:
        if val != arg:
            _interactive and log(_('Allowing users to connect X server from localhost'))
            msec.exists() and msec.replace_line_matching('/usr/X11R6/bin/xhost', '/usr/X11R6/bin/xhost + localhost', 1)
        
    elif arg == NONE:
        if val != arg:
            _interactive and log(_('Restricting X server connection to the console user'))
            msec.exists() and msec.remove_line_matching('/usr/X11R6/bin/xhost', 1)
        
    else:
        error(_('invalid allow_x_connections arg: %s') % arg)
        return

STARTX_REGEXP = '(\s*clientargs=".*) -nolisten tcp(.*")'
XSERVERS_REGEXP = '(\s*[^#]+/usr/X11R6/bin/X .*) -nolisten tcp(.*)'
GDMCONF_REGEXP = '(\s*command=.*/X.*?) -nolisten tcp(.*)$'
def allow_xserver_to_listen(arg):
    ''' The argument specifies if clients are authorized to connect
to the X server on the tcp port 6000 or not.'''
    
    startx = ConfigFile.get_config_file(STARTX)
    xservers = ConfigFile.get_config_file(XSERVERS)
    gdmconf = ConfigFile.get_config_file(GDMCONF)

    val_startx = startx.exists() and startx.get_match(STARTX_REGEXP)
    val_xservers = xservers.exists() and xservers.get_match(XSERVERS_REGEXP)
    val_gdmconf = gdmconf.exists() and gdmconf.get_match(GDMCONF_REGEXP)
    
    # don't lower security when not changing security level
    if _same_level:
        if val_startx and val_xservers and val_gdmconf:
            return
        
    if arg:
        if val_startx or val_xservers or val_gdmconf:
            _interactive and log(_('Allowing the X server to listen to tcp connections'))
            if not (_same_level and val_startx):
                startx.exists() and startx.replace_line_matching(STARTX_REGEXP, '@1@2')
            if not (_same_level and val_xservers):
                xservers.exists() and xservers.replace_line_matching(XSERVERS_REGEXP, '@1@2', 0, 1)
            if not (_same_level and val_gdmconf):
                gdmconf.exists() and gdmconf. replace_line_matching(GDMCONF_REGEXP, '@1@2', 0, 1)
    else:
        if not val_startx or not val_xservers or not val_gdmconf:
            _interactive and log(_('Forbidding the X server to listen to tcp connection'))
            startx.exists() and startx.replace_line_matching('clientargs="(.*?)( -nolisten tcp)?"', 'clientargs="@1 -nolisten tcp"')
            xservers.exists() and xservers.replace_line_matching('(\s*[^#]+/usr/X11R6/bin/X .*?)( -nolisten tcp)?$', '@1 -nolisten tcp', 0, 1)
            gdmconf.exists() and gdmconf. replace_line_matching('(\s*command=.*/X.*?)( -nolisten tcp)?$', '@1 -nolisten tcp', 0, 1)

def set_shell_timeout(val):
    '''  Set the shell timeout. A value of zero means no timeout.'''

    msec = ConfigFile.get_config_file(MSEC)

    if msec.exists():
        old = msec.get_shell_variable('TMOUT')
        if old != None:
            old = int(old)
    else:
        old = None
        
    # don't lower security when not changing security level
    if _same_level:
        if old != None and old > val:
            return

    if old != val:
        _interactive and log(_('Setting shell timeout to %s') % val)
        msec.set_shell_variable('TMOUT', val)

def set_shell_history_size(size):
    '''  Set shell commands history size. A value of -1 means unlimited.'''
    msec = ConfigFile.get_config_file(MSEC)

    if msec.exists():
        val = msec.get_shell_variable('HISTFILESIZE')
    else:
        val = None
        
    # don't lower security when not changing security level
    if _same_level:
        if val != None:
            val = int(val)
            if size == -1 or val < size:
                return
            
    if size >= 0:
        if val != size:
            _interactive and log(_('Setting shell history size to %s') % size)
            msec.set_shell_variable('HISTFILESIZE', size)
    else:
        if val != None:
            _interactive and log(_('Removing limit on shell history size'))
            msec.remove_line_matching('^HISTFILESIZE=')
        
def allow_reboot(arg):
    '''  Allow/Forbid reboot by the console user.'''
    shutdownallow = ConfigFile.get_config_file(SHUTDOWNALLOW)
    sysctlconf = ConfigFile.get_config_file(SYSCTLCONF)
    kdmrc = ConfigFile.get_config_file(KDMRC)
    gdmconf = ConfigFile.get_config_file(GDMCONF)
    
    val_shutdownallow = shutdownallow.exists()
    val_sysctlconf = sysctlconf.exists() and sysctlconf.get_shell_variable('kernel.sysrq')
    num = 0
    val = {}
    for f in [SHUTDOWN, POWEROFF, REBOOT, HALT]:
        val[f] = ConfigFile.get_config_file(f).exists()
        if val[f]:
            num = num + 1
    val_kdmrc = kdmrc.exists() and kdmrc.get_shell_variable('AllowShutdown')
    val_gdmconf = gdmconf.exists() and gdmconf.get_shell_variable('SystemMenu')

    # don't lower security when not changing security level
    if _same_level:
        if val_shutdownallow and val_sysctlconf == '0' and num == 0 and val_kdmrc == 'None' and val_gdmconf == 'false':
            return
        
    if arg:
        _interactive and log(_('Allowing reboot to the console user'))
        if not (_same_level and val_shutdownallow):
            shutdownallow.exists() and shutdownallow.move(SUFFIX)
        for f in [SHUTDOWN, POWEROFF, REBOOT, HALT]:
            cfg = ConfigFile.get_config_file(f)
            if not (_same_level and not val[f]):
                cfg.exists() or cfg.touch()
        if not (_same_level and val_sysctlconf == '0'):
            sysctlconf.set_shell_variable('kernel.sysrq', 1)
        if not (_same_level and val_kdmrc == 'None'):
            kdmrc.exists() and kdmrc.set_shell_variable('AllowShutdown', 'All', 'X-:\*-Greeter', '^\s*$')
        if not (_same_level and val_gdmconf == 'false'):
            gdmconf.exists() and gdmconf.set_shell_variable('SystemMenu', 'true', '\[greeter\]', '^\s*$')
    else:
        _interactive and log(_('Forbidding reboot to the console user'))
        ConfigFile.get_config_file(SHUTDOWNALLOW, SUFFIX).touch()
        for f in [SHUTDOWN, POWEROFF, REBOOT, HALT]:
            ConfigFile.get_config_file(f).unlink()
        sysctlconf.set_shell_variable('kernel.sysrq', 0)
        kdmrc.exists() and kdmrc.set_shell_variable('AllowShutdown', 'None', 'X-:\*-Greeter', '^\s*$')
        gdmconf.exists() and gdmconf.set_shell_variable('SystemMenu', 'false', '\[greeter\]', '^\s*$')
    
def allow_user_list(arg):
    '''  Allow/Forbid the list of users on the system on display managers (kdm and gdm).'''
    kdmrc = ConfigFile.get_config_file(KDMRC)
    gdmconf = ConfigFile.get_config_file(GDMCONF)
    
    oldval_kdmrc = kdmrc.exists() and kdmrc.get_shell_variable('ShowUsers')
    oldval_gdmconf = gdmconf.exists() and gdmconf.get_shell_variable('Browser')
    
    if arg:
        msg = 'Allowing the listing of users in display managers'
        val_kdmrc = 'All'
        val_gdmconf = 'true'
    else:
        msg = 'Disabling the listing of users in display managers'
        val_kdmrc = 'None'
        val_gdmconf = 'false'

    # don't lower security when not changing security level
    if _same_level:
        if oldval_kdmrc == 'None' and oldval_gdmconf == 'false':
            return
        if oldval_kdmrc == 'None':
            val_kdmrc = 'None'
        if oldval_gdmconf == 'false':
            val_gdmconf = 'false'
    
    if (gdmconf.exists() and oldval_gdmconf != val_gdmconf) or (kdmrc.exists() and oldval_kdmrc != val_kdmrc):
        _interactive and log(_(msg))
        oldval_kdmrc != val_gdmconf and kdmrc.exists() and kdmrc.set_shell_variable('ShowUsers', val_kdmrc)
        oldval_gdmconf != val_gdmconf and gdmconf.exists() and gdmconf.set_shell_variable('Browser', val_gdmconf)

def allow_root_login(arg):
    '''  Allow/Forbid direct root login.'''
    securetty = ConfigFile.get_config_file(SECURETTY)
    kde = ConfigFile.get_config_file(KDE)
    gdm = ConfigFile.get_config_file(GDM)
    xdm = ConfigFile.get_config_file(XDM)

    val = {}
    val[kde] = kde.exists() and kde.get_match('auth required /lib/security/pam_listfile.so onerr=succeed item=user sense=deny file=/etc/bastille-no-login')
    val[gdm] = gdm.exists() and gdm.get_match('auth required /lib/security/pam_listfile.so onerr=succeed item=user sense=deny file=/etc/bastille-no-login')
    val[xdm] = xdm.exists() and xdm.get_match('auth required /lib/security/pam_listfile.so onerr=succeed item=user sense=deny file=/etc/bastille-no-login')
    num = 0
    for n in range(1, 7):
        s = 'tty' + str(n)
        if securetty.get_match(s):
            num = num + 1
            val[s] = 1
        else:
            val[s] = 0
        s = 'vc/' + str(n)
        if securetty.get_match(s):
            num = num + 1
            val[s] = 1
        else:
            val[s] = 0
    
    # don't lower security when not changing security level
    if _same_level:
        if (not kde.exists() or val[kde]) and (not gdm.exists() or val[gdm]) and (not xdm.exists() or val[xdm]) and num == 12:
            return

    if arg:
        if val[kde] or val[gdm] or val[xdm] or num != 12:
            _interactive and log(_('Allowing direct root login'))
        
            for cnf in (kde, gdm, xdm):
                if not (_same_level and val[cnf]):
                    cnf.exists() and cnf.remove_line_matching('^auth\s*required\s*/lib/security/pam_listfile.so.*bastille-no-login', 1)
        
            for n in range(1, 7):
                s = 'tty' + str(n)
                if not (_same_level and not val[s]):
                    securetty.replace_line_matching(s, s, 1)
                s = 'vc/' + str(n)
                if not (_same_level and not val[s]):
                    securetty.replace_line_matching(s, s, 1)
    else:
        if (kde.exists() and not val_kde) or (gdm.exists() and not val_gdm) or (xdm.exists() and not val_xdm) or num > 0:
            _interactive and log(_('Forbidding direct root login'))
        
            bastillenologin = ConfigFile.get_config_file(BASTILLENOLOGIN)
            bastillenologin.replace_line_matching('^\s*root', 'root', 1)
        
            for cnf in (kde, gdm, xdm):
                cnf.exists() and (cnf.replace_line_matching('^auth\s*required\s*/lib/security/pam_listfile.so.*bastille-no-login', 'auth required /lib/security/pam_listfile.so onerr=succeed item=user sense=deny file=/etc/bastille-no-login') or \
                                  cnf.insert_at(0, 'auth required /lib/security/pam_listfile.so onerr=succeed item=user sense=deny file=/etc/bastille-no-login'))
        
            securetty.remove_line_matching('.+', 1)

def allow_remote_root_login(arg):
    '''  Allow/Forbid remote root login.'''
    sshd_config = ConfigFile.get_config_file(SSHDCONFIG)

    if sshd_config.exists():
        val = sshd_config.get_match('^\s*PermitRootLogin\s+(no|yes)', '@1')
    else:
        val = None

    # don't lower security when not changing security level
    if _same_level:
        if val == 'no':
            return

    val = (val == 'yes')

    if arg:
        if val != arg:
            _interactive and log(_('Allowing remote root login'))
            sshd_config.exists() and sshd_config.replace_line_matching('^\s*PermitRootLogin\s+(no|yes)',
                                                                       'PermitRootLogin yes', 1)
    else:
        if val != arg:
            _interactive and log(_('Forbidding remote root login'))
            sshd_config.exists() and sshd_config.replace_line_matching('^\s*PermitRootLogin\s+(no|yes)',
                                                                       'PermitRootLogin no', 1)

def enable_pam_wheel_for_su(arg):
    '''   Enabling su only from members of the wheel group or allow su from any user.'''
    su = ConfigFile.get_config_file(SU)

    val = su.exists() and su.get_match('^auth\s+required\s+/lib/security/pam_wheel.so\s+use_uid\s*$')
    
    # don't lower security when not changing security level
    if _same_level:
        if val:
            return

    if arg:
        if not val:
            _interactive and log(_('Allowing su only from wheel group members'))
            try:
                ent = grp.getgrnam('wheel')
            except KeyError:
                error(_('no wheel group'))
                return
            members = ent[3]
            if members == [] or members == ['root']:
                _interactive and error(_('wheel group is empty'))
                return
            su.exists() and (su.replace_line_matching('^auth\s+required\s+/lib/security/pam_wheel.so\s+use_uid\s*$',
                                                      'auth       required     /lib/security/pam_wheel.so use_uid') or \
                             su.insert_after('^auth\s+required',
                                             'auth       required     /lib/security/pam_wheel.so use_uid'))
    else:
        if val:
            _interactive and log(_('Allowing su for all'))
            su.exists() and su.remove_line_matching('^auth\s+required\s+/lib/security/pam_wheel.so\s+use_uid\s*$')
    
def allow_issues(arg):
    '''  If \\fIarg\\fP = ALL allow /etc/issue and /etc/issue.net to exist. If \\fIarg\\fP = NONE no issues are
allowed else only /etc/issue is allowed.'''
    issue = ConfigFile.get_config_file(ISSUE, SUFFIX)
    issuenet = ConfigFile.get_config_file(ISSUENET, SUFFIX)

    val = issue.exists(1)
    valnet = issuenet.exists(1)

    # don't lower security when not changing security level
    if _same_level:
        if not val and not valnet:
            return
        if arg == ALL and not valnet:
            return

    if arg == ALL:
        if not (val and valnet):
            _interactive and log(_('Allowing network pre-login messages'))    
            issue.exists() and issue.get_lines()
            issuenet.exists() and issuenet.get_lines()
    else:
        if arg == NONE:
            if val:
                _interactive and log(_('Disabling pre-login message'))
                issue.exists(1) and issue.move(SUFFIX) and issue.modified()
        else:
            if not val:
                _interactive and log(_('Allowing pre-login message'))
                issue.exists() and issue.get_lines()
        if valnet:
            _interactive and log(_('Disabling network pre-login message'))
            issuenet.exists(1) and issuenet.move(SUFFIX)

def allow_autologin(arg):
    '''  Allow/Forbid autologin.'''
    autologin = ConfigFile.get_config_file(AUTOLOGIN)

    if autologin.exists():
        val = autologin.get_shell_variable('AUTOLOGIN')
    else:
        val = None
        
    # don't lower security when not changing security level
    if _same_level:
        if val == 'no':
            return

    if arg:
        if val != 'yes':
            _interactive and log(_('Allowing autologin'))
            autologin.exists() and autologin.set_shell_variable('AUTOLOGIN', 'yes')
    else:
        if val != 'no':
            _interactive and log(_('Forbidding autologin'))
            autologin.exists() and autologin.set_shell_variable('AUTOLOGIN', 'no')

def password_loader(value):
    'D'
    _interactive and log(_('Activating password in boot loader'))
    liloconf = ConfigFile.get_config_file(LILOCONF)
    liloconf.exists() and (liloconf.replace_line_matching('^password=', 'password="' + value + '"', 0, 1) or \
                           liloconf.insert_after('^boot=', 'password="' + value + '"')) and \
                           Perms.chmod(liloconf.path, 0600)
    # TODO encrypt password in grub
    menulst = ConfigFile.get_config_file(MENULST)
    menulst.exists() and (menulst.replace_line_matching('^password\s', 'password "' + value + '"') or \
                          menulst.insert_at(0, 'password "' + value + '"')) and \
                          Perms.chmod(menulst.path, 0600)
    # TODO add yaboot support
        
def nopassword_loader():
    'D'
    _interactive and log(_('Removing password in boot loader'))
    liloconf = ConfigFile.get_config_file(LILOCONF)
    liloconf.exists() and liloconf.remove_line_matching('^password=', 1)
    menulst = ConfigFile.get_config_file(MENULST)
    menulst.exists() and menulst.remove_line_matching('^password\s')

def enable_console_log(arg, expr='*.*', dev='tty12'):
    '''  Enable/Disable syslog reports to console 12. \\fIexpr\\fP is the
expression describing what to log (see syslog.conf(5) for more details) and
dev the device to report the log.'''
    
    syslogconf = ConfigFile.get_config_file(SYSLOGCONF)

    if syslogconf.exists():
        val = syslogconf.get_match('\s*[^#]+/dev/([^ ]+)', '@1')
    else:
        val = None

    # don't lower security when not changing security level
    if _same_level:
        if val:
            return
        
    if arg:
        if dev != val:
            _interactive and log(_('Enabling log on console'))
            syslogconf.exists() and syslogconf.replace_line_matching('\s*[^#]+/dev/', expr + ' /dev/' + dev, 1)
    else:
        if val != None:
            _interactive and log(_('Disabling log on console'))
            syslogconf.exists() and syslogconf.remove_line_matching('\s*[^#]+/dev/')

CRON_ENTRY = '*/1 * * * *    root    /usr/share/msec/promisc_check.sh'
CRON_REGEX = '[^#]+/usr/share/msec/promisc_check.sh'

def enable_promisc_check(arg):
    '''  Activate/Disable ethernet cards promiscuity check.'''
    cron = ConfigFile.get_config_file(CRON)
    
    val = cron.exists() and cron.get_match(CRON_REGEX)
    
    # don't lower security when not changing security level
    if _same_level:
        if val == CRON_ENTRY:
            return
        
    if arg:
        if val != CRON_ENTRY:
            _interactive and log(_('Activating periodic promiscuity check'))
            cron.replace_line_matching(CRON_REGEX, CRON_ENTRY, 1)
    else:
        if val:
            _interactive and log(_('Disabling periodic promiscuity check'))
            cron.remove_line_matching('[^#]+/usr/share/msec/promisc_check.sh')

def enable_security_check(arg):
    '''   Activate/Disable daily security check.'''
    cron = ConfigFile.get_config_file(CRON)
    cron.remove_line_matching('[^#]+/usr/share/msec/security.sh')

    securitycron = ConfigFile.get_config_file(SECURITYCRON)

    val = securitycron.exists()
    
    # don't lower security when not changing security level
    if _same_level:
        if val:
            return
        
    if arg:
        if not val:
            _interactive and log(_('Activating daily security check'))
            securitycron.symlink(SECURITYSH)
    else:
        if val:
            _interactive and log(_('Disabling daily security check'))
            securitycron.unlink()

ALL_REGEXP = '^ALL:ALL:DENY'
ALL_LOCAL_REGEXP = '^ALL:ALL EXCEPT 127\.0\.0\.1:DENY'
def authorize_services(arg):
    '''  Authorize all services controlled by tcp_wrappers (see hosts.deny(5)) if \\fIarg\\fP = ALL. Only local ones
if \\fIarg\\fP = LOCAL and none if \\fIarg\\fP = NONE. To authorize the services you need, use /etc/hosts.allow
(see hosts.allow(5)).'''
    hostsdeny = ConfigFile.get_config_file(HOSTSDENY)
    
    if hostsdeny.exists():
        if hostsdeny.get_match(ALL_REGEXP):
            val = NONE
        elif hostsdeny.get_match(ALL_LOCAL_REGEXP):
            val = LOCAL
        else:
            val = ALL
    else:
        val = ALL
        
    # don't lower security when not changing security level
    if _same_level:
        if val == NONE or (val == LOCAL and arg == ALL):
            return
        
    if arg == ALL:
        if arg != val:
            _interactive and log(_('Authorizing all services'))
            hostsdeny.remove_line_matching(ALL_REGEXP, 1)
            hostsdeny.remove_line_matching(ALL_LOCAL_REGEXP, 1)
    elif arg == NONE:
        if arg != val:
            _interactive and log(_('Disabling all services'))
            hostsdeny.remove_line_matching('^ALL:ALL EXCEPT 127\.0\.0\.1:DENY', 1)
            hostsdeny.replace_line_matching('^ALL:ALL:DENY', 'ALL:ALL:DENY', 1)
    elif arg == LOCAL:
        if arg != val:
            _interactive and log(_('Disabling non local services'))
            hostsdeny.remove_line_matching(ALL_REGEXP, 1)
            hostsdeny.replace_line_matching(ALL_LOCAL_REGEXP, 'ALL:ALL EXCEPT 127.0.0.1:DENY', 1)
    else:
        error(_('authorize_services invalid argument: %s') % arg)

# helper function for enable_ip_spoofing_protection, accept_icmp_echo, accept_broadcasted_icmp_echo,
# accept_bogus_error_responses and enable_log_strange_packets.
def set_zero_one_variable(file, variable, value, secure_value, one_msg, zero_msg):
    'D'
    f = ConfigFile.get_config_file(file)

    if f.exists():
        val = int(f.get_shell_variable(variable))
    else:
        val = None
        
    # don't lower security when not changing security level
    if _same_level:
        if val == secure_value:
            return
    
    if value != val:
        if value:
            msg = _(one_msg)
        else:
            msg = _(zero_msg)
        
        _interactive and log(msg)
        f.set_shell_variable(variable, value)

# the alert argument is kept for backward compatibility
def enable_ip_spoofing_protection(arg, alert=1):
    '''  Enable/Disable IP spoofing protection.'''
    set_zero_one_variable(SYSCTLCONF, 'net.ipv4.conf.all.rp_filter', arg, 1, 'Enabling ip spoofing protection', 'Disabling ip spoofing protection')

def enable_dns_spoofing_protection(arg, alert=1):
    ''' Enable/Disable name resolution spoofing protection.  If
\\fIalert\\fP is true, also reports to syslog.'''
    hostconf = ConfigFile.get_config_file(HOSTCONF)

    val = hostconf.exists() and hostconf.get_match('nospoof\s+on')
    
    # don't lower security when not changing security level
    if _same_level:
        if val:
            return
        
    if arg:
        if not val:
            _interactive and log(_('Enabling name resolution spoofing protection'))
            hostconf.replace_line_matching('nospoof', 'nospoof on', 1)
            hostconf.replace_line_matching('spoofalert', 'spoofalert on', (alert != 0))
    else:
        if val:
            _interactive and log(_('Disabling name resolution spoofing protection'))
            hostconf.remove_line_matching('nospoof')
            hostconf.remove_line_matching('spoofalert')

def accept_icmp_echo(arg):
    '''   Accept/Refuse icmp echo.'''
    set_zero_one_variable(SYSCTLCONF, 'net.ipv4.icmp_echo_ignore_all', not arg, 1, 'Accepting icmp echo', 'Ignoring icmp echo')
    
def accept_broadcasted_icmp_echo(arg):
    '''   Accept/Refuse broadcasted icmp echo.'''
    set_zero_one_variable(SYSCTLCONF, 'net.ipv4.icmp_echo_ignore_broadcasts', not arg, 1, 'Accepting broadcasted icmp echo', 'Ignoring broadcasted icmp echo')
    
def accept_bogus_error_responses(arg):
    '''  Accept/Refuse bogus IPv4 error messages.'''
    set_zero_one_variable(SYSCTLCONF, 'net.ipv4.icmp_ignore_bogus_error_responses', not arg, 1, 'Accepting bogus icmp error responses', 'Ignoring bogus icmp error responses')
    
def enable_log_strange_packets(arg):
    '''  Enable/Disable the logging of IPv4 strange packets.'''
    set_zero_one_variable(SYSCTLCONF, 'net.ipv4.conf.all.log_martians', arg, 1, 'Enabling logging of strange packets', 'Disabling logging of strange packets')

def enable_libsafe(arg):
    '''  Enable/Disable libsafe if libsafe is found on the system.'''

    ldsopreload = ConfigFile.get_config_file(LDSOPRELOAD)

    val = ldsopreload.exists() and ldsopreload.get_match('/lib/libsafe.so.2')

    # don't lower security when not changing security level
    if _same_level:
        if val:
            return
    
    if arg:
        if not val:
            if os.path.exists(Config.get_config('root', '') + '/lib/libsafe.so.2'):
                _interactive and log(_('Enabling libsafe'))
                ldsopreload.replace_line_matching('[^#]*libsafe', '/lib/libsafe.so.2', 1)
    else:
        if val:
            _interactive and log(_('Disabling libsafe'))
            ldsopreload.remove_line_matching('[^#]*libsafe')        

LENGTH_REGEXP = '^(password\s+required\s+/lib/security/pam_cracklib.so.*?)\sminlen=([0-9]+)\s(.*)'
NDIGITS_REGEXP = '^(password\s+required\s+/lib/security/pam_cracklib.so.*?)\sdcredit=([0-9]+)\s(.*)'
UCREDIT_REGEXP = '^(password\s+required\s+/lib/security/pam_cracklib.so.*?)\sucredit=([0-9]+)\s(.*)'

def password_length(length, ndigits=0, nupper=0):
    '''  Set the password minimum length and minimum number of digit and minimum number of capitalized letters.'''
    
    passwd = ConfigFile.get_config_file(PASSWD)

    if passwd.exists():
        val_length  = passwd.get_match(LENGTH_REGEXP, '@2')
        if val_length:
            val_length = int(val_length)

        val_ndigits = passwd.get_match(NDIGITS_REGEXP, '@2')
        if val_ndigits:
            val_ndigits = int(val_ndigits)

        val_ucredit = passwd.get_match(UCREDIT_REGEXP, '@2')
        if val_ucredit:
            val_ucredit = int(val_ucredit)
            
    # don't lower security when not changing security level
    if _same_level:
        if val_length > length and val_ndigits > ndigits and val_ucredit > nupper:
            return
        
        if val_length > length:
            length = val_length
            
        if val_ndigits > ndigits:
            ndigits = val_ndigits

        if val_ucredit > nupper:
            nupper = val_ucredit

    if passwd.exists() and (val_length != length or val_ndigits != ndigits or val_ucredit != nupper):
        _interactive and log(_('Setting minimum password length %d') % length)
        (passwd.replace_line_matching(LENGTH_REGEXP,
                                      '@1 minlen=%s @3' % length) or \
         passwd.replace_line_matching('^password\s+required\s+/lib/security/pam_cracklib.so.*',
                                      '@0 minlen=%s ' % length))
    
        (passwd.replace_line_matching(NDIGITS_REGEXP,
                                      '@1 dcredit=%s @3' % ndigits) or \
         passwd.replace_line_matching('^password\s+required\s+/lib/security/pam_cracklib.so.*',
                                      '@0 dcredit=%s ' % ndigits))
    
        (passwd.replace_line_matching(UCREDIT_REGEXP,
                                      '@1 ucredit=%s @3' % nupper) or \
         passwd.replace_line_matching('^password\s+required\s+/lib/security/pam_cracklib.so.*',
                                      '@0 ucredit=%s ' % nupper))

PASSWORD_REGEXP = '^\s*auth\s+sufficient\s+/lib/security/pam_permit.so'
def enable_password(arg):
    '''  Use password to authenticate users.'''
    system_auth = ConfigFile.get_config_file(SYSTEM_AUTH)

    val = system_auth.exists() and system_auth.get_match(PASSWORD_REGEXP)
    
    # don't lower security when not changing security level
    if _same_level:
        if not val:
            return
        
    if arg:
        if val:
            _interactive and log(_('Using password to authenticate users'))
            system_auth.remove_line_matching(PASSWORD_REGEXP)
    else:
        if not val:
            _interactive and log(_('Don\'t use password to authenticate users'))
            system_auth.replace_line_matching(PASSWORD_REGEXP, 'auth        sufficient    /lib/security/pam_permit.so') or \
            system_auth.insert_before('auth\s+sufficient', 'auth        sufficient    /lib/security/pam_permit.so')

SULOGIN_REGEXP = '~~:S:wait:/sbin/sulogin'
def enable_sulogin(arg):
    '''   Enable/Disable sulogin(8) in single user level.'''
    inittab = ConfigFile.get_config_file(INITTAB)

    val = inittab.exists() and inittab.get_match(SULOGIN_REGEXP)
    
    # don't lower security when not changing security level
    if _same_level:
        if val:
            return
        
    if arg:
        if not val:
            _interactive and log(_('Enabling sulogin in single user runlevel'))
            inittab.replace_line_matching('[^#]+:S:', '~~:S:wait:/sbin/sulogin', 1)
    else:
        if val:
            _interactive and log(_('Disabling sulogin in single user runlevel'))
            inittab.remove_line_matching('~~:S:wait:/sbin/sulogin')

def enable_msec_cron(arg):
    '''  Enable/Disable msec hourly security check.'''
    mseccron = ConfigFile.get_config_file(MSECCRON)

    val = mseccron.exists()
    
    # don't lower security when not changing security level
    if _same_level:
        if val:
            return
        
    if arg:
        if arg != val:
            _interactive and log(_('Enabling msec periodic runs'))
            mseccron.symlink(MSECBIN)
    else:
        if arg != val:
            _interactive and log(_('Disabling msec periodic runs'))
            mseccron.unlink()

def enable_at_crontab(arg):
    '''  Enable/Disable crontab and at for users. Put allowed users in /etc/cron.allow and /etc/at.allow
(see man at(1) and crontab(1)).'''
    cronallow = ConfigFile.get_config_file(CRONALLOW)
    atallow = ConfigFile.get_config_file(ATALLOW)

    val_cronallow = cronallow.exists() and cronallow.get_match('root')
    val_atallow = atallow.exists() and atallow.get_match('root')
    
    # don't lower security when not changing security level
    if _same_level:
        if val_cronallow and val_atallow:
            return

    if arg:
        if val_cronallow or val_atallow:
            _interactive and log(_('Enabling crontab and at'))
            if not (_same_level and val_cronallow):
                cronallow.exists() and cronallow.move(SUFFIX)
            if not (_same_level and val_atallow):
                atallow.exists() and atallow.move(SUFFIX)
    else:
        if not val_cronallow or not val_atallow:
            _interactive and log(_('Disabling crontab and at'))
            cronallow.replace_line_matching('root', 'root', 1)
            atallow.replace_line_matching('root', 'root', 1)

maximum_regex = re.compile('^Maximum:\s*([0-9]+|-1)', re.MULTILINE)
inactive_regex = re.compile('^Inactive:\s*(-?[0-9]+)', re.MULTILINE)
no_aging_list = []

def no_password_aging_for(name):
    '''D Add the name as an exception to the handling of password aging by msec.'''
    no_aging_list.append(name)
    
# TODO FL Sat Dec 29 20:18:20 2001
# replace chage calls and /etc/shadow parsing by a python API to the shadow functions.
def password_aging(max, inactive=-1):
    '''  Set password aging to \\fImax\\fP days and delay to change to \\fIinactive\\fP.'''
    uid_min = 500
    _interactive and log(_('Setting password maximum aging for new user to %d') % max)
    logindefs = ConfigFile.get_config_file(LOGINDEFS)
    if logindefs.exists():
        logindefs.replace_line_matching('^\s*PASS_MAX_DAYS', 'PASS_MAX_DAYS ' + str(max), 1)
        uid_min = logindefs.get_match('^\s*UID_MIN\s+([0-9]+)', '@1')
        if uid_min:
            uid_min = int(uid_min)
    shadow = ConfigFile.get_config_file(SHADOW)
    if shadow.exists():
        _interactive and log(_('Setting password maximum aging for root and users with id greater than %d to %d and delay to %d days') % (uid_min, max, inactive))
        for line in shadow.get_lines():
            field = string.split(line, ':')
            if len(field) < 2:
                continue
            name = field[0]
            password = field[1]
            if name in no_aging_list:
                _interactive and log(_('User %s in password aging exception list') % (name,))
                continue
            try:
                entry = pwd.getpwnam(name)
            except KeyError:
                error(_('User %s in shadow but not in passwd file') % name)
                continue
            if (len(password) > 0 and password[0] != '!') and password != '*' and password != 'x' and (entry[2] >= uid_min or entry[2] == 0):
                cmd = '/usr/bin/chage -l %s' % entry[0]
                ret = commands.getstatusoutput(cmd)
                _interactive and log(_('got current maximum password aging for user %s with command \'%s\'') % (entry[0], cmd))
                if ret[0] == 0:
                    res = maximum_regex.search(ret[1])
                    res2 = inactive_regex.search(ret[1])
                    if res and res2:
                        current_max = int(res.group(1))
                        current_inactive = int(res2.group(1))
                        new_max = max
                        new_inactive = inactive
                        # don't lower security when not changing security level
                        if _same_level:
                            if current_max < max and current_inactive < inactive:
                                continue
                            if current_max < max:
                                new_max = current_max
                            if current_inactive < inactive:
                                new_inactive = current_inactive
                        if new_max != current_max or current_inactive != new_inactive:
                            cmd = '/usr/bin/chage -M %d -I %d %s' % (new_max, new_inactive, entry[0])
                            ret = commands.getstatusoutput(cmd)
                            log(_('changed maximum password aging for user %s with command %s') % (entry[0], cmd))
                            if ret[0] != 0:
                                error(ret[1])
                    else:
                        error(_('unable to parse chage output'))
                else:
                    error(_('unable to run chage: %s') % ret[1])

def set_security_conf(var, value):
    '''1 Set the variable \\fIvar\\fP to the value \\fIvalue\\fP in /var/lib/msec/security.conf.
The best way to override the default setting is to use create /etc/security/msec/security.conf
with the value you want.

The following variables are currentrly recognized by msec:

CHECK_UNOWNED if set to yes, report unowned files.

CHECK_SHADOW if set to yes, check empty password in /etc/shadow.

CHECK_SUID_MD5 if set to yes, verify checksum of the suid/sgid files.

CHECK_SECURITY if set to yes, run the daily security checks.

CHECK_PASSWD if set to yes, check for empty password, or a password while it should be in /etc/shadow or other users with id 0.

SYSLOG_WARN if set to yes, report check result to syslog.

CHECK_SUID_ROOT if set to yes, check additions/removals of suid root files.

CHECK_PERMS if set to yes, check permissions of files in the users' home.

CHKROOTKIT_CHECK if set to yes, run chkrootkit checks.

CHECK_PROMISC if set to yes, check if the network devices are in promiscuous mode.

RPM_CHECK if set to yes, run some checks against the rpm database.

TTY_WARN if set to yes, reports check result to tty.

CHECK_WRITEABLE if set to yes, check files/directories writable by everybody.

MAIL_WARN if set to yes, report check result by mail.

MAIL_USER if set, send the mail report to this email address else send it to root.

CHECK_OPEN_PORT if set to yes, check open ports.

CHECK_SUID_GROUP if set to yes, check additions/removals of sgid files.
'''
    securityconf = ConfigFile.get_config_file(SECURITYCONF)
    securityconf.set_shell_variable(var, value)

# various

def set_interactive(v):
    "D"
    
    global _interactive
    
    _interactive = v

# libmsec.py ends here