Direct sys_properties Write Detector
Status: Deferred to nowisor v1.1. This check ships in the v1.0.0 agent pack with
active: false. It does not execute against your instance and produces no findings. The v1.1 release will reactivate it with a redesigned detection predicate. Until then, follow the manual workaround below to assess this risk class.
What this check will detect (when reactivated)
The nowisor-direct-property-write check is a LinterCheck that walks every server-side script in your instance (Script Includes, Business Rules) and flags any GlideRecord operation against the sys_properties table. The target pattern:
var gr = new GlideRecord('sys_properties');
gr.addQuery('name', 'some.property');
gr.query();
if (gr.next()) {
gr.setValue('value', 'new_value');
gr.update(); // <-- this is the pattern the check flags
}
When active, the check produces one finding per detected occurrence, including the source script and line number. The finding's metadata identifies the sys_properties constructor argument's line and the .update(), .insert(), or .deleteRecord() call that completes the direct write.
Writing to sys_properties via GlideRecord (instead of gs.setProperty()) bypasses three platform mechanisms:
- Cache invalidation.
gs.setProperty()invalidates the property cache so the new value is immediately visible to other nodes. Direct GlideRecord writes don't always trigger this; the new value may be applied to disk but not picked up by code that reads the cache. - Change audit. The audit pipeline that records property changes is keyed to the
gs.setProperty()path. Direct writes may not produce the same audit signature. - Property-change listeners. Business Rules or platform hooks that fire on property changes are bound to
gs.setProperty(). Direct writes may bypass them.
The discipline is to always use gs.setProperty(name, value) for property changes. The check exists to catch deviations.
Why this check is deferred
During Tier 2 verification (2026-05-12), planted-artifact testing on dev265484 revealed that the v1.0.0 predicate produces zero findings against a known-positive case (new GlideRecord('sys_properties').update()). Two possible root causes were identified:
LITERAL.getValue()API behavior. The predicate matchesLITERALnodes with value'sys_properties'. TheLITERAL.getValue()API is in the "likely-available" list of platform AST helpers but its behavior on Zurich Patch 6 may differ from the assumed string-return contract. If it returns an undefined or different shape, the predicate's filter never matches.AST argument-chain anchor. The predicate walks up to three ancestor levels looking for a
NEWorCALLnode. The actual AST shape ofnew GlideRecord('sys_properties')followed by chained method calls may put the table-name literal further from the ancestor than three levels — the predicate's parent-walk may simply run out before finding the anchor.
Rather than ship a check that produces zero findings (creating a false sense of coverage — the dangerous failure mode for a security control), we deferred the check to v1.1. The deferred state is documented in source (src/fluent/scan-checks/direct-property-write.now.ts carries active: false and the deferred-reason comment) and in manifest.json (active: false + deferred_to: "v1.1" + deferred_reason field).
The v1.1 reactivation criteria: ship a one-off diagnostic LinterCheck that dumps node-type ancestor chains for every LITERAL node with value 'sys_properties', use the output to redesign the predicate, and verify the new predicate against the planted-artifact gate before reactivation.
Manual workaround until v1.1
Until v1.1 ships, you can run the manual audit below as a Background Script. It uses an encoded-query search against script-bearing tables — different pattern than the LinterCheck (which uses AST traversal) but produces comparable findings with some false positives that you'll review manually.
The key distinction from the nowisor-hardcoded-credentials deferred-stub workaround: that audit hunts for credential-shaped string literals in source. This audit hunts for the specific new GlideRecord('sys_properties') idiom followed by a write operation.
/*
* Manual workaround for nowisor-direct-property-write (v1.0.0 deferred)
* Read-only, safe for production.
* Pattern-scans script-bearing tables for direct sys_properties write idioms.
* Review each result manually — false positives are expected.
*/
(function manualScan() {
var tables = [
'sys_script_include', // Script Includes
'sys_script', // Business Rules
'sys_script_client', // Client Scripts (rare; mostly server-side context)
'sysauto_script', // Scheduled Scripts
'sys_processor' // Processors
];
var totalCandidates = 0;
for (var t = 0; t < tables.length; t++) {
var gr = new GlideRecord(tables[t]);
gr.addQuery('active', true);
gr.addEncodedQuery(
"scriptLIKEnew GlideRecord('sys_properties')" +
"^ORscriptLIKEnew GlideRecord(\"sys_properties\")"
);
gr.query();
if (gr.getRowCount() === 0) continue;
gs.print('--- ' + tables[t] + ' ---');
while (gr.next()) {
// Filter to those that also contain .update(), .insert(), or .deleteRecord()
var src = gr.getValue('script') || '';
if (
src.indexOf('.update(') > -1 ||
src.indexOf('.insert(') > -1 ||
src.indexOf('.deleteRecord(') > -1
) {
totalCandidates++;
gs.print(
' ' + gr.getValue('name') +
' (sys_id: ' + gr.getValue('sys_id') + ')'
);
}
}
}
gs.print('');
gs.print('Total candidate scripts: ' + totalCandidates);
gs.print(
'Review each manually for actual direct sys_properties writes. ' +
'Replace with gs.setProperty(name, value).'
);
})();
For each script that contains a real direct write:
Before (incorrect):
var gr = new GlideRecord('sys_properties');
gr.addQuery('name', 'my.custom.property');
gr.query();
if (gr.next()) {
gr.setValue('value', 'new_value');
gr.update(); // Bypasses audit and cache invalidation
}
After (correct):
gs.setProperty('my.custom.property', 'new_value'); // Triggers audit, cache invalidation, listeners
The platform API path (gs.setProperty()) is functionally equivalent for the property-write operation, but it goes through the documented property-handling code path that audit, cache, and listeners hook into. The discipline is "every property write uses the property API, not GlideRecord."
What to do next
This is one of two checks deferred to v1.1 — see nowisor-hardcoded-credentials for the other. The pack ships 49 checks, 47 of them active. The 2 deferred checks have explicit reactivation criteria documented in source and manifest, and v1.1 will reactivate them after the planted-artifact gate clears.
The nowisor advisor product is being built to track deferred checks and notifies subscribers when reactivation lands — so when v1.1 ships, you'll know to re-run the agent pack with the property-write detector enabled.
For now, run the manual audit above as a periodic task (quarterly aligns with most change-review cadences) and document the results as part of your code-review evidence package. Subscribe for v1.1 release notice →