aboutsummaryrefslogtreecommitdiffstats
path: root/src/msec/libmsec.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/msec/libmsec.py')
-rwxr-xr-xsrc/msec/libmsec.py1763
1 files changed, 1763 insertions, 0 deletions
diff --git a/src/msec/libmsec.py b/src/msec/libmsec.py
new file mode 100755
index 0000000..80de3fc
--- /dev/null
+++ b/src/msec/libmsec.py
@@ -0,0 +1,1763 @@
+#!/usr/bin/python -O
+"""This is the main msec module, responsible for all msec operations.
+
+The following classes are defined here:
+
+ ConfigFile: an individual config file. This class is responsible for
+ configuration modification, variable searching and replacing,
+ and so on.
+
+ ConfigFiles: this file contains the entire set of modifications performed
+ by msec, stored in list of ConfigFile instances. When required, all
+ changes are commited back to physical files. This way, no real
+ change occurs on the system until the msec app explicitly tells
+ to do so.
+
+ Log: logging class, that supports logging to terminal, a fixed log file,
+ and syslog. A single log instance can be shared by all other
+ classes.
+
+ MSEC: main msec class. It contains the callback functions for all msec
+ operations.
+
+All configuration variables, and config file names are defined here as well.
+"""
+
+#---------------------------------------------------------------
+# Project : Mandriva Linux
+# Module : mseclib
+# File : libmsec.py
+# Version : $Id$
+# Author : Eugeni Dodonov
+# Original Author : Frederic Lepied
+# Created On : Mon Dec 10 22:52:17 2001
+# Purpose : low-level msec functions
+#---------------------------------------------------------------
+
+import os
+import grp
+import gettext
+import pwd
+import re
+import string
+import commands
+import time
+import stat
+import traceback
+import sys
+import glob
+
+# logging
+import logging
+from logging.handlers import SysLogHandler
+
+# configuration
+import config
+
+# localization
+try:
+ cat = gettext.Catalog('msec')
+ _ = cat.gettext
+except IOError:
+ _ = str
+
+# backup file suffix
+SUFFIX = '.msec'
+
+# 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'
+FSTAB = '/etc/fstab'
+GDM = '/etc/pam.d/gdm'
+GDMCONF = '/etc/X11/gdm/custom.conf'
+HALT = '/usr/bin/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'
+SHELLCONF = '/etc/sysconfig/shell'
+MSECBIN = '/usr/sbin/msec'
+MSECCRON = '/etc/cron.hourly/msec'
+MSEC_XINIT = '/etc/X11/xinit.d/msec'
+OPASSWD = '/etc/security/opasswd'
+PASSWD = '/etc/pam.d/passwd'
+POWEROFF = '/usr/bin/poweroff'
+REBOOT = '/usr/bin/reboot'
+SECURETTY = '/etc/securetty'
+SECURITYCRON = '/etc/cron.daily/msec'
+SECURITYSH = '/usr/share/msec/security.sh'
+SERVER = '/etc/security/msec/server'
+SHADOW = '/etc/shadow'
+SHUTDOWN = '/usr/bin/shutdown'
+SHUTDOWNALLOW = '/etc/shutdown.allow'
+SIMPLE_ROOT_AUTHEN = '/etc/pam.d/simple_root_authen'
+SSHDCONFIG = '/etc/ssh/sshd_config'
+STARTX = '/usr/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'
+EXPORT = '/root/.xauth/export'
+
+# ConfigFile constants
+STRING_TYPE = type('')
+
+BEFORE=0
+INSIDE=1
+AFTER=2
+
+# regexps
+space = re.compile('\s')
+# X server
+STARTX_REGEXP = '(\s*serverargs=".*) -nolisten tcp(.*")'
+XSERVERS_REGEXP = '(\s*[^#]+/usr/bin/X .*) -nolisten tcp(.*)'
+GDMCONF_REGEXP = '(\s*command=.*/X.*?) -nolisten tcp(.*)$'
+KDMRC_REGEXP = re.compile('(.*?)-nolisten tcp(.*)$')
+# ctrl-alt-del
+CTRALTDEL_REGEXP = '^ca::ctrlaltdel:/sbin/shutdown.*'
+# consolehelper
+CONSOLE_HELPER = 'consolehelper'
+# ssh PermitRootLogin
+PERMIT_ROOT_LOGIN_REGEXP = '^\s*PermitRootLogin\s+(no|yes|without-password|forced-commands-only)'
+# pam
+SUCCEED_MATCH = '^auth\s+sufficient\s+pam_succeed_if.so\s+use_uid\s+user\s+ingroup\s+wheel\s*$'
+SUCCEED_LINE = 'auth sufficient pam_succeed_if.so use_uid user ingroup wheel'
+# cron
+CRON_ENTRY = '*/1 * * * * root /usr/share/msec/promisc_check.sh'
+CRON_REGEX = '[^#]+/usr/share/msec/promisc_check.sh'
+# tcp_wrappers
+ALL_REGEXP = '^ALL:ALL:DENY'
+ALL_LOCAL_REGEXP = '^ALL:ALL EXCEPT 127\.0\.0\.1:DENY'
+# password stuff
+LENGTH_REGEXP = re.compile('^(password\s+required\s+(?:/lib/security/)?pam_cracklib.so.*?)\sminlen=([0-9]+)\s(.*)')
+NDIGITS_REGEXP = re.compile('^(password\s+required\s+(?:/lib/security/)?pam_cracklib.so.*?)\sdcredit=([0-9]+)\s(.*)')
+UCREDIT_REGEXP = re.compile('^(password\s+required\s+(?:/lib/security/)?pam_cracklib.so.*?)\sucredit=([0-9]+)\s(.*)')
+PASSWORD_REGEXP = '^\s*auth\s+sufficient\s+(?:/lib/security/)?pam_permit.so'
+UNIX_REGEXP = re.compile('(^\s*password\s+sufficient\s+(?:/lib/security/)?pam_unix.so.*)\sremember=([0-9]+)(.*)')
+PAM_TCB_REGEXP = re.compile('(^\s*password\s+sufficient\s+(?:/lib/security/)?pam_tcb.so.*)')
+# sulogin
+SULOGIN_REGEXP = '~~:S:wait:/sbin/sulogin'
+
+# {{{ helper functions
+def move(old, new):
+ """Renames files, deleting existent ones when necessary."""
+ try:
+ os.unlink(new)
+ except OSError:
+ pass
+ try:
+ os.rename(old, new)
+ except:
+ error('rename %s %s: %s' % (old, new, str(sys.exc_value)))
+
+def substitute_re_result(res, s):
+ for idx in range(0, (res.lastindex or 0) + 1):
+ subst = res.group(idx) or ''
+ s = string.replace(s, '@' + str(idx), subst)
+ return s
+# }}}
+
+# {{{ Log
+class Log:
+ """Logging class. Logs to both syslog and log file"""
+ def __init__(self,
+ app_name="msec",
+ log_syslog=True,
+ log_file=True,
+ log_level = logging.INFO,
+ log_facility=SysLogHandler.LOG_AUTHPRIV,
+ syslog_address="/dev/log",
+ log_path="/var/log/msec.log",
+ interactive=True):
+ self.log_facility = log_facility
+ self.log_path = log_path
+
+ # buffer
+ self.buffer = None
+
+ # common logging stuff
+ self.logger = logging.getLogger(app_name)
+
+ # syslog
+ if log_syslog:
+ self.syslog_h = SysLogHandler(facility=log_facility, address=syslog_address)
+ formatter = logging.Formatter('%(name)s: %(levelname)s: %(message)s')
+ self.syslog_h.setFormatter(formatter)
+ self.logger.addHandler(self.syslog_h)
+
+ # log to file
+ if log_file:
+ self.file_h = logging.FileHandler(self.log_path)
+ formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s')
+ self.file_h.setFormatter(formatter)
+ self.logger.addHandler(self.file_h)
+
+ # interactive logging
+ if interactive:
+ self.interactive_h = logging.StreamHandler(sys.stderr)
+ formatter = logging.Formatter('%(levelname)s: %(message)s')
+ self.interactive_h.setFormatter(formatter)
+ self.logger.addHandler(self.interactive_h)
+
+ self.logger.setLevel(log_level)
+
+ def info(self, message):
+ """Informative message (normal msec operation)"""
+ if self.buffer:
+ self.buffer["info"].append(message)
+ else:
+ self.logger.info(message)
+
+ def error(self, message):
+ """Error message (security has changed: authentication, passwords, etc)"""
+ if self.buffer:
+ self.buffer["error"].append(message)
+ else:
+ self.logger.error(message)
+
+ def debug(self, message):
+ """Debugging message"""
+ if self.buffer:
+ self.buffer["debug"].append(message)
+ else:
+ self.logger.debug(message)
+
+ def critical(self, message):
+ """Critical message (big security risk, e.g., rootkit, etc)"""
+ if self.buffer:
+ self.buffer["critical"].append(message)
+ else:
+ self.logger.critical(message)
+
+ def warn(self, message):
+ """Warning message (slight security change, permissions change, etc)"""
+ if self.buffer:
+ self.buffer["warn"].append(message)
+ else:
+ self.logger.warn(message)
+
+ def start_buffer(self):
+ """Beginns message buffering"""
+ self.buffer = {"info": [], "error": [], "debug": [], "critical": [], "warn": []}
+
+ def get_buffer(self):
+ """Returns buffered messages"""
+ messages = self.buffer.copy()
+ del self.buffer
+ self.buffer = None
+ return messages
+
+# }}}
+
+# {{{ ConfigFiles - stores references to all configuration files
+class ConfigFiles:
+ """This class is responsible to store references to all configuration files,
+ mark them as changed, and update on disk when necessary"""
+ def __init__(self, log):
+ """Initializes list of ConfigFiles"""
+ self.files = {}
+ self.modified_files = []
+ self.action_assoc = []
+ self.log = log
+
+ def add(self, file, path):
+ """Appends a path to list of files"""
+ self.files[path] = file
+
+ def modified(self, path):
+ """Marks a file as modified"""
+ if not path in self.modified_files:
+ self.modified_files.append(path)
+
+ def get_config_file(self, path, suffix=None):
+ """Retreives corresponding config file"""
+ try:
+ return self.files[path]
+ except KeyError:
+ return ConfigFile(path, self, self.log, suffix=suffix)
+
+ def add_config_assoc(self, regex, action):
+ """Adds association between a file and an action"""
+ self.log.debug("Adding custom command '%s' for '%s'" % (action, regex))
+ self.action_assoc.append((re.compile(regex), action))
+
+ def write_files(self, commit=True):
+ """Writes all files back to disk"""
+ for f in self.files.values():
+ self.log.debug("Attempting to write %s" % f.path)
+ if commit:
+ f.write()
+
+ if len(self.modified_files) > 0:
+ self.log.info("%s: %s" % (config.MODIFICATIONS_FOUND, " ".join(self.modified_files)))
+ else:
+ self.log.info(config.MODIFICATIONS_NOT_FOUND)
+
+ for f in self.modified_files:
+ for a in self.action_assoc:
+ res = a[0].search(f)
+ if res:
+ s = substitute_re_result(res, a[1])
+ if commit:
+ self.log.info(_('%s modified so launched command: %s') % (f, s))
+ cmd = commands.getstatusoutput(s)
+ cmd = [0, '']
+ if cmd[0] == 0:
+ if cmd[1]:
+ self.log.info(cmd[1])
+ else:
+ self.log.error(cmd[1])
+ else:
+ self.log.info(_('%s modified so should have run command: %s') % (f, s))
+
+# }}}
+
+# {{{ ConfigFile - an individual config file
+class ConfigFile:
+ """This class represents an individual config file.
+ All config files are stored in meta (which is ConfigFiles).
+ All operations are performed in memory, and written when required"""
+ def __init__(self, path, meta, log, root='', suffix=None):
+ """Initializes a config file, and put reference to meta (ConfigFiles)"""
+ self.meta=meta
+ self.path = root + path
+ self.is_modified = 0
+ self.is_touched = 0
+ self.is_deleted = 0
+ self.is_moved = 0
+ self.suffix = suffix
+ self.lines = None
+ self.sym_link = None
+ self.log = log
+ self.meta.add(self, path)
+
+ def get_lines(self):
+ if self.lines == None:
+ file=None
+ try:
+ file = open(self.path, 'r')
+ except IOError:
+ if self.suffix:
+ try:
+ moved = self.path + self.suffix
+ file = open(moved, 'r')
+ move(moved, self.path)
+ self.meta.modified(self.path)
+ except IOError:
+ self.lines = []
+ else:
+ self.lines = []
+ if file:
+ self.lines = string.split(file.read(), "\n")
+ file.close()
+ return self.lines
+
+ def append(self, value):
+ lines = self.lines
+ l = len(lines)
+ if l > 0 and lines[l - 1] == '':
+ lines.insert(l - 1, value)
+ else:
+ lines.append(value)
+ lines.append('')
+
+ def modified(self):
+ self.is_modified = 1
+ self.meta.modified(self.path)
+ return self
+
+ def touch(self):
+ self.is_touched = 1
+ return self
+
+ def symlink(self, link):
+ self.sym_link = link
+ return self
+
+ def exists(self):
+ return os.path.lexists(self.path)
+ #return os.path.exists(self.path) or (self.suffix and os.path.exists(self.path + self.suffix))
+
+ def realpath(self):
+ return os.path.realpath(self.path)
+
+ def move(self, suffix):
+ self.suffix = suffix
+ self.is_moved = 1
+
+ def unlink(self):
+ self.is_deleted = 1
+ self.lines=[]
+ return self
+
+ def write(self):
+ if self.is_deleted:
+ if self.exists():
+ try:
+ os.unlink(self.path)
+ except:
+ error('unlink %s: %s' % (self.path, str(sys.exc_value)))
+ self.log.info(_('deleted %s') % (self.path,))
+ elif self.is_modified:
+ content = string.join(self.lines, "\n")
+ dirname = os.path.dirname(self.path)
+ if not os.path.exists(dirname):
+ os.makedirs(dirname)
+ file = open(self.path, 'w')
+ file.write(content)
+ file.close()
+ self.meta.modified(self.path)
+ elif self.is_touched:
+ if os.path.exists(self.path):
+ try:
+ os.utime(self.path, None)
+ except:
+ self.log.error('utime %s: %s' % (self.path, str(sys.exc_value)))
+ elif self.suffix and os.path.exists(self.path + self.suffix):
+ move(self.path + self.suffix, self.path)
+ try:
+ os.utime(self.path, None)
+ except:
+ self.log.error('utime %s: %s' % (self.path, str(sys.exc_value)))
+ else:
+ self.lines = []
+ self.is_modified = 1
+ file = open(self.path, 'w')
+ file.close()
+ self.log.info(_('touched file %s') % (self.path,))
+ elif self.sym_link:
+ done = 0
+ if self.exists():
+ full = os.lstat(self.path)
+ if stat.S_ISLNK(full[stat.ST_MODE]):
+ link = os.readlink(self.path)
+ # to be fixed: resolv relative symlink
+ done = (link == self.sym_link)
+ if not done:
+ try:
+ os.unlink(self.path)
+ except:
+ self.log.error('unlink %s: %s' % (self.path, str(sys.exc_value)))
+ self.log.info(_('deleted %s') % (self.path,))
+ if not done:
+ try:
+ os.symlink(self.sym_link, self.path)
+ except:
+ self.log.error('symlink %s %s: %s' % (self.sym_link, self.path, str(sys.exc_value)))
+ self.log.info(_('made symbolic link from %s to %s') % (self.sym_link, self.path))
+
+ if self.is_moved:
+ move(self.path, self.path + self.suffix)
+ self.log.info(_('moved file %s to %s') % (self.path, self.path + self.suffix))
+ self.meta.modified(self.path)
+ self.is_touched = 0
+ self.is_modified = 0
+ self.is_deleted = 0
+ self.is_moved = 0
+
+ def set_shell_variable(self, var, value, start=None, end=None):
+ regex = re.compile('^' + var + '="?([^#"]+)"?(.*)')
+ lines = self.get_lines()
+ idx=0
+ value=str(value)
+ start_regexp = start
+
+ if start:
+ status = BEFORE
+ start = re.compile(start)
+ else:
+ status = INSIDE
+
+ if end:
+ end = re.compile(end)
+
+ idx = None
+ for idx in range(0, len(lines)):
+ line = lines[idx]
+ if status == BEFORE:
+ if start.search(line):
+ status = INSIDE
+ else:
+ continue
+ elif end and end.search(line):
+ break
+ res = regex.search(line)
+ if res:
+ if res.group(1) != value:
+ if space.search(value):
+ lines[idx] = var + '="' + value + '"' + res.group(2)
+ else:
+ lines[idx] = var + '=' + value + res.group(2)
+ self.modified()
+ self.log.debug(_('set variable %s to %s in %s') % (var, value, self.path,))
+ return self
+ if status == BEFORE:
+ # never found the start delimiter
+ self.log.warning(_('WARNING: never found regexp %s in %s, not writing changes') % (start_regexp, self.path))
+ return self
+ if space.search(value):
+ s = var + '="' + value + '"'
+ else:
+ s = var + '=' + value
+ if idx == None or idx == len(lines):
+ self.append(s)
+ else:
+ lines.insert(idx, s)
+
+ self.modified()
+ self.log.info(_('set variable %s to %s in %s') % (var, value, self.path,))
+ return self
+
+ def get_shell_variable(self, var, start=None, end=None):
+ # if file does not exists, fail quickly
+ if not self.exists():
+ return None
+ if end:
+ end=re.compile(end)
+ if start:
+ start=re.compile(start)
+ regex = re.compile('^' + var + '="?([^#"]+)"?(.*)')
+ lines = self.get_lines()
+ llen = len(lines)
+ start_idx = 0
+ end_idx = llen
+ if start:
+ found = 0
+ for idx in range(0, llen):
+ if start.search(lines[idx]):
+ start_idx = idx
+ found = 1
+ break
+ if found:
+ for idx in range(start_idx, llen):
+ if end.search(lines[idx]):
+ end_idx = idx
+ break
+ else:
+ start_idx = 0
+ for idx in range(end_idx - 1, start_idx - 1, -1):
+ res = regex.search(lines[idx])
+ if res:
+ return res.group(1)
+ return None
+
+ def get_match(self, regex, replace=None):
+ # if file does not exists, fail quickly
+ if not self.exists():
+ return None
+ r=re.compile(regex)
+ lines = self.get_lines()
+ for idx in range(0, len(lines)):
+ res = r.search(lines[idx])
+ if res:
+ if replace:
+ s = substitute_re_result(res, replace)
+ return s
+ else:
+ return lines[idx]
+ return None
+
+ def replace_line_matching(self, regex, value, at_end_if_not_found=0, all=0, start=None, end=None):
+ # if at_end_if_not_found is a string its value will be used as the string to inster
+ r=re.compile(regex)
+ lines = self.get_lines()
+ matches = 0
+
+ if start:
+ status = BEFORE
+ start = re.compile(start)
+ else:
+ status = INSIDE
+
+ if end:
+ end = re.compile(end)
+
+ idx = None
+ for idx in range(0, len(lines)):
+ line = lines[idx]
+ if status == BEFORE:
+ if start.search(line):
+ status = INSIDE
+ else:
+ continue
+ elif end and end.search(line):
+ break
+ res = r.search(line)
+ if res:
+ s = substitute_re_result(res, value)
+ matches = matches + 1
+ if s != line:
+ self.log.debug(_("replaced in %s the line %d:\n%s\nwith the line:\n%s") % (self.path, idx, line, s))
+ lines[idx] = s
+ self.modified()
+ if not all:
+ return matches
+ if matches == 0 and at_end_if_not_found:
+ if type(at_end_if_not_found) == STRING_TYPE:
+ value = at_end_if_not_found
+ self.log.debug(_("appended in %s the line:\n%s") % (self.path, value))
+ if idx == None or idx == len(lines):
+ self.append(value)
+ else:
+ lines.insert(idx, value)
+ self.modified()
+ matches = matches + 1
+ return matches
+
+ def insert_after(self, regex, value, at_end_if_not_found=0, all=0):
+ matches = 0
+ r=re.compile(regex)
+ lines = self.get_lines()
+ for idx in range(0, len(lines)):
+ res = r.search(lines[idx])
+ if res:
+ s = substitute_re_result(res, value)
+ self.log.debug(_("inserted in %s after the line %d:\n%s\nthe line:\n%s") % (self.path, idx, lines[idx], s))
+ lines.insert(idx+1, s)
+ self.modified()
+ matches = matches + 1
+ if not all:
+ return matches
+ if matches == 0 and at_end_if_not_found:
+ self.log.debug(_("appended in %s the line:\n%s") % (self.path, value))
+ self.append(value)
+ self.modified()
+ matches = matches + 1
+ return matches
+
+ def insert_before(self, regex, value, at_top_if_not_found=0, all=0):
+ matches = 0
+ r=re.compile(regex)
+ lines = self.get_lines()
+ for idx in range(0, len(lines)):
+ res = r.search(lines[idx])
+ if res:
+ s = substitute_re_result(res, value)
+ self.log.debug(_("inserted in %s before the line %d:\n%s\nthe line:\n%s") % (self.path, idx, lines[idx], s))
+ lines.insert(idx, s)
+ self.modified()
+ matches = matches + 1
+ if not all:
+ return matches
+ if matches == 0 and at_top_if_not_found:
+ self.log.debug(_("inserted at the top of %s the line:\n%s") % (self.path, value))
+ lines.insert(0, value)
+ self.modified()
+ matches = matches + 1
+ return matches
+
+ def insert_at(self, idx, value):
+ lines = self.get_lines()
+ try:
+ lines.insert(idx, value)
+ self.log.debug(_("inserted in %s at the line %d:\n%s") % (self.path, idx, value))
+ self.modified()
+ return 1
+ except KeyError:
+ return 0
+
+ def remove_line_matching(self, regex, all=0):
+ matches = 0
+ r=re.compile(regex)
+ lines = self.get_lines()
+ for idx in range(len(lines) - 1, -1, -1):
+ res = r.search(lines[idx])
+ if res:
+ self.log.debug(_("removing in %s the line %d:\n%s") % (self.path, idx, lines[idx]))
+ lines.pop(idx)
+ self.modified()
+ matches = matches + 1
+ if not all:
+ return matches
+ return matches
+# }}}
+
+# {{{ MSEC - main class
+class MSEC:
+ """Main msec class. Contains all functions and performs the actions"""
+ def __init__(self, log):
+ """Initializes config files and associations"""
+ # all config files
+ self.log = log
+ self.configfiles = ConfigFiles(log)
+
+ # associate helper commands with files
+ self.configfiles.add_config_assoc(INITTAB, '/sbin/telinit q')
+ self.configfiles.add_config_assoc('/etc(?:/rc.d)?/init.d/(.+)', '[ -f /var/lock/subsys/@1 ] && @0 reload')
+ self.configfiles.add_config_assoc(SYSCTLCONF, '/sbin/sysctl -e -p /etc/sysctl.conf')
+ self.configfiles.add_config_assoc(SSHDCONFIG, '[ -f /var/lock/subsys/sshd ] && /etc/rc.d/init.d/sshd restart')
+ self.configfiles.add_config_assoc(LILOCONF, '[ `/usr/sbin/detectloader` = LILO ] && /sbin/lilo')
+ self.configfiles.add_config_assoc(SYSLOGCONF, '[ -f /var/lock/subsys/syslog ] && service syslog reload')
+ self.configfiles.add_config_assoc('^/etc/issue$', '/usr/bin/killall mingetty')
+
+ # TODO: add a common function to check parameters
+
+ def reset(self):
+ """Resets the configuration"""
+ self.log.debug("Resetting msec data.")
+ self.configfiles = ConfigFiles(self.log)
+
+ def get_action(self, name):
+ """Determines correspondent function for requested action."""
+ try:
+ func = getattr(self, name)
+ return func
+ except:
+ return None
+
+ def commit(self, really_commit=True):
+ """Commits changes"""
+ if not really_commit:
+ self.log.info(_("In check-only mode, nothing is written back to disk."))
+ self.configfiles.write_files(really_commit)
+
+ def apply(self, curconfig):
+ '''Applies configuration from a MsecConfig instance'''
+ # first, reset previous msec data
+ self.reset()
+ # process all options
+ for opt in curconfig.list_options():
+ # Determines correspondent function
+ action = None
+ callback = config.find_callback(opt)
+ valid_params = config.find_valid_params(opt)
+ if callback:
+ action = self.get_action(callback)
+ if not action:
+ # The required functionality is not supported
+ self.log.info(_("'%s' is not available in this version") % opt)
+ continue
+ self.log.debug("Processing action %s: %s(%s)" % (opt, callback, curconfig.get(opt)))
+ # validating parameters
+ param = curconfig.get(opt)
+ if param not in valid_params and '*' not in valid_params:
+ self.log.error(_("Invalid parameter for %s: '%s'. Valid parameters: '%s'.") % (opt,
+ param,
+ valid_values[opt]))
+ continue
+ action(curconfig.get(opt))
+
+ def create_server_link(self, param):
+ ''' 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.'''
+ __params__ = ["no", "default", "secure"]
+
+ server = self.configfiles.get_config_file(SERVER)
+
+ if param == "no":
+ if server.exists():
+ self.log.info(_('Allowing unrestricted chkconfig for packages'))
+ server.unlink()
+ else:
+ newpath = "%s.%s" % (SERVER, param)
+ if server.realpath() != newpath:
+ self.log.info(_('Restricting chkconfig for packages according to "%s" profile') % param)
+ server.symlink(newpath)
+
+ def set_root_umask(self, umask):
+ ''' Set the root umask.'''
+ msec = self.configfiles.get_config_file(SHELLCONF)
+
+ val = msec.get_shell_variable('UMASK_ROOT')
+
+ if val != umask:
+ self.log.info(_('Setting root umask to %s') % (umask))
+ msec.set_shell_variable('UMASK_ROOT', umask)
+
+ def set_user_umask(self, umask):
+ ''' Set the user umask.'''
+ msec = self.configfiles.get_config_file(SHELLCONF)
+
+ val = msec.get_shell_variable('UMASK_USER')
+
+ if val != umask:
+ self.log.info(_('Setting users umask to %s') % (umask))
+ msec.set_shell_variable('UMASK_USER', umask)
+
+ def allow_x_connections(self, arg):
+ ''' Allow/Forbid X connections. Accepted arguments: yes (all connections are allowed), local (only local connection), no (no connection).'''
+
+ xinit = self.configfiles.get_config_file(MSEC_XINIT)
+ val = xinit.get_match('/usr/bin/xhost\s*(\+\s*[^#]*)', '@1')
+
+ if val:
+ if val == '+':
+ val = "yes"
+ elif val == "+ localhost":
+ val = "local"
+ else:
+ val = "no"
+ else:
+ val = "no"
+
+ if val != arg:
+ if arg == "yes":
+ self.log.info(_('Allowing users to connect X server from everywhere'))
+ xinit.replace_line_matching('/usr/bin/xhost', '/usr/bin/xhost +', 1)
+ elif arg == "local":
+ self.log.info(_('Allowing users to connect X server from localhost'))
+ xinit.replace_line_matching('/usr/bin/xhost', '/usr/bin/xhost + localhost', 1)
+ elif arg == "no":
+ self.log.info(_('Restricting X server connection to the console user'))
+ xinit.remove_line_matching('/usr/bin/xhost', 1)
+ else:
+ self.log.error(_('invalid allow_x_connections arg: %s') % arg)
+
+ def allow_xserver_to_listen(self, arg):
+ ''' The argument specifies if clients are authorized to connect to the X server on the tcp port 6000 or not.'''
+
+ startx = self.configfiles.get_config_file(STARTX)
+ xservers = self.configfiles.get_config_file(XSERVERS)
+ gdmconf = self.configfiles.get_config_file(GDMCONF)
+ kdmrc = self.configfiles.get_config_file(KDMRC)
+
+ val_startx = startx.get_match(STARTX_REGEXP)
+ val_xservers = xservers.get_match(XSERVERS_REGEXP)
+ val_gdmconf = gdmconf.get_shell_variable('DisallowTCP')
+ str = kdmrc.get_shell_variable('ServerArgsLocal', 'X-\*-Core', '^\s*$')
+ if str:
+ val_kdmrc = KDMRC_REGEXP.search(str)
+ else:
+ val_kdmrc = None
+
+ # TODO: better check for file existance
+
+ if arg == "yes":
+ if val_startx or val_xservers or val_kdmrc or val_gdmconf != 'false':
+ self.log.info(_('Allowing the X server to listen to tcp connections'))
+ if startx.exists():
+ startx.replace_line_matching(STARTX_REGEXP, '@1@2')
+ if xservers.exists():
+ xservers.replace_line_matching(XSERVERS_REGEXP, '@1@2', 0, 1)
+ if gdmconf.exists():
+ gdmconf.set_shell_variable('DisallowTCP', 'false', '\[security\]', '^\s*$')
+ if kdmrc.exists():
+ kdmrc.replace_line_matching('^(ServerArgsLocal=.*?)-nolisten tcp(.*)$', '@1@2', 0, 0, 'X-\*-Core', '^\s*$')
+ else:
+ if not val_startx or not val_xservers or not val_kdmrc or val_gdmconf != 'true':
+ self.log.info(_('Forbidding the X server to listen to tcp connection'))
+ if not val_startx:
+ startx.exists() and startx.replace_line_matching('serverargs="(.*?)( -nolisten tcp)?"', 'serverargs="@1 -nolisten tcp"')
+ if not val_xservers:
+ xservers.exists() and xservers.replace_line_matching('(\s*[^#]+/usr/bin/X .*?)( -nolisten tcp)?$', '@1 -nolisten tcp', 0, 1)
+ if val_gdmconf != 'true':
+ gdmconf.exists() and gdmconf.set_shell_variable('DisallowTCP', 'true', '\[security\]', '^\s*$')
+ if val_kdmrc:
+ if not val_kdmrc.get_match('^ServerArgsLocal=.* -nolisten tcp'):
+ kdmrc.exists() and kdmrc.replace_line_matching('^(ServerArgsLocal=.*)$', '@1 -nolisten tcp', 'ServerArgsLocal=-nolisten tcp', 0, 'X-\*-Core', '^\s*$')
+
+ def set_shell_timeout(self, val):
+ ''' Set the shell timeout. A value of zero means no timeout.'''
+ msec = self.configfiles.get_config_file(SHELLCONF)
+ try:
+ timeout = int(val)
+ except:
+ self.log.error(_('Invalid shell timeout "%s"') % size)
+ return
+
+ old = msec.get_shell_variable('TMOUT')
+ if old:
+ old = int(old)
+
+ if old != timeout:
+ self.log.info(_('Setting shell timeout to %s') % timeout)
+ msec.set_shell_variable('TMOUT', timeout)
+
+ def set_shell_history_size(self, size):
+ ''' Set shell commands history size. A value of -1 means unlimited.'''
+ try:
+ size = int(size)
+ except:
+ self.log.error(_('Invalid shell history size "%s"') % size)
+ return
+
+ msec = self.configfiles.get_config_file(SHELLCONF)
+
+ val = msec.get_shell_variable('HISTFILESIZE')
+ if val:
+ val = int(val)
+
+ if size >= 0:
+ if val != size:
+ self.log.info(_('Setting shell history size to %s') % size)
+ msec.set_shell_variable('HISTFILESIZE', size)
+ else:
+ if val != None:
+ self.log.info(_('Removing limit on shell history size'))
+ msec.remove_line_matching('^HISTFILESIZE=')
+
+ def set_win_parts_umask(self, umask):
+ ''' Set umask option for mounting vfat and ntfs partitions. A value of None means default umask.'''
+ fstab = self.configfiles.get_config_file(FSTAB)
+
+ if umask == "no":
+ fstab.replace_line_matching("(.*\s(vfat|ntfs)\s+)umask=\d+(\s.*)", "@1defaults@3", 0, 1)
+ fstab.replace_line_matching("(.*\s(vfat|ntfs)\s+)umask=\d+,(.*)", "@1@3", 0, 1)
+ fstab.replace_line_matching("(.*\s(vfat|ntfs)\s+\S+),umask=\d+(.*)", "@1@3", 0, 1)
+ else:
+ fstab.replace_line_matching("(.*\s(vfat|ntfs)\s+\S*)umask=\d+(.*)", "@1umask=0@3", 0, 1)
+ fstab.replace_line_matching("(.*\s(vfat|ntfs)\s+)(?!.*umask=)(\S+)(.*)", "@1@3,umask=0@4", 0, 1)
+
+ def allow_reboot(self, arg):
+ ''' Allow/Forbid system reboot and shutdown to local users.'''
+ shutdownallow = self.configfiles.get_config_file(SHUTDOWNALLOW)
+ sysctlconf = self.configfiles.get_config_file(SYSCTLCONF)
+ kdmrc = self.configfiles.get_config_file(KDMRC)
+ gdmconf = self.configfiles.get_config_file(GDMCONF)
+ inittab = self.configfiles.get_config_file(INITTAB)
+ shutdown = self.configfiles.get_config_file(SHUTDOWN)
+ poweroff = self.configfiles.get_config_file(POWEROFF)
+ reboot = self.configfiles.get_config_file(REBOOT)
+ halt = self.configfiles.get_config_file(HALT)
+
+ val_shutdownallow = shutdownallow.exists()
+ val_shutdown = shutdown.exists()
+ val_poweroff = poweroff.exists()
+ val_reboot = reboot.exists()
+ val_halt = halt.exists()
+ val_sysctlconf = sysctlconf.get_shell_variable('kernel.sysrq')
+ val_inittab = inittab.get_match(CTRALTDEL_REGEXP)
+ val_gdmconf = gdmconf.get_shell_variable('SystemMenu')
+ oldval_kdmrc = kdmrc.get_shell_variable('AllowShutdown', 'X-:\*-Core', '^\s*$')
+
+ if arg == "yes":
+ if val_shutdownallow or not val_shutdown or not val_poweroff or not val_reboot or not val_halt:
+ self.log.info(_('Allowing reboot and shutdown to the console user'))
+ shutdownallow.exists() and shutdownallow.move(SUFFIX)
+ shutdown.exists() or shutdown.symlink(CONSOLE_HELPER)
+ poweroff.exists() or poweroff.symlink(CONSOLE_HELPER)
+ reboot.exists() or reboot.symlink(CONSOLE_HELPER)
+ halt.exists() or halt.symlink(CONSOLE_HELPER)
+ if val_sysctlconf == '0':
+ self.log.info(_('Allowing SysRq key to the console user'))
+ sysctlconf.set_shell_variable('kernel.sysrq', 1)
+ if val_gdmconf == 'false':
+ self.log.info(_('Allowing Shutdown/Reboot in GDM'))
+ gdmconf.exists() and gdmconf.set_shell_variable('SystemMenu', 'true', '\[greeter\]', '^\s*$')
+ if kdmrc.exists():
+ if oldval_kdmrc != 'All':
+ self.log.info(_('Allowing Shutdown/Reboot in KDM'))
+ kdmrc.set_shell_variable('AllowShutdown', 'All', 'X-:\*-Core', '^\s*$')
+ if not val_inittab:
+ self.log.info(_('Allowing Ctrl-Alt-Del from console'))
+ inittab.replace_line_matching(CTRALTDEL_REGEXP, 'ca::ctrlaltdel:/sbin/shutdown -t3 -r now', 1)
+ else:
+ if not val_shutdownallow or val_shutdown or val_poweroff or val_reboot or val_halt:
+ self.log.info(_('Forbidding reboot and shutdown to the console user'))
+ if not shutdownallow.exists():
+ self.configfiles.get_config_file(SHUTDOWNALLOW, SUFFIX).touch()
+ shutdown.exists() and shutdown.unlink()
+ poweroff.exists() and poweroff.unlink()
+ reboot.exists() and reboot.unlink()
+ halt.exists() and halt.unlink()
+ if val_sysctlconf != '0':
+ self.log.info(_('Forbidding SysRq key to the console user'))
+ sysctlconf.set_shell_variable('kernel.sysrq', 0)
+ if val_gdmconf != 'false':
+ self.log.info(_('Forbidding Shutdown/Reboot in GDM'))
+ gdmconf.exists() and gdmconf.set_shell_variable('SystemMenu', 'false', '\[greeter\]', '^\s*$')
+ if kdmrc.exists():
+ if oldval_kdmrc != 'None':
+ self.log.info(_('Forbidding Shutdown/Reboot in KDM'))
+ kdmrc.set_shell_variable('AllowShutdown', 'None', 'X-:\*-Core', '^\s*$')
+ if val_inittab:
+ self.log.info(_('Forbidding Ctrl-Alt-Del from console'))
+ inittab.remove_line_matching(CTRALTDEL_REGEXP)
+
+ def allow_user_list(self, arg):
+ ''' Allow/Forbid the list of users on the system on display managers (kdm and gdm).'''
+ kdmrc = self.configfiles.get_config_file(KDMRC)
+ gdmconf = self.configfiles.get_config_file(GDMCONF)
+
+ oldval_gdmconf = gdmconf.get_shell_variable('Browser')
+ oldval_kdmrc = kdmrc.get_shell_variable('ShowUsers', 'X-\*-Greeter', '^\s*$')
+
+ if arg == "yes":
+ if kdmrc.exists():
+ if oldval_kdmrc != 'NotHidden':
+ self.log.info(_("Allowing list of users in KDM"))
+ kdmrc.set_shell_variable('ShowUsers', 'NotHidden', 'X-\*-Greeter', '^\s*$')
+ if gdmconf.exists():
+ if oldval_gdmconf != 'true':
+ self.log.info(_("Allowing list of users in GDM"))
+ gdmconf.set_shell_variable('Browser', 'true')
+ else:
+ if kdmrc.exists():
+ if oldval_kdmrc != 'Selected':
+ self.log.info(_("Forbidding list of users in KDM"))
+ kdmrc.set_shell_variable('ShowUsers', 'Selected', 'X-\*-Greeter', '^\s*$')
+ if gdmconf.exists():
+ if oldval_gdmconf != 'false':
+ self.log.info(_("Forbidding list of users in GDM"))
+ gdmconf.set_shell_variable('Browser', 'false')
+
+ def allow_root_login(self, arg):
+ ''' Allow/Forbid direct root login.'''
+ securetty = self.configfiles.get_config_file(SECURETTY)
+ kde = self.configfiles.get_config_file(KDE)
+ gdm = self.configfiles.get_config_file(GDM)
+ gdmconf = self.configfiles.get_config_file(GDMCONF)
+ xdm = self.configfiles.get_config_file(XDM)
+
+ val = {}
+ val_kde = kde.get_match('auth required (?:/lib/security/)?pam_listfile.so onerr=succeed item=user sense=deny file=/etc/bastille-no-login')
+ val_gdm = gdm.get_match('auth required (?:/lib/security/)?pam_listfile.so onerr=succeed item=user sense=deny file=/etc/bastille-no-login')
+ val_xdm = 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
+ s = 'vc/' + str(n)
+ if securetty.get_match(s):
+ num = num + 1
+
+ if arg == "yes":
+ if val_kde or val_gdm or val_xdm or num != 12:
+ self.log.info(_('Allowing direct root login'))
+ if gdmconf.exists():
+ gdmconf.set_shell_variable('ConfigAvailable', 'true', '\[greeter\]', '^\s*$')
+
+ for cnf in [kde, gdm, xdm]:
+ if cnf.exists():
+ 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)
+ securetty.replace_line_matching(s, s, 1)
+ s = 'vc/' + str(n)
+ securetty.replace_line_matching(s, s, 1)
+ else:
+ if gdmconf.exists():
+ gdmconf.set_shell_variable('ConfigAvailable', 'false', '\[greeter\]', '^\s*$')
+ if (kde.exists() and not val_kde) or (gdm.exists() and not val_gdm) or (xdm.exists() and not val_xdm) or num > 0:
+ self.log.info(_('Forbidding direct root login'))
+
+ bastillenologin = self.configfiles.get_config_file(BASTILLENOLOGIN)
+ bastillenologin.replace_line_matching('^\s*root', 'root', 1)
+
+ # TODO: simplify this
+ for cnf in [kde, gdm, xdm]:
+ if cnf.exists():
+ (cnf.replace_line_matching('^auth\s*required\s*(?:/lib/security/)?pam_listfile.so.*bastille-no-login',
+ 'auth required pam_listfile.so onerr=succeed item=user sense=deny file=/etc/bastille-no-login') or
+ cnf.insert_at(0, 'auth required pam_listfile.so onerr=succeed item=user sense=deny file=/etc/bastille-no-login'))
+ securetty.remove_line_matching('.+', 1)
+
+ def allow_remote_root_login(self, arg):
+ ''' Allow/Forbid remote root login via sshd. You can specify yes, no and without-password. See sshd_config(5) man page for more information.'''
+ sshd_config = self.configfiles.get_config_file(SSHDCONFIG)
+
+ val = sshd_config.get_match(PERMIT_ROOT_LOGIN_REGEXP, '@1')
+
+ if val != arg:
+ if arg == "yes":
+ self.log.info(_('Allowing remote root login'))
+ sshd_config.exists() and sshd_config.replace_line_matching(PERMIT_ROOT_LOGIN_REGEXP,
+ 'PermitRootLogin yes', 1)
+ elif arg == "no":
+ self.log.info(_('Forbidding remote root login'))
+ sshd_config.exists() and sshd_config.replace_line_matching(PERMIT_ROOT_LOGIN_REGEXP,
+ 'PermitRootLogin no', 1)
+ elif arg == "without_password":
+ self.log.info(_('Allowing remote root login only by passphrase'))
+ sshd_config.exists() and sshd_config.replace_line_matching(PERMIT_ROOT_LOGIN_REGEXP,
+ 'PermitRootLogin without-password', 1)
+
+ def enable_pam_wheel_for_su(self, arg):
+ ''' Enabling su only from members of the wheel group or allow su from any user.'''
+ su = self.configfiles.get_config_file(SU)
+
+ val = su.get_match('^auth\s+required\s+(?:/lib/security/)?pam_wheel.so\s+use_uid\s*$')
+
+ if arg == "yes":
+ if not val:
+ self.log.info(_('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']:
+ self.log.error(_('wheel group is empty'))
+ return
+ if su.exists():
+ (su.replace_line_matching('^[#\s]*auth\s+required\s+(?:/lib/security/)?pam_wheel.so\s+use_uid\s*$',
+ 'auth required pam_wheel.so use_uid') or \
+ su.insert_after('^auth\s+required', 'auth required pam_wheel.so use_uid'))
+ else:
+ if val:
+ self.log.info(_('Allowing su for all'))
+ if su.exists():
+ su.replace_line_matching('^auth\s+required\s+(?:/lib/security/)?pam_wheel.so\s+use_uid\s*$',
+ '# auth required pam_wheel.so use_uid')
+
+ def enable_pam_root_from_wheel(self, arg):
+ ''' Allow root access without password for the members of the wheel group.'''
+ su = self.configfiles.get_config_file(SU)
+ simple = self.configfiles.get_config_file(SIMPLE_ROOT_AUTHEN)
+
+ if not su.exists():
+ return
+
+ val = su.get_match(SUCCEED_MATCH)
+
+ val_simple = simple.get_match(SUCCEED_MATCH)
+
+ if arg == "yes":
+ if not val or not val_simple:
+ self.log.info(_('Allowing transparent root access for wheel group members'))
+ if not val:
+ print "here2"
+ su.insert_before('^auth\s+sufficient', SUCCEED_LINE)
+ if simple.exists() and not val_simple:
+ simple.insert_before('^auth\s+sufficient', SUCCEED_LINE)
+ else:
+ if val or val_simple:
+ self.log.info(_('Disabling transparent root access for wheel group members'))
+ if val:
+ su.remove_line_matching(SUCCEED_MATCH)
+ if simple.exists() and val_simple:
+ simple.remove_line_matching(SUCCEED_MATCH)
+
+ def allow_autologin(self, arg):
+ ''' Allow/Forbid autologin.'''
+ autologin = self.configfiles.get_config_file(AUTOLOGIN)
+
+ val = autologin.get_shell_variable('AUTOLOGIN')
+
+ if val != arg:
+ if arg == "yes":
+ self.log.info(_('Allowing autologin'))
+ autologin.set_shell_variable('AUTOLOGIN', 'yes')
+ else:
+ self.log.info(_('Forbidding autologin'))
+ autologin.set_shell_variable('AUTOLOGIN', 'no')
+
+ def password_loader(self, value):
+ '''Unused'''
+ self.log.info(_('Activating password in boot loader'))
+ liloconf = self.configfiles.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 = self.configfiles.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(self):
+ '''Unused'''
+ self.log.info(_('Removing password in boot loader'))
+ liloconf = self.configfiles.get_config_file(LILOCONF)
+ liloconf.exists() and liloconf.remove_line_matching('^password=', 1)
+ menulst = self.configfiles.get_config_file(MENULST)
+ menulst.exists() and menulst.remove_line_matching('^password\s')
+
+ def enable_console_log(self, 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 = self.configfiles.get_config_file(SYSLOGCONF)
+
+ val = syslogconf.get_match('\s*[^#]+/dev/([^ ]+)', '@1')
+
+ if arg == "yes":
+ if dev != val:
+ self.log.info(_('Enabling log on console'))
+ syslogconf.exists() and syslogconf.replace_line_matching('\s*[^#]+/dev/', expr + ' /dev/' + dev, 1)
+ else:
+ if val != None:
+ self.log.info(_('Disabling log on console'))
+ syslogconf.exists() and syslogconf.remove_line_matching('\s*[^#]+/dev/')
+
+ def enable_security_check(self, arg):
+ ''' Activate/Disable daily security check.'''
+ cron = self.configfiles.get_config_file(CRON)
+ cron.remove_line_matching('[^#]+/usr/share/msec/security.sh')
+
+ securitycron = self.configfiles.get_config_file(SECURITYCRON)
+
+ if arg == "yes":
+ if not securitycron.exists():
+ self.log.info(_('Activating daily security check'))
+ securitycron.symlink(SECURITYSH)
+ else:
+ if securitycron.exists():
+ self.log.info(_('Disabling daily security check'))
+ securitycron.unlink()
+
+ def authorize_services(self, arg):
+ ''' Configure access to tcp_wrappers services (see hosts.deny(5)). If arg = yes, all services are authorized. If arg = local, only local ones are, and if arg = no, no services are authorized. In this case, To authorize the services you need, use /etc/hosts.allow (see hosts.allow(5)).'''
+
+ hostsdeny = self.configfiles.get_config_file(HOSTSDENY)
+
+ if hostsdeny.get_match(ALL_REGEXP):
+ val = "no"
+ elif hostsdeny.get_match(ALL_LOCAL_REGEXP):
+ val = "local"
+ else:
+ val = "yes"
+
+ if val != arg:
+ if arg == "yes":
+ self.log.info(_('Authorizing all services'))
+ hostsdeny.remove_line_matching(ALL_REGEXP, 1)
+ hostsdeny.remove_line_matching(ALL_LOCAL_REGEXP, 1)
+ elif arg == "no":
+ self.log.info(_('Disabling all services'))
+ hostsdeny.remove_line_matching(ALL_LOCAL_REGEXP, 1)
+ hostsdeny.replace_line_matching(ALL_REGEXP, 'ALL:ALL:DENY', 1)
+ elif arg == "local":
+ self.log.info(_('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)
+
+ def set_zero_one_variable(self, file, variable, value, one_msg, zero_msg):
+ ''' Helper function for enable_ip_spoofing_protection, accept_icmp_echo, accept_broadcasted_icmp_echo,
+ # accept_bogus_error_responses and enable_log_strange_packets.'''
+ f = self.configfiles.get_config_file(file)
+ curvalue = f.get_shell_variable(variable)
+ if value == "yes":
+ value = "1"
+ else:
+ value = "0"
+ if value != curvalue:
+ if value == "1":
+ self.log.info(one_msg)
+ f.set_shell_variable(variable, 1)
+ else:
+ self.log.info(zero_msg)
+ f.set_shell_variable(variable, 0)
+
+ def enable_ip_spoofing_protection(self, arg, alert=1):
+ ''' Enable/Disable IP spoofing protection.'''
+ # the alert argument is kept for backward compatibility
+ self.set_zero_one_variable(SYSCTLCONF, 'net.ipv4.conf.all.rp_filter', arg, 'Enabling ip spoofing protection', 'Disabling ip spoofing protection')
+
+ def enable_dns_spoofing_protection(self, arg, alert=1):
+ ''' Enable/Disable name resolution spoofing protection. If \\fIalert\\fP is true, also reports to syslog.'''
+ hostconf = self.configfiles.get_config_file(HOSTCONF)
+
+ val = hostconf.get_match('nospoof\s+on')
+
+ if arg:
+ if not val:
+ self.log.info(_('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:
+ self.log.info(_('Disabling name resolution spoofing protection'))
+ hostconf.remove_line_matching('nospoof')
+ hostconf.remove_line_matching('spoofalert')
+
+ def accept_icmp_echo(self, arg):
+ ''' Accept/Refuse icmp echo.'''
+ self.set_zero_one_variable(SYSCTLCONF, 'net.ipv4.icmp_echo_ignore_all', arg, 'Ignoring icmp echo', 'Accepting icmp echo')
+
+ def accept_broadcasted_icmp_echo(self, arg):
+ ''' Accept/Refuse broadcasted icmp echo.'''
+ self.set_zero_one_variable(SYSCTLCONF, 'net.ipv4.icmp_echo_ignore_broadcasts', arg, 'Ignoring broadcasted icmp echo', 'Accepting broadcasted icmp echo')
+
+ def accept_bogus_error_responses(self, arg):
+ ''' Accept/Refuse bogus IPv4 error messages.'''
+ self.set_zero_one_variable(SYSCTLCONF, 'net.ipv4.icmp_ignore_bogus_error_responses', arg, 'Ignoring bogus icmp error responses', 'Accepting bogus icmp error responses')
+
+ def enable_log_strange_packets(self, arg):
+ ''' Enable/Disable the logging of IPv4 strange packets.'''
+ self.set_zero_one_variable(SYSCTLCONF, 'net.ipv4.conf.all.log_martians', arg, 'Enabling logging of strange packets', 'Disabling logging of strange packets')
+
+ def password_length(self, arg):
+ ''' Set the password minimum length and minimum number of digit and minimum number of capitalized letters.'''
+
+ try:
+ length, ndigits, nupper = arg.split(",")
+ length = int(length)
+ ndigits = int(ndigits)
+ nupper = int(nupper)
+ except:
+ self.log.error(_('Invalid password length "%s". Use "length,ndigits,nupper" as parameter') % arg)
+ return
+
+ passwd = self.configfiles.get_config_file(SYSTEM_AUTH)
+
+ val_length = val_ndigits = val_ucredit = 999999
+
+ 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)
+
+ if passwd.exists() and (val_length != length or val_ndigits != ndigits or val_ucredit != nupper):
+ self.log.info(_('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))
+
+ def enable_password(self, arg):
+ ''' Use password to authenticate users. Take EXTREMELY care when disabling passwords, as it will leave the machine COMPLETELY vulnerable.'''
+ system_auth = self.configfiles.get_config_file(SYSTEM_AUTH)
+
+ val = system_auth.get_match(PASSWORD_REGEXP)
+
+ if arg == "yes":
+ if val:
+ self.log.info(_('Using password to authenticate users'))
+ system_auth.remove_line_matching(PASSWORD_REGEXP)
+ else:
+ if not val:
+ self.log.info(_('Don\'t use password to authenticate users'))
+ system_auth.replace_line_matching(PASSWORD_REGEXP, 'auth sufficient pam_permit.so') or \
+ system_auth.insert_before('auth\s+sufficient', 'auth sufficient pam_permit.so')
+
+ def password_history(self, arg):
+ ''' Set the password history length to prevent password reuse. This is not supported by pam_tcb. '''
+
+ system_auth = self.configfiles.get_config_file(SYSTEM_AUTH)
+
+ pam_tcb = system_auth.get_match(PAM_TCB_REGEXP)
+ if pam_tcb:
+ self.log.info(_('Password history not supported with pam_tcb.'))
+ return
+
+ # verify parameter validity
+ # max
+ try:
+ history = int(arg)
+ except:
+ self.log.error(_('Invalid maximum password history length: "%s"') % arg)
+ return
+
+ if system_auth.exists():
+ val = system_auth.get_match(UNIX_REGEXP, '@2')
+
+ if val and val != '':
+ val = int(val)
+ else:
+ val = 0
+ else:
+ val = 0
+
+ if history != val:
+ if history > 0:
+ self.log.info(_('Setting password history to %d.') % history)
+ system_auth.replace_line_matching(UNIX_REGEXP, '@1 remember=%d@3' % history) or \
+ system_auth.replace_line_matching('(^\s*password\s+sufficient\s+(?:/lib/security/)?pam_unix.so.*)', '@1 remember=%d' % history)
+ opasswd = self.configfiles.get_config_file(OPASSWD)
+ opasswd.exists() or opasswd.touch()
+ else:
+ self.log.info(_('Disabling password history'))
+ system_auth.replace_line_matching(UNIX_REGEXP, '@1@3')
+
+ def enable_sulogin(self, arg):
+ ''' Enable/Disable sulogin(8) in single user level.'''
+ inittab = self.configfiles.get_config_file(INITTAB)
+
+ val = inittab.get_match(SULOGIN_REGEXP)
+
+ if arg == "yes":
+ if not val:
+ self.log.info(_('Enabling sulogin in single user runlevel'))
+ inittab.replace_line_matching('[^#]+:S:', '~~:S:wait:/sbin/sulogin', 1)
+ else:
+ if val:
+ self.log.info(_('Disabling sulogin in single user runlevel'))
+ inittab.remove_line_matching('~~:S:wait:/sbin/sulogin')
+
+ # Do we need this?
+ def enable_msec_cron(self, arg):
+ ''' Enable/Disable msec hourly security check.'''
+ mseccron = self.configfiles.get_config_file(MSECCRON)
+
+ val = mseccron.exists()
+
+ if arg == "yes":
+ if not val:
+ self.log.info(_('Enabling msec periodic runs'))
+ mseccron.symlink(MSECBIN)
+ else:
+ if val:
+ self.log.info(_('Disabling msec periodic runs'))
+ mseccron.unlink()
+
+ def enable_at_crontab(self, 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 = self.configfiles.get_config_file(CRONALLOW)
+ atallow = self.configfiles.get_config_file(ATALLOW)
+
+ val_cronallow = cronallow.get_match('root')
+ val_atallow = atallow.get_match('root')
+
+ if arg == "yes":
+ if val_cronallow or val_atallow:
+ self.log.info(_('Enabling crontab and at'))
+ if val_cronallow:
+ cronallow.exists() and cronallow.move(SUFFIX)
+ if val_atallow:
+ atallow.exists() and atallow.move(SUFFIX)
+ else:
+ if not val_cronallow or not val_atallow:
+ self.log.info(_('Disabling crontab and at'))
+ cronallow.replace_line_matching('root', 'root', 1)
+ atallow.replace_line_matching('root', 'root', 1)
+
+ def allow_xauth_from_root(self, arg):
+ ''' Allow/forbid to export display when passing from the root account to the other users. See pam_xauth(8) for more details.'''
+ export = self.configfiles.get_config_file(EXPORT)
+
+ allow = export.get_match('^\*$')
+
+ if arg == 'yes':
+ if not allow:
+ self.log.info(_('Allowing export display from root'))
+ export.insert_at(0, '*')
+ else:
+ if allow:
+ self.log.info(_('Forbidding export display from root'))
+ export.remove_line_matching('^\*$')
+
+ def check_promisc(self, param):
+ ''' Activate/Disable ethernet cards promiscuity check.'''
+ cron = self.configfiles.get_config_file(CRON)
+
+ val = cron.get_match(CRON_REGEX)
+
+ if param == "yes":
+ if val != CRON_ENTRY:
+ self.log.info(_('Activating periodic promiscuity check'))
+ cron.replace_line_matching(CRON_REGEX, CRON_ENTRY, 1)
+ else:
+ if val:
+ self.log.info(_('Disabling periodic promiscuity check'))
+ cron.remove_line_matching('[^#]+/usr/share/msec/promisc_check.sh')
+
+ # The following checks are run from crontab. We only have these functions here
+ # to get their descriptions.
+
+ def check_security(self, param):
+ """ Enables daily security checks."""
+ pass
+
+ def check_perms(self, param):
+ """ Enables periodic permission checking for system files."""
+ pass
+
+ def check_user_files(self, param):
+ """ Enables permission checking on users' files that should not be owned by someone else, or writable."""
+ pass
+
+ def check_suid_root(self, param):
+ """ Enables checking for additions/removals of suid root files."""
+ pass
+
+ def check_suid_md5(self, param):
+ """ Enables checksum verification for suid files."""
+ pass
+
+ def check_sgid(self, param):
+ """ Enables checking for additions/removals of sgid files."""
+ pass
+
+ def check_writable(self, param):
+ """ Enables checking for files/directories writable by everybody."""
+ pass
+
+ def check_unowned(self, param):
+ """ Enables checking for unowned files."""
+ pass
+
+ def check_open_port(self, param):
+ """ Enables checking for open network ports."""
+ pass
+
+ def check_passwd(self, param):
+ """ Enables password-related checks, such as empty passwords and strange super-user accounts."""
+ pass
+
+ def check_shadow(self, param):
+ """ Enables checking for empty passwords."""
+ pass
+
+ def check_chkrootkit(self, param):
+ """ Enables checking for known rootkits using chkrootkit."""
+ pass
+
+ def check_rpm(self, param):
+ """ Enables verification of installed packages."""
+ pass
+
+ def tty_warn(self, param):
+ """ Enables periodic security check results to terminal."""
+ pass
+
+ def mail_warn(self, param):
+ """ Enables security results submission by email."""
+ pass
+
+ def mail_empty_content(self, param):
+ """ Enables sending of empty mail reports."""
+ pass
+
+ def syslog_warn(self, param):
+ """ Enables logging to system log."""
+ pass
+
+ def mail_user(self, param):
+ """ Defines email to receive security notifications."""
+ pass
+
+ def check_shosts(self, param):
+ """ Enables checking for dangerous options in users' .rhosts/.shosts files."""
+ pass
+# }}}
+
+# {{{ PERMS - permissions handling
+class PERMS:
+ """Permission checking/enforcing."""
+ def __init__(self, log):
+ """Initializes internal variables"""
+ self.log = log
+ self.USER = {}
+ self.GROUP = {}
+ self.USERID = {}
+ self.GROUPID = {}
+ self.files = {}
+ self.fs_regexp = self.build_non_localfs_regexp()
+
+ def get_user_id(self, name):
+ '''Caches and retreives user id correspondent to name'''
+ try:
+ return self.USER[name]
+ except KeyError:
+ try:
+ self.USER[name] = pwd.getpwnam(name)[2]
+ except KeyError:
+ error(_('user name %s not found') % name)
+ self.USER[name] = -1
+ return self.USER[name]
+
+ def get_user_name(self, id):
+ '''Caches and retreives user name correspondent to id'''
+ try:
+ return self.USERID[id]
+ except KeyError:
+ try:
+ self.USERID[id] = pwd.getpwuid(id)[0]
+ except KeyError:
+ error(_('user name not found for id %d') % id)
+ self.USERID[id] = str(id)
+ return self.USERID[id]
+
+ def get_group_id(self, name):
+ '''Caches and retreives group id correspondent to name'''
+ try:
+ return self.GROUP[name]
+ except KeyError:
+ try:
+ self.GROUP[name] = grp.getgrnam(name)[2]
+ except KeyError:
+ error(_('group name %s not found') % name)
+ self.GROUP[name] = -1
+ return self.GROUP[name]
+
+ def get_group_name(self, id):
+ '''Caches and retreives group name correspondent to id'''
+ try:
+ return self.GROUPID[id]
+ except KeyError:
+ try:
+ self.GROUPID[id] = grp.getgrgid(id)[0]
+ except KeyError:
+ error(_('group name not found for id %d') % id)
+ self.GROUPID[id] = str(id)
+ return self.GROUPID[id]
+
+ def build_non_localfs_regexp(self,
+ non_localfs = ['nfs', 'codafs', 'smbfs', 'cifs', 'autofs']):
+ """Build a regexp that matches all the non local filesystems"""
+ try:
+ file = open('/proc/mounts', 'r')
+ except IOError:
+ self.log.error(_('Unable to check /proc/mounts. Assuming all file systems are local.'))
+ return None
+
+ regexp = None
+
+ for line in file.readlines():
+ fields = string.split(line)
+ if fields[2] in non_localfs:
+ if regexp:
+ regexp = regexp + '|' + fields[1]
+ else:
+ regexp = '^(' + fields[1]
+
+ file.close()
+
+ if not regexp:
+ return None
+ else:
+ return re.compile(regexp + ')')
+
+ def commit(self, really_commit=True, enforce=False):
+ """Commits changes.
+ If enforce is True, the permissions on all files are enforced."""
+ if not really_commit:
+ self.log.info(_("In check-only mode, nothing is written back to disk."))
+
+ if len(self.files) > 0:
+ self.log.info("%s: %s" % (config.MODIFICATIONS_FOUND, " ".join(self.files)))
+ else:
+ self.log.info(config.MODIFICATIONS_NOT_FOUND)
+
+
+ for file in self.files:
+ newperm, newuser, newgroup, force = self.files[file]
+ # are we in enforcing mode?
+ if enforce:
+ force = True
+
+ if newuser != None:
+ self.log.info(_("Enforcing user on %s to %s") % (file, self.get_user_name(newuser)))
+ if force and really_commit:
+ try:
+ os.chown(file, newuser, -1)
+ except:
+ self.log.error(_("Error changing user on %s: %s") % (file, sys.exc_value))
+ if newgroup != None:
+ self.log.info(_("Enforcing group on %s to %s") % (file, self.get_group_name(newgroup)))
+ if force and really_commit:
+ try:
+ os.chown(file, -1, newgroup)
+ except:
+ self.log.error(_("Error changing group on %s: %s") % (file, sys.exc_value))
+ # permissions should be last, as chown resets them
+ # on suid files
+ if newperm != None:
+ self.log.info(_("Enforcing permissions on %s to %o") % (file, newperm))
+ if force and really_commit:
+ try:
+ os.chmod(file, newperm)
+ except:
+ self.log.error(_("Error changing permissions on %s: %s") % (file, sys.exc_value))
+
+
+ def check_perms(self, perms):
+ '''Checks permissions for all entries in perms (PermConfig).'''
+
+ for file in perms.list_options():
+ user_s, group_s, perm_s, force = perms.get(file)
+
+ # permission
+ if perm_s == 'current':
+ perm = -1
+ else:
+ try:
+ perm = int(perm_s, 8)
+ except ValueError:
+ self.log.error(_("bad permissions for '%s': '%s'") % (file, perm_s))
+ continue
+
+ # user
+ if user_s == 'current':
+ user = -1
+ else:
+ user = self.get_user_id(user_s)
+
+ # group
+ if group_s == 'current':
+ group = -1
+ else:
+ group = self.get_group_id(group_s)
+
+ # now check the permissions
+ for f in glob.glob(file):
+ # get file properties
+ f = os.path.realpath(f)
+ try:
+ full = os.lstat(f)
+ except OSError:
+ continue
+
+ if self.fs_regexp and self.fs_regexp.search(f):
+ self.log.info(_('Non local file: "%s". Nothing changed.') % fields[0])
+ continue
+
+ curperm = perm
+ mode = stat.S_IMODE(full[stat.ST_MODE])
+
+ if perm != -1 and stat.S_ISDIR(full[stat.ST_MODE]):
+ if curperm & 0400:
+ curperm = curperm | 0100
+ if curperm & 0040:
+ curperm = curperm | 0010
+ if curperm & 0004:
+ curperm = curperm | 0001
+
+ curuser = full[stat.ST_UID]
+ curgroup = full[stat.ST_GID]
+ curperm = mode
+ # checking for subdirectory permissions
+ if f != '/' and f[-1] == '/':
+ f = f[:-1]
+ if f[-2:] == '/.':
+ f = f[:-2]
+ # check for changes
+ newperm = None
+ newuser = None
+ newgroup = None
+ if perm != -1 and perm != curperm:
+ newperm = perm
+ if user != -1 and user != curuser:
+ newuser = user
+ if group != -1 and group != curgroup:
+ newgroup = group
+ if newperm != None or newuser != None or newgroup != None:
+ self.files[f] = (newperm, newuser, newgroup, force)
+ self.log.debug("Updating %s (matched by '%s')" % (f, file))
+ else:
+ # see if any other rule put this file into the list
+ if f in self.files:
+ self.log.debug("Removing previously selected %s (matched by '%s')" % (f, file))
+ del self.files[f]
+ return self.files
+# }}}
+
+
+if __name__ == "__main__":
+ # this should never ever be run directly
+ print >>sys.stderr, """This file should not be run directly."""
+