From 56fd7b0a07f54f7a69b91f855fb82e5bc04a3418 Mon Sep 17 00:00:00 2001 From: Patrick Wardle Date: Tue, 18 Dec 2018 21:13:23 -1000 Subject: [PATCH] version 2.0 (beta?) cli interface --- AppDelegate.h | 39 +- AppDelegate.m | 198 ++++------ Consts.h | 5 + FlaggedItemWindowController.m | 4 +- ItemView.m | 31 +- Items/Binary.h | 5 + Items/Binary.m | 74 +++- Items/File.h | 6 +- Items/File.m | 53 ++- Items/ItemBase.m | 18 +- PrefsWindowController.m | 38 -- Queue.h | 14 +- Queue.m | 50 +-- SearchWindowController.m | 26 +- Task.h | 2 +- Task.m | 114 +++--- TaskEnumerator.h | 14 +- TaskEnumerator.m | 86 ++--- TaskExplorer-Info.plist | 6 +- TaskExplorer.xcodeproj/project.pbxproj | 2 + .../UserInterfaceState.xcuserstate | Bin 90288 -> 65313 bytes .../xcschemes/TaskExplorer.xcscheme | 6 + TaskTableController.m | 16 +- UI/FlatView.xib | 24 +- UI/TreeView.xib | 10 +- UI/VTInfoWindow.xib | 34 +- Utilities.h | 10 + Utilities.m | 161 ++++++++ VTButton.m | 22 +- VTInfoWindowController.m | 22 +- VirusTotal.h | 6 +- VirusTotal.m | 82 ++-- main.h | 53 +++ main.m | 357 +++++++++++++++++- remoteTaskService/Info.plist | 4 +- 35 files changed, 1082 insertions(+), 510 deletions(-) create mode 100644 main.h diff --git a/AppDelegate.h b/AppDelegate.h index c7cf856..5fed390 100755 --- a/AppDelegate.h +++ b/AppDelegate.h @@ -24,6 +24,16 @@ #import "FlaggedItemWindowController.h" #import "RequestRootWindowController.h" +/* GLOBALS */ + +//shared enumerator +extern TaskEnumerator* taskEnumerator; + +//shared virustotal object +extern VirusTotal* virusTotal; + +//network connected flag +extern BOOL isConnected; @interface AppDelegate : NSObject { @@ -36,9 +46,6 @@ //start time @property NSTimeInterval startTime; -//connection flag -@property BOOL isConnected; - //'filter task' search box // ->top pane @property (weak) IBOutlet NSSearchField *filterTasksBox; @@ -46,13 +53,9 @@ //(current) bottom view controller @property(nonatomic, retain)TaskTableController *bottomViewController; +//top view/pane @property (weak) IBOutlet NSView *topPane; -//@property (weak) IBOutlet NSScrollView *taskScrollView; - -//task enumerator object -@property(nonatomic, retain)TaskEnumerator* taskEnumerator; - //task table controller object @property (nonatomic, retain)TaskTableController *taskTableController; @@ -83,9 +86,6 @@ @property (weak) IBOutlet NSButton *logoButton; - -//@property (weak) IBOutlet NSButton *showPreferencesButton; - //spinner @property (weak) IBOutlet NSProgressIndicator *progressIndicator; @@ -95,9 +95,6 @@ //filter object @property(nonatomic, retain)Filter* filterObj; -//virus total object -@property(nonatomic, retain)VirusTotal* virusTotalObj; - //array for all virus total threads @property(nonatomic, retain)NSMutableArray* vtThreads; @@ -152,9 +149,6 @@ //top constraint @property(nonatomic, retain)NSLayoutConstraint* trailingConstraint; -//flagged items -@property(nonatomic, retain)NSMutableArray* flaggedItems; - //flag for filter field (autocomplete) @property BOOL completePosting; @@ -196,13 +190,6 @@ // ->query OS to refresh/reload all tasks -(IBAction)refreshTasks:(id)sender; -//callback when user has updated prefs -// ->reload table, etc -//-(void)applyPreferences; - -//button handler for when settings icon (gear) is clicked -//-(IBAction)showPreferences:(id)sender; - //button handler for logo -(IBAction)logoButtonHandler:(id)sender; @@ -241,10 +228,6 @@ //display (in separate popup) all flagged items -(IBAction)showFlaggedItems:(id)sender; -//save a flagged binary -// ->also set text flagged items button label to red --(void)saveFlaggedBinary:(Binary*)binary; - //callback for custom search fields // ->handle auto-complete filterings -(void)filterAutoComplete:(NSTextView*)textField; diff --git a/AppDelegate.m b/AppDelegate.m index d827e4d..5845505 100755 --- a/AppDelegate.m +++ b/AppDelegate.m @@ -10,8 +10,8 @@ #import "Consts.h" #import "Binary.h" #import "Update.h" -#import "Connection.h" #import "Utilities.h" +#import "Connection.h" #import "AppDelegate.h" #import "serviceInterface.h" #import "TaskTableController.h" @@ -29,13 +29,9 @@ @synthesize bottomPane; @synthesize saveButton; @synthesize currentTask; -@synthesize isConnected; -@synthesize flaggedItems; @synthesize searchButton; @synthesize viewSelector; @synthesize scannerThread; -@synthesize virusTotalObj; -@synthesize taskEnumerator; @synthesize taskViewFormat; @synthesize commandHandling; @synthesize completePosting; @@ -57,18 +53,18 @@ //kick off main window/logic -(void)taskExplore { + //make foreground so it has an dock icon, etc + transformProcess(kProcessTransformToForegroundApplication); + //for autolayout [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"NSConstraintBasedLayoutVisualizeMutuallyExclusiveConstraints"]; //init virus total object - virusTotalObj = [[VirusTotal alloc] init]; + virusTotal = [[VirusTotal alloc] init]; //init filter obj filterObj = [[Filter alloc] init]; - //alloc flagged items - flaggedItems = [NSMutableArray array]; - //alloc/init custom search field for tasks customTasksFilter = [[CustomTextField alloc] init]; @@ -123,7 +119,7 @@ if(YES != [self isAuthenticated]) { //display auth popup - // ->will invoke 'go' method on successful auth + // will invoke 'go' method on successful auth [self askForRoot]; } //go! @@ -171,12 +167,9 @@ // ->main entry point -(void)applicationDidFinishLaunching:(NSNotification *)notification { - //init crash reporting - initCrashReporting(); - //first time run? // show thanks to friends window! - // note: on close invokes method to show main window + // note: on close, invokes method to show main window if(YES != [[NSUserDefaults standardUserDefaults] boolForKey:NOT_FIRST_TIME]) { //set key @@ -209,8 +202,10 @@ //register handler for hot keys -(void)registerKeypressHandler { - NSEvent * (^keypressHandler)(NSEvent *); + //handler + NSEvent * (^keypressHandler)(NSEvent *) = nil; + //handler block keypressHandler = ^NSEvent * (NSEvent * theEvent){ return [self handleKeypress:theEvent]; @@ -440,7 +435,7 @@ bail: -(void)exploreTasks { //alloc task enumerator - if(nil == self.taskEnumerator) + if(nil == taskEnumerator) { //alloc taskEnumerator = [[TaskEnumerator alloc] init]; @@ -448,7 +443,7 @@ bail: //kick off thread to enum task // ->will update table as results come in - [NSThread detachNewThreadSelector:@selector(enumerateTasks) toTarget:self.taskEnumerator withObject:nil]; + [NSThread detachNewThreadSelector:@selector(enumerateTasks) toTarget:taskEnumerator withObject:nil]; return; } @@ -532,10 +527,10 @@ bail: if(YES != self.taskTableController.isFiltered) { //sync to get row - @synchronized (self.taskEnumerator.tasks) + @synchronized (taskEnumerator.tasks) { //get row - row = [self.taskEnumerator.tasks indexOfKey:((Task*)item).pid]; + row = [taskEnumerator.tasks indexOfKey:((Task*)item).pid]; } } //filtering @@ -816,11 +811,11 @@ bail: if(YES != self.taskTableController.isFiltered) { //sync - @synchronized(self.taskEnumerator.tasks) + @synchronized(taskEnumerator.tasks) { //get tasks - tasks = self.taskEnumerator.tasks; + tasks = taskEnumerator.tasks; //reload each row w/ new VT info for(NSNumber* taskPid in tasks) @@ -871,6 +866,35 @@ bail: [self reloadRow:binary]; } + //main thread + //update 'flagged' item + // ...method might be invoked by VT callback, etc + dispatch_async(dispatch_get_main_queue(), ^{ + + //flagged items? + // set icon to red + if(0 != taskEnumerator.flaggedItems.count) + { + //set main image + self.flaggedButton.image = [NSImage imageNamed:@"flaggedRed"]; + + //set alternate image + self.flaggedButton.alternateImage = [NSImage imageNamed:@"flaggedRedBG"]; + } + //no flagged items + // set icon back to default + else + { + //set main image + self.flaggedButton.image = [NSImage imageNamed:@"flagged"]; + + //set alternate image + self.flaggedButton.alternateImage = [NSImage imageNamed:@"flaggedBG"]; + } + + }); + + return; } @@ -965,7 +989,7 @@ bail: -(void)reloadTaskTable { //sort tasks - [self sortTasksForView:self.taskEnumerator.tasks]; + [self sortTasksForView:taskEnumerator.tasks]; //when exec'ing on background thread // ->exec on main thread @@ -991,62 +1015,6 @@ bail: return; } -//callback when user has updated prefs -// ->reload table, etc --(void)applyPreferences -{ - //currently selected category - //NSUInteger selectedCategory = 0; - - /* - - //get currently selected category - //selectedCategory = self.categoryTableController.categoryTableView.selectedRow; - - //reload category table - [self.categoryTableController customReload]; - - //reloading the category table resets the selected plugin - // ->so manually (re)set it here - self.selectedPlugin = self.plugins[selectedCategory]; - - //reload item table - [self.taskTableController.itemTableView reloadData]; - - //if VT query was never done (e.g. scan was started w/ pref disabled) - // ->kick off VT queries now - if( (0 == self.vtThreads.count) && - (YES != self.prefsWindowController.disableVTQueries) ) - { - //iterate over all plugins - // ->do VT query for each - for(PluginBase* plugin in self.plugins) - { - //do query - [self queryVT:plugin]; - } - } - - //save results? - // ->if there was a previous scan - if( (nil != self.scannerThread) && - (YES == self.prefsWindowController.shouldSaveNow)) - { - //save - [self saveResults]; - - //alloc/init alert - saveAlert = [NSAlert alertWithMessageText:[NSString stringWithFormat:@"current results saved to %@", OUTPUT_FILE] defaultButton:@"OK" alternateButton:nil otherButton:nil informativeTextWithFormat:@"subsequent scans will overwrite this file"]; - - //show it - [saveAlert runModal]; - } - - */ - - return; -} - //automatically invoked when window is closing // ->terminate app -(void)windowWillClose:(NSNotification *)notification @@ -1128,7 +1096,7 @@ bail: else if(FLAGGED_BUTTON_TAG == tag) { //when no flagged items - if(0 == self.flaggedItems.count) + if(0 == taskEnumerator.flaggedItems.count) { //set imageName = @"flagged"; @@ -1175,7 +1143,7 @@ bail: else if(FLAGGED_BUTTON_TAG == tag) { //when no flagged items - if(0 == self.flaggedItems.count) + if(0 == taskEnumerator.flaggedItems.count) { //set imageName = @"flaggedOver"; @@ -1250,14 +1218,14 @@ bail: [output appendString:@"{\"tasks:\":["]; //sync - @synchronized(self.taskEnumerator.tasks) + @synchronized(taskEnumerator.tasks) { //get tasks - for(NSNumber* taskPid in self.taskEnumerator.tasks) + for(NSNumber* taskPid in taskEnumerator.tasks) { //append task JSON - [output appendFormat:@"{%@},", [self.taskEnumerator.tasks[taskPid] toJSON]]; + [output appendFormat:@"{%@},", [taskEnumerator.tasks[taskPid] toJSON]]; } }//sync @@ -1281,7 +1249,6 @@ bail: //init popup w/ error msg saveResultPopup = [NSAlert alertWithMessageText:@"ERROR: failed to save output" defaultButton:@"Ok" alternateButton:nil otherButton:nil informativeTextWithFormat:@"details: %@", error]; - } //happy // ->set result msg @@ -1506,7 +1473,7 @@ bail: ( (YES == self.taskTableController.isFiltered) && (0 != self.taskTableController.filteredItems.count) ) ) { //set to first - self.currentTask = self.taskEnumerator.tasks[[self.taskEnumerator.tasks keyAtIndex:0]]; + self.currentTask = taskEnumerator.tasks[[taskEnumerator.tasks keyAtIndex:0]]; } } @@ -1576,7 +1543,7 @@ bail: //(re)enumerate dylibs via XPC // ->triggers table reload when done - [self.currentTask enumerateDylibs:self.taskEnumerator.dylibs shouldWait:NO]; + [self.currentTask enumerateDylibs:taskEnumerator.dylibs shouldWait:NO]; break; @@ -1731,7 +1698,7 @@ bail: [self.taskTableController.filteredItems removeAllObjects]; //normal filter - [self.filterObj filterTasks:search.string items:self.taskEnumerator.tasks results:self.taskTableController.filteredItems pane:PANE_TOP]; + [self.filterObj filterTasks:search.string items:taskEnumerator.tasks results:self.taskTableController.filteredItems pane:PANE_TOP]; }//sync @@ -1835,8 +1802,20 @@ bail: //maks self.filteringOverlay.layer.masksToBounds = YES; - //set overlay's view color to gray - self.filteringOverlay.layer.backgroundColor = NSColor.grayColor.CGColor; + //dark mode + // set overlay to light + if(YES == isDarkMode()) + { + //light gray + self.filteringOverlay.layer.backgroundColor = NSColor.lightGrayColor.CGColor; + } + //light mode + // set overlay to gray + else + { + //gray + self.filteringOverlay.layer.backgroundColor = NSColor.grayColor.CGColor; + } //make it semi-transparent self.filteringOverlay.alphaValue = 0.95; @@ -2084,43 +2063,6 @@ bail: return; } -//save a flagged item -// ->also set text flagged items button label to red --(void)saveFlaggedBinary:(Binary*)binary -{ - //first check if item is already flagged - if(YES == [self.flaggedItems containsObject:binary]) - { - //no need to add - // ->so bail - goto bail; - } - - //sync to save - @synchronized(self.flaggedItems) - { - //save - [self.flaggedItems addObject:binary]; - } - - //when count is 1 - // ->means first flagged file so set image to red - if(1 == self.flaggedItems.count) - { - //set main image - [self.flaggedButton setImage:[NSImage imageNamed:@"flaggedRed"]]; - - //set alternate image - [self.flaggedButton setAlternateImage:[NSImage imageNamed:@"flaggedRedBG"]]; - - } - -//bail -bail: - - return; -} - //button handle for 'flagged items' button // ->display (in separate popup) all flagged items -(IBAction)showFlaggedItems:(id)sender @@ -2130,7 +2072,7 @@ bail: //handle case where there aren't any flagged items // ->just show alert - if(0 == self.flaggedItems.count) + if(0 == taskEnumerator.flaggedItems.count) { //alloc/init alert alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:@"No items flagged by VirusTotal"] defaultButton:@"Ok" alternateButton:nil otherButton:nil informativeTextWithFormat:@"hooray! 😇"]; @@ -2321,7 +2263,7 @@ bail: [self finalizeFiltration:PANE_TOP]; //filter - [self.filterObj filterTasks:filterString items:self.taskEnumerator.tasks results:self.taskTableController.filteredItems pane:PANE_TOP]; + [self.filterObj filterTasks:filterString items:taskEnumerator.tasks results:self.taskTableController.filteredItems pane:PANE_TOP]; } //refresh/update UI when not a keyword search diff --git a/Consts.h b/Consts.h index 905b315..ab27086 100644 --- a/Consts.h +++ b/Consts.h @@ -30,6 +30,11 @@ //success #define STATUS_SUCCESS 0 +//user name +#define USER_NAME @"userName" + +//user (home) directory +#define USER_DIRECTORY @"userDirectory" //signers enum Signer{None, Apple, AppStore, DevID, AdHoc}; diff --git a/FlaggedItemWindowController.m b/FlaggedItemWindowController.m index feed9c7..53ff96b 100644 --- a/FlaggedItemWindowController.m +++ b/FlaggedItemWindowController.m @@ -64,14 +64,14 @@ } //populate flagged items array - for(Binary* flaggedItem in ((AppDelegate*)[[NSApplication sharedApplication] delegate]).flaggedItems) + for(Binary* flaggedItem in taskEnumerator.flaggedItems) { //when binary is task binary // ->find/save all tasks instances if(YES == flaggedItem.isTaskBinary) { //get all tasks instances - flaggedTasks = [((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator tasksForBinary:flaggedItem]; + flaggedTasks = [taskEnumerator tasksForBinary:flaggedItem]; //sync to save @synchronized(self.flaggedItems) diff --git a/ItemView.m b/ItemView.m index de84b04..ecd8b5c 100644 --- a/ItemView.m +++ b/ItemView.m @@ -306,8 +306,8 @@ NSAttributedString* initBinaryString(id item, BOOL isSearchWindow) //add name [taskString appendAttributedString:[[NSMutableAttributedString alloc] initWithString:binary.name]]; - //init gray color for pid - attributes = [NSDictionary dictionaryWithObject:NSColor.grayColor forKey:NSForegroundColorAttributeName]; + //init default color for pid + attributes = [NSDictionary dictionaryWithObject:NSColor.controlTextColor forKey:NSForegroundColorAttributeName]; //search window // ->only for tasks, since dylibs in search window are handled elsewhere ('loaded in') @@ -334,8 +334,7 @@ NSAttributedString* initBinaryString(id item, BOOL isSearchWindow) (YES == binary.isPacked) ) { //init color for comma, etc - // ->light gray - attributes = [NSDictionary dictionaryWithObject:NSColor.grayColor forKey:NSForegroundColorAttributeName]; + attributes = [NSDictionary dictionaryWithObject:NSColor.controlTextColor forKey:NSForegroundColorAttributeName]; //tasks //add comma string @@ -376,7 +375,7 @@ NSAttributedString* initBinaryString(id item, BOOL isSearchWindow) (YES != binary.notFound)) { //init color for closing - attributes = [NSDictionary dictionaryWithObject:NSColor.grayColor forKey:NSForegroundColorAttributeName]; + attributes = [NSDictionary dictionaryWithObject:NSColor.controlTextColor forKey:NSForegroundColorAttributeName]; //close string [taskString appendAttributedString:[[NSMutableAttributedString alloc] initWithString:@")" attributes:attributes]]; @@ -392,8 +391,7 @@ NSAttributedString* initBinaryString(id item, BOOL isSearchWindow) (YES == binary.isPacked) ) { //init color for comma, - // ->light gray - attributes = [NSDictionary dictionaryWithObject:NSColor.grayColor forKey:NSForegroundColorAttributeName]; + attributes = [NSDictionary dictionaryWithObject:NSColor.controlTextColor forKey:NSForegroundColorAttributeName]; //add [taskString appendAttributedString:[[NSAttributedString alloc] initWithString:@", " attributes:attributes]]; @@ -402,8 +400,7 @@ NSAttributedString* initBinaryString(id item, BOOL isSearchWindow) else { //init color for comma, etc - // ->light gray - attributes = [NSDictionary dictionaryWithObject:NSColor.grayColor forKey:NSForegroundColorAttributeName]; + attributes = [NSDictionary dictionaryWithObject:NSColor.controlTextColor forKey:NSForegroundColorAttributeName]; //tasks //add comma string @@ -427,14 +424,14 @@ NSAttributedString* initBinaryString(id item, BOOL isSearchWindow) attributes = [NSDictionary dictionaryWithObject:[NSColor redColor] forKey:NSForegroundColorAttributeName]; //add - [taskString appendAttributedString:[[NSAttributedString alloc] initWithString:@"not found" attributes:attributes]]; + [taskString appendAttributedString:[[NSAttributedString alloc] initWithString:@"deleted" attributes:attributes]]; //dylib, need to close string here // ->normally it doesn't have anything after... if(YES != [item isKindOfClass:[Task class]]) { //init color for closing - attributes = [NSDictionary dictionaryWithObject:NSColor.grayColor forKey:NSForegroundColorAttributeName]; + attributes = [NSDictionary dictionaryWithObject:NSColor.controlTextColor forKey:NSForegroundColorAttributeName]; //close string [taskString appendAttributedString:[[NSMutableAttributedString alloc] initWithString:@")" attributes:attributes]]; @@ -448,8 +445,7 @@ NSAttributedString* initBinaryString(id item, BOOL isSearchWindow) if(YES == [item isKindOfClass:[Task class]]) { //init color for closing - // ->light gray - attributes = [NSDictionary dictionaryWithObject:NSColor.grayColor forKey:NSForegroundColorAttributeName]; + attributes = [NSDictionary dictionaryWithObject:NSColor.controlTextColor forKey:NSForegroundColorAttributeName]; //close string [taskString appendAttributedString:[[NSMutableAttributedString alloc] initWithString:@")" attributes:attributes]]; @@ -478,7 +474,7 @@ NSAttributedString* initLoadedInString(id item) //get host tasks // ->works with dylibs or files - tasks = [((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator loadedIn:item]; + tasks = [taskEnumerator loadedIn:item]; //dylibs/files // ->add name @@ -497,8 +493,7 @@ NSAttributedString* initLoadedInString(id item) } //init color for 'loaded in...' - // ->light gray - attributes = [NSDictionary dictionaryWithObject:NSColor.grayColor forKey:NSForegroundColorAttributeName]; + attributes = [NSDictionary dictionaryWithObject:NSColor.controlTextColor forKey:NSForegroundColorAttributeName]; //add dylib indicator //-> '(dylib, loaded in: ... ' @@ -851,8 +846,8 @@ void configVTButton(NSTableCellView *itemCell, id owner, Binary* binary) //set string (vt ratio), with attributes [vtButton setAttributedTitle:[[NSAttributedString alloc] initWithString:vtDetectionRatio attributes:stringAttributes]]; - //set color (gray) - stringAttributes[NSForegroundColorAttributeName] = NSColor.grayColor; + //set color + stringAttributes[NSForegroundColorAttributeName] = NSColor.controlTextColor; //set selected text color [vtButton setAttributedAlternateTitle:[[NSAttributedString alloc] initWithString:vtDetectionRatio attributes:stringAttributes]]; diff --git a/Items/Binary.h b/Items/Binary.h index 525442d..15937c6 100644 --- a/Items/Binary.h +++ b/Items/Binary.h @@ -31,6 +31,10 @@ //flag for task (main) executable @property BOOL isTaskBinary; +//loaded in +// ...for dylibs only +@property(nonatomic, retain)NSArray* loadedIn; + //hashes (md5, sha1) @property(nonatomic, retain)NSDictionary* hashes; @@ -49,6 +53,7 @@ //not found @property BOOL notFound; + /* VIRUS TOTAL INFO */ //dictionary returned by VT diff --git a/Items/Binary.m b/Items/Binary.m index 437686e..e414668 100644 --- a/Items/Binary.m +++ b/Items/Binary.m @@ -22,6 +22,7 @@ @synthesize parser; @synthesize vtInfo; @synthesize isPacked; +@synthesize loadedIn; @synthesize notFound; @synthesize isEncrypted; @synthesize signingInfo; @@ -52,6 +53,8 @@ //determine if its on disk self.notFound = ![[NSFileManager defaultManager] fileExistsAtPath:self.path]; + //get attributes + self.attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:self.path error:nil]; } //bail @@ -217,13 +220,8 @@ bail: //computes hashes // ->set 'md5' and 'sha1' iVars self.hashes = hashFile(self.path); - - //call into filter object to check if file is known - // ->apple-signed or whitelisted - //self.isTrusted = [((AppDelegate*)[[NSApplication sharedApplication] delegate]).filterObj isTrustedFile:self]; - + return; - } //format the signing info dictionary @@ -356,10 +354,10 @@ bail: -(NSString*)toJSON { //json string - NSString *json = nil; + NSMutableString *json = nil; //json data - // ->for intermediate conversions + // for intermediate conversions NSData *jsonData = nil; //hashes @@ -371,19 +369,25 @@ bail: //VT detection ratio NSString* vtDetectionRatio = nil; + //tasks loaded in + NSMutableArray* taskPids = nil; + + //'loaded in' list + NSString* tasks = nil; + //init file hash to default string - // ->used when hashes are nil, or serialization fails + // used when hashes are nil, or serialization fails fileHashes = @"\"unknown\""; //init file signature to default string - // ->used when signatures are nil, or serialization fails + // used when signatures are nil, or serialization fails fileSigs = @"\"unknown\""; //convert hashes to JSON if(nil != self.hashes) { //convert hash dictionary - // ->wrap since we are serializing JSON + // wrap since we are serializing JSON @try { //convert @@ -395,7 +399,7 @@ bail: } } //ignore exceptions - // ->file hashes will just be 'unknown' + // file hashes will just be 'unknown' @catch(NSException *exception) { ; @@ -406,7 +410,7 @@ bail: if(nil != self.signingInfo) { //convert signing dictionary - // ->wrap since we are serializing JSON + // wrap since we are serializing JSON @try { //convert @@ -424,15 +428,55 @@ bail: ; } } + + //dylibs + //covert 'loaded in' array to JSON + if(YES != isTaskBinary) + { + //init + taskPids = [NSMutableArray array]; + + //tasks loaded in + for(Task* task in self.loadedIn) + { + //add pid + [taskPids addObject:task.pid]; + } + //wrap since we are serializing JSON + @try + { + //convert + jsonData = [NSJSONSerialization dataWithJSONObject:taskPids options:kNilOptions error:NULL]; + if(nil != jsonData) + { + //convert data to string + tasks = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + } + } + //ignore exceptions + // ->file sigs will just be 'unknown' + @catch(NSException *exception) + { + ; + } + } + //init VT detection ratio vtDetectionRatio = [NSString stringWithFormat:@"%lu/%lu", (unsigned long)[self.vtInfo[VT_RESULTS_POSITIVES] unsignedIntegerValue], (unsigned long)[self.vtInfo[VT_RESULTS_TOTAL] unsignedIntegerValue]]; //init json - json = [NSString stringWithFormat:@"\"name\": \"%@\", \"path\": \"%@\", \"hashes\": %@, \"signature(s)\": %@, \"VT detection\": \"%@\", \"encrypted\": %d, \"packed\": %d, \"not found\": %d", self.name, self.path, fileHashes, fileSigs, vtDetectionRatio, self.isEncrypted, self.isPacked, self.notFound]; + json = [NSMutableString stringWithFormat:@"\"name\": \"%@\", \"path\": \"%@\", \"hashes\": %@, \"signature(s)\": %@, \"VT detection\": \"%@\", \"encrypted\": %d, \"packed\": %d, \"deleted\": %d", self.name, self.path, fileHashes, fileSigs, vtDetectionRatio, self.isEncrypted, self.isPacked, self.notFound]; + + //dylibs + // add tasks they are loaded in + if(YES != self.isTaskBinary) + { + //add + [json appendString:[NSString stringWithFormat:@", \"loaded in\": \"%@\"", tasks]]; + } return json; } - @end diff --git a/Items/File.h b/Items/File.h index 1473ada..657775c 100644 --- a/Items/File.h +++ b/Items/File.h @@ -10,6 +10,10 @@ #import +/* GLOBALS */ + +//(privacy) protected directories +extern NSArray* protectedDirectories; @interface File : ItemBase { @@ -35,6 +39,4 @@ // ->invokes 'file' cmd, the parses out result -(void)setFileType; - - @end diff --git a/Items/File.m b/Items/File.m index 756752c..e27da8f 100644 --- a/Items/File.m +++ b/Items/File.m @@ -16,12 +16,11 @@ @synthesize type; - //init method -(id)initWithParams:(NSDictionary*)params { //super - // ->saves path, etc + // saves path, etc self = [super initWithParams:params]; if(self) { @@ -31,18 +30,22 @@ //set icon self.icon = [[NSWorkspace sharedWorkspace] iconForFile:self.path]; - //grab attributes - self.attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:self.path error:nil]; + //check if protected + // if not, get file attrs + if(YES != [self isProtected]) + { + //get attrs + self.attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:self.path error:nil]; + } } - -//bail + bail: return self; } //set file type -// ->invokes 'file' cmd, the parses out result +// invokes 'file' cmd, the parses out result -(void)setFileType { //results from 'file' cmd @@ -82,7 +85,6 @@ bail: // ->also trim whitespace self.type = [parsedResults[1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; -//bail bail: return; @@ -100,14 +102,14 @@ bail: } //override method -// ->hash +// hash the file -(NSUInteger)hash { return [self.path hash]; } //override method -// ->equality check +// file equality check (path) -(BOOL)isEqual:(id)object { //flag @@ -143,12 +145,36 @@ bail: goto bail; } -//bail bail: return objEqual; } +//check if file is protected +// on mojave+, need to avoid prompts +-(BOOL)isProtected +{ + //flag + BOOL protected = NO; + + //skip any files in (privacy) protected directories + // as otherwise we will generate a privacy prompt (on Mojave) + for(NSString* directory in protectedDirectories) + { + //check + if(YES == [self.path hasPrefix:directory]) + { + //set flag + protected = YES; + + //done + break; + } + } + + return protected; +} + //convert object to JSON string -(NSString*)toJSON { @@ -162,7 +188,7 @@ bail: attributesJSON = [NSMutableString string]; //when attributes are nil - // ->init default string + // init default string, 'unknown' if(nil == self.attributes) { //init @@ -170,7 +196,7 @@ bail: } //file has attributes - // ->add each + // add each one to json else { //start @@ -208,5 +234,4 @@ bail: return json; } - @end diff --git a/Items/ItemBase.m b/Items/ItemBase.m index 82c58fd..9450ef9 100644 --- a/Items/ItemBase.m +++ b/Items/ItemBase.m @@ -29,10 +29,6 @@ //extract/save path self.path = params[KEY_RESULT_PATH]; - - //get attributes - // ->based off path - self.attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:self.path error:nil]; } return self; @@ -50,20 +46,10 @@ /* OPTIONAL METHODS */ - /* REQUIRED METHODS */ //stubs for inherited methods -// ->all just throw exceptions as they should be implemented in sub-classes - -//scan --(void)scan:(NSDictionary*)scanOptions -{ - @throw [NSException exceptionWithName:kExceptName - reason:[NSString stringWithFormat:kErrFormat, NSStringFromSelector(_cmd), [self class]] - userInfo:nil]; - return; -} +// throw exceptions as they should be implemented in sub-classes //convert object to JSON string -(NSString*)toJSON @@ -74,4 +60,4 @@ return nil; } -@end \ No newline at end of file +@end diff --git a/PrefsWindowController.m b/PrefsWindowController.m index f5af828..4dfb59d 100644 --- a/PrefsWindowController.m +++ b/PrefsWindowController.m @@ -82,44 +82,6 @@ return; } -/* -//save prefs --(void)savePrefs -{ - //first, any prefs changed, a 'save' set - // ->set 'save now' flag - if( ((self.showTrustedItems != self.showTrustedItemsBtn.state) || - (self.disableVTQueries != self.disableVTQueriesBtn.state) || - (self.saveOutput != self.saveOutputBtn.state) ) && - (YES == self.saveOutputBtn.state) ) - { - //set - self.shouldSaveNow = YES; - } - //don't save - else - { - //unset - self.shouldSaveNow = NO; - } - - //save hiding OS components flag - self.showTrustedItems = self.showTrustedItemsBtn.state; - - //save disabling VT flag - self.disableVTQueries = self.disableVTQueriesBtn.state; - - //save save output flag - self.saveOutput = self.saveOutputBtn.state; - - //call back up into app delegate for filtering/hiding OS components - [((AppDelegate*)[[NSApplication sharedApplication] delegate]) applyPreferences]; - - return; -} -*/ - - //'OK' button handler // ->save prefs and close window -(IBAction)closeWindow:(id)sender diff --git a/Queue.h b/Queue.h index 53f8ac4..bae2edb 100644 --- a/Queue.h +++ b/Queue.h @@ -6,7 +6,6 @@ // Copyright (c) 2014 Synack. All rights reserved. // - //from: https://github.com/esromneb/ios-queue-object/blob/master/NSMutableArray%2BQueueAdditions.h #import @@ -27,20 +26,27 @@ /* PROPERTIES */ +//items in +@property NSUInteger itemsIn; + +//items out +@property NSUInteger itemsOut; //event queue @property(retain, atomic)NSMutableArray* eventQueue; - +//thread to process events @property (nonatomic, retain)NSThread* qProcessorThread; -@property (nonatomic, retain)NSCondition* queueCondition; +//condition for queue +@property (nonatomic, retain)NSCondition* queueCondition; //METHODS //add an object to the queue -(void)enqueue:(id)anObject; - +//process events from queue +-(void)processQueue:(id)threadParam; @end diff --git a/Queue.m b/Queue.m index e6222a1..8d6954c 100644 --- a/Queue.m +++ b/Queue.m @@ -14,6 +14,8 @@ @implementation Queue +@synthesize itemsIn; +@synthesize itemsOut; @synthesize eventQueue; @synthesize queueCondition; @synthesize qProcessorThread; @@ -50,12 +52,6 @@ // ->don't want UI thread, etc to suffer [NSThread sleepForTimeInterval:5.0f]; - //VT object - VirusTotal* vtObject = nil; - - //grab VT object - vtObject = ((AppDelegate*)[[NSApplication sharedApplication] delegate]).virusTotalObj; - //for ever while(YES) { @@ -75,37 +71,30 @@ //get item off queue binary = [eventQueue dequeue]; - //sanity check - if(YES != [binary isKindOfClass:[Binary class]]) - { - //ignore - continue; - } - - //process - //->for now, just hash, etc - [binary generateDetailedInfo]; - - //when connected - // ->add item for VT processing - if(YES == ((AppDelegate*)[[NSApplication sharedApplication] delegate]).isConnected) - { - //add - [vtObject addItem:binary]; - } + //inc + itemsOut++; //unlock [self.queueCondition unlock]; - //pool + //generate hashes, etc + [binary generateDetailedInfo]; + + //when connected + // add item for VT processing + if(YES == isConnected) + { + //add + [virusTotal addItem:binary]; } + + } //pool }//foreverz process queue return; } - //add an object to the queue -(void)enqueue:(id)anObject { @@ -115,6 +104,9 @@ //add to queue [self.eventQueue enqueue:anObject]; + //inc + itemsIn++; + //signal [self.queueCondition signal]; @@ -124,10 +116,4 @@ return; } -//process binary --(void)processBinary:(Binary*)binary -{ - -} - @end diff --git a/SearchWindowController.m b/SearchWindowController.m index 594c6c3..0cfd106 100644 --- a/SearchWindowController.m +++ b/SearchWindowController.m @@ -85,7 +85,7 @@ { //not done? // ->make spinner keep spinning - if(ENUMERATION_STATE_COMPLETE != ((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator.state) + if(ENUMERATION_STATE_COMPLETE != taskEnumerator.state) { //(re)start [self.activityIndicator startAnimation:nil]; @@ -115,7 +115,7 @@ //still enumerating? // ->show the spinner - if(ENUMERATION_STATE_COMPLETE != ((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator.state) + if(ENUMERATION_STATE_COMPLETE != taskEnumerator.state) { //show self.activityIndicator.hidden = NO; @@ -163,7 +163,7 @@ }); //done? - if(ENUMERATION_STATE_COMPLETE == ((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator.state) + if(ENUMERATION_STATE_COMPLETE == taskEnumerator.state) { //stop spinner [self.activityIndicator stopAnimation:nil]; @@ -180,7 +180,7 @@ -(void)showEnumerationState { //set status - switch(((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator.state) { + switch(taskEnumerator.state) { //tasks case ENUMERATION_STATE_TASKS: @@ -533,8 +533,20 @@ bail: //maks self.overlay.layer.masksToBounds = YES; - //set overlay's view color to gray - self.overlay.layer.backgroundColor = NSColor.grayColor.CGColor; + //dark mode + // set overlay to light + if(YES == isDarkMode()) + { + //set overlay's view color to gray + self.overlay.layer.backgroundColor = NSColor.lightGrayColor.CGColor; + } + //light mode + // set overlay to gray + else + { + //set to gray + self.overlay.layer.backgroundColor = NSColor.grayColor.CGColor; + } //make it semi-transparent self.overlay.alphaValue = 0.95; @@ -553,7 +565,7 @@ bail: //grab all tasks // make copy to avoid threading issues - allTasks = [((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator.tasks copy]; + allTasks = [taskEnumerator.tasks copy]; //kick off filtering in background // will call back into to refresh UI when done diff --git a/Task.h b/Task.h index 9d21b3e..0e95827 100644 --- a/Task.h +++ b/Task.h @@ -91,6 +91,6 @@ struct dyld_image_info_32 { -(void)enumerateNetworking:(BOOL)shouldWait; //convert self to JSON string --(NSString*)toJSON; +-(NSString*)toJSON:(BOOL)detailed; @end diff --git a/Task.m b/Task.m index 22617e0..3b7d123 100644 --- a/Task.m +++ b/Task.m @@ -51,7 +51,7 @@ if(nil != self) { //grab existings binaries - existingBinaries = ((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator.executables; + existingBinaries = taskEnumerator.executables; //since root UID is zero // ->init UID to -1 @@ -111,7 +111,7 @@ //add to queue // ->this will process in background - [((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator.binaryQueue enqueue:self.binary]; + [taskEnumerator.binaryQueue enqueue:self.binary]; //sync @synchronized(existingBinaries) @@ -362,7 +362,7 @@ bail: //add to queue // ->this will trigger background processing - [((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator.binaryQueue enqueue:dylib]; + [taskEnumerator.binaryQueue enqueue:dylib]; //add to list of new dylibs // ->will allow for post processing @@ -492,7 +492,7 @@ bail: //reset existing files [self.files removeAllObjects]; - + //create/add all files for(NSMutableDictionary* fileDescriptor in fileDescriptors) { @@ -509,7 +509,7 @@ bail: [self.files addObject:file]; } } - + //sort by name self.files = [[self.files sortedArrayUsingComparator:^NSComparisonResult(id a, id b) { @@ -630,7 +630,7 @@ bail: } //convert self to JSON string --(NSString*)toJSON +-(NSString*)toJSON:(BOOL)detailed { //json string NSString *json = nil; @@ -738,63 +738,75 @@ bail: //init VT detection ratio vtDetectionRatio = [NSString stringWithFormat:@"%lu/%lu", (unsigned long)[self.binary.vtInfo[VT_RESULTS_POSITIVES] unsignedIntegerValue], (unsigned long)[self.binary.vtInfo[VT_RESULTS_TOTAL] unsignedIntegerValue]]; - //sync - @synchronized(self.dylibs) + //detailed? + // generate full json + if(YES == detailed) { - //convert all dylibs and add - for(Binary* dylib in self.dylibs) + //sync + @synchronized(self.dylibs) { - //convert/add - [dylibsJSON appendFormat:@"{%@},", [dylib toJSON]]; + //convert all dylibs and add + for(Binary* dylib in self.dylibs) + { + //convert/add + [dylibsJSON appendFormat:@"{%@},", [dylib toJSON]]; + } } - } - - //remove last ',' - if(YES == [dylibsJSON hasSuffix:@","]) - { - //remove - [dylibsJSON deleteCharactersInRange:NSMakeRange([dylibsJSON length]-1, 1)]; - } - - //sync - @synchronized(self.files) - { - //convert all file and add - for(File* file in self.files) + + //remove last ',' + if(YES == [dylibsJSON hasSuffix:@","]) { - //convert/add - [filesJSON appendFormat:@"{%@},", [file toJSON]]; + //remove + [dylibsJSON deleteCharactersInRange:NSMakeRange([dylibsJSON length]-1, 1)]; } - } - - //remove last ',' - if(YES == [filesJSON hasSuffix:@","]) - { - //remove - [filesJSON deleteCharactersInRange:NSMakeRange([filesJSON length]-1, 1)]; - } - - //sync - @synchronized(self.connections) - { - //convert all connections and add - for(Connection* connection in self.connections) + + //sync + @synchronized(self.files) { - //convert/add - [connectionsJSON appendFormat:@"{%@},", [connection toJSON]]; + //convert all file and add + for(File* file in self.files) + { + //convert/add + [filesJSON appendFormat:@"{%@},", [file toJSON]]; + } } + + //remove last ',' + if(YES == [filesJSON hasSuffix:@","]) + { + //remove + [filesJSON deleteCharactersInRange:NSMakeRange([filesJSON length]-1, 1)]; + } + + //sync + @synchronized(self.connections) + { + //convert all connections and add + for(Connection* connection in self.connections) + { + //convert/add + [connectionsJSON appendFormat:@"{%@},", [connection toJSON]]; + } + } + + //remove last ',' + if(YES == [connectionsJSON hasSuffix:@","]) + { + //remove + [connectionsJSON deleteCharactersInRange:NSMakeRange([connectionsJSON length]-1, 1)]; + } + + //init json + json = [NSString stringWithFormat:@"\"name\": \"%@\", \"path\": \"%@\", \"pid\": \"%@\", \"command line\": \"%@\", \"hashes\": %@, \"signature(s)\": %@, \"VT detection\": \"%@\", \"encrypted\": %d, \"packed\": %d, \"not found\": %d, \"dylibs\": [%@], \"files\": [%@], \"connections\": [%@]", self.binary.name, self.binary.path, self.pid, taskCommandLine, fileHashes, fileSigs, vtDetectionRatio, self.binary.isEncrypted, self.binary.isPacked, self.binary.notFound, dylibsJSON, filesJSON, connectionsJSON]; } - //remove last ',' - if(YES == [connectionsJSON hasSuffix:@","]) + //basic + else { - //remove - [connectionsJSON deleteCharactersInRange:NSMakeRange([connectionsJSON length]-1, 1)]; + //init json + json = [NSString stringWithFormat:@"\"name\": \"%@\", \"path\": \"%@\", \"pid\": \"%@\", \"command line\": \"%@\", \"hashes\": %@, \"signature(s)\": %@, \"VT detection\": \"%@\", \"encrypted\": %d, \"packed\": %d, \"not found\": %d", self.binary.name, self.binary.path, self.pid, taskCommandLine, fileHashes, fileSigs, vtDetectionRatio, self.binary.isEncrypted, self.binary.isPacked, self.binary.notFound]; } - //init json - json = [NSString stringWithFormat:@"\"name\": \"%@\", \"path\": \"%@\", \"pid\": \"%@\", \"command line\": \"%@\", \"hashes\": %@, \"signature(s)\": %@, \"VT detection\": \"%@\", \"encrypted\": %d, \"packed\": %d, \"not found\": %d, \"dylibs\": [%@], \"files\": [%@], \"connections\": [%@]", self.binary.name, self.binary.path, self.pid, taskCommandLine, fileHashes, fileSigs, vtDetectionRatio, self.binary.isEncrypted, self.binary.isPacked, self.binary.notFound, dylibsJSON, filesJSON, connectionsJSON]; - return json; } diff --git a/TaskEnumerator.h b/TaskEnumerator.h index 5d61894..5940905 100644 --- a/TaskEnumerator.h +++ b/TaskEnumerator.h @@ -6,12 +6,21 @@ // // +#import "Task.h" #import "Queue.h" +#import "Consts.h" +#import "Signing.h" +#import "Utilities.h" +#import "Connection.h" #import "3rdParty/OrderedDictionary.h" +#import +#import +#import +#import +#import #import - @interface TaskEnumerator : NSObject { @@ -28,6 +37,9 @@ //all dylibs @property(nonatomic, retain)NSMutableDictionary* dylibs; +//flagged items +@property(nonatomic, retain)NSMutableArray* flaggedItems; + //queue // ->contains binaries that should be processed @property (nonatomic, retain)Queue* binaryQueue; diff --git a/TaskEnumerator.m b/TaskEnumerator.m index 836eb93..b1e25b1 100644 --- a/TaskEnumerator.m +++ b/TaskEnumerator.m @@ -6,17 +6,6 @@ // // -#import -#import -#import -#import -#import - -#import "Task.h" -#import "Consts.h" -#import "Signing.h" -#import "Utilities.h" -#import "Connection.h" #import "AppDelegate.h" #import "TaskEnumerator.h" @@ -27,6 +16,7 @@ @synthesize dylibs; @synthesize binaryQueue; @synthesize executables; +@synthesize flaggedItems; //init -(id)init @@ -44,6 +34,9 @@ //alloc dylibs dictionary dylibs = [NSMutableDictionary dictionary]; + //alloc flagged items + flaggedItems = [NSMutableArray array]; + //init binary processing queue binaryQueue = [[Queue alloc] init]; } @@ -53,7 +46,7 @@ //enumerate all tasks -// ->calls back into app delegate to update task (top) table when pau +// calls back into app delegate to update task (top) table when pau -(void)enumerateTasks { //(new) task item @@ -71,10 +64,6 @@ //set state self.state = ENUMERATION_STATE_TASKS; - //determine if network is connected - // ->sets 'isConnected' flag - ((AppDelegate*)[[NSApplication sharedApplication] delegate]).isConnected = isNetworkConnected(); - //get all tasks // ->pids and binary obj with just path/name newTasks = [self getAllTasks]; @@ -138,13 +127,18 @@ //reload task table [((AppDelegate*)[[NSApplication sharedApplication] delegate]) reloadTaskTable]; - //call on main thread - dispatch_sync(dispatch_get_main_queue(), ^{ + //reload bottom pain + // call on main thread + if(YES != [NSThread isMainThread]) + { + //main thread + dispatch_sync(dispatch_get_main_queue(), ^{ - //reload bottom pane - [((AppDelegate*)[[NSApplication sharedApplication] delegate]) selectBottomPaneContent:nil]; - - }); + //reload bottom pane + [((AppDelegate*)[[NSApplication sharedApplication] delegate]) selectBottomPaneContent:nil]; + + }); + } //for new tasks // now generate signing info/encryption check/packer check @@ -226,7 +220,7 @@ //nap [NSThread sleepForTimeInterval:0.01]; } - + //set state self.state = ENUMERATION_STATE_FILES; @@ -234,7 +228,6 @@ count = 0; //begin file enumeration - // ->for search view for(NSNumber* key in newTasks) { //get task @@ -264,7 +257,7 @@ //nap [NSThread sleepForTimeInterval:0.01]; } - + //set state self.state = ENUMERATION_STATE_NETWORK; @@ -272,7 +265,6 @@ count = 0; //begin network enumeration - // ->for search view for(NSNumber* key in newTasks) { //get task @@ -544,10 +536,10 @@ bail: } //sync to remove from all executables - @synchronized(((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator.executables) + @synchronized(taskEnumerator.executables) { //remove dead executables - [((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator.executables removeObjectForKey:deadTask.binary.path]; + [taskEnumerator.executables removeObjectForKey:deadTask.binary.path]; } //get parent @@ -576,7 +568,7 @@ bail: for(Binary* dylib in deadTask.dylibs) { //skip dylibs that aren't flagged - if(YES != [((AppDelegate*)[[NSApplication sharedApplication] delegate]).flaggedItems containsObject:dylib]) + if(YES != [taskEnumerator.flaggedItems containsObject:dylib]) { //skip continue; @@ -596,15 +588,15 @@ bail: //dylib is flagged and only hosted in dead task // ->remove it from flaggedItems - @synchronized(((AppDelegate*)[[NSApplication sharedApplication] delegate]).flaggedItems) + @synchronized(taskEnumerator.flaggedItems) { //remove - [((AppDelegate*)[[NSApplication sharedApplication] delegate]).flaggedItems removeObject:dylib]; + [taskEnumerator.flaggedItems removeObject:dylib]; } } //also remove task if its flagged and only instance - if(YES == [((AppDelegate*)[[NSApplication sharedApplication] delegate]).flaggedItems containsObject:deadTask.binary]) + if(YES == [taskEnumerator.flaggedItems containsObject:deadTask.binary]) { //get number of task instances // ->might be more (flagged) instances that are still alive @@ -625,17 +617,17 @@ bail: if(1 == taskInstances) { //sync and remove - @synchronized(((AppDelegate*)[[NSApplication sharedApplication] delegate]).flaggedItems) + @synchronized(taskEnumerator.flaggedItems) { //remove - [((AppDelegate*)[[NSApplication sharedApplication] delegate]).flaggedItems removeObject:deadTask.binary]; + [taskEnumerator.flaggedItems removeObject:deadTask.binary]; } } } //when there are no flagged items // ->(re)set flagged icon to black - if(0 == ((AppDelegate*)[[NSApplication sharedApplication] delegate]).flaggedItems.count) + if(0 == taskEnumerator.flaggedItems.count) { //set main image [((AppDelegate*)[[NSApplication sharedApplication] delegate]).flaggedButton setImage:[NSImage imageNamed:@"flagged"]]; @@ -706,7 +698,6 @@ bail: return matchingTasks; } - //get all tasks a dylib/file is loaded into -(NSMutableArray*)loadedIn:(id)item { @@ -771,8 +762,8 @@ bail: if(YES == isDylib) { //sync - //@synchronized(task.dylibs) - //{ + @synchronized(task.dylibs) + { //check if dylib is loaded in task for(Binary* taskDylib in task.dylibs) { @@ -787,7 +778,7 @@ bail: } } - //}//sync + }//sync }//dylibs @@ -795,8 +786,8 @@ bail: else if(YES == isFile) { //sync - //@synchronized(task.files) - //{ + @synchronized(task.files) + { //check if file is loaded in task for(File* taskFile in task.files) { @@ -810,7 +801,7 @@ bail: break; } } - //}//sync + }//sync }//files @@ -818,8 +809,8 @@ bail: else if(YES == isConnection) { //sync - //@synchronized(task.connections) - //{ + @synchronized(task.connections) + { //check if connection is 'in' task for(Connection* taskConnection in task.connections) { @@ -834,12 +825,10 @@ bail: break; } } - //}//sync + }//sync }//connections - - - + }//all tasks }//sync @@ -850,5 +839,4 @@ bail: return hostTasks; } - @end diff --git a/TaskExplorer-Info.plist b/TaskExplorer-Info.plist index 30df5da..8d20bfe 100755 --- a/TaskExplorer-Info.plist +++ b/TaskExplorer-Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.7.0 + 2.0.0 CFBundleSignature ???? CFBundleVersion - 1.7.0 + 2.0.0 LSMinimumSystemVersion ${MACOSX_DEPLOYMENT_TARGET} NSHumanReadableCopyright @@ -30,5 +30,7 @@ MainMenu NSPrincipalClass NSApplicationKeyEvents + LSUIElement + diff --git a/TaskExplorer.xcodeproj/project.pbxproj b/TaskExplorer.xcodeproj/project.pbxproj index b5991b8..9814197 100755 --- a/TaskExplorer.xcodeproj/project.pbxproj +++ b/TaskExplorer.xcodeproj/project.pbxproj @@ -222,6 +222,7 @@ CD1508DE21BB13980081F1AF /* Signing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Signing.h; sourceTree = SOURCE_ROOT; }; CD1D14EB21B7A5DB00FF7F4B /* Sentry.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sentry.framework; path = Carthage/Build/Mac/Sentry.framework; sourceTree = ""; }; CD1D14F221B8F2FC00FF7F4B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = SOURCE_ROOT; }; + CD24FE7621C6FC3D00900B61 /* main.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = main.h; sourceTree = SOURCE_ROOT; }; CD3F4CE61AF5CF68002A2647 /* TaskEnumerator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TaskEnumerator.m; sourceTree = SOURCE_ROOT; }; CD3F4CE71AF5CF68002A2647 /* TaskEnumerator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TaskEnumerator.h; sourceTree = SOURCE_ROOT; }; CD3F4CE91AF6D948002A2647 /* OrderedDictionary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OrderedDictionary.h; path = 3rdParty/OrderedDictionary.h; sourceTree = ""; }; @@ -479,6 +480,7 @@ CDA81D581A95B4B4009790E2 /* TaskExplorer-Info.plist */, CDA81D591A95B4B4009790E2 /* TaskExplorer-Prefix.pch */, CDA81D5A1A95B4B4009790E2 /* main.m */, + CD24FE7621C6FC3D00900B61 /* main.h */, ); name = "Supporting Files"; sourceTree = ""; diff --git a/TaskExplorer.xcodeproj/project.xcworkspace/xcuserdata/patrick.xcuserdatad/UserInterfaceState.xcuserstate b/TaskExplorer.xcodeproj/project.xcworkspace/xcuserdata/patrick.xcuserdatad/UserInterfaceState.xcuserstate index ae266564e8eb922421035df474e13ab36aa0bebc..dee3b90763f5faf8d5ea995754e4fa6944454b59 100644 GIT binary patch delta 37751 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⁣I&hgHv&gssposT;I z?R?Ssy7OJ<*UlfEzq^)ot?1g(71EX7MeHi?s_de5)pjwu8oHXg4t5>yI@NWy>p~Z| zi`O;WHP$uJwFq!@IJ-IioDfbdCxesEA#sX1r5q}!np4MNavC@toK8*;=RBv6Gr$?* zjBOIOYQ@j`?~s$_MPkdr;pb+-Y4u+_Fd__ z+xMvNN#C=+#lDYyU;4iF{p|b0UB+F{-OSbEZsqE5jkqRUDA$~8!`;jE;`(v}xWU{I zZWuR`8_mUW*Mixzzk2!Tj0rg*Lk;icX;=C4|%V6KY4!!mk+KQTsyd7 zP-9SQ5HbiIgbi8_S`VTI?FY9HIt_*orVY{u&kW8CE)5wDSq|9@*$p`kxeU1v?Ht-Y zgd55jDjupFq7Bs!F@_q3I)*xjx`%p(jt(6k8XLMZv^ey0SbJE1*nW6BFzh_+GVC_& zF}!;?bU1uCayWXJFiaUfIDB?^a#%DxGb|on7?uyKhOZ6Z9KJJrfB4bx*WvHOzlQ&e zEFW1pvSwuc$fl9aBU&TcBf2AYBfCcMBSj-MBb<@`k-3q(BVR{1jhc_zjG{*EMmiOvbm#D$6BiLnX(gm7Yd zVs_%{#G{G#6Q3r&PW+ho&0oe}$zQ`?#|QDjd)_fG-p6|%_u=kO2mFY#yjSNS*ixB2(@5BZPzZ}{)|pZH(-Klr~U*H3Pm)STQp zsXM7R=`gu#a_^+~q~B!VWbkCnBz6)viJ#1zESe-wQYL}Ps>zy3)@0ixdy+GGX!6M9 zvB}}dtAZ5*O~F=yu0UU4C@>Nr1l9ti044Adgb5-6L9_rTh!Z3Tas;`8JONPv2&x1% z0=l4H&?x8@926W992Q&19ul4t{v*6B2qLZRCqVuAQqFzzIXizjF8W&B9u8D4n?uhP*9*7=^o`{}{mP9W_Z$$5= z)=rsBIZo}F!b~Mik*6AgsotrvDe;tSN;!3P>ekfVsRvV!rru0%n>L)boVJ;^owlEL zns%K=Pw$)#oIWreF^!qVPA5($Pp3|2OlM7ZP4`ZVr&ZI}rf*K)o?e{(JpFC@=k%YM z_%9;B!i!=YtyqbAC z^I_)a%%9oivnyxU%&wb-%?8fq&Nj_<&UVlC%pRFNJKHzgKg*jPnjM)Pn-$N>W)-tn zW^c~kp1n8wVD|OwySdeK>*hAjY0PQPZJ9HegUmtaU~?98+vl9;+~z#ycF*mdi<*m{ z%bv@dBhD4hmCTjSodV{#bG*5sxskcgVwl)Lyj|=hb`|dxdyDE4_lS>*kBcvh`C_4XT0AF~h-G4>_^SAc_?h@$@eA>5@jLNH@fY#8 z`PK8A=eNx3%sT`9G4ClGT!Rk_{4&1T3+aY?nAoTqS79PD!4GEUA>xB()NTq+Ze` zIVd?SIVw3P8I+7j#v~IGiR7l_j^v)?f#jv+o8+hDx8(1F`@;SO+yZ_fej#ZgYaw@m zxIh9H8W*}3dKQi@oLD%$aCSktaDU;^!jpyP3rhmnOO+I5g|bpvr(`JWl}$>PlA}DWyr{ga>{IqD zN0sABzEY^1Qc9J#m3NePmG_lzl<$=vm0y(KR9jTqs%Dh{YBRgEf^s#VplI;Og$>QnWrcq+L{rMjxRuDYpu oq zJ>YxuSNI(|KWC%>70ihr7ahJTiSj(?ti zf#1e&=XdZs`Ca^O{v&=b|1tjw{}q3L|CaxbKg9n*5W$2e60s0FaS#`A6A$r{PNW;@ zPWqEm$f;xii6QA^5E)EPBST0Q$tGu#F=R5CLZ*@sDJ8SWTr!W;kOhR2bICGtDY=YX zPOc-3WG%Uq+(RB9kB~>nljK?QBH2P-C)>$eWIy?Wd`Z3{2gujt8}cprPLKtwU=!?u zLvRX;pb9Rbzi^6hsxUx^5n_dbLYxpUBnXK@sxU-2UC0(j3*&?mp<0+H)Cjdgov=_? zDrf=~&J~sk7YmmNmkKL|tA!hdTZP+%RYJ3{PFOG8DcmJ&67CZo5*`+w5S|fU61EE4 zgk8dJ;a%Ya;S=F=;alN5;h^xn@S7-zq9}lOp_>B0h_?-B>__Fw#xK(^#{6PFr{7Bp@ek^_>eky(@ zej$D-ej|P>{v`e`aT1aQNtCRTO;RLPa!GEm8OHW8o zO0P&;q*tZaq^;5pX{Yp#v|IW?`ce8xIwbup{UZG;{U-e`{UQA+{bk`Syv1YjT6`A2 zC1B}f>1>Iz^tJS}^tYs1(k$tgL6%{bY)g)1gk_AS&@$5!v=mv2EhUzaWv*qOrN(le z<$TKpmJ2NxS+26Iv|Meu(Xz_2#&U<{Udw%!M=VcRp0~VUdC{`nvcs~|vdi+8dpE8lHvRC%WJ>_2VDRP{gDCfy%$|K~F@+f(tWnlzYpgZFnruz67FkQJv#fKi<<@HJJZrsmzIBmxv303c zvo5!;uwH1r$a8u5GyO4BJRszHO>)nr*tR%r?h1*H&(;u+6hA zv^ChM?OfY3+vT<^Y**T@vaPgTXS>z5#35(w=XhYM*AGZZEUXvCp-a+bitz>d+jQI4*Tu=D6H(h2v_+O^%x#&5qk0Ya98qIv;mF<$TV$!@1M>j&qN5zw-;{m(H)82b>3; z-#dSB{^|Tn;S{6@imY^2x+q{Q-Sb}R2IA1EIypD3Rz`;`OA*UCZVd*zVw zv+}$0hl*6JimIgARJ*FGF4d>{)h=pRwWr!kjaE-m`>Us@vFboIQB6|Q)O2--db*ma zW~sx~Gt`mlDD^CLoH{|Bs7_U!pRPRvlRPRwAR-aOzR$o)Ms;{eWsBfw})%Vmr>ig;k>OS>z zb-((f`jdJ{{aO7*{li6Ef=hHsF2$v~T(0h}9E zxN=>?U1zvPy7FBGuJNu3t{JXE*GyNrtHM?3s&ZAk>RgLmXSBCtaIePr06RZE?Nodd;=fwafLE>uuL3 zu1{T`x%Rm}cYWph-t~j)N7o;&KV5&hZEm~U;dZ(ex5wSZ-PPUA-Q9hX`($@tccMGV zo$O9=r@9BbGu>ypN4O`tC%GrPr?{uO3*DveS?<~HYWF;Mjr$z;Qn%)&?sMJexi58J z=Dys0o%?$C4es0BYuszy>)h+zce?L)Kj41C{iJ)ddyD&3_iOI0?$_Ph-MiiIy5Dnu z;{MeAnfsvod-o6SAKgE>fAt^__V6C!v3ndIr>BdjtEZc%yQhbzkEg%q6wj%iL{E|@ z*^}YP^kjLmJvp8{&sfh{o^hTjo~fQ`p4px<&m7NOPr0Ysv%s^^)8JX;S?oF6v)r@7 zbDrla&q~kLo@+eUdT#VIde(b3cs6<-@;vN$#Pg`SiZ@}Br+soVA8{>`j4)n%( z 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 diff --git a/TaskExplorer.xcodeproj/xcuserdata/patrick.xcuserdatad/xcschemes/TaskExplorer.xcscheme b/TaskExplorer.xcodeproj/xcuserdata/patrick.xcuserdatad/xcschemes/TaskExplorer.xcscheme index 8389691..a71d917 100644 --- a/TaskExplorer.xcodeproj/xcuserdata/patrick.xcuserdatad/xcschemes/TaskExplorer.xcscheme +++ b/TaskExplorer.xcodeproj/xcuserdata/patrick.xcuserdatad/xcschemes/TaskExplorer.xcscheme @@ -46,6 +46,12 @@ ReferencedContainer = "container:TaskExplorer.xcodeproj"> + + + + make sure there is table item for row @@ -325,7 +325,7 @@ bail: NSUInteger taskIndex = 0; //grab tasks - tasks = ((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator.tasks; + tasks = taskEnumerator.tasks; //get task selectedTask = [self taskForRow:nil]; @@ -427,7 +427,7 @@ bail: Task* task = nil; //grab tasks - tasks = ((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator.tasks; + tasks = taskEnumerator.tasks; //use sender if provided if(nil != sender) @@ -658,7 +658,7 @@ bail: OrderedDictionary* tasks = nil; //grab tasks - tasks = ((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator.tasks; + tasks = taskEnumerator.tasks; //number of children NSUInteger numberOfChildren = 0; @@ -708,7 +708,7 @@ bail: Task* task = nil; //grab all tasks - tasks = ((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator.tasks; + tasks = taskEnumerator.tasks; //root item if(nil == item) @@ -797,7 +797,7 @@ bail: } //grab tasks - tasks = ((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator.tasks; + tasks = taskEnumerator.tasks; //get index of newly selected row newlySelectedRow = [self.itemView selectedRow]; @@ -834,7 +834,7 @@ bail: ((kkRowCell*)selectedView).color = nil; //remove task from task emumerator - [((AppDelegate*)[[NSApplication sharedApplication] delegate]).taskEnumerator removeTask:task]; + [taskEnumerator removeTask:task]; //when filtering // ->remove deceased task from filter tasks diff --git a/UI/FlatView.xib b/UI/FlatView.xib index a731cf1..9b88315 100755 --- a/UI/FlatView.xib +++ b/UI/FlatView.xib @@ -1,10 +1,8 @@ - + - - - + @@ -75,7 +73,7 @@ - + + @@ -52,6 +55,7 @@ + @@ -60,6 +64,7 @@ + @@ -68,6 +73,7 @@ + @@ -76,6 +82,7 @@ + @@ -84,6 +91,7 @@ + @@ -92,11 +100,13 @@ + diff --git a/Utilities.h b/Utilities.h index 5f69ade..24877e2 100644 --- a/Utilities.h +++ b/Utilities.h @@ -15,6 +15,9 @@ /* FUNCTIONS */ +//disable std err +void disableSTDERR(void); + //loads a framework // note: assumes it is in 'Framework' dir NSBundle* loadFramework(NSString* name); @@ -84,8 +87,15 @@ BOOL Is32Bit(pid_t targetPID); // ->based on http://lapcatsoftware.com/articles/detect-app-translocation.html NSURL* getUnTranslocatedURL(); +//give a list of paths +// convert any `~` to all or current user +NSMutableArray* expandPaths(const __strong NSString* const paths[], int count); + //check if (full) dark mode // meaning, Mojave+ and dark mode enabled BOOL isDarkMode(); +//bring an app to foreground (to get an icon in the dock) or background +void transformProcess(ProcessApplicationTransformState location); + #endif diff --git a/Utilities.m b/Utilities.m index 0049820..bc92873 100644 --- a/Utilities.m +++ b/Utilities.m @@ -20,8 +20,28 @@ #import #import #import +#import +#import #import +//disable std err +void disableSTDERR() +{ + //file handle + int devNull = -1; + + //open /dev/null + devNull = open("/dev/null", O_RDWR); + + //dup + dup2(devNull, STDERR_FILENO); + + //close + close(devNull); + + return; +} + //init crash reporting void initCrashReporting() { @@ -1182,7 +1202,128 @@ bail: } return untranslocatedURL; +} + +//get all user +// includes name/home directory +NSMutableDictionary* allUsers() +{ + //users + NSMutableDictionary* users = nil; + //query + CSIdentityQueryRef query = nil; + + //query results + CFArrayRef results = NULL; + + //error + CFErrorRef error = NULL; + + //identiry + CBIdentity* identity = NULL; + + //alloc dictionary + users = [NSMutableDictionary dictionary]; + + //init query + query = CSIdentityQueryCreate(NULL, kCSIdentityClassUser, CSGetLocalIdentityAuthority()); + + //exec query + if(true != CSIdentityQueryExecute(query, 0, &error)) + { + //bail + goto bail; + } + + //grab results + results = CSIdentityQueryCopyResults(query); + + //process all results + // add user and home directory + for (int i = 0; i < CFArrayGetCount(results); ++i) + { + //grab identity + identity = [CBIdentity identityWithCSIdentity:(CSIdentityRef)CFArrayGetValueAtIndex(results, i)]; + + //add user + users[identity.UUIDString] = @{USER_NAME:identity.posixName, USER_DIRECTORY:NSHomeDirectoryForUser(identity.posixName)}; + } + +bail: + + //release results + if(NULL != results) + { + //release + CFRelease(results); + } + + //release query + if(NULL != query) + { + //release + CFRelease(query); + } + + return users; +} + +//give a list of paths +// convert any `~` to all or current user +NSMutableArray* expandPaths(const __strong NSString* const paths[], int count) +{ + //expanded paths + NSMutableArray* expandedPaths = nil; + + //(current) path + const NSString* path = nil; + + //all users + NSMutableDictionary* users = nil; + + //grab all users + users = allUsers(); + + //alloc list + expandedPaths = [NSMutableArray array]; + + //iterate/expand + for(NSInteger i = 0; i < count; i++) + { + //grab path + path = paths[i]; + + //no `~`? + // just add and continue + if(YES != [path hasPrefix:@"~"]) + { + //add as is + [expandedPaths addObject:path]; + + //next + continue; + } + + //handle '~' case + // root? add each user + if(0 == geteuid()) + { + //add each user + for(NSString* user in users) + { + [expandedPaths addObject:[users[user][USER_DIRECTORY] stringByAppendingPathComponent:[path substringFromIndex:1]]]; + } + } + //otherwise + // just convert to current user + else + { + [expandedPaths addObject:[path stringByExpandingTildeInPath]]; + } + } + + return expandedPaths; } //check if (full) dark mode @@ -1215,6 +1356,26 @@ bail: return darkMode; } +//bring an app to foreground (to get an icon in the dock) or background +void transformProcess(ProcessApplicationTransformState location) +{ + //process serial no + ProcessSerialNumber processSerialNo; + + //init process stuct + // ->high to 0 + processSerialNo.highLongOfPSN = 0; + + //init process stuct + // ->low to self + processSerialNo.lowLongOfPSN = kCurrentProcess; + + //transform to foreground + TransformProcessType(&processSerialNo, location); + + return; +} + diff --git a/VTButton.m b/VTButton.m index 0a78e2a..a1220a7 100644 --- a/VTButton.m +++ b/VTButton.m @@ -42,7 +42,7 @@ } //automatically invoked when mouse-down occurs -// ->set color to light gray or light red +// ->set color to default or red -(void)mouseDown:(NSEvent *)theEvent; { //mouse down/over color @@ -60,11 +60,10 @@ color = [NSColor colorWithCalibratedRed:(255/255.0f) green:(1.0/255.0f) blue:(1.0/255.0f) alpha:0.5]; } //non-flagged files - //gray else { - //gray - color = NSColor.grayColor; + //default + color = NSColor.controlTextColor; } //set string @@ -74,7 +73,7 @@ } //automatically invoked when mouse-up occurs -// ->reset color to gray or red and trigger mouse click logic (if necessary) +// ->reset color to default or red and trigger mouse click logic (if necessary) -(void)mouseUp:(NSEvent *)theEvent; { //mouse up color @@ -135,8 +134,8 @@ // set (back) to default else { - //gray - color = NSColor.grayColor; + //default + color = NSColor.controlTextColor; } //set string @@ -156,7 +155,7 @@ self.mouseExit = YES; //check if mouse is down - // ->set color to gray/lightish red + // ->set color to default/red if(YES == self.mouseDown) { //flagged files @@ -168,11 +167,10 @@ color = [NSColor colorWithCalibratedRed:(255/255.0f) green:(1.0/255.0f) blue:(1.0/255.0f) alpha:0.66]; } //non-flagged files - // ->just black else { - //gray - color = NSColor.grayColor; + //default + color = NSColor.controlTextColor; } } //mouse is up @@ -191,7 +189,7 @@ // set (back) to default else { - //gray + //default color = NSColor.controlTextColor; } } diff --git a/VTInfoWindowController.m b/VTInfoWindowController.m index ef522e6..4b77b77 100644 --- a/VTInfoWindowController.m +++ b/VTInfoWindowController.m @@ -135,7 +135,7 @@ makeTextViewHyperlink(self.analysisURL, [NSURL URLWithString:self.item.vtInfo[VT_RESULTS_URL]]); //set 'submit' button text to 'rescan' - self.submitButton.title = @"rescan?"; + self.submitButton.title = @"Rescan?"; } //unknown file else @@ -236,9 +236,21 @@ //pre-req [self.overlayView setWantsLayer:YES]; - //set overlay's view color to white - self.overlayView.layer.backgroundColor = [NSColor whiteColor].CGColor; - + //dark mode + // set overlay to light + if(YES == isDarkMode()) + { + //set overlay's view color to gray + self.overlayView.layer.backgroundColor = NSColor.lightGrayColor.CGColor; + } + //light mode + // set overlay to gray + else + { + //set to gray + self.overlayView.layer.backgroundColor = NSColor.grayColor.CGColor; + } + //make it semi-transparent self.overlayView.alphaValue = 0.85; @@ -252,7 +264,7 @@ [self.progressIndicator startAnimation:nil]; //rescan file? - if(YES == [((NSButton*)sender).title isEqualToString:@"rescan?"]) + if(YES == [((NSButton*)sender).title isEqualToString:@"Rescan?"]) { //set status msg [self.statusMsg setStringValue:[NSString stringWithFormat:@"submitting re-scan request for %@", self.item.name]]; diff --git a/VirusTotal.h b/VirusTotal.h index 0add6d2..96c7f1a 100644 --- a/VirusTotal.h +++ b/VirusTotal.h @@ -19,6 +19,8 @@ //array for (up to 25) items @property(nonatomic, retain)NSMutableArray* items; +//array for all threads +@property(nonatomic, retain)NSMutableArray* vtThreads; /* METHODS */ @@ -26,10 +28,6 @@ // ->will query VT when 25 items are hit -(void)addItem:(Binary*)binary; -//thread function -// ->runs in the background to get virus total info about a plugin's items -//-(void)getInfo:(PluginBase*)plugin; - //make the (POST)query to VT -(NSDictionary*)postRequest:(NSURL*)url parameters:(id)params; diff --git a/VirusTotal.m b/VirusTotal.m index ddc8ea4..85309b6 100644 --- a/VirusTotal.m +++ b/VirusTotal.m @@ -17,6 +17,7 @@ @implementation VirusTotal @synthesize items; +@synthesize vtThreads; //init -(id)init @@ -28,8 +29,11 @@ //alloc array for items items = [NSMutableArray array]; + //init array for virus total threads + vtThreads = [NSMutableArray array]; + //kick of thread to watch/flush queue - // ->will flush if item not processed in 30 seconds + // ->will flush if item not processed in 3 seconds [NSThread detachNewThreadSelector:@selector(queueFlusher) toTarget:self withObject:nil]; } @@ -98,6 +102,9 @@ //items to process NSMutableArray* vtItems = nil; + //virus total thread + NSThread* virusTotalThread = nil; + //sync @synchronized(self.items) { @@ -108,10 +115,21 @@ if(VT_MAX_QUERY_COUNT == self.items.count) { //make copy - vtItems = [NSMutableArray arrayWithArray:self.items]; - - //kick of thread to make a query to VT - [NSThread detachNewThreadSelector:@selector(queryVT:) toTarget:self withObject:vtItems]; + vtItems = [NSMutableArray arrayWithArray:self.items]; + + //alloc thread + // ->will query virus total to get info about all detected items + virusTotalThread = [[NSThread alloc] initWithTarget:self selector:@selector(queryVT:) object:vtItems]; + + //start thread + [virusTotalThread start]; + + //sync + @synchronized(self.vtThreads) + { + //save it into array + [self.vtThreads addObject:virusTotalThread]; + } //remove all items [self.items removeAllObjects]; @@ -226,22 +244,29 @@ //save flagged item if(0 != [results[VT_RESULTS_POSITIVES] unsignedIntegerValue]) { - //save - [((AppDelegate*)[[NSApplication sharedApplication] delegate]) saveFlaggedBinary:item]; + //sync to check/add + @synchronized(taskEnumerator.flaggedItems) + { + if(YES != [taskEnumerator.flaggedItems containsObject:item]) + { + //save + [taskEnumerator.flaggedItems addObject:item]; + } + } } //for non-flagged items - // ->remove from list, if they were previously flagged + // remove from list, if it was previously flagged else { - //check if previously flagged - // ->then remove - if(YES == [((AppDelegate*)[[NSApplication sharedApplication] delegate]).flaggedItems containsObject:item]) + //sync to check/remove + @synchronized(taskEnumerator.flaggedItems) { - //sync to remove - @synchronized(((AppDelegate*)[[NSApplication sharedApplication] delegate]).flaggedItems) + //check if previously flagged + // ...if so, then remove + if(YES == [taskEnumerator.flaggedItems containsObject:item]) { //remove - [((AppDelegate*)[[NSApplication sharedApplication] delegate]).flaggedItems removeObject:item]; + [taskEnumerator.flaggedItems removeObject:item]; } } } @@ -300,9 +325,6 @@ postData = [NSJSONSerialization dataWithJSONObject:params options:kNilOptions error:nil]; if(nil == postData) { - //err msg - syslog(LOG_ERR, "OBJECTIVE-SEE ERROR: failed to convert request %s to JSON", [[postData description] UTF8String]); - //bail goto bail; } @@ -354,9 +376,6 @@ //bail on any exceptions @catch (NSException *exception) { - //err msg - syslog(LOG_ERR, "OBJECTIVE-SEE ERROR: converting response %s to JSON threw %s", [[vtData description] UTF8String], [[exception description] UTF8String]); - //bail goto bail; } @@ -364,9 +383,6 @@ //sanity check if(nil == results) { - //err msg - syslog(LOG_ERR, "OBJECTIVE-SEE ERROR: failed to convert response %s to JSON", [[vtData description] UTF8String]); - //bail goto bail; } @@ -513,13 +529,11 @@ bail: goto bail; } -//bail bail: return results; } - //submit a rescan request -(NSDictionary*)reScan:(Binary*)item { @@ -551,7 +565,6 @@ bail: goto bail; } -//bail bail: return result; @@ -563,7 +576,7 @@ bail: { //queried binary obj Binary* queriedItem = nil; - + //process all results // ->save VT result dictionary into File obj for(NSDictionary* result in results[VT_RESULTS]) @@ -571,8 +584,6 @@ bail: //extract ('match') queried item // ->VT gives us back a hash queriedItem = queriedItems[result[@"hash"]]; - - //sanity check if(nil == queriedItem) { //skip @@ -582,11 +593,16 @@ bail: //save VT results into item queriedItem.vtInfo = result; - //save flagged item - if(0 != [result[VT_RESULTS_POSITIVES] unsignedIntegerValue]) + //sync to check/add + @synchronized(taskEnumerator.flaggedItems) { - //save - [((AppDelegate*)[[NSApplication sharedApplication] delegate]) saveFlaggedBinary:queriedItem]; + //save flagged item + if( (0 != [result[VT_RESULTS_POSITIVES] unsignedIntegerValue]) && + (YES != [taskEnumerator.flaggedItems containsObject:queriedItem]) ) + { + //save + [taskEnumerator.flaggedItems addObject:queriedItem]; + } } //call up into app delegate to smartly reload diff --git a/main.h b/main.h new file mode 100644 index 0000000..a5d203e --- /dev/null +++ b/main.h @@ -0,0 +1,53 @@ +// +// main.h +// TaskExplorer +// +// Created by Patrick Wardle on 12/16/18. +// Copyright © 2018 Lucas Derraugh. All rights reserved. +// + +#ifndef main_h +#define main_h + +#import "Consts.h" +#import "Filter.h" +#import "Utilities.h" +#import "VirusTotal.h" +#import "AppDelegate.h" + +#import "TaskEnumerator.h" + +#import + +//(privacy) protected directories +NSString * const PROTECTED_DIRECTORIES[] = {@"~/Library/Application Support/AddressBook", @"~/Library/Calendars", @"~/Pictures", @"~/Library/Mail", @"~/Library/Messages", @"~/Library/Safari", @"~/Library/Cookies", @"~/Library/HomeKit", @"~/Library/IdentityServices", @"~/Library/Metadata/CoreSpotlight", @"~/Library/PersonalizationPortrait", @"~/Library/Suggestions"}; + +/* GLOBALS */ + +//task enumerator obj +TaskEnumerator* taskEnumerator = nil; + +//virustotal obj +VirusTotal* virusTotal = nil; + +//network connected flag +BOOL isConnected = NO; + +//(privacy) protected directories +NSArray* protectedDirectories = nil; + +/* FUNCTIONS */ + +//print usage +void usage(void); + +//perform a cmdline enumeration +void cmdlineExplore(void); + +//block until vt queries are done +void completeVTQuery(void); + +//pretty print JSON +void prettyPrintJSON(NSString* output); + +#endif /* main_h */ diff --git a/main.m b/main.m index 3ab672c..ec9a248 100755 --- a/main.m +++ b/main.m @@ -6,19 +6,22 @@ // Copyright (c) 2015 Objective-See. All rights reserved. // -#import "Consts.h" -#import "Utilities.h" - -#import - +#import "main.h" //main interface -// ->contains extra logic to handle app translocation +// contains extra logic to handle app translocation int main(int argc, char *argv[]) { //return int status = -1; + //disable stderr + // sentry dumps to this, and we want only JSON to output... + disableSTDERR(); + + //init crash reporting + initCrashReporting(); + //untranslocated URL NSURL* untranslocatedURL = nil; @@ -29,8 +32,11 @@ int main(int argc, char *argv[]) //remove quarantine attributes of original execTask(XATTR, @[@"-cr", untranslocatedURL.path], NO); + //nap + [NSThread sleepForTimeInterval:0.5]; + //relaunch - // ->use 'open' since allows two instances of app to be run + // use 'open' since allows two instances of app to be run execTask(OPEN, @[@"-n", @"-a", untranslocatedURL.path], NO); //happy @@ -40,17 +46,348 @@ int main(int argc, char *argv[]) goto bail; } - //app isn't translocated - // ->can just run app as is + //set network connection flag + isConnected = isNetworkConnected(); + + //init set of (privacy) protected directories + // these will be skipped, as otherwise we will generate a privacy prompt + protectedDirectories = expandPaths(PROTECTED_DIRECTORIES, sizeof(PROTECTED_DIRECTORIES)/sizeof(PROTECTED_DIRECTORIES[0])); + + //handle '-h' or '-help' + if( (YES == [[[NSProcessInfo processInfo] arguments] containsObject:@"-h"]) || + (YES == [[[NSProcessInfo processInfo] arguments] containsObject:@"-help"]) ) + { + //print usage + usage(); + + //done + goto bail; + } + + //handle cmdline + // scan, explore, etc + if( (YES == [[[NSProcessInfo processInfo] arguments] containsObject:@"-scan"]) || + (YES == [[[NSProcessInfo processInfo] arguments] containsObject:@"-explore"]) ) + + { + //first check rooot + if(0 != geteuid()) + { + //err msg + printf("{\"ERROR\": \"TASKEXPLORER (cmdline) requires root\"}\n"); + + //bail + goto bail; + } + + //scan + cmdlineExplore(); + + //happy + status = 0; + + //done + goto bail; + } + + //otherwise + // just kick off app for UI instance else { //invoke app's main status = NSApplicationMain(argc, (const char **)argv); } -//bail bail: return status; } + +//print usage +void usage() +{ + //usage + printf("\nTASKEXPLORER USAGE:\n"); + printf(" -h or -help display this usage info\n"); + printf(" -scan scan all tasks and dylibs \n"); + printf(" -explore enumerate all tasks and dylibs\n"); + printf("\noptions:\n"); + printf(" -pretty json output is 'pretty-printed'\n"); + printf(" -skipVT do not query VirusTotal (when '-explore' is specified)\n"); + printf(" -full for each task; include dylibs, files, & network connections\n\n"); + + return; +} + +//perform a cmdline enumeration of all things +void cmdlineExplore() +{ + //filter obj + Filter* filter = nil; + + //flag + BOOL includeApple = NO; + + //flag + BOOL skipVirusTotal = NO; + + //flag + BOOL prettyPrint = NO; + + //flag + BOOL detailed = NO; + + //output + NSMutableString* output = nil; + + //init filter obj + filter = [[Filter alloc] init]; + + //init task enumerator object + taskEnumerator = [[TaskEnumerator alloc] init]; + + //set flag + // skip virus total? + skipVirusTotal = [[[NSProcessInfo processInfo] arguments] containsObject:@"-skipVT"]; + + //virus total? + if(YES != skipVirusTotal) + { + //init virus total object + virusTotal = [[VirusTotal alloc] init]; + } + + //be nice + nice(15); + + //enumerate all tasks/dylibs/files/etc + [taskEnumerator enumerateTasks]; + + //wait for items to complete processing + while(taskEnumerator.binaryQueue.itemsOut != taskEnumerator.binaryQueue.itemsOut) + { + //nap + [NSThread sleepForTimeInterval:1.0f]; + } + + //determine what each dylib is loaded in + // do here as all tasks and all dylibs are enum'd + for(NSString* dylib in taskEnumerator.dylibs) + { + //loaded in + ((Binary*)taskEnumerator.dylibs[dylib]).loadedIn = [taskEnumerator loadedIn:taskEnumerator.dylibs[dylib]]; + + }//sync + + //wait for all VT threads to exit + if(YES != skipVirusTotal) + { + //wait + completeVTQuery(); + } + + //set flag + // include apple items? + includeApple = [[[NSProcessInfo processInfo] arguments] containsObject:@"-apple"]; + + //set flag + // pretty print json? + prettyPrint = [[[NSProcessInfo processInfo] arguments] containsObject:@"-pretty"]; + + //set flag + // full output? + detailed = [[[NSProcessInfo processInfo] arguments] containsObject:@"-detailed"]; + + //alloc output JSON + output = [NSMutableString string]; + + //only flagged items? + if(YES == [[[NSProcessInfo processInfo] arguments] containsObject:@"-scan"]) + { + //start JSON + [output appendString:@"{\"flagged items\":["]; + + //add each item + for(Binary* flaggedItem in taskEnumerator.flaggedItems) + { + [output appendFormat:@"{%@},", [flaggedItem toJSON]]; + } + + //remove last ',' + if(YES == [output hasSuffix:@","]) + { + //remove + [output deleteCharactersInRange:NSMakeRange([output length]-1, 1)]; + } + + //terminate list/output + [output appendString:@"]}"]; + } + + else + { + //start JSON + [output appendString:@"{\"tasks\":["]; + + //get tasks + for(NSNumber* taskPid in taskEnumerator.tasks) + { + //skip apple? + if( (YES != includeApple) && + (YES == [filter isApple:((Task*)taskEnumerator.tasks[taskPid]).binary]) ) + { + //skip + continue; + } + + //append task JSON + [output appendFormat:@"{%@},", [taskEnumerator.tasks[taskPid] toJSON:detailed]]; + } + + //remove last ',' + if(YES == [output hasSuffix:@","]) + { + //remove + [output deleteCharactersInRange:NSMakeRange([output length]-1, 1)]; + } + + //not detailed? + // add separate array of dylibs + if(YES != detailed) + { + //append + [output appendString:@"],\"dylibs\":["]; + + //add each dylib + for(NSString* dylib in taskEnumerator.dylibs) + { + //add + [output appendFormat:@"{%@},", [((Binary*)taskEnumerator.dylibs[dylib]) toJSON]]; + } + + //remove last ',' + if(YES == [output hasSuffix:@","]) + { + //remove + [output deleteCharactersInRange:NSMakeRange([output length]-1, 1)]; + } + } + + //terminate list/output + [output appendString:@"]}"]; + } + + //pretty print? + if(YES == prettyPrint) + { + //make me pretty! + prettyPrintJSON(output); + } + else + { + //output + printf("%s\n", output.UTF8String); + } + + return; +} + +//block until vt queries are done +void completeVTQuery() +{ + //flag + BOOL queryingVT = NO; + + //nap + // VT threads take some time to spawn/process + [NSThread sleepForTimeInterval:5.0f]; + + //wait till threads are done + while(YES) + { + //reset flag + queryingVT = NO; + + //wait for vt to complete + @synchronized(virusTotal.vtThreads) + { + //check all threads + for(NSThread* vtThread in virusTotal.vtThreads) + { + //check if still running? + if(YES == [vtThread isExecuting]) + { + //set flag + queryingVT = YES; + + //bail + break; + } + } + } + + //check flag + if(YES != queryingVT) + { + //finally no active threads + break; + } + + //nap + [NSThread sleepForTimeInterval:5.0f]; + } + + return; +} + +//pretty print JSON +void prettyPrintJSON(NSString* output) +{ + //data + NSData* data = nil; + + //object + id object = nil; + + //pretty data + NSData* prettyData = nil; + + //pretty string + NSString* prettyString = nil; + + //covert to data + data = [output dataUsingEncoding:NSUTF8StringEncoding]; + + //convert to JSON + // wrap since we are serializing JSON + @try + { + //serialize + object = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + + //covert to pretty data + prettyData = [NSJSONSerialization dataWithJSONObject:object options:NSJSONWritingPrettyPrinted error:nil]; + } + @catch(NSException *exception) + { + ; + } + + //covert to pretty string + if(nil != prettyData) + { + //convert to string + prettyString = [[NSString alloc] initWithData:prettyData encoding:NSUTF8StringEncoding]; + } + else + { + //error + prettyString = @"{\"ERROR\" : \"failed to covert output to JSON\"}"; + } + + //output + printf("%s\n", prettyString.UTF8String); + + return; +} diff --git a/remoteTaskService/Info.plist b/remoteTaskService/Info.plist index 18c6cb7..72f8bbc 100644 --- a/remoteTaskService/Info.plist +++ b/remoteTaskService/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.7.0 + 2.0.0 CFBundleSignature ???? CFBundleVersion - 1.7.0 + 2.0.0 NSHumanReadableCopyright Copyright © 2018 Objective-See, LLC. All rights reserved. XPCService