Skip to content

fix(firewall): stop the rule audit timing out and echoing its own script - #259

Merged
dbfx merged 1 commit into
mainfrom
fix/firewall-audit-scan-timeout
Jul 27, 2026
Merged

fix(firewall): stop the rule audit timing out and echoing its own script#259
dbfx merged 1 commit into
mainfrom
fix/firewall-audit-scan-timeout

Conversation

@dbfx

@dbfx dbfx commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Makes the Firewall Audit scan complete instead of hitting its timeout, and makes it report a real reason when it genuinely fails.

  • Bulk filter join. Enumerate each firewall filter class once with -All and join on InstanceID (which is the rule's Name), rather than resolving three CIM associations per rule. -All can be denied on a policy store, so a rule missing from an index still falls back to its own association query.
  • Memoized Authenticode checks. Certificate revocation lookups can block on the network, and several rules routinely point at the same binary.
  • Streamed stdout via a new spawnTrackedLines helper in exec-utf8.ts, replacing the buffered execFile.
  • Real error messages. stderr, or the exit code — never the script. Applied to applyFirewallChanges too, which had the same trap.
  • Partial results are returned with truncated: true and an amber banner instead of being discarded.

Why?

Fixes #246.

The reported symptom was Error invoking remote method 'firewall:scan' echoing the entire PowerShell script with nothing below it. The issue suggested [Console]::OutputEncoding throws when PowerShell runs with no attached console. I tested that directly against the exact spawn shape Kudu uses (execFile + windowsHide: true + piped stdio) and it is not the cause — the preamble succeeds, [Console]::OutputEncoding.CodePage is 65001, accented output round-trips, exit code 0.

The actual cause is the 120 s timeout. Each $r | Get-NetFirewall*Filter is a CIM association query costing ~80 ms, and the scan did three per rule — measured at 244 ms/rule. Past roughly 500 inbound rules that alone exceeds 120 s, so Node kills PowerShell; and because the script sets $ErrorActionPreference = 'SilentlyContinue', nothing reaches stderr. Node's rejection message is Command failed: <the whole command line>, which for this script is several kilobytes of echoed PowerShell with the real reason nowhere in it. That is exactly the reporter's screenshot.

Two things follow from streaming rather than buffering:

  • The PROG| markers the script already emits only move the progress bar if they arrive while the scan is running. Buffering meant every progress event landed at once, after the scan was over.
  • A scan cut short can return the rules it did stream. A security audit must not present a partial rule set as a clean bill of health, hence the explicit truncated flag and banner rather than silently showing fewer rules.

How to test

  1. Open Kudu → Security → System Hardening → Firewall Audit, click Scan firewall rules. It should list inbound rules, with the progress bar advancing during the scan rather than jumping at the end.
  2. To confirm the output is unchanged, extract the String.raw script from scanFirewallRules on this branch and on main, run both, and diff the RULE| lines — they should be identical.

Measured on a real 204-rule machine, unelevated (the worst case: the address-filter index is denied, so every rule still takes the slow fallback path):

before after
scan wall time 55.2 s 20.5 s
RULE| output byte-identical

Kudu ships a requireAdministrator manifest, so in the app all three indexes populate and the join covers every rule.

End-to-end through the real scanFirewallRules: rules=204 total=204 truncated=false progressEvents=9.

Checklist

  • Tested on my platform (Windows / macOS / Linux) — Windows 11 Pro 26200
  • npm test passes — 2183 tests / 95 files, including 16 new (8 for spawnTrackedLines, 8 covering scan parsing, streamed progress, and the error/truncation paths)
  • npm run build succeeds
  • Commit messages follow Conventional Commits

Typecheck is unchanged at 206 pre-existing errors before and after, none in the touched files.

Resolving each rule's filters through the CIM association
($r | Get-NetFirewallPortFilter) costs ~80 ms, and the scan did three per
rule. Past roughly 500 inbound rules that alone exceeded the 120 s execFile
timeout, so Node killed PowerShell -- and because the script sets
$ErrorActionPreference to SilentlyContinue there was no stderr, leaving the
rejection message as nothing but the entire echoed script.

Enumerate each filter class once with -All and join on InstanceID instead,
keeping the per-rule association query as a fallback for anything missing
from an index (-All can be denied on a policy store). Verified byte-identical
RULE output against the old script; a 204-rule machine went from 55.2 s to
20.5 s unelevated, where the address-filter index is denied and every rule
takes the slow path. Authenticode verdicts are memoized per path since
revocation checks can block on the network.

Also stream stdout instead of buffering it, via a new spawnTrackedLines
helper. The PROG markers the script emits only move the progress bar if they
arrive during the scan, and streaming lets a run that is cut short return its
partial rules flagged as truncated rather than failing outright. When a run
genuinely fails, report stderr or the exit code -- never the script.

Fixes #246

@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: 79578ea48b

ℹ️ 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".

pending += chunk
let nl = pending.indexOf('\n')
while (nl !== -1) {
onLine(pending.slice(0, nl))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Catch streamed-line callback failures

If onLine throws—for example, when the firewall progress callback races with destruction of the Electron webContents and send() rejects—the exception escapes this asynchronous EventEmitter handler instead of rejecting spawnTrackedLines. In the main process that becomes an uncaught exception and can terminate the application while leaving this promise unsettled; wrap callback invocation, terminate the child, and reject the operation.

Useful? React with 👍 / 👎.

@dbfx
dbfx merged commit f1c3799 into main Jul 27, 2026
9 checks passed
@dbfx
dbfx deleted the fix/firewall-audit-scan-timeout branch July 27, 2026 02:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug with Firewall audit scan

1 participant