Skip to content

Commit 85f0d71

Browse files
Merge branch 'ServiceNowDevProgram:main' into main
2 parents 1ffd975 + e2e70a8 commit 85f0d71

File tree

19 files changed

+1274
-10
lines changed

19 files changed

+1274
-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: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# MID Server status JSON endpoint
2+
3+
## What this solves
4+
Operations teams often need a quick machine-readable view of MID Server health for dashboards and monitors. This Scripted REST API returns a compact JSON array of MID Servers with their status, last update time, and a simple "stale" flag if the record has not changed recently.
5+
6+
## Where to use
7+
Create a Scripted REST API with a single Resource and paste this script as the Resource Script. Call it from monitoring tools, dashboards, or widgets.
8+
9+
## How it works
10+
- Queries `ecc_agent` for active MID Servers
11+
- Returns `name`, `status`, `sys_id`, `sys_updated_on`, and a computed `stale` boolean based on a configurable `minutes_stale` query parameter (default 15)
12+
- Uses `gs.dateDiff` to compute minutes since last update
13+
14+
## Configure
15+
- Pass `minutes_stale` as a query parameter to override the default, for example `...?minutes_stale=30`
16+
- Extend the payload as needed (for example add `version`, `ip_address`) if available in your instance
17+
18+
## References
19+
- Scripted REST APIs
20+
https://www.servicenow.com/docs/bundle/zurich-application-development/page/build/applications/task/create-scripted-rest-api.html
21+
- MID Server overview
22+
https://www.servicenow.com/docs/bundle/zurich-servicenow-platform/page/product/mid-server/concept/c_MIDServer.html
23+
- GlideRecord API
24+
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideRecord/concept/c_GlideRecordAPI.html
25+
- GlideDateTime and dateDiff
26+
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideDateTime/concept/c_GlideDateTimeAPI.html
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Scripted REST API Resource Script: MID Server status JSON endpoint
2+
// Method: GET
3+
// Path: /mid/status
4+
5+
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
6+
try {
7+
// Configurable staleness threshold in minutes via query param
8+
var q = request.queryParams || {};
9+
var minutesStale = parseInt((q.minutes_stale && q.minutes_stale[0]) || '15', 10);
10+
if (!isFinite(minutesStale) || minutesStale <= 0) minutesStale = 15;
11+
12+
var now = new GlideDateTime();
13+
14+
var out = [];
15+
var gr = new GlideRecord('ecc_agent'); // MID Server table
16+
gr.addActiveQuery();
17+
gr.orderBy('name');
18+
gr.query();
19+
20+
while (gr.next()) {
21+
var updated = String(gr.getValue('sys_updated_on') || '');
22+
var minutesSince = 0;
23+
if (updated) {
24+
// gs.dateDiff returns seconds when third arg is true
25+
minutesSince = Math.floor(gs.dateDiff(updated, now.getValue(), true) / 60);
26+
}
27+
28+
out.push({
29+
sys_id: gr.getUniqueValue(),
30+
name: gr.getDisplayValue('name') || gr.getValue('name'),
31+
status: gr.getDisplayValue('status') || gr.getValue('status'), // Up, Down, etc.
32+
sys_updated_on: gr.getDisplayValue('sys_updated_on'),
33+
minutes_since_update: minutesSince,
34+
stale: minutesSince >= minutesStale
35+
});
36+
}
37+
38+
response.setStatus(200);
39+
response.setBody(out);
40+
} catch (e) {
41+
response.setStatus(500);
42+
response.setBody({ error: String(e) });
43+
}
44+
})(request, response);
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: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
api.controller = function($scope) {
2+
var c = this;
3+
var canvas, ctx;
4+
var drawing = false;
5+
var lastPos = { x: 0, y: 0 };
6+
7+
// Initialize after DOM is ready
8+
c.$onInit = function() {
9+
setTimeout(function() {
10+
canvas = document.getElementById('signature-pad');
11+
if (!canvas) return;
12+
13+
// Get 2D drawing context
14+
ctx = canvas.getContext('2d');
15+
ctx.lineWidth = 2;
16+
ctx.strokeStyle = '#000';
17+
18+
// Mouse event listeners
19+
canvas.addEventListener('mousedown', startDraw);
20+
canvas.addEventListener('mousemove', draw);
21+
canvas.addEventListener('mouseup', endDraw);
22+
23+
// Touch event listeners (for mobile/tablet)
24+
canvas.addEventListener('touchstart', startDraw);
25+
canvas.addEventListener('touchmove', draw);
26+
canvas.addEventListener('touchend', endDraw);
27+
}, 200);
28+
};
29+
30+
// Get drawing position based on mouse or touch input
31+
function getPosition(event) {
32+
var rect = canvas.getBoundingClientRect();
33+
if (event.touches && event.touches[0]) {
34+
return {
35+
x: event.touches[0].clientX - rect.left,
36+
y: event.touches[0].clientY - rect.top
37+
};
38+
}
39+
return {
40+
x: event.clientX - rect.left,
41+
y: event.clientY - rect.top
42+
};
43+
}
44+
45+
// Start drawing when mouse/touch pressed
46+
function startDraw(e) {
47+
drawing = true;
48+
lastPos = getPosition(e);
49+
}
50+
51+
// Draw line on canvas while dragging
52+
function draw(e) {
53+
if (!drawing) return;
54+
e.preventDefault(); // Prevent page scrolling on touch
55+
var pos = getPosition(e);
56+
ctx.beginPath();
57+
ctx.moveTo(lastPos.x, lastPos.y);
58+
ctx.lineTo(pos.x, pos.y);
59+
ctx.stroke();
60+
lastPos = pos;
61+
}
62+
63+
// Stop drawing when mouse/touch released
64+
function endDraw() {
65+
drawing = false;
66+
}
67+
68+
// Clear the canvas
69+
c.clearSignature = function() {
70+
if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);
71+
drawing = false;
72+
};
73+
74+
// Convert signature to base64 image and attach
75+
c.attachSignature = function() {
76+
if (!ctx) return alert("Canvas not initialized.");
77+
var data = canvas.toDataURL('image/png'); // Get base64 encoded image
78+
alert("Signature captured successfully. It will be attached after submission.\n\n" + data);
79+
};
80+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<div class="text-center">
2+
<!-- Canvas area for drawing signature -->
3+
<canvas id="signature-pad" width="400" height="200"
4+
style="border:1px solid #ccc; border-radius:8px; cursor:crosshair; touch-action:none;">
5+
</canvas>
6+
7+
<!-- Action buttons -->
8+
<div class="mt-3">
9+
<button class="btn btn-primary" ng-click="c.clearSignature()">Clear</button>
10+
<button class="btn btn-success" ng-click="c.attachSignature()">Attach Signature</button>
11+
</div>
12+
</div>

0 commit comments

Comments
 (0)