Masking sensitive data in ServiceNow fields

akshaybhardwaj
Mega Guru

Hi,
I want to mask the sensitive data added by the customer on the incident form through record producer. We already have a script, which is failing in some scenario. Any help on this will be appreciated.

We have a BR which written on question_answer table with On Insert, Before condition

(function executeRule(current, previous /*null when async*/ ) {

var siu = new SensitiveInformationUtil();
var dict = {};

// gather values to be sanitized
var fields = siu.getScopeForCore()['question_answer'];
fields.forEach(function(fieldName) {
dict[fieldName] = current[fieldName].toString();
});

// sanitize
var validationResults = siu.validateFieldsForCore(dict);

// remove data
fields.forEach(function(fieldName) {
current[fieldName] = siu.getAnonymizedValue(validationResults, fieldName, dict[fieldName]);
});

})(current, previous);


This BR calls the script include function

var SensitiveInformationUtil = Class.create();
SensitiveInformationUtil.prototype = Object.extendsObject(AbstractAjaxProcessor, {

getScope: function() {
return gs.getProperty('sensitive_info.scope');
},

getScopeForCore: function() {
return JSON.parse(this.getScope());
},

checkTasks: function() {
var scope = this.getScopeForCore();

for (var table in scope) {
var gr = new GlideRecord(table);
gr.addActiveQuery();
gr.query();
while (gr.next()) {
var fields = {};
scope[table].forEach(function(field) {
fields[field] = gr[field] + '';
});

var results = this.validateFieldsForCore(fields);

if (results.length > 0) {
results.forEach(function(result) {
var tagId = null;
switch (result[0]) {
case 'Bank account number':
tagId = '44e47a71db736850033e95e8f49619d6';
break;
case 'Bank card number':
tagId = 'ed5a8686dbb3e850033e95e8f4961978';
break;
case 'Company ID':
tagId = 'bde47a71db736850033e95e8f49619d8';
break;
case 'SSN':
tagId = '71843ab1db736850033e95e8f496199d';
break;
}

// should be tagged
if (tagId) {
var grTag = new GlideRecord('label_entry');
grTag.addQuery('table', gr.sys_class_name);
grTag.addQuery('table_key', gr.sys_id);
grTag.addQuery('label', tagId);
grTag.setLimit(1);
grTag.query();
// not tagged yet -> tag
if (!grTag.hasNext()) {
grTag.table = table;
grTag.table_key = gr.sys_id;
grTag.label = tagId;
grTag.title = grTag.label.getDisplayValue();
grTag.insert();
}
}
});
}
}
}
},

validateFields: function(fields) {
fields = fields || this.getParameter('sysparm_fields');
fields = JSON.parse(fields);
return JSON.stringify(this.validateFieldsForCore(fields), null, 2);
},

validateFieldsForCore: function(fields) {
var errors = [];

// parse property
var rules = gs.getProperty('sensitive_info.rules');
var rulesObj = JSON.parse(rules);
var regexes = {};
for (var key in rulesObj) {
regexes[key] = [];
rulesObj[key].toString().split(',').forEach(function(regexp) {
var regexpObj = new RegExp(regexp);
regexes[key].push(regexpObj);
});
}

// check all affected fields
for (var fieldName in fields) {
//gs.info('dt - ' + fieldName + ' / ' + fields[fieldName]);
var fieldValue = fields[fieldName];
if (fieldValue) {
var newlineSplit = fieldValue.split('\n');
var wordSplit = fieldValue.split(/[,.()\\s]/g);

if (newlineSplit.length > 0) // masking for multiple new lines
{
newlineSplit.forEach(function(field) {
gs.log("SplittingLines" + field);
for (var ruleName in regexes) {
regexes[ruleName].forEach(function(regex) {
var result = regex.exec(field);
if (result) {
errors.push([ruleName, fieldName, result[0]]);
}
});
}
});
}
if (wordSplit.length > 0) // masking for multiple words
{
wordSplit.forEach(function(field) {
gs.log("SplittingWords" + field);
for (var ruleName in regexes) {
regexes[ruleName].forEach(function(regex) {
var result = regex.exec(field);
if (result) {
errors.push([ruleName, fieldName, result[0]]);
}
});
}
});
} else {

for (var ruleName in regexes) {
regexes[ruleName].forEach(function(regex) {
var result = regex.exec(fieldValue);
if (result) {
errors.push([ruleName, fieldName, result[0]]);
}
});
}
}
}
}

return errors;
},

getAnonymizedValue: function(validationResults, fieldName, value) {
validationResults.forEach(function(validationResult) {
if (validationResult[1] == fieldName) {
var orig = validationResult[2].trim();
if (validationResult[0] == "Bank card number" || validationResult[0] == "Bank account number") {
var lastTwo = orig.substring(orig.length - 2);
gs.log('LastlyTwo' + lastTwo);
gs.log('LengthOriginal' + orig.length - 2);
var mask = orig.substring(0, orig.length - 2).replace(/\w/g, "X");
var replaceTo = mask + lastTwo;
value = value.replaceAll(orig, replaceTo);

} else if (validationResult[0] == "Company ID") {
var splitStr = orig.split('-');
var splitStrLastTwo = splitStr[0].substring(splitStr[0].length - 2);
var splitMask1 = splitStr[0].substring(0, splitStr[0].length - 2).replace(/\w/g, "X");
var splitMask2 = splitStr[1].replace(/\w/g, "X");
var replaceStr = splitMask1 + splitStrLastTwo + '-' + splitMask2;
value = value.replaceAll(orig, replaceStr);

} else if (validationResult[0] == "SSN") {
var splitStrSSN = orig.substring(0, 6);
var splitStrLastTwoSSN = splitStrSSN.substring(splitStrSSN.length - 2);
var splitMask1SSN = (splitStrSSN.substring(0, splitStrSSN.length - 2)).replace(/\w/g, "X");
var splitMask2SSN = orig.substring(7).replace(/\w/g, "X");
var replaceStrSSN = splitMask1SSN + splitStrLastTwoSSN + 'X' + splitMask2SSN;
value = value.replaceAll(orig, replaceStrSSN);


}
// var replaceTo = '[REMOVED: ' + validationResult[0] + ']';

}

});
return value;

},

type: 'SensitiveInformationUtil'
});


