diff --git a/oem-dell/dell-baseline.ps1 b/oem-dell/dell-baseline.ps1 new file mode 100644 index 0000000..4a15fb6 --- /dev/null +++ b/oem-dell/dell-baseline.ps1 @@ -0,0 +1,199 @@ +## 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:RunDCUInstall - "1" to ensure Dell Command Update is installed (default: "1") +## $env:RunDCURun - "1" to invoke DCU scan + applyUpdates (default: "1") +## $env:RunDellBiosConfig - "1" to push BIOS config via cctk (default: "0") +## $env:RunDellDebloat - "1" to remove Dell consumer software (default: "1") +## $env:CustomFieldDellStatus - Ninja text custom field for last-run status (default: "dellBaselineStatus") +## $env:LibTag - jsDelivr tag to fetch leaf scripts and libs from (default: "release") + +$ScriptLogName = "dell-baseline.log" + +# --- Default optional RMM environment variables -------------------------- + +if ([string]::IsNullOrEmpty($env:RunDCUInstall)) { $env:RunDCUInstall = "1" } +if ([string]::IsNullOrEmpty($env:RunDCURun)) { $env:RunDCURun = "1" } +if ([string]::IsNullOrEmpty($env:RunDellBiosConfig)) { $env:RunDellBiosConfig = "0" } +if ([string]::IsNullOrEmpty($env:RunDellDebloat)) { $env:RunDellDebloat = "1" } +if ([string]::IsNullOrEmpty($env:CustomFieldDellStatus)) { $env:CustomFieldDellStatus = "dellBaselineStatus" } +if ([string]::IsNullOrEmpty($env:LibTag)) { $env:LibTag = "release" } + +# === LIB BOOTSTRAP === +# TODO: Replace REPLACE_WITH_REAL_HASH values once the libs land on the @release tag. +# Capture with: Get-FileHash -Algorithm SHA256 + +$libs = @( + @{ Url = "https://cdn.jsdelivr.net/gh/dtc-inc/msp-script-library@$($env:LibTag)/oem-shared/lib/oem-manufacturer-detect.ps1" + Sha256 = "REPLACE_WITH_REAL_HASH" }, + @{ Url = "https://cdn.jsdelivr.net/gh/dtc-inc/msp-script-library@$($env:LibTag)/oem-dell/lib/dell-detection.ps1" + Sha256 = "REPLACE_WITH_REAL_HASH" } +) +foreach ($lib in $libs) { + $libPath = Join-Path $env:TEMP ([System.IO.Path]::GetFileName($lib.Url)) + Invoke-WebRequest -Uri $lib.Url -OutFile $libPath -UseBasicParsing -ErrorAction Stop + $actual = (Get-FileHash -Path $libPath -Algorithm SHA256).Hash + if ($actual -ne $lib.Sha256) { + throw "Lib SHA256 mismatch for $($lib.Url). Expected $($lib.Sha256), got $actual." + } + . $libPath +} + +# --- 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 Baseline" + } +} + +$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" + +# Ninja Custom Fields are written via Ninja-Property-Set when the agent CLI is present. +function Set-NinjaStatus { + param([string] $Value) + $cli = "$env:ProgramData\NinjaRMMAgent\ninjarmm-cli.exe" + if (Test-Path -Path $cli) { + try { + & $cli set $env:CustomFieldDellStatus $Value | Out-Null + } catch { + Write-Host "Warning: failed to set Ninja custom field '$($env:CustomFieldDellStatus)': $_" + } + } else { + Write-Host "Ninja CLI not present; status would be: $Value" + } +} + +$manufacturer = Get-OEMManufacturer +Write-Host "Manufacturer: $manufacturer" + +if ($manufacturer -ne "Dell") { + Write-Host "Not a Dell endpoint. Exiting." + Set-NinjaStatus "Skipped: not Dell" + if ($TranscriptStarted) { Stop-Transcript } + exit 0 +} + +$dellModel = Get-DellInstalledModel +Write-Host "Dell model: $dellModel" + +# Fetch + invoke a leaf script from jsDelivr at the same tag the libs came from. +# TODO: Replace REPLACE_WITH_REAL_HASH per leaf once the leaves land on @release. +function Invoke-DellLeaf { + param( + [Parameter(Mandatory)] [string] $LeafName, + [Parameter(Mandatory)] [string] $Sha256 + ) + $url = "https://cdn.jsdelivr.net/gh/dtc-inc/msp-script-library@$($env:LibTag)/oem-dell/$LeafName" + $local = Join-Path $env:TEMP $LeafName + Get-VerifiedDownload -Url $url -OutFile $local -Sha256 $Sha256 | Out-Null + & powershell.exe -ExecutionPolicy Bypass -NoProfile -File $local + return $LASTEXITCODE +} + +$leafResults = @{} +$overallStatus = "OK" + +if ($env:RunDCUInstall -eq "1") { + Write-Host "`n=== dell-command-update-install ===" + try { + $rc = Invoke-DellLeaf -LeafName "dell-command-update-install.ps1" -Sha256 "REPLACE_WITH_REAL_HASH" + $leafResults["dcuInstall"] = $rc + if ($rc -ne 0) { $overallStatus = "DCUInstallFailed" } + } catch { + Write-Host "dell-command-update-install threw: $_" + $leafResults["dcuInstall"] = -1 + $overallStatus = "DCUInstallFailed" + } +} + +if ($env:RunDCURun -eq "1" -and $overallStatus -eq "OK") { + Write-Host "`n=== dell-command-update-run ===" + try { + $rc = Invoke-DellLeaf -LeafName "dell-command-update-run.ps1" -Sha256 "REPLACE_WITH_REAL_HASH" + $leafResults["dcuRun"] = $rc + # DCU exit code 1 = reboot required; treat as success for status reporting. + if ($rc -notin 0, 1) { $overallStatus = "DCURunFailed" } + } catch { + Write-Host "dell-command-update-run threw: $_" + $leafResults["dcuRun"] = -1 + $overallStatus = "DCURunFailed" + } +} + +if ($env:RunDellBiosConfig -eq "1") { + Write-Host "`n=== dell-bios-config ===" + try { + $rc = Invoke-DellLeaf -LeafName "dell-bios-config.ps1" -Sha256 "REPLACE_WITH_REAL_HASH" + $leafResults["biosConfig"] = $rc + if ($rc -ne 0 -and $overallStatus -eq "OK") { $overallStatus = "BiosConfigFailed" } + } catch { + Write-Host "dell-bios-config threw: $_" + $leafResults["biosConfig"] = -1 + if ($overallStatus -eq "OK") { $overallStatus = "BiosConfigFailed" } + } +} + +if ($env:RunDellDebloat -eq "1") { + Write-Host "`n=== dell-debloat ===" + try { + $rc = Invoke-DellLeaf -LeafName "dell-debloat.ps1" -Sha256 "REPLACE_WITH_REAL_HASH" + $leafResults["debloat"] = $rc + if ($rc -ne 0 -and $overallStatus -eq "OK") { $overallStatus = "DebloatFailed" } + } catch { + Write-Host "dell-debloat threw: $_" + $leafResults["debloat"] = -1 + if ($overallStatus -eq "OK") { $overallStatus = "DebloatFailed" } + } +} + +Write-Host "`n=== Summary ===" +foreach ($k in $leafResults.Keys) { + Write-Host (" {0,-12} exit={1}" -f $k, $leafResults[$k]) +} +Write-Host "Overall: $overallStatus" + +$statusLine = "{0} | {1} | {2}" -f $overallStatus, $dellModel, (Get-Date -Format "yyyy-MM-dd HH:mm") +Set-NinjaStatus $statusLine + +if ($TranscriptStarted) { Stop-Transcript } + +if ($overallStatus -eq "OK") { exit 0 } else { exit 1 } diff --git a/oem-dell/dell-bios-config.ps1 b/oem-dell/dell-bios-config.ps1 new file mode 100644 index 0000000..9329bc5 --- /dev/null +++ b/oem-dell/dell-bios-config.ps1 @@ -0,0 +1,142 @@ +## 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:DellBiosPolicyURL - jsDelivr URL of the .cctk policy file to apply +## (default: dtc-baseline.cctk in this repo at $LibTag) +## $env:DellBiosPolicySha256 - SHA256 of the policy file (TODO: capture and pin) +## $env:DellBiosAdminPassword - Optional current BIOS admin password (passed to cctk via --valsetuppwd) +## $env:LibTag - jsDelivr tag for shared libs and policy file (default: "release") + +$ScriptLogName = "dell-bios-config.log" + +# --- Default optional RMM environment variables -------------------------- + +if ([string]::IsNullOrEmpty($env:LibTag)) { $env:LibTag = "release" } +if ([string]::IsNullOrEmpty($env:DellBiosPolicyURL)) { + $env:DellBiosPolicyURL = "https://cdn.jsdelivr.net/gh/dtc-inc/msp-script-library@$($env:LibTag)/oem-dell/policy/dtc-baseline.cctk" +} +# TODO: Capture SHA256 of the .cctk policy file once it lands at the URL above. +if ([string]::IsNullOrEmpty($env:DellBiosPolicySha256)) { + $env:DellBiosPolicySha256 = "REPLACE_WITH_REAL_HASH" +} + +# === LIB BOOTSTRAP === +# TODO: Replace REPLACE_WITH_REAL_HASH once libs land on @release. + +$libs = @( + @{ Url = "https://cdn.jsdelivr.net/gh/dtc-inc/msp-script-library@$($env:LibTag)/oem-shared/lib/oem-manufacturer-detect.ps1" + Sha256 = "REPLACE_WITH_REAL_HASH" }, + @{ Url = "https://cdn.jsdelivr.net/gh/dtc-inc/msp-script-library@$($env:LibTag)/oem-dell/lib/dell-detection.ps1" + Sha256 = "REPLACE_WITH_REAL_HASH" } +) +foreach ($lib in $libs) { + $libPath = Join-Path $env:TEMP ([System.IO.Path]::GetFileName($lib.Url)) + Invoke-WebRequest -Uri $lib.Url -OutFile $libPath -UseBasicParsing -ErrorAction Stop + $actual = (Get-FileHash -Path $libPath -Algorithm SHA256).Hash + if ($actual -ne $lib.Sha256) { + throw "Lib SHA256 mismatch for $($lib.Url). Expected $($lib.Sha256), got $actual." + } + . $libPath +} + +# --- 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 BIOS Config" + } +} + +$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 +} + +# cctk.exe is installed by Dell Command Configure. Dell ships both 32-bit and 64-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" + +$policyPath = Join-Path $env:TEMP "dtc-dell-baseline.cctk" +try { + Get-VerifiedDownload -Url $env:DellBiosPolicyURL -OutFile $policyPath -Sha256 $env:DellBiosPolicySha256 | Out-Null +} catch { + Write-Host "Failed to fetch BIOS policy: $_" + if ($TranscriptStarted) { Stop-Transcript } + exit 1 +} + +Write-Host "Applying policy: $policyPath" + +# Reference: https://www.dell.com/support/manuals/en-us/command-configure-v4.1/dcc_ug_4.1.0/applying-ini-or-cctk-file +$cctkArgs = @("--infile=$policyPath") +if (-not [string]::IsNullOrEmpty($env:DellBiosAdminPassword)) { + $cctkArgs += "--valsetuppwd=$env:DellBiosAdminPassword" +} + +$proc = Start-Process -FilePath $cctk -ArgumentList $cctkArgs -Wait -PassThru -NoNewWindow +$cctkExit = $proc.ExitCode +Write-Host "cctk exit code: $cctkExit" + +Remove-Item -Path $policyPath -Force -ErrorAction SilentlyContinue + +if ($TranscriptStarted) { Stop-Transcript } +exit $cctkExit diff --git a/oem-dell/dell-command-update-install.ps1 b/oem-dell/dell-command-update-install.ps1 new file mode 100644 index 0000000..fc51d54 --- /dev/null +++ b/oem-dell/dell-command-update-install.ps1 @@ -0,0 +1,133 @@ +## 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 Dell-hosted installer URL (default: see below) +## $env:DCUInstallerSha256 - SHA256 of the installer EXE (TODO: capture and pin) +## $env:LibTag - jsDelivr tag for shared libs (default: "release") + +$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 (driver ID RXT5N) +# from https://www.dell.com/support/kbdoc/en-us/000177325/dell-command-update and pin it. +if ([string]::IsNullOrEmpty($env:DCUInstallerURL)) { + $env:DCUInstallerURL = "https://dl.dell.com/FOLDER/RXT5N/Dell-Command-Update-Application_RXT5N_WIN_5.7.0_A00.EXE" +} +# TODO: Capture SHA256 of the installer once the canonical URL is confirmed. +if ([string]::IsNullOrEmpty($env:DCUInstallerSha256)) { + $env:DCUInstallerSha256 = "REPLACE_WITH_REAL_HASH" +} +if ([string]::IsNullOrEmpty($env:LibTag)) { $env:LibTag = "release" } + +# === LIB BOOTSTRAP === +# TODO: Replace REPLACE_WITH_REAL_HASH once libs land on @release. + +$libs = @( + @{ Url = "https://cdn.jsdelivr.net/gh/dtc-inc/msp-script-library@$($env:LibTag)/oem-shared/lib/oem-manufacturer-detect.ps1" + Sha256 = "REPLACE_WITH_REAL_HASH" }, + @{ Url = "https://cdn.jsdelivr.net/gh/dtc-inc/msp-script-library@$($env:LibTag)/oem-dell/lib/dell-detection.ps1" + Sha256 = "REPLACE_WITH_REAL_HASH" } +) +foreach ($lib in $libs) { + $libPath = Join-Path $env:TEMP ([System.IO.Path]::GetFileName($lib.Url)) + Invoke-WebRequest -Uri $lib.Url -OutFile $libPath -UseBasicParsing -ErrorAction Stop + $actual = (Get-FileHash -Path $libPath -Algorithm SHA256).Hash + if ($actual -ne $lib.Sha256) { + throw "Lib SHA256 mismatch for $($lib.Url). Expected $($lib.Sha256), got $actual." + } + . $libPath +} + +# --- 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 +} + +$targetVersion = [version]$env:DCUTargetVersion +$installedVersion = Get-DCUVersion + +if ($installedVersion -ne $null -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 { + Get-VerifiedDownload -Url $env:DCUInstallerURL -OutFile $installerPath -Sha256 $env:DCUInstallerSha256 | Out-Null +} catch { + Write-Host "Failed to download DCU installer: $_" + 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 ($postVersion -ne $null -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/oem-dell/dell-command-update-run.ps1 b/oem-dell/dell-command-update-run.ps1 new file mode 100644 index 0000000..bf0d6ad --- /dev/null +++ b/oem-dell/dell-command-update-run.ps1 @@ -0,0 +1,132 @@ +## 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") +## $env:LibTag - jsDelivr tag for shared libs (default: "release") + +$ScriptLogName = "dell-command-update-run.log" + +# --- Default optional RMM environment variables -------------------------- + +if ([string]::IsNullOrEmpty($env:DCUScanOnly)) { $env:DCUScanOnly = "0" } +if ([string]::IsNullOrEmpty($env:LibTag)) { $env:LibTag = "release" } + +# === LIB BOOTSTRAP === +# TODO: Replace REPLACE_WITH_REAL_HASH once libs land on @release. + +$libs = @( + @{ Url = "https://cdn.jsdelivr.net/gh/dtc-inc/msp-script-library@$($env:LibTag)/oem-shared/lib/oem-manufacturer-detect.ps1" + Sha256 = "REPLACE_WITH_REAL_HASH" }, + @{ Url = "https://cdn.jsdelivr.net/gh/dtc-inc/msp-script-library@$($env:LibTag)/oem-dell/lib/dell-detection.ps1" + Sha256 = "REPLACE_WITH_REAL_HASH" } +) +foreach ($lib in $libs) { + $libPath = Join-Path $env:TEMP ([System.IO.Path]::GetFileName($lib.Url)) + Invoke-WebRequest -Uri $lib.Url -OutFile $libPath -UseBasicParsing -ErrorAction Stop + $actual = (Get-FileHash -Path $libPath -Algorithm SHA256).Hash + if ($actual -ne $lib.Sha256) { + throw "Lib SHA256 mismatch for $($lib.Url). Expected $($lib.Sha256), got $actual." + } + . $libPath +} + +# --- 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" + +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/oem-dell/dell-debloat.ps1 b/oem-dell/dell-debloat.ps1 new file mode 100644 index 0000000..b06bc57 --- /dev/null +++ b/oem-dell/dell-debloat.ps1 @@ -0,0 +1,202 @@ +## 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:LibTag - jsDelivr tag for shared libs (default: "release") + +$ScriptLogName = "dell-debloat.log" + +# --- Default optional RMM environment variables -------------------------- + +if ([string]::IsNullOrEmpty($env:LibTag)) { $env:LibTag = "release" } + +# === LIB BOOTSTRAP === +# TODO: Replace REPLACE_WITH_REAL_HASH once libs land on @release. + +$libs = @( + @{ Url = "https://cdn.jsdelivr.net/gh/dtc-inc/msp-script-library@$($env:LibTag)/oem-shared/lib/oem-manufacturer-detect.ps1" + Sha256 = "REPLACE_WITH_REAL_HASH" } +) +foreach ($lib in $libs) { + $libPath = Join-Path $env:TEMP ([System.IO.Path]::GetFileName($lib.Url)) + Invoke-WebRequest -Uri $lib.Url -OutFile $libPath -UseBasicParsing -ErrorAction Stop + $actual = (Get-FileHash -Path $libPath -Algorithm SHA256).Hash + if ($actual -ne $lib.Sha256) { + throw "Lib SHA256 mismatch for $($lib.Url). Expected $($lib.Sha256), got $actual." + } + . $libPath +} + +# --- 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" + +$manufacturer = Get-OEMManufacturer +if ($manufacturer -ne "Dell") { + Write-Host "Not a Dell endpoint. Exiting." + if ($TranscriptStarted) { Stop-Transcript } + exit 0 +} + +# Dell consumer software to remove. Dell Command Update (the enterprise tool) and +# Dell Command Configure are intentionally NOT in this list ... they are the +# vendor lifecycle tools this baseline deploys. +$dellBloatPatterns = @( + "SupportAssist", + "Dell SupportAssist", + "Dell Update", + "Dell Mobile Connect", + "Dell Digital Delivery", + "Dell Customer Connect", + "Dell Help & Support", + "MyDell" +) + +# Patterns to exclude even if they match a bloat pattern (keep these installed). +$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 +} + +# Build the registry survey for 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 { + # Non-MSI uninstaller; append silent flags if possible. + $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/oem-dell/lib/dell-detection.ps1 b/oem-dell/lib/dell-detection.ps1 new file mode 100644 index 0000000..b14f270 --- /dev/null +++ b/oem-dell/lib/dell-detection.ps1 @@ -0,0 +1,42 @@ +# oem-dell/lib/dell-detection.ps1 +# +# Dell-specific detection helpers. Fetched at runtime alongside the shared +# OEM lib by the lib bootstrap block in each Dell leaf script. +# +# 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 the installed DCU, or $null. + +$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/oem-shared/lib/oem-manufacturer-detect.ps1 b/oem-shared/lib/oem-manufacturer-detect.ps1 new file mode 100644 index 0000000..c84b6e2 --- /dev/null +++ b/oem-shared/lib/oem-manufacturer-detect.ps1 @@ -0,0 +1,45 @@ +# oem-shared/lib/oem-manufacturer-detect.ps1 +# +# Shared helpers for OEM-specific scripts. Fetched at runtime via the lib +# bootstrap pattern (see CLAUDE.md -> Lib Bootstrap Pattern). +# +# Provides: +# Get-OEMManufacturer Returns "Dell" / "HP" / "Lenovo" / "Unknown". +# Get-VerifiedDownload Downloads a URL to a path and verifies SHA256. + +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" } + } +} + +function Get-VerifiedDownload { + param( + [Parameter(Mandatory)] [string] $Url, + [Parameter(Mandatory)] [string] $OutFile, + [Parameter(Mandatory)] [string] $Sha256 + ) + + $dir = Split-Path -Path $OutFile -Parent + if ($dir -and -not (Test-Path -Path $dir)) { + New-Item -Path $dir -ItemType Directory -Force | Out-Null + } + + Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing -ErrorAction Stop + + $actual = (Get-FileHash -Path $OutFile -Algorithm SHA256).Hash + if ($actual -ne $Sha256) { + Remove-Item -Path $OutFile -Force -ErrorAction SilentlyContinue + throw "SHA256 mismatch for $Url. Expected $Sha256, got $actual." + } + + return $OutFile +}