setWorkflow(false) Detector
This page describes the
nowisor-set-workflow-false-detectorcheck from the nowisor Instance Scan Pack v1.0.0. The check is a LinterCheck — it walks every server-side script and emits one finding per call tosetWorkflow()(with any argument; the broader detection catches bothfalseand runtime-variable cases). A typical instance produces tens of findings, concentrated in legacy migration scripts and bulk-update Business Rules.
What this finding means
Every finding identifies one line of server-side code that calls setWorkflow(). The check matches the AST shape NAME 'setWorkflow' whose parent is CALL OR whose grandparent is CALL — covering both the direct function-call form and the method-call form (gr.setWorkflow(false), which is the universal pattern in practice).
setWorkflow(false) is a GlideRecord method that disables three platform mechanisms for the subsequent write:
- Business Rules on the target table do not run.
- Audit logging is suppressed — the operation is not recorded in
sys_audit. - Notifications dispatched by Business Rules are not sent.
In other words: an authenticated write that calls setWorkflow(false) before insert, update, or delete operates outside the platform's enforcement and observation pipeline for that table. It's a deliberately-provided escape hatch for legitimate maintenance operations (bulk migrations, scheduled cleanups) where the side effects of BRs would create catastrophic load. It's also the canonical primitive used by attacker payloads to leave no trace.
A note on the predicate, which is mildly technical but useful for triage: the check accepts any first-argument value (false, true, a variable, or a computed expression). A stricter "only flag false" predicate would miss the variable-as-argument case where a script computes the boolean at runtime and passes it through. Because the audit surface is the call itself — not the argument value — the broader predicate is correct. You will see some findings where the call passes true; those are advisory rather than urgent, and the page treats them differently in triage.
Why it matters for your compliance
setWorkflow(false) is the audit-bypass control. Regulators that mandate audit trails treat every occurrence as a documented exception, not a tolerated default.
NIS2 Article 21§2(f) — policies and procedures to assess effectiveness of cybersecurity measures. Article 21§2(f) is about validating that your controls actually work. An audit trail that can be silently bypassed by a single method call is a control whose effectiveness is conditional. NIS2 supervisors are explicitly trained to look for this kind of control-bypass primitive — setWorkflow(false) is the canonical ServiceNow example.
ISO 27001 A.8.15 — logging. A.8.15 requires that event logging be implemented, stored, and protected. setWorkflow(false) violates the "implemented" part — for the specific operations preceded by the call, no log entry exists. Auditors increasingly understand the platform-specific bypass and ask for evidence that calls are inventoried and reviewed.
DORA Article 9 — ICT risk management framework. DORA Article 9§4(b) covers data integrity. A setWorkflow(false) call before a write to sys_user_has_role or sys_security_acl is a data-integrity bypass affecting controls. Financial entities under DORA face explicit expectations that operations affecting access control records are logged — setWorkflow(false) defeats this when the writing script is reachable from any compromise path.
The check is severity 1 (critical) because of the chained attack model (AP-007). An attacker who reaches a code-injection sink (eval, GlideEvaluator) can use setWorkflow(false) to make their post-compromise actions invisible. The detector therefore matters most in combination with the dynamic-evaluation detectors.
The attack path
The path is the second half of AP-007 — the audit-bypass leg of the eval-plus-setWorkflow privilege escalation chain.
Step 1 — The attacker has reached a code-injection sink. Through an eval() or GlideEvaluator.evaluateString() call reachable from their input (Scripted REST API parameter, table-stored expression, form-submitted code), the attacker can execute arbitrary JavaScript with the script's run-as privileges. Typically that's system privilege, which has no ACL restrictions.
Step 2 — Craft the privileged operation. The attacker writes payload code that performs the privileged action they want — granting themselves a role, modifying an ACL, resetting an admin password. As a GlideRecord operation:
var gr = new GlideRecord('sys_user_has_role');
gr.initialize();
gr.user = '<ATTACKER_USER_SYSID>';
gr.role = '<ADMIN_ROLE_SYSID>';
gr.setWorkflow(false); // <-- The trace-erasing line
gr.insert();
Step 3 — The bypass executes. Without setWorkflow(false), the insert would:
- Trigger Business Rules on
sys_user_has_role(some instances have BRs that alert on admin role grants). - Write an audit entry to
sys_auditrecording the new role assignment. - Dispatch notifications to security-admin distribution lists, if configured.
With setWorkflow(false), none of those happen. The role grant is silent. Subsequent platform behavior treats the attacker as having admin privileges; the attacker now operates as an admin with no historical trail of how they got the role.
Step 4 — Persistence. The attacker continues to use setWorkflow(false) on subsequent operations — modifying ACLs, planting Business Rules that grant access on demand, exfiltrating data. Each operation is invisible to the audit-driven detection surface that defenders rely on.
The defense is structural: ensure that setWorkflow(false) is not reachable from any code path that can be invoked with attacker-influenced input. In practice, this means the call should only exist in trusted maintenance scripts that are themselves protected from injection.
How to fix it
This is a triage check. Findings are inventory; remediation is per-call.
For each finding, classify:
Legitimate maintenance scripts (data migration, scheduled cleanup, batch backfill that has explicit operational justification) — Leave the
setWorkflow(false)in place. Add an inline comment explaining: who runs this, when, why BR side effects are not acceptable, and which audit compensating control covers the operations. Verify the script's protection: it must not be reachable from any user-influenced input (Scripted REST APIs, UI Actions withclient=true, Business Rules on user-facing tables).Business Rules on operational tables (
incident,change_request, etc.) — Usually wrong. The BR is shortcutting normal record processing for performance reasons. Refactor: chunk the operation into smaller batches that allow BRs to run, or use the platform's bulk-operation APIs (GlideMultipleUpdate) which handle the BR coordination properly.Scripted REST APIs or client-callable Script Includes — Critical priority. The call is reachable from external input. Remove
setWorkflow(false)from the script entirely. If the script genuinely needs to operate outside BRs, refactor: the operation should be queued to a scheduled job that runs in a trusted context, not invoked synchronously from a request.setWorkflow(true)(the inverse case the broader predicate catches) — Advisory. The call is a no-op in most contexts (workflow is on by default), but its presence usually signals that the script was originally written withsetWorkflow(false)and was reverted. Confirm the revert was intentional and the comment trail is clear.
The end state for a mature deployment is a small, documented, fully-isolated set of setWorkflow(false) calls in maintenance code paths — and zero occurrences in any code path reachable from external input.
How to verify the fix
/*
* Verify nowisor-set-workflow-false-detector remediation progress
* Read-only, safe for production.
* Lists open findings grouped by source script.
*/
(function pullSetWorkflowFindings() {
var byScript = {};
var f = new GlideRecord('scan_finding');
f.addQuery('check.sys_scope.scope', 'x_nowisor_isp');
f.addQuery('check.name', 'setWorkflow(false) Detector');
f.query();
while (f.next()) {
var sourceName = f.getDisplayValue('source') || '(unknown)';
if (!byScript[sourceName]) byScript[sourceName] = 0;
byScript[sourceName]++;
}
var names = Object.keys(byScript).sort();
gs.print('=== setWorkflow findings by source ===');
gs.print('Total scripts with findings: ' + names.length);
gs.print('');
for (var i = 0; i < names.length; i++) {
gs.print(' ' + names[i] + ': ' + byScript[names[i]] + ' call(s)');
}
})();
The expected end state is non-zero findings — legitimate maintenance code keeps some calls — but every remaining call should be documented as intentional with the operational context explained inline.
A note on the v1.0.0 build of this check: the original predicate required parent === CALL, which only caught the function-call AST shape and missed the universal method-call form (gr.setWorkflow(false)). The check produced zero findings on real code in early verification. The predicate was rewritten in May 2026 to accept both shapes — parent === CALL for the function-call form and grandparent === CALL for the method-call form — and now correctly catches every occurrence. If you have v1.0.0-build findings showing zero for this check on a deployment with confirmed setWorkflow(false) usage, upgrade to v1.0.0 GA.
What to do next
setWorkflow(false) is the second leg of the AP-007 chain. The first leg — code injection via eval or GlideEvaluator — is where attackers reach the audit-bypass primitive. Review the dynamic-evaluation findings first:
nowisor-eval-usage-detector— directeval()calls. Pair with this finding to identify AP-007 chains.nowisor-glide-evaluator-detector—GlideEvaluator.evaluateString(). Same risk class as eval.nowisor-set-roles-detector— privilege mutation. An attacker who reachessetRoles()and combines it withsetWorkflow(false)on the role grant can escalate silently.nowisor-glide-record-vs-secure— everyGlideRecordcall aftersetWorkflow(false)operates with no ACL evaluation (the SYSTEM context). The combination amplifies the data-access blast radius.
For organizations under DORA or NIS2 supervisory review, the nowisor advisor product is being built to cross-reference setWorkflow findings with audit-log volume metrics to surface scripts where the call's presence correlates with a measurable gap in audit coverage — turning the LinterCheck inventory into specific evidence of control-bypass risk. See the audit-bypass evidence flow →