nordlo.windows.gporeport 0.2.0-rc.1

Creates read-only ADMX/GPO cross-reference reports for a single Active Directory domain as a self-contained SQLite snapshot.

No packages depend on nordlo.windows.gporeport.

Added

  • Windows Firewall with Advanced Security is now parsed properly. It was reaching the report through the generic extension walker, which got two things wrong. Profile settings shattered into a row per setting named after the setting alone, so the domain, private and public profiles each produced a row called EnableFirewall with nothing saying which profile it belonged to. And a rule came out under its wire field names -- Dir, LPort, RA4, EmbedCtxt -- with a grid summary of Version | Action | Active, which reads identically for nearly every rule in a domain.

    A typed handler (ConvertFrom-GpoFirewallSetting, dispatched on the WindowsFirewallSettings xsi:type) now emits one row per profile and one row per rule, with fields labelled the way the WFAS console labels them, protocol numbers resolved (6 → TCP), directions and actions spelled out, and profiles normalised to a consistent Domain, Private, Public order. A rule's summary is what identifies it at a glance -- direction, verdict, protocol and port or program, profiles -- and a rule with Active=FALSE is marked (rule disabled), since a configured rule doing nothing must not read the same as a live one. Nothing is evaluated: addresses and ports are reported as configured, with no claim about what traffic they permit.

    Both rule encodings are handled. ConvertFrom-GpoFirewallRuleString parses the registry.pol form, the single pipe-delimited v2.31|Action=Allow|Dir=In|... string the CSE actually stores under Software\Policies\Microsoft\WindowsFirewall\FirewallRules -- values that orphan permanently by design (no ADMX describes them, which is why orphaned-settings classes that key CSE-owned). Unknown fields are kept under their own name rather than dropped, and a firewall element the handler does not recognise falls back to the generic walker with a warning, so a later Windows adding to the schema is visible rather than silently mis-rendered.

  • Firewall rules stored in registry.pol are decoded too, not just those in the report XML. The CSE stores one rule as one registry value under Software\Policies\Microsoft\WindowsFirewall\FirewallRules (and ConSecRules for connection security rules), named with a GUID, with the whole rule packed into the value data. No ADMX describes them, so they orphan permanently and appeared in the orphan views as an unreadable blob next to a meaningless value name — and since nothing is missing, "open it in GPMC" was the only alternative to reading the pipe string by hand.

    A new private Get-GpoDecodedValueText renders those values through the same rule formatter the report-XML path uses, so a rule reads identically whichever source it came from. gpo_setting_values gains a value_decoded column, NULL for the overwhelming majority of values, which already say what they mean. The orphan expansion grows a Rule column beside the raw value — shown only under keys where something actually decoded — and all-settings carries the reading as a column. Dispatch is on the registry key, never on the shape of the data: a value that merely contains pipe characters is not a firewall rule, and guessing from shape alone would put confident nonsense in the report. The reading also joins the GPO search index, so a rule can be found by the name that exists only in the decoding.

  • MUI indirect string references are resolved, with the reference kept beside the reading. A firewall rule created from a built-in template does not name itself in words: its name, description and group are pointers to string resources, @FirewallAPI.dll,-28756. A new private Resolve-GpoIndirectString resolves these through SHLoadIndirectString — the same API the firewall console calls — so the grid shows Inbound rule for the Remote Desktop service to allow RDP traffic. [TCP 3389] rather than the pointer.

    The reference is never discarded. gpo_other_setting_props gains a prop_raw_value column holding the value as the GPO stores it, NULL unless the displayed value is a rendering of something else, and the setting detail modal grows a "Raw value" column — shown only when a setting actually has one — under a note saying what the reading can and cannot claim. That pairing is the point: the resource is read from the machine that ran the scan, and a target on a different Windows build or MUI language can render the same reference differently, so the readable text is this host's reading and the reference is the authoritative record. A reference to a module the scanning machine does not have stays as written rather than becoming an error. Both forms are indexed for search, so a reference pasted in from GPMC still finds its rule.

  • Administrator comments on Administrative Templates settings are now reported. The Comment tab in the Group Policy editor writes to Machine\comment.cmtx / User\comment.cmtx, not to registry.pol, so a report built from registry.pol could not see the one field written in an administrator's own words -- usually the only record of why a setting is configured. The comment now appears as an "Administrator comment" section at the top of the setting detail modal, and as a comment column on every settings grain (all-settings, settings-per-gpo, settings-all-known, gpo-settings) — so the filter box on the Settings tab matches comment text like any other column, with the match highlighted. Comments are also folded into the GPOs tab's deep-search index, which makes a GPO findable by a ticket number, a colleague's initials or a phrase that appears nowhere in Microsoft's own vocabulary.

    Read by a new private Get-GpoPolicyComment and stored in a new gpo_setting_comments table, with the file's per-GPO namespace prefixes and string table resolved to the same <namespace>::<policyName> policy id the ADMX catalogue uses. Rows are not restricted to configured policies -- the editor keeps a comment after its setting goes back to Not Configured. A comment whose prefix the file never declared is kept with a NULL policy_id and a warning, rather than guessed at.

  • The full Get-GPOReport XML is now kept for every GPO and shown in the report: a collapsed "Full report XML" block at the foot of the expanded GPO row, and the same block in each setting's detail modal beside that item's own fragment, both with a Copy XML button that puts the raw document on the clipboard for an external tool. Previously only the per-item fragment survived the scan (gpo_other_settings.source_xml), so the whole document -- the authoritative answer whenever this tool's decoding is incomplete (recognized = 0) -- meant leaving the report for GPMC.

    Stored in a new gpo_report_xml table, gzipped as raw bytes in a BLOB and inflated in the browser with DecompressionStream. These documents compress roughly ten to one, which matters because the whole snapshot is base64-embedded into the HTML; a BLOB rather than base64 text avoids encoding the payload twice. Viewing the XML needs Chrome/Edge 80+, Firefox 113+ or Safari 16.4+; older browsers get a note pointing at Download database rather than an empty box. A GPO whose report could not be generated has no row, and its report offers no XML block at all.

  • Jump from a setting to the GPO that defines it. The GPO name in a setting detail modal is now a link that switches to the GPOs tab with that GPO filtered and expanded, and the conflict modal's list of writing GPOs is clickable the same way -- reusing the existing showGpoExpanded behaviour that until now was only reachable from the ADMX file expansion.

