Skip to main content

Converting Number from Scientific "E" Notation to GlideDateTime in ServiceNow

Today I hit a frustrating issue with a data import where a couple of fields which contained Unix date/time information were being received in scientific notation (e.g. 1.59855015616942E9).  How do we turn this into something we can use in ServiceNow? Requirements Convert a Unix Epoch timestamp, stored as number of seconds since 1970-01-01 00:00:00, in scientific 'e' notation (e.g. 1.59855015616942E9) into a GlideDateTime object containing the appropriate value (e.g. 2020-08-27 17:42:36). Approach Try not to tear out your hair figuring this one out! Implementation Make sure your number is actually a number (using the Number() function), then convert that into the number of milliseconds since the Unix epoch, then use the setValue() function on a GlideDateTime object to get a useful variable. var evilNumber = '1.59855015616942E9'; var gdt = new GlideDateTime(); // Make sure evilNumber is actually a number by using JavaScript Number() function. // Then multiply it by 1000 ...

Restricting Outbound Email by Domain Whitelist in ServiceNow

A while ago I came across a requirement to limit outbound emails sent by domain.  Of course there is an out of the box solution already available to restrict inbound email, but I struggled to find something that would restrict out-going email in the same way.  This requirement could help comply with Data Loss Prevention (DLP) policies.

You could do this (and many other things) using your own email infrastructure, but I wanted to see if we could do this without resorting to that.

I found a way to use a business rule, a custom system property, and (optionally) an event to build something that met the requirements and I originally posted it via the ServiceNow community in the hopes it might be useful to others.  I'm re-posting it here with some minor updates in the same vein!

Requirements

  1. Prevent any email from any part of the platform from being sent to a domain not on a predefined list of safe domain names; the White-List.
  2. Flag any attempt to send to a domain not on the White-List for investigation.

Approach

Intercept the email record on the sys_email table using a before update business rule when the record type changes to send-ready.  Referring to a White-List of safe domains in a system property, inspect and strip any email addresses that don't match.  Trigger an event containing the original list of recipients and the user name who triggered the email.

