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 @@
-
+
@@ -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) {