Skip to content

Commit aad768c

Browse files
committed
Add deep links redaction (#798)
Ref: LIB-686
1 parent 7bd4299 commit aad768c

File tree

11 files changed

+183
-26
lines changed

11 files changed

+183
-26
lines changed

Analytics.xcodeproj/project.pbxproj

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,6 @@
450450
EADEB8671DECD0EF005322DA /* Frameworks */,
451451
EADEB8681DECD0EF005322DA /* Resources */,
452452
B78A9CF1929BFE8D38B50D5C /* [CP] Embed Pods Frameworks */,
453-
B179A3824C53C87809396267 /* [CP] Copy Pods Resources */,
454453
);
455454
buildRules = (
456455
);
@@ -536,21 +535,6 @@
536535
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";
537536
showEnvVarsInLog = 0;
538537
};
539-
B179A3824C53C87809396267 /* [CP] Copy Pods Resources */ = {
540-
isa = PBXShellScriptBuildPhase;
541-
buildActionMask = 2147483647;
542-
files = (
543-
);
544-
inputPaths = (
545-
);
546-
name = "[CP] Copy Pods Resources";
547-
outputPaths = (
548-
);
549-
runOnlyForDeploymentPostprocessing = 0;
550-
shellPath = /bin/sh;
551-
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-AnalyticsTests/Pods-AnalyticsTests-resources.sh\"\n";
552-
showEnvVarsInLog = 0;
553-
};
554538
B78A9CF1929BFE8D38B50D5C /* [CP] Embed Pods Frameworks */ = {
555539
isa = PBXShellScriptBuildPhase;
556540
buildActionMask = 2147483647;

Analytics/Classes/Internal/SEGUtils.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@
1212
+ (NSData *_Nullable)dataFromPlist:(nonnull id)plist;
1313
+ (id _Nullable)plistFromData:(NSData *_Nonnull)data;
1414

15+
+ (id _Nullable)traverseJSON:(id _Nullable)object andReplaceWithFilters:(nonnull NSDictionary<NSString*, NSString*>*)patterns;
16+
1517
@end

Analytics/Classes/Internal/SEGUtils.m

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,62 @@ + (id _Nullable)plistFromData:(NSData *_Nonnull)data
3434
return plist;
3535
}
3636

37+
38+
+(id)traverseJSON:(id)object andReplaceWithFilters:(NSDictionary<NSString*, NSString*>*)patterns
39+
{
40+
if (object == nil || object == NSNull.null || [object isKindOfClass:NSNull.class]) {
41+
return object;
42+
}
43+
44+
if ([object isKindOfClass:NSDictionary.class]) {
45+
NSDictionary* dict = object;
46+
NSMutableDictionary* newDict = [NSMutableDictionary dictionaryWithCapacity:dict.count];
47+
48+
for (NSString* key in dict.allKeys) {
49+
newDict[key] = [self traverseJSON:dict[key] andReplaceWithFilters:patterns];
50+
}
51+
52+
return newDict;
53+
}
54+
55+
if ([object isKindOfClass:NSArray.class]) {
56+
NSArray* array = object;
57+
NSMutableArray* newArray = [NSMutableArray arrayWithCapacity:array.count];
58+
59+
for (int i = 0; i < array.count; i++) {
60+
newArray[i] = [self traverseJSON:array[i] andReplaceWithFilters:patterns];
61+
}
62+
63+
return newArray;
64+
}
65+
66+
if ([object isKindOfClass:NSString.class]) {
67+
NSError* error = nil;
68+
NSMutableString* str = [object mutableCopy];
69+
70+
for (NSString* pattern in patterns) {
71+
NSRegularExpression* re = [NSRegularExpression regularExpressionWithPattern:pattern
72+
options:0
73+
error:&error];
74+
75+
if (error) {
76+
@throw error;
77+
}
78+
79+
NSInteger matches = [re replaceMatchesInString:str
80+
options:0
81+
range:NSMakeRange(0, str.length)
82+
withTemplate:patterns[pattern]];
83+
84+
if (matches > 0) {
85+
SEGLog(@"%@ Redacted value from action: %@", self, pattern);
86+
}
87+
}
88+
89+
return str;
90+
}
91+
92+
return object;
93+
}
94+
3795
@end

Analytics/Classes/SEGAnalytics.m

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#import "SEGMiddleware.h"
1515
#import "SEGContext.h"
1616
#import "SEGIntegrationsManager.h"
17+
#import "Internal/SEGUtils.h"
1718

1819
static SEGAnalytics *__sharedInstance = nil;
1920

@@ -344,14 +345,17 @@ - (void)continueUserActivity:(NSUserActivity *)activity
344345
[properties addEntriesFromDictionary:activity.userInfo];
345346
properties[@"url"] = activity.webpageURL.absoluteString;
346347
properties[@"title"] = activity.title ?: @"";
348+
properties = [SEGUtils traverseJSON:properties
349+
andReplaceWithFilters:self.configuration.payloadFilters];
347350
[self track:@"Deep Link Opened" properties:[properties copy]];
348351
}
349352
}
350353

