]> git.saurik.com Git - apple/security.git/blob - Analytics/SFAnalytics.m
Security-58286.51.6.tar.gz
[apple/security.git] / Analytics / SFAnalytics.m
1 /*
2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
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
11 * file.
12 *
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.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #if __OBJC2__
25
26 #import "SFAnalytics+Internal.h"
27 #import "SFAnalyticsDefines.h"
28 #import "SFAnalyticsActivityTracker+Internal.h"
29 #import "SFAnalyticsSampler+Internal.h"
30 #import "SFAnalyticsMultiSampler+Internal.h"
31 #import "SFAnalyticsSQLiteStore.h"
32 #import "utilities/debugging.h"
33 #import <objc/runtime.h>
34 #import <CoreFoundation/CFPriv.h>
35
36 // SFAnalyticsDefines constants
37 NSString* const SFAnalyticsTableSuccessCount = @"success_count";
38 NSString* const SFAnalyticsTableHardFailures = @"hard_failures";
39 NSString* const SFAnalyticsTableSoftFailures = @"soft_failures";
40 NSString* const SFAnalyticsTableSamples = @"samples";
41 NSString* const SFAnalyticsTableAllEvents = @"all_events";
42
43 NSString* const SFAnalyticsColumnSuccessCount = @"success_count";
44 NSString* const SFAnalyticsColumnHardFailureCount = @"hard_failure_count";
45 NSString* const SFAnalyticsColumnSoftFailureCount = @"soft_failure_count";
46 NSString* const SFAnalyticsColumnSampleValue = @"value";
47 NSString* const SFAnalyticsColumnSampleName = @"name";
48
49 NSString* const SFAnalyticsEventTime = @"eventTime";
50 NSString* const SFAnalyticsEventType = @"eventType";
51 NSString* const SFAnalyticsEventClassKey = @"eventClass";
52
53 NSString* const SFAnalyticsAttributeErrorUnderlyingChain = @"errorChain";
54 NSString* const SFAnalyticsAttributeErrorDomain = @"errorDomain";
55 NSString* const SFAnalyticsAttributeErrorCode = @"errorCode";
56
57 NSString* const SFAnalyticsUserDefaultsSuite = @"com.apple.security.analytics";
58
59 char* const SFAnalyticsFireSamplersNotification = "com.apple.security.sfanalytics.samplers";
60
61 NSString* const SFAnalyticsTopicKeySync = @"KeySyncTopic";
62 NSString* const SFAnaltyicsTopicTrust = @"TrustTopic";
63
64 NSString* const SFAnalyticsTableSchema = @"CREATE TABLE IF NOT EXISTS hard_failures (\n"
65 @"id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
66 @"timestamp REAL,"
67 @"data BLOB\n"
68 @");\n"
69 @"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_hard_failures AFTER INSERT ON hard_failures\n"
70 @"BEGIN\n"
71 @"DELETE FROM hard_failures WHERE id != NEW.id AND id % 1000 = NEW.id % 1000;\n"
72 @"END;\n"
73 @"CREATE TABLE IF NOT EXISTS soft_failures (\n"
74 @"id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
75 @"timestamp REAL,"
76 @"data BLOB\n"
77 @");\n"
78 @"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_soft_failures AFTER INSERT ON soft_failures\n"
79 @"BEGIN\n"
80 @"DELETE FROM soft_failures WHERE id != NEW.id AND id % 1000 = NEW.id % 1000;\n"
81 @"END;\n"
82 @"CREATE TABLE IF NOT EXISTS all_events (\n"
83 @"id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
84 @"timestamp REAL,"
85 @"data BLOB\n"
86 @");\n"
87 @"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_all_events AFTER INSERT ON all_events\n"
88 @"BEGIN\n"
89 @"DELETE FROM all_events WHERE id != NEW.id AND id % 10000 = NEW.id % 10000;\n"
90 @"END;\n"
91 @"CREATE TABLE IF NOT EXISTS samples (\n"
92 @"id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
93 @"timestamp REAL,\n"
94 @"name STRING,\n"
95 @"value REAL\n"
96 @");\n"
97 @"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_samples AFTER INSERT ON samples\n"
98 @"BEGIN\n"
99 @"DELETE FROM samples WHERE id != NEW.id AND id % 1000 = NEW.id % 1000;\n"
100 @"END;\n"
101 @"CREATE TABLE IF NOT EXISTS success_count (\n"
102 @"event_type STRING PRIMARY KEY,\n"
103 @"success_count INTEGER,\n"
104 @"hard_failure_count INTEGER,\n"
105 @"soft_failure_count INTEGER\n"
106 @");\n";
107
108 NSUInteger const SFAnalyticsMaxEventsToReport = 1000;
109
110 // Local constants
111 NSString* const SFAnalyticsEventBuild = @"build";
112 NSString* const SFAnalyticsEventProduct = @"product";
113 const NSTimeInterval SFAnalyticsSamplerIntervalOncePerReport = -1.0;
114
115 @interface SFAnalytics ()
116 @property (nonatomic) SFAnalyticsSQLiteStore* database;
117 @end
118
119 @implementation SFAnalytics {
120 SFAnalyticsSQLiteStore* _database;
121 dispatch_queue_t _queue;
122 NSMutableDictionary<NSString*, SFAnalyticsSampler*>* _samplers;
123 NSMutableDictionary<NSString*, SFAnalyticsMultiSampler*>* _multisamplers;
124 unsigned int _disableLogging:1;
125 }
126
127 + (instancetype)logger
128 {
129 #if TARGET_OS_SIMULATOR
130 return nil;
131 #else
132
133 if (self == [SFAnalytics class]) {
134 secerror("attempt to instatiate abstract class SFAnalytics");
135 return nil;
136 }
137
138 SFAnalytics* logger = nil;
139 @synchronized(self) {
140 logger = objc_getAssociatedObject(self, "SFAnalyticsInstance");
141 if (!logger) {
142 logger = [[self alloc] init];
143 objc_setAssociatedObject(self, "SFAnalyticsInstance", logger, OBJC_ASSOCIATION_RETAIN);
144 }
145 }
146
147 [logger database]; // For unit testing so there's always a database. DB shouldn't be nilled in production though
148 return logger;
149 #endif
150 }
151
152 + (NSString*)databasePath
153 {
154 return nil;
155 }
156
157 + (NSInteger)fuzzyDaysSinceDate:(NSDate*)date
158 {
159 // Sentinel: it didn't happen at all
160 if (!date) {
161 return -1;
162 }
163
164 // Sentinel: it happened but we don't know when because the date doesn't make sense
165 // Magic number represents January 1, 2017.
166 if ([date compare:[NSDate dateWithTimeIntervalSince1970:1483228800]] == NSOrderedAscending) {
167 return 1000;
168 }
169
170 NSInteger secondsPerDay = 60 * 60 * 24;
171
172 NSTimeInterval timeIntervalSinceDate = [[NSDate date] timeIntervalSinceDate:date];
173 if (timeIntervalSinceDate < secondsPerDay) {
174 return 0;
175 }
176 else if (timeIntervalSinceDate < (secondsPerDay * 7)) {
177 return 1;
178 }
179 else if (timeIntervalSinceDate < (secondsPerDay * 30)) {
180 return 7;
181 }
182 else if (timeIntervalSinceDate < (secondsPerDay * 365)) {
183 return 30;
184 }
185 else {
186 return 365;
187 }
188 }
189
190 // Instantiate lazily so unit tests can have clean databases each
191 - (SFAnalyticsSQLiteStore*)database
192 {
193 if (!_database) {
194 _database = [SFAnalyticsSQLiteStore storeWithPath:self.class.databasePath schema:SFAnalyticsTableSchema];
195 }
196 return _database;
197 }
198
199 - (void)removeState
200 {
201 [_samplers removeAllObjects];
202 [_multisamplers removeAllObjects];
203
204 __weak __typeof(self) weakSelf = self;
205 dispatch_sync(_queue, ^{
206 __strong __typeof(self) strongSelf = weakSelf;
207 if (strongSelf) {
208 [strongSelf.database close];
209 strongSelf->_database = nil;
210 }
211 });
212 }
213
214 - (void)setDateProperty:(NSDate*)date forKey:(NSString*)key
215 {
216 __weak __typeof(self) weakSelf = self;
217 dispatch_sync(_queue, ^{
218 __strong __typeof(self) strongSelf = weakSelf;
219 if (strongSelf) {
220 [strongSelf.database setDateProperty:date forKey:key];
221 }
222 });
223 }
224
225 - (NSDate*)datePropertyForKey:(NSString*)key
226 {
227 __block NSDate* result = nil;
228 __weak __typeof(self) weakSelf = self;
229 dispatch_sync(_queue, ^{
230 __strong __typeof(self) strongSelf = weakSelf;
231 if (strongSelf) {
232 result = [strongSelf.database datePropertyForKey:key];
233 }
234 });
235 return result;
236 }
237
238 + (void)addOSVersionToEvent:(NSMutableDictionary*)eventDict {
239 static dispatch_once_t onceToken;
240 static NSString *build = NULL;
241 static NSString *product = NULL;
242 dispatch_once(&onceToken, ^{
243 NSDictionary *version = CFBridgingRelease(_CFCopySystemVersionDictionary());
244 if (version == NULL)
245 return;
246 build = version[(__bridge NSString *)_kCFSystemVersionBuildVersionKey];
247 product = version[(__bridge NSString *)_kCFSystemVersionProductNameKey];
248 });
249 if (build) {
250 eventDict[SFAnalyticsEventBuild] = build;
251 }
252 if (product) {
253 eventDict[SFAnalyticsEventProduct] = product;
254 }
255 }
256
257 - (instancetype)init
258 {
259 if (self = [super init]) {
260 _queue = dispatch_queue_create("SFAnalytics data access queue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
261 _samplers = [NSMutableDictionary<NSString*, SFAnalyticsSampler*> new];
262 _multisamplers = [NSMutableDictionary<NSString*, SFAnalyticsMultiSampler*> new];
263 [self database]; // for side effect of instantiating DB object. Used for testing.
264 }
265
266 return self;
267 }
268
269 // MARK: Event logging
270
271 - (void)logSuccessForEventNamed:(NSString*)eventName
272 {
273 [self logEventNamed:eventName class:SFAnalyticsEventClassSuccess attributes:nil];
274 }
275
276 - (void)logHardFailureForEventNamed:(NSString*)eventName withAttributes:(NSDictionary*)attributes
277 {
278 [self logEventNamed:eventName class:SFAnalyticsEventClassHardFailure attributes:attributes];
279 }
280
281 - (void)logSoftFailureForEventNamed:(NSString*)eventName withAttributes:(NSDictionary*)attributes
282 {
283 [self logEventNamed:eventName class:SFAnalyticsEventClassSoftFailure attributes:attributes];
284 }
285
286 - (void)logResultForEvent:(NSString*)eventName hardFailure:(bool)hardFailure result:(NSError*)eventResultError
287 {
288 if(!eventResultError) {
289 [self logSuccessForEventNamed:eventName];
290 } else {
291 // Make an Attributes dictionary
292 NSMutableDictionary* eventAttributes = [NSMutableDictionary dictionary];
293
294 /* if we have underlying errors, capture the chain below the top-most error */
295 NSError *underlyingError = eventResultError.userInfo[NSUnderlyingErrorKey];
296 if ([underlyingError isKindOfClass:[NSError class]]) {
297 NSMutableString *chain = [NSMutableString string];
298 int count = 0;
299 do {
300 [chain appendFormat:@"%@-%ld:", underlyingError.domain, (long)underlyingError.code];
301 underlyingError = underlyingError.userInfo[NSUnderlyingErrorKey];
302 } while (count++ < 5 && [underlyingError isKindOfClass:[NSError class]]);
303
304 eventAttributes[SFAnalyticsAttributeErrorUnderlyingChain] = chain;
305 }
306
307 eventAttributes[SFAnalyticsAttributeErrorDomain] = eventResultError.domain;
308 eventAttributes[SFAnalyticsAttributeErrorCode] = @(eventResultError.code);
309
310 if(hardFailure) {
311 [self logHardFailureForEventNamed:eventName withAttributes:eventAttributes];
312 } else {
313 [self logSoftFailureForEventNamed:eventName withAttributes:eventAttributes];
314 }
315 }
316 }
317
318 - (void)noteEventNamed:(NSString*)eventName
319 {
320 [self logEventNamed:eventName class:SFAnalyticsEventClassNote attributes:nil];
321 }
322
323 - (void)logEventNamed:(NSString*)eventName class:(SFAnalyticsEventClass)class attributes:(NSDictionary*)attributes
324 {
325 if (!eventName) {
326 secerror("SFAnalytics: attempt to log an event with no name");
327 return;
328 }
329
330 __weak __typeof(self) weakSelf = self;
331 dispatch_sync(_queue, ^{
332 __strong __typeof(self) strongSelf = weakSelf;
333 if (!strongSelf || strongSelf->_disableLogging) {
334 return;
335 }
336
337 NSDictionary* eventDict = [self eventDictForEventName:eventName withAttributes:attributes eventClass:class];
338 [strongSelf.database addEventDict:eventDict toTable:SFAnalyticsTableAllEvents];
339
340 if (class == SFAnalyticsEventClassHardFailure) {
341 [strongSelf.database addEventDict:eventDict toTable:SFAnalyticsTableHardFailures];
342 [strongSelf.database incrementHardFailureCountForEventType:eventName];
343 }
344 else if (class == SFAnalyticsEventClassSoftFailure) {
345 [strongSelf.database addEventDict:eventDict toTable:SFAnalyticsTableSoftFailures];
346 [strongSelf.database incrementSoftFailureCountForEventType:eventName];
347 }
348 else if (class == SFAnalyticsEventClassSuccess || class == SFAnalyticsEventClassNote) {
349 [strongSelf.database incrementSuccessCountForEventType:eventName];
350 }
351 });
352 }
353
354 - (NSDictionary*)eventDictForEventName:(NSString*)eventName withAttributes:(NSDictionary*)attributes eventClass:(SFAnalyticsEventClass)eventClass
355 {
356 NSMutableDictionary* eventDict = attributes ? attributes.mutableCopy : [NSMutableDictionary dictionary];
357 eventDict[SFAnalyticsEventType] = eventName;
358 // our backend wants timestamps in milliseconds
359 eventDict[SFAnalyticsEventTime] = @([[NSDate date] timeIntervalSince1970] * 1000);
360 eventDict[SFAnalyticsEventClassKey] = @(eventClass);
361 [SFAnalytics addOSVersionToEvent:eventDict];
362
363 return eventDict;
364 }
365
366 // MARK: Sampling
367
368 - (SFAnalyticsSampler*)addMetricSamplerForName:(NSString *)samplerName withTimeInterval:(NSTimeInterval)timeInterval block:(NSNumber *(^)(void))block
369 {
370 if (!samplerName) {
371 secerror("SFAnalytics: cannot add sampler without name");
372 return nil;
373 }
374 if (timeInterval < 1.0f && timeInterval != SFAnalyticsSamplerIntervalOncePerReport) {
375 secerror("SFAnalytics: cannot add sampler with interval %f", timeInterval);
376 return nil;
377 }
378 if (!block) {
379 secerror("SFAnalytics: cannot add sampler without block");
380 return nil;
381 }
382
383 __block SFAnalyticsSampler* sampler = nil;
384
385 __weak __typeof(self) weakSelf = self;
386 dispatch_sync(_queue, ^{
387 __strong __typeof(self) strongSelf = weakSelf;
388 if (strongSelf->_samplers[samplerName]) {
389 secerror("SFAnalytics: sampler \"%@\" already exists", samplerName);
390 } else {
391 sampler = [[SFAnalyticsSampler alloc] initWithName:samplerName interval:timeInterval block:block clientClass:[self class]];
392 strongSelf->_samplers[samplerName] = sampler; // If sampler did not init because of bad data this 'removes' it from the dict, so a noop
393 }
394 });
395
396 return sampler;
397 }
398
399 - (SFAnalyticsMultiSampler*)AddMultiSamplerForName:(NSString *)samplerName withTimeInterval:(NSTimeInterval)timeInterval block:(NSDictionary<NSString *,NSNumber *> *(^)(void))block
400 {
401 if (!samplerName) {
402 secerror("SFAnalytics: cannot add sampler without name");
403 return nil;
404 }
405 if (timeInterval < 1.0f && timeInterval != SFAnalyticsSamplerIntervalOncePerReport) {
406 secerror("SFAnalytics: cannot add sampler with interval %f", timeInterval);
407 return nil;
408 }
409 if (!block) {
410 secerror("SFAnalytics: cannot add sampler without block");
411 return nil;
412 }
413
414 __block SFAnalyticsMultiSampler* sampler = nil;
415 __weak __typeof(self) weakSelf = self;
416 dispatch_sync(_queue, ^{
417 __strong __typeof(self) strongSelf = weakSelf;
418 if (strongSelf->_multisamplers[samplerName]) {
419 secerror("SFAnalytics: multisampler \"%@\" already exists", samplerName);
420 } else {
421 sampler = [[SFAnalyticsMultiSampler alloc] initWithName:samplerName interval:timeInterval block:block clientClass:[self class]];
422 strongSelf->_multisamplers[samplerName] = sampler;
423 }
424
425 });
426
427 return sampler;
428 }
429
430 - (SFAnalyticsSampler*)existingMetricSamplerForName:(NSString *)samplerName
431 {
432 __block SFAnalyticsSampler* sampler = nil;
433
434 __weak __typeof(self) weakSelf = self;
435 dispatch_sync(_queue, ^{
436 __strong __typeof(self) strongSelf = weakSelf;
437 if (strongSelf) {
438 sampler = strongSelf->_samplers[samplerName];
439 }
440 });
441 return sampler;
442 }
443
444 - (SFAnalyticsMultiSampler*)existingMultiSamplerForName:(NSString *)samplerName
445 {
446 __block SFAnalyticsMultiSampler* sampler = nil;
447
448 __weak __typeof(self) weakSelf = self;
449 dispatch_sync(_queue, ^{
450 __strong __typeof(self) strongSelf = weakSelf;
451 if (strongSelf) {
452 sampler = strongSelf->_multisamplers[samplerName];
453 }
454 });
455 return sampler;
456 }
457
458 - (void)removeMetricSamplerForName:(NSString *)samplerName
459 {
460 if (!samplerName) {
461 secerror("Attempt to remove sampler without specifying samplerName");
462 return;
463 }
464
465 __weak __typeof(self) weakSelf = self;
466 dispatch_sync(_queue, ^{
467 __strong __typeof(self) strongSelf = weakSelf;
468 if (strongSelf) {
469 [strongSelf->_samplers[samplerName] pauseSampling]; // when dealloced it would also stop, but we're not sure when that is so let's stop it right away
470 [strongSelf->_samplers removeObjectForKey:samplerName];
471 }
472 });
473 }
474
475 - (void)removeMultiSamplerForName:(NSString *)samplerName
476 {
477 if (!samplerName) {
478 secerror("Attempt to remove multisampler without specifying samplerName");
479 return;
480 }
481
482 __weak __typeof(self) weakSelf = self;
483 dispatch_sync(_queue, ^{
484 __strong __typeof(self) strongSelf = weakSelf;
485 if (strongSelf) {
486 [strongSelf->_multisamplers[samplerName] pauseSampling]; // when dealloced it would also stop, but we're not sure when that is so let's stop it right away
487 [strongSelf->_multisamplers removeObjectForKey:samplerName];
488 }
489 });
490 }
491
492 - (SFAnalyticsActivityTracker*)logSystemMetricsForActivityNamed:(NSString *)eventName withAction:(void (^)(void))action
493 {
494 if (![eventName isKindOfClass:[NSString class]]) {
495 secerror("Cannot log system metrics without name");
496 return nil;
497 }
498 SFAnalyticsActivityTracker* tracker = [[SFAnalyticsActivityTracker alloc] initWithName:eventName clientClass:[self class]];
499 if (action)
500 [tracker performAction:action];
501 return tracker;
502 }
503
504 - (void)logMetric:(NSNumber *)metric withName:(NSString *)metricName
505 {
506 [self logMetric:metric withName:metricName oncePerReport:NO];
507 }
508
509 - (void)logMetric:(NSNumber*)metric withName:(NSString*)metricName oncePerReport:(BOOL)once
510 {
511 if (![metric isKindOfClass:[NSNumber class]] || ![metricName isKindOfClass:[NSString class]]) {
512 secerror("SFAnalytics: Need a valid result and name to log result");
513 return;
514 }
515
516 __weak __typeof(self) weakSelf = self;
517 dispatch_sync(_queue, ^{
518 __strong __typeof(self) strongSelf = weakSelf;
519 if (strongSelf && !strongSelf->_disableLogging) {
520 if (once) {
521 [strongSelf.database removeAllSamplesForName:metricName];
522 }
523 [strongSelf.database addSample:metric forName:metricName];
524 }
525 });
526 }
527
528 @end
529
530 #endif // __OBJC2__