Implementation

  1. First, create a new System Property with an appropriate name, to store the list of safe domain names.  We'll refer to this later in our Business Rule.


  2. Create a new Event type, to flag when an email is modified.  We'll also refer to this in our Business Rule.


  3. Now we need to create our Business Rule on the sys_email table.  This rule should run before insert or update when the Type field changes to send-ready.  This should allow our logic to run before ServiceNow processes and sends the email.


  4. We'll add our script to the Advanced tab.  I'll try to break down the logic below, but for those who want to skip ahead, the full script looks like this:
    (function executeRule(current, previous /*null when async*/) {
    	
    	// This inspects each of the recipient fields (To, CC, BCC) and strips out any
    	// email addresses that aren't for domains listed in the system property
    	// "custom.email.outbound.whitelist".
    	// If "bad" addresses are found, it triggers the "outbound.email.bad.domain"
    	// event so that the record can be investigated.
    	
    	// Get the safe domains we're allowed to send to:
    	var whiteListProperty = gs.getProperty('custom.email.outbound.whitelist');
    	
    	// Take a copy of the original recipients:
    	var originalRecipients = 'TO:' + current.getValue('direct')
    							+ ', CC:' + current.getValue('copied')
    							+ ', BCC:' + current.getValue('blind_copied');
    	
    	// Check each recipient field, and strip out "bad" addesses:
    	var toResults;
    	var ccResults;
    	var bccResults;
    	var recipientResults;
    	
    	if (current.direct) {
    		toResults = stripRecipients(current.getValue('direct'),whiteListProperty);
    		current.setValue('direct',toResults.safeList);
    	}
    	if (current.copied) {
    		ccResults = stripRecipients(current.getValue('copied'),whiteListProperty);
    		current.setValue('copied',ccResults.safeList);
    	}
    	if (current.blind_copied) {
    		bccResults = stripRecipients(current.getValue('blind_copied'),whiteListProperty);
    		current.setValue('blind_copied',bccResults.safeList);
    	}
    	if (current.recipients) {
    		recipientResults = stripRecipients(current.getValue('recipients'),whiteListProperty);
    		current.setValue('recipients',recipientResults.safeList);
    	}
    	
    	// Trigger an event if we had to remove bad addresses:
    	if (toResults.badAddress || ccResults.badAddress || bccResults.badAddress) {
    		gs.eventQueue('outbound.email.bad.domain',current,originalRecipients,gs.getUserName());
    	}
    	
    	// Returns an object with two key/value pairs:
    	// safeList - List of "safe" addresses, checked against the whiteList.
    	// badAddress - True/false, true if any "bad" addresses were removed.
    	function stripRecipients(targetField, whiteList) {
    		
    		// Original List of recipients:
    		var originalRecipients = targetField;
    		// Convert to an array:
    		var arrRecipients = [];
    		arrRecipients = originalRecipients.split(',');
    		
    		// Build a list of safe recipients:
    		var revisedRecipients = '';
    		
    		// Flag if we have "bad" recipients:
    		var foundBad = false;
    		
    		// Loop through the original list of recipients:
    		var arrLength = arrRecipients.length;
    		for (var i = 0; i < arrLength; i++) {
    			// Split the email address on the @ symbol:
    			var arrDomain = [];
    			arrDomain = arrRecipients[i].split('@');
    			// Compare the domain to the WhiteList...
    			if (whiteList.includes(arrDomain[1])) {
    				// ...if ok, add to our revised list of recipients:
    				revisedRecipients = revisedRecipients + arrRecipients[i] + ',';
    			}
    			else {
    				// Potentially bad recipient:
    				foundBad = true;
    			}
    		}
    		
    		// Remove the trailing comma:
    		if (revisedRecipients.length > 1) {
    			revisedRecipients = revisedRecipients.substring(0, revisedRecipients.length-1);
    		}
    		
    		// Return the results as an object:
    		var objResults = {
    			"safeList" : revisedRecipients,
    			"badAddress" : foundBad
    		};
    		
    		return objResults;
    		
    	}
    	
    })(current, previous);

Breakdown

For those who are interested, we can break down the script logic into 3 parts.

First we create some variables to store the data we need, grab the white list from the System Property, store the original recipients.

	// Take a copy of the original recipients:
	var originalRecipients = 'TO:' + current.getValue('direct')
							+ ', CC:' + current.getValue('copied')
							+ ', BCC:' + current.getValue('blind_copied');

There are 4 different fields that store email addresses, so we need to run the same logic on each to preserve where the address will sit on the email (e.g. To, CC, or BCC).  To achieve this we'll offload the work to a function we'll define in the next section.  After we have our results we will also trigger the event, assuming we found at least one field that contained a "bad" address.

	// Check each recipient field, and strip out "bad" addesses:
	var toResults;
	var ccResults;
	var bccResults;
	var recipientResults;
	
	if (current.direct) {
		toResults = stripRecipients(current.getValue('direct'),whiteListProperty);
		current.setValue('direct',toResults.safeList);
	}
	if (current.copied) {
		ccResults = stripRecipients(current.getValue('copied'),whiteListProperty);
		current.setValue('copied',ccResults.safeList);
	}
	if (current.blind_copied) {
		bccResults = stripRecipients(current.getValue('blind_copied'),whiteListProperty);
		current.setValue('blind_copied',bccResults.safeList);
	}
	if (current.recipients) {
		recipientResults = stripRecipients(current.getValue('recipients'),whiteListProperty);
		current.setValue('recipients',recipientResults.safeList);
	}
	
	// Trigger an event if we had to remove bad addresses:
	if (toResults.badAddress || ccResults.badAddress || bccResults.badAddress) {
		gs.eventQueue('outbound.email.bad.domain',current,originalRecipients,gs.getUserName());
	}

