Deployment9 min read

Audit mode to enforcement, and what audit mode does not tell you

Audit mode is not a perfect dress rehearsal. Four things are enforced or changed even in audit, and one failure mode produces no events at all.

Audit mode is the single most valuable feature App Control has, and it is routinely trusted further than it deserves. It logs what would have been blocked without stopping anything, which makes it the only safe way to find out what enforcement would break. It is not, however, a complete dress rehearsal, and the gaps are documented.

Knowing where audit mode is silent is the difference between an enforcement cutover that goes quietly and one that produces a surprise.

The mechanics are simpler than people expect

There is no enforce switch. Enforcement is the absence of the audit option.

Option 3 Enabled:Audit Mode instructs App Control to log information about applications, binaries and scripts that would have been blocked if the policy were enforced. Delete that option and the policy runs enforced.

# Remove the audit option to enforce the policy
Set-RuleOption -FilePath <PathAndFilename> -Option 3 -Delete

Microsoft's recommendation is to use audit mode initially, capture the policy information from the event log, and merge it into the existing policy to allow files that were flagged. That merge loop is the actual work of the audit stage, and how carefully it is done decides how enforcement goes.

Four things audit mode does not shield you from

This is the part worth planning around.

Script hosts do their own thing

Microsoft states that some features are always on and always enforced with any App Control policy that turns on user mode code integrity with option 0 Enabled:UMCI, and that "some script hosts might block code or run code with fewer privileges even in audit mode".

The specific case that matters is PowerShell. Under a user mode policy, a script not allowed by the policy runs in Constrained Language Mode rather than being refused. That is not a block, so it does not look like one. It is a script that half works, or fails on one particular call, while the policy is still supposedly only observing.

If you have automation that depends on capabilities Constrained Language Mode removes, audit mode is where that starts failing, not enforcement.

Dynamic Code Security has no audit mode

Option 19 Enabled:Dynamic Code Security enables policy enforcement for .NET applications and dynamically loaded libraries. Microsoft's note on it is explicit: the option "is always enforced if any App Control UMCI policy enables it. There's no audit mode for .NET dynamic code security hardening."

So if any user mode policy on the device sets option 19, it is enforcing, regardless of what mode the policy is nominally in. Know whether yours does.

COM object GUID mismatches are invisible until enforcement

This is the one that produces genuinely mysterious failures.

To harden against attackers exploiting GUID confusion, .NET performs an extra check that the registered COM object GUID matches the one calculated from the object's code. If they differ, .NET does not load the object and raises a general COM load error.

Three properties make this hard:

  • It only occurs when policy is enforced on user mode code, so you cannot see it in audit mode.
  • There is no logging and no events when a COM object fails to load due to this check.
  • There are no policy options to manage it. The check is always performed.

Repairing or reinstalling the application can resolve it temporarily, but a real fix needs the application's COM registration code corrected by whoever wrote it.

If an application starts behaving oddly the moment you enforce, and nothing appears in either event log, this is the candidate. It is also a good argument for enforcing on a small ring first, because it is the class of problem that only surfaces on the other side of the switch.

Native images can produce noise that is not a real problem

The code integrity logs can include error events for native images generated for .NET assemblies. Microsoft describes these as typically functionally benign, because a blocked native image falls back to its corresponding assembly and .NET regenerates the native image at its next scheduled maintenance window.

Worth knowing so you do not spend the audit stage chasing something that resolves itself. Compiling ahead of time to native code avoids it.

Audit mode has a performance cost

This one is counter-intuitive and it affects how long you leave audit running.

When a file is assessed against the active policies, the App Control engine sets kernel extended attributes on the file when it passes. If the same file runs again, App Control checks the attributes and reuses the cached result, as long as the policies in effect are unchanged. That is what lets App Control scale with many policies and large numbers of rules.

That optimisation is not used for policies in audit mode. Microsoft says you may observe a performance difference between systems with only enforced policies and systems with audit policies.

Two consequences. Audit mode is not free on constrained hardware, so a long audit period on low specification devices has a cost. And if you are relying heavily on the Intelligent Security Graph for authorisation, the two effects compound. Microsoft's advice is to "avoid running policies in audit mode that heavily rely on ISG authorization for large numbers of files".

Enforcement is the faster state. That is a genuinely useful thing to be able to tell an operations team that is nervous about the cutover.

Reading the audit evidence

The events you are working from during the audit stage:

3076 in the CodeIntegrity Operational log is the main block event for audit mode. It says the file would have been blocked if the policy were enforced. Its enforced counterpart is 3077.

