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