diff --git a/Analytics.xcodeproj/project.pbxproj b/Analytics.xcodeproj/project.pbxproj index 8ba2becb9..5cfaa57c5 100644 --- a/Analytics.xcodeproj/project.pbxproj +++ b/Analytics.xcodeproj/project.pbxproj @@ -450,7 +450,6 @@ EADEB8671DECD0EF005322DA /* Frameworks */, EADEB8681DECD0EF005322DA /* Resources */, B78A9CF1929BFE8D38B50D5C /* [CP] Embed Pods Frameworks */, - B179A3824C53C87809396267 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -536,21 +535,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - B179A3824C53C87809396267 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-AnalyticsTests/Pods-AnalyticsTests-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; B78A9CF1929BFE8D38B50D5C /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/Analytics/Classes/Internal/SEGUtils.h b/Analytics/Classes/Internal/SEGUtils.h index 761fed5dd..877fbe193 100644 --- a/Analytics/Classes/Internal/SEGUtils.h +++ b/Analytics/Classes/Internal/SEGUtils.h @@ -12,4 +12,6 @@ + (NSData *_Nullable)dataFromPlist:(nonnull id)plist; + (id _Nullable)plistFromData:(NSData *_Nonnull)data; ++ (id _Nullable)traverseJSON:(id _Nullable)object andReplaceWithFilters:(nonnull NSDictionary*)patterns; + @end diff --git a/Analytics/Classes/Internal/SEGUtils.m b/Analytics/Classes/Internal/SEGUtils.m index 8f350fae2..afbc01418 100644 --- a/Analytics/Classes/Internal/SEGUtils.m +++ b/Analytics/Classes/Internal/SEGUtils.m @@ -34,4 +34,62 @@ + (id _Nullable)plistFromData:(NSData *_Nonnull)data return plist; } + ++(id)traverseJSON:(id)object andReplaceWithFilters:(NSDictionary*)patterns +{ + if (object == nil || object == NSNull.null || [object isKindOfClass:NSNull.class]) { + return object; + } + + if ([object isKindOfClass:NSDictionary.class]) { + NSDictionary* dict = object; + NSMutableDictionary* newDict = [NSMutableDictionary dictionaryWithCapacity:dict.count]; + + for (NSString* key in dict.allKeys) { + newDict[key] = [self traverseJSON:dict[key] andReplaceWithFilters:patterns]; + } + + return newDict; + } + + if ([object isKindOfClass:NSArray.class]) { + NSArray* array = object; + NSMutableArray* newArray = [NSMutableArray arrayWithCapacity:array.count]; + + for (int i = 0; i < array.count; i++) { + newArray[i] = [self traverseJSON:array[i] andReplaceWithFilters:patterns]; + } + + return newArray; + } + + if ([object isKindOfClass:NSString.class]) { + NSError* error = nil; + NSMutableString* str = [object mutableCopy]; + + for (NSString* pattern in patterns) { + NSRegularExpression* re = [NSRegularExpression regularExpressionWithPattern:pattern + options:0 + error:&error]; + + if (error) { + @throw error; + } + + NSInteger matches = [re replaceMatchesInString:str + options:0 + range:NSMakeRange(0, str.length) + withTemplate:patterns[pattern]]; + + if (matches > 0) { + SEGLog(@"%@ Redacted value from action: %@", self, pattern); + } + } + + return str; + } + + return object; +} + @end diff --git a/Analytics/Classes/SEGAnalytics.m b/Analytics/Classes/SEGAnalytics.m index ed85883a5..a594960d0 100644 --- a/Analytics/Classes/SEGAnalytics.m +++ b/Analytics/Classes/SEGAnalytics.m @@ -14,6 +14,7 @@ #import "SEGMiddleware.h" #import "SEGContext.h" #import "SEGIntegrationsManager.h" +#import "Internal/SEGUtils.h" static SEGAnalytics *__sharedInstance = nil; @@ -344,6 +345,8 @@ - (void)continueUserActivity:(NSUserActivity *)activity [properties addEntriesFromDictionary:activity.userInfo]; properties[@"url"] = activity.webpageURL.absoluteString; properties[@"title"] = activity.title ?: @""; + properties = [SEGUtils traverseJSON:properties + andReplaceWithFilters:self.configuration.payloadFilters]; [self track:@"Deep Link Opened" properties:[properties copy]]; } } @@ -351,7 +354,8 @@ - (void)continueUserActivity:(NSUserActivity *)activity - (void)openURL:(NSURL *)url options:(NSDictionary *)options { SEGOpenURLPayload *payload = [[SEGOpenURLPayload alloc] init]; - payload.url = url; + payload.url = [NSURL URLWithString:[SEGUtils traverseJSON:url.absoluteString + andReplaceWithFilters:self.configuration.payloadFilters]]; payload.options = options; [self run:SEGEventTypeOpenURL payload:payload]; @@ -362,6 +366,8 @@ - (void)openURL:(NSURL *)url options:(NSDictionary *)options NSMutableDictionary *properties = [NSMutableDictionary dictionaryWithCapacity:options.count + 2]; [properties addEntriesFromDictionary:options]; properties[@"url"] = url.absoluteString; + properties = [SEGUtils traverseJSON:properties + andReplaceWithFilters:self.configuration.payloadFilters]; [self track:@"Deep Link Opened" properties:[properties copy]]; } diff --git a/Analytics/Classes/SEGAnalyticsConfiguration.h b/Analytics/Classes/SEGAnalyticsConfiguration.h index 43c1a236e..5ac2a8f33 100644 --- a/Analytics/Classes/SEGAnalyticsConfiguration.h +++ b/Analytics/Classes/SEGAnalyticsConfiguration.h @@ -128,4 +128,30 @@ typedef NSMutableURLRequest *_Nonnull (^SEGRequestFactory)(NSURL *_Nonnull); */ @property (nonatomic, strong, nullable) id application; +/** + * A dictionary of filters to redact payloads before they are sent. + * This is an experimental feature that currently only applies to Deep Links. + * It is subject to change to allow for more flexible customizations in the future. + * + * The key of this dictionary should be a regular expression string pattern, + * and the value should be a regular expression substitution template. + * + * By default, this contains a Facebook auth token filter, configured as such: + * @code + * @"(fb\\d+://authorize#access_token=)([^ ]+)": @"$1((redacted/fb-auth-token))" + * @endcode + * + * This will replace any matching occurences to a redacted version: + * @code + * "fb123456789://authorize#access_token=secretsecretsecretsecret&some=data" + * @endcode + * + * Becomes: + * @code + * "fb123456789://authorize#access_token=((redacted/fb-auth-token))" + * @endcode + * + */ +@property (nonatomic, strong, nonnull) NSDictionary* payloadFilters; + @end diff --git a/Analytics/Classes/SEGAnalyticsConfiguration.m b/Analytics/Classes/SEGAnalyticsConfiguration.m index 77e38038e..60d0a6efc 100644 --- a/Analytics/Classes/SEGAnalyticsConfiguration.m +++ b/Analytics/Classes/SEGAnalyticsConfiguration.m @@ -55,6 +55,9 @@ - (instancetype)init self.enableAdvertisingTracking = YES; self.shouldUseBluetooth = NO; self.flushAt = 20; + self.payloadFilters = @{ + @"(fb\\d+://authorize#access_token=)([^ ]+)": @"$1((redacted/fb-auth-token))" + }; _factories = [NSMutableArray array]; Class applicationClass = NSClassFromString(@"UIApplication"); if (applicationClass) { diff --git a/AnalyticsTests/AnalyticsTests-Bridging-Header.h b/AnalyticsTests/AnalyticsTests-Bridging-Header.h index 58aa5b215..877104033 100644 --- a/AnalyticsTests/AnalyticsTests-Bridging-Header.h +++ b/AnalyticsTests/AnalyticsTests-Bridging-Header.h @@ -11,6 +11,7 @@ #import #import #import +#import #import "NSData+SEGGUNZIPP.h" // Temp hack. We should fix the LSNocilla podspec to make this header publicly available diff --git a/AnalyticsTests/AnalyticsTests.swift b/AnalyticsTests/AnalyticsTests.swift index 1baed04a9..1b0aa0c73 100644 --- a/AnalyticsTests/AnalyticsTests.swift +++ b/AnalyticsTests/AnalyticsTests.swift @@ -120,6 +120,29 @@ class AnalyticsTests: QuickSpec { let task = UIApplication.shared.beginBackgroundTask(expirationHandler: nil) UIApplication.shared.endBackgroundTask(task) } + + it("redacts sensible URLs from deep links tracking") { + testMiddleware.swallowEvent = true + analytics.configuration.trackDeepLinks = true + analytics.open(URL(string: "fb123456789://authorize#access_token=hastoberedacted")!, options: [:]) + + + let event = testMiddleware.lastContext?.payload as? SEGTrackPayload + expect(event?.event) == "Deep Link Opened" + expect(event?.properties?["url"] as? String) == "fb123456789://authorize#access_token=((redacted/fb-auth-token))" + } + + it("redacts sensible URLs from deep links tracking using custom filters") { + testMiddleware.swallowEvent = true + analytics.configuration.payloadFilters["(myapp://auth\\?token=)([^&]+)"] = "$1((redacted/my-auth))" + analytics.configuration.trackDeepLinks = true + analytics.open(URL(string: "myapp://auth?token=hastoberedacted&other=stuff")!, options: [:]) + + + let event = testMiddleware.lastContext?.payload as? SEGTrackPayload + expect(event?.event) == "Deep Link Opened" + expect(event?.properties?["url"] as? String) == "myapp://auth?token=((redacted/my-auth))&other=stuff" + } } } diff --git a/AnalyticsTests/AnalyticsUtilTests.swift b/AnalyticsTests/AnalyticsUtilTests.swift index deda55864..739b6018b 100644 --- a/AnalyticsTests/AnalyticsUtilTests.swift +++ b/AnalyticsTests/AnalyticsUtilTests.swift @@ -34,6 +34,54 @@ class AnalyticsUtilTests: QuickSpec { expect(formattedString2) == "1992-08-06T11:32:04.335Z" } + describe("JSON traverse", { + let filters = [ + "(foo)": "$1-bar" + ] + + func equals(a: Any, b: Any) -> Bool { + let aData = try! JSONSerialization.data(withJSONObject: a, options: .prettyPrinted) as NSData + let bData = try! JSONSerialization.data(withJSONObject: b, options: .prettyPrinted) + + return aData.isEqual(to: bData) + } + + it("works with strings") { + expect(SEGUtils.traverseJSON("a b foo c", andReplaceWithFilters: filters) as? String) == "a b foo-bar c" + } + + it("works recursively") { + expect(SEGUtils.traverseJSON("a b foo foo c", andReplaceWithFilters: filters) as? String) == "a b foo-bar foo-bar c" + } + + it("works with nested dictionaries") { + let data = [ + "foo": [1, nil, "qfoob", ["baz": "foo"]], + "bar": "foo" + ] as [String : Any] + let input = SEGUtils.traverseJSON(data, andReplaceWithFilters: filters) + let output = [ + "foo": [1, nil, "qfoo-barb", ["baz": "foo-bar"]], + "bar": "foo-bar" + ] as [String : Any] + + expect(equals(a: input!, b: output)) == true + } + + it("works with nested arrays") { + let data = [ + [1, nil, "qfoob", ["baz": "foo"]], + "foo" + ] as [Any] + let input = SEGUtils.traverseJSON(data, andReplaceWithFilters: filters) + let output = [ + [1, nil, "qfoo-barb", ["baz": "foo-bar"]], + "foo-bar" + ] as [Any] + + expect(equals(a: input!, b: output)) == true + } + }) } } diff --git a/Podfile b/Podfile index 544af3af3..12da5d9a3 100644 --- a/Podfile +++ b/Podfile @@ -6,7 +6,7 @@ target 'AnalyticsTests' do use_frameworks! pod 'Quick', '~> 1.2.0' # runner lib - pod 'Nimble', '~> 7.0.2' # Matcher lib + pod 'Nimble', '~> 7.3.1' # Matcher lib pod 'Nocilla', '~> 0.11.0' # HTTP Mocking Library pod 'SwiftTryCatch', :git => 'https://github.com/segmentio/SwiftTryCatch.git' # Utils lib diff --git a/Podfile.lock b/Podfile.lock index 5819dbdf6..beb4b5c2d 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,30 +1,36 @@ PODS: - - Nimble (7.0.2) + - Nimble (7.3.1) - Nocilla (0.11.0) - Quick (1.2.0) - - SwiftTryCatch (0.0.1) + - SwiftTryCatch (1.0.0) DEPENDENCIES: - - Nimble (~> 7.0.2) + - Nimble (~> 7.3.1) - Nocilla (~> 0.11.0) - Quick (~> 1.2.0) - SwiftTryCatch (from `https://github.com/segmentio/SwiftTryCatch.git`) +SPEC REPOS: + https://github.com/cocoapods/specs.git: + - Nimble + - Nocilla + - Quick + EXTERNAL SOURCES: SwiftTryCatch: :git: https://github.com/segmentio/SwiftTryCatch.git CHECKOUT OPTIONS: SwiftTryCatch: - :commit: dcdc954cb0945faaf33800b4b913e04d2ebdd965 + :commit: 2cdec294628f73350c5d8f6f05d08886af57668b :git: https://github.com/segmentio/SwiftTryCatch.git SPEC CHECKSUMS: - Nimble: bfe1f814edabba69ff145cb1283e04ed636a67f2 + Nimble: 04f732da099ea4d153122aec8c2a88fd0c7219ae Nocilla: 7af7a386071150cc8aa5da4da97d060f049dd61c Quick: 58d203b1c5e27fff7229c4c1ae445ad7069a7a08 - SwiftTryCatch: fb6d2b34abe48efd69578dac919293a44f95b481 + SwiftTryCatch: 2f4ef36cf5396bdb450006b70633dbce5260d3b3 -PODFILE CHECKSUM: 25d553a80951f726d31098fb274eedfdc7fce4d9 +PODFILE CHECKSUM: 31008e8b9503ec3adb2fc152eb967391a0dd408d -COCOAPODS: 1.3.1 +COCOAPODS: 1.5.3