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