Search code examples
objective-ccocoaosx-gatekeeperxattr

Checking if macOS App Has Ever Been Un-Quarantined and Fully Launched


I'm trying to check whether an app downloaded from the internet has ever been fully launched. I'm trying to use xattr -p com.apple.quarantine for this, but the return value from this command, doesn't appear to be consistent.

On one Mac, I get these two Gatekeeper Score values in the return: 0183 if the app has never fully launched, and 01c3 if the app has launched and the user clicked Open in the GateKeeper "Do you really want to open this app" dialog. On another Mac I get completely different values: 0003 & 0063.

I'm guessing these are 4 digit hex numbers which I can convert like this:

NSString *gateKeeperScore = [outputItems firstObject];
NSScanner *scanner = [NSScanner scannerWithString:gateKeeperScore];
unsigned int number = 0;
if ([scanner scanHexInt:&number]) {
    NSLog(@"Gatekeeper Score is %u", number);
}

But is there a threshold, and once the score is over that threshold I can safely assume the app has launched fully and is no longer quarantined?

I've tried running a SQL select statement and getting the entire contents from table LSQuarantineEvent in ~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2, and then using grep to find the relevant row, but I'm not seeing any changes in the row before/after the app launches fully.

Is there any way to determine whether an app can fully launch and isn't quarantined? I'm trying to accomplish this using Objective-C. No sandbox. Thanks in advance!

Here's some sample code of what I am doing:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    NSString *output1 = [self runTask: [NSArray arrayWithObjects:@"-c", @"xattr -p com.apple.quarantine '/path/to/app'", nil]];

    NSArray *outputItems = [output1 componentsSeparatedByString:@";"];
    NSString *UUID = [outputItems lastObject];
    UUID = [UUID stringByReplacingOccurrencesOfString:@"[\r\n]" withString:@"" options:NSRegularExpressionSearch range:NSMakeRange(0, UUID.length)];

    NSString *output2 = [self runTask: [NSArray arrayWithObjects:@"-c", [NSString stringWithFormat:@"sqlite3 ~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2 \"SELECT * FROM LSQuarantineEvent WHERE LSQuarantineEventIdentifier == '%@'\"", UUID], nil]];
}

- (NSString *) runTask : (NSArray *) args
{
    NSTask *task = [[NSTask alloc] init];
    [task setLaunchPath: @"/bin/bash"];

    [task setArguments:args];

    NSPipe * taskOutput = [NSPipe pipe];
    [task setStandardOutput:taskOutput];

    [task launch];
    [task waitUntilExit];

    NSFileHandle * read = [taskOutput fileHandleForReading];
    NSData * dataRead = [read readDataToEndOfFile];
    NSString * taskOutputString = [[NSString alloc] initWithData:dataRead encoding:NSUTF8StringEncoding];

    return taskOutputString;
}

The value of output1 looks like this:

01c3;5e31c850;Safari;92CB3715-7A0F-4582-9FF3-9B0CBE2A23BB

Only the first 4 characters change before/after the app is fully launched.

And the value of output2 looks like this:

92CB3715-7A0F-4582-9FF3-9B0CBE2A23BB|602013648.990506|com.apple.Safari|Safari|https://url/of/files/origin|||0|||

This value/row in the SQL DB never seems to change.

I've read through this post and this one but I'm not seeing any way to accomplish what I'm looking to do.


Solution

  • You’ve missed one crucial point in the article you’ve linked. Don’t treat the numbers as magic numbers. These are flags and you should inspect individual bits to check a certain attribute.

    The article states (and your own answer somewhat confirms) that 6th bit is “App was opened” flag and 7th bit is “Verified by Gatekeeper”.

    You can check in Calculator app that these two bits change in your examples:

    calculator

    Example check in Swift:

    let flags = 0x1e3
    
    let checked =  (flags & 0b01000000) != 0 // true
    let launched = (flags & 0b00100000) != 0 // true