class InstallVLock(Rule): '''This class installs the vlock package to enable screen locking vlock is the package name on opensuse 15+, debian, ubuntu kbd is the package name on opensuse 42.3-, rhel, fedora, centos (contains vlock package) references: https://pkgs.org/download/vlock https://access.redhat.com/discussions/3543671 ''' def __init__(self, config, environ, logger, statechglogger): """ Constructor """ Rule.__init__(self, config, environ, logger, statechglogger) self.logger = logger self.rulenumber = 121 self.rulename = "InstallVLock" self.mandatory = True self.rootrequired = True self.formatDetailedResults("initialize") self.guidance = ["NSA 2.3.5.6"] self.applicable = {'type': 'white', 'family': ['linux', 'freebsd']} # Configuration item instantiation datatype = 'bool' key = 'INSTALLVLOCK' instructions = "To disable installation of the command line " + \ "screen lock program vlock set the value of INSTALLVLOCK to False." default = True self.ci = self.initCi(datatype, key, instructions, default) self.sethelptext() def set_pkg(self): '''set package name based on distro''' majorver = self.environ.getosmajorver() if self.ph.manager in ["yum", "dnf"]: self.pkg = "kbd" elif bool(self.ph.manager == "zypper" and majorver == "15"): self.pkg = "kbd" else: self.pkg = "vlock" def report(self): '''Perform a check to see if package is already installed. If so, there is no need to run Fix method :returns: self.compliant :rtype: bool @author: Derek T Walker ''' try: self.detailedresults = "" self.ph = Pkghelper(self.logger, self.environ) self.ch = CommandHelper(self.logger) self.compliant = True self.set_pkg() if not self.ph.check(self.pkg): self.compliant = False self.detailedresults += "\nvlock Package is NOT installed" else: self.detailedresults += "\nvlock Package is installed" except (KeyboardInterrupt, SystemExit): raise except Exception: self.compliant = False self.detailedresults += "\n" + traceback.format_exc() self.logdispatch.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.compliant def fix(self): '''The fix method will apply the required settings to the system. self.rulesuccess will be updated if the rule does not succeed. Attempt to install Vlock, record success or failure in event logger. :returns: self.rulesuccess :rtype: bool @author: Derek T Walker ''' try: self.detailedresults = "" self.rulesuccess = True self.iditerator = 0 if not self.ci.getcurrvalue(): return self.rulesuccess # Clear out event history so only the latest fix is recorded eventlist = self.statechglogger.findrulechanges(self.rulenumber) for event in eventlist: self.statechglogger.deleteentry(event) undocmd = self.ph.getRemove() if not self.ph.install(self.pkg): self.rulesuccess = False self.detailedresults += "\nFailed to install vlock package" else: undocmd += self.pkg self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "comm", "command": undocmd} self.statechglogger.recordchgevent(myid, event) self.detailedresults += "\nvlock Package was installed successfully" except (KeyboardInterrupt, SystemExit): raise except Exception: self.rulesuccess = False self.detailedresults += "\n" + traceback.format_exc() self.logdispatch.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.rulesuccess
class RestrictMounting(Rule): '''Class help text''' def __init__(self, config, enviro, logger, statechglogger): """ Constructor """ Rule.__init__(self, config, enviro, logger, statechglogger) self.logger = logger self.rulenumber = 112 self.rulename = "RestrictMounting" self.formatDetailedResults("initialize") self.mandatory = False self.sethelptext() # Configuration item instantiation datatype1 = "bool" key1 = "RESTRICTCONSOLEACCESS" instructions1 = "To restrict console device access, set " + \ "RESTRICTCONSOLEACCESS to True." default1 = False self.consoleCi = self.initCi(datatype1, key1, instructions1, default1) datatype2 = "bool" key2 = "DISABLEAUTOFS" instructions2 = "To disable dynamic NFS mounting through the " + \ "autofs service, set DISABLEAUTOFS to True." default2 = False self.autofsCi = self.initCi(datatype2, key2, instructions2, default2) datatype3 = "bool" key3 = "DISABLEGNOMEAUTOMOUNT" instructions3 = "To disable the GNOME desktop environment from " + \ "automounting devices and removable media, set " + \ "DISABLEGNOMEAUTOMOUNT to True." default3 = False self.gnomeCi = self.initCi(datatype3, key3, instructions3, default3) self.guidance = ["NSA 2.2.2.1", "NSA 2.2.2.3", "NSA 2.2.2.4", "CCE 3685-5", "CCE 4072-5", "CCE 4231-7", "CCE-RHEL7-CCE-TBD 2.2.2.6"] self.applicable = {"type": "white", "family": ["linux"]} self.iditerator = 0 self.gsettings = "/usr/bin/gsettings" self.gconftool = "/usr/bin/gconftool-2" self.dbuslaunch = "/usr/bin/dbus-launch" def report(self): ''' ''' self.automountMedia = True self.automountDrives = True self.sec_console_perms1 = "/etc/security/console.perms.d/50-default.perms" self.sec_console_perms2 = "/etc/security/console.perms" self.console_perms_temppath = self.sec_console_perms2 + ".stonixtmp" self.data = {"<console>": "tty[0-9][0-9]* vc/[0-9][0-9]* :0\.[0-9] :0", "<xconsole>": "0\.[0-9] :0"} self.autofspkg = "autofs" self.autofssvc = "autofs" self.ph = Pkghelper(self.logdispatch, self.environ) self.sh = ServiceHelper(self.environ, self.logdispatch) self.ch = CommandHelper(self.logdispatch) self.compliant = True self.detailedresults = "" try: if os.path.exists(self.sec_console_perms1): current_config = readFile(self.sec_console_perms1, self.logger) for line in current_config: if re.search("^<[x]?console>", line, re.M): self.compliant = False self.detailedresults += self.sec_console_perms1 + " contains unrestricted device access\n" break if os.path.exists(self.sec_console_perms2): self.editor2 = KVEditorStonix(self.statechglogger, self.logger, "conf", self.sec_console_perms2, self.console_perms_temppath, self.data, "present", "closedeq") if not self.editor2.report(): self.compliant = False self.detailedresults += self.sec_console_perms2 + " does not contain the correct values\n" if self.ph.check(self.autofspkg): if self.sh.auditService(self.autofssvc, _="_"): self.compliant = False self.detailedresults += "autofs is installed and enabled\n" if os.path.exists(self.gsettings): automountOff = False autorunNever = False cmd = [self.gsettings, "get", "org.gnome.desktop.media-handling", "automount"] self.ch.executeCommand(cmd) if re.search("false", self.ch.getOutputString()): automountOff = True cmd = [self.gsettings, "get", "org.gnome.desktop.media-handling", "autorun-never"] self.ch.executeCommand(cmd) if re.search("true", self.ch.getOutputString()): autorunNever = True debug = "autorun-never is enabled" self.logger.log(LogPriority.DEBUG, debug) self.automountOff = automountOff self.autorunNever = autorunNever if not automountOff or not autorunNever: self.compliant = False self.detailedresults += "GNOME automounting is enabled\n" # check for gnome automounting if os.path.exists(self.gconftool): cmd = [self.gconftool, "-R", "/desktop/gnome/volume_manager"] if os.path.exists("/desktop/gnome/volume_manager"): self.ch.executeCommand(cmd) retcode = self.ch.getReturnCode() if retcode != 0: self.compliant = False self.detailedresults += "\nFailed to read gnome volume manager config" output = self.ch.getOutputString() if re.search("automount_media.*false", output): self.automountMedia = False if re.search("automount_drives.*false", output): self.automountDrives = False else: self.automountMedia = False self.automountDrives = False mylist = [self.automountMedia, self.automountDrives] if any(mylist): self.compliant = False self.detailedresults += "GNOME automounting is enabled\n" # reset these directories to be owned by their respective users dirs = '' if os.path.exists('/run/user'): dirs = os.listdir('/run/user') if dirs: for d in dirs: # check if the directory is an integer representing a uid if re.search('^([+-]?[1-9]\d*|0)$', d, re.IGNORECASE): self.logger.log(LogPriority.DEBUG, "Found UID directory") try: os.chown('/run/user/' + d + '/dconf/user', int(d), int(d)) except Exception as errmsg: self.logger.log(LogPriority.DEBUG, str(errmsg)) continue else: self.logger.log(LogPriority.DEBUG, "no directories in /run/user") except (KeyboardInterrupt, SystemExit): raise except Exception: self.compliant = False self.detailedresults += "\n" + traceback.format_exc() self.logdispatch.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.compliant def fix(self): ''' ''' self.detailedresults = "" self.iditerator = 0 self.rulesuccess = True success = True consoleaccess = self.consoleCi.getcurrvalue() autofs = self.autofsCi.getcurrvalue() gnomeautomount = self.gnomeCi.getcurrvalue() mylist = [consoleaccess, autofs, gnomeautomount] try: # if none of the CIs are enabled, skip fix if not any(mylist): self.detailedresults += "\nNone of the CI's were enabled. Nothing was done." self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.rulesuccess # clear event list data eventlist = self.statechglogger.findrulechanges(self.rulenumber) for event in eventlist: self.statechglogger.deleteentry(event) # if restrict console access CI is enabled, restrict console access if self.consoleCi.getcurrvalue(): if os.path.exists(self.sec_console_perms1): tmpfile = self.sec_console_perms1 + ".stonixtmp" defaultPerms = open(self.sec_console_perms1, "r").read() defaultPerms = re.sub("(<[x]?console>)", r"#\1", defaultPerms) if writeFile(tmpfile, defaultPerms, self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "conf", "filepath": self.sec_console_perms1} self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange(self.sec_console_perms1, tmpfile, myid) os.rename(tmpfile, self.sec_console_perms1) resetsecon(self.sec_console_perms1) else: success = False self.detailedresults += "Problem writing new contents to " + \ "temporary file" if os.path.exists(self.sec_console_perms2): self.editor = KVEditorStonix(self.statechglogger, self.logger, "conf", self.sec_console_perms2, self.console_perms_temppath, self.data, "present", "closedeq") self.editor.report() if self.editor.fixables: if self.editor.fix(): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.editor.setEventID(myid) if self.editor.commit(): debug = self.sec_console_perms2 + "'s contents have been " \ + "corrected\n" self.logger.log(LogPriority.DEBUG, debug) resetsecon(self.sec_console_perms2) else: debug = "kveditor commit not successful\n" self.logger.log(LogPriority.DEBUG, debug) success = False self.detailedresults += self.sec_console_perms2 + \ " properties could not be set\n" else: debug = "kveditor fix not successful\n" self.logger.log(LogPriority.DEBUG, debug) success = False self.detailedresults += self.sec_console_perms2 + \ " properties could not be set\n" # if autofs CI is enabled, disable autofs if self.autofsCi.getcurrvalue(): if self.ph.check(self.autofspkg) and \ self.sh.auditService(self.autofssvc, _="_"): if self.sh.disableService(self.autofssvc, _="_"): debug = "autofs service successfully disabled\n" self.logger.log(LogPriority.DEBUG, debug) self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "servicehelper", "servicename": self.autofssvc, "startstate": "enabled", "endstate": "disabled"} self.statechglogger.recordchgevent(myid, event) else: success = False debug = "Unable to disable autofs service\n" self.logger.log(LogPriority.DEBUG, debug) returnCode = 0 if self.gnomeCi.getcurrvalue(): if os.path.exists(self.gsettings): # gsettings requires a D-Bus session bus in order to make # any changes. This is because the dconf daemon must be # activated using D-Bus. if not os.path.exists(self.dbuslaunch): self.ph.install("dbus-x11") if os.path.exists(self.dbuslaunch): if not self.automountOff: cmd = [self.dbuslaunch, self.gsettings, "set", "org.gnome.desktop.media-handling", "automount", "false"] self.ch.executeCommand(cmd) returnCode = self.ch.getReturnCode() if not returnCode: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "comm", "command": ["dbus-launch", "gsettings", "set", "org.gnome.desktop.media-handling", "automount", "true"]} self.statechglogger.recordchgevent(myid, event) if not self.autorunNever: cmd = [self.dbuslaunch, "gsettings", "set", "org.gnome.desktop.media-handling", "autorun-never", "true"] self.ch.executeCommand(cmd) returnCode += self.ch.getReturnCode() if not self.ch.getReturnCode(): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "comm", "command": [self.dbuslaunch, self.gsettings, "set", "org.gnome.desktop.media-handling", "autorun-never", "false"]} self.statechglogger.recordchgevent(myid, event) else: success = False debug = "Unable to disable GNOME automounting: " + \ "dbus-x11 is not installed" self.logger.log(LogPriority.DEBUG, debug) if os.path.exists(self.gconftool): if self.automountMedia: cmd = [self.gconftool, "--direct", "--config-source", "xml:readwrite:/etc/gconf/gconf.xml.mandatory", "--type", "bool", "--set", "/desktop/gnome/volume_manager/automount_media", "false"] self.ch.executeCommand(cmd) returnCode = self.ch.getReturnCode() if not returnCode: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "comm", "command": [self.gconftool, "--direct", "--config-source", "xml:readwrite:" + "/etc/gconf/gconf.xml.mandatory", "--type", "bool", "--set", "/desktop/gnome/volume_manager/" + "automount_media", "true"]} self.statechglogger.recordchgevent(myid, event) if self.automountDrives: cmd = [self.gconftool, "--direct", "--config-source", "xml:readwrite:/etc/gconf/gconf.xml.mandatory", "--type", "bool", "--set", "/desktop/gnome/volume_manager/automount_drives", "false"] self.ch.executeCommand(cmd) returnCode += self.ch.getReturnCode() if not self.ch.getReturnCode(): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "comm", "command": [self.gconftool, "--direct", "--config-source", "xml:readwrite:" + "/etc/gconf/gconf.xml.mandatory", "--type", "bool", "--set", "/desktop/gnome/volume_manager/" + "automount_drives", "true"]} self.statechglogger.recordchgevent(myid, event) if returnCode: success = False self.detailedresults += "Fix failed to disable GNOME automounting\n" # reset these directories to be owned by their respective users dirs = '' if os.path.exists('/run/user'): dirs = os.listdir('/run/user') if dirs: for d in dirs: # check if the directory is an integer representing a uid if re.search('^([+-]?[1-9]\d*|0)$', d, re.IGNORECASE): self.logger.log(LogPriority.DEBUG, "Found UID directory") try: os.chown('/run/user/' + d + '/dconf/user', int(d), int(d)) except Exception as errmsg: self.logger.log(LogPriority.DEBUG, str(errmsg)) continue else: self.logger.log(LogPriority.DEBUG, "no directories in /run/user") self.rulesuccess = success except (KeyboardInterrupt, SystemExit): raise except Exception: self.rulesuccess = False self.detailedresults += "\n" + traceback.format_exc() self.logdispatch.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.rulesuccess
class ConfigureMACPolicy(Rule): """The ConfigureMACPolicy class configures either selinux or apparmor depending on the os platform. @change: Derek Walker - created two config items, one for enable/disable, and another for whether the user wants to use permissive or enforcing """ def __init__(self, config, environ, logger, statechglogger): Rule.__init__(self, config, environ, logger, statechglogger) self.logger = logger self.rulenumber = 107 self.rulename = 'ConfigureMACPolicy' self.formatDetailedResults("initialize") self.mandatory = True self.sethelptext() self.guidance = ['NSA(2.1.1.6)(2.4.2)', 'CCE-3977-6', 'CCE-3999-0', 'CCE-3624-4', 'CIS 1.7'] self.applicable = {'type': 'white', 'family': ['linux']} datatype = "bool" key = "CONFIGUREMAC" instructions = "To prevent the configuration of a mandatory " + \ "access control policy, set the value of CONFIGUREMAC to " + \ "False. Note: The 'mandatory access control' is either SELinux " + \ "or AppArmor, depending on what is available to your current system." default = True self.ConfigureMAC = self.initCi(datatype, key, instructions, default) datatype2 = "string" key2 = "MODE" default2 = "permissive" instructions2 = "Valid modes for SELinux are: permissive or " + \ "enforcing\nValid modes for AppArmor are: complain or enforce" self.modeci = self.initCi(datatype2, key2, instructions2, default2) def report(self): """ :return: """ self.selinux = False self.apparmor = False self.ph = Pkghelper(self.logger, self.environ) self.ch = CommandHelper(self.logger) self.sh = ServiceHelper(self.environ, self.logger) selinux_packages = ["selinux", "libselinux", "selinux-basics"] apparmor_packages = ["apparmor"] self.mac_package = "" # discover whether this system can use - or is using - selinux # or if it should use apparmor (selinux takes precedence) for p in selinux_packages: if self.ph.check(p): self.selinux = True self.mac_package = p break if not self.selinux: for p in apparmor_packages: if self.ph.check(p): self.apparmor = True self.mac_package = p break if not bool(self.selinux or self.apparmor): for p in selinux_packages: if self.ph.checkAvailable(p): self.selinux = True self.mac_package = p break if not self.selinux: for p in apparmor_packages: if self.ph.checkAvailable(p): self.apparmor = True self.mac_package = p break self.compliant = True self.detailedresults = "" self.mode = str(self.modeci.getcurrvalue()) try: if self.selinux: if not self.reportSelinux(): self.compliant = False elif self.apparmor: if not self.reportApparmor(): self.compliant = False else: self.detailedresults += "\nCould not find either selinux or apparmor installed and nother appears to be available to this system!" self.compliant = False except (KeyboardInterrupt, SystemExit): raise except: self.compliant = False self.detailedresults += "\n" + traceback.format_exc() self.logger.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logger.log(LogPriority.INFO, self.detailedresults) return self.compliant def set_selinux_conf_path(self): """ :return: """ # discover correct location of selinux config file self.selinux_config_file = "/etc/sysconfig/selinux" selinux_config_files = ["/etc/selinux/config", "/etc/sysconfig/selinux"] for p in selinux_config_files: if os.path.exists(p): self.selinux_config_file = p break def reportSelinux(self): """ :return: """ compliant = True conf_option_dict = {"selinux\s+status:\s+enabled": "status", "current\s+mode:\s+" + self.mode: "mode"} # check if selinux is installed if not self.ph.check(self.mac_package): compliant = False self.detailedresults += "\nSELinux is not installed" # check sestatus for current configuration of selinux self.ch.executeCommand("/usr/sbin/sestatus") output = self.ch.getOutput() for co in conf_option_dict: rco = re.compile(co, re.I) if not list(filter(rco.match, output)): compliant = False self.detailedresults += "\nSELinux " + conf_option_dict[co] + " is not configured properly" self.set_selinux_conf_path() # check selinux config file for correct configuration so setting is # persistent after reboot selinux_tmp_file = self.selinux_config_file + ".stonixtmp" selinux_config_dict = {"SELINUX": self.mode} self.selinux_config_editor = KVEditorStonix(self.statechglogger, self.logger, "conf", self.selinux_config_file, selinux_tmp_file, selinux_config_dict, "present", "closedeq") self.selinux_config_editor.report() if self.selinux_config_editor.fixables: compliant = False self.detailedresults += "\nFollowing option(s) not configured correctly in " + self.selinux_config_file + " :\n" + "\n".join(self.selinux_config_editor.fixables) return compliant def reportApparmor(self): """ :return: """ compliant = True aa_enabled = "Yes" # check if apparmor is installed if not self.ph.check(self.mac_package): compliant = False self.detailedresults += "\nApparmor is not installed" # check if apparmor is enabled self.ch.executeCommand("/usr/bin/aa-enabled") output = self.ch.getOutputString() if not re.search(aa_enabled, output): compliant = False self.detailedresults += "\nApparmor is not enabled" # check if boot configuration for apparmor is correct f = open("/etc/default/grub", "r") contents = f.readlines() f.close() for line in contents: if re.search("GRUB_CMDLINE_LINUX_DEFAULT=", line): if not re.search("apparmor=1", line): compliant = False self.detailedresults += "\nApparmor not enabled in boot config" elif not re.search("security=apparmor", line): compliant = False self.detailedresults += "\nApparmor not enabled in boot config" break return compliant def fix(self): """ :return: """ self.rulesuccess = True self.detailedresults = "" self.iditerator = 0 try: if self.selinux: if not self.fixSelinux(): self.rulesuccess = False elif self.apparmor: if not self.fixApparmor(): self.rulesuccess = False else: self.rulesuccess = False self.detailedresults += "\nNeither SELinux nor Apparmor appears to be available to this system!" except (KeyboardInterrupt, SystemExit): raise except: self.rulesuccess = False self.detailedresults += "\n" + traceback.format_exc() self.logger.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) self.logger.log(LogPriority.INFO, self.detailedresults) return self.rulesuccess def fixSelinux(self): """ :return: """ success = True self.iditerator = 0 activate_utility = "/usr/sbin/selinux-activate" # install selinux package if it is not installed if not self.ph.check(self.mac_package): if not self.ph.install(self.mac_package): success = False self.detailedresults += "\nFailed to install selinux package" else: # if we just installed selinux, then we need to discover the correct conf path self.set_selinux_conf_path() self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype":"pkghelper", "pkgname":self.mac_package} self.statechglogger.recordchgevent(myid, event) if os.path.exists(activate_utility): self.ch.executeCommand(activate_utility) output = self.ch.getOutputString() if re.search("need to reboot", output, re.I): self.detailedresults += "\nSElinux has been configured, but you will need to reboot before selinux can become active. This rule will not report compliant until this is done." # set enforcement mode for selinux self.ch.executeCommand("/usr/sbin/setenforce " + self.mode) retcode = self.ch.getReturnCode() if retcode != 0: success = False self.detailedresults += "\nFailed to set selinux mode to: " + self.mode self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.selinux_config_editor.setEventID(myid) # configure the selinux config file for persistence through reboot if not self.selinux_config_editor.fix(): success = False self.detailedresults += "\nFailed to fix " + self.selinux_config_file elif not self.selinux_config_editor.commit(): success = False self.detailedresults += "\nFailed to fix " + self.selinux_config_file return success def fixApparmor(self): """ :return: """ success = True profiles_dir = "/etc/apparmor.d/" valid_modes = ["complain", "enforce"] grub_file = "/etc/default/grub" tmp_grub_file = grub_file + ".stonixtmp" # install apparmor package if not installed if not self.ph.check(self.mac_package): if not self.ph.install(self.mac_package): success = False self.detailedresults += "\nFailed to install apparmor package" else: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype":"pkghelper", "pkgname":self.mac_package} self.statechglogger.recordchgevent(myid, event) if not self.ph.check("apparmor-profiles"): self.ph.install("apparmor-profiles") if not self.ph.check("apparmor-utils"): self.ph.install("apparmor-utils") # set apparmor enforcement mode if self.mode.lower() in ["complain", "permissive"]: self.ch.executeCommand("/usr/sbin/aa-complain " + profiles_dir + "*") retcode = self.ch.getReturnCode() if retcode != 0: success = False self.detailedresults += "\nFailed to set apparmor profiles to complain mode" elif self.mode.lower() in ["enforce", "enforcing"]: if os.path.exists(profiles_dir): self.ch.executeCommand("/usr/sbin/aa-enforce " + profiles_dir + "*") retcode = self.ch.getReturnCode() if retcode != 0: success = False self.detailedresults += "\nFailed to set apparmor profiles to enforce mode" else: self.logger.log(LogPriority.DEBUG, "apparmor profiles directory does not exist") success = False self.detailedresults += "\nFailed to set apparmor mode to: " + self.mode else: success = False self.detailedresults += "\nPlease specify one of the following options in the MODE field:\n" + "\n".join(valid_modes) # correct apparmor boot config # (can't use kveditor because it can't handle appending to a config line) if os.path.exists(grub_file): f = open(grub_file, "r") contents = f.readlines() f.close() for n, i in enumerate(contents): if re.search("GRUB_CMDLINE_LINUX_DEFAULT=", i): contents[n] = i.strip()[:-1]+' apparmor=1 security=apparmor"\n' tf = open(tmp_grub_file, "w") tf.writelines(contents) tf.close() self.iditerator += 1 event = {"eventtype":"conf", "filepath":grub_file} myid = iterate(self.iditerator, self.rulenumber) self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange(grub_file, tmp_grub_file, myid) os.rename(tmp_grub_file, grub_file) # run update-grub to apply the new grub config self.ch.executeCommand("/usr/sbin/update-grub") return success
class SystemAccounting(Rule): """ """ def __init__(self, config, environ, logger, statechglogger): """ Constructor """ Rule.__init__(self, config, environ, logger, statechglogger) self.logger = logger self.rulenumber = 9 self.rulename = 'SystemAccounting' self.formatDetailedResults("initialize") self.mandatory = False self.rootrequired = True self.sethelptext() self.guidance = ['CIS 2.4', 'cce-3992-5'] self.applicable = { 'type': 'white', 'family': 'linux', 'os': { 'Mac OS X': ['10.15', 'r', '10.15.10'] } } # set up configuration item for this rule datatype = 'bool' key = 'SYSTEMACCOUNTING' instructions = "This is an optional rule and is disabled by default, due to the significant load it can place on the system when enabled. To enable system accounting, set the value of SYSTEMACCOUNTING to True." default = False self.ci = self.initCi(datatype, key, instructions, default) self.ostype = self.environ.getostype() self.ph = Pkghelper(self.logger, self.environ) self.ch = CommandHelper(self.logger) self._set_paths() def _set_paths(self): """ """ self.sysstat_package = "sysstat" self.sysstat_service_file = "" sysstat_service_locs = [ "/usr/lib/systemd/system/sysstat.service", "/lib/systemd/system/sysstat.service", "/etc/init.d/sysstat" ] for ss in sysstat_service_locs: if os.path.isfile(ss): self.sysstat_service_file = ss break self.accton = "/usr/sbin/accton" self.acct_file = "/var/account/acct" self.cron_file = "/etc/cron.d/sysstat" self.sa1 = "" sa1_locs = [ "/usr/lib64/sa/sa1", "/usr/local/lib64/sa/sa1", "/usr/lib/sysstat/sa1" ] for sl in sa1_locs: if os.path.isfile(sl): self.sa1 = sl break self.sa2 = "" sa2_locs = [ "/usr/lib64/sa/sa2", "/usr/local/lib64/sa/sa2", "/usr/lib/sysstat/sa2" ] for sl in sa2_locs: if os.path.isfile(sl): self.sa2 = sl break self.sysstat_service_contents = """# /usr/lib/systemd/system/sysstat.service # (C) 2012 Peter Schiffer (pschiffe <at> redhat.com) # # sysstat-10.1.7 systemd unit file: # Insert a dummy record in current daily data file. # This indicates that the counters have restarted from 0. [Unit] Description=Resets System Activity Logs [Service] Type=oneshot RemainAfterExit=yes User=root ExecStart=""" + self.sa1 + """ --boot [Install] WantedBy=multi-user.target """ self.sysstat_cron_contents = """# Run system activity accounting tool every 60 minutes */60 * * * * root """ + self.sa1 + """ 1 1 # Generate a daily summary of process accounting at 23:53 53 23 * * * root """ + self.sa2 + """ -A""" def _report_configuration(self): """ :return: compliant :rtype: bool """ compliant = True if self.ostype == "Mac OS X": self.conf_file = "" conf_files = ["/etc/rc.conf", "/etc/rc.common"] for cf in conf_files: if os.path.isfile(cf): self.conf_file = cf break tmpfile = self.conf_file + ".stonixtmp" config_data = {"accounting_enable": "YES"} self.conf_editor = KVEditorStonix(self.statechglogger, self.logger, "conf", self.conf_file, tmpfile, config_data, "present", "closedeq") if not self.conf_editor.report(): compliant = False else: if not os.path.isfile(self.sysstat_service_file): compliant = False self.detailedresults += "\nSystem accounting service file is missing" else: f = open(self.sysstat_service_file, "r") contents = f.read() f.close() if self.sysstat_service_file != "/etc/init.d/sysstat": if contents != self.sysstat_service_contents: compliant = False self.detailedresults += "\nSystem accounting service file has incorrect contents" if os.path.isfile("/etc/default/sysstat"): f = open("/etc/default/sysstat", "r") contents = f.read() f.close() if not re.search('ENABLED="true"', contents): compliant = False self.detailedresults += "\n/etc/default/sysstat file has incorrect contents" return compliant def _report_installation(self): """ :return: compliant :rtype: bool """ compliant = True if self.ostype != "Mac OS X": if not self.ph.check(self.sysstat_package): compliant = False self.detailedresults += "\nSystem accounting package is not installed" return compliant def _report_schedule(self): """ :return: compliant :rtype: bool """ compliant = True if self.ostype == "Mac OS X": if not os.path.isfile(self.acct_file): compliant = False self.detailedresults += "\nSystem accounting is not enabled on this system" else: if not os.path.isfile(self.cron_file): compliant = False else: f = open(self.cron_file, "r") contents = f.read() f.close() if contents != self.sysstat_cron_contents: self.compliant = False self.detailedresults += "\nSystem account cron job has incorrect contents" return compliant def report(self): """ :return: compliant :rtype: bool """ self.detailedresults = "" self.compliant = True try: if not self._report_installation(): self.compliant = False else: if not self._report_configuration(): self.compliant = False if not self._report_schedule(): self.compliant = False except (KeyboardInterrupt, SystemExit): raise except: self.compliant = False self.detailedresults += "\n" + traceback.format_exc() self.logger.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logger.log(LogPriority.INFO, self.detailedresults) return self.compliant def _fix_installation(self): """ :return: success :rtype: bool """ success = True if self.ostype != "Mac OS X": if not self.ph.install(self.sysstat_package): success = False self.logger.log(LogPriority.DEBUG, "Failed to install sysstat package") return success def _fix_configuration(self): """ :return: success :rtype: bool """ success = True if self.ostype == "Mac OS X": if not self.conf_editor.fix(): success = False self.logger.log(LogPriority.DEBUG, "kveditor failed to fix()") elif not self.conf_editor.commit(): success = False self.logger.log(LogPriority.DEBUG, "kveditor failed to commit()") else: try: if self.sysstat_service_file != "/etc/init.d/sysstat": f = open(self.sysstat_service_file, "w") f.write(self.sysstat_service_contents) f.close() except: success = False if os.path.isfile("/etc/default/sysstat"): default_sysstat_contents = """# # Default settings for /etc/init.d/sysstat, /etc/cron.d/sysstat # and /etc/cron.daily/sysstat files # # Should sadc collect system activity information? Valid values # are 'true' and 'false'. Please do not put other values, they # will be overwritten by debconf! ENABLED="true" """ f = open("/etc/default/sysstat", "w") f.write(default_sysstat_contents) f.close() return success def _fix_schedule(self): """ :return: success :rtype: bool """ success = True try: if self.ostype == "Mac OS X": if not os.path.isdir("/var/account"): os.mkdir("/var/account", 0o755) open(self.acct_file, "a").close() self.ch.executeCommand(self.accton + " " + self.acct_file) else: f = open(self.cron_file, "w") f.write(self.sysstat_cron_contents) f.close() os.chown(self.cron_file, 0, 0) os.chmod(self.cron_file, 0o644) except: success = False return success def fix(self): """ :return: self.rulesuccess :rtype: bool """ self.detailedresults = "" self.rulesuccess = True try: if not self._fix_installation(): self.rulesuccess = False else: self._set_paths() if not self._fix_configuration(): self.rulesuccess = False if not self._fix_schedule(): self.rulesuccess = False except (KeyboardInterrupt, SystemExit): raise except: self.rulesuccess = False self.detailedresults += "\n" + traceback.format_exc() self.logger.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) self.logger.log(LogPriority.INFO, self.detailedresults) return self.rulesuccess
class ConfigureScreenLocking(RuleKVEditor): def __init__(self, config, environ, logdispatcher, statechglogger): RuleKVEditor.__init__(self, config, environ, logdispatcher, statechglogger) self.logger = logdispatcher self.rulenumber = 74 self.rulename = "ConfigureScreenLocking" self.mandatory = True self.rootrequired = False self.applicable = { 'type': 'white', 'family': ['linux', 'solaris', 'freebsd'], 'os': { 'Mac OS X': ['10.15', 'r', '10.15.10'] } } self.effectiveUserID = self.environ.geteuid() self.sethelptext() self.formatDetailedResults("initialize") self.guidance = ["NSA 2.3.5.6.1"] if self.environ.getosfamily() == "darwin": if self.effectiveUserID == 0: self.addKVEditor( "SystemAskForPasswordSystem", "defaults", "/Library/Preferences/com.apple.screensaver", "", {"askForPassword": ["1", "-int 1"]}, "present", "", "Ask for password when system wide " + "screen saver is on.", None, False, {"askForPassword": ["0", "-int 0"]}) self.addKVEditor( "SystemSetScreenSaverIdleTime", "defaults", "/Library/Preferences/com.apple.screensaver", "", {"idleTime": ["840", "-int 840"]}, "present", "", "Sets system the screen saver to " + "activate after 14 minutes of idleTime.", None, False, { "idleTime": [ "The domain/default pair of ( .+" + "com\.apple\.screensaver, " + "idleTime) does not " + "exist", None ] }) self.addKVEditor( "SystemLoginWindowIdleTime", "defaults", "/Library/Preferences/com.apple.screensaver", "", {"loginWindowIdleTime": ["840", "-int 840"]}, "present", "", "Sets system LoginWindowIdleTime to " + "14 minutes.", None, False, { "loginWindowIdleTime": [ "The domain/default pair of ( .+" + "com\.apple\.screensaver, " + "loginWindowIdleTime) does not " + "exist", None ] }) else: self.addKVEditor( "AskForPassword", "defaults", "~/Library/Preferences/com.apple.screensaver", "", {"askForPassword": ["1", "-int 1"]}, "present", "", "Ask for password when screen saver is on.", None, False, {"askForPassword": ["0", "-int 0"]}) self.addKVEditor( "AskForPasswordDelay", "defaults", "~/Library/Preferences/com.apple.screensaver", "", {"askForPasswordDelay": ["0", "-int 0"]}, "present", "", "Delay asking for password by 0 seconds.", None, False, { "askForPasswordDelay": [ "The domain/default pair of ( .+" + "com\.apple\.screensaver, " + "askForPassword) does not " + "exist", None ] }) else: datatype = 'bool' key = 'CONFIGURESCREENLOCKING' instructions = "To prevent the configuration of idle screen locking, set the value of CONFIGURESCREENLOCKING to False." default = True self.ci = self.initCi(datatype, key, instructions, default) #self.gnomeInst variable determines in the fixGnome method #at the beginning if we even proceed. If False we don't proceed #and is fine. Gets set to True in reportGnome method if #either gconftool-2 or gsettings binaries exist. self.gnomeInst = False self.useGconf = True self.iditerator = 0 self.cmdhelper = CommandHelper(self.logger) self.ph = Pkghelper(self.logger, self.environ) self.ch = CommandHelper(self.logger) self.euid = self.environ.geteuid() def report(self): """ConfigureScreenLocking.report() method to report whether system is configured to screen locking NSA standards. If the system is linux, although many desktops are available, this rule will only check the two most popular desktops, KDE, and Gnome. @author: dwalker :param self: essential if you override this definition :return: self.compliant :rtype: bool """ self.detailedresults = "" self.compliant = True try: compliant = True self.detailedresults = "" if self.environ.osfamily == 'linux': if not self.check_package(): compliant = False if self.euid != 0: self.detailedresults += "\nThis is expected if not running with elevated privileges since STONIX " \ "requires elevated privileges to install packages. Please run STONIX with elevated privileges " \ "and run the fix for this rule again, to fix this issue." if self.ph.check("gdm") or self.ph.check("gdm3"): self.gnomeInstalled = True if not self.reportGnome(): self.detailedresults += "\nGnome GUI environment " + \ "does not appear to be correctly configured " + \ "for screen locking parameters." compliant = False else: self.detailedresults += "\nGnome GUI environment " + \ "appears to be correctly configured for " + \ "screen locking parameters." else: self.gnomeInstalled = False self.detailedresults += "\nGnome not installed. No need to configure for gnome." if self.ph.check("kdm") or self.ph.check("kde-workspace") or \ self.ph.check("sddm") or self.ph.check("patterns-kde-kde_yast"): self.kdeInstalled = True if not self.reportKde(): self.detailedresults += "\nKDE GUI environment " + \ "does not appear to be correctly configured " + \ "for screen locking parameters." compliant = False else: self.detailedresults += "\nKDE GUI environment " + \ "appears to be correctly configured for " + \ "screen locking parameters." else: self.kdeInstalled = False self.detailedresults += "\nKDE not installed. No need to configure for kde." elif self.environ.getosfamily() == "darwin": compliant = self.reportMac() self.compliant = compliant except (KeyboardInterrupt, SystemExit): raise except Exception: self.compliant = False self.detailedresults += traceback.format_exc() self.logger.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.compliant def check_package(self): """ for rhel 7 and similar generation linux rpm-based systems, the 'screen' package is required by the STIG for rhel 8 and beyond (and similar), the 'tmux' package is required. :return: installed :rtype: bool """ self.screen_pkg = "" installed = True if self.ph.checkAvailable("tmux"): self.screen_pkg = "tmux" elif self.ph.check("tmux"): self.screen_pkg = "tmux" else: self.screen_pkg = "screen" if not self.ph.check(self.screen_pkg): self.detailedresults += "\nThe required package: " + str( self.screen_pkg) + " is not installed" installed = False else: self.detailedresults += "\nThe required package: " + str( self.screen_pkg) + " is installed" return installed def reportMac(self): """Mac osx specific report submethod @author: dwalker :param self: essential if you override this definition :returns: bool - True if system is compliant, False if it isn't """ success = RuleKVEditor.report(self, True) return success def reportGnome(self): """determines if gnome is installed, if so, checks to see if the return value strings from running the gconftool-2 command are correct. Gconftool-2 command only works in root mode so if not root do not audit gnome and just return true @author: dwalker :param self: essential if you override this definition :returns: bool @change: dwalker - mass refactor, added comments """ compliant = True gsettings = "/usr/bin/gsettings" gconf = "/usr/bin/gconftool-2" self.gesttingsidletime = "" self.gconfidletime = "" self.setcmds = "" #may need to change code in the future to make gconf and #gsettings mutually exclusive with if elif else self.gnomeInstalled = False #if they are found to conflict with each other if os.path.exists(gconf): getcmds = { "/apps/gnome-screensaver/idle_activation_enabled": "true", "/apps/gnome-screensaver/lock_enabled": "true", "/apps/gnome-screensaver/mode": "blank-only", "/desktop/gnome/session/idle_delay": "15" } #make a copy of the getcmds dictionary tempdict = dict(getcmds) #check each value using the gconftool-2 command for cmd in getcmds: #set the get command for each value cmd2 = gconf + " --get " + cmd #execute the command self.cmdhelper.executeCommand(cmd2) #get output and error output output = self.cmdhelper.getOutput() error = self.cmdhelper.getError() #the value is set if output: #check this one separately since the value is #also able to be less than 15 mins and be compliant if cmd == "/desktop/gnome/session/idle_delay": if int(output[0].strip()) > 15: self.detailedresults += "Idle delay value " + \ "is not 15 or lower (value: " + \ output[0].strip() + ")\n" #if they have a value less than 15 mins this is ok #and we set the self.gconfidletime variable which is #used during setting gconf values in the fixGnome method elif int(output[0].strip()) < 15: self.gconfidletime = output[0].strip() del tempdict[cmd] #value is correct so remove it from the tempdic else: del tempdict[cmd] #check if value is correct with associated key elif output[0].strip() != getcmds[cmd]: self.detailedresults += cmd2 + " didn't produce the " + \ "desired value after being run which is " + \ getcmds[cmd] + "\n" #value is correct so remove it from the tempdict else: del tempdict[cmd] #else the value isn't set elif error: self.detailedresults += "There is no value set for:\n" + \ cmd2 + "\n" #if tempdict still has leftover values then #there were values that weren't correctly set #and is non compliant if tempdict: compliant = False #set self.setcmds variable to be a copy of tempdict which #we use later in the fix to determine if we fix this portion #of the rule or not self.setcmds = tempdict if os.path.exists(gsettings): self.gnomeInst = True #use gsettings command to see if the correct values are set #for each key in getcmds dictionary getcmds = { " get org.gnome.desktop.screensaver " + "idle-activation-enabled": "true", " get org.gnome.desktop.screensaver lock-enabled": "true", " get org.gnome.desktop.screensaver lock-delay": "0", " get org.gnome.desktop.screensaver picture-opacity": "100", " get org.gnome.desktop.screensaver picture-uri": "''", " get org.gnome.desktop.session idle-delay": "900" } #run each gsettings get command for each key and get value for cmd in getcmds: cmd2 = gsettings + cmd self.cmdhelper.executeCommand(cmd2) output = self.cmdhelper.getOutput() error = self.cmdhelper.getError() if output: #check this one separately since the value is #also able to be less than 900 secs and be compliant if cmd == " get org.gnome.desktop.session idle-delay": try: splitOut = output[0].split() if len(splitOut) > 1: num = splitOut[1] else: num = splitOut[0] if int(num) > 900: compliant = False self.detailedresults += "Idle delay value " + \ "is not 900 seconds (value: " +\ num + ")\n" #if they have a value less than 900 secs this is ok #and we set the self.gsettingsidletime variable which is #used during setting gsettings values in the fixGnome method # elif int(num) < 900: # self.gsettingsidletime = num elif int(num) == 0: compliant = False self.detailedresults += "Idle delay set " + \ "to 0, meaning it is disabled.\n" else: self.gsettingsidletime = "900" except ValueError: self.detailedresults += "Unexpected result: " + \ '"' + cmd2 + '" output was not a number\n' compliant = False elif cmd == " get org.gnome.desktop.screensaver lock-delay": try: splitOut = output[0].split() if len(splitOut) > 1: num = splitOut[1] else: num = splitOut[0] if int(num) != 0: compliant = False self.detailedresults += "Lock delay is not " + \ "set to 0\n" except ValueError: self.detailedresults += "Unexpected result: " + \ '"' + cmd2 + '" output was not a number\n' compliant = False elif output[0].strip() != getcmds[cmd]: self.detailedresults += '"' + cmd2 + \ "\" produced value: " + output[0].strip() + \ " instead of the desired value: " + getcmds[cmd] + "\n" compliant = False elif error: if re.search("No such key", error[0], re.I): continue self.detailedresults += "There is no value set for:" + \ cmd2 + "\n" compliant = False if self.environ.geteuid() == 0: # instantiate a kveditor to ensure self.dconfsettings file # contains correct contents self.dconfsettings = "/etc/dconf/db/local.d/local.key" if os.path.exists(self.dconfsettings): self.dconfdata = { "org/gnome/desktop/screensaver": { "idle-activation-enabled": "true", "lock-enabled": "true", "lock-delay": "0", "picture-opacity": "100", "picture-uri": "\'\'" }, "org/gnome/desktop/session": { "idle-delay": "uint32 900" } } self.kveditordconf = KVEditorStonix( self.statechglogger, self.logger, "tagconf", self.dconfsettings, self.dconfsettings + ".tmp", self.dconfdata, "present", "closedeq") if not self.kveditordconf.report(): self.detailedresults += self.dconfsettings + \ " doesn't cotain correct contents\n" compliant = False else: compliant = False self.detailedresults += self.dconfsettings + " not found\n" # check self.dconfuserprofile file to ensure existence # and/or correct contents self.dconfuserprofile = "/etc/dconf/profile/user" self.userprofilecontent = "user-db:user\n" + \ "system-db:local" if os.path.exists(self.dconfuserprofile): contents = readFile(self.dconfuserprofile, self.logger) contentstring = "" for line in contents: contentstring += line if not re.search(self.userprofilecontent, contentstring): compliant = False self.detailedresults += "Correct contents weren't " + \ "found in " + self.dconfuserprofile + "\n" else: compliant = False self.detailedresults += self.dconfuserprofile + " not " + \ "found\n" # the following file locks the settings we earlier set with the # gsettings command so that they don't default upon logout and/or # reboot. Check the file for the correct contents self.dconfsettingslock = "/etc/dconf/db/local.d/locks/stonix-settings.conf" self.dconflockdata = [ "/org/gnome/desktop/session/idle-delay", "/org/gnome/desktop/screensaver/idle-activation-enabled", "/org/gnome/desktop/screensaver/lock-enabled", "/org/gnome/desktop/screensaver/lock-delay", "/org/gnome/desktop/screensaver/picture-uri" ] locks_missing = [] if os.path.exists(self.dconfsettingslock): contents = readFile(self.dconfsettingslock, self.logger) for line in contents: if line.strip() not in self.dconflockdata: locks_missing.append(line.strip()) if locks_missing: self.detailedresults += "\nThe following settings should be locked from changes by the user but aren't:\n" + "\n".join( locks_missing) compliant = False else: compliant = False self.detailedresults += "\nGnome settings lock file not found" return compliant def reportKde(self): """determines if kde is installed, if so, ensures kde is configured by enabling screenlocking, automatically going black after 14 minutes and if inactivity ensues after 14 minutes, screen fully locks after 1 additional minute of inactivity for a total of 15 minutes activity @author: dwalker :param self: essential if you override this definition :returns: bool """ self.kdefix = {} if self.ph.manager == "apt-get" or self.ph.manager == "zypper": self.rcpath = ".config/kscreenlockerrc" self.kdeprops = { "Daemon": { "Autolock": "true", "LockGrace": "60", "LockOnResume": "true", "Timeout": "14" } } else: self.rcpath = ".kde/share/config/kscreensaverrc" self.kdeprops = { "ScreenSaver": { "Enabled": "true", "Lock": "true", "LockGrace": "60000", "Timeout": "840" } } if self.environ.geteuid() == 0: contents = readFile("/etc/passwd", self.logger) for line in contents: temp = line.split(":") try: username = temp[0] homepath = temp[5] except IndexError: self.logdispatch.log(LogPriority.DEBUG, [ 'ConfigureScreenLocking', 'IndexError processing ' + str(temp) ]) continue kdeparent1 = os.path.join(homepath, ".kde") kdeparent2 = os.path.join(homepath, ".kde4") kdefile = os.path.join(homepath, self.rcpath) if not os.path.exists(kdeparent1) and not os.path.exists( kdeparent2): # User does not user KDE continue elif not os.path.exists(kdefile): self.kdefix[username] = homepath self.detailedresults += kdefile + " not found for " + \ str(username) + "\n" self.logger.log(LogPriority.DEBUG, self.detailedresults) continue elif not self.searchFile(kdefile): self.detailedresults += "Did not find " + \ "required contents " + "in " + username + \ "'s " + kdefile + "\n" self.kdefix[username] = homepath if self.kdefix: self.detailedresults += "The following users don't " + \ "have kde properly configured for " + \ "screen locking:\n" for user in self.kdefix: self.detailedresults += user + "\n" return False else: return True else: kdeparent1 = os.path.join(self.environ.geteuidhome(), ".kde") kdeparent2 = os.path.join(self.environ.geteuidhome(), ".kde4") kdefile = os.path.join(self.environ.geteuidhome(), self.rcpath) if not os.path.exists(kdeparent1) and not os.path.exists( kdeparent2): self.detailedresults += "Current user doesn't use kde. " + \ "No need to configure.\n" self.logger.log(LogPriority.DEBUG, self.detailedresults) return True else: if not os.path.exists(kdefile): self.detailedresults += "Your " + kdefile + \ " file doesn't exist.\n" return False elif not self.searchFile(kdefile): self.detailedresults += "Did not find " + \ "required contents in " + kdefile + "\n" return False else: return True def fix(self): """ConfigureScreenLocking.fix() method to correct screen locking @author: dwalker :param self: essential if you override this definition :returns: bool - True if fix is successful, False if it isn't """ self.detailedresults = "" self.rulesuccess = True self.iditerator = 0 try: self.detailedresults = "" success = True if self.environ.getosfamily() == "linux": if not self.ci.getcurrvalue(): self.detailedresults += "Rule not enabled so nothing was done\n" self.logger.log( LogPriority.DEBUG, 'Rule was not enabled, so nothing was done') return if self.euid == 0: if not self.ph.install(self.screen_pkg): success = False self.logger.log( LogPriority.DEBUG, "Failed to install required package 'screen'") else: self.detailedresults += "\nNote: Some required packages may not be installed because STONIX is not running with elevated privileges." if self.gnomeInstalled: if not self.fixGnome(): success = False if self.kdeInstalled: if not self.fixKde(): success = False elif self.environ.getosfamily() == "darwin": if self.environ.geteuid() == 0: self.iditerator = 0 eventlist = self.statechglogger.findrulechanges( self.rulenumber) for event in eventlist: self.statechglogger.deleteentry(event) success = self.fixMac() self.rulesuccess = success except (KeyboardInterrupt, SystemExit): raise except Exception: self.detailedresults += "\n" + traceback.format_exc() self.logger.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.rulesuccess def fixKde(self): """This method checks if the kde screenlock file is configured properly. Please note, this rule may fail if the owner and group of configuration file are not that of the user in question but doesn't necessarily mean your system is out of compliance. If the fix fails Please check the logs to determing the real reason of non rule success. @author: dwalker :param self: essential if you override this definition :returns: bool - True if KDE is successfully configured, False if it isn't """ success = True if self.environ.geteuid() == 0: self.logdispatch.log(LogPriority.DEBUG, 'ConfigureScreenLocking.fixKde') if not self.kdefix: return True for user in self.kdefix: homepath = self.kdefix[user] kdefile = os.path.join(homepath, self.rcpath) if not self.correctFile(kdefile, user): success = False self.detailedresults += "Unable to configure " + \ kdefile + "\n" self.logger.log(LogPriority.DEBUG, self.detailedresults) else: username = "" homepath = self.environ.geteuidhome() kdefile = os.path.join(homepath, self.rcpath) uidnum = int(self.environ.geteuid()) passwddata = readFile("/etc/passwd", self.logger) found = False for user in passwddata: user = user.split(':') try: puidnum = int(user[2]) if puidnum == uidnum: username = user[0] found = True except IndexError: continue if not found: self.detailedresults += "Could not obtain your user id.\n" + \ "Stonix couldn't proceed with correcting " + kdefile + "\n" success = False elif not self.correctFile(kdefile, username): self.detailedresults += "Stonix couldn't correct the contents " + \ " of " + kdefile + "\n" success = False return success def fixGnome(self): """ensures gnome is configured to automatically screen lock after 15 minutes of inactivity, if gnome is installed @author: dwalker :param self: essential if you override this definition :returns: bool - True if gnome is successfully configured, False if it isn't """ info = "" success = True gconf = "/usr/bin/gconftool-2" gsettings = "/usr/bin/gsettings" if os.path.exists(gconf): #variable self.setcmds still has items left in its dictionary #which was set in the reportGnome method, meaning some values #either were incorrect or didn't have values. Go through and #set each remaining value that isn't correct cmd = "" if self.setcmds: for item in self.setcmds: if item == "/apps/gnome-screensaver/idle_activation_enabled": cmd = gconf + " --type bool --set /apps/gnome-screensaver/idle_activation_enabled true" elif item == "/apps/gnome-screensaver/lock_enabled": cmd = gconf + " --type bool --set /apps/gnome-screensaver/lock_enabled true" elif item == "/apps/gnome-screensaver/mode": cmd = gconf + ' --type string --set /apps/gnome-screensaver/mode "blank-only"' elif item == "/desktop/gnome/session/idle_delay": if self.gconfidletime: cmd = gconf + " --type int --set /desktop/gnome/session/idle_delay " + \ self.gconfidletime else: cmd = gconf + " --type int --set /desktop/gnome/session/idle_delay 15" if self.cmdhelper.executeCommand(cmd): if self.cmdhelper.getReturnCode() != 0: info += "Unable to set value for " + cmd + "\n" success = False else: info += "Unable to set value for " + cmd + "\n" success = False if os.path.exists(gsettings): setcmds = [ "org.gnome.desktop.screensaver idle-activation-enabled true", "org.gnome.desktop.screensaver lock-enabled true", "org.gnome.desktop.screensaver lock-delay 0", "org.gnome.desktop.screensaver picture-opacity 100", "org.gnome.desktop.screensaver picture-uri ''", "org.gnome.desktop.session idle-delay 900" ] # " set org.gnome.desktop.session idle-delay " + self.gsettingsidletime] for cmd in setcmds: cmd2 = gsettings + " set " + cmd self.cmdhelper.executeCommand(cmd2) if self.cmdhelper.getReturnCode() != 0: success = False info += "Unable to set value for " + cmd + \ " using gsettings\n" # Set gsettings with dconf # Unlock dconf settings # Create dconf settings lock file if self.environ.geteuid() == 0: if not os.path.exists(self.dconfsettingslock): if not createFile(self.dconfsettingslock, self.logger): self.rulesuccess = False self.detailedresults += "Unable to create stonix-settings file\n" self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) return False #write correct contents to dconf lock file if os.path.exists(self.dconfsettingslock): # Write to the lock file if self.dconflockdata: contents = "" tmpfile = self.dconfsettingslock + ".tmp" for line in self.dconflockdata: contents += line + "\n" if not writeFile(tmpfile, contents, self.logger): self.rulesuccess = False self.detailedresults += "Unable to write contents to " + \ "stonix-settings file\n" else: os.rename(tmpfile, self.dconfsettingslock) os.chown(self.dconfsettingslock, 0, 0) os.chmod(self.dconfsettingslock, 0o644) resetsecon(self.dconfsettingslock) # Create dconf user profile file if not os.path.exists(self.dconfuserprofile): if not createFile(self.dconfuserprofile, self.logger): self.rulesuccess = False self.detailedresults += "Unable to create dconf " + \ "user profile file\n" self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) return False # Write dconf user profile if os.path.exists(self.dconfuserprofile): tmpfile = self.dconfuserprofile + ".tmp" if not writeFile(tmpfile, self.userprofilecontent, self.logger): self.rulesuccess = False self.detailedresults += "Unabled to write to dconf user" + \ " profile file\n" self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) return False else: os.rename(tmpfile, self.dconfuserprofile) os.chown(self.dconfuserprofile, 0, 0) os.chmod(self.dconfuserprofile, 0o644) resetsecon(self.dconfuserprofile) # Fix dconf settings if not os.path.exists(self.dconfsettings): if not createFile(self.dconfsettings, self.logger): self.rulesuccess = False self.detailedresults += "Unable to create " + self.dconfsettings + " file \n" self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) return False self.dconfdata = { "org/gnome/desktop/screensaver": { "idle-activation-enabled": "true", "lock-enabled": "true", "lock-delay": "0", "picture-opacity": "100", "picture-uri": "\'\'" }, "org/gnome/desktop/session": { "idle-delay": "uint32 900" } } self.kveditordconf = KVEditorStonix( self.statechglogger, self.logger, "tagconf", self.dconfsettings, self.dconfsettings + ".tmp", self.dconfdata, "present", "closedeq") self.kveditordconf.report() if self.kveditordconf.fixables: if not self.kveditordconf.fix(): success = False self.detailedresults += "Unable to put correct settings inside " + \ self.dconfsettings + "\n" elif not self.kveditordconf.commit(): success = False self.detailedresults += "Unable to put correct settings inside " + \ self.dconfsettings + "\n" #run dconf update command to make dconf changes take effect if os.path.exists("/bin/dconf"): cmd = "/bin/dconf update" self.cmdhelper.executeCommand(cmd) elif os.path.exists("/usr/bin/dconf"): cmd = "/usr/bin/dconf update" self.cmdhelper.executeCommand(cmd) self.detailedresults += info return success def fixMac(self): """Mac osx specific fix submethod @author: dwalker :param self: essential if you override this definition :returns: bool - True if system is successfully fix, False if it isn't """ success = RuleKVEditor.fix(self, True) return success def searchFile(self, filehandle): """temporary method to separate the code to find directives from the rest of the code. Will put back all in one method eventually @author: dwalker :param filehandle: string :returns: bool """ self.editor = "" kvt = "tagconf" intent = "present" tpath = filehandle + ".tmp" conftype = "closedeq" self.editor = KVEditorStonix(self.statechglogger, self.logger, kvt, filehandle, tpath, self.kdeprops, intent, conftype) if not self.editor.report(): return False else: return True def correctFile(self, kfile, user): """separate method to find the correct contents of each file passed in as a parameter. @author: dwalker :param kfile: :param user: :returns: bool """ success = True if not os.path.exists(kfile): if not createFile(kfile, self.logger): self.detailedresults += "Unable to create " + kfile + \ " file for the user\n" self.logger.log(LogPriority.DEBUG, self.detailedresults) return False if not self.searchFile(kfile): if self.editor.fixables: if not self.editor.fix(): debug = "Kveditor fix is failing for file " + \ kfile + "\n" self.logger.log(LogPriority.DEBUG, debug) self.detailedresults += "Unable to correct contents for " + \ kfile + "\n" return False elif not self.editor.commit(): debug = "Kveditor commit is failing for file " + \ kfile + "\n" self.logger.log(LogPriority.DEBUG, debug) self.detailedresults += "Unable to correct contents for " + \ kfile + "\n" return False uid = getpwnam(user)[2] gid = getpwnam(user)[3] if uid != "" and gid != "": os.chmod(kfile, 0o600) os.chown(kfile, uid, gid) resetsecon(kfile) else: success = False self.detailedresults += "Unable to obtain uid and gid of " + user + "\n" self.logger.log(LogPriority.DEBUG, self.detailedresults) return success def undo(self): self.detailedresults += "This rule cannot be reverted\n" self.rulesuccess = False self.formatDetailedResults("undo", self.rulesuccess, self.detailedresults)
class SecurePOPIMAP(Rule): '''classdocs''' def __init__(self, config, environ, logger, statechglogger): ''' Constructor ''' Rule.__init__(self, config, environ, logger, statechglogger) self.config = config self.environ = environ self.logger = logger self.statechglogger = statechglogger self.rulenumber = 141 self.rulename = 'SecurePOPIMAP' self.mandatory = True self.sethelptext() self.rootrequired = True self.applicable = {'type': 'black', 'family': ['darwin']} self.guidance = ['NSA(3.17)', 'cce-4384-4', '3887-7', '4530-2', '4547-6', '4552-6', '4371-1', '4410-7'] data1 = 'bool' key1 = 'DISABLEPOPIMAP' instructions1 = 'To prevent POP/IMAP services from being disabled entirely, set the value of DisablePOPIMAP to False.' default1 = True self.disableci = self.initCi(data1, key1, instructions1, default1) data2 = 'bool' key2 = 'SECUREPOPIMAP' instructions2 = 'To securely configure POP/IMAP services, set the value of SecurePOPIMAP to True.' default2 = False self.secureci = self.initCi(data2, key2, instructions2, default2) data3 = 'string' key3 = 'RequireProtocols' instructions3 = 'If this system will be operating as a mail server, fill in the required/used protocols below. Please use a space-delimited list. Valid entries are limited to: imap, imaps, pop3, pop3s' default3 = '' self.reqprotocols = self.initCi(data3, key3, instructions3, default3) self.localization() def localization(self): '''determine which type of OS we are running on and set up class variables accordingly :returns: void @author: Breen Malmberg ''' self.initobjs() self.setcommon() if self.pkgh.manager not in ['apt-get', 'yum', 'zypper', 'dnf']: self.logger.log(LogPriority.DEBUG, "Could not identify OS type or OS not supported!") if self.pkgh.manager == 'apt-get': self.setdebian() if self.pkgh.manager == 'yum': self.setredhat() if self.pkgh.manager == 'dnf': self.setredhat() if self.pkgh.manager == 'zypper': self.setsuse() pass def initobjs(self): '''initialize objects needed/used by other methods within this class :returns: void @author: Breen Malmberg ''' try: # if you add class variables here, be sure to also add # checks for them, in the checkinitobjs() method self.pkgh = Pkghelper(self.logger, self.environ) self.svch = ServiceHelper(self.environ, self.logger) self.cmdh = CommandHelper(self.logger) self.debian = False self.suse = False self.redhat = False self.pkgdict = {} self.confpathdict = {} self.servicename = '' self.osdetected = False except Exception: raise def setcommon(self): '''set variables which are common to all platforms :returns: void @author: Breen Malmberg ''' self.detailedresults = "" self.reqprots = "" self.protocollist = ['imap', 'imaps', 'pop3', 'pop3s'] def setdebian(self): '''set debian specific variables :returns: void @author: Breen Malmberg ''' self.debian = True self.osdetected = True self.pkgdict = {'dovecot-core': False, 'dovecot-pop3d': False, 'dovecot-imapd': False} # the following dictionary is of the format: # {configfilepath1: {partialmatch1: fullreplacement1, # partialmatch2: fullreplacement2}, # configfilepath2: {partialmatch1: fullreplacement1, # partialmatch2: fullreplacement2} # } self.confpathdict = {'/etc/dovecot/dovecot.conf': {'protocols =': 'protocols = ' + str(self.reqprots) + '\n', 'disable_plaintext_auth =': 'disable_plaintext_auth = yes\n', 'login_process_per_connection =': 'login_process_per_connection = yes\n', 'mail_drop_priv_before_exec =': 'mail_drop_priv_before_exec = yes\n', 'login_trusted_networks =': '#login_trusted_networks =\n'}} self.servicename = 'dovecot' def setredhat(self): '''set redhat sepcific variables :returns: void @author: Breen Malmberg ''' self.redhat = True self.osdetected = True self.pkgdict = {'dovecot': False} # the following dictionary is of the format: # {configfilepath1: {partialmatch1: fullreplacement1, # partialmatch2: fullreplacement2}, # configfilepath2: {partialmatch1: fullreplacement1, # partialmatch2: fullreplacement2} # } self.confpathdict = {'/etc/dovecot/dovecot.conf': {'protocols =': 'protocols = ' + str(self.reqprots) + '\n', 'disable_plaintext_auth =': 'disable_plaintext_auth = yes\n', 'login_process_per_connection =': 'login_process_per_connection = yes\n', 'mail_drop_priv_before_exec =': 'mail_drop_priv_before_exec = yes\n', 'login_trusted_networks =': '#login_trusted_networks =\n'}} self.servicename = 'dovecot' def setsuse(self): '''set suse specific variables :returns: void @author: Breen Malmberg ''' self.suse = True self.osdetected = True self.pkgdict = {'dovecot': False} # the following dictionary is of the format: # {configfilepath1: {partialmatch1: fullreplacement1, # partialmatch2: fullreplacement2}, # configfilepath2: {partialmatch1: fullreplacement1, # partialmatch2: fullreplacement2} # } self.confpathdict = {'/etc/dovecot/dovecot.conf': {'protocols =': 'protocols = ' + str(self.reqprots) + '\n', 'disable_plaintext_auth =': 'disable_plaintext_auth = yes\n', 'login_process_per_connection =': 'login_process_per_connection = yes\n', 'mail_drop_priv_before_exec =': 'mail_drop_priv_before_exec = yes\n', 'login_trusted_networks =': '#login_trusted_networks =\n'}} self.servicename = 'dovecot' def getFileContents(self, filepath): ''' :param filepath: ''' filecontents = [] try: if not isinstance(filepath, str): self.logger.log(LogPriority.DEBUG, "Parameter filepath must be of type: str") if not filepath: self.logger.log(LogPriority.DEBUG, "Parameter filepath must not be blank") if not os.path.exists(filepath): self.logger.log(LogPriority.DEBUG, "Specified filepath not found") else: f = open(filepath, 'r') filecontents = f.readlines() f.close() if not filecontents: self.logger.log(LogPriority.DEBUG, "Specified file had no contents") except Exception: raise return filecontents def searchContents(self, regex, contents): ''' :param regex: :param contents: ''' retval = False try: if not isinstance(regex, str): self.logger.log(LogPriority.DEBUG, "Parameter regex must be of type: str") retval = False return retval if not isinstance(contents, list): self.logger.log(LogPriority.DEBUG, "Parameter contents must be of type: list") retval = False return retval if not regex: self.logger.log(LogPriority.DEBUG, "regex must not be blank") retval = False return retval if not contents: self.logger.log(LogPriority.DEBUG, "contents must not be blank") retval = False return retval for line in contents: if re.search(regex, line): retval = True except Exception: raise return retval def fixContents(self, fixdict, filepath, contents): ''' :param fixdict: :param filepath: :param contents: ''' retval = True if not isinstance(fixdict, dict): self.logger.log(LogPriority.DEBUG, "Parameter fixdict must be of type: dict") retval = False return retval if not isinstance(filepath, str): self.logger.log(LogPriority.DEBUG, "Parameter filepath must be of type: str") retval = False return retval if not isinstance(contents, list): self.logger.log(LogPriority.DEBUG, "Parameter contents must be of type: list") retval = False return retval if not fixdict: self.logger.log(LogPriority.DEBUG, "Parameter fixdict must not be empty") retval = False return retval if not filepath: self.logger.log(LogPriority.DEBUG, "Parameter filepath must not be blank") retval = False return retval if not contents: self.logger.log(LogPriority.DEBUG, "Parameter contents must not be empty") retval = False return retval contentdict = {} for item in fixdict: contentdict[item] = False tempfilepath = filepath + '.stonixtmp' for line in contents: for partialmatch in fixdict: if re.search(partialmatch, line): contents = [c.replace(line, fixdict[partialmatch]) for c in contents] contentdict[partialmatch] = True for item in contentdict: if not contentdict[item]: contents.append('\n' + fixdict[item]) tf = open(tempfilepath, 'w') tf.writelines(contents) tf.close() event = {'eventtype': 'conf', 'filepath': filepath} myid = iterate(self.iditerator, self.rulenumber) self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange(filepath, tempfilepath, myid) os.rename(tempfilepath, filepath) def report(self): '''return true if all check actions report compliant return false if one or more check actions reports not compliant :returns: self.compliant :rtype: bool @author: Breen Malmberg ''' self.compliant = True self.detailedresults = '' self.logger.log(LogPriority.DEBUG, "Entering SecurePOPIMAP.report()...") try: self.reqprots = self.reqprotocols.getcurrvalue() if self.suse: self.setsuse() if self.debian: self.setdebian() if self.redhat: self.setredhat() self.logger.log(LogPriority.DEBUG, "Checking init objects...") if not self.checkinitobjs(): self.logger.log(LogPriority.DEBUG, 'One or more class properties were not initialized or set correctly.') self.rulesuccess = False self.compliant = False self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.compliant self.logger.log(LogPriority.DEBUG, "Finished checking init objects. all were OK") # disable stuff if self.disableci.getcurrvalue(): self.logger.log(LogPriority.DEBUG, "The disable POP/IMAP option is set. Checking if POP/IMAP is disabled...") self.logger.log(LogPriority.DEBUG, "Checking for running service...") if not self.checksvc(): self.compliant = False self.detailedresults += "\nRule is not compliant because: The " + str(self.servicename) + " service is either running or enabled." self.logger.log(LogPriority.DEBUG, "Service check finished.") self.logger.log(LogPriority.DEBUG, "Checking packages...") if not self.checkpkgs(False): self.compliant = False self.detailedresults += "\nRule is not compliant because one or more of the packages is still installed." self.logger.log(LogPriority.DEBUG, "Package check finished.") # secure stuff elif self.secureci.getcurrvalue(): self.logger.log(LogPriority.DEBUG, "The secure POP/IMAP option is set. Checking if POP/IMAP is secured...") if not self.reqprots: self.detailedresults += '\nRequired protocols were not specified. Cannot securely configure POP/IMAP without them.' self.compliant = False slist = self.reqprots.split() if slist: for prot in slist: if prot.strip() != '' and prot.strip() not in self.protocollist: self.detailedresults += "\nRule is not compliant because: The specified protocol: " + str(prot) + " is not a valid protocol." self.compliant = False else: self.detailedresults += '\nUnable to read protocol list. Please use a space-delimited list when specifying your required protocols.' self.logger.log(LogPriority.DEBUG, "The secure POP/IMAP option is set. Checking if POP/IMAP is secured...") if self.checkpkgs(True): self.logger.log(LogPriority.DEBUG, "Required packages are installed.") self.logger.log(LogPriority.DEBUG, " Checking file configuration...") if not self.checkconfig(): self.compliant = False self.logger.log(LogPriority.DEBUG, "File configuration check finished.") else: self.logger.log(LogPriority.DEBUG, "One or more required packages are not installed.") self.compliant = False self.detailedresults += "\nRule is not compliant because one or more of the required packages are not installed." except (KeyboardInterrupt, SystemExit): raise except Exception: self.detailedresults += traceback.format_exc() self.rulesuccess = False self.logger.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.compliant def checkinitobjs(self): '''validate each class property and object to be used in this class before it is used return True if each object is properly assigned/initialized return False if not :returns: retval :rtype: bool @author: Breen Malmberg ''' retval = True try: if not self.pkgh: retval = False self.logger.log(LogPriority.DEBUG, "The package helper object was not initialized correctly") if not self.svch: retval = False self.logger.log(LogPriority.DEBUG, "The service helper object was not initialized correctly") if not self.cmdh: retval = False self.logger.log(LogPriority.DEBUG, "The command helper object was not initialized correctly") if not self.osdetected: retval = False self.logger.log(LogPriority.DEBUG, "Unable to determine OS type or package manager") if not self.servicename: retval = False self.logger.log(LogPriority.DEBUG, "servicename variable was not set") if not self.pkgdict: retval = False self.logger.log(LogPriority.DEBUG, "pgkdict variable was not set") if not self.confpathdict: retval = False self.logger.log(LogPriority.DEBUG, "confpathdict variable was not set") except AttributeError: retval = False self.logger.log(LogPriority.DEBUG, "One or more of the class variables are undefined") return retval return retval def checksvc(self): '''check to see if the dovecot service is enabled or running return False if it is either running or enabled if the service is neither running nor enabled, return True :returns: retval :rtype: bool @author: Breen Malmberg ''' retval = True enabled = False running = False try: if self.svch.auditService(self.servicename, _="_"): enabled = True self.detailedresults += '\nThe ' + str(self.servicename) + ' service is still enabled' if enabled: self.detailedresults += "\nThere are service(s) which need to be disabled" if self.svch.isRunning(self.servicename, _="_"): running = False self.detailedresults += '\nThe ' + str(self.servicename) + ' service is still running' if running: self.detailedresults += "\nThere are service(s) which need to be stopped" if enabled | running: retval = False except Exception: raise return retval def checkpkgs(self, desired): '''check compliance of packages portion of rule if desired, check if all required packages are installed: True if yes, False if no if not desired, check if any packages are installed: True if no, False if yes :param desired: :returns: retval :rtype: bool @author: Breen Malmberg ''' retval = True try: if not desired: for pkg in self.pkgdict: if self.pkgh.check(pkg): self.pkgdict[pkg] = True retval = False else: for pkg in self.pkgdict: if not self.pkgh.check(pkg): self.pkgdict[pkg] = False retval = False except Exception: raise return retval def checkconfig(self): '''verify configuration state of file(s) :returns: retval :rtype: bool @author: Breen Malmberg ''' retval = True contents = [] try: for path in self.confpathdict: contents = self.getFileContents(path) if contents: for confitem in self.confpathdict[path]: if not self.searchContents(str(self.confpathdict[path][confitem]), contents): retval = False self.detailedresults += "\nRequired configuration option: " + str(self.confpathdict[path][confitem]) + " was not found in file: " + str(path) else: self.logger.log(LogPriority.DEBUG, "Unable to check contents of file: " + str(path)) if retval: self.detailedresults += "\nAll required configuration options have been found in file: " + str(path) if retval: self.detailedresults += "\nAll required configuration options have been found in all required configuration files" except Exception: raise return retval def fix(self): '''run each fix action and get the success results of each one return True if all fix actions succeeded return False if not :returns: fixsuccess :rtype: bool @author: Breen Malmberg ''' fixsuccess = True self.detailedresults = '' self.iditerator = 0 try: if self.disableci.getcurrvalue(): self.detailedresults += '\nYou have selected the DisablePOPIMAP option. It will now be disabled/removed from this system.' if not self.turnoffsvc(): fixsuccess = False if not self.removePackages(): fixsuccess = False elif self.secureci.getcurrvalue(): self.detailedresults += '\nYou have selected the SecurePOPIMAP option. It will now be installed (if it is not already installed) and then securely configured.' if not self.installPackages(): fixsuccess = False if not self.configurefiles(): fixsuccess = False except (KeyboardInterrupt, SystemExit): raise except Exception: self.detailedresults += traceback.format_exc() self.rulesuccess = False self.logger.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("fix", fixsuccess, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return fixsuccess def turnoffsvc(self): '''disable dovecot service if it is enabled return True if dovecot service was successfully disabled or is not enabled return False if otherwise :returns: retval :rtype: bool @author: Breen Malmberg ''' self.logger.log(LogPriority.DEBUG, "Attempting to disable service: " + str(self.servicename)) retval = True try: self.svch.disableService(self.servicename, _="_") if self.svch.auditService(self.servicename, _="_"): retval = False self.logger.log(LogPriority.DEBUG, "Service is still enabled after executing disableservice!") if self.svch.isRunning(self.servicename, _="_"): retval = False self.logger.log(LogPriority.DEBUG, "Service is still running after executing disableservice!") if retval: self.logger.log(LogPriority.DEBUG, "Successfully disabled service: " + str(self.servicename)) else: self.logger.log(LogPriority.DEBUG, "Failed to disable service: " + str(self.servicename)) except Exception: raise return retval def removePackages(self): '''Remove all packages in self.pkgdict, which are currently installed return True if all installed packages were successfully removed return False if not :returns: retval :rtype: bool @author: Breen Malmberg ''' retval = True try: for pkg in self.pkgdict: if self.pkgdict[pkg]: if not self.pkgh.remove(pkg): retval = False self.detailedresults += '\nFailed to remove package: ' + str(pkg) else: self.pkgdict[pkg] = False if not retval: self.logger.log(LogPriority.DEBUG, "Failed to remove packages") else: self.logger.log(LogPriority.DEBUG, "Successfully removed all packages") except Exception: raise return retval def configurefiles(self): '''set the correct configuration options within the dovecot configuration file(s) :returns: void @author: Breen Malmberg ''' try: for path in self.confpathdict: contents = self.getFileContents(path) self.fixContents(self.confpathdict[path], path, contents) except Exception: raise def installPackages(self): '''install all necessary dovecot packages :returns: retval :rtype: bool @author: Breen Malmberg ''' retval = True self.logger.log(LogPriority.DEBUG, "Attempting to install all necessary packages...") try: for pkg in self.pkgdict: if not self.pkgdict[pkg]: if not self.pkgh.install(pkg): retval = False self.detailedresults += '\nFailed to install package ' + str(pkg) self.logger.log(LogPriority.DEBUG, "Failed to install package: " + str(pkg)) if not retval: self.logger.log(LogPriority.DEBUG, "Failed to install one or more required packages. Please check your connection to the network and ensure that your package repositories are correctly configured.") else: self.logger.log(LogPriority.DEBUG, "Successfully installed all required packages") except Exception: raise return retval
class ConfigureSystemAuthentication(Rule): """ Configure system authentication and password settings in accordance with rhel 7 stig requirements """ def __init__(self, config, environ, logger, statechglogger): """ :param config: :param environ: :param logger: :param statechglogger: """ Rule.__init__(self, config, environ, logger, statechglogger) self.logger = logger self.rulenumber = 57 self.rulename = "ConfigureSystemAuthentication" self.formatDetailedResults("initialize") self.mandatory = True self.sethelptext() self.applicable = {'type': 'white', 'family': 'linux'} datatype = "bool" key = "CONFIGSYSAUTH" instructions = "To disable this rule, set the value of " + \ "CONFIGSYSAUTH to False." default = True self.ci1 = self.initCi(datatype, key, instructions, default) datatype = "bool" key = "PASSWORDREQ" instructions = "To not configure password requirements, set " + \ "PASSWORDREQ to False. This configuration item will configure " + \ "PAM's password requirements when changing to a new password." default = True self.ci2 = self.initCi(datatype, key, instructions, default) datatype = "bool" key = "PASSWORDFAIL" instructions = "To not configure password fail locking, set " + \ "PASSWORDFAIL to False. This configuration item will " + \ "configure PAM's failed login attempts mechanism using either " + \ "faillock or tally2." default = True self.ci3 = self.initCi(datatype, key, instructions, default) datatype = "bool" key = "PWHASHING" instructions = "To not set the hashing algorithm, set " + \ "PWHASHING to False. This configuration item will configure " + \ "libuser and/or login.defs, which specifies the hashing " + \ "algorithm to use." default = True self.ci4 = self.initCi(datatype, key, instructions, default) self.guidance = ["NSA 2.3.3.1,", "NSA 2.3.3.2"] self.created = False self.localize() def localize(self): """ set up session variables based on system platform and version """ myos = self.environ.getostype().lower() if re.search("suse", myos): self.password = PASSWORD_ZYPPER self.auth = AUTH_ZYPPER self.acct = ACCOUNT_ZYPPER elif re.search("debian|ubuntu", myos): self.password = PASSWORD_APT self.auth = AUTH_APT self.acct = ACCOUNT_APT else: self.password = PASSWORD_YUM self.auth = AUTH_YUM self.acct = ACCOUNT_YUM self.session = SESSION_YUM def report(self): """ ConfigureSystemAuthentication() report method to report if system is compliant with authentication and password settings @author: Derek Walker :return: self.compliant :rtype: bool """ self.compliant = True self.detailedresults = "" self.ci2comp, self.ci3comp, self.ci4comp = True, True, True try: if not self.reportLinux(): self.compliant = False except (KeyboardInterrupt, SystemExit): raise except Exception: self.rulesuccess = False self.detailedresults += "\n" + traceback.format_exc() self.logger.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logger.log(LogPriority.INFO, self.detailedresults) return self.compliant def fix(self): """ ConfigureSystemAuthentication.fix() method to fix the system to be compliant with authentication and password settings @author: Derek Walker :return: self.rulesuccess :rtype: bool """ self.rulesuccess = True self.detailedresults = "" self.iditerator = 0 try: if not self.ci1.getcurrvalue(): return self.rulesuccess # delete past state change records from previous fix eventlist = self.statechglogger.findrulechanges(self.rulenumber) for event in eventlist: self.statechglogger.deleteentry(event) if not self.fixLinux(): self.rulesuccess = False except (KeyboardInterrupt, SystemExit): raise except Exception: self.rulesuccess = False self.detailedresults += "\n" + traceback.format_exc() self.logdispatch.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.rulesuccess def reportLinux(self): """ Linux-specific submethod for config reporting @author: Derek Walker :return: compliant :rtype: bool """ self.logindefs = "/etc/login.defs" debug = "" compliant = True self.editor1, self.editor2 = "", "" self.pwqeditor = "" self.usingpwquality, self.usingcracklib = False, False self.usingpamtally2, self.usingpamfail = False, False self.created1, self.created2 = False, False self.libuserfile = "/etc/libuser.conf" self.ph = Pkghelper(self.logger, self.environ) self.determine_pwreqs_mechanism() # set pam password and authentication file paths pampassfiles = ["/etc/pam.d/common-password", "/etc/pam.d/common-password-pc", "/etc/pam.d/password-auth", "/etc/pam.d/password-auth-ac"] pamauthfiles = ["/etc/pam.d/common-auth", "/etc/pam.d/common-auth-pc", "/etc/pam.d/system-auth", "/etc/pam.d/system-auth-ac"] for f in pampassfiles: if os.path.isfile(f): self.pampassfile = f for f in pamauthfiles: if os.path.isfile(f): self.pamauthfile = f if not bool(self.pampassfile and self.pamauthfile): if self.ph.manager == "apt-get": self.pampassfile = "/etc/pam.d/common-password" self.pamauthfile = "/etc/pam.d/common-auth" elif self.ph.manager == "zypper": self.pampassfile = "/etc/pam.d/common-password-pc" self.pamauthfile = "/etc/pam.d/common-auth-pc" else: self.pampassfile = "/etc/pam.d/password-auth" self.pamauthfile = "/etc/pam.d/system-auth" if not self.check_pwreqs_configured(): self.ci2comp = False debug += "checkpasswordreqs method is False compliancy\n" compliant = False if not self.checkaccountlockout(): self.ci3comp = False debug += "checkaccountlockout method is False compliancy\n" compliant = False if not self.checklogindefs(): self.ci4comp = False debug += "checklogindefs method is False compliancy\n" compliant = False if not self.checklibuser(): self.ci4comp = False debug += "checklibuser method is False compliancy\n" compliant = False if debug: self.logger.log(LogPriority.DEBUG, debug) if not self.check_showfailed_logins(): compliant = False return compliant def fixLinux(self): """ Linux specific submethod to correct linux distributions. If your system is portage based, i.e. gentoo, you will need to do a manual fix for everything except the login.defs file @author: Derek Walker :return: success :rtype: bool """ success = True try: if self.ph.manager == "dnf": if not self.ph.check("authconfig"): self.ph.install("authconfig") # this is needed by fedora to continue except: pass # """create backups of pamfiles""" if os.path.exists(self.pampassfile): createFile(self.pampassfile + ".backup", self.logger) if os.path.exists(self.pamauthfile): createFile(self.pamauthfile + ".backup", self.logger) if self.ci2.getcurrvalue(): if not self.ci2comp: # configure regex for pwquality if self.usingpwquality: self.password = re.sub("pam_cracklib\.so", "pam_pwquality.so", self.password) if self.environ.getsystemfismacat() == "high": self.password = re.sub("minlen=8", "minlen=14", self.password) self.password = re.sub("minclass=3", "minclass=4", self.password) regex = PWQUALITY_HIGH_REGEX else: regex = PWQUALITY_REGEX if self.pwqinstalled: if not self.setpasswordsetup(regex): success = False else: if not self.setpasswordsetup(regex, self.pwqualitypkgs): success = False # configure regex for cracklib elif self.usingcracklib: self.password = re.sub("pam_pwquality\.so", "pam_cracklib.so", self.password) if self.environ.getsystemfismacat() == "high": self.password = re.sub("minlen=8", "minlen=14", self.password) self.password = re.sub("minclass=3", "minclass=4", self.password) regex = CRACKLIB_HIGH_REGEX else: regex = CRACKLIB_REGEX if self.clinstalled: if not self.setpasswordsetup(regex): success = False else: if not self.setpasswordsetup(regex, self.cracklibpkgs): success = False else: error = "Could not find pwquality/cracklib pam module. Fix failed." self.logger.log(LogPriority.ERROR, error) self.detailedresults += error + "\n" return False if self.ci3.getcurrvalue(): if not self.ci3comp: if self.usingpamfail: regex = PAMFAIL_REGEX if not self.setaccountlockout(regex): success = False self.detailedresults += "Unable to configure pam for faillock\n" elif self.usingpamtally2: regex = PAMTALLY_REGEX if not self.setaccountlockout(regex): success = False self.detailedresults += "Unable to configure pam for pam_tally2\n" else: self.detailedresults += "There is no account lockout program available for this system\n" success = False if self.ci4.getcurrvalue(): if not self.ci4comp: if not self.checklibuser(): if not self.setlibuser(): debug = "setlibuser() failed\n" self.detailedresults += "Unable to configure /etc/libuser.conf\n" self.logger.log(LogPriority.DEBUG, debug) success = False if not self.checklogindefs(): if not self.setlogindefs(): debug = "setdefpasshash() failed\n" self.logger.log(LogPriority.DEBUG, debug) self.detailedresults += "Unable to configure /etc/login.defs file\n" success = False if not self.set_showfailed_logins(): success = False return success def check_showfailed_logins(self): """ config file: /etc/pam.d/postlogin-ac config option: session required pam_lastlog.so showfailed :return: """ configured = True search_line = {"session": "required pam_lastlog.so showfailed"} config_file = "/etc/pam.d/postlogin-ac" tmpfile = config_file + ".stonixtmp" self.lastlog_editor = KVEditorStonix(self.statechglogger, self.logger, "conf", config_file, tmpfile, search_line, "present", "space") if not self.lastlog_editor.report(): configured = False self.detailedresults += "\nShow failed logins option is not configured correctly in PAM" return configured def set_showfailed_logins(self): """ config file: /etc/pam.d/postlogin-ac config option: session required pam_lastlog.so showfailed :return: success :rtype: bool """ success = True if not self.lastlog_editor.fix(): success = False self.detailedresults += "\nFailed to enable show failed logins option in PAM" elif not self.lastlog_editor.commit(): success = False self.detailedresults += "\nFailed to enable show failed logins option in PAM" return success def determine_pwreqs_mechanism(self): """ determine whether this system is using cracklib or pwquality as a password requirements control mechanism use pwquality by default since cracklib is legacy """ self.usingcracklib = False self.usingpwquality = False self.pwqualitypkg = "" self.cracklibpkg = "" # potential password requirements package names self.cracklibpkgs = ["libpam-cracklib", "cracklib"] self.pwqualitypkgs = ["libpam-pwquality", "pam_pwquality", "libpwquality", "libpwquality1"] # pwquality check for pkg in self.pwqualitypkgs: if self.ph.check(pkg): self.usingpwquality = True self.pwqualitypkg = pkg break if not self.usingpwquality: for pkg in self.pwqualitypkgs: if self.ph.checkAvailable(pkg): self.usingpwquality = True self.pwqualitypkg = pkg break # cracklib check (only runs if pwquality check turns up nothing) if not self.usingpwquality: for pkg in self.cracklibpkgs: if self.ph.check(pkg): self.usingcracklib = True self.cracklibpkg = pkg break if not self.usingcracklib: for pkg in self.cracklibpkgs: if self.ph.checkAvailable(pkg): self.usingcracklib = True self.cracklibpkg = pkg break def check_pwreqs_installed(self): """ determine if either a cracklib package or pwquality package is installed :return: installed :rtype: bool """ installed = False self.pwqinstalled = False self.clinstalled = False for pkg in self.pwqualitypkgs: if self.ph.check(pkg): self.pwqinstalled = True for pkg in self.cracklibpkgs: if self.ph.check(pkg): self.clinstalled = True if bool(self.clinstalled or self.pwqinstalled): installed = True return installed def check_pwreqs_configured(self): """ check whether the password requirements have been properly configured in PAM, using either cracklib or pwquality :return: passwords_configured :rtype: bool """ passwords_configured = True if not self.check_pwreqs_installed(): passwords_configured = False return passwords_configured if self.usingpwquality: if not self.pwqinstalled: for pkg in self.pwqualitypkgs: if self.ph.install(pkg): self.pwqinstalled = True break if not self.checkpasswordsetup("pwquality"): self.detailedresults += "System is using pwquality but it's not configured properly in PAM\n" passwords_configured = False elif self.usingcracklib: if not self.clinstalled: for pkg in self.cracklibpkgs: if self.ph.install(pkg): self.clinstalled = True break if not self.checkpasswordsetup("cracklib"): self.detailedresults += "System is using cracklib but it's not configured properly in PAM\n" passwords_configured = False return passwords_configured def checkpasswordsetup(self, package): """ Method called from within checkpasswordreqs method @author: Derek Walker :param package: string; name of package to check for :return: compliant :rtype: bool """ compliant = True regex1 = "" if package == "pwquality": self.password = re.sub("pam_cracklib\.so", "pam_pwquality.so", self.password) if self.environ.getsystemfismacat() == "high": self.password = re.sub("minlen=8", "minlen=14", self.password) self.password = re.sub("minclass=3", "minclass=4", self.password) regex1 = PWQUALITY_HIGH_REGEX else: regex1 = PWQUALITY_REGEX if not self.chkpwquality(): compliant = False elif package == "cracklib": self.password = re.sub("pam_pwquality\.so", "pam_cracklib.so", self.password) if self.environ.getsystemfismacat() == "high": self.password = re.sub("minlen=8", "minlen=14", self.password) self.password = re.sub("minclass=3", "minclass=4", self.password) regex1 = CRACKLIB_HIGH_REGEX else: regex1 = CRACKLIB_REGEX regex2 = "^password[ \t]+sufficient[ \t]+pam_unix.so\s+sha512\s+shadow\s+try_first_pass\s+use_authtok\s+remember=10" pamfiles = [] if self.ph.manager in ("yum", "dnf"): pamfiles.append(self.pamauthfile) pamfiles.append(self.pampassfile) else: pamfiles.append(self.pampassfile) for pamfile in pamfiles: found1, found2 = False, False if not os.path.exists(pamfile): self.detailedresults += pamfile + " doesn't exist\n" compliant = False else: if not checkPerms(pamfile, [0, 0, 0o644], self.logger): self.detailedresults += "Incorrect permissions or ownership exist for file " + pamfile compliant = False contents = readFile(pamfile, self.logger) if not contents: self.detailedresults += pamfile + " is blank\n" compliant = False else: for line in contents: if re.search(regex1, line.strip()): found1 = True if re.search(regex2, line.strip()): found2 = True if not found1: self.detailedresults += "\n'password requisite ...' line not correct in " + pamfile if not found2: self.detailedresults += "\n'password sufficient ...' line not correct in " + pamfile compliant = False return compliant def setpasswordsetup(self, regex1, pkglist = None): """ configure password requirements in pam, install necessary packages :param regex1: string; regular expression :param pkglist: list; string names of packages to install :return: success :rtype: bool """ regex2 = "^password[ \t]+sufficient[ \t]+pam_unix.so sha512 shadow " + \ "try_first_pass use_authtok remember=10" success = True pamfiles = [] installed = False if pkglist: for pkg in pkglist: if self.ph.check(pkg): installed = True break else: installed = True if not installed: for pkg in pkglist: if self.ph.checkAvailable(pkg): if not self.ph.install(pkg): self.detailedresults += "Unable to install pkg " + pkg + "\n" return False else: installed = True if self.usingpwquality: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) comm = self.ph.getRemove() + pkg event = {"eventtype": "commandstring", "command": comm} self.statechglogger.recordchgevent(myid, event) pwqfile = "/etc/security/pwquality.conf" tmpfile = pwqfile + ".stonixtmp" if self.environ.getsystemfismacat() == "high": data = {"difok": "7", "minlen": "14", "dcredit": "0", "ucredit": "0", "lcredit": "0", "ocredit": "0", "maxrepeat": "3", "minclass": "4"} else: data = {"difok": "7", "minlen": "8", "dcredit": "0", "ucredit": "0", "lcredit": "0", "ocredit": "0", "maxrepeat": "3", "minclass": "3"} self.pwqeditor = KVEditorStonix(self.statechglogger, self.logger, "conf", pwqfile, tmpfile, data, "present", "openeq") self.pwqeditor.report() break if not installed: self.detailedresults += "No password checking program available\n" return False if self.usingpwquality: if not self.setpwquality(): success = False if self.ph.manager in ("yum", "dnf"): writecontents = self.auth + "\n" + self.acct + "\n" + \ self.password + "\n" + self.session pamfiles.append(self.pamauthfile) pamfiles.append(self.pampassfile) else: writecontents = self.password pamfiles.append(self.pampassfile) for pamfile in pamfiles: if not os.path.exists(pamfile): self.detailedresults += pamfile + " doesn't exist.\n" + \ "Stonix will not attempt to create this file " + \ "and the fix for the this rule will not continue\n" return False # """Check permissions on pam file(s)""" for pamfile in pamfiles: if not checkPerms(pamfile, [0, 0, 0o644], self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(pamfile, [0, 0, 0o644], self.logger, self.statechglogger, myid): success = False self.detailedresults += "Unable to set correct permissions on " + pamfile + "\n" contents = readFile(pamfile, self.logger) found1, found2 = False, False for line in contents: if re.search(regex1, line.strip()): found1 = True if re.search(regex2, line.strip()): found2 = True if not found1 or not found2: tmpfile = pamfile + ".stonixtmp" if writeFile(tmpfile, writecontents, self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {'eventtype': 'conf', 'filepath': pamfile} self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange(pamfile, tmpfile, myid) os.rename(tmpfile, pamfile) os.chown(pamfile, 0, 0) os.chmod(pamfile, 0o644) resetsecon(pamfile) else: self.detailedresults += "Unable to write to " + pamfile + "\n" success = False return success def checkaccountlockout(self): """ Method to determine which account locking program to use if any @author: Derek Walker :return: compliant :rtype: bool """ which = "/usr/bin/which " cmd1 = which + "faillock" cmd2 = which + "pam_tally2" ch = CommandHelper(self.logger) pamfiles = [] compliant = True regex = "" if ch.executeCommand(cmd1): debug = "ran " + cmd1 + " successfully\n" self.logger.log(LogPriority.DEBUG, debug) if ch.getReturnCode() == 0: debug = "return code of 0 and using faillock\n" self.logger.log(LogPriority.DEBUG, debug) self.usingpamfail = True elif ch.executeCommand(cmd2): debug = "ran " + cmd2 + " successfully\n" self.logger.log(LogPriority.DEBUG, debug) if ch.getReturnCode() == 0: debug = "return code of 0 and using pam_tally2\n" self.logger.log(LogPriority.DEBUG, debug) self.usingpamtally2 = True else: self.detailedresults += "There is no account " + \ "locking program available for this " + \ "distribution\n" return False elif ch.executeCommand(cmd2): debug = "ran " + cmd2 + " successfully\n" self.logger.log(LogPriority.DEBUG, debug) if ch.getReturnCode() == 0: debug = "return code of 0 and using pam_tally2\n" self.logger.log(LogPriority.DEBUG, debug) self.usingpamtally2 = True else: self.detailedresults += "There is no account " + \ "locking program available for this " + \ "distribution\n" return False else: self.detailedresults += "There is no account " + \ "locking program available for this " + \ "distribution\n" return False if self.usingpamfail: regex = "^auth[ \t]+required[ \t]+pam_faillock.so preauth silent audit " + \ "deny=5 unlock_time=900 fail_interval=900" elif self.usingpamtally2: regex = "^auth[ \t]+required[ \t]+pam_tally2.so deny=5 " + \ "unlock_time=900 onerr=fail" if self.ph.manager in("yum", "dnf"): pamfiles.append(self.pamauthfile) pamfiles.append(self.pampassfile) else: pamfiles.append(self.pamauthfile) for pamfile in pamfiles: found = False if not os.path.exists(pamfile): self.detailedresults += "Critical pam file " + pamfile + " doesn't exist\n" compliant = False else: if not checkPerms(pamfile, [0, 0, 0o644], self.logger): self.detailedresults += "Permissions aren't correct on " + pamfile + "\n" self.ci3comp = False compliant = False contents = readFile(pamfile, self.logger) if not contents: self.detailedresults += pamfile + " is blank\n" self.ci3comp = False compliant = False else: for line in contents: if re.search(regex, line.strip()): found = True if not found: self.detailedresults += "Didn't find the correct contents in " + pamfile + "\n" self.ci3comp = False compliant = False return compliant def setaccountlockout(self, regex): """ configure the account lockout time in pam :param regex: string; regular expression :return: success :rtype: bool """ success = True pamfiles = [] if self.ph.manager in ("yum", "dnf"): pamfiles.append(self.pamauthfile) pamfiles.append(self.pampassfile) writecontents = self.auth + "\n" + self.acct + "\n" + \ self.password + "\n" + self.session else: pamfiles.append(self.pamauthfile) writecontents = self.auth for pamfile in pamfiles: if not os.path.exists(pamfile): self.detailedresults += pamfile + " doesn't exist.\n" + \ "Stonix will not attempt to create this file " + \ "and the fix for the this rule will not continue\n" return False # """Check permissions on pam file(s)""" for pamfile in pamfiles: if not checkPerms(pamfile, [0, 0, 0o644], self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(pamfile, [0, 0, 0o644], self.logger, self.statechglogger, myid): success = False self.detailedresults += "Unable to set " + \ "correct permissions on " + pamfile + "\n" contents = readFile(pamfile, self.logger) found = False for line in contents: if re.search(regex, line.strip()): found = True if not found: tmpfile = pamfile + ".stonixtmp" if writeFile(tmpfile, writecontents, self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {'eventtype': 'conf', 'filepath': pamfile} self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange(pamfile, tmpfile, myid) os.rename(tmpfile, pamfile) os.chown(pamfile, 0, 0) os.chmod(pamfile, 0o644) resetsecon(pamfile) else: self.detailedresults += "Unable to write to " + pamfile + "\n" success = False return success def chkpwquality(self): """ check settings of pwquality pam plugin :return: compliant :rtype: bool """ compliant = True pwqfile = "/etc/security/pwquality.conf" if os.path.exists(pwqfile): tmpfile = pwqfile + ".stonixtmp" if self.environ.getsystemfismacat() == "high": data = {"difok": "7", "minlen": "14", "dcredit": "0", "ucredit": "0", "lcredit": "0", "ocredit": "0", "maxrepeat": "3", "minclass": "4"} else: data = {"difok": "7", "minlen": "8", "dcredit": "0", "ucredit": "0", "lcredit": "0", "ocredit": "0", "maxrepeat": "3", "minclass": "3"} self.pwqeditor = KVEditorStonix(self.statechglogger, self.logger, "conf", pwqfile, tmpfile, data, "present", "openeq") if not self.pwqeditor.report(): compliant = False self.detailedresults += "Not all correct contents were found in " + pwqfile + "\n" else: compliant = False self.detailedresults += "System is using pwquality and crucial file /etc/security/pwquality.conf doesn't exist\n" return compliant def checklogindefs(self): """ Method to check the password hash algorithm settings in login.defs :return: compliant :rtype: bool """ compliant = True if os.path.exists(self.logindefs): if not checkPerms(self.logindefs, [0, 0, 0o644], self.logger): self.detailedresults += "Permissions incorrect for " + \ self.logindefs + " file\n" compliant = False data = {"MD5_CRYPT_ENAB": "no", "ENCRYPT_METHOD": "SHA512", "PASS_MAX_DAYS": "180", "PASS_MIN_DAYS": "1", "PASS_WARN_AGE": "7", "FAIL_DELAY": "4"} tmppath = self.logindefs + ".stonixtmp" self.editor2 = KVEditorStonix(self.statechglogger, self.logger, "conf", self.logindefs, tmppath, data, "present", "space") if not self.editor2.report(): debug = self.logindefs + " doesn't contain the correct " + \ "contents\n" self.detailedresults += self.logindefs + " doesn't contain " + \ "the correct contents\n" self.logger.log(LogPriority.DEBUG, debug) compliant = False return compliant def checklibuser(self): """ Private method to check the password hash algorithm settings in libuser.conf @author: Derek Walker :return: compliant :rtype: bool """ compliant = True # """check if libuser is intalled""" if not self.ph.check("libuser"): # """if not, check if available""" if self.ph.checkAvailable("libuser"): self.detailedresults += "libuser available but not installed\n" return False else: # """not available, not a problem""" return True # """create a kveditor for file if it exists, if not, we do it in # the setlibuser method inside the fix""" if os.path.exists(self.libuserfile): data = {"defaults": {"crypt_style": "sha512"}} datatype = "tagconf" intent = "present" tmppath = self.libuserfile + ".stonixtmp" self.editor1 = KVEditorStonix(self.statechglogger, self.logger, datatype, self.libuserfile, tmppath, data, intent, "openeq") if not self.editor1.report(): debug = "/etc/libuser.conf doesn't contain the correct contents\n" self.detailedresults += "/etc/libuser.conf doesn't contain the correct contents\n" self.logger.log(LogPriority.DEBUG, debug) compliant = False if not checkPerms(self.libuserfile, [0, 0, 0o644], self.logger): self.detailedresults += "Permissions are incorrect on " + self.libuserfile + "\n" compliant = False else: self.detailedresults += "Libuser installed but libuser file doesn't exist\n" compliant = False return compliant def setpwquality(self): """ :return: """ success = True created = False pwqfile = "/etc/security/pwquality.conf" if not os.path.exists(pwqfile): createFile(pwqfile, self.logger) self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {'eventtype': 'creation', 'filepath': pwqfile} self.statechglogger.recordchgevent(myid, event) created = True tmpfile = pwqfile + ".stonixtmp" if self.environ.getsystemfismacat() == "high": data = {"difok": "7", "minlen": "14", "dcredit": "0", "ucredit": "0", "lcredit": "0", "ocredit": "0", "maxrepeat": "3", "minclass": "4"} else: data = {"difok": "7", "minlen": "8", "dcredit": "0", "ucredit": "0", "lcredit": "0", "ocredit": "0", "maxrepeat": "3", "minclass": "3"} self.pwqeditor = KVEditorStonix(self.statechglogger, self.logger, "conf", pwqfile, tmpfile, data, "present", "openeq") self.pwqeditor.report() if self.pwqeditor.fixables: if self.pwqeditor.fix(): if not created: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.pwqeditor.setEventID(myid) if not self.pwqeditor.commit(): success = False self.detailedresults += "Unable to correct " + pwqfile + "\n" else: success = False self.detailedresults += "Unable to correct " + pwqfile + "\n" return success def setlibuser(self): """Method to check if libuser is installed and the contents of libuser file. @author: Derek Walker :return: bool """ created = False success = True data = {"defaults": {"crypt_style": "sha512"}} # """check if installed""" if not self.ph.check("libuser"): # """if not installed, check if available""" if self.ph.checkAvailable("libuser"): # """if available, install it""" if not self.ph.install("libuser"): self.detailedresults += "Unable to install libuser\n" return False else: # """since we're just now installing it we know we now # need to create the kveditor""" self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) comm = self.ph.getRemove() + "libuser" event = {"eventtype": "commandstring", "command": comm} self.statechglogger.recordchgevent(myid, event) datatype = "tagconf" intent = "present" tmppath = self.libuserfile + ".stonixtmp" self.editor1 = KVEditorStonix(self.statechglogger, self.logger, datatype, self.libuserfile, tmppath, data, intent, "openeq") self.editor1.report() else: return True if not os.path.exists(self.libuserfile): if not createFile(self.libuserfile, self.logger): self.detailedresults += "Unable to create libuser file\n" debug = "Unable to create the libuser file\n" self.logger.log(LogPriority.DEBUG, debug) return False created = True self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "creation", "filepath": self.libuserfile} self.statechglogger.recordchgevent(myid, event) tmppath = self.libuserfile + ".stonixtmp" self.editor1 = KVEditorStonix(self.statechglogger, self.logger, "tagconf", self.libuserfile, tmppath, data, "present", "openeq") self.editor1.report() if not checkPerms(self.libuserfile, [0, 0, 0o644], self.logger): if not created: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(self.libuserfile, [0, 0, 0o644], self.logger, self.statechglogger, myid): success = False self.detailedresults += "Unable to set the permissions on " + self.libuserfile + "\n" elif not setPerms(self.libuserfile, [0, 0, 0o644], self.logger): success = False self.detailedresults += "Unable to set the permissions on " + self.libuserfile + "\n" if self.editor1.fixables: if not created: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.editor1.setEventID(myid) if self.editor1.fix(): if self.editor1.commit(): debug = "/etc/libuser.conf has been corrected\n" self.logger.log(LogPriority.DEBUG, debug) os.chown(self.libuserfile, 0, 0) os.chmod(self.libuserfile, 0o644) resetsecon(self.libuserfile) else: self.detailedresults += "/etc/libuser.conf couldn't be corrected\n" success = False else: self.detailedresults += "/etc/libuser.conf couldn't be corrected\n" success = False return success def setlogindefs(self): """ configure login.defs options :return: success :rtype: bool """ success = True if not checkPerms(self.logindefs, [0, 0, 0o644], self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(self.logindefs, [0, 0, 0o644], self.logger, self.statechglogger, myid): self.detailedresults += "Unable to set permissions on " + self.logindefs + " file\n" success = False if self.editor2: if self.editor2.fixables: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.editor2.setEventID(myid) if self.editor2.fix(): if self.editor2.commit(): debug = "/etc/login.defs file has been corrected\n" self.logger.log(LogPriority.DEBUG, debug) os.chown(self.logindefs, 0, 0) os.chmod(self.logindefs, 0o644) resetsecon(self.logindefs) else: debug = "Unable to correct the contents of /etc/login.defs\n" self.detailedresults += "Unable to correct the contents of /etc/login.defs\n" self.logger.log(LogPriority.DEBUG, debug) success = False else: self.detailedresults += "Unable to correct the contents of /etc/login.defs\n" debug = "Unable to correct the contents of /etc/login.defs\n" self.logger.log(LogPriority.DEBUG, debug) success = False return success
class InstallBanners(RuleKVEditor): '''Install and configure warning banners, to be displayed at startup.''' def __init__(self, config, environ, logger, statechglogger): """ Constructor """ RuleKVEditor.__init__(self, config, environ, logger, statechglogger) self.logger = logger self.rulenumber = 51 self.rulename = 'InstallBanners' self.formatDetailedResults("initialize") self.mandatory = True self.sethelptext() self.rootrequired = True self.compliant = False self.guidance = [ 'CIS', 'NSA 2.3.7.2', 'CCE 4188-9', 'CCE 4431-3', 'CCE 3717-6', 'CCE 4554-2', 'CCE 4603-7', 'CCE 4760-5', 'CCE 4301-8', 'CCE 4698-7', 'CCE 4222-6', 'CCE 4103-8', 'CCE 4870-2', 'CCE 4896-7' ] self.iditerator = 0 self.applicable = { 'type': 'white', 'family': ['linux', 'solaris', 'freebsd'], 'os': { 'Mac OS X': ['10.15', 'r', '10.15.10'] } } # init CIs datatype = 'bool' key = 'INSTALLBANNERS' instructions = "To prevent the installation of warning banners, " + \ "set the value of InstallBanners to False.\n\n!DEBIAN USERS! Due to " + \ "a bug in gdm3 (gnome 3) which has not been patched on debian, we are forced to " + \ "change the user's display manager to something else in order to be compliant " + \ "with the login banner requirement. As a result, the login banner changes will " + \ "only take effect after a system reboot." default = True self.ci = self.initCi(datatype, key, instructions, default) # Initial setup and deterministic resolution of variables constlist = [ WARNINGBANNER, GDMWARNINGBANNER, GDM3WARNINGBANNER, ALTWARNINGBANNER, OSXSHORTWARNINGBANNER ] if not self.checkConsts(constlist): self.applicable = {'type': 'white', 'family': []} return self.initobjs() self.setcommon() self.islinux() self.ismac() def initobjs(self): ''' ''' try: self.ph = Pkghelper(self.logger, self.environ) self.ch = CommandHelper(self.logger) self.linux = False self.mac = False self.gnome2 = False self.gnome3 = False self.kde = False self.lightdm = False self.badline = False except Exception: raise def setgnome2(self): '''set up all variables for use with gnome2-based systems @author: Breen Malmberg ''' self.gnome2 = True self.gnome2bannertext = GDMWARNINGBANNER def forcelightdm(self): '''force debian systems using gdm3 to change to the lightdm display manager in order to comply with login/display banner requirements :returns: retval :rtype: bool @author: Breen Malmberg ''' retval = True self.logger.log(LogPriority.DEBUG, "Forcing display manager to be lightdm...") if self.gnome2: cmd = '/usr/sbin/dpkg-reconfigure --frontend=noninteractive gdm' elif self.gnome3: cmd = '/usr/sbin/dpkg-reconfigure --frontend=noninteractive gdm3' else: cmd = '/usr/sbin/dpkg-reconfigure --frontend=noninteractive lightdm' cmd2 = "/usr/sbin/dpkg --configure lightdm" self.lightdm = True path = '/etc/X11/default-display-manager' opt = '/usr/sbin/lightdm\n' mode = 0o644 uid = 0 gid = 0 try: if not self.setFileContents(path, opt, [uid, gid, mode]): retval = False if not self.ph.check('lightdm'): self.logger.log( LogPriority.DEBUG, "Package: lightdm not installed yet. Installing lightdm..." ) if not self.ph.install('lightdm'): retval = False self.detailedresults += "\nFailed to install lightdm" if not self.ch.executeCommand(cmd): retval = False self.detailedresults += "\nFailed to set lightdm as default display manager" self.ch.executeCommand(cmd2) if retval: self.detailedresults += "\nThe display manager has been changed to lightdm. These changes will only take effect once the system has been restarted." else: self.detailedresults += "\nFailed to change default display manager to lightdm. Login banner may not display." except Exception: raise return retval def setgnome3(self): '''set up all variables for use with gnome3-based systems @author: Breen Malmberg ''' self.gnome3 = True self.gnome3bannertext = GDM3WARNINGBANNER def setkde(self): '''set up all variables for use with kde-based systems @author: Breen Malmberg ''' self.kde = True self.kdebannertext = ALTWARNINGBANNER self.kdelocs = [ '/etc/kde/kdm/kdmrc', '/etc/kde3/kdm/kdmrc', '/etc/kde4/kdm/kdmrc', '/usr/share/config/kdm/kdmrc', '/usr/share/kde4/config/kdm/kdmrc' ] self.kdefile = '/usr/share/kde4/config/kdm/kdmrc' for loc in self.kdelocs: if os.path.exists(loc): self.kdefile = str(loc) tmpfile = self.kdefile + '.stonixtmp' key1 = 'GreetString' val1 = '"' + str(self.kdebannertext) + '"' self.greetbanner = [key1, val1] key2 = 'UserList' val2 = 'false' key3 = 'UseTheme' val3 = 'false' key4 = 'PreselectUser' val4 = 'None' key5 = 'LogoArea' val5 = 'None' key6 = 'GreeterPos' val6 = '45,45' key7 = 'AntiAliasing' val7 = 'false' key8 = 'SortUsers' val8 = 'false' key9 = 'GreetFont' val9 = 'Serif,20,-1,5,50,0,0,0,0,0' key10 = 'FailFont' val10 = 'Sans Serif,10,-1,5,75,0,0,0,0,0' bkey1 = 'PreselectUser' bval1 = 'None' self.kdedict = { "X-*-Greeter": { key2: val2, key3: val3, key4: val4, key5: val5, key6: val6, key7: val7, key8: val8, key9: val9, key10: val10 }, "X-:*-Greeter": { bkey1: bval1 } } self.kdeditor = KVEditorStonix(self.statechglogger, self.logger, "tagconf", self.kdefile, tmpfile, self.kdedict, "present", "closedeq") def setlightdm(self): '''set up all variables for use with lightdm-based systems @author: Breen Malmberg ''' self.lightdm = True self.ldmbannertext = ALTWARNINGBANNER key1 = '/usr/share/lightdm/lightdm.conf.d/50-ubuntu.conf' val1 = [ '[SeatDefaults]', 'allow-guest=false', 'greeter-hide-users=true', 'greeter-show-manual-login=true', 'autologin-user='******'/etc/lightdm/lightdm.conf.d/stonixlightdm.conf' val3 = [ '[SeatDefaults]', 'greeter-setup-script=/bin/sh -c "until /usr/bin/zenity ' + '--width=600 --height=400 --title=WARNING --text-info ' + '--filename=' + str(self.loginbannerfile) + '; do :; done"' ] self.lightdmdict = {key1: val1, key2: val2, key3: val3} def setlinuxcommon(self): '''set up all variables for use with linux-based systems @author: Breen Malmberg ''' if not self.sshdfile: self.sshdfile = '/etc/ssh/sshd_config' self.bannerfiles = [ "/etc/banners/in.ftpd", "/etc/banners/in.rlogind", "/etc/banners/in.rshd", "/etc/banners/in.telnetd", "/etc/banner" ] def setcommon(self): '''set up all variables for use with all systems @author: Breen Malmberg ''' self.loginbannerfile = "/etc/issue" self.sshbannerfile = "/etc/banner" self.sshdfile = "" self.sshdlocs = [ "/etc/sshd_config", "/etc/ssh/sshd_config", "/private/etc/ssh/sshd_config", "/private/etc/sshd_config" ] for loc in self.sshdlocs: if os.path.exists(loc): self.sshdfile = str(loc) self.sshbannerdict = {"Banner": self.sshbannerfile} def setmac(self): '''set up all variables for use with darwin-based systems @author: Breen Malmberg ''' self.mac = True self.bannertext = WARNINGBANNER if not self.sshdfile: self.sshdfile = '/private/etc/sshd_config' self.ftpwelcomelocs = ["/etc/ftpwelcome", "/private/etc/ftpwelcome"] self.ftpwelcomefile = '/private/etc/ftpwelcome' for loc in self.ftpwelcomelocs: if os.path.exists(loc): self.ftpwelcomefile = str(loc) self.policybanner = "/Library/Security/PolicyBanner.txt" self.addKVEditor( "FileServerBannerText", "defaults", "/Library/Preferences/com.apple.AppleFileServer", "", { "loginGreeting": [re.escape(WARNINGBANNER), "'\"" + WARNINGBANNER + "\"'"] }, "present", "", "To prevent the installation of a warning banner," + " set the value of InstallBanners to False", self.ci) self.addKVEditor( "loginwindowBannerText", "defaults", "/Library/Preferences/com.apple.loginwindow", "", { "LoginwindowText": [ re.escape(OSXSHORTWARNINGBANNER), '"' + OSXSHORTWARNINGBANNER + '"' ] }, "present", "", "To prevent the installation of a warning banner," + " set the value of InstallBanners to False", self.ci) def ismac(self): '''determine whether the current system is macintosh, or darwin-based @author: Breen Malmberg ''' try: if self.environ.getosfamily() == 'darwin': self.setmac() self.logger.log( LogPriority.DEBUG, "System is Mac OS. Configuring Mac banners...") except Exception: raise def islinux(self): '''determine whether the current system is linux-based, and set all distro-specific variables @author: Breen Malmberg ''' try: if self.environ.getosfamily() == 'linux': self.linux = True self.setlinuxcommon() if os.path.exists("/usr/sbin/gdm3"): self.setgnome3() elif os.path.exists("/usr/sbin/gdm"): self.ch.executeCommand("/usr/sbin/gdm --version") gnomeversionstring = self.ch.getOutputString() if re.search("GDM\s+3\.", gnomeversionstring, re.IGNORECASE): self.setgnome3() else: self.setgnome2() if os.path.exists("/usr/sbin/lightdm"): self.setlightdm() if os.path.exists("/usr/bin/startkde"): self.setkde() except Exception: raise def getFileContents(self, filepath, returntype='list'): '''Retrieve and return file contents (in list format) of a given file path :param filepath: string full path to file to read :param returntype: string valid values: 'list', 'string' (Default value = 'list') :returns: filecontents :rtype: list @author: Breen Malmberg ''' self.logger.log(LogPriority.DEBUG, "Retrieving contents of file " + filepath) try: if returntype == 'list': filecontents = [] elif returntype == 'string': filecontents = '' else: filecontents = '' self.detailedresults += "\nReturntype parameter must be " + \ "either 'list' or 'string!'" if os.path.exists(filepath): f = open(filepath, 'r') if returntype == 'list': filecontents = f.readlines() elif returntype == 'string': filecontents = f.read() f.close() else: self.detailedresults += '\nCould not find specified file: ' + \ str(filepath) + '. Returning empty value...' except Exception: raise return filecontents def setFileContents(self, filepath, contents, perms=[0, 0, 0o644]): '''write (or append) specified contents to specified file :param filepath: string full path to file :param contents: list/string object to write to file (can be either list or string) :param mode: string indicates the IO method to use to open the file. Valid values are 'w' for write, and 'a' for append :param perms: integer-list of size 3. first index/position in list indicates octal permissions flag; second indicates uid; third indicates gid (Default value = [0) :param 0: :param 0644]: :returns: retval :rtype: boolean @author: Breen Malmberg ''' retval = True tmpPath = filepath + ".stonixtmp" self.logger.log(LogPriority.DEBUG, "Writing changes to file " + filepath) try: if os.path.exists(filepath): f = open(tmpPath, "w") if isinstance(contents, list): f.writelines(contents) else: f.write(contents) f.close() self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {'eventtype': 'conf', 'filepath': filepath} self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange(filepath, tmpPath, myid) os.rename(tmpPath, filepath) os.chmod(filepath, perms[2]) os.chown(filepath, perms[0], perms[1]) else: # get the parent directory of filepath basepath = ("/").join(filepath.split("/")[:-1]) # create parent directory if it doesn't exist if not os.path.exists(basepath): os.makedirs(basepath, 0o755) # then create the file f = open(filepath, "w") if isinstance(contents, list): f.writelines(contents) else: f.write(contents) f.close() self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {'eventtype': 'creation', 'filepath': filepath} self.statechglogger.recordchgevent(myid, event) os.chmod(filepath, perms[2]) os.chown(filepath, perms[0], perms[1]) if not fixInflation(filepath, self.logger, perms[2], [perms[0], perms[1]]): retval = False except Exception: retval = False raise return retval def replaceFileContents(self, filepath, contentdict): '''replace key from contentdict with value from contentdict, in filepath :param filepath: string full path to file :param contentdict: dictionary of strings :returns: retval :rtype: boolean @author: Breen Malmberg ''' retval = True appends = [] self.logger.log(LogPriority.DEBUG, "Replacing any existing, incorrect configurations...") try: contentlines = self.getFileContents(filepath) for line in contentlines: for key in contentdict: if any((re.search("\*", key), re.search("\*", line))): if re.search("^" + re.escape(key), re.escape(line)): contentlines = [ c.replace(line, key + contentdict[key]) for c in contentlines ] else: if re.search("^" + key, line): contentlines = [ c.replace(line, key + contentdict[key]) for c in contentlines ] for key in contentdict: if key.strip() not in contentlines: appends.append(key + contentdict[key]) self.appendFileContents(filepath, contentlines, appends) except Exception: self.rulesuccess = False raise return retval def appendFileContents(self, filepath, contents, appends): ''' :param filepath: :param contents: :param appends: ''' self.logger.log(LogPriority.DEBUG, "Appending missing configuration lines...") for a in appends: contents.append(a + "\n") self.setFileContents(filepath, contents) def reportFileContents(self, filepath, searchparams): '''verify that the give key:value pairs in contentdict exist in filepath :param filepath: string full path to file :param searchparams: list or string to search in file contents :returns: retval :rtype: boolean @author: Breen Malmberg ''' retval = True foundDict = {} findresult = 999 foundstring = False try: if not os.path.exists(filepath): retval = False self.detailedresults += "\nRequired configuration file " + filepath + " does not exist" return retval if isinstance(searchparams, str): contents = self.getFileContents(filepath, "string") findresult = contents.find(searchparams) if findresult != -1: foundstring = True if not foundstring: retval = False self.logger.log( LogPriority.DEBUG, "str.find() return code = " + str(findresult)) self.detailedresults += "\nSearch parameter:\n" + searchparams + "\nis missing from " + filepath elif isinstance(searchparams, list): for p in searchparams: foundDict[p] = False contentlines = self.getFileContents(filepath) for line in contentlines: for p in searchparams: if line.find(p) != -1: foundDict[p] = True for i in foundDict: if not foundDict[i]: retval = False self.detailedresults += "\nSearch parameter:\n" + i + "\nis missing from " + filepath except Exception: raise return retval def checkCommand(self, cmd, val="", regex=True): '''check the output of a given command to see if it matches given value :param cmd: :param val: (Default value = "") :param regex: (Default value = True) :returns: retval :rtype: boolean @author: Breen Malmberg ''' retval = True try: self.ch.executeCommand(cmd) retcode = self.ch.getReturnCode() # gconftool-2 returns either 0 or -1 on error if re.search("gconftool-2", cmd): if retcode == -1: retval = False errstr = self.ch.getErrorString() self.logger.log(LogPriority.DEBUG, errstr) self.detailedresults += "\nCommand:\n" + cmd + "\nFailed" else: # all other programs return 0 on success if retcode != 0: retval = False errstr = self.ch.getErrorString() self.logger.log(LogPriority.DEBUG, errstr) if val: outputstr = self.ch.getOutputString() if not regex: # outputstr = outputstr.strip() # val = val.strip() if outputstr.find(val) == -1: retval = False self.detailedresults += "\nDesired output:\n" self.detailedresults += val self.detailedresults += "\n\nActual output:\n" self.detailedresults += outputstr else: if not re.search(val, outputstr, re.IGNORECASE): retval = False self.logger.log( LogPriority.DEBUG, "Could not find search regex:\n" + val + "\nin command output") except Exception: raise return retval def report(self): '''The report method examines the current configuration and determines whether or not it is correct. If the config is correct then the self.compliant, self.detailed results and self.currstate properties are updated to reflect the system status. self.rulesuccess will be updated if the rule does not succeed. :returns: self.compliant :rtype: boolean @author Breen Malmberg ''' self.compliant = True self.detailedresults = "" try: if self.linux: if not self.reportlinux(): self.compliant = False elif self.mac: if not self.reportmac(): self.compliant = False else: self.compliant = False self.detailedresults += '\nCould not identify operating system, or operating system not supported.' except (KeyboardInterrupt, SystemExit): raise except Exception as err: self.compliant = False self.detailedresults += "\n" + traceback.format_exc() self.logdispatch.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.compliant def getgnome3version(self): '''get the gnome3 version as a float :returns: g3ver :rtype: float @author: Breen Malmberg ''' g3ver = 0.0 cmd = '/usr/sbin/gdm3 --version' backupcmd = '/usr/bin/gnome-session --version' if not any((self.checkCommand(cmd), self.checkCommand(backupcmd))): return g3ver else: outlines = self.ch.getOutput() for line in outlines: sline = line.split() if len(sline) > 1: splitver = sline[1].split('.') combinedver = splitver[0] + '.' + splitver[1] g3ver = float(combinedver) else: try: g3ver = float(sline[0]) except Exception: return g3ver return g3ver def reportlinux(self): '''run report functionality for linux-based systems :returns: retval :rtype: boolean @author: Breen Malmberg ''' compliant = True try: if all((self.gnome3, re.search('debian', self.environ.getostype(), re.IGNORECASE))): if self.getgnome3version() < 3.15: self.gnome3 = False self.forcelightdm() self.setlightdm() if not self.reportlinuxcommon(): compliant = False if self.gnome2: if not self.reportgnome2(): compliant = False elif self.gnome3: if not self.reportgnome3(): compliant = False if self.lightdm: if not self.reportlightdm(): compliant = False if self.kde: if not self.reportkde(): compliant = False except Exception: raise return compliant def reportlinuxcommon(self): '''run report functionality which is common to linux platforms :returns: retval :rtype: boolean @author: Breen Malmberg ''' compliant = True try: for f in self.bannerfiles: if not self.reportFileContents(f, WARNINGBANNER): compliant = False if not self.reportcommon(): compliant = False except Exception: raise return compliant def reportcommon(self): '''run report functionality which is common to all platforms :returns: retval :rtype: boolean @author: Breen Malmberg ''' compliant = True if not os.path.exists(self.sshdfile): self.detailedresults += "Required configuration file " + self.ssdhfile + \ " does not exist.\n" return False contents = readFile(self.sshdfile, self.logger) self.commoneditor = KVEditorStonix(self.statechglogger, self.logger, "conf", self.sshdfile, self.sshdfile + ".tmp", self.sshbannerdict, "present", "space") if not self.commoneditor.report(): compliant = False self.detailedresults += self.sshdfile + " doesn't contain correct contents\n" return compliant def reportgnome2(self): '''run report functionality for gnome2-based systems :returns: retval :rtype: boolean @author: Breen Malmberg ''' compliant = True try: gconf = "/usr/bin/gconftool-2" confDict = { "/apps/gdm/simple-greeter/disable_user_list": "true", "/apps/gdm/simple-greeter/banner_message_enable": "true", "/apps/gdm/simple-greeter/banner_message_text": ALTWARNINGBANNER } # gdm 2 db cannot reliably store/preserve/interpret newline characters gconfcommands = [] for item in confDict: gconfcommands.append(gconf + " -g " + item) for gcmd in gconfcommands: if not self.checkCommand(gcmd, confDict[gcmd.split()[2]], False): compliant = False except Exception: raise return compliant def reportgnome3(self): '''run report functionality for gnome3-based systems :returns: retval :rtype: boolean @author: Breen Malmberg ''' compliant = True try: if not self.checkprofilecfg(): compliant = False if not self.checkkeyfilecfg(): compliant = False except Exception: raise return compliant def checkprofilecfg(self): ''' ''' compliant = True profilefile = "/etc/dconf/profile/gdm" profileoptslist = ["user-db:user", "system-db:gdm"] if not self.reportFileContents(profilefile, profileoptslist): compliant = False return compliant def checkkeyfilecfg(self): ''' ''' compliant = True keyfile = "/etc/dconf/db/gdm.d/01-banner-message" keyfileoptslist = [ "[org/gnome/login-screen]", "banner-message-enable=true", "banner-message-text='" + GDM3WARNINGBANNER + "'", "disable-user-list=true" ] if not self.reportFileContents(keyfile, keyfileoptslist): compliant = False return compliant def reportlightdm(self): '''run report functionality for lightdm-based systems :returns: retval :rtype: boolean @author: Breen Malmberg ''' compliant = True try: for item in self.lightdmdict: if not self.reportFileContents(item, self.lightdmdict[item]): compliant = False self.detailedresults += '\nRequired configuration text not found in: ' + str( item) except Exception: raise return compliant def reportkde(self): '''run report functionality for kde-based systems :returns: retval :rtype: boolean @author: Breen Malmberg ''' retval = True try: if not os.path.exists(self.kdefile): retval = False self.detailedresults += 'Required configuration file: ' + str( self.kdefile) + ' not found' else: if not self.kdeditor.report(): retval = False if self.kdeditor.fixables: self.detailedresults += '\nThe following required options are missing from ' + \ str(self.kdefile) + ':\n' + '\n'.join(str(f) for f in self.kdeditor.fixables) \ + '\n' except Exception: raise return retval def reportmac(self): '''run report functionality for macintosh, or darwin-based systems :returns: retval :rtype: boolean @author: Breen Malmberg ''' retval = True try: if not RuleKVEditor.report(self, True): self.detailedresults += '\nEither file server banner text ' + \ 'or login window banner text is incorrect, or both' retval = False if os.path.exists(self.ftpwelcomefile): if not self.reportFileContents(self.ftpwelcomefile, self.bannertext): retval = False self.detailedresults += '\nIncorrect configuration ' + \ 'text in: ' + str(self.ftpwelcomefile) else: retval = False self.detailedresults += '\nRequired configuration file: ' + \ str(self.ftpwelcomefile) + ' not found' if os.path.exists(self.policybanner): if not self.reportFileContents(self.policybanner, self.bannertext): retval = False self.detailedresults += '\nIncorrect configuration ' + \ 'text in: ' + str(self.policybanner) else: retval = False self.detailedresults += '\nRequired configuration file: ' + \ str(self.policybanner) + ' not found' if not self.reportcommon(): retval = False except Exception: raise return retval def fix(self): '''Install warning banners, set the warning text and configure the file permissions for the warning banner files. :returns: success :rtype: boolean @author Breen Malmberg ''' self.rulesuccess = True self.detailedresults = "" self.iditerator = 0 eventlist = self.statechglogger.findrulechanges(self.rulenumber) for event in eventlist: self.statechglogger.deleteentry(event) try: if self.ci.getcurrvalue(): if not self.fixcommon(): self.rulesuccess = False if self.linux: if not self.fixlinux(): self.rulesuccess = False elif self.mac: if not self.fixmac(): self.rulesuccess = False else: self.rulesuccess = False self.detailedresults += '\nCould not identify ' + \ 'operating system, or operating system not supported.' else: self.detailedresults += '\nThe configuration item for ' + \ 'this rule was not enabled so nothing was done.' except (KeyboardInterrupt, SystemExit): raise except Exception as err: self.rulesuccess = False self.detailedresults += "\n" + traceback.format_exc() self.logdispatch.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.rulesuccess def fixlinux(self): '''run fix functionality for linux-based systems :returns: success :rtype: boolean @author: Breen Malmberg ''' success = True try: if not self.fixlinuxcommon(): success = False if self.gnome2: if not self.fixgnome2(): success = False elif self.gnome3: if not self.fixgnome3(): success = False if self.lightdm: if not self.fixlightdm(): success = False if self.kde: if not self.fixkde(): success = False except Exception: raise return success def fixlinuxcommon(self): '''run fix functionality which is common to linux platforms :returns: success :rtype: boolean @author: Breen Malmberg ''' success = True try: for f in self.bannerfiles: if not self.setFileContents(f, WARNINGBANNER): success = False except Exception: raise return success def fixcommon(self): '''run fix functionlity which is common to all platforms :returns: success :rtype: boolean @author: Breen Malmberg ''' success = True if self.commoneditor.fixables: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.commoneditor.setEventID(myid) if not self.commoneditor.fix(): debug = self.sshdfile + " editor fix is returning false\n" self.logger.log(LogPriority.DEBUG, debug) success = False elif not self.commoneditor.commit(): debug = self.sshdfile + " editor commit is returning false\n" self.logger.log(LogPriority.DEBUG, debug) success = False if not success: self.detailedresults += "Unable to correct contents of " + \ self.sshdfile + "\n" return success def fixgnome2(self): '''run fix functionality for gnome2-based systems (implementation based on: https://access.redhat.com/solutions/32480) :returns: success :rtype: boolean @author: Breen Malmberg ''' success = True gconf = "/usr/bin/gconftool-2" confDict = { "/apps/gdm/simple-greeter/disable_user_list": "boolean true", "/apps/gdm/simple-greeter/banner_message_enable": "boolean true", "/apps/gdm/simple-greeter/banner_message_text": 'string "$(cat /etc/issue)"' } gconfcommands = [] for item in confDict: gconfcommands.append( gconf + " -s " + item + " --config-source=xml:readwrite:/etc/gconf/gconf.xml.system --direct --type " + confDict[item]) try: if not self.setFileContents("/etc/issue", GDMWARNINGBANNER): success = False self.detailedresults += "\nFailed to properly configure the banner text in file /etc/issue" # write configuration options to gnome system-wide database for gcmd in gconfcommands: if not self.checkCommand(gcmd): success = False except Exception: raise return success def fixgnome3(self): '''run fix functionality for gnome3-based systems :returns: success :rtype: boolean @author: Breen Malmberg ''' success = True try: if not self.setFileContents("/etc/issue", GDM3WARNINGBANNER): success = False self.detailedresults += "\nFailed to properly configure the banner text in file /etc/issue" self.logger.log(LogPriority.DEBUG, "Applying gdm profile configurations...") # configure the gdm profile greeterdefaults = "" gdefaultslocs = [ "/usr/share/gdm/greeter-dconf-defaults", "/usr/share/gdm/greeter.dconf-defaults" ] for loc in gdefaultslocs: if os.path.exists(loc): greeterdefaults = loc profilebasedir = "/etc/dconf/profile" profilepath = profilebasedir + "/gdm" profileconflist = [ "user-db:user\n", "system-db:gdm\n", "file-db:" + greeterdefaults + "\n" ] if not os.path.exists(profilebasedir): self.logger.log( LogPriority.DEBUG, "gdm profile base directory does not exist. Creating it..." ) os.makedirs(profilebasedir, 0o755) if not self.setFileContents(profilepath, profileconflist): success = False self.logger.log(LogPriority.DEBUG, "Applying gdm keyfile configurations...") # configure the gdm keyfile gdmkeyfilebase = "/etc/dconf/db/gdm.d" gdmkeyfile = gdmkeyfilebase + "/01-banner-message" gdmkeyfileconflist = [ "[org/gnome/login-screen]\n", "banner-message-enable=true\n", "banner-message-text='" + GDM3WARNINGBANNER + "'\n", "disable-user-list=true\n" ] if not os.path.exists(gdmkeyfilebase): self.logger.log( LogPriority.DEBUG, "gdm keyfile base directory does not exist. Creating it..." ) os.makedirs(gdmkeyfilebase, 0o755) if not self.setFileContents(gdmkeyfile, gdmkeyfileconflist): success = False self.logger.log( LogPriority.DEBUG, "Updating dconf with new configuration settings...") # update dconf dconfupdate = "/usr/bin/dconf update" if not self.checkCommand(dconfupdate): success = False if success: self.detailedresults += "\nSuccessfully applied new banner configuration settings. On some systems, a reboot may be required to view updated settings" except Exception: raise return success def fixlightdm(self): '''run fix functionality for lightdm-based systems :returns: success :rtype: boolean @author: Breen Malmberg ''' success = True contentlines = [] try: if not os.path.exists('/etc/lightdm/lightdm.conf.d'): os.makedirs('/etc/lightdm/lightdm.conf.d/', 0o755) for f in self.lightdmdict: contentlines = [] if isinstance(self.lightdmdict[f], list): for opt in self.lightdmdict[f]: contentlines.append(opt + '\n') if not self.setFileContents(f, contentlines): success = False else: if not self.setFileContents(f, self.lightdmdict[f]): success = False except Exception: raise return success def fixkde(self): '''run fix functionality for kde-based systems :returns: success :rtype: boolean @author: Breen Malmberg @change: Breen Malmberg - 5/25/2016 - moved the commit calls to ensure they are only called if the fix calls completed successfully ''' success = True try: if not self.kdefile: self.logger.log( LogPriority.DEBUG, "Unable to identify kde configuration file. " + "Can not continue with fix") self.detailedresults += "Unable to identify kde " + \ "configuration file. Can not continue with fix." success = False return success fileCreated = False if not os.path.exists(self.kdefile): if createFile(self.kdefile, self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "creation", "filepath": self.kdefile} self.statechglogger.recordchgevent(myid, event) fileCreated = True debug = "kdmrc file successfully created at " + \ self.kdefile self.logger.log(LogPriority.DEBUG, debug) setPerms(self.kdefile, [0, 0, 0o644], self.logger) self.reportkde() else: self.detailedresults += "\nCould not create kdmrc file " + \ "at " + self.kdefile return False if not self.kdeditor.fix(): success = False self.detailedresults += "KVEditor fix failed. Fix not applied" else: if not fileCreated: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.kdeditor.setEventID(myid) if not self.kdeditor.commit(): success = False self.detailedresults += 'kdeditor commit failed. ' + \ 'Fix not applied' key1 = 'GreetString' val1 = '"' + self.kdebannertext + '"' data = {"X-*-Greeter": {key1: val1}} tmpfile = self.kdefile + ".stonixtmp2" greeteditor = KVEditorStonix(self.statechglogger, self.logger, "tagconf", self.kdefile, tmpfile, data, "present", "closedeq") if not greeteditor.report(): if greeteditor.fix(): if not greeteditor.commit(): if not fileCreated: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.kdeditor.setEventID(myid) success = False self.detailedresults += "\nGreetString commit failed" else: success = False self.detailedresults += "\nGreetString fix failed" except Exception: raise return success def fixmac(self): '''run fix functionality for macintosh, or darwin-based systems :returns: success :rtype: boolean @author: Breen Malmberg ''' success = True try: if not RuleKVEditor.fix(self, True): success = False if not self.setFileContents(self.ftpwelcomefile, self.bannertext): success = False self.detailedresults += '\nUnable to set warning banner text in ' + str( self.ftpwelcomefile) if not self.setFileContents(self.policybanner, self.bannertext): success = False self.detailedresults += '\nUnable to set warning banner text in ' + str( self.policybanner) except Exception: raise return success
class ConfigureKerberos(Rule): '''@author: Ekkehard J. Koch''' def __init__(self, config, environ, logdispatcher, statechglogger): """ @param config: @param environ: @param logdispatcher: @param statechglogger: """ Rule.__init__(self, config, environ, logdispatcher, statechglogger) self.rulenumber = 255 self.rulename = 'ConfigureKerberos' self.formatDetailedResults("initialize") self.mandatory = True self.sethelptext() self.rootrequired = True self.guidance = [] self.applicable = {'type': 'white', 'family': 'linux', 'os': {'Mac OS X': ['10.15', 'r', '10.15.10']}} # This if/else statement fixes a bug in Configure Kerberos that # occurs on Debian systems due to the fact that Debian has no wheel # group by default. if self.environ.getosfamily() == 'darwin': self.files = {"krb5.conf": {"path": "/etc/krb5.conf", "remove": False, "content": MACKRB5, "permissions": 0o644, "owner": os.getuid(), "group": "wheel", "eventid": str(self.rulenumber).zfill(4) + "krb5"}, "edu.mit.Kerberos": {"path": "/Library/Preferences/edu.mit.Kerberos", "remove": True, "content": None, "permissions": None, "owner": None, "group": None, "eventid": str(self.rulenumber).zfill(4) + "Kerberos"}, "edu.mit.Kerberos.krb5kdc.launchd": {"path": "/Library/Preferences/edu.mit.Kerberos.krb5kdc.launchd", "remove": True, "content": None, "permissions": None, "owner": None, "group": None, "eventid": str(self.rulenumber).zfill(4) + "krb5kdc"}, "kerb5.conf": {"path": "/etc/kerb5.conf", "remove": True, "content": None, "permissions": None, "owner": None, "group": None, "eventid": str(self.rulenumber).zfill(4) + "kerb5"}, "edu.mit.Kerberos.kadmind.launchd": {"path": "/Library/Preferences/edu.mit.Kerberos.kadmind.launchd", "remove": True, "content": None, "permissions": None, "owner": None, "group": None, "eventid": str(self.rulenumber).zfill(4) + "kadmind"}, } else: self.files = {"krb5.conf": {"path": "/etc/krb5.conf", "remove": False, "content": LINUXKRB5, "permissions": 0o644, "owner": "root", "group": "root", "eventid": str(self.rulenumber).zfill(4) + "krb5"}} self.ch = CommandHelper(self.logdispatch) self.fh = FileHelper(self.logdispatch, self.statechglogger) if self.environ.getosfamily() == 'linux': self.ph = Pkghelper(self.logdispatch, self.environ) self.filepathToConfigure = [] for filelabel, fileinfo in sorted(self.files.items()): if fileinfo["remove"]: msg = "Remove if present " + str(fileinfo["path"]) else: msg = "Add or update if needed " + str(fileinfo["path"]) self.filepathToConfigure.append(msg) self.fh.addFile(filelabel, fileinfo["path"], fileinfo["remove"], fileinfo["content"], fileinfo["permissions"], fileinfo["owner"], fileinfo["group"], fileinfo["eventid"] ) # Configuration item instantiation datatype = "bool" key = "CONFIGUREFILES" instructions = "When Enabled will fix these files: " + \ str(self.filepathToConfigure) default = True self.ci = self.initCi(datatype, key, instructions, default) def report(self): '''run report actions for configure kerberos determine compliance status of the current system return True if compliant, False if non-compliant :returns: self.compliant :rtype: bool @author: ??? @change: Breen Malmberg - 2/23/2017 - added doc string; added const checks preamble to report and fix methods ''' self.compliant = True self.detailedresults = "" # UPDATE THIS SECTION IF YOU CHANGE THE CONSTANTS BEING USED IN THE RULE constlist = [MACKRB5, LINUXKRB5] if not self.checkConsts(constlist): self.compliant = False self.detailedresults = "\nPlease ensure that the constants: MACKRB5, LINUXKRB5, in localize.py, are defined and are not None. This rule will not function without them." self.formatDetailedResults("report", self.compliant, self.detailedresults) return self.compliant try: if self.environ.getosfamily() == 'linux': packagesRpm = ["pam_krb5", "krb5-libs", "krb5-workstation", "sssd-krb5", "sssd-krb5-common"] packagesDeb = ["krb5-config", "krb5-user", "libpam-krb5"] packagesSuse = ["pam_krb5", "sssd-krb5", "sssd-krb5-common", "krb5-client", "krb5"] if self.ph.determineMgr() == "apt-get": self.packages = packagesDeb elif self.ph.determineMgr() == "zypper": self.packages = packagesSuse else: self.packages = packagesRpm for package in self.packages: if not self.ph.check(package) and self.ph.checkAvailable(package): self.compliant = False self.detailedresults += package + " is not installed\n" if not self.fh.evaluateFiles(): self.compliant = False self.detailedresults += self.fh.getFileMessage() except (KeyboardInterrupt, SystemExit): raise except Exception: self.compliant = False self.detailedresults += str(traceback.format_exc()) self.logdispatch.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.compliant def fix(self): '''run fix actions :returns: self.rulesuccess :rtype: bool @author: ??? @change: Breen Malmberg - 2/23/2017 - added doc string; added checkconsts preamble to ensure the rule does not attempt to run without requied information (from localize.py) ''' self.rulesuccess = True self.detailedresults = "" self.iditerator = 0 # UPDATE THIS SECTION IF YOU CHANGE THE CONSTANTS BEING USED IN THE RULE constlist = [MACKRB5, LINUXKRB5] if not self.checkConsts(constlist): fixsuccess = False self.formatDetailedResults("fix", fixsuccess, self.detailedresults) return fixsuccess try: eventlist = self.statechglogger.findrulechanges(self.rulenumber) for event in eventlist: self.statechglogger.deleteentry(event) if self.ci.getcurrvalue(): pkgsToInstall = [] if self.environ.getosfamily() == 'linux': for package in self.packages: if not self.ph.check(package): if self.ph.checkAvailable(package): pkgsToInstall.append(package) for package in pkgsToInstall: if self.ph.install(package): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "pkghelper", "pkgname": package, "startstate": "removed", "endstate": "installed"} self.statechglogger.recordchgevent(myid, event) else: self.rulesuccess = False self.detailedresults += "Installation of " + package + " did not succeed.\n" if not self.fh.fixFiles(): self.rulesuccess = False self.detailedresults += self.fh.getFileMessage() else: self.rulesuccess = False self.detailedresults = str(self.ci.getkey()) + " was disabled. No action was taken!" except (KeyboardInterrupt, SystemExit): raise except Exception: self.rulesuccess = False self.detailedresults += str(traceback.format_exc()) self.logdispatch.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.rulesuccess
class ConfigureAIDE(Rule): """Install and configure Advanced Intrusion Detection Environment (AIDE). This rule is optional and will install and configure AIDE when it is run.""" def __init__(self, config, environ, logger, statechglogger): """ private method to initialize the module :param config: configuration object instance :param environ: environment object instance :param logger: logdispatcher object instance :param statechglogger: statechglogger object instance """ Rule.__init__(self, config, environ, logger, statechglogger) self.logger = logger self.rulenumber = 110 self.rulename = 'ConfigureAIDE' self.formatDetailedResults("initialize") self.sethelptext() self.guidance = ['NSA(2.1.3)', 'cce-4209-3'] # init CIs datatype = 'bool' key = 'CONFIGUREAIDE' instructions = 'If you set the ConfigureAIDE variable to yes, or ' + \ 'true, ConfigureAIDE will install and set up the Advanced ' + \ 'Intrusion Detection Environment on this system.' default = False self.applicable = {'type': 'white', 'family': ['linux']} self.ci = self.initCi(datatype, key, instructions, default) datatype2 = 'string' key2 = 'AIDEJOBTIME' instructions2 = """This string contains the time when the cron job for /usr/sbin/aide --check will run in /etc/crontab. The default value is 00 18 * * 07 (which means weekly, on sundays at 6pm)""" default2 = "00 18 * * 07" self.aidetime = self.initCi(datatype2, key2, instructions2, default2) pattern = "^([0-9]{1,2})\s+([0-9]{1,2})\s+(\*|(3[0-1]|[0-2]?[0-9]))\s+(\*|(0[0-9]|[0-1]?[0-2]))\s+(\*|([0]?[0-7]))$" self.aidetime.setregexpattern(pattern) def report(self): """Check if AIDE is installed and properly configured. If the config is correct then the self.compliant, self.detailed results and self.currstate properties are updated to reflect the system status. self.rulesuccess will be updated if the rule does not succeed. :return: self.compliant :rtype: bool """ try: self.compliant = True self.detailedresults = "" self.init_objs() try: self.set_aide_paths() self.set_aide_cron_job() except: pass if not self.report_aide_installed(): self.compliant = False else: if not self.report_aide_conf(): self.compliant = False if not self.report_aide_cronjob(): self.compliant = False except (KeyboardInterrupt, SystemExit): raise except Exception: self.detailedresults += "\n" + traceback.format_exc() self.logger.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.compliant def init_objs(self): """ """ self.ph = Pkghelper(self.logger, self.environ) self.ch = CommandHelper(self.logger) def set_aide_paths(self): """ """ self.aide_conf_file = "" aide_conf_paths = ["/etc/aide/aide.conf", "/etc/aide.conf"] for p in aide_conf_paths: if os.path.isfile(p): self.aide_conf_file = p break self.aide_package = "aide" self.aide_cron_file = "/etc/cron.d/stonix_aide_check" self.aide_bin_path = "" aide_bin_paths = ["/usr/sbin/aide", "/usr/bin/aide"] for p in aide_bin_paths: if os.path.isfile(p): self.aide_bin_path = p break self.crontab_bin = "" crontab_bin_locs = ["/bin/crontab", "/sbin/crontab"] for b in crontab_bin_locs: if os.path.isfile(b): self.crontab_bin = b break def set_aide_cron_job(self): """ """ aidetime = self.aidetime.getcurrvalue() if not aidetime: self.aide_cron_job = "" else: if self.ph.manager == "apt-get": self.aide_cron_job = str( self.aidetime.getcurrvalue() ) + " root nice -n 19 " + self.aide_bin_path + " -c " + self.aide_conf_file + " --check" else: self.aide_cron_job = str(self.aidetime.getcurrvalue( )) + " root nice -n 19 " + self.aide_bin_path + " --check" def report_aide_installed(self): """ :return: compliant :rtype: bool """ compliant = True if not self.ph.check(self.aide_package): compliant = False self.detailedresults += "\naide is not installed" return compliant def report_aide_conf(self): """ :return: compliant :rtype: bool """ compliant = True aide_conf_dict = { "/boot": "R", "/etc": "R", "/lib": "R", "/lib64": "R", "/sbin$": "R" } tmppath = self.aide_conf_file + ".stonixtmp" self.aide_conf_editor = KVEditorStonix(self.statechglogger, self.logger, "conf", self.aide_conf_file, tmppath, aide_conf_dict, "present", "space") if not self.aide_conf_editor.report(): compliant = False self.detailedresults += "\nThe following aide conf file options are incorrect:\n" + "\n".join( self.aide_conf_editor.fixables) return compliant def report_aide_cronjob(self): """ :return: compliant :rtype: bool """ compliant = True if not os.path.isfile(self.aide_cron_file): compliant = False self.detailedresults += "\naide cron job not found" return compliant def fix(self): """Attempt to install and configure AIDE. self.rulesuccess will be updated if the rule does not succeed. :return: self.rulesuccess - True if fix succeeded; False if not :rtype: bool """ self.detailedresults = "" self.rulesuccess = True self.iditerator = 0 try: if not self.ci.getcurrvalue(): self.detailedresults += '\nThis rule is currently not enabled, so nothing was done' self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.rulesuccess eventlist = self.statechglogger.findrulechanges(self.rulenumber) for event in eventlist: self.statechglogger.deleteentry(event) if not self.fix_aide_installed(): self.rulesuccess = False if not self.fix_aide_conf(): self.rulesuccess = False if not self.fix_aide_init(): self.rulesuccess = False if not self.fix_aide_cronjob(): self.rulesuccess = False except (KeyboardInterrupt, SystemExit): raise except Exception: self.rulesuccess = False self.detailedresults += "\n" + traceback.format_exc() self.logger.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.rulesuccess def fix_aide_conf(self): """ :return: success :rtype: bool """ success = True self.logger.log(LogPriority.DEBUG, "Fixing aide configuration file") if not self.aide_conf_editor.fix(): success = False elif not self.aide_conf_editor.commit(): success = False if not success: self.logger.log(LogPriority.DEBUG, "Failed to configure aide.conf") return success def fix_aide_cronjob(self): """ :return: success :rtype: bool """ success = True self.logger.log(LogPriority.DEBUG, "Creating aide cron job") try: f = open(self.aide_cron_file, "w") f.write(self.aide_cron_job) f.close() self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "creation", "filepath": self.aide_cron_file} self.statechglogger.recordchgevent(myid, event) except: success = False self.logger.log(LogPriority.DEBUG, "Failed to create aide cron job") return success def fix_aide_installed(self): """ :return: success :rtype: bool """ success = True self.logger.log(LogPriority.DEBUG, "Installing aide package") if not self.ph.install(self.aide_package): success = False self.logger.log(LogPriority.DEBUG, "Failed to install aide package") if not self.ph.checkAvailable(self.aide_package): self.logger.log(LogPriority.DEBUG, "aide package not available on this system") else: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = { "eventtype": "pkghelper", "pkgname": self.aide_package, "startstate": "removed", "endstate": "installed" } self.statechglogger.recordchgevent(myid, event) self.set_aide_paths() self.set_aide_cron_job() self.report_aide_conf() self.report_aide_cronjob() return success def fix_aide_init(self): """ :return: success :rtype: bool """ success = True self.logger.log(LogPriority.DEBUG, "Initializing aide database") if self.ph.manager == "apt-get": aide_init_cmd = "aideinit" else: aide_init_cmd = self.aide_bin_path + " --init" self.ch.executeCommand(aide_init_cmd) retcode = self.ch.getReturnCode() if retcode != 0: success = False errmsg = self.ch.getErrorString() self.logger.log(LogPriority.DEBUG, errmsg) else: # to start using the aide database created by aide init, # remove the 'new' part of the aide database string # this must be done before check can be run # (https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/security_guide/sec-using-aide) if os.path.isfile('/var/lib/aide/aide.db.new.gz'): newaidedb = '/var/lib/aide/aide.db.new.gz' aidedb = '/var/lib/aide/aide.db.gz' elif os.path.isfile('/var/lib/aide/aide.db.new'): newaidedb = '/var/lib/aide/aide.db.new' aidedb = '/var/lib/aide/aide.db' else: newaidedb = "" aidedb = "" if bool(newaidedb and aidedb): os.rename(newaidedb, aidedb) os.chmod(aidedb, 0o600) os.chown(aidedb, 0, 0) else: self.logger.log(LogPriority.DEBUG, "Failed to locate aide database") success = False return success
class ConfigureNetworkTime(Rule): """The ConfigureNetworkTime class specifies network time servers and enables the appropriate service""" def __init__(self, config, environ, logger, statechglogger): """ Constructor """ Rule.__init__(self, config, environ, logger, statechglogger) self.rulenumber = 96 self.rulename = 'ConfigureNetworkTime' self.formatDetailedResults("initialize") self.logger = logger self.sethelptext() self.mandatory = True self.rootrequired = True self.guidance = [ 'CIS', 'NSA(3.10.2)', 'CCE-4134-3', 'CCE-4385-1', 'CCE-4032-9', 'CCE-4424-8', 'CCE-3487-6' ] # init CI self.ci = self.initCi( "bool", "CONFIGURENETWORKTIME", "To prevent STONIX from configuring network time servers for this system, set the value of CONFIGURENETWORKTIME to False.", True) self.applicable = { 'type': 'white', 'family': ['linux', 'solaris', 'freebsd'], 'os': { 'Mac OS X': ['10.15', 'r', '10.15.10'] } } self.libc = getLibc() def _set_paths(self): """ """ self.time_package = "chrony" self.time_conf_file = "" time_conf_files = ["/etc/chrony.conf", "/etc/chrony/chrony.conf"] for cf in time_conf_files: if os.path.isfile(cf): self.time_conf_file = cf break def _test_connection(self, hostname): """ :param str hostname: host to test connection to :return: reachable :rtype: bool """ reachable = False if isinstance(hostname, list): for h in hostname: response = os.system("ping -c 1 " + h) if response == 0: reachable = True break else: response = os.system("ping -c 1 " + hostname) if response == 0: reachable = True return reachable def _report_install(self): """ :return: installed :rtype: bool """ installed = True if not self.ph.check('chrony'): installed = False return installed def _report_conf(self): """ :return: configured :rtype: bool """ configured = True self.time_conf_dict = { "driftfile": "/var/lib/chrony/drift", "makestep": "1.0 3", "rtcsync": "", "logdir": "/var/log/chrony", "cmddeny": "all", "server": [] } for ts in self.time_servers: self.time_conf_dict["server"].append(ts) tmpfile = self.time_conf_file + ".stonixtmp" self.time_conf_editor = KVEditorStonix(self.statechglogger, self.logger, "conf", self.time_conf_file, tmpfile, self.time_conf_dict, "present", "space") if not self.time_conf_editor.report(): configured = False self.detailedresults += "\nThe following configuration options are incorrect in " + str( self.time_conf_file) + ":\n" + "\n".join( self.time_conf_editor.fixables) return configured def _report_linux(self): """ :return: compliant :rtype: bool """ self._set_paths() compliant = True if not self._test_connection(self.time_servers): compliant = False self.detailedresults += "\nCould not reach network time servers" if not self._report_install(): compliant = False self.detailedresults += "\nCould not install network time package" elif not self._report_conf(): compliant = False self.detailedresults += "\nFailed to properly configure network time configuration file" return compliant def report(self): """determine whether the fix() method of this rule has run successfully yet or not :return: self.compliant :rtype: bool """ self.detailedresults = "" # UPDATE THIS SECTION IF THE CONSTANTS BEING USED IN THIS CLASS CHANGE self.constlist = [NTPSERVERSEXTERNAL, NTPSERVERSINTERNAL] if not any(self.constlist): self.compliant = False self.detailedresults += "\nThis rule requires that at least one of the following constants, in localize.py, be defined and not None: NTPSERVERSEXTERNAL, NTPSERVERSINTERNAL" self.formatDetailedResults("report", self.compliant, self.detailedresults) return self.compliant self.ph = Pkghelper(self.logger, self.environ) self.ch = CommandHelper(self.logger) if self._test_connection(NTPSERVERSINTERNAL): self.time_servers = NTPSERVERSINTERNAL else: self.time_servers = NTPSERVERSEXTERNAL self.compliant = True try: if self.environ.getosfamily() == "darwin": self.ss = "/usr/sbin/systemsetup" if not self._report_darwin(): self.compliant = False else: if not self._report_linux(): self.compliant = False except (KeyboardInterrupt, SystemExit): raise except: self.compliant = False self.detailedresults += "\n" + traceback.format_exc() self.logdispatch.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.compliant def _report_darwin(self): """determine rule compliance status for darwin based systems :return: configured :rtype: """ # defaults configured = True usingnetworktime = False timeserverfound = False try: cmd = [self.ss, "-getnetworktimeserver"] self.ch.executeCommand(cmd) self.output = self.ch.getOutput() for line in self.output: for item in self.time_servers: if re.search(item, line): timeserverfound = True cmd2 = [self.ss, "-getusingnetworktime"] self.ch.executeCommand(cmd2) self.output2 = self.ch.getOutput() for line in self.output2: if re.search('On', line): usingnetworktime = True if not usingnetworktime: self.detailedresults += '\nusingnetworktime not set to on' configured = False if not timeserverfound: self.detailedresults += '\ncorrect time server not configured' configured = False except Exception: raise return configured def fix(self): """Decide which fix sub method to run, and run it to configure network time :return: self.rulesuccess :rtype: bool """ # UPDATE THIS SECTION IF THE CONSTANTS BEING USED IN THIS CLASS CHANGE if not self.checkConsts(self.constlist): self.rulesuccess = False self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) return self.rulesuccess self.detailedresults = "" self.iditerator = 0 self.rulesuccess = True try: if self.ci.getcurrvalue(): if self.environ.getosfamily() == "darwin": if not self._fix_darwin(): self.rulesuccess = False else: if not self._fix_linux(): self.rulesuccess = False else: self.detailedresults += "\nRule was not enabled. No action was taken." except (KeyboardInterrupt, SystemExit): raise except Exception: self.rulesuccess = False self.detailedresults += "\n" + traceback.format_exc() self.logdispatch.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.rulesuccess def _fix_linux(self): """ :return: success :rtype: bool """ success = True if not self._fix_install(): success = False else: self._set_paths() self._report_conf() if not self._fix_conf(): success = False return success def _fix_install(self): """ :return: success :rtype: bool """ success = True if not self.ph.install(self.time_package): success = False self.logger.log(LogPriority.DEBUG, "Failed to install network time package") return success def _fix_conf(self): """ :return: success :rtype: bool """ success = True if not self.time_conf_editor.fix(): success = False self.logger.log(LogPriority.DEBUG, "KVEditor failed to fix") elif not self.time_conf_editor.commit(): success = False self.logger.log(LogPriority.DEBUG, "KVEditor failed to commit") return success def _fix_darwin(self): """ private method to perform fix operations for mac os x machines :return: fixresult :rtype: bool """ parseoutput1 = [] parseoutput2 = [] fixresult = True try: # set network time on cmd1 = [self.ss, "-setusingnetworktime", "on"] try: self.ch.executeCommand(cmd1) self.libc.sync() except Exception as errmsg: self.logger.log(LogPriority.DEBUG, str(errmsg)) try: # set undo cmd to restore original network time state for line in self.output2: if re.search('Network Time', line): parseoutput1 = line.split(':') originaltimestate = parseoutput1[1].strip() except KeyError: originaltimestate = "off" undocmd1 = self.ss + " -setusingnetworktime " + originaltimestate event = {"eventtype": "commandstring", "command": undocmd1} self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.statechglogger.recordchgevent(myid, event) # set network time server cmd2 = [ self.ss, "-setnetworktimeserver", str(self.time_servers[0]) ] try: self.ch.executeCommand(cmd2) except Exception as errmsg: self.logger.log(LogPriority.DEBUG, str(errmsg)) try: # set undo cmd to reinstate original time server for line in self.output: if re.search('Network Time Server', line): parseoutput2 = line.split(':') originalnetworktimeserver = parseoutput2[1].strip() except (IndexError, KeyError): originalnetworktimeserver = NTPSERVERSINTERNAL[0] undocmd2 = self.ss + " -setusingnetworktime " + \ originalnetworktimeserver event = {"eventtype": "commandstring", "command": undocmd2} self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.statechglogger.recordchgevent(myid, event) except: raise return fixresult
class ConfigureProcessAccounting(Rule): '''Class docs''' def __init__(self, config, environ, logger, statechglogger): """ @param config: @param environ: @param logger: @param statechglogger: """ Rule.__init__(self, config, environ, logger, statechglogger) self.logger = logger self.rulenumber = 97 self.rulename = "ConfigureProcessAccounting" self.formatDetailedResults("initialize") self.mandatory = True self.sethelptext() self.guidance = ["CCE-RHEL7-CCE-TBD 3.2.15"] self.applicable = {"type": "white", "family": ["linux"]} # Configuration item instantiation datatype = "bool" key = "CONFIGUREPROCESSACCOUNTING" instructions = "To disable this rule, set the value of " + \ "CONFIGUREPROCESSACCOUNTING to False." default = True self.ci = self.initCi(datatype, key, instructions, default) self.ph = Pkghelper(self.logger, self.environ) self.sh = ServiceHelper(self.environ, self.logger) def report(self): ''' :returns: self.compliant :rtype: bool @author: Eric Ball @change: Breen Malmberg - 04/09/2019 - doc string added; method refactor; added debug logging ''' self.compliant = True self.detailedresults = "" self.packages = ["psacct", "acct"] try: if not any(self.ph.check(p) for p in self.packages): self.compliant = False self.detailedresults += "\nsystem accounting package is not installed" if not any(self.sh.auditService(p) for p in self.packages): self.compliant = False self.detailedresults += "\nsystem accounting service is not enabled" except (KeyboardInterrupt, SystemExit): raise except Exception: self.compliant = False self.detailedresults += "\n" + traceback.format_exc() self.logdispatch.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.compliant def fix(self): ''' :returns: self.rulesuccess :rtype: bool @author: Eric Ball @change: Breen Malmberg - 04/09/2019 - doc string added; method refactor; added debug logging ''' self.rulesuccess = True self.detailedresults = "" self.iditerator = 0 try: if not self.ci.getcurrvalue(): self.rulesuccess = False self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.rulesuccess eventlist = self.statechglogger.findrulechanges(self.rulenumber) for event in eventlist: self.statechglogger.deleteentry(event) for p in self.packages: if self.ph.install(p): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = { "eventtype": "pkghelper", "pkgname": p, "startstate": "removed", "endstate": "installed" } self.statechglogger.recordchgevent(myid, event) if self.iditerator == 0: self.rulesuccess = False self.detailedresults += "\nFailed to install system accounting package" for p in self.packages: if self.ph.check(p): self.sh.enableService(p) self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = { "eventtype": "servicehelper", "servicename": p, "startstate": "disabled", "endstate": "enabled" } self.statechglogger.recordchgevent(myid, event) except (KeyboardInterrupt, SystemExit): raise except Exception: self.rulesuccess = False self.detailedresults += "\n" + traceback.format_exc() self.logdispatch.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.rulesuccess
class SSHTimeout(Rule): '''This rule will configure the ssh timeout period for ssh sessions, if ssh is installed. @author: dwalker ''' def __init__(self, config, environ, logger, statechglogger): Rule.__init__(self, config, environ, logger, statechglogger) self.logger = logger self.rootrequired = True self.rulenumber = 127 self.rulename = 'SSHTimeout' self.formatDetailedResults("initialize") self.mandatory = True self.sethelptext() self.boolCi = self.initCi( "bool", "SSHTIMEOUTON", "To disable this rule set the value " + "of SSHTIMEOUTON to False", True) self.intCi = self.initCi( "int", "SSHTIMEOUT", "Set your preferred timeout value here, " + "in seconds. Default is 900 (15 minutes).", 900) self.guidance = ['NSA 3.5.2.3'] self.iditerator = 0 self.editor = "" self.applicable = { 'type': 'white', 'family': ['linux', 'solaris', 'freebsd'], 'os': { 'Mac OS X': ['10.15', 'r', '10.15.10'] } } self.ph = Pkghelper(self.logger, self.environ) def report(self): '''SSHTimeout.report(): produce a report on whether or not a valid time for timing out of ssh is set. @author: D.Walker ''' try: self.detailedresults = "" compliant = True results = "" timeout = self.intCi.getcurrvalue() if self.environ.getostype() == "Mac OS X": self.path = '/private/etc/ssh/sshd_config' self.tpath = '/private/etc/ssh/sshd_config.tmp' else: self.path = '/etc/ssh/sshd_config' self.tpath = '/etc/ssh/sshd_config.tmp' if self.ph.manager == "zypper": openssh = "openssh" else: openssh = "openssh-server" if not self.ph.check(openssh): self.compliant = True self.detailedresults += "Package " + openssh + " is not installed.\nNothing to configure." self.formatDetailedResults("report", self.compliant, self.detailedresults) return self.compliant self.ssh = { "ClientAliveInterval": str(timeout), "ClientAliveCountMax": "0" } if os.path.exists(self.path): compliant = True kvtype = "conf" intent = "present" self.editor = KVEditorStonix(self.statechglogger, self.logger, kvtype, self.path, self.tpath, self.ssh, intent, "space") if not self.editor.report(): compliant = False results += "Settings in " + self.path + " are not " + \ "correct\n" if not checkPerms(self.path, [0, 0, 0o644], self.logger): compliant = False results += self.path + " permissions are incorrect\n" else: compliant = False results += self.path + " does not exist\n" self.detailedresults = results self.compliant = compliant except (KeyboardInterrupt, SystemExit): raise except Exception: self.rulesuccess = False self.detailedresults += "\n" + traceback.format_exc() self.logdispatch.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.compliant def fix(self): '''SSHTimeout.fix(): set the correct values in /etc/ssh/sshd_config so that ssh sessions time out appropriately. @author: D.Walker ''' try: if not self.boolCi.getcurrvalue(): return debug = "inside fix method\n" self.logger.log(LogPriority.DEBUG, debug) created = False self.iditerator = 0 success = True self.detailedresults = "" debug = "" # clear out event history so only the latest fix is recorded self.iditerator = 0 eventlist = self.statechglogger.findrulechanges(self.rulenumber) for event in eventlist: self.statechglogger.deleteentry(event) if self.environ.getostype() != "Mac OS X": if self.ph.manager == "zypper": openssh = "openssh" else: openssh = "openssh-server" if not self.ph.check(openssh): debug = "openssh-server is not installed in fix\n" self.logger.log(LogPriority.DEBUG, debug) if self.ph.checkAvailable(openssh): debug = "openssh-server is not available in fix\n" self.logger.log(LogPriority.DEBUG, debug) if not self.ph.install(openssh): debug = "Unable to install openssh-server\n" self.logger.log(LogPriority.DEBUG, debug) self.rulesuccess = False return else: cmd = self.ph.getRemove() + openssh event = { "eventtype": "commandstring", "command": cmd } self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.statechglogger.recordchgevent(myid, event) self.detailedresults += "Installed openssh-server\n" self.editor = KVEditorStonix( self.statechglogger, self.logger, "conf", self.path, self.tpath, self.ssh, "present", "space") self.editor.report() else: debug += "openssh-server not available to install\n" self.logger.log(LogPriority.DEBUG, debug) self.rulesuccess = False return if not os.path.exists(self.path): createFile(self.path, self.logger) created = True self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "creation", "filepath": self.path} self.statechglogger.recordchgevent(myid, event) self.editor = KVEditorStonix(self.statechglogger, self.logger, "conf", self.path, self.tpath, self.ssh, "present", "space") self.editor.report() if os.path.exists(self.path): print("path exists\n") if not checkPerms(self.path, [0, 0, 0o644], self.logger): if not created: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(self.path, [0, 0, 0o644], self.logger, self.statechglogger, myid): debug += "Unable to set Permissions \ for: " + self.editor.getPath() + "\n" success = False else: if not setPerms(self.path, [0, 0, 0o644], self.logger): success = False if self.editor.fixables: print(("editor has fixables and they are " + str(self.editor.fixables) + "\n")) if not created: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.editor.setEventID(myid) if self.editor.fix(): debug += "kveditor fix ran successfully\n" if self.editor.commit(): debug += "kveditor commit ran successfully\n" os.chown(self.path, 0, 0) os.chmod(self.path, 0o644) if re.search("linux", self.environ.getosfamily()): resetsecon(self.path) else: debug += "Unable to complete kveditor commit\n" success = False else: debug += "Unable to complete kveditor fix\n" success = False self.rulesuccess = success if debug: self.logger.log(LogPriority.DEBUG, debug) except (KeyboardInterrupt, SystemExit): raise except Exception: self.rulesuccess = False self.detailedresults += "\n" + traceback.format_exc() self.logdispatch.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.rulesuccess
class PasswordExpiration(Rule): def __init__(self, config, environ, logger, statechglogger): Rule.__init__(self, config, environ, logger, statechglogger) self.logger = logger self.rulenumber = 42 self.rulename = "PasswordExpiration" self.formatDetailedResults("initialize") self.mandatory = True self.sethelptext() self.iditerator = 0 self.guidance = ["2.3.1.7"] self.applicable = {'type': 'black', 'family': ['darwin']} self.universal = "#The following lines were added by stonix\n" datatype = 'bool' key = 'PASSWORDEXPIRATION' instructions = "To disable this rule set the value of " + \ "PASSWORDEXPIRATION to False." default = True self.ci = self.initCi(datatype, key, instructions, default) self.libusercreate = False self.libuserinstall = False self.useraddcreate = False self.logindefcreate = False self.fixable, self.shadow = True, True self.editor1, self.editor2 = "", "" self.fixusers = [] ############################################################################### def report(self): try: self.detailedresults = "" self.ch = CommandHelper(self.logger) self.lockedpwds = '^\*LK\*|^!|^\*|^x$' if self.environ.getosfamily() == "linux": self.ph = Pkghelper(self.logger, self.environ) self.specs = {"PASS_MAX_DAYS": "180", "PASS_MIN_DAYS": "1", "PASS_MIN_LEN": "8", "PASS_WARN_AGE": "28"} if self.ph.manager in ("apt-get", "zypper"): # apt-get systems do not set min length in the same file # as other systems(login.defs) del self.specs["PASS_MIN_LEN"] self.shadowfile = "/etc/shadow" self.logdeffile = "/etc/login.defs" self.useraddfile = "/etc/default/useradd" self.libuserfile = "/etc/libuser.conf" self.compliant = self.reportLinux() elif self.environ.getosfamily() == "solaris": self.specs = {"PASSLENGTH": "8", "MINWEEKS": "1", "MAXWEEKS": "26", "WARNWEEKS": "4"} self.shadowfile = "/etc/shadow" self.logdeffile = "/etc/default/passwd" self.compliant = self.reportSolaris() elif self.environ.getosfamily() == "freebsd": self.specs = {"warnpassword": "******", "minpasswordlen": "8", "passwordtime": "180d"} self.shadowfile = "/etc/master.passwd" self.loginfile = "/etc/login.conf" self.compliant = self.reportFreebsd() except (KeyboardInterrupt, SystemExit): # User initiated exit raise except Exception: self.rulesuccess = False self.detailedresults += "\n" + traceback.format_exc() self.logdispatch.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.compliant ############################################################################### def reportLinux(self): compliant1 = self.checklogindefs() compliant2 = self.chkShadow() compliant3 = self.chkUserAdd() compliant4 = self.checklibuser() if compliant1 and compliant2 and compliant3 and compliant4: return True else: return False ############################################################################### def reportSolaris(self): compliant1 = self.checklogindefs() compliant2 = self.chkShadow() if compliant1 and compliant2: return True else: return False ############################################################################### def reportFreebsd(self, specs): compliant1 = self.chkPasswd() compliant2 = self.chkLogin(specs) if compliant1 and compliant2: return True else: return False ############################################################################### def checklogindefs(self): '''report method for various distros of linux and solaris''' compliant = True debug = "" if not os.path.exists(self.logdeffile): compliant = False self.detailedresults += self.logdeffile + " file does not exist\n" elif not checkPerms(self.logdeffile, [0, 0, 0o644], self.logger): compliant = False self.detailedresults += self.logdeffile + " does not have " + \ "the correct permissions. Expected 644, found " + \ str(getOctalPerms(self.logdeffile)) + ".\n" tmpfile = self.logdeffile + ".tmp" self.editor1 = KVEditorStonix(self.statechglogger, self.logger, "conf", self.logdeffile, tmpfile, self.specs, "present", "space") if not self.editor1.report(): self.detailedresults += self.logdeffile + " does not " + \ "contain the correct contents\n" debug = self.logdeffile + " doesn't contain the correct " + \ "contents\n" self.logger.log(LogPriority.DEBUG, debug) compliant = False return compliant ############################################################################### def chkShadow(self): debug = "" compliant = True if os.path.exists(self.shadowfile): if self.ph.manager == "apt-get": statdata = os.stat(self.shadowfile) mode = stat.S_IMODE(statdata.st_mode) retval = getUserGroupName(self.shadowfile) if retval[0] != "root" or retval[1] != "shadow": compliant = False self.detailedresults += self.shadowfile + " ownership " + \ "is not correct (either owner is not root, or " + \ "group is not shadow).\n" if mode != 0o640: compliant = False self.detailedresults += self.shadowfile + " does not have " + \ "the correct permissions. Expected 640, found " + \ str(getOctalPerms(self.shadowfile)) + ".\n" elif not checkPerms(self.shadowfile, [0, 0, 0o400], self.logger) and \ not checkPerms(self.shadowfile, [0, 0, 0], self.logger): compliant = False self.detailedresults += self.shadowfile + " does not have " + \ "the correct permissions. Expected 400 or 0, found " + \ str(getOctalPerms(self.shadowfile)) + ".\n" contents = readFile(self.shadowfile, self.logger) if self.environ.getosfamily() == "solaris" or \ self.environ.getosfamily() == "linux": if self.environ.getosfamily() == "linux": whichid = "/usr/bin/id" elif self.environ.getosfamily() == "solaris": whichid = "/usr/xpg4/bin/id" for line in contents: badacct = False debug = "" if re.search("^\#", line) or re.match("^\s*$", line): continue if re.search(":", line): field = line.split(":") cmd = [whichid, "-u", field[0]] self.ch.executeCommand(cmd) output = self.ch.getOutputString().strip() error = self.ch.getError() if error: continue if output: if output.isdigit(): uid = int(output) else: uid = 100 else: continue try: if uid >= 500 and not re.search(self.lockedpwds, field[1]): for i in [3, 4, 5, 6]: if field[i]: val = field[i] if val.isdigit(): field[i] = int(field[i]) elif i == 6: field[i] = 99 else: field[i] = 0 elif i == 6: field[i] = 99 else: field[i] = 0 if field[3] != 1 or field[3] == "": compliant = False self.detailedresults += "Shadow file: " + \ "Minimum age is not equal to 1\n" badacct = True if field[4] > 180 or field[4] == "": compliant = False self.detailedresults += "Shadow file: " + \ "Expiration is not 180 or less\n" badacct = True if field[5] != 28 or field[5] == "": compliant = False self.detailedresults += "Shadow file: " + \ "Password expiration warnings are " + \ "not set to 28 days\n" badacct = True if field[6] != 35 or field[6] == "": compliant = False self.detailedresults += "Shadow file: " + \ "Account lock is not set to 35 days\n" badacct = True except IndexError: compliant = False debug = traceback.format_exc() debug += ' Index out of range\n' badacct = True if debug: self.logger.log(LogPriority.DEBUG, debug) if badacct: self.fixusers.append(field[0]) if self.environ.getosfamily() == 'freebsd': for line in contents: debug = "" if re.search("^\#", line) or re.match('^\s*$', line): continue if re.search(':', line): field = line.split(':') message = Popen(['/usr/bin/id', '-u', field[0]], stderr=PIPE, stdout=PIPE, shell=False) uid = message.stdout.readline() uid = uid.strip() message.stdout.close() if uid.isdigit(): uid = int(uid) else: uid = 100 try: if uid >= 500 and not re.search(self.lockedpwds, field[1]): for i in [5, 6]: if field[i]: val = field[i] if not val.isdigit(): field[i] = 0 else: field[i] = 0 if int(field[5]) > 180 or field[5] == "": self.shadow = False compliant = False debug += "expiration is not 180 or less" if int(field[6]) != 1 or field[6] == "": self.shadow = False compliant = False debug += "Account lock is not set to 1" except IndexError: self.shadow = False compliant = False debug = traceback.format_exc() debug += ' Index out of range' self.logger.log(LogPriority.DEBUG, debug) if debug: self.logger.log(LogPriority.DEBUG, debug) else: self.detailedresults += self.shadowfile + " does not exist\n" compliant = False debug = "chkShadow method is returning " + str(compliant) + \ " compliance\n" self.logger.log(LogPriority.DEBUG, debug) return compliant ############################################################################### def chkUserAdd(self): compliant = True debug = "" if not os.path.exists(self.useraddfile): self.detailedresults += self.useraddfile + " file does not exist\n" compliant = False else: if not checkPerms(self.useraddfile, [0, 0, 0o600], self.logger): compliant = False self.detailedresults += self.useraddfile + " does not have " + \ "the correct permissions. Expected 600, found " + \ str(getOctalPerms(self.useraddfile)) + ".\n" contents = readFile(self.useraddfile, self.logger) found = False valcorrect = True for line in contents: if re.search("^\#", line) or re.match('^\s*$', line): continue if re.search('^INACTIVE', line.strip()) and re.search('=', line): found = True temp = line.split('=') if int(temp[1].strip()) <= -1 or int(temp[1].strip()) > 35: valcorrect = False break if not found: compliant = False self.detailedresults += "INACTIVE key was not found in " + \ self.useraddfile + "\n" if found and not valcorrect: compliant = False self.detailedresults += "INACTIVE key was found in " + \ self.useraddfile + ", but value is incorrect\n" debug += "chkUserAdd method is returning " + str(compliant) + \ " compliance\n" if debug: self.logger.log(LogPriority.DEBUG, debug) return compliant ############################################################################### def checklibuser(self): '''Private method to check the password hash algorithm settings in libuser.conf. @author: dwalker :returns: bool ''' compliant = True '''check if libuser is intalled''' if not self.ph.check("libuser"): '''if not, check if available''' if self.ph.checkAvailable("libuser"): self.detailedresults += "libuser available but not installed\n" return False else: '''not available, not a problem''' return True '''create a kveditor for file if it exists, if not, we do it in the setlibuser method inside the fix''' if os.path.exists(self.libuserfile): data = {"userdefaults": {"LU_SHADOWMAX": "", "LU_SHADOWMIN": "", "LU_SHADOWWARNING": "", "LU_UIDNUMBER": "", "LU_SHADOWINACTIVE": "", "LU_SHADOWEXPIRE": ""}} datatype = "tagconf" intent = "notpresent" tmppath = self.libuserfile + ".tmp" self.editor2 = KVEditorStonix(self.statechglogger, self.logger, datatype, self.libuserfile, tmppath, data, intent, "openeq") if not self.editor2.report(): debug = "/etc/libuser.conf doesn't contain the correct " + \ "contents\n" self.detailedresults += "/etc/libuser.conf doesn't " + \ "contain the correct contents\n" self.logger.log(LogPriority.DEBUG, debug) compliant = False if not checkPerms(self.libuserfile, [0, 0, 0o644], self.logger): self.detailedresults += "Permissions are incorrect on " + \ self.libuserfile + "\n" compliant = False else: self.detailedresults += "Libuser installed but libuser " + \ "file doesn't exist\n" compliant = False return compliant ############################################################################### def chkLogin(self): compliant = True if os.path.exists(self.loginfile): if not checkPerms(self.loginfile, [0, 0, 0o644], self.logger): compliant = False self.detailedresults += self.libuserfile + " does not have " + \ "the correct permissions. Expected 644, found " + \ str(getOctalPerms(self.libuserfile)) + ".\n" contents = readFile(self.loginfile, self.logger) iterator1 = 0 for line in contents: if re.search("^#", line) or re.match('^\s*$', line): iterator1 += 1 elif re.search('^default:\\\\$', line.strip()): found = True temp = contents[iterator1 + 1:] length2 = len(temp) - 1 iterator2 = 0 for line2 in temp: if re.search('^[^:][^:]*:\\\\$', line2): contents2 = temp[:iterator2] break elif iterator2 < length2: iterator2 += 1 elif iterator2 == length2: contents2 = temp[:iterator2] break else: iterator1 += 1 if contents2: for key in self.Fspecs: found = False for line in contents2: if re.search("^#", line) or re.match('^\s*$', line): continue elif re.search('^:' + key, line.strip()): if re.search('=', line): temp = line.split('=') if re.search(str(self.Fspecs[key]) + '(:\\\\|:|\\\\|\s)', temp[1]): found = True continue else: found = False break if not found: compliant = False return compliant else: self.detailedresults += self.loginfile + "does not exist. " + \ "Please note that the fix for this rule will not attempt " + \ "to create this file.\n" compliant = False debug = "chkLogin method is returning " + (compliant) + " compliance\n" self.logger.log(LogPriority.DEBUG, debug) return compliant def fix(self): try: if not self.ci.getcurrvalue(): return self.detailedresults = "" # clear out event history so only the latest fix is recorded self.iditerator = 0 eventlist = self.statechglogger.findrulechanges(self.rulenumber) for event in eventlist: self.statechglogger.deleteentry(event) if self.environ.getosfamily() == "linux": self.rulesuccess = self.fixLinux() if self.environ.getosfamily() == "solaris": self.rulesuccess = self.fixSolaris() if self.environ.getosfamily() == "freebsd": self.rulesuccess = self.fixFreebsd() except (KeyboardInterrupt, SystemExit): # User initiated exit raise except Exception: self.rulesuccess = False self.detailedresults += "\n" + traceback.format_exc() self.logdispatch.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.rulesuccess ############################################################################### def fixLinux(self): success1 = self.fixLogDef(self.specs) success2 = self.fixShadow() success3 = self.fixUserAdd() success4 = self.setlibuser() if success1 and success2 and success3 and success4: return True else: return False ############################################################################### def fixSolaris(self): success1 = self.fixLogDef() success2 = self.fixShadow() if success1 and success2: return True else: return False ############################################################################### def fixFreebsd(self): success1 = self.fixPasswd() success2 = self.fixLogin() if success1 and success2: return True else: return False ############################################################################### def fixLogDef(self, specs): success = True debug = "" if not os.path.exists(self.logdeffile): if createFile(self.logdeffile, self.logger): self.logindefcreate = True setPerms(self.logdeffile, [0, 0, 0o644], self.logger) tmpfile = self.logdeffile + ".tmp" self.editor1 = KVEditorStonix(self.statechglogger, self.logger, "conf", self.logdeffile, tmpfile, specs, "present", "space") else: self.detailedresults += "Was not able to create " + \ self.logdeffile + " file\n" success = False if self.logindefcreate: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "creation", "filepath": self.logdeffile} self.statechglogger.recordchgevent(myid, event) elif not checkPerms(self.logdeffile, [0, 0, 0o644], self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(self.logdeffile, [0, 0, 0o644], self.logger, self.statechglogger, myid): debug += "permissions not correct on: " + \ self.logdeffile + "\n" success = False if self.editor1.fixables or self.editor1.removeables: if not self.logindefcreate: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.editor1.setEventID(myid) if not self.editor1.fix(): debug += "fixLogDef editor.fix did not complete successfully\n" success = False elif not self.editor1.commit(): debug += "fixLogDef editor.commit did not complete successfully\n" success = False os.chown(self.logdeffile, 0, 0) os.chmod(self.logdeffile, 0o644) resetsecon(self.logdeffile) if debug: self.logger.log(LogPriority.DEBUG, debug) return success ############################################################################### def fixShadow(self): success = True if not os.path.exists(self.shadowfile): self.detailedresults += self.shadowfile + "does not exist. \ Will not perform fix on shadow file\n" return False if self.fixusers: contents = readFile(self.shadowfile, self.logger) if self.ph.manager == "apt-get": perms = [0, 42, 0o640] else: perms = [0, 0, 0o400] if not checkPerms(self.shadowfile, perms, self.logger) and \ not checkPerms(self.shadowfile, [0, 0, 0], self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) setPerms(self.shadowfile, perms, self.logger, self.statechglogger, myid) tmpdate = strftime("%Y%m%d") tmpdate = list(tmpdate) date = tmpdate[0] + tmpdate[1] + tmpdate[2] + tmpdate[3] + "-" + \ tmpdate[4] + tmpdate[5] + "-" + tmpdate[6] + tmpdate[7] for user in self.fixusers: cmd = ["chage", "-d", date, "-m", "1", "-M", "180", "-W", "28", "-I", "35", user] self.ch.executeCommand(cmd) # We have to do some gymnastics here, because chage writes directly # to /etc/shadow, but statechglogger expects the new contents to # be in a temp file. newContents = readFile(self.shadowfile, self.logger) shadowTmp = "/tmp/shadow.stonixtmp" createFile(shadowTmp, self.logger) writeFile(shadowTmp, "".join(newContents) + "\n", self.logger) writeFile(self.shadowfile, "".join(contents) + "\n", self.logger) self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {'eventtype': 'conf', 'filepath': self.shadowfile} self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange(self.shadowfile, shadowTmp, myid) shutil.move(shadowTmp, self.shadowfile) os.chmod(self.shadowfile, perms[2]) os.chown(self.shadowfile, perms[0], perms[1]) resetsecon(self.shadowfile) return success ############################################################################### def fixUserAdd(self): success = True if not os.path.exists(self.useraddfile): if createFile(self.useraddfile, self.logger): self.useraddcreate = True setPerms(self.useraddfile, [0, 0, 0o600], self.logger) else: self.detailedresults += self.useraddfile + \ " could not be created\n" success = False if self.useraddcreate: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "creation", "filepath": self.useraddfile} self.statechglogger.recordchgevent(myid, event) if not checkPerms(self.useraddfile, [0, 0, 0o600], self.logger): if not self.useraddcreate: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(self.useraddfile, [0, 0, 0o600], self.logger, self.statechglogger, myid): self.detailedresults += "Could not set permissions on " + \ self.useraddfile success = False tempstring = "" contents = readFile(self.useraddfile, self.logger) found = False for line in contents: if re.search("^\#", line) or re.match('^\s*$', line): tempstring += line continue if re.search("^INACTIVE", line.strip()): if re.search("=", line): temp = line.split("=") if int(temp[1].strip()) <= -1 or \ int(temp[1].strip()) > 35: continue else: found = True tempstring += line else: continue elif re.search("^" + self.universal, line.strip()): continue else: tempstring += line if not found: tempstring += "INACTIVE=35\n" tmpfile = self.useraddfile + ".tmp" if not writeFile(tmpfile, tempstring, self.logger): return False if not self.useraddcreate: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {'eventtype': 'conf', 'filepath': self.useraddfile} self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange(self.useraddfile, tmpfile, myid) shutil.move(tmpfile, self.useraddfile) os.chown(self.useraddfile, 0, 0) os.chmod(self.useraddfile, 0o600) resetsecon(self.useraddfile) return success ############################################################################### def setlibuser(self): success = True debug = "" created = False data = {"userdefaults": {"LU_SHADOWMAX": "", "LU_SHADOWMIN": "", "LU_SHADOWWARNING": "", "LU_UIDNUMBER": "", "LU_SHADOWINACTIVE": "", "LU_SHADOWEXPIRE": ""}} '''check if installed''' if not self.ph.check("libuser"): '''if not installed, check if available''' if self.ph.checkAvailable("libuser"): '''if available, install it''' if not self.ph.install("libuser"): self.detailedresults += "Unable to install libuser\n" return False else: '''since we're just now installing it we know we now need to create the kveditor''' self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) comm = self.ph.getRemove() event = {"eventtype": "commandstring", "command": comm} self.statechglogger.recordchgevent(myid, event) datatype = "tagconf" intent = "notpresent" tmppath = self.libuserfile + ".tmp" self.editor2 = KVEditorStonix(self.statechglogger, self.logger, datatype, self.libuserfile, tmppath, data, intent, "openeq") self.editor2.report() else: return True if not os.path.exists(self.libuserfile): if not createFile(self.libuserfile, self.logger): self.detailedresults += "Unable to create libuser file\n" debug = "Unable to create the libuser file\n" self.logger.log(LogPriority.DEBUG, debug) return False created = True self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "creation", "filepath": self.libuserfile} self.statechglogger.recordchgevent(myid, event) tmppath = self.libuserfile + ".tmp" self.editor2 = KVEditorStonix(self.statechglogger, self.logger, "tagconf", self.libuserfile, tmppath, data, "notpresent", "openeq") self.editor2.report() if not checkPerms(self.libuserfile, [0, 0, 0o644], self.logger): if not created: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(self.libuserfile, [0, 0, 0o644], self.logger, self.statechglogger, myid): self.detailedresults += "Could not set permissions on " + \ self.libuserfile success = False elif not setPerms(self.libuserfile, [0, 0, 0o644], self.logger): success = False self.detailedresults += "Unable to set the " + \ "permissions on " + self.libuserfile + "\n" if self.editor2.removeables: if not created: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.editor2.setEventID(myid) if self.editor2.fix(): if self.editor2.commit(): debug += "/etc/libuser.conf has been corrected\n" self.logger.log(LogPriority.DEBUG, debug) os.chown(self.libuserfile, 0, 0) os.chmod(self.libuserfile, 0o644) resetsecon(self.libuserfile) else: self.detailedresults += "/etc/libuser.conf " + \ "couldn't be corrected\n" success = False else: self.detailedresults += "/etc/libuser.conf couldn't " + \ "be corrected\n" success = False return success ############################################################################### def fixLogin(self): success = True tempstring = "" debug = "" if not os.path.exists(self.loginfile): self.detailedresults = self.loginfile + "does not exist. \ Will not perform fix on useradd file\n" return False if not checkPerms(self.loginfile, [0, 0, 0o644], self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(self.loginfile, [0, 0, 0o644], self.logger, self.statechglogger, myid): success = False contents = readFile(self.loginfile, self.logger) iterator1 = 0 for line in contents: if re.search("^#", line) or re.match('^\s*$', line): iterator1 += 1 elif re.search('^default:\\\\$', line.strip()): contents1 = contents[:iterator1 + 1] temp = contents[iterator1 + 1:] length2 = len(temp) - 1 iterator2 = 0 for line2 in temp: if re.search('^[^:][^:]*:\\\\$', line2): contents3 = temp[iterator2:] contents2 = temp[:iterator2] break elif iterator2 < length2: iterator2 += 1 elif iterator2 == length2: contents2 = temp[:iterator2] break else: iterator1 += 1 if contents2: for key in self.Fspecs: iterator = 0 found = False for line in contents2: if re.search("^#", line) or re.match('^\s*$', line): iterator += 1 continue elif re.search('^:' + key, line.strip()): if re.search('=', line): temp = line.split('=') if re.search(str(self.Fspecs[key]) + '(:\\\\|:|\\\\|\s)', temp[1]): iterator += 1 found = True else: contents2.pop(iterator) else: iterator += 1 if not found: contents2.append('\t' + key + '=' + str(self.Fspecs[key]) + ':\\\\\n') final = [] for line in contents1: final.append(line) for line in contents2: final.append(line) for line in contents3: final.append(line) for line in final: tempstring += line debug += "tempstring to be written to: " + self.loginfile + "\n" self.logger.log(LogPriority.DEBUG, debug) tmpfile = self.loginfile + ".tmp" if writeFile(tmpfile, tempstring, self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {'eventtype': 'conf', 'filepath': self.loginfile} self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange(self.loginfile, tmpfile, myid) shutil.move(tmpfile, self.loginfile) os.chown(self.loginfile, 0, 0) os.chmod(self.loginfile, 0o644) resetsecon(self.loginfile) else: success = False return success
def undo(self): """ Undo changes made by this rule. Some rules may not be undone. self.rulesuccess will be updated if the rule does not succeed. @author D. Kennel & D. Walker """ # pass if not self.environ.geteuid() == 0: self.detailedresults = "Root access required to revert changes." self.formatDetailedResults("undo", None, self.detailedresults) try: # This must be implemented later once the parameter option or observable # pattern for taking control of self.detailedresults refresh is implemented # see artf30937 : self.detailedresults through application flow for details self.detailedresults = "" undosuccessful = True eventlist = self.statechglogger.findrulechanges(self.rulenumber) if not eventlist: self.formatDetailedResults("undo", None, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return undosuccessful #eventlist.reverse() for entry in eventlist: try: event = self.statechglogger.getchgevent(entry) if event["eventtype"] == "perm": perms = event["startstate"] os.chmod(event["filepath"], perms[2]) os.chown(event["filepath"], perms[0], perms[1]) elif event["eventtype"] == "conf": self.statechglogger.revertfilechanges(event["filepath"], entry) elif event["eventtype"] == "comm" or \ event["eventtype"] == "commandstring": ch = CommandHelper(self.logdispatch) command = event["command"] ch.executeCommand(command) if ch.getReturnCode() != 0: self.detailedresults = "Couldn't run the " + \ "command to undo\n" self.logdispatch.log(LogPriority.DEBUG, self.detailedresults) elif event["eventtype"] == "creation": try: os.remove(event["filepath"]) except OSError as oser: if oser.errno == 21: try: os.rmdir(event["filepath"]) except OSError as oserr: if oserr.errno == 39: self.logdispatch.log(LogPriority.DEBUG, "Cannot remove file path: " + str(event["filepath"]) + " because it is a non-empty directory.") elif oser.errno == 2: self.logdispatch.log(LogPriority.DEBUG, "Cannot remove file path: " + str(event["filepath"]) + " because it does not exist.") elif event["eventtype"] == "deletion": self.statechglogger.revertfiledelete(event["filepath"]) elif event["eventtype"] == "pkghelper": ph = Pkghelper(self.logdispatch, self.environ) if event["startstate"] == "installed": ph.install(event["pkgname"]) elif event["startstate"] == "removed": ph.remove(event["pkgname"]) else: self.detailedresults = 'Invalid startstate for ' \ + 'eventtype "pkghelper". startstate should ' \ + 'either be "installed" or "removed"\n' self.logdispatch.log(LogPriority.ERROR, self.detailedresults) elif event["eventtype"] == "servicehelper": sh = ServiceHelper(self.environ, self.logdispatch) if event["startstate"] == "enabled": sh.enableservice(event["servicename"]) elif event["startstate"] == "disabled": sh.disableservice(event["servicename"]) else: self.detailedresults = 'Invalid startstate for ' \ + 'eventtype "servicehelper". startstate ' + \ 'should either be "enabled" or "disabled"\n' self.logdispatch.log(LogPriority.ERROR, self.detailedresults) except(IndexError, KeyError): self.detailedresults = "EventID " + entry + " not found" self.logdispatch.log(LogPriority.DEBUG, self.detailedresults) except(KeyboardInterrupt, SystemExit): raise except Exception: undosuccessful = False self.detailedresults = traceback.format_exc() self.logdispatch.log(LogPriority.ERROR, [self.rulename + ".undo", self.detailedresults]) self.formatDetailedResults("undo", undosuccessful, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return undosuccessful
class AuditFirefoxUsage(Rule): """ This module will audit firefox browser usage to determine if any site not listed in the APPROVEDDOMAINS CI has been visited while running in root mode """ def __init__(self, config, environ, logger, statechglogger): """ private method to initialize the module :param config: configuration object instance :param environ: environment object instance :param logger: logdispatcher object instance :param statechglogger: statechglogger object instance """ Rule.__init__(self, config, environ, logger, statechglogger) self.logger = logger self.rulenumber = 84 self.rulename = "AuditFirefoxUsage" self.mandatory = True self.rootrequired = True self.sethelptext() self.formatDetailedResults("initialize") self.guidance = [] self.applicable = {'type': 'white', 'family': ['linux']} # Configuration item instantiation datatype = 'list' key = 'APPROVEDDOMAINS' instructions = """This is a list of domains which the root user is \ approved to browse.""" default = LOCALDOMAINS if default == None: default = ["localhost"] elif not default: default = ["localhost"] self.approvedDomainsCi = self.initCi(datatype, key, instructions, default) datatype = 'bool' key = 'DISABLEPROXY' instructions = """To disable Firefox's proxy settings, ensuring that \ browsing is limited to local domains in a proxied environment, set \ DISABLEPROXY to True.""" default = True self.ci = self.initCi(datatype, key, instructions, default) self.ph = Pkghelper(self.logger, self.environ) self.auditonly = True def report(self): """ report the status of the rule's compliance with root-enabled firefox browsing :returns: self.compliant - True if compliant, False if not """ # UPDATE THIS SECTION IF YOU CHANGE THE CONSTANTS BEING USED IN THE RULE constlist = [LOCALDOMAINS] if not self.checkConsts(constlist): self.compliant = False self.detailedresults = "\nPlease ensure that the constant: LOCALDOMAINS, in localize.py, is defined and is not None. This rule will not function without it." self.formatDetailedResults("report", self.compliant, self.detailedresults) return self.compliant try: self.detailedresults = "" compliant = True ffDirs = self.getFirefoxDirs() urls = [] package = "" if self.ph.checkAvailable("sqlite3"): package = "sqlite3" elif self.ph.checkAvailable("sqlite"): package = "sqlite" if not self.ph.check(package): self.ph.install(package) for ffDir in ffDirs: placesPath = ffDir + "/places.sqlite" if not os.path.exists(placesPath): continue ch = CommandHelper(self.logger) command = ["/usr/bin/sqlite3", placesPath, ".tables"] ch.executeCommand(command) if not ch.getReturnCode() == 0: # The version of sqlite3 on RHEL 6 cannot read these dbs. # Instead, we will examine the file manually, looking for # entries beginning with %\x08. sqlBin = open(placesPath, "rb").read() urlList = re.findall("%\x08https?://.*?/", sqlBin) for url in urlList: urls.append(url[2:]) else: conn = sqlite3.connect(placesPath) c = conn.cursor() c.execute("SELECT host FROM moz_hosts") results = c.fetchall() debug = "Results of 'SELECT host FROM moz_hosts': " + \ str(results) self.logger.log(LogPriority.DEBUG, debug) # Results are inside of a tuple for item in results: urls.append(item[0]) badUrls = [] urls = list(set(urls)) if urls: self.logger.log(LogPriority.DEBUG, "URLs found: " + str(urls)) for url in urls: approved = self.approvedDomainsCi.getcurrvalue() foundApproved = False for site in approved: if re.search(site, url): foundApproved = True if not foundApproved: compliant = False badUrls.append(url) if badUrls: self.detailedresults = "URLs found in root's " + \ "Firefox history that are not on the approved " + \ "list: " + ", ".join(badUrls) + "\n" self.logger.log(LogPriority.DEBUG, "Bad URLs found: " + str(badUrls)) self.compliant = compliant except (KeyboardInterrupt, SystemExit): # User initiated exit raise except Exception: self.rulesuccess = False self.detailedresults += "\n" + traceback.format_exc() self.logdispatch.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.compliant def getFirefoxDirs(self): """Discover the randomly-generated Firefox profile directory(ies) for the root user. :returns: ffDirs - List of Firefox profile directories on the system """ # It's possible to have several FF profiles for each user account. This # method will therefore return a list. ffDirs = [] homeDir = "/root" ffParent = homeDir + "/.mozilla/firefox" if os.path.exists(ffParent): profileDirs = glob(ffParent + "/*.default") debug = "Found the following Firefox profile directories: " + \ str(profileDirs) self.logger.log(LogPriority.DEBUG, debug) for pDir in profileDirs: # Since we gave glob the full path, the returned list will # have the full path for each entry ffDirs.append(pDir) return ffDirs
def undo(self): """ Undo changes made by this rule. Some rules may not be undone. self.rulesuccess will be updated if the rule does not succeed. @author D. Kennel & D. Walker """ # pass if not self.environ.geteuid() == 0: self.detailedresults = "Root access required to revert changes." self.formatDetailedResults("undo", None, self.detailedresults) try: # This must be implemented later once the parameter option or observable # pattern for taking control of self.detailedresults refresh is implemented # see artf30937 : self.detailedresults through application flow for details self.detailedresults = "" undosuccessful = True eventlist = self.statechglogger.findrulechanges(self.rulenumber) if not eventlist: self.formatDetailedResults("undo", None, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return undosuccessful #eventlist.reverse() for entry in eventlist: try: event = self.statechglogger.getchgevent(entry) if event["eventtype"] == "perm": perms = event["startstate"] os.chmod(event["filepath"], perms[2]) os.chown(event["filepath"], perms[0], perms[1]) elif event["eventtype"] == "conf": self.statechglogger.revertfilechanges( event["filepath"], entry) elif event["eventtype"] == "comm" or \ event["eventtype"] == "commandstring": ch = CommandHelper(self.logdispatch) command = event["command"] ch.executeCommand(command) if ch.getReturnCode() != 0: self.detailedresults = "Couldn't run the " + \ "command to undo\n" self.logdispatch.log(LogPriority.DEBUG, self.detailedresults) elif event["eventtype"] == "creation": try: os.remove(event["filepath"]) except OSError as oser: if oser.errno == 21: try: os.rmdir(event["filepath"]) except OSError as oserr: if oserr.errno == 39: self.logdispatch.log( LogPriority.DEBUG, "Cannot remove file path: " + str(event["filepath"]) + " because it is a non-empty directory." ) elif oser.errno == 2: self.logdispatch.log( LogPriority.DEBUG, "Cannot remove file path: " + str(event["filepath"]) + " because it does not exist.") elif event["eventtype"] == "deletion": self.statechglogger.revertfiledelete(event["filepath"]) elif event["eventtype"] == "pkghelper": ph = Pkghelper(self.logdispatch, self.environ) if event["startstate"] == "installed": ph.install(event["pkgname"]) elif event["startstate"] == "removed": ph.remove(event["pkgname"]) else: self.detailedresults = 'Invalid startstate for ' \ + 'eventtype "pkghelper". startstate should ' \ + 'either be "installed" or "removed"\n' self.logdispatch.log(LogPriority.ERROR, self.detailedresults) elif event["eventtype"] == "servicehelper": sh = ServiceHelper(self.environ, self.logdispatch) if event["startstate"] == "enabled": sh.enableservice(event["servicename"]) elif event["startstate"] == "disabled": sh.disableservice(event["servicename"]) else: self.detailedresults = 'Invalid startstate for ' \ + 'eventtype "servicehelper". startstate ' + \ 'should either be "enabled" or "disabled"\n' self.logdispatch.log(LogPriority.ERROR, self.detailedresults) except (IndexError, KeyError): self.detailedresults = "EventID " + entry + " not found" self.logdispatch.log(LogPriority.DEBUG, self.detailedresults) except (KeyboardInterrupt, SystemExit): raise except Exception: undosuccessful = False self.detailedresults = traceback.format_exc() self.logdispatch.log( LogPriority.ERROR, [self.rulename + ".undo", self.detailedresults]) self.formatDetailedResults("undo", undosuccessful, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return undosuccessful
class EnablePAEandNX(Rule): '''Install PAE Kernel on Supported 32-bit x86 Systems. If the system is 32-bit and also supports the PAE and NX features, the kernel-PAE package should be installed to enable XD or NX support. ''' def __init__(self, config, environ, logger, statechglogger): ''' Constructor ''' # set up constructor and class variables Rule.__init__(self, config, environ, logger, statechglogger) self.config = config self.logger = logger self.environ = environ self.statechglogger = statechglogger self.rulenumber = 87 self.rulename = "EnablePAEandNX" self.formatDetailedResults("initialize") self.mandatory = True self.rootrequired = True self.guidance = ["CCE-RHEL7-CCE-TBD 2.2.4.4.1"] self.sethelptext() self.applicable = {'type': 'white', 'family': 'linux'} # set up CI datatype = "bool" key = "ENABLEPAEANDNX" instructions = "If you want to prevent this rule from running, set the value of EnablePAEandNX to False." default = True self.ci = self.initCi(datatype, key, instructions, default) self.initobjs() def initobjs(self): '''initialize helper objects @author: Breen Malmberg ''' self.ch = CommandHelper(self.logger) self.pkg = Pkghelper(self.logger, self.environ) def report(self): '''Run report actions for EnablePAEandNX :returns: self.compliant :rtype: bool @author: Breen Malmberg ''' # set default variables for this method systemOS = "" systemARCH = 0 self.detailedresults = "" self.compliant = True self.package = "" try: # get value of other variables to be used in this method systemOS = self.getSystemOS() systemARCH = self.getSystemARCH() self.package = self.getSysPackage(systemOS) # check if required utility exists; log warning if not if not os.path.exists("/proc/cpuinfo"): self.logger.log( LogPriority.WARNING, "Unable to verify presence of required system utility /proc/cpuinfo" ) # check for presence of pae cpu flag as well as pae kernel package if not self.checkPAE(self.package): self.compliant = False # check for presence of nx cpu flag if not self.checkNX(): self.compliant = False # if system architecture is not 32-bit, disregard previous; inform user if systemARCH == 64: self.compliant = True self.detailedresults = "This system is 64-bit and this rule only applies to 32-bit systems." except (KeyboardInterrupt, SystemExit): raise except Exception: self.rulesuccess = False self.detailedresults += traceback.format_exc() self.logger.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("report", self.compliant, self.detailedresults) return self.compliant def getSystemOS(self): '''return the name of the OS :returns: osname :rtype: string @author: Breen Malmberg ''' return self.environ.getostype() def getSystemARCH(self): '''return the architecture of the system (32 or 64 bit) :returns: sysARCH :rtype: int @author: Breen Malmberg ''' # set default variables for this method sysARCH = 0 command = ["uname", "-a"] try: self.ch.executeCommand(command) output = self.ch.getOutput() # check if the system is 64-bit or not for line in output: if re.search("x86_64", line): sysARCH = 64 # if x86_64 flag not found, set system arch as 32-bit if sysARCH == 0: sysARCH = 32 except Exception: raise return sysARCH def getSysPackage(self, systemos): '''return name of pae kernel package for this specific OS :param systemos: string The name of the system's operating system @author: Breen Malmberg :returns: packagename :rtype: string ''' packagename = "" self.logger.log(LogPriority.DEBUG, "Getting system-specific package name...") if not systemos: self.logger.log( LogPriority.DEBUG, "Required parameter: systemos was passed as blank") if not isinstance(systemos, str): self.logger.log( LogPriority.DEBUG, "Required parameter: systemos was passed as incorrect data type. Required data type: string" ) else: self.logger.log(LogPriority.DEBUG, "System info was: " + str(systemos)) # set default variables for this method defaultpackagename = "kernel-PAE" syspkgdict = { "redhat": "kernel-PAE", "red hat": "kernel-PAE", "centos": "kernel-PAE", "cent os": "kernel-PAE", "fedora": "kernel-PAE", "debian": "linux-image-686-pae", "ubuntu": "linux-generic-pae", "opensuse": "kernel-pae", "suse": "kernel-pae" } try: self.logger.log(LogPriority.DEBUG, "Determining system os name...") for opsys in syspkgdict: if re.search(opsys, systemos.lower()): self.logger.log(LogPriority.DEBUG, "System os name is: " + str(opsys)) packagename = syspkgdict[opsys] self.logger.log( LogPriority.DEBUG, "System-specific package name is: " + str(packagename)) except KeyError: # if systemos does not exist in syspkgdict, log debug and use default packagename packagename = defaultpackagename self.logger.log( LogPriority.DEBUG, "Unable to determine system-specific package name. Defaulting to " + str(defaultpackagename)) except Exception: raise if not packagename: packagename = defaultpackagename return packagename def checkPAE(self, package): '''check for the presence of the kernel-PAE package as well as the CPU pae flag :param package: string The name of the kernel PAE package as it appears to this specific system's OS @author: Breen Malmberg :returns: retval :rtype: bool ''' # set default variables for this method retval = True paeflag = False paepkg = False command = "cat /proc/cpuinfo | grep flags" osname = "" osver = "" checkpkg = True try: osname = self.environ.getostype() osver = self.environ.getosver() # do not check for existence of a pae package # on ubuntu 16 32 bit because it is built into # the default kernel and as a result there is no # separate pae kernel package to install if re.search("Ubuntu", osname, re.IGNORECASE): if re.search("16\.", osver, re.IGNORECASE): checkpkg = False paepkg = True if not package: self.detailedresults += "\nNo package was specified. No package check will be performed. Assuming: not installed." self.logger.log(LogPriority.DEBUG, "Required parameter: package was empty") elif not isinstance(package, str): self.logger.log( LogPriority.DEBUG, "Required parameter: package was not passed as the correct data type. Type required: string" ) self.detailedresults += "\nNo package was specified. No package check will be performed. Assuming: not installed." else: if checkpkg: # check for presence of kernel PAE package if self.pkg.check(package): paepkg = True self.ch.executeCommand(command) output = self.ch.getOutput() # check for presence of CPU pae flag for line in output: if re.search("pae", line): paeflag = True # let the user know what is wrong if not paepkg: self.detailedresults += "\nThe kernel pae package is not installed." if not paeflag: self.detailedresults += "\nThe pae CPU flag was not found." retval = bool(paeflag and paepkg) except Exception: raise return retval def checkNX(self): '''check for the presence of the CPU nx flag :returns: retval :rtype: bool @author: Breen Malmberg ''' # set default variables for this method retval = False command = "cat /proc/cpuinfo | grep flags" try: self.ch.executeCommand(command) output = self.ch.getOutput() # check for presence of CPU nx flag for line in output: if re.search("nx", line): retval = True # let the user know what is wrong if not retval: self.detailedresults += "\nThe CPU nx flag was not found." except Exception: raise return retval def fix(self): '''Run fix actions for EnablePAEandNX :returns: success :rtype: bool @author: Breen Malmberg ''' success = True self.detailedresults = "" try: # only run fix actions if the system is 32-bit and the CI is enabled if self.ci.getcurrvalue(): if self.getSystemARCH() == 32: # attempt to install the kernel pae package; inform user if this fails if not self.pkg.install(self.package): success = False self.detailedresults += "\nUnable to install package: " + str( self.package) else: # inform the user if the fix actions do not apply because the system is 64-bit self.detailedresults += "\nThis rule only applies to 32-bit systems. This system is 64-bit." else: # inform the user if the fix actions will not be run because the CI was not enabled self.detailedresults += "\nCI for this rule not enabled. Nothing was done." except (KeyboardInterrupt, SystemExit): raise except Exception: self.rulesuccess = False self.detailedresults += traceback.format_exc() self.logger.log(LogPriority.ERROR, self.detailedresults) self.formatDetailedResults("fix", success, self.detailedresults) return success