And to support the script there are two sys_properties
1. With the regex for the sensitive data that needs to be masked
Name:sensitive_info.rules
Value:{
"Bank account number" : [
"(\\s|:|;|^)[0-9]{8}(-)[0-9]{8}(\\s|\\.|$)",
"(\\s|:|;|^)[a-zA-Z]{2}[0-9]{2} [0-9]{4} [0-9]{4} [0-9]{4} [0-9]{2}(\\s|\\.|$)" ,
"(\\s|:|;|^)[a-zA-Z]{2}[0-9]{16}(\\s|\\.|$)" ],
"SSN": ["(\\s|:|;|^)(0[1-9]|[12]\\d|3[01])(0[1-9]|1[0-2])(\\d\\d-|\\d\\d[aA]|\\d\\d[aA])[0-9]{3}[0-9aAbBcCdDeEfFhHjJkKlLmMnNpPrRsStTuUvVwWxXyY]{1}(\\s|\\.|$)"],
"Bank card number": ["(\\s|:|;|^)[0-9]{4} [0-9]{4} [0-9]{4} [0-9]{4}(\\s|\\.|$)"],
"Company ID": ["(\\s|:|;|^)[0-9]{7}-[0-9]{1}(\\s|\\.|$)"]
}

2. Table and field name which needs to be masked
Name:sensitive_info.scope

Value:{
"question_answer": ["value"]
}
The above script is working if I add bank account number without special character in the end
ex. fewf wef we fwe fwe FI42 5555 1510 0000 11 dwqqwdqwd aadsa NO42 5000 1510 0000 99
fewf wef we fwe fwe NO42 5000 1510 0000 23 dwqqwdqwd aadsa NL42 5000 1510 0000 56

Outcome: fewf wef we fwe fwe XXXX XXXX XXXX XXXX 11 dwqqwdqwd aadsa XXXX XXXX XXXX XXXX 99
fewf wef we fwe fwe XXXX XXXX XXXX XXXX 23 dwqqwdqwd aadsa XXXX XXXX XXXX XXXX 56
But when it is used with a special character. It is not considered as a Bank account and is not getting masked.

ex.(Random digits)fewf wef we fwe fwe FI42 5555 1510 0000 11 dwqqwdqwd aadsa NO42 5000 1510 0000 99
fewf wef we fwe fwe NO42 5000 1510 0000 23 dwqqwdqwd aadsa NL42 5000 1510 0000 56
fewf wef we fwe fwe NO42 5000 1510 0000 66, dwqqwdqwd aadsa DN42 5000 1510 0000 77.

Outcome:(Also at the end of the string all the digits are masked except one which is different from the other masked data )
fewf wef we fwe fwe XXXX XXXX XXXX XXXX 11 dwqqwdqwd aadsa XXXX XXXX XXXX XXXX 99
fewf wef we fwe fwe XXXX XXXX XXXX XXXX 23 dwqqwdqwd aadsa XXXX XXXX XXXX XXXX 56
fewf wef we fwe fwe NO42 5000 1510 0000 66, dwqqwdqwd aadsa XXXX XXXX XXXX XXXX X7.





3 REPLIES 3

Tom Sienkiewicz
Mega Sage

It looks like you either have to expand your regex to allow for checking of preceeding and trailing characters (positive lookbehind/lookahead), or basically try to trim the text passed into the regex for checking, e.g. anything before a specical character, anything after a special character etc.

 

check online for suitable regexes perhaps, I would imagine you're not the first with this kind of problem. I would definitely test out your regexes against the different source texts first in a tool like regex101.com etc.

Fiz1
Giga Contributor

Another option is to use this tool from the store:

https://sysintegra.com.au/data-mask/

@akshaybhardwaj 

 

hakdi
Kilo Contributor

Can we mask the data and make a partial search on the value existed before masking for usage of partial name search in masked data?