Skip to content

Commit 00aec64

Browse files
committed
2 parents dbc1d10 + a5a7017 commit 00aec64

File tree

10 files changed

+899
-10
lines changed

10 files changed

+899
-10
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Redact PII from outbound email body
2+
3+
## What this solves
4+
Notifications sometimes leak personal data into emails. This mail script replaces common identifiers in the email body with redacted tokens before send.
5+
6+
## Where to use
7+
Notification or Email Script record, Advanced view, "Mail script" field. Invoke the function to get a safe body string and print it.
8+
9+
## How it works
10+
- Applies regex patterns to the email text for emails, phone numbers, IP addresses, NI number style patterns, and 16-digit card-like numbers
11+
- Replaces matches with descriptive placeholders
12+
- Leaves HTML tags intact by operating on the plain text portion you pass in
13+
14+
## Configure
15+
- Extend or tighten patterns for your organisation
16+
- Toggle specific scrubs on or off in the config block
17+
18+
## References
19+
- Email Scripts
20+
https://www.servicenow.com/docs/bundle/zurich-platform-administration/page/administer/notification/reference/email-scripts.html
21+
- Notifications
22+
https://www.servicenow.com/docs/bundle/zurich-platform-administration/page/administer/notification/concept/c_EmailNotifications.html
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Mail Script: Redact PII from outbound email body
2+
// Usage inside a Notification (Advanced view):
3+
// var safe = redactPii(current.short_description + '\n\n' + current.description);
4+
// template.print(safe);
5+
6+
(function() {
7+
function redactPii(text) {
8+
if (!text) return '';
9+
10+
// Config: toggle specific redactions
11+
var cfg = {
12+
email: true,
13+
phone: true,
14+
ip: true,
15+
niNumber: true,
16+
card16: true
17+
};
18+
19+
var out = String(text);
20+
21+
// Email addresses
22+
if (cfg.email) {
23+
out = out.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '[redacted email]');
24+
}
25+
26+
// Phone numbers (UK leaning, permissive, 7+ digits ignoring separators)
27+
if (cfg.phone) {
28+
out = out.replace(/\b(?:\+?\d{1,3}[\s-]?)?(?:\(?\d{3,5}\)?[\s-]?)?\d{3,4}[\s-]?\d{3,4}\b/g, '[redacted phone]');
29+
}
30+
31+
// IPv4 addresses
32+
if (cfg.ip) {
33+
out = out.replace(/\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b/g, '[redacted ip]');
34+
}
35+
36+
// National Insurance number style (AA 00 00 00 A) simplified - UK Specific
37+
if (cfg.niNumber) {
38+
out = out.replace(/\b([A-CEGHJ-PR-TW-Z]{2}\s*\d{2}\s*\d{2}\s*\d{2}\s*[A-D])\b/gi, '[redacted ni]');
39+
}
40+
41+
// 16 consecutive digits that look like a card (permit separators)
42+
if (cfg.card16) {
43+
out = out.replace(/\b(?:\d[ -]?){13,19}\b/g, function(match) {
44+
var digits = match.replace(/[ -]/g, '');
45+
return digits.length >= 13 && digits.length <= 19 ? '[redacted card]' : match;
46+
});
47+
}
48+
49+
return out;
50+
}
51+
52+
// Expose function to the mail template scope
53+
this.redactPii = redactPii;
54+
}).call(this);
Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
**This is an enhancement to the current code**
2-
1. Added "open in SOW" Button to open the record in service operations workspace, since it is gaining popularity now.
2+
1. Added "open in Workspace" Button to open the record in workspace(defined in option schema), since it is gaining popularity now.
33
2. Enhanced the visibility condition so that the button is only visible on pages having sys_id and table in url.
44
3. Enhanced css to improve visibility of button.
5+
4. This button wll look for the table to workspace mapping (in option schema) and create the URL to open record in respective workspace. If now mapping is found, the record is opened in SOW workspace(default).
6+
5. The button name has been changed to generic title "Open In Workspace"
57

