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
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
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)