Purpose: This document is a phased implementation plan for an AI coding agent to build the SysOps Commander application step by step. Each phase is self-contained with clear inputs, outputs, acceptance criteria, and technical guidance. Phases must be completed in order — each builds on the previous.
Reference Document: SysOpsCommander_DesignDocument.docx (v1.0, March 2026)
Source Control: Git repository (GitHub or Azure DevOps). Include a
.gitignorefor .NET (bin/, obj/, .vs/, *.user, *.suo, packages/) from Phase 0. All phases should produce atomic, well-described commits.
| Rev | Date | Changes |
|---|---|---|
| 1 | 2026-03-11 | Initial implementation plan |
| 2 | 2026-03-11 | Multi-domain AD support; WinRM auth/transport configurability (Kerberos, NTLM, CredSSP); parameter injection via AddParameter(); PS SDK version clarification; org-wide config via appsettings.json; configurable stale threshold; toast notifications; auto-update fleshed out; outputFormat rendering hint clarification |
| Decision | Answer |
|---|---|
| Shared script repository path | Org-wide default in appsettings.json, per-user override in Settings |
| Max concurrent target hosts | No practical upper limit — must stream results to disk |
| Audit log SIEM export | Deferred to v2 (Splunk integration) |
| SYSTEM context execution | Not needed — user-context + alternate credentials only |
| Application updates | Auto-update from network share (v1) |
| AD domain scope | Default to current user's domain; allow switching to any reachable domain |
| WinRM authentication | User-selectable: Kerberos (default), NTLM, CredSSP. CredSSP validated before use |
| WinRM transport | HTTP (5985) default; HTTPS (5986) configurable per-host or globally |
| Stale computer threshold | Configurable in settings, default 90 days |
| Source control | Git repository (GitHub or Azure DevOps) |
| Org-wide config source | appsettings.json bundled with the application |
READ BEFORE STARTING ANY PHASE. These are cross-cutting concerns that affect the entire codebase.
The application hosts the PowerShell 7.x SDK (Microsoft.PowerShell.SDK) locally for two purposes: (1) AST parsing for script validation, and (2) creating Runspace and PowerShell pipeline objects for remote execution. However, remote execution via WinRM connects to whatever PowerShell version is installed on the target host — typically Windows PowerShell 5.1 on most enterprise Windows machines. The SDK version on the app side does NOT determine the remote execution version.
Implications:
- All sample scripts use
#Requires -Version 5.1for maximum compatibility - AST parsing may flag PS 7-only syntax that would fail on PS 5.1 targets — this is a feature, not a bug (it catches compatibility issues)
- Do NOT set
PSVersiononWSManConnectionInfounless the user explicitly requests it - The manifest schema does not include a
psVersionfield in v1 — add if needed in v2
When executing a structured script plugin with parameters defined in its manifest, pass parameters using the PowerShell SDK's AddParameter() method on the PowerShell pipeline object:
// CORRECT — safe parameter injection
using var ps = PowerShell.Create();
ps.AddScript(scriptContent);
foreach (var param in parameters)
{
ps.AddParameter(param.Key, param.Value);
}
var results = await ps.InvokeAsync();// WRONG — injection risk, string escaping nightmare
var modified = $"$NameFilter = '{userInput}'\n{scriptContent}"; // NEVER DO THISThis applies to both local AST validation and remote execution via Invoke-Command -ScriptBlock.
The outputFormat field in the JSON manifest ("text", "table", "json") is a UI rendering hint, not a parsing directive. The app does NOT parse Format-Table output back into structured columns. Instead:
"text"→ display raw output in a monospace text block"table"→ display raw output in a monospace text block with slightly wider default width (hint that the output is tabular)"json"→ attemptJSON.parse()on the output and render as a formatted tree/grid. If parsing fails, fall back to raw text display
If structured output parsing is needed in v2, scripts should use ConvertTo-Json and the app should parse the JSON. Format-Table output is not machine-parseable over WinRM.
IHostTargetingService must be registered as a singleton in the DI container. It holds the current working set of target hosts and is shared between the AD Explorer (which adds hosts via "Send to Execution Targets") and the Execution view (which consumes them). This is the explicit mechanism for cross-view communication. ViewModels should inject this service and subscribe to its CollectionChanged events.
| Component | Package/Version | NuGet Package |
|---|---|---|
| Runtime | .NET 8 (LTS) | — |
| UI Framework | WPF | — |
| Language | C# 12 | — |
| MVVM Toolkit | CommunityToolkit.Mvvm | CommunityToolkit.Mvvm (8.x) |
| Dependency Injection | MS DI | Microsoft.Extensions.DependencyInjection |
| Configuration | MS Configuration | Microsoft.Extensions.Configuration.Json |
| AD Integration | DirectoryServices | System.DirectoryServices + System.DirectoryServices.Protocols |
| PowerShell SDK | PS 7.x SDK | Microsoft.PowerShell.SDK |
| WMI | System.Management | System.Management |
| SQLite | Dapper + MS Sqlite | Microsoft.Data.Sqlite + Dapper |
| Logging | Serilog | Serilog.Sinks.File + Serilog.Sinks.Console + Serilog.Formatting.Compact |
| Toast Notifications | MS Toolkit Notifications | Microsoft.Toolkit.Uwp.Notifications |
| Excel Export | ClosedXML | ClosedXML |
| CSV Export | CsvHelper | CsvHelper |
| Unit Testing | xUnit + NSubstitute | xUnit + NSubstitute + FluentAssertions |
| Roslyn Analyzers | Standard | Microsoft.CodeAnalysis.NetAnalyzers |
SysOpsCommander/
├── .gitignore
├── SysOpsCommander.sln
├── src/
│ ├── SysOpsCommander.Core/
│ │ ├── Interfaces/
│ │ │ ├── IActiveDirectoryService.cs
│ │ │ ├── IRemoteExecutionService.cs
│ │ │ ├── IScriptLoaderService.cs
│ │ │ ├── IHostTargetingService.cs # SINGLETON — shared across views
│ │ │ ├── ICredentialService.cs
│ │ │ ├── IAuditLogService.cs
│ │ │ ├── IExportService.cs
│ │ │ ├── ISettingsService.cs
│ │ │ ├── IExecutionStrategy.cs
│ │ │ ├── IAutoUpdateService.cs
│ │ │ └── INotificationService.cs
│ │ ├── Models/
│ │ │ ├── AdObject.cs
│ │ │ ├── AdSearchResult.cs
│ │ │ ├── ExecutionJob.cs
│ │ │ ├── HostTarget.cs
│ │ │ ├── HostResult.cs
│ │ │ ├── ScriptPlugin.cs
│ │ │ ├── ScriptManifest.cs
│ │ │ ├── AuditLogEntry.cs
│ │ │ ├── UserSettings.cs
│ │ │ ├── DomainConnection.cs # NEW — multi-domain support
│ │ │ └── WinRmConnectionOptions.cs # NEW — auth/transport config
│ │ ├── Enums/
│ │ │ ├── ExecutionStatus.cs
│ │ │ ├── HostStatus.cs
│ │ │ ├── ExecutionType.cs
│ │ │ ├── ScriptDangerLevel.cs
│ │ │ ├── OutputFormat.cs
│ │ │ ├── WinRmAuthMethod.cs # NEW — Kerberos, NTLM, CredSSP
│ │ │ └── WinRmTransport.cs # NEW — HTTP, HTTPS
│ │ ├── Constants/
│ │ │ └── AppConstants.cs
│ │ └── Validation/
│ │ ├── HostnameValidator.cs
│ │ ├── LdapFilterSanitizer.cs
│ │ └── ManifestSchemaValidator.cs
│ │
│ ├── SysOpsCommander.Services/
│ │ ├── ActiveDirectoryService.cs
│ │ ├── RemoteExecutionService.cs
│ │ ├── Strategies/
│ │ │ ├── PowerShellRemoteStrategy.cs
│ │ │ └── WmiQueryStrategy.cs
│ │ ├── ScriptLoaderService.cs
│ │ ├── ScriptValidationService.cs
│ │ ├── HostTargetingService.cs
│ │ ├── CredentialService.cs
│ │ ├── ExportService.cs
│ │ ├── AutoUpdateService.cs
│ │ └── NotificationService.cs # NEW — toast notifications
│ │
│ ├── SysOpsCommander.Infrastructure/
│ │ ├── Database/
│ │ │ ├── DatabaseInitializer.cs
│ │ │ ├── AuditLogRepository.cs
│ │ │ └── SettingsRepository.cs
│ │ ├── Logging/
│ │ │ ├── SerilogConfigurator.cs
│ │ │ └── CredentialDestructuringPolicy.cs
│ │ └── FileSystem/
│ │ └── ScriptFileProvider.cs
│ │
│ ├── SysOpsCommander.ViewModels/
│ │ ├── MainWindowViewModel.cs
│ │ ├── DashboardViewModel.cs
│ │ ├── AdExplorerViewModel.cs
│ │ ├── AdSearchViewModel.cs
│ │ ├── ExecutionViewModel.cs
│ │ ├── ScriptLibraryViewModel.cs
│ │ ├── AuditLogViewModel.cs
│ │ ├── SettingsViewModel.cs
│ │ └── Dialogs/
│ │ ├── CredentialDialogViewModel.cs
│ │ └── DomainSelectorViewModel.cs # NEW
│ │
│ └── SysOpsCommander.App/
│ ├── App.xaml / App.xaml.cs
│ ├── appsettings.json # NEW — org-wide defaults
│ ├── MainWindow.xaml / .cs
│ ├── Views/
│ │ ├── DashboardView.xaml
│ │ ├── AdExplorerView.xaml
│ │ ├── ExecutionView.xaml
│ │ ├── ScriptLibraryView.xaml
│ │ ├── AuditLogView.xaml
│ │ └── SettingsView.xaml
│ ├── Dialogs/
│ │ ├── CredentialDialog.xaml
│ │ └── DomainSelectorDialog.xaml # NEW
│ ├── Converters/
│ │ ├── StatusToColorConverter.cs
│ │ └── BoolToVisibilityConverter.cs
│ ├── Resources/
│ │ └── Styles.xaml
│ └── DependencyInjection/
│ └── ServiceCollectionExtensions.cs
│
├── tests/
│ └── SysOpsCommander.Tests/
│ ├── ViewModels/
│ ├── Services/
│ ├── Validation/
│ ├── Infrastructure/
│ └── Security/
│
└── scripts/
├── examples/
│ ├── Get-InstalledSoftware.ps1
│ ├── Get-InstalledSoftware.json
│ ├── Get-LocalAdmins.ps1
│ ├── Get-LocalAdmins.json
│ ├── Get-SecurityEventLog.ps1
│ ├── Get-SecurityEventLog.json
│ ├── Test-WinRMConnectivity.ps1
│ ├── Test-WinRMConnectivity.json
│ └── Invoke-QuickScan.ps1 # Simple drop-in (no manifest)
└── manifest-schema.json
Unchanged from Rev 1. See the 5 sample scripts (Get-InstalledSoftware, Get-LocalAdmins, Get-SecurityEventLog, Test-WinRMConnectivity, Invoke-QuickScan) and the manifest-schema.json defined in the original plan. Include all 5 in
scripts/examples/.
Goal: Create the solution structure, wire dependency injection, configure logging and application configuration, and verify the build pipeline works end-to-end. No features yet — just a solid skeleton that every subsequent phase builds on.
Why this is first: Every subsequent phase depends on DI, logging, configuration, and the project structure being in place.
-
Initialize the Git repository with a
.gitignorefor .NET projects (bin/, obj/, .vs/, *.user, *.suo, packages/, *.db). Create an initial commit with the empty solution structure. -
Create the .NET 8 solution and all 6 projects matching the solution structure above. Set up project references:
Corehas no project references (it's the dependency root)ServicesreferencesCoreInfrastructurereferencesCoreViewModelsreferencesCoreAppreferencesCore,ViewModels,Services,InfrastructureTestsreferencesCore,ViewModels,Services,Infrastructure
-
Install NuGet packages per the technology stack table. Pin versions explicitly — do not use floating versions.
-
Create
appsettings.jsonin theAppproject (set to Copy to Output Directory):{ "SysOpsCommander": { "SharedScriptRepositoryPath": "", "UpdateNetworkSharePath": "", "DefaultDomain": "", "DefaultWinRmTransport": "HTTP", "DefaultWinRmAuthMethod": "Kerberos", "DefaultThrottle": 5, "DefaultTimeoutSeconds": 60, "StaleComputerThresholdDays": 90, "AuditLogRetentionDays": 365 } }Wire
Microsoft.Extensions.Configuration.Jsonto load this file. Bind to a strongly-typedAppConfigurationclass viaIOptions<AppConfiguration>or direct binding. This file represents the org-wide defaults that ship with the application. Per-user overrides are stored in SQLite (Phase 1). -
Configure the DI composition root in
App.xaml.cs:- Load
appsettings.jsonviaConfigurationBuilder - Register
AppConfigurationas a singleton - Register all service interfaces → concrete implementations
- Register
IHostTargetingServiceas a singleton (shared across views) - Register all other services with appropriate lifetimes (most as singletons for desktop app)
- Register all ViewModels as transient
- Use
IServiceProviderto resolve theMainWindowand itsDataContext - Create
ServiceCollectionExtensions.cswithAddSysOpsServices(),AddSysOpsViewModels(),AddSysOpsInfrastructure()extension methods
- Load
-
Configure Serilog in
SerilogConfigurator.cs:- Rolling file sink →
%LOCALAPPDATA%\SysOpsCommander\Logs\sysops-{Date}.log - Compact JSON format for structured output
- Console sink for debug builds
- Default level:
Information(configurable via settings later) - Register the
CredentialDestructuringPolicythat replacesSecureString,PSCredential, andNetworkCredentialproperties with"[REDACTED]"in all log output - Wire into the DI container as
ILogger(Serilog's interface) - Enrichers:
Enrich.WithMachineName(),Enrich.WithThreadId(), customCorrelationIdEnricher
- Rolling file sink →
-
Create the
AppConstants.csfile with:public static class AppConstants { public const string AppName = "SysOps Commander"; public const string AppDataFolder = "SysOpsCommander"; public const int DefaultThrottle = 5; public const int DefaultWinRmTimeoutSeconds = 60; public const int DefaultAdQueryTimeoutSeconds = 30; public const int MaxResultsPerPage = 500; public const int WinRmHttpPort = 5985; public const int WinRmHttpsPort = 5986; public const int AuditLogRetentionDays = 365; public const int DefaultStaleComputerDays = 90; public const int ReachabilityCheckParallelism = 20; public const long MaxInMemoryResultBytes = 10 * 1024 * 1024; // 10MB public const string DefaultScriptCategory = "Uncategorized"; }
-
Create a minimal
MainWindow.xamlwith:- A sidebar (collapsed for now, just a
StackPanelwith placeholder buttons) - A
ContentControlin the main area bound to aCurrentViewproperty onMainWindowViewModel - Verify the app launches, the DI container resolves, and Serilog writes a startup log entry
- A sidebar (collapsed for now, just a
-
Wire global exception handlers in
App.xaml.cs:DispatcherUnhandledException→ log to Serilog, show aMessageBoxwith correlation ID, offer "Continue" or "Exit"AppDomain.CurrentDomain.UnhandledException→ log to SerilogTaskScheduler.UnobservedTaskException→ log to Serilog
-
Create a smoke test in
SysOpsCommander.Tests:- Verify DI container builds without errors
- Verify
CredentialDestructuringPolicyreplacesSecureStringvalues with"[REDACTED]" - Verify
appsettings.jsonloads and binds toAppConfigurationcorrectly
- Git repository initialized with .gitignore and initial commit
- Solution builds with zero warnings
- Application launches and displays a blank window with sidebar skeleton
-
appsettings.jsonloads and binds toAppConfiguration - Serilog writes a
sysops-{date}.logfile to%LOCALAPPDATA%\SysOpsCommander\Logs\ - Global exception handler catches a test exception and shows a dialog with correlation ID
-
CredentialDestructuringPolicyunit test passes -
IHostTargetingServiceis registered as a singleton (verified by test) - All 6 projects compile and reference each other correctly
- Roslyn analyzers are active and produce no warnings
Goal: Build the data layer — all models/DTOs, the SQLite database, settings persistence, and the repository pattern. Includes the new models for multi-domain and WinRM configurability.
-
Define all Core models and enums including the new connection models:
AdObject— generic AD object representation (DN, ObjectClass, Name, dictionary of attributes)AdSearchResult— wraps a list ofAdObjectwith metadata (query, execution time, result count)ExecutionJob— represents a single execution run (ID, script, parameters, target hosts, status, start/end time, results collection,WinRmConnectionOptions)HostTarget— hostname + reachability status + validation statusHostResult— per-host execution result (hostname, status enum, output text, error text, duration)ScriptPlugin— represents a loaded script (file path, manifest if present, is-validated, validation errors)ScriptManifest— strongly-typed deserialization of the JSON manifest schemaAuditLogEntry— maps to the SQLite AuditLog table columns. IncludesWinRmAuthMethodandWinRmTransportfieldsUserSettings— key-value setting with typed accessor methods- NEW:
DomainConnection— represents a target AD domain: domain name, domain controller FQDN (optional, for explicit DC targeting),DirectoryEntryroot path, IsCurrentDomain flag - NEW:
WinRmConnectionOptions— per-execution connection config:WinRmAuthMethodenum,WinRmTransportenum, custom port (optional), shell URI (optional). Defaults loaded fromAppConfiguration, overridable per-run in the Execution view
-
Define all Core enums:
ExecutionStatus: Pending, Validating, Running, Completed, PartialFailure, Failed, CancelledHostStatus: Pending, Reachable, Unreachable, Running, Success, Failed, Timeout, Cancelled, SkippedExecutionType: PowerShell, WMIScriptDangerLevel: Safe, Caution, DestructiveOutputFormat: Text, Table, Json- NEW:
WinRmAuthMethod: Kerberos, NTLM, CredSSP - NEW:
WinRmTransport: HTTP, HTTPS
-
Define all Core service interfaces with full method signatures. Key changes:
IActiveDirectoryService: - Task<IReadOnlyList<DomainConnection>> GetAvailableDomainsAsync(CancellationToken ct) - Task SetActiveDomainAsync(DomainConnection domain, CancellationToken ct) - DomainConnection GetActiveDomain() // ... plus all search/browse methods from original plan ISettingsService: - Task<string> GetAsync(string key, string defaultValue) - Task SetAsync(string key, string value) - Task<T> GetTypedAsync<T>(string key, T defaultValue) - string GetOrgDefault(string key) // reads from AppConfiguration - Task<string> GetEffectiveAsync(string key) // returns per-user override if set, otherwise org default INotificationService: // NEW - void ShowToast(string title, string message, NotificationSeverity severity) - void ShowExecutionComplete(ExecutionJob job) // rich notification with result summary IAuditLogService: - Task LogExecutionAsync(AuditLogEntry entry) - Task<IReadOnlyList<AuditLogEntry>> QueryAsync(AuditLogFilter filter) - Task PurgeOldEntriesAsync(int retentionDays) -
Implement
DatabaseInitializer.cs:- Create SQLite database at
%LOCALAPPDATA%\SysOpsCommander\audit.db - Create
AuditLogtable with the additional columns:WinRmAuthMethod TEXT,WinRmTransport TEXT,TargetDomain TEXT - Create
UserSettingstable - Run on app startup, use
IF NOT EXISTSfor idempotency - Add a
SchemaVersiontable for future migrations
- Create SQLite database at
-
Implement
SettingsRepository.csandAuditLogRepository.csusing Dapper. -
Implement
ISettingsServiceconcrete class:- Org-wide defaults read from
AppConfiguration(sourced fromappsettings.json) - Per-user overrides stored in SQLite
UserSettingstable GetEffectiveAsync(key)returns: per-user override if it exists, otherwise org default fromappsettings.json- Settings keys include:
SharedScriptRepositoryPath,DefaultThrottle,DefaultTimeoutSeconds,DefaultWinRmTransport,DefaultWinRmAuthMethod,StaleComputerThresholdDays,UpdateNetworkSharePath,LogLevel
- Org-wide defaults read from
-
Write unit tests:
- Model serialization/deserialization (especially
ScriptManifest,WinRmConnectionOptions) AuditLogRepositoryCRUD against in-memory SQLite (including new columns)SettingsRepositoryread/write/update- Settings layering: per-user override takes precedence over org default; absent override falls through to org default; absent org default falls through to
AppConstantshard default
- Model serialization/deserialization (especially
- All models, enums, and interfaces compile with XML documentation
-
DomainConnectionandWinRmConnectionOptionsmodels correctly represent multi-domain and auth config -
WinRmAuthMethodandWinRmTransportenums include all three auth methods and both transports - SQLite database is created on first run with correct schema (including new columns)
- Settings layering works: per-user override > appsettings.json org default > AppConstants hard default
- Audit log entries include WinRM auth/transport/domain fields
-
ScriptManifestcorrectly deserializes all 5 example JSON manifests - All unit tests pass
Goal: Build all validation logic before the features that depend on it.
-
Implement
HostnameValidator.csinCore/Validation/:- Validate NetBIOS names (max 15 chars, allowed: alphanumeric + hyphens, cannot start/end with hyphen)
- Validate FQDN format (proper dot-separated labels, each label 1-63 chars, total max 253)
- Validate IPv4 address format (four octets 0-255)
- Return
ValidationResultwith success/failure and error message - Reject empty strings, whitespace, strings with injection characters (
;,|,&,$,`,(,))
-
Implement
LdapFilterSanitizer.csinCore/Validation/:- Escape special LDAP characters per RFC 4515:
*→\2a,(→\28,)→\29,\→\5c, NUL →\00 SanitizeInput(string raw)→string sanitizedBuildSafeFilter(string attribute, string value)→string ldapFilter
- Escape special LDAP characters per RFC 4515:
-
Implement
ManifestSchemaValidator.csinCore/Validation/:- Required fields present: name, description, version, author, category
- Version matches semver pattern
^\d+\.\d+\.\d+$ - Category is one of the allowed enum values
- Parameter types are valid enum values
- Choice parameters have non-empty choices array
- No duplicate parameter names
- Return
ManifestValidationResultwith list of errors/warnings
-
Implement
ScriptValidationService.csinServices/:ValidateSyntax(string scriptPath)→ PowerShell AST parsing (System.Management.Automation.Language.Parser.ParseFile). Return list of parse errors with line/column/messageDetectDangerousPatterns(string scriptPath)→ AST walker scanning for:Remove-Itemwith-Recurse+-Force,Format-Volume,Stop-Computer,Restart-Computer,Clear-EventLog,Set-ExecutionPolicy,Disable-NetAdapter,Stop-Serviceon critical services. Return warnings with line numbersValidateManifestPair(string ps1Path)→ check JSON manifest exists and is valid; check that parameter names in manifest match theparam()block in the script (warning if mismatched — the script is authoritative, the manifest is documentation)- NEW:
ValidateCredSspAvailability(string hostname)→ test whether CredSSP is configured on the target host. Return clear error message if not: "CredSSP authentication is not configured on {host}. This requires GPO configuration on both client and server. See: https://learn.microsoft.com/en-us/powershell/module/microsoft.wsman.management/enable-wsmancredssp"
-
Write comprehensive unit tests:
HostnameValidator: valid NetBIOS, valid FQDN, valid IP, injection characters rejected, boundary cases (empty, max length, leading/trailing hyphens)LdapFilterSanitizer: all 5 special characters escaped, nested injection attempts blocked, empty input handledManifestSchemaValidator: valid manifests pass, missing required fields caught, invalid parameter types caught, duplicate param names caughtScriptValidationService: valid .ps1 passes, syntax errors with line numbers, dangerous patterns detected, manifest-script parameter mismatch- All validators: aim for 25+ test cases total
- Hostname validation correctly accepts/rejects all expected patterns including injection characters
- LDAP sanitizer escapes all RFC 4515 special characters
- Manifest validator catches all schema violations
- Script syntax validation returns parse errors with line numbers
- Dangerous pattern detection identifies all specified cmdlets
- CredSSP availability check returns clear error when not configured
- All validation unit tests pass (25+ cases)
Goal: Build the full AD integration with multi-domain support — quick search, tree browsing, attribute viewing, pre-built security filters, and the ability to switch between reachable domains.
-
Implement
IActiveDirectoryServiceinterface:// Domain management Task<IReadOnlyList<DomainConnection>> GetAvailableDomainsAsync(CancellationToken ct) Task SetActiveDomainAsync(DomainConnection domain, CancellationToken ct) DomainConnection GetActiveDomain() // Search Task<AdSearchResult> SearchAsync(string searchTerm, CancellationToken ct) Task<AdSearchResult> SearchWithFilterAsync(string ldapFilter, CancellationToken ct) // Browse Task<IReadOnlyList<AdObject>> BrowseChildrenAsync(string parentDn, CancellationToken ct) Task<AdObject> GetObjectDetailAsync(string distinguishedName, CancellationToken ct) // Group membership Task<IReadOnlyList<string>> GetGroupMembershipAsync(string objectDn, bool recursive, CancellationToken ct) // Pre-built security filters Task<AdSearchResult> GetLockedAccountsAsync(CancellationToken ct) Task<AdSearchResult> GetDisabledComputersAsync(CancellationToken ct) Task<AdSearchResult> GetStaleComputersAsync(int daysInactive, CancellationToken ct) Task<IReadOnlyList<string>> GetDomainControllersAsync(CancellationToken ct) -
Implement
ActiveDirectoryService.cs:- Multi-domain support:
- On initialization, detect the current user's domain from
Environment.UserDomainNameandDomain.GetCurrentDomain() GetAvailableDomainsAsync()→ useForest.GetCurrentForest().Domainsto enumerate trusted domains. Also allow manual domain entry for cross-forest scenariosSetActiveDomainAsync()→ update the internalDirectoryEntryroot to the selected domain's root DN. All subsequent queries target this domain. Store asDomainConnectionobject- Default behavior: app starts connected to the current user's domain. A domain selector (dropdown or dialog) in the UI allows switching
- On initialization, detect the current user's domain from
- Quick Search: LDAP filter searching
sAMAccountName,cn,displayName,mail,dNSHostName. All user input sanitized viaLdapFilterSanitizer - Tree Browse:
SearchScope.OneLevelfrom the parent DN, lazy loading on expand - Attribute Detail: load all attributes via
DirectoryEntry.Properties - Pre-built Filters:
- Locked accounts:
(&(objectClass=user)(lockoutTime>=1)) - Disabled computers:
(&(objectClass=computer)(userAccountControl:1.2.840.113556.1.4.803:=2)) - Stale computers:
(&(objectClass=computer)(lastLogonTimestamp<={daysAgoFileTime}))— days threshold loaded fromISettingsService.GetEffectiveAsync("StaleComputerThresholdDays"), default 90
- Locked accounts:
- Group Membership:
tokenGroupsattribute for recursive,memberOffor direct - All methods accept
CancellationTokenand enforce configurable timeout - Results paginated with
AppConstants.MaxResultsPerPage
- Multi-domain support:
-
Implement key AD attribute mapping (unchanged from Rev 1 — users, computers, groups).
-
Write unit tests:
- Quick search returns expected results for partial match
- Pre-built filters generate correct LDAP filter strings
- Stale computer filter uses configurable threshold (not hardcoded)
- Domain switching updates the search root correctly
- LDAP injection in search terms is sanitized
- Cancellation and timeout enforced
- App detects and connects to the current user's domain on startup
- Available domains are enumerable (from forest trusts)
- Domain switching updates all subsequent queries to target the new domain
- Quick search works for users, computers, and groups with partial matching
- Pre-built security filters work, with configurable stale threshold
- Tree browsing loads lazily
- Full attribute detail loads for selected objects
- All user input is sanitized
- All unit tests pass
Goal: Build the core execution engine with configurable WinRM authentication (Kerberos, NTLM, CredSSP) and transport (HTTP/HTTPS). This is the highest-risk technical component.
-
Define the
IExecutionStrategyinterface:public interface IExecutionStrategy { ExecutionType Type { get; } Task<HostResult> ExecuteAsync( string hostname, string scriptContent, IDictionary<string, object>? parameters, PSCredential? credential, WinRmConnectionOptions connectionOptions, // NEW — auth + transport config int timeoutSeconds, CancellationToken ct); }
-
Implement
PowerShellRemoteStrategy.cs:- Create
WSManConnectionInfofor the target host with configurable auth and transport:var connInfo = new WSManConnectionInfo( useSsl: options.Transport == WinRmTransport.HTTPS, hostname, options.Transport == WinRmTransport.HTTPS ? AppConstants.WinRmHttpsPort : AppConstants.WinRmHttpPort, "/wsman", "http://schemas.microsoft.com/powershell/Microsoft.PowerShell", credential); connInfo.AuthenticationMechanism = options.AuthMethod switch { WinRmAuthMethod.Kerberos => AuthenticationMechanism.Kerberos, WinRmAuthMethod.NTLM => AuthenticationMechanism.Negotiate, // Negotiate allows NTLM fallback WinRmAuthMethod.CredSSP => AuthenticationMechanism.Credssp, _ => AuthenticationMechanism.Default };
- CredSSP validation: If
CredSSPis selected, the execution engine should log a warning that CredSSP requires prior GPO configuration. If the connection fails with an auth error and CredSSP was selected, the error message should include remediation steps - Parameter injection via
AddParameter()— NEVER string interpolation:using var ps = PowerShell.Create(); ps.AddScript(scriptContent); if (parameters != null) { foreach (var kvp in parameters) ps.AddParameter(kvp.Key, kvp.Value); }
- Capture output streams: Output, Error, Warning, Verbose
- Enforce timeout using
CancellationTokencombined withTask.WhenAnyand a delay task - Error mapping (expanded for auth-specific errors):
PSRemotingTransportExceptionwith "Access is denied" → "Authentication failed for {host} using {authMethod}. Verify credentials and that {authMethod} is enabled on the target."PSRemotingTransportExceptionwith "CredSSP" → "CredSSP authentication failed for {host}. Ensure CredSSP is enabled via GPO on both client and server."PSRemotingTransportException(general) → "WinRM connection failed to {host} on {transport}:{port}. Verify WinRM is enabled and the {transport} listener is configured."UnauthorizedAccessException→ "Access denied to {host}. Check credentials and remote management permissions."OperationCanceledException→ "Execution cancelled by user."- Timeout → "Execution timed out after {n} seconds on {host}."
- Create
-
Implement
WmiQueryStrategy.cs:ConnectionOptionswith configurable auth:var connOpts = new ConnectionOptions { Authentication = options.AuthMethod switch { WinRmAuthMethod.Kerberos => AuthenticationLevel.PacketPrivacy, WinRmAuthMethod.NTLM => AuthenticationLevel.PacketPrivacy, WinRmAuthMethod.CredSSP => AuthenticationLevel.PacketPrivacy, _ => AuthenticationLevel.Default }, Impersonation = ImpersonationLevel.Impersonate };
- Credential handling via
ConnectionOptions.Username/ConnectionOptions.SecurePassword - Same timeout and error mapping approach
-
Implement
RemoteExecutionService.cs:- Accepts
ExecutionJobcontainingWinRmConnectionOptions(loaded from settings, overridable per-run) - Pre-flight: host reachability via TCP connect to the correct port (5985 for HTTP, 5986 for HTTPS, based on
WinRmConnectionOptions.Transport) - Parallel execution:
SemaphoreSlimwith configurable throttle - Progress reporting:
IProgress<HostResult>for real-time UI updates - Cancellation:
CancellationTokenpropagated to all child tasks - Error isolation: per-host try/catch
- Large result handling: track cumulative output size. If exceeds
AppConstants.MaxInMemoryResultBytes(10MB), switch to streaming per-host results to temp files in%LOCALAPPDATA%\SysOpsCommander\Temp\.HostResult.Outputbecomes a file path reference with aIsFileReferenceflag. UI reads on demand - Credential lifecycle: accept
PSCredential, pass by reference to strategies, never store. Caller disposes after execution
- Accepts
-
Implement
ICredentialServiceandCredentialService.cs:PromptForCredentials()→ raises event for ViewModel to show dialogValidateCredentialsAsync(PSCredential credential, string? targetDomain)→ LDAP bind test against the specified domain (or current domain if null)DisposeCredentials(PSCredential credential)→ dispose SecureString, null reference
-
Implement
IHostTargetingServiceandHostTargetingService.cs(SINGLETON):ObservableCollection<HostTarget> Targets— observable for UI bindingAddFromHostnames(IEnumerable<string> hostnames)→ validate, de-duplicateAddFromCsvFile(string filePath)→ parse, validate, de-duplicateAddFromAdSearchResults(IEnumerable<AdObject> computers)→ extractdNSHostNameorcn, add to targetsCheckReachabilityAsync(CancellationToken ct)→ TCP connect test on the correct port (based on current WinRM transport setting). Parallel with throttle of 20ClearTargets()/RemoveTarget(string hostname)
-
Implement
INotificationServiceandNotificationService.cs:- Windows toast notifications via
Microsoft.Toolkit.Uwp.Notifications ShowExecutionComplete(ExecutionJob job)→ toast showing: script name, host count, success/fail breakdown- Toast click opens the application and navigates to the Execution view's results panel
- Windows toast notifications via
-
Write unit and integration tests:
PowerShellRemoteStrategy: verifyWSManConnectionInfois constructed with correct auth method and transport for each enum combination (Kerberos/HTTP, NTLM/HTTPS, CredSSP/HTTP, etc.)- Verify parameters are passed via
AddParameter()not string interpolation (inspect thePowerShell.Commandscollection in the mock) RemoteExecutionService: throttle enforcement, cancellation, error isolation, progress reporting, large result disk streamingHostTargetingService: validation, de-duplication, CSV parsing, singleton behaviorCredentialService: LDAP bind validation, disposal
- WinRM connection uses the correct auth method (Kerberos, NTLM, or CredSSP) based on configuration
- WinRM connection uses correct transport and port (HTTP/5985 or HTTPS/5986)
- CredSSP failure produces a clear error with remediation steps
- Script parameters are injected via
AddParameter()(verified by unit test) - Parallel execution respects throttle limit
- Cancellation correctly stops remaining hosts
- Per-host error isolation works
- Large results (>10MB cumulative) stream to disk
- Progress reporting delivers real-time updates
- Host reachability pre-check uses the correct port for the configured transport
- Toast notification fires on execution completion
-
IHostTargetingServiceis a singleton and observable - All unit tests pass
Goal: Build the script loader and export services. Unchanged from Rev 1 except: ensure the outputFormat field is treated as a rendering hint per the Critical Technical Notes section.
(Same as Rev 1 — scanner, file provider, export service, sample scripts)
Additional clarification for the agent:
- When loading manifests, validate
outputFormatis one oftext,table,jsonbut do NOT build any output parsing logic. The UI will render all output as text, usingoutputFormatonly to select the display component (monospace block for text/table, JSON tree viewer for json with text fallback)
(Same as Rev 1)
Goal: Build the complete WPF UI shell including the domain selector in the status bar and the navigation framework.
(Mostly same as Rev 1 with these additions:)
-
MainWindow.xamladditions:- Status bar at the bottom must include: domain selector dropdown (showing the active domain, click to switch), current user, connection status indicator, log level badge
- Domain selector triggers
IActiveDirectoryService.SetActiveDomainAsync()and refreshes any active AD views
-
Build
DomainSelectorDialog.xaml(for the case where the user wants to manually enter a domain not in the forest trust list):- Domain name text field
- Optional: specific DC FQDN
- "Test Connection" button that validates the domain is reachable
- OK/Cancel
-
CredentialDialog.xamladdition:- Domain field pre-populated with the active domain (not just the user's home domain)
- Auth method selector: Kerberos / NTLM / CredSSP (pre-populated from default settings)
-
Keyboard shortcuts (expanded):
Ctrl+F→ Focus search barCtrl+E→ Navigate to Execution viewCtrl+D→ Open domain selectorF5→ Refresh current viewEscape→ Cancel current operation
(Same as Rev 1 plus:)
- Domain selector in status bar shows current domain and allows switching
- Domain selector dialog allows manual domain entry with connection test
- Credential dialog pre-populates with active domain and default auth method
-
Ctrl+Dopens domain selector
Goal: Wire the AD service layer into the UI. Includes domain-aware search and configurable stale threshold.
(Same as Rev 1 with these additions:)
-
AD Explorer view additions:
- Domain indicator badge at the top of the view showing which domain is being queried
- Stale computers filter uses the threshold from settings (not hardcoded 90)
- The stale computers button label shows the current threshold: "Stale Computers (90 days)" — updates dynamically if the setting changes
-
Dashboard view additions:
- Show active domain name
- "Quick Connect" section: enter a single hostname and immediately see its AD object detail + option to execute scripts against it. This is the fastest path for incident response — one box, one hostname, immediate action
(Same as Rev 1 plus:)
- AD Explorer shows which domain is active
- Stale computer threshold is loaded from settings, not hardcoded
- Dashboard "Quick Connect" resolves a hostname to its AD object and offers execution
Goal: Build the main execution interface with WinRM connection configuration exposed in the UI.
(Same as Rev 1 with these additions to the Execution View:)
-
Execution controls bar additions:
- WinRM Auth Method dropdown: Kerberos | NTLM | CredSSP (defaults from settings)
- WinRM Transport toggle: HTTP | HTTPS (defaults from settings)
- When CredSSP is selected, show an info banner: "CredSSP requires GPO configuration on both client and server hosts."
- These values are passed to the
ExecutionJoband stored in the audit log
-
Execution flow additions:
- Step 1.5 (after script validation): If CredSSP is selected as auth method and alternate credentials are NOT provided, warn: "CredSSP requires explicit credentials. Would you like to enter credentials now?" (CredSSP cannot use implicit Kerberos delegation)
- After execution completes, fire
INotificationService.ShowExecutionComplete()for toast notification
-
Audit log entry additions:
- Record
WinRmAuthMethod,WinRmTransport, andTargetDomainfor every execution
- Record
(Same as Rev 1 plus:)
- Auth method and transport are selectable in the execution controls
- CredSSP selection shows info banner and forces credential prompt
- Auth/transport choices are recorded in the audit log
- Toast notification fires on execution completion (even when app is not focused)
Goal: Build the audit log browser, settings page, and a properly designed auto-update system.
(Audit Log and Settings mostly same as Rev 1, with these additions:)
-
Audit Log view additions:
- Additional columns: Auth Method, Transport, Target Domain
- Filter by domain
-
Settings view additions:
- Domain & Connection section:
- Default domain (text, or "auto-detect" for current user's domain)
- Default WinRM auth method: Kerberos / NTLM / CredSSP dropdown
- Default WinRM transport: HTTP / HTTPS toggle
- Stale computer threshold (numeric days, default 90)
- Repository section:
- Org-wide default path shown as read-only (sourced from
appsettings.json) - Per-user override checkbox and path editor
- Org-wide default path shown as read-only (sourced from
- Domain & Connection section:
-
Auto-Update — Fully Specified Implementation:
The auto-update system uses a simple network share convention:
Update package structure on the network share:
\\server\share\SysOpsCommander\ ├── version.json # Metadata file └── SysOpsCommander.zip # Self-contained published appversion.jsonformat:{ "version": "1.1.0", "releaseDate": "2026-04-15", "releaseNotes": "Added CredSSP support, bug fixes.", "minimumVersion": "1.0.0", "sha256": "abc123..." }AutoUpdateService.csimplementation:CheckForUpdateAsync():- Read the update share path from
ISettingsService - Attempt to read
version.jsonfrom the share (handle network errors gracefully — update check failure is never a blocking error) - Compare
version.json:versionagainstAssembly.GetExecutingAssembly().GetName().Version - If newer: return
UpdateAvailablewith version info and release notes - If same or older: return
UpToDate
- Read the update share path from
DownloadAndApplyAsync():- Copy
SysOpsCommander.zipfrom the share to%LOCALAPPDATA%\SysOpsCommander\Updates\ - Verify SHA256 hash matches
version.json:sha256 - Extract to
%LOCALAPPDATA%\SysOpsCommander\Updates\staged\ - Write a
pending-update.jsonfile with the staged path - Prompt user: "Update downloaded. Restart to apply?" (do NOT force restart)
- Copy
- On application startup (
App.xaml.cs):- Check for
pending-update.json - If present: launch a small updater bootstrapper (
SysOpsUpdater.exe) that: a. Waits for the main app process to exit (poll with timeout) b. Copies staged files over the application directory c. Deletes the staged directory andpending-update.jsond. Re-launches the main application - The updater bootstrapper is a tiny (~50 line) console app included in the project
- Check for
- File locking: The app cannot overwrite its own running executables. The bootstrapper approach avoids this by running the copy after the main process exits
- Failure recovery: If the bootstrapper crashes, the original files are untouched (copy happens to a temp location first, then atomic move). On next startup, if
pending-update.jsonexists but the staged directory is missing, delete the pending file and continue normally
Update check timing:
- Check on app startup (background, non-blocking)
- Check available via Settings page "Check for Updates" button
- Show a subtle indicator in the status bar if an update is available (not a modal popup)
(Same as Rev 1 plus:)
- Audit log shows auth method, transport, and target domain columns
- Settings page includes domain/connection defaults and stale threshold
- Settings page shows org-wide default (read-only) vs per-user override clearly
- Auto-update reads
version.jsonfrom network share - Auto-update compares versions correctly (semver)
- Auto-update downloads, verifies SHA256, and stages the update
- Updater bootstrapper applies update on restart without file-locking errors
- Failed update checks do not block application startup
- Status bar shows update-available indicator
Goal: Final phase — hardening, performance, security verification, documentation, and release.
(Same as Rev 1 with these additions:)
-
Error handling audit additions:
- Test CredSSP failures on hosts where CredSSP is not configured → verify clear error message
- Test domain switching to an unreachable domain → verify graceful fallback
- Test auto-update with corrupted zip file → verify SHA256 check catches it
- Test auto-update with unreachable network share → verify non-blocking failure
-
Security verification additions:
- Verify WinRM CredSSP connections do not leak credentials to unauthorized delegates (test in lab environment)
- Verify auth method is correctly recorded in audit log for all three methods
-
Documentation additions:
CONTRIBUTING.mdmust include a complete example of creating a new script plugin:- Write the .ps1 file with
param()block - Create the .json manifest with matching parameter names
- Place both in the shared repository
- Refresh the library in the app
- Show the expected result in the Script Library view
- Write the .ps1 file with
- Deployment guide must include: CredSSP GPO configuration instructions (both client and server side), WinRM HTTPS listener setup if HTTPS is required, firewall port requirements (5985/5986)
appsettings.jsondocumentation: all available keys, what they control, acceptable values
-
Release preparation additions:
- Build the
SysOpsUpdater.exebootstrapper as a separate console project - Include it in the published output
- Create a sample
version.jsonfor the update share - Document the process for publishing an update to the network share
- Build the
(Same as Rev 1 plus:)
- CredSSP error handling produces actionable messages
- Domain switching failure is graceful
- Auto-update handles corrupted packages and unreachable shares
-
CONTRIBUTING.mdincludes complete script plugin walkthrough - Deployment guide covers CredSSP GPO, HTTPS listener, and firewall setup
-
SysOpsUpdater.exebootstrapper is included in published output -
appsettings.jsonis fully documented
When working through these phases, follow these principles:
- Complete each phase fully before starting the next. Don't jump ahead — each phase depends on the previous.
- Commit after each meaningful task within a phase. Use descriptive commit messages:
Phase 0: Configure Serilog with credential destructuring policy. - Run all existing tests after every change. Never break previously passing tests.
- Every public method gets an XML documentation comment. No exceptions.
- Every service method logs at appropriate levels. Use
Informationfor business events,Debugfor technical details,Warningfor recoverable issues,Errorfor failures. - Never store credentials. If you find yourself writing credential data to any file, database, log, or config — stop. That's a bug. Passwords never appear in audit logs, Serilog output, settings, or temp files.
- Use
CancellationTokenon every async method. Thread it through the entire call chain. - Use
ConfigureAwait(false)on non-UI async calls (Services, Infrastructure layers). Do NOT use it in ViewModel layer (needs SynchronizationContext for UI updates). - Interface-first development. Define the interface, then implement it. Register it in DI. Inject it where needed. Never
newup a service directly. - When in doubt about a design decision, check the design document (SysOpsCommander_DesignDocument.docx). It is the source of truth.
- Keep the solution building with zero warnings at all times. Treat warnings as errors.
- Parameters to remote scripts are ALWAYS injected via
AddParameter(). Never concatenate user input into script strings. IHostTargetingServiceis a SINGLETON. It is the shared state between AD Explorer and Execution views. Do not create multiple instances.- Read the Critical Technical Notes section at the top of this document before starting. It covers cross-cutting concerns that affect multiple phases.