Ejemplo n.º 1
0
    def printFiles(self,
                   showDeleted: bool = False,
                   showCreationTime: bool = False,
                   showDocumentsOnly: bool = False,
                   userHome: str = None,
                   showDesignatedOnly: bool = False):
        """Print all the files currently being stored."""

        for key in sorted(self.nameStore, key=lambda s: s.lower()):
            files = self.nameStore[key]
            last = files[-1]  # TODO handle multiple versions
            printpath = last.getName()

            # Print only files accessed by designation, if asked to
            if showDesignatedOnly:
                flags = EventFileFlags.designation
                if not last.getAccesses(flags):
                    continue

            # Print only user documents, if we have a home to compare to
            if showDocumentsOnly and userHome:
                if not last.isUserDocument(userHome, allowHiddenFiles=True):
                    continue

            # Ensure we print folders with a /, and files with leading space
            lastDir = printpath.rfind('/')
            if lastDir > 0:
                printpath = (lastDir + 1) * ' ' + printpath[lastDir + 1:]
                if last.isFolder():
                    printpath += "/"
            elif lastDir == 0 and last.isFolder():
                printpath += "/"

            # Non-deleted files
            if not last.getTimeOfEnd():
                if showCreationTime and last.getTimeOfStart():
                    print("%s\tCREATED on %s" %
                          (printpath, time2Str(last.getTimeOfStart())))
                else:
                    print("%s" % printpath)
            # Deleted files, if the callee wants them too
            elif showDeleted:
                if showCreationTime and last.getTimeOfStart():
                    print("%s\tCREATED on %s, DELETED on %s" %
                          (printpath, time2Str(last.getTimeOfStart()),
                           time2Str(last.getTimeOfEnd())))
                else:
                    print("%s\tDELETED on %s" %
                          (printpath, time2Str(last.getTimeOfEnd())))
 def __str__(self):
     prnt = ("Sql Event: %d\n\tpid: %d\n\ttime: %s\n\tinterpretation: %s\n"
             "\tmanifestation: %s\n\tactor: %s\n\tsubjects:\n" %
             (self.id, self.pid, time2Str(self.timestamp),
              self.interpretation, self.manifestation, self.actor_uri))
     for subject in self.subjects:
         prnt += "%s\n" % subject.__str__()
     return prnt + "\n"
    def simulateAllEvents(self):
        """Simulate all events to instantiate Files in the FileStore."""
        if not self._sorted:
            self.sort()

        fileStore = FileStore.get()
        fileFactory = FileFactory.get()

        # First, parse for Zeitgeist designation events in order to instantiate
        # the designation cache.
        if debugEnabled():
            print("Instantiating Zeitgeist acts of designation...")
        for event in self.store:
            if event.isInvalid():
                continue

            if event.getSource() == EventSource.zeitgeist:
                # The event grants 5 minutes of designation both ways.
                self.desigcache.addItem(event,
                                        start=event.time - 5*60*1000,
                                        duration=10*60*1000)
            # The current Event is an act of designation for future Events
            # related to the same Application and Files. Save it.
            elif event.getFileFlags() & EventFileFlags.designationcache:
                self.desigcache.addItem(event)

        if debugEnabled():
            print("Done. Starting simulation...")
        # Then, dispatch each event to the appropriate handler
        for event in self.store:
            if event.isInvalid():
                continue

            # Designation events are already processed.
            if event.getFileFlags() & EventFileFlags.designationcache:
                continue

            if debugEnabled():
                print("Simulating Event %s from %s at time %s." % (
                    event.evflags, event.actor.uid(), time2Str(event.time)))

            for data in event.data_app:
                if data[2] == FD_OPEN:
                    event.actor.openFD(data[0], data[1], event.time)
                elif data[2] == FD_CLOSE:
                    event.actor.closeFD(data[0], event.time)

            if event.getFileFlags() & EventFileFlags.destroy:
                res = self.simulateDestroy(event, fileFactory, fileStore)

            elif event.getFileFlags() & EventFileFlags.create:
                res = self.simulateCreate(event, fileFactory, fileStore)

            elif event.getFileFlags() & EventFileFlags.overwrite:
                res = self.simulateCreate(event, fileFactory, fileStore)

                # We received a list of files that were created
                if isinstance(res, list):
                    pass
                # We received instructions to hot-patch the event list
                else:
                    raise NotImplementedError  # TODO

            elif event.getFileFlags() & (EventFileFlags.read |
                                         EventFileFlags.write):
                self.simulateAccess(event, fileFactory, fileStore)

            # Keep me last, or use elif guards: I WILL change your event flags!
            elif event.getFileFlags() & EventFileFlags.move or \
                    event.getFileFlags() & EventFileFlags.copy:
                res = self.simulateCopy(event,
                                        fileFactory,
                                        fileStore,
                                        keepOld=event.getFileFlags() &
                                        EventFileFlags.copy)

        # Filter out invalid file descriptor references before computing stats.
        fileStore.purgeFDReferences()
    def simulateCopy(self,
                     event: Event,
                     fileFactory: FileFactory,
                     fileStore: FileStore,
                     keepOld: bool=True):
        """Simulate a file copy or move Event, based on :keepOld:."""
        newFiles = []
        ctype = 'copy' if keepOld else 'move'
        baseFlags = event.evflags

        def _delFile(event: Event, f, read: bool=False):
            event.evflags = (baseFlags |
                             EventFileFlags.write |
                             EventFileFlags.destroy)
            if read:
                event.evflags |= EventFileFlags.read
            res = self.desigcache.checkForDesignation(event, [f])
            fileFactory.deleteFile(f, event.actor, event.time, res[0][1])
            event.evflags = baseFlags

        def _addRead(event: Event, f):
            event.evflags = (baseFlags | EventFileFlags.read)
            res = self.desigcache.checkForDesignation(event, [f])
            f.addAccess(actor=event.actor,
                        flags=res[0][1],
                        time=event.time)
            event.evflags = baseFlags

        def _createCopy(event: Event, oldFile, newPath):
            # Create a file on the new path which is identical to the old File.
            event.evflags = (baseFlags |
                             EventFileFlags.write |
                             EventFileFlags.create |
                             EventFileFlags.overwrite)
            newFile = self.__doCreateFile(newPath,
                                          oldFile.ftype,
                                          event,
                                          fileFactory,
                                          fileStore)
            event.evflags = baseFlags

            # Update the files' links
            oldFile.addFollower(newFile.inode, event.time, ctype)
            newFile.setPredecessor(oldFile.inode, event.time, ctype)
            fileStore.updateFile(oldFile)
            fileStore.updateFile(newFile)

            return newFile

        # Get each file, set its starting time and type, and update the store
        subjects = list((old.path, new.path) for (old, new) in event.getData())
        for (old, new) in subjects:

            # Not legal. 'a' and 'a' are the same file.
            if old == new:
                # TODO DBG?
                continue

            # Get the old file. It must exist, or the simulation is invalid.
            oldFile = fileFactory.getFile(old, event.time)
            if not oldFile:
                raise ValueError("Attempting to move/copy from a file that "
                                 "does not exist: %s at time %d" % (
                                  old,
                                  event.time))

            if debugEnabled():
                print("Info: %s '%s' to '%s' at time %s, by actor %s." % (
                      ctype,
                      old,
                      new,
                      time2Str(event.time),
                      event.actor.uid()))

            # Check if the target is a directory, or a regular file. If it does
            # not exist, it's a regular file.
            newFile = fileFactory.getFileIfExists(new, event.time)
            newIsFolder = newFile and newFile.isFolder()

            # If the target is a directory, we will copy/move inside it.
            sourceIsFolder = oldFile.isFolder()
            targetPath = new if not newIsFolder else \
                new + "/" + oldFile.getFileName()
            # If mv/cp'ing a folder to an existing path, restrictions apply.
            if sourceIsFolder:
                # oldFile is a/, newFile is b/, targetFile is b/a/
                targetFile = fileFactory.getFileIfExists(targetPath,
                                                         event.time)
                targetIsFolder = targetFile and targetFile.isFolder()

                # cannot overwrite non-directory 'b' with directory 'a'
                # cannot overwrite non-directory 'b/a' with directory 'a'
                if targetFile and not targetIsFolder:
                    # TODO DBG?
                    continue

                # mv: cannot move 'a' to 'b/a': Directory not empty
                elif targetIsFolder and ctype == "move":
                    children = fileStore.getChildren(targetFile, event.time)
                    if len(children) == 0:
                        _delFile(event, targetFile)
                    else:
                        # TODO DBG?
                        continue

                # mv or cp would make the target directory here. Our code later
                # on in this function will create a copy of the old file, which
                # means the new folder will be made, with a creation access
                # from the actor that performs the copy event we are analysing.
                elif not targetFile and ctype == "copy":
                    pass

            # When the source is a file, just delete the new target path.
            else:
                targetFile = fileFactory.getFileIfExists(targetPath,
                                                         event.time)
                if targetFile:
                    _delFile(event, targetFile)

            # Collect the children of the source folder.
            children = fileStore.getChildren(oldFile, event.time) if \
                sourceIsFolder else []

            # Make the target file, and link the old and target files.
            _createCopy(event, oldFile, targetPath)
            if ctype == "move":
                _delFile(event, oldFile, read=True)
            else:
                _addRead(event, oldFile)

            # Move or copy the children.
            for child in children:
                childRelPath = child.path[len(oldFile.path)+1:]
                childTargetPath = targetPath + "/" + childRelPath
                childNewFile = None

                # Let the Python purists hang me for that. Iterators will catch
                # appended elements on a mutable list and this is easier to
                # read than other solutions that don't modify the list while it
                # is iterated over.
                subjects.append((child.path, childTargetPath))

            newFiles.append(targetFile)

        return newFiles
