Getting Instance Scan Linter Check working - ServiceNow Community
Mark Roethof
Tera Patron
Tera Patron

Articles, Blogs, Videos, Podcasts, Share projects - Experiences from the field

 

Hi there,

 

Linter Check, one of the last undocumented and unrevealed parts of Instance Scan. Or actually, not undocumented anymore… ServiceNow published a bit more documentation on Instance Scan two weeks ago. The documentation does help, though might still be a puzzle to get working, or even to visualize what it could bring.

 

So let's dig into this!


Linter Check

With the Quebec release, the Linter Check has been added as the fourth Scan Check type. In short, Linter Check is described as "Linter check scripts helps you in writing checks that look for issues in scripts". This sounds potentially interesting, checking for certain incorrect statements for example. With the Column Type Check you could also achieve this, though it would need some advanced regex, find, replace, etcetera. And still, it might not be bulletproof. What if the part you are after is in a comment, leading spaces or tabs are used, etcetera. Linter Check should be able to make this a lot easier and robust.


Node functions

Looking at the Docs, there are several Node functions available that can be used for the Linter Check. I won't repeat all of them here, though the interesting ones to get Linter Check working:

getRootNode()
getParent()
getTypeName()
getNameIdentifier()

 

For getNameIdentifier() good to know, if this isn't "NAME", then the result is null. You might want to filter on this, else the System Log will go nuts 😀.

 

Good to take note of this combination:

getParent().getTypeName()


Debugging

To get a visual feel for what this might bring you, let's just debug! For example creating a Linter Check with below script:

 

(function (engine) {

	engine.rootNode.visit(function(node) {
		if(node.getNameIdentifier()) {
			gs.info('>>> DEBUG: ' + node.getTypeName() + ' / ' + node.getNameIdentifier() + ' / ' + node.getParent().getTypeName());
		}
	});

})(engine);

 

If you would run this on a Business Rule for example, with script like below:

 

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

	new MLSolutionResult().findActiveSolution('dummy');
	
})(current, previous);

 

The debugging would provide us with:

>>> DEBUG: NAME / executeRule / FUNCTION
>>> DEBUG: NAME / current/ FUNCTION
>>> DEBUG: NAME / previous / FUNCTION
>>> DEBUG: NAME / MLSolutionResult / NEW
>>> DEBUG: NAME / findActiveSolution / GETPROP
>>> DEBUG: NAME / current / CALL
>>> DEBUG: NAME / previous / CALL

 

Good to know: Out-of-the-box objects which haven't been touched, won't be scanned. Also a Business Rule which is active is false, won't be scanned.

 

Debugging could also be done with use of tools like AST Explorer. I'm debugging within ServiceNow, as this would give the literal values we are after, we don't need to interpret anything. Though using the AST Explorer, you would see a full tree, very quickly to debug, etcetera. Just play around with this to get a bit more familiar.


Scan Check

You might want to create Scan Checks to check for reserved words used, deprecated API's, etcetera. For this example, checking "MLSolutionResult()", an API that has been deprecated with the Orlando release.


Column Type Check

If using a Column Type Check (of type Script), to check for "MLSolutionResult()" usage, we could use something like:

 

(function (finding, columnValue) {

	// Define variables
	var regex = /\bMLSolutionResult\s*\(\)/g;

	// Create scan finding
	if(columnValue && columnValue.match(regex)) {
		finding.increment();
	}

})(finding, columnValue);

 

For most cases, this would work. Though it's not completely bulletproof, for example commented code will also be evaluated.


Linter Check

So knowing the example script from the Docs, the debugging done above, we should be able to come up with a Linter Check to check for "MLSolutionResult()":

 

(function (engine) {

	engine.rootNode.visit(function(node) {
		if(node.getTypeName() === "NAME" &&
		   node.getNameIdentifier() === "MLSolutionResult" &&
		   node.getParent().getTypeName() === "NEW") {
			engine.finding.incrementWithNode(node);
		}
	});

})(engine);

 

Not sure if it's 100% bulletproof, though it's more robust than the RegEx used, and… if "MLSolutionResult" would be commented, it won't be listed as Finding, nice!

 

