src/OsecFS.py
author ft
Tue, 09 Sep 2014 09:16:06 +0200
changeset 20 11db6c7b63d2
parent 19 d2ab00839ba5
child 21 7fbccd896e17
permissions -rwxr-xr-x
Changed notification popups (if its only information then no popup will appear)
     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 netifaces
    22 import netaddr
    23 import hashlib
    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 STATUS_CODE_OK = 200
    42 
    43 SYSTEM_FILE_COMMAND = "file"
    44 httpPool = urllib3.PoolManager(num_pools = 1, timeout = 3)
    45 
    46 def checkMinimumOptions (config):
    47     for section, options in MINOPTS.iteritems ():
    48         for option in options:
    49             if (config.has_option(section, option) == False):
    50                 print (CONFIG_MISSING % (section, option))
    51                 exit (129)
    52 
    53 def printUsage ():
    54     print ("Usage:")
    55     print ("%s configfile mountpath ro/rw" % (sys.argv[0]))
    56     exit (128)
    57 
    58 def loadConfig ():
    59     print ("load config")
    60 
    61     if (len (sys.argv) < 4):
    62         printUsage ()
    63 
    64     configfile = sys.argv[1]
    65     config = ConfigParser.SafeConfigParser ()
    66 
    67     if ((os.path.exists (configfile) == False) or (os.path.isfile (configfile) == False) or (os.access (configfile, os.R_OK) == False)):
    68         print (CONFIG_NOT_READABLE)
    69         printUsage ()
    70 
    71     try:
    72         config.read (sys.argv[1])
    73     except Exception, e:
    74         print (CONFIG_WRONG)
    75         print ("Error: %s" % (e))
    76 
    77 
    78     config.set("Main", "Mountpoint", sys.argv[2])
    79     if (sys.argv[3] == "rw"):
    80         config.set("Main", "ReadOnly", "false")
    81     else:
    82         config.set("Main", "ReadOnly", "true")
    83 
    84     checkMinimumOptions (config)
    85 
    86     return config
    87 
    88 def initLog (config):
    89     print ("init log")
    90 
    91     global LOG
    92     logfile = config.get("Main", "Logfile")
    93     
    94     numeric_level = getattr(logging, config.get("Main", "LogLevel").upper(), None)
    95     if not isinstance(numeric_level, int):
    96         raise ValueError('Invalid log level: %s' % loglevel)
    97 
    98     # ToDo move log level and maybe other things to config file
    99     logging.basicConfig(
   100                         level = numeric_level,
   101                         format = "%(asctime)s %(name)-12s %(funcName)-15s %(levelname)-8s %(message)s",
   102                         datefmt = "%Y-%m-%d %H:%M:%S",
   103                         filename = logfile,
   104                         filemode = "a+",
   105     )
   106     LOG = logging.getLogger("fuse_main")
   107 
   108 
   109 def fixPath (path):
   110     return ".%s" % (path)
   111 
   112 def rootPath (rootpath, path):
   113     return "%s%s" % (rootpath, path)
   114 
   115 def flag2mode (flags):
   116     md = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'}
   117     m = md[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)]
   118 
   119     # windows sets append even if it would overwrite the whole file (seek 0)
   120     # so ignore append option
   121     #if flags | os.O_APPEND:
   122     #    m = m.replace('w', 'a', 1)
   123 
   124     return m
   125 
   126 def scanFile (path, fileobject):
   127     LOG.debug ("Scan File \"%s\" with malware Scanner" %(path,) )
   128     return MalwareScanner.scanFile (path, fileobject)
   129 
   130 
   131 def scanFileClamAV (path):
   132     infected = False
   133 
   134     LOG.debug ("Scan File: %s" % (path))
   135 
   136     result = pyclamav.scanfile (path)
   137     LOG.debug ("Result of file \"%s\": %s" % (path, result))
   138     if (result[0] != 0):
   139         infected = True
   140 
   141     if (infected == True):
   142         LOG.error ("Virus found, deny Access %s" % (result,))
   143 
   144     return infected
   145 
   146 def whitelistFile (path):
   147     whitelisted = False;
   148 
   149     LOG.debug ("Execute \"%s\" command on \"%s\"" %(SYSTEM_FILE_COMMAND, path))
   150     
   151     result = None
   152     try:
   153         result = subprocess.check_output ([SYSTEM_FILE_COMMAND, path]);
   154         # ToDo replace with real whitelist
   155         whitelisted = True
   156     except Exception as e:
   157         LOG.error ("Call returns with an error!")
   158         LOG.error (e)
   159 
   160     LOG.debug ("Type: %s" %(result))
   161 
   162     return whitelisted
   163 
   164 def sendDataToRest (urlpath, data):
   165     netifaces.ifaddresses("eth0")[2][0]["addr"]
   166     
   167     # Get first address in network (0 = network ip -> 192.168.0.0)
   168     remote_ip = netaddr.IPNetwork("%s/%s" %(netifaces.ifaddresses("eth0")[2][0]["addr"], netifaces.ifaddresses("eth0")[2][0]["netmask"]))[1]
   169     
   170     url = ("http://%s:8090//%s" %(remote_ip, urlpath))
   171 
   172     LOG.debug ("Send data to \"%s\"" %(url, ))
   173     LOG.debug ("Data: %s" %(data, ))
   174     
   175     try:
   176         response = httpPool.request_encode_body("POST", url, fields=data, retries=0)
   177     except Exception, e:
   178         LOG.error("Remote host not reachable")
   179         LOG.error ("Exception: %s" %(e,))
   180         return
   181     
   182     if response.status == STATUS_CODE_OK:
   183         LOG.info("Data sent successfully to rest server")
   184         return True
   185     else:
   186         LOG.error("Server returned errorcode: %s" %(response.status,))
   187         return False
   188     
   189 
   190 def sendNotification (type, message):
   191     data = {"msgtype" : type, "text" : message}
   192     
   193     if (type == "information"):
   194         sendDataToRest ("message", data)
   195     else:
   196         sendDataToRest ("notification", data)
   197 
   198 def sendReadOnlyNotification():
   199     sendNotification("critical", "Filesystem is in read only mode. If you want to export files please initialize an encrypted filesystem.")
   200     
   201 def sendLogNotPossibleNotification():
   202     sendNotification("critical", "Send log entry to opensecurity rest server failed.")
   203     
   204 def sendFileLog(filename, filesize, filehash, hashtype):
   205     data = {"filename" : filename, "filesize" : "%s" %(filesize,), "filehash" : filehash, "hashtype" : hashtype}
   206     retval = sendDataToRest ("log", data)
   207     if (retval == False):
   208         sendLogNotPossibleNotification()
   209 
   210 def calcMD5 (path, block_size=256*128, hr=True):
   211     md5 = hashlib.md5()
   212     with open(path,'rb') as f: 
   213         for chunk in iter(lambda: f.read(block_size), b''): 
   214              md5.update(chunk)
   215     if hr:
   216         return md5.hexdigest()
   217     return md5.digest()
   218 
   219 class OsecFS (Fuse):
   220 
   221     __rootpath = None
   222 
   223     # default fuse init
   224     def __init__(self, rootpath, *args, **kw):
   225         self.__rootpath = rootpath
   226         Fuse.__init__ (self, *args, **kw)
   227         LOG.debug ("Init complete.")
   228         sendNotification("information", "Filesystem successfully mounted.")
   229 
   230     # defines that our working directory will be the __rootpath
   231     def fsinit(self):
   232         os.chdir (self.__rootpath)
   233 
   234     def getattr(self, path):
   235         LOG.debug ("*** getattr (%s)" % (fixPath (path)))
   236         return os.lstat (fixPath (path));
   237 
   238     def getdir(self, path):
   239         LOG.debug ("*** getdir (%s)" % (path));
   240         return os.listdir (fixPath (path))
   241 
   242     def readdir(self, path, offset):
   243         LOG.debug ("*** readdir (%s %s)" % (path, offset));
   244         for e in os.listdir (fixPath (path)):
   245             yield fuse.Direntry(e)
   246 
   247     def chmod (self, path, mode):
   248         LOG.debug ("*** chmod %s %s" % (path, oct(mode)))
   249         if (config.get("Main", "ReadOnly") == "true"):
   250             sendReadOnlyNotification()
   251             return -errno.EACCES
   252         os.chmod (fixPath (path), mode)
   253 
   254     def chown (self, path, uid, gid):
   255         LOG.debug ("*** chown %s %s %s" % (path, uid, gid))
   256         if (config.get("Main", "ReadOnly") == "true"):
   257             sendReadOnlyNotification()
   258             return -errno.EACCES
   259         os.chown (fixPath (path), uid, gid)
   260 
   261     def link (self, targetPath, linkPath):
   262         LOG.debug ("*** link %s %s" % (targetPath, linkPath))
   263         if (config.get("Main", "ReadOnly") == "true"):
   264             sendReadOnlyNotification()
   265             return -errno.EACCES
   266         os.link (fixPath (targetPath), fixPath (linkPath))
   267 
   268     def mkdir (self, path, mode):
   269         LOG.debug ("*** mkdir %s %s" % (path, oct(mode)))
   270         if (config.get("Main", "ReadOnly") == "true"):
   271             sendReadOnlyNotification()
   272             return -errno.EACCES
   273         os.mkdir (fixPath (path), mode)
   274 
   275     def mknod (self, path, mode, dev):
   276         LOG.debug ("*** mknod %s %s %s" % (path, oct (mode), dev))
   277         if (config.get("Main", "ReadOnly") == "true"):
   278             sendReadOnlyNotification()
   279             return -errno.EACCES
   280         os.mknod (fixPath (path), mode, dev)
   281 
   282     # to implement virus scan
   283     def open (self, path, flags):
   284         LOG.debug ("*** open %s %s" % (path, oct (flags)))
   285         self.file = os.fdopen (os.open (fixPath (path), flags), flag2mode (flags))
   286         self.written = False
   287         self.fd = self.file.fileno ()
   288         
   289         LOG.debug(self.__rootpath)
   290         LOG.debug(path)
   291         
   292         retval = scanFile (rootPath(self.__rootpath, path), self.file)
   293         
   294         #if type(retval) is not dict:
   295         if (isinstance(retval, dict) == False):
   296             LOG.error(SCAN_WRONG_RETURN_VALUE)
   297             self.file.close ()
   298             return -errno.EACCES
   299         
   300         if ((retval.has_key("infected") == False) or (retval.has_key("virusname") == False)):
   301             LOG.error(SCAN_RETURN_VALUE_KEY_MISSING)
   302             self.file.close ()
   303             return -errno.EACCES
   304             
   305         
   306         if (retval.get("infected") == True):
   307             self.file.close ()
   308             sendNotification(NOTIFICATION_CRITICAL, "%s\nFile: %s\nVirus: %s" %(VIRUS_FOUND, path, retval.get("virusname")))
   309             LOG.error("%s" %(VIRUS_FOUND,))
   310             LOG.error("Virus: %s" %(retval.get("virusname"),))
   311             return -errno.EACCES
   312         
   313         whitelisted = whitelistFile (rootPath(self.__rootpath, path))
   314         if (whitelisted == False):
   315             self.file.close ()
   316             sendNotification(NOTIFICATION_CRITICAL, "File not in whitelist. Access denied.")
   317             return -errno.EACCES
   318 
   319     def read (self, path, length, offset):
   320         LOG.debug ("*** read %s %s %s" % (path, length, offset))
   321         self.file.seek (offset)
   322         return self.file.read (length)
   323 
   324     def readlink (self, path):
   325         LOG.debug ("*** readlink %s" % (path))
   326         return os.readlink (fixPath (path))
   327 
   328     def release (self, path, flags):
   329         LOG.debug ("*** release %s %s" % (path, oct (flags)))
   330         self.file.flush()
   331         os.fsync(self.file.fileno())
   332         self.file.close ()
   333         
   334         if (self.written == True):
   335             hashsum = calcMD5(fixPath(path))
   336             filesize = os.path.getsize(fixPath(path))
   337             sendFileLog(path, filesize, hashsum, "md5")
   338         
   339 
   340     def rename (self, oldPath, newPath):
   341         LOG.debug ("*** rename %s %s %s" % (oldPath, newPath, config.get("Main", "ReadOnly")))
   342         if (config.get("Main", "ReadOnly") == "true"):
   343             sendReadOnlyNotification()
   344             return -errno.EACCES
   345         os.rename (fixPath (oldPath), fixPath (newPath))
   346 
   347     def rmdir (self, path):
   348         LOG.debug ("*** rmdir %s %s" % (path, config.get("Main", "ReadOnly")))
   349         if (config.get("Main", "ReadOnly") == "true"):
   350             sendReadOnlyNotification()
   351             return -errno.EACCES
   352         os.rmdir (fixPath (path))
   353 
   354     def statfs (self):
   355         LOG.debug ("*** statfs")
   356         return os.statvfs(".")
   357 
   358     def symlink (self, targetPath, linkPath):
   359         LOG.debug ("*** symlink %s %s %s" % (targetPath, linkPath, config.get("Main", "ReadOnly")))
   360         if (config.get("Main", "ReadOnly") == "true"):
   361             sendReadOnlyNotification()
   362             return -errno.EACCES
   363         os.symlink (fixPath (targetPath), fixPath (linkPath))
   364 
   365     def truncate (self, path, length):
   366         LOG.debug ("*** truncate %s %s %s" % (path, length, config.get("Main", "ReadOnly")))
   367         if (config.get("Main", "ReadOnly") == "true"):
   368             sendReadOnlyNotification()
   369             return -errno.EACCES
   370         f = open (fixPath (path), "w+")
   371         f.truncate (length)
   372         f.close ()
   373 
   374     def unlink (self, path):
   375         LOG.debug ("*** unlink %s %s" % (path, config.get("Main", "ReadOnly")))
   376         if (config.get("Main", "ReadOnly") == "true"):
   377             sendReadOnlyNotification()
   378             return -errno.EACCES
   379         os.unlink (fixPath (path))
   380 
   381     def utime (self, path, times):
   382         LOG.debug ("*** utime %s %s" % (path, times))
   383         os.utime (fixPath (path), times)
   384 
   385     def write (self, path, buf, offset):
   386         #LOG.debug ("*** write %s %s %s %s" % (path, buf, offset, config.get("Main", "ReadOnly")))
   387         LOG.debug ("*** write %s %s %s %s" % (path, "filecontent", offset, config.get("Main", "ReadOnly")))
   388         if (config.get("Main", "ReadOnly") == "true"):
   389             self.file.close()
   390             sendReadOnlyNotification()
   391             return -errno.EACCES
   392         self.file.seek (offset)
   393         self.file.write (buf)
   394         self.written = True
   395         return len (buf)
   396 
   397     def access (self, path, mode):
   398         LOG.debug ("*** access %s %s" % (path, oct (mode)))
   399         if not os.access (fixPath (path), mode):
   400             return -errno.EACCES
   401 
   402     def create (self, path, flags, mode):
   403         LOG.debug ("*** create %s %s %s %s %s" % (fixPath (path), oct (flags), oct (mode), flag2mode (flags), config.get("Main", "ReadOnly")))
   404         if (config.get("Main", "ReadOnly") == "true"):
   405             sendReadOnlyNotification()
   406             return -errno.EACCES
   407 
   408         self.file = os.fdopen (os.open (fixPath (path), flags), flag2mode(flags))
   409         self.written = True
   410         self.fd = self.file.fileno ()
   411 
   412 
   413 if __name__ == "__main__":
   414     # Set api version
   415     fuse.fuse_python_api = (0, 2)
   416     fuse.feature_assert ('stateful_files', 'has_init')
   417 
   418     config = loadConfig ()
   419     initLog (config)
   420     
   421     #sendNotification("Info", "OsecFS started")
   422     
   423     # Import the Malware Scanner
   424     sys.path.append(config.get("Main", "ScannerPath"))
   425     
   426     MalwareModule = import_module(config.get("Main", "ScannerModuleName"))
   427     MalwareClass = getattr(MalwareModule, config.get("Main", "ScannerClassName"))
   428     
   429     MalwareScanner = MalwareClass (config.get("Main", "ScannerConfig"));
   430     
   431     osecfs = OsecFS (config.get ("Main", "Rootpath"))
   432     osecfs.flags = 0
   433     osecfs.multithreaded = 0
   434 
   435     fuse_args = [sys.argv[0], config.get ("Main", "Mountpoint")];
   436     osecfs.main (fuse_args)