351354
- (void)openURL:(NSURL *)url options:(NSDictionary *)options
352355
{
353356
SEGOpenURLPayload *payload = [[SEGOpenURLPayload alloc] init];
354-
payload.url = url;
357+
payload.url = [NSURL URLWithString:[SEGUtils traverseJSON:url.absoluteString
358+
andReplaceWithFilters:self.configuration.payloadFilters]];
355359
payload.options = options;
356360
[self run:SEGEventTypeOpenURL payload:payload];
357361

@@ -362,6 +366,8 @@ - (void)openURL:(NSURL *)url options:(NSDictionary *)options
362366
NSMutableDictionary *properties = [NSMutableDictionary dictionaryWithCapacity:options.count + 2];
363367
[properties addEntriesFromDictionary:options];
364368
properties[@"url"] = url.absoluteString;
369+
properties = [SEGUtils traverseJSON:properties
370+
andReplaceWithFilters:self.configuration.payloadFilters];
365371
[self track:@"Deep Link Opened" properties:[properties copy]];
366372
}
367373

Analytics/Classes/SEGAnalyticsConfiguration.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,30 @@ typedef NSMutableURLRequest *_Nonnull (^SEGRequestFactory)(NSURL *_Nonnull);
128128
*/
129129
@property (nonatomic, strong, nullable) id<SEGApplicationProtocol> application;
130130

131+
/**
132+
* A dictionary of filters to redact payloads before they are sent.
133+
* This is an experimental feature that currently only applies to Deep Links.
134+
* It is subject to change to allow for more flexible customizations in the future.
135+
*
136+
* The key of this dictionary should be a regular expression string pattern,
137+
* and the value should be a regular expression substitution template.
138+
*
139+
* By default, this contains a Facebook auth token filter, configured as such:
140+
* @code
141+
* @"(fb\\d+://authorize#access_token=)([^ ]+)": @"$1((redacted/fb-auth-token))"
142+
* @endcode
143+
*
144+
* This will replace any matching occurences to a redacted version:
145+
* @code
146+
* "fb123456789://authorize#access_token=secretsecretsecretsecret&some=data"
147+
* @endcode
148+
*
149+
* Becomes:
150+
* @code
151+
* "fb123456789://authorize#access_token=((redacted/fb-auth-token))"
152+
* @endcode
153+
*
154+
*/
155+
@property (nonatomic, strong, nonnull) NSDictionary<NSString*, NSString*>* payloadFilters;
156+
131157
@end

Analytics/Classes/SEGAnalyticsConfiguration.m

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ - (instancetype)init
5555
self.enableAdvertisingTracking = YES;
5656
self.shouldUseBluetooth = NO;
5757
self.flushAt = 20;
58+
self.payloadFilters = @{
59+
@"(fb\\d+://authorize#access_token=)([^ ]+)": @"$1((redacted/fb-auth-token))"
60+
};
5861
_factories = [NSMutableArray array];
5962
Class applicationClass = NSClassFromString(@"UIApplication");
6063
if (applicationClass) {

AnalyticsTests/AnalyticsTests-Bridging-Header.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#import <Analytics/UIViewController+SEGScreen.h>
1212
#import <Analytics/SEGAnalyticsUtils.h>
1313
#import <Analytics/SEGIntegrationsManager.h>
14+
#import <Analytics/SEGUtils.h>
1415

1516
#import "NSData+SEGGUNZIPP.h"
1617
// Temp hack. We should fix the LSNocilla podspec to make this header publicly available

AnalyticsTests/AnalyticsTests.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,29 @@ class AnalyticsTests: QuickSpec {
120120
let task = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
121121
UIApplication.shared.endBackgroundTask(task)
122122
}
123+
124+
it("redacts sensible URLs from deep links tracking") {
125+
testMiddleware.swallowEvent = true
126+
analytics.configuration.trackDeepLinks = true
127+
analytics.open(URL(string: "fb123456789://authorize#access_token=hastoberedacted")!, options: [:])
128+
129+
130+
let event = testMiddleware.lastContext?.payload as? SEGTrackPayload
131+
expect(event?.event) == "Deep Link Opened"
132+
expect(event?.properties?["url"] as? String) == "fb123456789://authorize#access_token=((redacted/fb-auth-token))"
133+
}
134+
135+
it("redacts sensible URLs from deep links tracking using custom filters") {
136+
testMiddleware.swallowEvent = true
137+
analytics.configuration.payloadFilters["(myapp://auth\\?token=)([^&]+)"] = "$1((redacted/my-auth))"
138+
analytics.configuration.trackDeepLinks = true
139+
analytics.open(URL(string: "myapp://auth?token=hastoberedacted&other=stuff")!, options: [:])
140+
141+
142+
let event = testMiddleware.lastContext?.payload as? SEGTrackPayload
143+
expect(event?.event) == "Deep Link Opened"
144+
expect(event?.properties?["url"] as? String) == "myapp://auth?token=((redacted/my-auth))&other=stuff"
145+
}
123146
}
124147