8028 in the AppLocker MSI and Script log is the audit equivalent for scripts, MSI installers and COM objects, with 8029 as the enforced counterpart. 8039 covers packaged apps in audit, with 8040 enforced.

3089 carries the signature information for each signature on a blocked or audit blocked file, and is correlated to the block event by the Correlation ActivityID in the System portion of the event. An unsigned file produces one 3089 with a TotalSignatureCount of 0.

3091 tells you a file did not have managed installer or ISG authorisation while the policy is in audit mode, with 3092 the enforced version. The details include PassesManagedInstaller, which is the field that answers whether your deployment tool trust is working.

Reading these one device at a time does not scale, which is why grouping by application and owner matters more than the raw event count. There is more detail on the correlation mechanics in our piece on App Control event IDs.

A sequence that works

Deploy in audit, with the audit option set and nothing else changed. Resist the urge to fix rules at the same time as deploying. You want a clean picture first.

Group the events by application and by owner, not by device. Four hundred events from one broken application is one conversation. Treated as four hundred events, it is a backlog.

Sort what you find into three piles. Things that should be allowed, and need a rule at the right level. Things that should never have been running, which are a finding rather than a gap. Things you cannot decide about yet, which need an owner to answer a question.

Fix the rules, then look at the events again. The second pass is much quieter than the first, and the difference between the two is the best evidence you have that the policy is converging.

Check what is actually loaded before you enforce. Event 3099 reports a policy has been loaded and its details include the policy options. What is on the device and what is in your deployment tool are not always the same thing, and enforcing on the basis of the second one is how surprises happen.

Enforce on one ring. Delete option 3 for that device group only. Read the 3077s. The COM object failure described above, and anything else audit mode structurally cannot show you, appears here or nowhere.

Then move outward, with each ring gated on the one before it going quiet. Not on a date.

Two options worth setting while you are at it

Option 16 Enabled:Update Policy No Reboot allows future policy updates to apply without requiring a restart, on Windows 10 version 1709 and later or Windows Server 2019 and later. Without it, expect event 3095 telling you the policy cannot be refreshed and must be rebooted instead, which is the usual explanation for a policy change that appears to have done nothing.

Option 10 Enabled:Boot Audit on Failure applies when the policy is in enforcement mode. If a boot critical driver fails during startup, the policy is placed in audit mode so Windows loads, and the reason can be read in the CodeIntegrity log. It converts a device that will not boot into a device that boots and tells you why. On any policy that touches drivers, it is worth having.

What audit mode is actually for

Audit mode does not prove enforcement will be uneventful. It cannot, because several things behave differently or invisibly on the other side of the switch.

What it does is convert an unknown into a list. Before audit, the question of what enforcement would break has no answer. After a properly read audit period it has a list with owners against it, and the residual risk is confined to a small set of documented behaviours you can plan for and a first ring you can afford to be wrong on.

That is a materially different position, and it is worth the time. Rushing this stage is the most common reason a programme ends up back in audit mode permanently, which is the one outcome that delivers no protection at all.

The platform puts this in sequence with the rest, and signed and unsigned policies covers the additional testing a signed policy needs before it goes anywhere near enforcement.

The reason this stage consumes programmes is that the work is reading and deciding rather than authoring. PoliEze exists for that half: it groups what the fleet reports into applications with owners against them, so the audit period is spent making decisions rather than assembling the list you need before you can make any. The platform covers where that sits.

Sources

Questions about this

How do you move an App Control policy from audit mode to enforcement?
Delete option 3 Enabled:Audit Mode from the policy. When that option is removed the policy runs in enforced mode. There is no separate enforce switch, enforcement is simply the absence of the audit option.
Is audit mode completely safe to run?
Almost, but not entirely. Some script hosts may block code or run it with fewer privileges even in audit mode, and option 19 Enabled:Dynamic Code Security is always enforced if any user mode policy enables it, with no audit mode available for it.
Does audit mode affect performance?
It can. App Control caches its verdict in kernel extended attributes when a file passes an enforced policy and reuses that result. That caching is not used for policies in audit mode, so a device running audit policies may perform differently to one running only enforced policies.
Can audit mode miss a problem that only appears under enforcement?
Yes. If .NET refuses to load a COM object because the registered GUID does not match the calculated one, that only happens under enforcement on user mode code, produces no events, and cannot be detected in audit mode at all.

Related reading

Stuck on the thing this article describes?

If you are working through this on a real fleet and it is not going the way the documentation suggests, that is the conversation we are best at. Send us the block events.