# #297 — the `[5, 20]` enforcement refusal: mapping, chronology, and the write-vs-capture comparison

**Subject:** `85f84d738fa702f35c83910f314aae17849d125c`
**Evidence:** r10 field run `20260912T231404Z`, archived at
`.spt/preserved/hertz-fp-driver-review/v9-exercises/r10-field-run-20260912T231404Z`
**Scope:** documentation and source reading only. No host probe, no rule recreation, no policy
change. **Strict `[1]` acceptance is unchanged.**

---

## 1. The refusal, verbatim

From `handoff/001-setup-command.err` (elevated setup, liam, exit 0):

```
bootstrap-firewall leg=verify-query    program=powershell.exe wall_ms=2191 outcome=completed
bootstrap-firewall leg=verify-query    program=powershell.exe wall_ms=1791 outcome=completed
bootstrap-firewall leg=reconcile-write program=powershell.exe wall_ms=2135 outcome=completed
bootstrap-firewall leg=verify-query    program=powershell.exe wall_ms=2137 outcome=completed
LAN_FIREWALL_UNVERIFIED: The admission pair was WRITTEN and then could not be verified: Bootstrap
rule spt-core-bootstrap-inbound-tcp is configured but ActiveStore enforcement codes are [5, 20],
not exactly [1].
```

Classified `wrapper=unverified-after-write cause=enforcement`, with `enforcement=1` and
`representation_fault=0` — the codes were transported as numbers; this is not a query failure.

## 2. Documented mapping — authoritative, with its limits

Microsoft Learn, **`MSFT_NetFirewallRule`** (namespace `Root\StandardCimv2`, MOF `WFasCim.mof`, DLL
`WFasCim.dll`). `uint16 EnforcementStatus[]` — *"If this object is retrieved from the ActiveStore,
describes the current enforcement status of the rule."*

| code | documented name | | code | documented name |
| --- | --- | --- | --- | --- |
| 0 | Invalid | | 12 | RemoteMachineEmpty |
| **1** | **Full** | | 13 | RemoteUserEmpty |
| 2 | FirewallOffInProfile | | 14 | LocalGlobalOpenPortsDisallowed |
| 3 | CategoryOff | | 15 | LocalAuthorizedApplicationsDisallowed |
| 4 | DisabledObject | | 16 | LocalFirewallRulesDisallowed |
| **5** | **InactiveProfile** | | 17 | LocalConsecRulesDisallowed |
| 6 | LocalAddressResolutionEmpty | | 18 | NotTargetPlatform |
| 7 | RemoteAddressResolutionEmpty | | 19 | OptimizedOut |
| 8 | LocalPortResolutionEmpty | | **20** | **LocalUserEmpty** |
| 9 | RemotePortResolutionEmpty | | 21 | TransportMachinesEmpty |
| 10 | InterfaceResolutionEmpty | | 22 | TunnelMachinesEmpty |
| 11 | ApplicationResolutionEmpty | | 23 | TupleResolutionEmpty |

**So `[5, 20]` = `InactiveProfile` + `LocalUserEmpty`, and `1` = `Full` = `ENFORCEMENT_SUCCESS`.**

**Limits, recorded rather than glossed:**

- The mapping comes from **documentation, not from the host**. The captured CIM metadata had the
  `Values` qualifier **absent**, which is why the codes arrived nameless; nothing in the capture
  confirms this host's provider agrees with the published names.
- Page minimum client Windows 8 / server 2012; subject host is Windows 11 Pro 26200. **Provider-version
  applicability is unverified.**
- Documented range is **0..23**. An earlier note of mine said 0..25; unreconciled, flagged, not
  resolved either way.
- **Array semantics remain unresolved, and the documentation confirms the gap rather than closing
  it**: the page describes *individual* codes and says nothing about how a multi-element array is to
  be read. Both codes can now be named; what reporting *both* means is still unknown.
- The refusal is correct regardless: neither 5 nor 20 is 1.

### 2.1 `PrimaryStatus` — a second rendering-versus-specification divergence, as a caveat only

