diff options
author | Eugeni Dodonov <eugeni@mandriva.org> | 2009-01-06 21:31:46 +0000 |
---|---|---|
committer | Eugeni Dodonov <eugeni@mandriva.org> | 2009-01-06 21:31:46 +0000 |
commit | ff31c9236b1fd7465ea9687fc735e8af882e780e (patch) | |
tree | eec89033b4ad0b2459fbb91fa6dd39077eeaf407 /src | |
parent | ab984707253940bf5ced3a379699e8d0dc757fa6 (diff) | |
download | msec-ff31c9236b1fd7465ea9687fc735e8af882e780e.tar msec-ff31c9236b1fd7465ea9687fc735e8af882e780e.tar.gz msec-ff31c9236b1fd7465ea9687fc735e8af882e780e.tar.bz2 msec-ff31c9236b1fd7465ea9687fc735e8af882e780e.tar.xz msec-ff31c9236b1fd7465ea9687fc735e8af882e780e.zip |
Updated to working version of new msec.
Conflicts:
Makefile
cron-sh/security_check.sh
share/msec.py
Diffstat (limited to 'src')
-rw-r--r-- | src/msec/.svnignore | 6 | ||||
-rw-r--r-- | src/msec/CHANGES | 73 | ||||
-rw-r--r-- | src/msec/Makefile | 34 | ||||
-rw-r--r-- | src/msec/README | 87 | ||||
-rwxr-xr-x | src/msec/compile.py | 17 | ||||
-rw-r--r-- | src/msec/config.py | 273 | ||||
-rwxr-xr-x | src/msec/help_draksec.py | 74 | ||||
-rwxr-xr-x | src/msec/libmsec.py | 1763 | ||||
-rwxr-xr-x | src/msec/man.py | 197 | ||||
-rwxr-xr-x | src/msec/msec | 34 | ||||
-rwxr-xr-x | src/msec/msec.py | 141 | ||||
-rwxr-xr-x | src/msec/msecgui | 34 | ||||
-rwxr-xr-x | src/msec/msecgui.py | 725 | ||||
-rwxr-xr-x | src/msec/msecperms | 34 | ||||
-rwxr-xr-x | src/msec/msecperms.py | 152 | ||||
-rw-r--r-- | src/msec/version.py | 1 | ||||
-rw-r--r-- | src/msec_find/Makefile | 2 | ||||
-rw-r--r-- | src/promisc_check/Makefile | 2 |
18 files changed, 3647 insertions, 2 deletions
diff --git a/src/msec/.svnignore b/src/msec/.svnignore new file mode 100644 index 0000000..17005aa --- /dev/null +++ b/src/msec/.svnignore @@ -0,0 +1,6 @@ +*.pyo +*.pyc +*.flog +mseclib.man +mseclib.py +level.* diff --git a/src/msec/CHANGES b/src/msec/CHANGES new file mode 100644 index 0000000..22e546a --- /dev/null +++ b/src/msec/CHANGES @@ -0,0 +1,73 @@ +changes in version 0.30 +======================= + + * don't lower security if the admin has already augmented it (when called without argument). + * splitted functions that worked on multiple levels. + +changes between version 0.18 and 0.19 +===================================== + +msec utility changes: + + * no password in level 0 + +Periodic security checks changes: + + * config file is now in /var/lib/msec/security.conf and can +be overriden by /etc/security/msec/security.conf. + +changes between version 0.17 and 0.18 +===================================== + +msec utility changes: + + * allow /etc/security/msec/level.local to override the default +setting of the level. + * promisc_check.sh works now. + * added mseclib man page. + +changes between version 0.16 and 0.17 +===================================== + +msec utility changes: + + * handle shell timeout (level 4 and 5) + * limit shell history (level 4 and 5) + * su only for wheel group (level 5) + * sulogin for single user mode (level 4 and 5) + * various sysctl.conf settings for icmp and network parameters + * password aging (level 4 and 5) + * suppress /etc/issue.net (level 4 and 5) and /etc/issue (level 5) + * removed manipulation of the groups of users + * removed removal of services + * logging in syslog according to the guideline for explanations in tools + * more correct prevention of direct root logins + * rewritten in python + +msec can be used to change level and it's also run hourly by cron to +maintain the security level on the system. Only the minimum of changes +on the filesystem are applied and the minimum of programs started. + +Periodic security checks changes: + + * added rpm database checks (rpm -va and rpm -qa) + * report when a user other than root is at uid 0 + * diff_check reports even when the log is empty + * use chkrootkit if present. + +Permissions settings changes: + + * / + * removed audio group handling because it has always conflicted with pam_console + * handle /var/log sub-directories in a generic manner + * /etc/rc.d/init.d/* + * corrected ssh and ping related paths + * /etc/sysconfig + * /proc + * corrected gcc files + * rpm related files to avoid exposing what is installed + * /var/lock/subsys + * added a local.perm to allow modifications without modifying level perms + * corrected all the inconsistencies between levels to be able to change and come back +without problem + * rewritten in python diff --git a/src/msec/Makefile b/src/msec/Makefile new file mode 100644 index 0000000..8ff37b0 --- /dev/null +++ b/src/msec/Makefile @@ -0,0 +1,34 @@ +#--------------------------------------------------------------- +# Project : Mandriva Linux +# Module : share +# File : Makefile +# Version : $Id$ +# Author : Frederic Lepied +# Created On : Sat Jan 26 20:17:55 2002 +#--------------------------------------------------------------- + +MAN=../../man/C/msec.8 +PFILES=msecperms.py compile.py libmsec.py man.py + +all: compile man help + +compile: + ./compile.py '/usr/share/msec/' *.py + +man: $(MAN) + +help: libmsec.py help_draksec.py + ./help_draksec.py + +$(MAN): libmsec.py man.py + rm -f $@ + ./man.py libmsec > $@ + +clean: + rm -f *.pyc *.pyo mseclib.py *~ help.* + +# Local variables: +# mode: makefile +# End: +# +# Makefile ends here diff --git a/src/msec/README b/src/msec/README new file mode 100644 index 0000000..4bb3846 --- /dev/null +++ b/src/msec/README @@ -0,0 +1,87 @@ +****************** +Configurations files in /etc/security/msec/ +Shell scripts in /usr/share/msec. +****************** + +Suggestions & comments: +flepied@mandriva.com + +****************** +Doc of the rewritting in python: + + 0 1 2 3 4 5 +root umask 022 022 022 022 022 077 +shell timeout 0 0 0 0 3600 900 +deny services none none none none local all +su only for wheel grp no no no no no yes +user umask 022 022 022 022 077 077 +shell history size default default default default 10 10 +direct root login yes yes yes yes no no +remote root login yes yes yes yes no no +sulogin for single user no no no no yes yes +user list in [kg]dm yes yes yes yes no no +promisc check no no no no yes yes +ignore icmp echo no no no no yes yes +ignore broadcasted icmp echo no no no no yes yes +ignore bogus error responses no no no no yes yes +enable libsafe no no no no yes yes +allow reboot by user yes yes yes yes no no +allow crontab/at yes yes yes yes no no +password aging no no no no 60 30 +allow autologin yes yes yes no no no +console log no no no yes yes yes +issues yes yes yes local local no +ip spoofing protection no no no yes yes yes +dns spoofing protection no no no yes yes yes +log stange ip packets no no no yes yes yes +periodic security check no yes yes yes yes yes +allow X connections yes local local no no no +allow xauth from root yes yes yes yes no no +X server listen to tcp tcp tcp tcp local local +run msec by cron yes yes yes yes yes yes + +Periodic security checks by level: + + 0 1 2 3 4 5 +CHECK_SECURITY no yes yes yes yes yes +CHECK_PERMS no no no yes yes yes +CHECK_SUID_ROOT no no yes yes yes yes +CHECK_SUID_MD5 no no yes yes yes yes +CHECK_SGID no no yes yes yes yes +CHECK_WRITABLE no no yes yes yes yes +CHECK_UNOWNED no no no no yes yes +CHECK_PROMISC no no no no yes yes +CHECK_OPEN_PORT no no no yes yes yes +CHECK_PASSWD no no no yes yes yes +CHECK_SHADOW no no no yes yes yes +TTY_WARN no no no no yes yes +MAIL_WARN no no no yes yes yes +SYSLOG_WARN no no yes yes yes yes +RPM_CHECK no no no yes yes yes +CHKROOTKIT_CHECK no no no yes yes yes + +These variables are configured by the user: + +MAIL_USER the user to send the dayly reports. If not set, the email is +sent to root. + +PERM_LEVEL is used to determine which file to use to fix +permissions/owners/groups (from /usr/share/msec/perm.$PERM_LEVEL). If +not set, the SECURE_LEVEL is used instead. If the file +/etc/security/msec/perm.local exists, it's used too. The syntax for +each line if the following: + +<file specification> <owner> <permission> [force] + +<file specification> can be any glob to specify one or multiple +files/diretories. + +<owner> must be in the form <user>.<group> or <user>. (force only +user) or .<group> (force only group) or current (keep current user and +group). + +<permission> is an octal number representing the access rights or +current to keep the current permissions. + +If force is present as a 4th argument, it means that msec will enforce +the permission even if the previous permission was lower. diff --git a/src/msec/compile.py b/src/msec/compile.py new file mode 100755 index 0000000..c42a0c2 --- /dev/null +++ b/src/msec/compile.py @@ -0,0 +1,17 @@ +#!/usr/bin/python -O +############################################################################# +# File : compile.py +# Package : rpmlint +# Author : Frederic Lepied +# Created on : Sat Oct 23 23:40:21 1999 +# Version : $Id$ +# Purpose : byte compile all python files given in arguments. +############################################################################# + +import py_compile +import sys + +for f in sys.argv[2:]: + py_compile.compile(f, f+"o", sys.argv[1] + f) + +# compile.py ends here diff --git a/src/msec/config.py b/src/msec/config.py new file mode 100644 index 0000000..ec55de1 --- /dev/null +++ b/src/msec/config.py @@ -0,0 +1,273 @@ +#!/usr/bin/python -O +"""This is the configuration file for msec. +The following variables are defined here: + SECURITY_LEVELS: list of supported security levels + SECURITYCONF: location of msec configuration file + SECURITYLOG: log file for msec messages + SETTINGS: all security settings, with correspondent options for each + level, callback functions, and regexp of valid parameters. + +A helper function load_defaults parses the SETTINGS variable. + +The MsecConfig class processes the main msec configuration file. +""" + +import gettext +import sys +import traceback +import re + +# security levels +SECURITY_LEVELS = [ "none", "default", "secure" ] +DEFAULT_LEVEL="default" +SECURITY_LEVEL="/etc/security/msec/level.%s" + +# msec configuration file +SECURITYCONF = '/etc/security/msec/security.conf' + +# permissions +PERMCONF = '/etc/security/msec/perms.conf' +PERMISSIONS_LEVEL = '/etc/security/msec/perm.%s' # for level + +# logging +SECURITYLOG = '/var/log/msec.log' + +# localization +try: + cat = gettext.Catalog('msec') + _ = cat.gettext +except IOError: + _ = str + +# shared strings +MODIFICATIONS_FOUND = _('Modified system files') +MODIFICATIONS_NOT_FOUND = _('No changes in system files') + +# msec callbacks and valid values +# OPTION callback valid values +SETTINGS = {'CHECK_SECURITY' : ("check_security", ['yes', 'no']), + 'CHECK_PERMS' : ("check_perms", ['yes', 'no']), + 'CHECK_USER_FILES' : ("check_user_files", ['yes', 'no']), + 'CHECK_SUID_ROOT' : ("check_suid_root", ['yes', 'no']), + 'CHECK_SUID_MD5' : ("check_suid_md5", ['yes', 'no']), + 'CHECK_SGID' : ("check_sgid", ['yes', 'no']), + 'CHECK_WRITABLE' : ("check_writable", ['yes', 'no']), + 'CHECK_UNOWNED' : ("check_unowned", ['yes', 'no']), + 'CHECK_PROMISC' : ("check_promisc", ['yes', 'no']), + 'CHECK_OPEN_PORT' : ("check_open_port", ['yes', 'no']), + 'CHECK_PASSWD' : ("check_passwd", ['yes', 'no']), + 'CHECK_SHADOW' : ("check_shadow", ['yes', 'no']), + 'CHECK_CHKROOTKIT' : ("check_chkrootkit", ['yes', 'no']), + 'CHECK_RPM' : ("check_rpm", ['yes', 'no']), + 'CHECK_SHOSTS' : ("check_shosts", ['yes', 'no']), + # notifications + 'TTY_WARN' : ("tty_warn", ['yes', 'no']), + 'MAIL_WARN' : ("mail_warn", ['yes', 'no']), + 'MAIL_USER' : ("mail_user", ['*']), + 'MAIL_EMPTY_CONTENT': ("mail_empty_content", ['yes', 'no']), + 'SYSLOG_WARN' : ("syslog_warn", ['yes', 'no']), + 'NOTIFY_WARN' : ("notify_warn", ['yes', 'no']), + # security options + 'USER_UMASK': ("set_user_umask", ['*']), + 'ROOT_UMASK': ("set_root_umask", ['*']), + 'WIN_PARTS_UMASK': ("set_win_parts_umask", ['no', '*']), + 'ACCEPT_BOGUS_ERROR_RESPONSES': ("accept_bogus_error_responses", ['yes', 'no']), + 'ACCEPT_BROADCASTED_ICMP_ECHO': ("accept_broadcasted_icmp_echo", ['yes', 'no']), + 'ACCEPT_ICMP_ECHO': ("accept_icmp_echo", ['yes', 'no']), + 'ALLOW_AUTOLOGIN': ("allow_autologin", ['yes', 'no']), + 'ALLOW_REBOOT': ("allow_reboot", ['yes', 'no']), + 'ALLOW_REMOTE_ROOT_LOGIN': ("allow_remote_root_login", ['yes', 'no', 'without_password']), + 'ALLOW_ROOT_LOGIN': ("allow_root_login", ['yes', 'no']), + 'ALLOW_USER_LIST': ("allow_user_list", ['yes', 'no']), + 'ALLOW_X_CONNECTIONS': ("allow_x_connections", ['yes', 'no', 'local']), + 'ALLOW_XAUTH_FROM_ROOT': ("allow_xauth_from_root", ['yes', 'no']), + 'ALLOW_XSERVER_TO_LISTEN': ("allow_xserver_to_listen", ['yes', 'no']), + 'AUTHORIZE_SERVICES': ("authorize_services", ['yes', 'no', 'local']), + 'CREATE_SERVER_LINK': ("create_server_link", ['no', 'default', 'secure']), + 'ENABLE_AT_CRONTAB': ("enable_at_crontab", ['yes', 'no']), + 'ENABLE_CONSOLE_LOG': ("enable_console_log", ['yes', 'no']), + 'ENABLE_DNS_SPOOFING_PROTECTION':("enable_ip_spoofing_protection", ['yes', 'no']), + 'ENABLE_IP_SPOOFING_PROTECTION': ("enable_dns_spoofing_protection", ['yes', 'no']), + 'ENABLE_LOG_STRANGE_PACKETS': ("enable_log_strange_packets", ['yes', 'no']), + 'ENABLE_MSEC_CRON': ("enable_msec_cron", ['yes', 'no']), + 'ENABLE_PAM_ROOT_FROM_WHEEL': ("enable_pam_root_from_wheel", ['yes', 'no']), + 'ENABLE_SUDO': ("enable_sudo", ['yes', 'no', 'wheel']), + 'ENABLE_PAM_WHEEL_FOR_SU': ("enable_pam_wheel_for_su", ['yes', 'no']), + 'ENABLE_SULOGIN': ("enable_sulogin", ['yes', 'no']), + 'ENABLE_APPARMOR': ("enable_apparmor", ['yes', 'no']), + 'ENABLE_POLICYKIT': ("enable_policykit", ['yes', 'no']), + # password stuff + 'ENABLE_PASSWORD': ("enable_password", ['yes', 'no']), + 'PASSWORD_HISTORY': ("password_history", ['*']), + # format: min length, num upper, num digits + 'PASSWORD_LENGTH': ("password_length", ['*']), + 'SHELL_HISTORY_SIZE': ("set_shell_history_size", ['*']), + 'SHELL_TIMEOUT': ("set_shell_timeout", ['*']), + } + +def find_callback(param): + '''Finds a callback for security option''' + if param not in SETTINGS: + return None + else: + callback, valid_params = SETTINGS[param] + return callback + +def find_valid_params(param): + '''Finds valid parameters for security option''' + if param not in SETTINGS: + return None + else: + callback, valid_params = SETTINGS[param] + return valid_params + +# helper functions +def load_defaults(log, level): + """Loads default configuration for given security level, returning a + MsecConfig instance. + """ + config = MsecConfig(log, config=SECURITY_LEVEL % level) + config.load() + return config + +def load_default_perms(log, level): + """Loads default permissions for given security level, returning a + MsecConfig instance. + """ + config = PermConfig(log, config=PERMISSIONS_LEVEL % level) + config.load() + return config + +# {{{ MsecConfig +class MsecConfig: + """Msec configuration parser""" + def __init__(self, log, config=SECURITYCONF): + self.config = config + self.options = {} + self.comments = [] + self.log = log + + def load(self): + """Loads and parses configuration file""" + try: + fd = open(self.config) + except: + self.log.error(_("Unable to load configuration file %s: %s") % (self.config, sys.exc_value[1])) + return False + for line in fd.readlines(): + line = line.strip() + if line[0] == "#": + # comment + self.comments.append(line) + continue + try: + option, val = line.split("=", 1) + self.options[option] = val + except: + self.log.warn(_("Bad config option: %s") % line) + continue + fd.close() + return True + + def get(self, option, default=None): + """Gets a configuration option, or defines it if not defined""" + if option not in self.options: + self.options[option] = default + return self.options[option] + + def remove(self, option): + """Removes a configuration option.""" + if option in self.options: + del self.options[option] + + def set(self, option, value): + """Sets a configuration option""" + self.options[option] = value + + def list_options(self): + """Sorts and returns configuration parameters""" + sortedparams = self.options.keys() + if sortedparams: + sortedparams.sort() + return sortedparams + + def save(self): + """Saves configuration. Comments go on top""" + try: + fd = open(self.config, "w") + except: + self.log.error(_("Unable to save %s: %s") % (self.config, sys.exc_value)) + return False + for comment in self.comments: + print >>fd, comment + # sorting keys + for option in self.list_options(): + print >>fd, "%s=%s" % (option, self.options[option]) + return True +# }}} + +# {{{ PermConfig +class PermConfig(MsecConfig): + """Msec file permission parser""" + def __init__(self, log, config=PERMCONF): + self.config = config + self.options = {} + self.options_order = [] + self.comments = [] + self.log = log + self.regexp = re.compile("^([^\s]*)\s*([a-z]*)\.([a-z]*)\s*([\d]?\d\d\d)\s*(force)?$") + + def load(self): + """Loads and parses configuration file""" + try: + fd = open(self.config) + except: + self.log.error(_("Unable to load configuration file %s: %s") % (self.config, sys.exc_value)) + return False + for line in fd.readlines(): + line = line.strip() + if line[0] == "#": + # comment + self.comments.append(line) + continue + try: + res = self.regexp.findall(line) + if res: + if len(res[0]) == 5: + file, user, group, perm, force = res[0] + else: + force = None + file, user, group, perm = res[0] + self.options[file] = (user, group, perm, force) + self.options_order.append(file) + except: + traceback.print_exc() + self.log.warn(_("Bad config option: %s") % line) + continue + fd.close() + return True + + def list_options(self): + """Sorts and returns configuration parameters""" + return self.options_order + + def save(self): + """Saves configuration. Comments go on top""" + try: + fd = open(self.config, "w") + except: + self.log.error(_("Unable to save %s: %s") % (self.config, sys.exc_value)) + return False + for comment in self.comments: + print >>fd, comment + # sorting keys + for file in self.options_order: + user, group, perm, force = self.options[file] + if force: + force = "\tforce" + else: + force = "" + print >>fd, "%s\t%s.%s\t%s%s" % (file, user, group, perm, force) + return True +# }}} + diff --git a/src/msec/help_draksec.py b/src/msec/help_draksec.py new file mode 100755 index 0000000..48f0df0 --- /dev/null +++ b/src/msec/help_draksec.py @@ -0,0 +1,74 @@ +#!/usr/bin/python +# +# This script creates DrakSec help strings from libmsec code docstrings. +# + +import sys +import imp +import inspect +import re + +import config +from libmsec import MSEC, Log + +help_perl_file = 'help.pm' +help_python_file = 'help.py' + +header_perl = '''package security::help; +# !! THIS FILE WAS AUTO-GENERATED BY draksec_help.py !! +# !! DO NOT MODIFY HERE, MODIFY IN THE *MSEC* CVS !! + +use strict; +use common; + +our %help = ( +''' + +header_python = '''# libmsec documentation for python. + +import gettext + +# localization +try: + cat = gettext.Catalog('msec') + _ = cat.gettext +except IOError: + _ = str + +HELP = { +''' + +footer_perl = '''); +''' + +footer_python = '''} +''' + +### strings used in the rewritting +function_str_perl = ''''%s' => N("%s"), +''' + +function_str_python = ''' + '%s': _("%s"), ''' + +help_perl = open(help_perl_file, "w") +help_python = open(help_python_file, "w") + +# process all configuration parameters +log = Log(log_syslog=False, log_file=False) +msec = MSEC(log) + +print >>help_perl, header_perl +print >>help_python, header_python + +for variable in config.SETTINGS: + callback, params = config.SETTINGS[variable] + func = msec.get_action(callback) + if func: + print >>help_perl, function_str_perl % (variable, func.__doc__.strip()) + print >>help_python, function_str_python % (variable, func.__doc__.strip()) + +print >>help_perl, footer_perl +print >>help_python, footer_python + +# draksec_help.py ends here 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.""" + diff --git a/src/msec/man.py b/src/msec/man.py new file mode 100755 index 0000000..ce8fb33 --- /dev/null +++ b/src/msec/man.py @@ -0,0 +1,197 @@ +#!/usr/bin/python +#--------------------------------------------------------------- +# Project : Mandriva Linux +# Module : share +# File : man.py +# Version : $Id$ +# Author : Frederic Lepied +# Created On : Sat Jan 26 17:38:39 2002 +# Purpose : loads a python module and creates a man page from +# the doc strings of the functions. +#--------------------------------------------------------------- + +import sys +import imp +import inspect + +import config +from libmsec import MSEC, Log +try: + from version import version +except: + version = "(development version)" + +header = '''.ds q \N'34' +.TH msec %s msec "Mandriva Linux" +.SH NAME +msec \- Mandriva Linux security tools +.SH SYNOPSIS +.nf +.B msec [options] +.B msecperms [options] +.B msecgui [options] +.fi +.SH DESCRIPTION +.B msec +is responsible to maintain system security in Mandriva. It supports different security +configurations, which can be organized into several security levels. Currently, three +preconfigured security levels are provided: + +.TP +\\fBnone\\fR +this level aims to provide the most basic security. It should be used when you want to +manage all aspects of system security on your own. + +.TP +\\fBdefault\\fR +this is the default security level, which configures a reasonably safe set of security +features. It activates several periodic system checks, and sends the results of their +execution by email (by default, the local 'root' account is used). + +.TP +\\fBsecure\\fR +this level is configured to provide maximum system security, even at the cost of limiting +the remote access to the system, and local user permissions. It also runs a wider set of +periodic checks, enforces the local password settings, and periodically checks if the +system security settings, configured by msec, were modified directly or by some other +application. + +.PP + +The security settings are stored in \\fB/etc/security/msec/security.conf\\fR +file, and default settings for each predefined level are stored in +\\fB/etc/security/msec/level.LEVEL\\fR. Permissions for files and directories +that should be enforced or checked for changes are stored in +\\fB/etc/security/msec/perms.conf\\fR, and default permissions for each +predefined level are stored in \\fB/etc/security/msec/perm.LEVEL\\fR. Note +that user-modified parameters take precedence over default level settings. For +example, when default level configuration forbids direct root logins, this +setting can be overridden by the user. + +.PP + +The following options are supported by msec applications: + +.TP +\\fBmsec\\fR: +.PP + +This is the console version of msec. It is responsible for system security configuration +and checking and transitions between security levels. + +When executed without parameters, msec will read the system configuration file +(/etc/security/msec/security.conf), and enforce the specified security +settings. The operations are logged to \\fB/var/log/msec.log\\fP file, and also +to syslog, using \\fBLOG_AUTHPRIV\\fR facility. Please note that msec should +by run as root. + +\\fB\-h, --help\\fR + This option will display the list of supported command line options. + +\\fB\-l, --level <level>\\fR + List the default configuration for given security level. + +\\fB\-f, --force <level>\\fR + Apply the specified security level to the system, overwritting all +local changes. This is necessary to initialize a security level, either on first +install, on when a change to a different level is required. + +\\fB\-d\\fR + Enable debugging messages. + +\\fB\-p, --pretend\\fR + Verify the actions that will be performed by msec, without actually +doing anything to the system. In this mode of operation, msec performs all the +required tasks, except effectively writting data back to disk. + +.TP +\\fBmsecperms\\fR: +.PP + +This application is responsible for system permission checking and enforcements. + +When executed without parameters, msecperms will read the permissions +configuration file (/etc/security/msec/perms.conf), and enforce the specified +security settings. The operations are logged to \\fB/var/log/msec.log\\fP file, +and also to syslog, using \\fBLOG_AUTHPRIV\\fR facility. Please note that msecperms +should by run as root. + +\\fB\-h, --help\\fR + This option will display the list of supported command line options. + +\\fB\-l, --level <level>\\fR + List the default configuration for given security level. + +\\fB\-f, --force <level>\\fR + Apply the specified security level to the system, overwritting all +local changes. This is necessary to initialize a security level, either on first +install, on when a change to a different level is required. + +\\fB\-e, --enforce\\fR + Enforce the default permissions on all files. + +\\fB\-d\\fR + Enable debugging messages. + +\\fB\-p, --pretend\\fR + Verify the actions that will be performed by msec, without actually +doing anything to the system. In this mode of operation, msec performs all the +required tasks, except effectively writting data back to disk. + +.TP +\\fBmsecgui\\fR: +.PP + +This is the GTK version of msec. It acts as frontend to all msec functionalities. + +\\fB\-h, --help\\fR + This option will display the list of supported command line options. + +\\fB\-d\\fR + Enable debugging messages. + +.SH "SECURITY OPTIONS" + +The following security options are supported by msec: + +''' % version + +footer = '''.RE +.SH NOTES +Msec applications must be run by root. +.SH AUTHORS +Frederic Lepied <flepied@mandriva.com> + +Eugeni Dodonov <eugeni@mandriva.com> +''' + +### strings used in the rewritting +function_str = ''' +.TP 4 +.B \\fI%s\\fP +%s + +MSEC parameter: \\fI%s\\fP + +Accepted values: \\fI%s\\fP +''' + +### code + +# process all configuration parameters +log = Log(log_syslog=False, log_file=False) +msec = MSEC(log) + +#print >>sys.stderr, dir(msec.create_server_link) + +print header + +for variable in config.SETTINGS: + callback, params = config.SETTINGS[variable] + func = msec.get_action(callback) + if func: + print function_str % (callback, func.__doc__.strip(), variable, ", ".join(params)) + +print footer + +# man.py ends here diff --git a/src/msec/msec b/src/msec/msec new file mode 100755 index 0000000..48527bb --- /dev/null +++ b/src/msec/msec @@ -0,0 +1,34 @@ +#!/bin/sh +# +# Wrapper for msec.py +# + +if [ "`whoami`" != "root" ]; then + echo 'msec: sorry, you must be root !' + exit 1 +fi + +LCK=/var/run/msec.pid + +function cleanup() { + rm -f $LCK +} + +if [ -f $LCK ]; then + if [ -d /proc/`cat $LCK` ]; then + exit 0 + else + rm -f $LCK + fi +fi + +echo -n $$ > $LCK + +trap cleanup 0 + +MSEC=/usr/share/msec/msec.py +OPT="$@" + +$MSEC $OPT + +# msec ends here diff --git a/src/msec/msec.py b/src/msec/msec.py new file mode 100755 index 0000000..010f9ce --- /dev/null +++ b/src/msec/msec.py @@ -0,0 +1,141 @@ +#!/usr/bin/python -O +"""This is the main msec module. +It checks/sets the security levels, configures security variables, +and works as a frontend to libmsec. +""" + +import sys +import os +import string +import getopt +import gettext +import imp +import re + +# config +import config + +# version +try: + from version import version +except: + version = "development version" + +# libmsec +from libmsec import MSEC, Log + +import logging + +# localization +try: + cat = gettext.Catalog('msec') + _ = cat.gettext +except IOError: + _ = str + +# {{{ usage +def usage(): + """Prints help message""" + print """Msec: Mandriva Security Center (%s). + +When run without parameters, msec will read the configuration from +/etc/security/msec/msec.conf, and enforce the specified security settings. +If no configuration file is found on the system, a default configuration +will be created. + +Arguments to msec: + -h, --help displays this helpful message. + -l, --level <level> displays configuration for specified security + level. + -f, --force <level> force new level, overwriting user settings. + -d enable debugging messages. + -p, --pretend only pretend to change the level, perform no real + actions. Use this to see what operations msec + will perform. +""" % version +# }}} + +if __name__ == "__main__": + # default options + force_level = False + log_level = logging.INFO + commit = True + level = config.DEFAULT_LEVEL + + # parse command line + try: + opt, args = getopt.getopt(sys.argv[1:], 'hl:f:dp', ['help', 'list', 'force', 'debug', 'pretend']) + except getopt.error: + usage() + sys.exit(1) + for o in opt: + # help + if o[0] == '-h' or o[0] == '--help': + usage() + sys.exit(0) + # list + elif o[0] == '-l' or o[0] == '--list': + level = o[1] + log = Log(interactive=True, log_syslog=False, log_file=False) + levelconf = config.load_defaults(log, level) + params = levelconf.list_options() + if not params: + print >>sys.stderr, _("Invalid security level '%s'.") % level + sys.exit(1) + for item in params: + print "%s=%s" % (item, levelconf.get(item) ) + sys.exit(0) + # force new level + elif o[0] == '-f' or o[0] == '--force': + level = o[1] + force_level = True + # debugging + elif o[0] == '-d' or o[0] == '--debug': + log_level = logging.DEBUG + # check-only mode + elif o[0] == '-p' or o[0] == '--pretend': + commit = False + + # verifying use id + if os.geteuid() != 0: + print >>sys.stderr, _("Msec: Mandriva Security Center (%s)\n") % version + print >>sys.stderr, _("Error: This application must be executed by root!") + print >>sys.stderr, _("Run with --help to get help.") + sys.exit(1) + + # configuring logging + interactive = sys.stdin.isatty() + if interactive: + # logs to file and to terminal + log = Log(log_path=config.SECURITYLOG, interactive=True, log_syslog=False, log_level=log_level) + else: + log = Log(log_path=config.SECURITYLOG, interactive=False, log_level=log_level) + + # loading initial config + msec_config = config.MsecConfig(log, config=config.SECURITYCONF) + if not msec_config.load() and not force_level: + log.error(_("Level configuration not found, please run '%s -f <level>' to initialize.") % sys.argv[0]) + + # forcing new level + if force_level: + # first load the default configuration for level + levelconf = config.load_defaults(log, level) + params = levelconf.list_options() + if not params: + log.error(_("Default configuration for level '%s' not found, aborting.") % level) + sys.exit(1) + for opt in params: + msec_config.set(opt, levelconf.get(opt)) + + # load the msec library + msec = MSEC(log) + + # apply the config to msec + msec.apply(msec_config) + # writing back changes + msec.commit(commit) + # saving updated config + if force_level and commit: + if not msec_config.save(): + log.error(_("Unable to save config!")) + sys.exit(0) diff --git a/src/msec/msecgui b/src/msec/msecgui new file mode 100755 index 0000000..843eed0 --- /dev/null +++ b/src/msec/msecgui @@ -0,0 +1,34 @@ +#!/bin/sh +# +# Wrapper for msecgui +# + +if [ "`whoami`" != "root" ]; then + echo 'msec: sorry, you must be root !' + exit 1 +fi + +LCK=/var/run/msec.pid + +function cleanup() { + rm -f $LCK +} + +if [ -f $LCK ]; then + if [ -d /proc/`cat $LCK` ]; then + exit 0 + else + rm -f $LCK + fi +fi + +echo -n $$ > $LCK + +trap cleanup 0 + +MSEC=/usr/share/msec/msecgui.py +OPT="$@" + +$MSEC $OPT + +# msec ends here diff --git a/src/msec/msecgui.py b/src/msec/msecgui.py new file mode 100755 index 0000000..3e86506 --- /dev/null +++ b/src/msec/msecgui.py @@ -0,0 +1,725 @@ +#!/usr/bin/python -O +""" +This is graphical frontend to msec. +""" + +import os +import sys +import string +import getopt + +# PyGTK +import gtk +#import gtk.glade +import pygtk +import gobject +import pango + +# config +import config + +# version +try: + from version import version +except: + version = "development version" + +# libmsec +from libmsec import MSEC, Log + +import logging + +# localization +import gettext +try: + cat = gettext.Catalog('msec') + _ = cat.gettext +except IOError: + _ = str + +# localized help +try: + from help import HELP +except: + HELP = {} + +# text strings +BASIC_SECURITY_TEXT=_("""Basic security options. + +These options control the basic aspects of system security. You may select +a pre-defined profile, or customize the options. + +The following security profiles are defined in this version: + + - <b>None</b>: this profile disables additional system security, and it should + be used when you want to fine-tune the system on your own. + + - <b>Default</b>: this is the default profile, which configures a reasonably + safe set of security features. It activates several periodic system checks, + and mails their results daily to the selected email (by default, the local + 'root' account is used to receive such emails). + + - <b>Secure</b>: this profile is configured to provide maximum security, even + at the cost of limiting the remote access to the system. It also runs a wider + set of periodic checks, enforces the local password settings, and periodically + checks if the system security settings, configured here, were modified. +""") + +SYSTEM_SECURITY_TEXT=_("""System security options. + +These options control the local security configuration, such as the login restrictions, +password configurations, integration with other security tools, and default file creation +permissions. +""") + +NETWORK_SECURITY_TEXT=_("""Network security options. + +These options define the network security agains remote treats, unauthorized accesses, +and breakin attempts. +""") + +PERIODIC_SECURITY_TEXT=_("""Periodic security checks. + +These options configure the security checks that should be executed periodically. +""") + +NOTIFICATIONS_TEXT=_("""Security notifications. + +This page allows to configure the different ways the security notifications can be +delivered. + +It is possible to receive notifications by e-mail, using syslog, using an exclusive +log file, or using desktop environment notification system. +""") + +PERMISSIONS_SECURITY_TEXT=_("""File permissions. + +These options allow to fine-tune system permissions for important files and directores. + +The following permissions are checked periodically, and any change to the owner, group, +or current permission is reported. The permissions can be enforced, automatically +changing them to the specified values when a change is detected. +""") + +class MsecGui: + """Msec GUI""" + # common columns + (COLUMN_OPTION, COLUMN_DESCR, COLUMN_VALUE) = range(3) + (COLUMN_PATH, COLUMN_USER, COLUMN_GROUP, COLUMN_PERM, COLUMN_FORCE) = range(5) + + def __init__(self, log, msec, config, perms): + """Initializes gui""" + self.log = log + self.msec = msec + self.config = config + # save original configuration + self.oldconfig = {} + for opt in config.list_options(): + self.oldconfig[opt] = config.get(opt) + self.perms = perms + self.window = gtk.Window() + self.window.set_default_size(640, 480) + self.window.connect('destroy', self.quit) + + # are we enforcing a level + self.enforced_level = None + self.enforcing_level = False + + main_vbox = gtk.VBox(homogeneous=False, spacing=5) + self.window.add(main_vbox) + + # main frame + frame = gtk.Frame() + main_vbox.pack_start(frame) + + # notebook + self.notebook = gtk.Notebook() + frame.add(self.notebook) + + self.notebook.append_page(self.basic_security_page(), gtk.Label(_("Basic security"))) + self.notebook.append_page(self.system_security_page(), gtk.Label(_("System security"))) + self.notebook.append_page(self.network_security_page(), gtk.Label(_("Network security"))) + self.notebook.append_page(self.periodic_security_page(), gtk.Label(_("Periodic checks"))) + self.notebook.append_page(self.notifications_page(), gtk.Label(_("Security notifications"))) + self.notebook.append_page(self.permissions_security_page(), gtk.Label(_("Permissions"))) + + # control hbox + hbox = gtk.HBox(homogeneous=False, spacing=10) + main_vbox.pack_start(hbox, False, False) + + # control buttons + # TODO: improve spacing + cancel = gtk.Button(gtk.STOCK_CANCEL) + cancel.set_use_stock(True) + cancel.connect('clicked', self.cancel) + hbox.pack_start(cancel, expand=True, fill=True) + help = gtk.Button(gtk.STOCK_HELP) + help.set_use_stock(True) + help.connect('clicked', self.help) + hbox.pack_start(help, expand=True, fill=True) + ok = gtk.Button(gtk.STOCK_OK) + ok.set_use_stock(True) + ok.connect('clicked', self.ok) + hbox.pack_start(ok, expand=True, fill=True) + + self.window.show_all() + + def cancel(self, widget): + """Cancel button""" + print "Cancel clicked." + self.quit(widget) + + def help(self, widget): + """Help button""" + print "Help clicked." + + def ok(self, widget): + """Ok button""" + print "Ok clicked." + # first, let's reset previous msec data + self.msec.reset() + # start buffered logging + self.log.start_buffer() + # are we enforcing a level? + if self.enforcing_level: + print ">> Enforcing level %s" % self.enforced_level + curconfig = config.load_defaults(self.log, self.enforced_level) + else: + curconfig = self.config + # apply config and preview changes + self.msec.apply(curconfig) + msec.commit(False) + messages = self.log.get_buffer() + + # creating preview window + dialog = gtk.Dialog(_("Preview changes"), + self.window, 0, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OK, gtk.RESPONSE_OK) + ) + sw = gtk.ScrolledWindow() + sw.set_shadow_type(gtk.SHADOW_ETCHED_IN) + sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + dialog.vbox.add(sw) + + vbox = gtk.VBox() + dialog.vbox.set_size_request(640, 300) + sw.add_with_viewport(vbox) + label = gtk.Label(_("Click OK to commit changes, or CANCEL to leave current configuration unmodified.")) + vbox.pack_start(label, False, False) + + # informative label + label = gtk.Label(_('<b>New msec configuration:</b>')) + label.set_use_markup(True) + vbox.pack_start(label, False, False) + + # check for changed options + opt_changes = [] + for opt in self.oldconfig: + if curconfig.get(opt) != self.oldconfig[opt]: + opt_changes.append(opt) + + if len(opt_changes) > 0: + # some configuration parameters were changed + label = gtk.Label(_('<b>MSEC option changed:</b> <i>%s</i>\n') % ", ".join(opt_changes)) + label.set_use_markup(True) + label.set_line_wrap(True) + vbox.pack_start(label, False, False) + else: + label = gtk.Label(_('<b>No changes in MSEC options.</b>')) + label.set_use_markup(True) + vbox.pack_start(label, False, False) + + # see if there were any changes to system files + for msg in messages['info']: + if msg.find(config.MODIFICATIONS_FOUND) != -1 or msg.find(config.MODIFICATIONS_NOT_FOUND) != -1: + label = gtk.Label('<i>%s</i>' % msg) + label.set_line_wrap(True) + label.set_use_markup(True) + vbox.pack_start(label, False, False) + break + # a separator + vbox.pack_start(gtk.HSeparator(), False, False) + # adding specific messages + for cat in ['info', 'critical', 'error', 'warn', 'debug']: + msgs = messages[cat] + expander = gtk.Expander(_('Verbose information (%s): %d') % (cat, len(msgs))) + textview = gtk.TextView() + textview.set_wrap_mode(gtk.WRAP_WORD_CHAR) + textview.set_editable(False) + expander.add(textview) + count = 1 + for msg in msgs: + buffer = textview.get_buffer() + end = buffer.get_end_iter() + buffer.insert(end, "%d: %s\n" % (count, msg)) + count += 1 + vbox.pack_start(expander, False, False) + + dialog.show_all() + response = dialog.run() + if response != gtk.RESPONSE_OK: + dialog.destroy() + return + dialog.destroy() + + # well, let's commit it! + if self.enforcing_level: + # rewriting configuration + for opt in curconfig.list_options(): + self.config.set(opt, curconfig.get(opt)) + # saving the configuration + self.config.save() + msec.apply(self.config) + msec.commit(True) + self.quit(widget) + + def create_treeview(self, options): + """Creates a treeview from given list of options""" + sw = gtk.ScrolledWindow() + sw.set_shadow_type(gtk.SHADOW_ETCHED_IN) + sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + + # list of options + lstore = gtk.ListStore( + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING) + + # treeview + treeview = gtk.TreeView(lstore) + treeview.set_rules_hint(True) + treeview.set_search_column(self.COLUMN_DESCR) + + treeview.connect('row-activated', self.option_changed, lstore) + + # configuring columns + + # column for option names + column = gtk.TreeViewColumn(_('Security Option'), gtk.CellRendererText(), text=self.COLUMN_OPTION) + column.set_sort_column_id(self.COLUMN_OPTION) + treeview.append_column(column) + + # column for descriptions + renderer = gtk.CellRendererText() + renderer.set_property('wrap-width', 400) + renderer.set_property('wrap-mode', pango.WRAP_WORD_CHAR) + column = gtk.TreeViewColumn(_('Description'), renderer, text=self.COLUMN_DESCR) + column.set_sort_column_id(self.COLUMN_DESCR) + treeview.append_column(column) + + # column for values + column = gtk.TreeViewColumn(_('Value'), gtk.CellRendererText(), text=self.COLUMN_VALUE) + column.set_sort_column_id(self.COLUMN_VALUE) + treeview.append_column(column) + + sw.add(treeview) + + for option in options: + # retreiving option description + if not config.SETTINGS.has_key(option): + # invalid option + self.log.error(_("Invalid option '%s'!") % option) + continue + # getting level settings, callback and valid params + callback, params = config.SETTINGS[option] + # getting the function description + if option in HELP: + self.log.debug("found localized help for %s" % option) + doc = HELP[option] + else: + # get description from function comments + func = msec.get_action(callback) + if func: + doc = func.__doc__.strip() + else: + doc = callback + + # now for the value + value = self.config.get(option) + + # building the option + iter = lstore.append() + lstore.set(iter, + self.COLUMN_OPTION, option, + self.COLUMN_DESCR, doc, + self.COLUMN_VALUE, value, + ) + return sw + + def basic_security_page(self): + """Builds the basic security page""" + vbox = gtk.VBox(homogeneous=False) + + # security levels + + levels = config.SECURITY_LEVELS + + entry = gtk.Label(BASIC_SECURITY_TEXT) + entry.set_use_markup(True) + vbox.pack_start(entry, False, False) + + # Are we enforcing a new security level + entry = gtk.CheckButton(_("Enforce a new security level")) + + # security levels + frame = gtk.Frame() + frame.set_sensitive(False) + levels_vbox = gtk.VBox() + frame.add(levels_vbox) + # none + button = gtk.RadioButton(group=None, label=_("Pre-defined security level: NONE")) + button.connect('clicked', self.force_level, 'none') + levels_vbox.pack_start(button) + # default + button = gtk.RadioButton(group=button, label=_("Pre-defined security level: DEFAULT")) + button.connect('clicked', self.force_level, 'default') + button.set_active(True) + levels_vbox.pack_start(button) + # secure + button = gtk.RadioButton(group=button, label=_("Pre-defined security level: SECURE")) + button.connect('clicked', self.force_level, 'secure') + levels_vbox.pack_start(button) + + # adding callback for enable button + entry.connect('clicked', self.enforce_level, frame) + vbox.pack_start(entry, False, False) + # putting levels to vbox + vbox.pack_start(frame) + + return vbox + + def enforce_level(self, widget, options): + """Enforces a new security level""" + frame = options + if widget.get_active(): + # we are enforcing a level + self.enforcing_level = True + frame.set_sensitive(True) + # disable notebook pages + npages = self.notebook.get_n_pages() + for page in range(1, npages): + curpage = self.notebook.get_nth_page(page) + curpage.set_sensitive(False) + label = self.notebook.get_tab_label(curpage) + label.set_sensitive(False) + else: + frame.set_sensitive(False) + # enable notebook pages + npages = self.notebook.get_n_pages() + for page in range(1, npages): + curpage = self.notebook.get_nth_page(page) + curpage.set_sensitive(True) + label = self.notebook.get_tab_label(curpage) + label.set_sensitive(True) + # disable level enforcing + self.enforcing_level = False + + def force_level(self, widget, level): + """Defines a given security level""" + if widget.get_active(): + self.enforced_level = level + print level + + def notifications_page(self): + """Builds the notifications page""" + vbox = gtk.VBox(homogeneous=False) + + # security levels + + entry = gtk.Label(NOTIFICATIONS_TEXT) + entry.set_use_markup(True) + vbox.pack_start(entry, False, False) + + # basic security options + options_view = self.create_treeview(["TTY_WARN", "SYSLOG_WARN", "NOTIFY_WARN", "MAIL_WARN", "MAIL_USER", "MAIL_EMPTY_CONTENT"]) + vbox.pack_start(options_view) + + return vbox + + def system_security_page(self): + """Builds the network security page""" + vbox = gtk.VBox(homogeneous=False) + + entry = gtk.Label(SYSTEM_SECURITY_TEXT) + entry.set_use_markup(True) + vbox.pack_start(entry, False, False) + + # system security options + options_view = self.create_treeview(["ENABLE_APPARMOR", "ENABLE_POLICYKIT", + "ENABLE_SUDO", "ENABLE_MSEC_CRON", "ENABLE_PAM_WHEEL_FOR_SU", + "ENABLE_SULOGIN", "CREATE_SERVER_LINK", "ENABLE_AT_CRONTAB", + "ALLOW_ROOT_LOGIN", "ALLOW_USER_LIST", "ENABLE_PASSWORD", + "ALLOW_AUTOLOGIN", "ENABLE_CONSOLE_LOG", + "ENABLE_PAM_WHEEL_FOR_SU", "CREATE_SERVER_LINK", "ALLOW_XAUTH_FROM_ROOT", + "ALLOW_REBOOT", "SHELL_HISTORY_SIZE", "SHELL_TIMEOUT", "PASSWORD_LENGTH", + "PASSWORD_HISTORY", "USER_UMASK", "ROOT_UMASK", + ]) + vbox.pack_start(options_view) + + return vbox + + def network_security_page(self): + """Builds the network security page""" + vbox = gtk.VBox(homogeneous=False) + + entry = gtk.Label(NETWORK_SECURITY_TEXT) + entry.set_use_markup(True) + vbox.pack_start(entry, False, False) + + # network security options + options_view = self.create_treeview(["ACCEPT_BOGUS_ERROR_RESPONSES", "ACCEPT_BROADCASTED_ICMP_ECHO", + "ACCEPT_ICMP_ECHO", "ALLOW_REMOTE_ROOT_LOGIN", + "ALLOW_X_CONNECTIONS", "ALLOW_XSERVER_TO_LISTEN", + "AUTHORIZE_SERVICES", "ENABLE_DNS_SPOOFING_PROTECTION", + "ENABLE_IP_SPOOFING_PROTECTION", "ENABLE_LOG_STRANGE_PACKETS", + ]) + vbox.pack_start(options_view) + + return vbox + + def periodic_security_page(self): + """Builds the network security page""" + vbox = gtk.VBox(homogeneous=False) + + entry = gtk.Label(PERIODIC_SECURITY_TEXT) + vbox.pack_start(entry, False, False) + + self.periodic_checks = gtk.CheckButton(_("Enable periodic security checks")) + if self.config.get("CHECK_SECURITY") == "yes": + self.periodic_checks.set_active(True) + vbox.pack_start(self.periodic_checks, False, False) + + # network security options + options_view = self.create_treeview(["CHECK_PERMS", "CHECK_USER_FILES", "CHECK_SUID_ROOT", "CHECK_SUID_MD5", + "CHECK_SGID", "CHECK_WRITABLE", "CHECK_UNOWNED", + "CHECK_PROMISC", "CHECK_OPEN_PORT", "CHECK_PASSWD", + "CHECK_SHADOW", "CHECK_CHKROOTKIT", "CHECK_RPM", + "CHECK_SHOSTS" + ]) + vbox.pack_start(options_view) + + # see if these tests are enabled + self.periodic_checks.connect('clicked', self.periodic_tests, options_view) + periodic_checks = self.config.get("CHECK_SECURITY") + if periodic_checks == 'no': + # disable all periodic tests + options_view.set_sensitive(False) + + return vbox + + def periodic_tests(self, widget, options): + '''Enables/disables periodic security tests.''' + status = widget.get_active() + if status: + self.config.set("CHECK_SECURITY", "yes") + options.set_sensitive(True) + else: + self.config.set("CHECK_SECURITY", "no") + options.set_sensitive(False) + + def permissions_security_page(self): + """Builds the network security page""" + vbox = gtk.VBox(homogeneous=False) + + entry = gtk.Label(PERMISSIONS_SECURITY_TEXT) + vbox.pack_start(entry, False, False) + + sw = gtk.ScrolledWindow() + sw.set_shadow_type(gtk.SHADOW_ETCHED_IN) + sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + + # list of options + lstore = gtk.ListStore( + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_BOOLEAN) + + # treeview + treeview = gtk.TreeView(lstore) + treeview.set_rules_hint(True) + treeview.set_search_column(self.COLUMN_DESCR) + + # TODO: fix + treeview.connect('row-activated', self.option_changed, lstore) + + # configuring columns + + # column for path mask + column = gtk.TreeViewColumn(_('Path'), gtk.CellRendererText(), text=self.COLUMN_PATH) + column.set_sort_column_id(self.COLUMN_PATH) + treeview.append_column(column) + + # column for user + column = gtk.TreeViewColumn(_('User'), gtk.CellRendererText(), text=self.COLUMN_USER) + column.set_sort_column_id(self.COLUMN_USER) + treeview.append_column(column) + + # column for group + column = gtk.TreeViewColumn(_('Group'), gtk.CellRendererText(), text=self.COLUMN_GROUP) + column.set_sort_column_id(self.COLUMN_GROUP) + treeview.append_column(column) + + # column for permissions + column = gtk.TreeViewColumn(_('Permissions'), gtk.CellRendererText(), text=self.COLUMN_PERM) + column.set_sort_column_id(self.COLUMN_VALUE) + treeview.append_column(column) + + # column for force option + renderer = gtk.CellRendererToggle() + renderer.connect('toggled', self.toggle_enforced, lstore) + column = gtk.TreeViewColumn(_('Enforce'), renderer, active=self.COLUMN_FORCE) + column.set_sort_column_id(self.COLUMN_FORCE) + column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + column.set_fixed_width(50) + + treeview.append_column(column) + + sw.add(treeview) + + for file in self.perms.list_options(): + user_s, group_s, perm_s, force = self.perms.get(file) + + # convert to boolean + if force: + force = True + else: + force = False + + # building the option + iter = lstore.append() + lstore.set(iter, + self.COLUMN_PATH, file, + self.COLUMN_USER, user_s, + self.COLUMN_GROUP, group_s, + self.COLUMN_PERM, perm_s, + self.COLUMN_FORCE, force, + ) + vbox.pack_start(sw) + return vbox + + def toggle_enforced(self, cell, path, model): + '''Toggles a forced permission on an item''' + iter = model.get_iter((int(path),)) + fixed = model.get_value(iter, self.COLUMN_FORCE) + + # do something with the value + fixed = not fixed + + # set new value + model.set(iter, self.COLUMN_FORCE, fixed) + + def option_changed(self, treeview, path, col, model): + """Processes an option change""" + print path + iter = model.get_iter(path) + param = model.get_value(iter, self.COLUMN_OPTION) + descr = model.get_value(iter, self.COLUMN_DESCR) + value = model.get_value(iter, self.COLUMN_VALUE) + + callback, params = config.SETTINGS[param] + + # asks for new parameter value + dialog = gtk.Dialog(_("Select new value for %s") % (param), + self.window, 0, + (gtk.STOCK_OK, gtk.RESPONSE_OK, + gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) + label = gtk.Label(_("Modifying <b>%s</b>.\n<i>%s</i>\nCurrent value: <b>%s</b>") % (param, descr, value)) + label.set_line_wrap(True) + label.set_use_markup(True) + dialog.vbox.pack_start(label) + if '*' in params: + # string parameter + entry = gtk.Entry() + entry.set_text(value) + dialog.vbox.pack_start(entry) + else: + # combobox parameter + entry = gtk.combo_box_new_text() + for item in params: + entry.append_text(item) + if value not in params: + entry.append_text(value) + params.append(value) + active = params.index(value) + entry.set_active(active) + dialog.vbox.pack_start(entry) + + dialog.show_all() + response = dialog.run() + if response != gtk.RESPONSE_OK: + dialog.destroy() + return + + # process new parameter + if '*' in params: + newval = entry.get_text() + else: + newval = entry.get_active_text() + dialog.destroy() + + # update options + self.config.set(param, newval) + + model.set(iter, self.COLUMN_VALUE, newval) + + + def quit(self, param): + """Quits the application""" + print "Leaving.." + gtk.main_quit() + + +# {{{ usage +def usage(): + """Prints help message""" + print """Msec: Mandriva Security Center (%s). + +Arguments to msecgui: + -h, --help displays this helpful message. + -d enable debugging messages. +""" % version +# }}} + +if __name__ == "__main__": + log_level = logging.INFO + + # parse command line + try: + opt, args = getopt.getopt(sys.argv[1:], 'hd', ['help', 'debug']) + except getopt.error: + usage() + sys.exit(1) + for o in opt: + # help + if o[0] == '-h' or o[0] == '--help': + usage() + sys.exit(0) + # list + elif o[0] == '-d' or o[0] == '--debug': + log_level = logging.DEBUG + + # configuring logging + log = Log(interactive=True, log_syslog=False, log_file=True, log_level=log_level, log_path=config.SECURITYLOG) + + # loading initial config + msec_config = config.MsecConfig(log, config=config.SECURITYCONF) + if not msec_config.load(): + log.info(_("Unable to load config, using default values")) + + # loading permissions config + perm_conf = config.PermConfig(log, config=config.PERMCONF) + if not perm_conf.load(): + log.info(_("Unable to load permissions, using default values")) + + # creating an msec instance + msec = MSEC(log) + + log.info("Starting gui..") + + gui = MsecGui(log, msec, msec_config, perm_conf) + gtk.main() + diff --git a/src/msec/msecperms b/src/msec/msecperms new file mode 100755 index 0000000..3ce0c50 --- /dev/null +++ b/src/msec/msecperms @@ -0,0 +1,34 @@ +#!/bin/sh +# +# Wrapper for msecperms.py +# + +if [ "`whoami`" != "root" ]; then + echo 'msec: sorry, you must be root !' + exit 1 +fi + +LCK=/var/run/msec.pid + +function cleanup() { + rm -f $LCK +} + +if [ -f $LCK ]; then + if [ -d /proc/`cat $LCK` ]; then + exit 0 + else + rm -f $LCK + fi +fi + +echo -n $$ > $LCK + +trap cleanup 0 + +MSEC=/usr/share/msec/msecperms.py +OPT="$@" + +$MSEC $OPT + +# msec ends here diff --git a/src/msec/msecperms.py b/src/msec/msecperms.py new file mode 100755 index 0000000..a0f1676 --- /dev/null +++ b/src/msec/msecperms.py @@ -0,0 +1,152 @@ +#!/usr/bin/python -O +"""This file is responsible for permissions checking and +(optionally) enforcing. +""" + +import glob +import re +import string +import os +import stat +import pwd +import grp +import sys +import logging +import getopt + +# localization +import gettext + +try: + cat = gettext.Catalog('msec') + _ = cat.gettext +except IOError: + _ = str + +# config +import config + +# version +try: + from version import version +except: + version = "development version" + +# libmsec +from libmsec import Log, PERMS + +# {{{ usage +def usage(): + """Prints help message""" + print """Msec: Mandriva Security Center (%s). + +This applications verifies and (when required) enforces permissions +of certain files and directories. + +The list of permissions is stored in %s. + +Available parameters: + -h, --help displays this helpful message. + -l, --level <level> displays configuration for specified security + level. + -f, --force <level> force new level, overwriting user settings. + -e, --enforce <level> enforce permissions on all files. + -d enable debugging messages. + -p, --pretend only pretend to change the level, perform no real + actions. Use this to see what operations msec + will perform. +""" % (version, config.PERMCONF) +# }}} + +if __name__ == "__main__": + # default options + log_level = logging.INFO + force_level = False + level = config.DEFAULT_LEVEL + commit = True + enforce = False + + # parse command line + try: + opt, args = getopt.getopt(sys.argv[1:], 'hel:f:dp', ['help', 'enforce', 'list', 'force', 'debug', 'pretend']) + except getopt.error: + usage() + sys.exit(1) + for o in opt: + # help + if o[0] == '-h' or o[0] == '--help': + usage() + sys.exit(0) + # list + elif o[0] == '-l' or o[0] == '--list': + level = o[1] + log = Log(interactive=True, log_syslog=False, log_file=False) + permconf = config.load_default_perms(log, level) + params = permconf.list_options() + if not params: + print >>sys.stderr, _("Invalid security level '%s'.") % level + sys.exit(1) + for file in params: + user, group, perm, force = permconf.get(file) + if force: + print "!! forcing permissions on %s" % file + print "%s: %s.%s perm %s" % (file, user, group, perm) + sys.exit(0) + # force new level + elif o[0] == '-f' or o[0] == '--force': + level = o[1] + force_level = True + # debugging + elif o[0] == '-d' or o[0] == '--debug': + log_level = logging.DEBUG + # permission enforcing + elif o[0] == '-e' or o[0] == '--enforce': + enforce = True + # check-only mode + elif o[0] == '-p' or o[0] == '--pretend': + commit = False + + # verifying use id + if os.geteuid() != 0: + print >>sys.stderr, _("Msec: Mandriva Security Center (%s)\n") % version + print >>sys.stderr, _("Error: This application must be executed by root!") + print >>sys.stderr, _("Run with --help to get help.") + sys.exit(1) + + # configuring logging + interactive = sys.stdin.isatty() + if interactive: + # logs to file and to terminal + log = Log(log_path=config.SECURITYLOG, interactive=True, log_syslog=False, log_level=log_level) + else: + log = Log(log_path=config.SECURITYLOG, interactive=False, log_level=log_level) + + # loading permissions + permconf = config.PermConfig(log, config=config.PERMCONF) + if not permconf.load() and not force_level: + log.error(_("Permissions configuration not found, please run '%s -f <level>' to initialize.") % sys.argv[0]) + + # forcing new level + if force_level: + # first load the default configuration for level + default_permconf = config.load_default_perms(log, level) + params = default_permconf.list_options() + if not params: + log.error(_("Default configuration for level '%s' not found, aborting.") % level) + sys.exit(1) + for opt in params: + permconf.set(opt, default_permconf.get(opt)) + + # load the main permission class + perm = PERMS(log) + + # check permissions + changed_files = perm.check_perms(permconf) + + # writing back changes + perm.commit(really_commit=commit, enforce=force_level) + # saving updated config + if force_level and commit: + if not permconf.save(): + log.error(_("Unable to save config!")) + sys.exit(0) diff --git a/src/msec/version.py b/src/msec/version.py new file mode 100644 index 0000000..3e3074d --- /dev/null +++ b/src/msec/version.py @@ -0,0 +1 @@ +version='0.60.1' diff --git a/src/msec_find/Makefile b/src/msec_find/Makefile index 3aec03f..e85221f 100644 --- a/src/msec_find/Makefile +++ b/src/msec_find/Makefile @@ -2,7 +2,7 @@ CC=gcc NAME=msec_find CFLAGS = -ggdb -Wall -Wmissing-prototypes -Wmissing-declarations \ --Wpointer-arith -m486 -O2 -finline-functions -fkeep-inline-functions \ +-Wpointer-arith -O2 -finline-functions -fkeep-inline-functions \ -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 OBJ=find.o diff --git a/src/promisc_check/Makefile b/src/promisc_check/Makefile index 8c87e07..776dde2 100644 --- a/src/promisc_check/Makefile +++ b/src/promisc_check/Makefile @@ -2,7 +2,7 @@ CC=gcc NAME=promisc_check CFLAGS = -ggdb -Wall -Wmissing-prototypes -Wmissing-declarations \ --Wpointer-arith -m486 -O2 -finline-functions -fkeep-inline-functions +-Wpointer-arith -O2 -finline-functions -fkeep-inline-functions OBJ=promisc_check.o |