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