Mason Sidebotto
ServiceNow Employee

What is Security Scanner?

At a high level, Security Scanner is a Security Center feature that helps instance administrators identify, review, and remediate risky configurations. It runs a collection of checks that detect specific configuration patterns. When a check identifies a pattern in a resource, Security Scanner creates a Scan Finding for that resource.

 

How does it work?

Security Scanner is built on Instance Scan and provides out-of-the-box logic through Scan Check records. A Scan Check defines the pattern to detect and can be tested individually with the Test Check UI action. To run related checks together or on a schedule, group them in a Scan Suite. Each execution creates a Scan Result, which groups the Scan Findings generated by that suite run. The following graphic shows a simplified mental model of these relationships.

 

scan-finding-mental-model.png

 

 

Most Security Center interactions occur at the Scan Finding level, especially when you use only the out-of-the-box content. However, understanding the underlying data model is useful when you begin designing custom checks.

 

Using Security Scanner’s Out-of-the-Box Content

This post describes the out-of-the-box content available in Security Center version 3.4 and later. If your instance differs from the examples shown, confirm that the plugin is on the latest available version.

 

Out of the box, ServiceNow provides a handful of checks that are run regularly based on one of the four out-of-the-box, cadence-driven Scan Suites: Daily, Weekly, Monthly, and Quarterly.

 

scan-findings-list-1.png

 

 

As findings are generated, the Findings page (Security Center > Security Scanner > Findings) displays findings from the latest Scan Result for each suite. Findings from earlier Scan Results are not displayed.

 

Findings can be opened by clicking the value in the “Created” column for the associated finding. The finding page contains the following primary sections:

 

Field Source Purpose
Description

Static – from Scan Check

Provides general context explaining the configuration and why it may be risky.

Details

Dynamic – populated per finding

Provides more specific details about the source record for the finding. This requires setting the “finding_details” field within the Scan Check.

Source

Dynamic – populated per finding

Provides a link to the Source record where the configuration was identified.

Resolution Details

Static – from Scan Check

Provides instructions on how to resolve this finding (if relevant per your use cases).

Docs

Static – from Scan Check

Provides documentation related to Scan Check.

 

For example, this will look like this:

scan-findings-record-2.png

 

 

 

Use these details to decide whether the finding requires action and, if so, how to resolve it. After you correct the configuration, the finding remains visible until the associated Scan Suite runs again. You can rerun the suite manually or wait for its next scheduled execution.

 

Some findings may identify configurations that are intentional and acceptable for your use case. Because the out-of-the-box Scan Checks are designed to detect patterns that can introduce risk, especially when configured unintentionally, they may also produce false positives. Review each finding in the context of your organization before deciding whether it requires remediation.

 

To further illustrate this, consider two businesses and how they make use of their instances’ Knowledge Bases:

 

  alectriTech alectriEnergy
Knowledge Base Use Cases Self Service Troubleshooting Articles Internal only process definitions

 

From the perspective of a Scan Check that identifies public Knowledge Bases, findings will be produced for alectriTech and alectriEnergy when a Knowledge Base is public, though their significance can differ:

  • Findings for alectriTech Knowledge Bases may be false positives, especially if any are raised against intentionally public Knowledge Bases.
  • Findings for alectriEnergy are much more likely to be true positives, as their use case was solely for internal processes only, and not intended to be public.

Many organizations use Knowledge Bases for both public and internal content, so review each finding to determine whether it represents unintended exposure or an intentional configuration. If a finding identifies an intentional configuration that does not require action, select Mute to prevent Security Scanner from raising the same finding for that artifact in future scans.

 

If ServiceNow’s out-of-the-box detections do not cover a pattern you need to detect, you can create a custom Scan Check and add it to an out-of-the-box suite. The rest of this post explains how to build custom Scan Checks for Security Center.

 

Building your own checks to run in Security Scanner

To run a custom Scan Check in Security Center, first create the check, as described in the following sections. Then add it to a Security Center Scan Suite through the suite’s Checks related list:

 

Note, you’ll need to move to the Security Center Application scope in order to see the edit button.

 

