diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index fa4f112..040d7d1 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -66,8 +66,8 @@ body: - type: textarea id: defaults attributes: - label: Relevant defaults.json snippet (REDACT SECRETS) - description: If the bug involves config (network, agents, file-server, etc.), paste the matching defaults.json fragment. **Remove any credentials, tokens, or client secrets first.** + label: Relevant rackstack.config.json snippet (REDACT SECRETS) + description: If the bug involves config (network, agents, file-server, etc.), paste the matching rackstack.config.json (or legacy defaults.json) fragment. **Remove any credentials, tokens, or client secrets first.** render: json - type: checkboxes diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 48de5ed..5b80cc8 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -42,7 +42,7 @@ body: - Hyper-V (VM deployment, replica, checkpoints) - Clustering (failover, quorum) - Active Directory / Identity - - Configuration (defaults.json, batch mode) + - Configuration (rackstack.config.json, batch mode) - CLI / Headless surface - Health / Monitoring / Reports - Security hardening diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 029ca04..ed32f0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -184,7 +184,7 @@ jobs: $assets = @( @{ Name = 'RackStack.exe'; Path = 'builds\RackStack.exe' } @{ Name = "RackStack v$ver.ps1"; Path = "builds\RackStack v$ver.ps1" } - @{ Name = 'defaults.example.json'; Path = 'defaults.example.json' } + @{ Name = 'rackstack.config.example.json'; Path = 'rackstack.config.example.json' } ) $lines = foreach ($a in $assets) { $h = (Get-FileHash -LiteralPath $a.Path -Algorithm SHA256).Hash.ToLower() @@ -371,7 +371,7 @@ jobs: $assetArgs = @( "builds\RackStack.exe" "builds\RackStack v$ver.ps1" - "defaults.example.json" + "rackstack.config.example.json" 'release-hashes.txt' ) $sbomPath = "builds\RackStack-$ver-sbom.cyclonedx.json" diff --git a/.gitignore b/.gitignore index 43c39a9..8239044 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ # Credentials (org-specific URLs, keys, passwords) +# Base + company config under both naming schemes; only the example is tracked +rackstack.config.json +*.rackstack.config.json defaults.json *.defaults.json +!rackstack.config.example.json !defaults.example.json # Build artifacts (monolithic, exe, archives) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7425ab6..53239a0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ Thanks for your interest in contributing! Here's how to get started. ## Getting Started 1. Fork the repo and clone it locally -2. Copy `defaults.example.json` to `defaults.json` and fill in your environment details +2. Copy `rackstack.config.example.json` to `rackstack.config.json` and fill in your environment details (a legacy `defaults.json` is still read when present) 3. Make your changes in the `Modules/` directory 4. Run the test suite before submitting @@ -30,7 +30,7 @@ powershell -ExecutionPolicy Bypass -File Tests\pssa-check.ps1 ## Pull Request Checklist -- [ ] All 5,048 tests pass (`Run-Tests.ps1` exits with code 0) +- [ ] The full structural test suite passes (`Run-Tests.ps1` exits with code 0) - [ ] PSScriptAnalyzer reports 0 errors (`pssa-check.ps1`) - [ ] Monolithic synced (`sync-to-monolithic.ps1` shows 0 parse errors) - [ ] New functions follow PowerShell verb-noun naming (`Get-`, `Set-`, `Test-`, `Show-`) @@ -64,7 +64,7 @@ powershell -ExecutionPolicy Bypass -File Tests\pssa-check.ps1 ## Configuration Safety -Never hardcode organization names, domains, contact info, or credentials in module code. Use the `$script:` variables that are loaded from `defaults.json`: +Never hardcode organization names, domains, contact info, or credentials in module code. Use the `$script:` variables that are loaded from `rackstack.config.json`: - `$script:Domain` for domain operations - `$script:SupportContact` for error messages diff --git a/Changelog.md b/Changelog.md index bf6309d..0cd78f8 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,18 @@ # Changelog +## v1.122.0 + +Namespaced config-file naming. The base config is now `rackstack.config.json` and company overrides are `.rackstack.config.json` -- names that stay unambiguous when the EXE runs from a shared or busy folder. Nothing breaks for existing setups: the legacy `defaults.json` and `.defaults.json` names are still read whenever the new-named file is absent, and the tool keeps saving to whichever file it loaded. + +- **New config names, per-file preference.** `rackstack.config.json` is used when it exists; otherwise a legacy `defaults.json` is read. The same rule applies per company file, and when both names exist for the same company the new name wins. Company selection via `_companyDefaults` is unchanged, and multiple company files are still supported. +- **New installs get the new names.** The first-run wizard and the in-tool defaults editor create `rackstack.config.json`; existing legacy files keep working in place with no migration step. +- **The example template is now `rackstack.config.example.json`** (renamed from `defaults.example.json`) and ships in releases under that name. +- **Menus and prompts show the actual loaded filename**, so a machine still running on `defaults.json` sees that name in the save picker and settings screens. +- **Uninstall/self-destruct cleans up config files under both naming schemes**, including company override files, which it previously left behind. +- **Docs, issue templates, and the config guide updated** to the new naming with the fallback behavior documented. + +No module or CLI action changes (81 modules, 201 actions). + ## v1.121.15 Final audit sweep — remaining fixes across the firewall/RDP, iSCSI, network-diagnostics, and session modules. diff --git a/Header.ps1 b/Header.ps1 index d332e93..09f301d 100644 --- a/Header.ps1 +++ b/Header.ps1 @@ -30,9 +30,9 @@ 7h3 4b1d3r .VERSION - 1.121.15 + 1.122.0 .LAST UPDATED - 07/01/2026 + 07/16/2026 .CHANGELOG v1.21.1 ROBUSTNESS, UX, CACHE CONSISTENCY: diff --git a/Modules/00-Initialization.ps1 b/Modules/00-Initialization.ps1 index abfe8a9..83d3d58 100644 --- a/Modules/00-Initialization.ps1 +++ b/Modules/00-Initialization.ps1 @@ -1,11 +1,11 @@ #region ===== SCRIPT INITIALIZATION ===== -# Tool identity (override via defaults.json) +# Tool identity (override via rackstack.config.json) $script:ToolName = "RackStack" # Short name (used in filenames, scheduled tasks) $script:ToolFullName = "RackStack" # Display name (used in UI banners, reports) -$script:SupportContact = "" # Support contact shown in agent installer messages (set via defaults.json) +$script:SupportContact = "" # Support contact shown in agent installer messages (set via rackstack.config.json) $script:ConfigDirName = "rackstackconfig" # Derived: config directory under USERPROFILE -# Company/Environment Variables (generic defaults - override via defaults.json) +# Company/Environment Variables (generic defaults - override via rackstack.config.json) $domain = "" # Default primary domain (empty = not configured) $localadminaccountname = 'localadmin' # Local administrator account name $FullName = "Local Administrator" # Full name for the new local admin account @@ -15,7 +15,7 @@ $BackupName = "Backup" # Backup NIC name made by SET $script:logFilePath = $null # Path for log file (set to enable logging) $script:emailAddress = $null # Email address for log notifications -# DNS Presets for quick configuration (custom presets merged from defaults.json) +# DNS Presets for quick configuration (custom presets merged from rackstack.config.json) $script:DNSPresets = [ordered]@{ "Google DNS" = @("8.8.8.8", "8.8.4.4") "Cloudflare" = @("1.1.1.1", "1.0.0.1") @@ -34,7 +34,7 @@ $script:DefaultDownloadTimeoutSeconds = 1800 # 30 minutes for standard downlo $script:MaxDownloadRetries = 3 # Max retry attempts for large file downloads (>500MB) # Centralized timeout configuration (seconds) -# These can be overridden via defaults.json "Timeouts" section +# These can be overridden via rackstack.config.json "Timeouts" section $script:Timeouts = @{ CIMQuery = 10 # Standard CIM/WMI queries CIMQuerySlow = 15 # CIM queries on older/slower systems @@ -53,7 +53,7 @@ $script:Timeouts = @{ HealthCheckCache = 30 # Health check result cache TTL } -# Configurable MSP agent installer (override via defaults.json AgentInstaller) +# Configurable MSP agent installer (override via rackstack.config.json AgentInstaller) $script:AgentInstaller = @{ ToolName = "MSP" FolderName = "Agents" @@ -73,7 +73,7 @@ $script:AgentInstaller = @{ # service registers (which left agents installed-but-unenrolled). Bounded by the install timeout. $script:AgentEarlyDetectGraceSeconds = 90 -# Additional agent installers (v1.8.0, override via defaults.json AdditionalAgents array) +# Additional agent installers (v1.8.0, override via rackstack.config.json AdditionalAgents array) $script:AdditionalAgents = @() # Agent installer cache @@ -90,10 +90,10 @@ $script:PowerPlanGUID = @{ # Default connectivity test target (used by SystemCheck, SET internet detection) $script:DefaultConnectivityTarget = "8.8.8.8" -# Default temp directory for transcripts, reports, and exports (override via defaults.json TempPath) +# Default temp directory for transcripts, reports, and exports (override via rackstack.config.json TempPath) $script:TempPath = "$env:SystemDrive\Temp" -# SAN target IP mappings - last octet suffixes paired with labels (override via defaults.json SANTargetMappings) +# SAN target IP mappings - last octet suffixes paired with labels (override via rackstack.config.json SANTargetMappings) $script:SANTargetMappings = @( @{ Suffix = 10; Label = "A0" } @{ Suffix = 11; Label = "B1" } @@ -105,13 +105,13 @@ $script:SANTargetMappings = @( @{ Suffix = 17; Label = "A3" } ) -# Custom SAN target pairings - defines A/B pairs and host-to-pair assignments (override via defaults.json SANTargetPairings) +# Custom SAN target pairings - defines A/B pairs and host-to-pair assignments (override via rackstack.config.json SANTargetPairings) # When set, this overrides the default Initialize-SANTargetPairs logic and Get-SANTargetsForHost retry order. # A side = even suffixes, B side = odd suffixes by convention. # CycleSize controls the modulo: host 5 maps the same as host 1, host 6 as host 2, etc. $script:SANTargetPairings = $null -# Defender exclusion paths for Hyper-V hosts (override via defaults.json) +# Defender exclusion paths for Hyper-V hosts (override via rackstack.config.json) $script:DefenderExclusionPaths = @( "C:\ProgramData\Microsoft\Windows\Hyper-V" "C:\ProgramData\Microsoft\Windows\Hyper-V\Snapshots" @@ -122,7 +122,7 @@ $script:DefenderCommonVMPaths = @() # Populated dynamically by Update-DefenderV # Operator-configurable list of services to surface in service-audit views. Set to $null # at init so callers can detect "not configured" and fall back to the built-in list (vmms, -# vmcompute, ClusSvc, MSiSCSI, etc.). Populated by Import-Defaults when defaults.json +# vmcompute, ClusSvc, MSiSCSI, etc.). Populated by Import-Defaults when rackstack.config.json # contains a `MonitoredServices` array. Prior to v1.98.8 callers read `$script:Defaults. # MonitoredServices` but $script:Defaults was never assigned, so the documented field # was dead code. @@ -132,7 +132,7 @@ $script:MonitoredServices = $null $script:WindowsLicensingAppId = "55c92734-d682-4d71-983e-d6ec3f16059f" # FileServer - Cloudflare Access-protected file server -# Override via defaults.json - empty BaseURL = cloud features disabled +# Override via rackstack.config.json - empty BaseURL = cloud features disabled $script:FileServer = @{ StorageType = "nginx" # nginx (default), azure, static BaseURL = "" @@ -146,10 +146,10 @@ $script:FileServer = @{ AgentFolder = "Agents" } -# Azure Arc server onboarding (override via defaults.json "AzureArc" section) +# Azure Arc server onboarding (override via rackstack.config.json "AzureArc" section) # Empty TenantId/SubscriptionId/ResourceGroup = Arc onboarding not configured. -# ServicePrincipalSecret should be left empty in defaults.json committed to -# source control — supply it at runtime, via an environment variable, or via +# ServicePrincipalSecret should be left empty in any rackstack.config.json kept +# under source control - supply it at runtime, via an environment variable, or via # a secrets manager. See 65-AzureArc.ps1 Resolve-AzureArcSecret. $script:AzureArc = @{ TenantId = "" # Azure AD / Entra tenant ID (GUID) @@ -163,7 +163,7 @@ $script:AzureArc = @{ Tags = "" # Optional Arc resource tags, "key1=val1,key2=val2" } -# Microsoft Defender for Endpoint onboarding (override via defaults.json "DefenderEndpoint") +# Microsoft Defender for Endpoint onboarding (override via rackstack.config.json "DefenderEndpoint") # Paths point to the per-tenant onboarding/offboarding .cmd scripts downloaded # from the Defender portal (security.microsoft.com). Empty = MDE not configured. $script:DefenderEndpoint = @{ @@ -171,7 +171,7 @@ $script:DefenderEndpoint = @{ OffboardingScriptPath = "" # Path to the offboarding .cmd (expires ~30 days after generation) } -# WSUS server setup (override via defaults.json "WSUS") +# WSUS server setup (override via rackstack.config.json "WSUS") # Empty ContentPath = WSUS setup not configured. ContentPath must be an # absolute local path; it holds downloaded update binaries (can grow large). $script:WSUS = @{ @@ -181,7 +181,7 @@ $script:WSUS = @{ SyncHour = 3 # Hour (0-23) for the daily automatic catalog sync } -# AD CS Certification Authority bootstrap (override via defaults.json "ADCS") +# AD CS Certification Authority bootstrap (override via rackstack.config.json "ADCS") # Empty CACommonName = AD CS setup not configured. Configuring a CA is hard # to reverse — the CA install requires a typed confirmation regardless. $script:ADCS = @{ @@ -192,7 +192,7 @@ $script:ADCS = @{ ValidityYears = 10 # Root CA certificate validity (1-25 years) } -# Storage Migration Service (override via defaults.json "StorageMigration") +# Storage Migration Service (override via rackstack.config.json "StorageMigration") # InstallProxy controls whether the StorageMigrationSetup CLI action also # installs the SMS-Proxy feature (run the proxy on the destination server). $script:StorageMigration = @{ @@ -211,11 +211,11 @@ $script:VHDCachePath = "D:\Virtual Machines\_BaseImages" # Cached sysprepped $script:ClusterVHDCachePath = "C:\ClusterStorage\Volume1\_BaseImages" # Cached VHDs on clusters $script:StorageInitialized = $false # Whether host storage has been initialized -# Dashboard color thresholds for CPU/memory/disk usage (override via defaults.json DashboardWarningPercent / DashboardCriticalPercent) +# Dashboard color thresholds for CPU/memory/disk usage (override via rackstack.config.json DashboardWarningPercent / DashboardCriticalPercent) $script:DashboardWarningPercent = 70 $script:DashboardCriticalPercent = 90 -# Storage backend type: iSCSI, FC, S2D, SMB3, NVMeoF, Local (override via defaults.json StorageBackendType) +# Storage backend type: iSCSI, FC, S2D, SMB3, NVMeoF, Local (override via rackstack.config.json StorageBackendType) $script:StorageBackendType = "iSCSI" # Store script path at startup (MUST be before functions for Exit-Script to work) @@ -233,7 +233,7 @@ if (-not $PSCommandPath -and $script:ScriptPath) { if (-not $script:ModuleRoot -and $script:ScriptPath) { $script:ModuleRoot = [System.IO.Path]::GetDirectoryName($script:ScriptPath) } -$script:ScriptVersion = "1.121.15" +$script:ScriptVersion = "1.122.0" $script:ScriptStartTime = Get-Date # Post-update cleanup: UpdateSelf / Rollback leave a `.pending-delete` sibling next to RackStack.exe. @@ -294,7 +294,7 @@ $script:RebootNeeded = $false $script:DisabledAdminReboot = $false $script:ReturnToMainMenu = $false # Flag to signal "go straight to main menu" -# Auto-update: if true, automatically download and install updates on startup (override via defaults.json) +# Auto-update: if true, automatically download and install updates on startup (override via rackstack.config.json) $script:AutoUpdate = $false # Auto-update state (populated by Test-StartupUpdateCheck on launch) @@ -323,9 +323,23 @@ $script:AppConfigDir = "$env:USERPROFILE\.$($script:ConfigDirName)" $script:FavoritesPath = "$script:AppConfigDir\favorites.json" $script:HistoryPath = "$script:AppConfigDir\history.json" $script:SessionStatePath = "$script:AppConfigDir\session.json" -$script:DefaultsPath = "$script:ModuleRoot\defaults.json" -$script:CompanyDefaultsName = $null # e.g., "acme" (loaded from acme.defaults.json) -$script:CompanyDefaultsPath = $null # e.g., "C:\...\acme.defaults.json" +# Base config resolution: the namespaced rackstack.config.json is preferred; the +# legacy defaults.json is still read when only it exists. New config files are +# always created under the new name, and saves target the resolved (loaded) file. +function Get-RackStackConfigPath { + param( + [Parameter(Mandatory=$true)] + [string]$Root + ) + $preferredPath = Join-Path $Root "rackstack.config.json" + $legacyPath = Join-Path $Root "defaults.json" + if (Test-Path -LiteralPath $preferredPath) { return $preferredPath } + if (Test-Path -LiteralPath $legacyPath) { return $legacyPath } + return $preferredPath +} +$script:DefaultsPath = Get-RackStackConfigPath -Root $script:ModuleRoot +$script:CompanyDefaultsName = $null # e.g., "acme" (loaded from acme.rackstack.config.json, or legacy acme.defaults.json) +$script:CompanyDefaultsPath = $null # e.g., "C:\...\acme.rackstack.config.json" $script:Favorites = @() $script:CommandHistory = @() $script:MaxHistoryItems = 100 @@ -338,7 +352,7 @@ $script:BuiltInVMTemplates = $null $script:CustomRoleTemplates = @{} $script:TimeZoneRegion = "" -# VM naming convention (override via defaults.json VMNaming) +# VM naming convention (override via rackstack.config.json VMNaming) $script:VMNaming = @{ SiteId = "" Pattern = "{Site}-{Prefix}{Seq}" diff --git a/Modules/02-Logging.ps1 b/Modules/02-Logging.ps1 index 9467e85..693fd20 100644 --- a/Modules/02-Logging.ps1 +++ b/Modules/02-Logging.ps1 @@ -164,7 +164,7 @@ function Write-CenteredOutput { $script:ErrorCodes = @{ # Core / System (1000-1999) "RS-1001" = @{ Message = "Script requires administrative privileges"; Category = "Core" } - "RS-1002" = @{ Message = "defaults.json parse error"; Category = "Core" } + "RS-1002" = @{ Message = "config file parse error"; Category = "Core" } "RS-1003" = @{ Message = "Module load failure"; Category = "Core" } "RS-1004" = @{ Message = "PowerShell version unsupported"; Category = "Core" } "RS-1005" = @{ Message = "Transcript start failed"; Category = "Core" } diff --git a/Modules/05-SystemCheck.ps1 b/Modules/05-SystemCheck.ps1 index 263e442..1227a39 100644 --- a/Modules/05-SystemCheck.ps1 +++ b/Modules/05-SystemCheck.ps1 @@ -434,7 +434,7 @@ function Test-AllConnectivity { } # DNS resolution test. The target is configurable via $script:NetworkTestDnsTarget - # (defaults.json key NetworkTestDnsTarget) so air-gapped / restricted environments that + # (rackstack.config.json key NetworkTestDnsTarget) so air-gapped / restricted environments that # block public hostnames but allow internal DNS aren't forced into a false-negative. # Default falls back to a public name for the common case. $dnsTarget = if ($script:NetworkTestDnsTarget) { $script:NetworkTestDnsTarget } else { "google.com" } diff --git a/Modules/09-SET.ps1 b/Modules/09-SET.ps1 index 26de15b..0d4ee02 100644 --- a/Modules/09-SET.ps1 +++ b/Modules/09-SET.ps1 @@ -380,8 +380,8 @@ function New-SwitchEmbeddedTeam { } if ($vnicReady) { - # Use $script:ManagementName (defaults.json override-able) rather than the - # function param default of "Management" — operators who set defaults.json + # Use $script:ManagementName (rackstack.config.json override-able) rather than the + # function param default of "Management" - operators who set rackstack.config.json # ManagementName were having it silently ignored by this SET branch even # though the External-switch branch already used $script:ManagementName. $resolvedMgmtName = if ($script:ManagementName) { $script:ManagementName } else { $ManagementName } @@ -804,7 +804,7 @@ function New-StandardVSwitch { } if ($vnicReady) { # $script:ManagementName is initialized in 00-Initialization.ps1 and - # overridable via defaults.json. The bare `$ManagementName` here was a + # overridable via rackstack.config.json. The bare `$ManagementName` here was a # typo that silently no-op'd the rename on every External vSwitch. Rename-VMNetworkAdapter -ManagementOS -Name $SwitchName -NewName $script:ManagementName -ErrorAction SilentlyContinue } diff --git a/Modules/17-DefenderExclusions.ps1 b/Modules/17-DefenderExclusions.ps1 index d918c9c..05611c9 100644 --- a/Modules/17-DefenderExclusions.ps1 +++ b/Modules/17-DefenderExclusions.ps1 @@ -261,7 +261,7 @@ function Add-HyperVDefenderExclusions { $addedProcesses = @() $addedExtensions = @() - # Path exclusions (configurable via defaults.json DefenderExclusionPaths) + # Path exclusions (configurable via rackstack.config.json DefenderExclusionPaths) $pathsToExclude = @($script:DefenderExclusionPaths) # Add custom VM storage path if set @@ -269,7 +269,7 @@ function Add-HyperVDefenderExclusions { $pathsToExclude += $script:HostVMStoragePath } - # Check common VM storage locations (configurable via defaults.json DefenderCommonVMPaths) + # Check common VM storage locations (configurable via rackstack.config.json DefenderCommonVMPaths) foreach ($vmPath in $script:DefenderCommonVMPaths) { if (Test-Path -LiteralPath $vmPath) { $pathsToExclude += $vmPath diff --git a/Modules/23-LocalAdmin.ps1 b/Modules/23-LocalAdmin.ps1 index 59bc4da..f7463c1 100644 --- a/Modules/23-LocalAdmin.ps1 +++ b/Modules/23-LocalAdmin.ps1 @@ -42,7 +42,7 @@ function Add-LocalAdminAccount { if ([string]::IsNullOrWhiteSpace($accountName)) { $accountName = $localadminaccountname } if ([string]::IsNullOrWhiteSpace($accountName)) { Write-OutputColor " Configuration error: localadminaccountname is not set." -color "Error" - Write-OutputColor " Set 'LocalAdminAccountName' in defaults.json and restart $script:ToolFullName." -color "Warning" + Write-OutputColor " Set 'LocalAdminAccountName' in rackstack.config.json and restart $script:ToolFullName." -color "Warning" Write-PressEnter return } diff --git a/Modules/30-ServiceManager.ps1 b/Modules/30-ServiceManager.ps1 index e7e51ac..1584b44 100644 --- a/Modules/30-ServiceManager.ps1 +++ b/Modules/30-ServiceManager.ps1 @@ -60,7 +60,7 @@ function Show-ServiceManager { Write-OutputColor " ╚════════════════════════════════════════════════════════════════════════╝" -color "Info" Write-OutputColor "" -color "Info" - # Key services to monitor (configurable via defaults.json MonitoredServices, + # Key services to monitor (configurable via rackstack.config.json MonitoredServices, # unpacked by Import-Defaults into $script:MonitoredServices). $keyServices = if ($script:MonitoredServices) { $script:MonitoredServices diff --git a/Modules/34-Help.ps1 b/Modules/34-Help.ps1 index 3f6c314..e613fb1 100644 --- a/Modules/34-Help.ps1 +++ b/Modules/34-Help.ps1 @@ -74,7 +74,7 @@ function Show-Help { Write-OutputColor " │$(" VM DEPLOYMENT".PadRight(72))│" -color "Info" Write-OutputColor " ├────────────────────────────────────────────────────────────────────────┤" -color "Info" Write-OutputColor " │$(" Connection modes: Local host, Remote host, Failover Cluster".PadRight(72))│" -color "Success" - Write-OutputColor " │$(" Built-in templates (add more via CustomVMTemplates in defaults.json):".PadRight(72))│" -color "Info" + Write-OutputColor " │$(" Built-in templates (CustomVMTemplates in rackstack.config.json):".PadRight(72))│" -color "Info" Write-OutputColor " │$(" DC (Domain Controller) Win 4 CPU 8GB C:100GB".PadRight(72))│" -color "Success" Write-OutputColor " │$(" FS (File Server) Win 4 CPU 8GB C:100GB D:200GB".PadRight(72))│" -color "Success" Write-OutputColor " │$(" WEB (Web Server) Win 4 CPU 8GB C:100GB".PadRight(72))│" -color "Success" @@ -145,8 +145,8 @@ function Show-Help { Write-OutputColor " │$(" [5]-[8] Compare Profiles, Check Updates, Credentials, Remote Apply".PadRight(72))│" -color "Success" Write-OutputColor " │$(" [9] Favorites Save and recall frequently used menus".PadRight(72))│" -color "Success" Write-OutputColor " │$(" [10] Command History Last 100 operations".PadRight(72))│" -color "Success" - Write-OutputColor " │$(" [11] Edit Environment Defaults Organization values in defaults.json".PadRight(72))│" -color "Success" - Write-OutputColor " │$(" [12] Edit Custom Licenses KMS/AVMA keys in defaults.json".PadRight(72))│" -color "Success" + Write-OutputColor " │$(" [11] Edit Environment Defaults Org values in rackstack.config.json".PadRight(72))│" -color "Success" + Write-OutputColor " │$(" [12] Edit Custom Licenses KMS/AVMA in rackstack.config.json".PadRight(72))│" -color "Success" Write-OutputColor " │$(" [13] View Audit Log JSON audit log with rotation".PadRight(72))│" -color "Success" Write-OutputColor " │$(" [14] What's New Highlights for the current version".PadRight(72))│" -color "Success" Write-OutputColor " │$(" [15] System Info Banner Hostname, IP, domain, OS, uptime".PadRight(72))│" -color "Success" @@ -255,7 +255,7 @@ function Search-HelpTopics { @{ Title = "Licensing & NTP"; Keywords = @("license", "activation", "kms", "avma", "ntp", "time", "timezone", "clock"); Description = "Windows licensing status (KMS/AVMA/Retail), NTP configuration, time sync, and timezone setup" } @{ Title = "VM Management"; Keywords = @("checkpoint", "snapshot", "export", "import", "migration", "vhd", "iso"); Description = "VM checkpoints, export/import, migration readiness, VHD health, and ISO inventory" } @{ Title = "CLI Actions"; Keywords = @("cli", "action", "headless", "automation", "fleet", "json", "audit", "scan", "score", "dashboard", "monitor", "policy", "sla", "netmap", "validate"); Description = "201 CLI actions for headless automation. Run -ListActions to see all. JSON output via -OutputFormat JSON. Key: ServerScore, HealthDashboard, FleetReport, CISScan, NPSSetup, AlwaysOnVPNSetup, SIEMStatus." } - @{ Title = "SelfTest Action"; Keywords = @("selftest", "self-test", "diagnose", "diagnostic", "verify", "healthcheck", "sanity"); Description = "Internal diagnostic. -Action SelfTest checks PS version, elevation, module count, version consistency, defaults.json validity, temp path writability, FileServer reachability, and agent installer config. Exit 1 on any failure. Use -OutputFormat JSON for structured output." } + @{ Title = "SelfTest Action"; Keywords = @("selftest", "self-test", "diagnose", "diagnostic", "verify", "healthcheck", "sanity"); Description = "Internal diagnostic. -Action SelfTest checks PS version, elevation, module count, version consistency, rackstack.config.json validity, temp path writability, FileServer reachability, and agent installer config. Exit 1 on any failure. Use -OutputFormat JSON for structured output." } @{ Title = "Security Audits"; Keywords = @("security", "audit", "hardening", "compliance", "tls", "smb", "kerberos", "credguard", "applocker", "bitlockeraudit", "defenderexclusionaudit", "audit-policy", "secureboot", "tpm"); Description = "Security-focused CLI audits: TLSAudit, SMBAudit, KerberosAudit, CredGuardAudit, AppLockerAudit, BitLockerAudit, DefenderExclusionAudit, AuditPolicyAudit, SecureBootAudit, TPMAudit, UserAudit, LogonAudit, InsecureServiceAudit, RegistryAudit. All support -OutputFormat JSON." } @{ Title = "Network Audits"; Keywords = @("netaudit", "dns", "firewall-audit", "firewalllog", "arp", "route", "tcp", "netstat", "dhcp", "netprofile", "winrm", "qos", "nicoffload"); Description = "Network audits: DNSAudit, DNSCacheAudit, FirewallAudit, FirewallRuleAudit, FirewallLogAudit, ARPTableAudit, RouteTableAudit, TcpSettingsAudit, NetStatAudit, DHCPAudit, NetworkProfileAudit, WinRMAudit, QoSPolicyAudit, NICOffloadAudit, NICErrorAudit, HostsFileAudit, VPNAudit, ProxyAudit." } @{ Title = "Storage & Cluster Audits"; Keywords = @("storage-audit", "cluster-audit", "csv", "s2d", "iscsi-audit", "mpio", "dedup-audit", "shadow", "shadowcopy", "vss", "volumelabel", "disklatency", "storagetimeout", "storage-health"); Description = "Storage/cluster audits: DiskAudit, DiskLatencyAudit, iSCSIAudit, MPIOPathAudit, CSVSpaceAudit, S2DAudit, DedupAudit, StorageAudit, StorageHealthScore, StorageTimeoutAudit, ShadowCopyAudit, VolumeLabelAudit, ClusterAudit, ClusterQuorumAudit, ClusterNetworkAudit, ClusterHealthScore, NTFSAudit." } @@ -485,8 +485,8 @@ function Show-SettingsMenu { Write-OutputColor " ┌────────────────────────────────────────────────────────────────────────┐" -color "Info" Write-OutputColor " │$(" CONFIGURATION".PadRight(72))│" -color "Info" Write-OutputColor " ├────────────────────────────────────────────────────────────────────────┤" -color "Info" - Write-MenuItem "[11] Edit Environment Defaults" -Status "File: defaults.json" -StatusColor "Info" - Write-MenuItem "[12] Edit Custom Licenses" -Status "In: defaults.json" -StatusColor "Info" + Write-MenuItem "[11] Edit Environment Defaults" -Status "File: $(Split-Path -Leaf $script:DefaultsPath)" -StatusColor "Info" + Write-MenuItem "[12] Edit Custom Licenses" -Status "In: $(Split-Path -Leaf $script:DefaultsPath)" -StatusColor "Info" Write-MenuItem "[13] View Audit Log" Write-OutputColor " └────────────────────────────────────────────────────────────────────────┘" -color "Info" Write-OutputColor "" -color "Info" diff --git a/Modules/36-BatchConfig.ps1 b/Modules/36-BatchConfig.ps1 index 6466ed3..d26343c 100644 --- a/Modules/36-BatchConfig.ps1 +++ b/Modules/36-BatchConfig.ps1 @@ -141,7 +141,7 @@ function New-BatchConfigTemplate { "_iSCSIHostNumber_Help": "Host number for iSCSI IP calculation (1-24). null = auto-detect from hostname.", "SANTargetPairings": null, - "_SANTargetPairings_Help": "Custom SAN target pair definitions. Overrides default A0/B1 cycling pattern. Set to null for defaults. See defaults.example.json for full example with Pairs, HostAssignments, and CycleSize.", + "_SANTargetPairings_Help": "Custom SAN target pair definitions. Overrides default A0/B1 cycling pattern. Set to null for defaults. See rackstack.config.example.json for full example with Pairs, HostAssignments, and CycleSize.", "SMB3SharePath": null, "_SMB3SharePath_Help": "UNC path to SMB3 share (e.g., '\\\\\\\\server\\\\share'). Only used when StorageBackendType=SMB3.", diff --git a/Modules/39-FileServer.ps1 b/Modules/39-FileServer.ps1 index 9e5b3cc..f8e50aa 100644 --- a/Modules/39-FileServer.ps1 +++ b/Modules/39-FileServer.ps1 @@ -87,7 +87,7 @@ function Get-FileServerFiles { # Validate FileServer is configured if (-not (Test-FileServerConfigured)) { - Write-OutputColor " FileServer not configured. Update settings in defaults.json." -color "Error" + Write-OutputColor " FileServer not configured. Update settings in rackstack.config.json." -color "Error" return @() } @@ -281,7 +281,7 @@ function Get-FileServerFile { if (-not (Test-FileServerConfigured)) { return @{ Success = $false - Error = "FileServer not configured. Update the settings in defaults.json." + Error = "FileServer not configured. Update the settings in rackstack.config.json." FilePath = $null } } @@ -640,7 +640,7 @@ function Get-FileServerFile { # show the error and delete so a tampered/unverifiable file never lingers on disk. if ($integrity.Reason -eq 'NoSidecar') { # No server-published .sha256 sidecar. But if the CALLER supplied a locally-trusted - # reference hash (e.g. the AgentInstaller HashManifest configured in defaults.json), + # reference hash (e.g. the AgentInstaller HashManifest configured in rackstack.config.json), # that hash is authoritative and independent of the server — enforce it HERE rather # than deferring to a weak size-only trust-on-first-use prompt. This is the documented # RMM primary path (unique per-site installers with no pre-published sidecar). A match diff --git a/Modules/41-VHDManagement.ps1 b/Modules/41-VHDManagement.ps1 index 5e1e69a..afe4e31 100644 --- a/Modules/41-VHDManagement.ps1 +++ b/Modules/41-VHDManagement.ps1 @@ -721,7 +721,7 @@ function Show-SysprepGuide { Write-OutputColor " │$(' '.PadRight(72))│" -color "Info" Write-OutputColor " │$(" 3. Upload to the FileServer VHDs folder:".PadRight(72))│" -color "Info" Write-OutputColor " │$(" - Include the OS version year in the filename (e.g. 2025)".PadRight(72))│" -color "Info" - Write-OutputColor " │$(" - Ensure FileServer is configured in defaults.json".PadRight(72))│" -color "Info" + Write-OutputColor " │$(" - Ensure FileServer is configured in rackstack.config.json".PadRight(72))│" -color "Info" Write-OutputColor " │$(" - The script discovers files automatically from the server".PadRight(72))│" -color "Info" Write-OutputColor " │$(' '.PadRight(72))│" -color "Info" Write-OutputColor " └────────────────────────────────────────────────────────────────────────┘" -color "Info" diff --git a/Modules/44-VMDeployment.ps1 b/Modules/44-VMDeployment.ps1 index 0f99793..7c758f3 100644 --- a/Modules/44-VMDeployment.ps1 +++ b/Modules/44-VMDeployment.ps1 @@ -7,7 +7,7 @@ $script:VMCredentialTimestamp = $null # When credentials were stored $script:VMDeploymentSiteNumber = $null # Site number (e.g., "123456") $script:VMDeploymentConnected = $false # Connection status -# Standard VM templates (generic built-ins; override or add more via CustomVMTemplates in defaults.json) +# Standard VM templates (generic built-ins; override or add more via CustomVMTemplates in rackstack.config.json) $script:StandardVMTemplates = @{ "DC" = @{ FullName = "Domain Controller" @@ -1051,7 +1051,7 @@ function New-VMConfiguration { } } else { - # Custom VM defaults (overridable via CustomVMDefaults in defaults.json) + # Custom VM defaults (overridable via CustomVMDefaults in rackstack.config.json) $cvd = $script:CustomVMDefaults $config = @{ VMName = "" diff --git a/Modules/47-ExitCleanup.ps1 b/Modules/47-ExitCleanup.ps1 index bd9bfee..2faf63b 100644 --- a/Modules/47-ExitCleanup.ps1 +++ b/Modules/47-ExitCleanup.ps1 @@ -136,7 +136,7 @@ function Exit-Script { ($_.Name -like "$toolName*.exe") -or # Only treat the listed config files as ours when they sit inside a directory # whose path contains the tool name — protects unrelated dev work in other repos. - ((($_.Name -eq "RackStack.ps1") -or ($_.Name -eq "defaults.json") -or ($_.Name -eq "defaults.example.json") -or ($_.Name -eq "PSScriptAnalyzerSettings.psd1") -or ($_.Name -eq "Header.ps1")) -and ($_.DirectoryName -like "*$toolName*")) + ((($_.Name -eq "RackStack.ps1") -or ($_.Name -eq "rackstack.config.json") -or ($_.Name -eq "rackstack.config.example.json") -or ($_.Name -eq "defaults.json") -or ($_.Name -eq "defaults.example.json") -or ($_.Name -like "*.rackstack.config.json") -or ($_.Name -like "*.defaults.json") -or ($_.Name -eq "rackstack.config.json.tmp") -or ($_.Name -eq "defaults.json.tmp") -or ($_.Name -like "*.rackstack.config.json.tmp") -or ($_.Name -like "*.defaults.json.tmp") -or ($_.Name -eq "PSScriptAnalyzerSettings.psd1") -or ($_.Name -eq "Header.ps1")) -and ($_.DirectoryName -like "*$toolName*")) } foreach ($f in $monoFiles) { $pathsToDelete.Add($f.FullName) } @@ -161,12 +161,32 @@ function Exit-Script { if (Test-Path -LiteralPath $modulesDir) { $pathsToDelete.Add($modulesDir) } $loaderPath = Join-Path $script:ModuleRoot "RackStack.ps1" if (Test-Path -LiteralPath $loaderPath) { $pathsToDelete.Add($loaderPath) } - $defaultsPath = Join-Path $script:ModuleRoot "defaults.json" - if (Test-Path -LiteralPath $defaultsPath) { $pathsToDelete.Add($defaultsPath) } + # Config files under the module root: base + example under both naming + # schemes, plus crash-leftover .tmp siblings from the atomic write path + # (Out-File to .tmp, then Move-Item) - these can hold credentials. + foreach ($cfgName in @("rackstack.config.json", "rackstack.config.example.json", "defaults.json", "defaults.example.json", "rackstack.config.json.tmp", "defaults.json.tmp")) { + $cfgPath = Join-Path $script:ModuleRoot $cfgName + if (Test-Path -LiteralPath $cfgPath) { $pathsToDelete.Add($cfgPath) } + } + # Company-file wildcard sweep only when the directory is identifiably the + # tool's own home: named after the runtime brand OR the literal product + # (package managers pin the install path to "rackstack" regardless of a + # ToolName rebrand), OR this tool's own modular layout is present (checked + # via Modules\00-Initialization.ps1 so a foreign project's Modules folder + # in a shared directory does not count). The monolithic .ps1 or EXE can + # run from a shared folder (e.g. Downloads), and an ungated + # *.defaults.json glob there would delete another application's config - + # the same hazard the profile scan's DirectoryName gate avoids. + if (($script:ModuleRoot -like "*$toolName*") -or ($script:ModuleRoot -like "*RackStack*") -or (Test-Path -LiteralPath (Join-Path $script:ModuleRoot "Modules\00-Initialization.ps1"))) { + foreach ($cfgFilter in @("*.rackstack.config.json", "*.defaults.json", "*.rackstack.config.json.tmp", "*.defaults.json.tmp")) { + $companyCfgs = Get-ChildItem -LiteralPath $script:ModuleRoot -Filter $cfgFilter -File -ErrorAction SilentlyContinue + foreach ($cc in $companyCfgs) { $pathsToDelete.Add($cc.FullName) } + } + } } # AppConfigDir cleanup — only if the directory's path matches the tool name (defensive - # check; AppConfigDir is configured at init and shouldn't drift, but a wrong defaults.json + # check; AppConfigDir is configured at init and shouldn't drift, but a wrong rackstack.config.json # value pointing at C:\ would otherwise wipe everything). if ($script:AppConfigDir -and (Test-Path -LiteralPath $script:AppConfigDir) -and $script:AppConfigDir -like "*$($script:ConfigDirName)*") { $pathsToDelete.Add($script:AppConfigDir) @@ -174,10 +194,20 @@ function Exit-Script { if ($currentScriptPath -and $currentScriptPath -match '\.exe$') { $exeDir = Split-Path $currentScriptPath -Parent - $adjacentDefaults = Join-Path $exeDir "defaults.json" - if (Test-Path -LiteralPath $adjacentDefaults) { $pathsToDelete.Add($adjacentDefaults) } - $adjacentExample = Join-Path $exeDir "defaults.example.json" - if (Test-Path -LiteralPath $adjacentExample) { $pathsToDelete.Add($adjacentExample) } + foreach ($cfgName in @("rackstack.config.json", "rackstack.config.example.json", "defaults.json", "defaults.example.json", "rackstack.config.json.tmp", "defaults.json.tmp")) { + $adjacentCfg = Join-Path $exeDir $cfgName + if (Test-Path -LiteralPath $adjacentCfg) { $pathsToDelete.Add($adjacentCfg) } + } + # Same tool-named-directory gate as the module-root sweep above (runtime + # brand OR literal product name, since package-manager install paths keep + # the product name even when ToolName is rebranded): never glob-delete + # company-style config files from a shared folder. + if (($exeDir -like "*$toolName*") -or ($exeDir -like "*RackStack*")) { + foreach ($cfgFilter in @("*.rackstack.config.json", "*.defaults.json", "*.rackstack.config.json.tmp", "*.defaults.json.tmp")) { + $adjacentCompanyCfgs = Get-ChildItem -LiteralPath $exeDir -Filter $cfgFilter -File -ErrorAction SilentlyContinue + foreach ($cc in $adjacentCompanyCfgs) { $pathsToDelete.Add($cc.FullName) } + } + } } $uniquePaths = @($pathsToDelete | Sort-Object -Unique) diff --git a/Modules/50-EntryPoint.ps1 b/Modules/50-EntryPoint.ps1 index 2b8fc2b..eb6c317 100644 --- a/Modules/50-EntryPoint.ps1 +++ b/Modules/50-EntryPoint.ps1 @@ -195,7 +195,7 @@ function Assert-Elevation { # Check for session to restore (v2.8.0) $null = Restore-SessionState - # Load environment defaults and custom licenses from defaults.json + # Load environment defaults and custom licenses from rackstack.config.json Import-Defaults # Silent update check (non-blocking, 5s timeout) @@ -394,9 +394,9 @@ function Assert-Elevation { @{ Action = 'Dashboard'; Description = 'Serve HealthDashboard as HTML + JSON via HttpListener (default 127.0.0.1:8080)' } @{ Action = 'History'; Description = 'Show the last N CLI invocations (rolling log of 50 entries)' } @{ Action = 'Replay'; Description = 'Re-run a prior invocation by index (1 = most recent; see History)' } - @{ Action = 'AzureArcEnroll'; Description = 'Onboard this server to Azure Arc (service-principal auth; config from defaults.json AzureArc)' } - @{ Action = 'DefenderEndpointOnboard'; Description = 'Onboard this server to Microsoft Defender for Endpoint (runs the onboarding .cmd from defaults.json DefenderEndpoint)' } - @{ Action = 'WSUSSetup'; Description = 'Install + post-install + default-configure the WSUS update server (config from defaults.json WSUS)' } + @{ Action = 'AzureArcEnroll'; Description = 'Onboard this server to Azure Arc (service-principal auth; config from rackstack.config.json AzureArc)' } + @{ Action = 'DefenderEndpointOnboard'; Description = 'Onboard this server to Microsoft Defender for Endpoint (runs the onboarding .cmd from rackstack.config.json DefenderEndpoint)' } + @{ Action = 'WSUSSetup'; Description = 'Install + post-install + default-configure the WSUS update server (config from rackstack.config.json WSUS)' } @{ Action = 'ADCSSetup'; Description = 'Install the AD CS role (CA configuration is interactive-only — it needs a typed confirmation)' } @{ Action = 'StorageMigrationSetup'; Description = 'Install the Storage Migration Service orchestrator role (+proxy if StorageMigration.InstallProxy is set)' } @{ Action = 'GPOBackup'; Description = 'Back up all GPOs (+reports +manifest) to -Config (default: \GPOBackups)' } @@ -1195,8 +1195,8 @@ function Invoke-CLIAction { $taskName = "$($script:ToolName)_UpdateCheck" $outputJsonPath = Join-Path $script:TempPath "$($script:ToolName)_UpdateCheck.json" # Also validate $outputJsonPath — it's built from $script:TempPath which can be - # influenced via defaults.json (post-v1.98.25 we strict-validate ToolName but not - # TempPath; an attacker who's already an admin and can edit defaults.json could + # influenced via rackstack.config.json (post-v1.98.25 we strict-validate ToolName but not + # TempPath; an attacker who's already an admin and can edit rackstack.config.json could # poison this path with quote characters to break out of the cmd arg). if ($outputJsonPath -match '["`&|<>^%()]' -or $outputJsonPath -match '[\x00-\x1F]') { Write-OutputColor " ScheduleUpdateCheck: output path '$outputJsonPath' contains unsafe characters." -color "Error" @@ -1780,7 +1780,7 @@ footer{text-align:center;color:#999;font-size:12px;padding:16px} $checks += @{ Name = 'ModuleCount'; Passed = $true; Detail = 'Skipped (monolithic/EXE mode)' } } - # Check 5: defaults.json validity — parses if present, missing is acceptable + # Check 5: base config (rackstack.config.json, legacy defaults.json) validity - parses if present, missing is acceptable $defOk = $true $defDetail = 'Not present (optional)' if ($script:DefaultsPath -and (Test-Path -LiteralPath $script:DefaultsPath)) { @@ -1864,9 +1864,9 @@ footer{text-align:center;color:#999;font-size:12px;padding:16px} } 'AzureArcEnroll' { # Onboard this server to Azure Arc. Config (tenant, subscription, - # resource group, service principal) comes from defaults.json + # resource group, service principal) comes from rackstack.config.json # "AzureArc"; the SP secret comes from $env:RACKSTACK_ARC_SECRET - # or defaults.json. Exit 0 on a confirmed Connected state, 1 otherwise. + # or rackstack.config.json. Exit 0 on a confirmed Connected state, 1 otherwise. $arcOk = Invoke-AzureArcOnboard if ($script:CLIOutputFormat -eq 'JSON') { $arcStatus = Get-AzureArcStatus @@ -1888,7 +1888,7 @@ footer{text-align:center;color:#999;font-size:12px;padding:16px} } 'DefenderEndpointOnboard' { # Onboard this server to Microsoft Defender for Endpoint by running - # the operator-supplied onboarding .cmd (path from defaults.json + # the operator-supplied onboarding .cmd (path from rackstack.config.json # "DefenderEndpoint"). Exit 0 on a confirmed onboarded state, 1 otherwise. $mdeOk = Invoke-DefenderEndpointOnboard if ($script:CLIOutputFormat -eq 'JSON') { @@ -1911,7 +1911,7 @@ footer{text-align:center;color:#999;font-size:12px;padding:16px} 'WSUSSetup' { # Stand up a WSUS update server end-to-end: install the role, run # post-install, and apply the default configuration. Config from - # defaults.json "WSUS". Exit 0 only if the server is post-installed. + # rackstack.config.json "WSUS". Exit 0 only if the server is post-installed. Write-OutputColor " Setting up WSUS update server..." -color "Info" $wsusRole = Install-WSUSRole $wsusPost = $false @@ -1971,7 +1971,7 @@ footer{text-align:center;color:#999;font-size:12px;padding:16px} } 'StorageMigrationSetup' { # Install the Storage Migration Service orchestrator role, and the - # proxy too if defaults.json StorageMigration.InstallProxy is set. + # proxy too if rackstack.config.json StorageMigration.InstallProxy is set. # Migration jobs themselves run interactively via Windows Admin # Center. Exit 0 if the orchestrator role is installed. Write-OutputColor " Installing Storage Migration Service..." -color "Info" @@ -3302,7 +3302,7 @@ footer{text-align:center;color:#999;font-size:12px;padding:16px} Write-OutputColor " Auditing key service status..." -color "Info" Write-OutputColor "" -color "Info" - # Configurable service list (from defaults.json MonitoredServices, unpacked + # Configurable service list (from rackstack.config.json MonitoredServices, unpacked # by Import-Defaults into $script:MonitoredServices) or built-in fallback. $auditServices = if ($script:MonitoredServices) { $script:MonitoredServices @@ -13123,7 +13123,7 @@ function Start-BatchMode { # Start transcript for batch mode too $null = Start-ScriptTranscript - # Load environment defaults and custom licenses from defaults.json (no wizard in batch mode) + # Load environment defaults and custom licenses from rackstack.config.json (no wizard in batch mode) Import-Defaults Write-OutputColor "" -color "Info" @@ -13955,7 +13955,7 @@ function Start-BatchMode { Write-OutputColor " [$stepNum/$totalSteps] Joining domain '$($Config.DomainName)'..." -color "Info" # Validate the supplied DomainName before invoking Add-Computer. The DC-promote # path already calls Test-ValidDomainName; this batch step was missing the same - # gate, so a malformed value in defaults.json reached Add-Computer with a + # gate, so a malformed value in rackstack.config.json reached Add-Computer with a # generic failure instead of a clear "bad domain name" error. if (-not (Test-ValidDomainName -DomainName $Config.DomainName)) { Write-OutputColor " ERROR: '$($Config.DomainName)' is not a valid domain name. Skipping join." -color "Error" diff --git a/Modules/56-OperationsMenu.ps1 b/Modules/56-OperationsMenu.ps1 index b73f672..3a2bc5d 100644 --- a/Modules/56-OperationsMenu.ps1 +++ b/Modules/56-OperationsMenu.ps1 @@ -591,20 +591,44 @@ function Initialize-SANTargetPairs { } } -# Get available company defaults files (*.defaults.json excluding defaults.json and defaults.example.json) +# Get available company config files. The namespaced .rackstack.config.json +# is preferred; the legacy .defaults.json is still recognized. When both +# names exist for the same company key, the new name wins (one entry per company). function Get-CompanyDefaultsFiles { $results = @() if (-not $script:ModuleRoot) { return $results } + # Plain hashtable (case-insensitive) + a List for names: an [ordered] + # dictionary's Keys collection does not enumerate through the pipeline + # under PowerShell 5.1, so Sort-Object would see it as a single object. + $companyPaths = @{} + $companyNames = New-Object System.Collections.Generic.List[string] + + $files = Get-ChildItem -LiteralPath $script:ModuleRoot -Filter "*.rackstack.config.json" -File -ErrorAction SilentlyContinue + foreach ($f in $files) { + # Exclude rackstack.config.json itself and rackstack.config.example.json + if ($f.Name -eq "rackstack.config.json" -or $f.Name -eq "rackstack.config.example.json") { continue } + $name = $f.Name -replace '\.rackstack\.config\.json$', '' + if (-not $name) { continue } # Skip files with empty extracted name + if (-not $companyPaths.ContainsKey($name)) { $companyPaths[$name] = $f.FullName; $companyNames.Add($name) } + } + $files = Get-ChildItem -LiteralPath $script:ModuleRoot -Filter "*.defaults.json" -File -ErrorAction SilentlyContinue foreach ($f in $files) { # Exclude defaults.json itself and defaults.example.json if ($f.Name -eq "defaults.json" -or $f.Name -eq "defaults.example.json") { continue } $name = $f.BaseName -replace '\.defaults$', '' if (-not $name) { continue } # Skip files with empty extracted name - $results += @{ Name = $name; Path = $f.FullName } + if (-not $companyPaths.ContainsKey($name)) { $companyPaths[$name] = $f.FullName; $companyNames.Add($name) } + } + + foreach ($name in ($companyNames | Sort-Object)) { + $results += @{ Name = $name; Path = $companyPaths[$name] } } - return ,$results + # Plain return (no comma-wrap): every caller wraps in @(), and a + # comma-wrapped return nests the array there, collapsing N companies + # into one Object[] entry. Masked historically by single-company use. + return $results } # Show interactive picker for company defaults files @@ -612,7 +636,7 @@ function Show-CompanyDefaultsPicker { $files = @(Get-CompanyDefaultsFiles) if ($files.Count -eq 0) { Write-OutputColor " No company defaults files found." -color "Warning" - Write-OutputColor " Place .defaults.json files in the script directory." -color "Debug" + Write-OutputColor " Place .rackstack.config.json files in the script directory (legacy .defaults.json also works)." -color "Debug" return $null } @@ -681,7 +705,7 @@ function Import-CompanyDefaults { } } -# Import environment defaults from defaults.json (merges with built-in generics) +# Import environment defaults from the base config file (merges with built-in generics) function Import-Defaults { # Reset agent installer to factory defaults before re-merge (prevents stale config from previous company) @@ -703,7 +727,7 @@ function Import-Defaults { # Check for company defaults files $companyFiles = @(Get-CompanyDefaultsFiles) - # Run first-run wizard if no defaults.json exists. Skip in batch, CLI-silent, + # Run first-run wizard if no base config file exists. Skip in batch, CLI-silent, # and test-runner contexts so non-interactive callers don't block on Read-Host. if (-not (Test-Path -LiteralPath $script:DefaultsPath) -and -not $script:CLISilent -and @@ -764,7 +788,7 @@ function Import-Defaults { } } } catch { - Write-OutputColor " Warning: Could not read defaults.json for company defaults: $_" -color "Warning" + Write-OutputColor " Warning: Could not read $(Split-Path -Leaf $script:DefaultsPath) for company defaults: $_" -color "Warning" } } # If not auto-resolved, show picker @@ -790,7 +814,7 @@ function Import-Defaults { foreach ($prop in $fileDefaults.PSObject.Properties) { if ($prop.Name -like "_*") { continue } # Skip metadata fields # When company defaults are active, don't let personal defaults override agent config - # (old defaults.json files may have stale AgentInstaller with ToolName="MSP") + # (old config files may have stale AgentInstaller with ToolName="MSP") if ($script:CompanyDefaultsPath -and $prop.Name -in @('AgentInstaller', 'AdditionalAgents')) { continue } # Skip ONLY null and empty STRINGS — not falsy values. `$prop.Value -ne ""` # silently dropped a boolean $false (PowerShell coerces "" to [bool]$false, so @@ -825,17 +849,17 @@ function Import-Defaults { # Apply tool identity from defaults. ToolName is the seed for ConfigDirName and AppConfigDir # below, AND is used as the safety check in 47-ExitCleanup's self-destruct path filter. # Whoever controls ToolName controls both sides of that guard. Strict-validate it here so - # a hostile defaults.json can't set ToolName='Documents' and have the self-destruct walker + # a hostile config file can't set ToolName='Documents' and have the self-destruct walker # match every folder under C:\Users\Administrator\Documents. Reject names that match # Windows-reserved tokens or are too short/long. $reservedToolNames = @('windows', 'system', 'system32', 'users', 'documents', 'desktop', 'temp', 'admin', 'administrator', 'config', 'program', 'programs', 'programfiles', 'programdata', 'appdata', 'roaming', 'local', 'public') if ($merged.ToolName) { $proposedName = "$($merged.ToolName)" if ($proposedName -notmatch '^[A-Za-z][A-Za-z0-9_-]{2,32}$') { - Write-OutputColor " defaults.json: rejecting ToolName '$proposedName' (must be 3-32 chars, letters/digits/underscore/hyphen, must start with a letter)." -color "Error" + Write-OutputColor " Config: rejecting ToolName '$proposedName' (must be 3-32 chars, letters/digits/underscore/hyphen, must start with a letter)." -color "Error" } elseif ($proposedName.ToLowerInvariant() -in $reservedToolNames) { - Write-OutputColor " defaults.json: rejecting ToolName '$proposedName' (collides with a Windows-reserved path token)." -color "Error" + Write-OutputColor " Config: rejecting ToolName '$proposedName' (collides with a Windows-reserved path token)." -color "Error" } else { $script:ToolName = $proposedName @@ -1017,7 +1041,7 @@ function Import-Defaults { # Unpack MonitoredServices into $script:MonitoredServices for the service-audit # consumers (30-ServiceManager, 50-EntryPoint health/export). Previously consumers # read `$script:Defaults.MonitoredServices` but $script:Defaults was never set, so - # operator customization in defaults.json silently did nothing. + # operator customization in the config file silently did nothing. if ($merged.MonitoredServices) { $msList = @() foreach ($svc in $merged.MonitoredServices) { @@ -1067,7 +1091,7 @@ function Import-Defaults { } } - # Update Azure Arc onboarding settings (deep-merge from defaults.json "AzureArc") + # Update Azure Arc onboarding settings (deep-merge from rackstack.config.json "AzureArc") $acArc = $merged.AzureArc if ($acArc) { if ($acArc -is [PSCustomObject]) { @@ -1082,7 +1106,7 @@ function Import-Defaults { } } - # Update Defender for Endpoint onboarding settings (deep-merge from defaults.json "DefenderEndpoint") + # Update Defender for Endpoint onboarding settings (deep-merge from rackstack.config.json "DefenderEndpoint") $acMde = $merged.DefenderEndpoint if ($acMde) { if ($acMde -is [PSCustomObject]) { @@ -1097,7 +1121,7 @@ function Import-Defaults { } } - # Update WSUS server setup settings (deep-merge from defaults.json "WSUS") + # Update WSUS server setup settings (deep-merge from rackstack.config.json "WSUS") $acWsus = $merged.WSUS if ($acWsus) { if ($acWsus -is [PSCustomObject]) { @@ -1112,7 +1136,7 @@ function Import-Defaults { } } - # Update AD CS Certification Authority settings (deep-merge from defaults.json "ADCS") + # Update AD CS Certification Authority settings (deep-merge from rackstack.config.json "ADCS") $acAdcs = $merged.ADCS if ($acAdcs) { if ($acAdcs -is [PSCustomObject]) { @@ -1127,7 +1151,7 @@ function Import-Defaults { } } - # Update Storage Migration Service settings (deep-merge from defaults.json "StorageMigration") + # Update Storage Migration Service settings (deep-merge from rackstack.config.json "StorageMigration") $acSms = $merged.StorageMigration if ($acSms) { if ($acSms -is [PSCustomObject]) { @@ -1299,7 +1323,7 @@ function Import-Defaults { } } catch { - Write-OutputColor " Warning: Could not load VM defaults from defaults.json: $($_.Exception.Message)" -color "Warning" + Write-OutputColor " Warning: Could not load VM defaults from $(Split-Path -Leaf $script:DefaultsPath): $($_.Exception.Message)" -color "Warning" } } @@ -1341,7 +1365,7 @@ function Import-Defaults { } # Export current defaults to a defaults file (includes custom license keys). -# TargetPath defaults to the personal defaults.json; pass $script:CompanyDefaultsPath +# TargetPath defaults to the personal base config file; pass $script:CompanyDefaultsPath # to update the shared company baseline instead. function Export-Defaults { param( @@ -1413,23 +1437,23 @@ function Export-Defaults { } } -# Save the current defaults, letting the operator pick the personal defaults.json +# Save the current defaults, letting the operator pick the personal base config # or (when a company defaults file is active) the shared company baseline. The # editor already updates the in-memory $script:* values as fields are changed, so # the running session uses the new values immediately — this just persists them. function Save-DefaultsInteractive { $target = $script:DefaultsPath - $targetLabel = "defaults.json" + $targetLabel = Split-Path -Leaf $script:DefaultsPath if ($script:CompanyDefaultsPath) { Write-OutputColor " Save to which file?" -color "Info" - Write-OutputColor " [1] Personal — defaults.json (this machine only)" -color "Info" - Write-OutputColor " [2] Company — $($script:CompanyDefaultsName).defaults.json (shared fleet baseline)" -color "Info" + Write-OutputColor " [1] Personal - $(Split-Path -Leaf $script:DefaultsPath) (this machine only)" -color "Info" + Write-OutputColor " [2] Company - $(Split-Path -Leaf $script:CompanyDefaultsPath) (shared fleet baseline)" -color "Info" $sel = Read-Host " Select [1/2] (default 1)" $navResult = Test-NavigationCommand -UserInput $sel if ($navResult.ShouldReturn) { return } if ("$sel".Trim() -eq "2") { $target = $script:CompanyDefaultsPath - $targetLabel = "$($script:CompanyDefaultsName).defaults.json" + $targetLabel = Split-Path -Leaf $script:CompanyDefaultsPath } } if (Export-Defaults -TargetPath $target) { @@ -1438,14 +1462,14 @@ function Save-DefaultsInteractive { Start-Sleep -Seconds 1 } -# Import-CustomLicenses is handled by Import-Defaults (licenses stored in defaults.json) +# Import-CustomLicenses is handled by Import-Defaults (licenses stored in the base config) # This wrapper exists for backward compatibility function Import-CustomLicenses { - # License keys are loaded as part of Import-Defaults from defaults.json + # License keys are loaded as part of Import-Defaults from the base config # No separate file needed } -# Export-CustomLicenses saves via Export-Defaults (licenses stored in defaults.json) +# Export-CustomLicenses saves via Export-Defaults (licenses stored in the base config) function Export-CustomLicenses { Export-Defaults } @@ -1686,7 +1710,7 @@ function Show-EditDefaults { $companyFiles = @(Get-CompanyDefaultsFiles) if ($companyFiles.Count -eq 0) { Write-OutputColor " No company defaults files found." -color "Warning" - Write-OutputColor " Place .defaults.json files in the script directory." -color "Debug" + Write-OutputColor " Place .rackstack.config.json files in the script directory (legacy .defaults.json also works)." -color "Debug" Start-Sleep -Seconds 1 } else { @@ -2016,7 +2040,7 @@ function Show-FirstRunWizard { # Multiple company files: show picker Write-OutputColor " Multiple company defaults files detected:" -color "Info" foreach ($cf in $companyFiles) { - Write-OutputColor " - $($cf.Name).defaults.json" -color "Info" + Write-OutputColor " - $(Split-Path -Leaf $cf.Path)" -color "Info" } Write-OutputColor "" -color "Info" $picked = Show-CompanyDefaultsPicker @@ -2028,7 +2052,7 @@ function Show-FirstRunWizard { } if ($script:CompanyDefaultsPath) { - # Save minimal defaults.json that just references the company defaults file + # Save a minimal base config that just references the company config file # Do NOT use Export-Defaults here — script variables haven't been merged yet, # so it would save built-in defaults (empty FileServer/AgentInstaller) that # would overwrite company values on subsequent loads. diff --git a/Modules/57-AgentInstaller.ps1 b/Modules/57-AgentInstaller.ps1 index de3c705..f588490 100644 --- a/Modules/57-AgentInstaller.ps1 +++ b/Modules/57-AgentInstaller.ps1 @@ -199,7 +199,7 @@ function ConvertFrom-AgentFilename { # Remove any environment / tier suffix (e.g. `name.staging.exe`, `name.workstations.exe`) # before the .exe extension so the site-number regex below sees the canonical filename. - # The accepted suffixes can be overridden via defaults.json AgentInstaller.SuffixesToStrip; + # The accepted suffixes can be overridden via rackstack.config.json AgentInstaller.SuffixesToStrip; # if unset, fall back to the two generic tokens. Site-specific suffixes do not belong here. $stripSuffixes = if ($script:AgentInstallerSuffixesToStrip) { $script:AgentInstallerSuffixesToStrip } else { @('staging', 'workstations') } $suffixPattern = '\.(' + (($stripSuffixes | ForEach-Object { [regex]::Escape($_) }) -join '|') + ')\.exe$' @@ -530,7 +530,7 @@ function Install-SelectedAgent { # must never reach Start-Process as SYSTEM. Operators whose FileServer doesn't publish # per-agent .sha256 sidecars — common for RMM consoles (Kaseya, ConnectWise, Datto…) # that mint a unique installer per site — can set AgentInstaller.RequireHash = false in - # defaults.json to fall back to size-only verification, or publish a sidecar / supply an + # rackstack.config.json to fall back to size-only verification, or publish a sidecar / supply an # ExpectedHash per agent entry for full cryptographic verification. $requireHash = if ($script:AgentInstaller.ContainsKey('RequireHash')) { [bool]$script:AgentInstaller.RequireHash } else { $true } @@ -951,11 +951,11 @@ function Install-Agent { } elseif (-not (Test-FileServerConfigured)) { Write-OutputColor " FileServer is not configured. Set FileServer.BaseURL in" -color "Warning" - Write-OutputColor " defaults.json or company defaults to enable agent downloads." -color "Warning" + Write-OutputColor " rackstack.config.json or company defaults to enable agent downloads." -color "Warning" } elseif ($script:AgentInstaller.ToolName -eq "MSP") { Write-OutputColor " Agent installer has not been customized. Set AgentInstaller.ToolName" -color "Warning" - Write-OutputColor " in defaults.json or company defaults to enable this feature." -color "Warning" + Write-OutputColor " in rackstack.config.json or company defaults to enable this feature." -color "Warning" } Write-OutputColor "" -color "Info" Write-PressEnter @@ -1324,7 +1324,7 @@ function Install-Agent { # MULTI-AGENT SUPPORT (v1.8.0) # ============================================================================ -# Get all agent configs (primary + additional agents from defaults.json) +# Get all agent configs (primary + additional agents from rackstack.config.json) function Get-AllAgentConfigs { $configs = @() @@ -1341,7 +1341,7 @@ function Get-AllAgentConfigs { IsPrimary = $true } - # Additional agents from defaults.json + # Additional agents from rackstack.config.json if ($script:AdditionalAgents -and $script:AdditionalAgents.Count -gt 0) { foreach ($agent in $script:AdditionalAgents) { $configs += @{ diff --git a/Modules/60-ServerRoleTemplates.ps1 b/Modules/60-ServerRoleTemplates.ps1 index 228a640..7c71891 100644 --- a/Modules/60-ServerRoleTemplates.ps1 +++ b/Modules/60-ServerRoleTemplates.ps1 @@ -85,10 +85,10 @@ $script:ServerRoleTemplates = @{ } } -# Custom role templates (loaded from defaults.json or user-defined) +# Custom role templates (loaded from rackstack.config.json or user-defined) $script:CustomRoleTemplates = @{} -# Validate a user-provided custom role template from defaults.json +# Validate a user-provided custom role template from rackstack.config.json function Test-CustomRoleTemplate { param( [Parameter(Mandatory=$true)] @@ -126,7 +126,7 @@ function Test-CustomRoleTemplate { } # Validate PostInstall. Must be a single token (no spaces / no args) that starts - # with an approved setup verb so a malicious defaults.json can't smuggle an + # with an approved setup verb so a malicious rackstack.config.json can't smuggle an # arbitrary cmdlet through `& $template.PostInstall`. if ($null -ne $Template.PostInstall) { if ($Template.PostInstall -isnot [string]) { diff --git a/Modules/65-AzureArc.ps1 b/Modules/65-AzureArc.ps1 index c562746..633c738 100644 --- a/Modules/65-AzureArc.ps1 +++ b/Modules/65-AzureArc.ps1 @@ -6,7 +6,7 @@ # all without the server living in Azure. # # Config comes from $script:AzureArc (see 00-Initialization.ps1, overridable -# via defaults.json "AzureArc"). The service-principal secret is treated as +# via rackstack.config.json "AzureArc"). The service-principal secret is treated as # sensitive throughout: the transcript is paused around the azcmagent call, # azcmagent's output is discarded (not merged into any captured stream), it # is never structured-logged in plaintext, and the plaintext copy is @@ -25,8 +25,8 @@ function Test-AzureArcConfigured { } # Resolve the service-principal secret as a SecureString. Priority order: -# 1. $env:RACKSTACK_ARC_SECRET (preferred — keeps the secret out of defaults.json) -# 2. $script:AzureArc.ServicePrincipalSecret (from defaults.json — discouraged for source-controlled files) +# 1. $env:RACKSTACK_ARC_SECRET (preferred - keeps the secret out of rackstack.config.json) +# 2. $script:AzureArc.ServicePrincipalSecret (from rackstack.config.json - discouraged for source-controlled files) # Returns $null if neither is set. function Resolve-AzureArcSecret { $raw = $null @@ -190,7 +190,7 @@ function Install-AzureArcAgent { function Invoke-AzureArcOnboard { if (-not (Test-AzureArcConfigured)) { Write-OutputColor " Azure Arc is not configured. Set TenantId, SubscriptionId," -color "Error" - Write-OutputColor " ResourceGroup, and ServicePrincipalId in defaults.json (AzureArc)." -color "Warning" + Write-OutputColor " ResourceGroup, and ServicePrincipalId in rackstack.config.json (AzureArc)." -color "Warning" return $false } @@ -235,7 +235,7 @@ function Invoke-AzureArcOnboard { $secureSecret = Resolve-AzureArcSecret if ($null -eq $secureSecret) { Write-OutputColor " No service-principal secret available. Set the RACKSTACK_ARC_SECRET" -color "Error" - Write-OutputColor " environment variable, or AzureArc.ServicePrincipalSecret in defaults.json." -color "Warning" + Write-OutputColor " environment variable, or AzureArc.ServicePrincipalSecret in rackstack.config.json." -color "Warning" return $false } @@ -420,7 +420,7 @@ function Show-AzureArcManagement { Write-OutputColor " │$(" Agent: installed (v$($status.AgentVersion))".PadRight(72))│" -color "Info" Write-OutputColor " │$(" State: not connected".PadRight(72))│" -color "Warning" } - $cfg = if (Test-AzureArcConfigured) { "configured" } else { "NOT configured (see defaults.json)" } + $cfg = if (Test-AzureArcConfigured) { "configured" } else { "NOT configured (see rackstack.config.json)" } Write-OutputColor " │$(" Config: $cfg".PadRight(72))│" -color "Info" Write-OutputColor " └────────────────────────────────────────────────────────────────────────┘" -color "Info" Write-OutputColor "" -color "Info" diff --git a/Modules/66-DefenderEndpoint.ps1 b/Modules/66-DefenderEndpoint.ps1 index e1d47b9..72ac4c5 100644 --- a/Modules/66-DefenderEndpoint.ps1 +++ b/Modules/66-DefenderEndpoint.ps1 @@ -11,7 +11,7 @@ # to handle — the tenant binding is inside the Microsoft-signed package. # # Config: $script:DefenderEndpoint (see 00-Initialization.ps1, overridable -# via defaults.json "DefenderEndpoint"). +# via rackstack.config.json "DefenderEndpoint"). # Registry locations MDE writes its onboarding state to. $script:MDEStatusKey = 'HKLM:\SOFTWARE\Microsoft\Windows Advanced Threat Protection\Status' @@ -107,7 +107,7 @@ function Test-MDEScriptPath { function Invoke-DefenderEndpointOnboard { if (-not (Test-DefenderEndpointConfigured)) { Write-OutputColor " Defender for Endpoint is not configured. Set OnboardingScriptPath" -color "Error" - Write-OutputColor " in defaults.json (DefenderEndpoint) to the .cmd downloaded from" -color "Warning" + Write-OutputColor " in rackstack.config.json (DefenderEndpoint) to the .cmd downloaded from" -color "Warning" Write-OutputColor " security.microsoft.com > Settings > Endpoints > Onboarding." -color "Warning" return $false } @@ -189,7 +189,7 @@ function Invoke-DefenderEndpointOnboard { function Invoke-DefenderEndpointOffboard { if ($null -eq $script:DefenderEndpoint -or [string]::IsNullOrWhiteSpace($script:DefenderEndpoint.OffboardingScriptPath)) { Write-OutputColor " No offboarding script configured. Set OffboardingScriptPath in" -color "Error" - Write-OutputColor " defaults.json (DefenderEndpoint). Offboarding packages expire ~30 days" -color "Warning" + Write-OutputColor " rackstack.config.json (DefenderEndpoint). Offboarding packages expire ~30 days" -color "Warning" Write-OutputColor " after generation — download a current one from the Defender portal." -color "Warning" return $false } @@ -291,7 +291,7 @@ function Show-DefenderEndpointManagement { if ($status.OrgId) { Write-OutputColor " │$(" Org ID: $($status.OrgId)".PadRight(72))│" -color "Info" } - $cfg = if (Test-DefenderEndpointConfigured) { "configured" } else { "NOT configured (see defaults.json)" } + $cfg = if (Test-DefenderEndpointConfigured) { "configured" } else { "NOT configured (see rackstack.config.json)" } Write-OutputColor " │$(" Config: $cfg".PadRight(72))│" -color "Info" Write-OutputColor " └────────────────────────────────────────────────────────────────────────┘" -color "Info" Write-OutputColor "" -color "Info" diff --git a/Modules/67-WSUS.ps1 b/Modules/67-WSUS.ps1 index 69a6673..9893de9 100644 --- a/Modules/67-WSUS.ps1 +++ b/Modules/67-WSUS.ps1 @@ -9,7 +9,7 @@ # (classifications, languages, sync schedule). # # Config: $script:WSUS (see 00-Initialization.ps1, overridable via -# defaults.json "WSUS"). +# rackstack.config.json "WSUS"). # Returns $true if a WSUS content path is configured. function Test-WSUSConfigured { @@ -155,7 +155,7 @@ function Invoke-WSUSPostInstall { return $true } if (-not (Test-WSUSConfigured)) { - Write-OutputColor " Set WSUS.ContentPath in defaults.json before post-install." -color "Error" + Write-OutputColor " Set WSUS.ContentPath in rackstack.config.json before post-install." -color "Error" return $false } @@ -275,7 +275,7 @@ function Set-WSUSDefaultConfiguration { $allClass = $wsus.GetUpdateClassifications() $selected = @($allClass | Where-Object { $_.Title -in $wantClassifications }) # Surface any requested classification that WSUS doesn't recognize — - # a typo in defaults.json must NOT be swallowed (a misconfigured WSUS + # a typo in rackstack.config.json must NOT be swallowed (a misconfigured WSUS # that syncs nothing while reporting success is the worst outcome). $matchedTitles = @($selected | ForEach-Object { $_.Title }) $missing = @($wantClassifications | Where-Object { $_ -notin $matchedTitles }) @@ -284,7 +284,7 @@ function Set-WSUSDefaultConfiguration { } if ($selected.Count -eq 0) { Write-OutputColor " No requested classification matched a WSUS classification — none enabled." -color "Error" - Write-OutputColor " Check the WSUS.Classifications values in defaults.json." -color "Warning" + Write-OutputColor " Check the WSUS.Classifications values in rackstack.config.json." -color "Warning" } else { $sub = $wsus.GetSubscription() @@ -323,7 +323,7 @@ function Set-WSUSDefaultConfiguration { if (-not $classificationsEnabled) { Add-SessionChange -Category "Roles" -Description "WSUS configuration incomplete — no update classifications enabled" Write-OutputColor " WSUS configuration is INCOMPLETE: no update classifications were enabled, so the" -color "Error" - Write-OutputColor " server will sync nothing. Fix WSUS.Classifications in defaults.json and re-run." -color "Warning" + Write-OutputColor " server will sync nothing. Fix WSUS.Classifications in rackstack.config.json and re-run." -color "Warning" return $false } @@ -380,7 +380,7 @@ function Show-WSUSManagement { Write-OutputColor " │$(" Updates: $($status.UpdateCount) Computers: $($status.ComputerCount)".PadRight(72))│" -color "Info" Write-OutputColor " │$(" Last sync: $($status.LastSync)".PadRight(72))│" -color "Info" } - $cfg = if (Test-WSUSConfigured) { "configured" } else { "NOT configured (see defaults.json)" } + $cfg = if (Test-WSUSConfigured) { "configured" } else { "NOT configured (see rackstack.config.json)" } Write-OutputColor " │$(" Config: $cfg".PadRight(72))│" -color "Info" Write-OutputColor " └────────────────────────────────────────────────────────────────────────┘" -color "Info" Write-OutputColor "" -color "Info" diff --git a/Modules/68-ADCS.ps1 b/Modules/68-ADCS.ps1 index d089cbc..ceec67a 100644 --- a/Modules/68-ADCS.ps1 +++ b/Modules/68-ADCS.ps1 @@ -15,7 +15,7 @@ # therefore requires a typed confirmation. # # Config: $script:ADCS (see 00-Initialization.ps1, overridable via -# defaults.json "ADCS"). +# rackstack.config.json "ADCS"). # Returns $true if a CA common name is configured. function Test-ADCSConfigured { @@ -144,11 +144,11 @@ function Install-ADCSCertificationAuthority { return $true } if (-not (Test-ADCSConfigured)) { - Write-OutputColor " Set ADCS.CACommonName in defaults.json before configuring the CA." -color "Error" + Write-OutputColor " Set ADCS.CACommonName in rackstack.config.json before configuring the CA." -color "Error" return $false } - # Trim the CN so a stray leading/trailing space in defaults.json does not + # Trim the CN so a stray leading/trailing space in rackstack.config.json does not # become part of a decade-lived certificate subject (and so the typed # confirmation below compares against a clean value). $caName = ([string]$script:ADCS.CACommonName).Trim() @@ -192,7 +192,7 @@ function Install-ADCSCertificationAuthority { try { $isDomainJoined = (Get-CimInstance Win32_ComputerSystem -ErrorAction Stop).PartOfDomain } catch { } if (-not $isDomainJoined) { Write-OutputColor " EnterpriseRootCA requires the machine to be domain-joined." -color "Error" - Write-OutputColor " Join the domain first, or use 'StandaloneRootCA' in defaults.json." -color "Warning" + Write-OutputColor " Join the domain first, or use 'StandaloneRootCA' in rackstack.config.json." -color "Warning" return $false } } @@ -299,7 +299,7 @@ function Show-ADCSManagement { Write-OutputColor " │$(" Type: $($status.CAType)".PadRight(72))│" -color "Info" Write-OutputColor " │$(" CertSvc: $($status.ServiceStatus)".PadRight(72))│" -color "Info" } - $cfg = if (Test-ADCSConfigured) { "configured" } else { "NOT configured (see defaults.json)" } + $cfg = if (Test-ADCSConfigured) { "configured" } else { "NOT configured (see rackstack.config.json)" } Write-OutputColor " │$(" Settings: $cfg".PadRight(72))│" -color "Info" Write-OutputColor " └────────────────────────────────────────────────────────────────────────┘" -color "Info" Write-OutputColor "" -color "Info" diff --git a/Modules/69-StorageMigration.ps1 b/Modules/69-StorageMigration.ps1 index 7d660a6..66dc58e 100644 --- a/Modules/69-StorageMigration.ps1 +++ b/Modules/69-StorageMigration.ps1 @@ -20,7 +20,7 @@ # job orchestration is a deliberate WAC workflow and the menu says so. # # Config: $script:StorageMigration (see 00-Initialization.ps1, overridable -# via defaults.json "StorageMigration"). +# via rackstack.config.json "StorageMigration"). # Returns $true if the SMS orchestrator role is installed. function Test-SMSInstalled { diff --git a/README.md b/README.md index 5b08733..6a596c1 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ OpenSSF Best Practices codecov PSScriptAnalyzer 0 errors - 5366 structural tests + 5402 structural tests Pester 312 tests SLSA Level 3

@@ -53,7 +53,7 @@ Built for MSPs, sysadmins, and infrastructure teams who build servers repeatedly **Storage Backends** -- Pluggable storage backend (iSCSI, Fibre Channel, Storage Spaces Direct, SMB3, NVMe-oF, Local); auto-detection from system state; per-backend management menus; generalized MPIO dispatching; all batch mode steps adapt to the selected backend -**Hyper-V** -- Role install, configurable VM templates (override specs or add new via `defaults.json`), batch queue deployment, VHD management, offline registry injection, Secure Boot Gen 2, cluster CSV support +**Hyper-V** -- Role install, configurable VM templates (override specs or add new via `rackstack.config.json`), batch queue deployment, VHD management, offline registry injection, Secure Boot Gen 2, cluster CSV support **Server Roles** -- Failover Clustering, MPIO, BitLocker, Deduplication, Storage Replica, 14 disk operations @@ -120,7 +120,7 @@ Grab `RackStack.exe` from the [latest release](https://github.com/TheAbider/Rack Every release artifact is signed with [Sigstore](https://www.sigstore.dev/) cosign (keyless) and carries [SLSA Level 3](https://slsa.dev/) build provenance; each release page lists SHA-256 hashes and the verification commands. The EXE is not Authenticode-signed, so Windows SmartScreen may show an "Unknown publisher" prompt on first run. -On first launch, a setup wizard walks you through configuring your environment (domain, DNS, admin account, iSCSI subnet). Your settings are saved to `defaults.json` next to the exe. To pre-configure, download `defaults.example.json` from the release, rename it to `defaults.json`, fill in your values, and place it alongside the exe. +On first launch, a setup wizard walks you through configuring your environment (domain, DNS, admin account, iSCSI subnet). Your settings are saved to `rackstack.config.json` next to the exe. To pre-configure, download `rackstack.config.example.json` from the release, rename it to `rackstack.config.json`, fill in your values, and place it alongside the exe. A legacy `defaults.json` from an earlier version is still read automatically when no `rackstack.config.json` exists -- no migration needed. -The exe auto-checks for updates from GitHub releases. Your `defaults.json` is never overwritten by updates. +The exe auto-checks for updates from GitHub releases. Your `rackstack.config.json` (or legacy `defaults.json`) is never overwritten by updates. ### From Source (Development) @@ -136,7 +136,7 @@ The exe auto-checks for updates from GitHub releases. Your `defaults.json` is ne git clone https://github.com/TheAbider/RackStack.git cd RackStack -# Run it (requires Administrator) -- first-run wizard creates defaults.json +# Run it (requires Administrator) -- first-run wizard creates rackstack.config.json Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process .\RackStack.ps1 ``` @@ -187,7 +187,7 @@ In-depth guides live in [`docs/`](docs/): ## Configuration -Copy `defaults.example.json` to `defaults.json` and customize. The example file has every configurable field with comments — here's a quick-start subset: +Copy `rackstack.config.example.json` to `rackstack.config.json` and customize. The example file has every configurable field with comments - here's a quick-start subset. A legacy `defaults.json` is still read when no `rackstack.config.json` exists, and the same per-file preference applies to company overrides: `contoso.rackstack.config.json` wins over a legacy `contoso.defaults.json`. ```json { @@ -337,7 +337,7 @@ Copy `defaults.example.json` to `defaults.json` and customize. The example file | `Timeouts` | Override per-operation timeouts (CIM queries, DNS resolves, network probes); seconds | | `CLIDefaults` | Per-action defaults for headless invocations (e.g. default `Tier` for `Cleanup`, default `Config` path) | -> `defaults.json` is gitignored -- your secrets never leave your machine. +> `rackstack.config.json` (and the legacy `defaults.json`) are gitignored -- your secrets never leave your machine. ## Batch Mode @@ -475,23 +475,23 @@ Run `RackStack.exe -ListActions` or `RackStack.exe -ListActions -OutputFormat JS ``` RackStack/ -├── RackStack.ps1 # Modular loader -- dot-sources 81 modules (dev use) -├── RackStack v{version}.ps1 # Monolithic build -- all modules in one file (deploy/compile) -├── RackStack.exe # Compiled from the monolithic .ps1 via ps2exe -├── defaults.json # Your environment config (gitignored) -├── defaults.example.json # Config template with examples -├── sync-to-monolithic.ps1 # Builds monolithic from Header.ps1 + Modules/ +├── RackStack.ps1 # Modular loader -- dot-sources 81 modules (dev use) +├── RackStack v{version}.ps1 # Monolithic build -- all modules in one file (deploy/compile) +├── RackStack.exe # Compiled from the monolithic .ps1 via ps2exe +├── rackstack.config.json # Your environment config (gitignored; legacy defaults.json still read) +├── rackstack.config.example.json # Config template with examples +├── sync-to-monolithic.ps1 # Builds monolithic from Header.ps1 + Modules/ ├── Modules/ -│ ├── 00-Initialization.ps1 # Constants, variables, config loading -│ ├── 01-Console.ps1 # Console window management -│ ├── ... # 75 more modules +│ ├── 00-Initialization.ps1 # Constants, variables, config loading +│ ├── 01-Console.ps1 # Console window management +│ ├── ... # 75 more modules │ └── 77-WindowsAdminCenter.ps1 ├── Tests/ -│ ├── Run-Tests.ps1 # 5,048 automated tests -│ ├── Validate-Release.ps1 # Pre-release validation suite +│ ├── Run-Tests.ps1 # Automated structural test suite (count in badge above) +│ ├── Validate-Release.ps1 # Pre-release validation suite │ └── ... └── docs/ - └── FileServer-Setup.md # Set up your own ISO/VHD file server + └── FileServer-Setup.md # Set up your own ISO/VHD file server ``` ### Module Architecture @@ -525,7 +525,7 @@ RackStack/ ## Testing ```powershell -# Full test suite (5,048 tests, ~4 minutes) +# Full structural test suite (count in the badge above, ~6 minutes) powershell -ExecutionPolicy Bypass -File Tests\Run-Tests.ps1 # PSScriptAnalyzer (0 errors on all 81 modules + monolithic) diff --git a/ROADMAP.md b/ROADMAP.md index 7c817fa..494245a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -36,7 +36,7 @@ rationale rather than shipped untested** — see "Deferred" below. | Item | Status | Why | |---|---|---| | OpenSSF Best Practices **Silver** badge | **Earned** (Passing + Silver both achieved) | Gold is structurally blocked by the single-maintainer bus factor. | -| In-program defaults editor + Extended Undo | Planned | Edit `defaults.json` / `.defaults.json` from inside the tool with hot-reload, and extend the single-level undo to multi-step. Both compose with the Dry-Run queue and need no schema break. Still unbuilt as of v1.119.x. | +| In-program defaults editor + Extended Undo | Planned | Edit `rackstack.config.json` / `.rackstack.config.json` from inside the tool with hot-reload, and extend the single-level undo to multi-step. Both compose with the Dry-Run queue and need no schema break. Still unbuilt as of v1.119.x. | | GPG-signed git tags | Planned | Closes the OpenSSF `version_tags_signed` criterion. One-time `git config commit.gpgsign true` + `tag.gpgsign true` + key registration at https://github.com/TheAbider.gpg. CI currently auto-tags releases without a maintainer GPG signature. | ## Next quarter (June–August 2026) @@ -79,7 +79,7 @@ of the next feature releases once they can be validated safely. | GUI front-end | The 72-char box-drawing console UI is intentional; it works over RDP, SSH-tunneled PowerShell, and emergency console-only scenarios where a GUI cannot. | | Web dashboard | Out of scope. Operators integrate via the `-OutputFormat JSON` CLI surface and route into their own dashboards. | | External REST API | Same as above — `-OutputFormat JSON` is the integration surface. | -| Major version bumps without operator pain | The 1.x line will continue with backwards-compatible additions. A 2.0 release would only happen for a `defaults.json` schema break, and would include a documented one-shot migration tool. | +| Major version bumps without operator pain | The 1.x line will continue with backwards-compatible additions. A 2.0 release would only happen for a `rackstack.config.json` schema break, and would include a documented one-shot migration tool. | | OSS-Fuzz integration | OSS-Fuzz primarily supports C/C++/Go/Python/JVM. PowerShell is not supported. The existing Pester property-based fuzz harness covers the validator surface; expanding it to more functions is a maintenance task, not a roadmap item. | | Multi-maintainer transition | Not currently planned. If the project grows beyond solo development, `GOVERNANCE.md` documents the transition path. | @@ -87,7 +87,7 @@ of the next feature releases once they can be validated safely. - **Patch releases** (`x.y.Z`) ship as needed for security fixes (Tier 1: within 14 days of confirmed disclosure, per `SECURITY.md`) and for CI/maintenance fixes (e.g. v1.119.1). - **Minor releases** (`x.Y.0`) — feature batches; the v1.109–v1.119 arc shipped them one feature at a time. -- **Major releases** (`X.0.0`) — only when a backwards-incompatible `defaults.json` schema or CLI surface change is unavoidable. None currently planned. +- **Major releases** (`X.0.0`) - only when a backwards-incompatible `rackstack.config.json` schema or CLI surface change is unavoidable. None currently planned. - The current line is **1.119.x**. CI auto-bumps and auto-releases on every commit to `master` that bumps `Header.ps1` `.VERSION`. ## How this roadmap is maintained diff --git a/RackStack.ps1 b/RackStack.ps1 index af64ecd..a1632f9 100644 --- a/RackStack.ps1 +++ b/RackStack.ps1 @@ -10,10 +10,10 @@ RackStack v{version}.ps1 (generated by sync-to-monolithic.ps1) The .exe is compiled from the monolithic, not from this loader. - Environment-specific settings are configured via defaults.json. + Environment-specific settings are configured via rackstack.config.json (a legacy defaults.json is still read). .VERSION - 1.121.15 + 1.122.0 .NOTES - Requires Windows Server 2012 R2 or later (or Windows 10/11 for testing) - Must be run as Administrator diff --git a/RackStack.psd1 b/RackStack.psd1 index f38cc11..6ef28ce 100644 --- a/RackStack.psd1 +++ b/RackStack.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'RackStack.psm1' - ModuleVersion = '1.121.15' + ModuleVersion = '1.122.0' GUID = 'c19b8e71-4a35-4f2b-9d06-8a24f7bc0e91' Author = 'TheAbider' CompanyName = 'TheAbider' diff --git a/SECURITY.md b/SECURITY.md index f3184d9..4a41db7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -63,7 +63,7 @@ In scope: - Any code path that runs with the Administrator privileges the tool requires Issues of particular concern: -- Command / argument injection via operator input or `defaults.json` fields +- Command / argument injection via operator input or `rackstack.config.json` fields - Credential exposure in transcripts, logs, exports, or error messages - Privilege escalation beyond the intended Administrator scope (or across a remote-PowerShell trust boundary) @@ -80,7 +80,7 @@ Out of scope: ## Verifying releases Every release publishes a `release-hashes.txt` containing SHA-256 hashes -for the EXE, the monolithic `.ps1`, and `defaults.example.json`. Verify +for the EXE, the monolithic `.ps1`, and `rackstack.config.example.json`. Verify before running: ```powershell @@ -111,13 +111,16 @@ The EXE is not Authenticode-signed, so Windows SmartScreen may show an download reputation. The SHA-256 hash, cosign signature, and SLSA provenance are the integrity guarantees in the meantime. -## defaults.json +## rackstack.config.json -The `defaults.json` file may contain sensitive data (Cloudflare Access -client secrets, KMS host keys, AD service-account hints). It is gitignored -by default. `defaults.example.json` contains only placeholder values; that +The `rackstack.config.json` file (or the legacy `defaults.json` it supersedes) +may contain sensitive data (Cloudflare Access client secrets, KMS host keys, +AD service-account hints). Company override files +(`.rackstack.config.json`, legacy `.defaults.json`) carry the +same sensitivity. All of them are gitignored by default. +`rackstack.config.example.json` contains only placeholder values; that is the file that ships in releases. Never commit a populated -`defaults.json` to a public repository. +config file to a public repository. ## Acknowledgements diff --git a/Tests/Pester/Logging.Tests.ps1 b/Tests/Pester/Logging.Tests.ps1 index 0e17b22..283b344 100644 --- a/Tests/Pester/Logging.Tests.ps1 +++ b/Tests/Pester/Logging.Tests.ps1 @@ -349,7 +349,7 @@ Describe 'Write-RackStackError' { { Write-RackStackError -Code 'RS-1001' } | Should -Not -Throw } - It 'renders RS-1002 (defaults.json parse error)' { + It 'renders RS-1002 (config file parse error)' { { Write-RackStackError -Code 'RS-1002' -Detail 'JSON line 14 col 5' } | Should -Not -Throw } diff --git a/Tests/Run-Tests.ps1 b/Tests/Run-Tests.ps1 index 598b294..7ae1559 100644 --- a/Tests/Run-Tests.ps1 +++ b/Tests/Run-Tests.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS - Automated Test Runner for RackStack v1.121.15 + Automated Test Runner for RackStack v1.122.0 .DESCRIPTION Comprehensive non-interactive test suite covering: @@ -97,8 +97,9 @@ $script:ModuleRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path $modulesPath = Join-Path $script:ModuleRoot "Modules" $loaderPath = Join-Path $script:ModuleRoot "RackStack.ps1" -# Derive monolithic path from defaults.json -$_testDefaultsJson = Join-Path $script:ModuleRoot "defaults.json" +# Derive monolithic path from the base config (rackstack.config.json, legacy defaults.json) +$_testDefaultsJson = Join-Path $script:ModuleRoot "rackstack.config.json" +if (-not (Test-Path $_testDefaultsJson)) { $_testDefaultsJson = Join-Path $script:ModuleRoot "defaults.json" } $_testToolFullName = "RackStack" $_testScriptVersion = "1.0.0" if (Test-Path $_testDefaultsJson) { @@ -2498,25 +2499,27 @@ try { $script:AppConfigDir = $origConfigDir } -# Test defaults.json ships alongside the tool (skip in CI / public repo where it's gitignored) -$defaultsFilePath = Join-Path (Split-Path $PSScriptRoot) 'defaults.json' +# Test the base config ships alongside the tool (rackstack.config.json preferred, legacy +# defaults.json fallback; skip in CI / public repo where it's gitignored) +$defaultsFilePath = Join-Path (Split-Path $PSScriptRoot) 'rackstack.config.json' +if (-not (Test-Path $defaultsFilePath)) { $defaultsFilePath = Join-Path (Split-Path $PSScriptRoot) 'defaults.json' } if (Test-Path $defaultsFilePath) { - Write-TestResult "defaults.json file exists" $true "Path: $defaultsFilePath" + Write-TestResult "base config file exists" $true "Path: $defaultsFilePath" try { $shippedDefaults = Get-Content $defaultsFilePath -Raw | ConvertFrom-Json - Write-TestResult "defaults.json: valid JSON" $true - Write-TestResult "defaults.json: has Domain" ($null -ne $shippedDefaults.Domain) - Write-TestResult "defaults.json: has FileServer" ($null -ne $shippedDefaults.FileServer) - Write-TestResult "defaults.json: has iSCSISubnet" ($null -ne $shippedDefaults.iSCSISubnet) - Write-TestResult "defaults.json: has CustomKMSKeys" ($null -ne $shippedDefaults.CustomKMSKeys) + Write-TestResult "base config: valid JSON" $true + Write-TestResult "base config: has Domain" ($null -ne $shippedDefaults.Domain) + Write-TestResult "base config: has FileServer" ($null -ne $shippedDefaults.FileServer) + Write-TestResult "base config: has iSCSISubnet" ($null -ne $shippedDefaults.iSCSISubnet) + Write-TestResult "base config: has CustomKMSKeys" ($null -ne $shippedDefaults.CustomKMSKeys) $has2019 = $null -ne $shippedDefaults.CustomKMSKeys.'Windows Server 2019' $has2022 = $null -ne $shippedDefaults.CustomKMSKeys.'Windows Server 2022' - Write-TestResult "defaults.json: CustomKMSKeys has 2019+2022" ($has2019 -and $has2022) "2019=$has2019, 2022=$has2022" + Write-TestResult "base config: CustomKMSKeys has 2019+2022" ($has2019 -and $has2022) "2019=$has2019, 2022=$has2022" } catch { - Write-TestResult "defaults.json: valid JSON" $false $_.Exception.Message + Write-TestResult "base config: valid JSON" $false $_.Exception.Message } } else { - Write-TestResult "defaults.json file exists" -Skipped -Message "gitignored - not present in public repo" + Write-TestResult "base config file exists" -Skipped -Message "gitignored - not present in public repo" } # ============================================================================ @@ -2742,8 +2745,9 @@ try { Write-TestResult "Monolithic region name check" $false $_.Exception.Message } -# --- 35d: defaults.json FileServer structure --- -$defaultsFilePath2 = Join-Path (Split-Path $PSScriptRoot) 'defaults.json' +# --- 35d: base config FileServer structure --- +$defaultsFilePath2 = Join-Path (Split-Path $PSScriptRoot) 'rackstack.config.json' +if (-not (Test-Path $defaultsFilePath2)) { $defaultsFilePath2 = Join-Path (Split-Path $PSScriptRoot) 'defaults.json' } if (Test-Path $defaultsFilePath2) { try { $defaults = Get-Content $defaultsFilePath2 -Raw | ConvertFrom-Json @@ -2754,29 +2758,40 @@ if (Test-Path $defaultsFilePath2) { $hasAll = $hasAll -and ($ac.ISOsFolder -eq "ISOs") $hasAll = $hasAll -and ($ac.VHDsFolder -eq "VirtualHardDrives") $hasAll = $hasAll -and (($ac.AgentFolder -eq "Agents") -or ($ac.KaseyaFolder -eq "KaseyaAgents")) - Write-TestResult "defaults.json: FileServer has required keys" $hasAll + Write-TestResult "base config: FileServer has required keys" $hasAll } catch { - Write-TestResult "defaults.json: FileServer structure" $false $_.Exception.Message + Write-TestResult "base config: FileServer structure" $false $_.Exception.Message } } else { - Write-TestResult "defaults.json: FileServer structure" -Skipped -Message "defaults.json not present" + Write-TestResult "base config: FileServer structure" -Skipped -Message "base config not present" } -# No credentials in script files - check that defaults.json values don't appear in code +# No credentials in script files - check that base config values don't appear in code try { $credFound = @() - # Load actual credential values from defaults.json (if it exists) to verify they don't leak - $defaultsFile = Join-Path $script:ModuleRoot "defaults.json" + # Load actual credential values from EVERY base config present (both naming + # schemes - a legacy defaults.json can coexist with rackstack.config.json, + # and both files' values must be scanned) to verify they don't leak $credPatterns = @() - if (Test-Path $defaultsFile) { - $dj = Get-Content $defaultsFile -Raw | ConvertFrom-Json - if ($dj.FileServer.ClientId -and $dj.FileServer.ClientId.Length -gt 10) { - $credPatterns += $dj.FileServer.ClientId - } - if ($dj.FileServer.ClientSecret -and $dj.FileServer.ClientSecret.Length -gt 10) { - $credPatterns += $dj.FileServer.ClientSecret + foreach ($credCfgName in @("rackstack.config.json", "defaults.json")) { + $credCfgFile = Join-Path $script:ModuleRoot $credCfgName + if (Test-Path $credCfgFile) { + try { + $dj = Get-Content $credCfgFile -Raw | ConvertFrom-Json + if ($dj.FileServer.ClientId -and $dj.FileServer.ClientId.Length -gt 10) { + $credPatterns += $dj.FileServer.ClientId + } + if ($dj.FileServer.ClientSecret -and $dj.FileServer.ClientSecret.Length -gt 10) { + $credPatterns += $dj.FileServer.ClientSecret + } + } catch { + # A present-but-unparseable config must fail loudly, not silently + # degrade the value-based credential scan to the pattern scan. + Write-TestResult "Credential harvest: $credCfgName parses" $false $_.Exception.Message + } } } + $credPatterns = @($credPatterns | Sort-Object -Unique) if ($credPatterns.Count -gt 0) { foreach ($moduleFile in $moduleFiles) { $content = Get-Content $moduleFile.FullName -Raw @@ -2787,7 +2802,7 @@ try { $pass = $credFound.Count -eq 0 Write-TestResult "No hardcoded credentials in module files" $pass $(if (-not $pass) { $credFound -join '; ' } else { "Clean" }) } else { - # No defaults.json on this host (public repo / CI without secrets). We can't + # No local config on this host (public repo / CI without secrets). We can't # scan for specific known values, but we CAN scan for likely-credential # *patterns* in the tracked source so the check isn't vacuous. $patternHits = @() @@ -2799,7 +2814,7 @@ try { } } $pass = $patternHits.Count -eq 0 - Write-TestResult "No hardcoded credentials in module files (pattern scan)" $pass $(if (-not $pass) { $patternHits -join '; ' } else { "Clean (no defaults.json — pattern scan only)" }) + Write-TestResult "No hardcoded credentials in module files (pattern scan)" $pass $(if (-not $pass) { $patternHits -join '; ' } else { "Clean (no local config - pattern scan only)" }) } } catch { Write-TestResult "Credential leak check" $false $_.Exception.Message @@ -2821,7 +2836,7 @@ try { $credLeaks += "looks-like-credential pattern in monolithic" } $pass = $credLeaks.Count -eq 0 - Write-TestResult "No hardcoded credentials in monolithic (pattern scan)" $pass $(if (-not $pass) { $credLeaks -join '; ' } else { "Clean (no defaults.json — pattern scan only)" }) + Write-TestResult "No hardcoded credentials in monolithic (pattern scan)" $pass $(if (-not $pass) { $credLeaks -join '; ' } else { "Clean (no local config - pattern scan only)" }) } } catch { Write-TestResult "Credential leak check (monolithic)" $false $_.Exception.Message @@ -2841,7 +2856,7 @@ try { } Write-TestResult "FileServer: VHDsFolder defaults to 'VirtualHardDrives'" ($script:FileServer.VHDsFolder -eq "VirtualHardDrives") -# AgentFolder may be overridden by KaseyaFolder remap from defaults.json +# AgentFolder may be overridden by KaseyaFolder remap from the base config Write-TestResult "FileServer: AgentFolder is non-empty string" (-not [string]::IsNullOrWhiteSpace($script:FileServer.AgentFolder)) # ClientId/ClientSecret may be populated by company defaults — check they're strings (empty or from company file) $clientIdIsString = $script:FileServer.ClientId -is [string] @@ -4496,7 +4511,7 @@ try { Write-TestResult "README: mentions 78 modules" ($readmeContent -match '81 module') Write-TestResult "README: has batch mode section" ($readmeContent -match 'Batch Mode') Write-TestResult "README: has testing section" ($readmeContent -match 'Testing') - Write-TestResult "README: has defaults.json example" ($readmeContent -match 'defaults\.json') + Write-TestResult "README: has rackstack.config.json example" ($readmeContent -match 'rackstack\.config\.json') } catch { Write-TestResult "README: content" $false $_.Exception.Message } @@ -5247,7 +5262,7 @@ try { Remove-Item $testDir -Recurse -Force -ErrorAction SilentlyContinue # Verify SQL template removed after restore (only built-in + real defaults remain) - # SQL only exists if the real defaults.json defines it + # SQL only exists if the real base config defines it } catch { Write-TestResult "Custom VM Templates merge" $false $_.Exception.Message $script:DefaultsPath = $origDefaultsPath @@ -5278,21 +5293,21 @@ try { Import-Defaults } -# defaults.example.json has new sections -$examplePath = Join-Path (Split-Path $PSScriptRoot) 'defaults.example.json' +# rackstack.config.example.json has new sections +$examplePath = Join-Path (Split-Path $PSScriptRoot) 'rackstack.config.example.json' if (Test-Path $examplePath) { try { $exampleData = Get-Content $examplePath -Raw | ConvertFrom-Json - Write-TestResult "defaults.example.json: has CustomVMTemplates" ($null -ne $exampleData.CustomVMTemplates) - Write-TestResult "defaults.example.json: has CustomVMDefaults" ($null -ne $exampleData.CustomVMDefaults) - Write-TestResult "defaults.example.json: CustomVMTemplates has FS example" ($null -ne $exampleData.CustomVMTemplates.FS) - Write-TestResult "defaults.example.json: CustomVMTemplates has SQL example" ($null -ne $exampleData.CustomVMTemplates.SQL) - Write-TestResult "defaults.example.json: CustomVMDefaults has vCPU" ($null -ne $exampleData.CustomVMDefaults.vCPU) - Write-TestResult "defaults.example.json: has VMNaming" ($null -ne $exampleData.VMNaming) - Write-TestResult "defaults.example.json: VMNaming has Pattern" ($null -ne $exampleData.VMNaming.Pattern) - Write-TestResult "defaults.example.json: CustomVMTemplates has APP example" ($null -ne $exampleData.CustomVMTemplates.APP) + Write-TestResult "rackstack.config.example.json: has CustomVMTemplates" ($null -ne $exampleData.CustomVMTemplates) + Write-TestResult "rackstack.config.example.json: has CustomVMDefaults" ($null -ne $exampleData.CustomVMDefaults) + Write-TestResult "rackstack.config.example.json: CustomVMTemplates has FS example" ($null -ne $exampleData.CustomVMTemplates.FS) + Write-TestResult "rackstack.config.example.json: CustomVMTemplates has SQL example" ($null -ne $exampleData.CustomVMTemplates.SQL) + Write-TestResult "rackstack.config.example.json: CustomVMDefaults has vCPU" ($null -ne $exampleData.CustomVMDefaults.vCPU) + Write-TestResult "rackstack.config.example.json: has VMNaming" ($null -ne $exampleData.VMNaming) + Write-TestResult "rackstack.config.example.json: VMNaming has Pattern" ($null -ne $exampleData.VMNaming.Pattern) + Write-TestResult "rackstack.config.example.json: CustomVMTemplates has APP example" ($null -ne $exampleData.CustomVMTemplates.APP) } catch { - Write-TestResult "defaults.example.json VM sections" $false $_.Exception.Message + Write-TestResult "rackstack.config.example.json VM sections" $false $_.Exception.Message } } @@ -5893,7 +5908,7 @@ try { Write-TestResult "AgentInstaller has '$key' key" ($script:AgentInstaller.ContainsKey($key)) } - # Default values (MSP generic when no defaults.json override) + # Default values (MSP generic when no config override) Write-TestResult "AgentInstaller: ToolName is string" ($script:AgentInstaller.ToolName -is [string]) Write-TestResult "AgentInstaller: InstallPaths is array" ($script:AgentInstaller.InstallPaths -is [array]) Write-TestResult "AgentInstaller: SuccessExitCodes is array" ($script:AgentInstaller.SuccessExitCodes -is [array]) @@ -6125,8 +6140,20 @@ try { # Cleanup targets config directory Write-TestResult "47-ExitCleanup: cleans up AppConfigDir" ($ecContent -match 'script:AppConfigDir') - # Searches for defaults.json in cleanup paths - Write-TestResult "47-ExitCleanup: targets defaults.json" ($ecContent -match 'defaults\.json') + # Searches for config files in cleanup paths (both naming schemes + company files) + Write-TestResult "47-ExitCleanup: targets rackstack.config.json" ($ecContent -match 'rackstack\.config\.json') + Write-TestResult "47-ExitCleanup: targets legacy defaults.json" ($ecContent -match '"defaults\.json"') + Write-TestResult "47-ExitCleanup: sweeps company config files" ($ecContent -match '\*\.rackstack\.config\.json') + + # Company-config wildcard sweeps must stay gated to the tool's own home + # (shared-folder glob-deletes would hit other applications' config files), + # and the gates must match the literal product name too (package-manager + # install paths keep "rackstack" even when ToolName is rebranded). + Write-TestResult "47-ExitCleanup: module-root company sweep is tool-home gated" ($ecContent -match '\(\$script:ModuleRoot -like "\*\$toolName\*"\) -or \(\$script:ModuleRoot -like "\*RackStack\*"\)') + Write-TestResult "47-ExitCleanup: module-root gate requires this tool's modular layout" ($ecContent -match 'Join-Path \$script:ModuleRoot "Modules\\00-Initialization\.ps1"') + Write-TestResult "47-ExitCleanup: exe-adjacent company sweep is tool-home gated" ($ecContent -match '\(\$exeDir -like "\*\$toolName\*"\) -or \(\$exeDir -like "\*RackStack\*"\)') + Write-TestResult "47-ExitCleanup: cleans crash-leftover config .tmp files" ($ecContent -match 'rackstack\.config\.json\.tmp') + Write-TestResult "47-ExitCleanup: sweeps company config .tmp files" ($ecContent -match '\*\.defaults\.json\.tmp') # Uses scheduled task for post-reboot cleanup Write-TestResult "47-ExitCleanup: schedules cleanup task" ($ecContent -match 'Register-ScheduledTask') @@ -6663,13 +6690,13 @@ try { $qolContent = Get-Content (Join-Path $modulesPath "55-QoLFeatures.ps1") -Raw Write-TestResult "55-QoLFeatures: FavoriteDispatch has Add Virtual NIC" ($qolContent -match '"Add Virtual NIC"') - # defaults.example.json has CustomVNICs - $defaultsExamplePath = Join-Path (Join-Path $PSScriptRoot "..") "defaults.example.json" + # rackstack.config.example.json has CustomVNICs + $defaultsExamplePath = Join-Path (Join-Path $PSScriptRoot "..") "rackstack.config.example.json" $defaultsContent = Get-Content $defaultsExamplePath -Raw # CustomVNICs was removed from defaults.example.json in v1.98.9 — the documented schema # was a lie (Import-Defaults never read it; the only consumer is batch_config.json's # separate schema). Test inverted to assert the lie is gone. - Write-TestResult "defaults.example.json: CustomVNICs schema lie removed" (-not ($defaultsContent -match '"CustomVNICs"')) + Write-TestResult "rackstack.config.example.json: CustomVNICs schema lie removed" (-not ($defaultsContent -match '"CustomVNICs"')) } catch { Write-TestResult "Custom vNIC Feature Tests" $false $_.Exception.Message @@ -6897,14 +6924,14 @@ try { Write-TestResult "50-Entry: backward compat ConfigureiSCSI" ($entryContent -match 'ConfigureiSCSI') Write-TestResult "50-Entry: Initialize-StorageBackendBatch for non-iSCSI" ($entryContent -match 'Initialize-StorageBackendBatch') - # defaults.example.json: storage backend section - $examplePath = Join-Path $script:ModuleRoot "defaults.example.json" + # rackstack.config.example.json: storage backend section + $examplePath = Join-Path $script:ModuleRoot "rackstack.config.example.json" if (Test-Path $examplePath) { $exampleContent = Get-Content $examplePath -Raw - Write-TestResult "defaults.example.json: has StorageBackendType" ($exampleContent -match 'StorageBackendType') - Write-TestResult "defaults.example.json: has storage backend help" ($exampleContent -match '_StorageBackendType_help') + Write-TestResult "rackstack.config.example.json: has StorageBackendType" ($exampleContent -match 'StorageBackendType') + Write-TestResult "rackstack.config.example.json: has storage backend help" ($exampleContent -match '_StorageBackendType_help') } else { - Write-TestResult "defaults.example.json: exists" $false "File not found" + Write-TestResult "rackstack.config.example.json: exists" $false "File not found" } # RackStack.ps1 loader includes 62-HyperVReplica.ps1 @@ -7009,7 +7036,7 @@ try { $opsContent = Get-Content (Join-Path $modulesPath "56-OperationsMenu.ps1") -Raw $iscsiContent = Get-Content (Join-Path $modulesPath "10-iSCSI.ps1") -Raw $batchContent = Get-Content (Join-Path $modulesPath "36-BatchConfig.ps1") -Raw - $exampleContent = Get-Content (Join-Path $script:ModuleRoot "defaults.example.json") -Raw + $exampleContent = Get-Content (Join-Path $script:ModuleRoot "rackstack.config.example.json") -Raw # Initialization Write-TestResult "00-Init: SANTargetPairings variable declared" ($initContent -match '\$script:SANTargetPairings') @@ -7035,12 +7062,12 @@ try { # Batch template has SANTargetPairings Write-TestResult "36-BatchConfig: template has SANTargetPairings field" ($batchContent -match '"SANTargetPairings"') - # defaults.example.json documents SANTargetPairings - Write-TestResult "defaults.example.json: has SANTargetPairings section" ($exampleContent -match '"SANTargetPairings"') - Write-TestResult "defaults.example.json: has Pairs with A/B" ($exampleContent -match '"Pairs"[\s\S]*?"A".*?"B"') - Write-TestResult "defaults.example.json: has HostAssignments with HostMod" ($exampleContent -match '"HostAssignments"[\s\S]*?"HostMod"') - Write-TestResult "defaults.example.json: has CycleSize" ($exampleContent -match '"CycleSize":\s*4') - Write-TestResult "defaults.example.json: pairs use A0/B0 labeling" ($exampleContent -match '"ALabel":\s*"A0"') + # rackstack.config.example.json documents SANTargetPairings + Write-TestResult "rackstack.config.example.json: has SANTargetPairings section" ($exampleContent -match '"SANTargetPairings"') + Write-TestResult "rackstack.config.example.json: has Pairs with A/B" ($exampleContent -match '"Pairs"[\s\S]*?"A".*?"B"') + Write-TestResult "rackstack.config.example.json: has HostAssignments with HostMod" ($exampleContent -match '"HostAssignments"[\s\S]*?"HostMod"') + Write-TestResult "rackstack.config.example.json: has CycleSize" ($exampleContent -match '"CycleSize":\s*4') + Write-TestResult "rackstack.config.example.json: pairs use A0/B0 labeling" ($exampleContent -match '"ALabel":\s*"A0"') # Test Get-SANTargetsForHost default behavior still works $script:SANTargetPairings = $null @@ -7757,7 +7784,7 @@ try { $aiContent2 = Get-Content "$modulesPath\57-AgentInstaller.ps1" -Raw $initContent2 = Get-Content "$modulesPath\00-Initialization.ps1" -Raw $opsContent2 = Get-Content "$modulesPath\56-OperationsMenu.ps1" -Raw - $exampleJson = Get-Content (Join-Path (Split-Path $PSScriptRoot) 'defaults.example.json') -Raw + $exampleJson = Get-Content (Join-Path (Split-Path $PSScriptRoot) 'rackstack.config.example.json') -Raw # Integrity gate distinguishes recoverable "no sidecar" from genuine tamper Write-TestResult "39-FS: Test-FileIntegrity result carries a Reason field" ($fsContent2 -match 'Reason\s*=\s*\$null') @@ -7779,7 +7806,7 @@ try { # Defaults: fail-closed out of the box, surfaced in the example config Write-TestResult "00-Init: AgentInstaller default RequireHash = true" ($initContent2 -match 'RequireHash\s*=\s*\$true') Write-TestResult "56-Ops: Import-Defaults reset includes RequireHash" ($opsContent2 -match 'RequireHash\s*=\s*\$true') - Write-TestResult "defaults.example.json: documents RequireHash" ($exampleJson -match '"RequireHash"') + Write-TestResult "rackstack.config.example.json: documents RequireHash" ($exampleJson -match '"RequireHash"') } catch { Write-TestResult "Agent Installer Hash-Verification Policy Tests" $false $_.Exception.Message @@ -8035,8 +8062,13 @@ try { Write-TestResult "Company: CompanyDefaultsName variable declared" ($initContent -match '\$script:CompanyDefaultsName\s*=\s*\$null') Write-TestResult "Company: CompanyDefaultsPath variable declared" ($initContent -match '\$script:CompanyDefaultsPath\s*=\s*\$null') - # Get-CompanyDefaultsFiles: scans for *.defaults.json, excludes defaults.json and defaults.example.json - Write-TestResult "Company: Get-CompanyDefaultsFiles scans *.defaults.json" ($opsContent -match 'Get-ChildItem.*\*\.defaults\.json') + # Get-CompanyDefaultsFiles: scans *.rackstack.config.json plus legacy *.defaults.json; + # excludes the base and example configs under both naming schemes; dedupes per key + Write-TestResult "Company: Get-CompanyDefaultsFiles scans *.rackstack.config.json" ($opsContent -match 'Get-ChildItem.*\*\.rackstack\.config\.json') + Write-TestResult "Company: Get-CompanyDefaultsFiles scans legacy *.defaults.json" ($opsContent -match 'Get-ChildItem.*\*\.defaults\.json') + Write-TestResult "Company: Get-CompanyDefaultsFiles excludes rackstack.config.json" ($opsContent -match '\$f\.Name\s*-eq\s*"rackstack\.config\.json"') + Write-TestResult "Company: Get-CompanyDefaultsFiles excludes rackstack.config.example.json" ($opsContent -match '\$f\.Name\s*-eq\s*"rackstack\.config\.example\.json"') + Write-TestResult "Company: Get-CompanyDefaultsFiles dedupes per company key" ($opsContent -match 'companyPaths\.ContainsKey\(\$name\)') Write-TestResult "Company: Get-CompanyDefaultsFiles excludes defaults.json" ($opsContent -match '\$f\.Name\s*-eq\s*"defaults\.json"') Write-TestResult "Company: Get-CompanyDefaultsFiles excludes defaults.example.json" ($opsContent -match '\$f\.Name\s*-eq\s*"defaults\.example\.json"') @@ -10081,7 +10113,7 @@ try { Write-TestResult "80-RDS: licensing Dry-Run apply no longer swallows write errors [1]" (-not ($rdsRaw -match "Name 'LicensingMode' -Value \`$mv -Type DWord -ErrorAction SilentlyContinue")) # [2] Write-TestResult "67-WSUS: tracks whether classifications were enabled [2]" (($wsusRaw -match '\$classificationsEnabled = \$false') -and ($wsusRaw -match '\$classificationsEnabled = \$true')) - Write-TestResult "67-WSUS: returns false when no classification enabled [2]" ($wsusRaw -match 'if \(-not \$classificationsEnabled\)[\s\S]{0,400}return \$false') + Write-TestResult "67-WSUS: returns false when no classification enabled [2]" ($wsusRaw -match 'if \(-not \$classificationsEnabled\)[\s\S]{0,450}return \$false') # [3] Write-TestResult "63-ScheduledTasks: never-run tasks are not flagged FAILED [3]" ($schRaw -match '\$lastResult -eq 0x00041300 -or \$lastResult -eq 0x00041303') } @@ -10172,6 +10204,111 @@ catch { Write-TestResult "Final-Sweep Hardening Tests" $false $_.Exception.Message } +# ============================================================================ +# SECTION 203: NAMESPACED CONFIG FILES (v1.122.0) +# ============================================================================ +# The base config is rackstack.config.json with legacy defaults.json fallback; +# company overrides are .rackstack.config.json with legacy +# .defaults.json fallback, deduped per company key (new name wins). +# Resolution happens once at init (Get-RackStackConfigPath); saves target the +# resolved (loaded) file so legacy setups keep writing to their legacy file. +Write-SectionHeader "SECTION 203: NAMESPACED CONFIG FILES" + +try { + $initContent203 = Get-Content "$modulesPath\00-Initialization.ps1" -Raw + $opsContent203 = Get-Content "$modulesPath\56-OperationsMenu.ps1" -Raw + + # Structural: resolver exists and drives DefaultsPath + Write-TestResult "Config: Get-RackStackConfigPath function exists" ($initContent203 -match 'function\s+Get-RackStackConfigPath\b') + Write-TestResult "Config: resolver prefers rackstack.config.json" ($initContent203 -match 'Join-Path \$Root "rackstack\.config\.json"') + Write-TestResult "Config: resolver falls back to defaults.json" ($initContent203 -match 'Join-Path \$Root "defaults\.json"') + Write-TestResult "Config: DefaultsPath assigned via resolver" ($initContent203 -match '\$script:DefaultsPath\s*=\s*Get-RackStackConfigPath\s+-Root\s+\$script:ModuleRoot') + + # Behavioral: resolution order in a sandbox + $cfgDir = Join-Path $env:TEMP "rackstack-test-cfgres-$(Get-Random)" + New-Item -ItemType Directory -Path $cfgDir -Force | Out-Null + try { + $resolved = Get-RackStackConfigPath -Root $cfgDir + Write-TestResult "Config: neither file -> new name for creation" ($resolved -eq (Join-Path $cfgDir "rackstack.config.json")) + + Set-Content -LiteralPath (Join-Path $cfgDir "defaults.json") -Value '{}' -Encoding UTF8 + $resolved = Get-RackStackConfigPath -Root $cfgDir + Write-TestResult "Config: legacy only -> legacy path" ($resolved -eq (Join-Path $cfgDir "defaults.json")) + + Set-Content -LiteralPath (Join-Path $cfgDir "rackstack.config.json") -Value '{}' -Encoding UTF8 + $resolved = Get-RackStackConfigPath -Root $cfgDir + Write-TestResult "Config: both files -> new name wins" ($resolved -eq (Join-Path $cfgDir "rackstack.config.json")) + + Remove-Item -LiteralPath (Join-Path $cfgDir "defaults.json") -Force + $resolved = Get-RackStackConfigPath -Root $cfgDir + Write-TestResult "Config: new only -> new path" ($resolved -eq (Join-Path $cfgDir "rackstack.config.json")) + } finally { + Remove-Item -LiteralPath $cfgDir -Recurse -Force -ErrorAction SilentlyContinue + } + + # Behavioral: company discovery under both naming schemes, deduped per key + $coDir = Join-Path $env:TEMP "rackstack-test-cfgco-$(Get-Random)" + New-Item -ItemType Directory -Path $coDir -Force | Out-Null + $origModuleRoot203 = $script:ModuleRoot + try { + Set-Content -LiteralPath (Join-Path $coDir "alpha.rackstack.config.json") -Value '{}' -Encoding UTF8 + Set-Content -LiteralPath (Join-Path $coDir "alpha.defaults.json") -Value '{}' -Encoding UTF8 + Set-Content -LiteralPath (Join-Path $coDir "beta.defaults.json") -Value '{}' -Encoding UTF8 + Set-Content -LiteralPath (Join-Path $coDir "gamma.rackstack.config.json") -Value '{}' -Encoding UTF8 + # Base + example files must be excluded from company discovery + Set-Content -LiteralPath (Join-Path $coDir "rackstack.config.json") -Value '{}' -Encoding UTF8 + Set-Content -LiteralPath (Join-Path $coDir "rackstack.config.example.json") -Value '{}' -Encoding UTF8 + Set-Content -LiteralPath (Join-Path $coDir "defaults.json") -Value '{}' -Encoding UTF8 + Set-Content -LiteralPath (Join-Path $coDir "defaults.example.json") -Value '{}' -Encoding UTF8 + + $script:ModuleRoot = $coDir + $companies = @(Get-CompanyDefaultsFiles) + $script:ModuleRoot = $origModuleRoot203 + + Write-TestResult "Config: company discovery finds 3 companies" ($companies.Count -eq 3) "Found: $($companies.Count)" + $alpha = $companies | Where-Object { $_.Name -eq 'alpha' } + $beta = $companies | Where-Object { $_.Name -eq 'beta' } + $gamma = $companies | Where-Object { $_.Name -eq 'gamma' } + Write-TestResult "Config: company keys are bare names" ($null -ne $alpha -and $null -ne $beta -and $null -ne $gamma) + Write-TestResult "Config: new name wins when both exist" ($alpha.Path -like '*alpha.rackstack.config.json') + Write-TestResult "Config: legacy-only company still discovered" ($beta.Path -like '*beta.defaults.json') + Write-TestResult "Config: new-only company discovered" ($gamma.Path -like '*gamma.rackstack.config.json') + } finally { + $script:ModuleRoot = $origModuleRoot203 + Remove-Item -LiteralPath $coDir -Recurse -Force -ErrorAction SilentlyContinue + } + + # Structural: labels use the resolved filename, not a hard-coded one + Write-TestResult "Config: save picker label uses resolved leaf" ($opsContent203 -match '\$targetLabel\s*=\s*Split-Path\s+-Leaf\s+\$script:DefaultsPath') + Write-TestResult "Config: company save label uses resolved leaf" ($opsContent203 -match '\$targetLabel\s*=\s*Split-Path\s+-Leaf\s+\$script:CompanyDefaultsPath') + + # Public-repo layout checks (the work mirror has its own .gitignore and CI; + # detect the public layout via dist/ and skip there, mirroring Section 174) + $isPublicLayout = Test-Path (Join-Path $script:ModuleRoot "dist\scoop\rackstack.json") + if ($isPublicLayout) { + $gitignore203 = Get-Content (Join-Path $script:ModuleRoot ".gitignore") -Raw + Write-TestResult "Config: .gitignore ignores rackstack.config.json" ($gitignore203 -match '(?m)^rackstack\.config\.json\s*$') + Write-TestResult "Config: .gitignore ignores *.rackstack.config.json" ($gitignore203 -match '(?m)^\*\.rackstack\.config\.json\s*$') + Write-TestResult "Config: .gitignore keeps legacy defaults.json ignored" ($gitignore203 -match '(?m)^defaults\.json\s*$') + Write-TestResult "Config: .gitignore keeps legacy *.defaults.json ignored" ($gitignore203 -match '(?m)^\*\.defaults\.json\s*$') + Write-TestResult "Config: .gitignore un-ignores the tracked example" ($gitignore203 -match '(?m)^!rackstack\.config\.example\.json\s*$') + + Write-TestResult "Config: rackstack.config.example.json exists" (Test-Path (Join-Path $script:ModuleRoot "rackstack.config.example.json")) + Write-TestResult "Config: legacy defaults.example.json removed" (-not (Test-Path (Join-Path $script:ModuleRoot "defaults.example.json"))) + + # Release-only CI steps never run on PR CI, so pin the renamed asset here + $ciYml203 = Get-Content (Join-Path $script:ModuleRoot ".github\workflows\ci.yml") -Raw + Write-TestResult "Config: ci.yml hashes rackstack.config.example.json" ($ciYml203 -match "Name = 'rackstack\.config\.example\.json'") + Write-TestResult "Config: ci.yml uploads rackstack.config.example.json" ($ciYml203 -match '"rackstack\.config\.example\.json"') + Write-TestResult "Config: ci.yml has no stale defaults.example.json refs" (-not ($ciYml203 -match 'defaults\.example\.json')) + } else { + Write-TestResult "Config: public-layout gitignore/CI checks" -Skipped -Message "work-mirror layout - dist/ not present" + } +} +catch { + Write-TestResult "Namespaced Config Tests" $false $_.Exception.Message +} + # ============================================================================ # SECTION 174: DOCUMENTATION FRESHNESS (counts must match the codebase) # ============================================================================ @@ -11252,7 +11389,7 @@ Write-TestResult "35-Utilities: task audit uses List instead of array +=" ($u $htmlContent3 = Get-Content (Join-Path $modulesPath "54-HTMLReports.ps1") -Raw Write-TestResult "54-HTMLReports: snapshot loading uses List instead of array +=" ($htmlContent3 -match 'snapshotList\s*=\s*\[System\.Collections\.Generic\.List') -# v1.21.2: Atomic defaults.json write (temp file + move) +# v1.21.2: Atomic config write (temp file + move) $opsContent2 = Get-Content (Join-Path $modulesPath "56-OperationsMenu.ps1") -Raw Write-TestResult "56-OperationsMenu: Export-Defaults writes to temp file first" ($opsContent2 -match 'Export-Defaults[\s\S]*?DefaultsPath\)\.tmp') Write-TestResult "56-OperationsMenu: Export-Defaults moves temp to final path" ($opsContent2 -match 'Export-Defaults[\s\S]*?Move-Item.*tempPath.*DefaultsPath') @@ -11312,15 +11449,15 @@ Write-TestResult "03-InputValidation: reports invalid characters" ($ivContent -m $hnContent = Get-Content (Join-Path $modulesPath "11-Hostname.ps1") -Raw Write-TestResult "11-Hostname: shows specific validation reason" ($hnContent -match 'Get-HostnameValidationError') -# v1.21.2: First-run wizard saves minimal defaults.json when adopting company defaults +# v1.21.2: First-run wizard saves a minimal base config when adopting company defaults $opsContent2 = Get-Content (Join-Path $modulesPath "56-OperationsMenu.ps1") -Raw Write-TestResult "56-OperationsMenu: wizard saves minimal defaults for company adoption" ($opsContent2 -match '\$minimalDefaults\s*=' -and $opsContent2 -match 'Do NOT use Export-Defaults here') # v1.21.2: Import-Defaults Tier 3 does deep merge for nested objects Write-TestResult "56-OperationsMenu: Tier 3 deep-merges nested objects" ($opsContent2 -match 'Deep-merge nested objects') -# v1.21.2: Import-Defaults prompts for company defaults even when defaults.json exists without _companyDefaults -Write-TestResult "56-OperationsMenu: prompts for company defaults when missing from existing defaults.json" ($opsContent2 -match 'companyDefaultsResolved') +# v1.21.2: Import-Defaults prompts for company defaults even when a base config exists without _companyDefaults +Write-TestResult "56-OperationsMenu: prompts for company defaults when missing from existing base config" ($opsContent2 -match 'companyDefaultsResolved') # ============================================================================ # SECTION 144: FUNCTION PARITY (modular vs monolithic) diff --git a/dist/scoop/rackstack.json b/dist/scoop/rackstack.json index 7773077..f950688 100644 --- a/dist/scoop/rackstack.json +++ b/dist/scoop/rackstack.json @@ -9,7 +9,7 @@ "bin": "RackStack.exe", "notes": [ "RackStack requires Administrator privileges and auto-elevates when launched.", - "First-run wizard creates defaults.json next to the EXE under the scoop shims dir.", + "First-run wizard creates rackstack.config.json next to the EXE under the scoop shims dir.", "Releases are cosign-signed (Sigstore) — verify with `gh attestation verify RackStack.exe --owner TheAbider`." ], "checkver": { diff --git a/docs/ASSURANCE_CASE.md b/docs/ASSURANCE_CASE.md index 87c7e79..37c5ca4 100644 --- a/docs/ASSURANCE_CASE.md +++ b/docs/ASSURANCE_CASE.md @@ -40,7 +40,7 @@ The rest of this document unpacks what that means concretely. | Administrator/SYSTEM access on the host running RackStack | High | The operator's identity is the bedrock of every other trust assumption. | | Operator-provided credentials (local admin password, AD service-account creds, BitLocker recovery keys) | High | Disclosure breaks domain trust, leaks BitLocker volumes, or enables lateral movement. | | Released artifacts (`RackStack.exe`, monolithic `.ps1`) | Medium | Compromise allows supply-chain attack on every downstream operator. | -| `defaults.json` operator config | Medium | Contains Cloudflare Access secrets, KMS host keys, AD service-account hints. | +| `rackstack.config.json` operator config (legacy `defaults.json` still read) | Medium | Contains Cloudflare Access secrets, KMS host keys, AD service-account hints. | | Audit log file (`Config__.log`) | Medium | Contains operational history; should not contain plaintext secrets. | | Session transcript | Medium | Same as above; PowerShell `Start-Transcript` records every line. | @@ -80,7 +80,7 @@ each one is where defensive code must be applied. | | RackStack.exe process | | | | (also runs as Administrator) | | | | | | -| | reads ----> defaults.json <----- Trust boundary 1 | | +| | reads ----> rackstack.config.json <-- Trust boundary 1 | | | | (operator-trusted; admin-only DACL by default) | | | | | | | reads ----> registry HKLM\...\Uninstall\RackStack | | @@ -115,7 +115,7 @@ applied at each: | # | Boundary | Defense | |---|---|---| -| 1 | `defaults.json` → process | Schema validation via `Test-BatchConfig`; type checking via `ValidateSet`; rejection of unsafe values (`Test-ValidHostname`, `Test-ValidIPAddress`). | +| 1 | `rackstack.config.json` -> process | Schema validation via `Test-BatchConfig`; type checking via `ValidateSet`; rejection of unsafe values (`Test-ValidHostname`, `Test-ValidIPAddress`). | | 2 | Registry → process | Allowlist regex on `InstallLocation` and `OutputPath` (`["`&|<>^%()]`, `..`, `[\x00-\x1F]` all rejected). | | 3 | State file → process | DACL-verified directory creation under `%ProgramData%`; `Test-RackStackStateFileAcl` refuses to load if a non-admin SID has write access. | | 4 | Temp file → process | Cryptographically random per-process names (`[guid]::NewGuid()`); atomic write-then-rename; cleanup on `Exit-Script`. | @@ -170,7 +170,7 @@ key but flow into the same code. | `Set-Transcript` paused around password display so the plaintext does not enter the session log | `Modules/22-Password.ps1` `New-StrongPassword` lines 393–410; `Modules/31-BitLocker.ps1` BitLocker key display block. | | Structured-log secret-key redaction | `Modules/02-Logging.ps1` `Write-StructuredLog` `$secretKeyPattern` — covers `password`, `token`, `secret`, `cookie`, `session`, `sid`, `signature`, `sig`, `private_key`, `pem`, `dsrm`, `recovery`, `webhook_url`, plus their snake_case and CamelCase variants. | | gitleaks scans every push for accidentally-committed secrets | `.github/workflows/gitleaks.yml` — weekly full-history sweep too. | -| `defaults.json` is gitignored; only `defaults.example.json` ships with placeholder values | `.gitignore`. | +| `rackstack.config.json` and the legacy `defaults.json` are gitignored; only `rackstack.config.example.json` ships with placeholder values | `.gitignore`. | **Counter-argument considered.** A future caller could route a credential through `Write-StructuredLog -Data` under a key not in the redact @@ -259,7 +259,7 @@ following are acknowledged and tracked: planned to fork. ### CR-5: Operator can disable defenses -- An operator with admin rights can edit `defaults.json` to skip +- An operator with admin rights can edit `rackstack.config.json` to skip BitLocker recovery checks, disable Defender exclusions, or run destructive operations without confirmation prompts via `-Silent`. - Mitigation: documented defaults are safe; out-of-scope per the diff --git a/docs/Cluster-Management.md b/docs/Cluster-Management.md index c18185e..0ece4c3 100644 --- a/docs/Cluster-Management.md +++ b/docs/Cluster-Management.md @@ -182,7 +182,7 @@ Several RackStack features are cluster-aware and adapt their behavior when runni ### Storage Path Configuration -Configure cluster paths in `defaults.json`: +Configure cluster paths in `rackstack.config.json`: ```json "StoragePaths": { diff --git a/docs/Configuration.md b/docs/Configuration.md index e6f0019..139333f 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1,10 +1,14 @@ # Configuration Guide -RackStack is configured via `defaults.json`, placed alongside the executable or script. On first launch without a config file, the setup wizard walks you through creating one. +RackStack is configured via `rackstack.config.json`, placed alongside the executable or script. A legacy `defaults.json` from earlier versions is still read automatically when no `rackstack.config.json` exists; whichever file is loaded is the one the tool writes back to when you save from the in-tool editor. On first launch with neither file present, the setup wizard walks you through creating `rackstack.config.json`. -Copy `defaults.example.json` to `defaults.json` and customize for your environment. +Copy `rackstack.config.example.json` to `rackstack.config.json` and customize for your environment. -> `defaults.json` is gitignored -- your secrets never leave your machine. +> `rackstack.config.json` (and the legacy `defaults.json`) are gitignored -- your secrets never leave your machine. + +## Company Config Files + +In addition to the base config, RackStack loads per-company overrides. Name them `.rackstack.config.json` (e.g. `contoso.rackstack.config.json`); the legacy `.defaults.json` name (e.g. `contoso.defaults.json`) is still read as a fallback, per company. When both names exist for the same company, the new name wins. Multiple company files can coexist, and the `_companyDefaults` key in the base config selects which one loads. Saving to the company baseline from the in-tool editor writes to whichever company file is loaded; new company files should use the new naming scheme. --- @@ -328,11 +332,11 @@ Default specs for non-template (custom) VMs created via the "Custom VM" option i Paths excluded from Windows Defender scanning on Hyper-V hosts to improve VM performance. - `DefenderExclusionPaths` -- Static system-level paths that are always excluded. -- `DefenderCommonVMPaths` -- VM storage paths. If not set in `defaults.json`, these are **auto-generated dynamically** from the selected host storage drive (see [Dynamic Defender Paths](#dynamic-defender-paths)). +- `DefenderCommonVMPaths` -- VM storage paths. If not set in `rackstack.config.json`, these are **auto-generated dynamically** from the selected host storage drive (see [Dynamic Defender Paths](#dynamic-defender-paths)). ### Dynamic Defender Paths -When `DefenderCommonVMPaths` is **not** specified in `defaults.json`, RackStack auto-generates VM exclusion paths based on the selected host storage drive. For example, if drive D: is selected: +When `DefenderCommonVMPaths` is **not** specified in `rackstack.config.json`, RackStack auto-generates VM exclusion paths based on the selected host storage drive. For example, if drive D: is selected: ``` D:\Virtual Machines @@ -343,7 +347,7 @@ D:\Virtual Machines\_BaseImages If Cluster Shared Volumes exist, each CSV volume is also added. This ensures Defender exclusions always match the actual storage location without manual configuration. -If `DefenderCommonVMPaths` **is** specified in `defaults.json`, those paths are used as-is and dynamic generation is skipped. +If `DefenderCommonVMPaths` **is** specified in `rackstack.config.json`, those paths are used as-is and dynamic generation is skipped. ### FileServer (File Server) diff --git a/docs/FileServer-Setup.md b/docs/FileServer-Setup.md index 6a4cd0f..d60da96 100644 --- a/docs/FileServer-Setup.md +++ b/docs/FileServer-Setup.md @@ -15,7 +15,7 @@ The tool expects: ## Folder Structure -All setups use the same layout. The `BaseURL` in `defaults.json` points to the root (e.g., `server-tools/`). +All setups use the same layout. The `BaseURL` in `rackstack.config.json` points to the root (e.g., `server-tools/`). **Linux:** ``` @@ -74,9 +74,9 @@ C:\FileServer\server-tools\ - **Enterprise Linux policy** -- [Rocky/Alma/RHEL](fileserver-rhel.md). - **Already in Azure/AWS** -- [Cloud Storage](fileserver-cloud.md) (Azure Blob native, S3 via CloudFront + index.json). -## defaults.json Reference +## rackstack.config.json Reference -All setups end with the same `defaults.json` configuration: +All setups end with the same `rackstack.config.json` configuration: ```json { @@ -107,7 +107,7 @@ Drop files into the appropriate directory. The tool caches file listings for 10 ### Rotating service tokens 1. Create a new token in Cloudflare Zero Trust -2. Update `defaults.json` with the new `ClientId` / `ClientSecret` +2. Update `rackstack.config.json` with the new `ClientId` / `ClientSecret` 3. Delete the old token ## Setup Guides diff --git a/docs/Runbook-HA-iSCSI.md b/docs/Runbook-HA-iSCSI.md index 6a9261c..459d619 100644 --- a/docs/Runbook-HA-iSCSI.md +++ b/docs/Runbook-HA-iSCSI.md @@ -4,7 +4,7 @@ This runbook covers the end-to-end setup of dual-path iSCSI SAN connectivity wit > **Note:** As of v1.3.0, RackStack supports six storage backends: iSCSI, Fibre Channel, S2D, SMB3, NVMe-oF, and Local. This runbook covers the iSCSI backend specifically. For other backends, see [Storage Backends](Storage-Backends.md). -> **Note:** IP addresses, subnet (default `172.16.1`), and target mappings shown in this guide are defaults. Configure yours in `defaults.json` via `iSCSISubnet` and `SANTargetMappings`. +> **Note:** IP addresses, subnet (default `172.16.1`), and target mappings shown in this guide are defaults. Configure yours in `rackstack.config.json` via `iSCSISubnet` and `SANTargetMappings`. --- @@ -111,7 +111,7 @@ Select `[A] Auto-configure`: 2. **IP calculation:** IPs are computed using the formula: - Port 1 (A-side): `{subnet}.{(host# + 1) * 10 + 1}` - Port 2 (B-side): `{subnet}.{(host# + 1) * 10 + 2}` - - Default subnet: `172.16.1` (configurable via `iSCSISubnet` in `defaults.json`) + - Default subnet: `172.16.1` (configurable via `iSCSISubnet` in `rackstack.config.json`) Examples: | Host | Port 1 (A-side) | Port 2 (B-side) | @@ -497,7 +497,7 @@ The retry pattern places the "opposite" pair second (primary + 2 mod 4), then fi ### Custom SAN Target Pairings (v1.5.0) -The default assignment table above can be fully customized via `SANTargetPairings` in `defaults.json`. This lets you: +The default assignment table above can be fully customized via `SANTargetPairings` in `rackstack.config.json`. This lets you: - Define custom A/B pairs (different number of controller ports, different IP layouts) - Override the host-to-pair assignments and retry order diff --git a/docs/Runbook-VM-Deployment.md b/docs/Runbook-VM-Deployment.md index 06079ab..fd37fc5 100644 --- a/docs/Runbook-VM-Deployment.md +++ b/docs/Runbook-VM-Deployment.md @@ -131,7 +131,7 @@ If you plan to use pre-installed OS images: 1. Enter a VM prefix (e.g., VM, APP, TEST). 2. Configure name, OS method, CPU, memory, disks, and NICs through guided prompts. -3. Custom VMs use defaults from `defaults.json` if configured (`CustomVMDefaults` section), otherwise: 4 vCPU, 8 GB dynamic memory, 100 GB fixed OS disk. +3. Custom VMs use defaults from `rackstack.config.json` if configured (`CustomVMDefaults` section), otherwise: 4 vCPU, 8 GB dynamic memory, 100 GB fixed OS disk. 4. Review and add to queue. ### Editing Queued VMs @@ -206,13 +206,13 @@ RackStack ships with three generic built-in templates: | 2 | File Server (FS) | Windows | 4 | 8 GB Dynamic | 100 GB Fixed | 200 GB Fixed | | | 3 | Web Server (WEB) | Windows | 4 | 8 GB Dynamic | 100 GB Fixed | -- | IIS | -Add custom templates (SQL, APP, etc.) or override built-in specs via `CustomVMTemplates` in `defaults.json`. See the [Configuration Guide](Configuration#custom-vm-templates) for details. +Add custom templates (SQL, APP, etc.) or override built-in specs via `CustomVMTemplates` in `rackstack.config.json`. See the [Configuration Guide](Configuration#custom-vm-templates) for details. --- ## Naming Conventions -VM names follow a configurable token-based pattern (default: `{Site}-{Prefix}{Seq}`). The pattern, site identifier, and detection regex are all configurable via `VMNaming` in `defaults.json`. See the [Configuration Guide](Configuration#vm-naming) for details. +VM names follow a configurable token-based pattern (default: `{Site}-{Prefix}{Seq}`). The pattern, site identifier, and detection regex are all configurable via `VMNaming` in `rackstack.config.json`. See the [Configuration Guide](Configuration#vm-naming) for details. Examples: - `123456-FS1` — numeric site ID, default pattern diff --git a/docs/Server-Role-Templates.md b/docs/Server-Role-Templates.md index b88d0b3..ae1e5aa 100644 --- a/docs/Server-Role-Templates.md +++ b/docs/Server-Role-Templates.md @@ -1,6 +1,6 @@ # Server Role Templates -RackStack includes 10 built-in server role templates for common Windows Server roles, plus support for custom templates defined in `defaults.json`. Templates automate the installation of Windows features and provide post-install guidance where applicable. +RackStack includes 10 built-in server role templates for common Windows Server roles, plus support for custom templates defined in `rackstack.config.json`. Templates automate the installation of Windows features and provide post-install guidance where applicable. > **New in v1.4.0:** Server role templates with built-in and custom template support. @@ -87,7 +87,7 @@ The template selector shows all available templates with their current installat ## Custom Templates -Define custom templates in `defaults.json` under `CustomRoleTemplates`. Custom templates appear in the selector alongside built-in templates. +Define custom templates in `rackstack.config.json` under `CustomRoleTemplates`. Custom templates appear in the selector alongside built-in templates. ```json "CustomRoleTemplates": { @@ -177,7 +177,7 @@ Use the `ServerRoleTemplate` key in your batch config to install a role template In batch mode: - The template key is case-insensitive (converted to uppercase) -- Both built-in and custom templates (from `defaults.json`) are available +- Both built-in and custom templates (from `rackstack.config.json`) are available - Invalid template keys cause a validation warning - Features are installed sequentially via `Install-WindowsFeature` - The reboot flag is set if the template requires it and features were actually installed diff --git a/docs/Storage-Manager.md b/docs/Storage-Manager.md index 947b383..b6a7e93 100644 --- a/docs/Storage-Manager.md +++ b/docs/Storage-Manager.md @@ -91,7 +91,7 @@ All storage paths are dynamically generated based on the selected drive letter. | `VHDCachePath` | `E:\Virtual Machines\_BaseImages` | | `HostISOPath` | `E:\ISOs` | -You can also pre-configure storage paths in `defaults.json`: +You can also pre-configure storage paths in `rackstack.config.json`: ```json "StoragePaths": { @@ -137,7 +137,7 @@ RackStack manages sysprepped VHD templates used for VM deployment. VHDs are cach ### File Server Configuration -VHD downloads require a file server configured in `defaults.json`: +VHD downloads require a file server configured in `rackstack.config.json`: ```json "FileServer": { @@ -188,7 +188,7 @@ These Hyper-V processes are excluded from Defender scanning: ### Static Exclusions -Additional static paths can be configured in `defaults.json`: +Additional static paths can be configured in `rackstack.config.json`: ```json "DefenderExclusionPaths": [ @@ -199,7 +199,7 @@ Additional static paths can be configured in `defaults.json`: ] ``` -If `DefenderCommonVMPaths` is specified in `defaults.json`, those paths are used as-is and dynamic path generation is skipped. +If `DefenderCommonVMPaths` is specified in `rackstack.config.json`, those paths are used as-is and dynamic path generation is skipped. ### Interactive Defender Management diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md index 48da6c5..ad40b11 100644 --- a/docs/Troubleshooting.md +++ b/docs/Troubleshooting.md @@ -108,7 +108,7 @@ Common issues encountered during server configuration with RackStack, organized **Resolution:** 1. Navigate to **iSCSI & SAN Management > [1] Configure iSCSI NICs**. -2. Use auto-configuration (recommended) which detects host number from the hostname (e.g., `123456-HV2` = Host #2) and calculates IPs using the formula: `{subnet}.{(host# + 1) * 10 + port#}` (default subnet: `172.16.1`, configurable via `defaults.json`). +2. Use auto-configuration (recommended) which detects host number from the hostname (e.g., `123456-HV2` = Host #2) and calculates IPs using the formula: `{subnet}.{(host# + 1) * 10 + port#}` (default subnet: `172.16.1`, configurable via `rackstack.config.json`). 3. Use **[2] Identify NICs** to temporarily disable a NIC and watch which switch port light goes out to confirm physical cabling. 4. Use **[3] Discover SAN Targets** to ping-test all known SAN target IPs. @@ -378,7 +378,7 @@ Hosts 5-24 cycle through the same pattern (host 5 = same as host 1, etc.). **Resolution:** 1. Role templates are designed for Windows Server deployments. They cannot be installed on client operating systems. -2. To test template functionality on a client, create a custom template in `defaults.json` with `ServerOnly: false` and client-compatible features. +2. To test template functionality on a client, create a custom template in `rackstack.config.json` with `ServerOnly: false` and client-compatible features. --- diff --git a/docs/fileserver-cloud.md b/docs/fileserver-cloud.md index 3dc6f01..ca6d955 100644 --- a/docs/fileserver-cloud.md +++ b/docs/fileserver-cloud.md @@ -91,7 +91,7 @@ GET https://rackstackfiles.blob.core.windows.net/server-tools?restype=container& RackStack handles the XML parsing natively with `StorageType: "azure"`. -### defaults.json configuration +### rackstack.config.json configuration ```json { @@ -152,7 +152,7 @@ For production, put a CloudFront distribution in front of the S3 bucket: 2. Use Origin Access Control (OAC) to restrict direct S3 access 3. Optionally add signed URLs or signed cookies for authentication -### defaults.json configuration +### rackstack.config.json configuration For S3 with CloudFront, use the `static` storage type with JSON index files: @@ -197,7 +197,7 @@ Re-run this whenever you add or remove files. ## Caching -Cloud API calls may have rate limits or costs. RackStack caches file listings for 10 minutes (`$script:CacheTTLMinutes` in `defaults.json`), which limits API calls. Azure Blob list operations are cheap (~$0.004 per 10,000 requests). +Cloud API calls may have rate limits or costs. RackStack caches file listings for 10 minutes (`$script:CacheTTLMinutes` in `rackstack.config.json`), which limits API calls. Azure Blob list operations are cheap (~$0.004 per 10,000 requests). --- diff --git a/docs/fileserver-debian.md b/docs/fileserver-debian.md index c5fbfb5..75f8e0a 100644 --- a/docs/fileserver-debian.md +++ b/docs/fileserver-debian.md @@ -155,9 +155,9 @@ Verify: `curl https://files.yourdomain.com/health` should return "OK" (if not ye 4. **Duration**: Non-expiring (or set a rotation schedule) 5. Copy the **Client ID** and **Client Secret** -- you won't see the secret again! -## Configure defaults.json +## Configure rackstack.config.json -Add the credentials to your `defaults.json`: +Add the credentials to your `rackstack.config.json`: ```json { diff --git a/docs/fileserver-docker.md b/docs/fileserver-docker.md index 61d78b8..f1f9e96 100644 --- a/docs/fileserver-docker.md +++ b/docs/fileserver-docker.md @@ -199,7 +199,7 @@ docker compose down -v Your files in `data/` are not affected by `docker compose down` since they're bind-mounted, not Docker volumes. -## Configure defaults.json +## Configure rackstack.config.json ```json { diff --git a/docs/fileserver-lan.md b/docs/fileserver-lan.md index 9db38fe..f45f7e8 100644 --- a/docs/fileserver-lan.md +++ b/docs/fileserver-lan.md @@ -10,7 +10,7 @@ See [FileServer-Setup.md](FileServer-Setup.md) for architecture overview and alt RackStack (workstation) --HTTP--> File server (LAN IP) --serves--> ISOs, VHDs, Agents ``` -No internet exposure. No authentication. Just a web server on your LAN serving static files. Point `defaults.json` at the server's LAN IP and you're done. +No internet exposure. No authentication. Just a web server on your LAN serving static files. Point `rackstack.config.json` at the server's LAN IP and you're done. ## Prerequisites @@ -190,7 +190,7 @@ Invoke-RestMethod -Uri "http://localhost/server-tools/ISOs/" --- -## Configure defaults.json +## Configure rackstack.config.json No `ClientId` or `ClientSecret` needed for LAN-only setups: diff --git a/docs/fileserver-rhel.md b/docs/fileserver-rhel.md index b3d8b89..2e96654 100644 --- a/docs/fileserver-rhel.md +++ b/docs/fileserver-rhel.md @@ -237,7 +237,7 @@ Verify: `curl https://files.yourdomain.com/health` should return "OK" (if not ye 4. **Duration**: Non-expiring (or set a rotation schedule) 5. Copy the **Client ID** and **Client Secret** -- you won't see the secret again! -## Configure defaults.json +## Configure rackstack.config.json ```json { diff --git a/docs/fileserver-tailscale.md b/docs/fileserver-tailscale.md index 7fe58b0..b0c6135 100644 --- a/docs/fileserver-tailscale.md +++ b/docs/fileserver-tailscale.md @@ -174,7 +174,7 @@ ping 100.64.0.5 ping fileserver.tail1234.ts.net ``` -## Configure defaults.json +## Configure rackstack.config.json Use the Tailscale IP or MagicDNS hostname. No auth headers needed -- Tailscale handles identity and encryption: @@ -262,7 +262,7 @@ AllowedIPs = 10.0.0.0/24 PersistentKeepalive = 25 ``` -Then point `defaults.json` at the WireGuard IP: +Then point `rackstack.config.json` at the WireGuard IP: ```json { diff --git a/docs/fileserver-windows.md b/docs/fileserver-windows.md index 88886ab..39c9e14 100644 --- a/docs/fileserver-windows.md +++ b/docs/fileserver-windows.md @@ -227,7 +227,7 @@ New-IISSiteBinding -Name "FileServer" -BindingInformation "*:443:" -Protocol htt > **Important:** With direct HTTPS (no Cloudflare), you won't have Cloudflare Access service token auth. You'll need another auth mechanism or restrict access by IP/VPN. For the simplest setup, combine this with [Tailscale](fileserver-tailscale.md). -## Configure defaults.json +## Configure rackstack.config.json ```json { diff --git a/defaults.example.json b/rackstack.config.example.json similarity index 97% rename from defaults.example.json rename to rackstack.config.example.json index 1701994..b32c742 100644 --- a/defaults.example.json +++ b/rackstack.config.example.json @@ -1,6 +1,7 @@ { "_description": "RackStack Configuration File", - "_setup": "Copy this file to 'defaults.json' (same folder as RackStack) and fill in YOUR values. RackStack will never overwrite your defaults.json during updates. Lines starting with _ are comments -- RackStack ignores them.", + "_setup": "Copy this file to 'rackstack.config.json' (same folder as RackStack) and fill in YOUR values. RackStack will never overwrite your rackstack.config.json during updates. A legacy 'defaults.json' is still read when no rackstack.config.json exists. Lines starting with _ are comments -- RackStack ignores them.", + "_companyConfig_help": "Optional per-company overrides live in '.rackstack.config.json' next to this file (e.g. contoso.rackstack.config.json; the legacy '.defaults.json' name is still read as a fallback). Multiple company files can coexist; the _companyDefaults key in the base config selects which one loads.", "_section_basic": "========== BASIC SERVER SETTINGS ==========", @@ -90,7 +91,7 @@ "ServicePrincipalId": "", "_ServicePrincipalId_help": "Application (client) ID of a service principal granted the 'Azure Connected Machine Onboarding' role on the resource group.", "ServicePrincipalSecret": "", - "_ServicePrincipalSecret_help": "SP client secret. STRONGLY PREFER leaving this blank here and setting the RACKSTACK_ARC_SECRET environment variable at runtime instead, so the secret never lands in a config file. defaults.json is gitignored, but env-var injection is still the safer pattern.", + "_ServicePrincipalSecret_help": "SP client secret. STRONGLY PREFER leaving this blank here and setting the RACKSTACK_ARC_SECRET environment variable at runtime instead, so the secret never lands in a config file. rackstack.config.json is gitignored, but env-var injection is still the safer pattern.", "Cloud": "AzureCloud", "_Cloud_help": "Azure cloud instance: 'AzureCloud' (default, commercial), 'AzureUSGovernment', or 'AzureChinaCloud'.", "AgentInstallerUrl": "https://aka.ms/AzureConnectedMachineAgent", diff --git a/sync-to-monolithic.ps1 b/sync-to-monolithic.ps1 index 4aa5225..43c2609 100644 --- a/sync-to-monolithic.ps1 +++ b/sync-to-monolithic.ps1 @@ -1,12 +1,14 @@ -param( +param( [switch]$DryRun ) $modulesDir = Join-Path $PSScriptRoot "Modules" $utf8Bom = New-Object System.Text.UTF8Encoding $true -# Derive monolithic filename from defaults.json (or fall back to reading version from 00-Initialization.ps1) -$defaultsJsonPath = Join-Path $PSScriptRoot "defaults.json" +# Derive monolithic filename from the base config (rackstack.config.json, legacy +# defaults.json fallback), or fall back to reading version from 00-Initialization.ps1 +$defaultsJsonPath = Join-Path $PSScriptRoot "rackstack.config.json" +if (-not (Test-Path $defaultsJsonPath)) { $defaultsJsonPath = Join-Path $PSScriptRoot "defaults.json" } $toolFullName = "RackStack" $scriptVersion = "1.0.0" if (Test-Path $defaultsJsonPath) {