src/OsecFS.py
changeset 25 a600a9b39dd7
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/src/OsecFS.py	Tue Nov 04 17:40:17 2014 +0100
     1.3 @@ -0,0 +1,471 @@
     1.4 +#!/usr/bin/python
     1.5 +
     1.6 +# ------------------------------------------------------------
     1.7 +# opensecurity package file
     1.8 +#
     1.9 +# Autor: X-Net Services GmbH <office@x-net.at>
    1.10 +#
    1.11 +# Copyright 2013-2014 X-Net and AIT Austrian Institute of Technology
    1.12 +#
    1.13 +#
    1.14 +#     X-Net Technologies GmbH
    1.15 +#     Elisabethstrasse 1
    1.16 +#     4020 Linz
    1.17 +#     AUSTRIA
    1.18 +#     https://www.x-net.at
    1.19 +#
    1.20 +#     AIT Austrian Institute of Technology
    1.21 +#     Donau City Strasse 1
    1.22 +#     1220 Wien
    1.23 +#     AUSTRIA
    1.24 +#     http://www.ait.ac.at
    1.25 +#
    1.26 +#
    1.27 +# Licensed under the Apache License, Version 2.0 (the "License");
    1.28 +# you may not use this file except in compliance with the License.
    1.29 +# You may obtain a copy of the License at
    1.30 +#
    1.31 +#    http://www.apache.org/licenses/LICENSE-2.0
    1.32 +#
    1.33 +# Unless required by applicable law or agreed to in writing, software
    1.34 +# distributed under the License is distributed on an "AS IS" BASIS,
    1.35 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    1.36 +# See the License for the specific language governing permissions and
    1.37 +# limitations under the License.
    1.38 +# ------------------------------------------------------------
    1.39 +
    1.40 +
    1.41 +from fuse import Fuse
    1.42 +import fuse
    1.43 +
    1.44 +import ConfigParser
    1.45 +
    1.46 +import sys
    1.47 +
    1.48 +import logging
    1.49 +import os
    1.50 +import errno
    1.51 +import time
    1.52 +
    1.53 +from importlib import import_module
    1.54 +
    1.55 +
    1.56 +import subprocess
    1.57 +
    1.58 +import urllib3
    1.59 +import netifaces
    1.60 +import netaddr
    1.61 +import hashlib
    1.62 +
    1.63 +
    1.64 +sys.stderr = open('/var/log/osecfs_error.log', 'a+')
    1.65 +
    1.66 +
    1.67 +MINOPTS = { "Main" : ["Logfile", "LogLevel", "Mountpoint", "Rootpath", "ScannerPath", "ScannerModuleName", "ScannerClassName", "ScannerConfig", "ReadOnly"]}
    1.68 +
    1.69 +CONFIG_NOT_READABLE = "Configfile is not readable"
    1.70 +CONFIG_WRONG = "Something is wrong with the config"
    1.71 +CONFIG_MISSING = "Section: \"%s\" Option: \"%s\" in configfile is missing"
    1.72 +SCAN_WRONG_RETURN_VALUE = "The return Value of the malware scanner is wrong. Has to be an dictionary"
    1.73 +SCAN_RETURN_VALUE_KEY_MISSING = "The dictionary has to include key \"infected\" (True, False) and \"virusname\" (String)"
    1.74 +VIRUS_FOUND = "Virus found. Access denied"
    1.75 +NOTIFICATION_CRITICAL = "critical"
    1.76 +NOTIFICATION_INFO = "info"
    1.77 +LOG = None
    1.78 +MalwareScanner = None
    1.79 +STATUS_CODE_OK = 200
    1.80 +
    1.81 +SYSTEM_FILE_COMMAND = "file"
    1.82 +httpPool = urllib3.PoolManager(num_pools = 1, timeout = 3)
    1.83 +
    1.84 +def checkMinimumOptions (config):
    1.85 +    for section, options in MINOPTS.iteritems ():
    1.86 +        for option in options:
    1.87 +            if (config.has_option(section, option) == False):
    1.88 +                print (CONFIG_MISSING % (section, option))
    1.89 +                exit (129)
    1.90 +
    1.91 +def printUsage ():
    1.92 +    print ("Usage:")
    1.93 +    print ("%s configfile mountpath ro/rw" % (sys.argv[0]))
    1.94 +    exit (128)
    1.95 +
    1.96 +def loadConfig ():
    1.97 +    print ("load config")
    1.98 +
    1.99 +    if (len (sys.argv) < 4):
   1.100 +        printUsage ()
   1.101 +
   1.102 +    configfile = sys.argv[1]
   1.103 +    config = ConfigParser.SafeConfigParser ()
   1.104 +
   1.105 +    if ((os.path.exists (configfile) == False) or (os.path.isfile (configfile) == False) or (os.access (configfile, os.R_OK) == False)):
   1.106 +        print (CONFIG_NOT_READABLE)
   1.107 +        printUsage ()
   1.108 +
   1.109 +    try:
   1.110 +        config.read (sys.argv[1])
   1.111 +    except Exception, e:
   1.112 +        print (CONFIG_WRONG)
   1.113 +        print ("Error: %s" % (e))
   1.114 +
   1.115 +
   1.116 +    config.set("Main", "Mountpoint", sys.argv[2])
   1.117 +    if (sys.argv[3] == "rw"):
   1.118 +        config.set("Main", "ReadOnly", "false")
   1.119 +    else:
   1.120 +        config.set("Main", "ReadOnly", "true")
   1.121 +
   1.122 +    checkMinimumOptions (config)
   1.123 +
   1.124 +    return config
   1.125 +
   1.126 +def initLog (config):
   1.127 +    print ("init log")
   1.128 +
   1.129 +    global LOG
   1.130 +    logfile = config.get("Main", "Logfile")
   1.131 +    
   1.132 +    numeric_level = getattr(logging, config.get("Main", "LogLevel").upper(), None)
   1.133 +    if not isinstance(numeric_level, int):
   1.134 +        raise ValueError('Invalid log level: %s' % loglevel)
   1.135 +
   1.136 +    # ToDo move log level and maybe other things to config file
   1.137 +    logging.basicConfig(
   1.138 +                        level = numeric_level,
   1.139 +                        format = "%(asctime)s %(name)-12s %(funcName)-15s %(levelname)-8s %(message)s",
   1.140 +                        datefmt = "%Y-%m-%d %H:%M:%S",
   1.141 +                        filename = logfile,
   1.142 +                        filemode = "a+",
   1.143 +    )
   1.144 +    LOG = logging.getLogger("fuse_main")
   1.145 +
   1.146 +
   1.147 +def fixPath (path):
   1.148 +    return ".%s" % (path)
   1.149 +
   1.150 +def rootPath (rootpath, path):
   1.151 +    return "%s%s" % (rootpath, path)
   1.152 +
   1.153 +def flag2mode (flags):
   1.154 +    md = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'}
   1.155 +    m = md[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)]
   1.156 +
   1.157 +    # windows sets append even if it would overwrite the whole file (seek 0)
   1.158 +    # so ignore append option
   1.159 +    #if flags | os.O_APPEND:
   1.160 +    #    m = m.replace('w', 'a', 1)
   1.161 +
   1.162 +    return m
   1.163 +
   1.164 +def scanFile (path, fileobject):
   1.165 +    LOG.debug ("Scan File \"%s\" with malware Scanner" %(path,) )
   1.166 +    return MalwareScanner.scanFile (path, fileobject)
   1.167 +
   1.168 +
   1.169 +def scanFileClamAV (path):
   1.170 +    infected = False
   1.171 +
   1.172 +    LOG.debug ("Scan File: %s" % (path))
   1.173 +
   1.174 +    result = pyclamav.scanfile (path)
   1.175 +    LOG.debug ("Result of file \"%s\": %s" % (path, result))
   1.176 +    if (result[0] != 0):
   1.177 +        infected = True
   1.178 +
   1.179 +    if (infected == True):
   1.180 +        LOG.error ("Virus found, deny Access %s" % (result,))
   1.181 +
   1.182 +    return infected
   1.183 +
   1.184 +def whitelistFile (path):
   1.185 +    whitelisted = False;
   1.186 +
   1.187 +    LOG.debug ("Execute \"%s\" command on \"%s\"" %(SYSTEM_FILE_COMMAND, path))
   1.188 +    
   1.189 +    result = None
   1.190 +    try:
   1.191 +        result = subprocess.check_output ([SYSTEM_FILE_COMMAND, path]);
   1.192 +        # ToDo replace with real whitelist
   1.193 +        whitelisted = True
   1.194 +    except Exception as e:
   1.195 +        LOG.error ("Call returns with an error!")
   1.196 +        LOG.error (e)
   1.197 +
   1.198 +    LOG.debug ("Type: %s" %(result))
   1.199 +
   1.200 +    return whitelisted
   1.201 +
   1.202 +def sendDataToRest (urlpath, data):
   1.203 +    netifaces.ifaddresses("eth0")[2][0]["addr"]
   1.204 +    
   1.205 +    # Get first address in network (0 = network ip -> 192.168.0.0)
   1.206 +    remote_ip = netaddr.IPNetwork("%s/%s" %(netifaces.ifaddresses("eth0")[2][0]["addr"], netifaces.ifaddresses("eth0")[2][0]["netmask"]))[1]
   1.207 +    
   1.208 +    url = ("http://%s:8090//%s" %(remote_ip, urlpath))
   1.209 +
   1.210 +    LOG.debug ("Send data to \"%s\"" %(url, ))
   1.211 +    LOG.debug ("Data: %s" %(data, ))
   1.212 +    
   1.213 +    try:
   1.214 +        response = httpPool.request_encode_body("POST", url, fields=data, retries=0)
   1.215 +    except Exception, e:
   1.216 +        LOG.error("Remote host not reachable")
   1.217 +        LOG.error ("Exception: %s" %(e,))
   1.218 +        return
   1.219 +    
   1.220 +    if response.status == STATUS_CODE_OK:
   1.221 +        LOG.info("Data sent successfully to rest server")
   1.222 +        return True
   1.223 +    else:
   1.224 +        LOG.error("Server returned errorcode: %s" %(response.status,))
   1.225 +        return False
   1.226 +    
   1.227 +
   1.228 +def sendNotification (type, message):
   1.229 +    data = {"msgtype" : type, "text" : message}
   1.230 +    
   1.231 +    if (type == "information"):
   1.232 +        sendDataToRest ("message", data)
   1.233 +    else:
   1.234 +        sendDataToRest ("notification", data)
   1.235 +
   1.236 +def sendReadOnlyNotification():
   1.237 +    sendNotification("critical", "Filesystem is in read only mode. If you want to export files please initialize an encrypted filesystem.")
   1.238 +    
   1.239 +def sendLogNotPossibleNotification():
   1.240 +    sendNotification("critical", "Send log entry to opensecurity rest server failed.")
   1.241 +    
   1.242 +def sendFileLog(filename, filesize, filehash, hashtype):
   1.243 +    data = {"filename" : filename, "filesize" : "%s" %(filesize,), "filehash" : filehash, "hashtype" : hashtype}
   1.244 +    retval = sendDataToRest ("log", data)
   1.245 +    if (retval == False):
   1.246 +        sendLogNotPossibleNotification()
   1.247 +
   1.248 +def calcMD5 (path, block_size=256*128, hr=True):
   1.249 +    md5 = hashlib.md5()
   1.250 +    with open(path,'rb') as f: 
   1.251 +        for chunk in iter(lambda: f.read(block_size), b''): 
   1.252 +             md5.update(chunk)
   1.253 +    if hr:
   1.254 +        return md5.hexdigest()
   1.255 +    return md5.digest()
   1.256 +
   1.257 +class OsecFS (Fuse):
   1.258 +
   1.259 +    __rootpath = None
   1.260 +
   1.261 +    # default fuse init
   1.262 +    def __init__(self, rootpath, *args, **kw):
   1.263 +        self.__rootpath = rootpath
   1.264 +        Fuse.__init__ (self, *args, **kw)
   1.265 +        LOG.debug ("Init complete.")
   1.266 +        sendNotification("information", "Filesystem successfully mounted.")
   1.267 +
   1.268 +    # defines that our working directory will be the __rootpath
   1.269 +    def fsinit(self):
   1.270 +        os.chdir (self.__rootpath)
   1.271 +
   1.272 +    def getattr(self, path):
   1.273 +        LOG.debug ("*** getattr (%s)" % (fixPath (path)))
   1.274 +        return os.lstat (fixPath (path));
   1.275 +
   1.276 +    def getdir(self, path):
   1.277 +        LOG.debug ("*** getdir (%s)" % (path));
   1.278 +        return os.listdir (fixPath (path))
   1.279 +
   1.280 +    def readdir(self, path, offset):
   1.281 +        LOG.debug ("*** readdir (%s %s)" % (path, offset));
   1.282 +        for e in os.listdir (fixPath (path)):
   1.283 +            yield fuse.Direntry(e)
   1.284 +
   1.285 +    def chmod (self, path, mode):
   1.286 +        LOG.debug ("*** chmod %s %s" % (path, oct(mode)))
   1.287 +        if (config.get("Main", "ReadOnly") == "true"):
   1.288 +            sendReadOnlyNotification()
   1.289 +            return -errno.EACCES
   1.290 +        os.chmod (fixPath (path), mode)
   1.291 +
   1.292 +    def chown (self, path, uid, gid):
   1.293 +        LOG.debug ("*** chown %s %s %s" % (path, uid, gid))
   1.294 +        if (config.get("Main", "ReadOnly") == "true"):
   1.295 +            sendReadOnlyNotification()
   1.296 +            return -errno.EACCES
   1.297 +        os.chown (fixPath (path), uid, gid)
   1.298 +
   1.299 +    def link (self, targetPath, linkPath):
   1.300 +        LOG.debug ("*** link %s %s" % (targetPath, linkPath))
   1.301 +        if (config.get("Main", "ReadOnly") == "true"):
   1.302 +            sendReadOnlyNotification()
   1.303 +            return -errno.EACCES
   1.304 +        os.link (fixPath (targetPath), fixPath (linkPath))
   1.305 +
   1.306 +    def mkdir (self, path, mode):
   1.307 +        LOG.debug ("*** mkdir %s %s" % (path, oct(mode)))
   1.308 +        if (config.get("Main", "ReadOnly") == "true"):
   1.309 +            sendReadOnlyNotification()
   1.310 +            return -errno.EACCES
   1.311 +        os.mkdir (fixPath (path), mode)
   1.312 +
   1.313 +    def mknod (self, path, mode, dev):
   1.314 +        LOG.debug ("*** mknod %s %s %s" % (path, oct (mode), dev))
   1.315 +        if (config.get("Main", "ReadOnly") == "true"):
   1.316 +            sendReadOnlyNotification()
   1.317 +            return -errno.EACCES
   1.318 +        os.mknod (fixPath (path), mode, dev)
   1.319 +
   1.320 +    # to implement virus scan
   1.321 +    def open (self, path, flags):
   1.322 +        LOG.debug ("*** open %s %s" % (path, oct (flags)))
   1.323 +        self.file = os.fdopen (os.open (fixPath (path), flags), flag2mode (flags))
   1.324 +        self.written = False
   1.325 +        self.fd = self.file.fileno ()
   1.326 +        
   1.327 +        LOG.debug(self.__rootpath)
   1.328 +        LOG.debug(path)
   1.329 +        
   1.330 +        retval = scanFile (rootPath(self.__rootpath, path), self.file)
   1.331 +        
   1.332 +        #if type(retval) is not dict:
   1.333 +        if (isinstance(retval, dict) == False):
   1.334 +            LOG.error(SCAN_WRONG_RETURN_VALUE)
   1.335 +            self.file.close ()
   1.336 +            return -errno.EACCES
   1.337 +        
   1.338 +        if ((retval.has_key("infected") == False) or (retval.has_key("virusname") == False)):
   1.339 +            LOG.error(SCAN_RETURN_VALUE_KEY_MISSING)
   1.340 +            self.file.close ()
   1.341 +            return -errno.EACCES
   1.342 +            
   1.343 +        
   1.344 +        if (retval.get("infected") == True):
   1.345 +            self.file.close ()
   1.346 +            sendNotification(NOTIFICATION_CRITICAL, "%s\nFile: %s\nVirus: %s" %(VIRUS_FOUND, path, retval.get("virusname")))
   1.347 +            LOG.error("%s" %(VIRUS_FOUND,))
   1.348 +            LOG.error("Virus: %s" %(retval.get("virusname"),))
   1.349 +            return -errno.EACCES
   1.350 +        
   1.351 +        whitelisted = whitelistFile (rootPath(self.__rootpath, path))
   1.352 +        if (whitelisted == False):
   1.353 +            self.file.close ()
   1.354 +            sendNotification(NOTIFICATION_CRITICAL, "File not in whitelist. Access denied.")
   1.355 +            return -errno.EACCES
   1.356 +
   1.357 +    def read (self, path, length, offset):
   1.358 +        LOG.debug ("*** read %s %s %s" % (path, length, offset))
   1.359 +        self.file.seek (offset)
   1.360 +        return self.file.read (length)
   1.361 +
   1.362 +    def readlink (self, path):
   1.363 +        LOG.debug ("*** readlink %s" % (path))
   1.364 +        return os.readlink (fixPath (path))
   1.365 +
   1.366 +    def release (self, path, flags):
   1.367 +        LOG.debug ("*** release %s %s" % (path, oct (flags)))
   1.368 +        self.file.flush()
   1.369 +        os.fsync(self.file.fileno())
   1.370 +        self.file.close ()
   1.371 +        
   1.372 +        if (self.written == True):
   1.373 +            hashsum = calcMD5(fixPath(path))
   1.374 +            filesize = os.path.getsize(fixPath(path))
   1.375 +            sendFileLog(path, filesize, hashsum, "md5")
   1.376 +        
   1.377 +
   1.378 +    def rename (self, oldPath, newPath):
   1.379 +        LOG.debug ("*** rename %s %s %s" % (oldPath, newPath, config.get("Main", "ReadOnly")))
   1.380 +        if (config.get("Main", "ReadOnly") == "true"):
   1.381 +            sendReadOnlyNotification()
   1.382 +            return -errno.EACCES
   1.383 +        os.rename (fixPath (oldPath), fixPath (newPath))
   1.384 +
   1.385 +    def rmdir (self, path):
   1.386 +        LOG.debug ("*** rmdir %s %s" % (path, config.get("Main", "ReadOnly")))
   1.387 +        if (config.get("Main", "ReadOnly") == "true"):
   1.388 +            sendReadOnlyNotification()
   1.389 +            return -errno.EACCES
   1.390 +        os.rmdir (fixPath (path))
   1.391 +
   1.392 +    def statfs (self):
   1.393 +        LOG.debug ("*** statfs")
   1.394 +        return os.statvfs(".")
   1.395 +
   1.396 +    def symlink (self, targetPath, linkPath):
   1.397 +        LOG.debug ("*** symlink %s %s %s" % (targetPath, linkPath, config.get("Main", "ReadOnly")))
   1.398 +        if (config.get("Main", "ReadOnly") == "true"):
   1.399 +            sendReadOnlyNotification()
   1.400 +            return -errno.EACCES
   1.401 +        os.symlink (fixPath (targetPath), fixPath (linkPath))
   1.402 +
   1.403 +    def truncate (self, path, length):
   1.404 +        LOG.debug ("*** truncate %s %s %s" % (path, length, config.get("Main", "ReadOnly")))
   1.405 +        if (config.get("Main", "ReadOnly") == "true"):
   1.406 +            sendReadOnlyNotification()
   1.407 +            return -errno.EACCES
   1.408 +        f = open (fixPath (path), "w+")
   1.409 +        f.truncate (length)
   1.410 +        f.close ()
   1.411 +
   1.412 +    def unlink (self, path):
   1.413 +        LOG.debug ("*** unlink %s %s" % (path, config.get("Main", "ReadOnly")))
   1.414 +        if (config.get("Main", "ReadOnly") == "true"):
   1.415 +            sendReadOnlyNotification()
   1.416 +            return -errno.EACCES
   1.417 +        os.unlink (fixPath (path))
   1.418 +
   1.419 +    def utime (self, path, times):
   1.420 +        LOG.debug ("*** utime %s %s" % (path, times))
   1.421 +        os.utime (fixPath (path), times)
   1.422 +
   1.423 +    def write (self, path, buf, offset):
   1.424 +        #LOG.debug ("*** write %s %s %s %s" % (path, buf, offset, config.get("Main", "ReadOnly")))
   1.425 +        LOG.debug ("*** write %s %s %s %s" % (path, "filecontent", offset, config.get("Main", "ReadOnly")))
   1.426 +        if (config.get("Main", "ReadOnly") == "true"):
   1.427 +            self.file.close()
   1.428 +            sendReadOnlyNotification()
   1.429 +            return -errno.EACCES
   1.430 +        self.file.seek (offset)
   1.431 +        self.file.write (buf)
   1.432 +        self.written = True
   1.433 +        return len (buf)
   1.434 +
   1.435 +    def access (self, path, mode):
   1.436 +        LOG.debug ("*** access %s %s" % (path, oct (mode)))
   1.437 +        if not os.access (fixPath (path), mode):
   1.438 +            return -errno.EACCES
   1.439 +
   1.440 +    def create (self, path, flags, mode):
   1.441 +        LOG.debug ("*** create %s %s %s %s %s" % (fixPath (path), oct (flags), oct (mode), flag2mode (flags), config.get("Main", "ReadOnly")))
   1.442 +        if (config.get("Main", "ReadOnly") == "true"):
   1.443 +            sendReadOnlyNotification()
   1.444 +            return -errno.EACCES
   1.445 +
   1.446 +        self.file = os.fdopen (os.open (fixPath (path), flags), flag2mode(flags))
   1.447 +        self.written = True
   1.448 +        self.fd = self.file.fileno ()
   1.449 +
   1.450 +
   1.451 +if __name__ == "__main__":
   1.452 +    # Set api version
   1.453 +    fuse.fuse_python_api = (0, 2)
   1.454 +    fuse.feature_assert ('stateful_files', 'has_init')
   1.455 +
   1.456 +    config = loadConfig ()
   1.457 +    initLog (config)
   1.458 +    
   1.459 +    #sendNotification("Info", "OsecFS started")
   1.460 +    
   1.461 +    # Import the Malware Scanner
   1.462 +    sys.path.append(config.get("Main", "ScannerPath"))
   1.463 +    
   1.464 +    MalwareModule = import_module(config.get("Main", "ScannerModuleName"))
   1.465 +    MalwareClass = getattr(MalwareModule, config.get("Main", "ScannerClassName"))
   1.466 +    
   1.467 +    MalwareScanner = MalwareClass (config.get("Main", "ScannerConfig"));
   1.468 +    
   1.469 +    osecfs = OsecFS (config.get ("Main", "Rootpath"))
   1.470 +    osecfs.flags = 0
   1.471 +    osecfs.multithreaded = 0
   1.472 +
   1.473 +    fuse_args = [sys.argv[0], config.get ("Main", "Mountpoint")];
   1.474 +    osecfs.main (fuse_args)