From c147c0b1f8ace9e17d15d1bb978dcc813e230cb2 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 11 May 2026 14:16:02 -0400 Subject: [PATCH 1/2] feature: Dell OEM scripts in new modular-source shape Seeds src/oem-dell/ and src/oem-shared/lib/ with four Dell leaves and three lib files, all using the # %INCLUDE marker pattern from the foundation PR (#75). RMM-driven configuration, internet check at the DCU install step only, no static .cctk policy file. Leaves: - dell-configure.ps1 ... BIOS config from $env:BIOS_* via translation table. No static policy file. - dell-command-update-install.ps1 ... idempotent DCU install with internet check at the top. - dell-command-update-run.ps1 ... DCU /scan and /applyUpdates with documented exit-code mapping. - dell-debloat.ps1 ... removes Dell consumer software, keeps Command Update and Command Configure. Libs: - oem-shared/lib/oem-manufacturer-detect.ps1 - oem-dell/lib/dell-detection.ps1 - oem-dell/lib/dell-bios-translation.ps1 (canonical BIOS settings -> cctk syntax map) Knowledge preserved from closed PR #69: DCU CLI path, exit-code mapping, debloat package list, cctk paths, AppX patterns. Dropped: runtime lib-bootstrap, SHA-256 hash placeholders, $env:LibTag, Invoke-DellLeaf orchestrator, dell-baseline name, static .cctk URL. --- src/oem-dell/dell-command-update-install.ps1 | 135 ++++++++++++ src/oem-dell/dell-command-update-run.ps1 | 116 +++++++++++ src/oem-dell/dell-configure.ps1 | 193 ++++++++++++++++++ src/oem-dell/dell-debloat.ps1 | 178 ++++++++++++++++ src/oem-dell/lib/dell-bios-translation.ps1 | 81 ++++++++ src/oem-dell/lib/dell-detection.ps1 | 43 ++++ .../lib/oem-manufacturer-detect.ps1 | 21 ++ 7 files changed, 767 insertions(+) create mode 100644 src/oem-dell/dell-command-update-install.ps1 create mode 100644 src/oem-dell/dell-command-update-run.ps1 create mode 100644 src/oem-dell/dell-configure.ps1 create mode 100644 src/oem-dell/dell-debloat.ps1 create mode 100644 src/oem-dell/lib/dell-bios-translation.ps1 create mode 100644 src/oem-dell/lib/dell-detection.ps1 create mode 100644 src/oem-shared/lib/oem-manufacturer-detect.ps1 diff --git a/src/oem-dell/dell-command-update-install.ps1 b/src/oem-dell/dell-command-update-install.ps1 new file mode 100644 index 0000000..517552a --- /dev/null +++ b/src/oem-dell/dell-command-update-install.ps1 @@ -0,0 +1,135 @@ +## PLEASE COMMENT YOUR VARIABLES DIRECTLY BELOW HERE IF YOU'RE RUNNING FROM A RMM +## NinjaRMM passes script preset variables as environment variables, so each is read via $env: in this script. +## $env:RMM - Set to "1" by NinjaRMM to indicate RMM (non-interactive) mode +## $env:Description - Ticket # or initials for audit trail +## $env:RMMScriptPath - Optional log directory base provided by the RMM +## +## Per-script variables: +## $env:DCUTargetVersion - Minimum acceptable DCU version (default: "5.7.0") +## $env:DCUInstallerURL - Override the Dell-hosted installer URL (see TODO below) + +# %INCLUDE src/oem-shared/lib/oem-manufacturer-detect.ps1 +# %INCLUDE src/oem-dell/lib/dell-detection.ps1 + +$ScriptLogName = "dell-command-update-install.log" + +# --- Default optional RMM environment variables -------------------------- + +if ([string]::IsNullOrEmpty($env:DCUTargetVersion)) { $env:DCUTargetVersion = "5.7.0" } + +# TODO: Confirm the canonical Dell-hosted EXE URL for DCU 5.7 Classic from +# https://www.dell.com/support/kbdoc/en-us/000177325/dell-command-update and +# pin it here. The placeholder below points at Dell's published download path +# pattern but the specific folder ID needs to be captured from a fresh +# manual download against the published KB so we know we're tracking the +# Dell-supported build. +if ([string]::IsNullOrEmpty($env:DCUInstallerURL)) { + $env:DCUInstallerURL = "https://dl.dell.com/FOLDER/Dell-Command-Update-Application_WIN_5.7.0_A00.EXE" +} + +# --- Input handling: RMM vs interactive ---------------------------------- + +if ($env:RMM -ne "1") { + $ValidInput = 0 + while ($ValidInput -ne 1) { + $env:Description = Read-Host "Please enter the ticket # and/or your initials for audit trail" + if ($env:Description) { + $ValidInput = 1 + } else { + Write-Host "Invalid input. Please try again." + } + } + $LogPath = "$env:WINDIR\logs\$ScriptLogName" +} else { + if (-not [string]::IsNullOrEmpty($env:RMMScriptPath)) { + $LogPath = "$env:RMMScriptPath\logs\$ScriptLogName" + } else { + $LogPath = "$env:WINDIR\logs\$ScriptLogName" + } + if ([string]::IsNullOrEmpty($env:Description)) { + Write-Host "Description is null. This was most likely run automatically from the RMM and no information was passed." + $env:Description = "Dell Command Update Install" + } +} + +$logDir = Split-Path -Path $LogPath -Parent +if (-not (Test-Path -Path $logDir)) { + New-Item -Path $logDir -ItemType Directory -Force | Out-Null +} + +# --- Script logic -------------------------------------------------------- + +$TranscriptStarted = $false +try { + Start-Transcript -Path $LogPath -ErrorAction Stop + $TranscriptStarted = $true +} catch { + Write-Host "Warning: Could not start transcript logging to $LogPath - $($_.Exception.Message)" +} + +Write-Host "Description: $env:Description" +Write-Host "Log path: $LogPath" +Write-Host "RMM: $env:RMM" + +if (-not (Test-DellHardware)) { + Write-Host "Not a Dell endpoint. Exiting." + if ($TranscriptStarted) { Stop-Transcript } + exit 0 +} + +# Internet check ... only the install step needs vendor download connectivity. +# dell-command-update-run / dell-configure / dell-debloat do NOT include this +# because they operate on already-installed tooling and should still work on +# an endpoint that's offline at the moment. +function Test-InternetAvailable { + try { + $resp = Invoke-WebRequest -Uri 'https://www.msftconnecttest.com/connecttest.txt' ` + -UseBasicParsing -TimeoutSec 5 -ErrorAction Stop + return ($resp.StatusCode -eq 200) + } catch { + return $false + } +} + +if (-not (Test-InternetAvailable)) { + Write-Host "No internet connectivity detected ... skipping DCU install. If DCU is already installed, dell-command-update-run will still work. Exit 0." + if ($TranscriptStarted) { Stop-Transcript } + exit 0 +} + +$targetVersion = [version]$env:DCUTargetVersion +$installedVersion = Get-DCUVersion + +if ($null -ne $installedVersion -and $installedVersion -ge $targetVersion) { + Write-Host "DCU $installedVersion already meets target $targetVersion. Skipping install." + if ($TranscriptStarted) { Stop-Transcript } + exit 0 +} + +Write-Host "DCU installed: $installedVersion. Target: $targetVersion. Installing." + +$installerPath = Join-Path $env:TEMP "dell-command-update-installer.exe" +try { + Invoke-WebRequest -Uri $env:DCUInstallerURL -OutFile $installerPath -UseBasicParsing -ErrorAction Stop +} catch { + Write-Host "Failed to download DCU installer from $env:DCUInstallerURL : $_" + if ($TranscriptStarted) { Stop-Transcript } + exit 1 +} + +Write-Host "Running silent install: $installerPath /s" +$proc = Start-Process -FilePath $installerPath -ArgumentList "/s" -Wait -PassThru -NoNewWindow +Write-Host "Installer exit code: $($proc.ExitCode)" + +Remove-Item -Path $installerPath -Force -ErrorAction SilentlyContinue + +$postVersion = Get-DCUVersion +if ($null -ne $postVersion -and $postVersion -ge $targetVersion) { + Write-Host "DCU $postVersion installed successfully." + if ($TranscriptStarted) { Stop-Transcript } + exit 0 +} else { + Write-Host "DCU install verification failed. Installed version: $postVersion" + if ($TranscriptStarted) { Stop-Transcript } + exit 1 +} diff --git a/src/oem-dell/dell-command-update-run.ps1 b/src/oem-dell/dell-command-update-run.ps1 new file mode 100644 index 0000000..c81c0b7 --- /dev/null +++ b/src/oem-dell/dell-command-update-run.ps1 @@ -0,0 +1,116 @@ +## PLEASE COMMENT YOUR VARIABLES DIRECTLY BELOW HERE IF YOU'RE RUNNING FROM A RMM +## NinjaRMM passes script preset variables as environment variables, so each is read via $env: in this script. +## $env:RMM - Set to "1" by NinjaRMM to indicate RMM (non-interactive) mode +## $env:Description - Ticket # or initials for audit trail +## $env:RMMScriptPath - Optional log directory base provided by the RMM +## +## Per-script variables: +## $env:DCUScanOnly - "1" to scan but not apply updates (default: "0") + +# %INCLUDE src/oem-shared/lib/oem-manufacturer-detect.ps1 +# %INCLUDE src/oem-dell/lib/dell-detection.ps1 + +$ScriptLogName = "dell-command-update-run.log" + +# --- Default optional RMM environment variables -------------------------- + +if ([string]::IsNullOrEmpty($env:DCUScanOnly)) { $env:DCUScanOnly = "0" } + +# --- Input handling: RMM vs interactive ---------------------------------- + +if ($env:RMM -ne "1") { + $ValidInput = 0 + while ($ValidInput -ne 1) { + $env:Description = Read-Host "Please enter the ticket # and/or your initials for audit trail" + if ($env:Description) { + $ValidInput = 1 + } else { + Write-Host "Invalid input. Please try again." + } + } + $LogPath = "$env:WINDIR\logs\$ScriptLogName" +} else { + if (-not [string]::IsNullOrEmpty($env:RMMScriptPath)) { + $LogPath = "$env:RMMScriptPath\logs\$ScriptLogName" + } else { + $LogPath = "$env:WINDIR\logs\$ScriptLogName" + } + if ([string]::IsNullOrEmpty($env:Description)) { + Write-Host "Description is null. This was most likely run automatically from the RMM and no information was passed." + $env:Description = "Dell Command Update Run" + } +} + +$logDir = Split-Path -Path $LogPath -Parent +if (-not (Test-Path -Path $logDir)) { + New-Item -Path $logDir -ItemType Directory -Force | Out-Null +} + +# --- Script logic -------------------------------------------------------- + +$TranscriptStarted = $false +try { + Start-Transcript -Path $LogPath -ErrorAction Stop + $TranscriptStarted = $true +} catch { + Write-Host "Warning: Could not start transcript logging to $LogPath - $($_.Exception.Message)" +} + +Write-Host "Description: $env:Description" +Write-Host "Log path: $LogPath" +Write-Host "RMM: $env:RMM" + +if (-not (Test-DellHardware)) { + Write-Host "Not a Dell endpoint. Exiting." + if ($TranscriptStarted) { Stop-Transcript } + exit 0 +} + +if (-not (Test-DCUInstalled)) { + Write-Host "Dell Command Update is not installed. Run dell-command-update-install.ps1 first." + if ($TranscriptStarted) { Stop-Transcript } + exit 2 +} + +$dcu = Get-DCUCliPath +Write-Host "DCU CLI: $dcu (version $(Get-DCUVersion))" + +# Reference: https://www.dell.com/support/manuals/en-us/command-update/dcu_rg/dell-command-update-cli-commands +# Exit codes for /applyUpdates: +# 0 = success, 1 = reboot required, 2 = unknown error, 3 = not a Dell system, +# 4 = not admin, 5 = reboot pending, 6 = another instance running, +# 7 = unsupported model, 8 = no update filters configured. + +Write-Host "`n--- dcu-cli /scan ---" +$scan = Start-Process -FilePath $dcu -ArgumentList "/scan" -Wait -PassThru -NoNewWindow +$scanExit = $scan.ExitCode +Write-Host "Scan exit code: $scanExit" + +if ($env:DCUScanOnly -eq "1") { + Write-Host "DCUScanOnly is set. Skipping applyUpdates." + if ($TranscriptStarted) { Stop-Transcript } + exit $scanExit +} + +Write-Host "`n--- dcu-cli /applyUpdates ---" +$apply = Start-Process -FilePath $dcu -ArgumentList "/applyUpdates -autoSuspendBitLocker=enable -reboot=disable" -Wait -PassThru -NoNewWindow +$applyExit = $apply.ExitCode +Write-Host "Apply exit code: $applyExit" + +# TODO: Capture any additional exit codes observed during real-endpoint testing +# (DCU has occasionally emitted undocumented codes on edge-case hardware). +switch ($applyExit) { + 0 { Write-Host "Updates applied successfully." } + 1 { Write-Host "Updates applied; reboot required to complete." } + 2 { Write-Host "Unknown application error." } + 3 { Write-Host "Not a Dell system. (Should not reach here ... Test-DellHardware passed.)" } + 4 { Write-Host "DCU CLI was not launched with administrative privilege." } + 5 { Write-Host "A reboot was pending from a previous operation." } + 6 { Write-Host "Another instance of DCU is already running." } + 7 { Write-Host "DCU does not support the current system model." } + 8 { Write-Host "No update filters have been applied or configured." } + default { Write-Host "Unrecognized exit code: $applyExit" } +} + +if ($TranscriptStarted) { Stop-Transcript } +exit $applyExit diff --git a/src/oem-dell/dell-configure.ps1 b/src/oem-dell/dell-configure.ps1 new file mode 100644 index 0000000..080669a --- /dev/null +++ b/src/oem-dell/dell-configure.ps1 @@ -0,0 +1,193 @@ +## PLEASE COMMENT YOUR VARIABLES DIRECTLY BELOW HERE IF YOU'RE RUNNING FROM A RMM +## NinjaRMM passes script preset variables as environment variables, so each is read via $env: in this script. +## $env:RMM - Set to "1" by NinjaRMM to indicate RMM (non-interactive) mode +## $env:Description - Ticket # or initials for audit trail +## $env:RMMScriptPath - Optional log directory base provided by the RMM +## +## BIOS settings (operator sets the ones they want to apply, leaves others blank): +## $env:BIOS_AdminPassword - Current BIOS admin password. Required to apply settings on most platforms. Empty = none set. +## $env:BIOS_AdminPasswordNew - New BIOS admin password to set. Empty = don't change. +## $env:BIOS_TPMEnabled - "Enabled" / "Disabled" +## $env:BIOS_TPMActivation - "Activated" / "Deactivated" +## $env:BIOS_SecureBoot - "Enabled" / "Disabled" +## $env:BIOS_VirtualizationCPU - "Enabled" / "Disabled" +## $env:BIOS_VirtualizationIOMMU - "Enabled" / "Disabled" +## $env:BIOS_WakeOnLAN - "Enabled" / "Disabled" / "LANOnly" / "LANWLAN" +## $env:BIOS_BootMode - "UEFI" / "Legacy" + +# %INCLUDE src/oem-shared/lib/oem-manufacturer-detect.ps1 +# %INCLUDE src/oem-dell/lib/dell-detection.ps1 +# %INCLUDE src/oem-dell/lib/dell-bios-translation.ps1 + +$ScriptLogName = "dell-configure.log" + +# --- Input handling: RMM vs interactive ---------------------------------- + +if ($env:RMM -ne "1") { + $ValidInput = 0 + while ($ValidInput -ne 1) { + $env:Description = Read-Host "Please enter the ticket # and/or your initials for audit trail" + if ($env:Description) { + $ValidInput = 1 + } else { + Write-Host "Invalid input. Please try again." + } + } + $LogPath = "$env:WINDIR\logs\$ScriptLogName" +} else { + if (-not [string]::IsNullOrEmpty($env:RMMScriptPath)) { + $LogPath = "$env:RMMScriptPath\logs\$ScriptLogName" + } else { + $LogPath = "$env:WINDIR\logs\$ScriptLogName" + } + if ([string]::IsNullOrEmpty($env:Description)) { + Write-Host "Description is null. This was most likely run automatically from the RMM and no information was passed." + $env:Description = "Dell Configure" + } +} + +$logDir = Split-Path -Path $LogPath -Parent +if (-not (Test-Path -Path $logDir)) { + New-Item -Path $logDir -ItemType Directory -Force | Out-Null +} + +# --- Script logic -------------------------------------------------------- + +$TranscriptStarted = $false +try { + Start-Transcript -Path $LogPath -ErrorAction Stop + $TranscriptStarted = $true +} catch { + Write-Host "Warning: Could not start transcript logging to $LogPath - $($_.Exception.Message)" +} + +Write-Host "Description: $env:Description" +Write-Host "Log path: $LogPath" +Write-Host "RMM: $env:RMM" + +if (-not (Test-DellHardware)) { + Write-Host "Not a Dell endpoint. Exiting." + if ($TranscriptStarted) { Stop-Transcript } + exit 0 +} + +Write-Host "Dell model: $(Get-DellInstalledModel)" + +# cctk.exe is installed by Dell Command Configure. Dell ships both 64-bit and 32-bit binaries. +$cctkCandidates = @( + "$env:ProgramFiles\Dell\Command Configure\X86_64\cctk.exe", + "${env:ProgramFiles(x86)}\Dell\Command Configure\X86\cctk.exe" +) +$cctk = $null +foreach ($candidate in $cctkCandidates) { + if (Test-Path -Path $candidate) { + $cctk = $candidate + break + } +} + +if (-not $cctk) { + Write-Host "cctk.exe not found. Dell Command Configure must be installed before this script can run." + Write-Host "Checked: $($cctkCandidates -join '; ')" + if ($TranscriptStarted) { Stop-Transcript } + exit 2 +} + +Write-Host "cctk path: $cctk" + +# Build per-setting cctk invocations from $env:BIOS_* vars via the translation map. +# Each setting is applied as its own cctk call so a single bad value doesn't fail +# the rest. Password flags are appended to every call when present. + +$passwordArgs = @() +if (-not [string]::IsNullOrEmpty($env:BIOS_AdminPassword)) { + $passwordArgs += "--valsetuppwd=$env:BIOS_AdminPassword" +} + +$applied = 0 +$skipped = 0 +$failed = 0 + +foreach ($canonical in @( + 'BIOS_TPMEnabled', + 'BIOS_TPMActivation', + 'BIOS_SecureBoot', + 'BIOS_VirtualizationCPU', + 'BIOS_VirtualizationIOMMU', + 'BIOS_WakeOnLAN', + 'BIOS_BootMode' +)) { + $value = (Get-Item -Path "Env:$canonical" -ErrorAction SilentlyContinue).Value + if ([string]::IsNullOrEmpty($value)) { continue } + + $desc = Get-DellBiosSetting -CanonicalName $canonical + if (-not $desc) { + Write-Host " $canonical=$value ... no Dell translation for this setting; skipping." + $skipped++ + continue + } + + if (-not $desc.Values.ContainsKey($value)) { + Write-Host " $canonical=$value ... value not supported by Dell (allowed: $($desc.Values.Keys -join ', ')); skipping." + $skipped++ + continue + } + + $native = $desc.Native + $nativeVal = $desc.Values[$value] + + switch ($desc.Form) { + 'KeyValue' { + $cctkArgs = @("--$native=$nativeVal") + $passwordArgs + } + 'Subcommand' { + $cctkArgs = @($native, "--$($desc.Param)=$nativeVal") + $passwordArgs + } + default { + Write-Host " $canonical ... unknown form '$($desc.Form)' in translation map; skipping." + $skipped++ + continue + } + } + + Write-Host " $canonical=$value -> cctk $($cctkArgs -join ' ')" + try { + $proc = Start-Process -FilePath $cctk -ArgumentList $cctkArgs -Wait -PassThru -NoNewWindow + if ($proc.ExitCode -eq 0) { + $applied++ + } else { + # TODO: Capture cctk exit codes observed during real-endpoint testing + # so we can map them to friendlier messages (e.g. 113 = password required). + Write-Host " cctk exit code: $($proc.ExitCode)" + $failed++ + } + } catch { + Write-Host " cctk invocation threw: $_" + $failed++ + } +} + +# Apply admin password change last so subsequent runs use the new password. +if (-not [string]::IsNullOrEmpty($env:BIOS_AdminPasswordNew)) { + Write-Host " Setting new BIOS admin password ..." + $pwdArgs = @("--setuppwd=$env:BIOS_AdminPasswordNew") + $passwordArgs + try { + $proc = Start-Process -FilePath $cctk -ArgumentList $pwdArgs -Wait -PassThru -NoNewWindow + if ($proc.ExitCode -eq 0) { + Write-Host " Admin password updated." + $applied++ + } else { + Write-Host " cctk setuppwd exit code: $($proc.ExitCode)" + $failed++ + } + } catch { + Write-Host " cctk setuppwd threw: $_" + $failed++ + } +} + +Write-Host "`nSummary: applied=$applied skipped=$skipped failed=$failed" + +if ($TranscriptStarted) { Stop-Transcript } + +if ($failed -gt 0) { exit 1 } else { exit 0 } diff --git a/src/oem-dell/dell-debloat.ps1 b/src/oem-dell/dell-debloat.ps1 new file mode 100644 index 0000000..b3be0c5 --- /dev/null +++ b/src/oem-dell/dell-debloat.ps1 @@ -0,0 +1,178 @@ +## PLEASE COMMENT YOUR VARIABLES DIRECTLY BELOW HERE IF YOU'RE RUNNING FROM A RMM +## NinjaRMM passes script preset variables as environment variables, so each is read via $env: in this script. +## $env:RMM - Set to "1" by NinjaRMM to indicate RMM (non-interactive) mode +## $env:Description - Ticket # or initials for audit trail +## $env:RMMScriptPath - Optional log directory base provided by the RMM + +# %INCLUDE src/oem-shared/lib/oem-manufacturer-detect.ps1 + +$ScriptLogName = "dell-debloat.log" + +# --- Input handling: RMM vs interactive ---------------------------------- + +if ($env:RMM -ne "1") { + $ValidInput = 0 + while ($ValidInput -ne 1) { + $env:Description = Read-Host "Please enter the ticket # and/or your initials for audit trail" + if ($env:Description) { + $ValidInput = 1 + } else { + Write-Host "Invalid input. Please try again." + } + } + $LogPath = "$env:WINDIR\logs\$ScriptLogName" +} else { + if (-not [string]::IsNullOrEmpty($env:RMMScriptPath)) { + $LogPath = "$env:RMMScriptPath\logs\$ScriptLogName" + } else { + $LogPath = "$env:WINDIR\logs\$ScriptLogName" + } + if ([string]::IsNullOrEmpty($env:Description)) { + Write-Host "Description is null. This was most likely run automatically from the RMM and no information was passed." + $env:Description = "Dell Debloat" + } +} + +$logDir = Split-Path -Path $LogPath -Parent +if (-not (Test-Path -Path $logDir)) { + New-Item -Path $logDir -ItemType Directory -Force | Out-Null +} + +# --- Script logic -------------------------------------------------------- + +$TranscriptStarted = $false +try { + Start-Transcript -Path $LogPath -ErrorAction Stop + $TranscriptStarted = $true +} catch { + Write-Host "Warning: Could not start transcript logging to $LogPath - $($_.Exception.Message)" +} + +Write-Host "Description: $env:Description" +Write-Host "Log path: $LogPath" +Write-Host "RMM: $env:RMM" + +if ((Get-OEMManufacturer) -ne "Dell") { + Write-Host "Not a Dell endpoint. Exiting." + if ($TranscriptStarted) { Stop-Transcript } + exit 0 +} + +# Dell consumer software to remove. Dell Command Update and Dell Command Configure +# are intentionally NOT in this list ... they are the vendor lifecycle tools the +# Dell configure / DCU scripts deploy. +$dellBloatPatterns = @( + "SupportAssist", + "Dell SupportAssist", + "Dell Update", + "Dell Mobile Connect", + "Dell Digital Delivery", + "Dell Customer Connect", + "Dell Help & Support", + "MyDell" +) + +# Patterns to keep even when they match a bloat pattern by substring. +$keepPatterns = @( + "Dell Command \| Update", + "Dell Command Update", + "Dell Command \| Configure", + "Dell Command Configure" +) + +function Test-ShouldKeep { + param([string] $Name) + foreach ($keep in $keepPatterns) { + if ($Name -match $keep) { return $true } + } + return $false +} + +# Survey both 64-bit and 32-bit uninstall hives. +$uninstallPaths = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" +) + +$removedCount = 0 +$failedCount = 0 + +foreach ($pattern in $dellBloatPatterns) { + $matched = foreach ($path in $uninstallPaths) { + Get-ItemProperty -Path $path -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName -and ($_.DisplayName -match [regex]::Escape($pattern)) } + } + foreach ($app in $matched) { + if (Test-ShouldKeep -Name $app.DisplayName) { + Write-Host "Keeping: $($app.DisplayName)" + continue + } + Write-Host "Removing: $($app.DisplayName) [$($app.DisplayVersion)]" + $uninstallString = $app.UninstallString + if ([string]::IsNullOrEmpty($uninstallString)) { + Write-Host " No uninstall string; skipping." + continue + } + try { + if ($uninstallString -match "msiexec") { + if ($app.PSChildName -match "^\{[0-9A-Fa-f-]+\}$") { + $proc = Start-Process -FilePath "msiexec.exe" -ArgumentList "/x $($app.PSChildName) /qn /norestart" -Wait -PassThru -NoNewWindow + if ($proc.ExitCode -eq 0) { $removedCount++ } else { $failedCount++; Write-Host " msiexec exit: $($proc.ExitCode)" } + } else { + Write-Host " Unknown product code shape: $($app.PSChildName)" + $failedCount++ + } + } else { + $cmd = $uninstallString + if ($cmd -notmatch "/S" -and $cmd -notmatch "/silent" -and $cmd -notmatch "/quiet") { + $cmd = "$cmd /S" + } + $proc = Start-Process -FilePath "cmd.exe" -ArgumentList "/c $cmd" -Wait -PassThru -NoNewWindow + if ($proc.ExitCode -eq 0) { $removedCount++ } else { $failedCount++; Write-Host " uninstaller exit: $($proc.ExitCode)" } + } + } catch { + Write-Host " Removal threw: $_" + $failedCount++ + } + } +} + +# Provisioned UWP / AppX consumer bundles ship preinstalled on Dell consumer SKUs. +$appxPatterns = @( + "*DellInc.DellMobileConnect*", + "*DellInc.DellDigitalDelivery*", + "*DellInc.MyDell*", + "*DellInc.DellCustomerConnect*" +) + +foreach ($pattern in $appxPatterns) { + $pkgs = Get-AppxPackage -AllUsers -Name $pattern -ErrorAction SilentlyContinue + foreach ($pkg in $pkgs) { + Write-Host "Removing AppX: $($pkg.Name)" + try { + Remove-AppxPackage -Package $pkg.PackageFullName -AllUsers -ErrorAction Stop + $removedCount++ + } catch { + Write-Host " Remove-AppxPackage failed: $_" + $failedCount++ + } + } + $provisioned = Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName -like $pattern.Trim('*') -or $_.PackageName -like $pattern } + foreach ($prov in $provisioned) { + Write-Host "Removing provisioned AppX: $($prov.DisplayName)" + try { + Remove-AppxProvisionedPackage -Online -PackageName $prov.PackageName -ErrorAction Stop | Out-Null + $removedCount++ + } catch { + Write-Host " Remove-AppxProvisionedPackage failed: $_" + $failedCount++ + } + } +} + +Write-Host "`nSummary: removed=$removedCount failed=$failedCount" + +if ($TranscriptStarted) { Stop-Transcript } + +if ($failedCount -gt 0) { exit 1 } else { exit 0 } diff --git a/src/oem-dell/lib/dell-bios-translation.ps1 b/src/oem-dell/lib/dell-bios-translation.ps1 new file mode 100644 index 0000000..b0236b6 --- /dev/null +++ b/src/oem-dell/lib/dell-bios-translation.ps1 @@ -0,0 +1,81 @@ +# oem-dell/lib/dell-bios-translation.ps1 +# +# Dell BIOS settings translation table. Maps the canonical $env:BIOS_* variable +# names defined in CLAUDE.md to the Dell Command Configure (cctk.exe) argument +# syntax. Inlined into dell-configure.ps1 at CI build time. +# +# Provides: +# $script:DellBiosSettingsMap hashtable: canonical name -> cctk descriptor. +# Get-DellBiosSetting returns the descriptor for a canonical name. +# +# Descriptor shape: +# @{ +# Native = '' +# Values = @{ = '' ; ... } +# Form = 'KeyValue' | 'Subcommand' +# } +# +# Form='KeyValue' emits --= (e.g. --tpm=on) +# Form='Subcommand' emits --= for things that need their +# own cctk subcommand line (e.g. bootorder --activebootlist=uefi). +# +# Password handling for $env:BIOS_AdminPassword / $env:BIOS_AdminPasswordNew is +# not in this table; dell-configure.ps1 emits the cctk password flags directly +# because the password mechanic is its own special case (current pw + new pw + +# clear pw all map to different cctk syntax). +# +# Settings that Dell does not support (or that we have not validated yet) are +# intentionally absent. dell-configure.ps1 logs and skips unknown canonical +# names so operators can set forward-looking vars without breaking the run. + +$script:DellBiosSettingsMap = @{ + BIOS_TPMEnabled = @{ + Native = 'tpm' + Values = @{ Enabled = 'on'; Disabled = 'off' } + Form = 'KeyValue' + } + BIOS_TPMActivation = @{ + Native = 'tpmactivation' + Values = @{ Activated = 'activate'; Deactivated = 'deactivate' } + Form = 'KeyValue' + } + BIOS_SecureBoot = @{ + Native = 'secureboot' + Values = @{ Enabled = 'enabled'; Disabled = 'disabled' } + Form = 'KeyValue' + } + BIOS_VirtualizationCPU = @{ + Native = 'virtualization' + Values = @{ Enabled = 'enable'; Disabled = 'disable' } + Form = 'KeyValue' + } + BIOS_VirtualizationIOMMU = @{ + Native = 'vtfordirectio' + Values = @{ Enabled = 'enable'; Disabled = 'disable' } + Form = 'KeyValue' + } + BIOS_WakeOnLAN = @{ + Native = 'wakeonlan' + Values = @{ + Enabled = 'lan' + Disabled = 'disable' + LANOnly = 'lan' + LANWLAN = 'lanwlan' + } + Form = 'KeyValue' + } + BIOS_BootMode = @{ + Native = 'bootorder' + Values = @{ UEFI = 'uefi'; Legacy = 'legacy' } + Form = 'Subcommand' + Param = 'activebootlist' + } +} + +function Get-DellBiosSetting { + param([Parameter(Mandatory)] [string] $CanonicalName) + if ($script:DellBiosSettingsMap.ContainsKey($CanonicalName)) { + return $script:DellBiosSettingsMap[$CanonicalName] + } + return $null +} diff --git a/src/oem-dell/lib/dell-detection.ps1 b/src/oem-dell/lib/dell-detection.ps1 new file mode 100644 index 0000000..cda4988 --- /dev/null +++ b/src/oem-dell/lib/dell-detection.ps1 @@ -0,0 +1,43 @@ +# oem-dell/lib/dell-detection.ps1 +# +# Dell-specific detection helpers. Inlined into Dell leaf scripts at CI build +# time via a `# %INCLUDE` marker. No runtime fetch, no hash check. +# +# Provides: +# Test-DellHardware True if this machine is Dell. +# Get-DellInstalledModel Returns Win32_ComputerSystem.Model or $null. +# Test-DCUInstalled True if dcu-cli.exe is present at the canonical path. +# Get-DCUVersion Returns [version] of installed DCU, or $null. +# Get-DCUCliPath Returns the canonical dcu-cli.exe path. + +$script:dcuCliPath = "$env:ProgramFiles\Dell\CommandUpdate\dcu-cli.exe" + +function Test-DellHardware { + $manufacturer = (Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction SilentlyContinue).Manufacturer + return ($manufacturer -like "Dell*") +} + +function Get-DellInstalledModel { + return (Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction SilentlyContinue).Model +} + +function Test-DCUInstalled { + return (Test-Path -Path $script:dcuCliPath) +} + +function Get-DCUVersion { + if (-not (Test-DCUInstalled)) { return $null } + try { + $info = (Get-Item -Path $script:dcuCliPath -ErrorAction Stop).VersionInfo + if ($info.ProductVersion) { + return [version]$info.ProductVersion + } + return $null + } catch { + return $null + } +} + +function Get-DCUCliPath { + return $script:dcuCliPath +} diff --git a/src/oem-shared/lib/oem-manufacturer-detect.ps1 b/src/oem-shared/lib/oem-manufacturer-detect.ps1 new file mode 100644 index 0000000..d1c7b93 --- /dev/null +++ b/src/oem-shared/lib/oem-manufacturer-detect.ps1 @@ -0,0 +1,21 @@ +# oem-shared/lib/oem-manufacturer-detect.ps1 +# +# Shared OEM manufacturer detection. Inlined into every OEM leaf script at +# CI build time via a `# %INCLUDE` marker. No runtime fetch, no hash check. +# +# Provides: +# Get-OEMManufacturer Returns "Dell" / "HP" / "Lenovo" / "Unknown". + +function Get-OEMManufacturer { + $manufacturer = (Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction SilentlyContinue).Manufacturer + if ([string]::IsNullOrEmpty($manufacturer)) { + return "Unknown" + } + switch -Wildcard ($manufacturer) { + "Dell*" { return "Dell" } + "HP*" { return "HP" } + "Hewlett*" { return "HP" } + "Lenovo*" { return "Lenovo" } + default { return "Unknown" } + } +} From 0299092889b26aadbd41fde3c8a0f7a42822f61a Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 11 May 2026 16:36:56 -0400 Subject: [PATCH 2/2] refactor(dell): flatten libs to src/lib/ (no per-category lib subfolders) --- src/{oem-dell => }/lib/dell-bios-translation.ps1 | 0 src/{oem-dell => }/lib/dell-detection.ps1 | 0 src/{oem-shared => }/lib/oem-manufacturer-detect.ps1 | 0 src/oem-dell/dell-command-update-install.ps1 | 4 ++-- src/oem-dell/dell-command-update-run.ps1 | 4 ++-- src/oem-dell/dell-configure.ps1 | 6 +++--- src/oem-dell/dell-debloat.ps1 | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) rename src/{oem-dell => }/lib/dell-bios-translation.ps1 (100%) rename src/{oem-dell => }/lib/dell-detection.ps1 (100%) rename src/{oem-shared => }/lib/oem-manufacturer-detect.ps1 (100%) diff --git a/src/oem-dell/lib/dell-bios-translation.ps1 b/src/lib/dell-bios-translation.ps1 similarity index 100% rename from src/oem-dell/lib/dell-bios-translation.ps1 rename to src/lib/dell-bios-translation.ps1 diff --git a/src/oem-dell/lib/dell-detection.ps1 b/src/lib/dell-detection.ps1 similarity index 100% rename from src/oem-dell/lib/dell-detection.ps1 rename to src/lib/dell-detection.ps1 diff --git a/src/oem-shared/lib/oem-manufacturer-detect.ps1 b/src/lib/oem-manufacturer-detect.ps1 similarity index 100% rename from src/oem-shared/lib/oem-manufacturer-detect.ps1 rename to src/lib/oem-manufacturer-detect.ps1 diff --git a/src/oem-dell/dell-command-update-install.ps1 b/src/oem-dell/dell-command-update-install.ps1 index 517552a..47f6b1e 100644 --- a/src/oem-dell/dell-command-update-install.ps1 +++ b/src/oem-dell/dell-command-update-install.ps1 @@ -8,8 +8,8 @@ ## $env:DCUTargetVersion - Minimum acceptable DCU version (default: "5.7.0") ## $env:DCUInstallerURL - Override the Dell-hosted installer URL (see TODO below) -# %INCLUDE src/oem-shared/lib/oem-manufacturer-detect.ps1 -# %INCLUDE src/oem-dell/lib/dell-detection.ps1 +# %INCLUDE src/lib/oem-manufacturer-detect.ps1 +# %INCLUDE src/lib/dell-detection.ps1 $ScriptLogName = "dell-command-update-install.log" diff --git a/src/oem-dell/dell-command-update-run.ps1 b/src/oem-dell/dell-command-update-run.ps1 index c81c0b7..3734728 100644 --- a/src/oem-dell/dell-command-update-run.ps1 +++ b/src/oem-dell/dell-command-update-run.ps1 @@ -7,8 +7,8 @@ ## Per-script variables: ## $env:DCUScanOnly - "1" to scan but not apply updates (default: "0") -# %INCLUDE src/oem-shared/lib/oem-manufacturer-detect.ps1 -# %INCLUDE src/oem-dell/lib/dell-detection.ps1 +# %INCLUDE src/lib/oem-manufacturer-detect.ps1 +# %INCLUDE src/lib/dell-detection.ps1 $ScriptLogName = "dell-command-update-run.log" diff --git a/src/oem-dell/dell-configure.ps1 b/src/oem-dell/dell-configure.ps1 index 080669a..f341db5 100644 --- a/src/oem-dell/dell-configure.ps1 +++ b/src/oem-dell/dell-configure.ps1 @@ -15,9 +15,9 @@ ## $env:BIOS_WakeOnLAN - "Enabled" / "Disabled" / "LANOnly" / "LANWLAN" ## $env:BIOS_BootMode - "UEFI" / "Legacy" -# %INCLUDE src/oem-shared/lib/oem-manufacturer-detect.ps1 -# %INCLUDE src/oem-dell/lib/dell-detection.ps1 -# %INCLUDE src/oem-dell/lib/dell-bios-translation.ps1 +# %INCLUDE src/lib/oem-manufacturer-detect.ps1 +# %INCLUDE src/lib/dell-detection.ps1 +# %INCLUDE src/lib/dell-bios-translation.ps1 $ScriptLogName = "dell-configure.log" diff --git a/src/oem-dell/dell-debloat.ps1 b/src/oem-dell/dell-debloat.ps1 index b3be0c5..cae85c2 100644 --- a/src/oem-dell/dell-debloat.ps1 +++ b/src/oem-dell/dell-debloat.ps1 @@ -4,7 +4,7 @@ ## $env:Description - Ticket # or initials for audit trail ## $env:RMMScriptPath - Optional log directory base provided by the RMM -# %INCLUDE src/oem-shared/lib/oem-manufacturer-detect.ps1 +# %INCLUDE src/lib/oem-manufacturer-detect.ps1 $ScriptLogName = "dell-debloat.log"