Changed

  • Orphaned settings now says which orphans are worth acting on, and lets you look inside one. The report was a flat count of registry keys with no backing ADMX, which is the shape of the gap but not an answer: several Group Policy areas orphan permanently by design, and the report gave a firewall key the same weight as a vendor template someone deleted from the store. Two columns now sort that out. likely_cause classifies the key itself — Managed policy branch (under a Policies branch, where a missing template is the usual explanation), Tattooing key (outside those branches, so the value survives the GPO no longer applying) and CSE-owned (certificate autoenrollment and trusted roots, Windows Firewall, software restriction policies — never ADMX-backed, so nothing is missing). live_gpo_count counts the carrying GPOs that could apply anywhere at all: an enabled link and the relevant hive switched on. Rows sort actionable-first, and the CSE-owned rows are recessed rather than tinted, being the one classification that is explicitly not a finding.

    In the HTML each row now expands in place to the GPOs carrying that key — with their links, hive status and a Live/Inert verdict, each clickable through to the GPOs tab — and to the actual values written under it, name, type and data. That is what separates an imported vendor baseline (expected; Import-GPO carries values but not templates) from a locally authored GPO whose template went missing, and a value name is usually recognisable enough to name the template that should be there.

  • Detail-modal caveats moved behind an (i) on the section heading. Notes such as "Which of these applies to a given machine or user is a matter of precedence, which this report does not resolve" were printed under every table that needed them — three lines of grey prose between the reader and the next answer, repeated four times in a modal with four sections, which is how a note worth reading once becomes wallpaper. Each is now folded into a native <details> on its own heading: click the (i) to read it, and it stays open while you work. Affects the precedence notes on "Also configured in…" and "Written by N GPOs", plus "This GPO applies to", "Editor metadata", "What this setting does", "How this policy maps to the registry" and "Administrator comment". Empty-state lines ("Not linked anywhere", "No GPO configures a setting from this file") are answers rather than caveats and stay visible, as do the per-item warning banners.

  • Link targets are shown as canonical names rather than distinguished namesfixture.test/Sites/Kista instead of OU=Kista,OU=Sites,DC=fixture,DC=test — in the GPO row expansion, dead-links and admx-to-gpo. A DN reads right to left; a canonical name reads the way the OU tree is navigated. The DN is unchanged in gpo_links.som_path and remains the key everything joins on and that the object counts were gathered against; the new ou_object_counts.som_canonical column carries the readable form, and the report falls back to the DN if it is absent. GPO search matches both forms, so a DN pasted from another tool still finds its GPO.

  • The report uses the width of the window it is given. An expanded GPO row capped its settings list at 40% of the window height and sized the table to its narrowest content, so a fifteen-setting GPO showed eight rows behind a second scrollbar nested inside the grid's own, with the Category column ellipsised next to two thirds of the screen left empty. The inner cap is now the larger of 78% of the window (to a 720px ceiling), and inner tables claim up to 1200px before they wrap. Detail modals grow to 1800px rather than 1400px, and their header facts pair up two per row on windows wider than 1180px instead of running down one column beside an empty half-panel.

  • The XML views are legible. The raw Get-GPOReport blocks -- an item's own fragment, a Scheduled Task's targeting text and the full report XML -- rendered at 11px, which is a squint for the one view you open precisely to read carefully. Now 13px with the line height opened to match, in a box raised from 40% to 78% of the window height (to a 640px ceiling) so the larger type does not show fewer lines than before.

