-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
163 lines (135 loc) · 6.99 KB
/
index.js
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
"use strict";
const _ = require("lodash");
const crypto = require("crypto");
const personalDataProperties = [
"account",
"accountid",
"authorization",
"email",
"ip",
"ipaddress",
"pass",
"password",
"pwd",
"user",
"useremail",
"userid",
"username",
];
// Source: https://stackoverflow.com/a/46181/4922411
const emailRegExpTemplate = `(([^/<>()\\[\\]\\\\.,;:\\s@"]+(\\.[^<>()\\[\\]\\\\.,;:\\s@"]+)*)|(".+"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))`;
// https://tools.ietf.org/html/rfc7519
const jwtRegExpTemplate = "^[A-Za-z0-9-_=]+\\.[A-Za-z0-9-_=]+\\.[A-Za-z0-9-_+/=]*$";
// Source: https://stackoverflow.com/a/11040993/4922411
const guidRegExpTemplate = `[{(]?[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}[)}]?`;
// Source: https://stackoverflow.com/a/34529037/4922411
const ipV4RegExpTemplate = "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)";
// Source: https://stackoverflow.com/a/9221063/4922411
const ipV6RegExpTemplate = "((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?";
const defaultRegularExpressions = [emailRegExpTemplate, guidRegExpTemplate, ipV4RegExpTemplate, ipV6RegExpTemplate, jwtRegExpTemplate];
class PersonalDataFilter {
constructor(config) {
config = config || {};
this._setRegularExpressions(config);
this._setMask(config);
this._setPersonalDataProperties(config);
this._setMatchReplacer(config);
}
filter(data) {
const result = this._filterRecursively(data, new Set());
return result;
}
_filterRecursively(data, referencesCache) {
if (_.isString(data)) {
return this._filterString(data);
} else if (_.isArray(data)) {
return _.map(data, v => this._filterRecursively(v, referencesCache));
} else if (_.isObject(data)) { // isObject check should always be after the isArray check because isObject returns true for [].
// if the current reference has already been traversed this means we've reaced a circular reference
// simply mask it in order to avoid maximum callstack due to endless traversal
if (referencesCache.has(data)) {
return this._mask;
} else {
referencesCache.add(data)
return this._maskPersonalDataProperties(data, referencesCache);
}
}
return data;
}
_maskPersonalDataProperties(data, referencesCache) {
// It's important to use getOwnPropertyNames here
// because if someone were to pass something with non-enumerable properties (e.g. an instance of the Error class)
// then a simple _.reduce would simply skip over the non-enumerable properties ultimately removing them from the result
const propertyNames = Object.getOwnPropertyNames(data);
return _.reduce(propertyNames, (result, key) => {
let value;
try {
value = data[key];
} catch (err) {
// We can't get the value and we should not include it in the result because we can't filter it.
return result;
}
if (this._personalDataProperties.indexOf(key.toString().toLowerCase()) >= 0) {
result[key] = this._mask;
} else {
result[key] = this._filterRecursively(value, referencesCache);
}
return result;
}, {});
}
_filterString(input) {
if (this._matchReplacer) {
return input.replace(this._filterRegExp, this._matchReplacer);
}
return input.replace(this._filterRegExp, this._mask)
}
_setRegularExpressions(config) {
const additionalRegularExpressionsSize = _.size(config.additionalRegularExpressions);
if (additionalRegularExpressionsSize > 0 && config.regularExpression) {
throw new Error("You can't use additionalRegularExpressions and regularExpression at the same time.");
}
if (config.regularExpression) {
this._filterRegExp = config.regularExpression;
return;
}
let regExps = _.concat([], defaultRegularExpressions);
if (additionalRegularExpressionsSize > 0) {
regExps = _.concat(regExps, config.additionalRegularExpressions);
}
const regExpString = regExps.map(r => `(${r})`).join("|");
this._filterRegExp = new RegExp(regExpString, "gi");
}
_setMask(config) {
this._mask = config.personalDataMask || "";
}
_setPersonalDataProperties(config) {
const additionalPropertiesSize = _.size(config.additionalPersonalDataProperties);
const propertiesSize = _.size(config.personalDataProperties);
if (additionalPropertiesSize > 0 && propertiesSize > 0) {
throw new Error("You can't use additionalPersonalDataProperties and personalDataProperties at the same time.");
}
if (propertiesSize > 0) {
this._personalDataProperties = config.personalDataProperties;
return;
}
let props = _.concat([], personalDataProperties);
if (additionalPropertiesSize > 0) {
props = _.concat(props, config.additionalPersonalDataProperties);
}
this._personalDataProperties = props;
}
_setMatchReplacer(config) {
if (config.useDefaultMatchReplacer) {
if (config.matchReplacer) {
throw new Error("You can't use the default match replacer and a cutom one.");
}
this._matchReplacer = (match) => {
return crypto.createHash("sha256").update(match).digest("hex");
};
}
if (config.matchReplacer) {
this._matchReplacer = config.matchReplacer;
}
}
}
module.exports.newFilter = (cfg) => new PersonalDataFilter(cfg);