src/OsecFS.py
author ft
Thu, 27 Mar 2014 14:04:48 +0100
changeset 12 e1961a1cbb61
parent 11 dc877520743b
child 13 df3d231adebb
permissions -rwxr-xr-x
fixed windows behaviour. Rewrite fle mode from "a+" to "rw+"
     1 #!/usr/bin/python
     2 
     3 from fuse import Fuse
     4 import fuse
     5 
     6 import ConfigParser
     7 
     8 import sys
     9 
    10 import logging
    11 import os
    12 import errno
    13 import time
    14 
    15 from importlib import import_module
    16 
    17 
    18 import subprocess
    19 
    20 import urllib3
    21 import urllib
    22 import netifaces
    23 import netaddr
    24 
    25 
    26 sys.stderr = open('/var/log/osecfs_error.log', 'a+')
    27 
    28 
    29 MINOPTS = { "Main" : ["Logfile", "LogLevel", "Mountpoint", "Rootpath", "ScannerPath", "ScannerModuleName", "ScannerClassName", "ScannerConfig", "ReadOnly"]}
    30 
    31 CONFIG_NOT_READABLE = "Configfile is not readable"
    32 CONFIG_WRONG = "Something is wrong with the config"
    33 CONFIG_MISSING = "Section: \"%s\" Option: \"%s\" in configfile is missing"
    34 SCAN_WRONG_RETURN_VALUE = "The return Value of the malware scanner is wrong. Has to be an dictionary"
    35 SCAN_RETURN_VALUE_KEY_MISSING = "The dictionary has to include key \"infected\" (True, False) and \"virusname\" (String)"
    36 VIRUS_FOUND = "Virus found. Access denied"
    37 NOTIFICATION_CRITICAL = "critical"
    38 NOTIFICATION_INFO = "info"
    39 LOG = None
    40 MalwareScanner = None
    41 
    42 SYSTEM_FILE_COMMAND = "file"
    43 httpPool = urllib3.PoolManager(num_pools = 1, timeout = 3)
    44 
    45 def checkMinimumOptions (config):
    46     for section, options in MINOPTS.iteritems ():
    47         for option in options:
    48             if (config.has_option(section, option) == False):
    49                 print (CONFIG_MISSING % (section, option))
    50                 exit (129)
    51 
    52 def printUsage ():
    53     print ("Usage:")
    54     print ("%s configfile mountpath ro/rw" % (sys.argv[0]))
    55     exit (128)
    56 
    57 def loadConfig ():
    58     print ("load config")
    59 
    60     if (len (sys.argv) < 4):
    61         printUsage ()
    62 
    63     configfile = sys.argv[1]
    64     config = ConfigParser.SafeConfigParser ()
    65 
    66     if ((os.path.exists (configfile) == False) or (os.path.isfile (configfile) == False) or (os.access (configfile, os.R_OK) == False)):
    67         print (CONFIG_NOT_READABLE)
    68         printUsage ()
    69 
    70     try:
    71         config.read (sys.argv[1])
    72     except Exception, e:
    73         print (CONFIG_WRONG)
    74         print ("Error: %s" % (e))
    75 
    76 
    77     config.set("Main", "Mountpoint", sys.argv[2])
    78     if (sys.argv[3] == "rw"):
    79         config.set("Main", "ReadOnly", "false")
    80     else:
    81         config.set("Main", "ReadOnly", "true")
    82 
    83     checkMinimumOptions (config)
    84 
    85     return config
    86 
    87 def initLog (config):
    88     print ("init log")
    89 
    90     global LOG
    91     logfile = config.get("Main", "Logfile")
    92     
    93     numeric_level = getattr(logging, config.get("Main", "LogLevel").upper(), None)
    94     if not isinstance(numeric_level, int):
    95         raise ValueError('Invalid log level: %s' % loglevel)
    96 
    97     # ToDo move log level and maybe other things to config file
    98     logging.basicConfig(
    99                         level = numeric_level,
   100                         format = "%(asctime)s %(name)-12s %(funcName)-15s %(levelname)-8s %(message)s",
   101                         datefmt = "%Y-%m-%d %H:%M:%S",
   102                         filename = logfile,
   103                         filemode = "a+",
   104     )
   105     LOG = logging.getLogger("fuse_main")
   106 
   107 
   108 def fixPath (path):
   109     return ".%s" % (path)
   110 
   111 def rootPath (rootpath, path):
   112     return "%s%s" % (rootpath, path)
   113 
   114 def flag2mode (flags):
   115     md = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'}
   116     m = md[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)]
   117 
   118     if flags | os.O_APPEND:
   119         m = m.replace('w', 'a', 1)
   120 
   121     return m
   122 
   123 def scanFile (path, fileobject):
   124     LOG.debug ("Scan File \"%s\" with malware Scanner" %(path,) )
   125     return MalwareScanner.scanFile (path, fileobject)
   126 
   127 
   128 def scanFileClamAV (path):
   129     infected = False
   130 
   131     LOG.debug ("Scan File: %s" % (path))
   132 
   133     result = pyclamav.scanfile (path)
   134     LOG.debug ("Result of file \"%s\": %s" % (path, result))
   135     if (result[0] != 0):
   136         infected = True
   137 
   138     if (infected == True):
   139         LOG.error ("Virus found, deny Access %s" % (result,))
   140 
   141     return infected
   142 
   143 def whitelistFile (path):
   144     whitelisted = False;
   145 
   146     LOG.debug ("Execute \"%s\" command on \"%s\"" %(SYSTEM_FILE_COMMAND, path))
   147     
   148     result = None
   149     try:
   150         result = subprocess.check_output ([SYSTEM_FILE_COMMAND, path]);
   151         # ToDo replace with real whitelist
   152         whitelisted = True
   153     except Exception as e:
   154         LOG.error ("Call returns with an error!")
   155         LOG.error (e)
   156 
   157     LOG.debug ("Type: %s" %(result))
   158 
   159     return whitelisted
   160 
   161 def sendNotification (type, message):
   162     netifaces.ifaddresses("eth0")[2][0]["addr"]
   163     
   164     # Get first address in network (0 = network ip -> 192.168.0.0)
   165     remote_ip = netaddr.IPNetwork("%s/%s" %(netifaces.ifaddresses("eth0")[2][0]["addr"], netifaces.ifaddresses("eth0")[2][0]["netmask"]))[1]
   166     
   167     url_options = {"type" : type, "message" : message }
   168     
   169     # BUG in urllib3. Starting / is missing -> workarround use 2 of them -.- 
   170     url = ("http://%s:8090//notification?%s" %(remote_ip, urllib.urlencode(url_options)))
   171     
   172     LOG.debug ("Send notification to \"%s\"" %(url, ))
   173     
   174     try:
   175         #response = httpPool.request_encode_body('GET', url, retries = 0)
   176         response = httpPool.request("GET", url, retries = 0)
   177     except:
   178         LOG.error("Remote host not reachable")
   179         LOG.error ("Exception: %s" %(sys.exc_info()[0]))
   180         return
   181     
   182     if response.status == STATUS_CODE_OK:
   183         LOG.info("Notification sent successfully")
   184     else:
   185         LOG.error("Server returned errorcode: %s" %(response.status,))
   186 
   187 def sendReadOnlyNotification():
   188     sendNotification("critical", "Filesystem is in read only mode. If you want to export files please initialize an encrypted filesystem.")
   189 
   190 class OsecFS (Fuse):
   191 
   192     __rootpath = None
   193 
   194     # default fuse init
   195     def __init__(self, rootpath, *args, **kw):
   196         self.__rootpath = rootpath
   197         Fuse.__init__ (self, *args, **kw)
   198         LOG.debug ("Init complete.")
   199         sendNotification("information", "Filesystem successfully mounted.")
   200 
   201     # defines that our working directory will be the __rootpath
   202     def fsinit(self):
   203         os.chdir (self.__rootpath)
   204 
   205     def getattr(self, path):
   206         LOG.debug ("*** getattr (%s)" % (fixPath (path)))
   207         return os.lstat (fixPath (path));
   208 
   209     def getdir(self, path):
   210         LOG.debug ("*** getdir (%s)" % (path));
   211         return os.listdir (fixPath (path))
   212 
   213     def readdir(self, path, offset):
   214         LOG.debug ("*** readdir (%s %s)" % (path, offset));
   215         for e in os.listdir (fixPath (path)):
   216             yield fuse.Direntry(e)
   217 
   218     def chmod (self, path, mode):
   219         LOG.debug ("*** chmod %s %s" % (path, oct(mode)))
   220         if (config.get("Main", "ReadOnly") == "true"):
   221             sendReadOnlyNotification()
   222             return -errno.EACCES
   223         os.chmod (fixPath (path), mode)
   224 
   225     def chown (self, path, uid, gid):
   226         LOG.debug ("*** chown %s %s %s" % (path, uid, gid))
   227         if (config.get("Main", "ReadOnly") == "true"):
   228             sendReadOnlyNotification()
   229             return -errno.EACCES
   230         os.chown (fixPath (path), uid, gid)
   231 
   232     def link (self, targetPath, linkPath):
   233         LOG.debug ("*** link %s %s" % (targetPath, linkPath))
   234         if (config.get("Main", "ReadOnly") == "true"):
   235             sendReadOnlyNotification()
   236             return -errno.EACCES
   237         os.link (fixPath (targetPath), fixPath (linkPath))
   238 
   239     def mkdir (self, path, mode):
   240         LOG.debug ("*** mkdir %s %s" % (path, oct(mode)))
   241         if (config.get("Main", "ReadOnly") == "true"):
   242             sendReadOnlyNotification()
   243             return -errno.EACCES
   244         os.mkdir (fixPath (path), mode)
   245 
   246     def mknod (self, path, mode, dev):
   247         LOG.debug ("*** mknod %s %s %s" % (path, oct (mode), dev))
   248         if (config.get("Main", "ReadOnly") == "true"):
   249             sendReadOnlyNotification()
   250             return -errno.EACCES
   251         os.mknod (fixPath (path), mode, dev)
   252 
   253     # to implement virus scan
   254     def open (self, path, flags):
   255         LOG.debug ("*** open %s %s" % (path, oct (flags)))
   256         self.file = os.fdopen (os.open (fixPath (path), flags), flag2mode (flags))
   257         self.fd = self.file.fileno ()
   258         
   259         LOG.debug(self.__rootpath)
   260         LOG.debug(path)
   261         
   262         retval = scanFile (rootPath(self.__rootpath, path), self.file)
   263         
   264         #if type(retval) is not dict:
   265         if (isinstance(retval, dict) == False):
   266             LOG.error(SCAN_WRONG_RETURN_VALUE)
   267             self.file.close ()
   268             return -errno.EACCES
   269         
   270         if ((retval.has_key("infected") == False) or (retval.has_key("virusname") == False)):
   271             LOG.error(SCAN_RETURN_VALUE_KEY_MISSING)
   272             self.file.close ()
   273             return -errno.EACCES
   274             
   275         
   276         if (retval.get("infected") == True):
   277             self.file.close ()
   278             sendNotification(NOTIFICATION_CRITICAL, "%s\nFile: %s\nVirus: %s" %(VIRUS_FOUND, path, retval.get("virusname")))
   279             LOG.error("%s" %(VIRUS_FOUND,))
   280             LOG.error("Virus: %s" %(retval.get("virusname"),))
   281             return -errno.EACCES
   282         
   283         whitelisted = whitelistFile (rootPath(self.__rootpath, path))
   284         if (whitelisted == False):
   285             self.file.close ()
   286             sendNotification(NOTIFICATION_CRITICAL, "File not in whitelist. Access denied.")
   287             return -errno.EACCES
   288 
   289     def read (self, path, length, offset):
   290         LOG.debug ("*** read %s %s %s" % (path, length, offset))
   291         self.file.seek (offset)
   292         return self.file.read (length)
   293 
   294     def readlink (self, path):
   295         LOG.debug ("*** readlink %s" % (path))
   296         return os.readlink (fixPath (path))
   297 
   298     def release (self, path, flags):
   299         LOG.debug ("*** release %s %s" % (path, oct (flags)))
   300         self.file.close ()
   301 
   302     def rename (self, oldPath, newPath):
   303         LOG.debug ("*** rename %s %s %s" % (oldPath, newPath, config.get("Main", "ReadOnly")))
   304         if (config.get("Main", "ReadOnly") == "true"):
   305             sendReadOnlyNotification()
   306             return -errno.EACCES
   307         os.rename (fixPath (oldPath), fixPath (newPath))
   308 
   309     def rmdir (self, path):
   310         LOG.debug ("*** rmdir %s %s" % (path, config.get("Main", "ReadOnly")))
   311         if (config.get("Main", "ReadOnly") == "true"):
   312             sendReadOnlyNotification()
   313             return -errno.EACCES
   314         os.rmdir (fixPath (path))
   315 
   316     def statfs (self):
   317         LOG.debug ("*** statfs")
   318         return os.statvfs(".")
   319 
   320     def symlink (self, targetPath, linkPath):
   321         LOG.debug ("*** symlink %s %s %s" % (targetPath, linkPath, config.get("Main", "ReadOnly")))
   322         if (config.get("Main", "ReadOnly") == "true"):
   323             sendReadOnlyNotification()
   324             return -errno.EACCES
   325         os.symlink (fixPath (targetPath), fixPath (linkPath))
   326 
   327     def truncate (self, path, length):
   328         LOG.debug ("*** truncate %s %s %s" % (path, length, config.get("Main", "ReadOnly")))
   329         if (config.get("Main", "ReadOnly") == "true"):
   330             sendReadOnlyNotification()
   331             return -errno.EACCES
   332         f = open (fixPath (path), "rw+")
   333         f.truncate (length)
   334         f.close ()
   335 
   336     def unlink (self, path):
   337         LOG.debug ("*** unlink %s %s" % (path, config.get("Main", "ReadOnly")))
   338         if (config.get("Main", "ReadOnly") == "true"):
   339             sendReadOnlyNotification()
   340             return -errno.EACCES
   341         os.unlink (fixPath (path))
   342 
   343     def utime (self, path, times):
   344         LOG.debug ("*** utime %s %s" % (path, times))
   345         os.utime (fixPath (path), times)
   346 
   347     def write (self, path, buf, offset):
   348         LOG.debug ("*** write %s %s %s %s" % (path, buf, offset, config.get("Main", "ReadOnly")))
   349         if (config.get("Main", "ReadOnly") == "true"):
   350             self.file.close()
   351             sendReadOnlyNotification()
   352             return -errno.EACCES
   353         self.file.seek (offset)
   354         self.file.write (buf)
   355         return len (buf)
   356 
   357     def access (self, path, mode):
   358         LOG.debug ("*** access %s %s" % (path, oct (mode)))
   359         if not os.access (fixPath (path), mode):
   360             return -errno.EACCES
   361 
   362     def create (self, path, flags, mode):
   363         LOG.debug ("*** create %s %s %s %s %s" % (fixPath (path), oct (flags), oct (mode), flag2mode (flags), config.get("Main", "ReadOnly")))
   364         if (config.get("Main", "ReadOnly") == "true"):
   365             sendReadOnlyNotification()
   366             return -errno.EACCES
   367         #self.file = os.fdopen (os.open (fixPath (path), flags, mode), flag2mode (flags))
   368         # fix strange Windows behaviour
   369         self.file = os.fdopen (os.open (fixPath (path), flags, mode), "rw+")
   370         self.fd = self.file.fileno ()
   371 
   372 
   373 if __name__ == "__main__":
   374     # Set api version
   375     fuse.fuse_python_api = (0, 2)
   376     fuse.feature_assert ('stateful_files', 'has_init')
   377 
   378     config = loadConfig ()
   379     initLog (config)
   380     
   381     #sendNotification("Info", "OsecFS started")
   382     
   383     # Import the Malware Scanner
   384     sys.path.append(config.get("Main", "ScannerPath"))
   385     
   386     MalwareModule = import_module(config.get("Main", "ScannerModuleName"))
   387     MalwareClass = getattr(MalwareModule, config.get("Main", "ScannerClassName"))
   388     
   389     MalwareScanner = MalwareClass (config.get("Main", "ScannerConfig"));
   390     
   391     osecfs = OsecFS (config.get ("Main", "Rootpath"))
   392     osecfs.flags = 0
   393     osecfs.multithreaded = 0
   394 
   395     fuse_args = [sys.argv[0], config.get ("Main", "Mountpoint")];
   396     osecfs.main (fuse_args)