scan-suite-page-3.png

 

 

This approach is more upgrade and patch-resistant because you don’t need to modify the base suite record; instead, this method is inserting a many-to-many record in the [scan_check_suite_table].

 

Running Checks while Testing

There are a few ways to run checks, including running the Scan Suite; however, when building a check and confirming that it works, this is often more than you need.

 

The next commonly used approach is to use the “Test Check” UI button on the top of the Scan Check form. This is generally fine for Table Checks and Script Only checks but can run longer for Column Checks.

 

The next approach is to click the “Run Point Scan” Related Link on records you are testing with, which executes all applicable checks against that record. This is particularly useful when building Column Checks, as you can target specific records. Note that Point Scans can also be run against an application or update set.

 

More details about all the ways to run scans can be found in our docs here: Executing a scan.

 

Building Checks

The following section will cover three different types—Table, Script Only, and Column—that can be created, with some simple examples to help illustrate how to build your own checks and the types of detections for which they can be useful.

 

You can create a check within Security Scanner under the Checks tab, or directly via the [scan_check] table and using the “New” action on the list. Before being routed to the form, an interceptor will let you select the type of check to create.

 

Please note that there is a final check type that you may encounter when implementing checks, called Linter Checks. These are a unique type of check and are designed to navigate Abstract Syntax Tree representations of scripts within the instance to identify issues in scripts. Given the complexity of these types of checks, they will be the independent subject of a future post.

 

Table Checks

Starting with Table Checks, these allow you to select a table and condition against which to detect configurations. They also let you use a script if you need to perform a more complex evaluation of a certain record’s state to create findings. These are a strong option when you're looking to evaluate records on a single table or a few tables.

 

As a simple example, the following check identifies mappings of the admin role to users. You can use the resulting findings to replace broad admin access with more granular admin roles where appropriate. For additional context, see Granular Admin Roles in Australia: Precise access control for a platform that's moving fast.

 

table-check-4.png

 

When we run this check with Test Check, the finding’s source is the [sys_user_has_role] record rather than the referenced user. If you want reviewers to act on the user directly, map the finding’s source to the user record instead.

 

table-check-result-5.png

 

 

To do this, we can edit our Table Scan Check by enabling the “Advanced” box and adding a script like this:

(function (finding, current) {
    finding.setCurrentSource(current.user.getRefRecord());
    finding.increment();
})(finding, current);

 

The script receives the finding object, which provides access to the Findings API. The setCurrentSource call maps the finding to the referenced user record, and increment creates or updates the finding count.

current behaves similarly to current in a business rule. For each row that matches the Table Check condition, it contains a GlideRecord for that row. In this example, the script evaluates only sys_user_has_role records whose role is admin.

 

With this adjustment, we can now see that our users are showing up as the source user, and we can easily start to action them.

 

table-check-result-fixed-6.png

 

 

Once we’re happy with how this check is working, we can add it to one of our suites. For this check, I’ll add it to my “Security Center - Weekly” suite (remember to make sure you are in the Security Center application scope, so that you see the “Edit” button).

 

scan-suite-config.png

 

 

Next, select Execute Suite Scan to refresh Security Scanner with the current state of the instance. Then review the findings to confirm that the expected test results appear. When you open a finding, verify that it renders the static content defined in the check.

 

scan-finding-page-7.png

 

 

Script Only Check

A Script Only Check differs from a Table Check because it is evaluated entirely through script rather than beginning with records from a selected table. The script must retrieve the data it evaluates—for example, with GlideRecord or gs.getProperty—and use the Findings API to create findings when the defined condition is met.

 

For example, the following script starts by identifying users with the admin role, then evaluates whether they are in our trusted domain. If they aren’t in our trusted domain, we raise a finding.

 

(function (finding) {
    const ADMIN_ROLE_SYS_ID = '2831a114c611228501d4ea6c309d626d';
    const TRUSTED_DOMAIN = '@example.com';

    var gr = new GlideRecord('sys_user_has_role');
    gr.addQuery('role', ADMIN_ROLE_SYS_ID);
    gr.query();

    while (gr.next()) {
        if(gr.user.email.endsWith(TRUSTED_DOMAIN)) continue;
        finding.setCurrentSource(gr.user.getRefRecord());
        finding.increment();
    }
})(finding);

 

