Discover-WindowsServer.ps1
|
#Requires -Version 5.1 # Windows Server 2012 R2 ships PowerShell 4.0 and fails this check. tools\Install-LegacyPrerequisites.ps1 # installs PowerShell 7 there (asks first, never restarts); then run this script with pwsh.exe. <# .SYNOPSIS Ultimate Modular Windows Server Discovery Toolkit - main entry point. .DESCRIPTION Read-only, modular Windows Server discovery for MSP project scoping, server refresh / Hyper-V refresh, application / Azure migration, decommission readiness, CMMC/security review, and infrastructure documentation. Runs LOCALLY on the server being discovered. Nothing in this toolkit changes the system except writing its own output files. See docs\README.md and the "Critical Safety Rules" in the build spec. .EXAMPLE .\Discover-WindowsServer.ps1 .EXAMPLE .\Discover-WindowsServer.ps1 -Mode Deep -ProjectType Decommission -IncludeConfigDependencyScan .EXAMPLE .\Discover-WindowsServer.ps1 -Mode Custom -IncludeModules SystemInventory,Applications,SQL,IIS .EXAMPLE .\Discover-WindowsServer.ps1 -Mode Fast -ComplianceLens CMMC .EXAMPLE .\Discover-WindowsServer.ps1 -Mode Deep -WhatIf Prints which modules would run and writes discovery-plan.md, then exits without collecting anything - useful to show a client exactly what will be touched first. #> [CmdletBinding()] param( [ValidateSet('Fast','Deep','Custom')] [string]$Mode = 'Fast', [ValidateSet('GeneralDiscovery','ServerRefresh','HyperVRefresh','Decommission','AzureMigration','AppMigration','CMMCReadiness')] [string]$ProjectType = 'GeneralDiscovery', [string[]]$IncludeModules, [string[]]$ExcludeModules, [string]$OutputRoot = 'C:\Temp', [switch]$DeepFileShareScan, [int]$MaxDepth = 3, [int]$LargeFileThresholdGB = 5, [int]$OldFileYears = 7, [int]$EventLogDays = 14, [int]$MaxEventSamplesPerLog = 50, [switch]$FullEventLogExport, [switch]$IncludeConfigDependencyScan, [int]$ConfigScanMaxFileSizeMB = 10, [switch]$IncludeUserProfiles, [switch]$IncludeRecycleBin, [switch]$IncludeWindowsFolder, [switch]$AttemptSqlIntegratedAuth, [switch]$SkipZip, [switch]$GenerateEvidenceManifest, [ValidateSet('None','CMMC','GeneralSecurity')] [string]$ComplianceLens = 'None', [switch]$Quiet, [switch]$VerboseLogging, [switch]$WhatIf ) $ErrorActionPreference = 'Continue' $scriptRoot = $PSScriptRoot if (-not $scriptRoot) { $scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path } # --- Import framework + engine (global so collector modules can resolve them) --- $coreModule = Join-Path $scriptRoot 'modules\Core\Core.psm1' $outputModule = Join-Path $scriptRoot 'modules\Output\Output.psm1' $engineModule = Join-Path $scriptRoot 'Discover-WindowsServer.psm1' try { Import-Module $coreModule -Force -DisableNameChecking -Global -ErrorAction Stop Import-Module $outputModule -Force -DisableNameChecking -Global -ErrorAction Stop Import-Module $engineModule -Force -DisableNameChecking -Global -ErrorAction Stop } catch { Write-Host ("FATAL: Failed to load toolkit framework modules: {0}" -f $_.Exception.Message) -ForegroundColor Red throw } # --- Load configuration ------------------------------------------------------ $configDir = Join-Path $scriptRoot 'config' $config = Get-DiscoveryConfigBundle -ConfigDirectory $configDir # --- Resolve identity context ------------------------------------------------ $isAdmin = Test-IsAdministrator $isSystem = Test-IsSystem # --- Resolve effective parameters -------------------------------------------- # Precedence: explicit command line > mode config (fast/deep parameterDefaults) # > global config (default.discovery.json "defaults") > built-in fallback. # # NOTE: this resolver is deliberately type-agnostic. The earlier version only handled # [bool], so the numeric keys in fast/deep parameterDefaults (eventLogDays, # maxEventSamplesPerLog) were silently ignored and Deep mode never actually collected # more event-log samples than Fast. Cast the result at the call site. $modeCfg = if ($Mode -eq 'Deep') { $config.Deep } elseif ($Mode -eq 'Fast') { $config.Fast } else { $null } $globalDefaults = $null if ($config -and $config.Default -and $config.Default.defaults) { $globalDefaults = $config.Default.defaults } function Resolve-EffValue { param([bool]$IsBound, $UserValue, [string]$ConfigKey, $ModeConfig, $GlobalDefaults, $Default) if ($IsBound) { return $UserValue } if ($ModeConfig -and $ModeConfig.parameterDefaults) { $pd = $ModeConfig.parameterDefaults if ($pd.PSObject.Properties[$ConfigKey] -and ($null -ne $pd.$ConfigKey)) { return $pd.$ConfigKey } } if ($GlobalDefaults -and $GlobalDefaults.PSObject.Properties[$ConfigKey] -and ($null -ne $GlobalDefaults.$ConfigKey)) { return $GlobalDefaults.$ConfigKey } return $Default } # Switches $effDeepFileShareScan = [bool](Resolve-EffValue -IsBound $PSBoundParameters.ContainsKey('DeepFileShareScan') -UserValue $DeepFileShareScan.IsPresent -ConfigKey 'deepFileShareScan' -ModeConfig $modeCfg -GlobalDefaults $globalDefaults -Default $false) $effIncludeConfigDepScan = [bool](Resolve-EffValue -IsBound $PSBoundParameters.ContainsKey('IncludeConfigDependencyScan') -UserValue $IncludeConfigDependencyScan.IsPresent -ConfigKey 'includeConfigDependencyScan' -ModeConfig $modeCfg -GlobalDefaults $globalDefaults -Default $false) $effAttemptSqlAuth = [bool](Resolve-EffValue -IsBound $PSBoundParameters.ContainsKey('AttemptSqlIntegratedAuth') -UserValue $AttemptSqlIntegratedAuth.IsPresent -ConfigKey 'attemptSqlIntegratedAuth' -ModeConfig $modeCfg -GlobalDefaults $globalDefaults -Default $false) $effFullEventLogExport = [bool](Resolve-EffValue -IsBound $PSBoundParameters.ContainsKey('FullEventLogExport') -UserValue $FullEventLogExport.IsPresent -ConfigKey 'fullEventLogExport' -ModeConfig $modeCfg -GlobalDefaults $globalDefaults -Default $false) $effIncludeUserProfiles = [bool](Resolve-EffValue -IsBound $PSBoundParameters.ContainsKey('IncludeUserProfiles') -UserValue $IncludeUserProfiles.IsPresent -ConfigKey 'includeUserProfiles' -ModeConfig $modeCfg -GlobalDefaults $globalDefaults -Default $false) $effIncludeRecycleBin = [bool](Resolve-EffValue -IsBound $PSBoundParameters.ContainsKey('IncludeRecycleBin') -UserValue $IncludeRecycleBin.IsPresent -ConfigKey 'includeRecycleBin' -ModeConfig $modeCfg -GlobalDefaults $globalDefaults -Default $false) $effIncludeWindowsFolder = [bool](Resolve-EffValue -IsBound $PSBoundParameters.ContainsKey('IncludeWindowsFolder') -UserValue $IncludeWindowsFolder.IsPresent -ConfigKey 'includeWindowsFolder' -ModeConfig $modeCfg -GlobalDefaults $globalDefaults -Default $false) # Numeric / string tuning values (previously hardcoded to the param() defaults regardless of config) $effMaxDepth = [int](Resolve-EffValue -IsBound $PSBoundParameters.ContainsKey('MaxDepth') -UserValue $MaxDepth -ConfigKey 'maxDepth' -ModeConfig $modeCfg -GlobalDefaults $globalDefaults -Default 3) $effLargeFileThresholdGB = [int](Resolve-EffValue -IsBound $PSBoundParameters.ContainsKey('LargeFileThresholdGB') -UserValue $LargeFileThresholdGB -ConfigKey 'largeFileThresholdGB' -ModeConfig $modeCfg -GlobalDefaults $globalDefaults -Default 5) $effOldFileYears = [int](Resolve-EffValue -IsBound $PSBoundParameters.ContainsKey('OldFileYears') -UserValue $OldFileYears -ConfigKey 'oldFileYears' -ModeConfig $modeCfg -GlobalDefaults $globalDefaults -Default 7) $effEventLogDays = [int](Resolve-EffValue -IsBound $PSBoundParameters.ContainsKey('EventLogDays') -UserValue $EventLogDays -ConfigKey 'eventLogDays' -ModeConfig $modeCfg -GlobalDefaults $globalDefaults -Default 14) $effMaxEventSamples = [int](Resolve-EffValue -IsBound $PSBoundParameters.ContainsKey('MaxEventSamplesPerLog') -UserValue $MaxEventSamplesPerLog -ConfigKey 'maxEventSamplesPerLog' -ModeConfig $modeCfg -GlobalDefaults $globalDefaults -Default 50) $effConfigScanMaxFileMB = [int](Resolve-EffValue -IsBound $PSBoundParameters.ContainsKey('ConfigScanMaxFileSizeMB') -UserValue $ConfigScanMaxFileSizeMB -ConfigKey 'configScanMaxFileSizeMB' -ModeConfig $modeCfg -GlobalDefaults $globalDefaults -Default 10) $effOutputRoot = [string](Resolve-EffValue -IsBound $PSBoundParameters.ContainsKey('OutputRoot') -UserValue $OutputRoot -ConfigKey 'outputRoot' -ModeConfig $modeCfg -GlobalDefaults $globalDefaults -Default 'C:\Temp') if ([string]::IsNullOrWhiteSpace($effOutputRoot)) { $effOutputRoot = 'C:\Temp' } # Always absolute: child processes (secedit, ...) resolve relative paths against the process # cwd, which is not PowerShell's location, and drive-relative forms like C:out mean something else again. $effOutputRoot = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($effOutputRoot) # Custom mode requires include and/or exclude. if ($Mode -eq 'Custom' -and -not ($IncludeModules -or $ExcludeModules)) { Write-Host 'Custom mode requires -IncludeModules and/or -ExcludeModules. Falling back to Fast mode.' -ForegroundColor Yellow $Mode = 'Fast' } # --- Prepare output folder --------------------------------------------------- $timestamp = (Get-Date).ToString('yyyyMMdd_HHmmss') $outputPath = Join-Path $effOutputRoot ("Discover-WindowsServer_{0}_{1}" -f $env:COMPUTERNAME, $timestamp) Ensure-Directory -Path $effOutputRoot | Out-Null Ensure-Directory -Path $outputPath | Out-Null # Output layout. TWO top-level folders, because a run folder with a dozen sibling # directories and thirty loose files at its root is not a deliverable, it is a dump: # # reports\ what a human reads - two finished documents plus their supporting pack. # evidence\ everything those documents were built from: logs, raw captures, live # status, and every dataset as CSV and JSON. # # The PATH KEYS below are the contract every module codes against ($Context.Paths['Csv'] # and friends). They are deliberately unchanged from the previous flat layout - only the # directories they resolve to have moved - so no collector needed editing to adopt this. $folderMap = [ordered]@{ Reports = 'reports' ReportsSupporting = 'reports\supporting' Html = 'reports' Markdown = 'reports\supporting' ClientSafe = 'reports\supporting\client-safe' Internal = 'reports\supporting\internal' EvidenceRoot = 'evidence' Csv = 'evidence\data\csv' Json = 'evidence\data\json' Logs = 'evidence\logs' Raw = 'evidence\raw' Status = 'evidence\status' Evidence = 'evidence\manifest' Archive = 'archive' } $paths = [ordered]@{} foreach ($k in $folderMap.Keys) { $p = Join-Path $outputPath $folderMap[$k] Ensure-Directory -Path $p | Out-Null $paths[$k] = $p } # Log file paths + create empty files so appends succeed. $logPaths = [ordered]@{ Summary = Join-Path $paths['Logs'] 'summary.txt' Errors = Join-Path $paths['Logs'] 'errors.txt' Warnings = Join-Path $paths['Logs'] 'warnings.txt' Debug = Join-Path $paths['Logs'] 'debug.log' } foreach ($lp in $logPaths.Values) { if (-not (Test-Path -LiteralPath $lp)) { New-Item -ItemType File -Path $lp -Force | Out-Null } } # --- Build the parameters hashtable passed into the context ------------------ $parameters = @{ Mode = $Mode ProjectType = $ProjectType IncludeModules = $IncludeModules ExcludeModules = $ExcludeModules OutputRoot = $effOutputRoot DeepFileShareScan = $effDeepFileShareScan MaxDepth = $effMaxDepth LargeFileThresholdGB = $effLargeFileThresholdGB OldFileYears = $effOldFileYears EventLogDays = $effEventLogDays MaxEventSamplesPerLog = $effMaxEventSamples FullEventLogExport = $effFullEventLogExport IncludeConfigDependencyScan = $effIncludeConfigDepScan ConfigScanMaxFileSizeMB = $effConfigScanMaxFileMB IncludeUserProfiles = $effIncludeUserProfiles IncludeRecycleBin = $effIncludeRecycleBin IncludeWindowsFolder = $effIncludeWindowsFolder AttemptSqlIntegratedAuth = $effAttemptSqlAuth SkipZip = $SkipZip.IsPresent GenerateEvidenceManifest = $GenerateEvidenceManifest.IsPresent ComplianceLens = $ComplianceLens Quiet = $Quiet.IsPresent VerboseLogging = $VerboseLogging.IsPresent WhatIf = $WhatIf.IsPresent } # --- Build context ----------------------------------------------------------- $context = New-DiscoveryContext -Mode $Mode -ProjectType $ProjectType -ComplianceLens $ComplianceLens ` -OutputRoot $effOutputRoot -OutputPath $outputPath -IsAdmin $isAdmin -IsSystem $isSystem ` -Parameters $parameters -Config $config -Quiet:$Quiet.IsPresent -VerboseLogging:$VerboseLogging.IsPresent $context.Paths = $paths $context.LogPaths = $logPaths if (-not $Quiet.IsPresent) { Write-Host '' Write-Host '=== Ultimate Modular Windows Server Discovery Toolkit ===' -ForegroundColor Cyan Write-Host ("Computer: {0} | Mode: {1} | Project: {2} | Lens: {3}" -f $env:COMPUTERNAME, $Mode, $ProjectType, $ComplianceLens) -ForegroundColor Cyan Write-Host ("Elevated: {0} | SYSTEM: {1} | Output: {2}" -f $isAdmin, $isSystem, $outputPath) -ForegroundColor Cyan Write-Host '' } # --- Run --------------------------------------------------------------------- $null = Invoke-Discovery -Context $context # Return the output path for callers / pipelines. $outputPath |