Skip to content

Set-DbaPrivilege - Write secedit's working database to temp, not cwd - #10544

Open
potatoqualitee wants to merge 4 commits into
developmentfrom
fix/set-dbaprivilege-secedit-tempdir
Open

Set-DbaPrivilege - Write secedit's working database to temp, not cwd#10544
potatoqualitee wants to merge 4 commits into
developmentfrom
fix/set-dbaprivilege-secedit-tempdir

Conversation

@potatoqualitee

@potatoqualitee potatoqualitee commented Aug 8, 2026

Copy link
Copy Markdown
Member

Summary

A user reported that Set-DbaPrivilege leaves secedit.sdb and secedit.jfm behind in the current working directory after it runs.

Root cause: the command calls secedit /configure /cfg $tempfile /db secedit.sdb /areas USER_RIGHTS /overwrite /quiet. /db takes a bare, relative filename, which secedit.exe resolves against the process's current directory - not $env:TEMP - so the working database (and its .jfm journal file) land wherever the caller's shell happened to be.

  • public/Set-DbaPrivilege.ps1: secedit's three working files - the exported .cfg, the /db database .sdb, and its .jfm journal - all now live under an absolute $temp path, named with a single Get-Random token generated once per invocation and threaded through the export, configure, and cleanup Invoke-Command2 calls via splatted -ArgumentList. Tokenizing all three (not just the .sdb/.jfm pair) matters: two Set-DbaPrivilege runs against the same computer at the same time would otherwise still collide on, or delete, each other's shared .cfg file and apply the wrong privileges. Cleanup removes all three tokenized files alongside the working database.
  • tests/Set-DbaPrivilege.Tests.ps1: added a real (non-mocked) IntegrationTests Describe block. A single combined test grants CreateGlobalObjects, watches temp with a FileSystemWatcher (via a bounded Wait-Event) to positively confirm the tokenized .sdb database is actually created under temp during the run, tracks the exact path(s) observed and confirms they're gone afterward, and separately checks the working directory for both the tokenized pattern and the legacy hardcoded secedit.sdb/secedit.jfm names (so a regression back to the pre-fix filename would still be caught). AfterAll reverts the granted privilege unless the user already held it, only doing so once BeforeAll has actually confirmed the prior state, validating secedit's exit code, and cleaning up its own scratch files in a finally block.

This same bug also exists in the compiled C# port (dbatools.library, SetDbaPrivilegeCommand.cs) - see the companion PR at dataplat/dbatools.library.

Follow-up fixes from automated review (2 rounds)

  • Fixed the pre-existing Invoke-Command2 mock in the Set-DbaPrivilege regressions unit test, which discriminated "set privileges" vs. "cleanup" calls by $ArgumentList.Count, and later derives the same per-run token dbatools now uses so its fabricated .cfg path matches what the real scriptblock reads/writes.
  • Fixed a SID-removal regex in the integration test's AfterAll that could match a SID as a string-prefix of a different, longer SID sharing the same prefix.
  • Splatted every Invoke-Command2/Register-ObjectEvent/Get-ChildItem call that grew to 3+ parameters, with purpose-specific variable names for the per-run token in each scope (seceditRunToken, ExportRunToken, ConfigureRunToken, CleanupRunToken) instead of reusing one name across scopes.
  • Guarded the AfterAll revert on a $preTestStateCaptured flag so a BeforeAll failure can't be misread as "user didn't already have the privilege" and trigger an unwanted revert.
  • Replaced an immediate post-call Get-Event with a bounded Wait-Event, since FileSystemWatcher delivers Created events asynchronously and a same-tick poll can race the event queue.

Verification limitation

My local shell isn't elevated, and both secedit.exe and dbatools' own Test-ElevationRequirement check require Administrator rights for a localhost target. I confirmed:

  • AST parse of both changed files is clean.
  • The UnitTests-tagged tests (parameter validation + the Mock-based regression test) pass locally.
  • The new IntegrationTests test fails locally, but only for the expected/honest reason: Set-DbaPrivilege throws Console not elevated, but elevation is required... via its own Test-ElevationRequirement check, and the AfterAll revert fails with secedit /export failed with exit code 740 (ERROR_ELEVATION_REQUIRED) for the same reason. Neither the FileSystemWatcher assertion nor the /db path fix has been exercised end-to-end on my machine - that needs a run on the elevated self-hosted CI runner to be conclusive.

