JournalRedactor

JournalRedactor can be used to redact or delete journal entries that may contain sensitive or private information, or customer PII. JournalRedactor can be instantiated using the new keyword once the Script Include has been installed; as in var redactor = new JournalRedactor(). If you're curious, you can find the code in this script (as of v1.3) in this Gist

Full article with detailed explanation here!

Note: Many organizations have legal requirements for data retention, and redaction (or especially deletion) may be the sort of thing that requires approval from your legal department. It is strongly recommended that you consult your legal department for your use-case before implementing this API in your organization, and possibly even limit the usage of this API to specific roles, depending on your company's legal obligations. 

The parameters listed below, are the parameters that should be passed in when instantiating an object of this class. See example usage for more info.

The XML download link can be found at the bottom of this page. 

Parameters

 Name Type Required Description
journalEntryID String No

The sys_id of a specific record in the sys_journal_field table corresponding to the journal entry you'd like to redact or delete. You can also specify this via the .setJournalID() method. 
If you don't know the sys_id of the journal entry's sys_journal_field record, you can find it by calling the .findJournalID() method.

Example

//Method 1:
var journalID = '8521de9b3710230090b68cf6c3990ebc'; //sys_id of a record in the sys_journal_field record
var redactor = new JournalRedactor(journalID);
//...

//Method 2:
var redactor = new JournalRedactor(); 
//...
/*
    Instantiating without a journal record ID means you'll need to call 
    .setJournalID() or .findJournalID() before you can redact.
*/

redact()

The redact() method can be called with either one or two arguments. If only one is specified (the deleteEntry parameter), it must be set to true. If two arguments are specified, the first should be set to false. The deleteEntry parameter exists largely to prevent unintentional deletions. 

Note: Before this method can be called, you must have either (1) specified a sys_journal_field record sys_id in the constructor, or by using the .setJournalID() method, or (2) found the journal record by using the .findJournalID() method.

Parameters

 Name Type Required Description
deleteEntry Boolean Yes

True/false depending on whether or not the journal entry should be deleted as opposed to updated to something else.
If set to true, the second parameter (newValue) is not required, and the journal entry will be deleted completely. If set to false, the second parameter (newValueis required, as the redaction cannot complete without having something to replace the redacted journal text with. 

newValue String No

This parameter is only required if deleteEntry is set to false, in which case this string will replace the entire journal entry. For example, if the current journal entry contains the text "My name is Tim", you might specify "My name is [redacted]" for the newValue parameter.

Output

This method returns a boolean value, indicating whether the redaction was successful. If false is returned, an error or warning should have been logged to the system logs. You can see more info about what might have caused the issue by using the Script Debugger in ServiceNow, or by calling the .setVerbose() method, and passing in true

Example

//Method 1: Simply delete the journal entry
var journalID = '8521de9b3710230090b68cf6c3990ebc'; //sys_id of a record in the sys_journal_field record
var redactor = new JournalRedactor(journalID);
redactor.redact(true);

//Method 2: Replace the journal entry with different text
var journalID = '8521de9b3710230090b68cf6c3990ebc'; //sys_id of a record in the sys_journal_field record
var redactor = new JournalRedactor(journalID);
redactor.redact(false, 'The customer\'s social security number is [REDACTED].');

setJournalID()

This method allows you to explicitly set the journal entry you'd like to redact by specifying the sys_id of that record in the sys_journal_field table. You can also specify this in the constructor, as you can see in the first example on this page. If you don't know the journal entry sys_id, you can use the .findJournalID() method described below.

Parameters

 Name Type Required Description
journalEntryID String Yes

The sys_id of the record in the sys_journal_field table, corresponding to the journal entry you'd like to redact. 

Example

var redactor = new JournalRedactor();
redactor.setJournalID('8521de9b3710230090b68cf6c3990ebc');
redactor.redact(false, 'The customer\'s social security number is [REDACTED].');

findJournalID()

Use this method if you don't already know the sys_id of the sys_journal_field record corresponding to the journal entry you'd like to redact or delete. This will grab all the necessary data

This method accepts twothree, or four arguments (see the table below for a description of each argument).

Parameters

 Name Type Required Description
recordID String Yes

The sys_id of the record on which the journal field resides (not the sys_id of the journal entry itself). For example, if there is a comment on INC0012345 that I want to redact, I would specify the sys_id of the INC0012345 record.

journalText String Yes The exact and complete text of the journal entry you'd like to redact. That is, the current text; not the text you'd like to replace it with.
targetTableName String No Optional: The table that the record on which the journal field resides is in. For example: 'incident' or 'sc_req_item'.
journalFieldName String No The name of the journal field into which the entry to be redacted was entered. For example: 'work_notes', or 'comments'

Output

This method returns a boolean, indicating whether the journal entry was successfully found or not. 

Example

var targetRecordID = '1c741bd70b2322007518478d83673af3'; //sys_id of the record on which the journal entry resides
var oldJournalText = 'customer social is 123-45-6789'; //The EXACT, COMPLETE text of the journal entry to be redacted.
var targetRecordTableName = 'incident';
var targetJournalFieldName = 'work_notes';
var redactor = new JournalRedactor();

//Method 1: 2 parameters
redactor.findJournalID(targetRecordID, oldJournalText);
//Method 2: 3 parameters
redactor.findJournalID(targetRecordID, oldJournalText, targetRecordTableName);
//Method 3: 4 parameters
redactor.findJournalID(targetRecordID, oldJournalText, targetRecordTableName, targetJournalFieldName);
//(You only need to use one of the three methods above)

redactor.redact(false, 'The customer\'s social security number is [REDACTED].');

Note: Since this method returns a boolean, you might also want to put a condition block in there to make sure it's successful before attempting the redaction (which would fail):

var targetRecordID = '1c741bd70b2322007518478d83673af3'; 
var oldJournalText = 'customer social is 123-45-6789'; 
var redactor = new JournalRedactor();

if (redactor.findJournalID(targetRecordID, oldJournalText)) {
    redactor.redact(true);
} else {
    //Failed to locate journal ID
}

setVerbose()

This method simply enables additional logging if true is passed in, or disables that logging if false is provided. 

Parameters

 Name Type Required Description
b Boolean Yes

Sets the _verbose property to true, meaning that additional logging is enabled for this instantiation of JournalRedactor

Example

var targetRecordID = '1c741bd70b2322007518478d83673af3';
var oldJournalText = 'customer social is 123-45-6789';
var redactor = new JournalRedactor();
redactor.setVerbose(true); //enable additional logging

if (redactor.findJournalID(targetRecordID, oldJournalText)) {
    redactor.redact(true);
} else {
    //Failed to locate journal ID
}

qEipJ6k[1].png

To deploy JournalRedactor in your ServiceNow instance, follow these steps:

  1. Download the XML file from the link above

  2. Navigate to your Script Includes table (sys_script_include), right-click in the table header, and click Import XML.

  3. On the next page, click Choose File, and select the XML file you've just downloaded.

  4. Finally, click Upload.


You can now use JournalRedactor from any server-side script or scope. 

If you prefer to simply copy-and-paste the code into a new Script Include in your environment, you can do so from the Code section of this page.


Changelog

  • v1.3

    • Fixed an occasional bug where the redactor would throw an error “journalEntryData not ready for redaction…” when the journal entry should be deleted instead of altered.

  • V1.2

    • Added support for redacting in one quick go without needing a “new value” to replace the journal entry with.

    • Started work on upcoming feature: Will soon be able to redact emails and other sys_history_line sort of stuff. Watch this space.

  • v1.1

    • Added loads more JSDoc and info on all public methods, and made logging more effective when verbose mode is enabled.

  • v1.0

    • Initial release

Code

Below, you can see the exact code in the Gist, which you can copy and paste into your environment if you prefer that over importing the XML for the Script Include using the above instructions.

/*
Script Include definition:
Name: JournalRedactor
Client Callable: false
Accessible from: All application scopes
Additional details:
Version: 1.3
Usage documentation: http://redactor.snc.guru/
License: https://gist.github.com/thisnameissoclever/767b8a738b929a0bd943965431061c1e
*/
var JournalRedactor = Class.create();
JournalRedactor.prototype = {
initialize: function(journalEntryID) {
this.journalEntryData = new this._JournalEntry();
if (journalEntryID) {
this.journalEntryData.journal_entry_id = journalEntryID;
this._populateJournalEntryData(journalEntryID);
}
this._verbose = false;
},
/**
* Redact or delete a given journal entry. This will redact or delete the sys_journal_field
* entry, the sys_audit record, and the sys_history_set (along with all sys_history_lines
* associated with it).
* @param deleteEntry {boolean} True if the journal entry should be deleted rather than
* replaced with some other text, false if it should be replaced. If this param is set to
* false, then the second param (newValue) must be specified.
* @param newValue {string=} The new value to replace the old journal text with. If deleteEntry
* is set to false, this param is mandatory. Otherwise, it is not necessary.
* @returns {boolean} True if redaction was successful; false if not. If redaction was
* unsuccessful, an error should be found in the system logs from the source "redact method
* of JournalRedactor Script Include".
*/
redact: function(deleteEntry, newValue) {
if (!this._readyForRedaction(this.journalEntryData)) {
gs.logError('Not ready for redaction. Some data is missing from journalEntryData: ' +
JSON.stringify(this.journalEntryData),
'redact method of JournalRedactor Script Include');
return false;
}
if (!deleteEntry && !newValue) {
//If deleteEntry is not true, but no new value is specified, log an error
gs.logError('deleteEntry is not true, but newValue is not specified. Cannot ' +
'continue.', 'redact method of JournalRedactor Script Include');
return false;
} else if (!this.journalEntryData.journal_entry_id) {
gs.logError('Journal entry ID not known. Please call setJournalID() or ' +
'findJournalID() methods to set or find the journal ID before attempting ' +
'to redact.', 'redact method of JournalRedactor Script Include');
return false;
}
if (newValue) {
this.journalEntryData.new_journal_value = newValue;
}
var success = (this._redactJournalEntry(this.journalEntryData, deleteEntry, newValue) &&
this._redactAudit(this.journalEntryData, deleteEntry, newValue) &&
this._deleteHistory(this.journalEntryData));
return success;
},
/**
* This method allows you to explicitly set the journal entry you'd like to redact by
* specifying the sys_id of that record in the sys_journal_field table.
* You can also specify this in the constructor. If you don't know the journal entry
* sys_id, you can use the .findJournalID() method.
* @param journalEntryID {string} The sys_id of the record in the sys_journal_field
* table, corresponding to the journal entry you'd like to redact.
* @returns {boolean} true if successful, false if not.
*/
setJournalID: function(journalEntryID) {
if (!journalEntryID) {
gs.logWarning('setJournalID method of JournalRedactor Script Include called, ' +
'but no journal entry sys_id was specified. Clearing journal entry ID value.',
'JournalRedactor Script Include setJournalID method');
this.journalEntryData.journal_entry_id = '';
return false;
} else {
this.journalEntryData.journal_entry_id = journalEntryID;
}
return this._populateJournalEntryData(journalEntryID); //Need this data in the data object for future redaction steps.
},
/**
* Use this method if you don't already know the sys_id of the sys_journal_field
* record corresponding to the journal entry you'd like to redact or delete.
* This will grab all the necessary data and populate it into the journalEntryData
* object.
* This method accepts two, three, or four arguments.
* @param recordID {String}
* @param journalText {String}
* @param targetTableName {String=}
* @param journalFieldName {String=}
* @returns {Boolean} True if the journal entry was successfully located, or false if it was not.
*/
findJournalID: function(recordID, journalText, targetTableName, journalFieldName) {
var journalEntryID;
if (this._verbose) {
gs.log('Attempting to find record in sys_journal_field table, with ' +
arguments.length + ' arguments: ' + JSON.stringify(arguments));
}
/*//Set variables based on number of arguments specified
if (arguments.length == 2) {
recordID = arguments[0];
journalText = arguments[1];
} else if (arguments.length == 3) {
recordID = arguments[0];
targetTableName = arguments[1];
journalText = arguments[2];
} else if (arguments.length = 4) {
recordID = arguments[0];
targetTableName = arguments[1];
journalFieldName = arguments[2];
journalText = arguments[3];
}*/
if (arguments.length < 2 || arguments.length > 4) { //ERROR!
gs.logError('Incorrect number of arguments specified. ' + arguments.length +
' arguments specified, but 2, 3, or 4 arguments are expected.',
'getJournalID method of JournalRedactor Script Include');
return false;
}
/*if (!recordID || !journalText) {
gs.logError('getJournalID method of JournalRedactor Script Include could not ' +
'continue. Invalid argument(s) specified. recordID: ' + recordID + '. ' +
'journalText: ' + journalText + '.');
return this.journalEntryData;
}*/ //replaced by handling for different numbers of arguments above
//Update details in journalEntryData
this.journalEntryData.target_record_id = recordID;
this.journalEntryData.old_journal_value = journalText;
var grJournal = new GlideRecord('sys_journal_field');
grJournal.addQuery('element_id', recordID);
grJournal.addQuery('value', journalText);
if (targetTableName) {
grJournal.addQuery('name', targetTableName);
}
if (journalFieldName) {
grJournal.addQuery('element', journalFieldName);
}
grJournal.query();
if (grJournal.next()) {
journalEntryID = grJournal.getValue('sys_id');
journalFieldName = grJournal.getValue('element');
targetTableName = grJournal.getValue('name');
this.journalEntryData.journal_entry_id = journalEntryID;
this.journalEntryData.journal_field_name = journalFieldName;
this.journalEntryData.target_table_name = targetTableName;
} else {
return false;
}
if (grJournal.hasNext() && this._verbose) {
gs.log('findJournalID() method of JournalRedactor Script Include: ' +
'Additional journal entry was found, matching these arguments: ' +
JSON.stringify(arguments) + '. You may want to run this script again.');
}
return journalEntryID;
},
/**
* Set verbose mode to true or false. This enables additional logging if true.
* @param b {boolean}
*/
setVerbose: function(b) {
this._verbose = b;
},
/******PRIVATE METHODS BELOW******/
_readyForRedaction: function(journalEntryData) {
var p;
journalEntryData = journalEntryData ? journalEntryData : this.journalEntryData;
if (this._verbose) {
gs.log('Checking if journalEntryData is ready for redaction: ' +
JSON.stringify(journalEntryData));
}
for (p in journalEntryData) {
if (journalEntryData.hasOwnProperty(p)) {
if (p != 'old_journal_value' && p != 'new_journal_value' && !journalEntryData[p]) {
if (this._verbose) {
gs.log('journalEntryData not ready for redaction: ' +
JSON.stringify(journalEntryData));
}
return false;
}
}
}
if (this._verbose) {
gs.log('journalEntryData IS ready for redaction: ' +
JSON.stringify(journalEntryData));
}
return true;
},
_redactJournalEntry: function(journalEntryData, deleteEntry, newValue) {
journalEntryData = journalEntryData ? journalEntryData : this.journalEntryData;
if (this._verbose) {
gs.log('Beginning redaction of sys_journal_field record: ' +
JSON.stringify(journalEntryData));
}
if (!deleteEntry && !newValue) {
//If deleteEntry is not true, but no new value is specified, log an error
gs.logError('deleteEntry is not true, but newValue is not specified. Cannot ' +
'continue.', '_redactJournalEntry method of JournalRedactor Script Include');
return false;
} else if (!journalEntryData.journal_entry_id) {
gs.logError('Journal entry ID not known. Please call setJournalID() or ' +
'findJournalID() methods to set or find the journal ID before attempting ' +
'to redact.', '_redactJournalEntry method of JournalRedactor Script Include');
return false;
}
var grJournal = new GlideRecord('sys_journal_field');
grJournal.get(journalEntryData.journal_entry_id);
if (deleteEntry) {
grJournal.deleteRecord();
if (this._verbose) {
gs.log('Deleted journal entry with ID ' + journalEntryData.journal_entry_id);
}
} else {
grJournal.setValue('value', newValue);
grJournal.update();
if (this._verbose) {
gs.log('Updated journal entry with ID ' + journalEntryData.journal_entry_id +
' to new value: ' + newValue);
}
}
if (this._verbose) {
gs.log('redaction complete: ' +
JSON.stringify(journalEntryData));
}
return true;
},
_redactAudit: function(journalEntryData, deleteEntry, newValue) {
journalEntryData = journalEntryData ? journalEntryData : this.journalEntryData;
if (this._verbose) {
gs.log('Beginning redaction of sys_audit record: ' +
JSON.stringify(journalEntryData));
}
if (!deleteEntry && !newValue) {
//If deleteEntry is not true, but no new value is specified, log an error
gs.logError('deleteEntry is not true, but newValue is not specified. Cannot ' +
'continue.', '_redactAudit method of JournalRedactor Script Include');
return false;
} else if (!journalEntryData.journal_entry_id) {
gs.logError('Journal entry ID not known. Please call setJournalID() or ' +
'findJournalID() methods to set or find the journal ID before attempting ' +
'to redact.', '_redactAudit method of JournalRedactor Script Include');
return false;
}
var grAudit = new GlideRecord('sys_audit');
grAudit.addQuery('tablename', journalEntryData.target_table_name);
grAudit.addQuery('documentkey', journalEntryData.target_record_id);
grAudit.addQuery('newvalue', journalEntryData.old_journal_value);
//field names are not misspelled, they're just weird in this table.
grAudit.query();
while (grAudit.next()) {
if (deleteEntry) {
if (this._verbose) {
gs.log('Deleting audit record because deleteEntry param is set to ' +
deleteEntry);
}
grAudit.deleteRecord();
} else {
if (this._verbose) {
gs.log('Updating audit record because deleteEntry param is set to ' +
deleteEntry + ' and newValue is set to ' + newValue);
}
grAudit.setValue('newvalue', newValue);
grAudit.update();
}
}
if (this._verbose) {
gs.log('_redactAudit() finished');
}
return true;
},
_deleteHistory: function(journalEntryData) {
journalEntryData = journalEntryData ? journalEntryData : this.journalEntryData;
if (this._verbose) {
gs.log('_deleteHistory() running with journalEntryData: ' +
JSON.stringify(journalEntryData));
}
if (!journalEntryData.target_record_id || !journalEntryData.target_table_name) {
gs.logError(
'Journal entry ID or target table name not known. Please call setJournalID() or ' +
'findJournalID() methods to set or find the journal ID before attempting ' +
'to redact.', '_deleteHistory method of JournalRedactor Script Include');
return false;
}
var grHistorySet = new GlideRecord('sys_history_set');
grHistorySet.addQuery('id', journalEntryData.target_record_id);
grHistorySet.addQuery('table', journalEntryData.target_table_name);
grHistorySet.deleteMultiple();
if (this._verbose) {
gs.log('_deleteHistory() finished with journalEntryData: ' +
JSON.stringify(journalEntryData));
}
return true;
},
/**
* Populates journalEntryData object using a sys_journal_field record sys_id.
* @param journalEntryID {string}
* @returns {boolean}
* @private
*/
_populateJournalEntryData: function(journalEntryID) {
if (this._verbose) {
gs.log('_populateJournalEntryData() running with journalEntryID: ' + journalEntryID);
}
var grJournal = new GlideRecord('sys_journal_field');
if (grJournal.get(journalEntryID)) {
this.journalEntryData.target_record_id = grJournal.getValue('element_id');
this.journalEntryData.target_table_name = grJournal.getValue('name');
this.journalEntryData.old_journal_value = grJournal.getValue('value');
this.journalEntryData.journal_field_name = grJournal.getValue('element');
grJournal.update();
if (this._verbose) {
gs.log('_populateJournalEntryData() finished with journalEntryData: ' +
JSON.stringify(this.journalEntryData));
}
return true;
} else {
gs.logError('sys_journal_field record with sys_id ' + journalEntryID + ' not found.',
'_populateJournalEntryData method of JournalRedactor Script Include');
return false;
}
},
_JournalEntry: function() {
this.journal_entry_id = '';
this.target_record_id = '';
this.target_table_name = '';
this.old_journal_value = '';
this.journal_field_name = '';
this.new_journal_value = '';
},
type: 'JournalRedactor'
};