6-
Widget will create a button that will only be visable to users with the itil role that will take them to the same record in platform. will work with the form and standard ticket pages (or anywhere with the table and sysId in the url.
8+
**Sample**
9+
{
10+
"name":"define_workspace",
11+
"type":"json",
12+
"label":"Define Table Workspace JSON Mapping",
13+
"value":{
14+
"sn_grc_issue":"risk/privacy", // will open issue records in RISK workspace
15+
"sn_si_incident":"sir" // will open security incidents in SIR workspace.
16+
}
17+
}
18+
19+
Widget will create a button that will only be visible to users with the itil role that will take them to the same record in platform. will work with the form and standard ticket pages (or anywhere with the table and sysId in the url.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<div>
22
<span class="btn-cntr" ng-if="::data.role==true">
33
<a href="{{::data.platform_url}}" target='_blank'class='btn btn-default'>{{::options.open_in_platform}}</a><br> <!--Platform button configuration-->
4-
<a href="{{::data.sow_url}}" target='_blank'class='btn btn-default'>{{::options.open_in_sow}}</a> <!--SOW button configuration-->
4+
<a href="{{::data.workspace_url}}" target='_blank'class='btn btn-default'>{{::options.open_in_workspace}}</a> <!--Workspace button-->
55
</span>
66
</div>

Modern Development/Service Portal Widgets/Open in Platform/option schema.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,18 @@
66
"default_value":"Open in Platform"
77
},
88
{
9-
"name":"open_in_sow",
9+
"name":"open_in_workspace",
1010
"type":"string",
11-
"label":"Name for SOW Button",
12-
"default_value":"Open in SOW"
11+
"label":"Name for Workspace Button",
12+
"default_value":"Open In workspace"
13+
},
14+
{
15+
"name":"define_workspace",
16+
"type":"json",
17+
"label":"Define Table Workspace JSON Mapping",
18+
"value":{
19+
"sn_grc_issue":"risk/privacy",
20+
"sn_si_incident":"sir"
21+
}
1322
}
1423
]