Fixed

  • The per-GPO report XML is stored as a BLOB again, so the browser can actually decompress it. Compress-GpoReportXml guarded its byte[] return with Write-Output -NoEnumerate, which does not hold on PS 5.1 — -NoEnumerate stops Write-Output from unrolling the array, and the function's own output processing then unrolls it anyway. The caller got an Object[] of boxed bytes, which binds as TEXT rather than BLOB with no error anywhere along the way, and the stored column failed to inflate. Returned with the unary comma form instead.

  • The conflict detail modal no longer calls every conflict "different values". The verdict badge counted distinct GPO GUIDs instead of distinct written values, and since a row only reaches that modal because more than one GPO writes the target, the count was always greater than one. Rows where every GPO writes the same value now show same value in all, matching the grid's verdict.

  • Following a GPO link from a setting modal no longer traps the report in a redirect loop. The page flickered and stopped responding to input. Routing allows one hash write per navigation -- the guard that tells the report's own hashchange from a user's back/forward consumes exactly one event -- but jumping to a GPO wrote two: selectTab('gpos') wrote #gpos, then showGpoExpanded wrote #gpos/<guid>. The unclaimed second event read as a user navigation, re-entered routing, and wrote twice again, forever. Clicking a row on the GPOs tab was safe only by accident: the hash is already #gpos there, so the first write hits the equal-value early return and never fires. Arriving from another tab -- a setting modal's GPO link, or a Findings row drilling through -- made both writes real. Applying a route from the hash had the same defect independently, so back/forward onto a GPO deep link looped as well.

    Multi-step navigations now run inside a hold that collects their writes and performs only the last one, which is also the most specific. Applying a route is held wholesale, so back, forward and a pasted link resolve to the hash they started from and write nothing at all; a hash that genuinely needs normalising (an alias, an unknown view, a stale findings chip, a GUID no longer in the snapshot) is corrected with a single write.

  • An unresolvable security-filtering trustee shows its SID instead of (unnamed). GPMC omits the <Name> element when it cannot resolve a trustee — a deleted account, or one from a domain the scanning host cannot reach — and the parser printed a placeholder, discarding the SID that was sitting in the same element. Five trustees across four GPOs in the reference domain read (unnamed) in "This GPO applies to", which says nothing about who the GPO applies to. The SID now stands in as the principal, so the entry can be chased down.

  • Group Policy Preferences registry collections are no longer swallowed. Registry preferences created by the GPMC wizard nest one <Collection> per key segment, and every Collection carries a name attribute — which made the outermost one look like a settings item. The walker claimed it, never descended, and reported a single meaningless row (item HKEY_LOCAL_MACHINE, summary \ = ()) while silently discarding every real value beneath it. Two GPOs in the reference domain lost five values each this way. <Collection> is now always treated as a container.

  • Folder Redirection no longer fragments. <Folder Id="Documents"> identifies itself with an Id attribute rather than the name the rest of the report uses, so the walker did not recognise it as an item and descended into it — turning one redirected folder into four rows (DestinationPath, the security group, GrantExclusiveRights, MoveContents), none of which is a setting. Id is now an item-identifying attribute alongside name, and supplies the row name instead of being repeated as a property.

  • Registry preference summaries handle empty fields. Wizard items come in shapes where hive, name, type or value are empty — a key-only item that creates the key and writes nothing, and a default-value item. Both rendered as \SOFTWARE\...\ = (). They now read ...\{GUID} (key only, no value) and ...\(Default) = (empty) (REG_SZ).

Any 0.0

Version Downloads Last updated
0.2.0-rc.1 3 08/18/2026
0.1.0 3 07/31/2026
0.1.0-rc.3 18 07/31/2026