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