Skip to content

feat(machine-health): add browser-extension, network-listener and firewall-rule checks - #8940

Merged
kyle-sexton merged 11 commits into
mainfrom
feat/machine-health-intruder-inventories
Oct 21, 2026
Merged

kyle-sexton merged 11 commits into
mainfrom
feat/machine-health-intruder-inventories

Conversation

@kyle-sexton

Copy link
Copy Markdown
Member

No related issue: Phase 7 of the owner-approved machine-health PC-checkup plan (ADR 0081, #8906).

Summary

The owner asked to "look for anything that shouldn't be here". This PR adds three intruder inventories, each as mutable FindingItems:

  • browser extensions, including policy-forced ones;
  • listening network ports with their owning programs;
  • foreign inbound firewall allow rules. The firewall check is INFO only.

There is one commit per check, followed by the review and verifier rounds.

Fix

Browser extensions

  • Get-ChromiumExtension merges Preferences with Secure Preferences, and drops and counts the stub entries that have no location. Each profile is read inside its own try. Files have a size cap, and a profile name that is not a single folder name is refused.
  • Get-FirefoxExtension handles Firefox.
  • Get-ExtensionPolicy reads both forcelist keys for Edge and Chrome, in both hives, labeled by hive.
  • New-ExtensionRecord is the one record constructor.
  • An extension whose self-reported location fails a cross-check reads WARN: an absolute path, policy with no matching policy entry, or component outside the browser install folder.
  • An off-store forced entry reads listed-off-store.
  • An unreadable profile makes the check UNKNOWN and is listed in unreadable_sources, but the check still keeps the other profiles' items.

Network listeners

  • Get-ListenerOwner resolves each process image with the kernel-supplied QueryFullProcessImageNameW, falling back to Get-Process.
  • Listeners owned by the kernel http.sys are traced to their owning process through netsh http show servicestate view=requestq.
  • An unresolved owner is UNKNOWN with no key. Loopback is INFO; exposed plus unsigned, or a user-writable path, is WARN. Any unreadable source forces UNKNOWN.

Firewall rules

  • Get-FirewallRuleInventory makes bulk reads of rules, SystemDefaults names, application filters (exactly one call) and port filters.
  • A per-rule fallback runs only for grouped candidates the bulk read missed. It is capped at 20 calls and 15 s.
  • A grouped candidate whose port data is still unread gets no key, so no mute is ever computed from missing data.
  • UNC and network-drive programs are never probed.
  • The check is INFO on every path. baseline_hosts = 2 after the second-host check.

Shared

  • New-FindingItem -NoKey builds unkeyed items.
  • Format-MachineText caps machine strings at 300 characters and strips C0, C1, ESC, bidi and U+2028/2029 characters.
  • Summaries carry counts only.

Catalog and changelog

  • Catalog entries 23 to 25, all security, and sections 23 to 25, with pointers anchored to the Learn sections and pinned Chromium source lines.
  • Changelog fragment (minor).

Verification

  • run-tests.ps1 exits 0: 985 passed, 0 failed, 1 skipped. All 10 filtered suites exit 0.
  • A8: the candidate rule names on the second fleet host do not intersect this host's SystemDefaults names, in both directions. Both hosts have identical SystemDefaults (409 names).
  • A9: every not-found program fails Get-Item with item-not-found, never access-denied (7 of 7).
  • Host smoke exits 0: browser-extensions 1418 ms, network-listeners 14573 ms, firewall-rules 3766 ms.
  • Security review: 6 IMPORTANT. Fixed: a failed read hidden behind items, a spoofable process path, http.sys owners, a trusted self-reported location, unbounded machine text, network-share probes, and malformed JSON. A mute digest and the svchost ServiceDll and ACL checks are deferred follow-ups.
  • Code-design review: 2 CRITICAL (hand-built unkeyed items, a duplicated extension record shape) and 3 IMPORTANT, all fixed.
  • Fresh-context phase verifier: passed 10 of 12, and both gaps are fixed. They were the browser check reporting a failed run on one bad profile, and anchors on the Chromium pointers.

Related

The sibling top-processes and backup PR adds the generic appendix arm that renders these items in the markdown report.

🤖 Generated with Claude Code

kyle-sexton and others added 10 commits October 21, 2026 17:43
Adds the browser-extensions check (catalog 23, security): every Chrome,
Edge and Firefox extension per profile as a mutable FindingItem, and the
Edge and Chrome forcelist and ExtensionSettings policies read in both
hives. Unpacked, command-line, sideloaded, unsigned or foreign-installed
extensions and high-attention permissions are WARN; an unreadable source
is UNKNOWN. Location-less Chromium stubs are dropped and counted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the network-listeners check (catalog 24, security): each listening
program, protocol and port as a mutable FindingItem, resolved through the
process path, then the hosting service, else left unresolved. Exposed
listeners whose binary is unsigned or user-writable are WARN; unresolved
listeners carry no key and read UNKNOWN, never OK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the firewall-rules check (catalog 25, security, INFO always): inbound
allow rules whose Name is not in the SystemDefaults store, read in bulk
and joined on InstanceID, with per-version rules grouped so an update
keeps the mute. program_state separates a missing program from one under
a folder the caller cannot traverse. A per-version rule the unelevated
port-filter read missed gets a capped per-rule read; one still unread is
listed without a key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A second fleet host holds the same SystemDefaults rule names, and neither
host's candidates intersect the other's SystemDefaults names, so
firewall-rules now reports baseline_hosts = 2 and section 25 records the
confirmation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New-FindingItem gains -NoKey/-NoKeyReason for an item whose identity
could not be read: key null, never muted, the reason recorded. Every
title passes through the new Format-MachineText, which caps machine
text and replaces control, escape and bidi-override characters; checks
use it for their machine-derived fields. output-schema.md says when key
is null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An unreadable browser, profile file or policy key now makes the check
UNKNOWN even when items are listed. Each profile reads inside its own
guard, with a size cap, an object-root check and TryParse for location.
Chromium claims are cross-checked: an absolute install path, a policy
location no policy names, and a component outside the browser install
are flagged. Off-store forcelist entries are listed-off-store unless the
extension is installed. Items name the profile folder, update URLs lose
their query string, and machine text is sanitized. Both adapters build
records through New-ExtensionRecord.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
….sys queues

Listener owners now resolve from the kernel-reported image path
(QueryFullProcessImageNameW), with Get-Process's path kept as a
process-reported fallback. Kernel TCP ports served by http.sys are traced
to their request queue owner through netsh; an owner that cannot be
found, or unreadable netsh output, leaves the port unresolved. An unread
socket table or service list now makes the check UNKNOWN even with items
listed, unresolved listeners are built as unkeyed FindingItems, and
machine text is sanitized.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unts

firewall-rules never probes a UNC path or a network-drive program (it
reports them unknown), runs its program probes under the adapter's time
budget, keeps rule names out of the summary, builds unkeyed items through
New-FindingItem, sanitizes machine text, and has one catch instead of
two. Catalog section 25 keeps the rubric and points to the adapter for
the mechanics; the listener check's bind-rank comment now points to
section 24.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e names

Section 25's baseline and missing-program records named capture files
and a plan that live outside the public repository. Both now state the
decision and what was observed on the fleet hosts, with the as-of date
and recheck trigger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and tighten sources

One malformed profile no longer fails browser-extensions: it reads
UNKNOWN, keeps the other profiles' items, leaves ran_successfully true,
and names the unread sources in detail.unreadable_sources. Chromium
pointers now carry commit and line anchors; section 24 documents the
fixed `system` identity for kernel-owned listeners; Format-MachineText
also replaces U+2028 and U+2029; test headers cite the catalog or
describe their redacted fixture shapes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T21:53:04.610580Z 54241f9 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review reviewed b5dece2f273114fede39399207b1787768f4d969: 0 finding(s), 0 as inline comments.

Security review (incremental, 54241f9..b5dece2): no security findings.

I read .claude-lane/incremental.diff for the eight changed files. The protected-class list in docs/conventions/instruction-exception-register/README.md was not consulted. The diff deletes no standing instruction, so that list does not apply.

  • Test-BrowserExtensions.ps1, Get-ExtensionPolicy.ps1: detail.policy_entries now includes ExtensionSettings entries that install an extension, plus a new source field and an install_mode field.
    • install_mode comes from registry policy JSON, which is untrusted machine text. It goes through Format-MachineText like the other string fields, and the new Pester test "sanitizes machine text in policy entries" checks that an ESC sequence is stripped.
    • update_url is still stripped of its query string in New-PolicyEntry, so tokens in query strings do not reach the report.
    • The audit stays read-only and unelevated. The diff adds no command execution, network access or file writes.
  • Get-FirefoxExtension.ps1: the diff adds entries to the location-to-kind lookup table (app-builtin-addons, app-system-profile, app-global) and a comment. The table is static data.
  • Documentation and test files: the changes to check-catalog.md and the three test files are comments, doc text and test expectations only.
  • Plugin manifests: the diff touches no plugin.json or marketplace.json, so there are no manifest <field> lines to report.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 54241f9f50

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

claude-review reviewed b5dece2f273114fede39399207b1787768f4d969: 0 finding(s), 0 as inline comments.

Incremental review (54241f9..b5dece2): no findings

Read the incremental diff against the PR context and the surrounding code. Network was blocked, so I could not open the new Mozilla source links or the replacement netsh http Learn link.

  • Policy entries: Test-BrowserExtensions.ps1 no longer skips extension-settings rows. Each entry now carries source and a sanitised install_mode, and Get-ExtensionPolicy.ps1 fills InstallMode from installation_mode.
    • blocked_marker_count, policy_entry_count and claimedBy count the same set as before plus the installing ExtensionSettings ids.
    • The summary says "policy entries" now. A search of the plugin found no other consumer of the old "forcelist" wording.
    • The new tests cover an off-store ExtensionSettings entry and escape-character sanitising of install_mode.
    • The catalog's "Policy entries" bullet matches the code.
  • Firefox locations: the added names (app-builtin-addons, app-system-profile, app-global) now map to a kind. Firefox never gets the Chromium install-folder cross-check, so a component kind exempts the extension from high-attention permission reasons. That was already true for the existing component locations, and app-system-profile is now in that group. I did not flag it: it extends an existing design choice, and I could not verify the Mozilla constants.
  • Docs and test headers: the new header comments, as-of pointers and citations follow the skill-body rule (pointer, as-of date, recheck trigger).
  • Manifests and closing issue: no userConfig or manifest files changed, so there are no manifest <field> lines. The PR has no closing issue ("No related issue"), so there were no acceptance criteria to check.

I ran no tests myself. The PR's pass counts are author-claimed and unverified.

…icy_entries

detail.policy_entries now carries every installing ExtensionSettings id
beside the forcelist rows, labeled by source, with its hive, update URL
without the query string, and install mode, so an off-store entry that
raises blocked_marker_count is identifiable. Machine text in each entry
is sanitized. Catalog section 23 pins the Mozilla signedState and
install-location sources and says the kind mapping is our judgment; the
netsh pointer in section 24 follows Learn's redirect to a live anchor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit 1e8a698 Oct 21, 2026
27 checks passed
@kyle-sexton
kyle-sexton deleted the feat/machine-health-intruder-inventories branch October 21, 2026 22:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant