diff -r 000000000000 -r 23028352807f osecfs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/osecfs Tue Nov 04 16:28:40 2014 +0100 @@ -0,0 +1,471 @@ +#!/usr/bin/python + +# ------------------------------------------------------------ +# opensecurity package file +# +# Autor: X-Net Services GmbH +# +# Copyright 2013-2014 X-Net and AIT Austrian Institute of Technology +# +# +# X-Net Technologies GmbH +# Elisabethstrasse 1 +# 4020 Linz +# AUSTRIA +# https://www.x-net.at +# +# AIT Austrian Institute of Technology +# Donau City Strasse 1 +# 1220 Wien +# AUSTRIA +# http://www.ait.ac.at +# +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------ + + +from fuse import Fuse +import fuse + +import ConfigParser + +import sys + +import logging +import os +import errno +import time + +from importlib import import_module + + +import subprocess + +import urllib3 +import netifaces +import netaddr +import hashlib + + +sys.stderr = open('/var/log/osecfs_error.log', 'a+') + + +MINOPTS = { "Main" : ["Logfile", "LogLevel", "Mountpoint", "Rootpath", "ScannerPath", "ScannerModuleName", "ScannerClassName", "ScannerConfig", "ReadOnly"]} + +CONFIG_NOT_READABLE = "Configfile is not readable" +CONFIG_WRONG = "Something is wrong with the config" +CONFIG_MISSING = "Section: \"%s\" Option: \"%s\" in configfile is missing" +SCAN_WRONG_RETURN_VALUE = "The return Value of the malware scanner is wrong. Has to be an dictionary" +SCAN_RETURN_VALUE_KEY_MISSING = "The dictionary has to include key \"infected\" (True, False) and \"virusname\" (String)" +VIRUS_FOUND = "Virus found. Access denied" +NOTIFICATION_CRITICAL = "critical" +NOTIFICATION_INFO = "info" +LOG = None +MalwareScanner = None +STATUS_CODE_OK = 200 + +SYSTEM_FILE_COMMAND = "file" +httpPool = urllib3.PoolManager(num_pools = 1, timeout = 3) + +def checkMinimumOptions (config): + for section, options in MINOPTS.iteritems (): + for option in options: + if (config.has_option(section, option) == False): + print (CONFIG_MISSING % (section, option)) + exit (129) + +def printUsage (): + print ("Usage:") + print ("%s configfile mountpath ro/rw" % (sys.argv[0])) + exit (128) + +def loadConfig (): + print ("load config") + + if (len (sys.argv) < 4): + printUsage () + + configfile = sys.argv[1] + config = ConfigParser.SafeConfigParser () + + if ((os.path.exists (configfile) == False) or (os.path.isfile (configfile) == False) or (os.access (configfile, os.R_OK) == False)): + print (CONFIG_NOT_READABLE) + printUsage () + + try: + config.read (sys.argv[1]) + except Exception, e: + print (CONFIG_WRONG) + print ("Error: %s" % (e)) + + + config.set("Main", "Mountpoint", sys.argv[2]) + if (sys.argv[3] == "rw"): + config.set("Main", "ReadOnly", "false") + else: + config.set("Main", "ReadOnly", "true") + + checkMinimumOptions (config) + + return config + +def initLog (config): + print ("init log") + + global LOG + logfile = config.get("Main", "Logfile") + + numeric_level = getattr(logging, config.get("Main", "LogLevel").upper(), None) + if not isinstance(numeric_level, int): + raise ValueError('Invalid log level: %s' % loglevel) + + # ToDo move log level and maybe other things to config file + logging.basicConfig( + level = numeric_level, + format = "%(asctime)s %(name)-12s %(funcName)-15s %(levelname)-8s %(message)s", + datefmt = "%Y-%m-%d %H:%M:%S", + filename = logfile, + filemode = "a+", + ) + LOG = logging.getLogger("fuse_main") + + +def fixPath (path): + return ".%s" % (path) + +def rootPath (rootpath, path): + return "%s%s" % (rootpath, path) + +def flag2mode (flags): + md = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'} + m = md[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)] + + # windows sets append even if it would overwrite the whole file (seek 0) + # so ignore append option + #if flags | os.O_APPEND: + # m = m.replace('w', 'a', 1) + + return m + +def scanFile (path, fileobject): + LOG.debug ("Scan File \"%s\" with malware Scanner" %(path,) ) + return MalwareScanner.scanFile (path, fileobject) + + +def scanFileClamAV (path): + infected = False + + LOG.debug ("Scan File: %s" % (path)) + + result = pyclamav.scanfile (path) + LOG.debug ("Result of file \"%s\": %s" % (path, result)) + if (result[0] != 0): + infected = True + + if (infected == True): + LOG.error ("Virus found, deny Access %s" % (result,)) + + return infected + +def whitelistFile (path): + whitelisted = False; + + LOG.debug ("Execute \"%s\" command on \"%s\"" %(SYSTEM_FILE_COMMAND, path)) + + result = None + try: + result = subprocess.check_output ([SYSTEM_FILE_COMMAND, path]); + # ToDo replace with real whitelist + whitelisted = True + except Exception as e: + LOG.error ("Call returns with an error!") + LOG.error (e) + + LOG.debug ("Type: %s" %(result)) + + return whitelisted + +def sendDataToRest (urlpath, data): + netifaces.ifaddresses("eth0")[2][0]["addr"] + + # Get first address in network (0 = network ip -> 192.168.0.0) + remote_ip = netaddr.IPNetwork("%s/%s" %(netifaces.ifaddresses("eth0")[2][0]["addr"], netifaces.ifaddresses("eth0")[2][0]["netmask"]))[1] + + url = ("http://%s:8090//%s" %(remote_ip, urlpath)) + + LOG.debug ("Send data to \"%s\"" %(url, )) + LOG.debug ("Data: %s" %(data, )) + + try: + response = httpPool.request_encode_body("POST", url, fields=data, retries=0) + except Exception, e: + LOG.error("Remote host not reachable") + LOG.error ("Exception: %s" %(e,)) + return + + if response.status == STATUS_CODE_OK: + LOG.info("Data sent successfully to rest server") + return True + else: + LOG.error("Server returned errorcode: %s" %(response.status,)) + return False + + +def sendNotification (type, message): + data = {"msgtype" : type, "text" : message} + + if (type == "information"): + sendDataToRest ("message", data) + else: + sendDataToRest ("notification", data) + +def sendReadOnlyNotification(): + sendNotification("critical", "Filesystem is in read only mode. If you want to export files please initialize an encrypted filesystem.") + +def sendLogNotPossibleNotification(): + sendNotification("critical", "Send log entry to opensecurity rest server failed.") + +def sendFileLog(filename, filesize, filehash, hashtype): + data = {"filename" : filename, "filesize" : "%s" %(filesize,), "filehash" : filehash, "hashtype" : hashtype} + retval = sendDataToRest ("log", data) + if (retval == False): + sendLogNotPossibleNotification() + +def calcMD5 (path, block_size=256*128, hr=True): + md5 = hashlib.md5() + with open(path,'rb') as f: + for chunk in iter(lambda: f.read(block_size), b''): + md5.update(chunk) + if hr: + return md5.hexdigest() + return md5.digest() + +class OsecFS (Fuse): + + __rootpath = None + + # default fuse init + def __init__(self, rootpath, *args, **kw): + self.__rootpath = rootpath + Fuse.__init__ (self, *args, **kw) + LOG.debug ("Init complete.") + sendNotification("information", "Filesystem successfully mounted.") + + # defines that our working directory will be the __rootpath + def fsinit(self): + os.chdir (self.__rootpath) + + def getattr(self, path): + LOG.debug ("*** getattr (%s)" % (fixPath (path))) + return os.lstat (fixPath (path)); + + def getdir(self, path): + LOG.debug ("*** getdir (%s)" % (path)); + return os.listdir (fixPath (path)) + + def readdir(self, path, offset): + LOG.debug ("*** readdir (%s %s)" % (path, offset)); + for e in os.listdir (fixPath (path)): + yield fuse.Direntry(e) + + def chmod (self, path, mode): + LOG.debug ("*** chmod %s %s" % (path, oct(mode))) + if (config.get("Main", "ReadOnly") == "true"): + sendReadOnlyNotification() + return -errno.EACCES + os.chmod (fixPath (path), mode) + + def chown (self, path, uid, gid): + LOG.debug ("*** chown %s %s %s" % (path, uid, gid)) + if (config.get("Main", "ReadOnly") == "true"): + sendReadOnlyNotification() + return -errno.EACCES + os.chown (fixPath (path), uid, gid) + + def link (self, targetPath, linkPath): + LOG.debug ("*** link %s %s" % (targetPath, linkPath)) + if (config.get("Main", "ReadOnly") == "true"): + sendReadOnlyNotification() + return -errno.EACCES + os.link (fixPath (targetPath), fixPath (linkPath)) + + def mkdir (self, path, mode): + LOG.debug ("*** mkdir %s %s" % (path, oct(mode))) + if (config.get("Main", "ReadOnly") == "true"): + sendReadOnlyNotification() + return -errno.EACCES + os.mkdir (fixPath (path), mode) + + def mknod (self, path, mode, dev): + LOG.debug ("*** mknod %s %s %s" % (path, oct (mode), dev)) + if (config.get("Main", "ReadOnly") == "true"): + sendReadOnlyNotification() + return -errno.EACCES + os.mknod (fixPath (path), mode, dev) + + # to implement virus scan + def open (self, path, flags): + LOG.debug ("*** open %s %s" % (path, oct (flags))) + self.file = os.fdopen (os.open (fixPath (path), flags), flag2mode (flags)) + self.written = False + self.fd = self.file.fileno () + + LOG.debug(self.__rootpath) + LOG.debug(path) + + retval = scanFile (rootPath(self.__rootpath, path), self.file) + + #if type(retval) is not dict: + if (isinstance(retval, dict) == False): + LOG.error(SCAN_WRONG_RETURN_VALUE) + self.file.close () + return -errno.EACCES + + if ((retval.has_key("infected") == False) or (retval.has_key("virusname") == False)): + LOG.error(SCAN_RETURN_VALUE_KEY_MISSING) + self.file.close () + return -errno.EACCES + + + if (retval.get("infected") == True): + self.file.close () + sendNotification(NOTIFICATION_CRITICAL, "%s\nFile: %s\nVirus: %s" %(VIRUS_FOUND, path, retval.get("virusname"))) + LOG.error("%s" %(VIRUS_FOUND,)) + LOG.error("Virus: %s" %(retval.get("virusname"),)) + return -errno.EACCES + + whitelisted = whitelistFile (rootPath(self.__rootpath, path)) + if (whitelisted == False): + self.file.close () + sendNotification(NOTIFICATION_CRITICAL, "File not in whitelist. Access denied.") + return -errno.EACCES + + def read (self, path, length, offset): + LOG.debug ("*** read %s %s %s" % (path, length, offset)) + self.file.seek (offset) + return self.file.read (length) + + def readlink (self, path): + LOG.debug ("*** readlink %s" % (path)) + return os.readlink (fixPath (path)) + + def release (self, path, flags): + LOG.debug ("*** release %s %s" % (path, oct (flags))) + self.file.flush() + os.fsync(self.file.fileno()) + self.file.close () + + if (self.written == True): + hashsum = calcMD5(fixPath(path)) + filesize = os.path.getsize(fixPath(path)) + sendFileLog(path, filesize, hashsum, "md5") + + + def rename (self, oldPath, newPath): + LOG.debug ("*** rename %s %s %s" % (oldPath, newPath, config.get("Main", "ReadOnly"))) + if (config.get("Main", "ReadOnly") == "true"): + sendReadOnlyNotification() + return -errno.EACCES + os.rename (fixPath (oldPath), fixPath (newPath)) + + def rmdir (self, path): + LOG.debug ("*** rmdir %s %s" % (path, config.get("Main", "ReadOnly"))) + if (config.get("Main", "ReadOnly") == "true"): + sendReadOnlyNotification() + return -errno.EACCES + os.rmdir (fixPath (path)) + + def statfs (self): + LOG.debug ("*** statfs") + return os.statvfs(".") + + def symlink (self, targetPath, linkPath): + LOG.debug ("*** symlink %s %s %s" % (targetPath, linkPath, config.get("Main", "ReadOnly"))) + if (config.get("Main", "ReadOnly") == "true"): + sendReadOnlyNotification() + return -errno.EACCES + os.symlink (fixPath (targetPath), fixPath (linkPath)) + + def truncate (self, path, length): + LOG.debug ("*** truncate %s %s %s" % (path, length, config.get("Main", "ReadOnly"))) + if (config.get("Main", "ReadOnly") == "true"): + sendReadOnlyNotification() + return -errno.EACCES + f = open (fixPath (path), "w+") + f.truncate (length) + f.close () + + def unlink (self, path): + LOG.debug ("*** unlink %s %s" % (path, config.get("Main", "ReadOnly"))) + if (config.get("Main", "ReadOnly") == "true"): + sendReadOnlyNotification() + return -errno.EACCES + os.unlink (fixPath (path)) + + def utime (self, path, times): + LOG.debug ("*** utime %s %s" % (path, times)) + os.utime (fixPath (path), times) + + def write (self, path, buf, offset): + #LOG.debug ("*** write %s %s %s %s" % (path, buf, offset, config.get("Main", "ReadOnly"))) + LOG.debug ("*** write %s %s %s %s" % (path, "filecontent", offset, config.get("Main", "ReadOnly"))) + if (config.get("Main", "ReadOnly") == "true"): + self.file.close() + sendReadOnlyNotification() + return -errno.EACCES + self.file.seek (offset) + self.file.write (buf) + self.written = True + return len (buf) + + def access (self, path, mode): + LOG.debug ("*** access %s %s" % (path, oct (mode))) + if not os.access (fixPath (path), mode): + return -errno.EACCES + + def create (self, path, flags, mode): + LOG.debug ("*** create %s %s %s %s %s" % (fixPath (path), oct (flags), oct (mode), flag2mode (flags), config.get("Main", "ReadOnly"))) + if (config.get("Main", "ReadOnly") == "true"): + sendReadOnlyNotification() + return -errno.EACCES + + self.file = os.fdopen (os.open (fixPath (path), flags), flag2mode(flags)) + self.written = True + self.fd = self.file.fileno () + + +if __name__ == "__main__": + # Set api version + fuse.fuse_python_api = (0, 2) + fuse.feature_assert ('stateful_files', 'has_init') + + config = loadConfig () + initLog (config) + + #sendNotification("Info", "OsecFS started") + + # Import the Malware Scanner + sys.path.append(config.get("Main", "ScannerPath")) + + MalwareModule = import_module(config.get("Main", "ScannerModuleName")) + MalwareClass = getattr(MalwareModule, config.get("Main", "ScannerClassName")) + + MalwareScanner = MalwareClass (config.get("Main", "ScannerConfig")); + + osecfs = OsecFS (config.get ("Main", "Rootpath")) + osecfs.flags = 0 + osecfs.multithreaded = 0 + + fuse_args = [sys.argv[0], config.get ("Main", "Mountpoint")]; + osecfs.main (fuse_args)