Ejemplo n.º 5
0
 def __str__(self):
     """Human-readable version of the File."""
     ret = "<File %d - '%s' created on '%s'%s" % (
         self.inode, self.path, time2Str(self.tstart),
         ", deleted on '%s'" % time2Str(self.tend) if self.tend else '')
     return ret
Ejemplo n.º 6
0
    def checkForDesignation(self, event: Event, files: list):
        """Check for acts of designation that match an Event and its Files.

        Browse the DesignationCache to find acts of designation that match the
        actor of an Event, and a list of Files. If some files are matched, and
        if the act has a different designation / programmatic flag than the
        Event, this function returns updated EventFileFlags for each matching
        File, so that the EventStore simulator records prior acts of
        designation for these Files.

        Returns a list of (File, EventFileFlags) tuples.
        """
        l = self.store.get(event.getActor().uid()) or []
        lChanged = False

        # Bypass Zeitgeist events as they're all by designation.
        if event.getSource() == EventSource.zeitgeist:
            l = []

        # Check latest acts of designation first, and loop till we're done.
        res = []
        for (actIdx, act) in enumerate(reversed(l)):
            # If there are no files left, we can exit.
            if not len(files):
                break

            # If we reach events that have expired, we can get rid of them.
            if act.tend and act.tend < event.time:
                # FIXME review how this index is calculated (l is reversed!)
                # del l[actIdx]
                # lChanged = True
                continue

            # We can't yet rely on this designation cache item.
            if act.tstart > event.time:
                continue

            # Compare the event's flags to the act's.
            crossover = event.getFileFlags() & act.evflags

            #  The event and act already have the same authorisation type.
            if crossover & (EventFileFlags.designation |
                            EventFileFlags.programmatic):
                continue

            # The flags for which the act applies don't match all event flags.
            accesses = event.getFileFlags() & ~(EventFileFlags.designation |
                                                EventFileFlags.programmatic)
            if crossover != accesses:
                # print("Debug: an act of designation was found for Event %s,"
                #       " but the access flags don't match. Event: %s. Act:"
                #       " %s" % (event, accesses, crossover))
                continue

            # From now on we build a return value with new flags for files that
            # match the act of designation.
            auths = act.evflags & (EventFileFlags.designation |
                                   EventFileFlags.programmatic)
            newFlags = EventFileFlags.no_flags
            newFlags |= accesses
            newFlags |= auths

            # Now find Files that match the act of designation's own Files
            for (fIdx, f) in enumerate(files):
                if f.path in act.cmdline:
                    # print("Info: Event '%s' performed on %s by App '%s' on "
                    #       "File '%s' is turned into a %s event based on an "
                    #       "act of designation performed on %s." % (
                    #        event.getFileFlags(),
                    #        time2Str(event.getTime()),
                    #        event.getActor().uid(),
                    #        f.path,
                    #        "designation" if newFlags &
                    #        EventFileFlags.designation else "programmatic",
                    #        time2Str(act.tstart)))
                    res.append((f, newFlags))
                    del files[fIdx]
                elif f.path in [x.path for x in act.files]:
                    print("Info: Event '%s' performed on %s by App '%s' on "
                          "File '%s' is turned into a %s event based on an "
                          "Zeitgeit event performed on %s." % (
                           event.getFileFlags(),
                           time2Str(event.getTime()),
                           event.getActor().uid(),
                           f.path,
                           "designation" if newFlags &
                           EventFileFlags.designation else "programmatic",
                           time2Str(act.tstart)))
                    res.append((f, newFlags))
                    del files[fIdx]
        # Now that we've checked all acts of designation for this Application,
        # check if some files have not matched any act. We return those with
        # the original event flags.
        else:
            for f in files:
                res.append((f, event.getFileFlags()))

        # If we've removed expired acts, we should update the store.
        if lChanged:
            self.store[event.getActor().uid()] = l

        return res
    def _runAttackRound(self, attack: Attack, policy: Policy, acListInst: dict,
                        lookUps: dict, allowedCache: dict):
        """Run an attack round with a set source and time."""
        fileStore = FileStore.get()
        appStore = ApplicationStore.get()
        userConf = UserConfigLoader.get()
        userHome = userConf.getHomeDir()

        seen = set()  # Already seen targets.
        spreadTimes = dict()  # Times from which the attack can spread.

        toSpread = deque()
        toSpread.append(attack.source)
        spreadTimes[attack.source] = attack.time

        # Statistics counters.
        appSet = set()
        userAppSet = set()
        fileCount = 0
        docCount = 0

        if debugEnabled():
            tprnt("Launching attack on %s at time %s %s app memory." %
                  (attack.source if isinstance(attack.source, File) else
                   attack.source.uid(), time2Str(attack.time),
                   "with" if attack.appMemory else "without"))

        def _allowed(policy, f, acc):
            k = (policy, f, acc)
            if k not in allowedCache:
                v = (policy.fileOwnedByApp(f, acc)
                     or policy.allowedByPolicy(f, acc.actor)
                     or policy.accessAllowedByPolicy(f, acc))
                allowedCache[k] = v
                return v
            else:
                return allowedCache[k]

        # As long as there are reachable targets, loop.
        while toSpread:
            current = toSpread.popleft()
            currentTime = spreadTimes[current]

            # When the attack spreads to a File.
            if isinstance(current, File):
                fileCount += 1
                if current.isUserDocument(userHome):
                    docCount += 1
                if debugEnabled():
                    tprnt("File added @%d: %s" % (currentTime, current))

                # Add followers.
                for f in current.follow:
                    if f.time > currentTime:
                        follower = fileStore.getFile(f.inode)
                        if follower not in seen:
                            toSpread.append(follower)
                            seen.add(follower)
                            spreadTimes[follower] = f.time

                # Add future accesses.
                for acc in current.accesses:
                    if acc.time > currentTime and \
                            acc.actor.desktopid not in appSet and \
                            _allowed(policy, current, acc):
                        toSpread.append(acc.actor)
                        spreadTimes[acc.actor] = acc.time

            # When the attack spreads to an app instance.
            elif isinstance(current, Application):
                if debugEnabled():
                    tprnt("App added @%d: %s" % (currentTime, current.uid()))

                # Add files accessed by the app.
                for (accFile, acc) in acListInst.get(current.uid()) or []:
                    if acc.time > currentTime and \
                            accFile not in seen and \
                            _allowed(policy, accFile, acc):
                        toSpread.append(accFile)
                        seen.add(accFile)
                        spreadTimes[accFile] = acc.time

                # Add future versions of the app.
                if attack.appMemory and current.desktopid not in appSet:
                    for app in appStore.lookupDesktopId(current.desktopid):
                        if app.tstart > currentTime:
                            toSpread.append(app)
                            spreadTimes[app] = app.tstart

                # We do this last to use appSet as a cache for already seen
                # apps, so we append all future instances once and for all to
                # the spread list.
                appSet.add(current.desktopid)
                if current.isUserlandApp():
                    userAppSet.add(current.desktopid)

            else:
                print("Error: attack simulator attempting to parse an unknown"
                      " object (%s)" % type(current),
                      file=sys.stderr)

        return (appSet, userAppSet, fileCount, docCount)
    def performAttack(self,
                      policy: Policy,
                      acListInst: dict,
                      lookUps: dict,
                      allowedCache: dict,
                      attackName: str = "none",
                      startingApps: list = [],
                      filePattern: str = None):
        fileStore = FileStore.get()
        appStore = ApplicationStore.get()
        userConf = UserConfigLoader.get()

        msg = "\n\n## Performing attack '%s'\n" % attackName

        # First, check if the proposed attack pattern is applicable to the
        # current participant. If not, return.
        startingPoints = []

        # Case where an app is at the origin of an attack.
        if startingApps:
            consideredApps = []
            for did in startingApps:
                apps = appStore.lookupDesktopId(did)
                if apps:
                    startingPoints.extend(apps)
                    consideredApps.append(did)
            msg += ("Simulating attack starting from an app among %s.\n" %
                    consideredApps)
            # tprnt("Simulating '%s' attack starting from an app among %s." %
            #       (attackName, consideredApps))

            if not startingPoints:
                msg += ("No such app found, aborting attack simulation.")
                # tprnt("No such app found, aborting attack simulation.")
                return (msg, None)

        # Case where a file is at the origin of the attack.
        elif filePattern:
            msg += ("Simulating attack starting from a file matching %s.\n" %
                    filePattern)
            # tprnt("Simulating '%s' attack starting from a file matching %s." %
            #       (attackName, filePattern))

            home = userConf.getHomeDir() or "/MISSING-HOME-DIR"
            desk = userConf.getSetting("XdgDesktopDir") or "~/Desktop"
            down = userConf.getSetting("XdgDownloadsDir") or "~/Downloads"
            user = userConf.getSetting("Username") or "user"
            host = userConf.getSetting("Hostname") or "localhost"
            filePattern = filePattern.replace('@XDG_DESKTOP_DIR@', desk)
            filePattern = filePattern.replace('@XDG_DOWNLOADS_DIR@', down)
            filePattern = filePattern.replace('@USER@', user)
            filePattern = filePattern.replace('@HOSTNAME@', host)
            filePattern = filePattern.replace('~', home)
            # tprnt("\tfinal pattern: %s." % filePattern)
            fileRe = re.compile(filePattern)

            for f in fileStore:
                if fileRe.match(f.path):
                    startingPoints.append(f)

            if not startingPoints:
                msg += ("No such file found, aborting attack simulation.")
                # tprnt("No such file found, aborting attack simulation.")
                return (msg, None)
        else:
            msg += ("No starting point defined, aborting attack simulation.")
            # tprnt("No starting point defined, aborting attack simulation.")
            return (msg, None)

        # Now, roll the attack.
        msg += ("%d starting points found. Performing %d rounds of attacks."
                "\n\n" % (len(startingPoints), AttackSimulator.passCount))
        # tprnt("%d starting points found. Performing %d rounds of attacks." %
        #       (len(startingPoints), AttackSimulator.passCount))

        apps = []
        uapps = []
        wapps = []
        wuapps = []
        files = []
        docs = []

        if AttackSimulator.passCount < len(startingPoints):
            startingIndexes = random.sample(range(len(startingPoints)),
                                            AttackSimulator.passCount)
        else:
            startingIndexes = []
            for i in range(AttackSimulator.passCount):
                startingIndexes.append(random.randrange(len(startingPoints)))

        for i in range(0, AttackSimulator.passCount):
            source = startingPoints[startingIndexes[i]]
            # Files corrupt from the start, apps become corrupt randomly.
            try:
                time = source.tstart if isinstance(source, File) else \
                    random.randrange(source.tstart, source.tend)
            except (ValueError):  # occurs when tstart == tend
                time = source.tstart
            attack = Attack(source=source, time=time, appMem=True)

            msg += ("Pass %d:\tattack on %s at time %s %s app memory.\n" %
                    (i + 1, attack.source if isinstance(attack.source, File)
                     else attack.source.uid(), time2Str(attack.time),
                     "with" if attack.appMemory else "without"))

            (appSet, userAppSet, fileCount, docCount) = \
              self._runAttackRound(attack,
                                   policy,
                                   acListInst,
                                   lookUps,
                                   allowedCache)
            appCount = len(appSet)
            userAppCount = len(userAppSet)

            msg += ("        \t%d apps infected (%s); %d files infected; %d "
                    "user apps infected; %d documents infected.\n\n" %
                    (appCount, appSet, fileCount, userAppCount, docCount))
            # tprnt("Pass %d: %d apps infected; %d files (%d documents)"
            #       " infected" % (i+1, appCount, fileCount, docCount))
            apps.append(appCount)
            uapps.append(userAppCount)
            files.append(fileCount)
            docs.append(docCount)

            # Calculate weighted impact on apps and user apps.
            instCountPerDid = appStore.getInstCountPerApp()
            weightedAppSum = 0
            weightedUAppSum = 0
            for app in appSet:
                weightedAppSum += instCountPerDid[app]
            for app in userAppSet:
                weightedUAppSum += instCountPerDid[app]

            wapps.append(weightedAppSum)
            wuapps.append(weightedUAppSum)

        medApps = statistics.median(apps)
        medUApps = statistics.median(uapps)
        medWApps = statistics.median(wapps)
        medWUApps = statistics.median(wuapps)
        medFiles = statistics.median(files)
        medDocs = statistics.median(docs)
        avgApps = sum(apps) / len(apps)
        avgUApps = sum(uapps) / len(uapps)
        avgWApps = sum(wapps) / len(wapps)
        avgWUApps = sum(wuapps) / len(wuapps)
        avgFiles = sum(files) / len(files)
        avgDocs = sum(docs) / len(docs)
        minApps = min(apps)
        minUApps = min(uapps)
        minWApps = min(wapps)
        minWUApps = min(wuapps)
        minFiles = min(files)
        minDocs = min(docs)
        maxApps = max(apps)
        maxUApps = max(uapps)
        maxWApps = max(wapps)
        maxWUApps = max(wuapps)
        maxFiles = max(files)
        maxDocs = max(docs)

        appCount = appStore.getAppCount()
        userAppCount = appStore.getUserAppCount()
        instCount = len(appStore)
        userInstCount = appStore.getUserInstCount()
        fileCount = len(fileStore)
        docCount = fileStore.getUserDocumentCount(userConf.getHomeDir())

        avgPropApps = avgWUApps / userInstCount
        avgPropFiles = avgDocs / docCount
        avgOfProportions = (avgPropApps + avgPropFiles) / 2

        msg += "\nMin: %.2f (%f%%) apps infected; " \
               "%.2f (%f%%) weighted-apps infected; " \
               "%.2f (%f%%) files infected; " \
               "%.2f (%f%%) user-apps infected; " \
               "%.2f (%f%%) weighted-user-apps infected; " \
               "%.2f (%f%%) documents infected\n" % (
                minApps, 100* minApps / appCount,
                minWApps, 100* minWApps / instCount,
                minFiles, 100* minFiles / fileCount,
                minUApps, 100* minUApps / userAppCount,
                minWUApps, 100* minWUApps / userInstCount,
                minDocs, 100* minDocs / docCount)
        msg += "Max: %.2f (%f%%) apps infected; " \
               "%.2f (%f%%) weighted-apps infected; " \
               "%.2f (%f%%) files infected; " \
               "%.2f (%f%%) user-apps infected; " \
               "%.2f (%f%%) weighted-user-apps infected; " \
               "%.2f (%f%%) documents infected\n" % (
                maxApps, 100* maxApps / appCount,
                maxWApps, 100* maxWApps / instCount,
                maxFiles, 100* maxFiles / fileCount,
                maxUApps, 100* maxUApps / userAppCount,
                maxWUApps, 100* maxWUApps / userInstCount,
                maxDocs, 100* maxDocs / docCount)
        msg += "Avg: %.2f (%f%%) apps infected; " \
               "%.2f (%f%%) weighted-apps infected; " \
               "%.2f (%f%%) files infected; " \
               "%.2f (%f%%) user-apps infected; " \
               "%.2f (%f%%) weighted-user-apps infected; " \
               "%.2f (%f%%) documents infected\n" % (
                avgApps, 100* avgApps / appCount,
                avgWApps, 100* avgWApps / instCount,
                avgFiles, 100* avgFiles / fileCount,
                avgUApps, 100* avgUApps / userAppCount,
                avgWUApps, 100* avgWUApps / userInstCount,
                avgDocs, 100* avgDocs / docCount)
        msg += "Med: %.2f (%f%%) apps infected; " \
               "%.2f (%f%%) weighted-apps infected; " \
               "%.2f (%f%%) files infected; " \
               "%.2f (%f%%) user-apps infected; " \
               "%.2f (%f%%) weighted-user-apps infected; " \
               "%.2f (%f%%) documents infected\n" % (
                medApps, 100* medApps / appCount,
                medWApps, 100* medWApps / instCount,
                medFiles, 100* medFiles / fileCount,
                medUApps, 100* medUApps / userAppCount,
                medWUApps, 100* medWUApps / userInstCount,
                medDocs, 100* medDocs / docCount)

        return (msg, avgOfProportions)