Note that this type of check can also be implemented as a Table Check, like the example above, where the email check could be performed against the current record’s user record for the Table Check.

This gives results like this:

 

script-check-result-8.png

 

  

 

Adding dynamic content to Findings

The check correctly flags Carol because the domain in the user’s email address is not trusted. However, the reviewer must open the user record to see the domain that triggered the finding. To make the finding self-contained, update the script to populate the finding’s dynamic Details content with the detected domain.

    ...
    while (gr.next()) {
        if(gr.user.email.endsWith(TRUSTED_DOMAIN)) continue;
        finding.setCurrentSource(gr.user.getRefRecord());
        var domain = gr.user.email.toString().split('@')[1];
        var content = "<p>The user " + gr.user.getDisplayValue() + " has the following domain on their email address.</p><p><strong> " + domain + "</strong></p>"
        finding.setValue('finding_details', content);
        finding.increment();
    }
    ...

 

With this adjustment, our findings now look like this:

 

script-check-result-9.png

 

 

 

Column Check

Column Checks are also scripted checks, but they operate against records of a specific field type across numerous tables. Supported field types are script, XML, and HTML. 

 

Instead of reading the target data via GlideRecord, it can be accessed via the engine's column value (engine.columnValue). For example, we are going to target finding API keys in scripts across the instance, like this dummy script include where we’ve put a key in the source code:

 

script-include-10.png

 

The following check looks for scripts that have secret keys (in this example, we'll just look for the simple format /sk-[a-fA-F0-9]{32}/).

 

(function (engine) {
    var skip_tables = ["sys_script_execution_history"];
    if (skip_tables.indexOf(engine.current.getTableName()) > -1)
    return;

    //check for our secret key format usage at least once
    var search_regex = /sk-[a-fA-F0-9]{32}/;
    if (!search_regex.test(engine.columnValue))
    return;

    //remove code comments
    var code_comments_regex = /\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm;
    var commentsRemovedValue = engine.columnValue.replace(code_comments_regex, '');

    //raise finding if no comments found or usage found in commentsRemovedValue
    if (commentsRemovedValue.length == engine.columnValue.length || search_regex.test(commentsRemovedValue))
    engine.finding.increment();

})(engine);

 

Note, this follows a pattern that you may see in deprecated API checks in Security Center.

 

Because column-type checks evaluate numerous tables in each execution, they can take a long time to run when you click “Test Check”; instead, it can be valuable to use the “Run Point Scan” approach on some records for initial testing.

 

When we run our point scan on the dummy record, we see that we correctly got a finding:

 

MasonSidebotto_12-1792079035009.png

 

 

Let’s consider that we fix this scan by making the following change:

 

script-include-comment-11.png

 

After this change, the point scan no longer detects the credential when it appears in a comment. The original script removes comments before evaluating the remaining code, which is useful when you want to detect only executable behavior, such as deprecated API usage. For secrets detection, however, comments must remain in scope because anyone with access to the script may be able to read the embedded value. The check can therefore be simplified to scan the entire column value.

 

(function (engine) {
    var skip_tables = ["sys_script_execution_history"];
    if (skip_tables.indexOf(engine.current.getTableName()) > -1)
    return;

    //check for our secret key format usage at least once
    var search_regex = /sk-[a-fA-F0-9]{32}/;
    if (!search_regex.test(engine.columnValue))
    return;

    engine.finding.increment();
})(engine);

 

With this modification, we catch even the values in comments.

 

Conclusion

Table, Script Only, and Column Checks give you flexible ways to detect configuration patterns across your instance. Start with the check type that best matches the scope of the data you need to evaluate, test it against representative records, and then add it to an appropriate suite for recurring scans.

If you have questions, leave a comment on this post, and I’ll be happy to help.

 

Resources

Version history
Last update:
an hour ago
Updated by:
Contributors