A thing I haven't sorted out yet, what if the API is called without using "new"? Then debugging would show is "CALL" instead of "NEW". I'm not sure yet if we should expand our Linter Check with this, for example using:

 

(node.getParent().getTypeName() === "NEW" || node.getParent().getTypeName() === "CALL")


Recap

I've described a bit more background on the Linter Check, how to debug, and getting an example working. Ones understanding the structure a bit better, building a basic Linter Checks isn't that tough to do. It does look promising, and now I can look into transforming some of my Column Type Checks into a Linter Check.

Though… I'm still missing some small bits. For example, what if I would like to check for "gs.now()". I haven't got a clue yet. Or what if I would like to use the Linter Check on Client Scripts though not on UI Policies. I haven't found a property yet that might provide us with the table name or class of the objects where scanning ☹️.

 

Anyway, there will be multiple good use cases for using the Linter Check!

---

And that's it. Hope you like it. If any questions or remarks, let me know!

 

C

If this content helped you, I would appreciate it if you hit bookmark or mark it as helpful.

 

Interested in more Articles, Blogs, Videos, Podcasts, Share projects I shared/participated in?
- Articles, Blogs, Videos, Podcasts, Share projects - Experiences from the field

 

Kind regards,


Mark Roethof

ServiceNow Technical Platform Architect @ Quint Technology

2x ServiceNow Developer MVP

2x ServiceNow Community MVP

---

LinkedIn

Comments
Daniel Draes
ServiceNow Employee
ServiceNow Employee

Awesome summary and a great way to start your way linting ServiceNow 😄

In the past days I have also worked on this and tried to figure out how to scan for a bespoke function name like gs.setProperty (we all know this is a bad idea to be used on scripts.... right?).

It was not as easy as I expected, mainly I guess due to me not know how an abstract syntax tree as generated by our linter works. I would recommend to read a bit about these like here in Wikipedia.

My AHA moment was when a colleague told me that the structure of these trees is somewhat unintuitive. Like when you have an operation as 'a + b' the tree would use '+' as the parent of 'a' and 'b'. This is similar with gs.setProperty. The '.' will be our parent with gs and setProperty as child elements. Knowing that makes actually writing the linter check much simpler. In Pseudo code we need to do something like:


Find all nodes of type GETPROP
traverse child nodes and verify if 'gs' and 'getProperty' exist

Now... the API does not allow to traverse the tree up or side-ways. All we get is the 'visit' function which will traverse the child nodes.

The way we can do this is by:

visit nodes in rootNode:
    if node is a GETPROP:
        visit subnodes in node:
             keep track of both children during visit iteration
             if both child subnodes are NAME nodes with "gs" and "setProperty":
                 finding.increment()

Sounds simple, right? Ok, here we go, that is the script I came up with which will search for gs.getProperty and gs.cacheFlush. Both essentially bad practice anyway.

(function(engine) {

engine.rootNode.visit(function(node) {
// AST will have the '.' as parent to what we need. Start here and visit child nodes
if (node.getTypeName() === "GETPROP") {
var gs_found = false;
var method_found = false;

node.visit(function(childnode) {
// now we are child to '.', check if we are gs or setProperty
if (childnode.getTypeName() != "NAME") {
return;
}

if (childnode.getNameIdentifier() === "setProperty" || node.getNameIdentifier() === "cacheFlush") {
method_found = true;
return;
}

if (childnode.getNameIdentifier() === "gs") {
gs_found = true;
return;
}
});

if (gs_found && method_found) {
engine.finding.incrementWithNode(node);
}
}
});
})(engine);

I'd like to conclude stating that we at ServiceNow are aware of this rather unintuitive behavior and will enhance this with upcoming releases. So what I documented here is valid for Quebec and Rome release and might need to be amended in the future.

Daniel Oderbolz
Kilo Sage

Dear @SaschaWildgrube 
The URL to your idea has changed, it is now 

 

https://support.servicenow.com/now?id=view_idea&sysparm_idea_id=23716958db25d514904fa9fb13961948&sys...

 

BTW: I do not understand why SN is not able to re-route such URLs after a migration. This adds unnecessary friction.
(The user already put in a LOT of effort to create these ideas in the first place)

 

Best
Daniel

Version history
Last update:
‎08-14-2024 11:05 AM
Updated by:
Contributors