-
Notifications
You must be signed in to change notification settings - Fork 269
Expand file tree
/
Copy pathscrub.ts
More file actions
196 lines (183 loc) · 7.77 KB
/
scrub.ts
File metadata and controls
196 lines (183 loc) · 7.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
import { Privacy } from "@clarity-types/core";
import * as Data from "@clarity-types/data";
import * as Layout from "@clarity-types/layout";
import config from "@src/core/config";
const catchallRegex = /\S/gi;
let unicodeRegex = true;
let digitRegex = null;
let letterRegex = null;
let currencyRegex = null;
export function text(value: string, hint: string, privacy: Privacy, mangle: boolean = false, type?: string): string {
if (value) {
if (hint == "input" && (type === "checkbox" || type === "radio")) {
return value;
}
switch (privacy) {
case Privacy.None:
return value;
case Privacy.Sensitive:
switch (hint) {
case Layout.Constant.TextTag:
case "value":
case "placeholder":
case "click":
return redact(value);
case "input":
case "change":
return mangleToken(value);
}
return value;
case Privacy.Text:
case Privacy.TextImage:
switch (hint) {
case Layout.Constant.TextTag:
case Layout.Constant.DataAttribute:
return mangle ? mangleText(value) : mask(value);
case "src":
case "srcset":
case "title":
case "alt":
case "href":
case "xlink:href":
if (privacy === Privacy.TextImage) {
return value?.startsWith('blob:') ? 'blob:' : Data.Constant.Empty;
}
return value;
case "value":
case "click":
case "input":
case "change":
return mangleToken(value);
case "placeholder":
return mask(value);
}
break;
case Privacy.Exclude:
switch (hint) {
case Layout.Constant.TextTag:
case Layout.Constant.DataAttribute:
return mangle ? mangleText(value) : mask(value);
case "value":
case "input":
case "click":
case "change":
return Array(Data.Setting.WordLength).join(Data.Constant.Mask);
case "checksum":
return Data.Constant.Empty;
}
break;
case Privacy.Snapshot:
switch (hint) {
case Layout.Constant.TextTag:
case Layout.Constant.DataAttribute:
return scrub(value, Data.Constant.Letter, Data.Constant.Digit);
case "value":
case "input":
case "click":
case "change":
return Array(Data.Setting.WordLength).join(Data.Constant.Mask);
case "checksum":
case "src":
case "srcset":
case "alt":
case "title":
return Data.Constant.Empty;
}
break;
}
}
return value;
}
export function url(input: string, electron: boolean = false): string {
let result = input;
// Replace the URL for Electron apps so we don't send back file:/// URL
if (electron) {
result = `${Data.Constant.HTTPS}${Data.Constant.Electron}`;
} else {
let drop = config.drop;
if (drop && drop.length > 0 && input && input.indexOf("?") > 0) {
let [path, query] = input.split("?");
let swap = Data.Constant.Dropped;
result = path + "?" + query.split("&").map(p => drop.some(x => p.indexOf(`${x}=`) === 0) ? `${p.split("=")[0]}=${swap}` : p).join("&");
}
}
return result;
}
function mangleText(value: string): string {
let trimmed = value.trim();
if (trimmed.length > 0) {
let first = trimmed[0];
let index = value.indexOf(first);
let prefix = value.substr(0, index);
let suffix = value.substr(index + trimmed.length);
return `${prefix}${trimmed.length.toString(36)}${suffix}`;
}
return value;
}
function mask(value: string): string {
return value.replace(catchallRegex, Data.Constant.Mask);
}
export function scrub(value: string, letter: string, digit: string): string {
regex(); // Initialize regular expressions
return value ? value.replace(letterRegex, letter).replace(digitRegex, digit) : value;
}
function mangleToken(value: string): string {
let length = ((Math.floor(value.length / Data.Setting.WordLength) + 1) * Data.Setting.WordLength);
let output: string = Layout.Constant.Empty;
for (let i = 0; i < length; i++) {
output += i > 0 && i % Data.Setting.WordLength === 0 ? Data.Constant.Space : Data.Constant.Mask;
}
return output;
}
function regex(): void {
// Initialize unicode regex, if supported by the browser
// Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Unicode_Property_Escapes
if (unicodeRegex && digitRegex === null) {
try {
digitRegex = new RegExp("\\p{N}", "gu");
letterRegex = new RegExp("\\p{L}", "gu");
currencyRegex = new RegExp("\\p{Sc}", "gu");
} catch { unicodeRegex = false; }
}
}
function redact(value: string): string {
let spaceIndex = -1;
let gap = 0;
let hasDigit = false;
let hasEmail = false;
let hasWhitespace = false;
let array = null;
regex(); // Initialize regular expressions
for (let i = 0; i < value.length; i++) {
let c = value.charCodeAt(i);
hasDigit = hasDigit || (c >= Data.Character.Zero && c <= Data.Character.Nine); // Check for digits in the current word
hasEmail = hasEmail || c === Data.Character.At; // Check for @ sign anywhere within the current word
hasWhitespace = c === Data.Character.Tab || c === Data.Character.NewLine || c === Data.Character.Return || c === Data.Character.Blank;
// Process each word as an individual token to redact any sensitive information
if (i === 0 || i === value.length - 1 || hasWhitespace) {
// Performance optimization: Lazy load string -> array conversion only when required
if (hasDigit || hasEmail) {
if (array === null) { array = value.split(Data.Constant.Empty); }
// Work on a token at a time so we don't have to apply regex to a larger string
let token = value.substring(spaceIndex + 1, hasWhitespace ? i : i + 1);
// Check if unicode regex is supported, otherwise fallback to calling mask function on this token
if (unicodeRegex && currencyRegex !== null) {
// Do not redact information if the token contains a currency symbol
token = token.match(currencyRegex) ? token : scrub(token, Data.Constant.Letter, Data.Constant.Digit);
} else {
token = mask(token);
}
// Merge token back into array at the right place
array.splice(spaceIndex + 1 - gap, token.length, token);
gap += token.length - 1;
}
// Reset digit and email flags after every word boundary, except the beginning of string
if (hasWhitespace) {
hasDigit = false;
hasEmail = false;
spaceIndex = i;
}
}
}
return array ? array.join(Data.Constant.Empty) : value;
}