2 * Copyright (c) 2017-2018 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
26 #if !TARGET_OS_SIMULATOR
28 #import "SFAnalyticsDefines.h"
29 #import "SFAnalyticsSQLiteStore.h"
30 #import <Security/SFAnalytics.h>
32 #include <utilities/SecFileLocations.h>
33 #import "utilities/debugging.h"
34 #import <os/variant_private.h>
37 #import "keychain/ckks/CKKSControl.h"
40 #import <AuthKit/AKAppleIDAuthenticationContext.h>
41 #import <AuthKit/AKAppleIDAuthenticationController.h>
42 #import <AuthKit/AKAppleIDAuthenticationController_Private.h>
45 #include "dirhelper_priv.h"
49 #import <CrashReporterSupport/CrashReporterSupportPrivate.h>
51 #import <CrashReporterSupport/CrashReporterSupport.h>
54 #import <Accounts/Accounts.h>
55 #import <Accounts/ACAccountStore_Private.h>
56 #import <Accounts/ACAccountType_Private.h>
57 #import <Accounts/ACAccountStore.h>
58 #import <AppleAccount/AppleAccount.h>
59 #import <AppleAccount/ACAccount+AppleAccount.h>
60 #import <AppleAccount/ACAccountStore+AppleAccount.h>
63 NSString* const SFAnalyticsSplunkTopic = @"topic";
64 NSString* const SFAnalyticsSplunkPostTime = @"postTime";
65 NSString* const SFAnalyticsClientId = @"clientId";
66 NSString* const SFAnalyticsInternal = @"internal";
68 NSString* const SFAnalyticsMetricsBase = @"metricsBase";
69 NSString* const SFAnalyticsDeviceID = @"ckdeviceID";
70 NSString* const SFAnalyticsAltDSID = @"altDSID";
72 NSString* const SFAnalyticsSecondsCustomerKey = @"SecondsBetweenUploadsCustomer";
73 NSString* const SFAnalyticsSecondsInternalKey = @"SecondsBetweenUploadsInternal";
74 NSString* const SFAnalyticsSecondsSeedKey = @"SecondsBetweenUploadsSeed";
75 NSString* const SFAnalyticsMaxEventsKey = @"NumberOfEvents";
76 NSString* const SFAnalyticsDevicePercentageCustomerKey = @"DevicePercentageCustomer";
77 NSString* const SFAnalyticsDevicePercentageInternalKey = @"DevicePercentageInternal";
78 NSString* const SFAnalyticsDevicePercentageSeedKey = @"DevicePercentageSeed";
80 NSString* const SupdErrorDomain = @"com.apple.security.supd";
82 #define SFANALYTICS_SPLUNK_DEV 0
83 #define OS_CRASH_TRACER_LOG_BUG_TYPE "226"
85 #if SFANALYTICS_SPLUNK_DEV
86 NSUInteger const secondsBetweenUploadsCustomer = 10;
87 NSUInteger const secondsBetweenUploadsInternal = 10;
88 NSUInteger const secondsBetweenUploadsSeed = 10;
89 #else // SFANALYTICS_SPLUNK_DEV
90 NSUInteger const secondsBetweenUploadsCustomer = (3 * (60 * 60 * 24));
91 NSUInteger const secondsBetweenUploadsInternal = (60 * 60 * 24);
92 NSUInteger const secondsBetweenUploadsSeed = (60 * 60 * 24);
93 #endif // SFANALYTICS_SPLUNK_DEV
95 @implementation SFAnalyticsReporter
96 - (BOOL)saveReport:(NSData *)reportData fileName:(NSString *)fileName
98 BOOL writtenToLog = NO;
100 NSDictionary *optionsDictionary = @{ (__bridge NSString *)kCRProblemReportSubmissionPolicyKey: (__bridge NSString *)kCRSubmissionPolicyAlternate };
101 #else // !TARGET_OS_OSX
102 NSDictionary *optionsDictionary = nil; // The keys above are not defined or required on iOS.
103 #endif // !TARGET_OS_OSX
106 secdebug("saveReport", "calling out to `OSAWriteLogForSubmission`");
107 writtenToLog = OSAWriteLogForSubmission(@OS_CRASH_TRACER_LOG_BUG_TYPE, fileName,
108 nil, optionsDictionary, ^(NSFileHandle *fileHandle) {
109 secnotice("OSAWriteLogForSubmission", "Writing log data to report: %@", fileName);
110 [fileHandle writeData:reportData];
116 #define DEFAULT_SPLUNK_MAX_EVENTS_TO_REPORT 1000
117 #define DEFAULT_SPLUNK_DEVICE_PERCENTAGE 100
119 static supd *_supdInstance = nil;
121 BOOL runningTests = NO;
122 BOOL deviceAnalyticsOverride = NO;
123 BOOL deviceAnalyticsEnabled = NO;
124 BOOL iCloudAnalyticsOverride = NO;
125 BOOL iCloudAnalyticsEnabled = NO;
128 _isDeviceAnalyticsEnabled(void)
130 // This flag is only set during tests.
131 if (deviceAnalyticsOverride) {
132 return deviceAnalyticsEnabled;
135 static BOOL dataCollectionEnabled = NO;
136 static dispatch_once_t onceToken;
137 dispatch_once(&onceToken, ^{
139 dataCollectionEnabled = DiagnosticLogSubmissionEnabled();
141 dataCollectionEnabled = CRIsAutoSubmitEnabled();
144 return dataCollectionEnabled;
150 ACAccountStore *accountStore = [[ACAccountStore alloc] init];
151 ACAccount *primaryAccount = [accountStore aa_primaryAppleAccount];
152 if (primaryAccount == nil) {
155 return [primaryAccount aa_altDSID];
158 static NSString *const kAnalyticsiCloudIdMSKey = @"com.apple.idms.config.privacy.icloud.data";
160 static NSDictionary *
161 _getiCloudConfigurationInfoWithError(NSError **outError)
163 __block NSDictionary *outConfigurationInfo = nil;
164 __block NSError *localError = nil;
166 NSString *altDSID = accountAltDSID();
167 if (altDSID != nil) {
168 secnotice("_getiCloudConfigurationInfoWithError", "Fetching configuration info");
170 dispatch_semaphore_t sema = dispatch_semaphore_create(0);
171 AKAppleIDAuthenticationController *authController = [AKAppleIDAuthenticationController new];
172 [authController configurationInfoWithIdentifiers:@[kAnalyticsiCloudIdMSKey]
174 completion:^(NSDictionary<NSString *, id<NSSecureCoding>> *configurationInfo, NSError *error) {
176 secerror("_getiCloudConfigurationInfoWithError: Error fetching configurationInfo: %@", error);
178 } else if (![configurationInfo isKindOfClass:[NSDictionary class]]) {
179 secerror("_getiCloudConfigurationInfoWithError: configurationInfo dict was not a dict, it was a %{public}@", [configurationInfo class]);
181 configurationInfo = nil;
183 secnotice("_getiCloudConfigurationInfoWithError", "fetched configurationInfo %@", configurationInfo);
184 outConfigurationInfo = configurationInfo;
186 dispatch_semaphore_signal(sema);
188 dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, (uint64_t)(5 * NSEC_PER_SEC)));
190 secerror("_getiCloudConfigurationInfoWithError: Failed to fetch primary account info.");
193 if (localError && outError) {
194 *outError = localError;
196 return outConfigurationInfo;
200 _isiCloudAnalyticsEnabled()
202 // This flag is only set during tests.
203 if (iCloudAnalyticsOverride) {
204 return iCloudAnalyticsEnabled;
207 static bool cachedAllowsICloudAnalytics = false;
209 static dispatch_once_t onceToken;
210 dispatch_once(&onceToken, ^{
211 NSError *error = nil;
212 NSDictionary *accountConfiguration = _getiCloudConfigurationInfoWithError(&error);
213 if (error == nil && accountConfiguration != nil) {
214 id iCloudAnalyticsOptIn = accountConfiguration[kAnalyticsiCloudIdMSKey];
215 if (iCloudAnalyticsOptIn != nil) {
216 BOOL iCloudAnalyticsOptInHasCorrectType = ([iCloudAnalyticsOptIn isKindOfClass:[NSNumber class]] || [iCloudAnalyticsOptIn isKindOfClass:[NSString class]]);
217 if (iCloudAnalyticsOptInHasCorrectType) {
218 NSNumber *iCloudAnalyticsOptInNumber = @([iCloudAnalyticsOptIn integerValue]);
219 cachedAllowsICloudAnalytics = ![iCloudAnalyticsOptInNumber isEqualToNumber:[NSNumber numberWithInteger:0]];
222 } else if (error != nil) {
223 secerror("_isiCloudAnalyticsEnabled: %@", error);
227 return cachedAllowsICloudAnalytics;
230 /* NSData GZip category based on GeoKit's implementation */
231 @interface NSData (GZip)
232 - (NSData *)supd_gzipDeflate;
235 #define GZIP_OFFSET 16
236 #define GZIP_STRIDE_LEN 16384
238 @implementation NSData (Gzip)
239 - (NSData *)supd_gzipDeflate
241 if ([self length] == 0) {
246 memset(&strm, 0, sizeof(strm));
247 strm.next_in=(uint8_t *)[self bytes];
248 strm.avail_in = (unsigned int)[self length];
251 if (Z_OK != deflateInit2(&strm, Z_BEST_COMPRESSION, Z_DEFLATED,
252 MAX_WBITS + GZIP_OFFSET, MAX_MEM_LEVEL, Z_DEFAULT_STRATEGY)) {
256 NSMutableData *compressed = [NSMutableData dataWithLength:GZIP_STRIDE_LEN];
259 if (strm.total_out >= [compressed length]) {
260 [compressed increaseLengthBy: 16384];
263 strm.next_out = [compressed mutableBytes] + strm.total_out;
264 strm.avail_out = (int)[compressed length] - (int)strm.total_out;
266 deflate(&strm, Z_FINISH);
268 } while (strm.avail_out == 0);
272 [compressed setLength: strm.total_out];
273 if (strm.avail_in == 0) {
274 return [NSData dataWithData:compressed];
281 @implementation SFAnalyticsClient {
284 BOOL _requireDeviceAnalytics;
285 BOOL _requireiCloudAnalytics;
288 @synthesize storePath = _path;
289 @synthesize name = _name;
291 - (instancetype)initWithStorePath:(NSString*)path name:(NSString*)name
292 deviceAnalytics:(BOOL)deviceAnalytics iCloudAnalytics:(BOOL)iCloudAnalytics {
293 if (self = [super init]) {
296 _requireDeviceAnalytics = deviceAnalytics;
297 _requireiCloudAnalytics = iCloudAnalytics;
304 @interface SFAnalyticsTopic ()
305 @property NSURL* _splunkUploadURL;
307 @property BOOL allowInsecureSplunkCert;
308 @property BOOL ignoreServersMessagesTellingUsToGoAway;
309 @property BOOL disableUploads;
310 @property BOOL disableClientId;
312 @property NSUInteger secondsBetweenUploads;
313 @property NSUInteger maxEventsToReport;
314 @property float devicePercentage; // for sampling reporting devices
316 @property NSDictionary* metricsBase; // data the server provides and wants us to send back
317 @property NSArray* blacklistedFields;
318 @property NSArray* blacklistedEvents;
321 @implementation SFAnalyticsTopic
323 - (void)setupClientsForTopic:(NSString *)topicName
325 NSMutableArray<SFAnalyticsClient*>* clients = [NSMutableArray<SFAnalyticsClient*> new];
326 if ([topicName isEqualToString:SFAnalyticsTopicKeySync]) {
327 [clients addObject:[[SFAnalyticsClient alloc] initWithStorePath:[self.class databasePathForCKKS]
328 name:@"ckks" deviceAnalytics:NO iCloudAnalytics:YES]];
329 [clients addObject:[[SFAnalyticsClient alloc] initWithStorePath:[self.class databasePathForSOS]
330 name:@"sos" deviceAnalytics:NO iCloudAnalytics:YES]];
331 [clients addObject:[[SFAnalyticsClient alloc] initWithStorePath:[self.class databasePathForPCS]
332 name:@"pcs" deviceAnalytics:NO iCloudAnalytics:YES]];
333 [clients addObject:[[SFAnalyticsClient alloc] initWithStorePath:[self.class databasePathForSignIn]
334 name:@"signins" deviceAnalytics:NO iCloudAnalytics:YES]];
335 [clients addObject:[[SFAnalyticsClient alloc] initWithStorePath:[self.class databasePathForLocal]
336 name:@"local" deviceAnalytics:YES iCloudAnalytics:NO]];
337 } else if ([topicName isEqualToString:SFAnalyticsTopicCloudServices]) {
338 [clients addObject:[[SFAnalyticsClient alloc] initWithStorePath:[self.class databasePathForCloudServices]
339 name:@"CloudServices"
341 iCloudAnalytics:NO]];
342 } else if ([topicName isEqualToString:SFAnalyticsTopicTrust]) {
344 _set_user_dir_suffix("com.apple.trustd"); // supd needs to read trustd's cache dir for these
346 [clients addObject:[[SFAnalyticsClient alloc] initWithStorePath:[self.class databasePathForTrust]
347 name:@"trust" deviceAnalytics:YES iCloudAnalytics:NO]];
348 [clients addObject:[[SFAnalyticsClient alloc] initWithStorePath:[self.class databasePathForTrustdHealth]
349 name:@"trustdHealth" deviceAnalytics:YES iCloudAnalytics:NO]];
350 [clients addObject:[[SFAnalyticsClient alloc] initWithStorePath:[self.class databasePathForTLS]
351 name:@"tls" deviceAnalytics:YES iCloudAnalytics:NO]];
354 _set_user_dir_suffix(NULL); // set back to the default cache dir
356 } else if ([topicName isEqualToString:SFAnalyticsTopicTransparency]) {
357 [clients addObject:[[SFAnalyticsClient alloc] initWithStorePath:[self.class databasePathForTransparency]
358 name:@"transparency" deviceAnalytics:NO iCloudAnalytics:YES]];
361 _topicClients = clients;
364 - (instancetype)initWithDictionary:(NSDictionary *)dictionary name:(NSString *)topicName samplingRates:(NSDictionary *)rates {
365 if (self = [super init]) {
366 _internalTopicName = topicName;
367 [self setupClientsForTopic:topicName];
368 _splunkTopicName = dictionary[@"splunk_topic"];
369 __splunkUploadURL = [NSURL URLWithString:dictionary[@"splunk_uploadURL"]];
370 _splunkBagURL = [NSURL URLWithString:dictionary[@"splunk_bagURL"]];
371 _allowInsecureSplunkCert = [[dictionary valueForKey:@"splunk_allowInsecureCertificate"] boolValue];
372 NSString* splunkEndpoint = dictionary[@"splunk_endpointDomain"];
373 if (dictionary[@"disableClientId"]) {
374 _disableClientId = YES;
377 NSUserDefaults* defaults = [[NSUserDefaults alloc] initWithSuiteName:SFAnalyticsUserDefaultsSuite];
378 NSString* userDefaultsSplunkTopic = [defaults stringForKey:@"splunk_topic"];
379 if (userDefaultsSplunkTopic) {
380 _splunkTopicName = userDefaultsSplunkTopic;
383 NSURL* userDefaultsSplunkUploadURL = [NSURL URLWithString:[defaults stringForKey:@"splunk_uploadURL"]];
384 if (userDefaultsSplunkUploadURL) {
385 __splunkUploadURL = userDefaultsSplunkUploadURL;
388 NSURL* userDefaultsSplunkBagURL = [NSURL URLWithString:[defaults stringForKey:@"splunk_bagURL"]];
389 if (userDefaultsSplunkBagURL) {
390 _splunkBagURL = userDefaultsSplunkBagURL;
393 BOOL userDefaultsAllowInsecureSplunkCert = [defaults boolForKey:@"splunk_allowInsecureCertificate"];
394 _allowInsecureSplunkCert |= userDefaultsAllowInsecureSplunkCert;
396 NSString* userDefaultsSplunkEndpoint = [defaults stringForKey:@"splunk_endpointDomain"];
397 if (userDefaultsSplunkEndpoint) {
398 splunkEndpoint = userDefaultsSplunkEndpoint;
401 #if SFANALYTICS_SPLUNK_DEV
402 _secondsBetweenUploads = secondsBetweenUploadsInternal;
403 _maxEventsToReport = SFAnalyticsMaxEventsToReport;
404 _devicePercentage = DEFAULT_SPLUNK_DEVICE_PERCENTAGE;
406 bool internal = os_variant_has_internal_diagnostics("com.apple.security");
409 NSNumber *secondsNum = internal ? rates[SFAnalyticsSecondsInternalKey] : rates[SFAnalyticsSecondsSeedKey];
410 NSNumber *percentageNum = internal ? rates[SFAnalyticsDevicePercentageInternalKey] : rates[SFAnalyticsDevicePercentageSeedKey];
412 NSNumber *secondsNum = internal ? rates[SFAnalyticsSecondsInternalKey] : rates[SFAnalyticsSecondsCustomerKey];
413 NSNumber *percentageNum = internal ? rates[SFAnalyticsDevicePercentageInternalKey] : rates[SFAnalyticsDevicePercentageCustomerKey];
415 _secondsBetweenUploads = [secondsNum integerValue];
416 _maxEventsToReport = [rates[SFAnalyticsMaxEventsKey] unsignedIntegerValue];
417 _devicePercentage = [percentageNum floatValue];
420 _secondsBetweenUploads = internal ? secondsBetweenUploadsInternal : secondsBetweenUploadsSeed;
422 _secondsBetweenUploads = internal ? secondsBetweenUploadsInternal : secondsBetweenUploadsCustomer;
424 _maxEventsToReport = SFAnalyticsMaxEventsToReport;
425 _devicePercentage = DEFAULT_SPLUNK_DEVICE_PERCENTAGE;
428 secnotice("supd", "created %@ with %lu seconds between uploads, %lu max events, %f percent of uploads",
429 _internalTopicName, (unsigned long)_secondsBetweenUploads, (unsigned long)_maxEventsToReport, _devicePercentage);
431 #if SFANALYTICS_SPLUNK_DEV
432 _ignoreServersMessagesTellingUsToGoAway = YES;
434 if (!_splunkUploadURL && splunkEndpoint) {
435 NSString* urlString = [NSString stringWithFormat:@"https://%@/report/2/%@", splunkEndpoint, _splunkTopicName];
436 _splunkUploadURL = [NSURL URLWithString:urlString];
439 (void)splunkEndpoint;
445 - (BOOL)isSampledUpload
447 uint32_t sample = arc4random();
448 if ((double)_devicePercentage < ((double)1 / UINT32_MAX) * 100) {
449 /* Requested percentage is smaller than we can sample. just do 1 out of UINT32_MAX */
454 if ((double)sample <= (double)UINT32_MAX * ((double)_devicePercentage / 100)) {
461 - (BOOL)postJSON:(NSData*)json toEndpoint:(NSURL*)endpoint error:(NSError**)error
465 NSString *description = [NSString stringWithFormat:@"No endpoint for %@", _internalTopicName];
466 *error = [NSError errorWithDomain:@"SupdUploadErrorDomain"
468 userInfo:@{NSLocalizedDescriptionKey : description}];
473 * Create the NSURLSession
474 * We use the ephemeral session config because we don't need cookies or cache
476 NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
478 configuration.HTTPAdditionalHeaders = @{ @"User-Agent" : [NSString stringWithFormat:@"securityd/%s", SECURITY_BUILD_VERSION]};
480 NSURLSession* postSession = [NSURLSession sessionWithConfiguration:configuration
484 NSMutableURLRequest* postRequest = [[NSMutableURLRequest alloc] init];
485 postRequest.URL = endpoint;
486 postRequest.HTTPMethod = @"POST";
487 postRequest.HTTPBody = [json supd_gzipDeflate];
488 [postRequest setValue:@"gzip" forHTTPHeaderField:@"Content-Encoding"];
491 * Create the upload task.
493 dispatch_semaphore_t sem = dispatch_semaphore_create(0);
494 __block BOOL uploadSuccess = NO;
495 NSURLSessionDataTask* uploadTask = [postSession dataTaskWithRequest:postRequest
496 completionHandler:^(NSData * _Nullable __unused data, NSURLResponse * _Nullable response, NSError * _Nullable requestError) {
498 secerror("Error in uploading the events to splunk for %@: %@", self->_internalTopicName, requestError);
499 } else if (![response isKindOfClass:NSHTTPURLResponse.class]){
500 Class class = response.class;
501 secerror("Received the wrong kind of response for %@: %@", self->_internalTopicName, NSStringFromClass(class));
503 NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
504 if(httpResponse.statusCode >= 200 && httpResponse.statusCode < 300) {
507 secnotice("upload", "Splunk upload success for %@", self->_internalTopicName);
509 secnotice("upload", "Splunk upload for %@ unexpected status to URL: %@ -- status: %d",
510 self->_internalTopicName, endpoint, (int)(httpResponse.statusCode));
513 dispatch_semaphore_signal(sem);
515 secnotice("upload", "Splunk upload start for %@", self->_internalTopicName);
517 dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, (uint64_t)(5 * 60 * NSEC_PER_SEC)));
518 return uploadSuccess;
521 - (BOOL)eventIsBlacklisted:(NSMutableDictionary*)event {
522 return _blacklistedEvents ? [_blacklistedEvents containsObject:event[SFAnalyticsEventType]] : NO;
525 - (void)removeBlacklistedFieldsFromEvent:(NSMutableDictionary*)event {
526 for (NSString* badField in self->_blacklistedFields) {
527 [event removeObjectForKey:badField];
531 - (void)addRequiredFieldsToEvent:(NSMutableDictionary*)event {
532 [_metricsBase enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
539 - (BOOL)prepareEventForUpload:(NSMutableDictionary*)event {
540 if ([self eventIsBlacklisted:event]) {
544 [self removeBlacklistedFieldsFromEvent:event];
545 [self addRequiredFieldsToEvent:event];
546 if (_disableClientId) {
547 event[SFAnalyticsClientId] = @(0);
549 event[SFAnalyticsSplunkTopic] = self->_splunkTopicName ?: [NSNull null];
553 - (void)addFailures:(NSMutableArray<NSArray*>*)failures toUploadRecords:(NSMutableArray*)records threshold:(NSUInteger)threshold
555 // The first 0 through 'threshold' items are getting uploaded in any case (which might be 0 for lower priority data)
557 for (NSArray* client in failures) {
558 [client enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
559 NSMutableDictionary* event = (NSMutableDictionary*)obj;
560 if (idx >= threshold) {
564 if ([self prepareEventForUpload:event]) {
565 if ([NSJSONSerialization isValidJSONObject:event]) {
566 [records addObject:event];
568 secerror("supd: Replacing event with errorEvent because invalid JSON: %@", event);
569 NSString* originalType = event[SFAnalyticsEventType];
570 NSDictionary* errorEvent = @{ SFAnalyticsEventType : SFAnalyticsEventTypeErrorEvent,
571 SFAnalyticsEventErrorDestription : [NSString stringWithFormat:@"JSON:%@", originalType]};
572 [records addObject:errorEvent];
578 // Are there more items than we shoved into the upload records?
579 NSInteger excessItems = 0;
580 for (NSArray* client in failures) {
581 NSInteger localExcess = client.count - threshold;
582 excessItems += localExcess > 0 ? localExcess : 0;
585 // Then, if we have space and items left, apply a scaling factor to distribute events across clients to fill upload buffer
586 if (records.count < _maxEventsToReport && excessItems > 0) {
587 double scale = (_maxEventsToReport - records.count) / (double)excessItems;
592 for (NSArray* client in failures) {
593 if (client.count > threshold) {
594 NSRange range = NSMakeRange(threshold, (client.count - threshold) * scale);
595 NSArray* sub = [client subarrayWithRange:range];
596 [sub enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
597 if ([self prepareEventForUpload:obj]) {
598 [records addObject:obj];
606 - (NSMutableDictionary*)sampleStatisticsForSamples:(NSArray*)samples withName:(NSString*)name
608 NSMutableDictionary* statistics = [NSMutableDictionary dictionary];
609 NSUInteger count = samples.count;
610 NSArray* sortedSamples = [samples sortedArrayUsingSelector:@selector(compare:)];
611 NSArray* samplesAsExpressionArray = @[[NSExpression expressionForConstantValue:sortedSamples]];
614 statistics[name] = samples[0];
616 // NSExpression takes population standard deviation. Our data is a sample of whatever we sampled over time,
617 // but the difference between the two is fairly minor (divide by N before taking sqrt versus divide by N-1).
618 statistics[[NSString stringWithFormat:@"%@-dev", name]] = [[NSExpression expressionForFunction:@"stddev:" arguments:samplesAsExpressionArray] expressionValueWithObject:nil context:nil];
620 statistics[[NSString stringWithFormat:@"%@-min", name]] = [[NSExpression expressionForFunction:@"min:" arguments:samplesAsExpressionArray] expressionValueWithObject:nil context:nil];
621 statistics[[NSString stringWithFormat:@"%@-max", name]] = [[NSExpression expressionForFunction:@"max:" arguments:samplesAsExpressionArray] expressionValueWithObject:nil context:nil];
622 statistics[[NSString stringWithFormat:@"%@-avg", name]] = [[NSExpression expressionForFunction:@"average:" arguments:samplesAsExpressionArray] expressionValueWithObject:nil context:nil];
623 statistics[[NSString stringWithFormat:@"%@-med", name]] = [[NSExpression expressionForFunction:@"median:" arguments:samplesAsExpressionArray] expressionValueWithObject:nil context:nil];
627 NSString* q1 = [NSString stringWithFormat:@"%@-1q", name];
628 NSString* q3 = [NSString stringWithFormat:@"%@-3q", name];
629 // From Wikipedia, which is never wrong
630 if (count % 2 == 0) {
631 // The lower quartile value is the median of the lower half of the data. The upper quartile value is the median of the upper half of the data.
632 statistics[q1] = [[NSExpression expressionForFunction:@"median:" arguments:@[[NSExpression expressionForConstantValue:[sortedSamples subarrayWithRange:NSMakeRange(0, count / 2)]]]] expressionValueWithObject:nil context:nil];
633 statistics[q3] = [[NSExpression expressionForFunction:@"median:" arguments:@[[NSExpression expressionForConstantValue:[sortedSamples subarrayWithRange:NSMakeRange((count / 2), count / 2)]]]] expressionValueWithObject:nil context:nil];
634 } else if (count % 4 == 1) {
635 // If there are (4n+1) data points, then the lower quartile is 25% of the nth data value plus 75% of the (n+1)th data value;
636 // the upper quartile is 75% of the (3n+1)th data point plus 25% of the (3n+2)th data point.
637 // (offset n by -1 since we count from 0)
638 NSUInteger n = count / 4;
639 statistics[q1] = @(([sortedSamples[n - 1] doubleValue] + [sortedSamples[n] doubleValue] * 3.0) / 4.0);
640 statistics[q3] = @(([sortedSamples[(3 * n)] doubleValue] * 3.0 + [sortedSamples[(3 * n) + 1] doubleValue]) / 4.0);
641 } else if (count % 4 == 3){
642 // If there are (4n+3) data points, then the lower quartile is 75% of the (n+1)th data value plus 25% of the (n+2)th data value;
643 // the upper quartile is 25% of the (3n+2)th data point plus 75% of the (3n+3)th data point.
644 // (offset n by -1 since we count from 0)
645 NSUInteger n = count / 4;
646 statistics[q1] = @(([sortedSamples[n] doubleValue] * 3.0 + [sortedSamples[n + 1] doubleValue]) / 4.0);
647 statistics[q3] = @(([sortedSamples[(3 * n) + 1] doubleValue] + [sortedSamples[(3 * n) + 2] doubleValue] * 3.0) / 4.0);
654 - (NSMutableDictionary*)healthSummaryWithName:(NSString*)name store:(SFAnalyticsSQLiteStore*)store
656 __block NSMutableDictionary* summary = [NSMutableDictionary new];
658 // Add some events of our own before pulling in data
659 summary[SFAnalyticsEventType] = [NSString stringWithFormat:@"%@HealthSummary", name];
660 if ([self eventIsBlacklisted:summary]) {
663 summary[SFAnalyticsEventTime] = @([[NSDate date] timeIntervalSince1970] * 1000); // Splunk wants milliseconds
664 [SFAnalytics addOSVersionToEvent:summary];
665 if (store.uploadDate) {
666 summary[SFAnalyticsAttributeLastUploadTime] = @([store.uploadDate timeIntervalSince1970] * 1000);
668 summary[SFAnalyticsAttributeLastUploadTime] = @(0);
672 NSDictionary* successCounts = store.summaryCounts;
673 __block NSInteger totalSuccessCount = 0;
674 __block NSInteger totalHardFailureCount = 0;
675 __block NSInteger totalSoftFailureCount = 0;
676 [successCounts enumerateKeysAndObjectsUsingBlock:^(NSString* _Nonnull eventType, NSDictionary* _Nonnull counts, BOOL* _Nonnull stop) {
677 summary[[NSString stringWithFormat:@"%@-success", eventType]] = counts[SFAnalyticsColumnSuccessCount];
678 summary[[NSString stringWithFormat:@"%@-hardfail", eventType]] = counts[SFAnalyticsColumnHardFailureCount];
679 summary[[NSString stringWithFormat:@"%@-softfail", eventType]] = counts[SFAnalyticsColumnSoftFailureCount];
680 totalSuccessCount += [counts[SFAnalyticsColumnSuccessCount] integerValue];
681 totalHardFailureCount += [counts[SFAnalyticsColumnHardFailureCount] integerValue];
682 totalSoftFailureCount += [counts[SFAnalyticsColumnSoftFailureCount] integerValue];
685 summary[SFAnalyticsColumnSuccessCount] = @(totalSuccessCount);
686 summary[SFAnalyticsColumnHardFailureCount] = @(totalHardFailureCount);
687 summary[SFAnalyticsColumnSoftFailureCount] = @(totalSoftFailureCount);
688 if (os_variant_has_internal_diagnostics("com.apple.security")) {
689 summary[SFAnalyticsInternal] = @YES;
693 NSMutableDictionary<NSString*,NSMutableArray*>* samplesBySampler = [NSMutableDictionary<NSString*,NSMutableArray*> dictionary];
694 for (NSDictionary* sample in [store samples]) {
695 if (!samplesBySampler[sample[SFAnalyticsColumnSampleName]]) {
696 samplesBySampler[sample[SFAnalyticsColumnSampleName]] = [NSMutableArray array];
698 [samplesBySampler[sample[SFAnalyticsColumnSampleName]] addObject:sample[SFAnalyticsColumnSampleValue]];
700 [samplesBySampler enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSMutableArray * _Nonnull obj, BOOL * _Nonnull stop) {
701 NSMutableDictionary* event = [self sampleStatisticsForSamples:obj withName:key];
702 [summary addEntriesFromDictionary:event];
705 // Should always return yes because we already checked for event blacklisting specifically (unless summary itself is blacklisted)
706 if (![self prepareEventForUpload:summary]) {
707 secwarning("supd: health summary for %@ blacklisted", name);
711 // Seems unlikely because we only insert strings, samplers only take NSNumbers and frankly, sampleStatisticsForSamples probably would have crashed
712 if (![NSJSONSerialization isValidJSONObject:summary]) {
713 secerror("json: health summary for client %@ is invalid JSON: %@", name, summary);
714 return [@{ SFAnalyticsEventType : SFAnalyticsEventTypeErrorEvent,
715 SFAnalyticsEventErrorDestription : [NSString stringWithFormat:@"JSON:%@HealthSummary", name]} mutableCopy];
721 - (void)updateUploadDateForClients:(NSArray<SFAnalyticsClient*>*)clients date:(NSDate *)date clearData:(BOOL)clearData
723 for (SFAnalyticsClient* client in clients) {
724 SFAnalyticsSQLiteStore* store = [SFAnalyticsSQLiteStore storeWithPath:client.storePath schema:SFAnalyticsTableSchema];
725 secnotice("postprocess", "Setting upload date (%@) for client: %@", date, client.name);
726 store.uploadDate = date;
728 secnotice("postprocess", "Clearing collected data for client: %@", client.name);
729 [store clearAllData];
734 - (NSData*)getLoggingJSON:(bool)pretty
735 forUpload:(BOOL)upload
736 participatingClients:(NSMutableArray<SFAnalyticsClient*>**)clients
737 force:(BOOL)force // supdctl uploads ignore privacy settings and recency
738 error:(NSError**)error
740 NSMutableArray<SFAnalyticsClient*>* localClients = [NSMutableArray new];
741 __block NSMutableArray* uploadRecords = [NSMutableArray arrayWithCapacity:_maxEventsToReport];
742 __block NSError *localError;
743 __block NSMutableArray<NSArray*>* hardFailures = [NSMutableArray new];
744 __block NSMutableArray<NSArray*>* softFailures = [NSMutableArray new];
745 NSString* ckdeviceID = nil;
746 NSString* accountID = nil;
748 if (os_variant_has_internal_diagnostics("com.apple.security") && [_internalTopicName isEqualToString:SFAnalyticsTopicKeySync]) {
749 ckdeviceID = [self askSecurityForCKDeviceID];
750 accountID = accountAltDSID();
752 for (SFAnalyticsClient* client in self->_topicClients) {
753 if (!force && [client requireDeviceAnalytics] && !_isDeviceAnalyticsEnabled()) {
754 // Client required device analytics, yet the user did not opt in.
755 secnotice("getLoggingJSON", "Client '%@' requires device analytics yet user did not opt in.", [client name]);
758 if (!force && [client requireiCloudAnalytics] && !_isiCloudAnalyticsEnabled()) {
759 // Client required iCloud analytics, yet the user did not opt in.
760 secnotice("getLoggingJSON", "Client '%@' requires iCloud analytics yet user did not opt in.", [client name]);
764 SFAnalyticsSQLiteStore* store = [SFAnalyticsSQLiteStore storeWithPath:client.storePath schema:SFAnalyticsTableSchema];
767 NSDate* uploadDate = store.uploadDate;
768 if (!force && uploadDate && [[NSDate date] timeIntervalSinceDate:uploadDate] < _secondsBetweenUploads) {
769 secnotice("json", "ignoring client '%@' for %@ because last upload too recent: %@",
770 client.name, _internalTopicName, uploadDate);
775 secnotice("json", "client '%@' for topic '%@' force-included", client.name, _internalTopicName);
777 secnotice("json", "including client '%@' for topic '%@' for upload", client.name, _internalTopicName);
779 [localClients addObject:client];
782 NSMutableDictionary* healthSummary = [self healthSummaryWithName:client.name store:store];
785 healthSummary[SFAnalyticsDeviceID] = ckdeviceID;
788 healthSummary[SFAnalyticsAltDSID] = accountID;
790 [uploadRecords addObject:healthSummary];
793 [hardFailures addObject:store.hardFailures];
794 [softFailures addObject:store.softFailures];
797 if (upload && [localClients count] == 0) {
799 NSString *description = [NSString stringWithFormat:@"Upload too recent for all clients for %@", _internalTopicName];
800 *error = [NSError errorWithDomain:@"SupdUploadErrorDomain"
802 userInfo:@{NSLocalizedDescriptionKey : description}];
808 *clients = localClients;
811 [self addFailures:hardFailures toUploadRecords:uploadRecords threshold:_maxEventsToReport/10];
812 [self addFailures:softFailures toUploadRecords:uploadRecords threshold:0];
814 NSDictionary* jsonDict = @{
815 SFAnalyticsSplunkPostTime : @([[NSDate date] timeIntervalSince1970] * 1000),
816 @"events" : uploadRecords
819 // This check is "belt and suspenders" because we already checked each event separately
820 if (![NSJSONSerialization isValidJSONObject:jsonDict]) {
821 secemergency("json: final dictionary invalid JSON. This is terrible!");
823 *error = [NSError errorWithDomain:SupdErrorDomain code:SupdInvalidJSONError
824 userInfo:@{NSLocalizedDescriptionKey : [NSString localizedStringWithFormat:@"Final dictionary for upload is invalid JSON: %@", jsonDict]}];
829 NSData *json = [NSJSONSerialization dataWithJSONObject:jsonDict
830 options:(pretty ? NSJSONWritingPrettyPrinted : 0)
840 // Is at least one client eligible for data collection based on user consent? Otherwise callers should NOT reach off-device.
841 - (BOOL)haveEligibleClients {
842 for (SFAnalyticsClient* client in self.topicClients) {
843 if ((!client.requireDeviceAnalytics || _isDeviceAnalyticsEnabled()) &&
844 (!client.requireiCloudAnalytics || _isiCloudAnalyticsEnabled())) {
851 - (NSString*)askSecurityForCKDeviceID
853 NSError* error = nil;
854 CKKSControl* rpc = [CKKSControl controlObject:&error];
856 secerror("unable to obtain CKKS endpoint: %@", error);
860 __block NSString* localCKDeviceID;
861 dispatch_semaphore_t sema = dispatch_semaphore_create(0);
862 [rpc rpcGetCKDeviceIDWithReply:^(NSString* ckdeviceID) {
863 localCKDeviceID = ckdeviceID;
864 dispatch_semaphore_signal(sema);
867 if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 10)) != 0) {
868 secerror("timed out waiting for a response from security");
872 return localCKDeviceID;
875 // this method is kind of evil for the fact that it has side-effects in pulling other things besides the metricsURL from the server, and as such should NOT be memoized.
876 // TODO redo this, probably to return a dictionary.
877 - (NSURL*)splunkUploadURL:(BOOL)force
879 if (!force && ![self haveEligibleClients]) { // force is true IFF called from supdctl. Customers don't have it and internal audiences must call it explicitly.
880 secnotice("getURL", "Not going to talk to server for topic %@ because no eligible clients", [self internalTopicName]);
884 if (__splunkUploadURL) {
885 return __splunkUploadURL;
888 secnotice("getURL", "Asking server for endpoint and config data for topic %@", [self internalTopicName]);
890 __weak __typeof(self) weakSelf = self;
891 dispatch_semaphore_t sem = dispatch_semaphore_create(0);
893 __block NSError* error = nil;
894 NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
895 NSURLSession* storeBagSession = [NSURLSession sessionWithConfiguration:configuration
899 NSURL* requestEndpoint = _splunkBagURL;
900 __block NSURL* result = nil;
901 NSURLSessionDataTask* storeBagTask = [storeBagSession dataTaskWithURL:requestEndpoint completionHandler:^(NSData * _Nullable data,
902 NSURLResponse * _Nullable __unused response,
903 NSError * _Nullable responseError) {
905 __strong __typeof(self) strongSelf = weakSelf;
910 if (data && !responseError) {
911 NSData *responseData = data; // shut up compiler
912 NSDictionary* responseDict = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error];
913 if([responseDict isKindOfClass:NSDictionary.class] && !error) {
914 if (!self->_ignoreServersMessagesTellingUsToGoAway) {
915 self->_disableUploads = [[responseDict valueForKey:@"sendDisabled"] boolValue];
916 if (self->_disableUploads) {
917 // then don't upload anything right now
918 secerror("not returning a splunk URL because uploads are disabled for %@", self->_internalTopicName);
919 dispatch_semaphore_signal(sem);
923 // backend works with milliseconds
924 NSUInteger secondsBetweenUploads = [[responseDict valueForKey:@"postFrequency"] unsignedIntegerValue] / 1000;
925 if (secondsBetweenUploads > 0) {
926 if (os_variant_has_internal_diagnostics("com.apple.security") &&
927 self->_secondsBetweenUploads < secondsBetweenUploads) {
928 secnotice("getURL", "Overriding server-sent post frequency because device is internal (%lu -> %lu)", (unsigned long)secondsBetweenUploads, (unsigned long)self->_secondsBetweenUploads);
930 strongSelf->_secondsBetweenUploads = secondsBetweenUploads;
934 strongSelf->_blacklistedEvents = responseDict[@"blacklistedEvents"];
935 strongSelf->_blacklistedFields = responseDict[@"blacklistedFields"];
938 strongSelf->_metricsBase = responseDict[@"metricsBase"];
940 NSString* metricsEndpoint = responseDict[@"metricsUrl"];
941 if([metricsEndpoint isKindOfClass:NSString.class]) {
943 NSString* endpoint = [metricsEndpoint stringByAppendingFormat:@"/2/%@", strongSelf->_splunkTopicName];
944 secnotice("upload", "got metrics endpoint %@ for %@", endpoint, self->_internalTopicName);
945 NSURL* endpointURL = [NSURL URLWithString:endpoint];
946 if([endpointURL.scheme isEqualToString:@"https"]) {
947 result = endpointURL;
953 error = responseError;
956 secnotice("upload", "Unable to fetch splunk endpoint at URL for %@: %@ -- error: %@",
957 self->_internalTopicName, requestEndpoint, error.description);
960 secnotice("upload", "Malformed iTunes config payload for %@!", self->_internalTopicName);
963 dispatch_semaphore_signal(sem);
966 [storeBagTask resume];
967 dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, (uint64_t)(60 * NSEC_PER_SEC)));
972 - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
973 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
974 assert(completionHandler);
976 secnotice("upload", "Splunk upload challenge for %@", _internalTopicName);
977 NSURLCredential *cred = nil;
979 if ([challenge previousFailureCount] > 0) {
980 // Previous failures occurred, bail
981 completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
983 } else if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
985 * Evaluate trust for the certificate
988 SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
989 // Coverity gets upset if we don't check status even though result is all we need.
990 bool trustResult = SecTrustEvaluateWithError(serverTrust, NULL);
991 if (_allowInsecureSplunkCert || trustResult) {
993 * All is well, accept the credentials
995 if(_allowInsecureSplunkCert) {
996 secnotice("upload", "Force Accepting Splunk Credential for %@", _internalTopicName);
998 cred = [NSURLCredential credentialForTrust:serverTrust];
999 completionHandler(NSURLSessionAuthChallengeUseCredential, cred);
1003 * An error occurred in evaluating trust, bail
1005 completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
1009 * Just perform the default handling
1011 completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
1015 - (NSDictionary*)eventDictWithBlacklistedFieldsStrippedFrom:(NSDictionary*)eventDict
1017 NSMutableDictionary* strippedDict = eventDict.mutableCopy;
1018 for (NSString* blacklistedField in _blacklistedFields) {
1019 [strippedDict removeObjectForKey:blacklistedField];
1021 return strippedDict;
1024 // MARK: Database path retrieval
1026 + (NSString*)databasePathForCKKS
1028 return [(__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)@"Analytics/ckks_analytics.db") path];
1031 + (NSString*)databasePathForSOS
1033 return [(__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)@"Analytics/sos_analytics.db") path];
1036 + (NSString*)AppSupportPath
1038 #if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
1039 return @"/var/mobile/Library/Application Support";
1041 NSArray<NSString *>*paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, true);
1042 if ([paths count] < 1) {
1045 return [NSString stringWithString: paths[0]];
1046 #endif /* TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR */
1049 + (NSString*)databasePathForPCS
1051 NSString *appSup = [self AppSupportPath];
1055 NSString *dbpath = [NSString stringWithFormat:@"%@/com.apple.ProtectedCloudStorage/PCSAnalytics.db", appSup];
1056 secnotice("supd", "PCS Database path (%@)", dbpath);
1060 + (NSString*)databasePathForLocal
1062 return [(__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)@"Analytics/localkeychain.db") path];
1065 + (NSString*)databasePathForTrustdHealth
1067 #if TARGET_OS_IPHONE
1068 return [(__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory(CFSTR("Analytics/trustd_health_analytics.db")) path];
1070 return [(__bridge_transfer NSURL*)SecCopyURLForFileInUserCacheDirectory(CFSTR("Analytics/trustd_health_analytics.db")) path];
1074 + (NSString*)databasePathForTrust
1076 #if TARGET_OS_IPHONE
1077 return [(__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory(CFSTR("Analytics/trust_analytics.db")) path];
1079 return [(__bridge_transfer NSURL*)SecCopyURLForFileInUserCacheDirectory(CFSTR("Analytics/trust_analytics.db")) path];
1083 + (NSString*)databasePathForTLS
1085 #if TARGET_OS_IPHONE
1086 return [(__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory(CFSTR("Analytics/TLS_analytics.db")) path];
1088 return [(__bridge_transfer NSURL*)SecCopyURLForFileInUserCacheDirectory(CFSTR("Analytics/TLS_analytics.db")) path];
1092 + (NSString*)databasePathForSignIn
1094 return [(__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory(CFSTR("Analytics/signin_metrics.db")) path];
1097 + (NSString*)databasePathForCloudServices
1099 return [(__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory(CFSTR("Analytics/CloudServicesAnalytics.db")) path];
1102 + (NSString*)databasePathForTransparency
1104 return [(__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)@"Analytics/TransparencyAnalytics.db") path];
1110 @property NSDictionary *topicsSamplingRates;
1113 @implementation supd
1116 NSDictionary* systemDefaultValues = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle bundleWithPath:@"/System/Library/Frameworks/Security.framework"] pathForResource:@"SFAnalytics" ofType:@"plist"]];
1117 NSMutableArray <SFAnalyticsTopic*>* topics = [NSMutableArray array];
1118 for (NSString *topicKey in systemDefaultValues) {
1119 NSDictionary *topicSamplingRates = _topicsSamplingRates[topicKey];
1120 SFAnalyticsTopic *topic = [[SFAnalyticsTopic alloc] initWithDictionary:systemDefaultValues[topicKey] name:topicKey samplingRates:topicSamplingRates];
1121 [topics addObject:topic];
1123 _analyticsTopics = [NSArray arrayWithArray:topics];
1126 + (void)instantiate {
1130 + (instancetype)instance {
1131 if (!_supdInstance) {
1132 _supdInstance = [self new];
1134 return _supdInstance;
1137 // Use this for testing to get rid of any state
1138 + (void)removeInstance {
1139 _supdInstance = nil;
1143 static NSString *SystemTrustStorePath = @"/System/Library/Security/Certificates.bundle";
1144 static NSString *AnalyticsSamplingRatesFilename = @"AnalyticsSamplingRates";
1145 static NSString *ContentVersionKey = @"MobileAssetContentVersion";
1146 static NSString *AssetContextFilename = @"OTAPKIContext.plist";
1148 static NSNumber *getSystemVersion(NSBundle *trustStoreBundle) {
1149 NSDictionary *systemVersionPlist = [NSDictionary dictionaryWithContentsOfURL:[trustStoreBundle URLForResource:@"AssetVersion"
1150 withExtension:@"plist"]];
1151 if (!systemVersionPlist || ![systemVersionPlist isKindOfClass:[NSDictionary class]]) {
1154 NSNumber *systemVersion = systemVersionPlist[ContentVersionKey];
1155 if (systemVersion == nil || ![systemVersion isKindOfClass:[NSNumber class]]) {
1158 return systemVersion;
1161 static NSNumber *getAssetVersion(NSURL *directory) {
1162 NSDictionary *assetContextPlist = [NSDictionary dictionaryWithContentsOfURL:[directory URLByAppendingPathComponent:AssetContextFilename]];
1163 if (!assetContextPlist || ![assetContextPlist isKindOfClass:[NSDictionary class]]) {
1166 NSNumber *assetVersion = assetContextPlist[ContentVersionKey];
1167 if (assetVersion == nil || ![assetVersion isKindOfClass:[NSNumber class]]) {
1170 return assetVersion;
1173 static bool ShouldInitializeWithAsset(NSBundle *trustStoreBundle, NSURL *directory) {
1174 NSNumber *systemVersion = getSystemVersion(trustStoreBundle);
1175 NSNumber *assetVersion = getAssetVersion(directory);
1177 if (assetVersion == nil || systemVersion == nil) {
1180 if ([assetVersion compare:systemVersion] == NSOrderedDescending) {
1186 - (void)setupSamplingRates {
1187 NSBundle *trustStoreBundle = [NSBundle bundleWithPath:SystemTrustStorePath];
1189 NSURL *keychainsDirectory = CFBridgingRelease(SecCopyURLForFileInSystemKeychainDirectory(nil));
1190 NSURL *directory = [keychainsDirectory URLByAppendingPathComponent:@"SupplementalsAssets/" isDirectory:YES];
1192 NSDictionary *analyticsSamplingRates = nil;
1193 if (ShouldInitializeWithAsset(trustStoreBundle, directory)) {
1194 /* Try to get the asset version of the sampling rates */
1195 NSURL *analyticsSamplingRateURL = [directory URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.plist", AnalyticsSamplingRatesFilename]];
1196 analyticsSamplingRates = [NSDictionary dictionaryWithContentsOfURL:analyticsSamplingRateURL];
1197 secnotice("supd", "read sampling rates from SupplementalsAssets dir");
1198 if (!analyticsSamplingRates || ![analyticsSamplingRates isKindOfClass:[NSDictionary class]]) {
1199 analyticsSamplingRates = nil;
1202 if (!analyticsSamplingRates) {
1203 analyticsSamplingRates = [NSDictionary dictionaryWithContentsOfURL: [trustStoreBundle URLForResource:AnalyticsSamplingRatesFilename
1204 withExtension:@"plist"]];
1206 if (analyticsSamplingRates && [analyticsSamplingRates isKindOfClass:[NSDictionary class]]) {
1207 _topicsSamplingRates = analyticsSamplingRates[@"Topics"];
1208 if (!_topicsSamplingRates || ![analyticsSamplingRates isKindOfClass:[NSDictionary class]]) {
1209 _topicsSamplingRates = nil; // Something has gone terribly wrong, so we'll use the hardcoded defaults in this case
1214 - (instancetype)initWithReporter:(SFAnalyticsReporter *)reporter
1216 if (self = [super init]) {
1217 [self setupSamplingRates];
1219 _reporter = reporter;
1221 xpc_activity_register("com.apple.securityuploadd.triggerupload", XPC_ACTIVITY_CHECK_IN, ^(xpc_activity_t activity) {
1222 xpc_activity_state_t activityState = xpc_activity_get_state(activity);
1223 secnotice("supd", "hit xpc activity trigger, state: %ld", activityState);
1224 if (activityState == XPC_ACTIVITY_STATE_RUN) {
1225 // Run our regularly scheduled scan
1226 [self performRegularlyScheduledUpload];
1234 - (instancetype)init {
1235 SFAnalyticsReporter *reporter = [[SFAnalyticsReporter alloc] init];
1236 return [self initWithReporter:reporter];
1239 - (void)sendNotificationForOncePerReportSamplers
1241 notify_post(SFAnalyticsFireSamplersNotification);
1242 [NSThread sleepForTimeInterval:3.0];
1245 - (void)performRegularlyScheduledUpload {
1246 secnotice("upload", "Starting uploads in response to regular trigger");
1247 NSError *error = nil;
1248 if ([self uploadAnalyticsWithError:&error force:NO]) {
1249 secnotice("upload", "Regularly scheduled upload successful");
1251 secerror("upload: Failed to complete regularly scheduled upload: %@", error);
1255 - (BOOL)uploadAnalyticsWithError:(NSError**)error force:(BOOL)force {
1256 [self sendNotificationForOncePerReportSamplers];
1259 NSError* localError = nil;
1260 for (SFAnalyticsTopic *topic in _analyticsTopics) {
1261 @autoreleasepool { // The logging JSONs get quite large. Ensure they're deallocated between topics.
1262 __block NSURL* endpoint = [topic splunkUploadURL:force]; // has side effects!
1265 secnotice("upload", "Skipping upload for %@ because no endpoint", [topic internalTopicName]);
1269 if ([topic disableUploads]) {
1270 secnotice("upload", "Aborting upload task for %@ because uploads are disabled", [topic internalTopicName]);
1274 NSMutableArray<SFAnalyticsClient*>* clients = [NSMutableArray new];
1275 NSData* json = [topic getLoggingJSON:false forUpload:YES participatingClients:&clients force:force error:&localError];
1277 if ([topic isSampledUpload]) {
1278 if (![self->_reporter saveReport:json fileName:[topic internalTopicName]]) {
1279 secerror("upload: failed to write analytics data to log");
1281 if ([topic postJSON:json toEndpoint:endpoint error:&localError]) {
1282 secnotice("upload", "Successfully posted JSON for %@", [topic internalTopicName]);
1284 [topic updateUploadDateForClients:clients date:[NSDate date] clearData:YES];
1286 secerror("upload: Failed to post JSON for %@: %@", [topic internalTopicName], localError);
1289 /* If we didn't sample this report, update date to prevent trying to upload again sooner
1290 * than we should. Clear data so that per-day calculations remain consistent. */
1291 secnotice("upload", "skipping unsampled upload for %@ and clearing data", [topic internalTopicName]);
1292 [topic updateUploadDateForClients:clients date:[NSDate date] clearData:YES];
1295 if ([[localError domain] isEqualToString:SupdErrorDomain] && [localError code] == SupdInvalidJSONError) {
1296 // Pretend this was a success because at least we'll get rid of bad data.
1297 // If someone keeps logging bad data and we only catch it here then
1298 // this causes sustained data loss for the entire topic.
1299 [topic updateUploadDateForClients:clients date:[NSDate date] clearData:YES];
1301 secerror("upload: failed to get logging JSON for topic %@: %@", [topic internalTopicName], localError);
1304 if (error && localError) {
1305 *error = localError;
1311 - (NSString*)sysdiagnoseStringForEventRecord:(NSDictionary*)eventRecord
1313 NSMutableDictionary* mutableEventRecord = eventRecord.mutableCopy;
1314 [mutableEventRecord removeObjectForKey:SFAnalyticsSplunkTopic];
1316 NSDate* eventDate = [NSDate dateWithTimeIntervalSince1970:[[eventRecord valueForKey:SFAnalyticsEventTime] doubleValue] / 1000];
1317 [mutableEventRecord removeObjectForKey:SFAnalyticsEventTime];
1319 NSString* eventName = eventRecord[SFAnalyticsEventType];
1320 [mutableEventRecord removeObjectForKey:SFAnalyticsEventType];
1322 SFAnalyticsEventClass eventClass = [[eventRecord valueForKey:SFAnalyticsEventClassKey] integerValue];
1323 NSString* eventClassString = [self stringForEventClass:eventClass];
1324 [mutableEventRecord removeObjectForKey:SFAnalyticsEventClassKey];
1326 NSMutableString* additionalAttributesString = [NSMutableString string];
1327 if (mutableEventRecord.count > 0) {
1328 [additionalAttributesString appendString:@" - Attributes: {" ];
1329 __block BOOL firstAttribute = YES;
1330 [mutableEventRecord enumerateKeysAndObjectsUsingBlock:^(NSString* key, id object, BOOL* stop) {
1331 NSString* openingString = firstAttribute ? @"" : @", ";
1332 [additionalAttributesString appendString:[NSString stringWithFormat:@"%@%@ : %@", openingString, key, object]];
1333 firstAttribute = NO;
1335 [additionalAttributesString appendString:@" }"];
1338 return [NSString stringWithFormat:@"%@ %@: %@%@", eventDate, eventClassString, eventName, additionalAttributesString];
1341 - (NSString*)getSysdiagnoseDump
1343 NSMutableString* sysdiagnose = [[NSMutableString alloc] init];
1345 for (SFAnalyticsTopic* topic in _analyticsTopics) {
1346 for (SFAnalyticsClient* client in topic.topicClients) {
1347 [sysdiagnose appendString:[NSString stringWithFormat:@"Client: %@\n", client.name]];
1348 SFAnalyticsSQLiteStore* store = [SFAnalyticsSQLiteStore storeWithPath:client.storePath schema:SFAnalyticsTableSchema];
1349 NSArray* allEvents = store.allEvents;
1350 for (NSDictionary* eventRecord in allEvents) {
1351 [sysdiagnose appendFormat:@"%@\n", [self sysdiagnoseStringForEventRecord:eventRecord]];
1353 if (allEvents.count == 0) {
1354 [sysdiagnose appendString:@"No data to report for this client\n"];
1361 - (void)setUploadDateWith:(NSDate *)date reply:(void (^)(BOOL, NSError*))reply
1363 for (SFAnalyticsTopic* topic in _analyticsTopics) {
1364 [topic updateUploadDateForClients:topic.topicClients date:date clearData:NO];
1369 - (void)clientStatus:(void (^)(NSDictionary<NSString *, id> *, NSError *))reply
1371 NSMutableDictionary *info = [NSMutableDictionary dictionary];
1372 for (SFAnalyticsTopic* topic in _analyticsTopics) {
1373 for (SFAnalyticsClient *client in topic.topicClients) {
1374 SFAnalyticsSQLiteStore* store = [SFAnalyticsSQLiteStore storeWithPath:client.storePath schema:SFAnalyticsTableSchema];
1376 NSMutableDictionary *clientInfo = [NSMutableDictionary dictionary];
1377 clientInfo[@"uploadDate"] = store.uploadDate;
1378 info[client.name] = clientInfo;
1387 - (NSString*)stringForEventClass:(SFAnalyticsEventClass)eventClass
1389 if (eventClass == SFAnalyticsEventClassNote) {
1390 return @"EventNote";
1392 else if (eventClass == SFAnalyticsEventClassSuccess) {
1393 return @"EventSuccess";
1395 else if (eventClass == SFAnalyticsEventClassHardFailure) {
1396 return @"EventHardFailure";
1398 else if (eventClass == SFAnalyticsEventClassSoftFailure) {
1399 return @"EventSoftFailure";
1402 return @"EventUnknown";
1406 // MARK: XPC Procotol Handlers
1408 - (void)getSysdiagnoseDumpWithReply:(void (^)(NSString*))reply {
1409 reply([self getSysdiagnoseDump]);
1412 - (void)getLoggingJSON:(bool)pretty topic:(NSString *)topicName reply:(void (^)(NSData*, NSError*))reply {
1413 secnotice("rpcGetLoggingJSON", "Building a JSON blob resembling the one we would have uploaded");
1414 NSError* error = nil;
1415 [self sendNotificationForOncePerReportSamplers];
1417 for (SFAnalyticsTopic* topic in self->_analyticsTopics) {
1418 if ([topic.internalTopicName isEqualToString:topicName]) {
1419 json = [topic getLoggingJSON:pretty forUpload:NO participatingClients:nil force:!runningTests error:&error];
1423 secerror("Unable to obtain JSON: %@", error);
1428 - (void)forceUploadWithReply:(void (^)(BOOL, NSError*))reply {
1429 secnotice("upload", "Performing upload in response to rpc message");
1430 NSError* error = nil;
1431 BOOL result = [self uploadAnalyticsWithError:&error force:YES];
1432 secnotice("upload", "Result of manually triggered upload: %@, error: %@", result ? @"success" : @"failure", error);
1433 reply(result, error);
1438 #endif // !TARGET_OS_SIMULATOR