Finally we need to define a function that does the comparison with the white list and returns a list of safe addresses to be updated into the corresponding field.

This function takes a comma separated list of email addresses, and a comma separated list of safe domains.  It takes the email address list and breaks it into an array of separate addresses, then loops through the array checking if anything after the @ matches something on the white list.

In the end, the function returns an object with two key/value pairs: the first is a comma separated list of all the address(es) that matched the white list, and the second is a true/false flag that is true if we encountered any addresses that didn't match.

	
	// Returns an object with two key/value pairs:
	// safeList - List of "safe" addresses, checked against the whiteList.
	// badAddress - True/false, true if any "bad" addresses were removed.
	function stripRecipients(targetField, whiteList) {
		
		// Original List of recipients:
		var originalRecipients = targetField;
		// Convert to an array:
		var arrRecipients = [];
		arrRecipients = originalRecipients.split(',');
		
		// Build a list of safe recipients:
		var revisedRecipients = '';
		
		// Flag if we have "bad" recipients:
		var foundBad = false;
		
		// Loop through the original list of recipients:
		var arrLength = arrRecipients.length;
		for (var i = 0; i < arrLength; i++) {
			// Split the email address on the @ symbol:
			var arrDomain = [];
			arrDomain = arrRecipients[i].split('@');
			// Compare the domain to the WhiteList...
			if (whiteList.includes(arrDomain[1])) {
				// ...if ok, add to our revised list of recipients:
				revisedRecipients = revisedRecipients + arrRecipients[i] + ',';
			}
			else {
				// Potentially bad recipient:
				foundBad = true;
			}
		}
		
		// Remove the trailing comma:
		if (revisedRecipients.length > 1) {
			revisedRecipients = revisedRecipients.substring(0, revisedRecipients.length-1);
		}
		
		// Return the results as an object:
		var objResults = {
			"safeList" : revisedRecipients,
			"badAddress" : foundBad
		};
		
		return objResults;
		
	}

That's it!  Time to put it all together and test.

Enhancements

I'm sure there are ways to expand on this idea, for example, you could add a new field on the sys_email table to store the original list of recipients, so you wouldn't need to dig through the event records to capture that.  Obviously you can trigger any number of things based on the event - an email notification, a security incident, etc.

Another thing to think about is that this solution still allows the email to be sent to any recipients that do match the white list, you may wish to modify it so that the email is redirected or doesn't get sent at all.

Disclaimer

As always, please test and evaluate anything you find in the community before adopting it for your own use.  Hope someone finds this useful, comments and suggestions welcome!

Updated: 2018-10-02, tidied up the script and removed any chance of 'pass by reference' problems (thanks Tim of SN Pro Tips for your informative articles!).

Updated: 2020-08-26, minor tweaks for republishing on Eldvyn blog.

Comments

Popular posts from this blog

Converting Number from Scientific "E" Notation to GlideDateTime in ServiceNow

Today I hit a frustrating issue with a data import where a couple of fields which contained Unix date/time information were being received in scientific notation (e.g. 1.59855015616942E9).  How do we turn this into something we can use in ServiceNow? Requirements Convert a Unix Epoch timestamp, stored as number of seconds since 1970-01-01 00:00:00, in scientific 'e' notation (e.g. 1.59855015616942E9) into a GlideDateTime object containing the appropriate value (e.g. 2020-08-27 17:42:36). Approach Try not to tear out your hair figuring this one out! Implementation Make sure your number is actually a number (using the Number() function), then convert that into the number of milliseconds since the Unix epoch, then use the setValue() function on a GlideDateTime object to get a useful variable. var evilNumber = '1.59855015616942E9'; var gdt = new GlideDateTime(); // Make sure evilNumber is actually a number by using JavaScript Number() function. // Then multiply it by 1000 ...