← All KB Checks
HIGHScriptOnlyCheck

Audit Coverage Gap

nowisor-audit-coverage-gap··Source on GitHub →

Audit Coverage Gap

This page describes the nowisor-audit-coverage-gap check from the nowisor Instance Scan Pack. The check reads the sys_dictionary collection row for three security-critical tables and reports whether table-level auditing is switched on. It does not detect tampering — it detects the absence of the record that would reveal tampering.

What this finding means

ServiceNow auditing is opt-in per table. sys_dictionary holds one row per (table, column) plus one collection row per table — the entry where name=<table> and element is empty. That collection row carries the audit flag, and it is the canonical table-level switch: when it is true, changes to the table emit sys_audit rows; when it is false or blank, they do not.

The check inspects three tables and nothing else:

Table What is lost when audit is off
sys_user_has_role who was granted which role, and when
sys_security_acl what each role was permitted to do, and what changed
sys_properties which hardening settings were toggled, and by whom

It reports three distinct outcomes, and only two of them are findings:

Dictionary collection row audit Result
present 1 / true pass (silent)
present 0, blank, anything else finding — un-audited
missing, or read threw n/a finding — unverifiable

The third row is the one worth understanding. If the cross-scope read of sys_dictionary fails, the check reports the table as unverifiable rather than passing it. A control it could not evaluate is never scored as a control that passed.

Why it matters for your compliance

These three tables are the substrate every other security control sits on. Turn auditing off and you can still see the current state — you simply cannot reconstruct how it got there. You can observe that an ACL is weak; you cannot say when it was weakened, by whom, or from what previous value. That is the difference between a security finding and an incident investigation.

NIS2 Article 21(2)(b) requires an incident-handling capability. Handling an incident means reconstructing it, and reconstruction needs a log that reaches back before the incident was noticed. An un-audited sys_security_acl means the reconstruction stops at "the ACL is wrong today".

ISO 27001 A.8.15 (logging) requires security events to be logged, and A.8.16 (monitoring activities) requires those logs to be reviewable. A table with auditing off produces no event to log, so both controls fail upstream of any review process you might have.

DORA Article 10 (detection) requires ICT-related incidents to be detectable from telemetry. Role-grant and ACL changes are precisely the telemetry a privilege-escalation incident is detected from.

The attack path

The exposure is not that an un-audited table is directly exploitable — it is that it converts a detectable attack into an undetectable one.

An attacker who reaches a position to grant themselves a role, weaken an ACL, or flip a hardening property normally leaves three records behind in sys_audit. With auditing off on these tables, the same actions leave the platform in an altered state with no history of the alteration. Post-incident review can establish that the instance is misconfigured; it cannot establish whether that misconfiguration is an attacker's work from last week or a consultant's shortcut from two years ago.

This is why the check pairs with the ones that watch those same three tables — nowisor-oob-acl-modifications (ACL drift), nowisor-direct-property-write (property writes), and nowisor-elevated-role-assignments (role grants). Each of those depends on a change record existing. If this check fires, treat their results as covering current state only: they are reading a history that was never written.

How to fix it

Step 1 — switch auditing on for each flagged table. Navigate to System Definition → Dictionary, filter Table = the flagged table name and Column name = empty to reach the collection row, open it, set Audit = true, and update. Repeat per flagged table.

Step 2 — accept that the gap before today is permanent. Auditing takes effect immediately for subsequent changes. Changes made before the toggle emit nothing retroactively and cannot be recovered. If you are remediating this during an active investigation, record the toggle time — it is the boundary your evidence starts at, and an assessor will ask.

Step 3 — re-run the scan. The check reads current dictionary state, so a passing result after the toggle confirms the switch landed on the collection row rather than on a field row (filtering Column name = empty is what distinguishes them; setting audit on a column row does nothing at table level).

If a table reports as unverifiable: the scoped app could not read sys_dictionary. Confirm the CrossScopePrivilege shipped with the pack (read on sys_dictionary) is present and committed, then re-run. Do not interpret unverifiable as compliant.

How this check stays honest

A read failure is a finding, not a pass. The sys_dictionary read runs cross-scope from x_nowisor_isp. If the privilege is missing or revoked, the natural failure mode of a naive check is to catch the exception, find nothing, and report clean — a silent pass on the exact control the customer is paying to have verified. This check pushes the failed table into the unverifiable list and says so in the finding text.

The collection row is resolved, not assumed. The query filters element='' with an OR on element=NULL, because the table-level flag lives only on that row. A check that queried sys_dictionary by table name alone would read whichever column row came back first and report a field's audit setting as if it were the table's.

Scope is stated, not implied. Three tables are inspected and they are named in the finding evidence (critical_tables_inspected). This check does not assert anything about audit coverage on the rest of your instance — a pass here means these three tables are audited, not that your instance is.

The detection logic is verified against ServiceNow Zurich Patch 6 (build glide-zurich-07-01-2025__patch6-01-16-2026).