1 /**
2 * Copyright © DiamondMVC 2019
3 * License: MIT (https://github.com/DiamondMVC/Diamond/blob/master/LICENSE)
4 * Author: Jacob Jensen (bausshf)
5 */
6 module diamond.security.validation.sensitive;
7 
8 import std.regex : Regex, regex, matchAll;
9 import std.algorithm : filter, canFind;
10 import std.array : array;
11 
12 /// Collection of patterns used to create the sensitive data regex.
13 private static __gshared string[][SecurityLevel] _sensitiveDataPatterns;
14 
15 // Collection of names to look for in the input which might disclose sensitive data.
16 private static __gshared string[][SecurityLevel] _sensitiveDataNames;
17 
18 /// The generated regex from sensitive data patterns.
19 private static __gshared Regex!char[][SecurityLevel] _sensitiveDataRegexes;
20 
21 /// Regex for phone numbers.
22 private static const _phoneNumberRegex = `(\+?(?:(?:9[976]\d|8[987530]\d|6[987]\d|5[90]\d|42\d|3[875]\d|2[98654321]\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)|\((?:9[976]\d|8[987530]\d|6[987]\d|5[90]\d|42\d|3[875]\d|2[98654321]\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)\))[0-9. -]{4,14})(?:\b|x\d+)`;
23 
24 /// Regex for amex cards.
25 private static const _amexCardRegex = `^3[47][0-9]{13}$`;
26 
27 /// Regex for BCGlobal cards.
28 private static const _BCGlobalRegex = `^(6541|6556)[0-9]{12}$`;
29 
30 /// Regex for Carte Blanche cards.
31 private static const _carteBlancheCardRegex = `^389[0-9]{11}$`;
32 
33 /// Regex for Diners Club cards.
34 private static const _dinersClubCardRegex = `^3(?:0[0-5]|[68][0-9])[0-9]{11}$`;
35 
36 /// Regex for Discover cards.
37 private static const _discoverCardRegex = `^65[4-9][0-9]{13}|64[4-9][0-9]{13}|6011[0-9]{12}|(622(?:12[6-9]|1[3-9][0-9]|[2-8][0-9][0-9]|9[01][0-9]|92[0-5])[0-9]{10})$`;
38 
39 /// Regex for Insta Payment cards.
40 private static const _instaPaymentCardRegex = `^63[7-9][0-9]{13}$`;
41 
42 /// Regex for JCB cards.
43 private static const _JCBCardRegex = `^(?:2131|1800|35\d{3})\d{11}$`;
44 
45 /// Regex for Korean Local cards.
46 private static const _koreanLocalCardRegex = `^9[0-9]{15}$`;
47 
48 /// Regex for Maestro cards.
49 private static const _maestroCardRegex = `^(5018|5020|5038|6304|6759|6761|6763)[0-9]{8,15}$`;
50 
51 /// Regex for Mastercard cards.
52 private static const _mastercardRegex = `^5[1-5][0-9]{14}$`;
53 
54 /// Regex for Solo cards.
55 private static const _soloCardRegex = `^(6334|6767)[0-9]{12}|(6334|6767)[0-9]{14}|(6334|6767)[0-9]{15}$`;
56 
57 /// Regex for Union Pay cards.
58 private static const _unionPayCardRegex = `^(62[0-9]{14,17})$`;
59 
60 /// Regex for Visa cards.
61 private static const _visaCardRegex = `^4[0-9]{12}(?:[0-9]{3})?$`;
62 
63 /// Regex for Visa master cards.
64 private static const _visaMasterCardRegex = `^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14})$`;
65 
66 /// Regex for Mastercards after 2016.
67 private static const _masterCard2016Regex = `^5$|^5[1-5][0-9]{0,14}$|^2[2]?[2]?$|^2221[0-9]{0,12}$|^222[3-9][0-9]{0,12}$|^22[3-9][0-9]{0,13}$|^2[3-6][0-9]{0,14}$|^2[7]?$|^27[2]?$|^27[0-1][0-9]{0,12}$|^2720[0-9]{0,12}$`;
68 
69 /// Regex for Mastercard Bin cards.
70 private static const _masterCardBinRegex = `^(?:4[0-9]{12}(?:[0-9]{3})?|[25][1-7][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$`;
71 
72 /// Regex for generic credit/debit cards.
73 private static const _genericCardRegex = `^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$`;
74 
75 /// Regex for CPR numbers.
76 private static const _cprRegex = `^(?:(?:31(?:0[13578]|1[02])|(?:30|29)(?:0[13-9]|1[0-2])|(?:0[1-9]|1[0-9]|2[0-8])(?:0[1-9]|1[0-2]))[0-9]{2}\s?-?\s?[0-9]|290200\s?-?\s?[4-9]|2902(?:(?!00)[02468][048]|[13579][26])\s?-?\s?[0-3])[0-9]{3}|000000\s?-?\s?0000$`;
77 
78 /// Regex for Social Security numbers.
79 private static const _ssnRegex = `^(?!(000|666|9))\d{3}-(?!00)\d{2}-(?!0000)\d{4}$`;
80 
81 /// Regex for UK Insurance numbers.
82 private static const _ukInsuranceNumberRegex = `^\s*[a-zA-Z]{2}(?:\s*\d\s*){6}[a-zA-Z]?\s*$`;
83 
84 /// Enumeration of security levels.
85 enum SecurityLevel
86 {
87   /// Allows everything under medium, but also phone numbers, emails and addresses.
88   minimum,
89   /// Allows names, authentication (Not password), zip codes / postal codes, job / occupation, age, politics, race and ethnicity.
90   medium,
91   /// Allows no sensitive data.
92   maximum
93 }
94 
95 /// Initializing the sensitive data validator.
96 void initializeSensitiveDataValidator()
97 {
98   _sensitiveDataNames = [
99     SecurityLevel.minimum :
100     [
101       "password",
102       "cpr", "ssn", "social security", "socialsecurity", "social-security",
103       "social number", "social-number", "socialnumber",
104       "salary", "payment",
105       "money", "cash", "bank", "bank account", "bank-account",
106       "cc", "credit", "credit card", "credit-card", "creditcard",
107       "dc", "debit", "debit card", "debit-card", "debitcard",
108       "credit info", "creditinfo", "credit-info",
109       "debit info", "debitinfo", "debit-info",
110       "debt", "bill",
111       "insurance", "insurance number", "insurance-number"
112     ],
113     SecurityLevel.medium :
114     [
115       "password",
116       "cpr", "ssn", "social security", "socialsecurity", "social-security",
117       "social number", "social-number", "socialnumber",
118       "address",
119       "email", "e-mail", "mail", "phone", "telephone", "salary", "payment",
120       "money", "cash", "bank", "bank account", "bank-account",
121       "cc", "credit", "credit card", "credit-card", "creditcard",
122       "dc", "debit", "debit card", "debit-card", "debitcard",
123       "credit info", "creditinfo", "credit-info",
124       "debit info", "debitinfo", "debit-info",
125       "debt", "bill",
126       "insurance", "insurance number", "insurance-number"
127     ],
128     SecurityLevel.maximum :
129     [
130       "name", "account", "username", "password", "auth", "login",
131       "cpr", "ssn", "social security", "socialsecurity", "social-security",
132       "social number", "social-number", "socialnumber",
133       "age", "race", "politic", "address", "zip", "zipcode", "postal", "postalcode",
134       "job", "jobtitle", "occupation", "nick", "nickname",
135       "email", "e-mail", "mail", "phone", "telephone", "salary", "payment",
136       "money", "cash", "bank", "bank account", "bank-account",
137       "cc", "credit", "credit card", "credit-card", "creditcard",
138       "dc", "debit", "debit card", "debit-card", "debitcard",
139       "credit info", "creditinfo", "credit-info",
140       "debit info", "debitinfo", "debit-info",
141       "debt", "bill",
142       "ethnic", "ethnicity",
143       "insurance", "insurance number", "insurance-number"
144     ]
145   ];
146 
147   _sensitiveDataPatterns = [
148     SecurityLevel.minimum :
149     [
150       _amexCardRegex,
151       _BCGlobalRegex,
152       _carteBlancheCardRegex,
153       _dinersClubCardRegex,
154       _discoverCardRegex,
155       _instaPaymentCardRegex,
156       _JCBCardRegex,
157       _koreanLocalCardRegex,
158       _maestroCardRegex,
159       _mastercardRegex,
160       _soloCardRegex,
161       _unionPayCardRegex,
162       _visaCardRegex,
163       _visaMasterCardRegex,
164       _masterCard2016Regex,
165       _masterCardBinRegex,
166       _genericCardRegex,
167 
168       _cprRegex,
169       _ssnRegex,
170       _ukInsuranceNumberRegex
171     ],
172     SecurityLevel.medium :
173     [
174       _phoneNumberRegex,
175 
176       _amexCardRegex,
177       _BCGlobalRegex,
178       _carteBlancheCardRegex,
179       _dinersClubCardRegex,
180       _discoverCardRegex,
181       _instaPaymentCardRegex,
182       _JCBCardRegex,
183       _koreanLocalCardRegex,
184       _maestroCardRegex,
185       _mastercardRegex,
186       _soloCardRegex,
187       _unionPayCardRegex,
188       _visaCardRegex,
189       _visaMasterCardRegex,
190       _masterCard2016Regex,
191       _masterCardBinRegex,
192       _genericCardRegex,
193 
194        _cprRegex,
195       _ssnRegex,
196       _ukInsuranceNumberRegex
197     ],
198     SecurityLevel.maximum :
199     [
200       _phoneNumberRegex,
201 
202       _amexCardRegex,
203       _BCGlobalRegex,
204       _carteBlancheCardRegex,
205       _dinersClubCardRegex,
206       _discoverCardRegex,
207       _instaPaymentCardRegex,
208       _JCBCardRegex,
209       _koreanLocalCardRegex,
210       _maestroCardRegex,
211       _mastercardRegex,
212       _soloCardRegex,
213       _unionPayCardRegex,
214       _visaCardRegex,
215       _visaMasterCardRegex,
216       _masterCard2016Regex,
217       _masterCardBinRegex,
218       _genericCardRegex,
219 
220       _cprRegex,
221       _ssnRegex,
222       _ukInsuranceNumberRegex
223     ]
224   ];
225 
226   updateRegexPattern();
227 }
228 
229 /// Updates the regex pattern for sensitive data patterns.
230 private void updateRegexPattern()
231 {
232   foreach (level,patterns; _sensitiveDataPatterns)
233   {
234     foreach (pattern; patterns)
235     {
236       _sensitiveDataRegexes[level] ~= regex(pattern);
237     }
238   }
239 }
240 
241 /**
242 * Adds a sensitive data name.
243 * Params:
244 *   name =  The name to add.
245 *   level = The security level to add the name to.
246 */
247 void addSensitiveDataName(string name, SecurityLevel level)
248 {
249   _sensitiveDataNames[level] ~= name;
250 }
251 
252 /**
253 * Adds a sensitive data pattern.
254 * Params:
255 *   pattern =     The pattern to add.
256 *   level =       The security level to add the pattern to.
257 *   updateRegex = Boolean determining whether the validation regex should be updated. False by default to allow bulk-adds.
258 */
259 void addSensitiveDataPattern(string pattern, SecurityLevel level, bool updateRegex = false)
260 {
261   _sensitiveDataPatterns[level] ~= pattern;
262 
263   if (updateRegex)
264   {
265     updateRegexPattern();
266   }
267 }
268 
269 /**
270 * Removes a sensitive data name.
271 * Params:
272 *   name = The name to remove.
273 *   level =   The security level to remove the name from.
274 */
275 void removeSensitiveDataName(string name, SecurityLevel level)
276 {
277   _sensitiveDataNames[level] = _sensitiveDataNames[level].filter!(n => n != name).array;
278 }
279 
280 /**
281 * Removes a sensitive data pattern.
282 * Params:
283 *   pattern = The pattern to remove.
284 *   level =   The security level to remove the pattern from.
285 */
286 void removeSensitiveDataPattern(string pattern, SecurityLevel level)
287 {
288   _sensitiveDataPatterns[level] = _sensitiveDataPatterns[level].filter!(p => p != pattern).array;
289 
290   updateRegexPattern();
291 }
292 
293 /// Clears all sensitive data names.
294 void clearSensitiveDataNames()
295 {
296   _sensitiveDataNames.clear();
297 }
298 
299 /// Clears all sensitive data patterns.
300 void clearSensitiveDataPatterns()
301 {
302   _sensitiveDataPatterns.clear();
303 }
304 
305 /**
306 * Checks whether a specific string contains sensitive data.
307 * Params:
308 *   data =  The data to check.
309 *   level = The security level for the validation
310 * Returns:
311 *   True if the string contains sensitive data, false otherwise.
312 */
313 bool hasSensitiveData(string data, SecurityLevel level)
314 {
315   if (!data || !data.length)
316   {
317     return false;
318   }
319 
320   import diamond.core..string : splitIntoGroupedWords;
321 
322   auto words = splitIntoGroupedWords(data);
323 
324   foreach (word; words)
325   {
326     if (_sensitiveDataNames && level in _sensitiveDataNames && _sensitiveDataNames[level].length)
327     {
328       foreach (name; _sensitiveDataNames[level])
329       {
330         if (word.canFind(name))
331         {
332           return true;
333         }
334       }
335     }
336 
337     if (_sensitiveDataPatterns && level in _sensitiveDataPatterns && level in _sensitiveDataRegexes && _sensitiveDataPatterns[level].length)
338     {
339       auto regexes = _sensitiveDataRegexes.get(level, null);
340 
341       if (regexes && regexes.length)
342       {
343         foreach (regex; regexes)
344         {
345           auto regexResult = word.matchAll(regex);
346 
347           if (!regexResult.empty)
348           {
349             return true;
350           }
351         }
352       }
353     }
354   }
355 
356   return false;
357 }
358 
359 /**
360 * Checks whether a specific string contains sensitive data.
361 * Params:
362 *   data = The data to check.
363 *   level = The security level for the validation
364 * Throws:
365 *   SensitiveDataException when the string contains sensitive data.
366 */
367 void validateSensitiveData(string data, SecurityLevel level)
368 {
369   if (hasSensitiveData(data, level))
370   {
371     import diamond.errors.exceptions : SensitiveDataException;
372     
373 	  throw new SensitiveDataException("The input contains sensitive data. Try to change security policies or exclude the sensitive data from the input.");
374   }
375 }