**Captured evidence keeps `PrimaryStatus` NUMERIC.** The documented labels for its third value do not
agree with each other:

| source | third value |
| --- | --- |
| `MSFT_NetFirewallRule` class reference (enumeration) | `Degraded` (2) |
| `Get-NetFirewallRule` cmdlet reference (description prose) | `Degraded` |
| `Get-NetFirewallRule` cmdlet reference (**accepted values** list) | `Inactive` |

**This is an interpretation caveat, not a blocker**, and **no product or acceptance-policy change
follows from it.** It does not expand this diagnostic and it is not a second investigation.

Its only operational consequence: the number is the evidence, and if a capture returns that value,
whichever label is then attached to it is a **deliberate choice recorded as such** — never a name
picked from whichever reference happened to be open. That is the same
rendering-versus-specification divergence this whole investigation began with (raw `1` rendering as
"Enforced" where the specification says `Full`), appearing in a second field.

## 3. Corrected observation chronology

Two **independent** observations, not one. Logged UTC and file mtime are kept separate — only the
first is the run's own testimony.

| # | observation | evidence | time |
| --- | --- | --- | --- |
| 1 | product's post-write verify-query | `001-setup-receipt.txt` (**logged**) | bounded inside `[23:38:10Z, 23:38:19Z]` |
| | | `001-setup-command.err` (mtime) | 23:38:18Z |
| 2 | D1 diagnostic invocation — a **separate** invocation, per findings | `census.log tag=b-setup-post` (**logged**) | after 23:40:00Z |
| | | `d1-…-snapshot.json` (mtime) | 23:40:07Z |
| | | `002-teardown-request.txt` (mtime) | before 23:40:45Z |

**Roughly 109 seconds separates the two artifact writes** — *artifact writes, not measured snapshot
instants*. There is no D1 `START`/`END` row in `timeline.txt`; that is a gap in the run's own
instrumentation, and the bounds above are the tightest honest ones rather than a reconstruction.

**What this does and does not establish.** `[5, 20]` **was observed again later**. That is not proof
the state never cleared between the two observations — nothing observed the interval. Whether later
codes converge to `[1]` is a **separate question** from the registered-populated-timing experiment;
neither substitutes for the other.

**Attempt 2's `{ProfileInactive, NoLocalUser}`** display strings are **consistent with** the same two
conditions. They are **not proof of identical numeric state**, and are not to be equated without that
run's raw evidence.

## 4. What was written, versus what was captured

Write template, `render_writes` (windows.rs:460-478 at the subject sha), one call per half:

```
New-NetFirewallRule -PolicyStore PersistentStore -Name '{name}' -DisplayName '{name}' -Group $ruleGroup `
    -Enabled True -Direction Inbound -Action Allow -Profile {profile} -Protocol TCP -LocalPort {port} `
    -RemotePort Any -LocalAddress Any -RemoteAddress {remotes} {program_arg}-Package Any -Service Any `
    -InterfaceAlias Any -InterfaceType Any -Authentication NotRequired -Encryption NotRequired `
    -LocalUser Any -RemoteUser Any -RemoteMachine Any -OverrideBlockRules $false -DynamicTarget Any `
    -EdgeTraversalPolicy Block -ErrorAction Stop | Out-Null
