@servicenow/sdk
Version:
ServiceNow SDK
283 lines (227 loc) • 11.3 kB
Markdown
---
tags: [instance-scan, scan-check, health-check, compliance, upgradability, security-scan, linter, column-scan, table-check, script-only-check]
---
# Instance Scan Guide
Create ServiceNow Instance Scan checks using Fluent API. Covers all scan check types: `TableCheck`, `LinterCheck`, `ColumnTypeCheck`, and `ScriptOnlyCheck`. Instance scan checks evaluate an instance for upgradability, performance, security, manageability, and user experience issues. Supported in SDK 4.0.0 or higher.
## When to Use
- Creating instance scan checks for health or compliance evaluation
- Choosing between TableCheck, LinterCheck, ColumnTypeCheck, or ScriptOnlyCheck
- Writing server-side scan scripts for advanced checks
- Configuring scoring, run conditions, or metadata for checks
## Instructions
1. **Import:** `import { TableCheck, LinterCheck, ColumnTypeCheck, ScriptOnlyCheck } from '@servicenow/sdk/core'`
2. **Unique $id:** Every check requires a unique `$id` in `Now.ID[<value>]` format. Use descriptive kebab-case values.
3. **Never use `scan_check` directly:** Always use one of the four scan types.
4. **camelCase properties:** Use `shortDescription`, `resolutionDetails`, `columnType`, `scoreMin`, `scoreMax`, `scoreScale`, `runCondition`, `findingType`, `useManifest`, `documentationUrl`.
5. **Scripts:** Always use inline scripts. Do not use `Now.include()` or external files.
6. **File organization:** Place `.now.ts` definitions in `src/fluent/scan-checks/`.
7. **ES5 scripts:** Server-side scripts must use `Class.create()` or plain functions. Do not use ES6 class syntax.
8. **Resolution details:** Always provide `resolutionDetails` so findings are actionable.
## Check Type Selection
| Use Case | Check Type | Key Property |
|----------|-----------|-------------|
| Scan records in a table by condition only | TableCheck | `conditions` (encoded query) |
| Filter records then evaluate with script | TableCheck | `conditions` + `advanced: true` + `script` |
| Lint code across the instance | LinterCheck | (base properties only) |
| Scan columns of a content type | ColumnTypeCheck | `columnType: 'script' \| 'xml' \| 'html'` |
| Standalone script check, no table binding | ScriptOnlyCheck | `script` (inline) |
| Scope findings to upgrade changes | TableCheck | `useManifest: true` |
### Decision Tree
1. "Scan records in a table where every match is a finding" -- **TableCheck** with `conditions`
2. "Filter records then evaluate each with custom logic" -- **TableCheck** with `conditions` + `advanced: true` + `script`
3. "Lint code for bad patterns" -- **LinterCheck**
4. "Scan all script/XML/HTML columns" -- **ColumnTypeCheck** with `columnType`
5. "Run a standalone check with no table binding" -- **ScriptOnlyCheck** with `script`
6. "Only check records changed during upgrades" -- **TableCheck** with `useManifest: true`
### Decision Matrix by Category
| Category | Recommended Check Types | Example |
|----------|------------------------|---------|
| security | ColumnTypeCheck (script) + ScriptOnlyCheck | Scan scripts for hardcoded credentials |
| upgradability | TableCheck + LinterCheck | Find deprecated API usage |
| performance | TableCheck (conditions) + ColumnTypeCheck | Find large attachments |
| manageability | ScriptOnlyCheck + TableCheck | Detect orphaned records |
| user_experience | LinterCheck + ColumnTypeCheck (html) | Lint for accessibility |
## API Reference
For the full property reference, see the `tablecheck-api`, `lintercheck-api`, `columntypecheck-api`, and `scriptonlycheck-api` topics.
### How Conditions and Scripts Work Together (TableCheck)
| Aspect | Condition-Only Mode | Advanced Mode |
|--------|-------------------|---------------|
| **When to use** | Every matching record is a finding | Need custom logic per record |
| **Configuration** | `conditions: 'encoded_query'` | `conditions` + `advanced: true` + `script` |
| **Script required** | No | Yes |
| **Flow** | conditions -- all matches = findings | conditions -- filter -- script per record -- `finding.increment()` |
## Scripting Patterns
### Script Signatures by Check Type
| Check Type | Signature | Key Objects |
|-----------|-----------|-------------|
| TableCheck (advanced) | `(function(engine, current) {...})(engine, current)` | `engine.finding`, `current` (filtered record) |
| ScriptOnlyCheck | `(function(finding) {...})(finding)` or `(function(engine) {...})(engine)` | `finding` / `engine.finding` |
| ColumnTypeCheck | `(function(engine, current, columnValue) {...})(engine, current, columnValue)` | `engine.finding`, `current`, `columnValue` |
| LinterCheck | `(function(engine) {...})(engine)` | `engine.finding`, `engine.rootNode` (AST root) |
### Finding API
| Method | Description |
|--------|-------------|
| `engine.finding.increment()` | Register a finding |
| `engine.finding.setCurrentSource(record)` | Set the source GlideRecord |
| `engine.finding.setValue('finding_details', '...')` | Add descriptive text |
| `engine.finding.setValue('count', number)` | Set the finding count |
### LinterCheck Engine API
| Property / Method | Description |
|-------------------|-------------|
| `engine.rootNode` | The AST root node |
| `engine.rootNode.visit(callback)` | Walk the AST tree |
| `node.getTypeName()` | AST node type (`"NAME"`, `"CALL"`, `"FUNCTION"`) |
| `node.getNameIdentifier()` | Identifier name for NAME nodes |
| `node.getParent()` | Parent AST node |
| `node.getLineNo()` | Line number (0-based) |
### ES5 Compatibility
| ES6 (avoid) | ES5 (use instead) |
|-------------|-------------------|
| `class Foo {}` | `var Foo = Class.create(); Foo.prototype = {...}` |
| `const` / `let` | `var` |
| Arrow functions | `function() {}` |
| Template literals | `'string ' + x` |
| `for...of` | `for (var i = 0; ...)` |
## Scoring Configuration
| Property | Default | Description |
|----------|---------|-------------|
| `scoreMin` | `0` | Minimum findings before scoring applies |
| `scoreMax` | `100` | Maximum findings for scoring |
| `scoreScale` | `1` | Multiplier applied to finding count |
**High-impact security check:** `scoreMin: 0, scoreMax: 50, scoreScale: 5`
**Low-impact informational check:** `scoreMin: 10, scoreMax: 500, scoreScale: 1`
## Metadata
Control when checks are installed using `$meta.installMethod`:
```javascript
$meta: { installMethod: "demo" } // Installed with "Load demo data"
$meta: { installMethod: "first install" } // Installed only on first app install
$meta: { installMethod: "once" } // Applied only once (scripts)
```
Omit `$meta` for checks that should always be installed.
## Avoidance
- Never use the `scan_check` table directly
- Never set `advanced: true` on TableCheck without providing a `script`
- Never confuse `conditions` (TableCheck record filter) with `runCondition` (execution gate for all types)
- Never use ES6 class syntax in server-side scripts
- Never use `Now.include()` or external script files
- Never omit `$id`
- Never hard-code sys_ids without documenting their source
## Examples
### Condition-Based TableCheck
```typescript fluent
import { TableCheck } from "@servicenow/sdk/core";
export const inactiveUsersWithRoles = TableCheck({
$id: Now.ID["check-inactive-users-roles"],
name: "Inactive Users with Roles",
active: true,
category: "security",
priority: "2",
shortDescription: "Finds inactive users that still have active role assignments",
table: "sys_user",
conditions: "active=false^roles!=",
resolutionDetails: "Remove role assignments from inactive user accounts."
});
```
### Advanced Script-Based TableCheck
```typescript fluent
import { TableCheck } from "@servicenow/sdk/core";
export const largeAttachmentCheck = TableCheck({
$id: Now.ID["check-large-attachments"],
name: "Large Attachment Detector",
active: true,
category: "performance",
priority: "3",
shortDescription: "Identifies oversized attachments that impact performance",
table: "sys_attachment",
advanced: true,
script: `(function(engine, current) {
var size = parseInt(current.getValue('size_bytes'), 10);
if (size > 10485760) {
engine.finding.setCurrentSource(current);
engine.finding.increment();
}
})(engine, current);`,
resolutionDetails: "Review large attachments and move to external storage.",
useManifest: true
});
```
### ScriptOnlyCheck
```typescript fluent
import { ScriptOnlyCheck } from "@servicenow/sdk/core";
export const adminRoleAudit = ScriptOnlyCheck({
$id: Now.ID["check-admin-ratio"],
name: "Admin Role Ratio Check",
active: true,
category: "security",
priority: "1",
shortDescription: "Flags if admin users exceed 5% of total active users",
script: `(function(finding) {
var gr = new GlideRecord('sys_user_has_role');
gr.addQuery('role.name', 'admin');
gr.addQuery('user.active', true);
gr.query();
var adminCount = gr.getRowCount();
var ga = new GlideAggregate('sys_user');
ga.addQuery('active', true);
ga.addAggregate('COUNT');
ga.query();
ga.next();
var total = parseInt(ga.getAggregate('COUNT'), 10);
if (total > 0 && (adminCount / total) > 0.05) {
finding.increment();
}
})(finding);`,
resolutionDetails: "Review admin role assignments and reduce to minimum necessary."
});
```
### LinterCheck
```typescript fluent
import { LinterCheck } from "@servicenow/sdk/core";
export const evalUsageCheck = LinterCheck({
$id: Now.ID["check-eval-usage"],
name: "Eval Usage Detector",
active: true,
category: "security",
priority: "1",
shortDescription: "Detects usage of eval() in scripts",
script: `(function(engine) {
var line_numbers = [];
engine.rootNode.visit(function(node) {
if (node.getTypeName() === 'NAME' &&
node.getNameIdentifier() === 'eval' &&
node.getParent().getTypeName() === 'CALL') {
line_numbers.push(node.getLineNo() + 1);
}
});
if (line_numbers.length == 0) return;
engine.finding.setValue('finding_details', 'Found on lines: ' + line_numbers.join(', '));
engine.finding.setValue('count', line_numbers.length);
engine.finding.increment();
})(engine);`,
resolutionDetails: "Replace eval() with safer alternatives."
});
```
### ColumnTypeCheck
```typescript fluent
import { ColumnTypeCheck } from "@servicenow/sdk/core";
export const scriptPatternCheck = ColumnTypeCheck({
$id: Now.ID["check-script-pattern"],
name: "Dangerous Script Pattern Detector",
active: true,
category: "security",
priority: "2",
shortDescription: "Scans script columns for dangerous patterns",
columnType: "script",
script: `(function(engine, current, columnValue) {
var skip_tables = ['sys_script_execution_history', 'scan_column_type_check'];
if (current.getTableName && skip_tables.indexOf(current.getTableName()) > -1) return;
var search_regex = /PATTERN_TO_FIND/;
if (!search_regex.test(columnValue)) return;
var comments_regex = /\\/\\*[\\s\\S]*?\\*\\/|([^:]|^)\\/\\/.*$/gm;
var clean = columnValue.replace(comments_regex, '');
if (clean.length == columnValue.length || search_regex.test(clean))
engine.finding.increment();
})(engine, current, columnValue);`,
resolutionDetails: "Review and remediate flagged script patterns."
});
```