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 SecureMDNS(Rule): '''The Avahi daemon implements the DNS Service Discovery and Multicast DNS protocols, which provide service and host discovery on a network. It allows a system to automatically identify resources on the network, such as printers or web servers. This capability is also known as mDNSresponder and is a major part of Zeroconf networking. By default, it is enabled. This rule makes a number of configuration changes to the avahi service in order to secure it. @change: 04/16/2014 ekkehard ci and self.setkvdefaultscurrenthost updates ''' def __init__(self, config, environ, logger, statechglogger): ''' Constructor ''' Rule.__init__(self, config, environ, logger, statechglogger) self.logger = logger self.rulenumber = 135 self.rulename = 'SecureMDNS' self.formatDetailedResults("initialize") self.mandatory = True self.sethelptext() self.rootrequired = True self.compliant = False self.rulesuccess = True self.guidance = [ 'NSA(3.7.2)', 'CCE 4136-8', 'CCE 4409-9', 'CCE 4426-3', 'CCE 4193-9', 'CCE 4444-6', 'CCE 4352-1', 'CCE 4433-9', 'CCE 4451-1', 'CCE 4341-4', 'CCE 4358-8', 'CCE-RHEL7-CCE-TBD 2.5.2' ] self.applicable = { 'type': 'white', 'family': ['linux', 'solaris', 'freebsd'], 'os': { 'Mac OS X': ['10.15', 'r', '10.15.10'] } } # set up command helper object self.ch = CommandHelper(self.logger) # init helper classes self.sh = ServiceHelper(self.environ, self.logger) self.serviceTarget = "" if self.environ.getostype() == "Mac OS X": self.ismac = True self.hasSIP = False self.plb = "/usr/libexec/PlistBuddy" osxversion = str(self.environ.getosver()) versplit = osxversion.split(".") if len(versplit) > 2: minorVersion = int(versplit[1]) releaseVersion = int(versplit[2]) elif len(versplit) == 2: minorVersion = int(versplit[1]) releaseVersion = 0 else: self.logger.log(LogPriority.ERROR, "Unexpected version string length") raise Exception if minorVersion == 10 and releaseVersion < 4: self.service = "/System/Library/LaunchDaemons/com.apple." + \ "discoveryd.plist" self.servicename = "com.apple.networking.discoveryd" self.parameter = "--no-multicast" self.pbr = self.plb + " -c Print " + self.service + \ " | grep 'no-multicast'" self.pbf = self.plb + ' -c "Add :ProgramArguments: string ' + \ self.parameter + '" ' + self.service elif minorVersion > 10: self.hasSIP = True self.service = "/System/Library/LaunchDaemons/" + \ "com.apple.mDNSResponder.plist" self.servicename = "com.apple.mDNSResponder.reloaded" self.parameter = "NoMulticastAdvertisements" self.preferences = "/Library/Preferences/" + \ "com.apple.mDNSResponder.plist" self.pbr = self.plb + " -c Print " + self.preferences + \ " | grep 'NoMulticastAdvertisements'" self.pbf = "defaults write " + self.preferences + " " + \ self.parameter + " -bool YES" else: self.service = "/System/Library/LaunchDaemons/" + \ "com.apple.mDNSResponder.plist" if minorVersion >= 10: self.servicename = "com.apple.mDNSResponder.reloaded" else: self.servicename = "com.apple.mDNSResponder" self.parameter = "-NoMulticastAdvertisements" self.pbr = self.plb + " -c Print " + self.service + \ " | grep 'NoMulticastAdvertisements'" self.pbf = self.plb + ' -c "Add :ProgramArguments: string ' + \ self.parameter + '" ' + self.service else: self.ismac = False # init CIs datatype = 'bool' mdnskey = 'SECUREMDNS' avahikey = 'DISABLEAVAHI' mdnsinstructions = 'To configure the Avahi server daemon ' + \ 'securely set the value of SECUREMDNS to True and the ' + \ 'value of DISABLEAVAHI to False.' avahiinstructions = 'To completely disable the Avahi server ' + \ 'daemon rather than configure it, set the value of ' + \ 'DISABLEAVAHI to True and the value of SECUREMDNS to False.' mdnsdefault = False avahidefault = True self.SecureMDNS = self.initCi(datatype, mdnskey, mdnsinstructions, mdnsdefault) self.DisableAvahi = self.initCi(datatype, avahikey, avahiinstructions, avahidefault) self.configparser = configparser.SafeConfigParser() self.confavahidict = { 'use-ipv6': { 'section': 'server', 'val': 'no' }, 'check-response-ttl': { 'section': 'server', 'val': 'yes' }, 'disallow-other-stacks': { 'section': 'server', 'val': 'yes' }, 'disable-publishing': { 'section': 'publish', 'val': 'yes' }, 'disable-user-service-publishing': { 'section': 'publish', 'val': 'yes' }, 'publish-addresses': { 'section': 'publish', 'val': 'no' }, 'publish-hinfo': { 'section': 'publish', 'val': 'no' }, 'publish-workstation': { 'section': 'publish', 'val': 'no' }, 'publish-domain': { 'section': 'publish', 'val': 'no' } } self.confoptions = { 'server': { 'use-ipv6': 'no', 'check-response-ttl': 'yes', 'disallow-other-stacks': 'yes' }, 'publish': { 'disable-publishing': 'yes', 'disable-user-service-publishing': 'yes', 'publish-addresses': 'no', 'publish-hinfo': 'no', 'publish-workstation': 'no', 'publish-domain': 'no' } } self.iditerator = 0 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: bool @author: Breen Malmberg @change: dwalker - added conditional call to reportmac() @change: Breen Malmberg - 12/05/2017 - removed unnecessary argument "serviceTarget" in linux-only call to servicehelper; removed assignment of unused local variable serviceTarget to self.servicename since servicename is not assigned in the linux code logic path (which was resulting in variable referenced before assignment error) ''' try: # defaults compliant = True self.detailedresults = '' self.rulesuccess = True # if system is a mac, run reportmac if self.ismac: compliant = self.reportmac() # if not mac os x, then run this portion else: self.editor = None # set up package helper object only if not mac os x self.pkghelper = Pkghelper(self.logger, self.environ) # if the disableavahi CI is set, we want to make sure it is # completely disabled if self.DisableAvahi.getcurrvalue(): self.package = "avahi-daemon" # if avahi-daemon is still running, it is not disabled if self.sh.auditService('avahi-daemon'): compliant = False self.detailedresults += 'DisableAvahi has been ' + \ 'set to True, but avahi-daemon service is ' + \ 'currently running.\n' self.numdependencies = 0 if self.pkghelper.determineMgr() == 'yum' or \ self.pkghelper.determineMgr() == 'dnf': self.package = "avahi" # The following KVEditor for /etc/sysconfig/network is # used to meet the zeroconf requirement in # CCE-RHEL7-CCE-TBD 2.5.2 path = "/etc/sysconfig/network" self.path = path if os.path.exists(path): tmppath = path + ".tmp" data = {"NOZEROCONF": "yes"} self.editor = KVEditorStonix( self.statechglogger, self.logger, "conf", path, tmppath, data, "present", "closedeq") if not self.editor.report(): self.compliant = False self.detailedresults += path + " does not " + \ "have the correct settings.\n" else: self.compliant = False self.detailedresults += path + " does not exist.\n" self.numdependencies = \ self.parseNumDependencies(self.package) if self.numdependencies <= 3: if self.pkghelper.check(self.package): compliant = False self.detailedresults += 'DisableAvahi is ' + \ 'set to True, but Avahi is currently ' + \ 'installed.\n' else: self.detailedresults += 'Avahi has too many ' + \ 'dependent packages. Will not attempt to ' + \ 'remove it.\n' elif self.pkghelper.determineMgr() == "zypper": self.package = "avahi" elif self.pkghelper.check(self.package): compliant = False self.detailedresults += 'DisableAvahi is ' + \ 'set to True, but Avahi is currently ' + \ 'installed.\n' # otherwise if the securemdns CI is set, we want to make sure # it is securely configured if self.SecureMDNS.getcurrvalue(): # if the config file is found, proceed if os.path.exists('/etc/avahi/avahi-daemon.conf'): kvtype = "tagconf" intent = "present" filepath = '/etc/avahi/avahi-daemon.conf' tmpfilepath = '/etc/avahi/avahi-daemon.conf.stonixtmp' conftype = "closedeq" self.avahiconfeditor = KVEditorStonix( self.statechglogger, self.logger, kvtype, filepath, tmpfilepath, self.confoptions, intent, conftype) self.avahiconfeditor.report() if self.avahiconfeditor.fixables: compliant = False self.detailedresults += "\nThe following configuration options are missing or incorrect in " + str( filepath) + ":\n" + "\n".join( self.avahiconfeditor.fixables) # if config file not found, check if avahi is installed else: # if not installed, we can't configure anything if not self.pkghelper.check('avahi'): self.detailedresults += 'Avahi Daemon not ' + \ 'installed. Cannot configure it.\n' compliant = True self.logger.log(LogPriority.DEBUG, self.detailedresults) # if it is installed, then the config file is missing else: compliant = False self.detailedresults += 'Avahi is installed ' + \ 'but could not find config file in ' + \ 'expected location.\n' self.logger.log(LogPriority.DEBUG, self.detailedresults) self.compliant = compliant except (IOError): self.detailedresults += '\n' + traceback.format_exc() self.logger.log(LogPriority.ERROR, self.detailedresults) 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 reportmac(self): '''check for configuration items needed for mac os x :returns: bool @author: Breen Malmberg @change: dwalker - implemented kveditor defaults ''' try: self.detailedresults = "" # See if parameter is set self.ch.executeCommand(self.pbr) resultOutput = self.ch.getOutput() if len(resultOutput) >= 1: if (resultOutput[0] == ""): commandsuccess = False self.detailedresults += "Parameter: " + str(self.parameter) + \ " for service " + self.servicename + " is not set.\n" else: commandsuccess = True debug = "Parameter: " + str(self.parameter) + \ " for service " + self.servicename + \ " is set correctly." self.logger.log(LogPriority.DEBUG, debug) else: commandsuccess = False self.detailedresults += "Parameter: " + str(self.parameter) + \ " for service " + self.servicename + " is not set.\n" # see if service is running if not re.match("^10.11", self.environ.getosver()): servicesuccess = self.sh.auditService( self.service, serviceTarget=self.servicename) else: servicesuccess = self.sh.auditService( self.service, serviceTarget=self.servicename) if servicesuccess: debug = "Service: " + str(self.service) + ", " + \ self.servicename + " audit successful." self.logger.log(LogPriority.DEBUG, debug) else: self.detailedresults += "Service: " + str(self.service) + \ ", " + self.servicename + " audit failed.\n" if servicesuccess and commandsuccess: return True else: return False except (KeyboardInterrupt, SystemExit): raise except Exception: self.compliant = False raise 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. @author: Breen Malmberg @change: dwalker - added statechglogger findrulechanges and deleteentry @changed: Breen Malmberg - 12/05/2017 - removed unnecessary servicetarget ''' try: self.rulesuccess = True self.iditerator = 0 eventlist = self.statechglogger.findrulechanges(self.rulenumber) for event in eventlist: self.statechglogger.deleteentry(event) self.detailedresults = "" # if this system is a mac, run fixmac() if self.ismac: self.rulesuccess = self.fixmac() # if not mac os x, run this portion else: # if DisableAvahi CI is enabled, disable the avahi service # and remove the package if self.DisableAvahi.getcurrvalue(): avahi = self.package avahid = 'avahi-daemon' if self.sh.auditService(avahid): debug = "Disabling " + avahid + " service" self.logger.log(LogPriority.DEBUG, debug) self.sh.disableService(avahid) self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = { "eventtype": "servicehelper", "servicename": avahid, "startstate": "enabled", "endstate": "disabled" } self.statechglogger.recordchgevent(myid, event) if self.environ.getosfamily() == 'linux' and \ self.pkghelper.check(avahi): if self.numdependencies <= 3: self.pkghelper.remove(avahi) self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = { "eventtype": "pkghelper", "pkgname": avahi, "startstate": "installed", "endstate": "removed" } self.statechglogger.recordchgevent(myid, event) else: debug += 'Avahi package has too many dependent ' \ + 'packages. Will not attempt to remove.\n' self.logger.log(LogPriority.DEBUG, debug) if self.pkghelper.determineMgr() == 'yum' or \ self.pkghelper.determineMgr() == 'dnf': path = self.path if not os.path.exists(path): if createFile(path, self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = { "eventtype": "creation", "filepath": path } self.statechglogger.recordchgevent(myid, event) else: self.rulesuccess = False self.detailedresults += "Failed to create " + \ "file: " + path + ".\n" if self.editor is None: tmppath = path + ".tmp" data = {"NOZEROCONF": "yes"} self.editor = KVEditorStonix( self.statechglogger, self.logger, "conf", path, tmppath, data, "present", "closedeq") if not self.editor.report(): if self.editor.fix(): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.editor.setEventID(myid) if not self.editor.commit(): self.rulesuccess = False self.detailedresults += "Could not " + \ "commit changes to " + path + ".\n" else: self.rulesuccess = False self.detailedresults += "Could not fix " + \ "file " + path + ".\n" # if SecureMDNS CI is enabled, configure avahi-daemon.conf if self.SecureMDNS.getcurrvalue(): # if config file is present, proceed avahiconf = '/etc/avahi/avahi-daemon.conf' if os.path.exists(avahiconf): if self.avahiconfeditor.fixables: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.avahiconfeditor.setEventID(myid) if not self.avahiconfeditor.fix(): self.rulesuccess = False debug = "KVEditor fix for " + avahiconf + \ "failed" self.logger.log(LogPriority.DEBUG, debug) elif not self.avahiconfeditor.commit(): self.rulesuccess = False debug = "KVEditor commit for " + avahiconf + \ "failed" self.logger.log(LogPriority.DEBUG, debug) self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) setPerms(avahiconf, [0, 0, 0o644], self.logger, self.statechglogger, myid) resetsecon(avahiconf) # if config file is not present and avahi not installed, # then we can't configure it else: if not self.pkghelper.check(avahi): debug = 'Avahi Daemon not installed. ' + \ 'Cannot configure it.' self.logger.log(LogPriority.DEBUG, debug) else: self.detailedresults += 'Avahi daemon ' + \ 'installed, but could not locate the ' + \ 'configuration file for it.\n' self.rulesuccess = False except IOError: self.detailedresults += '\n' + traceback.format_exc() self.logger.log(LogPriority.DEBUG, self.detailedresults) 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 fixmac(self): '''apply fixes needed for mac os x @author: Breen Malmberg @change: dwalker - implemented kveditor instead of direct editing ''' try: self.detailedresults = "" success = True # See if parameter is set self.ch.executeCommand(self.pbr) resultOutput = self.ch.getOutput() if len(resultOutput) >= 1: if (resultOutput[0] == ""): fixit = True else: fixit = False else: fixit = True # Add parameter if fixit: # Due to weaknesses in using PlistBuddy and defaults to delete # from plists, as well as shortcomings in STONIX's state change # logging, we will record this change as a file deletion. # If the rule's undo is run on OS X, it will restore the # previous version of this file. self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if self.hasSIP: self.statechglogger.recordfiledelete( self.preferences, myid) else: self.statechglogger.recordfiledelete(self.service, myid) self.ch.executeCommand(self.pbf) resultOutput = self.ch.getOutput() errorcode = self.ch.getReturnCode() if errorcode == 0: debug = self.parameter + " was set successfully!" self.logger.log(LogPriority.DEBUG, debug) else: self.detailedresults += self.parameter + \ " was not set successfully!\n" self.statechglogger.deleteentry(myid) success = False else: debug = self.parameter + " was already set!" self.logger.log(LogPriority.DEBUG, debug) # Reload Service if success: success = self.sh.reloadService(self.service, serviceTarget=self.servicename) if success: debug = "Service: " + str(self.service) + ", " + \ self.servicename + " was reloaded successfully." self.logger.log(LogPriority.DEBUG, debug) else: debug = "Service: " + str(self.service) + ", " + \ self.servicename + " reload failed!" self.logger.log(LogPriority.DEBUG, debug) except (KeyboardInterrupt, SystemExit): raise except Exception: success = False raise return success def parseNumDependencies(self, pkgname): '''parse output of yum command to determine number of dependent packages to the given pkgname :param pkgname: :returns: int @author: Breen Malmberg ''' numdeps = 0 flag = 0 try: if self.pkghelper.determineMgr() == 'zypper': command = ['zypper', 'info', '--requires', pkgname] self.ch.executeCommand(command) output = self.ch.getOutput() for line in output: if flag: numdeps += 1 if re.search('Requires:', line): flag = 1 elif self.pkghelper.determineMgr() == 'yum': command = ['yum', '--assumeno', 'remove', pkgname] self.ch.wait = False self.ch.executeCommand(command) output = self.ch.getOutput() for line in output: if re.search('Dependent packages\)', line): sline = line.split('(+') if len(sline) < 2: return numdeps cline = [ int(s) for s in sline[1].split() if s.isdigit() ] numdeps = int(cline[0]) elif self.pkghelper.determineMgr() == 'dnf': command = ['dnf', '--assumeno', 'remove', pkgname] self.ch.wait = False self.ch.executeCommand(command) output = self.ch.getOutput() for line in output: if re.search('Dependent packages\)', line): sline = line.split('(+') if len(sline) < 2: return numdeps cline = [ int(s) for s in sline[1].split() if s.isdigit() ] numdeps = int(cline[0]) elif self.pkghelper.determineMgr() == 'apt-get': command = ['apt-cache', 'depends', pkgname] self.ch.executeCommand(command) output = self.ch.getOutput() for line in output: if re.search('Depends:', line): numdeps += 1 else: self.detailedresults += 'Unable to detect package manager\n' return numdeps except (IOError, OSError): self.detailedresults += 'Specified package: ' + str(pkgname) + \ ' not found.\n' return numdeps except Exception: raise return numdeps
class DisableWeakAuthentication(Rule): '''This rule will remove rsh(server and client) if installed, remove pam_rhosts entry from any pam file, ''' def __init__(self, config, environ, logger, statechglogger): Rule.__init__(self, config, environ, logger, statechglogger) self.logger = logger self.rulenumber = 30 self.rulename = "DisableWeakAuthentication" self.mandatory = True self.formatDetailedResults("initialize") self.guidance = ["NSA 3.2.3.1"] self.applicable = { 'type': 'white', 'family': ['linux', 'solaris', 'freebsd'] } # Configuration item instantiation datatype = 'bool' key = 'DISABLEWEAKAUTHENTICATION' instructions = "To prevent the disabling of services using weak " + \ "authentication set DISABLEWEAKAUTHENTICATION to False." default = True self.ci = self.initCi(datatype, key, instructions, default) self.rsh = ["rshell", "rsh-client", "rsh-server", "rsh", "SUNWrcmdc"] self.pams = [ "/etc/pam.conf", "/etc/pam_ldap.conf", "/etc/pam.conf-winbind" ] self.incorrects = [] self.iditerator = 0 self.sethelptext() def report(self): '''DisableWeakAuthentication.report() Public method to report on the presence of certain r-command packages and contents in pam files. @author: dwalker :returns: bool - False if the method died during execution ''' self.detailedresults = "" try: self.helper = Pkghelper(self.logger, self.environ) compliant = True for item in self.rsh: if self.helper.check(item): compliant = False self.detailedresults += item + " is still installed\n" break for item in self.pams: found = False if os.path.exists(item): contents = readFile(item, self.logger) if contents: for line in contents: if re.match('^#', line) or \ re.match(r'^\s*$', line): continue elif re.search("pam_rhosts", line): found = True compliant = False self.detailedresults += "pam_rhosts line " + \ "found in " + item + "\n" break if found: self.incorrects.append(item) if not checkPerms(item, [0, 0, 420], self.logger): compliant = False self.detailedresults += "Permissions for " + \ item + " are incorrect\n" if os.path.exists("/etc/pam.d/"): fileItems = glob.glob("/etc/pam.d/*") for item in fileItems: found = False if os.path.islink(item) or os.path.isdir(item): continue contents = readFile(item, self.logger) if not contents: continue for line in contents: if re.match('^#', line) or re.match(r'^\s*$', line): continue elif re.search("pam_rhosts", line): found = True compliant = False self.detailedresults += "pam_rhosts line " + \ "found in " + item + "\n" break if found: self.incorrects.append(item) for item in fileItems: if os.path.islink(item) or os.path.isdir(item): continue if not checkPerms(item, [0, 0, 420], self.logger): compliant = False self.detailedresults += "Permissions for " + \ item + " are incorrect\n" break if self.incorrects: debug = "The following files need to be corrected: " + \ str(self.incorrects) + "\n\n" self.logger.log(LogPriority.DEBUG, debug) 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): '''DisableWeakAuthentication.fix() Public method to fix any issues that were found in the report method. @author: dwalker :returns: bool - False if the method died during execution ''' try: self.detailedresults = "" if not self.ci.getcurrvalue(): return success = True for item in self.rsh: if self.helper.check(item): if not self.helper.remove(item): success = False if self.incorrects: for item in self.incorrects: tempstring = "" contents = readFile(item, self.logger) if not contents: continue for line in contents: if re.match('^#', line) or re.match(r'^\s*$', line): tempstring += line elif re.search("pam_rhosts", line): continue else: tempstring += line if not checkPerms(item, [0, 0, 420], self.logger): if not setPerms(item, [0, 0, 420], self.logger): success = False tmpfile = item + ".tmp" if writeFile(tmpfile, tempstring, self.logger): os.rename(tmpfile, item) os.chown(item, 0, 0) os.chmod(item, 420) resetsecon(item) else: success = False for item in self.pams: if os.path.exists(item): if not checkPerms(item, [0, 0, 420], self.logger): if not setPerms(item, [0, 0, 420], self.logger): success = False if os.path.exists("/etc/pam.d/"): fileItems = glob.glob("/etc/pam.d/*") for item in fileItems: if not checkPerms(item, [0, 0, 420], self.logger): if not setPerms(item, [0, 0, 420], self.logger): success = False return 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 def undo(self): '''There is no undo method for this rule since we don't ever want rsh installed or for the r services to be enabled. Overrides the undo inside the rule.py class ''' try: info = "no undo available" self.logger.log(LogPriority.INFO, info) except (KeyboardInterrupt, SystemExit): raise except Exception: self.detailedresults = traceback.format_exc() self.logger.log(LogPriority.ERROR, self.detailedresults) return False
class SecureSquidProxy(Rule): def __init__(self, config, enviro, logger, statechglogger): Rule.__init__(self, config, enviro, logger, statechglogger) self.logger = logger self.rulenumber = 143 self.rulename = "SecureSquidProxy" self.formatDetailedResults("initialize") self.mandatory = True self.sethelptext() datatype1 = "bool" key1 = "SECURESQUIDPROXY" instructions1 = "To disable this rule set the value of " + \ "SECURESQUIDPROXY to False. MinimizeServices rule disables " + \ "squid by default however this rule will still configure it " + \ "if installed" default1 = True self.ci = self.initCi(datatype1, key1, instructions1, default1) self.guidance = ["NSA 3.19", "CCE 4454-5", "CCE 4353-9", "CCE 4503-9", "CCE 3585-7", "CCE 4419-8", "CCE 3692-1", "CCE 4459-4", "CCE 4476-8", "CCE 4181-4", "CCE 4577-3", "CCE 4344-8", "CCE 4494-1", "CCE 4511-2", "CCE 4529-4", "CCE 3610-3", "CCE 4466-9", "CCE 4607-8", "CCE 4255-6", "CCE 4127-7", "CCE 4519-5", "CCE 4413-1", "CCE 4373-7"] self.applicable = {"type": "white", "family": ["linux"]} self.iditerator = 0 def report(self): ''' ''' try: self.detailedresults = "" compliant = True debug = "" self.ph = Pkghelper(self.logger, self.environ) self.installed = False if self.ph.manager == "apt-get": if self.ph.check("squid3"): self.installed = True self.squidfile = "/etc/squid3/squid.conf" elif self.ph.check("squid"): self.installed = True self.squidfile = "/etc/squid/squid.conf" if self.ph.check("squid"): self.installed = True self.squidfile = "/etc/squid/squid.conf" if self.installed: self.data1 = {"ftp_passive": "on", "ftp_sanitycheck": "on", "check_hostnames": "on", "request_header_max_size": "20 KB", "reply_header_max_size": "20 KB", "cache_effective_user": "******", "cache_effective_group": "squid", "ignore_unknown_nameservers": "on", "allow_underscore": "off", "httpd_suppress_version_string": "on", "forwarded_for": "off", "log_mime_hdrs": "on"} self.data2 = {"http_access": "deny to_localhost"} #make sure these aren't in the file self.denied = ["acl Safe_ports port 70", "acl Safe_ports port 210", "acl Safe_ports port 280", "acl Safe_ports port 488", "acl Safe_ports port 591", "acl Safe_ports port 777"] if os.path.exists(self.squidfile): if not checkPerms(self.squidfile, [0, 0, 420], self.logger): self.detailedresults += "Permissions are not correct " + \ "on " + self.squidfile + "\n" contents = readFile(self.squidfile, self.logger) if contents: found = False for line in contents: if re.search("^http_access", line.strip()): temp = line.strip() temp = re.sub("\s+", " ", temp) temp = re.sub("http_access\s+", "", temp) if re.search("^deny to_localhost", temp): found = True break if not found: compliant = False for key in self.data1: found = False for line in contents: if re.match('^#', line) or re.match(r'^\s*$', line): continue elif re.search("^" + key + " ", line): temp = line.strip() temp = re.sub("\s+", " ", temp) temp = temp.split(" ") if len(temp) >= 3: joinlist = [temp[1], temp[2]] joinstring = " ".join(joinlist) if self.data1[key] == joinstring: found = True else: found = False break else: if self.data1[key] == temp[1]: found = True else: found = False break if not found: debug += key + " either not found or has wrong value\n" self.detailedresults += key + " either not found " + \ "or has wrong value\n" compliant = False if debug: self.logger.log(LogPriority.DEBUG, debug) debug = "" for entry in self.denied: for line in contents: if re.search(entry + "\s+", line.strip()): debug += "line: " + line + \ "should not exist in this file\n" self.detailedresults += "line: " + line + \ "should not exist in this file\n" compliant = False if debug: self.logger.log(LogPriority.DEBUG, debug) else: compliant = False self.detailedresults += "Contents of squid " + \ "configuration file are blank\n" else: compliant = False self.detailedresults += "squid configuration file " + \ "doesn't exist\n" 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): ''' ''' try: if not self.ci.getcurrvalue(): return self.detailedresults = "" success = True created = False self.iditerator = 0 eventlist = self.statechglogger.findrulechanges(self.rulenumber) for event in eventlist: self.statechglogger.deleteentry(event) if self.installed: if not os.path.exists(self.squidfile): if not createFile(self.squidfile, self.logger): success = False else: created = True self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "creation", "filepath": self.squidfile} self.statechglogger.recordchgevent(myid, event) if not checkPerms(self.squidfile, [0, 0, 420], self.logger): if not created: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(self.squidfile, [0, 0, 420], self.logger, self.statechglogger, myid): success = False else: if not setPerms(self.squidfile, [0, 0, 420], self.logger): success = False tempstring = "" contents = readFile(self.squidfile, self.logger) newcontents = [] if contents: '''Remove any undesired acl lines''' for line in contents: if re.match('^#', line) or re.match(r'^\s*$', line): newcontents.append(line) elif re.search("^acl Safe_ports port ", line.strip()): m = re.search("acl Safe_ports port ([0-9]+).*", line) if m.group(1): item = "acl Safe_ports port " + m.group(1) if item in self.denied: continue else: newcontents.append(line) else: newcontents.append(line) '''removeables list holds key vals we find in the file that we can remove from self.data''' removeables = [] '''deleteables list holds key vals we can delete from newcontents list if it's incorrect.''' deleteables = {} for key in self.data1: found = False for line in reversed(newcontents): if re.match('^#', line) or re.match(r'^\s*$', line): continue elif re.search("^" + key + " ", line) or re.search("^" + key, line): temp = line.strip() temp = re.sub("\s+", " ", temp) temp = temp.split(" ") if len(temp) >= 3: joinlist = [temp[1], temp[2]] joinstring = " ".join(joinlist) if self.data1[key] == joinstring: '''We already found this line and value No need for duplicates''' if found: newcontents.remove(line) continue removeables.append(key) found = True else: try: deleteables[line] = "" except Exception: continue continue elif len(temp) == 2: if self.data1[key] == temp[1]: '''We already found this line and value No need for duplicates''' if found: newcontents.remove(line) continue removeables.append(key) found = True else: try: deleteables[line] = "" except Exception: continue continue elif len(temp) == 1: try: deleteables[line] = "" except Exception: continue continue if deleteables: for item in deleteables: newcontents.remove(item) '''anything in removeables we found in the file so we will remove from the self.data1 dictionary''' if removeables: for item in removeables: del(self.data1[item]) '''now check if there is anything left over in self.data1 if there is we need to add that to newcontents list''' if self.data1: for item in self.data1: line = item + " " + self.data1[item] + "\n" newcontents.append(line) for line in newcontents: found = False if re.search("^http_access", line.strip()): temp = line.strip() temp = re.sub("\s+", " ", temp) temp = re.sub("http_access\s+", "", temp) if re.search("^deny to_localhost", temp): found = True break if not found: newcontents.append("http_access deny to_localhost\n") for item in newcontents: tempstring += item tmpfile = self.squidfile + ".tmp" if not writeFile(tmpfile, tempstring, self.logger): success = False else: if not created: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "conf", "filepath": self.squidfile} self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange(self.squidfile, tmpfile, myid) os.rename(tmpfile, self.squidfile) os.chown(self.squidfile, 0, 0) os.chmod(self.squidfile, 420) resetsecon(self.squidfile) else: tempstring = "" for item in self.data1: tempstring += item + " " + self.data1[item] + "\n" for item in self.data2: tempstring += item + " " + self.data2[item] + "\n" tmpfile = self.squidfile + ".tmp" if not writeFile(tmpfile, tempstring, self.logger): success = False else: if not created: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "conf", "filepath": self.squidfile} self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange(self.squidfile, tmpfile, myid) os.rename(tmpfile, self.squidfile) os.chown(self.squidfile, 0, 0) os.chmod(self.squidfile, 420) resetsecon(self.squidfile) self.rulesuccess = success except (KeyboardInterrupt, SystemExit): # User initiated exit raise except Exception as err: self.rulesuccess = False self.detailedresults = self.detailedresults + "\n" + str(err) + \ " - " + 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 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 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 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 SecureNFS(Rule): 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 = 39 self.rulename = "SecureNFS" self.formatDetailedResults("initialize") self.sethelptext() self.applicable = { 'type': 'white', 'family': ['linux', 'solaris', 'freebsd'], 'os': { 'Mac OS X': ['10.15', 'r', '10.15.10'] } } self.guidance = [ "NSA(3.13.4)", "cce-4241-6", "cce-4465-1", "cce-4559-1", "cce-4015-4", "cce-3667-3", "cce-4310-9", "cce-4438-8", "cce-3579-0" ] # Configuration item instantiation datatype = 'bool' key = 'SECURENFS' instructions = "To disable this rule set the value of SECURENFS to False." default = True self.ci = self.initCi(datatype, key, instructions, default) def report(self): """ Run report actions for SecureNFS :return: self.compliant :rtype: bool """ self.detailedresults = "" self.compliant = True nfsexports = True try: if self.environ.getosfamily() == "linux": self.ph = Pkghelper(self.logger, self.environ) # secure NFS service self.sh = ServiceHelper(self.environ, self.logger) if self.environ.getostype() == "Mac OS X": nfsfile = "/etc/nfs.conf" data1 = { "nfs.lockd.port": "", "nfs.lockd.tcp": "1", "nfs.lockd.udp": "1" } if not self.sh.auditService('/System/Library/LaunchDaemons/' + 'com.apple.nfsd.plist', serviceTarget='com.apple.nfsd'): self.compliant = True self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.compliant elif self.ph.manager in ("yum", "zypper", "dnf"): nfsfile = "/etc/sysconfig/nfs" data1 = { "LOCKD_TCPPORT": "32803", "LOCKD_UDPPORT": "32769", "MOUNTD_PORT": "892", "RQUOTAD_PORT": "875", "STATD_PORT": "662", "STATD_OUTGOING_PORT": "2020" } if self.ph.manager == "zypper": nfspackage = "nfs-kernel-server" elif self.ph.manager == "yum" or self.ph.manager == "dnf": nfspackage = "nfs-utils" elif self.ph.manager == "apt-get": nfsfile = "/etc/services" data1 = { "rpc.lockd": ["32803/tcp", "32769/udp"], "rpc.mountd": ["892/tcp", "892/udp"], "rpc.quotad": ["875/tcp", "875/udp"], "rpc.statd": ["662/tcp", "662/udp"], "rpc.statd-bc": ["2020/tcp", "2020/udp"] } nfspackage = "nfs-kernel-server" if self.environ.getostype() != "Mac OS X": if self.ph.manager in ("apt-get", "zypper", "yum", "dnf"): if not self.ph.check(nfspackage): self.compliant = True self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.compliant if os.path.exists(nfsfile): nfstemp = nfsfile + ".stonixtmp" eqtype = "" eqtypestr = "" if self.environ.getostype() == "Mac OS X": eqtype = "openeq" self.editor1 = KVEditorStonix(self.statechglogger, self.logger, "conf", nfsfile, nfstemp, data1, "present", eqtype) elif self.ph.manager in ("yum", "zypper", "dnf"): eqtype = "closedeq" self.editor1 = KVEditorStonix(self.statechglogger, self.logger, "conf", nfsfile, nfstemp, data1, "present", eqtype) elif self.ph.manager == "apt-get": eqtype = "space" self.editor1 = KVEditorStonix(self.statechglogger, self.logger, "conf", nfsfile, nfstemp, data1, "present", eqtype) if eqtype == "openeq": eqtypestr = " = " elif eqtype == "closedeq": eqtypestr = "=" elif eqtype == "space": eqtypestr = " " if not self.editor1.report(): if self.editor1.fixables: missingconfiglines = [] for item in self.editor1.fixables: if isinstance(data1[item], list): for li in data1[item]: missingconfiglines.append( str(item) + eqtypestr + str(li)) else: missingconfiglines.append( str(item) + eqtypestr + str(data1[item])) self.detailedresults += "\nThe following configuration lines are missing from " + str( nfsfile) + ":\n" + "\n".join(missingconfiglines) else: self.detailedresults += "\nOne or more configuration lines are missing from " + str( nfsfile) self.logger.log(LogPriority.DEBUG, self.detailedresults) self.compliant = False if not checkPerms(nfsfile, [0, 0, 420], self.logger): self.detailedresults += "\nPermissions aren't correct on " \ + nfsfile self.logger.log(LogPriority.DEBUG, self.detailedresults) self.compliant = False else: self.detailedresults += "\n" + nfsfile + " does not exist" self.logger.log(LogPriority.DEBUG, self.detailedresults) self.compliant = False # secure NFS exports export = "/etc/exports" if os.path.exists(export): if not self.checkNFSexports(): nfsexports = False extemp = export + ".stonixtmp" data2 = { "all_squash": "", "no_root_squash": "", "insecure_locks": "" } self.editor2 = KVEditorStonix(self.statechglogger, self.logger, "conf", export, extemp, data2, "notpresent", "space") if not self.editor2.report(): incorrectconfiglines = [] if self.editor2.fixables: for item in self.editor2.fixables: # no fixables being generated for items not compliant with data2 and the "notpresent" directive incorrectconfiglines.append(str(item)) self.detailedresults += "\nThe following configuration options are insecure, in file " + str( export) + ":\n" + "\n".join(incorrectconfiglines) self.logger.log(LogPriority.DEBUG, self.detailedresults) self.compliant = False if not checkPerms(export, [0, 0, 420], self.logger): self.detailedresults += "\n" + export + " file doesn't " + \ "have the correct permissions" self.logger.log(LogPriority.DEBUG, self.detailedresults) self.compliant = False if not nfsexports: 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.logger.log(LogPriority.INFO, self.detailedresults) return self.compliant def checkNFSexports(self): """ check the NFS export lines in the exports configuration file :return: retval :rtype: bool """ retval = True filename = "/etc/exports" directory = "/etc/exports.d" fileslist = [] try: if os.path.exists(filename): f = open(filename, "r") contentlines = f.readlines() f.close() if not self.checkNFScontents(contentlines, filename): retval = False if os.path.exists(directory): fileslist = [ f for f in listdir(directory) if isfile(join(directory, f)) ] if fileslist: for filename in fileslist: f = open(filename, "r") contentlines = f.readlines() f.close() if not self.checkNFScontents(contentlines, str(f)): retval = False else: self.logger.log(LogPriority.DEBUG, "No NFS exports to check") except Exception: raise return retval def checkNFScontents(self, contentlines, filename=""): """ check given list of contentlines for required nfs export formatting :param contentlines: :param filename: (Default value = "") :return: retval :rtype: bool """ retval = True if not filename: filename = "(file name not specified)" if not isinstance(filename, str): filename = "(file name not specified)" if not isinstance(contentlines, list): self.logger.log(LogPriority.DEBUG, "Parameter contentlines must be of type: list!") if not contentlines: self.logger.log(LogPriority.DEBUG, "Parameter contentlines was empty!") try: if contentlines: for line in contentlines: if re.search('^#', line): continue elif re.search('^\/', line): # search for overly broad exports broadexports = [ '^\/\w+.*\s*\d{2,3}\.\d{2,3}\.0\.0\/16\b', '^\/\w+.*\s*\d{1,3}\.0\.0\.0\/8\b', '^\/\w+.*\s*.*\*.*' ] for be in broadexports: if re.search(be, line): retval = False self.detailedresults += "The nfs export line:\n" + str( line ) + "\nin " + str( filename ) + " contains an export that is overly broad." else: if re.search('^([^ !$`&*()+]|(\\[ !$`&*()+]))+\s*', line): sline = line.split() if len(sline) < 2: retval = False self.detailedresults += "\nThe export line:\n" + str( '\"' + line.strip() + '\"') + " in " + str( filename ) + " lacks a host specification." if not retval: self.detailedresults += "\n\nThere is no automatic fix action for host exports. Please ensure that each NFS export in " + str( filename ) + " has a host specification.\nSee man exports for help.\n" except Exception: raise return retval def fix(self): """ Run fix actions for SecureNFS :return: self.rulesuccess :rtype: bool @author: dwalker @change: Breen Malmberg - 4/26/2016 - changed location of defaults variables in method; added detailedresults message if fix run while CI disabled; added formatdetailedresults update if fix called when CI disabled; changed return value to always be self.rulesuccess; updated self.rulesuccess based on success variable as well @change: Breen Malmberg - 7/11/2017 - added another service check on mac os x; no files will be created on mac if the service is not enabled """ self.logdispatch.log(LogPriority.DEBUG, "Entering SecureNFS.fix()...") success = True changed1, changed2 = False, False installed = False self.detailedresults = "" try: if not self.ci.getcurrvalue(): self.detailedresults += "\nThe CI for this rule was not enabled. Nothing has been done." success = True self.formatDetailedResults("fix", success, self.detailedresults) self.logdispatch.log(LogPriority.DEBUG, "Exiting SecureNFS.fix()...") return success # 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": nfsservice = "nfsd" nfsfile = "/etc/nfs.conf" data1 = { "nfs.lockd.port": "", "nfs.lockd.tcp": "1", "nfs.lockd.udp": "1" } elif self.ph.manager in ("yum", "zypper", "dnf"): nfsfile = "/etc/sysconfig/nfs" data1 = { "LOCKD_TCPPORT": "32803", "LOCKD_UDPPORT": "32769", "MOUNTD_PORT": "892", "RQUOTAD_PORT": "875", "STATD_PORT": "662", "STATD_OUTGOING_PORT": "2020" } nfsservice = "nfs" if self.ph.manager == "zypper": nfspackage = "nfs-kernel-server" elif self.ph.manager == "yum" or self.ph.manager == "dnf": nfspackage = "nfs-utils" elif self.ph.manager == "apt-get": nfsservice = "nfs-kernel-server" nfspackage = "nfs-kernel-server" nfsfile = "/etc/services" data1 = { "rpc.lockd": ["32803/tcp", "32769/udp"], "rpc.mountd": ["892/tcp", "892/udp"], "rpc.quotad": ["875/tcp", "875/udp"], "rpc.statd": ["662/tcp", "662/udp"], "rpc.statd-bc": ["2020/tcp", "2020/udp"] } if self.environ.getostype() != "Mac OS X": if self.ph.manager in ("apt-get", "zypper"): if not self.ph.check(nfspackage): success = True self.formatDetailedResults("fix", success, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return success if not os.path.exists(nfsfile): if createFile(nfsfile, self.logger): nfstemp = nfsfile + ".stonixtmp" if self.environ.getostype() == "Mac OS X": if not self.sh.auditService( '/System/Library/LaunchDaemons/com.apple.nfsd.plist', serviceTarget='com.apple.nfsd'): success = True self.formatDetailedResults("fix", success, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return success self.editor1 = KVEditorStonix(self.statechglogger, self.logger, "conf", nfsfile, nfstemp, data1, "present", "openeq") elif self.ph.manager in ("yum", "zypper", "dnf"): self.editor1 = KVEditorStonix(self.statechglogger, self.logger, "conf", nfsfile, nfstemp, data1, "present", "closedeq") elif self.ph.manager == "apt-get": self.editor1 = KVEditorStonix(self.statechglogger, self.logger, "conf", nfsfile, nfstemp, data1, "present", "space") if not self.editor1.report(): if not self.editor1.fix(): success = False debug = "fix for editor1 failed" self.logger.log(LogPriority.DEBUG, debug) else: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.editor1.setEventID(myid) if not self.editor1.commit(): success = False debug = "commit for editor1 failed" self.logger.log(LogPriority.DEBUG, debug) else: changed1 = True if not checkPerms(nfsfile, [0, 0, 420], self.logger): if not setPerms(nfsfile, [0, 0, 420], self.logger, self.statechglogger): success = False debug = "Unable to set permissions on " + nfsfile self.logger.log(LogPriority.DEBUG, debug) self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "creation", "filepath": nfsfile} self.statechglogger.recordchgevent(myid, event) else: success = False debug = "Unable to create " + nfsfile + " file" self.logger.log(LogPriority.DEBUG, debug) else: if self.editor1.fixables: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.editor1.setEventID(myid) if not self.editor1.fix(): success = False debug = "editor1 fix failed" self.logger.log(LogPriority.DEBUG, debug) else: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.editor1.setEventID(myid) if not self.editor1.commit(): success = False debug = "editor1 commit failed" self.logger.log(LogPriority.DEBUG, debug) else: changed1 = True if not checkPerms(nfsfile, [0, 0, 420], self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(nfsfile, [0, 0, 420], self.logger, self.statechglogger, myid): debug = "Unable to set permissions on " + nfsfile self.logger.log(LogPriority.DEBUG, debug) export = "/etc/exports" if not os.path.exists(export): # mac os x will automatically enable the nfs # service and related ports if the file /etc/exports # is created if self.environ.getostype() == "Mac OS X": if not self.sh.auditService( '/System/Library/LaunchDaemons/com.apple.nfsd.plist', serviceTarget='com.apple.nfsd'): success = True self.formatDetailedResults("fix", success, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return success if createFile(export, self.logger): extemp = export + ".stonixtmp" data2 = { "all_squash": "", "no_root_squash": "", "insecure_locks": "" } self.editor2 = KVEditorStonix(self.statechglogger, self.logger, "conf", export, extemp, data2, "notpresent", "space") if not self.editor2.report(): if not self.editor2.fix(): success = False debug = "fix for editor2 failed" self.logger.log(LogPriority.DEBUG, debug) else: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.editor2.setEventID(myid) if not self.editor2.commit(): success = False debug = "commit for editor2 failed" self.logger.log(LogPriority.DEBUG, debug) else: changed2 = True if not checkPerms(export, [0, 0, 420], self.logger): if not setPerms(export, [0, 0, 420], self.logger, self.statechglogger): success = False debug = "Unable to set permissions on " + export self.logger.log(LogPriority.DEBUG, debug) self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "creation", "filepath": export} self.statechglogger.recordchgevent(myid, event) else: if installed: extemp = export + ".stonixtmp" data2 = { "all_squash": "", "no_root_squash": "", "insecure_locks": "" } self.editor2 = KVEditorStonix(self.statechglogger, self.logger, "conf", export, extemp, data2, "notpresent", "space") self.editor2.report() if self.editor2.removeables: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.editor2.setEventID(myid) if not self.editor2.fix(): success = False debug = "editor2 fix failed" self.logger.log(LogPriority.DEBUG, debug) else: if not self.editor2.commit(): success = False debug = "editor2 commit failed" self.logger.log(LogPriority.DEBUG, debug) else: changed2 = True if not checkPerms(export, [0, 0, 420], self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(export, [0, 0, 420], self.logger, self.statechglogger, myid): success = False debug = "Unable to set permissions on " + export self.logger.log(LogPriority.DEBUG, debug) if changed1 or changed2: ## CHANGE (Breen Malmberg) 1/23/2017 # The self.sh.reloadservice() call, for SHlaunchd, will start # the service even if it is not already running. # We don't want to start THIS service if it is not # already running/enabled! # We also don't want to change this functionality at the # SHlaunchd class-level, because there may be other # instances in which we want it to do a stop and start # (aka a full reload), so this decision should be made at # the rule-level. ## if self.sh.isRunning(nfsservice, serviceTarget=nfsservice): self.sh.reloadService(nfsservice, serviceTarget=nfsservice) self.logdispatch.log(LogPriority.DEBUG, "Exiting SecureNFS.fix()...") 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", success, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return success
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 SecureCUPS(Rule): '''With this rule, you can: Disable the CUPS service Configure CUPS service Disable Printer Browsing Limit Printer Browsing Disable Print Server Capabilities Set the Default Auth Type Setup default set of policy blocks for CUPS ''' 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 = 128 self.rulename = 'SecureCUPS' self.rulesuccess = True self.formatDetailedResults("initialize") self.mandatory = True self.sethelptext() self.rootrequired = True self.guidance = ['CCE 4420-6', 'CCE 4407-3'] self.applicable = { 'type': 'white', 'os': { 'Mac OS X': ['10.15', 'r', '10.15.10'] }, 'family': ['linux'] } # init CIs datatype1 = 'bool' key1 = 'SECURECUPS' instructions1 = 'To prevent to STONIX from securing the CUPS service, set the value of ' + \ 'SecureCUPS to False.' default1 = True self.SecureCUPS = self.initCi(datatype1, key1, instructions1, default1) datatype2 = 'bool' key2 = 'DISABLECUPS' instructions2 = 'To have STONIX completely disable the CUPS service on this system, set ' + \ 'the value of DisableCUPS to True.' default2 = False self.DisableCUPS = self.initCi(datatype2, key2, instructions2, default2) datatype3 = 'bool' key3 = 'DISABLEPRINTBROWSING' instructions3 = 'If this machine is to be used as a print server and you wish to enable automatic client discovery of ' \ 'its shared printers/print resources, then set the value of DISABLEPRINTBROWSING TO False' default3 = True self.DisablePrintBrowsing = self.initCi(datatype3, key3, instructions3, default3) datatype4 = 'string' key4 = 'PRINTBROWSESUBNET' instructions4 = 'If this machine is to be used as a print server and you wish to enable automatic client discovery of ' \ 'its shared printers/print resources, you may wish to limit that discovery to clients only on a certain subnet. ' \ 'Please customize the value of PRINTBROWSESUBNET to match your networks requirements. Ex: 192.168.0.0/16' default4 = '' self.PrintBrowseSubnet = self.initCi(datatype4, key4, instructions4, default4) datatype5 = 'bool' key5 = 'DISABLEGENERICPORT' instructions5 = 'To prevent remote users from potentially connecting ' + \ 'to and using locally configured printers by disabling the CUPS print ' + \ 'server sharing capabilities, set the value of DisableGenericPort to ' + \ 'True.' default5 = False self.DisableGenericPort = self.initCi(datatype5, key5, instructions5, default5) datatype6 = 'bool' key6 = 'SETDEFAULTAUTHTYPE' instructions6 = 'To prevent the defaultauthtype for cups from being ' + \ 'set to Digest, set the value of SetDefaultAuthType to False.' default6 = True self.SetDefaultAuthType = self.initCi(datatype6, key6, instructions6, default6) datatype7 = 'bool' key7 = 'SETUPDEFAULTPOLICYBLOCKS' instructions7 = "To prevent default policy blocks for cups from " + \ "being defined in the cups config file, set the value of " + \ "SetupDefaultPolicyBlocks to False. Note that if you choose to setup " + \ "the default set of policy blocks you can (and probably should) edit " + \ "them in the cups config file afterward to customize these policies to " + \ "your site's particular needs." default7 = False self.SetupDefaultPolicyBlocks = self.initCi(datatype7, key7, instructions7, default7) self.localize() def localize(self): '''set various settings and variables and objects based on which OS is currently running :returns: void @author: Breen Malmberg ''' self.linux = False self.darwin = False if self.environ.getosfamily() == 'darwin': self.darwin = True if self.environ.getosfamily() == 'linux': self.linux = True self.initObjs() self.setVars() def initObjs(self): '''initialize all required class objects :returns: void @author: Breen Malmberg ''' if self.linux: self.ph = Pkghelper(self.logger, self.environ) if self.darwin: pass self.sh = ServiceHelper(self.environ, self.logger) self.serviceTarget = "" self.ch = CommandHelper(self.logger) def setVars(self): '''set all class variables depending on which OS is currently running :returns: void @author: Breen Malmberg @change: Breen Malmberg - 2/8/2017 - fixed a typo with the var name of the dict for cupsd conf options; ''' try: # linux config if self.linux: self.configfileperms = '0640' errorlog = "/var/log/cups/error_log" logfileperms = '0644' self.pkgname = "cups" self.svcname = "cups" accesslog = "/var/log/cups/access_log" self.cupsfilesopts = {} self.cupsfilesconf = "" self.cupsdconf = "" self.cupsdconfremopts = {} # darwin config if self.darwin: accesslog = "/private/var/log/cups/access_log" # a value of 'strict' causes issues connecting to printers on some mac systems self.configfileperms = '0644' logfileperms = '0644' errorlog = "/private/var/log/cups/error_log" self.svclongname = "/System/Library/LaunchDaemons/org.cups.cupsd.plist" self.svcname = "org.cups.cupsd" # common config cupsdconflocs = [ '/etc/cups/cupsd.conf', '/private/etc/cups/cupsd.conf' ] for loc in cupsdconflocs: if os.path.exists(loc): self.cupsdconf = loc self.tmpcupsdconf = self.cupsdconf + ".stonixtmp" cupsfileslocs = [ '/etc/cups/cups-files.conf', '/private/etc/cups/cups-files.conf' ] for loc in cupsfileslocs: if os.path.exists(loc): self.cupsfilesconf = loc self.tmpcupsfilesconf = self.cupsfilesconf + ".stonixtmp" # options for cups-files.conf self.cupsfilesopts["ConfigFilePerm"] = self.configfileperms self.cupsfilesopts["ErrorLog"] = errorlog self.cupsfilesopts["LogFilePerm"] = logfileperms # cupsd conf default configuration options loglevel = "warn" self.cupsdconfopts = {"LogLevel": loglevel} self.cupsdconfopts["AccessLog"] = accesslog self.cupsdconfopts["AccessLogLevel"] = "config" # cupsd conf remove these options if self.DisableGenericPort.getcurrvalue(): self.cupsdconfremopts = {"Port": "631"} # this line added to prevent kvaconf trying to load files # it can't access during __init__, if the program is being # run in user mode if self.environ.geteuid() == 0: ## create kveditor objects if os.path.exists(self.cupsfilesconf): kvtype1 = "conf" path1 = self.cupsfilesconf tmpPath1 = path1 + ".stonixtmp" data1 = self.cupsfilesopts intent1 = "present" configType1 = "space" self.KVcupsfiles = KVEditorStonix(self.statechglogger, self.logger, kvtype1, path1, tmpPath1, data1, intent1, configType1) if os.path.exists(self.cupsdconf): kvtype2 = "conf" path2 = self.cupsdconf tmpPath2 = path2 + ".stonixtmp" data2 = self.cupsdconfopts intent2 = "present" configType2 = "space" self.KVcupsd = KVEditorStonix(self.statechglogger, self.logger, kvtype2, path2, tmpPath2, data2, intent2, configType2) # policy blocks self.serveraccess = """# Restrict access to the server... <Location /> Encryption Required Order allow,deny </Location>""" self.adminpagesaccess = """# Restrict access to the admin pages... <Location /admin> Encryption Required Order allow,deny </Location>""" self.configfilesaccess = """# Restrict access to configuration files... <Location /admin/conf> AuthType Default Encryption IfRequested Require user @SYSTEM Order allow,deny </Location>""" self.logfileaccess = """# Restrict access to log files... <Location /admin/log> AuthType Default Encryption IfRequested Require user @SYSTEM Order allow,deny </Location>""" self.defaultprinterpolicies = """# Set the default printer/job policies... <Policy default> # Job-related operations must be done by the owner or an administrator... <Limit Send-Document Send-URI Hold-Job Release-Job Restart-Job Purge-Jobs Set-Job-Attributes Create-Job-Subscription Renew-Subscription Cancel-Subscription Get-Notifications Reprocess-Job Cancel-Current-Job Suspend-Current-Job Resume-Job CUPS-Move-Job> Require user @OWNER @SYSTEM Order deny,allow </Limit> # All administration operations require an administrator to authenticate... <Limit CUPS-Add-Modify-Printer CUPS-Delete-Printer CUPS-Add-Modify-Class CUPS-Delete-Class CUPS-Set-Default> AuthType Default Require user @SYSTEM Order deny,allow </Limit> # All printer operations require a printer operator to authenticate... <Limit Pause-Printer Resume-Printer Enable-Printer Disable-Printer Pause-Printer-After-Current-Job Hold-New-Jobs Release-Held-New-Jobs Deactivate-Printer Activate-Printer Restart-Printer Shutdown-Printer Startup-Printer Promote-Job Schedule-Job-After CUPS-Accept-Jobs CUPS-Reject-Jobs> AuthType Default Require user @SYSTEM Order deny,allow </Limit> # Only the owner or an administrator can cancel or authenticate a job... <Limit Cancel-Job CUPS-Authenticate-Job> Require user @OWNER @SYSTEM Order deny,allow </Limit> <Limit All> Order deny,allow </Limit> </Policy>""" except Exception: raise def sanityCheck(self): '''perform sanity check on the cups configuration files :returns: sane :rtype: bool @author: Breen Malmberg ''' sane = True sanitycheck = "/usr/sbin/cupsd -t" try: self.ch.executeCommand(sanitycheck) retcode = self.ch.getReturnCode() output = self.ch.getOutput() if retcode != 0: sane = False self.detailedresults += "\nError while running command: " + str( sanitycheck) for line in output: if re.search("Bad|Missing|Incorrect|Error|Wrong", line, re.IGNORECASE): sane = False self.detailedresults += "\n" + line except Exception: raise return sane def updateOpts(self): '''update the kveditor values for the different CIs based on their current user-specified values :returns: void @author: Breen Malmberg @change: Breen Malmberg - 2/8/2017 - KVcupsdrem will now only be processed if self.DisableGenericPort CI is True ''' try: if self.DisableCUPS.getcurrvalue(): self.PrintBrowseSubnet.updatecurrvalue(False) if self.DisablePrintBrowsing.getcurrvalue(): self.cupsdconfopts["Browsing"] = "Off" self.cupsdconfopts["BrowseAllow"] = "none" self.cupsdconfopts["BrowseWebIF"] = "No" if self.PrintBrowseSubnet.getcurrvalue(): self.cupsdconfopts["Browsing"] = "On" self.cupsdconfopts["BrowseOrder"] = "allow,deny" self.cupsdconfopts["BrowseDeny"] = "all" self.cupsdconfopts[ "BrowseAllow"] = self.PrintBrowseSubnet.getcurrvalue() if self.DisableGenericPort.getcurrvalue(): self.cupsdconfremopts["Port"] = "631" if self.SetDefaultAuthType.getcurrvalue(): self.cupsdconfopts["DefaultAuthType"] = "Negotiate" # don't try to create, or update, the kv object, # if the file it's based on doesn't exist. # this would cause tracebacks in kveditor if os.path.exists(self.cupsfilesconf): kvtype1 = "conf" path1 = self.cupsfilesconf tmpPath1 = path1 + ".stonixtmp" data1 = self.cupsfilesopts intent1 = "present" configType1 = "space" self.KVcupsfiles = KVEditorStonix(self.statechglogger, self.logger, kvtype1, path1, tmpPath1, data1, intent1, configType1) else: self.logger.log( LogPriority.DEBUG, "Location of required configuration file cups-files.conf could not be determined" ) if os.path.exists(self.cupsdconf): kvtype2 = "conf" path2 = self.cupsdconf tmpPath2 = path2 + ".stonixtmp" data2 = self.cupsdconfopts intent2 = "present" configType2 = "space" self.KVcupsd = KVEditorStonix(self.statechglogger, self.logger, kvtype2, path2, tmpPath2, data2, intent2, configType2) else: self.logger.log( LogPriority.DEBUG, "Location of required configuration file cupsd.conf could not be determined" ) except Exception: raise def report(self): '''run report methods/actions appropriate for the current OS and return compliancy status :returns: self.compliant :rtype: bool @author: Breen Malmberg ''' self.logger.log(LogPriority.DEBUG, "\n\nREPORT()\n\n") # DEFAULTS self.detailedresults = "" self.compliant = True badopts = [ "^HostNameLookups\s+Double", "^Sandboxing\s+strict", "^FatalErrors\s+config" ] badoptsfiles = [self.cupsdconf, self.cupsfilesconf] try: # check for bad config options in files for f in badoptsfiles: if os.path.exists(f): fh = open(f, 'r') contentlines = fh.readlines() fh.close() for line in contentlines: for opt in badopts: if re.search(opt, line, re.IGNORECASE): self.compliant = False # if these opts exist, then we don't want to do anything else ## except just remove them if not self.compliant: self.logger.log( LogPriority.DEBUG, "Bad configuration options found in cups config files. Will now remove them." ) self.formatDetailedResults('report', self.compliant, self.detailedresults) return self.compliant if self.linux: if not self.ph.check(self.pkgname): self.detailedresults += "\nCUPS not installed on this system. Nothing to secure." self.formatDetailedResults('report', self.compliant, self.detailedresults) return self.compliant # update kv objects with any new # user-specified information self.updateOpts() if self.SecureCUPS.getcurrvalue(): # is cups secured? if not self.reportSecure(): self.compliant = False # make sure config syntax is correct if not self.sanityCheck(): self.compliant = False if self.DisableCUPS.getcurrvalue(): # is cups disabled? if not self.reportDisabled(): self.compliant = False 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 reportDisabled(self): '''return True if cups is disabled return False if cups is enabled :returns: retval :rtype: bool @author: Breen Malmberg ''' retval = True try: if self.linux: if self.sh.auditService(self.svcname, serviceTarget=self.serviceTarget): retval = False self.detailedresults += "\nThe " + str( self.svcname) + " service is still configured to run" elif self.darwin: if self.sh.auditService(self.svclongname, serviceTarget=self.svcname): retval = False self.detailedresults += "\nThe " + str( self.svcname) + " service is still configured to run" except Exception: raise return retval def checkPolicyBlocks(self): '''report on whether default policy blocks are currently set up in cups configuration. Note that if these already exist, we do not want to overwrite them as the local admin may have them customised to their specific environment. :returns: retval :rtype: bool @author: Breen Malmberg ''' self.logger.log(LogPriority.DEBUG, "\n\nCHECKPOLICYBLOCKS()\n\n") retval = True rootfound = False adminfound = False adminconffound = False defpolicyfound = False try: if os.path.exists(self.cupsdconf): self.logger.log( LogPriority.DEBUG, "\n\nCUPSD CONF EXISTS. OPENING FILE AND READING CONTENTS...\n\n" ) f = open(self.cupsdconf, 'r') contentlines = f.readlines() f.close() self.logger.log( LogPriority.DEBUG, "\n\nCONTENTS READ. CHECKING FOR POLICY BLOCKS...\n\n") for line in contentlines: if re.search('\<Location \/\>', line, re.IGNORECASE): rootfound = True if re.search('\<Location \/admin\>', line, re.IGNORECASE): adminfound = True if re.search('\<Location \/admin\/conf\>', line, re.IGNORECASE): adminconffound = True if re.search('\<Policy default\>', line, re.IGNORECASE): defpolicyfound = True else: self.logger.log( LogPriority.DEBUG, "\n\ncupsd.conf file does not exist. Nothing to check...\n\n" ) return retval if not rootfound: retval = False self.detailedresults += "\nCUPS Root location policy not defined" if not adminfound: retval = False self.detailedresults += "\nCUPS admin location policy not defined" if not adminconffound: retval = False self.detailedresults += "\nCUPS admin/conf location policy not defined" if not defpolicyfound: retval = False self.detailedresults += "\nCUPS Default Policy block not defined" if retval: self.logger.log(LogPriority.DEBUG, "\n\nALL POLICY BLOCKS OK\n\n") except Exception: raise return retval def reportSecure(self): '''run report actions common to all platforms :returns: retval :rtype: bool @author: Breen Malmberg @change: Breen Malmberg - 2/8/2017 - KVcupsdrem will now only be run if self.DisableGenericPort CI is True ''' self.logger.log(LogPriority.DEBUG, "\n\nREPORTSECURE()\n\n") retval = True try: if self.SecureCUPS.getcurrvalue(): if os.path.exists(self.cupsdconf): # Report on cupsd.conf change/add options self.KVcupsd.setData(self.cupsdconfopts) self.KVcupsd.setIntent("present") self.KVcupsd.report() # Report on cupsd.conf remove options self.KVcupsd.setData(self.cupsdconfremopts) self.KVcupsd.setIntent("notpresent") self.KVcupsd.report() if self.KVcupsd.fixables: retval = False self.detailedresults += "\nThe following configuration options, in " + str( self.cupsdconf) + ", are incorrect:\n" + "\n".join( self.KVcupsd.fixables) if not self.checkPolicyBlocks(): retval = False else: pass if os.path.exists(self.cupsfilesconf): self.KVcupsfiles.report() if self.KVcupsfiles.fixables: retval = False self.detailedresults += "\nThe following configuration options, in " + str( self.cupsfilesconf ) + ", are incorrect:\n" + "\n".join( self.KVcupsfiles.fixables) else: pass else: self.detailedresults += "\nNeither SecureCUPS nor DisableCUPS CI's was enabled. Nothing was done." except Exception: raise return retval def fix(self): '''run fix methods/actions appropriate for the current OS and return success status of fix :returns: success :rtype: bool @author: Breen Malmberg ''' # DEFAULTS self.detailedresults = "" success = True self.iditerator = 0 badopts = [ "^HostNameLookups\s+Double", "^Sandboxing\s+strict", "^FatalErrors\s+config" ] badoptsfiles = [self.cupsdconf, self.cupsfilesconf] foundbadopts = False try: # fix bad opts; do not record state change, ## so that a possible revert, afterward, will ## not revert back to the bad state ## this code is a one-off for a hotfix and ## should probably be discontinued in some future ## release at some point for f in badoptsfiles: contentlines = [] if os.path.exists(f): fh = open(f, 'r') contentlines = fh.readlines() fh.close() for line in contentlines: for opt in badopts: if re.search(opt, line, re.IGNORECASE): foundbadopts = True contentlines = [ c.replace(line, '\n') for c in contentlines ] # finished building new contentlines; now write them for each file fn = open(f, 'w') fn.writelines(contentlines) fn.close() os.chmod(f, 0o644) if foundbadopts: # do not continue with rest of fix because that would save state for this fix run self.logger.log( LogPriority.DEBUG, "Reloading cups service to read configuration changes...") if self.darwin: self.sh.reloadService(self.svclongname, serviceTarget=self.svcname) else: self.sh.reloadService(self.svcname, serviceTarget=self.serviceTarget) self.logger.log( LogPriority.DEBUG, "Removed bad configuration options from cups config files. Exiting..." ) self.formatDetailedResults('fix', success, self.detailedresults) return success # Are any of the CIs enabled? # If not, then exit, returning True if not self.SecureCUPS.getcurrvalue() and \ not self.DisableCUPS.getcurrvalue(): self.detailedresults += "\nNo CI was enabled, so nothing was done." self.logger.log( LogPriority.DEBUG, "SecureCUPS rule was run, but neither SecureCUPS, nor DisableCUPS CI's were enabled so nothing was done!" ) self.formatDetailedResults('fix', success, self.detailedresults) return success if self.linux and not self.ph.check("cups"): self.detailedresults += "\nCUPS is not installed. Nothing to do." self.formatDetailedResults('fix', success, self.detailedresults) return success if self.SecureCUPS.getcurrvalue(): if not self.fixSecure(): success = False if self.DisableCUPS.getcurrvalue(): if not self.disableCUPS(): success = False 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 def fixSecure(self): '''run fix actions common to all platforms :returns: retval :rtype: bool @author: Breen Malmberg @change: Breen Malmberg - 2/8/2017 - added inline comments; changed the way KVCupsdrem was being handled (will not be run if there are no options to remove; aka if self.DisableGenericPort CI is False) ''' retval = True pdefaultfound = False serveraccessfound = False adminaccessfound = False configaccessfound = False logfileaccessfound = False try: if os.path.exists(self.cupsdconf): # Fix cupsd.conf add/change/remove options self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.KVcupsd.setEventID(myid) if self.KVcupsd.fix(): if not self.KVcupsd.commit(): self.detailedresults += "\nCommit failed for cupsd.conf" self.logger.log(LogPriority.DEBUG, "Commit failed for KVcupsd") retval = False # cups-files conf add/change options if os.path.exists(self.cupsfilesconf): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.KVcupsfiles.setEventID(myid) if self.KVcupsfiles.fix(): if not self.KVcupsfiles.commit(): self.detailedresults += "\nCommit failed for cups-files.conf" self.logger.log(LogPriority.DEBUG, "Commit failed for KVcupsfiles") retval = False # cupsdconf default policy blocks # this portion cannot be handled by kveditor because of its # xml style formatting if os.path.exists(self.cupsdconf): if self.SetupDefaultPolicyBlocks.getcurrvalue(): f = open(self.cupsdconf, 'r') contentlines = f.readlines() f.close() for line in contentlines: if re.search("\<Location \/\>", line, re.IGNORECASE): serveraccessfound = True for line in contentlines: if re.search("\<Location \/admin\>", line, re.IGNORECASE): adminaccessfound = True for line in contentlines: if re.search("\<Location \/admin\/conf\>", line, re.IGNORECASE): configaccessfound = True for line in contentlines: if re.search("\<Location \/admin\/log\>", line, re.IGNORECASE): logfileaccessfound = True for line in contentlines: if re.search("\<Policy default\>", line, re.IGNORECASE): pdefaultfound = True if not serveraccessfound: contentlines.append("\n" + self.serveraccess + "\n") self.logger.log( LogPriority.DEBUG, "\n\nroot access policy block not found. adding it...\n\n" ) if not adminaccessfound: contentlines.append("\n" + self.adminpagesaccess + "\n") self.logger.log( LogPriority.DEBUG, "\n\nadmin access policy block not found. adding it...\n\n" ) if not configaccessfound: contentlines.append("\n" + self.configfilesaccess + "\n") self.logger.log( LogPriority.DEBUG, "\n\nconfig access policy block not found. adding it...\n\n" ) if not logfileaccessfound: contentlines.append("\n" + self.logfileaccess + "\n") self.logger.log( LogPriority.DEBUG, "\n\nlog file access policy block not found. adding it...\n\n" ) if not pdefaultfound: contentlines.append("\n" + self.defaultprinterpolicies + "\n") self.logger.log( LogPriority.DEBUG, "\n\ndefault policy block not found. adding it...\n\n" ) tf = open(self.tmpcupsdconf, 'w') tf.writelines(contentlines) tf.close() self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "conf", "filename": self.cupsdconf} self.statechglogger.recordfilechange( self.cupsdconf, self.tmpcupsdconf, myid) self.statechglogger.recordchgevent(myid, event) self.logger.log( LogPriority.DEBUG, "\n\nwriting changes to " + str(self.cupsdconf) + " file...\n\n") os.rename(self.tmpcupsdconf, self.cupsdconf) self.logger.log( LogPriority.DEBUG, "\n\nsetting permissions and ownership for " + str(self.cupsdconf) + " file...\n\n") os.chown(self.cupsdconf, 0, 0) if self.linux: os.chmod(self.cupsdconf, 0o640) elif self.darwin: os.chmod(self.cupsdconf, 0o644) if not self.reloadCUPS(): retval = False except Exception: raise return retval def reloadCUPS(self): '''reload the cups service to read the new configurations :returns: retval :rtype: bool @author: Breen Malmberg ''' retval = True try: # do not attempt to reload the service # if the rule is set to disable it if self.DisableCUPS.getcurrvalue(): return retval if self.linux: if not self.sh.reloadService(self.svcname, serviceTarget=self.serviceTarget): retval = False self.detailedresults += "|nThere was a problem reloading the " + str( self.svcname) + " service" elif self.darwin: if not self.sh.reloadService(self.svclongname, serviceTarget=self.svcname): retval = False self.detailedresults += "|nThere was a problem reloading the " + str( self.svcname) + " service" except Exception: raise return retval def disableCUPS(self): '''disable the cups service :returns: retval :rtype: bool @author: Breen Malmberg ''' retval = True try: if self.linux: if not self.sh.disableService( self.svcname, serviceTarget=self.serviceTarget): retval = False self.detailedresults += "\nThere was a problem disabling the " + str( self.svcname) + " service" elif self.darwin: if not self.sh.disableService(self.svclongname, serviceTarget=self.svcname): retval = False self.detailedresults += "\nThere was a problem disabling the " + str( self.svcname) + " service" except Exception: raise return retval
class DisableCloudServices(RuleKVEditor): '''This method runs all the report methods for RuleKVEditors defined in the dictionary @author: ekkehard j. koch @change: 07/23/2014 added ubuntu methods and applicability; fixed typos in doc strings; added class doc string; implemented pkghelper and iterate methods - bemalmbe ''' ############################################################################### def __init__(self, config, environ, logdispatcher, statechglogger): RuleKVEditor.__init__(self, config, environ, logdispatcher, statechglogger) self.rulenumber = 159 self.rulename = 'DisableCloudServices' self.formatDetailedResults("initialize") self.mandatory = True self.rootrequired = True self.logger = self.logdispatch self.guidance = [] self.applicable = {'type': 'white', 'os': {'Mac OS X': ['10.15', 'r', '10.15.10'], 'Ubuntu': ['12.04', '+']}} self.ch = CommandHelper(self.logdispatch) # init CIs datatype = 'bool' key = 'DISABLECLOUDSERVICES' instructions = "To prevent cloud services from being disabled, " + \ "set the value of DisableCloudServices to False." default = True self.DisableCloudServices = self.initCi(datatype, key, instructions, default) if self.environ.getosfamily() == 'darwin': self.addKVEditor("iCloudSaveNewDocumentsToDisk", "defaults", "NSGlobalDomain", "", {"NSDocumentSaveNewDocumentsToCloud": ["0", "-bool no"]}, "present", "", "Save new documents to disk not to iCloud.", None, False, {"NSDocumentSaveNewDocumentsToCloud": ["1", "-bool yes"]}) else: self.debianpkglist = ['unity-webapps-common', 'unity-lens-shopping'] self.sethelptext() def report(self): '''choose which report method to run based on OS archetype''' self.detailedresults = "" if self.environ.getosfamily() == 'darwin': RuleKVEditor.report(self, False) elif re.search('Ubuntu', self.environ.getostype()): retval = self.reportUbuntu() return retval ############################################################################### def reportUbuntu(self): '''if debian, check for unity-lens-shopping and unity-webapps-common cloud service packages ''' # defaults self.compliant = True self.detailedresults = "" try: self.ph = Pkghelper(self.logdispatch, self.environ) for package in self.debianpkglist: if self.ph.check(package): self.compliant = False except (KeyboardInterrupt, SystemExit): raise except Exception as err: self.rulesuccess = False self.detailedresults = self.detailedresults + "\n" + str(err) + \ " - " + 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): '''choose which fix method to run, based on OS archetype''' self.detailedresults = "" if self.DisableCloudServices.getcurrvalue(): if self.environ.getosfamily() == 'darwin': RuleKVEditor.fix(self, False) elif re.search('Ubuntu', self.environ.getostype()): self.fixUbuntu() ############################################################################### def fixUbuntu(self): '''if debian, disable unity-lens-shopping and unity-webapps-common cloud service packages, if they are installed ''' # defaults self.iditerator = 0 try: for package in self.debianpkglist: if self.ph.check(package): self.ph.remove(package) cmd = self.ph.getInstall() + package event = {'eventtype': 'commandstring', 'command': cmd} self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.statechglogger.recordchgevent(myid, event) except (KeyboardInterrupt, SystemExit): raise except Exception as err: self.rulesuccess = False self.detailedresults += "\n" + str(err) + 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 ForceIdleLogout(Rule): '''The ForceIdleLogout class attempts to configure the system to log out users after long periods of inactivity. This control reinforces the protection offered by the screen lock by terminating long idle sessions. Note that the guidance for this control, AC-2(5) from 800-53, seems to be written to the capabilities of Microsoft's active directory product which has the ability to establish work schedules in the directory service and limit logons to those time windows. *NIX systems typically do not have that type of tooling so we rely on a long idle time. ''' 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 = 23 self.rulename = 'ForceIdleLogout' self.formatDetailedResults("initialize") self.mandatory = True self.helptext = '''The ForceIdleLogout rule will configure \ the system to log out GUI sessions that have been idle for a long time. This \ helps prevent take over and illicit use of idle sessions and frees system \ resources. Because some environments may rely on the capability of interactive \ sessions to execute long running jobs this control is optional and will need \ to be enabled below for environments that require it. The idle time before \ logout may also be customized. N.B. Please note that most Linux window \ managers will not save work in progress when the logout occurs. ''' self.rootrequired = True self.applicable = { 'type': 'white', 'family': ['linux'], 'os': { 'Mac OS X': ['10.15', 'r', '10.15.10'] }, 'fisma': 'high' } self.cmdhelper = CommandHelper(self.logger) self.guidance = ['NIST 800-53 AC-2(5)'] myos = self.environ.getostype().lower() self.gconf = "" if re.search("red hat", myos) or re.search("centos", myos): self.gconf = "GConf2" else: self.gconf = "gconf2" datatype = 'bool' key = 'FORCEIDLELOGOUT' instructions = '''To disable this rule set the value of \ FORCEIDLELOGOUT to False.''' default = False self.filci = self.initCi(datatype, key, instructions, default) datatype2 = 'int' key2 = 'FORCEIDLELOGOUTTIMEOUT' instructions2 = '''To customize the timeout period set the \ FORCEIDLELOGOUTTIMEOUT to the desired duration in minutes.''' default2 = 240 self.timeoutci = self.initCi(datatype2, key2, instructions2, default2) self.ph = Pkghelper(self.logger, self.environ) self.gnomesettingpath = "/etc/dconf/db/local.d/00-autologout" self.gnomelockpath = "/etc/dconf/db/local.d/locks/autologout" self.undotimeout = "" self.undoforcelogout = "" self.kdesddm = "" self.gnomeInstalled, self.kdeInstalled = False, False def report(self): '''Report on whether the Idle Logout settings are correct. :returns: bool @author: D.Kennel ''' try: compliant = True self.detailedresults = "" try: self.seconds = self.timeoutci.getcurrvalue() * 60 except (TypeError): self.detailedresults += "FORCEIDLELOGOUTTIMEOUT value is not " + \ "valid!\n" return False if self.environ.osfamily == 'linux': self.kdesddm = self.ph.check("sddm") if self.ph.check("gdm") or self.ph.check("gdm3"): self.gnomeInstalled = True if not self.chkgnome(): self.detailedresults += "Gnome GUI environment " + \ "does not appear to be correctly configured " + \ "for automatic logout of idle sessions. This " + \ "guidance is optional in STONIX, check local " + \ "policy to see if it is required.\n" compliant = False else: self.detailedresults += "Gnome GUI environment " + \ "appears to be correctly configured for " + \ "automatic logout of idle sessions.\n" else: self.gnomeInstalled = False self.detailedresults += "Gnome not installed. No need to configure for gnome\n" 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.chkkde(): self.detailedresults += "KDE GUI environment " + \ "does not appear to be correctly configured " + \ "for automatic logout of idle sessions. This " + \ "guidance is optional in STONIX, check local " + \ "policy to see if it is required.\n" compliant = False else: self.detailedresults += "KDE GUI environment " + \ "appears to be correctly configured for " + \ "automatic logout of idle sessions.\n" else: self.kdeInstalled = False self.detailedresults += "KDE not installed. No need to configure for kde\n" elif self.environ.getosfamily() == 'darwin': if not self.chkosx(): self.detailedresults += "Idle logout value is not set to " + \ str(self.timeoutci.getcurrvalue()) + "\n" self.compliant = compliant except (KeyboardInterrupt, SystemExit): raise except Exception: self.detailedresults = 'ForceIdleLogout: ' self.detailedresults = 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 chkgnome(self): '''Check that the GNOME 3 auto logout settings are set correctly :returns: boot - true if settings are set to logout @author: D. Kennel ''' if not self.environ.geteuid() == 0: # Short circuit for user mode run # manipulating the GNOME settings requires privilege return True compliant = True self.timeoutwrong = False self.logoutwrong = False havedconffile = False havelockfile = False havetimeout = False havetimeoutlock = False havelogout = False havelogoutlock = False if os.path.exists("/usr/bin/gsettings"): if os.path.exists('/etc/dconf/db/local.d'): if os.path.exists(self.gnomesettingpath): havedconffile = True self.logdispatch.log(LogPriority.DEBUG, [ 'ForceIdleLogout.__chkgnome3', 'Found Gnome settings file' ]) contents = readFile(self.gnomesettingpath, self.logger) for line in contents: if re.search( 'sleep-inactive-ac-timeout=' + str(self.seconds), line): havetimeout = True self.logdispatch.log(LogPriority.DEBUG, [ 'ForceIdleLogout.__chkgnome3', 'Found Gnome timeout' ]) if re.search("sleep-inactive-ac-type='logout'", line): havelogout = True self.logdispatch.log(LogPriority.DEBUG, [ 'ForceIdleLogout.__chkgnome3', 'Found Gnome logout' ]) if os.path.exists(self.gnomelockpath): havelockfile = True self.logdispatch.log(LogPriority.DEBUG, [ 'ForceIdleLogout.__chkgnome3', 'Found Gnome lock file' ]) contents = readFile(self.gnomelockpath, self.logger) for line in contents: if re.search( '/org/gnome/settings-daemon/plugins/power/sleep-inactive-ac-timeout', line): havetimeoutlock = True self.logdispatch.log(LogPriority.DEBUG, [ 'ForceIdleLogout.__chkgnome3', 'Found Gnome timeout lock' ]) if re.search( "/org/gnome/settings-daemon/plugins/power/sleep-inactive-ac-type", line): havelogoutlock = True self.logdispatch.log(LogPriority.DEBUG, [ 'ForceIdleLogout.__chkgnome3', 'Found Gnome logout lock' ]) if not havedconffile: self.detailedresults += "GNOME 3 autologout settings " + \ "file not found at: " + \ "/etc/dconf/db/local.d/00-autologout\n" compliant = False if not havetimeout: self.detailedresults += "GNOME 3 autologout timeout " + \ "not found or does not match expected value. Set " + \ "sleep-inactive-ac-timeout=" + str(self.seconds) + \ " in /etc/dconf/db/local.d/00-autologout\n" compliant = False if not havelogout: self.detailedresults += "GNOME 3 autologout logout " + \ "not found. Set sleep-inactive-ac-type='logout' in " + \ "/etc/dconf/db/local.d/00-autologout\n" compliant = False if not havelockfile: self.detailedresults += "GNOME 3 autologout lock " + \ "file not found at: " + \ "/etc/dconf/db/local.d/locks/autologout\n" compliant = False if not havetimeoutlock: self.detailedresults += "GNOME 3 autologout timeout " + \ "lock not found. Set /org/gnome/settings-daemon/" + \ "plugins/power/sleep-inactive-ac-timeout in " + \ "/etc/dconf/db/local.d/locks/autologout\n" compliant = False if not havelogoutlock: self.detailedresults += "GNOME 3 autologout lock not " + \ "found. Set /org/gnome/settings-daemon/plugins/" + \ "power/sleep-inactive-ac-type in " + \ "/etc/dconf/db/local.d/locks/autologout\n" compliant = False if self.ph.check(self.gconf): self.logdispatch.log( LogPriority.DEBUG, ['ForceIdleLogout.__chkgnome3', 'Checking GNOME with gconf']) prefix = '/usr/bin/gconftool-2 --direct --config-source ' + \ 'xml:readwrite:/etc/gconf/gconf.xml.mandatory --get ' idletimecmd = prefix + '/desktop/gnome/session/max_idle_time' idleactcmd = prefix + '/desktop/gnome/session/max_idle_action' self.cmdhelper.executeCommand(idletimecmd) output = self.cmdhelper.getOutput() self.logdispatch.log(LogPriority.DEBUG, [ 'ForceIdleLogout.__chkgnome3', 'Value of idle time ' + str(output) ]) if output: try: if int(output[0].strip()) != self.seconds: compliant = False self.timeoutwrong = True if re.search("No value set", output[0]): self.detailedresults += "GNOME autologout time not found\n" self.undotimeout = "unset" elif isinstance(output[0], int): self.detailedresults += "GNOME autologout time not correct.\n" self.undotimeout = output[0] else: self.detailedresults += "GNOME autologout time not correct\n" self.undotimeout = "''" except (ValueError): # value not found message pass self.cmdhelper.executeCommand(idleactcmd) output2 = self.cmdhelper.getOutput() self.logdispatch.log(LogPriority.DEBUG, [ 'ForceIdleLogout.__chkgnome3', 'Value of idle action ' + str(output2) ]) if output2: if not re.search('forced-logout', output2[0]): compliant = False self.logoutwrong = True if re.search("No value set", output2[0]): self.detailedresults += "GNOME 3 autologout settings " + \ "not found.\n" self.undoforcelogout = "unset" else: self.detailedresults += "GNOME 3 autologout settings " + \ "not correct.\n" self.undoforcelogout = str(output2[0]) return compliant def chkkde(self): '''Check that settings are correct for KDE 4 auto logout. Note that this setting lives in each user's home folder and is the reason this rule has root & non-root components. With root perms we can work on homes that are on local storage but user perms are required for NFS mounted home directories. :returns: bool - true if settings are set to logout for inspected users @author: D. Kennel ''' debug = "Inside chkkde method" self.logger.log(LogPriority.DEBUG, debug) self.kdefix = {} if self.kdesddm: self.kdecheck = ".config/kdeglobals" self.rcpath = ".config/kscreenlockerrc" self.kdeprops = {"ScreenSaver": {"Timeout": str(self.seconds)}} else: self.kdecheck = ".kde" self.rcpath = ".kde/share/config/kscreensaverrc" self.kdeprops = { "ScreenSaver": { "AutoLogout": "true", "AutoLogoutTimeout": str(self.seconds) } } if self.environ.geteuid() == 0: contents = readFile("/etc/passwd", self.logger) for line in contents: username = "" homepath = "" temp = line.split(':') try: username = temp[0] homepath = temp[5] except (IndexError): self.logdispatch.log(LogPriority.DEBUG, [ 'ForceIdleLogout.__chkkde4', 'IndexError processing ' + str(username) ]) continue kdeparent = os.path.join(homepath, self.kdecheck) kdefile = os.path.join(homepath, self.rcpath) if not os.path.exists(kdeparent): # User does not use KDE continue elif not os.path.exists(kdefile): #User uses kde but doesn't have the necessary config file 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 " + \ "auto logout:\n" for user in self.kdefix: self.detailedresults += user + "\n" return False else: return True else: self.logdispatch.log(LogPriority.DEBUG, [ 'ForceIdleLogout.__chkkde4', 'Non root user context starting check' ]) kdeparent = os.path.join(self.environ.geteuidhome(), self.kdecheck) kdefile = os.path.join(kdeparent, self.rcpath) if not os.path.exists(kdeparent): # User does not use KDE 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 does not 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 chkosx(self): ''' ''' globalprefs = "/Library/Preferences/.GlobalPreferences.plist" globalprefstemp = globalprefs + ".stonixtmp" timeout = self.timeoutci.getcurrvalue() * 60 data = { "com.apple.autologout.AutoLogOutDelay": [str(timeout), "-int " + str(timeout)] } self.editor = KVEditorStonix(self.statechglogger, self.logger, "defaults", globalprefs, globalprefstemp, data, "present") return self.editor.report() def fix(self): '''Configure the system to enforce logout of idle GUI sessions @author: D. Kennel ''' try: if not self.filci.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 self.detailedresults = "" success = True if self.environ.geteuid() == 0: self.iditerator = 0 eventlist = self.statechglogger.findrulechanges( self.rulenumber) for event in eventlist: self.statechglogger.deleteentry(event) if self.environ.getosfamily() == "linux": if self.gnomeInstalled: if not self.fixgnome(): success = False if self.kdeInstalled: if not self.fixkde(): success = False elif self.environ.getosfamily() == "darwin": success = self.fixosx() 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 fixgnome(self): '''Configure GNOME 3 for automatic logout. @author: d.kennel ''' if self.environ.geteuid() != 0: debug = "Configuring gnome requires root privileges" self.logger.log(LogPriority.DEBUG, debug) self.detailedresults += "Configuring gnome requires root " + \ "privileges\n" return False success = True createddir = False debug = "" if os.path.exists("/usr/bin/gsettings"): if not os.path.exists('/etc/dconf/db/local.d'): if not os.makedirs('/etc/dconf/db/local.d/'): success = False debug = "Unable to create the /etc/dconf/db/local.d/ directory" self.detailedresults += "Unable to create the /etc/dconf/db/local.d/ directory" self.logger.log(LogPriority.DEBUG, debug) else: createddir = True self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = { "eventtype": "creation", "filepath": "/etc/dconf/db/local.d" } self.statechglogger.recordchgevent(myid, event) if os.path.exists('/etc/dconf/db/local.d'): created1, created2 = False, False self.logdispatch.log(LogPriority.DEBUG, [ 'ForceIdleLogout.__fixgnome3', 'Working GNOME with dconf' ]) try: seconds = self.timeoutci.getcurrvalue() * 60 except (TypeError): self.detailedresults += "FORCEIDLELOGOUTTIMEOUT value is " + \ "not valid!\n" self.rulesuccess = False return False if not os.path.exists(self.gnomesettingpath): if not createFile(self.gnomesettingpath, self.logger): success = False self.detailedresults += "Unable to create " + self.gnomesettingpath + " file\n" else: created1 = True if not createddir: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = { "eventtype": "creation", "filepath": self.gnomesettingpath } self.statechglogger.recordchgevent(myid, event) if os.path.exists(self.gnomesettingpath): gdirectives = { "org.gnome.settings-daemon.plugins.power": { "sleep-inactive-ac-type": "'logout'", 'sleep-inactive-ac-timeout': str(seconds) } } geditor = KVEditorStonix(self.statechglogger, self.logger, "tagconf", self.gnomesettingpath, self.gnomesettingpath + '.tmp', gdirectives, "present", "closedeq") geditor.report() if geditor.fixables: if geditor.fix(): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) geditor.setEventID(myid) if geditor.commit(): debug = self.gnomesettingpath + "'s contents have been " + \ "corrected\n" self.logger.log(LogPriority.DEBUG, debug) os.chown(self.gnomesettingpath, 0, 0) os.chmod(self.gnomesettingpath, 0o644) resetsecon(self.gnomesettingpath) else: debug = "kveditor commit not successful\n" self.logger.log(LogPriority.DEBUG, debug) self.detailedresults += self.gnomesettingpath + \ " properties could not be set\n" succeess = False else: debug = "kveditor fix not successful\n" self.logger.log(LogPriority.DEBUG, debug) self.detailedresults += self.gnomesettingpath + \ " properties could not be set\n" succeess = False if not checkPerms(self.gnomesettingpath, [0, 0, 0o644], self.logger): if not created1: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(self.gnomesettingpath, [0, 0, 0o644], self.logger, self.statechglogger, myid): self.detailedresults += "Unable to set permissions on " + \ self.gnomesettingpath + " file\n" success = False else: if not setPerms(self.gnomesettingpath, [0, 0, 0o644], self.logger): self.detailedresults += "Unable to set permissions on " + \ self.gnomesettingpath + " file\n" success = False havetimeoutlock = False havelogoutlock = False lockdata = [] if not os.path.exists(self.gnomelockpath): if not createFile(self.gnomelockpath, self.logger): success = False self.detailedresults += "Unable to create " + self.gnomelockpath + " file\n" else: created2 = True if not createddir: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = { "eventtype": "creation", "filepath": self.gnomelockpath } self.statechglogger.recordchgevent(myid, event) if os.path.exists(self.gnomelockpath): contents = readFile(self.gnomelockpath, self.logger) for line in contents: if re.search( '/org/gnome/settings-daemon/plugins/power/sleep-inactive-ac-timeout', line): havetimeoutlock = True if re.search( "/org/gnome/settings-daemon/plugins/power/sleep-inactive-ac-type", line): havelogoutlock = True if not havetimeoutlock: lockdata.append( '/org/gnome/settings-daemon/plugins/power/sleep-inactive-ac-timeout\n' ) if not havelogoutlock: lockdata.append( "/org/gnome/settings-daemon/plugins/power/sleep-inactive-ac-type\n" ) tempfile = self.gnomelockpath + ".tmp" if writeFile(tempfile, lockdata, self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = { "eventtype": "conf", "filepath": self.gnomelockpath } self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange( self.gnomelockpath, tempfile, myid) os.rename(tempfile, self.gnomelockpath) os.chown(self.gnomelockpath, 0, 0) os.chmod(self.gnomelockpath, 0o644) resetsecon(self.gnomelockpath) else: self.detailedresults += "Unable to write contents to " + \ self.gnomelockpath success = False if not checkPerms(self.gnomelockpath, [0, 0, 0o644], self.logger): if not created2: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(self.gnomelockpath, [0, 0, 0o644], self.logger, self.statechglogger, myid): self.detailedresults += "Unable to set permissions on " + \ self.gnomelockpath + " file\n" success = False else: if not setPerms(self.gnomelockpath, [0, 0, 0o644], self.logger): self.detailedresults += "Unable to set permissions on " + \ self.gnomelockpath + " file\n" success = False if self.ph.check(self.gconf): self.logdispatch.log( LogPriority.DEBUG, ['ForceIdleLogout.__fixgnome3', 'Working GNOME with gconf']) undocmd = "" undoprefix = '/usr/bin/gconftool-2 --direct --config-source ' + \ 'xml:readwrite:/etc/gconf/gconf.xml.mandatory ' setprefix = '/usr/bin/gconftool-2 --direct --config-source ' + \ 'xml:readwrite:/etc/gconf/gconf.xml.mandatory --set ' settime = setprefix + \ '--type integer /desktop/gnome/session/max_idle_time ' + \ str(self.timeoutci.getcurrvalue()) setlogout = setprefix + \ '--type string /desktop/gnome/session/' + \ 'max_idle_action forced-logout' if self.timeoutwrong: self.cmdhelper.executeCommand(settime) retcode = self.cmdhelper.getReturnCode() if retcode != 0: errstr = self.cmdhelper.getErrorString() self.logger.log(LogPriority.DEBUG, errstr) self.detailedresults += 'Failed to configure idle time limit option.\n' success = False else: if self.undotimeout == "unset": undocmd = undoprefix + '--unset /desktop/gnome/session/max_idle_time' else: undocmd = setprefix + '--type integer /desktop/gnome/session/max_idle_time ' + \ self.undotimeout self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "command", "command": undocmd} self.statechglogger.recordchgevent(myid, event) self.detailedresults += 'Maximum idle time limit configured successfully.\n' if self.logoutwrong: self.cmdhelper.executeCommand(setlogout) retcode = self.cmdhelper.getReturnCode() if retcode != 0: errstr = self.cmdhelper.getErrorString() self.logger.log(LogPriority.DEBUG, errstr) self.detailedresults += 'Failed to configure idle forced-logout option.\n' success = False else: if self.undoforcelogout == "unset": undocmd = undoprefix + '--unset /desktop/gnome/session/max_idle_action' else: undocmd = setprefix + '--type integer /desktop/gnome/session/max_idle_action ' + \ self.undoforcelogout self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "command", "command": undocmd} self.statechglogger.recordchgevent(myid, event) self.detailedresults += 'Idle forced-logout option configured successfully.\n' return success def fixkde(self): '''Configure KDE 4 for automatic logout. @author: d.kennel ''' success = True if self.environ.geteuid() == 0: self.logdispatch.log( LogPriority.DEBUG, ['ForceIdleLogout.__fixkde4', 'Root context starting loop']) 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: 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]) pdefgid = int(user[3]) except (IndexError): continue if puidnum == uidnum: homepath = user[5] found = True 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, homepath): self.detailedresults += "Stonix couldn't correct the contents " + \ " of " + kdefile + "\n" success = False return success def fixosx(self): if not self.editor.report(): if self.editor.fix(): if self.editor.commit(): self.rulesuccess = True else: self.detailedresults += "KVEditor could not commit " + \ "correct configuration\n" self.rulesuccess = False else: self.detailedresults += "KVEditor could not fix configuration\n" self.rulesuccess = False 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 filehandle: string :param kfile: :param user: :returns: bool ''' created = False success = True self.editor = "" debug = "" 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 created = True if self.environ.geteuid() == 0: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "creation", "filepath": kfile} self.statechglogger.recordchgevent(myid, event) if not self.searchFile(kfile): if self.editor.fixables: if self.environ.geteuid() == 0 and not created: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.editor.setEventID(myid) 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] os.chmod(kfile, 0o600) os.chown(kfile, uid, gid) resetsecon(kfile) return success
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 DisableRemoveableStorage(Rule): '''Disable removeable storage. This rule is optional, and disables USB, thunderbolt and firewire storage devices from accessing, or being accessed, by the system. ''' 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 = 29 self.rulename = 'DisableRemoveableStorage' self.mandatory = False self.formatDetailedResults("initialize") self.guidance = ['NSA 2.2.2.2, CIS, NSA(2.2.2.2), cce-4006-3,4173-1'] self.applicable = { 'type': 'white', 'family': ['linux', 'solaris', 'freebsd'], 'os': { 'Mac OS X': ['10.15', 'r', '10.15.10'] }, 'fisma': 'high' } # configuration item instantiation datatype = "bool" key = "DISABLEREMOVEABLESTORAGE" instructions = "To disable removeable storage devices on this system, set the value of DISABLEREMOVEABLESTORAGE to True" default = False self.storageci = self.initCi(datatype, key, instructions, default) #global variables self.grubfiles = [ "/boot/grub2/grub.cfg", "/boot/grub/grub.cfg", "/boot/grub/grub.conf" ] self.pcmcialist = ['pcmcia-cs', 'kernel-pcmcia-cs', 'pcmciautils'] self.pkgremovedlist = [] self.iditerator = 0 self.created = False self.daemonpath = os.path.abspath( os.path.join(os.path.dirname( sys.argv[0]))) + "/stonix_resources/disablestorage.py" self.sethelptext() self.grubperms = "" self.ph = Pkghelper(self.logger, self.environ) self.ch = CommandHelper(self.logger) def report(self): '''report the current rule-compliance status of this system. update self.rulesuccess if method does not succeed. self.compliant if rule succeeds and reports true. :returns: self.compliant :rtype: bool @author: Breen Malmberg @change: dwalker - implementing kveditor and completely revamped rule logic. added event deletion at the beginning of the fix @change: dwalker 8/13/2014 changed name of rule to DisableRemoveableStorage and rule now supports disabling other ports such thunderbolt and firewire ''' try: # defaults self.detailedresults = "" if self.environ.getostype() == "Mac OS X": compliant = self.reportMac() else: compliant = self.reportLinux() self.compliant = compliant except OSError: self.detailedresults = traceback.format_exc() self.logger.log(LogPriority.ERROR, self.detailedresults) 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): '''sub method for linux portion of compliance reporting @author: dwalker :returns: compliant :rtype: boolean ''' compliant = True lsmodcmd = "" # determine location of lsmod binary if os.path.exists("/sbin/lsmod"): lsmodcmd = "/sbin/lsmod" elif os.path.exists("/usr/bin/lsmod"): lsmodcmd = "/usr/bin/lsmod" usbmods = ["usb_storage", "usb-storage"] # run lsmod command and look for any of the items from # usbmods list in the output. If item exists in output # then that usb module is not disabled. This is for # reporting only. There is no fix using lsmod command. if lsmodcmd: for usb in usbmods: cmd = [lsmodcmd, "|", "grep", usb] self.ch.executeCommand(cmd) if self.ch.getReturnCode() == "0": compliant = False self.detailedresults += "lsmod command shows usb not disabled\n" break # check compliance of grub file(s) if files exist if re.search("Red Hat", self.environ.getostype()) and \ re.search("^6", self.environ.getosver()): self.grubperms = [0, 0, 0o600] elif self.ph.manager is "apt-get": self.grubperms = [0, 0, 0o400] else: self.grubperms = [0, 0, 0o644] for grub in self.grubfiles: if os.path.exists(grub): if self.grubperms: if not checkPerms(grub, self.grubperms, self.logger): compliant = False self.detailedresults += "Permissions " + \ "incorrect on " + grub + " file\n" contents = readFile(grub, self.logger) if contents: for line in contents: if re.search("^kernel", line.strip()) \ or re.search("^linux", line.strip()) \ or re.search("^linux16", line.strip()) \ or re.search("^set default_kernelopts", line.strip()): if not re.search("\s+nousb\s*", line): debug = grub + " file doesn't " + \ "contain nousb kernel option\n" self.detailedresults += grub + " file doesn't " + \ "contain nousb kernel option\n" self.logger.log(LogPriority.DEBUG, debug) compliant = False if not re.search( "\s+usbcore\.authorized_default=0\s*", line): debug = grub + " file doesn't " + \ "contain usbcore.authorized_default=0 " + \ "kernel option\n" self.detailedresults += grub + " file doesn't " + \ "contain usbcore.authorized_default=0 " + \ "kernel option\n" self.logger.log(LogPriority.DEBUG, debug) compliant = False # check for existence of certain usb packages, non-compliant # if any exist for item in self.pcmcialist: if self.ph.check(item): self.detailedresults += item + " is installed " + \ "and shouldn't be\n" compliant = False # check modprobe files inside modprobe.d directory for # contents inside self.blacklist variable removeables = [] found1 = True # self.blacklist dictionary contains the directives # and the value we're looking for (key) and contains # a default value of False for each one. Upon finding # each directive and value pair e.g. blacklist usb_storage # the dictionary is updated with a True value. This keeps # track of the directives we didnt find or that had # incorrect values self.blacklist = { "blacklist usb_storage": False, "install usbcore /bin/true": False, "install usb-storage /bin/true": False, "blacklist uas": False, "blacklist firewire-ohci": False, "blacklist firewire-sbp2": False } #check if /etc/modprobe.d directory exists if os.path.exists("/etc/modprobe.d"): #extract all files inside modprobe.d dirs = glob.glob("/etc/modprobe.d/*") # since file name doesn't matter # i.e. all files are read and treated the same in # modprobe.d, if directives are found in any of # the files inside this directory, where they don't # have to be in the same file, the system is compliant for directory in dirs: if os.path.isdir(directory): continue contents = readFile(directory, self.logger) for item in self.blacklist: for line in contents: if re.search("^" + item, line.strip()): self.blacklist[item] = True # if we don't find all directives in any of the files in # modprobe.d, we will now check /etc/modprobe.conf as # they are all equivalent. We will still keep track of # whether we already found one directive in one of the # files in modprobe.d for item in self.blacklist: if not self.blacklist[item]: found1 = False else: found1 = False # either not all directives inside self.blacklist were found # or /etc/modprobe.d didn't exist. Now we check /etc/modprobe.conf # for any remaining unfound directives. if not found1: if os.path.exists("/etc/modprobe.conf"): contents = readFile("/etc/modprobe.conf") if contents: for item in self.blacklist: for line in contents: if re.search("^" + item, line.strip()): self.blacklist[item] = True for item in self.blacklist: if not self.blacklist[item]: debug = "modprobe.conf nor blacklist " + \ "files contain " + item + "\n" self.logger.log(LogPriority.DEBUG, debug) compliant = False # any directives that were found we remove from self.blacklist # We must add to a variable called removeables first then # iterate through removeables and remove each item self.blacklist for item in self.blacklist: if self.blacklist[item]: removeables.append(item) for item in removeables: del (self.blacklist[item]) # check the contents of the udev file for a certain desired line self.udevfile = "/etc/udev/rules.d/10-local.rules" found2 = False if os.path.exists(self.udevfile): if not checkPerms(self.udevfile, [0, 0, 0o644], self.logger): self.detailedresults += "Permissions not correct " + \ "on " + self.udevfile + "\n" compliant = False contents = readFile(self.udevfile, self.logger) for line in contents: if re.search( "ACTION\=\=\"add\"\, SUBSYSTEMS\=\=\"usb\"\, RUN\+\=\"/bin/sh \-c \'for host in /sys/bus/usb/devices/usb\*\; do echo 0 \> \$host/authorized\_default\; done\'\"", line.strip()): found2 = True if not found2: self.detailedresults += "Udev rule not found to disable usb at boot\n" debug = "Udev rule not found to disable usb at boot\n" self.logger.log(LogPriority.DEBUG, debug) compliant = False else: self.detailedresults += "Udev file doesn't exist to disable usb at boot\n" debug = "Udev file doesn't exist to disable usb at boot\n" self.logger.log(LogPriority.DEBUG, debug) compliant = False return compliant def reportMac(self): ''' :returns: compliant :rtype: bool ''' self.detailedresults = "" compliant = True self.setvars() if not self.usbprofile: self.detailedresults += "Could not locate the appropriate usb disablement profile for your system.\n" compliant = False self.formatDetailedResults("report", compliant, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return compliant self.usbdict = { "com.apple.systemuiserver": { "harddisk-external": { "val": ["deny", "eject"], "type": "", "accept": "", "result": False } } } self.usbeditor = KVEditorStonix(self.statechglogger, self.logger, "profiles", self.usbprofile, "", self.usbdict, "", "") if not self.usbeditor.report(): if self.usbeditor.badvalues: self.detailedresults += self.usbeditor.badvalues + "\n" self.detailedresults += "USB Disablement profile either not installed or values are incorrect\n" compliant = False return compliant def setvars(self): self.usbprofile = "" baseconfigpath = "/Applications/stonix4mac.app/Contents/Resources/stonix.app/Contents/MacOS/stonix_resources/files/" self.usbprofile = baseconfigpath + "stonix4macDisableUSB.mobileconfig" # the following path and dictionaries are for testing on local vm's # without installing stonix package each time. DO NOT DELETE # basetestpath = "/Users/username/stonix/src/stonix_resources/files/" # self.usbprofile = basetestpath + "stonix4macDisableUSB.mobileconfig" if not os.path.exists(self.usbprofile): self.logger.log( LogPriority.DEBUG, "Could not locate appropriate usb disablement profile\n") self.usbprofile = "" def fix(self): '''attempt to perform necessary operations to bring the system into compliance with this rule. @author Breen Malmberg @change: dwalker - implemented event deletion at the beginning of fix, also implemented a check for the ci value to see if fix should even be run. ''' try: success = True 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 not self.storageci.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.environ.getostype() == "Mac OS X": success = self.fixMac() else: success = self.fixLinux() 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 def fixMac(self): '''This method will attempt to disable certain storage ports by moving certain kernel extensions. If the check box is checked we will move the kernel (if present) associated with that storage port/device into a folder designated for those disabled extensions. If the check box is unchecked, we will assume the user doesn't want this disabled and if the kernel is no longer where it should be, we will check the disabled extensions folder to see if it was previously disabled. If it's in that folder, we will move it back. @author: Breen Malmberg :returns: bool @change: dwalker 8/19/2014 ''' success = True if not self.usbprofile: return False if not self.usbeditor.report(): if self.usbeditor.fix(): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.usbeditor.setEventID(myid) if not self.usbeditor.commit(): success = False self.detailedresults += "Unable to install " + self.usbprofile + " profile\n" self.logdispatch.log(LogPriority.DEBUG, "Kveditor commit failed") else: success = False self.detailedresults += "Unable to install " + self.passprofile + "profile\n" self.logdispatch.log(LogPriority.DEBUG, "Kveditor fix failed") else: success = False self.detailedresults += "Password CI was not enabled.\n" return success def fixLinux(self): '''sub method for linux portion of compliance fixing @author: dwalker :returns: success :rtype: boolean ''' success = True created1, created2 = False, False changed = False tempstring = "" grubfilefound = False for grub in self.grubfiles: if os.path.exists(grub): grubfilefound = True if self.grubperms: if not checkPerms(grub, self.grubperms, self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(grub, self.grubperms, self.logger, self.statechglogger, myid): success = False contents = readFile(grub, self.logger) kernellinefound = False if contents: for line in contents: if re.search("^kernel", line.strip()) or re.search("^linux", line.strip()) \ or re.search("^linux16", line.strip()): kernellinefound = True if not re.search("\s+nousb\s*", line): changed = True tempstring += line.strip() + " nousb" if not re.search( "\s+usbcore\.authorized_default=0\s+", line): changed = True tempstring += line.strip( ) + " usbcore.authorized_default=0" tempstring += "\n" elif re.search("^set default_kernelopts", line.strip()): # Fedora 31 has changed it's kernel option line format kernellinefound = True kernelline = line.strip() if not re.search("\s+nousb\s*", kernelline): changed = True kernelline = re.sub("\"$", " nousb\"", kernelline) if not re.search( "\s+usbcore\.authorized_default=0\s+", kernelline): changed = True kernelline = re.sub( "\"$", " usbcore.authorized_default=0\"", kernelline) tempstring += kernelline + "\n" else: tempstring += line if not kernellinefound: changed = False self.detailedresults += "The grub file doesn't contain kernel line\n" + \ "Unable to fully implement fixes in this rule\n" success = False if changed: tmpfile = grub + ".tmp" if writeFile(tmpfile, tempstring, self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "conf", "filepath": grub} self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange( grub, tmpfile, myid) os.rename(tmpfile, grub) if not setPerms(grub, self.grubperms, self.logger): success = False self.detailedresults += "Unable to set permissions on " + \ grub + " file\n" else: success = False if not grubfilefound: self.detailedresults += "No grub configuration file found\n" + \ "Unable to fully fix system for this rule\n" success = False blacklistf = "/etc/modprobe.d/stonix-blacklist.conf" tempstring = "" # Check if self.blacklist still contains values, if it # does, then we didn't find all the blacklist values # in report if self.blacklist: # didn't find one or more directives in the files # inside modprobe.d so we now check an alternate file # we create stonixblacklist file if it doesn't # exist and put remaining unfound blacklist # items there if not os.path.exists(blacklistf): created1 = True createFile(blacklistf, self.logger) self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "creation", "filepath": blacklistf} self.statechglogger.recordchgevent(myid, event) # file was already present and we need contents already # inside file to remain in newly written file if not created1: contents = readFile(blacklistf, self.logger) for item in contents: tempstring += item for item in self.blacklist: tempstring += item + "\n" tmpfile = blacklistf + ".tmp" if writeFile(tmpfile, tempstring, self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "conf", "filepath": blacklistf} self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange(blacklistf, tmpfile, myid) os.rename(tmpfile, blacklistf) os.chown(blacklistf, 0, 0) os.chmod(blacklistf, 420) resetsecon(blacklistf) if not checkPerms(blacklistf, [0, 0, 420], self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(blacklistf, [0, 0, 420], self.logger, self.statechglogger, myid): success = False if self.ph.manager == "apt-get": cmd = ["/usr/sbin/update-initramfs", "-u"] if not self.ch.executeCommand(cmd): success = False self.detailedresults += "Unable to run update-initramfs command\n" for item in self.pcmcialist: if self.ph.check(item): self.ph.remove(item) self.pkgremovedlist.append(item) if not os.path.exists(self.udevfile): if not createFile(self.udevfile, self.logger): self.detailedresults += "Unable to create " + \ self.udevfile + " file\n" success = False else: created2 = True if os.path.exists(self.udevfile): if not checkPerms(self.udevfile, [0, 0, 0o644], self.logger): if created2: if not setPerms(self.udevfile, [0, 0, 0o644], self.logger): success = False self.detailedresults += "Unable to set " + \ "permissions on " + self.udevfile + "\n" else: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(self.udevfile, [0, 0, 0o644], self.logger, self.statechglogger, myid): success = False self.detailedresults += "Unable to set " + \ "permissions on " + self.udevfile + "\n" found = False contents = readFile(self.udevfile, self.logger) tempstring = "" for line in contents: if re.search( "ACTION==\"add\"\, SUBSYSTEMS==\"usb\"\, RUN+=\"/bin/sh -c \'for host in /sys/bus/usb/devices/usb\*\; do echo 0 > \$host/authorized_default; done\'\"", line.strip()): found = True tempstring += line if not found: tempstring += "ACTION==\"add\", SUBSYSTEMS==\"usb\", RUN+=\"/bin/sh -c \'for host in /sys/bus/usb/devices/usb*; do echo 0 > $host/authorized_default; done\'\"" tmpfile = self.udevfile + ".tmp" if writeFile(tmpfile, tempstring, self.logger): if not created2: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = { "eventtype": "conf", "filepath": self.udevfile } self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange( self.udevfile, tmpfile, myid) os.rename(tmpfile, self.udevfile) os.chown(self.udevfile, 0, 0) os.chmod(self.udevfile, 0o644) resetsecon(self.udevfile) else: success = False self.detailedresults += "Unable to write changes " + \ "to " + self.udevfile + "\n" return success
class SetTFTPDSecureMode(Rule): """ """ 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.rootrequired = True self.rulenumber = 98 self.rulename = 'SetTFTPDSecureMode' self.formatDetailedResults("initialize") self.mandatory = True self.sethelptext() datatype = 'bool' key = 'SETTFTPDSECUREMODE' instructions = "To disable this rule set the value of " + \ "SETTFTPDSECUREMODE to False" default = True self.tftpdci = self.initCi(datatype, key, instructions, default) self.guidance = ["NSA 3.1.5.4"] self.iditerator = 0 self.editor = "" self.applicable = { 'type': 'white', 'family': ['linux', 'solaris', 'freebsd'] } def report(self): """ :return: self.compliant :rtype: bool """ try: self.detailedresults = "" compliant = True self.ph = Pkghelper(self.logger, self.environ) if self.ph.manager == "apt-get": pkg = "tftpd-hpa" if self.ph.check(pkg): self.tftpFile = "/etc/default/tftpd-hpa" if os.path.exists(self.tftpFile): compliant = self.reportDebianSys() else: pkg = "tftp-server" if self.ph.check(pkg): self.tftpFile = "/etc/xinetd.d/tftp" if os.path.exists(self.tftpFile): compliant = self.reportOtherSys() self.compliant = compliant 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 reportDebianSys(self): """ :return: compliant :rtype: bool """ contents = readFile(self.tftpFile, self.logger) found1 = False found2 = False compliant = True compliant1 = True compliant2 = True for line in contents: if re.search("TFTP_OPTIONS", line): found1 = True if re.search("=", line): tmp = line.strip() tmp = tmp.split("=") try: #remove beginning and ending spaces of the part #after = if necessary opts = tmp[1].strip() #opts = "\" sdfads \"" #replace actual quotes with nothing opts = re.sub("\"", "", opts) #opts = " sdfads " #once again replace any whitespace with just one space opts = re.sub("/s+", " ", opts.strip()) #split by single whitespace opts = opts.split(" ") if "--secure" not in opts: compliant1 = False except IndexError: self.detailedresults += "No value after = \n" + \ "Bad file format.\n" compliant1 = False elif re.search("TFTP_DIRECTORY", line): found2 = True if re.search("=", line): tmp = line.strip() tmp = tmp.split("=") try: opts = tmp[1].strip() opts = re.sub("\"", "", opts) if not re.search("^/var/lib/tftpboot$", opts): compliant2 = False except IndexError: self.detailedresults += "No value after = \n" + \ "Bad file format.\n" compliant2 = False if not compliant1: self.detailedresults += self.tftpFile + " doesn't contain " + \ "the --secure option for the TFTP_OPTIONS key\n" compliant = False if not compliant2: self.detailedresults += self.tftpFile + " doesn't contain " + \ "the desired directory for the TFTP_DIRECTORY key\n" compliant = False if not found1: self.detailedresults += self.tftpFile + " doesn't contain " + \ "the TFTP_OPTIONS key at all\n" compliant = False if not found2: self.detailedresults += self.tftpFile + " doesn't contain " + \ "the TFTP_DIRECTORY key at all\n" compliant = False return compliant def reportOtherSys(self): """ :return: compliant :rtype: bool """ tftpoptions, contents2 = [], [] contents = readFile(self.tftpFile, self.logger) found = False compliant = True i = 0 if not checkPerms(self.tftpFile, [0, 0, 420], self.logger): self.detailedresults += "Permissions on tftp file are incorrect\n" compliant = False try: for line in contents: if re.search("service tftp", line.strip()): contents2 = contents[i + 1:] else: i += 1 except IndexError: pass if contents2: if contents2[0].strip() == "{": del (contents2[0]) if contents2: i = 0 while i <= len(contents2) and contents2[i].strip( ) != "}" and contents2[i].strip() != "{": tftpoptions.append(contents2[i]) i += 1 if tftpoptions: for line in tftpoptions: if re.search("server_args", line): found = True if re.search("=", line): line = line.split("=") val = re.sub("\s+", " ", line[1].strip()) if not re.search("-s", val) and not re.search( "--search", val): compliant = False self.detailedresults += "server_args line " + \ "doesn't contain the -s option\n" elif not re.search( "\s?-s /var/lib/tftpboot", val ) and not re.search( "\s?--search /var/lib/tftpboot", val): compliant = False self.detailedresults += "server_args line " + \ "doesn't contain the correct contents\n" else: self.detailedresults += "server_args line " + \ "found but contains no = sign, bad format.\n" compliant = False else: compliant = False self.detailedresults += "There are no tftp " + \ "options inside tftp file\n" else: compliant = False self.detailedresults += "tftp file doesn't contain the " + \ "line service tftp\nBad Format\n" if not found: self.detailedresults += "server_args line not found.\n" compliant = False elif found and not compliant: compliant = False self.detailedresults += "server_args line found but " + \ "either doesn't contain -s argument or has bad " + \ "format.\n" return compliant def fix(self): """ :return: self.rulesuccess :rtype: bool """ try: success = True 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 not self.tftpdci.getcurrvalue(): return elif self.ph.manager == "apt-get": if os.path.exists(self.tftpFile): success = self.fixDebianSys() else: if os.path.exists(self.tftpFile): success = self.fixOtherSys() 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 def fixOtherSys(self): """ :return: success :rtype: bool """ success = True if not checkPerms(self.tftpFile, [0, 0, 420], self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(self.tftpFile, [0, 0, 420], self.logger, self.statechglogger, myid): self.detailedresults += "Unable to set " + \ "permissions on " + self.tftpFile + "\n" success = False try: contents2 = [] contents = readFile(self.tftpFile, self.logger) found = False tempstring = "" i = 0 tftplinefound = False for line in contents: if re.search("service tftp", line.strip()): tempstring += line tftplinefound = True contents2 = contents[i + 1:] break else: tempstring += line i += 1 if not tftplinefound: self.detailedresults += "tftp file doesn't contain " + \ "\"service tftp\" line. Stonix will not attempt to " + \ "fix this. This will require a manual fix\n" return False if contents2: if contents2[0].strip() == "{": tempstring += contents2[0] del (contents2[0]) if contents2: for line in contents2: if re.search("server_args", line): found = True if re.search("=", line): tmp = line.split("=") val = re.sub("/s+", " ", tmp[1].strip()) if re.search("-s", val) or re.search( "--secure", val): if not re.search( "-s /var/lib/tftpboot", val ) and not re.search( "--secure /var/lib/tftpboot", val): val = re.sub( "-s\s{0,1}/{0,1}.*\s{0,1}", "-s /var/lib/tftpboot", tmp[1]) tempstring += "\tserver_args \t\t= " + val + "\n" else: tempstring += line else: tempstring += line + " -s /var/lib/tftpboot\n" else: tempstring += "\tserver_args\t\t= -s /var/lib/tftpboot\n" elif re.search("}", line.strip()): if not found: tempstring += "\tserver_args\t\t= -s /var/lib/tftpboot\n" tempstring += "}" break else: tempstring = "" break else: tempstring += line except IndexError: self.detailedresults += "The tftp file is in bad format\n " + \ "Will not attempt to correct this file. Exiting\n" return False if not tempstring: return True tmpfile = self.tftpFile + ".tmp" if writeFile(tmpfile, tempstring, self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "conf", "filepath": self.tftpFile} self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange(self.tftpFile, tmpfile, myid) os.rename(tmpfile, self.tftpFile) os.chown(self.tftpFile, 0, 0) os.chmod(self.tftpFile, 420) resetsecon(self.tftpFile) else: self.detailedresults += "Unable to write new contents " + \ "to " + self.tftpFile + " file.\n" success = False return success def fixDebianSys(self): """ :return: success :rtype: bool """ success = True if not checkPerms(self.tftpFile, [0, 0, 420], self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(self.tftpFile, [0, 0, 420], self.logger, self.statechglogger, myid): self.detailedresults += "Unable to set " + \ "permissions on " + self.tftpFile + "\n" success = False contents = readFile(self.tftpFile, self.logger) found1 = False found2 = False tempstring = "" for line in contents: if re.search("TFTP_OPTIONS", line): if found1: continue if re.search("=", line): tmp = line.strip() tmp = tmp.split("=") try: #remove beginning and ending spaces of the part #after = if necessary opts = tmp[1].strip() #opts = "\" sdfads \"" #replace actual quotes with nothing opts = re.sub("\"", "", opts) #opts = " sdfads " #once again replace any whitespace with just one space opts = re.sub("/s+", " ", opts.strip()) #split by single whitespace opts = opts.split(" ") if "--secure" not in opts: continue else: tempstring += line found1 = True except IndexError: continue else: continue elif re.search("TFTP_DIRECTORY", line): if found2: continue if re.search("=", line): tmp = line.strip() tmp = tmp.split("=") try: opts = tmp[1].strip() opts = re.sub("\"", "", opts) if not re.search("^/var/lib/tftpboot$", opts): continue else: tempstring += line found2 = True except IndexError: continue else: tempstring += line if not found1: tempstring += "TFTP_OPTIONS=\"--secure\"\n" if not found2: tempstring += "TFTP_DIRECTORY=\"/var/lib/tftpboot\"\n" tmpfile = self.tftpFile + ".tmp" if writeFile(tmpfile, tempstring, self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "conf", "filepath": self.tftpFile} self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange(self.tftpFile, tmpfile, myid) os.rename(tmpfile, self.tftpFile) os.chown(self.tftpFile, 0, 0) os.chmod(self.tftpFile, 420) resetsecon(self.tftpFile) else: self.detailedresults += "Unable to write new contents " + \ "to " + self.tftpFile + " file.\n" success = False return success
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 SecureSSH(Rule): """ The SecureSSH class makes a number of configuration changes to SSH in \ order to ensure secure use of the functionality. """ def __init__(self, config, environ, logger, statechglogger): """ Constructor """ Rule.__init__(self, config, environ, logger, statechglogger) self.logger = logger self.rulenumber = 8 self.rulename = 'SecureSSH' self.formatDetailedResults("initialize") self.mandatory = True self.environ = environ self.sethelptext() self.applicable = {'type': 'white', 'family': ['linux', 'solaris', 'freebsd'], 'os': {'Mac OS X': ['10.15', 'r', '10.15.10']}} datatype = 'bool' key = 'SECURESSH' instructions = "To disable this rule set the value " + \ "of SECURESSH to False" default = True self.ci = self.initCi(datatype, key, instructions, default) self.guidance = ['CIS, NSA(3.5.2.1)', 'CCE 4325-7', 'CCE 4726-6', 'CCE 4475-0', 'CCE 4370-3', 'CCE 4387-7', 'CCE 3660-8', 'CCE 4431-3', 'CCE 14716-5', 'CCE 14491-5'] self.ed1, self.ed2 = "", "" self.osname = self.environ.getosname() if self.osname == "Mac OS": mpa_datatype = "bool" mpa_key = "ENABLEMACPIVAUTHSSH" mpa_instructions = "To enable piv authentication over ssh, on Mac OS, set the value of ENABLEMACPIVAUTHSSH to True" mpa_default = False self.mac_piv_auth_CI = self.initCi(mpa_datatype, mpa_key, mpa_instructions, mpa_default) def report(self): """ check if ssh is installed and if the correct configuration options are set in the configuration files :return: self.compliant :rtype: bool """ try: self.detailedresults = "" self.installed = False packages = ["ssh", "openssh", "openssh-server", "openssh-client"] self.ph = Pkghelper(self.logger, self.environ) self.compliant = True self.sh = ServiceHelper(self.environ, self.logger) self.macloaded = False compliant = True self.client = {"Host": "*", "Protocol": "2", "GSSAPIAuthentication": "yes", "GSSAPIDelegateCredentials": "yes"} self.server = {"Protocol": "2", "SyslogFacility": "AUTHPRIV", "PermitRootLogin": "******", "MaxAuthTries": "5", "RhostsRSAAuthentication": "no", "HostbasedAuthentication": "no", "IgnoreRhosts": "yes", "PermitEmptyPasswords": "no", "PasswordAuthentication": "yes", "ChallengeResponseAuthentication": "no", "KerberosAuthentication": "yes", "GSSAPIAuthentication": "yes", "GSSAPICleanupCredentials": "yes", "UsePAM": "yes", "Ciphers": "aes128-ctr,aes192-ctr,aes256-ctr", "PermitUserEnvironment": "no", "PrintLastLog": "yes", "MACs": "hmac-sha2-256,hmac-sha2-512", "UsePrivilegeSeparation": "sandbox", "StrictModes": "yes", "Compression": "no"} if self.environ.getostype() == "Mac OS X": self.serverfile = '/private/etc/ssh/sshd_config' self.clientfile = '/private/etc/ssh/ssh_config' else: self.serverfile = "/etc/ssh/sshd_config" # server file self.clientfile = "/etc/ssh/ssh_config" # client file # Portion for non mac systems i.e. linux if self.environ.getostype() != "Mac OS X": for package in packages: if self.ph.check(package): self.installed = True break # If ssh is not installed there is no need to configure, return True if not self.installed: self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.compliant # Portion for Mac systems else: # ssh is installed/enabled if self.sh.auditService("/System/Library/LaunchDaemons/ssh.plist", serviceTarget="com.openssh.sshd"): self.macloaded = True # if ssh not installed/enabled, no need to configure, return True if not self.macloaded: self.detailedresults += "SSH not installed/enabled. Nothing to configure.\n" self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.compliant # if not self.reportMac(): # compliant = False # Portion for both mac and linux reporting if not self.reportSSHFile(self.serverfile, self.server): compliant = False if not self.reportSSHFile(self.clientfile, self.client): compliant = False self.compliant = compliant except (KeyboardInterrupt, SystemExit): raise except Exception: self.rulesuccess = False 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): """ Main fix method divided up into mac and linux sub methods for correcting ssh server and client files as well as configuring Mac OS smart card authentication. :return: self.rulesuccess :rtype: bool """ try: self.detailedresults = "" self.iditerator = 0 success = True debug = "" if not self.ci.getcurrvalue(): self.detailedresults += "The rule CI was not enabled, so nothing was done.\n" self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) self.rulesuccess = success return self.rulesuccess # Clear events from any previous fix run eventlist = self.statechglogger.findrulechanges(self.rulenumber) for event in eventlist: self.statechglogger.deleteentry(event) # Portion for both linux and Mac if not self.fixSSHFile(self.serverfile, self.server): success = False if not self.fixSSHFile(self.clientfile, self.client): success = False # Mac only portion if self.osname == "Mac OS": # runs the fix for the smartcard authentication for mac if not self.fixMac(): success = False 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 def reportSSHFile(self, sshfile, directives): """ Report configuration options of config files :param: sshfile - filepath string :param: directives - dictionary of desired directives :return: compliant :rtype: bool """ compliant = True debug = "" directives = dict(directives) tpath = sshfile + ".tmp" if os.path.exists(sshfile): if re.search("Ubuntu", self.environ.getostype()): if sshfile == "/etc/ssh/sshd_config": del (directives["GSSAPIAuthentication"]) del (directives["KerberosAuthentication"]) elif sshfile == "/etc/ssh/ssh_config": del (directives["GSSAPIAuthentication"]) elif self.environ.getostype() == "Mac OS X" and self.mac_piv_auth_CI.getcurrvalue(): if sshfile == "/private/etc/ssh/sshd_config": directives["PasswordAuthentication"] = "no" self.server = directives editor = KVEditorStonix(self.statechglogger, self.logger, "conf", sshfile, tpath, directives, "present", "space") if not editor.report(): self.detailedresults += "Did not find the correct " + \ "contents in sshd_config\n" compliant = False # for ubuntu systems we want to make sure the following two # directives don't exist in the server file if re.search("Ubuntu", self.environ.getostype()): if sshfile == "/etc/ssh/sshd_config": directives = {"GSSAPIAuthentication": "", "KerberosAuthentication": ""} elif sshfile == "/etc/ssh/ssh_config": directives = {"GSSAPIAuthentication": ""} editor.setIntent("notpresent") editor.setData(directives) if not editor.report(): self.detailedresults += "didn't find the correct" + \ " contents in sshd_config\n" self.logger.log(LogPriority.DEBUG, debug) compliant = False if not checkPerms(sshfile, [0, 0, 0o644], self.logger): self.detailedresults += "Incorrect permissions for " + \ "file " + self.serverfile + "\n" compliant = False else: self.detailedresults += sshfile + " does not exist\n" compliant = False return compliant def fixSSHFile(self, sshfile, directives): """ apply configuration options to config files :param: sshfile - filepath string :param: directives - dictionary of desired directives :return: compliant :rtype: bool """ success = True debug = "" directives = dict(directives) tpath = sshfile + ".tmp" created = False if not os.path.exists(sshfile): createFile(sshfile, self.logger) created = True self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "creation", "filepath": sshfile} self.statechglogger.recordchgevent(myid, event) if os.path.exists(sshfile): if re.search("Ubuntu", self.environ.getostype()): if sshfile == "/etc/ssh/sshd_config": del (directives["GSSAPIAuthentication"]) del (directives["KerberosAuthentication"]) elif sshfile == "/etc/ssh/ssh_config": del (directives["GSSAPIAuthentication"]) elif self.environ.getostype() == "Mac OS X" and self.mac_piv_auth_CI.getcurrvalue(): if sshfile == "/private/etc/ssh/sshd_config": directives["ChallengeResponseAuthentication"] = "no" directives["PasswordAuthentication"] = "no" editor = KVEditorStonix(self.statechglogger, self.logger, "conf", sshfile, tpath, directives, "present", "space") editor.report() if re.search("Ubuntu", self.environ.getostype()): if sshfile == "/etc/ssh/sshd_config": directives = {"GSSAPIAuthentication": "", "KerberosAuthentication": ""} elif sshfile == "/etc/ssh/ssh_config": directives = {"GSSAPIAuthentication": ""} editor.setIntent("notpresent") editor.setData(directives) editor.report() if not checkPerms(sshfile, [0, 0, 0o644], self.logger): if not created: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(sshfile, [0, 0, 0o644], self.logger, self.statechglogger, myid): success = False else: if not setPerms(sshfile, [0, 0, 0o644], self.logger): success = False if editor.fixables or editor.removeables: if not created: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) editor.setEventID(myid) if editor.fix(): if editor.commit(): os.chown(sshfile, 0, 0) os.chmod(sshfile, 0o644) resetsecon(sshfile) else: self.detailedresults += "Unable to correct contents " + \ "in " + sshfile + "\n" debug = "kveditor1 commit did not run successfully" self.logger.log(LogPriority.DEBUG, debug) success = False else: self.detailedresults += "Unable to correct contents " + \ "in " + sshfile + "\n" debug = "kveditor1 fix did not run successfully" self.logger.log(LogPriority.DEBUG, debug) success = False return success def fixMac(self): """ :return: success :rtype: bool """ success = True # reload ssh to read the new configurations self.logger.log(LogPriority.DEBUG, "Restarting sshd service to read/load the new configuration changes") if self.osname == "Mac OS": if not self.sh.reloadService("/System/Library/LaunchDaemons/ssh.plist", serviceTarget="com.openssh.sshd"): success = False self.detailedresults += "Failed to load the new ssh configuration changes\n" else: if not self.sh.reloadService("sshd"): success = False self.detailedresults += "Failed to load the new ssh configuration changes\n" return success
class DisableThumbnailers(Rule): """disable the thumbnail creation feature in nautilus/gnome""" def __init__(self, config, environ, logdispatcher, statechglogger): """ :param config: :param environ: :param logdispatcher: :param statechglogger: """ Rule.__init__(self, config, environ, logdispatcher, statechglogger) self.logger = logdispatcher self.rulenumber = 111 self.mandatory = True self.applicable = {'type': 'white', 'family': ['linux', 'solaris', 'freebsd']} self.formatDetailedResults("initialize") self.guidance = ["NSA 2.2.2.6"] self.rulename = "DisableThumbnailers" self.rulesuccess = True self.sethelptext() datatype = 'bool' key = 'DISABLETHUMBNAILERS' instructions = "To disable this rule set the value of DISABLETHUMBNAILERS to False." default = True self.ci = self.initCi(datatype, key, instructions, default) def localize(self): """ set paths based on what versions of which utilities exist on the system :return: void """ self.gsettings = "/usr/bin/gsettings" self.gconf = "/usr/bin/gconftool-2" self.dconf = "/usr/bin/dconf" self.getcmd = "" self.setcmd = "" self.updatecmd = "" packages = ["gnome", "gdm", "gdm3", "gnome3"] self.lockfile = "/etc/dconf/db/local.d/locks/stonix-thumbnailers" self.lockthumbnails = {"/org/gnome/desktop/thumbnailers/disable-all":""} if os.path.exists(self.gsettings): self.getcmd = self.gsettings + " get org.gnome.desktop.thumbnailers disable-all" self.setcmd = self.gsettings + " set org.gnome.desktop.thummailerss disable-all true" elif os.path.exists(self.gconf): self.getcmd = self.gconf + " --get /schemas/org/gnome/desktop/thumbnailers/disable_all" self.setcmd = self.gconf + " --type bool --set /schemass/org/gnome/desktop/thumbnailers/disable_all true" if os.path.exists(self.dconf): self.updatecmd = self.dconf + " update" self.ch = CommandHelper(self.logger) self.ph = Pkghelper(self.logger, self.environ) self.gnome_installed = False if self.environ.getosname() != "Mac OS": if [self.ph.check(p) for p in packages]: self.gnome_installed = True def report(self): """check the gdm/gnome setting for thumbnailers to determine if it is off or on. report compliant if it is off, non-compliant if it is on. :return: self.compliant :rtype: bool """ self.compliant = True self.ch = CommandHelper(self.logger) self.ph = Pkghelper(self.logger, self.environ) self.detailedresults = "" try: self.localize() if self.gnome_installed: # This portion of the code is relevant to user context if self.environ.geteuid() != 0: self.ch.executeCommand(self.getcmd) if not self.ch.findInOutput("true"): self.compliant = False self.detailedresults += "\nGnome thumbnailers are enabled" # This portion of the code is relevant to root context else: if not self.checkLockFile(): self.compliant = False else: self.logger.log(LogPriority.DEBUG, "Gnome is not installed. Nothing to check.") except (KeyboardInterrupt, SystemExit): raise except Exception: 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 checkLockFile(self): """ :return: compliant :rtype: bool """ compliant = True kvtype = "conf" path = self.lockfile tmppath = self.lockfile + ".stonixtmp" intent = "present" conftype = "space" self.lock_editor = KVEditorStonix(self.statechglogger, self.logdispatch, kvtype, path, tmppath, self.lockthumbnails, intent, conftype) if not self.lock_editor.report(): compliant = False if self.lock_editor.fixables: self.detailedresults += "\nThe following required configuration lines were not found in the gnome lock file:\n" + "\n".join(self.lock_editor.fixables) else: self.detailedresults += "\nOne or more required configuration lines were not found in the gnome lock file" return compliant def fix(self): """ set the value of schema thumbnailers disable-all to true and create the thumbnailers lock file if it doesn't exist :return: self.rulesuccess :rtype: bool """ self.rulesuccess = True self.detailedresults = "" self.iditerator = 0 try: if self.ci.getcurrvalue(): # This portion of the code is run in user context if self.environ.geteuid() != 0: if not self.setValue(): self.rulesuccess = False else: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "commandstring", "command": re.sub("true", "false", self.setcmd)} self.statechglogger.recordchgevent(myid, event) # This portion of the code has to be run in root context else: if not self.setLockFile(): self.rulesuccess = False if os.path.exists(self.dconf): self.ch.executeCommand(self.updatecmd) if self.ch.getReturnCode() == 0: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "commandstring", "command": self.updatecmd} self.statechglogger.recordchgevent(myid, event) else: self.logger.log(LogPriority.DEBUG, "CI not enabled. Fix was not performed.") 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.logger.log(LogPriority.INFO, self.detailedresults) return self.rulesuccess def setValue(self): """ set the thumbnailers disable-all configuration value to true :return: success :rtype: bool """ success = True self.ch.executeCommand(self.setcmd) retcode = self.ch.getReturnCode() if retcode != 0: success = False errstr = self.ch.getErrorString() self.detailedresults += "\n" + errstr return success def setLockFile(self): """ create the lock file for disable thumbnailers (if running in root context) :return: success - True if file is created; False if not :rtype: bool """ success = True if not os.path.isdir("/etc/dconf/db/local.d/locks"): try: os.makedirs("/etc/dconf/db/local.d/locks", 0o755) except Exception: pass try: if not self.lock_editor.fix(): self.detailedresults += "\nFailed to lock thumbnailers setting" success = False elif not self.lock_editor.commit(): self.detailedresults += "\nFailed to lock thumbnailers setting" success = False except Exception: success = False if success: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "conf", "filepath": self.lockfile} self.statechglogger.recordchgevent(myid, event) 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 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 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
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 DisableIPV6(Rule): def __init__(self, config, environ, logger, statechglogger): Rule.__init__(self, config, environ, logger, statechglogger) self.logger = logger self.rulenumber = 123 self.rulename = "DisableIPV6" self.formatDetailedResults("initialize") self.guidance = ["NSA 2.5.3.1"] self.applicable = { 'type': 'white', 'family': ['linux', 'solaris', 'freebsd'], 'os': { 'Mac OS X': ['10.15', 'r', '10.15.10'] } } # configuration item instantiation datatype = 'bool' key = 'DISABLEIPV6' instructions = "To disable this rule set the value of DISABLEIPV6 " + \ "to False." default = False self.ci = self.initCi(datatype, key, instructions, default) self.iditerator = 0 self.created = False self.created2 = False # self.editor1: sysctl file editor # self.editor2: network file editor # self.editor3: sshd file editor self.editor1, self.editor2, self.editor3 = "", "", "" self.sh = ServiceHelper(self.environ, self.logger) self.sethelptext() def report(self): try: self.detailedresults = "" if self.environ.getosfamily() == "linux": self.ph = Pkghelper(self.logger, self.environ) self.compliant = self.reportLinux() elif self.environ.getosfamily() == "freebsd": self.compliant = self.reportFree() elif self.environ.getostype() == "Mac OS X": self.compliant = self.reportMac() 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): 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() elif self.environ.getosfamily() == "freebsd": self.rulesuccess = self.fixFree() elif self.environ.getosfamily() == "darwin": self.rulesuccess = self.fixMac() 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 reportMac(self): self.ifaces = [] compliant = True self.cmdhelper = CommandHelper(self.logger) cmd = ["/usr/sbin/networksetup", "-listallnetworkservices"] if not self.cmdhelper.executeCommand(cmd): self.detailedresults += "Unable to run " + \ "networksetup -listallnetworkservices command\n" return False output = self.cmdhelper.getOutput() for item in output: item = item.strip() cmd = ["/usr/sbin/networksetup", "-getinfo", item] if not self.cmdhelper.executeCommand(cmd): self.detailedresults += "Unable to run " + \ "networksetup -getinfo command\n" return False output2 = self.cmdhelper.getOutput() for item2 in output2: if re.search("^IPv6:", item2): check = item2.split(":") if check[1].strip() != "Off": self.detailedresults += "IPV6 is not turned off " + \ "for " + item + " interface\n" self.ifaces.append(item) compliant = False return compliant def fixMac(self): if self.ifaces: for item in self.ifaces: cmd = ["/usr/sbin/networksetup", "setv6off", item] if not self.cmdhelper.executeCommand(cmd): self.rulesuccess = False else: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = { 'eventtype': 'comm', 'command': ['/usr/sbin/networksetup', 'setv6automatic', item] } self.statechglogger.recordchgevent(myid, event) return self.rulesuccess def reportFree(self): compliant = True self.editor1, self.editor2 = "", "" directives1 = { "ipv6_network_interfaces": "none", "ipv6_activate_all_interfaces": "NO", "ip6addrctl_enable": "NO", "ip6addrctl_policy": "NO" } directives2 = { "net.ipv6.conf.all.disable_ipv6": "1", "net.ipv6.conf.default.disable_ipv6": "1", "net.ipv6.conf.lo.disable_ipv6": "1" } path1 = "/etc/rc.conf" path2 = "/etc/sysctl.conf" tmpfile1 = "/etc/rc.conf.tmp" tmpfile2 = "/etc/sysctl.conf.tmp" # try and create /etc/rc.conf if doesn't exist if not os.path.exists(path1): if not createFile(path1, self.logger): compliant = False self.detailedresults += "Unable to create the file: " + \ path1 + ", so this file will not be configured, " + \ "resulting in failed compliance\n" if os.path.exists(path1): if not checkPerms(path1, [0, 0, 420], self.logger): compliant = False self.editor1 = KVEditorStonix(self.statechglogger, self.logger, "conf", path1, tmpfile1, directives1, "present", "closedeq") if not self.editor1.report(): compliant = False # try and create /etc/sysctl.conf if doesn't exist if not os.path.exists(path2): if not createFile(path2, self.logger): compliant = False self.detailedresults += "Unable to create the file: " + \ path2 + " so this file will not be configured " + \ "resulting in failed compliance\n" if os.path.exists(path2): if not checkPerms(path2, [0, 0, 384], self.logger): compliant = False self.editor2 = KVEditorStonix(self.statechglogger, self.logger, "conf", path2, tmpfile2, directives2, "present", "closedeq") if not self.editor2.report(): compliant = False else: compliant = False cmdhelper = CommandHelper(self.logger) cmd = ["/sbin/ifconfig", "-a"] if not cmdhelper.executeCommand(cmd): return False output = cmdhelper.getOutput() for line in output: line = line.strip() if re.search("^nd6", line): if not re.search("(.)*IFDISABLED(.)*", line): compliant = False return compliant def fixFree(self): # debug messages are used for developers, self.detailedresults # are used for the users information path1 = "/etc/rc.conf" path2 = "/etc/sysctl.conf" success = True debug = "" if os.path.exists(path1): if not checkPerms(path1, [0, 0, 420], self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(self.path, [0, 0, 420], self.logger, self.statechglogger, myid): success = False if self.editor1: if self.editor1.fixables: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.editor1.setEventID(myid) if not self.editor1.fix(): debug += "Kveditor unable to correct file: " + \ path1 + "\n" self.detailedresults += "Unable to correct " + path1 + \ "\n" success = False elif not self.editor1.commit(): self.detailedresults += "Unable to correct " + path1 + \ "\n" debug += "commit for kveditor1 was not successful\n" success = False else: debug += "Editor2 was never created so path didn't exist \ and/or wasn't able to be created\n" success = False else: self.detailedresults += path1 + " doesn't exist!\n" debug += path1 + " doesn't exist, unble to fix file\n" success = False if os.path.exists(path2): # check permissions on /etc/sysctl.conf if not checkPerms(path2, [0, 0, 384], self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) # set permissions if wrong if not setPerms(self.path, [0, 0, 384, self.logger], self.statechglogger, myid): success = False # check if editor is present if self.editor2: if self.editor2.fixables(): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.editor2.setEventID(myid) if not self.editor2.fix(): debug += "Kveditor unable to correct file: " + \ path2 + "\n" self.detailedresults += "Unable to correct " + path2 + \ "\n" success = False elif not self.editor2.commit(): self.detailedresults += "Unable to correct " + path2 + \ "\n" debug += "commit for kveditor2 was not successful\n" success = False else: debug += "Editor2 was never created so path didn't exist \ and/or wasn't able to be created\n" success = False else: self.detailedresults += path2 + " doesn't exist!\n" debug += path2 + " doesn't exist, unble to fix file\n" success = False # restart the network ch = CommandHelper(self.logger) cmd = ["/etc/rc.d/netif", "restart"] if not ch.executeCommand(cmd): self.detaileresults += "Unable to restart network\n" success = False return success def reportLinux(self): self.ch = CommandHelper(self.logger) compliant = True netwrkfile = "" ifacefile = "" self.modprobefiles = [] #self.modprobeOK = False self.modprobeOK = True sysctl = "/etc/sysctl.conf" self.interface = {"IPV6INIT": "no", "NETWORKING_IPV6": "no"} self.sysctls = { "net.ipv6.conf.all.disable_ipv6": "1", "net.ipv6.conf.default.disable_ipv6": "1" } self.modprobes = { "options": ["ipv6 disable=1"], "install": ["ipv6 /bin/true"] } # stig portion, check netconfig file for correct contents if self.ph.manager == "apt-get": nfspkg = "nfs-common" else: nfspkg = "nfs-utils.x86_64" if self.ph.check(nfspkg): if os.path.exists("/etc/netconfig"): item1 = "udp6 tpi_clts v inet6 udp - -" item2 = "tcp6 tpi_cots_ord v inet6 tcp - -" contents = readFile("/etc/netconfig", self.logger) for line in contents: line = re.sub("\s+", " ", line.strip()) if re.search(item1, line) or re.search(item2, line): self.detailedresults += "/etc/netconfig file contains " + \ "lines we don't want present\n" compliant = False # "ifconfig" has been deprecated on Debian9 and some otherd distros # so use "ip addr" instead # Here we check if the system is giving out ipv6 ip addresses if os.path.exists("/sbin/ifconfig"): cmd = ["/sbin/ifconfig"] else: cmd = ["/sbin/ip", "addr"] if not self.ch.executeCommand(cmd): compliant = False else: output = self.ch.getOutput() for line in output: if re.search("^inet6", line.strip()): self.detailedresults += "inet6 exists in the " + \ "ifconfig output\n" compliant = False break # check for ipv6 address in hostname file if os.path.exists("/etc/hosts"): contents = readFile("/etc/hosts", self.logger) for line in contents: if re.search("^#", line) or re.match("^\s*$", line): continue if re.search(":", line): compliant = False # check compliancy of /etc/sysctl.conf file if not os.path.exists(sysctl): self.detailedresults += "File " + sysctl + " does not exist\n" compliant = False else: tmpfile = sysctl + ".tmp" self.editor1 = KVEditorStonix(self.statechglogger, self.logger, "conf", sysctl, tmpfile, self.sysctls, "present", "openeq") if not self.editor1.report(): self.detailedresults += "/etc/sysctl file doesn't contain \ the correct contents\n" compliant = False if not checkPerms(sysctl, [0, 0, 0o644], self.logger): self.detailedresults += "Permissions for " + sysctl + \ "are incorrect\n" compliant = False # in addition to checking /etc/sysctl.conf contents we need to # also check sysctl compliancy using the sysctl command for key in self.sysctls: self.ch.executeCommand("/sbin/sysctl " + key) retcode = self.ch.getReturnCode() output = self.ch.getOutputString() errmsg = output + self.ch.getErrorString() if retcode != 0: if re.search("unknown key", errmsg): continue else: self.detailedresults += "Failed to get value " + key + " using sysctl command\n" errmsg = self.ch.getErrorString() self.logger.log(LogPriority.DEBUG, errmsg) compliant = False else: actualoutput = output.strip() expectedoutput = key + " = " + self.sysctls[key] if actualoutput != expectedoutput: compliant = False self.detailedresults += "\nsysctl output has " + \ "incorrect value: expected " + \ expectedoutput + ", found " + \ actualoutput + "\n" # check files inside modprobe.d directory for correct contents if os.path.exists("/etc/modprobe.d/"): modprobefiles = glob.glob("/etc/modprobe.d/*") for modfile in modprobefiles: tmpfile = "" modprobekveditor = KVEditorStonix(self.statechglogger, self.logger, "conf", modfile, tmpfile, self.modprobes, "present", "space") if modprobekveditor.report(): self.modprobeOK = True break if not self.modprobeOK: self.detailedresults += "Didn't find desired contents inside files " + \ "within /etc/modprobe.d/" compliant = False else: # system isn't using loadable kernel modules, not an issue self.modprobeOK = True # Check for existence of interface and network files to be configured if self.ph.manager == "yum": ifacefile = "/etc/sysconfig/network-scripts/" if not os.path.exists(ifacefile): ifacefile = "" netwrkfile = "/etc/sysconfig/network" if not os.path.exists(netwrkfile): netwrkfile = "" elif self.ph.manager == "zypper": ifacefile = "/etc/sysconfig/network/" if not os.path.exists(ifacefile): ifacefile = "" # go through interface directory and check each interface file # for correct contents if ifacefile: dirs = glob.glob(ifacefile + '*') for loc in dirs: contents = [] if re.search('^' + ifacefile + 'ifcfg', loc): if not checkPerms(loc, [0, 0, 0o644], self.logger): compliant = False contents = readFile(loc, self.logger) if contents: for key in self.interface: found = False for line in contents: if re.search("^#", line) or re.match( "^\s*$", line): continue if re.search("^" + key, line): if re.search("=", line): temp = line.split("=") if temp[1].strip( ) == self.interface[key]: found = True continue else: found = False break else: compliant = False self.detailedresults += loc + \ " file in bad format\n" if not found: self.detailedresults += "contents of " + \ loc + " file is wrong\n" compliant = False break else: continue else: compliant = False # check network file for correct contents if netwrkfile: if os.path.exists(netwrkfile): if not checkPerms(netwrkfile, [0, 0, 0o644], self.logger): compliant = False tmpfile = netwrkfile + ".tmp" self.editor2 = KVEditorStonix(self.statechglogger, self.logger, "conf", netwrkfile, tmpfile, self.interface, "present", "closedeq") if not self.editor2.report(): self.detailedresults += netwrkfile + " doesn't contain \ the correct contents\n" compliant = False else: self.detailedresults += netwrkfile + " doesn't exist\n" compliant = False # This subpart is only for apt-get based systems # sshd needs the inet directive for ipv6 disablement if self.ph.manager == "apt-get": data = {"AddressFamily": "inet"} kvtype = "conf" path = "/etc/ssh/sshd_config" tmpPath = path + ".tmp" intent = "present" configtype = "space" self.editor3 = KVEditorStonix(self.statechglogger, self.logger, kvtype, path, tmpPath, data, intent, configtype) if not self.editor3.report(): self.detailedresults += "/etc/ssh/ssdh_config doesn't \ contain the correct contents\n" compliant = False return compliant def fixLinux(self): ''' @change: dkennel removed extraneous arg from setperms call on 864 ''' universal = "#The following lines were added by stonix\n" debug = "" success = True ifacefile = "" netwrkfile = "" sysctl = "/etc/sysctl.conf" blacklistfile = "/etc/modprobe.d/stonix-blacklist.conf" # STIG portion, correct netconfig file if self.ph.manager == "apt-get": nfspkg = "nfs-common" else: nfspkg = "nfs-utils.x86_64" # if package not installed, no need to configure it if self.ph.check(nfspkg): if os.path.exists("/etc/netconfig"): filestring = "" # we want to make sure the following two lines don't # appear in the netconfig file item1 = "udp6 tpi_clts v inet6 udp - -" item2 = "tcp6 tpi_cots_ord v inet6 tcp - -" contents = readFile("/etc/netconfig", self.logger) for line in contents: templine = re.sub("\s+", " ", line.strip()) # if we find the lines, skip them thus leaving them out of # of the rewrite if re.search(item1, templine) or re.search( item2, templine): continue else: filestring += line tmpfile = "/etc/netconfig.tmp" if not writeFile(tmpfile, filestring, self.logger): success = False else: # record event, rename file, set perms self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "conf", "filepath": "/etc/netconfig"} self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange( "/etc/netconfig", tmpfile, myid) os.rename(tmpfile, "/etc/netconfig") os.chown("/etc/netconfig", 0, 0) os.chmod("/etc/netconfig", 420) resetsecon("/etc/netconfig") # remove any ipv6 addresses from /etc/hosts file if os.path.exists("/etc/hosts"): contents = readFile("/etc/hosts", self.logger) tempstring = "" tmpfile = "/etc/hosts.tmp" for line in contents: if re.search("^#", line) or re.match("^\s*$", line): tempstring += line continue elif re.search(":", line): tempstring += "#" + line else: tempstring += line if writeFile(tmpfile, tempstring, self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "conf", "filepath": "/etc/hosts"} self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange("/etc/hosts", tmpfile, myid) os.rename(tmpfile, "/etc/hosts") os.chown("/etc/hosts", 0, 0) os.chmod("/etc/hosts", 420) resetsecon("/etc/hosts") else: success = False debug = "Unable to write to file /etc/hosts\n" self.logger.log(LogPriority.DEBUG, debug) # fix sysctl / tuning kernel parameters # manually write key value pairs to /etc/sysctl.conf created = False if not os.path.exists(sysctl): if createFile(sysctl, self.logger): created = True setPerms(sysctl, [0, 0, 0o644], self.logger) self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "creation", "filepath": sysctl} self.statechglogger.recordchgevent(myid, event) else: success = False debug = "Unable to create " + sysctl + "\n" self.logger.log(LogPriority.DEBUG, debug) if os.path.exists(sysctl): if not checkPerms(sysctl, [0, 0, 0o644], self.logger): if not created: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(sysctl, [0, 0, 0o644], self.logger, self.statechglogger, myid): success = False tmpfile = sysctl + ".tmp" self.editor1 = KVEditorStonix(self.statechglogger, self.logger, "conf", sysctl, tmpfile, self.sysctls, "present", "openeq") if not self.editor1.report(): if self.editor1.fixables: # If we did not create the file, set an event ID for the # KVEditor's undo event if not created: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.editor1.setEventID(myid) if not self.editor1.fix(): success = False debug = "Unable to complete kveditor fix method" + \ "for /etc/sysctl.conf file\n" self.logger.log(LogPriority.DEBUG, debug) elif not self.editor1.commit(): success = False debug = "Unable to complete kveditor commit " + \ "method for /etc/sysctl.conf file\n" self.logger.log(LogPriority.DEBUG, debug) if not checkPerms(sysctl, [0, 0, 0o644], self.logger): if not setPerms(self.path, [0, 0, 0o644], self.logger): self.detailedresults += "Could not set permissions on " + \ self.path + "\n" success = False resetsecon(sysctl) # here we also check the output of the sysctl command for each key # to cover all bases for key in self.sysctls: if self.ch.executeCommand("/sbin/sysctl " + key): output = self.ch.getOutputString().strip() errmsg = output + self.ch.getErrorString() if re.search("unknown key", errmsg): continue if not re.search(self.sysctls[key] + "$", output): undovalue = output[-1] self.ch.executeCommand("/sbin/sysctl -q -e -w " + key + "=" + self.sysctls[key]) retcode = self.ch.getReturnCode() if retcode != 0: success = False self.detailedresults += "Failed to set " + key + " = " + self.sysctls[ key] + "\n" errmsg = self.ch.getErrorString() self.logger.log(LogPriority.DEBUG, errmsg) else: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) command = "/sbin/sysctl -q -e -w " + key + "=" + undovalue event = { "eventtype": "commandstring", "command": command } self.statechglogger.recordchgevent(myid, event) else: self.detailedresults += "Unable to get value for " + key + "\n" success = False # at the end do a print and ignore any key errors to ensure # the new values are read into the kernel self.ch.executeCommand("/sbin/sysctl -q -e -p") retcode2 = self.ch.getReturnCode() if retcode2 != 0: success = False self.detailedresults += "Failed to load new sysctl configuration from config file\n" errmsg2 = self.ch.getErrorString() self.logger.log(LogPriority.DEBUG, errmsg2) # We never found the correct contents in any of the modprobe.d files # so we're going to created the stonix-blacklist file # this file is used in other rules if not self.modprobeOK: created = False tmpfile = blacklistfile + ".tmp" modprobekveditor = KVEditorStonix(self.statechglogger, self.logger, "conf", blacklistfile, tmpfile, self.modprobes, "notpresent", "space") if not os.path.exists(blacklistfile): # create the file and record the event as file creation if createFile(blacklistfile, self.logger): created = True self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = { "eventtype": "creation", "filepath": blacklistfile } self.statechglogger.recordchgevent(myid, event) if os.path.exists(blacklistfile): if not modprobekveditor.report(): if not modprobekveditor.fix(): success = False self.detailedresults += "Unable to correct contents in " + \ blacklistfile + "\n" else: # if the file was created, then we already recorded an event # for that, so this step would get skipped if not created: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) modprobekveditor.setEventID(myid) if not modprobekveditor.commit(): success = False self.detailedresults += "Unable to correct contents in " + \ blacklistfile + "\n" # fix ifcfg (interface) files if self.ph.manager == "yum": ifacefile = "/etc/sysconfig/network-scripts/" netwrkfile = "/etc/sysconfig/network" elif self.ph.manager == "zypper": ifacefile = "/etc/sysconfig/network/" if ifacefile: if os.path.exists(ifacefile): dirs = glob.glob(ifacefile + "*") if dirs: for loc in dirs: interface = {"IPV6INIT": "no", "NETWORKING_IPV6": "no"} interface2 = { "IPV6INIT": "no", "NETWORKING_IPV6": "no" } found = False tempstring = "" if re.search('^' + ifacefile + 'ifcfg', loc): filename = loc tmpfile = filename + ".tmp" contents = readFile(filename, self.logger) if not checkPerms(filename, [0, 0, 420], self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(filename, [0, 0, 420], self.logger, self.statechglogger, myid): debug = "Unable to set permissions on " + \ filename + "\n" self.logger.log(LogPriority.DEBUG, debug) success = False for key in interface: found = False for line in contents: if re.search("^#", line) or \ re.match("^\s*$", line): continue if re.search("^" + key, line): if re.search("=", line): temp = line.split("=") if temp[1].strip( ) == interface[key]: if found: continue found = True else: contents.remove(line) if found: del interface2[key] for line in contents: tempstring += line tempstring += universal for key in interface2: tempstring += key + "=" + interface2[key] + \ "\n" if not writeFile(tmpfile, tempstring, self.logger): success = False debug = "Unable to write to file " + loc + "\n" self.logger.log(LogPriority.DEBUG, debug) self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {'eventtype': 'conf', 'filepath': filename} self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange( filename, tmpfile, myid) os.rename(tmpfile, filename) os.chown(filename, 0, 0) os.chmod(filename, 420) resetsecon(filename) elif not os.path.exists(ifacefile) and ifacefile != "": # will not attempt to create the interface files debug = "interface directory which holds interface \ files, doesn't exist, stonix will not attempt to make this \ directory or the files contained therein" success = False self.logger.log(LogPriority.DEBUG, debug) # fix network file if it exists if netwrkfile: if not os.path.exists(netwrkfile): if not createFile(netwrkfile, self.logger): debug = "Unable to create " + netwrkfile + "file\n" self.logger.log(LogPriority.DEBUG, debug) success = False else: if not checkPerms(netwrkfile, [0, 0, 420], self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms(netwrkfile, [0, 0, 420], self.logger, self.statechglogger, myid): debug = "Unable to set permissions on " + \ netwrkfile + "\n" self.logger.log(LogPriority.DEBUG, debug) success = False tmpfile = netwrkfile + ".tmp" self.editor2 = KVEditorStonix(self.statechglogger, self.logger, "conf", netwrkfile, tmpfile, self.interface, "present", "closedeq") if not self.editor2.report(): self.detailedresults += netwrkfile + " doesn't contain \ the correct contents\n" if self.editor2: if self.editor2.fixables: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.editor2.setEventID(myid) if not self.editor2.fix(): success = False debug = "Unable to complete kveditor fix method" + \ "for " + netwrkfile + "\n" self.logger.log(LogPriority.DEBUG, debug) elif not self.editor2.commit(): success = False debug = "Unable to complete kveditor commit " + \ "method for " + netwrkfile + "\n" self.logger.log(LogPriority.DEBUG, debug) os.chown(netwrkfile, 0, 0) os.chmod(netwrkfile, 420) resetsecon(netwrkfile) # fix sshd_config file for apt-get systems if ssh is installed if self.ph.manager == "apt-get": if not os.path.exists("/etc/ssh/sshd_config"): msg = "/etc/ssh/ssd_config doesn\'t exist. This could mean ssh \ is not installed or the file has been inadvertantly deleted. Due to the \ complexity of this file stonix will not attempt to create this file" self.logger.log(LogPriority.DEBUG, msg) success = False else: if not checkPerms("/etc/ssh/sshd_config", [0, 0, 420], self.logger): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) if not setPerms("/etc/ssh/sshd_config", [0, 0, 420], self.logger, self.statechglogger, myid): success = False debug = "Unable to set permissions on " + \ "/etc/ssh/sshd_config\n" self.logger.log(LogPriority.DEBUG, debug) if self.editor3: if self.editor3.fixables: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) self.editor3.setEventID(myid) if not self.editor3.fix(): success = False debug = "Unable to complete kveditor fix method" + \ "for /etc/ssh/sshd_config file\n" self.logger.log(LogPriority.DEBUG, debug) elif not self.editor3.commit(): success = False debug = "Unable to complete kveditor commit " + \ "method for /etc/ssh/sshd_config file\n" self.logger.log(LogPriority.DEBUG, debug) os.chown("/etc/ssh/sshd_config", 0, 0) os.chmod("/etc/ssh/sshd_config", 420) resetsecon("/etc/ssh/sshd_config") return success
class DisableUbuntuDataCollection(Rule): 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.environ = environ self.rulenumber = 311 self.rulename = "DisableUbuntuDataCollection" self.mandatory = True self.rootrequired = True self.formatDetailedResults("initialize") self.guidance = [] self.applicable = {'type': 'white', 'os': {'Ubuntu': ['16.04', '+']}} self.sethelptext() datatype = "bool" key = "DISABLEUBUNTUDATACOLLECTION" instructions = """To prevent the diabling of data collection from this system, set the value of DISABLEUBUNTUDATACOLLECTION to False.""" default = True self.enabledCI = self.initCi(datatype, key, instructions, default) def report(self): '''Check for the existence of any of a number of data-collection and reporting utilities on the system report compliance status as not compliant if any exist report compliance status as compliant if none exist :returns: self.compliant :rtype: bool @author: Breen Malmberg ''' self.detailedresults = "" self.ph = Pkghelper(self.logger, self.environ) self.compliant = True self.pkgslist = ["popularity-contest", "apport", "ubuntu-report"] self.removepkgs = [] try: for pkg in self.pkgslist: if self.ph.check(pkg): self.compliant = False self.detailedresults += "\nData collection utility: " + str( pkg) + " is still installed" self.removepkgs.append(pkg) 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): '''Remove any data-collection and reporting utilities from the system report success status as True if all are removed report success status as False if any remain :returns: self.rulesuccess :rtype: bool @author: Breen Malmberg ''' self.detailedresults = "" self.rulesuccess = True self.iditerator = 0 try: if self.enabledCI.getcurrvalue(): eventlist = self.statechglogger.findrulechanges( self.rulenumber) for event in eventlist: self.statechglogger.deleteentry(event) for pkg in self.removepkgs: if not self.ph.remove(pkg): self.detailedresults += "\nUnable to remove package: " + str( pkg) self.rulesuccess = False else: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) comm = self.ph.getRemove() + pkg event = {"eventtype": "commandstring", "command": comm} self.statechglogger.recordchgevent(myid, event) self.logger.log(LogPriority.DEBUG, "Removing package: " + str(pkg)) else: self.logger.log(LogPriority.DEBUG, "Rule was NOT enabled. Nothing was done.") 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 SecureSNMP(Rule): '''classdocs''' def __init__(self, config, environ, logger, statechglogger): ''' Constructor ''' Rule.__init__(self, config, environ, logger, statechglogger) self.logger = logger self.statechglogger = statechglogger self.rulenumber = 144 self.rulename = 'SecureSNMP' self.formatDetailedResults("initialize") self.mandatory = True self.sethelptext() self.rootrequired = True self.guidance = ['NSA 3.20', 'CCE 4540-1'] self.applicable = {'type': 'white', 'family': ['linux', 'solaris', 'freebsd'], 'os': {'Mac OS X': ['10.15', 'r', '10.15.10']}} datatype = 'bool' key = 'DISABLESNMP' instructions = "If there is a mission-critical need for hosts at" + \ "this site to be remotely monitored by a SNMP " + \ "tool, then prevent the disabling and removal " + \ "of SNMP by setting the value of DisableSNMP " + \ "to False." default = True self.disablesnmp = self.initCi(datatype, key, instructions, default) datatype2 = 'bool' key2 = 'CONFIGURESNMP' instructions2 = "To configure SNMP on this system, make sure " + \ "you have the value for DisableSNMP set to " + \ "False, and set the value of ConfigureSNMP to True." default2 = True self.configuresnmp = self.initCi(datatype2, key2, instructions2, default2) self.snmpdconflocations = ['/etc/snmp/conf/snmpd.conf', '/etc/snmp/conf/snmp.conf', '/etc/snmp/snmpd.conf', '/etc/snmp/snmp.conf'] self.snmpv3directives = {'defContext': 'none', 'defVersion': '3', 'defAuthType': 'SHA', 'defSecurityLevel': 'authNoPriv'} # add any other possible snmp configuration file paths from the environment # variable SNMPCONFPATH # .get does not throw keyerror but instead returns None if env doesn't exist snmpconfpathstring = os.environ.get('SNMPCONFPATH') if snmpconfpathstring: snmpconfpathlist = snmpconfpathstring.split() for path in snmpconfpathlist: path = str(path).strip() self.snmpdconflocations.append(path) def report(self): '''Determine which report method(s) to run and run them :returns: bool @author bemalmbe ''' # defaults self.detailedresults = "" try: if self.environ.getostype() == 'Mac OS X': self.compliant = self.reportmac() self.formatDetailedResults("report", self.compliant, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.compliant compliant = True self.svchelper = ServiceHelper(self.environ, self.logger) self.pkghelper = Pkghelper(self.logger, self.environ) if self.disablesnmp.getcurrvalue(): if not self.reportDisableSNMP(): compliant = False if self.configuresnmp.getcurrvalue(): if not self.reportConfigureSNMP(): compliant = False self.compliant = compliant except AttributeError: self.detailedresults = traceback.format_exc() self.logger.log(LogPriority.DEBUG, self.detailedresults) except (KeyboardInterrupt, SystemExit): raise except Exception as err: self.rulesuccess = False self.detailedresults = self.detailedresults + "\n" + str(err) + \ " - " + 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 reportmac(self): '''@author: Breen Malmberg''' configured = True try: self.cmdhelper = CommandHelper(self.logger) filepath = '/System/Library/LaunchDaemons/org.net-snmp.snmpd.plist' if not os.path.exists(filepath): configured = False self.detailedresults += '\nrequired plist configuration file not found: ' + filepath cmd = '/usr/bin/defaults read ' + filepath + ' Disabled' self.cmdhelper.executeCommand(cmd) output = self.cmdhelper.getOutputString() errout = self.cmdhelper.getErrorString() if errout: configured = False self.detailedresults += '\nunable to execute defaults read command, or \"Disabled\" key does not exist' else: if not re.search('^1', output): configured = False self.detailedresults += '\nsnmpd is not yet disabled' except Exception: raise return configured def reportDisableSNMP(self): '''Determine whether SNMP service is disabled and uninstalled :returns: bool @author bemalmbe ''' # defaults secure = False svcenabled = False pkginstalled = False try: svcenabled = self.svchelper.auditService('snmpd', _="_") pkginstalled = self.pkghelper.check('net-snmpd') if not svcenabled and not pkginstalled: secure = True return secure except AttributeError: self.detailedresults = traceback.format_exc() self.logger.log(LogPriority.DEBUG, self.detailedresults) except (KeyboardInterrupt, SystemExit): raise except Exception: self.detailedresults = traceback.format_exc() self.logger.log(LogPriority.ERROR, self.detailedresults) return False def reportConfigureSNMP(self): '''Determine whether the SNMP service is securely configured :returns: bool @author bemalmbe ''' # defaults kvintent = 'present' kvconftype = 'conf' kvtype = 'space' secure = True # check to make sure perms on conf files are 640 # check to make sure ownership of conf files is root:root # check to make sure conf files are not using weak or default community # string and that they are not using anything other than version 3 # security model as per NSA guidance try: if self.reportDisableSNMP(): return True for location in self.snmpdconflocations: if os.path.exists(location): perms = getOctalPerms(location) if perms != 640: secure = False kvpath = location kvtmppath = location + '.stonixtmp' self.kvosnmp = KVEditorStonix(self.statechglogger, self.logger, kvtype, kvpath, kvtmppath, self.snmpv3directives, kvintent, kvconftype) kvosnmpretval = self.kvosnmp.report() if not kvosnmpretval: secure = False f = open(location, 'r') contentlines = f.readlines() f.close() for line in contentlines: if re.search('^group', line): line = line.split() if line[2] in ['v1', 'v2', 'v2c']: secure = False self.detailedresults += '''You are currently using an outdated security model for your SNMP configuration. Please update to model 3.''' for line in contentlines: if re.search('^com2sec', line): line = line.split() if line[3] in ['public', 'community']: secure = False self.detailedresults += '''You are currently using a default or weak community string.''' for line in contentlines: if re.search('^access', line): line = line.split() if line[3] in ['any', 'v1', 'v2', 'v2c']: secure = False self.detailedresults += '''You are currently using an outdated security model for your SNMP configuration. Please update to model 3.''' if line[4] == 'noauth': secure = False self.detailedresults += '''You are currently not requiring authentication for SNMP. This is an unsecure practice. Please change to authNoPriv or authPriv.''' return secure except (IndexError, OSError): self.detailedresults = traceback.format_exc() self.logger.log(LogPriority.DEBUG, self.detailedresults) except (KeyboardInterrupt, SystemExit): raise except Exception: self.detailedresults = traceback.format_exc() self.logger.log(LogPriority.ERROR, self.detailedresults) return False def fix(self): '''Determine which fix method(s) to run and run them @author bemalmbe ''' self.detailedresults = "" self.rulesuccess = True try: if self.environ.getostype() == 'Mac OS X': if self.disablesnmp.getcurrvalue() or self.configuresnmp.getcurrvalue(): self.rulesuccess = self.fixmac() self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) self.logdispatch.log(LogPriority.INFO, self.detailedresults) return self.rulesuccess if self.disablesnmp.getcurrvalue(): self.fixDisableSNMP() if self.configuresnmp.getcurrvalue(): self.fixConfigureSNMP() except AttributeError: self.detailedresults = traceback.format_exc() self.logger.log(LogPriority.DEBUG, self.detailedresults) except (KeyboardInterrupt, SystemExit): raise except Exception as err: self.detailedresults = self.detailedresults + "\n" + str(err) + \ " - " + 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 def fixmac(self): '''@author: Breen Malmberg''' success = True try: defaults = '/usr/bin/defaults ' operation = 'write ' filepath = '/System/Library/LaunchDaemons/org.net-snmp.snmpd.plist ' key = 'DISABLED' val = ' -bool true' cmd = defaults + operation + filepath + key + val self.cmdhelper.executeCommand(cmd) errout = self.cmdhelper.getErrorString() if errout: success = False self.detailedresults += '\ncould not set Disabled key to true in org.net-snmp.snmpd.plist' except Exception: raise return success def fixDisableSNMP(self): '''Disable the SNMP service and uninstall the package for it @author bemalmbe ''' try: if not self.reportDisableSNMP(): self.svchelper.disableService('snmpd', _="_") self.pkghelper.remove('net-snmpd') except AttributeError: self.detailedresults = traceback.format_exc() self.logger.log(LogPriority.DEBUG, self.detailedresults) except (KeyboardInterrupt, SystemExit): raise except Exception: raise def fixConfigureSNMP(self): '''Securely configure the SNMP service. This option should be used instead of disabling SNMP only if there is a mission-critical need for the SNMP service to operate in the environment. @author bemalmbe ''' # set auth type to SHA, security model version to 3, and security level to # authNoPriv set permissions for the SNMP conf file to 640 # change owner and group of the SNMP conf file to root and root # admin must set up security on version 3 themselves because it is # account-based security and they must set up their own account(s) try: myid = '0144001' self.kvosnmp.setEventID(myid) self.kvosnmp.fix() self.kvosnmp.commit() for location in self.snmpdconflocations: if os.path.exists(location): os.chmod(location, 0o640) os.chown(location, 0, 0) except (KeyError, OSError): self.detailedresults = traceback.format_exc() self.logger.log(LogPriority.DEBUG, self.detailedresults) except (KeyboardInterrupt, SystemExit): raise except Exception: raise
class DisableGUILogon(Rule): def __init__(self, config, environ, logger, statechglogger): Rule.__init__(self, config, environ, logger, statechglogger) self.logger = logger self.rulenumber = 105 self.rulename = "DisableGUILogon" self.formatDetailedResults("initialize") self.mandatory = False self.applicable = {'type': 'white', 'family': ['linux']} # Configuration item instantiation datatype = "bool" key = "DISABLEX" instructions = "To enable this item, set the value of DISABLEX " + \ "to True. When enabled, this rule will disable the automatic " + \ "GUI login, and the system will instead boot to the console " + \ "(runlevel 3). This will not remove any GUI components, and the " + \ "GUI can still be started using the \"startx\" command." default = False self.ci1 = self.initCi(datatype, key, instructions, default) datatype = "bool" key = "LOCKDOWNX" instructions = "To enable this item, set the value of LOCKDOWNX " + \ "to True. When enabled, this item will help secure X Windows by " + \ "disabling the X Font Server (xfs) service and disabling X " + \ "Window System Listening. This item should be enabled if X " + \ "Windows is disabled but will be occasionally started via " + \ "startx, unless there is a mission-critical need for xfs or " + \ "a remote display." default = False self.ci2 = self.initCi(datatype, key, instructions, default) datatype = "bool" key = "REMOVEX" instructions = "To enable this item, set the value of REMOVEX " + \ "to True. When enabled, this item will COMPLETELY remove X " + \ "Windows from the system, and on most platforms will disable " + \ "any currently running display manager. It is therefore " + \ "recommended that this rule be run from a console session " + \ "rather than from the GUI.\nREMOVEX cannot be undone." default = False self.ci3 = self.initCi(datatype, key, instructions, default) self.guidance = ["NSA 3.6.1.1", "NSA 3.6.1.2", "NSA 3.6.1.3", "CCE 4462-8", "CCE 4422-2", "CCE 4448-7", "CCE 4074-1"] self.iditerator = 0 self.ph = Pkghelper(self.logger, self.environ) self.ch = CommandHelper(self.logger) self.sh = ServiceHelper(self.environ, self.logger) self.myos = self.environ.getostype().lower() self.sethelptext() def report(self): '''@author: Eric Ball :param self: essential if you override this definition :returns: bool - True if system is compliant, False if it isn't ''' try: compliant = True results = "" if os.path.exists("/bin/systemctl"): self.initver = "systemd" compliant, results = self.reportSystemd() elif re.search("debian", self.myos): self.initver = "debian" compliant, results = self.reportDebian() elif re.search("ubuntu", self.myos): self.initver = "ubuntu" compliant, results = self.reportUbuntu() else: self.initver = "inittab" compliant, results = self.reportInittab() # NSA guidance specifies disabling of X Font Server (xfs), # however, this guidance seems to be obsolete as of RHEL 6, # and does not apply to the Debian family. if self.sh.auditService("xfs", _="_"): compliant = False results += "xfs is currently enabled\n" xremoved = True self.xservSecure = True if re.search("debian|ubuntu", self.myos): if self.ph.check("xserver-xorg-core"): compliant = False xremoved = False results += "Core X11 components are present\n" elif re.search("opensuse", self.myos): if self.ph.check("xorg-x11-server"): compliant = False xremoved = False results += "Core X11 components are present\n" else: if self.ph.check("xorg-x11-server-Xorg"): compliant = False xremoved = False results += "Core X11 components are present\n" # Removing X will take xserverrc with it. If X is not present, we # do not need to check for xserverrc if not xremoved: self.serverrc = "/etc/X11/xinit/xserverrc" self.xservSecure = False if os.path.exists(self.serverrc): serverrcText = readFile(self.serverrc, self.logger) if re.search("opensuse", self.myos): for line in serverrcText: reSearch = r'exec (/usr/bin/)?X \$dspnum.*\$args' if re.search(reSearch, line): self.xservSecure = True break else: for line in serverrcText: reSearch = r'^exec (/usr/bin/)?X (:0 )?-nolisten tcp ("$@"|.?\$@)' if re.search(reSearch, line): self.xservSecure = True break if not self.xservSecure: compliant = False results += self.serverrc + " does not contain proper " \ + "settings to disable X Window System " + \ "Listening/remote display\n" else: compliant = False results += self.serverrc + " does not exist; X Window " + \ "System Listening/remote display has not " + \ "been disabled\n" self.detailedresults = results self.compliant = compliant 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 reportInittab(self): compliant = False results = "" inittab = "/etc/inittab" if os.path.exists(inittab): initData = readFile(inittab, self.logger) for line in initData: if line.strip() == "id:3:initdefault:": compliant = True break else: self.logger.log(LogPriority.ERROR, inittab + " not found, init " + "system unknown") if not compliant: results = "inittab not set to runlevel 3; GUI logon is enabled\n" return compliant, results def reportSystemd(self): compliant = True results = "" cmd = ["/bin/systemctl", "get-default"] self.ch.executeCommand(cmd) defaultTarget = self.ch.getOutputString() if not re.search("multi-user.target", defaultTarget): compliant = False results = "systemd default target is not multi-user.target; " + \ "GUI logon is enabled\n" return compliant, results def reportDebian(self): compliant = True results = "" dmlist = ["gdm", "gdm3", "lightdm", "xdm", "kdm"] for dm in dmlist: if self.sh.auditService(dm, _="_"): compliant = False results = dm + \ " is still in init folders; GUI logon is enabled\n" return compliant, results def reportUbuntu(self): compliant = True results = "" ldmover = "/etc/init/lightdm.override" grub = "/etc/default/grub" if os.path.exists(ldmover): lightdmText = readFile(ldmover, self.logger) if not re.search("manual", lightdmText[0], re.IGNORECASE): compliant = False results += ldmover + ' exists, but does not contain text ' + \ '"manual". GUI logon is still enabled\n' else: compliant = False results += ldmover + " does not exist; GUI logon is enabled\n" if os.path.exists(grub): tmppath = grub + ".tmp" data = {"GRUB_CMDLINE_LINUX_DEFAULT": '"quiet"'} editor = KVEditorStonix(self.statechglogger, self.logger, "conf", grub, tmppath, data, "present", "closedeq") if not editor.report(): compliant = False results += grub + " does not contain the correct values: " + \ str(data) else: compliant = False results += "Cannot find file " + grub if not compliant: results += "/etc/init does not contain proper override file " + \ "for lightdm; GUI logon is enabled\n" return compliant, results def fix(self): '''@author: Eric Ball :param self: essential if you override this definition :returns: bool - True if fix is successful, False if it isn't ''' try: if not self.ci1.getcurrvalue() and not self.ci2.getcurrvalue() \ and not self.ci3.getcurrvalue(): return success = True self.detailedresults = "" # Delete past state change records from previous fix self.iditerator = 0 eventlist = self.statechglogger.findrulechanges(self.rulenumber) for event in eventlist: self.statechglogger.deleteentry(event) # If we are doing DISABLEX or REMOVEX, we want to boot to # non-graphical multi-user mode. if self.ci1.getcurrvalue() or self.ci3.getcurrvalue(): success &= self.fixBootMode() # Since LOCKDOWNX depends on having X installed, and REMOVEX # completely removes X from the system, LOCKDOWNX fix will only be # executed if REMOVEX is not. if self.ci3.getcurrvalue(): success &= self.fixRemoveX() elif self.ci2.getcurrvalue(): success &= self.fixLockdownX() self.rulesuccess = success 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 fixBootMode(self): success = True if self.initver == "systemd": cmd = ["/bin/systemctl", "set-default", "multi-user.target"] if not self.ch.executeCommand(cmd): success = False self.detailedresults += '"systemctl set-default ' \ + 'multi-user.target" did not succeed\n' else: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) commandstring = "/bin/systemctl set-default " + \ "graphical.target" event = {"eventtype": "commandstring", "command": commandstring} self.statechglogger.recordchgevent(myid, event) elif self.initver == "debian": dmlist = ["gdm", "gdm3", "lightdm", "xdm", "kdm"] for dm in dmlist: cmd = ["update-rc.d", "-f", dm, "disable"] if not self.ch.executeCommand(cmd): self.detailedresults += "Failed to disable desktop " + \ "manager " + dm else: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "servicehelper", "servicename": dm, "startstate": "enabled", "endstate": "disabled"} self.statechglogger.recordchgevent(myid, event) elif self.initver == "ubuntu": ldmover = "/etc/init/lightdm.override" tmpfile = ldmover + ".tmp" created = False if not os.path.exists(ldmover): createFile(ldmover, self.logger) self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "creation", "filepath": ldmover} self.statechglogger.recordchgevent(myid, event) created = True writeFile(tmpfile, "manual\n", self.logger) if not created: self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "conf", "filepath": ldmover} self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange(ldmover, tmpfile, myid) os.rename(tmpfile, ldmover) resetsecon(ldmover) grub = "/etc/default/grub" if not os.path.exists(grub): createFile(grub, self.logger) self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "creation", "filepath": grub} self.statechglogger.recordchgevent(myid, event) tmppath = grub + ".tmp" data = {"GRUB_CMDLINE_LINUX_DEFAULT": '"quiet"'} editor = KVEditorStonix(self.statechglogger, self.logger, "conf", grub, tmppath, data, "present", "closedeq") editor.report() if editor.fixables: if editor.fix(): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) editor.setEventID(myid) debug = "kveditor fix ran successfully\n" self.logger.log(LogPriority.DEBUG, debug) if editor.commit(): debug = "kveditor commit ran successfully\n" self.logger.log(LogPriority.DEBUG, debug) else: error = "kveditor commit did not run " + \ "successfully\n" self.logger.log(LogPriority.ERROR, error) success = False else: error = "kveditor fix did not run successfully\n" self.logger.log(LogPriority.ERROR, error) success = False cmd = "update-grub" self.ch.executeCommand(cmd) else: inittab = "/etc/inittab" tmpfile = inittab + ".tmp" if os.path.exists(inittab): initText = open(inittab, "r").read() initre = r"id:\d:initdefault:" if re.search(initre, initText): initText = re.sub(initre, "id:3:initdefault:", initText) writeFile(tmpfile, initText, self.logger) self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "conf", "filepath": inittab} self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange(inittab, tmpfile, myid) os.rename(tmpfile, inittab) resetsecon(inittab) else: initText += "\nid:3:initdefault:\n" writeFile(tmpfile, initText, self.logger) self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "conf", "filepath": inittab} self.statechglogger.recordchgevent(myid, event) self.statechglogger.recordfilechange(inittab, tmpfile, myid) os.rename(tmpfile, inittab) resetsecon(inittab) else: self.detailedresults += inittab + " not found, no other " + \ "init system found. If you are using a supported " + \ "Linux OS, please report this as a bug\n" return success def fixRemoveX(self): success = True # Due to automatic removal of dependent packages, the full # removal of X and related packages cannot be undone if re.search("opensuse", self.myos): cmd = ["zypper", "-n", "rm", "-u", "xorg-x11*", "kde*", "xinit*"] self.ch.executeCommand(cmd) elif re.search("debian|ubuntu", self.myos): xpkgs = ["unity.*", "xserver-xorg-video-ati", "xserver-xorg-input-synaptics", "xserver-xorg-input-wacom", "xserver-xorg-core", "xserver-xorg", "lightdm.*", "libx11-data"] for xpkg in xpkgs: self.ph.remove(xpkg) elif re.search("fedora", self.myos): # Fedora does not use the same group packages as other # RHEL-based OSs. Removing this package will remove the X # Windows system, just less efficiently than using a group self.ph.remove("xorg-x11-server-Xorg") self.ph.remove("xorg-x11-xinit*") else: cmd = ["yum", "groups", "mark", "convert"] self.ch.executeCommand(cmd) self.ph.remove("xorg-x11-xinit") self.ph.remove("xorg-x11-server-Xorg") cmd2 = ["yum", "groupremove", "-y", "X Window System"] if not self.ch.executeCommand(cmd2): success = False self.detailedresults += '"yum groupremove -y X Window System" command failed\n' return success def fixLockdownX(self): success = True if self.sh.disableService("xfs", _="_"): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "servicehelper", "servicename": "xfs", "startstate": "enabled", "endstate": "disabled"} self.statechglogger.recordchgevent(myid, event) else: success = False self.detailedresults += "STONIX was unable to disable the " + \ "xfs service\n" if not self.xservSecure: serverrcString = "exec X :0 -nolisten tcp $@" if not os.path.exists(self.serverrc): createFile(self.serverrc, self.logger) self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = {"eventtype": "creation", "filepath": self.serverrc} self.statechglogger.recordchgevent(myid, event) writeFile(self.serverrc, serverrcString, self.logger) else: open(self.serverrc, "a").write(serverrcString) return success
class RemoveSUIDGames(Rule): def __init__(self, config, enviro, logger, statechglogger): Rule.__init__(self, config, enviro, logger, statechglogger) self.logger = logger self.rulenumber = 244 self.rulename = "RemoveSUIDGames" self.formatDetailedResults("initialize") self.mandatory = True self.sethelptext() self.applicable = {'type': 'white', 'family': 'linux'} # Configuration item instantiation datatype = "bool" key = "REMOVESUIDGAMES" instructions = "To disable this rule, set the value of " + \ "REMOVESUIDGAMES to false." default = True self.ci = self.initCi(datatype, key, instructions, default) self.guidance = ["LANL 15.7"] self.iditerator = 0 self.ph = Pkghelper(self.logger, self.environ) self.gamelist = [ 'atlantik', 'bomber', 'bovo', 'gnuchess', 'kapman', 'kasteroids', 'katomic', 'kbackgammon', 'kbattleship', 'kblackbox', 'kblocks', 'kbounce', 'kbreakout', 'kdiamond', 'kenolaba', 'kfouleggs', 'kfourinline', 'kgoldrunner', 'killbots', 'kiriki', 'kjumpingcube', 'klickety', 'klines', 'kmahjongg', 'kmines', 'knetwalk', 'kolf', 'kollision', 'konquest', 'kpat', 'kpoker', 'kreversi', 'ksame', 'kshisen', 'ksirk', 'ksirkskineditor', 'ksirtet', 'ksmiletris', 'ksnake', 'kspaceduel', 'ksquares', 'ksudoku', 'ktron', 'ktuberling', 'kubrick', 'kwin4', 'kwin4proc', 'lskat', 'lskatproc', 'gnome-games', 'kdegames', 'gnome-taquin' ] def report(self): try: compliant = True self.gamesfound = [] self.gamedirsfound = [] for game in self.gamelist: if self.ph.check(game): self.gamesfound.append(game) compliant = False self.detailedresults += game + " installed on system\n" if os.path.exists("/usr/bin/" + game): self.gamedirsfound.append("/usr/bin/" + game) compliant = False self.detailedresults += "/usr/bin/" + game + "path exists\n" if (os.path.exists("/usr/games")): usrgames = os.listdir("/usr/games") if len(usrgames) > 0: compliant = False self.detailedresults += "/usr/games directory is not empty\n" 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): try: if not self.ci.getcurrvalue(): return success = True debug = "" self.iditerator = 0 eventlist = self.statechglogger.findrulechanges(self.rulenumber) for event in eventlist: self.statechglogger.deleteentry(event) for game in self.gamesfound: # pkgName = self.ph.getPackageFromFile(game) # if pkgName is not None and str(pkgName) is not "": if self.ph.remove(game): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = { "eventtype": "pkghelper", "pkgname": game, "startstate": "installed", "endstate": "removed" } self.statechglogger.recordchgevent(myid, event) else: success = False self.detailedresults += "Unable to remove " + game + "\n" for game in self.gamedirsfound: #did not put state change event recording here due to python #not sending valid return code back for success or failure #with os.remove command therefore not knowing if successful #to record event if os.path.exists(game): os.remove(game) if os.path.exists("/usr/games/"): if not self.__cleandir("/usr/games/"): success = False self.detailedresults += "Unable to remove all games from /usr/games\n" self.rulesuccess = success 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 __cleandir(self, directory, depth=0): '''Recursively finds the package name for each file in a directory, and all child directories, and uninstalls the package. Ignores links. This is best-effort; no error checking is done for the uninstall requests. @param directory: Name of the directory to search @return: False if a package name is not found for any file, True otherwise. @author: Eric Ball ''' success = True dirlist = os.listdir(directory) for path in dirlist: path = directory + path if os.path.isfile(path): pkgName = self.ph.getPackageFromFile(path) if pkgName is not None: if self.ph.remove(pkgName): self.iditerator += 1 myid = iterate(self.iditerator, self.rulenumber) event = { "eventtype": "pkghelper", "pkgname": pkgName, "startstate": "installed", "endstate": "removed" } self.statechglogger.recordchgevent(myid, event) else: debug = "Could not remove package " + pkgName self.logger.log(LogPriority.DEBUG, debug) success = False else: debug = "Could not find package name for " + path self.logger.log(LogPriority.DEBUG, debug) success = False elif os.path.isdir(path): if depth < 6: success &= self.__cleandir(path, depth + 1) dirlist = os.listdir(directory) if dirlist: # There is a possibility that removing some packages will result in # other packages being installed. We will attempt to remove these # additional packages, but limit this to avoid infinite loops. if depth < 6: success &= self.__cleandir(directory, depth + 1) return success
class RemoveSoftware(Rule): """This class removes any unnecessary software installed on the system""" def __init__(self, config, environ, logger, statechglogger): """ Constructor """ Rule.__init__(self, config, environ, logger, statechglogger) self.logger = logger self.rulenumber = 91 self.rulename = "RemoveSoftware" self.mandatory = True self.formatDetailedResults("initialize") self.sethelptext() self.rootrequired = True self.guidance = ["NSA 2.3.5.6"] self.applicable = {'type': 'white', 'family': ['linux', 'freebsd']} # Configuration item instantiation datatype1 = "bool" key1 = "REMOVESOFTWARE" instructions1 = "To disable this rule set the value of REMOVESOFTWARE TO False." default1 = False self.ci = self.initCi(datatype1, key1, instructions1, default1) datatype2 = "list" key2 = "PACKAGES" instructions2 = "Enter the package(s) that you wish to remove. By " + \ "default we already list packages that we recommend for removal." default2 = [ "squid", "telnet-server", "rsh-server", "rsh", "rsh-client", "talk", "talk-server", "talkd", "libpam-ccreds", "pam_ccreds", "tftp-server", "tftp", "tftpd", "udhcpd", "dhcpd", "dhcp", "dhcp-server", "yast2-dhcp-server", "vsftpd", "httpd", "dovecot", "dovecot-imapd", "dovecot-pop3d", "snmpd", "net-snmpd", "net-snmp", "ipsec-tools", "irda-utils", "slapd", "openldap-servers", "openldap2", "bind9", "bind9.i386", "bind", "dnsutils", "bind-utils", "redhat-access-insights", "insights-client" ] self.pkgci = self.initCi(datatype2, key2, instructions2, default2) def report(self): """ report whether any of the packages listed in the self.pkgci ci are installed return False if any are installed return True if none of them are installed :returns: self.compliant :rtype: bool """ self.detailedresults = "" self.compliant = True self.ph = Pkghelper(self.logger, self.environ) packages = self.pkgci.getcurrvalue() self.remove_packages = [] try: if packages: for pkg in packages: if self.ph.check(pkg): self.remove_packages.append(pkg) self.detailedresults += pkg + " is installed\n" self.compliant = False except Exception: self.compliant = False self.detailedresults += 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): """ remove all software packages listed in the self.pkgci list :returns: self.rulesuccess :rtype: bool """ self.rulesuccess = True self.detailedresults = "" self.iditerator = 0 enabled = self.ci.getcurrvalue() try: if not enabled: self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) return self.rulesuccess elif not self.remove_packages: self.formatDetailedResults("fix", self.rulesuccess, self.detailedresults) 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) for pkg in self.remove_packages: try: if self.ph.remove(pkg): self.iditerator += 1 self.detailedresults += "\nRemoved package: " + str( pkg) myid = iterate(self.iditerator, self.rulenumber) event = { "eventtype": "pkghelper", "pkgname": pkg, "startstate": "installed", "endstate": "removed" } self.statechglogger.recordchgevent(myid, event) else: self.rulesuccess = False self.detailedresults += "\nFailed to remove package: " + str( pkg) except Exception: continue except (KeyboardInterrupt, SystemExit): raise except Exception: self.rulesuccess = False self.detailedresults += 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