```

Governing constants: `DESIRED_TAILNET_PROFILE = "Any"`, `DESIRED_LAN_PROFILE = "Private,Domain"`,
`DESIRED_TAILNET_REMOTES = ["100.64.0.0/10"]`, `DESIRED_PROGRAM = false` (so `-Program` is **absent
entirely**, not empty).

| field | written | captured, rule 0 (tailnet) | captured, rule 1 (lan) | agrees? |
| --- | --- | --- | --- | --- |
| name | the two owned names | `spt-core-bootstrap-inbound-tcp` | `…-tcp-lan` | ✅ |
| profile | `Any` / `Private,Domain` | `Any` | `Domain, Private` | ✅ (host word order) |
| remotes | `100.64.0.0/10` / derived LAN | `100.64.0.0/255.192.0.0` | `192.168.1.0/255.255.255.0` | ✅ (mask form) |
| port | bound port | `['29470']` | `['29470']` | ✅ |
| program | argument absent | `Any` | `Any` | ✅ |
| source | `-PolicyStore PersistentStore` | `sourceType Local` | `sourceType Local` | ✅ |
| hygiene rollup | enabled/inbound/allow, no restrictions | `true` | `true` | ✅ |
| **enforcement** | — | **`[5, 20]`** | **`[5, 20]`** | **the refusal** |

**Every captured field matches what was written.** That is what makes the two codes interesting:

- **`-Profile Any` is written for the tailnet half, and it still reports `InactiveProfile`.** A rule
  scoped to *all* profiles reporting an inactive-profile condition is not explained by its own
  profile argument.
- **`-LocalUser Any` is written explicitly on both halves, and both report `LocalUserEmpty`.**

**The captured projection cannot settle this**, and that is the crux: the query's `Rule` struct
projects only name, program, ports, profile, remotes, `hygiene` and `enforcement`. **There is no
local-user field in the capture at all** — `hygiene` is a boolean rollup that includes "no user or
machine restriction", not the field itself. So the evidence shows a disagreement between the written
argument and the reported condition, and contains nothing that resolves it.

## 5. The smallest missing observation

Two properties **already present on the objects the query enumerates**, and not currently projected:

| property | type | documented as |
| --- | --- | --- |
| `Status` | `string` | "The detailed status of the rule, as a string" |
| `StatusCode` | `uint32` | "The detailed status of the rule, as a numeric error code" |
| `PrimaryStatus` | `uint16` | Unknown 0 / OK 1 / Degraded 2 / Error 3 |

Plus, one level out, the rule's **own security filter** (`LocalUser` / `RemoteUser` / `RemoteMachine`
as stored) and the host's **currently active profiles**.

**How that discriminates:**

| observation | reads as |
| --- | --- |
| stored `LocalUser` is `Any`, as written, while ActiveStore says `LocalUserEmpty` | **provider/host state or an unmodelled semantic** — the write did what it said |
| stored `LocalUser` is empty/null despite `-LocalUser Any` | **rule-construction defect** — the argument did not take as intended |
| host's active profile set excludes the rule's `Profiles` | **host state** — `InactiveProfile` is descriptive |
| rule's `Profiles` bitmask disagrees with the written `-Profile` | **construction defect** |

`Status` / `StatusCode` are the cheapest of these by a wide margin: same class, same enumeration, no
new query surface — the provider's own prose explanation of the condition it is already reporting
numerically.

**Two hypotheses this would separate, neither adopted here:**

1. **Construction defect** — something about how the pair is written leaves a condition unresolved.
2. **Descriptive-not-faulty** — `LocalUserEmpty` may simply describe a rule carrying no local-user
   restriction, in which case `[5, 20]` could be an ordinary state for a rule of this shape rather
   than a fault. That reading would bear directly on strict-`[1]` acceptance. **It is a hypothesis;
   acceptance is unchanged and is not mine to change.**

The enumeration's `…Empty` family (`LocalAddressResolutionEmpty`, `ApplicationResolutionEmpty`,
`RemoteUserEmpty`, `LocalUserEmpty`) reads as "this condition resolved to empty", which in firewall
terms more often means a rule that cannot match than one that is unrestricted — which is an argument
*against* hypothesis 2 and *for* measuring rather than reasoning.

## 6. Not concluded

- Why either condition holds for these rules. Both codes are now **named**; neither is **explained**,
  and a name is not a mechanism.
- Whether `[5, 20]` ever clears.
- Whether the pair admits traffic. `REACHABILITY=NOT_TESTED`, by the run's own ceiling.
- Port-plus-remote scope is treated as **already ruled**; `program=Any` does not reopen it.

Sources: <https://learn.microsoft.com/en-us/windows/win32/fwp/wmi/wfascimprov/msft-netfirewallrule>
