Skip to content

Exclude shell-spawning ProcessRunner tests on Windows (EDR alerts) - #163

Merged
0101 merged 1 commit into
mainfrom
edr-exclude-shell-scaffolding-tests-on-windows
Aug 3, 2026
Merged

Exclude shell-spawning ProcessRunner tests on Windows (EDR alerts)#163
0101 merged 1 commit into
mainfrom
edr-exclude-shell-scaffolding-tests-on-windows

Conversation

@0101

@0101 0101 commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Problem

Five tests in ProcessRunnerTests.fs shell out to get a real child process that outlives a deadline, overruns a capture limit, or spawns a grandchild for the process-tree kill to reach.

On Unix that shell is sh and is unremarkable. On Windows it is PowerShell, and the resulting command line trips Defender for Endpoint's "Suspicious PowerShell command line" rule on managed devices. This has now raised two security incidents:

  • 30 Jul 2026 — traced to a scratch script written while investigating this same file.
  • 03 Aug 2026 (MSDetectID 24562977) — the timeout test itself, from a Release run in a feature worktree. The alert quoted the %TEMP%\treemon-process-runner <guid>\child.pid path written by that test.

Each incident requires a manual attestation, and the frequency scales with the number of parallel worktrees — which is precisely the workflow this repo exists to support.

Why not just change the command

What scores is the scaffolding, not the code under test. ping 127.0.0.1 -n 30 is a documented evasion sleep (MITRE ATT&CK T1497.003), and around it sit an inline -Command, a detached -PassThru spawn, a PID persisted under a GUID-named temp directory, and Wait-Process — collectively the shape of a loader.

Substituting the delay binary does not fix it:

  • timeout /t and waitfor /t are the same technique.
  • Start-Process powershell -Command 'Start-Sleep 30' is worse (PowerShell spawning PowerShell).
  • The surrounding shape still scores with the delay removed entirely.

Critically, the rule is a heuristic we cannot read, and the only way to test a tweak is to provoke another incident and wait days. Deliberately tuning a command line until it stops tripping EDR, on a monitored device, is also itself the most attacker-shaped activity in this story. There is no safe iteration loop, so the right move is to leave the detection space rather than try to score just under the threshold.

The 30 Jul investigation concluded the ping was "incidental" and left the test in place. Four days later, same test, same machine, second incident.

Change

Exclude those five tests on Windows only, via [<Platform(Exclude = "Win")>].

CI runs ubuntu-latest, so the sh branches continue to cover real-process timeout, process-tree termination, and the shell-driven truncation paths on every push. No coverage is lost in CI — a Windows developer loses only local pre-push feedback on these five.

A comment at the top of the file records the incidents, why tweaking the command does not work, and the re-enable plan, so the reasoning is not lost:

Add src/TestChild (OutputType=Exe, ~40 lines) supporting sleep <ms>, spawn-child <pidFile> <ms>, and emit --stdout <n> --stderr <n> --exit <code>; reference it from Tests.fsproj and resolve it at AppContext.BaseDirectory.

That approach was validated in a scratch probe before being written down: a ProjectReference to an Exe copies its apphost into the test output (Cli.exe and Treemon.exe already arrive that way, and FSharp.Core.dll is already present), and Kill(entireProcessTree = true) reaches the grandchild exactly as it does today. It would also retire the four IsOSPlatform command branches and both quote-escaping schemes in this file.

Verification (Windows)

Check Result
--filter FullyQualifiedName~ProcessRunner 10 passed, 0 failed (was 15 — the five are excluded, not skipped)
--filter Category=Fast 1884 passed, 0 failed (two runs)
powershell (5.1) processes spawned by the suite 0

On process sampling: the first sampled Fast run observed one powershell process, which did not reproduce in a second run, and no reachable Fast-tier code path spawns powershell after this change (the terminal fixture that does is Explicit, see below). This machine has heavy ambient PowerShell activity, so that observation is attributed to noise rather than the suite.

Remaining PowerShell spawns during the suite are pwsh -NoProfile -File from the post-fork hook tests. Those are deliberately left alone: they are file-based script execution with no inline -Command, no delay binary, and no PID harvesting, and they mirror exactly what production does at GitWorktree.fs:731 — converting them would mean no longer testing the real feature.

Linux behaviour is unchanged and is validated by CI on this PR.

Scope note

The terminal-session fixture (TerminalSessionFixture.fs) enumerates processes via powershell.exe + Get-CimInstance Win32_Process, which is a stronger detection shape than the ping. It is not touched here because its consumers are already Category("Local") + Explicit, so it never runs in routine or CI runs — only when invoked deliberately.

Five tests spawn a shell to get a child process that outlives a deadline,
overruns a capture limit, or spawns a grandchild for the process-tree kill to
reach. On Unix that shell is `sh`; on Windows it is PowerShell, and the
resulting command line trips Defender for Endpoint's "Suspicious PowerShell
command line" rule on managed devices. That has raised two security incidents
(30 Jul 2026, and 03 Aug 2026 / MSDetectID 24562977 — the latter quoting the
`%TEMP%\treemon-process-runner <guid>\child.pid` path written by the timeout
test).

What scores is the scaffolding, not the code under test: `ping 127.0.0.1 -n 30`
is a documented evasion sleep (MITRE T1497.003), surrounded by an inline
`-Command`, a detached `-PassThru` spawn, a PID persisted under a GUID-named
temp directory, and `Wait-Process`. Swapping the delay binary does not help —
`timeout /t` and `waitfor /t` are the same technique, the surrounding shape
still scores, and the rule is a heuristic that cannot be read, so no tweak can
be confirmed short of provoking another incident.

Excluded on Windows only, via `[<Platform(Exclude = "Win")>]`. CI runs
ubuntu-latest, so the `sh` branches keep covering real-process timeout,
process-tree termination, and the shell-driven truncation paths on every push;
a Windows developer only loses local pre-push feedback on these five.

The file comment records the re-enable plan (a small `src/TestChild` helper
process replacing the shell scaffolding) so the fix is not lost.

Verified on Windows: ProcessRunner filter 10 passed / 0 failed (was 15, the
five excluded), Fast suite 1884 passed / 0 failed, and no `powershell` process
is spawned by the suite.
Copilot AI balanced review requested due to automatic review settings August 3, 2026 12:02

Copilot AI 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.

Pull request overview

Excludes five EDR-triggering PowerShell-based process tests on Windows while retaining Linux CI coverage.

Changes:

  • Adds Windows exclusions to five shell-spawning tests.
  • Documents the security rationale and re-enable plan.
  • Centralizes the NUnit exclusion reason.
Show a summary per file
File Description
src/Tests/ProcessRunnerTests.fs Adds Windows-specific exclusions and supporting documentation.

Review details

  • Files reviewed: 1/1 changed files
  • Comments generated: 0
  • Review effort level: Balanced

@0101
0101 merged commit 72759e6 into main Aug 3, 2026
2 checks passed
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.

2 participants