mirror of
https://github.com/objective-see/BlockBlock.git
synced 2026-03-22 07:02:39 +00:00
1899 lines
45 KiB
Objective-C
1899 lines
45 KiB
Objective-C
//
|
|
// file: utilities.m
|
|
// project: BlockBlock (shared)
|
|
// description: various helper/utility functions
|
|
//
|
|
// created by Patrick Wardle
|
|
// copyright (c) 2017 Objective-See. All rights reserved.
|
|
//
|
|
|
|
#import "consts.h"
|
|
#import "utilities.h"
|
|
|
|
#import <dlfcn.h>
|
|
#import <signal.h>
|
|
#import <unistd.h>
|
|
#import <libproc.h>
|
|
#import <sys/stat.h>
|
|
#import <sys/sysctl.h>
|
|
#import <Carbon/Carbon.h>
|
|
#import <Security/Security.h>
|
|
#import <Foundation/Foundation.h>
|
|
#import <CommonCrypto/CommonDigest.h>
|
|
#import <SystemConfiguration/SystemConfiguration.h>
|
|
|
|
@import OSLog;
|
|
|
|
/* GLOBALS */
|
|
|
|
//log handle
|
|
extern os_log_t logHandle;
|
|
|
|
//get app's version
|
|
// extracted from Info.plist
|
|
NSString* getAppVersion(void)
|
|
{
|
|
//read and return 'CFBundleVersion' from bundle
|
|
return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
|
|
}
|
|
|
|
//given an app binary
|
|
// try get app's bundle
|
|
NSBundle* getAppBundle(NSString* binaryPath)
|
|
{
|
|
//bundle
|
|
NSBundle* appBundle = nil;
|
|
|
|
//app path
|
|
NSString* appPath = nil;
|
|
|
|
//build app path
|
|
// assuming path is <blah.app>/Contents/MacOS/<blah>
|
|
appPath = [[[binaryPath stringByDeletingLastPathComponent] stringByDeletingLastPathComponent] stringByDeletingLastPathComponent];
|
|
if(YES != [appPath hasSuffix:@".app"])
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//try to load app's bundle
|
|
appBundle = [NSBundle bundleWithPath:appPath];
|
|
if(nil == appBundle)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//sanity check
|
|
// binary paths match?
|
|
if(YES != [appBundle.executablePath isEqualToString:binaryPath])
|
|
{
|
|
//unset
|
|
appBundle = nil;
|
|
goto bail;
|
|
}
|
|
|
|
bail:
|
|
|
|
return appBundle;
|
|
}
|
|
|
|
//figure out binary's name
|
|
// either via app bundle, or from path
|
|
NSString* getBinaryName(NSString* path)
|
|
{
|
|
//name
|
|
NSString* name = nil;
|
|
|
|
//bundle
|
|
NSBundle* bundle = nil;
|
|
|
|
//try get bundle
|
|
// then extract name from 'CFBundleName'
|
|
bundle = getAppBundle(path);
|
|
if(nil != bundle)
|
|
{
|
|
//extract name
|
|
name = [bundle infoDictionary][@"CFBundleName"];
|
|
}
|
|
|
|
//no app bundle || no 'CFBundleName'?
|
|
// just use last component from the path
|
|
if(nil == name)
|
|
{
|
|
//set name
|
|
name = [path lastPathComponent];
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
//get path to (main) app of a login item
|
|
// login item is in app bundle, so parse up to get main app
|
|
NSString* getMainAppPath(void)
|
|
{
|
|
//path components
|
|
NSArray *pathComponents = nil;
|
|
|
|
//path to config (main) app
|
|
NSString* mainApp = nil;
|
|
|
|
//get path components
|
|
// then build full path to main app
|
|
pathComponents = [[[NSBundle mainBundle] bundlePath] pathComponents];
|
|
if(pathComponents.count > 4)
|
|
{
|
|
//init path to full (main) app
|
|
mainApp = [NSString pathWithComponents:[pathComponents subarrayWithRange:NSMakeRange(0, pathComponents.count - 4)]];
|
|
}
|
|
|
|
//when (still) nil
|
|
// use default path
|
|
if(nil == mainApp)
|
|
{
|
|
//default
|
|
mainApp = [@"/Applications" stringByAppendingPathComponent:APP_NAME];
|
|
}
|
|
|
|
return mainApp;
|
|
}
|
|
|
|
//give path to app
|
|
// get full path to its binary
|
|
NSString* getBundleExecutable(NSString* appPath)
|
|
{
|
|
//binary path
|
|
NSString* binaryPath = nil;
|
|
|
|
//app bundle
|
|
NSBundle* appBundle = nil;
|
|
|
|
//load app bundle
|
|
appBundle = [NSBundle bundleWithPath:appPath];
|
|
if(nil == appBundle)
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: failed to load app bundle for %{public}@", appPath);
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//extract executable
|
|
binaryPath = [appBundle.executablePath stringByResolvingSymlinksInPath];
|
|
|
|
bail:
|
|
|
|
return binaryPath;
|
|
}
|
|
|
|
#ifndef DAEMON_BUILD
|
|
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
|
|
|
//get (true) parent
|
|
NSDictionary* getRealParent(pid_t pid)
|
|
{
|
|
//process info
|
|
NSDictionary* processInfo = nil;
|
|
|
|
//process serial number
|
|
ProcessSerialNumber psn = {0, kNoProcess};
|
|
|
|
//(parent) process serial number
|
|
ProcessSerialNumber ppsn = {0, kNoProcess};
|
|
|
|
//get process serial number from pid
|
|
if(noErr != GetProcessForPID(pid, &psn))
|
|
{
|
|
//err
|
|
goto bail;
|
|
}
|
|
|
|
//get process (carbon) info
|
|
processInfo = CFBridgingRelease(ProcessInformationCopyDictionary(&psn, (UInt32)kProcessDictionaryIncludeAllInformationMask));
|
|
if(nil == processInfo)
|
|
{
|
|
//err
|
|
goto bail;
|
|
}
|
|
|
|
//extract/convert parent ppsn
|
|
ppsn.lowLongOfPSN = [processInfo[@"ParentPSN"] longLongValue] & 0x00000000FFFFFFFFLL;
|
|
ppsn.highLongOfPSN = ([processInfo[@"ParentPSN"] longLongValue] >> 32) & 0x00000000FFFFFFFFLL;
|
|
|
|
//get parent process (carbon) info
|
|
processInfo = CFBridgingRelease(ProcessInformationCopyDictionary(&ppsn, (UInt32)kProcessDictionaryIncludeAllInformationMask));
|
|
if(nil == processInfo)
|
|
{
|
|
//err
|
|
goto bail;
|
|
}
|
|
|
|
bail:
|
|
|
|
return processInfo;
|
|
}
|
|
|
|
#pragma GCC diagnostic pop
|
|
|
|
//build an array of processes ancestry
|
|
// note: only call from UI session, due to use of carbon APIs
|
|
NSMutableArray* generateProcessHierarchy(pid_t pid, NSString* name)
|
|
{
|
|
//process hierarchy
|
|
NSMutableArray* processHierarchy = nil;
|
|
|
|
//current process id
|
|
pid_t currentPID = -1;
|
|
|
|
//parent pid
|
|
pid_t parentPID = -1;
|
|
|
|
//process name
|
|
NSString* parentName = nil;
|
|
|
|
//alloc
|
|
processHierarchy = [NSMutableArray array];
|
|
|
|
//parent
|
|
NSDictionary* parent = nil;
|
|
|
|
//add current process (leaf)
|
|
// parent(s) will then be added at front...
|
|
[processHierarchy addObject:[@{@"pid":[NSNumber numberWithInt:pid], @"name":valueForStringItem(name)} mutableCopy]];
|
|
|
|
//init current to self
|
|
currentPID = pid;
|
|
|
|
//scan back
|
|
while(YES)
|
|
{
|
|
//get (real) parent
|
|
parent = getRealParent(currentPID);
|
|
if(nil == parent)
|
|
{
|
|
break;
|
|
}
|
|
|
|
//get parent pid
|
|
parentPID = [parent[@"pid"] intValue];
|
|
|
|
//end of heirarchy?
|
|
if( (0 == parentPID) ||
|
|
(-1 == parentPID) ||
|
|
(currentPID == parentPID) )
|
|
{
|
|
//bail
|
|
break;
|
|
}
|
|
|
|
//get name
|
|
// first from bundle, then from executable
|
|
parentName = parent[@"CFBundleName"];
|
|
if(0 == name.length)
|
|
{
|
|
//via executable
|
|
parentName = [parent[@"CFBundleExecutable"] lastPathComponent];
|
|
}
|
|
|
|
//add parent
|
|
// always at front
|
|
[processHierarchy insertObject:[@{@"pid":[NSNumber numberWithInt:parentPID], @"name":valueForStringItem(parentName)} mutableCopy] atIndex:0];
|
|
|
|
//update
|
|
currentPID = parentPID;
|
|
}
|
|
|
|
return processHierarchy;
|
|
}
|
|
|
|
#endif
|
|
|
|
//check if something is nil
|
|
// if so, return a default ('unknown') value
|
|
NSString* valueForStringItem(NSString* item)
|
|
{
|
|
return (nil != item) ? item : @"unknown";
|
|
}
|
|
|
|
//find 'top-level' app of binary
|
|
// useful to determine if binary (or other app) is embedded in a 'parent' app bundle
|
|
NSString* topLevelApp(NSString* binaryPath)
|
|
{
|
|
//app path
|
|
NSString* appPath = nil;
|
|
|
|
//offset of (first) '.app'
|
|
NSRange offset;
|
|
|
|
//find first instance of '.app' in path
|
|
offset = [binaryPath rangeOfString:@".app/" options:NSCaseInsensitiveSearch];
|
|
if(NSNotFound == offset.location)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//extact app path
|
|
// from start, to & including '.app'
|
|
appPath = [binaryPath substringWithRange:NSMakeRange(0, offset.location+4)];
|
|
|
|
bail:
|
|
|
|
return appPath;
|
|
}
|
|
|
|
//verify that an app bundle is valid
|
|
// signed & with (our) signing auth / identifier
|
|
OSStatus verifyApp(NSString* path, NSString* signingAuth)
|
|
{
|
|
//status
|
|
OSStatus status = !noErr;
|
|
|
|
//signing req string
|
|
NSString *requirement = nil;
|
|
|
|
//code
|
|
SecStaticCodeRef staticCode = NULL;
|
|
|
|
//signing reqs
|
|
SecRequirementRef requirementRef = NULL;
|
|
|
|
//init signing req string
|
|
requirement = [NSString stringWithFormat:@"anchor apple generic and identifier \"%@\" and certificate leaf [subject.CN] = \"%@\" and info [CFBundleShortVersionString] >= \"1.0.0\"", INSTALLER_ID, signingAuth];
|
|
|
|
//create static code
|
|
status = SecStaticCodeCreateWithPath((__bridge CFURLRef)([NSURL fileURLWithPath:path]), kSecCSDefaultFlags, &staticCode);
|
|
if(noErr != status)
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: 'SecStaticCodeCreateWithPath' failed with %d/%#x", status, status);
|
|
goto bail;
|
|
}
|
|
|
|
//create req string
|
|
status = SecRequirementCreateWithString((__bridge CFStringRef _Nonnull)(requirement), kSecCSDefaultFlags, &requirementRef);
|
|
if( (noErr != status) ||
|
|
(requirementRef == NULL) )
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: 'SecRequirementCreateWithString' failed with %d/%#x", status, status);
|
|
goto bail;
|
|
}
|
|
|
|
//check if file is signed w/ apple dev id by checking if it conforms to req string
|
|
status = SecStaticCodeCheckValidity(staticCode, kSecCSDefaultFlags, requirementRef);
|
|
if(noErr != status)
|
|
{
|
|
os_log_error(logHandle, "ERROR: 'SecStaticCodeCheckValidity failed with %d/%#x", status, status);
|
|
goto bail;
|
|
}
|
|
|
|
//happy
|
|
status = noErr;
|
|
|
|
bail:
|
|
|
|
//free req reference
|
|
if(NULL != requirementRef)
|
|
{
|
|
//free
|
|
CFRelease(requirementRef);
|
|
requirementRef = NULL;
|
|
}
|
|
|
|
//free static code
|
|
if(NULL != staticCode)
|
|
{
|
|
//free
|
|
CFRelease(staticCode);
|
|
staticCode = NULL;
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
//get name of logged in user
|
|
NSString* getConsoleUser(void)
|
|
{
|
|
//copy/return user
|
|
return CFBridgingRelease(SCDynamicStoreCopyConsoleUser(NULL, NULL, NULL));
|
|
}
|
|
|
|
//get process name
|
|
// either via app bundle, or path
|
|
NSString* getProcessName(NSString* path)
|
|
{
|
|
//process name
|
|
NSString* processName = nil;
|
|
|
|
//app bundle
|
|
NSBundle* appBundle = nil;
|
|
|
|
//try find an app bundle
|
|
appBundle = findAppBundle(path);
|
|
if(nil != appBundle)
|
|
{
|
|
//grab name from app's bundle
|
|
processName = [appBundle infoDictionary][@"CFBundleName"];
|
|
}
|
|
|
|
//still nil?
|
|
// ->just grab from path
|
|
if(nil == processName)
|
|
{
|
|
//from path
|
|
processName = [path lastPathComponent];
|
|
}
|
|
|
|
return processName;
|
|
}
|
|
|
|
//given a path to binary
|
|
// parse it back up to find app's bundle
|
|
NSBundle* findAppBundle(NSString* path)
|
|
{
|
|
//app's bundle
|
|
NSBundle* appBundle = nil;
|
|
|
|
//standarized path
|
|
NSString* standardedPath = nil;
|
|
|
|
//app's path
|
|
NSString* appPath = nil;
|
|
|
|
//standardize path
|
|
standardedPath = [[path stringByStandardizingPath] stringByResolvingSymlinksInPath];
|
|
|
|
//first just try full path
|
|
appPath = standardedPath;
|
|
|
|
//try to find the app's bundle
|
|
do
|
|
{
|
|
//try to load app's bundle
|
|
appBundle = [NSBundle bundleWithPath:appPath];
|
|
|
|
//was an app passed in?
|
|
if(YES == [appBundle.bundlePath isEqualToString:standardedPath])
|
|
{
|
|
//all done
|
|
break;
|
|
}
|
|
|
|
//check for match
|
|
// binary path's match
|
|
if( (nil != appBundle) &&
|
|
(YES == [appBundle.executablePath isEqualToString:standardedPath]))
|
|
{
|
|
//all done
|
|
break;
|
|
}
|
|
|
|
//unset
|
|
appBundle = nil;
|
|
|
|
//remove last part
|
|
// will try this next
|
|
appPath = [appPath stringByDeletingLastPathComponent];
|
|
|
|
//scan until we get to root
|
|
// of course, loop will exit if app info dictionary is found/loaded
|
|
} while( (nil != appPath) &&
|
|
(YES != [appPath isEqualToString:@"/"]) &&
|
|
(YES != [appPath isEqualToString:@""]) );
|
|
|
|
return appBundle;
|
|
}
|
|
|
|
//set dir's|file's group/owner
|
|
BOOL setFileOwner(NSString* path, NSNumber* groupID, NSNumber* ownerID, BOOL recursive)
|
|
{
|
|
//ret var
|
|
BOOL bSetOwner = NO;
|
|
|
|
//owner dictionary
|
|
NSDictionary* fileOwner = nil;
|
|
|
|
//sub paths
|
|
NSArray* subPaths = nil;
|
|
|
|
//full path
|
|
// ->for recursive
|
|
NSString* fullPath = nil;
|
|
|
|
//init permissions dictionary
|
|
fileOwner = @{NSFileGroupOwnerAccountID:groupID, NSFileOwnerAccountID:ownerID};
|
|
|
|
//set group/owner
|
|
if(YES != [[NSFileManager defaultManager] setAttributes:fileOwner ofItemAtPath:path error:NULL])
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: failed to set ownership for %{public}@ (%{public}@)", path, fileOwner);
|
|
goto bail;
|
|
}
|
|
|
|
//dbg msg
|
|
os_log_debug(logHandle, "set ownership for %{public}@ (%{public}@)", path, fileOwner);
|
|
|
|
//do it recursively
|
|
if(YES == recursive)
|
|
{
|
|
//sanity check
|
|
// ->make sure root starts with '/'
|
|
if(YES != [path hasSuffix:@"/"])
|
|
{
|
|
//add '/'
|
|
path = [NSString stringWithFormat:@"%@/", path];
|
|
}
|
|
|
|
//get all subpaths
|
|
subPaths = [[NSFileManager defaultManager] subpathsAtPath:path];
|
|
for(NSString *subPath in subPaths)
|
|
{
|
|
//init full path
|
|
fullPath = [path stringByAppendingString:subPath];
|
|
|
|
//set group/owner
|
|
if(YES != [[NSFileManager defaultManager] setAttributes:fileOwner ofItemAtPath:fullPath error:NULL])
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: failed to set ownership for %{public}@ (%{public}@)", fullPath, fileOwner);
|
|
goto bail;
|
|
}
|
|
}
|
|
}
|
|
|
|
//no errors
|
|
bSetOwner = YES;
|
|
|
|
//bail
|
|
bail:
|
|
|
|
return bSetOwner;
|
|
}
|
|
|
|
//set permissions for file
|
|
BOOL setFilePermissions(NSString* file, int permissions, BOOL recursive)
|
|
{
|
|
//ret var
|
|
BOOL bSetPermissions = NO;
|
|
|
|
//file permissions
|
|
NSDictionary* filePermissions = nil;
|
|
|
|
//root directory
|
|
NSURL* root = nil;
|
|
|
|
//directory enumerator
|
|
NSDirectoryEnumerator* enumerator = nil;
|
|
|
|
//error
|
|
NSError* error = nil;
|
|
|
|
//init dictionary
|
|
filePermissions = @{NSFilePosixPermissions: [NSNumber numberWithInt:permissions]};
|
|
|
|
//apply file permissions recursively
|
|
if(YES == recursive)
|
|
{
|
|
//init root
|
|
root = [NSURL fileURLWithPath:file];
|
|
|
|
//init enumerator
|
|
enumerator = [[NSFileManager defaultManager] enumeratorAtURL:root includingPropertiesForKeys:[NSArray arrayWithObject:NSURLIsDirectoryKey] options:0 errorHandler:nil];
|
|
|
|
//set file permissions on each
|
|
for(NSURL* currentFile in enumerator)
|
|
{
|
|
//set permissions
|
|
if(YES != [[NSFileManager defaultManager] setAttributes:filePermissions ofItemAtPath:currentFile.path error:&error])
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: failed to set permissions for %{public}@ (%{public}@), %{public}@", currentFile.path, filePermissions, error);
|
|
goto bail;
|
|
}
|
|
}
|
|
}
|
|
|
|
//always set permissions on passed in file (or top-level directory)
|
|
// note: recursive enumerator skips root directory, so execute this always
|
|
if(YES != [[NSFileManager defaultManager] setAttributes:filePermissions ofItemAtPath:file error:NULL])
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: failed to set permissions for %{public}@ (%{public}@)", file, filePermissions);
|
|
goto bail;
|
|
}
|
|
|
|
//happy
|
|
bSetPermissions = YES;
|
|
|
|
bail:
|
|
|
|
return bSetPermissions;
|
|
}
|
|
|
|
//get process's path
|
|
NSString* getProcessPath(pid_t pid)
|
|
{
|
|
//task path
|
|
NSString* processPath = nil;
|
|
|
|
//buffer for process path
|
|
char pathBuffer[PROC_PIDPATHINFO_MAXSIZE] = {0};
|
|
|
|
//status
|
|
int status = -1;
|
|
|
|
//'management info base' array
|
|
int mib[3] = {0};
|
|
|
|
//system's size for max args
|
|
unsigned long systemMaxArgs = 0;
|
|
|
|
//process's args
|
|
char* taskArgs = NULL;
|
|
|
|
//# of args
|
|
int numberOfArgs = 0;
|
|
|
|
//size of buffers, etc
|
|
size_t size = 0;
|
|
|
|
//reset buffer
|
|
memset(pathBuffer, 0x0, PROC_PIDPATHINFO_MAXSIZE);
|
|
|
|
//first attempt to get path via 'proc_pidpath()'
|
|
status = proc_pidpath(pid, pathBuffer, sizeof(pathBuffer));
|
|
if(0 != status)
|
|
{
|
|
//init task's name
|
|
processPath = [NSString stringWithUTF8String:pathBuffer];
|
|
}
|
|
//otherwise
|
|
// try via task's args ('KERN_PROCARGS2')
|
|
else
|
|
{
|
|
//init mib
|
|
// ->want system's size for max args
|
|
mib[0] = CTL_KERN;
|
|
mib[1] = KERN_ARGMAX;
|
|
|
|
//set size
|
|
size = sizeof(systemMaxArgs);
|
|
|
|
//get system's size for max args
|
|
if(-1 == sysctl(mib, 2, &systemMaxArgs, &size, NULL, 0))
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//alloc space for args
|
|
taskArgs = malloc(systemMaxArgs);
|
|
if(NULL == taskArgs)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//init mib
|
|
// ->want process args
|
|
mib[0] = CTL_KERN;
|
|
mib[1] = KERN_PROCARGS2;
|
|
mib[2] = pid;
|
|
|
|
//set size
|
|
size = (size_t)systemMaxArgs;
|
|
|
|
//get process's args
|
|
if(-1 == sysctl(mib, 3, taskArgs, &size, NULL, 0))
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//sanity check
|
|
// ensure buffer is somewhat sane
|
|
if(size <= sizeof(int))
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//extract number of args
|
|
memcpy(&numberOfArgs, taskArgs, sizeof(numberOfArgs));
|
|
|
|
//extract task's name
|
|
// follows # of args (int) and is NULL-terminated
|
|
processPath = [NSString stringWithUTF8String:taskArgs + sizeof(int)];
|
|
}
|
|
|
|
bail:
|
|
|
|
//free process args
|
|
if(NULL != taskArgs)
|
|
{
|
|
//free
|
|
free(taskArgs);
|
|
|
|
//reset
|
|
taskArgs = NULL;
|
|
}
|
|
|
|
return processPath;
|
|
}
|
|
|
|
//given a process path and user
|
|
// return array of all matching pids
|
|
NSMutableArray* getProcessIDs(NSString* processPath, int userID)
|
|
{
|
|
//status
|
|
int status = -1;
|
|
|
|
//process IDs
|
|
NSMutableArray* processIDs = nil;
|
|
|
|
//# of procs
|
|
int numberOfProcesses = 0;
|
|
|
|
//array of pids
|
|
pid_t* pids = NULL;
|
|
|
|
//process info struct
|
|
struct kinfo_proc procInfo;
|
|
|
|
//size of struct
|
|
size_t procInfoSize = sizeof(procInfo);
|
|
|
|
//mib
|
|
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, -1};
|
|
|
|
//clear buffer
|
|
memset(&procInfo, 0x0, procInfoSize);
|
|
|
|
//get # of procs
|
|
numberOfProcesses = proc_listallpids(NULL, 0);
|
|
if(-1 == numberOfProcesses)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//alloc buffer for pids
|
|
pids = calloc((unsigned long)numberOfProcesses, sizeof(pid_t));
|
|
|
|
//alloc
|
|
processIDs = [NSMutableArray array];
|
|
|
|
//get list of pids
|
|
status = proc_listallpids(pids, numberOfProcesses * (int)sizeof(pid_t));
|
|
if(status < 0)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//iterate over all pids
|
|
// ->get name for each process
|
|
for(int i = 0; i < (int)numberOfProcesses; i++)
|
|
{
|
|
//skip blank pids
|
|
if(0 == pids[i])
|
|
{
|
|
//skip
|
|
continue;
|
|
}
|
|
|
|
//skip if path doesn't match
|
|
if(YES != [processPath isEqualToString:getProcessPath(pids[i])])
|
|
{
|
|
//next
|
|
continue;
|
|
}
|
|
|
|
//need to also match on user?
|
|
// caller can pass in -1 to skip this check
|
|
if(-1 != userID)
|
|
{
|
|
//init mib
|
|
mib[0x3] = pids[i];
|
|
|
|
//make syscall to get proc info for user
|
|
if( (0 != sysctl(mib, 0x4, &procInfo, &procInfoSize, NULL, 0)) ||
|
|
(0 == procInfoSize) )
|
|
{
|
|
//skip
|
|
continue;
|
|
}
|
|
|
|
//skip if user id doesn't match
|
|
if(userID != (int)procInfo.kp_eproc.e_ucred.cr_uid)
|
|
{
|
|
//skip
|
|
continue;
|
|
}
|
|
}
|
|
|
|
//got match
|
|
// add to list
|
|
[processIDs addObject:[NSNumber numberWithInt:pids[i]]];
|
|
}
|
|
|
|
bail:
|
|
|
|
//free buffer
|
|
if(NULL != pids)
|
|
{
|
|
//free
|
|
free(pids);
|
|
|
|
//reset
|
|
pids = NULL;
|
|
}
|
|
|
|
return processIDs;
|
|
}
|
|
|
|
//enable/disable a menu
|
|
void toggleMenu(NSMenu* menu, BOOL shouldEnable)
|
|
{
|
|
//disable autoenable
|
|
menu.autoenablesItems = NO;
|
|
|
|
//iterate over
|
|
// set state of each item
|
|
for(NSMenuItem* item in menu.itemArray)
|
|
{
|
|
//set state
|
|
item.enabled = shouldEnable;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//get an icon for a process
|
|
// for apps, this will be app's icon, otherwise just a standard system one
|
|
//get icon for a process
|
|
NSImage* getIconForProcess(NSString* path)
|
|
{
|
|
NSImage* icon = nil;
|
|
|
|
//prefer the app's icon
|
|
NSBundle *appBundle = findAppBundle(path);
|
|
if(appBundle) {
|
|
icon = [NSWorkspace.sharedWorkspace iconForFile:appBundle.bundlePath];
|
|
}
|
|
|
|
//otherwise, generic executable icon
|
|
if(!icon) {
|
|
if(@available(macOS 11.0, *)) {
|
|
icon = [NSWorkspace.sharedWorkspace iconForContentType:UTTypeUnixExecutable];
|
|
} else {
|
|
icon = [NSWorkspace.sharedWorkspace iconForFileType:@"public.unix-executable"];
|
|
}
|
|
}
|
|
|
|
[icon setSize:NSMakeSize(128, 128)];
|
|
return icon;
|
|
}
|
|
|
|
//wait until a window is non nil
|
|
// then make it modal
|
|
void makeModal(NSWindowController* windowController)
|
|
{
|
|
//wait up to 1 second window to be non-nil
|
|
// then make modal
|
|
for(int i=0; i<20; i++)
|
|
{
|
|
//can make it modal once we have a window
|
|
if(nil != windowController.window)
|
|
{
|
|
//make modal on main thread
|
|
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
|
|
//modal
|
|
[[NSApplication sharedApplication] runModalForWindow:windowController.window];
|
|
|
|
});
|
|
|
|
//all done
|
|
break;
|
|
}
|
|
|
|
//nap
|
|
[NSThread sleepForTimeInterval:0.05f];
|
|
|
|
}//until 1 second
|
|
|
|
return;
|
|
}
|
|
|
|
//find a process by name
|
|
pid_t findProcess(NSString* processName)
|
|
{
|
|
//pid
|
|
pid_t processID = 0;
|
|
|
|
//status
|
|
int status = -1;
|
|
|
|
//# of procs
|
|
int numberOfProcesses = 0;
|
|
|
|
//array of pids
|
|
pid_t* pids = NULL;
|
|
|
|
//process path
|
|
NSString* processPath = nil;
|
|
|
|
//get # of procs
|
|
numberOfProcesses = proc_listpids(PROC_ALL_PIDS, 0, NULL, 0);
|
|
if(-1 == numberOfProcesses)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//alloc buffer for pids
|
|
pids = calloc((unsigned long)numberOfProcesses, sizeof(pid_t));
|
|
|
|
//get list of pids
|
|
status = proc_listpids(PROC_ALL_PIDS, 0, pids, numberOfProcesses * (int)sizeof(pid_t));
|
|
if(status < 0)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//iterate over all pids
|
|
// get name for each via helper function
|
|
for(int i = 0; i < numberOfProcesses; ++i)
|
|
{
|
|
//skip blank pids
|
|
if(0 == pids[i])
|
|
{
|
|
//skip
|
|
continue;
|
|
}
|
|
|
|
//get name
|
|
processPath = getProcessPath(pids[i]);
|
|
if( (nil == processPath) ||
|
|
(0 == processPath.length) )
|
|
{
|
|
//skip
|
|
continue;
|
|
}
|
|
|
|
//match?
|
|
if(YES == [processPath isEqualToString:processName])
|
|
{
|
|
//save
|
|
processID = pids[i];
|
|
|
|
//pau
|
|
break;
|
|
}
|
|
|
|
}//all procs
|
|
|
|
bail:
|
|
|
|
//free buffer
|
|
if(NULL != pids)
|
|
{
|
|
//free
|
|
free(pids);
|
|
}
|
|
|
|
return processID;
|
|
}
|
|
|
|
//for login item enable/disable
|
|
// we use the launch services APIs, since replacements don't always work :(
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
|
|
//toggle login item
|
|
// either add (install) or remove (uninstall)
|
|
BOOL toggleLoginItem(NSURL* loginItem, int toggleFlag)
|
|
{
|
|
//flag
|
|
BOOL wasToggled = NO;
|
|
|
|
//login item ref
|
|
LSSharedFileListRef loginItemsRef = NULL;
|
|
|
|
//login items
|
|
CFArrayRef loginItems = NULL;
|
|
|
|
//current login item
|
|
CFURLRef currentLoginItem = NULL;
|
|
|
|
//get reference to login items
|
|
loginItemsRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
|
|
|
|
//add (install)
|
|
if(ACTION_INSTALL_FLAG == toggleFlag)
|
|
{
|
|
//dbg msg
|
|
os_log_debug(logHandle, "adding login item %{public}@", loginItem);
|
|
|
|
//add
|
|
LSSharedFileListItemRef itemRef = LSSharedFileListInsertItemURL(loginItemsRef, kLSSharedFileListItemLast, NULL, NULL, (__bridge CFURLRef)(loginItem), NULL, NULL);
|
|
|
|
//release item ref
|
|
if(NULL != itemRef)
|
|
{
|
|
//dbg msg
|
|
os_log_debug(logHandle, "added %{public}@/%{public}@", loginItem, itemRef);
|
|
|
|
//release
|
|
CFRelease(itemRef);
|
|
|
|
//reset
|
|
itemRef = NULL;
|
|
}
|
|
//failed
|
|
else
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: failed to add login item");
|
|
goto bail;
|
|
}
|
|
|
|
//happy
|
|
wasToggled = YES;
|
|
}
|
|
//remove (uninstall)
|
|
else
|
|
{
|
|
//dbg msg
|
|
os_log_debug(logHandle, "removing login item %{public}@", loginItem);
|
|
|
|
//grab existing login items
|
|
loginItems = LSSharedFileListCopySnapshot(loginItemsRef, nil);
|
|
|
|
//iterate over all login items
|
|
// look for self, then remove it
|
|
for(id item in (__bridge NSArray *)loginItems)
|
|
{
|
|
//get current login item
|
|
currentLoginItem = LSSharedFileListItemCopyResolvedURL((__bridge LSSharedFileListItemRef)item, 0, NULL);
|
|
if(NULL == currentLoginItem)
|
|
{
|
|
//skip
|
|
continue;
|
|
}
|
|
|
|
//current login item match self?
|
|
if(YES == [(__bridge NSURL *)currentLoginItem isEqual:loginItem])
|
|
{
|
|
//remove
|
|
if(noErr != LSSharedFileListItemRemove(loginItemsRef, (__bridge LSSharedFileListItemRef)item))
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: failed to remove login item");
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//dbg msg
|
|
os_log_debug(logHandle, "removed login item: %{public}@", loginItem);
|
|
|
|
//happy
|
|
wasToggled = YES;
|
|
|
|
//all done
|
|
goto bail;
|
|
}
|
|
|
|
//release
|
|
CFRelease(currentLoginItem);
|
|
|
|
//reset
|
|
currentLoginItem = NULL;
|
|
|
|
}//all login items
|
|
|
|
}//remove/uninstall
|
|
|
|
bail:
|
|
|
|
//release login items
|
|
if(NULL != loginItems)
|
|
{
|
|
//release
|
|
CFRelease(loginItems);
|
|
|
|
//reset
|
|
loginItems = NULL;
|
|
}
|
|
|
|
//release login ref
|
|
if(NULL != loginItemsRef)
|
|
{
|
|
//release
|
|
CFRelease(loginItemsRef);
|
|
|
|
//reset
|
|
loginItemsRef = NULL;
|
|
}
|
|
|
|
//release url
|
|
if(NULL != currentLoginItem)
|
|
{
|
|
//release
|
|
CFRelease(currentLoginItem);
|
|
|
|
//reset
|
|
currentLoginItem = NULL;
|
|
}
|
|
|
|
return wasToggled;
|
|
}
|
|
|
|
#pragma clang diagnostic pop
|
|
|
|
//check if process is alive
|
|
BOOL isProcessAlive(pid_t processID)
|
|
{
|
|
//ret var
|
|
BOOL bIsAlive = NO;
|
|
|
|
//signal status
|
|
int signalStatus = -1;
|
|
|
|
//send kill with 0 to determine if alive
|
|
signalStatus = kill(processID, 0);
|
|
|
|
//is alive?
|
|
if( (0 == signalStatus) ||
|
|
((0 != signalStatus) && (errno != ESRCH)) )
|
|
{
|
|
//alive!
|
|
bIsAlive = YES;
|
|
}
|
|
|
|
return bIsAlive;
|
|
}
|
|
|
|
|
|
//hash a file
|
|
NSMutableString* hashFile(NSString* filePath)
|
|
{
|
|
//file's contents
|
|
NSData* fileContents = nil;
|
|
|
|
//hash digest
|
|
uint8_t digestSHA256[CC_SHA256_DIGEST_LENGTH] = {0};
|
|
|
|
//hash as string
|
|
NSMutableString* sha256 = nil;
|
|
|
|
//index var
|
|
NSUInteger index = 0;
|
|
|
|
//init
|
|
sha256 = [NSMutableString string];
|
|
|
|
//load file
|
|
if(nil == (fileContents = [NSData dataWithContentsOfFile:filePath]))
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//sha256 it
|
|
CC_SHA256(fileContents.bytes, (unsigned int)fileContents.length, digestSHA256);
|
|
|
|
//convert to NSString
|
|
// iterate over each bytes in computed digest and format
|
|
for(index=0; index < CC_SHA256_DIGEST_LENGTH; index++)
|
|
{
|
|
//format/append
|
|
[sha256 appendFormat:@"%02lX", (unsigned long)digestSHA256[index]];
|
|
}
|
|
|
|
bail:
|
|
|
|
return sha256;
|
|
}
|
|
|
|
/*
|
|
|
|
//given a pid, get its parent (ppid)
|
|
pid_t getParentID(int pid)
|
|
{
|
|
//parent id
|
|
pid_t parentID = -1;
|
|
|
|
//kinfo_proc struct
|
|
struct kinfo_proc processStruct;
|
|
|
|
//size
|
|
size_t procBufferSize = sizeof(processStruct);
|
|
|
|
//mib
|
|
const u_int mibLength = 4;
|
|
|
|
//syscall result
|
|
int sysctlResult = -1;
|
|
|
|
//init mib
|
|
int mib[mibLength] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
|
|
|
|
//clear buffer
|
|
memset(&processStruct, 0x0, procBufferSize);
|
|
|
|
//make syscall
|
|
sysctlResult = sysctl(mib, mibLength, &processStruct, &procBufferSize, NULL, 0);
|
|
|
|
//check if got ppid
|
|
if( (noErr == sysctlResult) &&
|
|
(0 != procBufferSize) )
|
|
{
|
|
//save ppid
|
|
parentID = processStruct.kp_eproc.e_ppid;
|
|
|
|
//dbg msg
|
|
os_log_debug(logHandle, "extracted parent ID %d for process: %d", parentID, pid]);
|
|
}
|
|
|
|
return parentID;
|
|
}
|
|
|
|
*/
|
|
|
|
//start app
|
|
// note: executed with 'NSWorkspaceLaunchWithoutActivation'
|
|
BOOL startApplication(NSURL* path, NSUInteger launchOptions)
|
|
{
|
|
//status var
|
|
BOOL result = NO;
|
|
|
|
//error
|
|
NSError* error = nil;
|
|
|
|
//dbg msg
|
|
os_log_debug(logHandle, "starting application: %{public}@", path);
|
|
|
|
//launch it
|
|
if(nil == [[NSWorkspace sharedWorkspace] launchApplicationAtURL:path options:launchOptions configuration:@{} error:&error])
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: failed to launch application: %{public}@/%{public}@", path, error);
|
|
goto bail;
|
|
}
|
|
|
|
//happy
|
|
result = YES;
|
|
|
|
bail:
|
|
|
|
return result;
|
|
}
|
|
|
|
//wait for file to be written to disk
|
|
void waitForFile(NSString* path, float maxWait)
|
|
{
|
|
//wait interval
|
|
float waitInterval = 0.1f;
|
|
|
|
//count var for loop
|
|
NSUInteger count = 0;
|
|
|
|
//wait for file
|
|
do
|
|
{
|
|
//wait for file
|
|
if(YES == [[NSFileManager defaultManager] fileExistsAtPath:path])
|
|
{
|
|
//happy
|
|
break;
|
|
}
|
|
|
|
//nap
|
|
[NSThread sleepForTimeInterval:waitInterval];
|
|
|
|
//try up to specified max
|
|
} while(count++ < maxWait/waitInterval);
|
|
}
|
|
|
|
//given a bundle path
|
|
// wait for plist, then load bundle
|
|
NSBundle* getBundle(NSString* path, float maxWait)
|
|
{
|
|
//bundle
|
|
NSBundle* bundle = nil;
|
|
|
|
//plist path
|
|
NSString* plist = nil;
|
|
|
|
//sanity check
|
|
if(nil == path) goto bail;
|
|
|
|
//init path to plist in bundle
|
|
plist = [NSString pathWithComponents:@[path, @"/Contents/Info.plist"]];
|
|
|
|
//wait for plist
|
|
// indicator that bundle 'ready'
|
|
waitForFile(plist, maxWait);
|
|
|
|
//now load bundle
|
|
bundle = [NSBundle bundleWithPath:path];
|
|
|
|
bail:
|
|
|
|
//load/return bundle
|
|
return bundle;
|
|
}
|
|
|
|
//extract value from plist
|
|
// takes optional wait time...
|
|
id getValueFromPlist(NSString* plistFile, NSString* plistKey, BOOL insensitiveKey, float maxWait)
|
|
{
|
|
//return var
|
|
id plistValue;
|
|
|
|
//contents of plist
|
|
NSDictionary* plistContents = nil;
|
|
|
|
//wait for file
|
|
waitForFile(plistFile, maxWait);
|
|
|
|
//load it
|
|
plistContents = [NSDictionary dictionaryWithContentsOfFile:plistFile];
|
|
if(nil == plistContents)
|
|
{
|
|
//dbg msg
|
|
os_log_debug(logHandle, "failed to open/read %{public}@", plistFile);
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//try grab value
|
|
plistValue = plistContents[plistKey];
|
|
|
|
//not found?
|
|
// try insensitve search
|
|
if( (nil == plistValue) &&
|
|
(YES == insensitiveKey))
|
|
{
|
|
//search all keys
|
|
for(NSString* key in plistContents)
|
|
{
|
|
//key match?
|
|
// extract value
|
|
if(YES == [key.lowercaseString isEqualToString:plistKey.lowercaseString])
|
|
{
|
|
//found!
|
|
plistValue = plistContents[key];
|
|
|
|
//done
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
bail:
|
|
|
|
return plistValue;
|
|
}
|
|
|
|
//exec a process with args
|
|
// if 'shouldWait' is set, wait and return stdout/in and termination status
|
|
NSMutableDictionary* execTask(NSString* binaryPath, NSArray* arguments, BOOL shouldWait, BOOL grabOutput)
|
|
{
|
|
//task
|
|
NSTask* task = nil;
|
|
|
|
//output pipe for stdout
|
|
NSPipe* stdOutPipe = nil;
|
|
|
|
//output pipe for stderr
|
|
NSPipe* stdErrPipe = nil;
|
|
|
|
//read handle for stdout
|
|
NSFileHandle* stdOutReadHandle = nil;
|
|
|
|
//read handle for stderr
|
|
NSFileHandle* stdErrReadHandle = nil;
|
|
|
|
//results dictionary
|
|
NSMutableDictionary* results = nil;
|
|
|
|
//output for stdout
|
|
NSMutableData *stdOutData = nil;
|
|
|
|
//output for stderr
|
|
NSMutableData *stdErrData = nil;
|
|
|
|
//init dictionary for results
|
|
results = [NSMutableDictionary dictionary];
|
|
|
|
//init task
|
|
task = [[NSTask alloc] init];
|
|
|
|
//sanity check
|
|
// NSTask throws if path isn't found...
|
|
if(YES != [NSFileManager.defaultManager fileExistsAtPath:binaryPath])
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//only setup pipes if wait flag is set
|
|
if(YES == grabOutput)
|
|
{
|
|
//init stdout pipe
|
|
stdOutPipe = [NSPipe pipe];
|
|
|
|
//init stderr pipe
|
|
stdErrPipe = [NSPipe pipe];
|
|
|
|
//init stdout read handle
|
|
stdOutReadHandle = [stdOutPipe fileHandleForReading];
|
|
|
|
//init stderr read handle
|
|
stdErrReadHandle = [stdErrPipe fileHandleForReading];
|
|
|
|
//init stdout output buffer
|
|
stdOutData = [NSMutableData data];
|
|
|
|
//init stderr output buffer
|
|
stdErrData = [NSMutableData data];
|
|
|
|
//set task's stdout
|
|
task.standardOutput = stdOutPipe;
|
|
|
|
//set task's stderr
|
|
task.standardError = stdErrPipe;
|
|
}
|
|
|
|
//set task's path
|
|
task.launchPath = binaryPath;
|
|
|
|
//set task's args
|
|
if(nil != arguments)
|
|
{
|
|
//set
|
|
task.arguments = arguments;
|
|
}
|
|
|
|
//dbg msg
|
|
os_log_debug(logHandle, "execing task, %{public}@ (arguments: %{public}@)", task.launchPath, task.arguments);
|
|
|
|
//wrap task launch
|
|
@try
|
|
{
|
|
//launch
|
|
[task launch];
|
|
}
|
|
@catch(NSException *exception)
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: failed to launch task (%{public}@)", exception);
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//no need to wait
|
|
// can just bail w/ no output
|
|
if( (YES != shouldWait) &&
|
|
(YES != grabOutput) )
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//wait
|
|
// ...but no output
|
|
else if( (YES == shouldWait) &&
|
|
(YES != grabOutput) )
|
|
{
|
|
//wait
|
|
[task waitUntilExit];
|
|
|
|
//add exit code
|
|
results[EXIT_CODE] = [NSNumber numberWithInteger:task.terminationStatus];
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//grab output?
|
|
// even if wait not set, still will wait!
|
|
else
|
|
{
|
|
//read in stdout/stderr
|
|
while(YES == [task isRunning])
|
|
{
|
|
//accumulate stdout
|
|
[stdOutData appendData:[stdOutReadHandle readDataToEndOfFile]];
|
|
|
|
//accumulate stderr
|
|
[stdErrData appendData:[stdErrReadHandle readDataToEndOfFile]];
|
|
}
|
|
|
|
//grab any leftover stdout
|
|
[stdOutData appendData:[stdOutReadHandle readDataToEndOfFile]];
|
|
|
|
//grab any leftover stderr
|
|
[stdErrData appendData:[stdErrReadHandle readDataToEndOfFile]];
|
|
|
|
//add stdout
|
|
if(0 != stdOutData.length)
|
|
{
|
|
//add
|
|
results[STDOUT] = stdOutData;
|
|
}
|
|
|
|
//add stderr
|
|
if(0 != stdErrData.length)
|
|
{
|
|
//add
|
|
results[STDERR] = stdErrData;
|
|
}
|
|
|
|
//add exit code
|
|
results[EXIT_CODE] = [NSNumber numberWithInteger:task.terminationStatus];
|
|
}
|
|
|
|
bail:
|
|
|
|
//dbg msg
|
|
os_log_debug(logHandle, "task completed with %{public}@", results);
|
|
|
|
return results;
|
|
}
|
|
|
|
|
|
//loads a framework
|
|
// note: assumes it is in 'Framework' dir
|
|
NSBundle* loadFramework(NSString* name)
|
|
{
|
|
//handle
|
|
NSBundle* framework = nil;
|
|
|
|
//framework path
|
|
NSString* path = nil;
|
|
|
|
//init path
|
|
path = [NSString stringWithFormat:@"%@/../Frameworks/%@", [NSProcessInfo.processInfo.arguments[0] stringByDeletingLastPathComponent], name];
|
|
|
|
//standardize path
|
|
path = [path stringByStandardizingPath];
|
|
|
|
//init framework (bundle)
|
|
framework = [NSBundle bundleWithPath:path];
|
|
if(NULL == framework)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//load framework
|
|
if(YES != [framework loadAndReturnError:nil])
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
bail:
|
|
|
|
return framework;
|
|
}
|
|
|
|
//check if a file is restricted (SIP)
|
|
BOOL isFileRestricted(NSString* file)
|
|
{
|
|
//flag
|
|
BOOL restricted = NO;
|
|
|
|
//info
|
|
struct stat info = {0};
|
|
|
|
//clear
|
|
memset(&info, 0x0, sizeof(struct stat));
|
|
|
|
//get file info
|
|
if(0 == lstat(file.UTF8String, &info))
|
|
{
|
|
//check flags
|
|
restricted = (BOOL)(info.st_flags & SF_RESTRICTED);
|
|
}
|
|
|
|
return restricted;
|
|
}
|
|
|
|
//in dark mode?
|
|
BOOL isDarkMode(void)
|
|
{
|
|
return [[[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"] isEqualToString:@"Dark"];
|
|
}
|
|
|
|
|
|
//convert es_string_token_t to string
|
|
NSString* convertStringToken(es_string_token_t* stringToken)
|
|
{
|
|
//string
|
|
NSString* string = nil;
|
|
|
|
//sanity check(s)
|
|
if( (NULL == stringToken) ||
|
|
(NULL == stringToken->data) ||
|
|
(stringToken->length <= 0) )
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//convert to data, then to string
|
|
string = [[NSString alloc] initWithBytes:stringToken->data length:stringToken->length encoding:NSUTF8StringEncoding];
|
|
|
|
bail:
|
|
|
|
return string;
|
|
}
|
|
|
|
//convert a textview to a clickable hyperlink
|
|
void makeTextViewHyperlink(NSTextField* textField, NSURL* url)
|
|
{
|
|
//hyperlink
|
|
NSMutableAttributedString *hyperlinkString = nil;
|
|
|
|
//range
|
|
NSRange range = {0};
|
|
|
|
//init hyper link
|
|
hyperlinkString = [[NSMutableAttributedString alloc] initWithString:textField.stringValue];
|
|
|
|
//init range
|
|
range = NSMakeRange(0, [hyperlinkString length]);
|
|
|
|
//start editing
|
|
[hyperlinkString beginEditing];
|
|
|
|
//add url
|
|
[hyperlinkString addAttribute:NSLinkAttributeName value:url range:range];
|
|
|
|
//make it blue
|
|
[hyperlinkString addAttribute:NSForegroundColorAttributeName value:[NSColor blueColor] range:NSMakeRange(0, [hyperlinkString length])];
|
|
|
|
//underline
|
|
[hyperlinkString addAttribute:
|
|
NSUnderlineStyleAttributeName value:[NSNumber numberWithInt:NSSingleUnderlineStyle] range:NSMakeRange(0, [hyperlinkString length])];
|
|
|
|
//done editing
|
|
[hyperlinkString endEditing];
|
|
|
|
//set text
|
|
[textField setAttributedStringValue:hyperlinkString];
|
|
|
|
return;
|
|
}
|
|
|
|
#ifdef DAEMON_BUILD
|
|
|
|
//get current working directory of process
|
|
NSString* getCWD(pid_t pid) {
|
|
|
|
struct proc_vnodepathinfo vinfo = {0};
|
|
|
|
if(proc_pidinfo(pid, PROC_PIDVNODEPATHINFO, 0, &vinfo, sizeof(vinfo)) > 0) {
|
|
NSString* cwd = [NSString stringWithUTF8String:vinfo.pvi_cdir.vip_path];
|
|
if(cwd.length) {
|
|
return [cwd stringByResolvingSymlinksInPath];
|
|
}
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
//extract scripts from process arguments
|
|
NSArray* getScripts(pid_t pid, NSMutableArray* args, NSString* cwd) {
|
|
|
|
if(args.count < 2) {
|
|
return nil;
|
|
}
|
|
|
|
if(!cwd) {
|
|
cwd = getCWD(pid);
|
|
}
|
|
|
|
BOOL isDirectory = NO;
|
|
NSMutableArray* scripts = [NSMutableArray array];
|
|
|
|
for(NSUInteger i = 1; i < args.count; i++) {
|
|
|
|
NSString* arg = args[i];
|
|
|
|
//resolve relative paths against cwd
|
|
NSString* fullPath = nil;
|
|
if([arg hasPrefix:@"/"]) {
|
|
fullPath = arg;
|
|
} else if(cwd) {
|
|
fullPath = [cwd stringByAppendingPathComponent:arg];
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
//resolve symlinks for consistent paths
|
|
fullPath = [fullPath stringByResolvingSymlinksInPath];
|
|
|
|
//must be an existing regular file
|
|
if([NSFileManager.defaultManager fileExistsAtPath:fullPath isDirectory:&isDirectory] && !isDirectory) {
|
|
[scripts addObject:fullPath];
|
|
}
|
|
}
|
|
|
|
return scripts.count > 0 ? scripts : nil;
|
|
}
|
|
|
|
//mach time to nanoseconds
|
|
// from: https://developer.apple.com/documentation/apple-silicon/addressing-architectural-differences-in-your-macos-code
|
|
uint64_t machTimeToNanoseconds(uint64_t machTime)
|
|
{
|
|
uint64_t nanoseconds = 0;
|
|
static mach_timebase_info_data_t sTimebase;
|
|
if (sTimebase.denom == 0)
|
|
(void)mach_timebase_info(&sTimebase);
|
|
|
|
nanoseconds = ((machTime * sTimebase.numer) / sTimebase.denom);
|
|
|
|
return nanoseconds;
|
|
}
|
|
|
|
//check if item is downloaded
|
|
BOOL isDownloaded(NSString* path) {
|
|
|
|
//sanity check
|
|
if(!path.length) {
|
|
return NO;
|
|
}
|
|
|
|
//get flags
|
|
uint32_t quarantineFlags = getQuarantineFlags(path);
|
|
|
|
//not quarantined?
|
|
if(quarantineFlags == QTN_NOT_QUARANTINED) {
|
|
os_log_debug(logHandle, "%{public}@ is not quarantined", path);
|
|
return NO;
|
|
}
|
|
|
|
//quarantined, but allowed
|
|
if(quarantineFlags & QTN_FLAG_USER_APPROVED) {
|
|
os_log_debug(logHandle, "%{public}@ is quarantined, but user approved", path);
|
|
return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
//get items quarantine flags
|
|
// thanks: https://trac.webkit.org/changeset/281056/webkit
|
|
uint32_t getQuarantineFlags(NSString* path)
|
|
{
|
|
//error
|
|
int error = noErr;
|
|
|
|
//flags
|
|
uint32_t flags = QTN_NOT_QUARANTINED;
|
|
|
|
//once token
|
|
static dispatch_once_t onceToken = 0;
|
|
|
|
//dylib handle
|
|
static void* handle = NULL;
|
|
|
|
//function pointers
|
|
static qtn_file_t(*qtn_file_alloc_FP)(void) = NULL;
|
|
static void (*qtn_file_free_FP)(qtn_file_t qf) = NULL;
|
|
static uint32_t (*qtn_file_get_flags_FP)(qtn_file_t qf) = NULL;
|
|
static int (*qtn_file_init_with_path_FP)(qtn_file_t qf, const char *path) = NULL;
|
|
|
|
//quarantine file
|
|
qtn_file_t quarantineFile = NULL;
|
|
|
|
//dbg msg
|
|
os_log_debug(logHandle, "checking if %{public}@ is quarantined", path);
|
|
|
|
//sanity check(s)
|
|
if(0 == path.length)
|
|
{
|
|
//err msg
|
|
os_log_debug(logHandle, "invalid path");
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//load/open framework
|
|
dispatch_once(&onceToken, ^{
|
|
|
|
//open quarantine dylib
|
|
handle = dlopen("/usr/lib/system/libquarantine.dylib", RTLD_LAZY);
|
|
if(NULL != handle)
|
|
{
|
|
//resolve function pointers
|
|
qtn_file_free_FP = dlsym(handle, "_qtn_file_free");
|
|
qtn_file_alloc_FP = dlsym(handle, "_qtn_file_alloc");
|
|
qtn_file_get_flags_FP = dlsym(handle, "_qtn_file_get_flags");
|
|
qtn_file_init_with_path_FP = dlsym(handle, "_qtn_file_init_with_path");
|
|
}
|
|
//err
|
|
else
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: failed to 'dlopen' the 'libquarantine.dylib'");
|
|
}
|
|
|
|
});
|
|
|
|
//sanity check(s)
|
|
if( (NULL == qtn_file_free_FP) ||
|
|
(NULL == qtn_file_alloc_FP) ||
|
|
(NULL == qtn_file_get_flags_FP) ||
|
|
(NULL == qtn_file_init_with_path_FP) )
|
|
{
|
|
//err msg
|
|
os_log_error(logHandle, "ERROR: failed to resolve 'libquarantine' function pointers");
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//alloc file
|
|
quarantineFile = qtn_file_alloc_FP();
|
|
if(NULL == quarantineFile)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//init file
|
|
error = qtn_file_init_with_path_FP(quarantineFile, [NSURL fileURLWithPath:path].path.fileSystemRepresentation);
|
|
if(ENOENT == error)
|
|
{
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//not quarantined?
|
|
if(QTN_NOT_QUARANTINED == error)
|
|
{
|
|
//dbg msg
|
|
os_log_debug(logHandle, "%{public}@ is *not* quarantined (QTN_NOT_QUARANTINED)", path);
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//get flags
|
|
flags = qtn_file_get_flags_FP(quarantineFile);
|
|
|
|
//dbg msg
|
|
os_log_debug(logHandle, "quarantine flags: %#x", flags);
|
|
|
|
bail:
|
|
|
|
//cleanup
|
|
if(NULL != quarantineFile)
|
|
{
|
|
//free
|
|
qtn_file_free_FP(quarantineFile);
|
|
quarantineFile = NULL;
|
|
}
|
|
|
|
return flags;
|
|
}
|
|
|
|
#endif
|