ServiceNow forensics is platform-level rather than disk-level: your evidence is audit records, transaction logs, system logs, login history and configuration change records, not memory dumps or disk images. Collect them read-only, hash every export, and record who exported what and when — an investigation that cannot prove its evidence was unaltered will not support a regulatory filing or survive a challenge. The scripts below collect without writing anything back to the instance.
What This Is
When a security incident occurs in ServiceNow, the platform's built-in logging and auditing capabilities become the primary source of forensic evidence. Unlike traditional infrastructure forensics (disk images, memory dumps, network captures), ServiceNow forensics focuses on platform-level artifacts: audit records, transaction logs, system logs, login histories, and configuration change records.
Key Forensic Data Sources in ServiceNow
| Data Source | Table | What It Records | Retention |
|---|---|---|---|
| Audit Log | sys_audit |
Field-level changes on audited tables (old/new values) | Configurable (default: 180 days) |
| Audit Delete | sys_audit_delete |
Record deletions with who deleted, when, and original values | Configurable (default: 180 days) |
| Transaction Log | syslog_transaction |
HTTP requests: URL, method, user, session, response time, status | 7 days (default, often shorter) |
| System Log | syslog |
Application-level events, errors, script output | 7 days (default) |
| Login History | sys_user_session |
Login/logout events, IP address, session ID, user agent | Varies |
| Security Event Log | sysevent |
System events including security-relevant triggers | Configurable |
| Email Log | sys_email_log |
All email notifications sent including recipients and content | Configurable |
| Update History | sys_update_version |
Record version history with snapshots | Varies |
| Import Log | sys_import_log |
Data import operations from external sources | Configurable |
| Outbound HTTP | ecc_queue |
All MID Server communications and integration payloads | 7 days (default) |
The Forensic Challenge
ServiceNow is a multi-tenant SaaS platform (for most customers). This means:
- No disk-level access — you cannot image the underlying servers
- No memory forensics — no access to JVM heap or process memory
- No network captures — no packet-level analysis between ServiceNow components
- Log retention is aggressive — transaction logs default to 7 days; critical evidence may be purged before investigation begins
- Audit coverage is selective — only tables/fields with auditing enabled generate audit records
- Admin can disable auditing — a compromised admin account can turn off auditing to cover tracks
Chain of Custody Requirements
For forensic evidence to be admissible in legal proceedings, regulatory investigations, or insurance claims, it must demonstrate:
| Requirement | Challenge in ServiceNow |
|---|---|
| Integrity | Admins can modify sys_audit records (if ACLs are weak) |
| Authenticity | No cryptographic signing of log entries |
| Completeness | Gaps where auditing was disabled or retention purged records |
| Preservation | No native write-once storage; logs can be deleted |
| Documentation | No built-in chain of custody tracking for exported evidence |
Why This Is Dangerous
Attack Scenario: Audit Trail Destruction by Compromised Admin
Precondition: Attacker has gained admin access (via credential compromise, privilege escalation, or SAML bypass).
Attack chain:
Attacker checks current audit configuration to understand what is logged:
// Enumerate which tables have auditing enabled var dict = new GlideRecord('sys_dictionary'); dict.addQuery('audit', true); dict.query(); var auditedTables = {}; while (dict.next()) { var table = dict.getValue('name'); if (!auditedTables[table]) { auditedTables[table] = 0; } auditedTables[table]++; } for (var t in auditedTables) { gs.info('Audited: ' + t + ' (' + auditedTables[t] + ' fields)'); }Attacker disables auditing on critical tables before making malicious changes:
// Attacker disables audit on sys_user_has_role to hide privilege escalation // This change itself IS logged in sys_audit (if sys_dictionary is audited) // But many orgs don't audit sys_dictionary changes var dict = new GlideRecord('sys_dictionary'); dict.addQuery('name', 'sys_user_has_role'); dict.addQuery('audit', true); dict.query(); while (dict.next()) { dict.setValue('audit', false); dict.update(); } // Now privilege changes to sys_user_has_role are invisibleAttacker performs malicious actions (role assignments, data export, backdoor creation) — none are logged because auditing was disabled.
Attacker re-enables auditing to avoid detection by monitoring that checks for disabled audit flags.
The forensic gap: Investigators see audit trail up to the disable point, then nothing, then the trail resumes. The window of unlogged activity is the attack window.
Impact: Complete destruction of forensic evidence for the most critical attack actions. Investigators cannot reconstruct what happened during the blind window.
Attack Scenario: Evidence Tampering via sys_audit Manipulation
Precondition: Attacker has admin access and write access to sys_audit table.
Attack chain:
Attacker queries for their own audit trail:
// Find all audit records created by the attacker's session var audit = new GlideRecord('sys_audit'); audit.addQuery('user', 'compromised_admin'); audit.addEncodedQuery('sys_created_onONLast 24 hours@javascript:gs.daysAgoStart(1)@javascript:gs.daysAgoEnd(0)'); audit.query(); gs.info('Audit records to clean: ' + audit.getRowCount());If sys_audit has no delete ACL restriction, the attacker deletes incriminating audit records.
Alternative: Attacker modifies audit records to attribute actions to a different user (if the
userfield onsys_auditis not read-only).Investigators reviewing the audit trail see a clean history — or worse, see evidence pointing to an innocent user.
Impact: Forensic evidence is tampered with, leading investigators to wrong conclusions or making evidence inadmissible in legal proceedings.
Attack Scenario: Evidence Loss via Default Retention
Precondition: Default log retention settings. Incident discovered 10 days after initial compromise.
Attack chain:
Attacker compromises an account on Day 0. Transaction logs record the malicious login with source IP, user agent, and session ID.
Transaction log retention is 7 days. By Day 8, the login evidence is purged by the scheduled cleanup job.
Incident is discovered on Day 10 via anomalous behavior detection.
Investigators attempt to determine initial access vector but the transaction logs showing the first malicious login are gone. The
sys_audittrail shows what changed but not the HTTP-level detail of how the attacker authenticated.
Impact: Critical initial-access evidence is permanently lost. Investigators cannot determine the attack vector, cannot scope the compromise timeline accurately, and cannot provide a definitive root cause for regulatory reporting.
How to Detect
Forensic Readiness Assessment
/*
* IR-004 Detection Script
* Assesses forensic evidence collection readiness:
* audit coverage, log retention, integrity controls,
* and chain of custody capabilities
*
* Run as: Background script with admin role
* Impact: Read-only, safe for production
* Versions: Washington+
*/
gs.info('=== IR-004: FORENSIC READINESS ASSESSMENT ===');
gs.info('Scan started: ' + new GlideDateTime().getDisplayValue());
gs.info('');
var totalFindings = 0;
// 1. Check audit coverage on critical security tables
gs.info('--- AUDIT COVERAGE ON CRITICAL TABLES ---');
var criticalTables = [
'sys_user', 'sys_user_has_role', 'sys_user_group',
'sys_user_grmember', 'sys_properties', 'sys_security_acl',
'sys_script', 'sys_script_include', 'sys_ws_operation',
'sys_rest_message', 'sn_si_incident', 'sys_dictionary',
'sys_db_object', 'oauth_entity', 'sys_portal_page'
];
for (var i = 0; i < criticalTables.length; i++) {
var tableName = criticalTables[i];
var dict = new GlideRecord('sys_dictionary');
dict.addQuery('name', tableName);
dict.addQuery('audit', true);
dict.query();
var auditedFields = dict.getRowCount();
// Also check if table-level auditing is enabled
var tableDict = new GlideRecord('sys_dictionary');
tableDict.addQuery('name', tableName);
tableDict.addQuery('element', '');
tableDict.query();
var tableAudit = false;
if (tableDict.next()) {
tableAudit = tableDict.getValue('audit') === 'true' ||
tableDict.getValue('audit') === '1';
}
if (!tableAudit && auditedFields === 0) {
gs.info('[CRITICAL] ' + tableName + ' — NO auditing enabled');
totalFindings++;
} else {
gs.info('[OK] ' + tableName + ' — ' + auditedFields + ' audited fields' +
(tableAudit ? ' (table-level audit on)' : ''));
}
}
gs.info('');
// 2. Check sys_audit table size and oldest record
gs.info('--- AUDIT LOG RETENTION ---');
var oldestAudit = new GlideRecord('sys_audit');
oldestAudit.orderBy('sys_created_on');
oldestAudit.setLimit(1);
oldestAudit.query();
if (oldestAudit.next()) {
var oldest = new GlideDateTime(oldestAudit.getValue('sys_created_on'));
var now = new GlideDateTime();
var retentionDays = Math.round(
(now.getNumericValue() - oldest.getNumericValue()) / 86400000
);
gs.info('Oldest audit record: ' + oldest.getDisplayValue() +
' (' + retentionDays + ' days ago)');
if (retentionDays < 90) {
gs.info('[WARNING] Audit retention is less than 90 days — may not meet regulatory requirements');
totalFindings++;
}
if (retentionDays < 365) {
gs.info('[INFO] NIS2 and DORA may require 12+ months of audit data for investigations');
}
}
// Check transaction log retention
var oldestTx = new GlideRecord('syslog_transaction');
if (oldestTx.isValid()) {
oldestTx.orderBy('sys_created_on');
oldestTx.setLimit(1);
oldestTx.query();
if (oldestTx.next()) {
var oldestTxDate = new GlideDateTime(oldestTx.getValue('sys_created_on'));
var nowTx = new GlideDateTime();
var txRetention = Math.round(
(nowTx.getNumericValue() - oldestTxDate.getNumericValue()) / 86400000
);
gs.info('Oldest transaction log: ' + oldestTxDate.getDisplayValue() +
' (' + txRetention + ' days ago)');
if (txRetention < 30) {
gs.info('[CRITICAL] Transaction log retention under 30 days — HTTP-level forensics limited');
totalFindings++;
}
}
}
gs.info('');
// 3. Check sys_audit write protection
gs.info('--- AUDIT LOG INTEGRITY CONTROLS ---');
var auditAcl = new GlideRecord('sys_security_acl');
auditAcl.addQuery('name', 'sys_audit');
auditAcl.query();
var hasWriteProtection = false;
var hasDeleteProtection = false;
while (auditAcl.next()) {
var operation = auditAcl.getValue('operation');
gs.info(' ACL on sys_audit: operation=' + operation +
' | active=' + auditAcl.getValue('active'));
if (operation === 'write') hasWriteProtection = true;
if (operation === 'delete') hasDeleteProtection = true;
}
if (!hasWriteProtection) {
gs.info('[CRITICAL] No write ACL on sys_audit — audit records can be modified');
totalFindings++;
}
if (!hasDeleteProtection) {
gs.info('[CRITICAL] No delete ACL on sys_audit — audit records can be deleted');
totalFindings++;
}
gs.info('');
// 4. Check for recent audit disabling events
gs.info('--- RECENT AUDIT CONFIGURATION CHANGES ---');
var dictAudit = new GlideRecord('sys_audit');
dictAudit.addQuery('tablename', 'sys_dictionary');
dictAudit.addQuery('fieldname', 'audit');
dictAudit.addEncodedQuery('sys_created_onONLast 90 days@javascript:gs.daysAgoStart(90)@javascript:gs.daysAgoEnd(0)');
dictAudit.query();
var auditChanges = dictAudit.getRowCount();
if (auditChanges > 0) {
gs.info('[WARNING] ' + auditChanges + ' audit configuration changes in last 90 days');
totalFindings++;
dictAudit.setLimit(10);
dictAudit.query();
while (dictAudit.next()) {
gs.info(' Table: ' + dictAudit.getValue('documentkey') +
' | Changed by: ' + dictAudit.getValue('user') +
' | Old: ' + dictAudit.getValue('oldvalue') +
' | New: ' + dictAudit.getValue('newvalue') +
' | When: ' + dictAudit.getValue('sys_created_on'));
}
} else {
gs.info('[OK] No audit configuration changes in last 90 days');
}
gs.info('');
// 5. Check for sys_audit_delete coverage
gs.info('--- DELETION AUDIT COVERAGE ---');
var auditDel = new GlideRecord('sys_audit_delete');
if (auditDel.isValid()) {
auditDel.addEncodedQuery('sys_created_onONLast 30 days@javascript:gs.daysAgoStart(30)@javascript:gs.daysAgoEnd(0)');
auditDel.query();
gs.info('Deletion audit records (last 30 days): ' + auditDel.getRowCount());
} else {
gs.info('[WARNING] sys_audit_delete table not accessible');
totalFindings++;
}
gs.info('');
// 6. Check for external log forwarding (SIEM integration)
gs.info('--- EXTERNAL LOG FORWARDING ---');
var syslogDest = new GlideRecord('syslog_destination');
if (syslogDest.isValid()) {
syslogDest.query();
var destCount = syslogDest.getRowCount();
gs.info('Syslog destinations configured: ' + destCount);
if (destCount === 0) {
gs.info('[CRITICAL] No external log forwarding — all evidence is inside ServiceNow only');
gs.info(' If attacker has admin access, they can tamper with ALL evidence');
totalFindings++;
}
while (syslogDest.next()) {
gs.info(' Destination: ' + syslogDest.getValue('name') +
' | Type: ' + syslogDest.getValue('type') +
' | Active: ' + syslogDest.getValue('active'));
}
} else {
gs.info('[INFO] syslog_destination table not found — check SIEM integration via different method');
}
gs.info('');
// 7. Check login tracking coverage
gs.info('--- LOGIN TRACKING ---');
var sessions = new GlideRecord('sys_user_session');
if (sessions.isValid()) {
sessions.addEncodedQuery('sys_created_onONLast 7 days@javascript:gs.daysAgoStart(7)@javascript:gs.daysAgoEnd(0)');
sessions.query();
gs.info('Login sessions tracked (last 7 days): ' + sessions.getRowCount());
}
// Check if login events are in syslog
var loginLogs = new GlideRecord('syslog');
loginLogs.addEncodedQuery('messageLIKElogin^ORmessageLIKEauthentication^ORmessageLIKELogin');
loginLogs.addEncodedQuery('sys_created_onONLast 1 day@javascript:gs.daysAgoStart(1)@javascript:gs.daysAgoEnd(0)');
loginLogs.query();
gs.info('Authentication log entries (last 24h): ' + loginLogs.getRowCount());
gs.info('');
// Summary
gs.info('=== SUMMARY ===');
gs.info('Total forensic readiness findings: ' + totalFindings);
gs.info('');
gs.info('CRITICAL ACTIONS:');
gs.info('1. Enable auditing on all critical security tables (see list above)');
gs.info('2. Extend sys_audit retention to minimum 365 days');
gs.info('3. Extend syslog_transaction retention to minimum 90 days');
gs.info('4. Add write/delete ACL restrictions on sys_audit and sys_audit_delete');
gs.info('5. Configure external log forwarding to SIEM for tamper-proof evidence');
gs.info('6. Monitor sys_dictionary changes to audit flags via business rule');
gs.info('7. Implement evidence export procedures with hash verification');
Remediation
Step 1: Enable Auditing on All Critical Tables
Tables requiring audit for forensic readiness:
| Priority | Table | Why |
|----------|-------|-----|
| P1 | sys_user | Track account creation, modification, deactivation |
| P1 | sys_user_has_role | Track role assignments and revocations |
| P1 | sys_security_acl | Track ACL changes that affect access control |
| P1 | sys_properties | Track property changes (many control security) |
| P1 | sys_dictionary | Track schema changes including audit flag toggles |
| P1 | sys_script | Track business rule changes |
| P1 | sys_script_include | Track script include modifications |
| P2 | sys_rest_message | Track integration credential changes |
| P2 | oauth_entity | Track OAuth app modifications |
| P2 | sys_user_group | Track group membership changes |
| P2 | sys_user_grmember | Track group membership assignments |
| P2 | sys_ws_operation | Track web service operation changes |
| P3 | sys_update_set | Track update set promotions |
| P3 | sys_attachment | Track attachment uploads/deletions |
Configuration path:
1. Navigate to System Definition > Dictionary
2. Filter by table name
3. Set "Audit" = true on the table record and critical fields
4. Verify by checking sys_audit for new entries
Step 2: Protect Audit Log Integrity
1. Create restrictive ACLs on sys_audit:
- READ: admin, security_admin roles only
- WRITE: No one (set condition: false)
- DELETE: No one (set condition: false)
2. Create restrictive ACLs on sys_audit_delete:
- Same restrictions as sys_audit
3. Create a business rule on sys_dictionary to alert on audit flag changes:
- Table: sys_dictionary
- When: Before update
- Condition: current.audit.changes()
- Action: Create security incident + send email to security team
4. Enable sys_dictionary auditing to ensure the business rule cannot be
disabled without an audit trail
Step 3: Extend Log Retention
Minimum retention for forensic readiness:
| Log Type | Default | Recommended | Regulatory Driver |
|----------|---------|-------------|-------------------|
| sys_audit | 180 days | 365+ days | NIS2 Art.23§4: final report up to 1 month; investigations may extend |
| sys_audit_delete | 180 days | 365+ days | Same as sys_audit |
| syslog_transaction | 7 days | 90+ days | HTTP-level forensics for attack reconstruction |
| syslog | 7 days | 90+ days | Application-level event correlation |
| sys_user_session | Varies | 365+ days | Login forensics for compromise timeline |
| ecc_queue | 7 days | 30+ days | MID Server communication forensics |
Configuration:
1. Navigate to System Scheduler > Scheduled Jobs
2. Find cleanup jobs for each table
3. Modify retention period
4. Monitor database size impact
5. Consider archiving to external storage for long-term retention
Step 4: Configure External Log Forwarding
Forward critical logs to an external SIEM for tamper-proof storage:
1. Configure syslog forwarding:
- Navigate to System Logs > Syslog Destinations
- Add SIEM endpoint (Splunk, QRadar, Sentinel, etc.)
- Select log sources: audit, transaction, security events
2. Use ServiceNow Event Management integration:
- Forward security events to SIEM in real-time
- Configure bidirectional sync for incident correlation
3. Implement scheduled export for audit trail backup:
- Create scheduled job exporting sys_audit to encrypted external storage
- Include SHA-256 hash of each export file for integrity verification
- Store hashes separately from data for independent verification
Step 5: Create Evidence Export Procedures
Standard evidence export procedure for investigations:
1. Define scope: time range, tables, users, affected records
2. Export using encoded query to JSON or XML format
3. Generate SHA-256 hash of export file
4. Record export metadata: who exported, when, query used, record count
5. Store export + hash + metadata in evidence management system
6. Two-person rule: second analyst verifies export completeness
Evidence export template fields:
- Case/Incident number
- Export date/time (UTC)
- Exported by (name + role)
- Source table(s)
- Query used
- Record count
- File hash (SHA-256)
- Storage location
- Verification signature
Regulatory Impact
NIS2 Mapping
| Article | Requirement | How Forensic Gaps Violate It | Evidence After Fix |
|---|---|---|---|
| Art.23§4 | Final report with root cause analysis | Insufficient audit coverage and short retention prevent root cause determination | 365-day retention, full table auditing, external log backup enabling complete timeline reconstruction |
DORA Mapping
| Article | Requirement | How Forensic Gaps Violate It | Evidence After Fix |
|---|---|---|---|
| Art.17§3 | Root cause analysis for major ICT incidents | Missing transaction logs and tampered audit trails prevent accurate root cause analysis | Tamper-proof external logs, extended retention, comprehensive audit coverage |
ISO 27001:2022 Mapping
| Control | Requirement | How Forensic Gaps Violate It | Evidence After Fix |
|---|---|---|---|
| A.5.28 | Collection of evidence | No chain of custody procedures, no integrity controls on audit logs, inadequate retention | Evidence export procedures with hashing, write-protected audit tables, SIEM forwarding for independent evidence store |
Expert Notes
Practitioner annotations pending — article content has been technically validated.