Modern Development/Service Portal Widgets/Open in Platform/server.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
(function() {
22
/*
3-
Code will get table and sys_id parameter from url and create url for platform and sow.
3+
Code will get table and sys_id parameter from url and create url for platform and workspace(defined in option schema).
44
This widget can be used in any page having sys_id and table in url , eg: ticket page.
55
*/
6-
data.table = input.table || $sp.getParameter("table"); // get table from url
7-
data.sys_id = input.sys_id || $sp.getParameter("sys_id"); // get sys_id from url
6+
data.table = $sp.getParameter("table"); // get table from url
7+
data.sys_id = $sp.getParameter("sys_id"); // get sys_id from url
88

9+
var tableWorkspaceMapping = JSON.parse(options.define_workspace); // get the table to workspace mapping from instance options.
10+
Object.keys(tableWorkspaceMapping).forEach(function(key) {
11+
if (key == data.table)
12+
data.workspace_url = "now/" + tableWorkspaceMapping[key] + "/record/" + data.table + "/" + data.sys_id; // if table to workspce mapping is found, the create workspace URL.
13+
else
14+
data.workspace_url = "now/sow/record/" + data.table + "/" + data.sys_id; // open in SOW
15+
});
916
data.platform_url = "/nav_to.do?uri=" + data.table + ".do?sys_id=" + data.sys_id;
10-
data.sow_url = "now/sow/record/" + data.table + "/" + data.sys_id;
1117

1218
data.role = false;
1319
if (gs.hasRole("itil") && data.table && data.sys_id) { // only visible to users with itil role and if url has required parameters.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Similarity Calculator for ServiceNow Incidents
2+
3+
## Overview
4+
This utility provides manual similarity scoring between ServiceNow incident records using text analysis, without requiring machine learning. It helps developers and admins find similar incidents by comparing descriptions and calculating similarity scores programmatically.
5+
6+
## How It Works
7+
1. Extracts keywords from incident descriptions
8+
2. Compares keyword overlap between incidents
9+
3. Calculates a similarity score (0-100%)
10+
4. Finds and ranks similar incidents based on score
11+
12+
## Features
13+
- Compare incident descriptions using keyword matching
14+
- Calculate similarity scores between incidents
15+
- Find and rank similar incidents programmatically
16+
- No ML or Predictive Intelligence required
17+
18+
## Use Cases
19+
- Manual clustering of incidents
20+
- Identifying duplicate or related tickets
21+
- Data quality analysis before ML model training
22+
- Root cause analysis and incident triage
23+
24+
## Setup Requirements
25+
- ServiceNow instance with access to the `incident` table
26+
- Script execution permissions (Background Script or Script Include)
27+
- No external dependencies
28+
29+
## Customization
30+
- Adjust keyword extraction logic for your environment
31+
- Change scoring algorithm to use TF-IDF, cosine similarity, etc.
32+
- Filter by assignment group, category, or other fields
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// ========================================
2+
// Similarity Calculator for ServiceNow Incidents
3+
// ========================================
4+
// Purpose: Manually score similarity between incidents using text analysis
5+
// No ML required
6+
// ========================================
7+
8+
(function similarityCalculator() {
9+
// --- CONFIG ---
10+
var config = {
11+
table: 'incident',
12+
baseIncidentSysId: '89b325155370f610de0038e0a0490ec5', // Set to the sys_id of the incident to compare
13+
fields: ['short_description', 'description'],
14+
maxResults: 50,
15+
minSimilarity: 0 // Minimum similarity % to report
16+
};
17+
18+
// --- Helper: Extract keywords from text ---
19+
function extractKeywords(text) {
20+
if (!text) return [];
21+
// Simple keyword extraction: split, lowercase, remove stopwords
22+
var stopwords = ['the','and','a','an','to','of','in','for','on','with','at','by','from','is','it','this','that','as','are','was','were','be','has','have','had','but','or','not','can','will','do','does','did','if','so','then','than','too','very','just','also','into','out','up','down','over','under','again','more','less','most','least','such','no','yes','you','your','our','their','my','me','i'];
23+
var words = text.toLowerCase().replace(/[^a-z0-9 ]/g, ' ').split(/\s+/);
24+
var keywords = [];
25+
for (var i = 0; i < words.length; i++) {
26+
var word = words[i];
27+
if (word && stopwords.indexOf(word) === -1 && word.length > 2) {
28+
keywords.push(word);
29+
}
30+
}
31+
return keywords;
32+
}
33+
34+
// --- Helper: Calculate similarity score ---
35+
function calcSimilarity(keywordsA, keywordsB) {
36+
if (!keywordsA.length || !keywordsB.length) return 0;
37+
var mapA = {};
38+
var mapB = {};
39+
for (var i = 0; i < keywordsA.length; i++) {
40+
mapA[keywordsA[i]] = true;
41+
}
42+
for (var j = 0; j < keywordsB.length; j++) {
43+
mapB[keywordsB[j]] = true;
44+
}
45+
var intersection = 0;
46+
var unionMap = {};
47+
for (var k in mapA) {
48+
unionMap[k] = true;
49+
if (mapB[k]) intersection++;
50+
}
51+
for (var l in mapB) {
52+
unionMap[l] = true;
53+
}
54+
var union = Object.keys(unionMap).length;
55+
return union ? (intersection / union * 100) : 0;
56+
}
57+
58+
// --- Get base incident ---
59+
var baseGr = new GlideRecord(config.table);
60+
if (!baseGr.get(config.baseIncidentSysId)) {
61+
gs.error('Base incident not found: ' + config.baseIncidentSysId);
62+
return;
63+
}
64+
var baseText = config.fields.map(function(f) { return baseGr.getValue(f); }).join(' ');
65+
var baseKeywords = extractKeywords(baseText);
66+
67+
// --- Find candidate incidents ---
68+
var gr = new GlideRecord(config.table);
69+
gr.addQuery('active', true);
70+
gr.addQuery('sys_id', '!=', config.baseIncidentSysId);
71+
gr.setLimit(config.maxResults);
72+
gr.query();
73+
74+
var results = [];
75+
while (gr.next()) {
76+
var compareText = config.fields.map(function(f) { return gr.getValue(f); }).join(' ');
77+
var compareKeywords = extractKeywords(compareText);
78+
var score = calcSimilarity(baseKeywords, compareKeywords);
79+
results.push({
80+
sys_id: gr.getUniqueValue(),
81+
number: gr.getValue('number'),
82+
short_description: gr.getValue('short_description'),
83+
similarity: score
84+
});
85+
}
86+
87+
// --- Sort and print results ---
88+
results.sort(function(a, b) { return b.similarity - a.similarity; });
89+
gs.info('=== Similarity Results ===');
90+
for (var i = 0; i < results.length; i++) {
91+
var r = results[i];
92+
gs.info((i+1) + '. ' + r.number + ' (' + r.similarity.toFixed(1) + '%) - ' + r.short_description);
93+
}
94+
if (results.length === 0) {
95+
gs.info('No similar incidents found above threshold.');
96+
}
97+
})();
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Training Data Quality Analyzer for ServiceNow Predictive Intelligence
2+
3+
## Overview
4+
This script analyzes the quality of incident data in ServiceNow to determine readiness for Predictive Intelligence (PI) model training. It provides detailed statistics and quality metrics to help ServiceNow developers and admins identify and address data issues before starting ML training jobs.
5+
6+
## Purpose
7+
- Assess completeness and quality of key fields in incident records
8+
- Identify common data issues that could impact PI model performance
9+
- Provide actionable insights for improving training data
10+
11+
## Features
12+
- Checks completeness of important fields (e.g., short_description, description, category, subcategory, close_notes, assignment_group)
13+
- Analyzes text quality for description and close notes
14+
- Evaluates category diversity and resolution times
15+
- Calculates an overall data quality score
16+
- Outputs results to the ServiceNow system logs
17+
18+
## Setup Requirements
19+
1. **ServiceNow Instance** with Predictive Intelligence plugin enabled
20+
2. **Script Execution Permissions**: Run as a background script or Script Include with access to the `incident` table
21+
3. **No external dependencies**: Uses only standard ServiceNow APIs (GlideRecord, GlideAggregate, GlideDateTime)
22+
4. **Sufficient Data Volume**: At least 50 resolved/closed incidents recommended for meaningful analysis
23+
24+
## How It Works
25+
1. **Field Existence Check**: Dynamically verifies that each key field exists on the incident table or its parent tables
26+
2. **Statistics Gathering**: Collects counts for total, resolved, and recent incidents
27+
3. **Completeness Analysis**: Calculates the percentage of records with each key field filled
28+
4. **Text Quality Analysis**: Measures average length and quality of description and close notes
29+
5. **Category Distribution**: Reports on the spread and diversity of incident categories
30+
6. **Resolution Time Analysis**: Evaluates how quickly incidents are resolved
31+
7. **Quality Scoring**: Combines all metrics into a single overall score
32+
8. **Log Output**: Prints all results and warnings to the ServiceNow logs for review
33+
34+
## Customization
35+
- Adjust the `keyFields` array in the config section to match your organization's data requirements
36+
- Modify thresholds for text length, resolution time, and completeness as needed
37+
- Increase `sampleSize` for more detailed analysis if you have a large dataset
38+
39+
## Security & Best Practices
40+
- Do not run in production without review
41+
- Ensure no sensitive data is exposed in logs
42+
- Validate script results in a sub-production environment before using for model training

0 commit comments

Comments
 (0)