125148
}

AnalyticsTests/AnalyticsUtilTests.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,54 @@ class AnalyticsUtilTests: QuickSpec {
3434
expect(formattedString2) == "1992-08-06T11:32:04.335Z"
3535
}
3636

37+
describe("JSON traverse", {
38+
let filters = [
39+
"(foo)": "$1-bar"
40+
]
41+
42+
func equals(a: Any, b: Any) -> Bool {
43+
let aData = try! JSONSerialization.data(withJSONObject: a, options: .prettyPrinted) as NSData
44+
let bData = try! JSONSerialization.data(withJSONObject: b, options: .prettyPrinted)
45+
46+
return aData.isEqual(to: bData)
47+
}
48+
49+
it("works with strings") {
50+
expect(SEGUtils.traverseJSON("a b foo c", andReplaceWithFilters: filters) as? String) == "a b foo-bar c"
51+
}
52+
53+
it("works recursively") {
54+
expect(SEGUtils.traverseJSON("a b foo foo c", andReplaceWithFilters: filters) as? String) == "a b foo-bar foo-bar c"
55+
}
56+
57+
it("works with nested dictionaries") {
58+
let data = [
59+
"foo": [1, nil, "qfoob", ["baz": "foo"]],
60+
"bar": "foo"
61+
] as [String : Any]
62+
let input = SEGUtils.traverseJSON(data, andReplaceWithFilters: filters)
63+
let output = [
64+
"foo": [1, nil, "qfoo-barb", ["baz": "foo-bar"]],
65+
"bar": "foo-bar"
66+
] as [String : Any]
67+
68+
expect(equals(a: input!, b: output)) == true
69+
}
70+
71+
it("works with nested arrays") {
72+
let data = [
73+
[1, nil, "qfoob", ["baz": "foo"]],
74+
"foo"
75+
] as [Any]
76+
let input = SEGUtils.traverseJSON(data, andReplaceWithFilters: filters)
77+
let output = [
78+
[1, nil, "qfoo-barb", ["baz": "foo-bar"]],
79+
"foo-bar"
80+
] as [Any]
81+
82+
expect(equals(a: input!, b: output)) == true
83+
}
84+
})
3785
}
3886

3987
}

Podfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ target 'AnalyticsTests' do
66
use_frameworks!
77

88
pod 'Quick', '~> 1.2.0' # runner lib
9-
pod 'Nimble', '~> 7.0.2' # Matcher lib
9+
pod 'Nimble', '~> 7.3.1' # Matcher lib
1010
pod 'Nocilla', '~> 0.11.0' # HTTP Mocking Library
1111
pod 'SwiftTryCatch', :git => 'https://github.com/segmentio/SwiftTryCatch.git' # Utils lib
1212

Podfile.lock

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,36 @@
11
PODS:
2-
- Nimble (7.0.2)
2+
- Nimble (7.3.1)
33
- Nocilla (0.11.0)
44
- Quick (1.2.0)
5-
- SwiftTryCatch (0.0.1)
5+
- SwiftTryCatch (1.0.0)
66

77
DEPENDENCIES:
8-
- Nimble (~> 7.0.2)
8+
- Nimble (~> 7.3.1)
99
- Nocilla (~> 0.11.0)
1010
- Quick (~> 1.2.0)
1111
- SwiftTryCatch (from `https://github.com/segmentio/SwiftTryCatch.git`)
1212

13+
SPEC REPOS:
14+
https://github.com/cocoapods/specs.git:
15+
- Nimble
16+
- Nocilla
17+
- Quick
18+
1319
EXTERNAL SOURCES:
1420
SwiftTryCatch:
1521
:git: https://github.com/segmentio/SwiftTryCatch.git
1622

1723
CHECKOUT OPTIONS:
1824
SwiftTryCatch:
19-
:commit: dcdc954cb0945faaf33800b4b913e04d2ebdd965
25+
:commit: 2cdec294628f73350c5d8f6f05d08886af57668b
2026
:git: https://github.com/segmentio/SwiftTryCatch.git
2127

2228
SPEC CHECKSUMS:
23-
Nimble: bfe1f814edabba69ff145cb1283e04ed636a67f2
29+
Nimble: 04f732da099ea4d153122aec8c2a88fd0c7219ae
2430
Nocilla: 7af7a386071150cc8aa5da4da97d060f049dd61c
2531
Quick: 58d203b1c5e27fff7229c4c1ae445ad7069a7a08
26-
SwiftTryCatch: fb6d2b34abe48efd69578dac919293a44f95b481
32+
SwiftTryCatch: 2f4ef36cf5396bdb450006b70633dbce5260d3b3
2733

28-
PODFILE CHECKSUM: 25d553a80951f726d31098fb274eedfdc7fce4d9
34+
PODFILE CHECKSUM: 31008e8b9503ec3adb2fc152eb967391a0dd408d
2935

30-
COCOAPODS: 1.3.1
36+
COCOAPODS: 1.5.3

0 commit comments

Comments
 (0)