ft@0: #!/usr/bin/python ft@0: ft@0: from fuse import Fuse ft@0: import fuse ft@0: ft@0: import ConfigParser ft@0: ft@0: import sys ft@0: ft@0: import logging ft@0: import os ft@0: import errno ck@7: import time ft@0: ft@11: from importlib import import_module ft@11: ft@11: ft@0: import subprocess ft@0: ck@5: import urllib3 ft@8: import netifaces ft@8: import netaddr ft@15: import hashlib ck@1: ck@1: ft@11: sys.stderr = open('/var/log/osecfs_error.log', 'a+') ft@11: ft@11: ft@11: MINOPTS = { "Main" : ["Logfile", "LogLevel", "Mountpoint", "Rootpath", "ScannerPath", "ScannerModuleName", "ScannerClassName", "ScannerConfig", "ReadOnly"]} ft@0: ft@0: CONFIG_NOT_READABLE = "Configfile is not readable" ft@0: CONFIG_WRONG = "Something is wrong with the config" ft@0: CONFIG_MISSING = "Section: \"%s\" Option: \"%s\" in configfile is missing" ft@11: SCAN_WRONG_RETURN_VALUE = "The return Value of the malware scanner is wrong. Has to be an dictionary" ft@11: SCAN_RETURN_VALUE_KEY_MISSING = "The dictionary has to include key \"infected\" (True, False) and \"virusname\" (String)" ft@11: VIRUS_FOUND = "Virus found. Access denied" ft@11: NOTIFICATION_CRITICAL = "critical" ft@11: NOTIFICATION_INFO = "info" ft@0: LOG = None ft@11: MalwareScanner = None ft@13: STATUS_CODE_OK = 200 ft@0: ft@0: SYSTEM_FILE_COMMAND = "file" ck@7: httpPool = urllib3.PoolManager(num_pools = 1, timeout = 3) ft@0: ft@0: def checkMinimumOptions (config): ft@0: for section, options in MINOPTS.iteritems (): ft@0: for option in options: ft@0: if (config.has_option(section, option) == False): ft@0: print (CONFIG_MISSING % (section, option)) ft@0: exit (129) ft@0: ft@0: def printUsage (): ft@0: print ("Usage:") ft@3: print ("%s configfile mountpath ro/rw" % (sys.argv[0])) ft@0: exit (128) ft@0: ft@0: def loadConfig (): ft@0: print ("load config") ft@0: ft@3: if (len (sys.argv) < 4): ft@0: printUsage () ft@0: ft@0: configfile = sys.argv[1] ft@0: config = ConfigParser.SafeConfigParser () ft@0: ft@0: if ((os.path.exists (configfile) == False) or (os.path.isfile (configfile) == False) or (os.access (configfile, os.R_OK) == False)): ft@0: print (CONFIG_NOT_READABLE) ft@0: printUsage () ft@0: ft@0: try: ft@0: config.read (sys.argv[1]) ft@0: except Exception, e: ft@0: print (CONFIG_WRONG) ft@0: print ("Error: %s" % (e)) ft@0: ft@3: ft@3: config.set("Main", "Mountpoint", sys.argv[2]) ft@3: if (sys.argv[3] == "rw"): ft@3: config.set("Main", "ReadOnly", "false") ft@3: else: ft@3: config.set("Main", "ReadOnly", "true") ft@3: ft@0: checkMinimumOptions (config) ft@0: ft@0: return config ft@0: ft@0: def initLog (config): ft@0: print ("init log") ft@0: ft@0: global LOG ft@0: logfile = config.get("Main", "Logfile") ft@11: ft@11: numeric_level = getattr(logging, config.get("Main", "LogLevel").upper(), None) ft@11: if not isinstance(numeric_level, int): ft@11: raise ValueError('Invalid log level: %s' % loglevel) ft@0: ft@0: # ToDo move log level and maybe other things to config file ft@0: logging.basicConfig( ft@11: level = numeric_level, ft@0: format = "%(asctime)s %(name)-12s %(funcName)-15s %(levelname)-8s %(message)s", ft@0: datefmt = "%Y-%m-%d %H:%M:%S", ft@0: filename = logfile, ft@0: filemode = "a+", ft@0: ) ft@0: LOG = logging.getLogger("fuse_main") ft@0: ft@0: ft@0: def fixPath (path): ft@0: return ".%s" % (path) ft@0: ft@0: def rootPath (rootpath, path): ft@0: return "%s%s" % (rootpath, path) ft@0: ft@0: def flag2mode (flags): ft@0: md = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'} ft@0: m = md[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)] ft@0: ft@14: # windows sets append even if it would overwrite the whole file (seek 0) ft@14: # so ignore append option ft@14: #if flags | os.O_APPEND: ft@14: # m = m.replace('w', 'a', 1) ft@0: ft@0: return m ft@0: ft@11: def scanFile (path, fileobject): ft@11: LOG.debug ("Scan File \"%s\" with malware Scanner" %(path,) ) ft@11: return MalwareScanner.scanFile (path, fileobject) ck@7: ck@2: ck@2: def scanFileClamAV (path): ft@0: infected = False ft@0: ft@0: LOG.debug ("Scan File: %s" % (path)) ft@0: ck@2: result = pyclamav.scanfile (path) ft@0: LOG.debug ("Result of file \"%s\": %s" % (path, result)) ck@2: if (result[0] != 0): ft@0: infected = True ft@0: ft@0: if (infected == True): ck@2: LOG.error ("Virus found, deny Access %s" % (result,)) ft@0: ft@0: return infected ft@0: ft@0: def whitelistFile (path): ft@0: whitelisted = False; ft@0: ft@0: LOG.debug ("Execute \"%s\" command on \"%s\"" %(SYSTEM_FILE_COMMAND, path)) ft@0: ft@0: result = None ft@0: try: ft@0: result = subprocess.check_output ([SYSTEM_FILE_COMMAND, path]); ft@0: # ToDo replace with real whitelist ft@0: whitelisted = True ft@0: except Exception as e: ft@0: LOG.error ("Call returns with an error!") ft@0: LOG.error (e) ft@0: ft@0: LOG.debug ("Type: %s" %(result)) ft@0: ft@0: return whitelisted ft@0: ft@15: def sendDataToRest (urlpath, data): ft@8: netifaces.ifaddresses("eth0")[2][0]["addr"] ft@8: ft@8: # Get first address in network (0 = network ip -> 192.168.0.0) ft@8: remote_ip = netaddr.IPNetwork("%s/%s" %(netifaces.ifaddresses("eth0")[2][0]["addr"], netifaces.ifaddresses("eth0")[2][0]["netmask"]))[1] ft@8: ft@15: url = ("http://%s:8090//%s" %(remote_ip, urlpath)) ft@15: ft@15: LOG.debug ("Send data to \"%s\"" %(url, )) ft@15: LOG.debug ("Data: %s" %(data, )) ft@8: ft@8: try: ft@15: response = httpPool.request_encode_body("POST", url, fields=data, retries=0) ft@16: except Exception, e: ft@8: LOG.error("Remote host not reachable") ft@16: LOG.error ("Exception: %s" %(e,)) ft@8: return ft@8: ft@8: if response.status == STATUS_CODE_OK: ft@15: LOG.info("Data sent successfully to rest server") ft@15: return True ft@8: else: ft@8: LOG.error("Server returned errorcode: %s" %(response.status,)) ft@15: return False ft@15: ft@15: ft@15: def sendNotification (type, message): ft@15: data = {"type" : type, "message" : message} ft@15: sendDataToRest ("notification", data) ft@8: ft@9: def sendReadOnlyNotification(): ft@9: sendNotification("critical", "Filesystem is in read only mode. If you want to export files please initialize an encrypted filesystem.") ft@15: ft@15: def sendLogNotPossibleNotification(): ft@15: sendNotification ("critical", "Send log entry to opensecurity rest server failed.") ft@15: ft@15: def sendFileLog(filename, filesize, filehash, hashtype): ft@16: data = {"filename" : filename, "filesize" : "%s" %(filesize,), "filehash" : filehash, "hashtype" : hashtype} ft@15: retval = sendDataToRest ("log", data) ft@15: if (retval == False): ft@15: sendLogNotPossibleNotification() ft@15: ft@15: def calcMD5 (path, block_size=256*128, hr=True): ft@15: md5 = hashlib.md5() ft@15: with open(path,'rb') as f: ft@15: for chunk in iter(lambda: f.read(block_size), b''): ft@15: md5.update(chunk) ft@15: if hr: ft@15: return md5.hexdigest() ft@15: return md5.digest() ft@9: ft@0: class OsecFS (Fuse): ft@0: ft@0: __rootpath = None ft@0: ft@0: # default fuse init ft@0: def __init__(self, rootpath, *args, **kw): ft@0: self.__rootpath = rootpath ft@0: Fuse.__init__ (self, *args, **kw) ft@0: LOG.debug ("Init complete.") ft@9: sendNotification("information", "Filesystem successfully mounted.") ft@0: ft@0: # defines that our working directory will be the __rootpath ft@0: def fsinit(self): ft@0: os.chdir (self.__rootpath) ft@0: ft@0: def getattr(self, path): ft@0: LOG.debug ("*** getattr (%s)" % (fixPath (path))) ft@0: return os.lstat (fixPath (path)); ft@0: ft@0: def getdir(self, path): ft@0: LOG.debug ("*** getdir (%s)" % (path)); ft@0: return os.listdir (fixPath (path)) ft@0: ft@0: def readdir(self, path, offset): ft@0: LOG.debug ("*** readdir (%s %s)" % (path, offset)); ft@0: for e in os.listdir (fixPath (path)): ft@0: yield fuse.Direntry(e) ft@0: ft@0: def chmod (self, path, mode): ft@0: LOG.debug ("*** chmod %s %s" % (path, oct(mode))) ft@3: if (config.get("Main", "ReadOnly") == "true"): ft@9: sendReadOnlyNotification() ft@3: return -errno.EACCES ft@0: os.chmod (fixPath (path), mode) ft@0: ft@0: def chown (self, path, uid, gid): ft@0: LOG.debug ("*** chown %s %s %s" % (path, uid, gid)) ft@3: if (config.get("Main", "ReadOnly") == "true"): ft@9: sendReadOnlyNotification() ft@3: return -errno.EACCES ft@0: os.chown (fixPath (path), uid, gid) ft@0: ft@0: def link (self, targetPath, linkPath): ft@0: LOG.debug ("*** link %s %s" % (targetPath, linkPath)) ft@3: if (config.get("Main", "ReadOnly") == "true"): ft@9: sendReadOnlyNotification() ft@3: return -errno.EACCES ft@0: os.link (fixPath (targetPath), fixPath (linkPath)) ft@0: ft@0: def mkdir (self, path, mode): ft@0: LOG.debug ("*** mkdir %s %s" % (path, oct(mode))) ft@3: if (config.get("Main", "ReadOnly") == "true"): ft@9: sendReadOnlyNotification() ft@3: return -errno.EACCES ft@0: os.mkdir (fixPath (path), mode) ft@0: ft@0: def mknod (self, path, mode, dev): ft@0: LOG.debug ("*** mknod %s %s %s" % (path, oct (mode), dev)) ft@3: if (config.get("Main", "ReadOnly") == "true"): ft@9: sendReadOnlyNotification() ft@3: return -errno.EACCES ft@0: os.mknod (fixPath (path), mode, dev) ft@0: ft@0: # to implement virus scan ft@0: def open (self, path, flags): ft@0: LOG.debug ("*** open %s %s" % (path, oct (flags))) ft@0: self.file = os.fdopen (os.open (fixPath (path), flags), flag2mode (flags)) ft@15: self.written = False ft@0: self.fd = self.file.fileno () ft@11: ft@11: LOG.debug(self.__rootpath) ft@11: LOG.debug(path) ft@11: ft@11: retval = scanFile (rootPath(self.__rootpath, path), self.file) ft@11: ft@11: #if type(retval) is not dict: ft@11: if (isinstance(retval, dict) == False): ft@11: LOG.error(SCAN_WRONG_RETURN_VALUE) ft@0: self.file.close () ft@11: return -errno.EACCES ft@11: ft@11: if ((retval.has_key("infected") == False) or (retval.has_key("virusname") == False)): ft@11: LOG.error(SCAN_RETURN_VALUE_KEY_MISSING) ft@11: self.file.close () ft@11: return -errno.EACCES ft@11: ft@11: ft@11: if (retval.get("infected") == True): ft@11: self.file.close () ft@11: sendNotification(NOTIFICATION_CRITICAL, "%s\nFile: %s\nVirus: %s" %(VIRUS_FOUND, path, retval.get("virusname"))) ft@11: LOG.error("%s" %(VIRUS_FOUND,)) ft@11: LOG.error("Virus: %s" %(retval.get("virusname"),)) ft@0: return -errno.EACCES ft@0: ft@0: whitelisted = whitelistFile (rootPath(self.__rootpath, path)) ft@0: if (whitelisted == False): ft@0: self.file.close () ft@11: sendNotification(NOTIFICATION_CRITICAL, "File not in whitelist. Access denied.") ft@0: return -errno.EACCES ft@0: ft@0: def read (self, path, length, offset): ft@0: LOG.debug ("*** read %s %s %s" % (path, length, offset)) ft@0: self.file.seek (offset) ft@0: return self.file.read (length) ft@0: ft@0: def readlink (self, path): ft@0: LOG.debug ("*** readlink %s" % (path)) ft@0: return os.readlink (fixPath (path)) ft@0: ft@0: def release (self, path, flags): ft@0: LOG.debug ("*** release %s %s" % (path, oct (flags))) ft@0: self.file.close () ft@15: ft@15: if (self.written == True): ft@15: hashsum = calcMD5(fixPath(path)) ft@15: filesize = os.path.getsize(fixPath(path)) ft@15: sendFileLog(path, filesize, hashsum, "md5") ft@15: ft@0: ft@0: def rename (self, oldPath, newPath): ft@3: LOG.debug ("*** rename %s %s %s" % (oldPath, newPath, config.get("Main", "ReadOnly"))) ft@3: if (config.get("Main", "ReadOnly") == "true"): ft@9: sendReadOnlyNotification() ft@3: return -errno.EACCES ft@0: os.rename (fixPath (oldPath), fixPath (newPath)) ft@0: ft@0: def rmdir (self, path): ft@3: LOG.debug ("*** rmdir %s %s" % (path, config.get("Main", "ReadOnly"))) ft@3: if (config.get("Main", "ReadOnly") == "true"): ft@9: sendReadOnlyNotification() ft@3: return -errno.EACCES ft@0: os.rmdir (fixPath (path)) ft@0: ft@0: def statfs (self): ft@0: LOG.debug ("*** statfs") ft@0: return os.statvfs(".") ft@0: ft@0: def symlink (self, targetPath, linkPath): ft@3: LOG.debug ("*** symlink %s %s %s" % (targetPath, linkPath, config.get("Main", "ReadOnly"))) ft@3: if (config.get("Main", "ReadOnly") == "true"): ft@9: sendReadOnlyNotification() ft@3: return -errno.EACCES ft@0: os.symlink (fixPath (targetPath), fixPath (linkPath)) ft@0: ft@0: def truncate (self, path, length): ft@3: LOG.debug ("*** truncate %s %s %s" % (path, length, config.get("Main", "ReadOnly"))) ft@3: if (config.get("Main", "ReadOnly") == "true"): ft@9: sendReadOnlyNotification() ft@3: return -errno.EACCES ft@14: f = open (fixPath (path), "w+") ft@0: f.truncate (length) ft@0: f.close () ft@0: ft@0: def unlink (self, path): ft@3: LOG.debug ("*** unlink %s %s" % (path, config.get("Main", "ReadOnly"))) ft@3: if (config.get("Main", "ReadOnly") == "true"): ft@9: sendReadOnlyNotification() ft@3: return -errno.EACCES ft@0: os.unlink (fixPath (path)) ft@0: ft@0: def utime (self, path, times): ft@0: LOG.debug ("*** utime %s %s" % (path, times)) ft@0: os.utime (fixPath (path), times) ft@0: ft@0: def write (self, path, buf, offset): ft@15: #LOG.debug ("*** write %s %s %s %s" % (path, buf, offset, config.get("Main", "ReadOnly"))) ft@15: LOG.debug ("*** write %s %s %s %s" % (path, "filecontent", offset, config.get("Main", "ReadOnly"))) ft@3: if (config.get("Main", "ReadOnly") == "true"): ft@3: self.file.close() ft@9: sendReadOnlyNotification() ft@3: return -errno.EACCES ft@0: self.file.seek (offset) ft@0: self.file.write (buf) ft@15: self.written = True ft@0: return len (buf) ft@0: ft@0: def access (self, path, mode): ft@0: LOG.debug ("*** access %s %s" % (path, oct (mode))) ft@0: if not os.access (fixPath (path), mode): ft@0: return -errno.EACCES ft@0: ft@0: def create (self, path, flags, mode): ft@3: LOG.debug ("*** create %s %s %s %s %s" % (fixPath (path), oct (flags), oct (mode), flag2mode (flags), config.get("Main", "ReadOnly"))) ft@3: if (config.get("Main", "ReadOnly") == "true"): ft@9: sendReadOnlyNotification() ft@3: return -errno.EACCES ft@15: ft@15: self.file = os.fdopen (os.open (fixPath (path), flags), flag2mode(flags)) ft@15: self.written = True ft@0: self.fd = self.file.fileno () ft@0: ft@0: ft@0: if __name__ == "__main__": ft@0: # Set api version ft@0: fuse.fuse_python_api = (0, 2) ft@0: fuse.feature_assert ('stateful_files', 'has_init') ft@0: ft@0: config = loadConfig () ft@0: initLog (config) ft@8: ft@8: #sendNotification("Info", "OsecFS started") ft@11: ft@11: # Import the Malware Scanner ft@11: sys.path.append(config.get("Main", "ScannerPath")) ft@11: ft@11: MalwareModule = import_module(config.get("Main", "ScannerModuleName")) ft@11: MalwareClass = getattr(MalwareModule, config.get("Main", "ScannerClassName")) ft@11: ft@11: MalwareScanner = MalwareClass (config.get("Main", "ScannerConfig")); ck@7: ft@0: osecfs = OsecFS (config.get ("Main", "Rootpath")) ft@0: osecfs.flags = 0 ft@0: osecfs.multithreaded = 0 ft@0: ft@0: fuse_args = [sys.argv[0], config.get ("Main", "Mountpoint")]; ft@0: osecfs.main (fuse_args)