diff --git a/Server-Side Components/Business Rules/Prevent Deletion of Policy with Active Controls/README.md b/Server-Side Components/Business Rules/Prevent Deletion of Policy with Active Controls/README.md new file mode 100644 index 0000000000..6a0e6ca820 --- /dev/null +++ b/Server-Side Components/Business Rules/Prevent Deletion of Policy with Active Controls/README.md @@ -0,0 +1,36 @@ +GRC Policy Deletion Guard +Overview +This Business Rule enhances data integrity for ServiceNow's Governance, Risk, and Compliance (GRC) module. It prevents the deletion of a sn_compliance_policy record if it is still associated with any active controls. This ensures that policy changes are managed properly and prevents compliance gaps that could occur if a foundational policy is removed while dependent controls are still active. +Details +Script Name: Prevent Delete of Policy with Active Controls +Target Table: sn_compliance_policy +Run Time: before delete +Action: Prevents a policy from being deleted if it has active, linked controls. Displays an error message to the user and aborts the deletion action. +Logic: +Uses GlideAggregate for an efficient query on the many-to-many (m2m) table (sn_compliance_m2m_policy_policy_statement) that links policies to control statements. +The query filters for records where: +The document field matches the sys_id of the policy being deleted. +The related content record (the control statement) has its active field set to true. +A COUNT aggregate is performed on the filtered records. +If the count is greater than zero, the script adds an error message to the form and aborts the deletion process using current.setAbortAction(true). +Business Rule Configuration +To implement this functionality, configure the following settings in the Business Rule record: +Name: Prevent Delete of Policy with Active Controls +Table: sn_compliance_policy +When to run: +When: before +Delete: checked +Advanced: checked + + +Purpose and Benefits +This Business Rule provides the following benefits to the GRC application: +Data Integrity: Prevents the accidental or erroneous deletion of policies that are still in active use, thereby preventing broken relationships in your GRC data model. +Controlled Changes: Enforces a process where administrators must first inactivate or re-associate all controls linked to a policy before it can be deleted, ensuring proper change management. +User Feedback: Provides clear and immediate feedback to the user, explaining why the requested action was denied and outlining the necessary steps to proceed. +Performance: Utilizes the efficient GlideAggregate method, which scales well even on large production instances. +Usage +This script is a core part of GRC data governance. If a user attempts to delete a policy with active controls, they will see an error message and the deletion will be stopped. The user must navigate to the related controls and either make them inactive or associate them with a different policy before attempting to delete the original policy again. + + + diff --git a/Server-Side Components/Business Rules/Prevent Deletion of Policy with Active Controls/script.js b/Server-Side Components/Business Rules/Prevent Deletion of Policy with Active Controls/script.js new file mode 100644 index 0000000000..e2697fc20b --- /dev/null +++ b/Server-Side Components/Business Rules/Prevent Deletion of Policy with Active Controls/script.js @@ -0,0 +1,52 @@ +(function executeRule(current, previous /*null when async*/ ) { + // This Business Rule runs 'before' a record is updated on the 'sn_compliance_policy' table. + // Its purpose is to prevent a policy from being retired if it is currently linked to any active Control Objectives. + // This enforces a proper decommissioning process, ensuring that Control Objectives are delinked. + // before the policy that governs them, thereby preventing compliance gaps. + // The condition for this rule would be: 'State' changes to 'Retired'. + + // Instantiate a GlideAggregate object on the many-to-many (m2m) table + // 'sn_compliance_m2m_policy_policy_statement'. This table links policies (via the 'document' field) + // to control statements (via the 'content' field). Using GlideAggregate is more + // performant than GlideRecord for counting records, as it performs the aggregation + // directly in the database. + var grControlAggregate = new GlideAggregate('sn_compliance_m2m_policy_policy_statement'); + + // Add a query to filter for records in the m2m table where the 'document' field matches + // the sys_id of the policy record currently being retired. + grControlAggregate.addQuery('document', current.getUniqueValue()); + + // Add a second query using 'dot-walking' to filter for records where the related + // control statement ('content' field) is currently active. This ensures only active + // controls are considered. + grControlAggregate.addQuery('content.active', true); + + // Set the aggregate function to COUNT. This tells the database to return the total + // number of records that match the query conditions. + grControlAggregate.addAggregate('COUNT'); + + // Execute the database query. + grControlAggregate.query(); + + // Initialize a variable to store the count of active controls. + var activeControlCount = 0; + + // Check if the query returned any results. If it did, retrieve the count. + // Note: GlideAggregate.next() returns a row even if the count is zero. + if (grControlAggregate.next()) { + // Retrieve the aggregated count result and assign it to the variable. + activeControlCount = grControlAggregate.getAggregate('COUNT'); + } + + // Check if the count of active controls is greater than zero. + if (activeControlCount > 0) { + // If active control objectives were found, add an error message to display to the user. + // The message includes the count for better clarity. + gs.addErrorMessage('Cannot retire this policy because it has ' + activeControlCount + ' active control objectives linked to it. All control objectives must be delinked first.'); + + // This crucial line aborts the current database transaction (the update operation). + // It prevents the policy record from being marked as 'Retired'. + current.setAbortAction(true); + } + +})(current, previous); diff --git a/Server-Side Components/Business Rules/Prevent Retirement of Policy with Active Control objectives b/Server-Side Components/Business Rules/Prevent Retirement of Policy with Active Control objectives new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/Server-Side Components/Business Rules/Prevent Retirement of Policy with Active Control objectives @@ -0,0 +1 @@ +