Test plan

  • CI runs Set-DbaPrivilege IntegrationTests on the elevated self-hosted runner and the new test passes
  • UnitTests pass locally
  • AST parse clean

馃 Generated with Claude Code

claude and others added 4 commits August 9, 2026 00:44
secedit /configure /db resolves a bare filename against the process's
current directory, not $env:TEMP. The relative "secedit.sdb" argument
left secedit.sdb and its secedit.jfm journal file behind wherever the
caller happened to be running the command. Point /db at an absolute
$temp path instead, and clean up the database and journal file
alongside the exported cfg.

(do Set-DbaPrivilege)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Make the secedit database name unique per invocation (Get-Random token)
so two concurrent Set-DbaPrivilege runs against the same computer can no
longer collide on, or delete, each other's temp database/journal files.

Test suite fixes:
- Fix the Invoke-Command2 mock in the regressions Describe block, which
  discriminated calls by argument count and started misrouting the
  cleanup call once it gained an -ArgumentList; it now matches on the
  scriptblock body instead.
- Harden the integration test's AfterAll revert: check secedit's exit
  code instead of ignoring failures, use a Get-Random-suffixed cfg/db/jfm
  set instead of fixed names, fix a SID-removal regex that could strip a
  different SID sharing the same prefix, and clean up in a finally block.
- Replace the two vacuous "artifact absent" It blocks with one test that
  proves the database is actually written under temp during the run
  (via a FileSystemWatcher on Path/Filter/EnableRaisingEvents, not a
  splat, since splatting New-Object applies the keys to New-Object
  itself rather than the constructed object) instead of only inferring
  it from post-hoc absence, and asserts with EnableException instead of
  swallowing warnings.

(do Set-DbaPrivilege)
- Tokenize the shared secpolByDbatools.cfg name too, not just the .sdb/.jfm
  pair: the cfg file was still fixed, so two concurrent Set-DbaPrivilege runs
  against the same computer could still overwrite or delete each other's cfg
  mid-flight and apply the wrong privileges. The token is now generated once
  before the export step and threaded through export, configure, and cleanup.
- Splat all three Invoke-Command2 calls (each now has 4-6 named parameters)
  with aligned, purpose-named hashtables, and give the token parameter a
  distinct, purpose-specific name in each scope (ExportRunToken/
  ConfigureRunToken/CleanupRunToken at the remote scriptblocks, seceditRunToken
  at the caller) instead of reusing $dbToken/$DbToken across all of them.
- Fix the regressions unit test's Invoke-Command2 mock, which still wrote/read
  a fixed secpolByDbatools.cfg while production code now uses a tokenized
  name; the mock now derives the same token from $ArgumentList.
- Guard the integration test's AfterAll revert on a $preTestStateCaptured flag
  set only after BeforeAll successfully determines whether the user already
  held the privilege, so a BeforeAll failure can no longer be misread as
  "didn't have it" and trigger an unwanted revert.
- Replace the immediate post-call Get-Event with a bounded Wait-Event, since
  FileSystemWatcher delivers Created events asynchronously and a same-tick
  poll can race the event queue.
- Track the exact file path(s) the watcher observed being created and assert
  those specific paths are gone afterward, instead of re-scanning temp with
  the same wildcard (which can't distinguish this invocation's artifact from
  a leftover or a concurrent run). Add an explicit check for the legacy
  hardcoded secedit.sdb/secedit.jfm names in the working directory, so a
  regression back to the pre-fix filename would still be caught.
- Splat Register-ObjectEvent and combine the four Get-ChildItem existence
  checks into one splatted call with -Include covering both the tokenized and
  legacy filenames.

(do Set-DbaPrivilege)
AfterAll set $PSDefaultParameterValues["*-Dba*:EnableException"] and never removed
it, so the enable leaked into whatever test file ran next in the same session. The
file's own tests pass either way; the repo-wide check in dbatools.Tests.ps1 caught
it as "2 enabled, 1 removed" during a full suite run.

(do Set-DbaPrivilege)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

3 participants