From 271ce3909708fbfae42a56807061d387e1b552f2 Mon Sep 17 00:00:00 2001
From: TheAbider <51920546+TheAbider@users.noreply.github.com>
Date: Thu, 16 Jul 2026 22:01:10 -0700
Subject: [PATCH] v1.122.0: namespaced config file naming with legacy fallback
The base config is now rackstack.config.json and company overrides are
.rackstack.config.json, so the files stay unambiguous when the
EXE or monolithic script runs from a shared folder. Fully backward
compatible: legacy defaults.json and .defaults.json are still
read whenever the new-named file is absent (per file), and the tool
keeps saving to whichever file it loaded.
- Get-RackStackConfigPath resolves the base config once at init
(prefer new name, fall back to legacy, create new name when neither
exists); the first-run wizard and defaults editor write through the
resolved path
- Get-CompanyDefaultsFiles discovers company files under both naming
schemes, deduped per company key with the new name winning; also
fixes a latent bug where the returned array nested inside callers'
@() wrappers and collapsed multiple companies into one entry
- Save picker, menus, and prompts show the actually-loaded filename
- Exit cleanup removes config files under both schemes plus company
files and crash-leftover .tmp siblings; the company-file wildcard
sweeps are gated to directories identifiable as the tool's own home
so shared folders are never glob-swept
- Example template renamed to rackstack.config.example.json and the
release asset list updated to match
- .gitignore covers both schemes; the tracked example stays tracked
- Docs, wiki-facing guides, issue templates, and the config guide
updated; company config files now documented with the fallback rules
- New test section covers resolution order, dual-name company
discovery, cleanup gating, .gitignore, and release-asset pins
---
.github/ISSUE_TEMPLATE/bug_report.yml | 4 +-
.github/ISSUE_TEMPLATE/feature_request.yml | 2 +-
.github/workflows/ci.yml | 4 +-
.gitignore | 4 +
CONTRIBUTING.md | 6 +-
Changelog.md | 13 +
Header.ps1 | 4 +-
Modules/00-Initialization.ps1 | 70 +++--
Modules/02-Logging.ps1 | 2 +-
Modules/05-SystemCheck.ps1 | 2 +-
Modules/09-SET.ps1 | 6 +-
Modules/17-DefenderExclusions.ps1 | 4 +-
Modules/23-LocalAdmin.ps1 | 2 +-
Modules/30-ServiceManager.ps1 | 2 +-
Modules/34-Help.ps1 | 12 +-
Modules/36-BatchConfig.ps1 | 2 +-
Modules/39-FileServer.ps1 | 6 +-
Modules/41-VHDManagement.ps1 | 2 +-
Modules/44-VMDeployment.ps1 | 4 +-
Modules/47-ExitCleanup.ps1 | 46 ++-
Modules/50-EntryPoint.ps1 | 30 +-
Modules/56-OperationsMenu.ps1 | 84 ++++--
Modules/57-AgentInstaller.ps1 | 12 +-
Modules/60-ServerRoleTemplates.ps1 | 6 +-
Modules/65-AzureArc.ps1 | 12 +-
Modules/66-DefenderEndpoint.ps1 | 8 +-
Modules/67-WSUS.ps1 | 12 +-
Modules/68-ADCS.ps1 | 10 +-
Modules/69-StorageMigration.ps1 | 2 +-
README.md | 40 +--
ROADMAP.md | 6 +-
RackStack.ps1 | 4 +-
RackStack.psd1 | 2 +-
SECURITY.md | 17 +-
Tests/Pester/Logging.Tests.ps1 | 2 +-
Tests/Run-Tests.ps1 | 283 +++++++++++++-----
dist/scoop/rackstack.json | 2 +-
docs/ASSURANCE_CASE.md | 10 +-
docs/Cluster-Management.md | 2 +-
docs/Configuration.md | 16 +-
docs/FileServer-Setup.md | 8 +-
docs/Runbook-HA-iSCSI.md | 6 +-
docs/Runbook-VM-Deployment.md | 6 +-
docs/Server-Role-Templates.md | 6 +-
docs/Storage-Manager.md | 8 +-
docs/Troubleshooting.md | 4 +-
docs/fileserver-cloud.md | 6 +-
docs/fileserver-debian.md | 4 +-
docs/fileserver-docker.md | 2 +-
docs/fileserver-lan.md | 4 +-
docs/fileserver-rhel.md | 2 +-
docs/fileserver-tailscale.md | 4 +-
docs/fileserver-windows.md | 2 +-
...mple.json => rackstack.config.example.json | 5 +-
sync-to-monolithic.ps1 | 8 +-
55 files changed, 532 insertions(+), 300 deletions(-)
rename defaults.example.json => rackstack.config.example.json (97%)
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) {